Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions LoopStructural/modelling/core/_model_relationships.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"""Fault/unconformity relationship bookkeeping for GeologicalModel (see API.md).

Extracted from GeologicalModel to separate feature-stack relationship logic
(domain faults, unconformities) from feature-container state and
construction. GeologicalModel's @public_api methods (``add_unconformity``,
``add_onlap_unconformity``) stay defined directly on the class -- their
__qualname__ is part of the CI-checked stable API surface -- and delegate to
the staticmethods here. The private ``_add_faults``/``_add_domain_fault_*``
helpers also delegate, since they're called throughout GeologicalModel's
feature-construction methods.
"""

from ...modelling.features import FeatureType, UnconformityFeature
from ...utils import getLogger

logger = getLogger(__name__)


class FeatureRelationshipManager:
@staticmethod
def add_faults(model, feature_builder, features=None):
"""Adds all existing faults to a geological feature builder

Parameters
----------
model : GeologicalModel
feature_builder : GeologicalFeatureBuilder/StructuralFrameBuilder
The feature buider to add the faults to
features : list, optional
A specific list of features rather than all features in the model
"""
if features is None:
features = model.features
for f in reversed(features):
if isinstance(f, str):
f = model.__getitem__(f)
if f.type == FeatureType.FAULT:
feature_builder.add_fault(f)

@staticmethod
def add_domain_fault_above(model, feature):
"""
Looks through the feature list and adds any domain faults to the feature. The domain fault masks everything
where the fault scalar field is < 0 as being active when added to feature.

Parameters
----------
model : GeologicalModel
feature : GeologicalFeatureBuilder
the feature being added to the model where domain faults should be added
"""
for f in reversed(model.features):
if f.name == feature.name:
continue
if f.type == "domain_fault":
feature.add_region(lambda pos, fault=f: fault.evaluate_value(pos) < 0)
break

@staticmethod
def add_domain_fault_below(model, domain_fault):
"""
Looks through the feature list and adds any the domain_fault to the features
that already exist in the stack until an unconformity is reached. domain faults
to the feature. The domain fault masks everything where the fault scalar field
is < 0 as being active when added to feature.

Parameters
----------
model : GeologicalModel
domain_fault : GeologicalFeatureBuilder
the feature being added to the model where domain faults should be added
"""
for f in reversed(model.features):
if f.name == domain_fault.name:
continue
f.add_region(lambda pos: domain_fault.evaluate_value(pos) > 0)
if f.type == FeatureType.UNCONFORMITY:
break

@staticmethod
def add_unconformity_above(model, feature):
"""
Adds a region to the feature to prevent the value from being
interpolated where the unconformities exists above e.g.
if there is another feature above and the unconformity is at 0
then the features added below (after) will only be visible where the
uncomformity is <0

Parameters
----------
model : GeologicalModel
feature - GeologicalFeature
"""

if feature.type == FeatureType.FAULT:
return
for f in reversed(model.features):
if f.type == FeatureType.UNCONFORMITY and f.name != feature.name:
logger.info(f"Adding {f.name} as unconformity to {feature.name}")
feature.add_region(f)
if f.type == FeatureType.ONLAPUNCONFORMITY and f.name != feature.name:
feature.add_region(f)
break

@staticmethod
def add_unconformity(model, feature, value, index=None):
"""
Use an existing feature to add an unconformity to the model.

Parameters
----------
model : GeologicalModel
feature : GeologicalFeature
existing geological feature
value : float
scalar value of isosurface that represents

Returns
-------
unconformity : GeologicalFeature
unconformity feature
"""
logger.debug(f"Adding {feature.name} as unconformity at {value}")
if feature is None:
logger.warning("Cannot add unconformtiy, base feature is None")
return
# look backwards through features and add the unconformity as a region until
# we get to an unconformity
uc_feature = UnconformityFeature(feature, value)
feature.add_region(uc_feature.inverse())
for f in reversed(model.features):
if f.type == FeatureType.UNCONFORMITY:
logger.debug(f"Reached unconformity {f.name}")
break
logger.debug(f"Adding {uc_feature.name} as unconformity to {f.name}")
if f.type == FeatureType.FAULT or f.type == FeatureType.INACTIVEFAULT:
continue
if f == feature:
continue
else:
f.add_region(uc_feature)
# now add the unconformity to the feature list
model._add_feature(uc_feature, index=index)
return uc_feature

@staticmethod
def add_onlap_unconformity(model, feature, value, index=None):
"""
Use an existing feature to add an unconformity to the model.

Parameters
----------
model : GeologicalModel
feature : GeologicalFeature
existing geological feature
value : float
scalar value of isosurface that represents

Returns
-------
unconformity_feature : GeologicalFeature
the created unconformity
"""
feature.regions = []
uc_feature = UnconformityFeature(feature, value, False, onlap=True)
feature.add_region(uc_feature.inverse())
for f in reversed(model.features):
if f.type in (FeatureType.UNCONFORMITY, FeatureType.ONLAPUNCONFORMITY):
logger.debug(f"Reached unconformity {f.name}")
break
if f.type == FeatureType.FAULT or f.type == FeatureType.INACTIVEFAULT:
continue
if f != feature:
f.add_region(uc_feature)
model._add_feature(uc_feature.inverse(), index=index)

return uc_feature
107 changes: 7 additions & 100 deletions LoopStructural/modelling/core/geological_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
)
from ._feature_registry import FeatureBuilderRegistry
from ._model_exporter import ModelExporter
from ._model_relationships import FeatureRelationshipManager
from ._model_serializer import ModelSerializer
from .stratigraphic_column import StratigraphicColumn

Expand Down Expand Up @@ -543,9 +544,9 @@ def _add_feature(self, feature, index: int | None = None):
self.features.append(feature)
self.feature_name_index[feature.name] = len(self.features) - 1
logger.info(f"Adding {feature.name} to model at location {len(self.features)}")
self._add_domain_fault_above(feature)
FeatureRelationshipManager.add_domain_fault_above(self, feature)
if feature.type == FeatureType.INTERPOLATED:
self._add_unconformity_above(feature)
FeatureRelationshipManager.add_unconformity_above(self, feature)
feature.model = self

def data_for_feature(self, feature_name: str) -> pd.DataFrame:
Expand Down Expand Up @@ -1413,34 +1414,7 @@ def _add_faults(self, feature_builder, features=None):
-------

"""
if features is None:
features = self.features
for f in reversed(features):
if isinstance(f, str):
f = self.__getitem__(f)
if f.type == FeatureType.FAULT:
feature_builder.add_fault(f)

def _add_domain_fault_above(self, feature):
"""
Looks through the feature list and adds any domain faults to the feature. The domain fault masks everything
where the fault scalar field is < 0 as being active when added to feature.

Parameters
----------
feature : GeologicalFeatureBuilder
the feature being added to the model where domain faults should be added

Returns
-------

"""
for f in reversed(self.features):
if f.name == feature.name:
continue
if f.type == "domain_fault":
feature.add_region(lambda pos, fault=f: fault.evaluate_value(pos) < 0)
break
FeatureRelationshipManager.add_faults(self, feature_builder, features=features)

def _add_domain_fault_below(self, domain_fault):
"""
Expand All @@ -1458,40 +1432,7 @@ def _add_domain_fault_below(self, domain_fault):
-------

"""
for f in reversed(self.features):
if f.name == domain_fault.name:
continue
f.add_region(lambda pos: domain_fault.evaluate_value(pos) > 0)
if f.type == FeatureType.UNCONFORMITY:
break

def _add_unconformity_above(self, feature):
"""

Adds a region to the feature to prevent the value from being
interpolated where the unconformities exists above e.g.
if there is another feature above and the unconformity is at 0
then the features added below (after) will only be visible where the
uncomformity is <0

Parameters
----------
feature - GeologicalFeature

Returns
-------

"""

if feature.type == FeatureType.FAULT:
return
for f in reversed(self.features):
if f.type == FeatureType.UNCONFORMITY and f.name != feature.name:
logger.info(f"Adding {f.name} as unconformity to {feature.name}")
feature.add_region(f)
if f.type == FeatureType.ONLAPUNCONFORMITY and f.name != feature.name:
feature.add_region(f)
break
FeatureRelationshipManager.add_domain_fault_below(self, domain_fault)

@public_api(tier="stable")
def add_unconformity(
Expand All @@ -1513,28 +1454,7 @@ def add_unconformity(
unconformity feature

"""
logger.debug(f"Adding {feature.name} as unconformity at {value}")
if feature is None:
logger.warning("Cannot add unconformtiy, base feature is None")
return
# look backwards through features and add the unconformity as a region until
# we get to an unconformity
uc_feature = UnconformityFeature(feature, value)
feature.add_region(uc_feature.inverse())
for f in reversed(self.features):
if f.type == FeatureType.UNCONFORMITY:
logger.debug(f"Reached unconformity {f.name}")
break
logger.debug(f"Adding {uc_feature.name} as unconformity to {f.name}")
if f.type == FeatureType.FAULT or f.type == FeatureType.INACTIVEFAULT:
continue
if f == feature:
continue
else:
f.add_region(uc_feature)
# now add the unconformity to the feature list
self._add_feature(uc_feature, index=index)
return uc_feature
return FeatureRelationshipManager.add_unconformity(self, feature, value, index=index)

@public_api(tier="stable")
def add_onlap_unconformity(
Expand All @@ -1556,20 +1476,7 @@ def add_onlap_unconformity(
the created unconformity

"""
feature.regions = []
uc_feature = UnconformityFeature(feature, value, False, onlap=True)
feature.add_region(uc_feature.inverse())
for f in reversed(self.features):
if f.type in (FeatureType.UNCONFORMITY, FeatureType.ONLAPUNCONFORMITY):
logger.debug(f"Reached unconformity {f.name}")
break
if f.type == FeatureType.FAULT or f.type == FeatureType.INACTIVEFAULT:
continue
if f != feature:
f.add_region(uc_feature)
self._add_feature(uc_feature.inverse(), index=index)

return uc_feature
return FeatureRelationshipManager.add_onlap_unconformity(self, feature, value, index=index)

@public_api(tier="provisional")
def add_fold_to_feature(
Expand Down
Loading