From f8e2014fd866dcd0032da7663b03170df8780a12 Mon Sep 17 00:00:00 2001 From: domfournier Date: Wed, 19 Aug 2026 15:29:22 -0700 Subject: [PATCH 01/16] Initial implementation of borehole tem --- .../uijson/borehole_tdem_forward.ui.json | 231 ++++++ .../uijson/borehole_tdem_inversion.ui.json | 660 ++++++++++++++++++ simpeg_drivers/__init__.py | 7 + simpeg_drivers/components/data.py | 1 + .../factories/directives_factory.py | 2 + .../components/factories/receiver_factory.py | 3 + .../components/factories/simpeg_factory.py | 1 + .../factories/simulation_factory.py | 2 + .../borehole_time_domain/__init__.py | 9 + .../borehole_time_domain/forward.py | 27 + .../borehole_time_domain/inversion.py | 29 + .../borehole_time_domain/options.py | 64 ++ 12 files changed, 1036 insertions(+) create mode 100644 simpeg_drivers-assets/uijson/borehole_tdem_forward.ui.json create mode 100644 simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json create mode 100644 simpeg_drivers/electromagnetics/borehole_time_domain/__init__.py create mode 100644 simpeg_drivers/electromagnetics/borehole_time_domain/forward.py create mode 100644 simpeg_drivers/electromagnetics/borehole_time_domain/inversion.py create mode 100644 simpeg_drivers/electromagnetics/borehole_time_domain/options.py diff --git a/simpeg_drivers-assets/uijson/borehole_tdem_forward.ui.json b/simpeg_drivers-assets/uijson/borehole_tdem_forward.ui.json new file mode 100644 index 000000000..cfc7fd72b --- /dev/null +++ b/simpeg_drivers-assets/uijson/borehole_tdem_forward.ui.json @@ -0,0 +1,231 @@ +{ + "version": "0.0.0", + "title": "Borehole TDEM Forward", + "icon": "surveyairborneem", + "documentation": "https://mirageoscience-simpeg-drivers.readthedocs-hosted.com/en/latest/", + "conda_environment": "simpeg-drivers", + "run_command": "simpeg_drivers.electromagnetics.borehole_time_domain.forward", + "geoh5": "", + "monitoring_directory": "", + "data_object": { + "main": true, + "group": "Survey", + "label": "Object", + "meshType": [ + "{19730589-fd28-4649-9de0-ad47249d9aba}", + "{deebe11a-b57b-4a03-99d6-8f27b25eb2a8}" + ], + "value": "" + }, + "receivers_orientation": { + "group": "Survey", + "main": true, + "association": "Vertex", + "dataType": "Float", + "dataGroupType": [ + "Dip direction & dip", + "3D vector" + ], + "label": "Receivers orientation", + "tooltip": [ + "Receivers orientation provided as 'Dip direction & dip' or '3D vector' data group. If not provided, it assumes:
", + "
In-line component
Positive towards North (Y)
", + "
Cross-line component
Positive towards East (X)
", + "
Vertical component
Positive up (Z)
", + "
" + ], + "optional": true, + "enabled": false, + "parent": "data_object", + "value": "", + "visible": false + }, + "data_units": { + "choiceList": [ + "Airborne dB/dt (V/Am^4)", + "Airborne B (T/Am^2)", + "Ground dB/dt (V/Am^2)", + "Ground B (T/A)" + ], + "group": "Survey", + "main": true, + "label": "Data type", + "tooltip": [ + "Set the units of the data.

", + "Data are expected to be normalized by the source strength, dependent on the type:

", + "
Dipole
Current * area * number of turns (NIA)
", + "
Wire
Current * number of turns (NI)
", + "
" + ], + "value": "Airborne dB/dt (V/Am^4)" + }, + "inline_channel_bool": { + "group": "Survey", + "main": true, + "label": "A-component", + "tooltip": "Axial (A) component parallel to the hole trace.\nPositive upward along the hole trace", + "value": true + }, + "vertical_channel_bool": { + "group": "Survey", + "main": true, + "label": "U-component", + "tooltip": "Perpendicular (U) component of the magnetic data.\nPositive upward in the hole dip direction", + "value": true + }, + "crossline_channel_bool": { + "group": "Survey", + "main": true, + "label": "V-component", + "tooltip": "Perpendicular (V) component of the magnetic data.\nHorizontal, to left of hole dip direction", + "value": true + }, + "mesh": { + "group": "Mesh and models", + "main": true, + "label": "Mesh", + "meshType": "{4ea87376-3ece-438b-bf12-3479733ded46}", + "value": "", + "optional": true, + "enabled": false, + "tooltip": "Select a mesh for the forward modelling" + }, + "model_type": { + "choiceList": [ + "Conductivity (S/m)", + "Resistivity (Ohm-m)" + ], + "main": true, + "group": "Mesh and models", + "label": "Model units", + "tooltip": "Select the units of the model", + "value": "Conductivity (S/m)" + }, + "starting_model": { + "association": [ + "Cell", + "Vertex" + ], + "dataType": "Float", + "group": "Mesh and models", + "main": true, + "isValue": true, + "parent": "mesh", + "label": "Value(s)", + "property": "", + "value": 0.001 + }, + "topography_object": { + "main": true, + "group": "Topography", + "label": "Topography", + "meshType": [ + "{202c5db1-a56d-4004-9cad-baafd8899406}", + "{6a057fdc-b355-11e3-95be-fd84a7ffcb88}", + "{f26feba3-aded-494b-b9e9-b2bbcbe298e1}", + "{48f5054a-1c5c-4ca4-9048-80f36dc60a06}", + "{b020a277-90e2-4cd7-84d6-612ee3f25051}" + ], + "value": "", + "optional": true, + "enabled": true, + "tooltip": "Select a topography object to define the active cells for forward modelling" + }, + "topography": { + "association": [ + "Vertex", + "Cell" + ], + "dataType": "Float", + "group": "Topography", + "main": true, + "optional": true, + "enabled": false, + "label": "Elevation channel", + "tooltip": "Set elevation from channel. If not set the topography will be set from the geometry of the selected 'topography' object", + "parent": "topography_object", + "dependency": "topography_object", + "dependencyType": "enabled", + "value": "", + "verbose": 2 + }, + "active_model": { + "association": "Cell", + "dataType": [ + "Referenced", + "Boolean", + "Integer" + ], + "group": "Topography", + "main": true, + "enabled": false, + "dependency": "topography_object", + "dependencyType": "disabled", + "label": "Active model", + "tooltip": "Provide the active cell Boolean model directly if topography not set", + "parent": "mesh", + "value": "" + }, + "n_cpu": { + "min": 1, + "group": "Compute", + "optional": true, + "enabled": false, + "label": "Number of CPUs", + "value": 1, + "visible": false + }, + "solver_type": { + "choiceList": [ + "Pardiso", + "Mumps" + ], + "group": "Compute", + "label": "Direct solver", + "tooltip": "Direct solver to use for the forward calculations", + "value": "Pardiso" + }, + "tile_spatial": { + "group": "Compute", + "label": "Number of tiles", + "value": 1, + "min": 1, + "max": 1000, + "verbose": 2, + "tooltip": "Splits the objective function into spatial tiles for distributed computation using the Dask library" + }, + "max_chunk_size": { + "min": 0, + "group": "Compute", + "optional": true, + "enabled": true, + "label": "Maximum chunk size (MB)", + "value": 128, + "verbose": 3, + "visible": false, + "tooltip": "Limit the chunk size used by Dask for distributed computation" + }, + "out_group": { + "label": "SimPEG group", + "value": "", + "groupType": "{55ed3daf-c192-4d4b-a439-60fa987fe2b8}", + "group": "Drag-and-drop options", + "visible": true, + "optional": true, + "enabled": false, + "tooltip": "Optionally set the SimPEG group to which results will be saved" + }, + "generate_sweep": { + "label": "Generate sweep file", + "group": "Python run preferences", + "main": true, + "value": false, + "visible": false, + "tooltip": "Generates a file for sweeping parameters instead of running the application" + }, + "n_workers": null, + "n_threads": null, + "max_ram": "", + "performance_report": false, + "distributed_workers": "" +} diff --git a/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json b/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json new file mode 100644 index 000000000..ccaf8bfe5 --- /dev/null +++ b/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json @@ -0,0 +1,660 @@ +{ + "version": "0.0.0", + "title": "Borehole TDEM Forward", + "icon": "surveyairborneem", + "documentation": "https://mirageoscience-simpeg-drivers.readthedocs-hosted.com/en/latest/", + "conda_environment": "simpeg-drivers", + "run_command": "simpeg_drivers.electromagnetics.borehole_time_domain.inversion", + "geoh5": "", + "monitoring_directory": "", + "data_object": { + "main": true, + "group": "Data", + "label": "Object", + "meshType": [ + "{19730589-fd28-4649-9de0-ad47249d9aba}", + "{deebe11a-b57b-4a03-99d6-8f27b25eb2a8}" + ], + "value": "" + }, + "receivers_orientation": { + "group": "Survey", + "main": true, + "association": "Vertex", + "dataType": "Float", + "dataGroupType": [ + "Dip direction & dip", + "3D vector" + ], + "label": "Receivers orientation", + "tooltip": [ + "Receivers orientation provided as 'Dip direction & dip' or '3D vector' data group. If not provided, it assumes:
", + "
In-line component
Positive towards North (Y)
", + "
Cross-line component
Positive towards East (X)
", + "
Vertical component
Positive up (Z)
", + "
" + ], + "optional": true, + "enabled": false, + "parent": "data_object", + "value": "", + "visible": false + }, + "data_units": { + "choiceList": [ + "Airborne dB/dt (V/Am^4)", + "Airborne B (T/Am^2)", + "Ground dB/dt (V/Am^2)", + "Ground B (T/A)" + ], + "group": "Survey", + "main": true, + "label": "Data type", + "tooltip": [ + "Set the units of the data.

", + "Data are expected to be normalized by the source strength, dependent on the type:

", + "
Dipole
Current * area * number of turns (NIA)
", + "
Wire
Current * number of turns (NI)
", + "
" + ], + "value": "Airborne dB/dt (V/Am^4)" + }, + "inline_channel": { + "association": [ + "Cell", + "Vertex" + ], + "dataType": "Float", + "group": "Data", + "dataGroupType": "Multi-element", + "main": true, + "label": "A-component", + "parent": "data_object", + "tooltip": "Axial (A) component parallel to the hole trace.\nPositive upward along the hole trace", + "optional": true, + "enabled": false, + "value": "" + }, + "inline_uncertainty": { + "association": [ + "Cell", + "Vertex" + ], + "dataType": "Float", + "group": "Data", + "dataGroupType": "Multi-element", + "main": true, + "label": "Uncertainty", + "parent": "data_object", + "dependency": "inline_channel", + "dependencyType": "enabled", + "value": "" + }, + "vertical_channel": { + "association": [ + "Cell", + "Vertex" + ], + "dataType": "Float", + "group": "Data", + "dataGroupType": "Multi-element", + "main": true, + "label": "U-component", + "parent": "data_object", + "tooltip": "Perpendicular (U) component of the magnetic data.\nPositive upward in the hole dip direction", + "optional": true, + "enabled": true, + "value": "" + }, + "vertical_uncertainty": { + "association": [ + "Cell", + "Vertex" + ], + "dataType": "Float", + "group": "Data", + "dataGroupType": "Multi-element", + "main": true, + "label": "Uncertainty", + "parent": "data_object", + "dependency": "vertical_channel", + "dependencyType": "enabled", + "value": "" + }, + "crossline_channel": { + "association": [ + "Cell", + "Vertex" + ], + "dataType": "Float", + "group": "Data", + "dataGroupType": "Multi-element", + "main": true, + "label": "V-component", + "tooltip": "Perpendicular (V) component of the magnetic data.\nHorizontal, to left of hole dip direction", + "parent": "data_object", + "optional": true, + "enabled": false, + "value": "" + }, + "crossline_uncertainty": { + "association": [ + "Cell", + "Vertex" + ], + "dataType": "Float", + "group": "Data", + "dataGroupType": "Multi-element", + "main": true, + "label": "Uncertainty", + "parent": "data_object", + "dependency": "crossline_channel", + "dependencyType": "enabled", + "value": "" + }, + "mesh": { + "group": "Mesh and models", + "main": true, + "label": "Mesh", + "meshType": "{4ea87376-3ece-438b-bf12-3479733ded46}", + "value": "", + "optional": true, + "enabled": false, + "tooltip": "Select a mesh for the inversion" + }, + "model_type": { + "choiceList": [ + "Conductivity (S/m)", + "Resistivity (Ohm-m)" + ], + "main": true, + "group": "Mesh and models", + "label": "Model units", + "tooltip": "Select the units of the model", + "value": "Conductivity (S/m)" + }, + "starting_model": { + "association": [ + "Cell", + "Vertex" + ], + "dataType": "Float", + "group": "Mesh and models", + "main": true, + "isValue": true, + "parent": "mesh", + "label": "Initial", + "property": "", + "value": 0.001 + }, + "reference_model": { + "association": [ + "Cell", + "Vertex" + ], + "dataType": "Float", + "main": true, + "group": "Mesh and models", + "isValue": true, + "parent": "mesh", + "label": "Reference", + "property": "", + "optional": true, + "enabled": false, + "value": 0.001 + }, + "lower_bound": { + "association": "Cell", + "main": true, + "dataType": "Float", + "group": "Mesh and models", + "isValue": true, + "parent": "mesh", + "label": "Lower bound", + "property": "", + "optional": true, + "value": 1e-08, + "enabled": false + }, + "upper_bound": { + "association": "Cell", + "main": true, + "dataType": "Float", + "group": "Mesh and models", + "isValue": true, + "parent": "mesh", + "label": "Upper bound", + "property": "", + "optional": true, + "value": 100.0, + "enabled": false + }, + "topography_object": { + "main": true, + "group": "Topography", + "label": "Topography", + "meshType": [ + "{202c5db1-a56d-4004-9cad-baafd8899406}", + "{6a057fdc-b355-11e3-95be-fd84a7ffcb88}", + "{f26feba3-aded-494b-b9e9-b2bbcbe298e1}", + "{48f5054a-1c5c-4ca4-9048-80f36dc60a06}", + "{b020a277-90e2-4cd7-84d6-612ee3f25051}" + ], + "value": "", + "optional": true, + "enabled": true, + "tooltip": "Select a topography object to define the active cells for inversion" + }, + "topography": { + "association": [ + "Vertex", + "Cell" + ], + "dataType": "Float", + "group": "Topography", + "main": true, + "optional": true, + "enabled": false, + "label": "Elevation channel", + "tooltip": "Set elevation from channel. If not set the topography will be set from the geometry of the selected 'topography' object", + "parent": "topography_object", + "dependency": "topography_object", + "dependencyType": "enabled", + "value": "", + "verbose": 2 + }, + "active_model": { + "association": "Cell", + "dataType": [ + "Referenced", + "Boolean", + "Integer" + ], + "group": "Topography", + "main": true, + "enabled": false, + "dependency": "topography_object", + "dependencyType": "disabled", + "label": "Active model", + "tooltip": "Provide the active cell Boolean model directly if topography not set", + "parent": "mesh", + "value": "" + }, + "alpha_s": { + "min": 0.0, + "group": "Regularization", + "label": "Reference weight", + "value": 1.0, + "tooltip": "Constant ratio compared to other weights. Larger values result in models that remain close to the reference model", + "dependency": "reference_model", + "dependencyType": "enabled", + "isValue": true, + "parent": "mesh", + "association": "Cell", + "dataType": "Float", + "property": "", + "enabled": true + }, + "length_scale_x": { + "min": 0.0, + "group": "Regularization", + "label": "X-smoothness weight", + "tooltip": "Larger values relative to other smoothness weights will result in x biased smoothness", + "value": 1.0, + "isValue": true, + "parent": "mesh", + "association": "Cell", + "dataType": "Float", + "property": "", + "enabled": true + }, + "length_scale_y": { + "min": 0.0, + "group": "Regularization", + "label": "Y-smoothness weight", + "tooltip": "Larger values relative to other smoothness weights will result in y biased smoothness", + "value": 1.0, + "isValue": true, + "parent": "mesh", + "association": "Cell", + "dataType": "Float", + "property": "", + "enabled": true + }, + "length_scale_z": { + "min": 0.0, + "group": "Regularization", + "label": "Z-smoothness weight", + "tooltip": "Larger values relative to other smoothness weights will result in z biased smoothness", + "value": 1.0, + "isValue": true, + "parent": "mesh", + "association": "Cell", + "dataType": "Float", + "property": "", + "enabled": true + }, + "gradient_rotation": { + "group": "Regularization", + "association": "Cell", + "dataType": "Float", + "dataGroupType": [ + "Strike & dip", + "Dip direction & dip", + "3D vector" + ], + "label": "Gradient rotation", + "parent": "mesh", + "value": "", + "optional": true, + "tooltip": "Rotate the gradient of the regularization to align with a provided structural orientation.\nThe data should be provided as a data group in the form of either strike & dip, dip direction & dip, or x/y/z components.", + "enabled": false + }, + "s_norm": { + "association": "Cell", + "dataType": "Float", + "group": "Sparse/blocky model", + "label": "Smallness norm", + "isValue": true, + "parent": "mesh", + "property": "", + "value": 0.0, + "min": 0.0, + "max": 2.0, + "precision": 2, + "lineEdit": true, + "enabled": true, + "dependency": "reference_model", + "dependencyType": "enabled", + "tooltip": "Lp-norm used in the smallness term of the objective function" + }, + "x_norm": { + "association": "Cell", + "dataType": "Float", + "group": "Sparse/blocky model", + "label": "X-smoothness norm", + "isValue": true, + "parent": "mesh", + "property": "", + "value": 2.0, + "min": 0.0, + "max": 2.0, + "precision": 2, + "lineEdit": false, + "enabled": true, + "tooltip": "Lp-norm used in the x-smoothness term of the objective function" + }, + "y_norm": { + "association": "Cell", + "dataType": "Float", + "group": "Sparse/blocky model", + "label": "Y-smoothness norm", + "isValue": true, + "parent": "mesh", + "property": "", + "value": 2.0, + "min": 0.0, + "max": 2.0, + "precision": 2, + "lineEdit": false, + "enabled": true, + "tooltip": "Lp-norm used in the y-smoothness term of the objective function" + }, + "z_norm": { + "association": "Cell", + "dataType": "Float", + "group": "Sparse/blocky model", + "label": "Z-smoothness norm", + "isValue": true, + "parent": "mesh", + "property": "", + "value": 2.0, + "min": 0.0, + "max": 2.0, + "precision": 2, + "lineEdit": false, + "enabled": true, + "tooltip": "Lp-norm used in the z-smoothness term of the objective function" + }, + "max_irls_iterations": { + "min": 0, + "group": "Sparse/blocky model", + "label": "Maximum IRLS iterations", + "tooltip": "Iterative Re-Weighted Least-squares (IRLS) iterations for non-L2 problems", + "value": 25, + "enabled": true, + "verbose": 2 + }, + "starting_chi_factor": { + "group": "Sparse/blocky model", + "label": "IRLS start chi factor", + "enabled": true, + "value": 1.0, + "tooltip": "This chi factor will be used to determine the misfit threshold after which IRLS iterations begin", + "verbose": 3 + }, + "beta_tol": { + "group": "Update IRLS directive", + "label": "Beta tolerance", + "value": 0.5, + "min": 0.0001, + "verbose": 3, + "visible": false + }, + "percentile": { + "group": "Update IRLS directive", + "label": "Percentile", + "value": 95, + "max": 100, + "min": 5, + "verbose": 3, + "visible": false + }, + "chi_factor": { + "min": 0.1, + "max": 20.0, + "precision": 1, + "lineEdit": false, + "group": "Cooling schedule/target", + "label": "Chi factor", + "value": 1.0, + "enabled": true, + "tooltip": "The global target data misfit value" + }, + "auto_scale_tiles": { + "group": "Cooling schedule/target", + "label": "Auto-scale tiles", + "value": false, + "verbose": 3, + "visible": true, + "tooltip": "Whether to auto-scale the misfit function of tiles based on chi-factor" + }, + "initial_beta_ratio": { + "min": 0.0, + "precision": 2, + "group": "Cooling schedule/target", + "optional": true, + "enabled": true, + "label": "Initial beta ratio", + "value": 100.0, + "verbose": 2, + "tooltip": "Estimate the trade-off parameter by scaling the ratio between the largest derivatives in the objective function gradients" + }, + "initial_beta": { + "min": 0.0, + "group": "Cooling schedule/target", + "optional": true, + "enabled": false, + "dependency": "initial_beta_ratio", + "dependencyType": "disabled", + "label": "Initial beta", + "value": 1.0, + "verbose": 2, + "tooltip": "Trade-off parameter between data misfit and regularization" + }, + "cooling_factor": { + "group": "Cooling schedule/target", + "label": "Beta cooling factor", + "tooltip": "Each beta cooling step will be calculated by dividing the current beta by this factor", + "value": 2.0, + "min": 1.1, + "max": 100, + "precision": 1, + "lineEdit": false, + "verbose": 2 + }, + "cooling_rate": { + "group": "Optimization", + "label": "Iterations per beta", + "value": 2, + "min": 1, + "LineEdit": false, + "max": 10, + "precision": 1, + "verbose": 2, + "enabled": true, + "tooltip": "Set the number of iterations per beta value. Use higher values for more non-linear optimization problems" + }, + "epsilon_cooling_factor": 1.2, + "max_global_iterations": { + "min": 1, + "lineEdit": false, + "group": "Optimization", + "label": "Maximum iterations", + "tooltip": "Number of L2 and IRLS iterations combined", + "value": 50, + "enabled": true + }, + "max_line_search_iterations": { + "group": "Optimization", + "label": "Maximum number of line searches", + "value": 20, + "min": 1, + "enabled": true, + "verbose": 3, + "tooltip": "Perform an Armijo backtracking line search for the provided number of iterations" + }, + "max_cg_iterations": { + "min": 0, + "group": "Optimization", + "label": "Maximum CG iterations", + "value": 30, + "enabled": true, + "tooltip": "Maximum number of iterations for the conjugate gradient solver used in the optimization", + "verbose": 2 + }, + "tol_cg": { + "min": 0, + "group": "Optimization", + "label": "Conjugate gradient tolerance", + "value": 0.0001, + "enabled": true, + "tooltip": "Tolerance for the conjugate gradient solver used in the optimization", + "verbose": 3 + }, + "f_min_change": { + "group": "Optimization", + "label": "Minimum change in objective function", + "value": 0.01, + "min": 1e-06, + "verbose": 3, + "enabled": true, + "tooltip": "Minimum decrease in regularization beyond which the IRLS procedure is deemed to have completed" + }, + "sens_wts_threshold": { + "group": "Update sensitivity weights directive", + "tooltip": "Threshold below which sensitivity weights become constant. Measured as a percentage of the maximum sensitivity weight.", + "label": "Threshold (%)", + "value": 1.0, + "max": 100.0, + "min": 0.0, + "precision": 3, + "enabled": true, + "verbose": 2 + }, + "every_iteration_bool": { + "group": "Update sensitivity weights directive", + "tooltip": "Update weights at every iteration", + "label": "Every iteration", + "value": true, + "verbose": 2, + "enabled": true + }, + "save_sensitivities": { + "group": "Update sensitivity weights directive", + "label": "Save sensitivities", + "tooltip": "Save the summed square row sensitivities to geoh5", + "value": false + }, + "n_cpu": { + "min": 1, + "group": "Compute", + "optional": true, + "enabled": false, + "label": "Number of CPUs", + "value": 1, + "visible": false + }, + "solver_type": { + "choiceList": [ + "Pardiso", + "Mumps" + ], + "group": "Compute", + "label": "Direct solver", + "tooltip": "Direct solver to use for the forward calculations", + "value": "Pardiso" + }, + "tile_spatial": { + "group": "Compute", + "label": "Number of tiles", + "value": 1, + "min": 1, + "max": 1000, + "verbose": 2, + "tooltip": "Splits the objective function into spatial tiles for distributed computation using the Dask library" + }, + "store_sensitivities": { + "choiceList": [ + "ram" + ], + "group": "Compute", + "label": "Storage device", + "tooltip": "Only RAM storage available for now", + "value": "ram" + }, + "max_chunk_size": { + "min": 0, + "group": "Compute", + "optional": true, + "enabled": true, + "label": "Maximum chunk size (MB)", + "value": 128, + "verbose": 3, + "visible": false, + "tooltip": "Limit the chunk size used by Dask for distributed computation" + }, + "out_group": { + "label": "SimPEG group", + "value": "", + "groupType": "{55ed3daf-c192-4d4b-a439-60fa987fe2b8}", + "group": "Drag-and-drop options", + "visible": true, + "optional": true, + "enabled": false, + "tooltip": "Optionally set the SimPEG group to which results will be saved" + }, + "generate_sweep": { + "label": "Generate sweep file", + "group": "Python run preferences", + "main": true, + "value": false, + "visible": false, + "tooltip": "Generates a file for sweeping parameters instead of running the application" + }, + "n_workers": null, + "n_threads": null, + "max_ram": "", + "performance_report": false, + "distributed_workers": "" +} diff --git a/simpeg_drivers/__init__.py b/simpeg_drivers/__init__.py index 10d2b354d..c8ef0ff1d 100644 --- a/simpeg_drivers/__init__.py +++ b/simpeg_drivers/__init__.py @@ -54,6 +54,13 @@ def assets_path() -> Path: "simpeg_drivers.natural_sources.apparent_conductivity", {"forward": "AppConForwardDriver", "inversion": "AppConInversionDriver"}, ), + "borehole tdem": ( + "simpeg_drivers.electromagnetics.borehole_time_domain", + { + "forward": "BoreholeTDEMForwardDriver", + "inversion": "BoreholeTDEMInversionDriver", + }, + ), "direct current 3d": ( "simpeg_drivers.electricals.direct_current.three_dimensions", { diff --git a/simpeg_drivers/components/data.py b/simpeg_drivers/components/data.py index ebcd1a957..eed8089bb 100644 --- a/simpeg_drivers/components/data.py +++ b/simpeg_drivers/components/data.py @@ -188,6 +188,7 @@ def save_data(self): """Write out the data to geoh5""" has_channels = self.params.inversion_type in [ "apparent conductivity", + "borehole tdem", "magnetotellurics", "tipper", "tdem", diff --git a/simpeg_drivers/components/factories/directives_factory.py b/simpeg_drivers/components/factories/directives_factory.py index c7b9c5631..8502f14da 100644 --- a/simpeg_drivers/components/factories/directives_factory.py +++ b/simpeg_drivers/components/factories/directives_factory.py @@ -262,6 +262,7 @@ def save_iteration_residual_directive(self): not in [ "apparent conductivity", "tdem", + "borehole tdem", "tdem 1d", "fdem", "fdem 1d", @@ -440,6 +441,7 @@ def assemble_keyword_arguments( "magnetotellurics", "tipper", "tdem", + "borehole tdem", "tdem 1d", "fdem", "fdem 1d", diff --git a/simpeg_drivers/components/factories/receiver_factory.py b/simpeg_drivers/components/factories/receiver_factory.py index aac55f10e..da3df0964 100644 --- a/simpeg_drivers/components/factories/receiver_factory.py +++ b/simpeg_drivers/components/factories/receiver_factory.py @@ -271,4 +271,7 @@ def validate_orientations(self): return orientations + # elif "borehole" in self.params.inversion_type: + # pass + return normals diff --git a/simpeg_drivers/components/factories/simpeg_factory.py b/simpeg_drivers/components/factories/simpeg_factory.py index 853aa76cc..c145ce916 100644 --- a/simpeg_drivers/components/factories/simpeg_factory.py +++ b/simpeg_drivers/components/factories/simpeg_factory.py @@ -46,6 +46,7 @@ class SimPEGFactory(ABC): valid_factory_types = [ "apparent conductivity", + "borehole tdem", "direct current pseudo 3d", "direct current 3d", "direct current 2d", diff --git a/simpeg_drivers/components/factories/simulation_factory.py b/simpeg_drivers/components/factories/simulation_factory.py index fcc59606e..c9f37d67b 100644 --- a/simpeg_drivers/components/factories/simulation_factory.py +++ b/simpeg_drivers/components/factories/simulation_factory.py @@ -41,6 +41,7 @@ def __init__(self, params: BaseParams | BaseOptions): self.solver = None if self.factory_type in [ "apparent conductivity", + "borehole tdem", "direct current pseudo 3d", "direct current 3d", "direct current 2d", @@ -185,6 +186,7 @@ def assemble_keyword_arguments(self, survey=None, mesh=None, models=None, **kwar if self.factory_type in [ "apparent conductivity", + "borehole tdem", "direct current 3d", "direct current 2d", "magnetotellurics", diff --git a/simpeg_drivers/electromagnetics/borehole_time_domain/__init__.py b/simpeg_drivers/electromagnetics/borehole_time_domain/__init__.py new file mode 100644 index 000000000..df32b2043 --- /dev/null +++ b/simpeg_drivers/electromagnetics/borehole_time_domain/__init__.py @@ -0,0 +1,9 @@ +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +# Copyright (c) 2023-2026 Mira Geoscience Ltd. ' +# ' +# This file is part of simpeg-drivers package. ' +# ' +# simpeg-drivers is distributed under the terms and conditions of the MIT License ' +# (see LICENSE file at the root of this source code package). ' +# ' +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' diff --git a/simpeg_drivers/electromagnetics/borehole_time_domain/forward.py b/simpeg_drivers/electromagnetics/borehole_time_domain/forward.py new file mode 100644 index 000000000..2a4a8ff56 --- /dev/null +++ b/simpeg_drivers/electromagnetics/borehole_time_domain/forward.py @@ -0,0 +1,27 @@ +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +# Copyright (c) 2023-2026 Mira Geoscience Ltd. ' +# ' +# This file is part of simpeg-drivers package. ' +# ' +# simpeg-drivers is distributed under the terms and conditions of the MIT License ' +# (see LICENSE file at the root of this source code package). ' +# ' +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' + + +from __future__ import annotations + +from simpeg_drivers.driver import ForwardDriver +from simpeg_drivers.electromagnetics.time_domain.options import TDEMForwardOptions +from simpeg_drivers.utils.utils import argument_parser + + +class BoreholeTDEMForwardDriver(ForwardDriver): + """Time Domain Electromagnetic forward driver.""" + + _params_class = TDEMForwardOptions + + +if __name__ == "__main__": + file, args = argument_parser() + BoreholeTDEMForwardDriver.start_dask_run(file, **args) diff --git a/simpeg_drivers/electromagnetics/borehole_time_domain/inversion.py b/simpeg_drivers/electromagnetics/borehole_time_domain/inversion.py new file mode 100644 index 000000000..031c471c2 --- /dev/null +++ b/simpeg_drivers/electromagnetics/borehole_time_domain/inversion.py @@ -0,0 +1,29 @@ +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +# Copyright (c) 2023-2026 Mira Geoscience Ltd. ' +# ' +# This file is part of simpeg-drivers package. ' +# ' +# simpeg-drivers is distributed under the terms and conditions of the MIT License ' +# (see LICENSE file at the root of this source code package). ' +# ' +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' + + +from __future__ import annotations + +from simpeg_drivers.driver import InversionDriver +from simpeg_drivers.electromagnetics.borehole_time_domain.options import ( + BoreholeTDEMInversionOptions, +) +from simpeg_drivers.utils.utils import argument_parser + + +class BoreholeTDEMInversionDriver(InversionDriver): + """Time Domain Electromagnetic inversion driver.""" + + _params_class = BoreholeTDEMInversionOptions + + +if __name__ == "__main__": + file, args = argument_parser() + BoreholeTDEMInversionDriver.start_dask_run(file, **args) diff --git a/simpeg_drivers/electromagnetics/borehole_time_domain/options.py b/simpeg_drivers/electromagnetics/borehole_time_domain/options.py new file mode 100644 index 000000000..cde98f7f6 --- /dev/null +++ b/simpeg_drivers/electromagnetics/borehole_time_domain/options.py @@ -0,0 +1,64 @@ +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +# Copyright (c) 2023-2026 Mira Geoscience Ltd. ' +# ' +# This file is part of simpeg-drivers package. ' +# ' +# simpeg-drivers is distributed under the terms and conditions of the MIT License ' +# (see LICENSE file at the root of this source code package). ' +# ' +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' + + +from __future__ import annotations + +from pathlib import Path +from typing import ClassVar + +from simpeg_drivers import assets_path +from simpeg_drivers.electromagnetics.time_domain.options import ( + TDEMForwardOptions, + TDEMInversionOptions, +) + + +class BoreholeTDEMForwardOptions(TDEMForwardOptions): + """ + Time Domain Electromagnetic forward options for borehole surveys. + + :param vertical_channel_bool: Vertical (U) data channel boolean. + :param inline_channel_bool: In-line (A) data channel boolean. + :param crossline_channel_bool: Cross-line (V) data channel boolean. + """ + + name: ClassVar[str] = "Borehole TDEM Forward" + default_ui_json: ClassVar[Path] = ( + assets_path() / "uijson/borehole_tdem_forward.ui.json" + ) + run_command: str = "simpeg_drivers.electromagnetics.borehole_time_domain.forward" + + title: str = "Borehole TDEM Forward" + icon: str = "surveyairborneem" + inversion_type: str = "borehole tdem" + + +class BoreholeTDEMInversionOptions(TDEMInversionOptions): + """ + Time Domain Electromagnetic Inversion options for borehole surveys. + + :param vertical_channel: Vertical (U) component data channel. + :param vertical_uncertainty: Vertical (U) component data channel uncertainty. + :param inline_channel: In-line (A) data channel. + :param inline_uncertainty: In-line (A) data channel uncertainty. + :param crossline_channel: Cross-line (V) data channel. + :param crossline_uncertainty: Cross-line(V) data channel uncertainty. + """ + + name: ClassVar[str] = "Borehole TDEM Inversion" + default_ui_json: ClassVar[Path] = ( + assets_path() / "uijson/borehole_tdem_inversion.ui.json" + ) + run_command: str = "simpeg_drivers.electromagnetics.borehole_time_domain.inversion" + title: str = "Borehole TDEM Inversion" + icon: str = "surveyairborneem" + physical_property: str = "conductivity" + inversion_type: str = "borehole tdem" From af0d7b8b520b7e0b1aa468c78a6d658ddab2e835 Mon Sep 17 00:00:00 2001 From: domfournier Date: Thu, 20 Aug 2026 10:24:39 -0700 Subject: [PATCH 02/16] Improve logic for orientation on boerhole --- .../uijson/borehole_tdem_forward.ui.json | 3 +-- .../uijson/borehole_tdem_inversion.ui.json | 3 +-- .../components/factories/receiver_factory.py | 21 ++++++++----------- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/simpeg_drivers-assets/uijson/borehole_tdem_forward.ui.json b/simpeg_drivers-assets/uijson/borehole_tdem_forward.ui.json index cfc7fd72b..508b6d6ba 100644 --- a/simpeg_drivers-assets/uijson/borehole_tdem_forward.ui.json +++ b/simpeg_drivers-assets/uijson/borehole_tdem_forward.ui.json @@ -37,8 +37,7 @@ "optional": true, "enabled": false, "parent": "data_object", - "value": "", - "visible": false + "value": "" }, "data_units": { "choiceList": [ diff --git a/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json b/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json index ccaf8bfe5..189277bb8 100644 --- a/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json +++ b/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json @@ -37,8 +37,7 @@ "optional": true, "enabled": false, "parent": "data_object", - "value": "", - "visible": false + "value": "" }, "data_units": { "choiceList": [ diff --git a/simpeg_drivers/components/factories/receiver_factory.py b/simpeg_drivers/components/factories/receiver_factory.py index da3df0964..e0b18900e 100644 --- a/simpeg_drivers/components/factories/receiver_factory.py +++ b/simpeg_drivers/components/factories/receiver_factory.py @@ -14,15 +14,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING - - -if TYPE_CHECKING: - from geoapps_utils.driver.params import BaseParams - - from simpeg_drivers.options import BaseOptions - import numpy as np +from geoapps_utils.utils.locations import azimuth_dip_from_segments from geoapps_utils.utils.transformations import x_rotation_matrix, z_rotation_matrix from geoh5py.objects.surveys.electromagnetics.base import ( AirborneEMSurvey, @@ -30,6 +23,7 @@ ) from simpeg_drivers.components.factories.simpeg_factory import SimPEGFactory +from simpeg_drivers.options import CoreOptions from simpeg_drivers.utils.regularization import direction_and_dip, get_cell_normals @@ -45,7 +39,7 @@ class ReceiversFactory(SimPEGFactory): """Build SimPEG receivers objects based on factory type.""" - def __init__(self, params: BaseParams | BaseOptions): + def __init__(self, params: CoreOptions): """ :param params: Options object containing SimPEG object parameters. @@ -259,9 +253,15 @@ def validate_orientations(self): for comp in "xyz" } + azi_dip = None if getattr(self.params, "receivers_orientation", None): azm, dip = direction_and_dip(self.params.receivers_orientation) azi_dip = np.deg2rad(np.c_[azm.values, dip.values]) + + elif "borehole" in self.params.inversion_type: + azi_dip = azimuth_dip_from_segments(self.params.data_object) + + if azi_dip is not None: orientations = {} for axis in "xyz": orientations[axis] = ( @@ -271,7 +271,4 @@ def validate_orientations(self): return orientations - # elif "borehole" in self.params.inversion_type: - # pass - return normals From 5e2868df40a6532078e51cb67acfcb7bc9c506b9 Mon Sep 17 00:00:00 2001 From: domfournier Date: Thu, 20 Aug 2026 16:41:37 -0700 Subject: [PATCH 03/16] Switch to AUV channels --- .../uijson/borehole_tdem_forward.ui.json | 6 +- .../uijson/borehole_tdem_inversion.ui.json | 22 +++---- .../components/factories/receiver_factory.py | 27 ++++++-- .../factories/simulation_factory.py | 2 +- .../borehole_time_domain/forward.py | 11 ++-- .../borehole_time_domain/inversion.py | 5 +- .../borehole_time_domain/options.py | 65 +++++++++++++++---- 7 files changed, 99 insertions(+), 39 deletions(-) diff --git a/simpeg_drivers-assets/uijson/borehole_tdem_forward.ui.json b/simpeg_drivers-assets/uijson/borehole_tdem_forward.ui.json index 508b6d6ba..5e6ac4c73 100644 --- a/simpeg_drivers-assets/uijson/borehole_tdem_forward.ui.json +++ b/simpeg_drivers-assets/uijson/borehole_tdem_forward.ui.json @@ -58,21 +58,21 @@ ], "value": "Airborne dB/dt (V/Am^4)" }, - "inline_channel_bool": { + "A_channel_bool": { "group": "Survey", "main": true, "label": "A-component", "tooltip": "Axial (A) component parallel to the hole trace.\nPositive upward along the hole trace", "value": true }, - "vertical_channel_bool": { + "U_channel_bool": { "group": "Survey", "main": true, "label": "U-component", "tooltip": "Perpendicular (U) component of the magnetic data.\nPositive upward in the hole dip direction", "value": true }, - "crossline_channel_bool": { + "V_channel_bool": { "group": "Survey", "main": true, "label": "V-component", diff --git a/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json b/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json index 189277bb8..8ee1c8034 100644 --- a/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json +++ b/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json @@ -58,7 +58,7 @@ ], "value": "Airborne dB/dt (V/Am^4)" }, - "inline_channel": { + "A_channel": { "association": [ "Cell", "Vertex" @@ -71,10 +71,10 @@ "parent": "data_object", "tooltip": "Axial (A) component parallel to the hole trace.\nPositive upward along the hole trace", "optional": true, - "enabled": false, + "enabled": true, "value": "" }, - "inline_uncertainty": { + "A_uncertainty": { "association": [ "Cell", "Vertex" @@ -85,11 +85,11 @@ "main": true, "label": "Uncertainty", "parent": "data_object", - "dependency": "inline_channel", + "dependency": "A_channel", "dependencyType": "enabled", "value": "" }, - "vertical_channel": { + "U_channel": { "association": [ "Cell", "Vertex" @@ -102,10 +102,10 @@ "parent": "data_object", "tooltip": "Perpendicular (U) component of the magnetic data.\nPositive upward in the hole dip direction", "optional": true, - "enabled": true, + "enabled": false, "value": "" }, - "vertical_uncertainty": { + "U_uncertainty": { "association": [ "Cell", "Vertex" @@ -116,11 +116,11 @@ "main": true, "label": "Uncertainty", "parent": "data_object", - "dependency": "vertical_channel", + "dependency": "U_channel", "dependencyType": "enabled", "value": "" }, - "crossline_channel": { + "V_channel": { "association": [ "Cell", "Vertex" @@ -136,7 +136,7 @@ "enabled": false, "value": "" }, - "crossline_uncertainty": { + "V_uncertainty": { "association": [ "Cell", "Vertex" @@ -147,7 +147,7 @@ "main": true, "label": "Uncertainty", "parent": "data_object", - "dependency": "crossline_channel", + "dependency": "V_channel", "dependencyType": "enabled", "value": "" }, diff --git a/simpeg_drivers/components/factories/receiver_factory.py b/simpeg_drivers/components/factories/receiver_factory.py index e0b18900e..efb8028f1 100644 --- a/simpeg_drivers/components/factories/receiver_factory.py +++ b/simpeg_drivers/components/factories/receiver_factory.py @@ -16,7 +16,12 @@ import numpy as np from geoapps_utils.utils.locations import azimuth_dip_from_segments -from geoapps_utils.utils.transformations import x_rotation_matrix, z_rotation_matrix +from geoapps_utils.utils.transformations import ( + cartesian_to_azimuth_dip, + x_rotation_matrix, + z_rotation_matrix, +) +from geoh5py.groups.property_group_type import GroupTypeEnum from geoh5py.objects.surveys.electromagnetics.base import ( AirborneEMSurvey, LargeLoopGroundEMSurvey, @@ -33,6 +38,9 @@ "vertical": "z", "inline": "y", "crossline": "x", + "A": "y", + "U": "z", + "V": "x", } @@ -254,9 +262,20 @@ def validate_orientations(self): } azi_dip = None - if getattr(self.params, "receivers_orientation", None): - azm, dip = direction_and_dip(self.params.receivers_orientation) - azi_dip = np.deg2rad(np.c_[azm.values, dip.values]) + + if property_group := getattr(self.params, "receivers_orientation", None): + group_type = property_group.property_group_type + azi_dip = np.vstack( + [ + property_group.parent.get_data(k)[0].values + for k in property_group.properties + ] + ).T + + if group_type == GroupTypeEnum.VECTOR: + azi_dip = cartesian_to_azimuth_dip(azi_dip) + else: + azi_dip = np.deg2rad(azi_dip) elif "borehole" in self.params.inversion_type: azi_dip = azimuth_dip_from_segments(self.params.data_object) diff --git a/simpeg_drivers/components/factories/simulation_factory.py b/simpeg_drivers/components/factories/simulation_factory.py index c9f37d67b..ad9b20a30 100644 --- a/simpeg_drivers/components/factories/simulation_factory.py +++ b/simpeg_drivers/components/factories/simulation_factory.py @@ -111,7 +111,7 @@ def concrete_object(self): return simulation_1d.Simulation1DLayered - if self.factory_type in ["tdem"]: + if self.factory_type in ["tdem", "borehole tdem"]: from simpeg.electromagnetics.time_domain import simulation return simulation.Simulation3DMagneticFluxDensity diff --git a/simpeg_drivers/electromagnetics/borehole_time_domain/forward.py b/simpeg_drivers/electromagnetics/borehole_time_domain/forward.py index 2a4a8ff56..5893d84a3 100644 --- a/simpeg_drivers/electromagnetics/borehole_time_domain/forward.py +++ b/simpeg_drivers/electromagnetics/borehole_time_domain/forward.py @@ -12,16 +12,19 @@ from __future__ import annotations from simpeg_drivers.driver import ForwardDriver -from simpeg_drivers.electromagnetics.time_domain.options import TDEMForwardOptions +from simpeg_drivers.electromagnetics.borehole_time_domain.options import ( + BoreholeTDEMForwardOptions, +) from simpeg_drivers.utils.utils import argument_parser class BoreholeTDEMForwardDriver(ForwardDriver): """Time Domain Electromagnetic forward driver.""" - _params_class = TDEMForwardOptions + _params_class = BoreholeTDEMForwardOptions if __name__ == "__main__": - file, args = argument_parser() - BoreholeTDEMForwardDriver.start_dask_run(file, **args) + # file, args = argument_parser() + file = r"C:\Users\dominiquef\Documents\tests\Borehole_EM\fwr_model_A.ui.json" + BoreholeTDEMForwardDriver.start_dask_run(file) diff --git a/simpeg_drivers/electromagnetics/borehole_time_domain/inversion.py b/simpeg_drivers/electromagnetics/borehole_time_domain/inversion.py index 031c471c2..c3743165e 100644 --- a/simpeg_drivers/electromagnetics/borehole_time_domain/inversion.py +++ b/simpeg_drivers/electromagnetics/borehole_time_domain/inversion.py @@ -25,5 +25,6 @@ class BoreholeTDEMInversionDriver(InversionDriver): if __name__ == "__main__": - file, args = argument_parser() - BoreholeTDEMInversionDriver.start_dask_run(file, **args) + # file, args = argument_parser() + file = r"C:\Users\dominiquef\Documents\tests\Borehole_EM\inv_run1.ui.json" + BoreholeTDEMInversionDriver.start_dask_run(file) diff --git a/simpeg_drivers/electromagnetics/borehole_time_domain/options.py b/simpeg_drivers/electromagnetics/borehole_time_domain/options.py index cde98f7f6..4bcb4feba 100644 --- a/simpeg_drivers/electromagnetics/borehole_time_domain/options.py +++ b/simpeg_drivers/electromagnetics/borehole_time_domain/options.py @@ -14,20 +14,29 @@ from pathlib import Path from typing import ClassVar +from geoh5py.groups import PropertyGroup +from geoh5py.objects import ( + AirborneTEMReceivers, + LargeLoopGroundTEMReceivers, + MovingLoopGroundTEMReceivers, +) + from simpeg_drivers import assets_path -from simpeg_drivers.electromagnetics.time_domain.options import ( - TDEMForwardOptions, - TDEMInversionOptions, +from simpeg_drivers.electromagnetics.time_domain.options import BaseTDEMOptions +from simpeg_drivers.options import ( + BaseForwardOptions, + BaseInversionOptions, + ConductivityModelOptions, ) -class BoreholeTDEMForwardOptions(TDEMForwardOptions): +class BoreholeTDEMForwardOptions(BaseTDEMOptions, BaseForwardOptions): """ Time Domain Electromagnetic forward options for borehole surveys. - :param vertical_channel_bool: Vertical (U) data channel boolean. - :param inline_channel_bool: In-line (A) data channel boolean. - :param crossline_channel_bool: Cross-line (V) data channel boolean. + :param A_channel_bool: In-line (A) data channel boolean. + :param U_channel_bool: Vertical (U) data channel boolean. + :param V_channel_bool: Cross-line (V) data channel boolean. """ name: ClassVar[str] = "Borehole TDEM Forward" @@ -39,18 +48,31 @@ class BoreholeTDEMForwardOptions(TDEMForwardOptions): title: str = "Borehole TDEM Forward" icon: str = "surveyairborneem" inversion_type: str = "borehole tdem" + physical_property: str = "conductivity" + + data_object: ( + MovingLoopGroundTEMReceivers + | LargeLoopGroundTEMReceivers + | AirborneTEMReceivers + ) + receivers_orientation: PropertyGroup | None = None + A_channel_bool: bool = False + U_channel_bool: bool = False + V_channel_bool: bool = False + models: ConductivityModelOptions -class BoreholeTDEMInversionOptions(TDEMInversionOptions): + +class BoreholeTDEMInversionOptions(BaseTDEMOptions, BaseInversionOptions): """ Time Domain Electromagnetic Inversion options for borehole surveys. - :param vertical_channel: Vertical (U) component data channel. - :param vertical_uncertainty: Vertical (U) component data channel uncertainty. - :param inline_channel: In-line (A) data channel. - :param inline_uncertainty: In-line (A) data channel uncertainty. - :param crossline_channel: Cross-line (V) data channel. - :param crossline_uncertainty: Cross-line(V) data channel uncertainty. + :param U_channel: Vertical (U) component data channel. + :param U_uncertainty: Vertical (U) component data channel uncertainty. + :param A_channel: In-line (A) data channel. + :param A_uncertainty: In-line (A) data channel uncertainty. + :param V_channel: Cross-line (V) data channel. + :param V_uncertainty: Cross-line(V) data channel uncertainty. """ name: ClassVar[str] = "Borehole TDEM Inversion" @@ -62,3 +84,18 @@ class BoreholeTDEMInversionOptions(TDEMInversionOptions): icon: str = "surveyairborneem" physical_property: str = "conductivity" inversion_type: str = "borehole tdem" + + data_object: ( + MovingLoopGroundTEMReceivers + | LargeLoopGroundTEMReceivers + | AirborneTEMReceivers + ) + receivers_orientation: PropertyGroup | None = None + U_channel: PropertyGroup | None = None + U_uncertainty: PropertyGroup | None = None + A_channel: PropertyGroup | None = None + A_uncertainty: PropertyGroup | None = None + V_channel: PropertyGroup | None = None + V_uncertainty: PropertyGroup | None = None + + models: ConductivityModelOptions From 67dfd67fe328a45b478594adb023c17c6e01f2cf Mon Sep 17 00:00:00 2001 From: domfournier Date: Thu, 20 Aug 2026 16:49:31 -0700 Subject: [PATCH 04/16] Update deps-lock --- .../py-3.12-linux-64-dev.conda.lock.yml | 2 +- .../py-3.12-win-64-dev.conda.lock.yml | 6 ++-- environments/py-3.12-win-64.conda.lock.yml | 4 +-- .../py-3.13-linux-64-dev.conda.lock.yml | 2 +- .../py-3.13-win-64-dev.conda.lock.yml | 6 ++-- environments/py-3.13-win-64.conda.lock.yml | 4 +-- py-3.12.conda-lock.yml | 30 +++++++++---------- py-3.13.conda-lock.yml | 30 +++++++++---------- 8 files changed, 42 insertions(+), 42 deletions(-) diff --git a/environments/py-3.12-linux-64-dev.conda.lock.yml b/environments/py-3.12-linux-64-dev.conda.lock.yml index f59830762..1a1a76488 100644 --- a/environments/py-3.12-linux-64-dev.conda.lock.yml +++ b/environments/py-3.12-linux-64-dev.conda.lock.yml @@ -12,7 +12,7 @@ dependencies: - annotated-types=0.8.0=pyhd8ed1ab_0 - anyio=4.14.2=pyhcf101f3_0 - argon2-cffi=25.1.0=pyhd8ed1ab_0 - - argon2-cffi-bindings=25.1.0=py312h4c3975b_2 + - argon2-cffi-bindings=26.1.0=py312h5cc1888_0 - arrow=1.4.0=pyhcf101f3_0 - asciitree=0.3.3=py_2 - astroid=4.0.4=py312h7900ff3_0 diff --git a/environments/py-3.12-win-64-dev.conda.lock.yml b/environments/py-3.12-win-64-dev.conda.lock.yml index 86c03fcfd..146f67e80 100644 --- a/environments/py-3.12-win-64-dev.conda.lock.yml +++ b/environments/py-3.12-win-64-dev.conda.lock.yml @@ -12,7 +12,7 @@ dependencies: - annotated-types=0.8.0=pyhd8ed1ab_0 - anyio=4.14.2=pyhcf101f3_0 - argon2-cffi=25.1.0=pyhd8ed1ab_0 - - argon2-cffi-bindings=25.1.0=py312he06e257_2 + - argon2-cffi-bindings=26.1.0=py312he06e257_0 - arrow=1.4.0=pyhcf101f3_0 - asciitree=0.3.3=py_2 - astroid=4.0.4=py312h2e8e312_0 @@ -146,8 +146,8 @@ dependencies: - libwebp-base=1.6.0=h4d5522a_1 - libwinpthread=12.0.0.r4.gg4f2fc60ca=h57928b3_10 - libxcb=1.17.0=h874e120_1 - - libxml2=2.15.3=h8ef44ab_0 - - libxml2-16=2.15.3=h3cfd58e_0 + - libxml2=2.15.3=h8ef44ab_1 + - libxml2-16=2.15.3=h3cfd58e_1 - libzlib=1.3.2=hfd05255_3 - llvm-openmp=22.1.8=h4fa8253_0 - locket=1.0.0=pyhd8ed1ab_0 diff --git a/environments/py-3.12-win-64.conda.lock.yml b/environments/py-3.12-win-64.conda.lock.yml index 8ce1ffa08..28b9cbcda 100644 --- a/environments/py-3.12-win-64.conda.lock.yml +++ b/environments/py-3.12-win-64.conda.lock.yml @@ -85,8 +85,8 @@ dependencies: - libwebp-base=1.6.0=h4d5522a_1 - libwinpthread=12.0.0.r4.gg4f2fc60ca=h57928b3_10 - libxcb=1.17.0=h874e120_1 - - libxml2=2.15.3=h8ef44ab_0 - - libxml2-16=2.15.3=h3cfd58e_0 + - libxml2=2.15.3=h8ef44ab_1 + - libxml2-16=2.15.3=h3cfd58e_1 - libzlib=1.3.2=hfd05255_3 - llvm-openmp=22.1.8=h4fa8253_0 - locket=1.0.0=pyhd8ed1ab_0 diff --git a/environments/py-3.13-linux-64-dev.conda.lock.yml b/environments/py-3.13-linux-64-dev.conda.lock.yml index be6c30a34..37fe0f41f 100644 --- a/environments/py-3.13-linux-64-dev.conda.lock.yml +++ b/environments/py-3.13-linux-64-dev.conda.lock.yml @@ -12,7 +12,7 @@ dependencies: - annotated-types=0.8.0=pyhd8ed1ab_0 - anyio=4.14.2=pyhcf101f3_0 - argon2-cffi=25.1.0=pyhd8ed1ab_0 - - argon2-cffi-bindings=25.1.0=py313h07c4f96_2 + - argon2-cffi-bindings=26.1.0=py313h995f894_0 - arrow=1.4.0=pyhcf101f3_0 - asciitree=0.3.3=py_2 - astroid=4.0.4=py313h78bf25f_0 diff --git a/environments/py-3.13-win-64-dev.conda.lock.yml b/environments/py-3.13-win-64-dev.conda.lock.yml index cee746e26..cb570c442 100644 --- a/environments/py-3.13-win-64-dev.conda.lock.yml +++ b/environments/py-3.13-win-64-dev.conda.lock.yml @@ -12,7 +12,7 @@ dependencies: - annotated-types=0.8.0=pyhd8ed1ab_0 - anyio=4.14.2=pyhcf101f3_0 - argon2-cffi=25.1.0=pyhd8ed1ab_0 - - argon2-cffi-bindings=25.1.0=py313h5ea7bf4_2 + - argon2-cffi-bindings=26.1.0=py313h5ea7bf4_0 - arrow=1.4.0=pyhcf101f3_0 - asciitree=0.3.3=py_2 - astroid=4.0.4=py313hfa70ccb_0 @@ -147,8 +147,8 @@ dependencies: - libwebp-base=1.6.0=h4d5522a_1 - libwinpthread=12.0.0.r4.gg4f2fc60ca=h57928b3_10 - libxcb=1.17.0=h874e120_1 - - libxml2=2.15.3=h8ef44ab_0 - - libxml2-16=2.15.3=h3cfd58e_0 + - libxml2=2.15.3=h8ef44ab_1 + - libxml2-16=2.15.3=h3cfd58e_1 - libzlib=1.3.2=hfd05255_3 - llvm-openmp=22.1.8=h4fa8253_0 - locket=1.0.0=pyhd8ed1ab_0 diff --git a/environments/py-3.13-win-64.conda.lock.yml b/environments/py-3.13-win-64.conda.lock.yml index 23a45ba42..95471aa82 100644 --- a/environments/py-3.13-win-64.conda.lock.yml +++ b/environments/py-3.13-win-64.conda.lock.yml @@ -86,8 +86,8 @@ dependencies: - libwebp-base=1.6.0=h4d5522a_1 - libwinpthread=12.0.0.r4.gg4f2fc60ca=h57928b3_10 - libxcb=1.17.0=h874e120_1 - - libxml2=2.15.3=h8ef44ab_0 - - libxml2-16=2.15.3=h3cfd58e_0 + - libxml2=2.15.3=h8ef44ab_1 + - libxml2-16=2.15.3=h3cfd58e_1 - libzlib=1.3.2=hfd05255_3 - llvm-openmp=22.1.8=h4fa8253_0 - locket=1.0.0=pyhd8ed1ab_0 diff --git a/py-3.12.conda-lock.yml b/py-3.12.conda-lock.yml index bf4002e6e..383888552 100644 --- a/py-3.12.conda-lock.yml +++ b/py-3.12.conda-lock.yml @@ -189,23 +189,23 @@ package: category: dev optional: true - name: argon2-cffi-bindings - version: 25.1.0 + version: 26.1.0 manager: conda platform: linux-64 dependencies: __glibc: '>=2.17,<3.0.a0' cffi: '>=1.0.1' - libgcc: '>=14' + libgcc: '>=15' python: '>=3.12,<3.13.0a0' python_abi: 3.12.* - url: https://repo.prefix.dev/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py312h4c3975b_2.conda + url: https://repo.prefix.dev/conda-forge/linux-64/argon2-cffi-bindings-26.1.0-py312h5cc1888_0.conda hash: - md5: 1567f06d717246abab170736af8bad1b - sha256: 7988c207b2b766dad5ebabf25a92b8d75cb8faed92f256fd7a4e0875c9ec6d58 + md5: 206a3c2e73a15a4956df784f165f4ed8 + sha256: 9b97ce25a43e1aa998286971da972a13f5803cccb12df45013edff46ed3dc2b3 category: dev optional: true - name: argon2-cffi-bindings - version: 25.1.0 + version: 26.1.0 manager: conda platform: win-64 dependencies: @@ -215,10 +215,10 @@ package: ucrt: '>=10.0.20348.0' vc: '>=14.3,<15' vc14_runtime: '>=14.44.35208' - url: https://repo.prefix.dev/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py312he06e257_2.conda + url: https://repo.prefix.dev/conda-forge/win-64/argon2-cffi-bindings-26.1.0-py312he06e257_0.conda hash: - md5: 5dc3781bbc4ddce0bf250a04c1a192c2 - sha256: 38c5e43d991b0c43713fa2ceba3063afa4ccad2dd4c8eb720143de54d461a338 + md5: 38764c8f1723841acac6c0bc2d766afc + sha256: ae4d0f74b48266849555b69b195ee96e5db7067359ffde700d5f35dc41d5f8f1 category: dev optional: true - name: arrow @@ -4385,10 +4385,10 @@ package: ucrt: '>=10.0.20348.0' vc: '>=14.3,<15' vc14_runtime: '>=14.44.35208' - url: https://repo.prefix.dev/conda-forge/win-64/libxml2-2.15.3-h8ef44ab_0.conda + url: https://repo.prefix.dev/conda-forge/win-64/libxml2-2.15.3-h8ef44ab_1.conda hash: - md5: 95591ca5671d2213f5b2d5aa7818420d - sha256: a4599c6bbbbdd7db570896e520c557eec8e66d94e839a59d17dc1f24a3d5f82b + md5: 45a5a1c709a69f14cf176220788d18a9 + sha256: a83e9adcf7c50f20289dc6890707c8e4e84151b4204b0f7344998138807458d1 category: main optional: false - name: libxml2-16 @@ -4420,10 +4420,10 @@ package: ucrt: '>=10.0.20348.0' vc: '>=14.3,<15' vc14_runtime: '>=14.44.35208' - url: https://repo.prefix.dev/conda-forge/win-64/libxml2-16-2.15.3-h3cfd58e_0.conda + url: https://repo.prefix.dev/conda-forge/win-64/libxml2-16-2.15.3-h3cfd58e_1.conda hash: - md5: 9e8dd0d90ed830107b2c36801035b7db - sha256: 3b61ee3caba702d2ff432fa3920835db963026e5c99c4e6fdca0c6114f59e7ce + md5: 6e7dadff3f3bde2d29d29a3ec34f2d58 + sha256: 8163de24a7ddb4ebf9587c3a45ba950caf3690b9646b76f007ca15e6e52b5881 category: main optional: false - name: libzlib diff --git a/py-3.13.conda-lock.yml b/py-3.13.conda-lock.yml index a3219438b..753ada955 100644 --- a/py-3.13.conda-lock.yml +++ b/py-3.13.conda-lock.yml @@ -189,23 +189,23 @@ package: category: dev optional: true - name: argon2-cffi-bindings - version: 25.1.0 + version: 26.1.0 manager: conda platform: linux-64 dependencies: __glibc: '>=2.17,<3.0.a0' cffi: '>=1.0.1' - libgcc: '>=14' + libgcc: '>=15' python: '>=3.13,<3.14.0a0' python_abi: 3.13.* - url: https://repo.prefix.dev/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py313h07c4f96_2.conda + url: https://repo.prefix.dev/conda-forge/linux-64/argon2-cffi-bindings-26.1.0-py313h995f894_0.conda hash: - md5: 27bbec9f2f3a15d32b60ec5734f5b41c - sha256: ad188ccc06a06c633dc124b09e9e06fb9df4c32ffc38acc96ecc86e506062090 + md5: db73f7be4911943040efc5ca79c9775e + sha256: e07a2c721f480ee8109b519ca2201603abcc3aed5a9673fb830adbc9b6768ab8 category: dev optional: true - name: argon2-cffi-bindings - version: 25.1.0 + version: 26.1.0 manager: conda platform: win-64 dependencies: @@ -215,10 +215,10 @@ package: ucrt: '>=10.0.20348.0' vc: '>=14.3,<15' vc14_runtime: '>=14.44.35208' - url: https://repo.prefix.dev/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py313h5ea7bf4_2.conda + url: https://repo.prefix.dev/conda-forge/win-64/argon2-cffi-bindings-26.1.0-py313h5ea7bf4_0.conda hash: - md5: eabb4b677b54874d7d6ab775fdaa3d27 - sha256: 3f8a1affdfeb2be5289d709e365fc6e386d734773895215cf8cbc5100fa6af9a + md5: 0e05f2b2dba8c28bc6be0ee952ce4fbc + sha256: 0532093a043aa452c90798cbff19dfc3ab4e331726239dad42ffce83538d94cc category: dev optional: true - name: arrow @@ -4384,10 +4384,10 @@ package: ucrt: '>=10.0.20348.0' vc: '>=14.3,<15' vc14_runtime: '>=14.44.35208' - url: https://repo.prefix.dev/conda-forge/win-64/libxml2-2.15.3-h8ef44ab_0.conda + url: https://repo.prefix.dev/conda-forge/win-64/libxml2-2.15.3-h8ef44ab_1.conda hash: - md5: 95591ca5671d2213f5b2d5aa7818420d - sha256: a4599c6bbbbdd7db570896e520c557eec8e66d94e839a59d17dc1f24a3d5f82b + md5: 45a5a1c709a69f14cf176220788d18a9 + sha256: a83e9adcf7c50f20289dc6890707c8e4e84151b4204b0f7344998138807458d1 category: main optional: false - name: libxml2-16 @@ -4419,10 +4419,10 @@ package: ucrt: '>=10.0.20348.0' vc: '>=14.3,<15' vc14_runtime: '>=14.44.35208' - url: https://repo.prefix.dev/conda-forge/win-64/libxml2-16-2.15.3-h3cfd58e_0.conda + url: https://repo.prefix.dev/conda-forge/win-64/libxml2-16-2.15.3-h3cfd58e_1.conda hash: - md5: 9e8dd0d90ed830107b2c36801035b7db - sha256: 3b61ee3caba702d2ff432fa3920835db963026e5c99c4e6fdca0c6114f59e7ce + md5: 6e7dadff3f3bde2d29d29a3ec34f2d58 + sha256: 8163de24a7ddb4ebf9587c3a45ba950caf3690b9646b76f007ca15e6e52b5881 category: main optional: false - name: libzlib From 408b7bd68048a959e6948c232a5b69d410ee75eb Mon Sep 17 00:00:00 2001 From: domfournier Date: Thu, 20 Aug 2026 16:57:05 -0700 Subject: [PATCH 05/16] Fix title of borehole inversion --- simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json b/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json index 8ee1c8034..83c4db3f6 100644 --- a/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json +++ b/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json @@ -1,6 +1,6 @@ { "version": "0.0.0", - "title": "Borehole TDEM Forward", + "title": "Borehole TDEM Inversion", "icon": "surveyairborneem", "documentation": "https://mirageoscience-simpeg-drivers.readthedocs-hosted.com/en/latest/", "conda_environment": "simpeg-drivers", From 0be23371a97f6a2d148fdc2c56b93db3d4c331d7 Mon Sep 17 00:00:00 2001 From: domfournier Date: Thu, 20 Aug 2026 17:06:01 -0700 Subject: [PATCH 06/16] Reset mains --- .../electromagnetics/borehole_time_domain/forward.py | 5 ++--- .../electromagnetics/borehole_time_domain/inversion.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/simpeg_drivers/electromagnetics/borehole_time_domain/forward.py b/simpeg_drivers/electromagnetics/borehole_time_domain/forward.py index 5893d84a3..d9c7e0eb7 100644 --- a/simpeg_drivers/electromagnetics/borehole_time_domain/forward.py +++ b/simpeg_drivers/electromagnetics/borehole_time_domain/forward.py @@ -25,6 +25,5 @@ class BoreholeTDEMForwardDriver(ForwardDriver): if __name__ == "__main__": - # file, args = argument_parser() - file = r"C:\Users\dominiquef\Documents\tests\Borehole_EM\fwr_model_A.ui.json" - BoreholeTDEMForwardDriver.start_dask_run(file) + file, args = argument_parser() + BoreholeTDEMForwardDriver.start_dask_run(file, **args) diff --git a/simpeg_drivers/electromagnetics/borehole_time_domain/inversion.py b/simpeg_drivers/electromagnetics/borehole_time_domain/inversion.py index c3743165e..031c471c2 100644 --- a/simpeg_drivers/electromagnetics/borehole_time_domain/inversion.py +++ b/simpeg_drivers/electromagnetics/borehole_time_domain/inversion.py @@ -25,6 +25,5 @@ class BoreholeTDEMInversionDriver(InversionDriver): if __name__ == "__main__": - # file, args = argument_parser() - file = r"C:\Users\dominiquef\Documents\tests\Borehole_EM\inv_run1.ui.json" - BoreholeTDEMInversionDriver.start_dask_run(file) + file, args = argument_parser() + BoreholeTDEMInversionDriver.start_dask_run(file, **args) From ddf85a90d75ac4dcbedc6528dea2a78c46c806f7 Mon Sep 17 00:00:00 2001 From: domfournier Date: Wed, 26 Aug 2026 12:49:20 -0700 Subject: [PATCH 07/16] Switch to lower case components a,u,v --- pyproject.toml | 8 +- .../uijson/borehole_tdem_forward.ui.json | 6 +- .../uijson/borehole_tdem_inversion.ui.json | 18 +- .../components/factories/receiver_factory.py | 6 +- .../borehole_time_domain/options.py | 36 +- tests/run_tests/driver_borehole_tem_test.py | 307 ++++++++++++++++++ 6 files changed, 344 insertions(+), 37 deletions(-) create mode 100644 tests/run_tests/driver_borehole_tem_test.py diff --git a/pyproject.toml b/pyproject.toml index 9cf2e7dd9..2ac06cadc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,16 +94,16 @@ trimesh = "^4.1.3" ## Pip dependencies from Git repositories #---------------------------------------- # geoh5py = {version = ">=0.14.0a, 0.14.*", source = "pypi", allow-prereleases = true} -geoh5py = {git = "https://github.com/MiraGeoscience/geoh5py.git", rev = "feature/uijson"} +geoh5py = {git = "https://github.com/MiraGeoscience/geoh5py.git", rev = "develop"} # grid-apps = {version = ">=0.3.0a, 0.3.*", source = "pypi", allow-prereleases = true} -grid-apps = {git = "https://github.com/MiraGeoscience/grid-apps.git", rev = "GEOPY-2965"} +grid-apps = {git = "https://github.com/MiraGeoscience/grid-apps.git", rev = "develop"} # geoapps-utils = {version = ">=0.8.0a, 0.8.*", source = "pypi", allow-prereleases = true} -geoapps-utils = {git = "https://github.com/MiraGeoscience/geoapps-utils.git", rev = "feature/uijson"} +geoapps-utils = {git = "https://github.com/MiraGeoscience/geoapps-utils.git", rev = "develop"} # mira-simpeg = {version = ">=0.25.0.2a, 0.25.0.*", source="pypi", allow-prereleases = true, extras = ["dask"]} -mira-simpeg = {git = "https://github.com/MiraGeoscience/simpeg.git", rev = "GEOPY-2880", extras = ["dask"]} +mira-simpeg = {git = "https://github.com/MiraGeoscience/simpeg.git", rev = "develop", extras = ["dask"]} ## about pip dependencies # to be specified to work with conda-lock diff --git a/simpeg_drivers-assets/uijson/borehole_tdem_forward.ui.json b/simpeg_drivers-assets/uijson/borehole_tdem_forward.ui.json index 5e6ac4c73..dacb53101 100644 --- a/simpeg_drivers-assets/uijson/borehole_tdem_forward.ui.json +++ b/simpeg_drivers-assets/uijson/borehole_tdem_forward.ui.json @@ -58,21 +58,21 @@ ], "value": "Airborne dB/dt (V/Am^4)" }, - "A_channel_bool": { + "a_channel_bool": { "group": "Survey", "main": true, "label": "A-component", "tooltip": "Axial (A) component parallel to the hole trace.\nPositive upward along the hole trace", "value": true }, - "U_channel_bool": { + "u_channel_bool": { "group": "Survey", "main": true, "label": "U-component", "tooltip": "Perpendicular (U) component of the magnetic data.\nPositive upward in the hole dip direction", "value": true }, - "V_channel_bool": { + "v_channel_bool": { "group": "Survey", "main": true, "label": "V-component", diff --git a/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json b/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json index 83c4db3f6..d211c2c48 100644 --- a/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json +++ b/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json @@ -58,7 +58,7 @@ ], "value": "Airborne dB/dt (V/Am^4)" }, - "A_channel": { + "a_channel": { "association": [ "Cell", "Vertex" @@ -74,7 +74,7 @@ "enabled": true, "value": "" }, - "A_uncertainty": { + "a_uncertainty": { "association": [ "Cell", "Vertex" @@ -85,11 +85,11 @@ "main": true, "label": "Uncertainty", "parent": "data_object", - "dependency": "A_channel", + "dependency": "a_channel", "dependencyType": "enabled", "value": "" }, - "U_channel": { + "u_channel": { "association": [ "Cell", "Vertex" @@ -105,7 +105,7 @@ "enabled": false, "value": "" }, - "U_uncertainty": { + "u_uncertainty": { "association": [ "Cell", "Vertex" @@ -116,11 +116,11 @@ "main": true, "label": "Uncertainty", "parent": "data_object", - "dependency": "U_channel", + "dependency": "u_channel", "dependencyType": "enabled", "value": "" }, - "V_channel": { + "v_channel": { "association": [ "Cell", "Vertex" @@ -136,7 +136,7 @@ "enabled": false, "value": "" }, - "V_uncertainty": { + "v_uncertainty": { "association": [ "Cell", "Vertex" @@ -147,7 +147,7 @@ "main": true, "label": "Uncertainty", "parent": "data_object", - "dependency": "V_channel", + "dependency": "v_channel", "dependencyType": "enabled", "value": "" }, diff --git a/simpeg_drivers/components/factories/receiver_factory.py b/simpeg_drivers/components/factories/receiver_factory.py index efb8028f1..1a0423081 100644 --- a/simpeg_drivers/components/factories/receiver_factory.py +++ b/simpeg_drivers/components/factories/receiver_factory.py @@ -38,9 +38,9 @@ "vertical": "z", "inline": "y", "crossline": "x", - "A": "y", - "U": "z", - "V": "x", + "a": "y", + "u": "z", + "v": "x", } diff --git a/simpeg_drivers/electromagnetics/borehole_time_domain/options.py b/simpeg_drivers/electromagnetics/borehole_time_domain/options.py index 4bcb4feba..7902009bf 100644 --- a/simpeg_drivers/electromagnetics/borehole_time_domain/options.py +++ b/simpeg_drivers/electromagnetics/borehole_time_domain/options.py @@ -34,9 +34,9 @@ class BoreholeTDEMForwardOptions(BaseTDEMOptions, BaseForwardOptions): """ Time Domain Electromagnetic forward options for borehole surveys. - :param A_channel_bool: In-line (A) data channel boolean. - :param U_channel_bool: Vertical (U) data channel boolean. - :param V_channel_bool: Cross-line (V) data channel boolean. + :param a_channel_bool: In-line (A) data channel boolean. + :param u_channel_bool: Vertical (U) data channel boolean. + :param v_channel_bool: Cross-line (V) data channel boolean. """ name: ClassVar[str] = "Borehole TDEM Forward" @@ -56,9 +56,9 @@ class BoreholeTDEMForwardOptions(BaseTDEMOptions, BaseForwardOptions): | AirborneTEMReceivers ) receivers_orientation: PropertyGroup | None = None - A_channel_bool: bool = False - U_channel_bool: bool = False - V_channel_bool: bool = False + a_channel_bool: bool = False + u_channel_bool: bool = False + v_channel_bool: bool = False models: ConductivityModelOptions @@ -67,12 +67,12 @@ class BoreholeTDEMInversionOptions(BaseTDEMOptions, BaseInversionOptions): """ Time Domain Electromagnetic Inversion options for borehole surveys. - :param U_channel: Vertical (U) component data channel. - :param U_uncertainty: Vertical (U) component data channel uncertainty. - :param A_channel: In-line (A) data channel. - :param A_uncertainty: In-line (A) data channel uncertainty. - :param V_channel: Cross-line (V) data channel. - :param V_uncertainty: Cross-line(V) data channel uncertainty. + :param u_channel: Vertical (U) component data channel. + :param u_uncertainty: Vertical (U) component data channel uncertainty. + :param a_channel: In-line (A) data channel. + :param a_uncertainty: In-line (A) data channel uncertainty. + :param v_channel: Cross-line (V) data channel. + :param v_uncertainty: Cross-line(V) data channel uncertainty. """ name: ClassVar[str] = "Borehole TDEM Inversion" @@ -91,11 +91,11 @@ class BoreholeTDEMInversionOptions(BaseTDEMOptions, BaseInversionOptions): | AirborneTEMReceivers ) receivers_orientation: PropertyGroup | None = None - U_channel: PropertyGroup | None = None - U_uncertainty: PropertyGroup | None = None - A_channel: PropertyGroup | None = None - A_uncertainty: PropertyGroup | None = None - V_channel: PropertyGroup | None = None - V_uncertainty: PropertyGroup | None = None + u_channel: PropertyGroup | None = None + u_uncertainty: PropertyGroup | None = None + a_channel: PropertyGroup | None = None + a_uncertainty: PropertyGroup | None = None + v_channel: PropertyGroup | None = None + v_uncertainty: PropertyGroup | None = None models: ConductivityModelOptions diff --git a/tests/run_tests/driver_borehole_tem_test.py b/tests/run_tests/driver_borehole_tem_test.py new file mode 100644 index 000000000..389cff256 --- /dev/null +++ b/tests/run_tests/driver_borehole_tem_test.py @@ -0,0 +1,307 @@ +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +# Copyright (c) 2023-2026 Mira Geoscience Ltd. ' +# ' +# This file is part of simpeg-drivers package. ' +# ' +# simpeg-drivers is distributed under the terms and conditions of the MIT License ' +# (see LICENSE file at the root of this source code package). ' +# ' +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' + +from __future__ import annotations + +import re +from logging import INFO, getLogger +from pathlib import Path + +import numpy as np +from geoapps_utils.modelling.plates import PlateModel +from geoh5py.workspace import Workspace +from pymatsolver.direct import Mumps + +from simpeg_drivers.electromagnetics.time_domain.forward import ( + TDEMForwardDriver, + TDEMForwardOptions, +) +from simpeg_drivers.electromagnetics.time_domain.inversion import ( + TDEMInversionDriver, + TDEMInversionOptions, +) +from simpeg_drivers.utils.synthetics.driver import ( + SyntheticsComponents, +) +from simpeg_drivers.utils.synthetics.options import ( + MeshOptions, + ModelOptions, + SurveyOptions, + SyntheticsComponentsOptions, +) +from tests.utils.targets import check_target, get_inversion_output, get_workspace + + +logger = getLogger(__name__) + + +# To test the full run and validate the inversion. +# Move this file out of the test directory and run. + +target_run = {"data_norm": 1.8460e-11, "phi_d": 3.0390e01, "phi_m": 2.0320e04} + + +def test_tiling_ground_tem( + tmp_path: Path, + *, + n_grid_points=4, + cell_size=(20.0, 20.0, 20.0), + refinement=(2,), + **_, +): + # Run the forward + opts = SyntheticsComponentsOptions( + method="ground tdem", + refine_plate=True, + survey=SurveyOptions( + n_stations=n_grid_points, + n_lines=n_grid_points, + drape=5.0, + topography=lambda x, y: np.zeros(x.shape), + name="ground_tdem_survey", + ), + mesh=MeshOptions( + u_cell_size=cell_size[0], + v_cell_size=cell_size[1], + w_cell_size=cell_size[2], + survey_refinement=list(refinement), + topography_refinement=[0, 0, 1], + plate_refinement=[1], + padding_distance=1000.0, + ), + model=ModelOptions( + background=0.001, + plate=PlateModel( + strike_length=40.0, + dip_length=40.0, + width=40.0, + easting=0.0, + northing=0.0, + elevation=-50.0, + ), + ), + ) + with get_workspace(tmp_path / "inversion_test.ui.geoh5") as geoh5: + components = SyntheticsComponents(geoh5, options=opts) + params = TDEMForwardOptions.build( + geoh5=geoh5, + mesh=components.mesh, + topography_object=components.topography, + data_object=components.survey, + starting_model=components.model, + x_channel_bool=True, + y_channel_bool=True, + z_channel_bool=True, + tile_spatial=4, + solver_type="Mumps", + ) + fwr_driver = TDEMForwardDriver(params) + + with geoh5.open(): + tiles = fwr_driver.get_tiles() + + assert len(tiles[None]) == 4 + + for tile in tiles[None]: + assert len(np.unique(components.survey.tx_id_property.values[tile])) == 1 + + fwr_driver.run() + + +def test_ground_tem_fwr_run( + tmp_path: Path, + caplog, + n_grid_points=4, + refinement=(2,), + cell_size=(20.0, 20.0, 20.0), + pytest=True, +): + if pytest and caplog: + caplog.set_level(INFO) + # Run the forward + opts = SyntheticsComponentsOptions( + method="ground tdem", + refine_plate=True, + survey=SurveyOptions( + n_stations=n_grid_points, + n_lines=n_grid_points, + drape=5.0, + topography=lambda x, y: np.zeros(x.shape), + ), + mesh=MeshOptions( + u_cell_size=cell_size[0], + v_cell_size=cell_size[1], + w_cell_size=cell_size[2], + survey_refinement=list(refinement), + topography_refinement=[0, 0, 1], + plate_refinement=[1], + padding_distance=1000.0, + ), + model=ModelOptions( + background=0.001, + plate=PlateModel( + strike_length=40.0, + dip_length=40.0, + width=40.0, + easting=0.0, + northing=0.0, + elevation=-50.0, + ), + ), + ) + with get_workspace(tmp_path / "inversion_test.ui.geoh5") as geoh5: + components = SyntheticsComponents(geoh5, options=opts) + components.survey.transmitters.remove_cells([15]) + params = TDEMForwardOptions.build( + geoh5=geoh5, + mesh=components.mesh, + topography_object=components.topography, + data_object=components.survey, + starting_model=components.model, + x_channel_bool=True, + y_channel_bool=True, + z_channel_bool=True, + data_units="Ground B (T/A)", + solver_type="Mumps", + ) + + fwr_driver = TDEMForwardDriver(params) + + assert fwr_driver.out_group is not None + with components.survey.workspace.open(): + components.survey.tx_id_property.name = "tx_id" + assert fwr_driver.inversion_data.survey.source_list[0].n_segments == 16 + + if pytest and caplog: + loop_warnings = [ + k + for k in caplog.records + if re.match(r"Loop \d+ modified", k.message) is not None + ] + assert len(loop_warnings) == 2 + for record in loop_warnings: + assert record.levelname == "INFO" + assert "counter-clockwise" in record.message + + assert "closed" in loop_warnings[0].message + + assert ( + fwr_driver.data_misfit.objfcts[0].simulation.simulations[0].solver == Mumps + ) + fwr_driver.run() + + +def test_ground_tem_run(tmp_path: Path, max_iterations=1, pytest=True): + workpath = tmp_path / "inversion_test.ui.geoh5" + if pytest: + workpath = ( + tmp_path.parent / "test_ground_tem_fwr_run0" / "inversion_test.ui.geoh5" + ) + + with Workspace(workpath) as geoh5: + components = SyntheticsComponents(geoh5) + data = {} + uncertainties = {} + channels = { + "vertical": "vertical", + } + + for chan, cname in channels.items(): + data[cname] = [] + uncertainties[f"{cname} uncertainties"] = [] + for ii, _ in enumerate(components.survey.channels): + data_entity = geoh5.get_entity(f"Iteration_0_{chan}_[{ii}]")[0].copy( + parent=components.survey + ) + data[cname].append(data_entity) + + uncert = components.survey.add_data( + { + f"uncertainty_{chan}_[{ii}]": { + "values": np.ones_like(data_entity.values) + * np.median(np.abs(data_entity.values)) + / 2.0 + } + } + ) + uncertainties[f"{cname} uncertainties"].append(uncert) + + components.survey.add_components_data(data) + components.survey.add_components_data(uncertainties) + + data_kwargs = {} + for chan in channels: + data_kwargs[f"{chan}_channel"] = components.survey.fetch_property_group( + name="vertical" + ) + data_kwargs[f"{chan}_uncertainty"] = components.survey.fetch_property_group( + name="vertical uncertainties" + ) + + orig_dBzdt = geoh5.get_entity("Iteration_0_vertical_[0]")[0].values + + # Run the inverse + params = TDEMInversionOptions.build( + geoh5=geoh5, + mesh=components.mesh, + topography_object=components.topography, + data_object=components.survey, + starting_model=1e-3, + reference_model=1e-3, + chi_factor=0.1, + s_norm=2.0, + x_norm=2.0, + y_norm=2.0, + z_norm=2.0, + alpha_s=0e-1, + lower_bound=2e-6, + upper_bound=1e2, + max_global_iterations=max_iterations, + initial_beta_ratio=1e1, + cooling_rate=2, + max_cg_iterations=200, + percentile=100, + solver_type="Mumps", + data_units="Ground B (T/A)", + **data_kwargs, + ) + params.write_ui_json(path=tmp_path / "Inv_run.ui.json") + + driver = TDEMInversionDriver(params) + driver.run() + + with geoh5.open() as run_ws: + output = get_inversion_output( + driver.params.geoh5.h5file, driver.params.out_group.uid + ) + assert driver.inversion_data.entity.tx_id_property.name == "tx_id" + output["data"] = orig_dBzdt + if pytest: + check_target(output, target_run) + nan_ind = np.isnan(run_ws.get_entity("Iteration_0_model")[0].values) + inactive_ind = run_ws.get_entity("active_cells")[0].values == 0 + assert np.all(nan_ind == inactive_ind) + + +if __name__ == "__main__": + # Full run + test_ground_tem_fwr_run( + Path("./"), + None, + n_grid_points=5, + refinement=(2, 2, 2), + cell_size=(5.0, 5.0, 5.0), + pytest=False, + ) + test_ground_tem_run( + Path("./"), + max_iterations=15, + pytest=False, + ) From d7200ace65c5de3696e2c93c1563a31578adb340 Mon Sep 17 00:00:00 2001 From: domfournier Date: Wed, 26 Aug 2026 12:49:48 -0700 Subject: [PATCH 08/16] Clean up some imports --- simpeg_drivers/components/data.py | 1 - simpeg_drivers/components/factories/source_factory.py | 6 ++---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/simpeg_drivers/components/data.py b/simpeg_drivers/components/data.py index 630994a9e..73b9c6f05 100644 --- a/simpeg_drivers/components/data.py +++ b/simpeg_drivers/components/data.py @@ -33,7 +33,6 @@ if TYPE_CHECKING: from geoh5py.workspace import Workspace - from simpeg_drivers.components.meshes import InversionMesh from simpeg_drivers.options import InversionBaseOptions diff --git a/simpeg_drivers/components/factories/source_factory.py b/simpeg_drivers/components/factories/source_factory.py index c9498c146..a67124cfc 100644 --- a/simpeg_drivers/components/factories/source_factory.py +++ b/simpeg_drivers/components/factories/source_factory.py @@ -28,15 +28,13 @@ if TYPE_CHECKING: - from geoapps_utils.driver.params import BaseParams - - from simpeg_drivers.options import BaseOptions + from simpeg_drivers.options import CoreOptions class SourcesFactory(SimPEGFactory): """Build SimPEG sources objects based on factory type.""" - def __init__(self, params: BaseParams | BaseOptions): + def __init__(self, params: CoreOptions): """ :param params: Options object containing SimPEG object parameters. From 7171a25a8719c60a1356ba4d627b120af004d12e Mon Sep 17 00:00:00 2001 From: domfournier Date: Wed, 26 Aug 2026 14:14:01 -0700 Subject: [PATCH 09/16] Fix changes to numpy cross --- simpeg_drivers/utils/surveys.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/simpeg_drivers/utils/surveys.py b/simpeg_drivers/utils/surveys.py index b3de3035b..8e7e6b4bd 100644 --- a/simpeg_drivers/utils/surveys.py +++ b/simpeg_drivers/utils/surveys.py @@ -57,10 +57,15 @@ def counter_clockwise_sort(segments: np.ndarray, vertices: np.ndarray) -> np.nda :return: Sorted segments. """ - center = np.mean(vertices[segments[:, 0], :2], axis=0) - center_to_vertices = vertices[segments[:, 0], :2] - center[:2] - deltas = vertices[segments[:, 1], :2] - vertices[segments[:, 0], :2] + center = np.mean(vertices[segments[:, 0], :], axis=0) + center_to_vertices = vertices[segments[:, 0], :] - center[:] + deltas = vertices[segments[:, 1], :] - vertices[segments[:, 0], :] + + # Turn into a flat 2D problem + center_to_vertices[:, -1] = 0 + deltas[:, -1] = 0 cross = np.cross(center_to_vertices, deltas) + cross = cross[:, -1] # Only keep the Z component if np.mean(np.sign(cross[cross != 0])) < 0: segments = segments[::-1, ::-1] From bedfe30ddc6833095f3daa3145700f1cdcae040c Mon Sep 17 00:00:00 2001 From: domfournier Date: Wed, 26 Aug 2026 14:16:22 -0700 Subject: [PATCH 10/16] Add dedicated synthetic borehole survey constructor --- .../components/factories/receiver_factory.py | 2 +- .../utils/synthetics/surveys/factory.py | 11 +++-- .../{airborne_tdem.py => airborne.py} | 0 .../surveys/time_domain/borehole.py | 44 +++++++++++++++++++ .../time_domain/{ground_tdem.py => ground.py} | 13 +++++- tests/plate_simulation/leroi_air/__init__.py | 2 +- 6 files changed, 65 insertions(+), 7 deletions(-) rename simpeg_drivers/utils/synthetics/surveys/time_domain/{airborne_tdem.py => airborne.py} (100%) create mode 100644 simpeg_drivers/utils/synthetics/surveys/time_domain/borehole.py rename simpeg_drivers/utils/synthetics/surveys/time_domain/{ground_tdem.py => ground.py} (90%) diff --git a/simpeg_drivers/components/factories/receiver_factory.py b/simpeg_drivers/components/factories/receiver_factory.py index 1a0423081..bee4c6de5 100644 --- a/simpeg_drivers/components/factories/receiver_factory.py +++ b/simpeg_drivers/components/factories/receiver_factory.py @@ -278,7 +278,7 @@ def validate_orientations(self): azi_dip = np.deg2rad(azi_dip) elif "borehole" in self.params.inversion_type: - azi_dip = azimuth_dip_from_segments(self.params.data_object) + azi_dip = azimuth_dip_from_segments(self.params.data_object, reverse=True) if azi_dip is not None: orientations = {} diff --git a/simpeg_drivers/utils/synthetics/surveys/factory.py b/simpeg_drivers/utils/synthetics/surveys/factory.py index ef86c809a..a3af9f860 100644 --- a/simpeg_drivers/utils/synthetics/surveys/factory.py +++ b/simpeg_drivers/utils/synthetics/surveys/factory.py @@ -22,8 +22,9 @@ from .natural_sources.apparent_conductivity import generate_apparent_conductivity_survey from .natural_sources.magnetotellurics import generate_magnetotellurics_survey from .natural_sources.tipper import generate_tipper_survey -from .time_domain.airborne_tdem import generate_airborne_tdem_survey -from .time_domain.ground_tdem import generate_tdem_survey +from .time_domain.airborne import generate_airborne_tdem_survey +from .time_domain.borehole import generate_borehole_tdem_survey +from .time_domain.ground import generate_tdem_survey def grid_layout( @@ -102,11 +103,13 @@ def get_survey( return generate_airborne_tdem_survey( geoh5, grid_x, grid_y, grid_z, name=options.name ) - else: - return generate_tdem_survey( + elif "borehole" in method: + return generate_borehole_tdem_survey( geoh5, grid_x, grid_y, grid_z, name=options.name ) + return generate_tdem_survey(geoh5, grid_x, grid_y, grid_z, name=options.name) + return Points.create( geoh5, vertices=np.column_stack( diff --git a/simpeg_drivers/utils/synthetics/surveys/time_domain/airborne_tdem.py b/simpeg_drivers/utils/synthetics/surveys/time_domain/airborne.py similarity index 100% rename from simpeg_drivers/utils/synthetics/surveys/time_domain/airborne_tdem.py rename to simpeg_drivers/utils/synthetics/surveys/time_domain/airborne.py diff --git a/simpeg_drivers/utils/synthetics/surveys/time_domain/borehole.py b/simpeg_drivers/utils/synthetics/surveys/time_domain/borehole.py new file mode 100644 index 000000000..bc5fd7722 --- /dev/null +++ b/simpeg_drivers/utils/synthetics/surveys/time_domain/borehole.py @@ -0,0 +1,44 @@ +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +# Copyright (c) 2023-2026 Mira Geoscience Ltd. ' +# ' +# This file is part of simpeg-drivers package. ' +# ' +# simpeg-drivers is distributed under the terms and conditions of the MIT License ' +# (see LICENSE file at the root of this source code package). ' +# ' +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' + +import numpy as np +from geoapps_utils.utils.transformations import y_rotation_matrix +from geoh5py import Workspace +from geoh5py.objects import ( + LargeLoopGroundTEMReceivers, +) + +from simpeg_drivers.utils.synthetics.surveys.time_domain import CHANNELS, WAVEFORM +from simpeg_drivers.utils.synthetics.surveys.time_domain.ground import ( + generate_tdem_survey, +) + + +def generate_borehole_tdem_survey( + geoh5: Workspace, + X: np.ndarray, + Y: np.ndarray, + Z: np.ndarray, + channels: np.ndarray = CHANNELS, + waveform: np.ndarray = WAVEFORM, + name: str = "survey", +) -> LargeLoopGroundTEMReceivers: + """Create a large loop TDEM survey object from survey grid locations.""" + + survey = generate_tdem_survey( + geoh5, X, Y, Z, channels=channels, waveform=waveform, name=name + ) + + center = survey.vertices[0, :] + survey.vertices = ( + y_rotation_matrix(np.pi / 2) @ (survey.vertices - center).T + ).T + center + + return survey diff --git a/simpeg_drivers/utils/synthetics/surveys/time_domain/ground_tdem.py b/simpeg_drivers/utils/synthetics/surveys/time_domain/ground.py similarity index 90% rename from simpeg_drivers/utils/synthetics/surveys/time_domain/ground_tdem.py rename to simpeg_drivers/utils/synthetics/surveys/time_domain/ground.py index 5421e9d2c..582749a0a 100644 --- a/simpeg_drivers/utils/synthetics/surveys/time_domain/ground_tdem.py +++ b/simpeg_drivers/utils/synthetics/surveys/time_domain/ground.py @@ -10,6 +10,7 @@ import numpy as np from geoapps_utils.utils.locations import gaussian +from geoapps_utils.utils.transformations import y_rotation_matrix from geoh5py import Workspace from geoh5py.objects import ( LargeLoopGroundTEMReceivers, @@ -95,7 +96,17 @@ def generate_tdem_survey( name=f"{name}_tx", ) transmitters.tx_id_property = transmitters.parts + 1 - survey = LargeLoopGroundTEMReceivers.create(geoh5, name=name, vertices=vertices) + + cells = [] + count = 0 + for _ in range(X.shape[0]): + inds = np.arange(count, count + X.shape[1] - 1) + cells.append(np.c_[inds, inds + 1]) + count += X.shape[1] + + survey = LargeLoopGroundTEMReceivers.create( + geoh5, name=name, vertices=vertices, cells=np.vstack(cells) + ) survey.transmitters = transmitters survey.tx_id_property = np.hstack(loop_id) diff --git a/tests/plate_simulation/leroi_air/__init__.py b/tests/plate_simulation/leroi_air/__init__.py index 94728ad80..b8f4cca56 100644 --- a/tests/plate_simulation/leroi_air/__init__.py +++ b/tests/plate_simulation/leroi_air/__init__.py @@ -17,7 +17,7 @@ LeroiAirOptions, SurveyOptions, ) -from simpeg_drivers.utils.synthetics.surveys.time_domain.airborne_tdem import ( +from simpeg_drivers.utils.synthetics.surveys.time_domain.airborne import ( generate_airborne_tdem_survey, ) From 11835d74525cb040a9c31aa5d6cde7e1bc5598c3 Mon Sep 17 00:00:00 2001 From: domfournier Date: Fri, 28 Aug 2026 10:44:08 -0500 Subject: [PATCH 11/16] Add orientation of receivers if not supplied --- .../components/factories/entity_factory.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/simpeg_drivers/components/factories/entity_factory.py b/simpeg_drivers/components/factories/entity_factory.py index fa3e0620f..314040770 100644 --- a/simpeg_drivers/components/factories/entity_factory.py +++ b/simpeg_drivers/components/factories/entity_factory.py @@ -18,6 +18,9 @@ import numpy as np from geoapps_utils.utils.importing import GeoAppsError +from geoapps_utils.utils.locations import azimuth_dip_from_segments +from geoapps_utils.utils.transformations import x_rotation_matrix, z_rotation_matrix +from geoh5py.groups import PropertyGroup from geoh5py.objects import ( CurrentElectrode, Curve, @@ -96,8 +99,45 @@ def _build(self, inversion_data: InversionData): if tx_freq: tx_freq[0].copy(parent=entity.transmitters) + if "borehole" in self.params.inversion_type: + if property_group := self.params.receivers_orientation is not None: + property_group.copy(parent=entity) + else: + self._add_auv_data_groups(entity) + return entity + @staticmethod + def _add_auv_data_groups(entity: Curve): + """ + Compute the segments orientation and add A, U and V vector data + to the entity. + + :param entity: Curve entity + """ + azi_dip = azimuth_dip_from_segments(entity, reverse=True) + + for ind, comp in enumerate("vau"): + vector = np.zeros((azi_dip.shape[0], 3)) + vector[:, ind] = 1 + vector = ( + z_rotation_matrix(-azi_dip[:, 0]) + * (x_rotation_matrix(-azi_dip[:, 1]) * vector.flatten()) + ).reshape((-1, 3)) + vec_data = entity.add_data( + { + f"{comp}_x": {"values": vector[:, 0]}, + f"{comp}_y": {"values": vector[:, 1]}, + f"{comp}_z": {"values": vector[:, 2]}, + } + ) + PropertyGroup( + entity, + property_group_type="3D vector", + name=f"{comp}_ori".capitalize(), + properties=vec_data, + ) + @staticmethod def _prune_from_indices(curve: Curve, cell_indices: np.ndarray): cells = curve.cells[cell_indices] From e23ecd9e4db3ad222822966b2e40aa2b40b9d048 Mon Sep 17 00:00:00 2001 From: domfournier Date: Fri, 28 Aug 2026 10:48:55 -0500 Subject: [PATCH 12/16] Allow to specify number of transmitter loop in synthetic ground EM --- .../components/factories/receiver_factory.py | 2 +- .../borehole_time_domain/options.py | 4 ++-- .../surveys/time_domain/borehole.py | 2 +- .../synthetics/surveys/time_domain/ground.py | 22 ++++++++----------- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/simpeg_drivers/components/factories/receiver_factory.py b/simpeg_drivers/components/factories/receiver_factory.py index bee4c6de5..90ee1098b 100644 --- a/simpeg_drivers/components/factories/receiver_factory.py +++ b/simpeg_drivers/components/factories/receiver_factory.py @@ -29,7 +29,7 @@ from simpeg_drivers.components.factories.simpeg_factory import SimPEGFactory from simpeg_drivers.options import CoreOptions -from simpeg_drivers.utils.regularization import direction_and_dip, get_cell_normals +from simpeg_drivers.utils.regularization import get_cell_normals ORIENTATION_MAP = { diff --git a/simpeg_drivers/electromagnetics/borehole_time_domain/options.py b/simpeg_drivers/electromagnetics/borehole_time_domain/options.py index 7902009bf..5268c887f 100644 --- a/simpeg_drivers/electromagnetics/borehole_time_domain/options.py +++ b/simpeg_drivers/electromagnetics/borehole_time_domain/options.py @@ -91,10 +91,10 @@ class BoreholeTDEMInversionOptions(BaseTDEMOptions, BaseInversionOptions): | AirborneTEMReceivers ) receivers_orientation: PropertyGroup | None = None - u_channel: PropertyGroup | None = None - u_uncertainty: PropertyGroup | None = None a_channel: PropertyGroup | None = None a_uncertainty: PropertyGroup | None = None + u_channel: PropertyGroup | None = None + u_uncertainty: PropertyGroup | None = None v_channel: PropertyGroup | None = None v_uncertainty: PropertyGroup | None = None diff --git a/simpeg_drivers/utils/synthetics/surveys/time_domain/borehole.py b/simpeg_drivers/utils/synthetics/surveys/time_domain/borehole.py index bc5fd7722..43ccd9afa 100644 --- a/simpeg_drivers/utils/synthetics/surveys/time_domain/borehole.py +++ b/simpeg_drivers/utils/synthetics/surveys/time_domain/borehole.py @@ -33,7 +33,7 @@ def generate_borehole_tdem_survey( """Create a large loop TDEM survey object from survey grid locations.""" survey = generate_tdem_survey( - geoh5, X, Y, Z, channels=channels, waveform=waveform, name=name + geoh5, X, Y, Z, channels=channels, waveform=waveform, name=name, n_loops=1 ) center = survey.vertices[0, :] diff --git a/simpeg_drivers/utils/synthetics/surveys/time_domain/ground.py b/simpeg_drivers/utils/synthetics/surveys/time_domain/ground.py index 582749a0a..5472b7697 100644 --- a/simpeg_drivers/utils/synthetics/surveys/time_domain/ground.py +++ b/simpeg_drivers/utils/synthetics/surveys/time_domain/ground.py @@ -28,6 +28,7 @@ def generate_tdem_survey( channels: np.ndarray = CHANNELS, waveform: np.ndarray = WAVEFORM, name: str = "survey", + n_loops: int = 2, ) -> LargeLoopGroundTEMReceivers: """Create a large loop TDEM survey object from survey grid locations.""" @@ -36,19 +37,15 @@ def generate_tdem_survey( center = np.mean(vertices, axis=0) if flatten: center[2] -= np.mean(Z) - n_lines = X.shape[0] + + x_blocks = np.array_split(X, n_loops, axis=0) + y_blocks = np.array_split(Y, n_loops, axis=0) + z_blocks = np.array_split(Z, n_loops, axis=0) arrays = [ - np.c_[ - X[: int(n_lines / 2), :].flatten(), - Y[: int(n_lines / 2), :].flatten(), - Z[: int(n_lines / 2), :].flatten(), - ], - np.c_[ - X[int(n_lines / 2) :, :].flatten(), - Y[int(n_lines / 2) :, :].flatten(), - Z[int(n_lines / 2) :, :].flatten(), - ], + np.c_[x.flatten(), y.flatten(), z.flatten()] + for x, y, z in zip(x_blocks, y_blocks, z_blocks, strict=True) ] + loops = [] loop_cells = [] loop_id = [] @@ -83,8 +80,7 @@ def generate_tdem_survey( if flatten else gaussian(loop[:, 0], loop[:, 1], amplitude=50.0, width=100.0) ) - loop = np.c_[loop, elevation] - loops += [loop + np.asarray(center)] + loops += [np.c_[loop, elevation]] loop_cells += [np.c_[np.arange(15) + count, np.arange(15) + count + 1]] loop_cells += [np.c_[count + 15, count]] count += 16 From 5b5cb56d0c5d161614e9c399c6a13d8f15fceae4 Mon Sep 17 00:00:00 2001 From: domfournier Date: Fri, 28 Aug 2026 11:04:04 -0500 Subject: [PATCH 13/16] Add unit test for borehole TEM --- tests/run_tests/driver_borehole_tem_test.py | 179 +++++--------------- 1 file changed, 45 insertions(+), 134 deletions(-) diff --git a/tests/run_tests/driver_borehole_tem_test.py b/tests/run_tests/driver_borehole_tem_test.py index 389cff256..66ef501de 100644 --- a/tests/run_tests/driver_borehole_tem_test.py +++ b/tests/run_tests/driver_borehole_tem_test.py @@ -10,7 +10,6 @@ from __future__ import annotations -import re from logging import INFO, getLogger from pathlib import Path @@ -19,13 +18,13 @@ from geoh5py.workspace import Workspace from pymatsolver.direct import Mumps -from simpeg_drivers.electromagnetics.time_domain.forward import ( - TDEMForwardDriver, - TDEMForwardOptions, +from simpeg_drivers.electromagnetics.borehole_time_domain.forward import ( + BoreholeTDEMForwardDriver, + BoreholeTDEMForwardOptions, ) -from simpeg_drivers.electromagnetics.time_domain.inversion import ( - TDEMInversionDriver, - TDEMInversionOptions, +from simpeg_drivers.electromagnetics.borehole_time_domain.inversion import ( + BoreholeTDEMInversionDriver, + BoreholeTDEMInversionOptions, ) from simpeg_drivers.utils.synthetics.driver import ( SyntheticsComponents, @@ -45,77 +44,10 @@ # To test the full run and validate the inversion. # Move this file out of the test directory and run. -target_run = {"data_norm": 1.8460e-11, "phi_d": 3.0390e01, "phi_m": 2.0320e04} +target_run = {"data_norm": 6.3414e-11, "phi_d": 1.1820e04, "phi_m": 9.7920e02} -def test_tiling_ground_tem( - tmp_path: Path, - *, - n_grid_points=4, - cell_size=(20.0, 20.0, 20.0), - refinement=(2,), - **_, -): - # Run the forward - opts = SyntheticsComponentsOptions( - method="ground tdem", - refine_plate=True, - survey=SurveyOptions( - n_stations=n_grid_points, - n_lines=n_grid_points, - drape=5.0, - topography=lambda x, y: np.zeros(x.shape), - name="ground_tdem_survey", - ), - mesh=MeshOptions( - u_cell_size=cell_size[0], - v_cell_size=cell_size[1], - w_cell_size=cell_size[2], - survey_refinement=list(refinement), - topography_refinement=[0, 0, 1], - plate_refinement=[1], - padding_distance=1000.0, - ), - model=ModelOptions( - background=0.001, - plate=PlateModel( - strike_length=40.0, - dip_length=40.0, - width=40.0, - easting=0.0, - northing=0.0, - elevation=-50.0, - ), - ), - ) - with get_workspace(tmp_path / "inversion_test.ui.geoh5") as geoh5: - components = SyntheticsComponents(geoh5, options=opts) - params = TDEMForwardOptions.build( - geoh5=geoh5, - mesh=components.mesh, - topography_object=components.topography, - data_object=components.survey, - starting_model=components.model, - x_channel_bool=True, - y_channel_bool=True, - z_channel_bool=True, - tile_spatial=4, - solver_type="Mumps", - ) - fwr_driver = TDEMForwardDriver(params) - - with geoh5.open(): - tiles = fwr_driver.get_tiles() - - assert len(tiles[None]) == 4 - - for tile in tiles[None]: - assert len(np.unique(components.survey.tx_id_property.values[tile])) == 1 - - fwr_driver.run() - - -def test_ground_tem_fwr_run( +def test_borehole_tem_fwr_run( tmp_path: Path, caplog, n_grid_points=4, @@ -127,10 +59,10 @@ def test_ground_tem_fwr_run( caplog.set_level(INFO) # Run the forward opts = SyntheticsComponentsOptions( - method="ground tdem", + method="borehole tdem", refine_plate=True, survey=SurveyOptions( - n_stations=n_grid_points, + n_stations=n_grid_points * 2, n_lines=n_grid_points, drape=5.0, topography=lambda x, y: np.zeros(x.shape), @@ -150,59 +82,36 @@ def test_ground_tem_fwr_run( strike_length=40.0, dip_length=40.0, width=40.0, - easting=0.0, + easting=-40.0, northing=0.0, - elevation=-50.0, + elevation=-75.0, ), ), ) with get_workspace(tmp_path / "inversion_test.ui.geoh5") as geoh5: components = SyntheticsComponents(geoh5, options=opts) - components.survey.transmitters.remove_cells([15]) - params = TDEMForwardOptions.build( + params = BoreholeTDEMForwardOptions.build( geoh5=geoh5, mesh=components.mesh, topography_object=components.topography, data_object=components.survey, starting_model=components.model, - x_channel_bool=True, - y_channel_bool=True, - z_channel_bool=True, - data_units="Ground B (T/A)", + a_channel_bool=True, + u_channel_bool=True, + v_channel_bool=True, solver_type="Mumps", + data_units="Ground B (T/A)", ) - fwr_driver = TDEMForwardDriver(params) - - assert fwr_driver.out_group is not None - with components.survey.workspace.open(): - components.survey.tx_id_property.name = "tx_id" - assert fwr_driver.inversion_data.survey.source_list[0].n_segments == 16 - - if pytest and caplog: - loop_warnings = [ - k - for k in caplog.records - if re.match(r"Loop \d+ modified", k.message) is not None - ] - assert len(loop_warnings) == 2 - for record in loop_warnings: - assert record.levelname == "INFO" - assert "counter-clockwise" in record.message - - assert "closed" in loop_warnings[0].message - - assert ( - fwr_driver.data_misfit.objfcts[0].simulation.simulations[0].solver == Mumps - ) + fwr_driver = BoreholeTDEMForwardDriver(params) fwr_driver.run() -def test_ground_tem_run(tmp_path: Path, max_iterations=1, pytest=True): +def test_borehole_tem_run(tmp_path: Path, max_iterations=1, pytest=True): workpath = tmp_path / "inversion_test.ui.geoh5" if pytest: workpath = ( - tmp_path.parent / "test_ground_tem_fwr_run0" / "inversion_test.ui.geoh5" + tmp_path.parent / "test_borehole_tem_fwr_run0" / "inversion_test.ui.geoh5" ) with Workspace(workpath) as geoh5: @@ -210,28 +119,28 @@ def test_ground_tem_run(tmp_path: Path, max_iterations=1, pytest=True): data = {} uncertainties = {} channels = { - "vertical": "vertical", + "a": "a", + "u": "u", + "v": "v", } - for chan, cname in channels.items(): - data[cname] = [] - uncertainties[f"{cname} uncertainties"] = [] + for chan in channels: + data[chan] = [] + uncertainties[f"{chan} uncertainties"] = [] for ii, _ in enumerate(components.survey.channels): data_entity = geoh5.get_entity(f"Iteration_0_{chan}_[{ii}]")[0].copy( parent=components.survey ) - data[cname].append(data_entity) + data[chan].append(data_entity) uncert = components.survey.add_data( { f"uncertainty_{chan}_[{ii}]": { - "values": np.ones_like(data_entity.values) - * np.median(np.abs(data_entity.values)) - / 2.0 + "values": np.abs(data_entity.values) * 0.05 + 3e-13 } } ) - uncertainties[f"{cname} uncertainties"].append(uncert) + uncertainties[f"{chan} uncertainties"].append(uncert) components.survey.add_components_data(data) components.survey.add_components_data(uncertainties) @@ -239,49 +148,51 @@ def test_ground_tem_run(tmp_path: Path, max_iterations=1, pytest=True): data_kwargs = {} for chan in channels: data_kwargs[f"{chan}_channel"] = components.survey.fetch_property_group( - name="vertical" + name=f"{chan}" ) data_kwargs[f"{chan}_uncertainty"] = components.survey.fetch_property_group( - name="vertical uncertainties" + name=f"{chan} uncertainties" ) - orig_dBzdt = geoh5.get_entity("Iteration_0_vertical_[0]")[0].values + orig_dBzdt = geoh5.get_entity("Iteration_0_a_[0]")[0].values # Run the inverse - params = TDEMInversionOptions.build( + params = BoreholeTDEMInversionOptions.build( geoh5=geoh5, mesh=components.mesh, topography_object=components.topography, data_object=components.survey, - starting_model=1e-3, + starting_model=3e-3, reference_model=1e-3, chi_factor=0.1, - s_norm=2.0, + s_norm=0.0, x_norm=2.0, y_norm=2.0, z_norm=2.0, - alpha_s=0e-1, + alpha_s=0e-0, lower_bound=2e-6, upper_bound=1e2, max_global_iterations=max_iterations, initial_beta_ratio=1e1, - cooling_rate=2, + starting_chi_factor=1000, + cooling_rate=1, max_cg_iterations=200, - percentile=100, + percentile=5, + sens_wts_threshold=1.0, solver_type="Mumps", data_units="Ground B (T/A)", **data_kwargs, ) params.write_ui_json(path=tmp_path / "Inv_run.ui.json") - driver = TDEMInversionDriver(params) + driver = BoreholeTDEMInversionDriver(params) driver.run() with geoh5.open() as run_ws: output = get_inversion_output( driver.params.geoh5.h5file, driver.params.out_group.uid ) - assert driver.inversion_data.entity.tx_id_property.name == "tx_id" + assert driver.inversion_data.entity.tx_id_property.name == "Transmitter ID" output["data"] = orig_dBzdt if pytest: check_target(output, target_run) @@ -292,7 +203,7 @@ def test_ground_tem_run(tmp_path: Path, max_iterations=1, pytest=True): if __name__ == "__main__": # Full run - test_ground_tem_fwr_run( + test_borehole_tem_fwr_run( Path("./"), None, n_grid_points=5, @@ -300,8 +211,8 @@ def test_ground_tem_run(tmp_path: Path, max_iterations=1, pytest=True): cell_size=(5.0, 5.0, 5.0), pytest=False, ) - test_ground_tem_run( + test_borehole_tem_run( Path("./"), - max_iterations=15, + max_iterations=10, pytest=False, ) From 523572da3393f7d5327d57507e392a6594321845 Mon Sep 17 00:00:00 2001 From: domfournier Date: Mon, 31 Aug 2026 12:34:50 -0700 Subject: [PATCH 14/16] Coplilot comments --- simpeg_drivers/components/factories/entity_factory.py | 2 +- simpeg_drivers/utils/surveys.py | 2 +- tests/run_tests/driver_borehole_tem_test.py | 8 +------- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/simpeg_drivers/components/factories/entity_factory.py b/simpeg_drivers/components/factories/entity_factory.py index 314040770..e4b0c6006 100644 --- a/simpeg_drivers/components/factories/entity_factory.py +++ b/simpeg_drivers/components/factories/entity_factory.py @@ -100,7 +100,7 @@ def _build(self, inversion_data: InversionData): tx_freq[0].copy(parent=entity.transmitters) if "borehole" in self.params.inversion_type: - if property_group := self.params.receivers_orientation is not None: + if (property_group := self.params.receivers_orientation) is not None: property_group.copy(parent=entity) else: self._add_auv_data_groups(entity) diff --git a/simpeg_drivers/utils/surveys.py b/simpeg_drivers/utils/surveys.py index 8e7e6b4bd..a9056f668 100644 --- a/simpeg_drivers/utils/surveys.py +++ b/simpeg_drivers/utils/surveys.py @@ -156,7 +156,7 @@ def compute_em_projections(locations, simulation): projection = 0.0 orientations = receiver.orientation.reshape((-1, 3)) for orientation, comp in zip(orientations.T, "xyz", strict=True): - if len(orientation) == 1 and orientation == 0: + if len(orientation) == 1 and orientation.item() == 0: continue orientation = diags(orientation) diff --git a/tests/run_tests/driver_borehole_tem_test.py b/tests/run_tests/driver_borehole_tem_test.py index 66ef501de..62f1fec29 100644 --- a/tests/run_tests/driver_borehole_tem_test.py +++ b/tests/run_tests/driver_borehole_tem_test.py @@ -10,7 +10,7 @@ from __future__ import annotations -from logging import INFO, getLogger +from logging import getLogger from pathlib import Path import numpy as np @@ -49,14 +49,10 @@ def test_borehole_tem_fwr_run( tmp_path: Path, - caplog, n_grid_points=4, refinement=(2,), cell_size=(20.0, 20.0, 20.0), - pytest=True, ): - if pytest and caplog: - caplog.set_level(INFO) # Run the forward opts = SyntheticsComponentsOptions( method="borehole tdem", @@ -205,11 +201,9 @@ def test_borehole_tem_run(tmp_path: Path, max_iterations=1, pytest=True): # Full run test_borehole_tem_fwr_run( Path("./"), - None, n_grid_points=5, refinement=(2, 2, 2), cell_size=(5.0, 5.0, 5.0), - pytest=False, ) test_borehole_tem_run( Path("./"), From 9641451600739cc79c4149318d98e1067c6a7ad5 Mon Sep 17 00:00:00 2001 From: domfournier Date: Tue, 1 Sep 2026 13:15:32 -0700 Subject: [PATCH 15/16] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../uijson/borehole_tdem_forward.ui.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/simpeg_drivers-assets/uijson/borehole_tdem_forward.ui.json b/simpeg_drivers-assets/uijson/borehole_tdem_forward.ui.json index dacb53101..6a6af4142 100644 --- a/simpeg_drivers-assets/uijson/borehole_tdem_forward.ui.json +++ b/simpeg_drivers-assets/uijson/borehole_tdem_forward.ui.json @@ -28,10 +28,10 @@ ], "label": "Receivers orientation", "tooltip": [ - "Receivers orientation provided as 'Dip direction & dip' or '3D vector' data group. If not provided, it assumes:
", - "
In-line component
Positive towards North (Y)
", - "
Cross-line component
Positive towards East (X)
", - "
Vertical component
Positive up (Z)
", + "Receiver orientation provided as a 'Dip direction & dip' or '3D vector' data group. If not provided, it is derived from the borehole trace:
", + "
Axial (A)
Parallel to and positive upward along the hole trace
", + "
Perpendicular (U)
Positive upward in the hole dip direction
", + "
Perpendicular (V)
Horizontal, to the left of the hole dip direction
", "
" ], "optional": true, From a57850a866015f3ecb8a3c2957cf8dc560b56afb Mon Sep 17 00:00:00 2001 From: domfournier Date: Tue, 1 Sep 2026 13:15:48 -0700 Subject: [PATCH 16/16] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../uijson/borehole_tdem_inversion.ui.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json b/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json index d211c2c48..40d3f48a5 100644 --- a/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json +++ b/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json @@ -28,10 +28,10 @@ ], "label": "Receivers orientation", "tooltip": [ - "Receivers orientation provided as 'Dip direction & dip' or '3D vector' data group. If not provided, it assumes:
", - "
In-line component
Positive towards North (Y)
", - "
Cross-line component
Positive towards East (X)
", - "
Vertical component
Positive up (Z)
", + "Receiver orientation provided as a 'Dip direction & dip' or '3D vector' data group. If not provided, it is derived from the borehole trace:
", + "
Axial (A)
Parallel to and positive upward along the hole trace
", + "
Perpendicular (U)
Positive upward in the hole dip direction
", + "
Perpendicular (V)
Horizontal, to the left of the hole dip direction
", "
" ], "optional": true,