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, + )