|
6 | 6 | name to stamp on each extracted isosurface, using its own idea of which |
7 | 7 | value belongs to which unit. |
8 | 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. |
| 9 | +
|
| 10 | +Both walk `reversed(group.units)`, accumulating cumulative thickness the |
| 11 | +same way, so a unit's own training value must equal `u.min()` -- the |
| 12 | +cumulative thickness *before* that unit's own thickness is added. This is |
| 13 | +also each unit's true base: `add_unit(..., where='top')` (the default) |
| 14 | +appends to the end of the column, so building a column correctly means |
| 15 | +adding the truly oldest unit first and progressively younger ones after -- |
| 16 | +each unit's own base is the boundary shared with the next-older neighbour |
| 17 | +processed just before it, i.e. `min()`. See LoopStructural's own |
| 18 | +`test_get_isovalues_multi_unit_group` (`tests/unit/modelling/ |
| 19 | +test_stratigraphic_column.py`), whose comment states this explicitly: "the |
| 20 | +base of the oldest unit in a group is 0". |
| 21 | +
|
| 22 | +If training and `get_isovalues()` disagree on this, every extracted surface |
| 23 | +gets labelled with the wrong unit while keeping correct geometry -- see the |
| 24 | +"stratigraphic column was reversed" fixes in model_manager.py (2025-07-21) |
| 25 | +and the widget (2025-08-21, reverted 2025-09-08). This has flipped back and |
| 26 | +forth as this plugin and LoopStructural evolved independently; this test |
| 27 | +pins the invariant so a future change on either side fails loudly here |
| 28 | +instead of silently inverting a user's model. |
| 29 | +
|
| 30 | +Note this is a separate concern from whether a stratigraphic column's units |
| 31 | +were themselves *added* in the correct oldest-to-youngest order -- if they |
| 32 | +weren't, `min()`/`max()` stop corresponding to true geological base/top no |
| 33 | +matter what training does, and the fix is to reorder the column's units, |
| 34 | +not to change which value training uses. |
17 | 35 | """ |
18 | 36 |
|
19 | 37 | import pandas as pd |
@@ -96,6 +114,42 @@ def test_two_groups_split_by_unconformity(self, manager): |
96 | 114 | for unit_name in ('basin_floor', 'basin_fill', 'cover_lower', 'cover_upper'): |
97 | 115 | assert training_values[unit_name] == pytest.approx(expected_values[unit_name]) |
98 | 116 |
|
| 117 | + |
| 118 | + def test_undigitised_unit_does_not_shift_later_units_in_group(self, manager): |
| 119 | + """Regression test for a real bug: a unit with no digitised contact |
| 120 | + or orientation data (e.g. a "Top" unit nobody has mapped points for) |
| 121 | + must still contribute its own thickness to `val` for every unit |
| 122 | + that follows it in the group -- `update_foliation_features` used to |
| 123 | + `continue` past an undigitised unit before accumulating its |
| 124 | + thickness, which shifted every later unit's trained value relative |
| 125 | + to what `get_isovalues()` expects. |
| 126 | + """ |
| 127 | + column = StratigraphicColumn() |
| 128 | + column.clear(basement=False) |
| 129 | + column.add_unit(name='basin_floor', thickness=50.0, where='top') |
| 130 | + column.add_unit(name='basin_fill', thickness=150.0, where='top') |
| 131 | + column.add_unit(name='Top', thickness=999.0, where='top') |
| 132 | + |
| 133 | + manager.stratigraphic_column = column |
| 134 | + for name in ('basin_floor', 'basin_fill'): |
| 135 | + manager.stratigraphy[name]['contact'] = _contact(name) |
| 136 | + # 'Top' deliberately has no entry in manager.stratigraphy at all. |
| 137 | + |
| 138 | + manager.update_foliation_features() |
| 139 | + |
| 140 | + training_values = self._training_values_by_unit(manager._captured_calls) |
| 141 | + expected_values = { |
| 142 | + name: entry['value'] for name, entry in column.get_isovalues().items() |
| 143 | + } |
| 144 | + |
| 145 | + for unit_name in ('basin_floor', 'basin_fill'): |
| 146 | + assert training_values[unit_name] == pytest.approx(expected_values[unit_name]), ( |
| 147 | + f"'{unit_name}' was trained with val={training_values[unit_name]} but " |
| 148 | + f"get_isovalues() expects value={expected_values[unit_name]} -- an " |
| 149 | + f"undigitised unit earlier in the group must still shift later units' " |
| 150 | + f"trained values by its own thickness." |
| 151 | + ) |
| 152 | + |
99 | 153 | @staticmethod |
100 | 154 | def _training_values_by_unit(captured_calls): |
101 | 155 | combined = pd.concat(captured_calls, ignore_index=True) |
|
0 commit comments