Skip to content

Commit ed17d93

Browse files
lachlangroseclaude
andcommitted
test: pin training-value/isovalue direction agreement for stratigraphic columns
Guards against the swap fixed in 814be12: model_manager.py's per-unit training value and LoopStructural's get_isovalues() must agree on which direction values increase, or extracted isosurfaces get labelled with the wrong unit while keeping correct geometry. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 814be12 commit ed17d93

1 file changed

Lines changed: 102 additions & 0 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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

Comments
 (0)