|
| 1 | +"""Regression test for the training-value / isovalue direction bug. |
| 2 | +
|
| 3 | +`GeologicalModelManager.update_foliation_features` assigns a scalar `val` to |
| 4 | +each unit's basal contact before handing the data to the interpolator. |
| 5 | +`StratigraphicColumn.get_isovalues` (LoopStructural core) later decides which |
| 6 | +name to stamp on each extracted isosurface, using its own idea of which |
| 7 | +value belongs to which unit. |
| 8 | +
|
| 9 | +These two must agree on direction (does value increase from oldest-to- |
| 10 | +youngest, or youngest-to-oldest?), or every extracted surface gets labelled |
| 11 | +with the wrong unit while keeping correct geometry -- see the "stratigraphic |
| 12 | +column was reversed" fixes in model_manager.py (2025-07-21) and the widget |
| 13 | +(2025-08-21, reverted 2025-09-08). This has flipped back and forth as this |
| 14 | +plugin and LoopStructural evolved independently; this test pins the |
| 15 | +invariant so a future change on either side fails loudly here instead of |
| 16 | +silently inverting a user's model. |
| 17 | +""" |
| 18 | + |
| 19 | +import pandas as pd |
| 20 | +import pytest |
| 21 | +from LoopStructural import StratigraphicColumn |
| 22 | + |
| 23 | +from loopstructural.main.model_manager import GeologicalModelManager |
| 24 | + |
| 25 | + |
| 26 | +def _contact(unit_name): |
| 27 | + """A minimal single-point basal contact, tagged with its unit name so |
| 28 | + the test can recover which row came from which unit after the group |
| 29 | + DataFrames get concatenated.""" |
| 30 | + return pd.DataFrame({'X': [0.0], 'Y': [0.0], 'Z': [0.0], 'source_unit': [unit_name]}) |
| 31 | + |
| 32 | + |
| 33 | +@pytest.fixture |
| 34 | +def manager(monkeypatch): |
| 35 | + manager = GeologicalModelManager() |
| 36 | + |
| 37 | + captured_calls = [] |
| 38 | + |
| 39 | + def fake_create_and_add_foliation(name, data=None, **kwargs): |
| 40 | + captured_calls.append(data) |
| 41 | + return object() # stand-in foliation, only passed back into add_unconformity |
| 42 | + |
| 43 | + monkeypatch.setattr(manager.model, 'create_and_add_foliation', fake_create_and_add_foliation) |
| 44 | + monkeypatch.setattr(manager.model, 'add_unconformity', lambda *a, **k: None) |
| 45 | + manager._captured_calls = captured_calls |
| 46 | + return manager |
| 47 | + |
| 48 | + |
| 49 | +class TestTrainingValueMatchesIsovalue: |
| 50 | + def test_single_group_three_units(self, manager): |
| 51 | + column = StratigraphicColumn() |
| 52 | + column.clear(basement=False) # single flat group, no unconformities |
| 53 | + column.add_unit(name='oldest', thickness=100.0, where='top') |
| 54 | + column.add_unit(name='middle', thickness=200.0, where='top') |
| 55 | + column.add_unit(name='youngest', thickness=300.0, where='top') |
| 56 | + |
| 57 | + manager.stratigraphic_column = column |
| 58 | + for name in ('oldest', 'middle', 'youngest'): |
| 59 | + manager.stratigraphy[name]['contact'] = _contact(name) |
| 60 | + |
| 61 | + manager.update_foliation_features() |
| 62 | + |
| 63 | + training_values = self._training_values_by_unit(manager._captured_calls) |
| 64 | + expected_values = { |
| 65 | + name: entry['value'] for name, entry in column.get_isovalues().items() |
| 66 | + } |
| 67 | + |
| 68 | + for unit_name in ('oldest', 'middle', 'youngest'): |
| 69 | + assert training_values[unit_name] == pytest.approx(expected_values[unit_name]), ( |
| 70 | + f"'{unit_name}' was trained with val={training_values[unit_name]} but " |
| 71 | + f"get_isovalues() will label the value={expected_values[unit_name]} surface " |
| 72 | + f"with this unit's name -- the trained field and the isosurface labels " |
| 73 | + f"disagree on direction, so extracted surfaces will get the wrong unit name." |
| 74 | + ) |
| 75 | + |
| 76 | + def test_two_groups_split_by_unconformity(self, manager): |
| 77 | + column = StratigraphicColumn() |
| 78 | + column.clear(basement=False) |
| 79 | + column.add_unit(name='basin_floor', thickness=50.0, where='top') |
| 80 | + column.add_unit(name='basin_fill', thickness=150.0, where='top') |
| 81 | + column.add_unconformity(name='regional_unconformity', where='top') |
| 82 | + column.add_unit(name='cover_lower', thickness=80.0, where='top') |
| 83 | + column.add_unit(name='cover_upper', thickness=120.0, where='top') |
| 84 | + |
| 85 | + manager.stratigraphic_column = column |
| 86 | + for name in ('basin_floor', 'basin_fill', 'cover_lower', 'cover_upper'): |
| 87 | + manager.stratigraphy[name]['contact'] = _contact(name) |
| 88 | + |
| 89 | + manager.update_foliation_features() |
| 90 | + |
| 91 | + training_values = self._training_values_by_unit(manager._captured_calls) |
| 92 | + expected_values = { |
| 93 | + name: entry['value'] for name, entry in column.get_isovalues().items() |
| 94 | + } |
| 95 | + |
| 96 | + for unit_name in ('basin_floor', 'basin_fill', 'cover_lower', 'cover_upper'): |
| 97 | + assert training_values[unit_name] == pytest.approx(expected_values[unit_name]) |
| 98 | + |
| 99 | + @staticmethod |
| 100 | + def _training_values_by_unit(captured_calls): |
| 101 | + combined = pd.concat(captured_calls, ignore_index=True) |
| 102 | + return dict(zip(combined['source_unit'], combined['val'])) |
0 commit comments