Skip to content

Commit 0331b60

Browse files
committed
fix: give observation projections a UNITNAME geology and a projection length
map2loop's SorterObservationProjections reads the literal 'UNITNAME' column from the geology data, so a layer with a different unit name field failed with KeyError: 'UNITNAME'. Rename the unit name field for the geology that is given to the sorters, and add an 'ID' column to the structure data when it has none, because the sorter logs row.ID. Add a projection length setting (default 1000) to the sorter panel and to sort_stratigraphic_column.
1 parent feab444 commit 0331b60

3 files changed

Lines changed: 71 additions & 8 deletions

File tree

‎loopstructural/gui/map2loop_tools/sorter_widget.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ def _restore_selection(self):
177177
self.sortingAlgorithmComboBox.setCurrentIndex(settings['sorting_algorithm'])
178178
if 'orientation_type' in settings:
179179
self.orientationTypeComboBox.setCurrentIndex(settings['orientation_type'])
180+
if 'projection_length' in settings:
181+
self.projectionLengthSpinBox.setValue(settings['projection_length'])
180182
for key, combo in (
181183
('unit_name_field', self.unitNameFieldComboBox),
182184
('min_age_field', self.minAgeFieldComboBox),
@@ -209,6 +211,7 @@ def _persist_selection(self):
209211
),
210212
'sorting_algorithm': self.sortingAlgorithmComboBox.currentIndex(),
211213
'orientation_type': self.orientationTypeComboBox.currentIndex(),
214+
'projection_length': self.projectionLengthSpinBox.value(),
212215
'unit_name_field': self.unitNameFieldComboBox.currentField(),
213216
'min_age_field': self.minAgeFieldComboBox.currentField(),
214217
'max_age_field': self.maxAgeFieldComboBox.currentField(),
@@ -299,6 +302,8 @@ def _on_algorithm_changed(self):
299302
self.orientationTypeComboBox.setVisible(False)
300303
self.dtmLayerLabel.setVisible(False)
301304
self.dtmLayerComboBox.setVisible(False)
305+
self.projectionLengthLabel.setVisible(False)
306+
self.projectionLengthSpinBox.setVisible(False)
302307

303308
# Show widgets based on required fields
304309
geology_layer = self.geologyLayerComboBox.currentLayer()
@@ -326,6 +331,8 @@ def _on_algorithm_changed(self):
326331
self.orientationTypeComboBox.setVisible(True)
327332
self.dtmLayerLabel.setVisible(True)
328333
self.dtmLayerComboBox.setVisible(True)
334+
self.projectionLengthLabel.setVisible(True)
335+
self.projectionLengthSpinBox.setVisible(True)
329336
elif algorithm_name in SORTERS_WITHOUT_YOUNGING:
330337
# Optional: the structure data sets which end of the order is youngest.
331338
self.structureLayerLabel.setVisible(True)
@@ -413,6 +420,7 @@ def _run_sorter(self):
413420
self.orientationTypeComboBox.currentIndex()
414421
]
415422
kwargs['dtm'] = self.dtmLayerComboBox.currentLayer()
423+
kwargs['projection_length'] = self.projectionLengthSpinBox.value()
416424

417425
def target(progress_callback):
418426
return sort_stratigraphic_column(
@@ -518,6 +526,7 @@ def get_parameters(self):
518526
params['dipdir_field'] = self.dipDirFieldComboBox.currentField()
519527
params['orientation_type'] = self.orientationTypeComboBox.currentIndex()
520528
params['dtm_layer'] = self.dtmLayerComboBox.currentLayer()
529+
params['projection_length'] = self.projectionLengthSpinBox.value()
521530

522531
return params
523532

@@ -533,3 +542,5 @@ def set_parameters(self, params):
533542
self.sortingAlgorithmComboBox.setCurrentIndex(params['sorting_algorithm'])
534543
if params.get('geology_layer'):
535544
self.geologyLayerComboBox.setLayer(params['geology_layer'])
545+
if params.get('projection_length'):
546+
self.projectionLengthSpinBox.setValue(params['projection_length'])

‎loopstructural/gui/map2loop_tools/sorter_widget.ui‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,35 @@
125125
</property>
126126
</widget>
127127
</item>
128+
<item row="10" column="0">
129+
<widget class="QLabel" name="projectionLengthLabel">
130+
<property name="text">
131+
<string>Projection Length:</string>
132+
</property>
133+
</widget>
134+
</item>
135+
<item row="10" column="1">
136+
<widget class="QDoubleSpinBox" name="projectionLengthSpinBox">
137+
<property name="toolTip">
138+
<string>Length of the line drawn from each structure point in the dip direction, in map units.</string>
139+
</property>
140+
<property name="decimals">
141+
<number>0</number>
142+
</property>
143+
<property name="minimum">
144+
<double>1.000000000000000</double>
145+
</property>
146+
<property name="maximum">
147+
<double>1000000.000000000000000</double>
148+
</property>
149+
<property name="singleStep">
150+
<double>100.000000000000000</double>
151+
</property>
152+
<property name="value">
153+
<double>1000.000000000000000</double>
154+
</property>
155+
</widget>
156+
</item>
128157
</layout>
129158
</item>
130159
<item>

‎loopstructural/main/m2l_api.py‎

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -419,14 +419,13 @@ def extract_basal_contacts(
419419
return {'basal_contacts': basal_contacts}
420420

421421

422-
def _extract_contacts_for_sorting(geology_gdf, unit_name_field, updater=None):
423-
"""Derive unit-to-unit contacts directly from geology, for sorters that need adjacency.
422+
def _geology_with_unitname(geology_gdf, unit_name_field):
423+
"""Return a copy of the geology with the unit names in a 'UNITNAME' column.
424424
425-
SorterAlpha, SorterMaximiseContacts and SorterObservationProjections all
426-
require a 'contacts' GeoDataFrame with 'UNITNAME_1'/'UNITNAME_2' columns.
427-
Unlike basal contacts, this adjacency doesn't depend on a stratigraphic
428-
order -- which isn't known yet at this point, since sorting is what
429-
produces it -- so it can always be derived from the geology layer alone.
425+
map2loop reads the literal 'UNITNAME' column from the geology data (for
426+
example ContactExtractor, SorterUseNetworkX and
427+
SorterObservationProjections), so a layer that uses a different unit name
428+
field must be renamed first.
430429
"""
431430
geology_gdf = geology_gdf.copy()
432431
if unit_name_field and unit_name_field != 'UNITNAME' and unit_name_field in geology_gdf.columns:
@@ -438,6 +437,19 @@ def _extract_contacts_for_sorting(geology_gdf, unit_name_field, updater=None):
438437
if 'UNITNAME' in geology_gdf.columns:
439438
geology_gdf = geology_gdf.drop(columns=['UNITNAME'])
440439
geology_gdf = geology_gdf.rename(columns={unit_name_field: 'UNITNAME'})
440+
return geology_gdf
441+
442+
443+
def _extract_contacts_for_sorting(geology_gdf, unit_name_field, updater=None):
444+
"""Derive unit-to-unit contacts directly from geology, for sorters that need adjacency.
445+
446+
SorterAlpha, SorterMaximiseContacts and SorterObservationProjections all
447+
require a 'contacts' GeoDataFrame with 'UNITNAME_1'/'UNITNAME_2' columns.
448+
Unlike basal contacts, this adjacency doesn't depend on a stratigraphic
449+
order -- which isn't known yet at this point, since sorting is what
450+
produces it -- so it can always be derived from the geology layer alone.
451+
"""
452+
geology_gdf = _geology_with_unitname(geology_gdf, unit_name_field)
441453
if updater:
442454
updater("Extracting contacts from geology...")
443455
return ContactExtractor(geology_gdf, None).extract_all_contacts()
@@ -459,6 +471,7 @@ def sort_stratigraphic_column(
459471
debug_manager=None,
460472
updater=None,
461473
contacts=None,
474+
projection_length=1000.0,
462475
):
463476
"""Sort stratigraphic units using map2loop sorters.
464477
@@ -493,6 +506,9 @@ def sort_stratigraphic_column(
493506
Digital terrain model, by default None.
494507
updater : callable, optional
495508
Callback function for progress updates, by default None.
509+
projection_length : float, optional
510+
Length in map units of the line that Observation projections draws
511+
from each structure point in the dip direction, by default 1000.
496512
497513
Returns
498514
-------
@@ -593,6 +609,10 @@ def sort_stratigraphic_column(
593609
structure_gdf['DIPDIR'] = structure_gdf[dipdir_field]
594610
if dip_values is not None:
595611
structure_gdf['DIP'] = dip_values
612+
# SorterObservationProjections logs row.ID for points that are not
613+
# in a unit, so the column must exist.
614+
if 'ID' not in structure_gdf.columns:
615+
structure_gdf['ID'] = range(len(structure_gdf))
596616

597617
# Convert DTM to a GDAL dataset, as map2loop's sorters read it via GDAL calls.
598618
dtm_gdal = None
@@ -604,7 +624,7 @@ def sort_stratigraphic_column(
604624

605625
# Prepare all possible arguments
606626
all_args = {
607-
'geology_data': geology_gdf,
627+
'geology_data': _geology_with_unitname(geology_gdf, unit_name_field),
608628
'contacts': contacts_gdf,
609629
'relationships': relationships_df,
610630
'unit_name_field': unit_name_field,
@@ -628,6 +648,9 @@ def sort_stratigraphic_column(
628648

629649
# Only pass required arguments to the sorter
630650
sorter_args = {k: v for k, v in all_args.items() if k in required_args}
651+
# 'length' is optional in map2loop, so it is not in required_arguments.
652+
if sorter_cls is SorterObservationProjections and projection_length:
653+
sorter_args['length'] = float(projection_length)
631654
logger.debug('Calling sorter with args: %s', list(sorter_args.keys()))
632655
sorter = sorter_cls(**sorter_args)
633656
# If debugging, pickle sorter and write a small runner script

0 commit comments

Comments
 (0)