diff --git a/LoopStructural/modelling/core/_model_relationships.py b/LoopStructural/modelling/core/_model_relationships.py new file mode 100644 index 00000000..dcb59c53 --- /dev/null +++ b/LoopStructural/modelling/core/_model_relationships.py @@ -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 diff --git a/LoopStructural/modelling/core/geological_model.py b/LoopStructural/modelling/core/geological_model.py index 5b050cb8..8c5a8e01 100644 --- a/LoopStructural/modelling/core/geological_model.py +++ b/LoopStructural/modelling/core/geological_model.py @@ -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 @@ -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: @@ -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): """ @@ -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( @@ -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( @@ -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(