Skip to content

Commit c487dec

Browse files
committed
fix: add generic exporter for m2l objects
1 parent e590b19 commit c487dec

File tree

2 files changed

+184
-10
lines changed

2 files changed

+184
-10
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import json
2+
import pickle
3+
from doctest import debug
4+
from pathlib import Path
5+
from typing import Any, Dict, Optional
6+
7+
8+
def export_debug_package(
9+
debug_manager,
10+
m2l_object,
11+
runner_script_name: str = "run_debug_model.py",
12+
params: Dict[str, Any] = {},
13+
):
14+
15+
exported: Dict[str, str] = {}
16+
if not debug_manager or not getattr(debug_manager, "is_debug", lambda: False)():
17+
return exported
18+
# store the m2l object (calculator/sampler etc) and the parameters used
19+
# in its main function e.g. compute(), sample() etc
20+
# these will be pickled and saved to the debug directory
21+
# with the prefix of the runner script name to avoid name clashes
22+
# e.g. run_debug_model_m2l_object.pkl, run_debug_model_parameters.pkl
23+
pickles = {'m2l_object': m2l_object, 'params': params}
24+
25+
if pickles:
26+
for name, obj in pickles.items():
27+
pkl_name = f"{runner_script_name.replace('.py', '')}_{name}.pkl"
28+
try:
29+
debug_manager.save_debug_file(pkl_name, pickle.dumps(obj))
30+
exported[name] = pkl_name
31+
except Exception as e:
32+
debug_manager.logger(f"Failed to save debug file '{pkl_name}': {e}")
33+
34+
script = (
35+
open(Path(__file__).parent / 'template.txt')
36+
.read()
37+
.format(runner_name=runner_script_name.replace('.py', ''))
38+
)
39+
debug_manager.save_debug_file(runner_script_name, script.encode("utf-8"))
40+
debug_manager.logger(f"Exported debug package with runner script '{runner_script_name}'")
41+
return exported

loopstructural/main/m2l_api.py

Lines changed: 143 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from unittest import runner
2+
13
import pandas as pd
24
from map2loop.contact_extractor import ContactExtractor
35
from map2loop.sampler import SamplerDecimator, SamplerSpacing
@@ -10,8 +12,12 @@
1012
)
1113
from map2loop.thickness_calculator import InterpolatedStructure, StructuralPoint
1214
from osgeo import gdal
15+
from pkg_resources import run_main
16+
17+
from loopstructural.main.debug import export
1318

1419
from ..main.vectorLayerWrapper import qgsLayerToDataFrame, qgsLayerToGeoDataFrame
20+
from .debug.export import export_debug_package
1521

1622
# Mapping of sorter names to sorter classes
1723
SORTER_LIST = {
@@ -38,6 +44,7 @@ def extract_basal_contacts(
3844
unit_name_field=None,
3945
all_contacts=False,
4046
updater=None,
47+
debug_manager=None,
4148
):
4249
"""Extract basal contacts from geological data.
4350
@@ -76,9 +83,40 @@ def extract_basal_contacts(
7683
faults = qgsLayerToGeoDataFrame(faults) if faults else None
7784
if unit_name_field and unit_name_field != 'UNITNAME' and unit_name_field in geology.columns:
7885
geology = geology.rename(columns={unit_name_field: 'UNITNAME'})
86+
# Log parameters via DebugManager if provided
87+
if debug_manager:
88+
debug_manager.log_params(
89+
"extract_basal_contacts",
90+
{
91+
"stratigraphic_order": stratigraphic_order,
92+
"ignore_units": ignore_units,
93+
"unit_name_field": unit_name_field,
94+
"all_contacts": all_contacts,
95+
"geology": geology,
96+
"faults": faults,
97+
},
98+
)
7999
if updater:
80100
updater("Extracting Basal Contacts...")
81101
contact_extractor = ContactExtractor(geology, faults)
102+
# If debug_manager present and debug mode enabled, export tool, layers and params
103+
try:
104+
if debug_manager and getattr(debug_manager, "is_debug", lambda: False)():
105+
106+
layers = {"geology": geology, "faults": faults}
107+
pickles = {"contact_extractor": contact_extractor}
108+
# export layers and pickles first to get the actual filenames used
109+
exported = export_debug_package(
110+
debug_manager,
111+
runner_script_name="run_extract_basal_contacts.py",
112+
m2l_object=contact_extractor,
113+
params={'stratigraphic_order': stratigraphic_order},
114+
)
115+
116+
except Exception as e:
117+
print("Failed to save sampler debug info")
118+
print(e)
119+
82120
all_contacts_result = contact_extractor.extract_all_contacts()
83121
basal_contacts = contact_extractor.extract_basal_contacts(stratigraphic_order)
84122

@@ -104,6 +142,7 @@ def sort_stratigraphic_column(
104142
dipdir_field="DIPDIR",
105143
orientation_type="Dip Direction",
106144
dtm=None,
145+
debug_manager=None,
107146
updater=None,
108147
contacts=None,
109148
):
@@ -153,6 +192,23 @@ def sort_stratigraphic_column(
153192
# Convert layers to GeoDataFrames
154193
geology_gdf = qgsLayerToGeoDataFrame(geology)
155194
contacts_gdf = qgsLayerToGeoDataFrame(contacts)
195+
196+
# Log parameters via DebugManager if provided
197+
if debug_manager:
198+
debug_manager.log_params(
199+
"sort_stratigraphic_column",
200+
{
201+
"sorting_algorithm": sorting_algorithm,
202+
"unit_name_field": unit_name_field,
203+
"min_age_field": min_age_field,
204+
"max_age_field": max_age_field,
205+
"orientation_type": orientation_type,
206+
"dtm": dtm,
207+
"geology": geology_gdf,
208+
"contacts": contacts_gdf,
209+
},
210+
)
211+
156212
# Build units DataFrame
157213
if (
158214
unit_name_field
@@ -208,6 +264,21 @@ def sort_stratigraphic_column(
208264
sorter_args = {k: v for k, v in all_args.items() if k in required_args}
209265
print(f'Calling sorter with args: {sorter_args.keys()}')
210266
sorter = sorter_cls(**sorter_args)
267+
# If debugging, pickle sorter and write a small runner script
268+
try:
269+
if debug_manager and getattr(debug_manager, "is_debug", lambda: False)():
270+
271+
_exported = export_debug_package(
272+
debug_manager,
273+
m2l_object=sorter,
274+
params={'units_df': units_df},
275+
runner_script_name="run_sort_stratigraphic_column.py",
276+
)
277+
278+
except Exception as e:
279+
print("Failed to save sampler debug info")
280+
print(e)
281+
211282
order = sorter.sort(units_df)
212283
if updater:
213284
updater(f"Sorting complete: {len(order)} units ordered")
@@ -222,6 +293,7 @@ def sample_contacts(
222293
spacing=None,
223294
dtm=None,
224295
geology=None,
296+
debug_manager=None,
225297
updater=None,
226298
):
227299
"""Sample spatial data using map2loop samplers.
@@ -267,6 +339,20 @@ def sample_contacts(
267339
if geology is not None:
268340
geology_gdf = qgsLayerToGeoDataFrame(geology)
269341

342+
# Log parameters via DebugManager if provided
343+
if debug_manager:
344+
debug_manager.log_params(
345+
"sample_contacts",
346+
{
347+
"sampler_type": sampler_type,
348+
"decimation": decimation,
349+
"spacing": spacing,
350+
"dtm": dtm,
351+
"geology": geology_gdf,
352+
"spatial_data": spatial_gdf,
353+
},
354+
)
355+
270356
# Run sampler
271357
if sampler_type == "Decimator":
272358
if decimation is None:
@@ -281,8 +367,19 @@ def sample_contacts(
281367

282368
samples = sampler.sample(spatial_gdf)
283369

284-
if updater:
285-
updater(f"Sampling complete: {len(samples)} samples generated")
370+
try:
371+
if debug_manager and getattr(debug_manager, "is_debug", lambda: False)():
372+
_exported = export_debug_package(
373+
debug_manager,
374+
m2l_object=sampler,
375+
params={'spatial_data': spatial_gdf},
376+
runner_script_name='run_sample_contacts.py',
377+
)
378+
379+
except Exception as e:
380+
print("Failed to save sampler debug info")
381+
print(e)
382+
pass
286383

287384
return samples
288385

@@ -300,6 +397,7 @@ def calculate_thickness(
300397
orientation_type="Dip Direction",
301398
max_line_length=None,
302399
stratigraphic_order=None,
400+
debug_manager=None,
303401
updater=None,
304402
basal_contacts_unit_name=None,
305403
):
@@ -352,6 +450,24 @@ def calculate_thickness(
352450
)
353451
sampled_contacts_gdf = qgsLayerToGeoDataFrame(sampled_contacts)
354452
structure_gdf = qgsLayerToDataFrame(structure)
453+
454+
# Log parameters via DebugManager if provided
455+
if debug_manager:
456+
debug_manager.log_params(
457+
"calculate_thickness",
458+
{
459+
"calculator_type": calculator_type,
460+
"unit_name_field": unit_name_field,
461+
"orientation_type": orientation_type,
462+
"max_line_length": max_line_length,
463+
"stratigraphic_order": stratigraphic_order,
464+
"geology": geology_gdf,
465+
"basal_contacts": basal_contacts_gdf,
466+
"sampled_contacts": sampled_contacts_gdf,
467+
"structure": structure_gdf,
468+
},
469+
)
470+
355471
bounding_box = {
356472
'maxx': geology_gdf.total_bounds[2],
357473
'minx': geology_gdf.total_bounds[0],
@@ -408,7 +524,30 @@ def calculate_thickness(
408524
units_unique = units.drop_duplicates(subset=['UNITNAME']).reset_index(drop=True)
409525
units = pd.DataFrame({'name': units_unique['UNITNAME']})
410526
basal_contacts_gdf['type'] = 'BASAL' # required by calculator
411-
527+
528+
# No local export path placeholders required; export_debug_package handles exports
529+
try:
530+
if debug_manager and getattr(debug_manager, "is_debug", lambda: False)():
531+
# Export layers and pickled objects first to get their exported filenames
532+
533+
_exported = export_debug_package(
534+
debug_manager,
535+
runner_script_name="run_calculate_thickness.py",
536+
m2l_object=calculator,
537+
params={
538+
'units': units,
539+
'stratigraphic_order': stratigraphic_order,
540+
'basal_contacts': basal_contacts_gdf,
541+
'structure': structure_gdf,
542+
'geology': geology_gdf,
543+
'sampled_contacts': sampled_contacts_gdf,
544+
},
545+
)
546+
547+
except Exception as e:
548+
print("Failed to save sampler debug info")
549+
raise e
550+
412551
thickness = calculator.compute(
413552
units,
414553
stratigraphic_order,
@@ -417,12 +556,6 @@ def calculate_thickness(
417556
geology_gdf,
418557
sampled_contacts_gdf,
419558
)
559+
# Ensure result object exists for return and for any debug export
420560
res = {'thicknesses': thickness}
421-
if updater:
422-
updater(f"Thickness calculation complete: {len(thickness)} records")
423-
if hasattr(calculator, 'lines'):
424-
res['lines'] = calculator.lines
425-
if hasattr(calculator, 'location_tracking'):
426-
res['location_tracking'] = calculator.location_tracking
427-
428561
return res

0 commit comments

Comments
 (0)