Skip to content

Commit 9ee4853

Browse files
lachlangroseclaude
andcommitted
feat: evaluate stratigraphic units at arbitrary points for topography draping
Add sample_dem_grid, evaluate_stratigraphy_on_points and get_stratigraphic_column_colours, giving callers what they need to drape a DEM-derived surface with the stratigraphic column's colours: a DEM-sampled XY grid, per-point unit ids from evaluate_model, and a colour list ordered to match those ids. evaluate_model relies on each unit's min()/max() scalar-field range, which only gets computed by update_unit_values() -- a side effect of the interactive add_unit path. A column set as a whole (e.g. restored from a saved project, via add_element) never got that, so every unit fell back to range (0, inf) and evaluate_model couldn't distinguish any unit in a group from any other. Call update_unit_values() whenever set_stratigraphic_column/update_stratigraphic_column attach a column to the model, regardless of how it was built. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 0d13a6c commit 9ee4853

1 file changed

Lines changed: 67 additions & 0 deletions

File tree

loopstructural/main/model_manager.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,13 @@ def load_model(self, filepath):
214214
def set_stratigraphic_column(self, stratigraphic_column: StratigraphicColumn):
215215
"""Set the stratigraphic column for the geological model manager."""
216216
self.stratigraphic_column = stratigraphic_column
217+
# A column built via `add_element` (e.g. restored from a saved
218+
# project) never has each unit's min/max scalar-field range computed
219+
# -- only the interactive `add_unit` path does that as a side
220+
# effect. Without it every unit falls back to range (0, inf), so
221+
# `evaluate_model` can't tell any unit in a group apart from any
222+
# other and just picks whichever was added last.
223+
self.stratigraphic_column.update_unit_values()
217224
# changing the stratigraphic column changes model geometry
218225
self._emit('stratigraphic_column_changed')
219226

@@ -518,6 +525,7 @@ def update_structural_data(
518525
def update_stratigraphic_column(self, stratigraphic_column: StratigraphicColumn):
519526
"""Update the stratigraphic column with a new stratigraphic column"""
520527
self.stratigraphic_column = stratigraphic_column
528+
self.stratigraphic_column.update_unit_values()
521529
self.update_foliation_features()
522530

523531
# def update_stratigraphic_unit(self, unit_data):
@@ -1268,6 +1276,65 @@ def evaluate_feature_on_points(
12681276
# Re-raise with context preserved for the caller/UI to handle
12691277
raise
12701278

1279+
def sample_dem_grid(self, resolution: int = 100) -> 'tuple[np.ndarray, np.ndarray, np.ndarray]':
1280+
"""Sample the current DEM on a regular grid covering the model's XY extent.
1281+
1282+
Parameters
1283+
----------
1284+
resolution : int, optional
1285+
Number of sample points along each axis, by default 100.
1286+
1287+
Returns
1288+
-------
1289+
tuple(np.ndarray, np.ndarray, np.ndarray)
1290+
X, Y, Z meshgrid arrays of shape (resolution, resolution). Z comes
1291+
from `self.dem_function` (see `set_dem_function`).
1292+
"""
1293+
if self.model is None:
1294+
raise RuntimeError('No model available to build a topography surface')
1295+
bb = self.model.bounding_box
1296+
x = np.linspace(bb.origin[0], bb.maximum[0], resolution)
1297+
y = np.linspace(bb.origin[1], bb.maximum[1], resolution)
1298+
xx, yy = np.meshgrid(x, y)
1299+
zz = np.vectorize(self.dem_function)(xx, yy)
1300+
return xx, yy, zz
1301+
1302+
def evaluate_stratigraphy_on_points(self, points: np.ndarray) -> np.ndarray:
1303+
"""Evaluate the stratigraphic unit id of the model at the given points.
1304+
1305+
Parameters
1306+
----------
1307+
points : array_like
1308+
An (N, 3) array-like of points [x, y, z] at which to evaluate.
1309+
1310+
Returns
1311+
-------
1312+
numpy.ndarray
1313+
(N,) array of stratigraphic unit ids. A point outside every
1314+
stratigraphic unit gets id -1 (see `GeologicalModel.evaluate_model`).
1315+
"""
1316+
if self.model is None:
1317+
raise RuntimeError('No model available for evaluation')
1318+
pts = np.asarray(points)
1319+
if pts.ndim != 2 or pts.shape[1] < 3:
1320+
raise ValueError('points must be an Nx3 array')
1321+
return np.asarray(self.model.evaluate_model(pts))
1322+
1323+
def get_stratigraphic_column_colours(self) -> list:
1324+
"""Return unit colours ordered to line up with `evaluate_model`'s ids.
1325+
1326+
`GeologicalModel.evaluate_model` assigns each stratigraphic unit an id
1327+
by counting through `reversed(stratigraphic_column.get_groups())`, so
1328+
this walks the column the same way -- `colours[i]` is the colour of
1329+
whichever unit `evaluate_model` labels `i`.
1330+
"""
1331+
if self.model is None or self.model.stratigraphic_column is None:
1332+
return []
1333+
colours = []
1334+
for group in reversed(self.model.stratigraphic_column.get_groups()):
1335+
colours.extend(unit.colour for unit in group.units)
1336+
return colours
1337+
12711338
def export_feature_values_to_geodataframe(
12721339
self,
12731340
feature_name: str,

0 commit comments

Comments
 (0)