Source code for resqpy.organize.fault_interpretation

"""Class for RESQML Fault Interpretation organizational objects."""

import math as maths

import resqpy.olio.uuid as bu
import resqpy.olio.xml_et as rqet
import resqpy.organize
import resqpy.organize.tectonic_boundary_feature as tbf
import resqpy.organize._utils as ou
from resqpy.olio.base import BaseResqpy
from resqpy.olio.xml_namespaces import curly_namespace as ns


class FaultInterpretation(BaseResqpy):
    """Class for RESQML Fault Interpretation organizational objects.

    RESQML documentation:

       A type of boundary feature, this class contains the data describing an opinion
       about the characterization of the fault, which includes the attributes listed below.
    """

    resqml_type = "FaultInterpretation"
    valid_domains = ('depth', 'time', 'mixed')

    # note: many of the attributes could be deduced from geometry

[docs] def __init__(self, parent_model, uuid = None, title = None, tectonic_boundary_feature = None, domain = 'depth', is_normal = None, is_listric = None, maximum_throw = None, mean_azimuth = None, mean_dip = None, originator = None, extra_metadata = None): """Initialises a Fault interpretation organisational object.""" # note: will create a paired TectonicBoundaryFeature object when loading from xml # if not extracting from xml,: # tectonic_boundary_feature is required and must be a TectonicBoundaryFeature object # domain is required and must be one of 'depth', 'time' or 'mixed' # is_listric is required if the fault is not normal (and must be None if normal) # max throw, azimuth & dip are all optional # the throw interpretation list is not supported for direct initialisation self.tectonic_boundary_feature = tectonic_boundary_feature # InterpretedFeature RESQML field, when not loading from xml self.feature_root = None if self.tectonic_boundary_feature is None else self.tectonic_boundary_feature.root if (not title) and self.tectonic_boundary_feature is not None: title = self.tectonic_boundary_feature.feature_name self.main_has_occurred_during = (None, None) self.is_normal = is_normal # extra field, not explicitly in RESQML self.domain = domain # RESQML xml business rule: IsListric must be present if the fault is normal; must not be present if the fault is not normal self.is_listric = is_listric self.maximum_throw = maximum_throw self.mean_azimuth = mean_azimuth self.mean_dip = mean_dip self.throw_interpretation_list = None # list of (list of throw kind, (base chrono uuid, top chrono uuid))) super().__init__(model = parent_model, uuid = uuid, title = title, originator = originator, extra_metadata = extra_metadata)
@property def feature_uuid(self): """Returns the UUID of the interpreted feature""" # TODO: rewrite using uuid as primary key return rqet.uuid_for_part_root(self.feature_root) def _load_from_xml(self): root_node = self.root self.domain = rqet.find_tag_text(root_node, 'Domain') interp_feature_ref_node = rqet.find_tag(root_node, 'InterpretedFeature') assert interp_feature_ref_node is not None self.feature_root = self.model.referenced_node(interp_feature_ref_node) if self.feature_root is not None: self.tectonic_boundary_feature = tbf.TectonicBoundaryFeature(self.model, uuid = self.feature_root.attrib['uuid'], feature_name = self.model.title_for_root( self.feature_root)) self.main_has_occurred_during = ou.extract_has_occurred_during(root_node) self.is_listric = rqet.find_tag_bool(root_node, 'IsListric') self.is_normal = (self.is_listric is None) self.maximum_throw = rqet.find_tag_float(root_node, 'MaximumThrow') # todo: check that type="eml:LengthMeasure" is simple float self.mean_azimuth = rqet.find_tag_float(root_node, 'MeanAzimuth') self.mean_dip = rqet.find_tag_float(root_node, 'MeanDip') throw_interpretation_nodes = rqet.list_of_tag(root_node, 'ThrowInterpretation') if throw_interpretation_nodes is not None and len(throw_interpretation_nodes): self.throw_interpretation_list = [] for ti_node in throw_interpretation_nodes: hod_pair = ou.extract_has_occurred_during(ti_node) throw_kind_list = rqet.list_of_tag(ti_node, 'Throw') for tk_node in throw_kind_list: self.throw_interpretation_list.append((tk_node.text, hod_pair))
[docs] def is_equivalent(self, other, check_extra_metadata = True): """Returns True if this interpretation is essentially the same as the other; otherwise False.""" if other is None or not isinstance(other, FaultInterpretation): return False if self is other or bu.matching_uuids(self.uuid, other.uuid): return True if self.title != other.title: return False attr_list = ['tectonic_boundary_feature', 'maximum_throw', 'mean_azimuth', 'mean_dip'] one_none_attr_list = [(getattr(self, v) is None) != (getattr(other, v) is None) for v in attr_list] if any(one_none_attr_list): # If only one of self or other has a None attribute return False # List of attributes that are not None in either self or other non_none_attr_list = [a for a in attr_list if getattr(self, a) is not None] # Additional tests for attributes that are not None if (self.tectonic_boundary_feature is not None and not self.tectonic_boundary_feature.is_equivalent(other.tectonic_boundary_feature, check_extra_metadata = check_extra_metadata)): return False if (self.maximum_throw is not None and not maths.isclose(self.maximum_throw, other.maximum_throw, rel_tol = 1e-3)): return False if (self.mean_azimuth is not None and not maths.isclose(self.mean_azimuth, other.mean_azimuth, abs_tol = 0.5)): return False if (self.mean_dip is not None and not maths.isclose(self.mean_dip, other.mean_dip, abs_tol = 0.5)): return False if (not ou.equivalent_chrono_pairs(self.main_has_occurred_during, other.main_has_occurred_during) or self.is_normal != other.is_normal or self.domain != other.domain or self.is_listric != other.is_listric): return False if check_extra_metadata and not ou.equivalent_extra_metadata(self, other): return False if not self.throw_interpretation_list and not other.throw_interpretation_list: return True if not self.throw_interpretation_list or not other.throw_interpretation_list: return False if len(self.throw_interpretation_list) != len(other.throw_interpretation_list): return False for this_ti, other_ti in zip(self.throw_interpretation_list, other.throw_interpretation_list): if this_ti[0] != other_ti[0]: return False # throw kind if not ou.equivalent_chrono_pairs(this_ti[1], other_ti[1]): return False return True
[docs] def create_xml(self, tectonic_boundary_feature_root = None, add_as_part = True, add_relationships = True, originator = None, title_suffix = None, reuse = True): """Creates a fault interpretation organisational xml node from a fault interpretation object.""" # note: related tectonic boundary feature node should be created first and referenced here assert self.is_normal == (self.is_listric is None) if not self.title: if tectonic_boundary_feature_root is not None: title = rqet.find_nested_tags_text(tectonic_boundary_feature_root, ['Citation', 'Title']) else: title = 'fault interpretation' if title_suffix: title += ' ' + title_suffix if reuse and self.try_reuse(): return self.root fi = super().create_xml(add_as_part = False, originator = originator) if self.tectonic_boundary_feature is not None: tbf_root = self.tectonic_boundary_feature.root if tbf_root is not None: if tectonic_boundary_feature_root is None: tectonic_boundary_feature_root = tbf_root else: assert tbf_root is tectonic_boundary_feature_root, 'tectonic boundary feature mismatch' assert tectonic_boundary_feature_root is not None assert self.domain in self.valid_domains, 'illegal domain value for fault interpretation' dom_node = rqet.SubElement(fi, ns['resqml2'] + 'Domain') dom_node.set(ns['xsi'] + 'type', ns['resqml2'] + 'Domain') dom_node.text = self.domain self.model.create_ref_node('InterpretedFeature', self.model.title_for_root(tectonic_boundary_feature_root), tectonic_boundary_feature_root.attrib['uuid'], content_type = 'obj_TectonicBoundaryFeature', root = fi) ou.create_xml_has_occurred_during(self.model, fi, self.main_has_occurred_during) if self.is_listric is not None: listric = rqet.SubElement(fi, ns['resqml2'] + 'IsListric') listric.set(ns['xsi'] + 'type', ns['xsd'] + 'boolean') listric.text = str(self.is_listric).lower() # todo: check eml:LengthMeasure and PlaneAngleMeasure structures: uom? if self.maximum_throw is not None: max_throw = rqet.SubElement(fi, ns['resqml2'] + 'MaximumThrow') max_throw.set(ns['xsi'] + 'type', ns['eml'] + 'LengthMeasure') max_throw.text = str(self.maximum_throw) if self.mean_azimuth is not None: azimuth = rqet.SubElement(fi, ns['resqml2'] + 'MeanAzimuth') azimuth.set(ns['xsi'] + 'type', ns['eml'] + 'PlaneAngleMeasure') azimuth.text = str(self.mean_azimuth) if self.mean_dip is not None: dip = rqet.SubElement(fi, ns['resqml2'] + 'MeanDip') dip.set(ns['xsi'] + 'type', ns['eml'] + 'PlaneAngleMeasure') dip.text = str(self.mean_azimuth) if self.throw_interpretation_list is not None and len(self.throw_interpretation_list): previous_has_occurred_during = ('never', 'never') ti_node = None for (throw_kind, (base_chrono_uuid, top_chrono_uuid)) in self.throw_interpretation_list: if (str(base_chrono_uuid), str(top_chrono_uuid)) != previous_has_occurred_during: previous_has_occurred_during = (str(base_chrono_uuid), str(top_chrono_uuid)) ti_node = rqet.SubElement(fi, 'ThrowInterpretation') ti_node.set(ns['xsi'] + 'type', ns['resqml2'] + 'FaultThrow') ti_node.text = rqet.null_xml_text ou.create_xml_has_occurred_during(self.model, ti_node, (base_chrono_uuid, top_chrono_uuid)) tk_node = rqet.SubElement(ti_node, 'Throw') tk_node.set(ns['xsi'] + 'type', ns['resqml2'] + 'ThrowKind') tk_node.text = throw_kind if add_as_part: self.model.add_part('obj_FaultInterpretation', self.uuid, fi) if add_relationships: self.model.create_reciprocal_relationship(fi, 'destinationObject', tectonic_boundary_feature_root, 'sourceObject') return fi