diff --git a/loopstructural/gui/modelling/steps/checks.py b/loopstructural/gui/modelling/steps/checks.py index b750a5f..627fb9f 100644 --- a/loopstructural/gui/modelling/steps/checks.py +++ b/loopstructural/gui/modelling/steps/checks.py @@ -65,13 +65,18 @@ def check_data(data_manager, model_manager=None) -> StepCheck: def check_stratigraphy(data_manager, model_manager=None) -> StepCheck: """Step 2: the column and the results that come from it.""" problems, todo = [], [] + unit_names = data_manager.get_stratigraphic_unit_names() + if not unit_names: + # The step is optional: a model of faults only has no column + todo.append( + "Add units to the stratigraphic column. This step is optional if you model " + "only faults." + ) + return StepCheck(tuple(problems), tuple(todo)) if data_manager.get_layer_role(layer_roles.GEOLOGY) is None: todo.append("Select the geology layer.") if data_manager.get_layer_role(layer_roles.STRUCTURE) is None: todo.append("Select the structure layer.") - unit_names = data_manager.get_stratigraphic_unit_names() - if not unit_names: - todo.append("Add units to the stratigraphic column.") problems.extend(_out_of_date_messages(data_manager)) contact_units = set(data_manager.get_unique_basal_units()) diff --git a/loopstructural/gui/modelling/steps/export_panel.py b/loopstructural/gui/modelling/steps/export_panel.py index 3b43905..4897e9a 100644 --- a/loopstructural/gui/modelling/steps/export_panel.py +++ b/loopstructural/gui/modelling/steps/export_panel.py @@ -169,9 +169,30 @@ def set_model_solved(self, solved): ) for tab_index in range(self.tabs.count()): self.tabs.widget(tab_index).setEnabled(self._model_solved and self._task is None) + # A model of faults only has no units: no stratigraphic surfaces, no block model + has_units = not solved or self._has_stratigraphic_features() + if has_units and not self.surface_strat.isEnabled(): + self.surface_strat.setChecked(True) + self.surface_strat.setEnabled(has_units) + if not has_units: + self.surface_strat.setChecked(False) + self.tabs.setTabEnabled(1, has_units) + if not has_units: + self.hint.setText("The model has faults only, so the block model is not offered.") if self._model_solved: self._set_plane_defaults() + def _has_stratigraphic_features(self): + """Return False if every feature of the model is a fault.""" + manager = self.model_manager + if manager is None: + return True + faults = set(getattr(manager, 'faults', {}) or {}) | set( + getattr(manager, 'parametric_faults', {}) or {} + ) + names = [f.name for f in manager.features() if not f.name.startswith('__')] + return any(name not in faults for name in names) + def _plane_values(self): return tuple(w.value() for w in self.section_origin) + (self.section_size.value(),) diff --git a/loopstructural/main/derived_refresh.py b/loopstructural/main/derived_refresh.py index 9e74b41..126cc2f 100644 --- a/loopstructural/main/derived_refresh.py +++ b/loopstructural/main/derived_refresh.py @@ -31,12 +31,16 @@ def names_to_refresh(data_manager) -> List[str]: because the layer is an input of the user. - Thickness: only when it is out of date. A thickness that was never calculated can be typed, so the build does not calculate it. + + With no units in the column (a model of faults only), nothing comes from + the column, so the list is empty. """ derived = data_manager.derived names = [] + if not data_manager.get_stratigraphic_unit_names(): + return names if ( data_manager.layer_roles.contacts_source == layer_roles.CONTACTS_FROM_GEOLOGY - and data_manager.get_stratigraphic_unit_names() and data_manager.get_layer_role(layer_roles.GEOLOGY) is not None and data_manager.get_layer_role(layer_roles.GEOLOGY_UNIT_FIELD) and derived.status(derived_data.BASAL_CONTACTS) != derived_data.CURRENT diff --git a/loopstructural/main/model_manager.py b/loopstructural/main/model_manager.py index d7bc1f7..f87199e 100644 --- a/loopstructural/main/model_manager.py +++ b/loopstructural/main/model_manager.py @@ -1106,6 +1106,14 @@ def update_foliation_features(self): and the basal contacts and structural orientations data. This method will automatically add unconformities based on the stratigraphic column. """ + if self.stratigraphic_column is not None: + # also for an empty column, so the model does not keep the units + # of the last build + self.model.stratigraphic_column = self.stratigraphic_column + # A model with faults only has no column, or a column with no units + if self.stratigraphic_column is None or not self.stratigraphic_column.get_groups(): + self._emit('foliation_features_updated') + return stratigraphic_column = {} # Take each unit's training value straight from `get_isovalues()` # (LoopStructural core), which is what labels the extracted @@ -1719,18 +1727,19 @@ def model_state(self) -> str: @property def valid(self): - valid = True - if len(self.groups) == 0: - valid = False - if len(self.stratigraphy) == 0: - valid = False - if len(self.faults) > 0: - for _fault_name, fault_data in self.faults.items(): - if 'data' in fault_data and not fault_data['data'].empty: - valid = True - else: - valid = False - return valid + """True if the model has stratigraphy or faults, and each fault trace has data. + + A model with faults only (traces or parametric faults) has no groups + and no stratigraphy. + """ + if any( + 'data' not in fault_data or fault_data['data'].empty + for fault_data in self.faults.values() + ): + return False + has_stratigraphy = len(self.groups) > 0 and len(self.stratigraphy) > 0 + has_faults = len(self.faults) > 0 or len(self.parametric_faults) > 0 + return has_stratigraphy or has_faults def update_model( self, diff --git a/tests/qgis/test_parametric_fault.py b/tests/qgis/test_parametric_fault.py index 3a84b48..f1a0758 100644 --- a/tests/qgis/test_parametric_fault.py +++ b/tests/qgis/test_parametric_fault.py @@ -62,6 +62,20 @@ def test_the_fault_is_built_with_constraints(self, manager): manager.update_model(notify_observers=False) assert 'F1' in _names(manager) + def test_a_model_of_faults_only_with_an_empty_column(self, manager): + column = StratigraphicColumn() + column.clear(basement=False) + manager.stratigraphic_column = column + manager.add_parametric_fault(_spec()) + manager.update_model(notify_observers=False) + manager.update_all_features(notify_observers=False) + assert 'F1' in _names(manager) + # no stratigraphic feature + assert all(name.startswith(('F1', '__')) for name in _names(manager)) + assert manager.model.stratigraphic_column is column + assert manager.valid + assert manager.model_state == 'solved' + def test_the_fault_surface_is_where_the_fault_is(self, manager): manager.add_parametric_fault(_spec()) manager.update_model(notify_observers=False) diff --git a/tests/unit/test_build_plan.py b/tests/unit/test_build_plan.py index 0743959..357611c 100644 --- a/tests/unit/test_build_plan.py +++ b/tests/unit/test_build_plan.py @@ -176,6 +176,13 @@ def test_no_column_calculates_no_contacts(self, dm): dm.units = [] assert names_to_refresh(dm) == [] + def test_no_column_calculates_no_thickness_even_if_out_of_date(self, dm): + dm.derived.record(derived_data.BASAL_CONTACTS) + dm.derived.record(derived_data.THICKNESS) + dm.inputs = {'unit_order': []} + dm.units = [] + assert names_to_refresh(dm) == [] + def test_no_geology_layer_calculates_no_contacts(self, dm): dm.layer_roles.values[layer_roles.GEOLOGY] = None assert names_to_refresh(dm) == [] diff --git a/tests/unit/test_step_checks.py b/tests/unit/test_step_checks.py index 5f365a5..668fbfa 100644 --- a/tests/unit/test_step_checks.py +++ b/tests/unit/test_step_checks.py @@ -119,9 +119,16 @@ def map_layers(self, dm, request): def test_an_empty_column_is_not_started(self, dm): check = checks.check_stratigraphy(dm) assert check.status == Status.NOT_STARTED - assert 'Add units to the stratigraphic column.' in check.todo + assert any(t.startswith('Add units to the stratigraphic column.') for t in check.todo) + assert 'optional' in check.todo[0] + + def test_an_empty_column_does_not_ask_for_layers_or_contacts(self, dm): + todo = checks.check_stratigraphy(dm).todo + assert len(todo) == 1 + assert not checks.check_stratigraphy(dm).problems def test_missing_map_layers_are_a_todo(self, dm): + dm.units = ['a'] todo = checks.check_stratigraphy(dm).todo assert 'Select the geology layer.' in todo assert 'Select the structure layer.' in todo