@@ -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