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..6a6af4142
--- /dev/null
+++ b/simpeg_drivers-assets/uijson/borehole_tdem_forward.ui.json
@@ -0,0 +1,230 @@
+{
+ "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": [
+ "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,
+ "enabled": false,
+ "parent": "data_object",
+ "value": ""
+ },
+ "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)"
+ },
+ "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": {
+ "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": {
+ "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..40d3f48a5
--- /dev/null
+++ b/simpeg_drivers-assets/uijson/borehole_tdem_inversion.ui.json
@@ -0,0 +1,659 @@
+{
+ "version": "0.0.0",
+ "title": "Borehole TDEM Inversion",
+ "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": [
+ "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,
+ "enabled": false,
+ "parent": "data_object",
+ "value": ""
+ },
+ "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)"
+ },
+ "a_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": true,
+ "value": ""
+ },
+ "a_uncertainty": {
+ "association": [
+ "Cell",
+ "Vertex"
+ ],
+ "dataType": "Float",
+ "group": "Data",
+ "dataGroupType": "Multi-element",
+ "main": true,
+ "label": "Uncertainty",
+ "parent": "data_object",
+ "dependency": "a_channel",
+ "dependencyType": "enabled",
+ "value": ""
+ },
+ "u_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": false,
+ "value": ""
+ },
+ "u_uncertainty": {
+ "association": [
+ "Cell",
+ "Vertex"
+ ],
+ "dataType": "Float",
+ "group": "Data",
+ "dataGroupType": "Multi-element",
+ "main": true,
+ "label": "Uncertainty",
+ "parent": "data_object",
+ "dependency": "u_channel",
+ "dependencyType": "enabled",
+ "value": ""
+ },
+ "v_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": ""
+ },
+ "v_uncertainty": {
+ "association": [
+ "Cell",
+ "Vertex"
+ ],
+ "dataType": "Float",
+ "group": "Data",
+ "dataGroupType": "Multi-element",
+ "main": true,
+ "label": "Uncertainty",
+ "parent": "data_object",
+ "dependency": "v_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 f24cea4cb..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
@@ -188,6 +187,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/entity_factory.py b/simpeg_drivers/components/factories/entity_factory.py
index fa3e0620f..e4b0c6006 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]
diff --git a/simpeg_drivers/components/factories/receiver_factory.py b/simpeg_drivers/components/factories/receiver_factory.py
index aac55f10e..90ee1098b 100644
--- a/simpeg_drivers/components/factories/receiver_factory.py
+++ b/simpeg_drivers/components/factories/receiver_factory.py
@@ -14,23 +14,22 @@
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.transformations import x_rotation_matrix, z_rotation_matrix
+from geoapps_utils.utils.locations import azimuth_dip_from_segments
+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,
)
from simpeg_drivers.components.factories.simpeg_factory import SimPEGFactory
-from simpeg_drivers.utils.regularization import direction_and_dip, get_cell_normals
+from simpeg_drivers.options import CoreOptions
+from simpeg_drivers.utils.regularization import get_cell_normals
ORIENTATION_MAP = {
@@ -39,13 +38,16 @@
"vertical": "z",
"inline": "y",
"crossline": "x",
+ "a": "y",
+ "u": "z",
+ "v": "x",
}
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 +261,26 @@ def validate_orientations(self):
for comp in "xyz"
}
- 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])
+ azi_dip = None
+
+ 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, reverse=True)
+
+ if azi_dip is not None:
orientations = {}
for axis in "xyz":
orientations[axis] = (
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..ad9b20a30 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",
@@ -110,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
@@ -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/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.
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..d9c7e0eb7
--- /dev/null
+++ b/simpeg_drivers/electromagnetics/borehole_time_domain/forward.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 ForwardDriver
+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 = BoreholeTDEMForwardOptions
+
+
+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..5268c887f
--- /dev/null
+++ b/simpeg_drivers/electromagnetics/borehole_time_domain/options.py
@@ -0,0 +1,101 @@
+# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
+# 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 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 BaseTDEMOptions
+from simpeg_drivers.options import (
+ BaseForwardOptions,
+ BaseInversionOptions,
+ ConductivityModelOptions,
+)
+
+
+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.
+ """
+
+ 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"
+ 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(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.
+ """
+
+ 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"
+
+ data_object: (
+ MovingLoopGroundTEMReceivers
+ | LargeLoopGroundTEMReceivers
+ | AirborneTEMReceivers
+ )
+ receivers_orientation: 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
+
+ models: ConductivityModelOptions
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/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..43ccd9afa
--- /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, n_loops=1
+ )
+
+ 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 82%
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..5472b7697 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,
@@ -27,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."""
@@ -35,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 = []
@@ -82,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
@@ -95,7 +92,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,
)
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..62f1fec29
--- /dev/null
+++ b/tests/run_tests/driver_borehole_tem_test.py
@@ -0,0 +1,212 @@
+# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
+# 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 logging import 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.borehole_time_domain.forward import (
+ BoreholeTDEMForwardDriver,
+ BoreholeTDEMForwardOptions,
+)
+from simpeg_drivers.electromagnetics.borehole_time_domain.inversion import (
+ BoreholeTDEMInversionDriver,
+ BoreholeTDEMInversionOptions,
+)
+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": 6.3414e-11, "phi_d": 1.1820e04, "phi_m": 9.7920e02}
+
+
+def test_borehole_tem_fwr_run(
+ tmp_path: Path,
+ n_grid_points=4,
+ refinement=(2,),
+ cell_size=(20.0, 20.0, 20.0),
+):
+ # Run the forward
+ opts = SyntheticsComponentsOptions(
+ method="borehole tdem",
+ refine_plate=True,
+ survey=SurveyOptions(
+ n_stations=n_grid_points * 2,
+ 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=-40.0,
+ northing=0.0,
+ elevation=-75.0,
+ ),
+ ),
+ )
+ with get_workspace(tmp_path / "inversion_test.ui.geoh5") as geoh5:
+ components = SyntheticsComponents(geoh5, options=opts)
+ params = BoreholeTDEMForwardOptions.build(
+ geoh5=geoh5,
+ mesh=components.mesh,
+ topography_object=components.topography,
+ data_object=components.survey,
+ starting_model=components.model,
+ a_channel_bool=True,
+ u_channel_bool=True,
+ v_channel_bool=True,
+ solver_type="Mumps",
+ data_units="Ground B (T/A)",
+ )
+
+ fwr_driver = BoreholeTDEMForwardDriver(params)
+ fwr_driver.run()
+
+
+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_borehole_tem_fwr_run0" / "inversion_test.ui.geoh5"
+ )
+
+ with Workspace(workpath) as geoh5:
+ components = SyntheticsComponents(geoh5)
+ data = {}
+ uncertainties = {}
+ channels = {
+ "a": "a",
+ "u": "u",
+ "v": "v",
+ }
+
+ 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[chan].append(data_entity)
+
+ uncert = components.survey.add_data(
+ {
+ f"uncertainty_{chan}_[{ii}]": {
+ "values": np.abs(data_entity.values) * 0.05 + 3e-13
+ }
+ }
+ )
+ uncertainties[f"{chan} 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=f"{chan}"
+ )
+ data_kwargs[f"{chan}_uncertainty"] = components.survey.fetch_property_group(
+ name=f"{chan} uncertainties"
+ )
+
+ orig_dBzdt = geoh5.get_entity("Iteration_0_a_[0]")[0].values
+
+ # Run the inverse
+ params = BoreholeTDEMInversionOptions.build(
+ geoh5=geoh5,
+ mesh=components.mesh,
+ topography_object=components.topography,
+ data_object=components.survey,
+ starting_model=3e-3,
+ reference_model=1e-3,
+ chi_factor=0.1,
+ s_norm=0.0,
+ x_norm=2.0,
+ y_norm=2.0,
+ z_norm=2.0,
+ alpha_s=0e-0,
+ lower_bound=2e-6,
+ upper_bound=1e2,
+ max_global_iterations=max_iterations,
+ initial_beta_ratio=1e1,
+ starting_chi_factor=1000,
+ cooling_rate=1,
+ max_cg_iterations=200,
+ 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 = 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 == "Transmitter 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_borehole_tem_fwr_run(
+ Path("./"),
+ n_grid_points=5,
+ refinement=(2, 2, 2),
+ cell_size=(5.0, 5.0, 5.0),
+ )
+ test_borehole_tem_run(
+ Path("./"),
+ max_iterations=10,
+ pytest=False,
+ )