"""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