From 67e095c6bbc35e5519b7c6df78d38046ad27dd7c Mon Sep 17 00:00:00 2001 From: dominiquef Date: Wed, 11 Mar 2026 13:58:33 -0700 Subject: [PATCH 01/17] Start adding inline and cross line components --- .../uijson/fdem_forward.ui.json | 31 ++- .../uijson/fdem_inversion.ui.json | 135 ++++++++++ .../frequency_domain/options.py | 12 + .../frequency_domain_1d/options.py | 23 +- tests/run_tests/oriented_fem_receiver_test.py | 237 ++++++++++++++++++ 5 files changed, 429 insertions(+), 9 deletions(-) create mode 100644 tests/run_tests/oriented_fem_receiver_test.py diff --git a/simpeg_drivers-assets/uijson/fdem_forward.ui.json b/simpeg_drivers-assets/uijson/fdem_forward.ui.json index d8279797..693caadd 100644 --- a/simpeg_drivers-assets/uijson/fdem_forward.ui.json +++ b/simpeg_drivers-assets/uijson/fdem_forward.ui.json @@ -22,13 +22,40 @@ "z_imag_channel_bool": { "group": "Survey", "main": true, - "label": "Z imag component", + "label": "Vertical imag component", + "tooltip": "Vertical (positive up) imaginary component of the magnetic data", "value": true }, "z_real_channel_bool": { "group": "Survey", "main": true, - "label": "Z real component", + "label": "Vertical real component", + "tooltip": "Vertical (positive up) real component of the magnetic data", + "value": true + }, + "inline_imag_channel_bool": { + "group": "Survey", + "main": true, + "label": "In-line imag component", + "tooltip": "Horizontal imaginary component of the magnetic data, parallel to the survey direction", + "value": true + }, + "inline_real_channel_bool": { + "group": "Survey", + "main": true, + "label": "Horizontal real component of the magnetic data, parallel to the survey direction", + "value": true + }, + "crossline_imag_channel_bool": { + "group": "Survey", + "main": true, + "label": "Horizontal imaginary component of the magnetic data, perpendicular to the survey direction", + "value": true + }, + "crossline_real_channel_bool": { + "group": "Survey", + "main": true, + "label": "Horizontal real component of the magnetic data, perpendicular to the survey direction", "value": true }, "mesh": { diff --git a/simpeg_drivers-assets/uijson/fdem_inversion.ui.json b/simpeg_drivers-assets/uijson/fdem_inversion.ui.json index a79f50e2..173677c7 100644 --- a/simpeg_drivers-assets/uijson/fdem_inversion.ui.json +++ b/simpeg_drivers-assets/uijson/fdem_inversion.ui.json @@ -19,6 +19,20 @@ ], "value": "" }, + "receivers_orientation": { + "group": "Data", + "association": "Vertex", + "dataType": "Float", + "dataGroupType": [ + "Dip direction & dip", + "3D vector" + ], + "label": "Receivers orientation provided as either Dip & dip direction or 3D vector components", + "optional": true, + "enabled": false, + "parent": "data_object", + "value": "" + }, "z_imag_channel": { "association": [ "Cell", @@ -79,6 +93,127 @@ "dependencyType": "enabled", "value": "" }, + "inline_imag_channel": { + "association": [ + "Cell", + "Vertex" + ], + "dataType": "Float", + "group": "Data", + "dataGroupType": "Multi-element", + "main": true, + "label": "In-line imag component", + "parent": "data_object", + "optional": true, + "enabled": true, + "value": "" + }, + "inline_imag_uncertainty": { + "association": [ + "Cell", + "Vertex" + ], + "dataType": "Float", + "group": "Data", + "dataGroupType": "Multi-element", + "main": true, + "label": "Uncertainty", + "parent": "data_object", + "dependency": "inline_imag_channel", + "dependencyType": "enabled", + "value": "" + }, + "inline_real_channel": { + "association": [ + "Cell", + "Vertex" + ], + "dataType": "Float", + "group": "Data", + "dataGroupType": "Multi-element", + "main": true, + "label": "In-line real component", + "parent": "data_object", + "optional": true, + "enabled": true, + "value": "" + }, + "inline_real_uncertainty": { + "association": [ + "Cell", + "Vertex" + ], + "dataType": "Float", + "group": "Data", + "dataGroupType": "Multi-element", + "main": true, + "label": "Uncertainty", + "parent": "data_object", + "dependency": "inline_real_channel", + "dependencyType": "enabled", + "value": "" + }, + "crossline_imag_channel": { + "association": [ + "Cell", + "Vertex" + ], + "dataType": "Float", + "group": "Data", + "dataGroupType": "Multi-element", + "main": true, + "label": "Cross-line imag component", + "tooltip": "Horizontal imaginary component of the magnetic data, perpendicular to the survey direction", + "parent": "data_object", + "optional": true, + "enabled": true, + "value": "" + }, + "crossline_imag_uncertainty": { + "association": [ + "Cell", + "Vertex" + ], + "dataType": "Float", + "group": "Data", + "dataGroupType": "Multi-element", + "main": true, + "label": "Uncertainty", + "parent": "data_object", + "dependency": "crossline_imag_channel", + "dependencyType": "enabled", + "value": "" + }, + "crossline_real_channel": { + "association": [ + "Cell", + "Vertex" + ], + "dataType": "Float", + "group": "Data", + "dataGroupType": "Multi-element", + "main": true, + "label": "Horizontal real component of the magnetic data, parallel to the survey direction", + "parent": "data_object", + "optional": true, + "enabled": true, + "value": "" + }, + "crossline_real_uncertainty": { + "association": [ + "Cell", + "Vertex" + ], + "dataType": "Float", + "group": "Data", + "dataGroupType": "Multi-element", + "main": true, + "label": "Uncertainty", + "parent": "data_object", + "dependency": "crossline_real_channel", + "dependencyType": "enabled", + "value": "" + }, "mesh": { "group": "Mesh and models", "main": true, diff --git a/simpeg_drivers/electromagnetics/frequency_domain/options.py b/simpeg_drivers/electromagnetics/frequency_domain/options.py index 5e0c672f..693f0fff 100644 --- a/simpeg_drivers/electromagnetics/frequency_domain/options.py +++ b/simpeg_drivers/electromagnetics/frequency_domain/options.py @@ -103,6 +103,10 @@ class FDEMForwardOptions(BaseForwardOptions, BaseFDEMOptions): ) z_real_channel_bool: bool z_imag_channel_bool: bool + inline_real_channel_bool: bool + inline_imag_channel_bool: bool + crossline_real_channel_bool: bool + crossline_imag_channel_bool: bool models: ConductivityModelOptions @@ -132,6 +136,14 @@ class FDEMInversionOptions(BaseFDEMOptions, BaseInversionOptions): z_real_uncertainty: PropertyGroup | None = None z_imag_channel: PropertyGroup | None = None z_imag_uncertainty: PropertyGroup | None = None + inline_real_channel: PropertyGroup | None = None + inline_real_uncertainty: PropertyGroup | None = None + inline_imag_channel: PropertyGroup | None = None + inline_imag_uncertainty: PropertyGroup | None = None + crossline_real_channel: PropertyGroup | None = None + crossline_real_uncertainty: PropertyGroup | None = None + crossline_imag_channel: PropertyGroup | None = None + crossline_imag_uncertainty: PropertyGroup | None = None models: ConductivityModelOptions diff --git a/simpeg_drivers/electromagnetics/frequency_domain_1d/options.py b/simpeg_drivers/electromagnetics/frequency_domain_1d/options.py index 875db4a6..fd9fddb8 100644 --- a/simpeg_drivers/electromagnetics/frequency_domain_1d/options.py +++ b/simpeg_drivers/electromagnetics/frequency_domain_1d/options.py @@ -15,17 +15,20 @@ from typing import ClassVar from geoh5py.groups import PropertyGroup +from geoh5py.objects import AirborneFEMReceivers from simpeg_drivers import assets_path from simpeg_drivers.electromagnetics.base_1d_options import Base1DOptions -from simpeg_drivers.electromagnetics.frequency_domain.options import ( - FDEMForwardOptions, - FDEMInversionOptions, +from simpeg_drivers.electromagnetics.frequency_domain.options import BaseFDEMOptions +from simpeg_drivers.options import ( + BaseForwardOptions, + BaseInversionOptions, + ConductivityModelOptions, + DirectiveOptions, ) -from simpeg_drivers.options import DirectiveOptions -class FDEM1DForwardOptions(FDEMForwardOptions, Base1DOptions): +class FDEM1DForwardOptions(BaseForwardOptions, BaseFDEMOptions, Base1DOptions): """ Frequency Domain Electromagnetic forward options. @@ -38,13 +41,15 @@ class FDEM1DForwardOptions(FDEMForwardOptions, Base1DOptions): default_ui_json: ClassVar[Path] = assets_path() / "uijson/fdem1d_forward.ui.json" title: str = "Frequency-domain EM-1D (FEM-1D) Forward" + physical_property: str = "conductivity" inversion_type: str = "fdem 1d" - + data_object: AirborneFEMReceivers z_real_channel_bool: bool z_imag_channel_bool: bool + models: ConductivityModelOptions -class FDEM1DInversionOptions(FDEMInversionOptions, Base1DOptions): +class FDEM1DInversionOptions(BaseInversionOptions, BaseFDEMOptions, Base1DOptions): """ Frequency Domain Electromagnetic Inversion options. @@ -58,8 +63,10 @@ class FDEM1DInversionOptions(FDEMInversionOptions, Base1DOptions): name: ClassVar[str] = "Frequency Domain 1D Electromagnetics Inversion" default_ui_json: ClassVar[Path] = assets_path() / "uijson/fdem1d_inversion.ui.json" title: str = "Frequency-domain EM-1D (FEM-1D) Inversion" + physical_property: str = "conductivity" inversion_type: str = "fdem 1d" + data_object: AirborneFEMReceivers directives: DirectiveOptions = DirectiveOptions( sens_wts_threshold=100.0, ) @@ -67,3 +74,5 @@ class FDEM1DInversionOptions(FDEMInversionOptions, Base1DOptions): z_real_uncertainty: PropertyGroup | None = None z_imag_channel: PropertyGroup | None = None z_imag_uncertainty: PropertyGroup | None = None + models: ConductivityModelOptions + directives: DirectiveOptions = DirectiveOptions() diff --git a/tests/run_tests/oriented_fem_receiver_test.py b/tests/run_tests/oriented_fem_receiver_test.py new file mode 100644 index 00000000..4322ec46 --- /dev/null +++ b/tests/run_tests/oriented_fem_receiver_test.py @@ -0,0 +1,237 @@ +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +# 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). ' +# ' +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' + +# pylint: disable=too-many-locals + +from __future__ import annotations + +from pathlib import Path + +import numpy as np +from geoapps_utils.modelling.plates import PlateModel +from geoh5py import Workspace +from geoh5py.groups import SimPEGGroup + +from simpeg_drivers.electromagnetics.frequency_domain.driver import ( + FDEMForwardDriver, + FDEMInversionDriver, +) +from simpeg_drivers.electromagnetics.frequency_domain.options import ( + FDEMForwardOptions, + FDEMInversionOptions, +) +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 + + +# To test the full run and validate the inversion. +# Move this file out of the test directory and run. + +target_run = {"data_norm": 91.18814842528005, "phi_d": 4250, "phi_m": 968} + + +def test_fem_name_change(tmp_path): + # Run the forward + opts = SyntheticsComponentsOptions( + method="fdem", + survey=SurveyOptions(n_stations=2, n_lines=2, drape=15.0), + mesh=MeshOptions(refinement=(2,), padding_distance=400.0), + model=ModelOptions( + background=1e-3, + plate=PlateModel( + strike_length=500.0, + dip_length=150.0, + width=20.0, + origin=(0.0, 0.0, -10.0), + direction=60.0, + dip=70.0, + ), + ), + ) + with get_workspace(tmp_path / "inversion_test.ui.geoh5") as geoh5: + components = SyntheticsComponents(geoh5, options=opts) + + FDEMForwardOptions.build( + geoh5=geoh5, + mesh=components.mesh, + topography_object=components.topography, + data_object=components.survey, + starting_model=components.model, + z_real_channel_bool=True, + z_imag_channel_bool=True, + inversion_type="fdem", + ) + + +def test_fem_fwr_run( + tmp_path: Path, + n_grid_points=3, + refinement=(2,), + cell_size=(20.0, 20.0, 20.0), +): + # Run the forward + opts = SyntheticsComponentsOptions( + method="fdem", + survey=SurveyOptions( + n_stations=n_grid_points, + n_lines=n_grid_points, + drape=15.0, + topography=lambda x, y: np.zeros(x.shape), + ), + mesh=MeshOptions( + cell_size=cell_size, refinement=refinement, padding_distance=400.0 + ), + model=ModelOptions( + background=1e-3, + plate=PlateModel( + strike_length=40.0, + dip_length=40.0, + width=40.0, + origin=(0.0, 0.0, -50.0), + ), + ), + ) + with get_workspace(tmp_path / "inversion_test.ui.geoh5") as geoh5: + components = SyntheticsComponents(geoh5, options=opts) + params = FDEMForwardOptions.build( + geoh5=geoh5, + mesh=components.mesh, + topography_object=components.topography, + data_object=components.survey, + starting_model=components.model, + z_real_channel_bool=True, + z_imag_channel_bool=True, + ) + + fwr_driver = FDEMForwardDriver(params) + fwr_driver.run() + + +def test_fem_run(tmp_path: Path, max_iterations=1, pytest=True): + workpath = tmp_path / "inversion_test.ui.geoh5" + if pytest: + workpath = tmp_path.parent / "test_fem_fwr_run0" / "inversion_test.ui.geoh5" + + with Workspace(workpath) as geoh5: + components = SyntheticsComponents(geoh5) + data = {} + uncertainties = {} + channels = { + "z_real": "z_real", + "z_imag": "z_imag", + } + + for chan, cname in channels.items(): + data[cname] = [] + uncertainties[f"{cname} uncertainties"] = [] + for ind, freq in enumerate(components.survey.channels): + data_entity = geoh5.get_entity(f"Iteration_0_{chan}_[{ind}]")[0].copy( + parent=components.survey + ) + data[cname].append(data_entity) + abs_val = np.abs(data_entity.values) + uncert = components.survey.add_data( + { + f"uncertainty_{chan}_[{ind}]": { + "values": np.ones_like(abs_val) + * freq + / 200.0 # * 2**(np.abs(ind-1)) + } + } + ) + uncertainties[f"{cname} uncertainties"].append( + uncert.copy(parent=components.survey) + ) + + data_groups = components.survey.add_components_data(data) + uncert_groups = components.survey.add_components_data(uncertainties) + + data_kwargs = {} + for chan, data_group, uncert_group in zip( + channels, data_groups, uncert_groups, strict=True + ): + data_kwargs[f"{chan}_channel"] = data_group + data_kwargs[f"{chan}_uncertainty"] = uncert_group + + orig_z_real_1 = geoh5.get_entity("Iteration_0_z_real_[0]")[0].values + + # Run the inverse + params = FDEMInversionOptions.build( + geoh5=geoh5, + mesh=components.mesh, + topography_object=components.topography, + data_object=components.survey, + starting_model=1e-3, + reference_model=1e-3, + alpha_s=0.0, + s_norm=0.0, + x_norm=0.0, + y_norm=0.0, + z_norm=0.0, + upper_bound=0.75, + max_global_iterations=max_iterations, + initial_beta_ratio=1e1, + percentile=100, + cooling_rate=1, + chi_factor=0.25, + auto_scale_channels=True, + tile_spatial=2, + **data_kwargs, + ) + params.write_ui_json(path=tmp_path / "Inv_run.ui.json") + driver = FDEMInversionDriver(params) + driver.run() + + # Scaling is done evenly on channels + np.testing.assert_allclose( + driver.data_misfit.multipliers, + [1.0, 1.0, 0.6004, 0.6004, 0.5047, 0.5047], + atol=1e-3, + ) + + with geoh5.open() as run_ws: + output = get_inversion_output( + driver.params.geoh5.h5file, driver.params.out_group.uid + ) + output["data"] = orig_z_real_1 + + assert ( + run_ws.get_entity("Iteration_1_z_imag_[1]")[0].entity_type.uid + == run_ws.get_entity("Observed_z_imag_[1]")[0].entity_type.uid + ) + + 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_fem_fwr_run( + Path("./"), + n_grid_points=5, + cell_size=(5.0, 5.0, 5.0), + refinement=(4, 4, 4), + ) + test_fem_run( + Path("./"), + max_iterations=15, + pytest=False, + ) From 8d41d54dd2315dbcd7331a2863280c15de37c7c1 Mon Sep 17 00:00:00 2001 From: dominiquef Date: Wed, 11 Mar 2026 15:07:06 -0700 Subject: [PATCH 02/17] Add receiver orientation in EM options --- simpeg_drivers/options.py | 1 + 1 file changed, 1 insertion(+) diff --git a/simpeg_drivers/options.py b/simpeg_drivers/options.py index 458573ac..31bd4af5 100644 --- a/simpeg_drivers/options.py +++ b/simpeg_drivers/options.py @@ -438,6 +438,7 @@ class EMDataMixin: """ data_object: BaseEMSurvey + receiver_orientation: PropertyGroup | None = None def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) From 617aa0a0a8d49236b7377edb04276705dbd92627 Mon Sep 17 00:00:00 2001 From: dominiquef Date: Wed, 11 Mar 2026 16:14:11 -0700 Subject: [PATCH 03/17] Add receivers_oritentation --- simpeg_drivers-assets/uijson/fdem_forward.ui.json | 14 ++++++++++++++ simpeg_drivers-assets/uijson/tdem_forward.ui.json | 14 ++++++++++++++ .../uijson/tdem_inversion.ui.json | 14 ++++++++++++++ .../uijson/tipper_forward.ui.json | 14 ++++++++++++++ .../uijson/tipper_inversion.ui.json | 14 ++++++++++++++ .../electromagnetics/frequency_domain/options.py | 2 ++ .../electromagnetics/time_domain/options.py | 2 ++ simpeg_drivers/natural_sources/tipper/options.py | 2 ++ simpeg_drivers/options.py | 1 - 9 files changed, 76 insertions(+), 1 deletion(-) diff --git a/simpeg_drivers-assets/uijson/fdem_forward.ui.json b/simpeg_drivers-assets/uijson/fdem_forward.ui.json index 693caadd..909588ce 100644 --- a/simpeg_drivers-assets/uijson/fdem_forward.ui.json +++ b/simpeg_drivers-assets/uijson/fdem_forward.ui.json @@ -19,6 +19,20 @@ ], "value": "" }, + "receivers_orientation": { + "group": "Data", + "association": "Vertex", + "dataType": "Float", + "dataGroupType": [ + "Dip direction & dip", + "3D vector" + ], + "label": "Receivers orientation provided as either Dip & dip direction or 3D vector components", + "optional": true, + "enabled": false, + "parent": "data_object", + "value": "" + }, "z_imag_channel_bool": { "group": "Survey", "main": true, diff --git a/simpeg_drivers-assets/uijson/tdem_forward.ui.json b/simpeg_drivers-assets/uijson/tdem_forward.ui.json index f46567a5..e3fb74d0 100644 --- a/simpeg_drivers-assets/uijson/tdem_forward.ui.json +++ b/simpeg_drivers-assets/uijson/tdem_forward.ui.json @@ -20,6 +20,20 @@ ], "value": "" }, + "receivers_orientation": { + "group": "Data", + "association": "Vertex", + "dataType": "Float", + "dataGroupType": [ + "Dip direction & dip", + "3D vector" + ], + "label": "Receivers orientation provided as either Dip & dip direction or 3D vector components", + "optional": true, + "enabled": false, + "parent": "data_object", + "value": "" + }, "data_units": { "choiceList": [ "Airborne dB/dt (V/Am^4)", diff --git a/simpeg_drivers-assets/uijson/tdem_inversion.ui.json b/simpeg_drivers-assets/uijson/tdem_inversion.ui.json index 28b47d0b..b9285930 100644 --- a/simpeg_drivers-assets/uijson/tdem_inversion.ui.json +++ b/simpeg_drivers-assets/uijson/tdem_inversion.ui.json @@ -20,6 +20,20 @@ ], "value": "" }, + "receivers_orientation": { + "group": "Data", + "association": "Vertex", + "dataType": "Float", + "dataGroupType": [ + "Dip direction & dip", + "3D vector" + ], + "label": "Receivers orientation provided as either Dip & dip direction or 3D vector components", + "optional": true, + "enabled": false, + "parent": "data_object", + "value": "" + }, "data_units": { "choiceList": [ "Airborne dB/dt (V/Am^4)", diff --git a/simpeg_drivers-assets/uijson/tipper_forward.ui.json b/simpeg_drivers-assets/uijson/tipper_forward.ui.json index 2c73996a..c92cc294 100644 --- a/simpeg_drivers-assets/uijson/tipper_forward.ui.json +++ b/simpeg_drivers-assets/uijson/tipper_forward.ui.json @@ -17,6 +17,20 @@ "meshType": "{0b639533-f35b-44d8-92a8-f70ecff3fd26}", "value": "" }, + "receivers_orientation": { + "group": "Data", + "association": "Vertex", + "dataType": "Float", + "dataGroupType": [ + "Dip direction & dip", + "3D vector" + ], + "label": "Receivers orientation provided as either Dip & dip direction or 3D vector components", + "optional": true, + "enabled": false, + "parent": "data_object", + "value": "" + }, "txz_imag_channel_bool": { "group": "Survey", "main": true, diff --git a/simpeg_drivers-assets/uijson/tipper_inversion.ui.json b/simpeg_drivers-assets/uijson/tipper_inversion.ui.json index 848948d1..ea981220 100644 --- a/simpeg_drivers-assets/uijson/tipper_inversion.ui.json +++ b/simpeg_drivers-assets/uijson/tipper_inversion.ui.json @@ -17,6 +17,20 @@ "meshType": "{0b639533-f35b-44d8-92a8-f70ecff3fd26}", "value": "" }, + "receivers_orientation": { + "group": "Data", + "association": "Vertex", + "dataType": "Float", + "dataGroupType": [ + "Dip direction & dip", + "3D vector" + ], + "label": "Receivers orientation provided as either Dip & dip direction or 3D vector components", + "optional": true, + "enabled": false, + "parent": "data_object", + "value": "" + }, "txz_imag_channel": { "association": [ "Cell", diff --git a/simpeg_drivers/electromagnetics/frequency_domain/options.py b/simpeg_drivers/electromagnetics/frequency_domain/options.py index 693f0fff..829e2008 100644 --- a/simpeg_drivers/electromagnetics/frequency_domain/options.py +++ b/simpeg_drivers/electromagnetics/frequency_domain/options.py @@ -101,6 +101,7 @@ class FDEMForwardOptions(BaseForwardOptions, BaseFDEMOptions): | LargeLoopGroundFEMReceivers | AirborneFEMReceivers ) + receivers_orientation: PropertyGroup | None = None z_real_channel_bool: bool z_imag_channel_bool: bool inline_real_channel_bool: bool @@ -132,6 +133,7 @@ class FDEMInversionOptions(BaseFDEMOptions, BaseInversionOptions): | LargeLoopGroundFEMReceivers | AirborneFEMReceivers ) + receivers_orientation: PropertyGroup | None = None z_real_channel: PropertyGroup | None = None z_real_uncertainty: PropertyGroup | None = None z_imag_channel: PropertyGroup | None = None diff --git a/simpeg_drivers/electromagnetics/time_domain/options.py b/simpeg_drivers/electromagnetics/time_domain/options.py index 0f3e4fbb..591a1c21 100644 --- a/simpeg_drivers/electromagnetics/time_domain/options.py +++ b/simpeg_drivers/electromagnetics/time_domain/options.py @@ -92,6 +92,7 @@ class TDEMForwardOptions(BaseTDEMOptions, BaseForwardOptions): | LargeLoopGroundTEMReceivers | AirborneTEMReceivers ) + receivers_orientation: PropertyGroup | None = None z_channel_bool: bool | None = None x_channel_bool: bool | None = None y_channel_bool: bool | None = None @@ -121,6 +122,7 @@ class TDEMInversionOptions(BaseTDEMOptions, BaseInversionOptions): | LargeLoopGroundTEMReceivers | AirborneTEMReceivers ) + receivers_orientation: PropertyGroup | None = None z_channel: PropertyGroup | None = None z_uncertainty: PropertyGroup | None = None x_channel: PropertyGroup | None = None diff --git a/simpeg_drivers/natural_sources/tipper/options.py b/simpeg_drivers/natural_sources/tipper/options.py index 4f55f377..59647440 100644 --- a/simpeg_drivers/natural_sources/tipper/options.py +++ b/simpeg_drivers/natural_sources/tipper/options.py @@ -46,6 +46,7 @@ class TipperForwardOptions(EMDataMixin, BaseForwardOptions): inversion_type: str = "tipper" data_object: TipperReceivers + receivers_orientation: PropertyGroup | None = None txz_real_channel_bool: bool | None = None txz_imag_channel_bool: bool | None = None tyz_real_channel_bool: bool | None = None @@ -75,6 +76,7 @@ class TipperInversionOptions(EMDataMixin, BaseInversionOptions): inversion_type: str = "tipper" data_object: TipperReceivers + receivers_orientation: PropertyGroup | None = None txz_real_channel: PropertyGroup | None = None txz_real_uncertainty: PropertyGroup | None = None txz_imag_channel: PropertyGroup | None = None diff --git a/simpeg_drivers/options.py b/simpeg_drivers/options.py index 31bd4af5..458573ac 100644 --- a/simpeg_drivers/options.py +++ b/simpeg_drivers/options.py @@ -438,7 +438,6 @@ class EMDataMixin: """ data_object: BaseEMSurvey - receiver_orientation: PropertyGroup | None = None def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) From 8b16768c45af5b4c109e6c8b2acd9eccc0d601fa Mon Sep 17 00:00:00 2001 From: dominiquef Date: Thu, 12 Mar 2026 11:02:22 -0700 Subject: [PATCH 04/17] Use x,y,z for variable name, but vertical inline cross-line for labels. Update uijson and options. --- .../uijson/fdem_forward.ui.json | 29 +++++---- .../uijson/fdem_inversion.ui.json | 63 ++++++++++--------- .../frequency_domain/options.py | 59 ++++++++++------- 3 files changed, 86 insertions(+), 65 deletions(-) diff --git a/simpeg_drivers-assets/uijson/fdem_forward.ui.json b/simpeg_drivers-assets/uijson/fdem_forward.ui.json index 909588ce..2510350c 100644 --- a/simpeg_drivers-assets/uijson/fdem_forward.ui.json +++ b/simpeg_drivers-assets/uijson/fdem_forward.ui.json @@ -36,40 +36,43 @@ "z_imag_channel_bool": { "group": "Survey", "main": true, - "label": "Vertical imag component", - "tooltip": "Vertical (positive up) imaginary component of the magnetic data", + "label": "Vertical (imaginary)", + "tooltip": "Vertical (w) imaginary component of the magnetic data.\\Positive up along the z-axis if no receiver orientation provided", "value": true }, "z_real_channel_bool": { "group": "Survey", "main": true, - "label": "Vertical real component", - "tooltip": "Vertical (positive up) real component of the magnetic data", + "label": "Vertical (real)", + "tooltip": "Vertical (w) real component of the magnetic data.\\Positive up along the z-axis if no receiver orientation provided", "value": true }, - "inline_imag_channel_bool": { + "x_imag_channel_bool": { "group": "Survey", "main": true, - "label": "In-line imag component", - "tooltip": "Horizontal imaginary component of the magnetic data, parallel to the survey direction", + "label": "In-line (imaginary)", + "tooltip": "In-line (u) imaginary component of the magnetic data.\\Positive towards North if no receiver orientation provided", "value": true }, - "inline_real_channel_bool": { + "x_real_channel_bool": { "group": "Survey", "main": true, - "label": "Horizontal real component of the magnetic data, parallel to the survey direction", + "label": "In-line (real)", + "tooltip": "In-line (u) real component of the magnetic data.\\Positive towards North if no receiver orientation provided", "value": true }, - "crossline_imag_channel_bool": { + "y_imag_channel_bool": { "group": "Survey", "main": true, - "label": "Horizontal imaginary component of the magnetic data, perpendicular to the survey direction", + "label": "Cross-line (imaginary)", + "tooltip": "Cross-line (v) imaginary component of the magnetic data.\\Positive towards East if no receiver orientation provided", "value": true }, - "crossline_real_channel_bool": { + "y_real_channel_bool": { "group": "Survey", "main": true, - "label": "Horizontal real component of the magnetic data, perpendicular to the survey direction", + "label": "Cross-line (real)", + "tooltip": "Cross-line (v) real component of the magnetic data.\\Positive towards East if no receiver orientation provided", "value": true }, "mesh": { diff --git a/simpeg_drivers-assets/uijson/fdem_inversion.ui.json b/simpeg_drivers-assets/uijson/fdem_inversion.ui.json index 173677c7..d686c267 100644 --- a/simpeg_drivers-assets/uijson/fdem_inversion.ui.json +++ b/simpeg_drivers-assets/uijson/fdem_inversion.ui.json @@ -42,7 +42,8 @@ "group": "Data", "dataGroupType": "Multi-element", "main": true, - "label": "z-imag component", + "label": "Vertical (imaginary)", + "tooltip": "Vertical (w) imaginary component of the magnetic data.\\Positive up along the z-axis if no receiver orientation provided", "parent": "data_object", "optional": true, "enabled": true, @@ -60,7 +61,7 @@ "label": "Uncertainty", "parent": "data_object", "dependency": "z_imag_channel", - "dependencyType": "enabled", + "dependencyType": "show", "value": "" }, "z_real_channel": { @@ -72,7 +73,8 @@ "group": "Data", "dataGroupType": "Multi-element", "main": true, - "label": "z-real component", + "label": "Vertical (real)", + "tooltip": "Vertical (w) real component of the magnetic data.\\Positive up along the z-axis if no receiver orientation provided", "parent": "data_object", "optional": true, "enabled": true, @@ -90,10 +92,10 @@ "label": "Uncertainty", "parent": "data_object", "dependency": "z_real_channel", - "dependencyType": "enabled", + "dependencyType": "show", "value": "" }, - "inline_imag_channel": { + "x_imag_channel": { "association": [ "Cell", "Vertex" @@ -102,13 +104,14 @@ "group": "Data", "dataGroupType": "Multi-element", "main": true, - "label": "In-line imag component", + "label": "In-line (imaginary)", + "tooltip": "In-line (u) imaginary component of the magnetic data.\\Positive towards North if no receiver orientation provided", "parent": "data_object", "optional": true, - "enabled": true, + "enabled": false, "value": "" }, - "inline_imag_uncertainty": { + "x_imag_uncertainty": { "association": [ "Cell", "Vertex" @@ -119,11 +122,11 @@ "main": true, "label": "Uncertainty", "parent": "data_object", - "dependency": "inline_imag_channel", - "dependencyType": "enabled", + "dependency": "x_imag_channel", + "dependencyType": "show", "value": "" }, - "inline_real_channel": { + "x_real_channel": { "association": [ "Cell", "Vertex" @@ -132,13 +135,14 @@ "group": "Data", "dataGroupType": "Multi-element", "main": true, - "label": "In-line real component", + "label": "In-line (real)", + "tooltip": "In-line (u) real component of the magnetic data.\\Positive towards North if no receiver orientation provided", "parent": "data_object", "optional": true, - "enabled": true, + "enabled": false, "value": "" }, - "inline_real_uncertainty": { + "x_real_uncertainty": { "association": [ "Cell", "Vertex" @@ -149,11 +153,11 @@ "main": true, "label": "Uncertainty", "parent": "data_object", - "dependency": "inline_real_channel", - "dependencyType": "enabled", + "dependency": "x_real_channel", + "dependencyType": "show", "value": "" }, - "crossline_imag_channel": { + "y_imag_channel": { "association": [ "Cell", "Vertex" @@ -162,14 +166,14 @@ "group": "Data", "dataGroupType": "Multi-element", "main": true, - "label": "Cross-line imag component", - "tooltip": "Horizontal imaginary component of the magnetic data, perpendicular to the survey direction", + "label": "Cross-line (imaginary)", + "tooltip": "Cross-line (v) imaginary component of the magnetic data.\\Positive towards East if no receiver orientation provided", "parent": "data_object", "optional": true, - "enabled": true, + "enabled": false, "value": "" }, - "crossline_imag_uncertainty": { + "y_imag_uncertainty": { "association": [ "Cell", "Vertex" @@ -180,11 +184,11 @@ "main": true, "label": "Uncertainty", "parent": "data_object", - "dependency": "crossline_imag_channel", - "dependencyType": "enabled", + "dependency": "y_imag_channel", + "dependencyType": "show", "value": "" }, - "crossline_real_channel": { + "y_real_channel": { "association": [ "Cell", "Vertex" @@ -193,13 +197,14 @@ "group": "Data", "dataGroupType": "Multi-element", "main": true, - "label": "Horizontal real component of the magnetic data, parallel to the survey direction", + "label": "Cross-line (real)", + "tooltip": "Cross-line (v) real component of the magnetic data.\\Positive towards East if no receiver orientation provided", "parent": "data_object", "optional": true, - "enabled": true, + "enabled": false, "value": "" }, - "crossline_real_uncertainty": { + "y_real_uncertainty": { "association": [ "Cell", "Vertex" @@ -210,8 +215,8 @@ "main": true, "label": "Uncertainty", "parent": "data_object", - "dependency": "crossline_real_channel", - "dependencyType": "enabled", + "dependency": "y_real_channel", + "dependencyType": "show", "value": "" }, "mesh": { diff --git a/simpeg_drivers/electromagnetics/frequency_domain/options.py b/simpeg_drivers/electromagnetics/frequency_domain/options.py index 829e2008..2c5cfe9d 100644 --- a/simpeg_drivers/electromagnetics/frequency_domain/options.py +++ b/simpeg_drivers/electromagnetics/frequency_domain/options.py @@ -22,7 +22,7 @@ LargeLoopGroundFEMReceivers, MovingLoopGroundFEMReceivers, ) -from pydantic import field_validator +from pydantic import AliasChoices, Field, field_validator from simpeg_drivers import assets_path from simpeg_drivers.options import ( @@ -85,9 +85,14 @@ class FDEMForwardOptions(BaseForwardOptions, BaseFDEMOptions): """ Frequency Domain Electromagnetic Forward options. - :param z_real_channel_bool: Real impedance channel boolean. - :param z_imag_channel_bool: Imaginary impedance channel boolean. - :param model_type: Specify whether the models are provided in resistivity or conductivity. + :param receivers_orientation: Orientation of the receivers provided as a group. + :param z_real_channel_bool: Vertical (real) component of impedance channel boolean. + :param z_imag_channel_bool: Vertical (imaginary) component of impedance channel boolean. + :param x_real_channel_bool: In-line (real) component of impedance channel boolean. + :param x_imag_channel_bool: In-line (imaginary) component of impedance channel boolean. + :param y_real_channel_bool: Cross-line (real) component of impedance channel boolean. + :param y_imag_channel_bool: Cross-line (imaginary) component of impedance channel + :param models: Specify whether the models are provided in resistivity or conductivity. """ name: ClassVar[str] = "Frequency Domain Electromagnetics Forward" @@ -102,12 +107,12 @@ class FDEMForwardOptions(BaseForwardOptions, BaseFDEMOptions): | AirborneFEMReceivers ) receivers_orientation: PropertyGroup | None = None - z_real_channel_bool: bool - z_imag_channel_bool: bool - inline_real_channel_bool: bool - inline_imag_channel_bool: bool - crossline_real_channel_bool: bool - crossline_imag_channel_bool: bool + z_real_channel_bool: bool = False + z_imag_channel_bool: bool = False + x_real_channel_bool: bool = False + x_imag_channel_bool: bool = False + y_real_channel_bool: bool = False + y_imag_channel_bool: bool = False models: ConductivityModelOptions @@ -115,11 +120,19 @@ class FDEMInversionOptions(BaseFDEMOptions, BaseInversionOptions): """ Frequency Domain Electromagnetic Inversion options. - :param z_real_channel: Real impedance channel. - :param z_real_uncertainty: Real impedance uncertainty channel. - :param z_imag_channel: Imaginary impedance channel. - :param z_imag_uncertainty: Imaginary impedance uncertainty channel. - :param model_type: Specify whether the models are provided in resistivity or conductivity. + :param z_real_channel: Vertical (real) impedance channel. + :param z_real_uncertainty: Vertical (real) impedance uncertainty channel. + :param z_imag_channel: Vertical (imaginary) impedance channel. + :param z_imag_uncertainty: Vertical (imaginary) impedance uncertainty channel. + :param x_real_channel: In-line (real) impedance channel. + :param x_real_uncertainty: In-line (real) impedance uncertainty channel. + :param x_imag_channel: In-line (imaginary) impedance channel. + :param x_imag_uncertainty: In-line (imaginary) impedance uncertainty channel + :param y_real_channel: Cross-line (real) impedance channel. + :param y_real_uncertainty: Cross-line (real) impedance uncertainty channel. + :param y_imag_channel: Cross-line (imaginary) impedance channel. + :param y_imag_uncertainty: Cross-line (imaginary) impedance uncertainty channel + :param models: Specify whether the models are provided in resistivity or conductivity. """ name: ClassVar[str] = "Frequency Domain Electromagnetics Inversion" @@ -138,14 +151,14 @@ class FDEMInversionOptions(BaseFDEMOptions, BaseInversionOptions): z_real_uncertainty: PropertyGroup | None = None z_imag_channel: PropertyGroup | None = None z_imag_uncertainty: PropertyGroup | None = None - inline_real_channel: PropertyGroup | None = None - inline_real_uncertainty: PropertyGroup | None = None - inline_imag_channel: PropertyGroup | None = None - inline_imag_uncertainty: PropertyGroup | None = None - crossline_real_channel: PropertyGroup | None = None - crossline_real_uncertainty: PropertyGroup | None = None - crossline_imag_channel: PropertyGroup | None = None - crossline_imag_uncertainty: PropertyGroup | None = None + x_real_channel: PropertyGroup | None = None + x_real_uncertainty: PropertyGroup | None = None + x_imag_channel: PropertyGroup | None = None + x_imag_uncertainty: PropertyGroup | None = None + y_real_channel: PropertyGroup | None = None + y_real_uncertainty: PropertyGroup | None = None + y_imag_channel: PropertyGroup | None = None + y_imag_uncertainty: PropertyGroup | None = None models: ConductivityModelOptions From e4f97f7cb2b1d611ea29a460416c1e55e37db7da Mon Sep 17 00:00:00 2001 From: dominiquef Date: Thu, 12 Mar 2026 15:40:28 -0700 Subject: [PATCH 05/17] Dive into receiver orientations. Revise inline to be Y, xline is X --- .../uijson/fdem_forward.ui.json | 8 +- .../uijson/fdem_inversion.ui.json | 24 +- .../components/factories/receiver_factory.py | 21 +- .../components/factories/survey_factory.py | 27 ++- simpeg_drivers/utils/synthetics/options.py | 1 + .../utils/synthetics/surveys/factory.py | 57 +++-- .../surveys/frequency_domain/fdem.py | 7 +- tests/run_tests/oriented_fem_receiver_test.py | 217 ++++++------------ 8 files changed, 173 insertions(+), 189 deletions(-) diff --git a/simpeg_drivers-assets/uijson/fdem_forward.ui.json b/simpeg_drivers-assets/uijson/fdem_forward.ui.json index 2510350c..ec3b84f7 100644 --- a/simpeg_drivers-assets/uijson/fdem_forward.ui.json +++ b/simpeg_drivers-assets/uijson/fdem_forward.ui.json @@ -47,28 +47,28 @@ "tooltip": "Vertical (w) real component of the magnetic data.\\Positive up along the z-axis if no receiver orientation provided", "value": true }, - "x_imag_channel_bool": { + "y_imag_channel_bool": { "group": "Survey", "main": true, "label": "In-line (imaginary)", "tooltip": "In-line (u) imaginary component of the magnetic data.\\Positive towards North if no receiver orientation provided", "value": true }, - "x_real_channel_bool": { + "y_real_channel_bool": { "group": "Survey", "main": true, "label": "In-line (real)", "tooltip": "In-line (u) real component of the magnetic data.\\Positive towards North if no receiver orientation provided", "value": true }, - "y_imag_channel_bool": { + "x_imag_channel_bool": { "group": "Survey", "main": true, "label": "Cross-line (imaginary)", "tooltip": "Cross-line (v) imaginary component of the magnetic data.\\Positive towards East if no receiver orientation provided", "value": true }, - "y_real_channel_bool": { + "x_real_channel_bool": { "group": "Survey", "main": true, "label": "Cross-line (real)", diff --git a/simpeg_drivers-assets/uijson/fdem_inversion.ui.json b/simpeg_drivers-assets/uijson/fdem_inversion.ui.json index d686c267..b6befcea 100644 --- a/simpeg_drivers-assets/uijson/fdem_inversion.ui.json +++ b/simpeg_drivers-assets/uijson/fdem_inversion.ui.json @@ -95,7 +95,7 @@ "dependencyType": "show", "value": "" }, - "x_imag_channel": { + "y_imag_channel": { "association": [ "Cell", "Vertex" @@ -111,7 +111,7 @@ "enabled": false, "value": "" }, - "x_imag_uncertainty": { + "y_imag_uncertainty": { "association": [ "Cell", "Vertex" @@ -122,11 +122,11 @@ "main": true, "label": "Uncertainty", "parent": "data_object", - "dependency": "x_imag_channel", + "dependency": "y_imag_channel", "dependencyType": "show", "value": "" }, - "x_real_channel": { + "y_real_channel": { "association": [ "Cell", "Vertex" @@ -142,7 +142,7 @@ "enabled": false, "value": "" }, - "x_real_uncertainty": { + "y_real_uncertainty": { "association": [ "Cell", "Vertex" @@ -153,11 +153,11 @@ "main": true, "label": "Uncertainty", "parent": "data_object", - "dependency": "x_real_channel", + "dependency": "y_real_channel", "dependencyType": "show", "value": "" }, - "y_imag_channel": { + "x_imag_channel": { "association": [ "Cell", "Vertex" @@ -173,7 +173,7 @@ "enabled": false, "value": "" }, - "y_imag_uncertainty": { + "x_imag_uncertainty": { "association": [ "Cell", "Vertex" @@ -184,11 +184,11 @@ "main": true, "label": "Uncertainty", "parent": "data_object", - "dependency": "y_imag_channel", + "dependency": "x_imag_channel", "dependencyType": "show", "value": "" }, - "y_real_channel": { + "x_real_channel": { "association": [ "Cell", "Vertex" @@ -204,7 +204,7 @@ "enabled": false, "value": "" }, - "y_real_uncertainty": { + "x_real_uncertainty": { "association": [ "Cell", "Vertex" @@ -215,7 +215,7 @@ "main": true, "label": "Uncertainty", "parent": "data_object", - "dependency": "y_real_channel", + "dependency": "x_real_channel", "dependencyType": "show", "value": "" }, diff --git a/simpeg_drivers/components/factories/receiver_factory.py b/simpeg_drivers/components/factories/receiver_factory.py index 596a1035..c150be9c 100644 --- a/simpeg_drivers/components/factories/receiver_factory.py +++ b/simpeg_drivers/components/factories/receiver_factory.py @@ -23,7 +23,6 @@ from simpeg_drivers.options import BaseOptions import numpy as np -from geoapps_utils.utils.transformations import rotate_xyz from simpeg_drivers.components.factories.simpeg_factory import SimPEGFactory @@ -94,7 +93,12 @@ def concrete_object(self): return receivers.ApparentConductivity def assemble_arguments( - self, locations=None, data=None, local_index=None, component=None + self, + locations=None, + data=None, + local_index=None, + component=None, + orientation=None, ): """Provides implementations to assemble arguments for receivers object.""" @@ -128,7 +132,12 @@ def assemble_arguments( return args def assemble_keyword_arguments( - self, locations=None, data=None, local_index=None, component=None + self, + locations=None, + data=None, + local_index=None, + component=None, + orientation=None, ): """Provides implementations to assemble keyword arguments for receivers object.""" kwargs = {} @@ -141,14 +150,20 @@ def assemble_keyword_arguments( comp = component.split("_")[0] kwargs["orientation"] = comp[0] if "fdem" in self.factory_type else comp[1:] kwargs["component"] = component.split("_")[1] + if self.factory_type in ["tipper"]: kwargs["orientation"] = kwargs["orientation"][::-1] + if "tdem" in self.factory_type: kwargs["orientation"] = component if self.factory_type == "fdem 1d": kwargs["data_type"] = "ppm" + # Overload orientation if provided + if self.factory_type in ["tdem", "fdem"] and orientation is not None: + kwargs["orientation"] = orientation + return kwargs def _dcip_arguments(self, locations=None, local_index=None): diff --git a/simpeg_drivers/components/factories/survey_factory.py b/simpeg_drivers/components/factories/survey_factory.py index 052a5a41..02f0090a 100644 --- a/simpeg_drivers/components/factories/survey_factory.py +++ b/simpeg_drivers/components/factories/survey_factory.py @@ -13,7 +13,6 @@ from __future__ import annotations -from gc import is_finalized from typing import TYPE_CHECKING @@ -25,6 +24,7 @@ import numpy as np import simpeg.electromagnetics.time_domain as tdem from geoapps_utils.utils.importing import GeoAppsError +from geoapps_utils.utils.transformations import x_rotation_matrix, z_rotation_matrix from geoh5py.objects.surveys.electromagnetics.ground_tem import ( LargeLoopGroundTEMTransmitters, ) @@ -33,6 +33,14 @@ from simpeg_drivers.components.factories.receiver_factory import ReceiversFactory from simpeg_drivers.components.factories.simpeg_factory import SimPEGFactory from simpeg_drivers.components.factories.source_factory import SourcesFactory +from simpeg_drivers.utils.regularization import direction_and_dip + + +DEFAULT_ORIENTATIONS = { + "y": np.array([1.0, 0.0, 0.0]), + "x": np.array([0.0, 1.0, 0.0]), + "z": np.array([0.0, 0.0, 1.0]), +} class SurveyFactory(SimPEGFactory): @@ -51,6 +59,8 @@ def __init__(self, params: BaseParams | BaseOptions): self.ordering = None self.sorting = None + self.orientation = self.validate_orientation() + def concrete_object(self): if self.factory_type in ["magnetic vector", "magnetic scalar"]: from simpeg.potential_fields.magnetics import survey @@ -330,13 +340,16 @@ def _fem_arguments(self, data=None): tx_factory = SourcesFactory(self.params) receiver_groups = [] block_ordering = [] - for rx_id, locs in enumerate(rx_locs): + for rx_id, (locs, orientation) in enumerate( + zip(rx_locs, self.orientation, strict=True) + ): receivers = [] for comp_id, component in enumerate(data.components): receiver = rx_factory.build( locations=locs, data=data, component=component, + orientation=orientation, ) block_ordering.append([comp_id, rx_id]) receivers.append(receiver) @@ -427,3 +440,13 @@ def _naturalsource_arguments(self, data=None): self.ordering = np.vstack(ordering).astype(int) return [sources] + + def validate_orientation(self): + """ + Validate the various options for the orientation parameter and + return an orientation array of shape (n_receivers, 3) for use in SimPEG receivers. + """ + if self.params.receivers_orientation is not None: + return direction_and_dip(self.params.receivers_orientation) + + return np.zeros((self.params.data_object.n_vertices, 3)) diff --git a/simpeg_drivers/utils/synthetics/options.py b/simpeg_drivers/utils/synthetics/options.py index 87fd1da0..d1d46b7e 100644 --- a/simpeg_drivers/utils/synthetics/options.py +++ b/simpeg_drivers/utils/synthetics/options.py @@ -22,6 +22,7 @@ class SurveyOptions(BaseModel): drape: float = 0.0 n_stations: int = 20 n_lines: int = 5 + rotation: float = 0.0 topography: Callable = lambda x, y: gaussian(x, y, amplitude=50.0, width=100.0) name: str = "survey" diff --git a/simpeg_drivers/utils/synthetics/surveys/factory.py b/simpeg_drivers/utils/synthetics/surveys/factory.py index 83a8a945..ef86c809 100644 --- a/simpeg_drivers/utils/synthetics/surveys/factory.py +++ b/simpeg_drivers/utils/synthetics/surveys/factory.py @@ -11,6 +11,7 @@ from collections.abc import Callable import numpy as np +from geoapps_utils.utils.transformations import rotate_xyz from geoh5py import Workspace from geoh5py.objects import ObjectBase, Points @@ -30,22 +31,28 @@ def grid_layout( n_stations: int, n_lines: int, topography: Callable, -): + center: tuple[float, float] = (0.0, 0.0), + rotation: float = 0.0, +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: """ Generates grid locations based on limits and spacing. - - :param limits: Tuple of (xmin, xmax, ymin, ymax). - :param n_stations: Number of stations along each line. - :param n_lines: Number of lines in the grid. - :param topography: Callable that generates the topography (z values). """ x = np.linspace(limits[0], limits[1], n_stations) y = np.linspace(limits[2], limits[3], n_lines) - X, Y = np.meshgrid(x, y) - Z = topography(X, Y) + grid_x, grid_y = np.meshgrid(x, y) + + xy_locs = rotate_xyz( + np.c_[grid_x.flatten(), grid_y.flatten()], list(center), rotation + ) - return X, Y, Z + z = topography(xy_locs[:, 0], xy_locs[:, 1]) + + return ( + xy_locs[:, 0].reshape(grid_x.shape), + xy_locs[:, 1].reshape(grid_y.shape), + z.reshape(grid_x.shape), + ) def get_survey( @@ -61,37 +68,49 @@ def get_survey( :param options: Survey options. """ - X, Y, Z = grid_layout( + grid_x, grid_y, grid_z = grid_layout( limits=options.limits, n_stations=options.n_stations, n_lines=options.n_lines, topography=options.topography, + center=options.center, + rotation=options.rotation, ) - Z += options.drape + grid_z += options.drape if "current" in method or "polarization" in method: - return generate_dc_survey(geoh5, X, Y, Z, name=options.name) + return generate_dc_survey(geoh5, grid_x, grid_y, grid_z, name=options.name) if "magnetotellurics" in method: - return generate_magnetotellurics_survey(geoh5, X, Y, Z, name=options.name) + return generate_magnetotellurics_survey( + geoh5, grid_x, grid_y, grid_z, name=options.name + ) if "tipper" in method: - return generate_tipper_survey(geoh5, X, Y, Z, name=options.name) + return generate_tipper_survey(geoh5, grid_x, grid_y, grid_z, name=options.name) if "apparent conductivity" in method: - return generate_apparent_conductivity_survey(geoh5, X, Y, Z, name=options.name) + return generate_apparent_conductivity_survey( + geoh5, grid_x, grid_y, grid_z, name=options.name + ) if method in ["fdem", "fem", "fdem 1d"]: - return generate_fdem_survey(geoh5, X, Y, Z, name=options.name) + return generate_fdem_survey(geoh5, grid_x, grid_y, grid_z, name=options.name) if "tdem" in method: if "airborne" in method: - return generate_airborne_tdem_survey(geoh5, X, Y, Z, name=options.name) + return generate_airborne_tdem_survey( + geoh5, grid_x, grid_y, grid_z, name=options.name + ) else: - return generate_tdem_survey(geoh5, X, Y, 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([X.flatten(), Y.flatten(), Z.flatten()]), + vertices=np.column_stack( + [grid_x.flatten(), grid_y.flatten(), grid_z.flatten()] + ), name=options.name, ) diff --git a/simpeg_drivers/utils/synthetics/surveys/frequency_domain/fdem.py b/simpeg_drivers/utils/synthetics/surveys/frequency_domain/fdem.py index c1eaf1c5..87381d53 100644 --- a/simpeg_drivers/utils/synthetics/surveys/frequency_domain/fdem.py +++ b/simpeg_drivers/utils/synthetics/surveys/frequency_domain/fdem.py @@ -34,8 +34,11 @@ def generate_fdem_survey( tx_locs_list = [] frequency_list = [] for config in frequency_config: - tx_vertices = vertices.copy() - tx_vertices[:, 0] -= config["Offset"] + delta = np.diff(vertices, axis=0) + delta /= np.linalg.norm(delta, axis=1)[:, None] + delta = np.vstack([delta, delta[-1, :]]) # Repeat last offset + + tx_vertices = vertices - delta * config["Offset"] tx_locs_list.append(tx_vertices) frequency_list.append([[config["Frequency"]] * len(vertices)]) tx_locs = np.vstack(tx_locs_list) diff --git a/tests/run_tests/oriented_fem_receiver_test.py b/tests/run_tests/oriented_fem_receiver_test.py index 4322ec46..e985475a 100644 --- a/tests/run_tests/oriented_fem_receiver_test.py +++ b/tests/run_tests/oriented_fem_receiver_test.py @@ -16,16 +16,13 @@ import numpy as np from geoapps_utils.modelling.plates import PlateModel -from geoh5py import Workspace -from geoh5py.groups import SimPEGGroup +from geoh5py.groups import PropertyGroup from simpeg_drivers.electromagnetics.frequency_domain.driver import ( FDEMForwardDriver, - FDEMInversionDriver, ) from simpeg_drivers.electromagnetics.frequency_domain.options import ( FDEMForwardOptions, - FDEMInversionOptions, ) from simpeg_drivers.utils.synthetics.driver import ( SyntheticsComponents, @@ -36,7 +33,7 @@ SurveyOptions, SyntheticsComponentsOptions, ) -from tests.utils.targets import check_target, get_inversion_output, get_workspace +from tests.utils.targets import get_workspace # To test the full run and validate the inversion. @@ -45,28 +42,41 @@ target_run = {"data_norm": 91.18814842528005, "phi_d": 4250, "phi_m": 968} -def test_fem_name_change(tmp_path): - # Run the forward +def test_fem_fwr_run( + tmp_path: Path, + refinement=(4,), + cell_size=(10.0, 10.0, 10.0), +): + # Run the forward east-west opts = SyntheticsComponentsOptions( method="fdem", - survey=SurveyOptions(n_stations=2, n_lines=2, drape=15.0), - mesh=MeshOptions(refinement=(2,), padding_distance=400.0), + survey=SurveyOptions( + height=0.0, + n_stations=16, + n_lines=1, + drape=15.0, + rotation=0, + topography=lambda x, y: np.zeros(x.shape), + name="survey - EW", + ), + mesh=MeshOptions( + cell_size=cell_size, refinement=refinement, padding_distance=400.0 + ), model=ModelOptions( background=1e-3, plate=PlateModel( - strike_length=500.0, - dip_length=150.0, + strike_length=100.0, + dip_length=100.0, width=20.0, - origin=(0.0, 0.0, -10.0), - direction=60.0, - dip=70.0, + origin=(0.0, 0.0, -40.0), + direction=90.0, + dip=45.0, ), ), ) with get_workspace(tmp_path / "inversion_test.ui.geoh5") as geoh5: components = SyntheticsComponents(geoh5, options=opts) - - FDEMForwardOptions.build( + params = FDEMForwardOptions.build( geoh5=geoh5, mesh=components.mesh, topography_object=components.topography, @@ -74,164 +84,77 @@ def test_fem_name_change(tmp_path): starting_model=components.model, z_real_channel_bool=True, z_imag_channel_bool=True, - inversion_type="fdem", + x_real_channel_bool=True, + x_imag_channel_bool=True, + y_real_channel_bool=True, + y_imag_channel_bool=True, ) + fwr_driver = FDEMForwardDriver(params) + fwr_driver.run() -def test_fem_fwr_run( - tmp_path: Path, - n_grid_points=3, - refinement=(2,), - cell_size=(20.0, 20.0, 20.0), -): - # Run the forward + # Repeat at 45 azimuth opts = SyntheticsComponentsOptions( method="fdem", survey=SurveyOptions( - n_stations=n_grid_points, - n_lines=n_grid_points, + height=0.0, + n_stations=16, + n_lines=1, drape=15.0, + rotation=45, topography=lambda x, y: np.zeros(x.shape), + name="survey - ROT 45", ), mesh=MeshOptions( - cell_size=cell_size, refinement=refinement, padding_distance=400.0 + cell_size=cell_size, + refinement=refinement, + padding_distance=400.0, + name="mesh - ROT 45", ), model=ModelOptions( background=1e-3, plate=PlateModel( - strike_length=40.0, - dip_length=40.0, - width=40.0, - origin=(0.0, 0.0, -50.0), + strike_length=100.0, + dip_length=100.0, + width=20.0, + origin=(0.0, 0.0, -40.0), + direction=45.0, + dip=45.0, ), + name="model - ROT 45", ), ) - with get_workspace(tmp_path / "inversion_test.ui.geoh5") as geoh5: + with geoh5.open(): components = SyntheticsComponents(geoh5, options=opts) + mesh = components.mesh + # Create property group with orientation + dip = np.ones(mesh.n_cells) * 0 + azimuth = np.ones(mesh.n_cells) * 45 + data_list = mesh.add_data( + { + "azimuth": {"values": azimuth}, + "dip": {"values": dip}, + } + ) + pg = PropertyGroup( + mesh, properties=data_list, property_group_type="Dip direction & dip" + ) + params = FDEMForwardOptions.build( geoh5=geoh5, mesh=components.mesh, topography_object=components.topography, data_object=components.survey, starting_model=components.model, + title="FDEM Forward Run 45", z_real_channel_bool=True, z_imag_channel_bool=True, + x_real_channel_bool=True, + x_imag_channel_bool=True, + y_real_channel_bool=True, + y_imag_channel_bool=True, + receivers_orientation=pg, ) fwr_driver = FDEMForwardDriver(params) fwr_driver.run() - - -def test_fem_run(tmp_path: Path, max_iterations=1, pytest=True): - workpath = tmp_path / "inversion_test.ui.geoh5" - if pytest: - workpath = tmp_path.parent / "test_fem_fwr_run0" / "inversion_test.ui.geoh5" - - with Workspace(workpath) as geoh5: - components = SyntheticsComponents(geoh5) - data = {} - uncertainties = {} - channels = { - "z_real": "z_real", - "z_imag": "z_imag", - } - - for chan, cname in channels.items(): - data[cname] = [] - uncertainties[f"{cname} uncertainties"] = [] - for ind, freq in enumerate(components.survey.channels): - data_entity = geoh5.get_entity(f"Iteration_0_{chan}_[{ind}]")[0].copy( - parent=components.survey - ) - data[cname].append(data_entity) - abs_val = np.abs(data_entity.values) - uncert = components.survey.add_data( - { - f"uncertainty_{chan}_[{ind}]": { - "values": np.ones_like(abs_val) - * freq - / 200.0 # * 2**(np.abs(ind-1)) - } - } - ) - uncertainties[f"{cname} uncertainties"].append( - uncert.copy(parent=components.survey) - ) - - data_groups = components.survey.add_components_data(data) - uncert_groups = components.survey.add_components_data(uncertainties) - - data_kwargs = {} - for chan, data_group, uncert_group in zip( - channels, data_groups, uncert_groups, strict=True - ): - data_kwargs[f"{chan}_channel"] = data_group - data_kwargs[f"{chan}_uncertainty"] = uncert_group - - orig_z_real_1 = geoh5.get_entity("Iteration_0_z_real_[0]")[0].values - - # Run the inverse - params = FDEMInversionOptions.build( - geoh5=geoh5, - mesh=components.mesh, - topography_object=components.topography, - data_object=components.survey, - starting_model=1e-3, - reference_model=1e-3, - alpha_s=0.0, - s_norm=0.0, - x_norm=0.0, - y_norm=0.0, - z_norm=0.0, - upper_bound=0.75, - max_global_iterations=max_iterations, - initial_beta_ratio=1e1, - percentile=100, - cooling_rate=1, - chi_factor=0.25, - auto_scale_channels=True, - tile_spatial=2, - **data_kwargs, - ) - params.write_ui_json(path=tmp_path / "Inv_run.ui.json") - driver = FDEMInversionDriver(params) - driver.run() - - # Scaling is done evenly on channels - np.testing.assert_allclose( - driver.data_misfit.multipliers, - [1.0, 1.0, 0.6004, 0.6004, 0.5047, 0.5047], - atol=1e-3, - ) - - with geoh5.open() as run_ws: - output = get_inversion_output( - driver.params.geoh5.h5file, driver.params.out_group.uid - ) - output["data"] = orig_z_real_1 - - assert ( - run_ws.get_entity("Iteration_1_z_imag_[1]")[0].entity_type.uid - == run_ws.get_entity("Observed_z_imag_[1]")[0].entity_type.uid - ) - - 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_fem_fwr_run( - Path("./"), - n_grid_points=5, - cell_size=(5.0, 5.0, 5.0), - refinement=(4, 4, 4), - ) - test_fem_run( - Path("./"), - max_iterations=15, - pytest=False, - ) From 4935ab4b6494e687c8c52a1a8eccee32945808b9 Mon Sep 17 00:00:00 2001 From: dominiquef Date: Fri, 13 Mar 2026 10:42:03 -0700 Subject: [PATCH 06/17] Move rotation of orientation under ReceiverFactory. Full run --- .../components/factories/receiver_factory.py | 51 ++++++++++++++----- .../components/factories/survey_factory.py | 38 ++------------ 2 files changed, 43 insertions(+), 46 deletions(-) diff --git a/simpeg_drivers/components/factories/receiver_factory.py b/simpeg_drivers/components/factories/receiver_factory.py index c150be9c..00b751d1 100644 --- a/simpeg_drivers/components/factories/receiver_factory.py +++ b/simpeg_drivers/components/factories/receiver_factory.py @@ -23,8 +23,10 @@ from simpeg_drivers.options import BaseOptions import numpy as np +from geoapps_utils.utils.transformations import x_rotation_matrix, z_rotation_matrix from simpeg_drivers.components.factories.simpeg_factory import SimPEGFactory +from simpeg_drivers.utils.regularization import direction_and_dip, get_cell_normals class ReceiversFactory(SimPEGFactory): @@ -37,6 +39,7 @@ def __init__(self, params: BaseParams | BaseOptions): """ super().__init__(params) self.simpeg_object = self.concrete_object() + self.orientations = self.validate_orientations() def concrete_object(self): if self.factory_type in ["magnetic vector", "magnetic scalar"]: @@ -96,9 +99,8 @@ def assemble_arguments( self, locations=None, data=None, - local_index=None, + local_indices=None, component=None, - orientation=None, ): """Provides implementations to assemble arguments for receivers object.""" @@ -110,7 +112,7 @@ def assemble_arguments( ): args += self._dcip_arguments( locations=locations, - local_index=local_index, + local_indices=local_indices, ) elif self.factory_type in [ "apparent conductivity", @@ -135,9 +137,8 @@ def assemble_keyword_arguments( self, locations=None, data=None, - local_index=None, + local_indices=None, component=None, - orientation=None, ): """Provides implementations to assemble keyword arguments for receivers object.""" kwargs = {} @@ -161,18 +162,20 @@ def assemble_keyword_arguments( kwargs["data_type"] = "ppm" # Overload orientation if provided - if self.factory_type in ["tdem", "fdem"] and orientation is not None: - kwargs["orientation"] = orientation + if self.factory_type in ["tdem", "fdem"] and local_indices is not None: + kwargs["orientation"] = self.orientations[kwargs["orientation"]][ + local_indices, : + ] return kwargs - def _dcip_arguments(self, locations=None, local_index=None): + def _dcip_arguments(self, locations=None, local_indices=None): args = [] - local_index = np.vstack(local_index) + local_indices = np.vstack(local_indices) - args.append(locations[local_index[:, 0], :]) + args.append(locations[local_indices[:, 0], :]) - if np.all(local_index[:, 0] == local_index[:, 1]): + if np.all(local_indices[:, 0] == local_indices[:, 1]): if "direct current" in self.factory_type: from simpeg.electromagnetics.static.resistivity import receivers else: @@ -181,7 +184,7 @@ def _dcip_arguments(self, locations=None, local_index=None): ) self.simpeg_object = receivers.Pole else: - args.append(locations[local_index[:, 1], :]) + args.append(locations[local_indices[:, 1], :]) return args @@ -211,3 +214,27 @@ def _base_station_arguments(self, locations=None): # H-field on locations with base stations return locations, stations + + def validate_orientations(self): + """ + Validate the various options for the orientation parameter and + return an orientation array of shape (n_receivers, 3) for use in SimPEG receivers. + """ + n_recs = self.params.data_object.n_vertices + normals = { + comp: get_cell_normals(n_recs, comp, True, 3).reshape((-1, 3)) + for comp in "xyz" + } + + if getattr(self.params, "receivers_orientation", None): + azi_dip = np.deg2rad(direction_and_dip(self.params.receivers_orientation)) + orientations = {} + for axis in "xyz": + orientations[axis] = ( + z_rotation_matrix(azi_dip[:, 0]) + * (x_rotation_matrix(-azi_dip[:, 1]) * normals[axis].flatten()) + ).reshape((-1, 3)) + + return orientations + + return normals diff --git a/simpeg_drivers/components/factories/survey_factory.py b/simpeg_drivers/components/factories/survey_factory.py index 02f0090a..4496ceb2 100644 --- a/simpeg_drivers/components/factories/survey_factory.py +++ b/simpeg_drivers/components/factories/survey_factory.py @@ -24,7 +24,6 @@ import numpy as np import simpeg.electromagnetics.time_domain as tdem from geoapps_utils.utils.importing import GeoAppsError -from geoapps_utils.utils.transformations import x_rotation_matrix, z_rotation_matrix from geoh5py.objects.surveys.electromagnetics.ground_tem import ( LargeLoopGroundTEMTransmitters, ) @@ -33,14 +32,6 @@ from simpeg_drivers.components.factories.receiver_factory import ReceiversFactory from simpeg_drivers.components.factories.simpeg_factory import SimPEGFactory from simpeg_drivers.components.factories.source_factory import SourcesFactory -from simpeg_drivers.utils.regularization import direction_and_dip - - -DEFAULT_ORIENTATIONS = { - "y": np.array([1.0, 0.0, 0.0]), - "x": np.array([0.0, 1.0, 0.0]), - "z": np.array([0.0, 0.0, 1.0]), -} class SurveyFactory(SimPEGFactory): @@ -54,13 +45,9 @@ def __init__(self, params: BaseParams | BaseOptions): """ super().__init__(params) self.simpeg_object = self.concrete_object() - self.local_index = None - self.survey = None self.ordering = None self.sorting = None - self.orientation = self.validate_orientation() - def concrete_object(self): if self.factory_type in ["magnetic vector", "magnetic scalar"]: from simpeg.potential_fields.magnetics import survey @@ -197,7 +184,7 @@ def _dcip_arguments(self, data=None): sorting.append(receiver_indices) receivers = ReceiversFactory(self.params).build( locations=receiver_locations, - local_index=receiver_entity.cells[receiver_indices], + local_indices=receiver_entity.cells[receiver_indices], ) if receivers.nD == 0: @@ -302,9 +289,7 @@ def _tdem_arguments(self, data=None): for comp_id, component in enumerate(data.components): rx_obj = rx_factory.build( - locations=locs, - data=data, - component=component, + locations=locs, data=data, component=component, local_indices=rx_ids ) rx_list.append(rx_obj) n_times = len(receivers.channels) @@ -340,16 +325,11 @@ def _fem_arguments(self, data=None): tx_factory = SourcesFactory(self.params) receiver_groups = [] block_ordering = [] - for rx_id, (locs, orientation) in enumerate( - zip(rx_locs, self.orientation, strict=True) - ): + for rx_id, locs in enumerate(rx_locs): receivers = [] for comp_id, component in enumerate(data.components): receiver = rx_factory.build( - locations=locs, - data=data, - component=component, - orientation=orientation, + locations=locs, data=data, component=component, local_indices=rx_id ) block_ordering.append([comp_id, rx_id]) receivers.append(receiver) @@ -440,13 +420,3 @@ def _naturalsource_arguments(self, data=None): self.ordering = np.vstack(ordering).astype(int) return [sources] - - def validate_orientation(self): - """ - Validate the various options for the orientation parameter and - return an orientation array of shape (n_receivers, 3) for use in SimPEG receivers. - """ - if self.params.receivers_orientation is not None: - return direction_and_dip(self.params.receivers_orientation) - - return np.zeros((self.params.data_object.n_vertices, 3)) From 809d16536d43b64bc30bd8a32e20c47d26700509 Mon Sep 17 00:00:00 2001 From: dominiquef Date: Fri, 13 Mar 2026 14:40:12 -0700 Subject: [PATCH 07/17] Allow to refine plate. Full run test for group angles on FEM --- .../components/factories/receiver_factory.py | 2 +- simpeg_drivers/utils/synthetics/driver.py | 1 + .../utils/synthetics/meshes/factory.py | 3 + .../utils/synthetics/meshes/octrees.py | 28 +++- simpeg_drivers/utils/synthetics/options.py | 1 + tests/run_tests/oriented_fem_receiver_test.py | 134 +++++++++--------- 6 files changed, 99 insertions(+), 70 deletions(-) diff --git a/simpeg_drivers/components/factories/receiver_factory.py b/simpeg_drivers/components/factories/receiver_factory.py index 00b751d1..6e49bc57 100644 --- a/simpeg_drivers/components/factories/receiver_factory.py +++ b/simpeg_drivers/components/factories/receiver_factory.py @@ -231,7 +231,7 @@ def validate_orientations(self): orientations = {} for axis in "xyz": orientations[axis] = ( - z_rotation_matrix(azi_dip[:, 0]) + z_rotation_matrix(-azi_dip[:, 0]) * (x_rotation_matrix(-azi_dip[:, 1]) * normals[axis].flatten()) ).reshape((-1, 3)) diff --git a/simpeg_drivers/utils/synthetics/driver.py b/simpeg_drivers/utils/synthetics/driver.py index 9b6c848f..f116f2dd 100644 --- a/simpeg_drivers/utils/synthetics/driver.py +++ b/simpeg_drivers/utils/synthetics/driver.py @@ -80,6 +80,7 @@ def mesh(self): survey=self.survey, topography=self.topography, options=self.options.mesh, + plate=self.options.model.plate if self.options.refine_plate else None, ) self._mesh = entity return self._mesh diff --git a/simpeg_drivers/utils/synthetics/meshes/factory.py b/simpeg_drivers/utils/synthetics/meshes/factory.py index 8ee89802..b8439508 100644 --- a/simpeg_drivers/utils/synthetics/meshes/factory.py +++ b/simpeg_drivers/utils/synthetics/meshes/factory.py @@ -9,6 +9,7 @@ # ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' from discretize import TensorMesh, TreeMesh +from geoapps_utils.modelling.plates import PlateModel from geoh5py.objects import CellObject, DrapeModel, Octree, Points, Surface from simpeg_drivers.utils.synthetics.options import MeshOptions @@ -22,6 +23,7 @@ def get_mesh( survey: Points, topography: Surface, options: MeshOptions, + plate: PlateModel, ) -> DrapeModel | Octree: """Factory for mesh creation with behaviour modified by the provided method.""" @@ -40,5 +42,6 @@ def get_mesh( cell_size=options.cell_size, refinement=options.refinement, padding_distance=options.padding_distance, + plate=plate, name=options.name, ) diff --git a/simpeg_drivers/utils/synthetics/meshes/octrees.py b/simpeg_drivers/utils/synthetics/meshes/octrees.py index 795cf952..8945de57 100644 --- a/simpeg_drivers/utils/synthetics/meshes/octrees.py +++ b/simpeg_drivers/utils/synthetics/meshes/octrees.py @@ -11,10 +11,14 @@ import numpy as np from discretize import TreeMesh from discretize.utils import mesh_builder_xyz +from geoapps_utils.modelling.plates import PlateModel from geoh5py.objects import Octree, Points, Surface from grid_apps.octree_creation.driver import OctreeDriver from grid_apps.utils import treemesh_2_octree +from simpeg_drivers.plate_simulation.models.options import PlateOptions +from simpeg_drivers.plate_simulation.models.parametric import Plate + def get_base_octree( survey: Points, @@ -58,6 +62,7 @@ def get_octree_mesh( cell_size: tuple[float, float, float], refinement: tuple | list, padding_distance: float, + plate: PlateModel | None = None, name: str = "mesh", ) -> Octree: """Generate a survey centered mesh with topography and survey refinement. @@ -68,11 +73,12 @@ def get_octree_mesh( :param cell_size: Tuple defining the cell size in all directions. :param refinement: Tuple containing the number of cells to refine at each level around the topography. - :param padding: Distance to pad the mesh in all directions. + :param padding_distance: Distance to pad the mesh in all directions. + :param plate: Optional PlateModel object to refine the mesh around the plate. + :param name: Name of the Octree object to create in geoh5. Default is "mesh". :return entity: The geoh5py Octree object to store the results of computation in the shared cells of the computational mesh. - :return mesh: The discretize TreeMesh object for computations. """ mesh = get_base_octree(survey, topography, cell_size, (0, 0, 1), padding_distance) @@ -81,6 +87,24 @@ def get_octree_mesh( mesh, survey.vertices, levels=refinement, finalize=False ) + if plate is not None: + # TODO Consolidate PlateOptions and PlateModel into a single class to avoid this redundancy + plate_options = PlateOptions( + plate=1.0, # thickness + width=plate.width, + strike_length=plate.strike_length, + dip_length=plate.dip_length, + dip=plate.dip, + dip_direction=plate.direction, + elevation=0, + ) + center = list(plate.origin) + center[2] += plate.width # Unclear why offsetted vertically + plate = Plate(plate_options, center=center, workspace=survey.workspace) + mesh = OctreeDriver.refine_tree_from_surface( + mesh, plate.surface, levels=(4,), finalize=False + ) + mesh.finalize() entity = treemesh_2_octree(survey.workspace, mesh, name=name) diff --git a/simpeg_drivers/utils/synthetics/options.py b/simpeg_drivers/utils/synthetics/options.py index d1d46b7e..18ef2b81 100644 --- a/simpeg_drivers/utils/synthetics/options.py +++ b/simpeg_drivers/utils/synthetics/options.py @@ -63,6 +63,7 @@ class SyntheticsComponentsOptions(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) method: str = "gravity" + refine_plate: bool = False survey: SurveyOptions = SurveyOptions() mesh: MeshOptions = MeshOptions() model: ModelOptions = ModelOptions() diff --git a/tests/run_tests/oriented_fem_receiver_test.py b/tests/run_tests/oriented_fem_receiver_test.py index e985475a..a40522e5 100644 --- a/tests/run_tests/oriented_fem_receiver_test.py +++ b/tests/run_tests/oriented_fem_receiver_test.py @@ -15,8 +15,12 @@ from pathlib import Path import numpy as np +import pytest from geoapps_utils.modelling.plates import PlateModel -from geoh5py.groups import PropertyGroup +from geoh5py import Workspace +from geoh5py.groups import PropertyGroup, UIJsonGroup +from geoh5py.objects import AirborneFEMReceivers +from geoh5py.shared.utils import fetch_active_workspace from simpeg_drivers.electromagnetics.frequency_domain.driver import ( FDEMForwardDriver, @@ -42,22 +46,41 @@ target_run = {"data_norm": 91.18814842528005, "phi_d": 4250, "phi_m": 968} -def test_fem_fwr_run( - tmp_path: Path, - refinement=(4,), - cell_size=(10.0, 10.0, 10.0), -): +def collect_real_components(geoh5): + # Load results and validate + data_list = {} + with fetch_active_workspace(geoh5) as ws: + group = next(group for group in ws.groups if isinstance(group, UIJsonGroup)) + survey = next( + child for child in group.children if isinstance(child, AirborneFEMReceivers) + ) + for comp in "xyz": + data_group = survey.get_entity(f"Iteration_0_{comp}_real")[0] + data_list[comp] = np.vstack( + [survey.get_data(uid)[0].values for uid in data_group.properties] + ) + return data_list + + +@pytest.mark.parametrize("azimuth, dip", [(90, 0), (45, 0), (90, 90)]) +def test_fem_fwr_run(tmp_path: Path, azimuth, dip): + """ + Forward simulations with variable receiver orientations. + The results are not expected to be the same, but should be similar. + """ + refinement = (2, 4) + cell_size = (5.0, 5.0, 5.0) # Run the forward east-west opts = SyntheticsComponentsOptions( method="fdem", + refine_plate=True, survey=SurveyOptions( height=0.0, n_stations=16, n_lines=1, drape=15.0, - rotation=0, + rotation=90 - azimuth, topography=lambda x, y: np.zeros(x.shape), - name="survey - EW", ), mesh=MeshOptions( cell_size=cell_size, refinement=refinement, padding_distance=400.0 @@ -65,88 +88,40 @@ def test_fem_fwr_run( model=ModelOptions( background=1e-3, plate=PlateModel( - strike_length=100.0, + strike_length=70.0, dip_length=100.0, - width=20.0, - origin=(0.0, 0.0, -40.0), - direction=90.0, + width=10.0, + origin=(0.0, 0.0, -60.0), + direction=azimuth, dip=45.0, ), ), ) + with get_workspace(tmp_path / "inversion_test.ui.geoh5") as geoh5: components = SyntheticsComponents(geoh5, options=opts) - params = FDEMForwardOptions.build( - geoh5=geoh5, - mesh=components.mesh, - topography_object=components.topography, - data_object=components.survey, - starting_model=components.model, - z_real_channel_bool=True, - z_imag_channel_bool=True, - x_real_channel_bool=True, - x_imag_channel_bool=True, - y_real_channel_bool=True, - y_imag_channel_bool=True, - ) - - fwr_driver = FDEMForwardDriver(params) - fwr_driver.run() + survey = components.survey - # Repeat at 45 azimuth - opts = SyntheticsComponentsOptions( - method="fdem", - survey=SurveyOptions( - height=0.0, - n_stations=16, - n_lines=1, - drape=15.0, - rotation=45, - topography=lambda x, y: np.zeros(x.shape), - name="survey - ROT 45", - ), - mesh=MeshOptions( - cell_size=cell_size, - refinement=refinement, - padding_distance=400.0, - name="mesh - ROT 45", - ), - model=ModelOptions( - background=1e-3, - plate=PlateModel( - strike_length=100.0, - dip_length=100.0, - width=20.0, - origin=(0.0, 0.0, -40.0), - direction=45.0, - dip=45.0, - ), - name="model - ROT 45", - ), - ) - with geoh5.open(): - components = SyntheticsComponents(geoh5, options=opts) - mesh = components.mesh # Create property group with orientation - dip = np.ones(mesh.n_cells) * 0 - azimuth = np.ones(mesh.n_cells) * 45 - data_list = mesh.add_data( + dip = np.ones(survey.n_vertices) * dip + azimuth = np.ones(survey.n_vertices) * azimuth + data_list = survey.add_data( { "azimuth": {"values": azimuth}, "dip": {"values": dip}, } ) pg = PropertyGroup( - mesh, properties=data_list, property_group_type="Dip direction & dip" + survey, properties=data_list, property_group_type="Dip direction & dip" ) params = FDEMForwardOptions.build( geoh5=geoh5, + title="Forward: Azimuth {azimuth}, Dip {dip}", mesh=components.mesh, topography_object=components.topography, data_object=components.survey, starting_model=components.model, - title="FDEM Forward Run 45", z_real_channel_bool=True, z_imag_channel_bool=True, x_real_channel_bool=True, @@ -158,3 +133,28 @@ def test_fem_fwr_run( fwr_driver = FDEMForwardDriver(params) fwr_driver.run() + + +def test_validate_orientations(tmp_path: Path): + + with Workspace( + tmp_path / "../test_fem_fwr_run_90_0_0/inversion_test.ui.geoh5" + ) as geoh5: + sim_90_0 = collect_real_components(geoh5) + + with Workspace( + tmp_path / "../test_fem_fwr_run_45_0_0/inversion_test.ui.geoh5" + ) as geoh5: + sim_45_0 = collect_real_components(geoh5) + + # Components almost the same at 45 + assert np.mean((sim_90_0["y"] - sim_45_0["y"]) / sim_90_0["y"]) < 0.2 + + with Workspace( + tmp_path / "../test_fem_fwr_run_90_90_0/inversion_test.ui.geoh5" + ) as geoh5: + sim_90_90 = collect_real_components(geoh5) + + # 90 dip makes Y point down and Z east, so Y should be -Z, and Z should be Y + assert np.mean((sim_90_0["y"] - sim_90_90["z"]) / sim_90_0["y"]) < 0.2 + assert np.mean((sim_90_0["z"] + sim_90_90["y"]) / sim_90_0["z"]) < 0.2 From 08e76e38461130934b89cb767668a4080cf17287 Mon Sep 17 00:00:00 2001 From: dominiquef Date: Fri, 13 Mar 2026 15:24:59 -0700 Subject: [PATCH 08/17] Fix uisjon tooltips. Add test for airborne tem --- .../uijson/fdem_forward.ui.json | 12 +- .../uijson/fdem_inversion.ui.json | 12 +- .../uijson/tdem_forward.ui.json | 12 +- .../uijson/tdem_inversion.ui.json | 12 +- .../oriented_airborne_tem_receiver_test.py | 151 ++++++++++++++++++ 5 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 tests/run_tests/oriented_airborne_tem_receiver_test.py diff --git a/simpeg_drivers-assets/uijson/fdem_forward.ui.json b/simpeg_drivers-assets/uijson/fdem_forward.ui.json index ec3b84f7..29f41ec6 100644 --- a/simpeg_drivers-assets/uijson/fdem_forward.ui.json +++ b/simpeg_drivers-assets/uijson/fdem_forward.ui.json @@ -20,14 +20,22 @@ "value": "" }, "receivers_orientation": { - "group": "Data", + "group": "Survey", + "main": true, "association": "Vertex", "dataType": "Float", "dataGroupType": [ "Dip direction & dip", "3D vector" ], - "label": "Receivers orientation provided as either Dip & dip direction or 3D vector components", + "label": "Receivers orientation", + "tooltip": [ + "Receivers orientation provided as 'Dip direction & dip' data group. If not provided, it is assumes:
", + "
In-line component
Positive towards North (Y).
", + "
Cross-line component
Positive towards East (X).
", + "
Vertical component
Positive up (Z).
", + "
" + ], "optional": true, "enabled": false, "parent": "data_object", diff --git a/simpeg_drivers-assets/uijson/fdem_inversion.ui.json b/simpeg_drivers-assets/uijson/fdem_inversion.ui.json index b6befcea..6f9d833c 100644 --- a/simpeg_drivers-assets/uijson/fdem_inversion.ui.json +++ b/simpeg_drivers-assets/uijson/fdem_inversion.ui.json @@ -20,14 +20,22 @@ "value": "" }, "receivers_orientation": { - "group": "Data", + "group": "Survey", + "main": true, "association": "Vertex", "dataType": "Float", "dataGroupType": [ "Dip direction & dip", "3D vector" ], - "label": "Receivers orientation provided as either Dip & dip direction or 3D vector components", + "label": "Receivers orientation", + "tooltip": [ + "Receivers orientation provided as 'Dip direction & dip' data group. If not provided, it is assumes:
", + "
In-line component
Positive towards North (Y).
", + "
Cross-line component
Positive towards East (X).
", + "
Vertical component
Positive up (Z).
", + "
" + ], "optional": true, "enabled": false, "parent": "data_object", diff --git a/simpeg_drivers-assets/uijson/tdem_forward.ui.json b/simpeg_drivers-assets/uijson/tdem_forward.ui.json index e3fb74d0..6d7150d0 100644 --- a/simpeg_drivers-assets/uijson/tdem_forward.ui.json +++ b/simpeg_drivers-assets/uijson/tdem_forward.ui.json @@ -21,14 +21,22 @@ "value": "" }, "receivers_orientation": { - "group": "Data", + "group": "Survey", + "main": true, "association": "Vertex", "dataType": "Float", "dataGroupType": [ "Dip direction & dip", "3D vector" ], - "label": "Receivers orientation provided as either Dip & dip direction or 3D vector components", + "label": "Receivers orientation", + "tooltip": [ + "Receivers orientation provided as 'Dip direction & dip' data group. If not provided, it is assumes:
", + "
In-line component
Positive towards North (Y).
", + "
Cross-line component
Positive towards East (X).
", + "
Vertical component
Positive up (Z).
", + "
" + ], "optional": true, "enabled": false, "parent": "data_object", diff --git a/simpeg_drivers-assets/uijson/tdem_inversion.ui.json b/simpeg_drivers-assets/uijson/tdem_inversion.ui.json index b9285930..24383f12 100644 --- a/simpeg_drivers-assets/uijson/tdem_inversion.ui.json +++ b/simpeg_drivers-assets/uijson/tdem_inversion.ui.json @@ -21,14 +21,22 @@ "value": "" }, "receivers_orientation": { - "group": "Data", + "group": "Survey", + "main": true, "association": "Vertex", "dataType": "Float", "dataGroupType": [ "Dip direction & dip", "3D vector" ], - "label": "Receivers orientation provided as either Dip & dip direction or 3D vector components", + "label": "Receivers orientation", + "tooltip": [ + "Receivers orientation provided as 'Dip direction & dip' data group. If not provided, it is assumes:
", + "
In-line component
Positive towards North (Y).
", + "
Cross-line component
Positive towards East (X).
", + "
Vertical component
Positive up (Z).
", + "
" + ], "optional": true, "enabled": false, "parent": "data_object", diff --git a/tests/run_tests/oriented_airborne_tem_receiver_test.py b/tests/run_tests/oriented_airborne_tem_receiver_test.py new file mode 100644 index 00000000..bff27718 --- /dev/null +++ b/tests/run_tests/oriented_airborne_tem_receiver_test.py @@ -0,0 +1,151 @@ +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +# 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). ' +# ' +# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' + +# pylint: disable=too-many-locals + +from __future__ import annotations + +from pathlib import Path + +import numpy as np +import pytest +from geoapps_utils.modelling.plates import PlateModel +from geoh5py import Workspace +from geoh5py.groups import PropertyGroup, UIJsonGroup +from geoh5py.objects import AirborneTEMReceivers +from geoh5py.shared.utils import fetch_active_workspace + +from simpeg_drivers.electromagnetics.time_domain.driver import ( + TDEMForwardDriver, +) +from simpeg_drivers.electromagnetics.time_domain.options import ( + TDEMForwardOptions, +) +from simpeg_drivers.utils.synthetics.driver import ( + SyntheticsComponents, +) +from simpeg_drivers.utils.synthetics.options import ( + MeshOptions, + ModelOptions, + SurveyOptions, + SyntheticsComponentsOptions, +) +from tests.utils.targets import get_workspace + + +def collect_components(geoh5): + # Load results and validate + data_list = {} + with fetch_active_workspace(geoh5) as ws: + group = next(group for group in ws.groups if isinstance(group, UIJsonGroup)) + survey = next( + child for child in group.children if isinstance(child, AirborneTEMReceivers) + ) + for comp in "xyz": + data_group = survey.get_entity(f"Iteration_0_{comp}")[0] + data_list[comp] = np.vstack( + [survey.get_data(uid)[0].values for uid in data_group.properties] + ) + return data_list + + +@pytest.mark.parametrize("azimuth, dip", [(90, 0), (45, 0), (90, 90)]) +def test_tem_fwr_run(tmp_path: Path, azimuth, dip): + """ + Forward simulations with variable receiver orientations. + The results are not expected to be the same, but should be similar. + """ + refinement = (2, 4) + cell_size = (5.0, 5.0, 5.0) + # Run the forward east-west + opts = SyntheticsComponentsOptions( + method="airborne tdem", + refine_plate=True, + survey=SurveyOptions( + height=0.0, + n_stations=16, + n_lines=1, + drape=15.0, + rotation=90 - azimuth, + topography=lambda x, y: np.zeros(x.shape), + ), + mesh=MeshOptions( + cell_size=cell_size, refinement=refinement, padding_distance=400.0 + ), + model=ModelOptions( + background=1e-3, + plate=PlateModel( + strike_length=70.0, + dip_length=100.0, + width=10.0, + origin=(0.0, 0.0, -60.0), + direction=azimuth, + dip=45.0, + ), + ), + ) + + with get_workspace(tmp_path / "inversion_test.ui.geoh5") as geoh5: + components = SyntheticsComponents(geoh5, options=opts) + survey = components.survey + + # Create property group with orientation + dip = np.ones(survey.n_vertices) * dip + azimuth = np.ones(survey.n_vertices) * azimuth + data_list = survey.add_data( + { + "azimuth": {"values": azimuth}, + "dip": {"values": dip}, + } + ) + pg = PropertyGroup( + survey, properties=data_list, property_group_type="Dip direction & dip" + ) + + params = TDEMForwardOptions.build( + geoh5=geoh5, + title=f"Forward: Azimuth {azimuth}, Dip {dip}", + mesh=components.mesh, + topography_object=components.topography, + data_object=components.survey, + starting_model=components.model, + z_channel_bool=True, + x_channel_bool=True, + y_channel_bool=True, + receivers_orientation=pg, + ) + + fwr_driver = TDEMForwardDriver(params) + fwr_driver.run() + + +def test_validate_orientations(tmp_path: Path): + + with Workspace( + tmp_path / "../test_tem_fwr_run_90_0_0/inversion_test.ui.geoh5" + ) as geoh5: + sim_90_0 = collect_components(geoh5) + + with Workspace( + tmp_path / "../test_tem_fwr_run_45_0_0/inversion_test.ui.geoh5" + ) as geoh5: + sim_45_0 = collect_components(geoh5) + + # Components almost the same at 45 + assert np.mean((sim_90_0["y"] - sim_45_0["y"]) / sim_90_0["y"]) < 0.3 + + with Workspace( + tmp_path / "../test_tem_fwr_run_90_90_0/inversion_test.ui.geoh5" + ) as geoh5: + sim_90_90 = collect_components(geoh5) + + # 90 dip makes Y point down and Z east, so Y should be -Z, and Z should be Y + assert np.mean((sim_90_0["y"] - sim_90_90["z"]) / sim_90_0["y"]) < 0.1 + assert np.mean((sim_90_0["z"] + sim_90_90["y"]) / sim_90_0["z"]) < 0.1 From 7a2d611ec157c9c532078bec2ed2e8c6f375776e Mon Sep 17 00:00:00 2001 From: domfournier Date: Mon, 16 Mar 2026 14:42:06 -0700 Subject: [PATCH 09/17] Improve synthetic for fem --- .../surveys/frequency_domain/fdem.py | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/simpeg_drivers/utils/synthetics/surveys/frequency_domain/fdem.py b/simpeg_drivers/utils/synthetics/surveys/frequency_domain/fdem.py index 87381d53..61510cc4 100644 --- a/simpeg_drivers/utils/synthetics/surveys/frequency_domain/fdem.py +++ b/simpeg_drivers/utils/synthetics/surveys/frequency_domain/fdem.py @@ -22,25 +22,38 @@ def generate_fdem_survey( - geoh5: Workspace, X: np.ndarray, Y: np.ndarray, Z: np.ndarray, name: str = "survey" + geoh5: Workspace, x_grid: np.ndarray, y_grid: np.ndarray, z_grid: np.ndarray, name: str = "survey" ) -> AirborneFEMReceivers: """Create an FDEM survey object from survey grid locations.""" - vertices = np.column_stack([X.flatten(), Y.flatten(), Z.flatten()]) - survey = AirborneFEMReceivers.create(geoh5, vertices=vertices, name=name) + vertices = np.column_stack([x_grid.flatten(), y_grid.flatten(), z_grid.flatten()]) + + if len(vertices) < 2: + raise ValueError("FDEM survey requires at least 2 vertices") + survey = AirborneFEMReceivers.create(geoh5, vertices=vertices, name=name) + survey.remove_cells(mask_large_connections(survey, 200.0)) survey.metadata["EM Dataset"]["Frequency configurations"] = frequency_config tx_locs_list = [] frequency_list = [] + for config in frequency_config: - delta = np.diff(vertices, axis=0) - delta /= np.linalg.norm(delta, axis=1)[:, None] - delta = np.vstack([delta, delta[-1, :]]) # Repeat last offset + for part in np.unique(survey.parts): + line = survey.parts == part + delta = np.diff(vertices[line, :], axis=0) + length = np.linalg.norm(delta, axis=1) + + if np.any(length <= 0): + raise ValueError("FDEM should not have duplicate vertices") + + delta /= length[:, None] + delta = np.vstack([delta, delta[-1, :]]) # Repeat last offset + + tx_vertices = vertices[line, :] - delta * config["Offset"] + tx_locs_list.append(tx_vertices) + frequency_list.append([[config["Frequency"]] * sum(line)]) - tx_vertices = vertices - delta * config["Offset"] - tx_locs_list.append(tx_vertices) - frequency_list.append([[config["Frequency"]] * len(vertices)]) tx_locs = np.vstack(tx_locs_list) freqs = np.hstack(frequency_list).flatten() @@ -61,7 +74,7 @@ def generate_fdem_survey( } ) - survey.remove_cells(mask_large_connections(survey, 200.0)) + transmitters.remove_cells(mask_large_connections(transmitters, 200.0)) return survey From 351951d99e8962a9103b39423028495f57ccd68f Mon Sep 17 00:00:00 2001 From: domfournier Date: Mon, 16 Mar 2026 14:42:24 -0700 Subject: [PATCH 10/17] Unused imports --- simpeg_drivers/electromagnetics/frequency_domain/options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simpeg_drivers/electromagnetics/frequency_domain/options.py b/simpeg_drivers/electromagnetics/frequency_domain/options.py index 2c5cfe9d..74ec99ad 100644 --- a/simpeg_drivers/electromagnetics/frequency_domain/options.py +++ b/simpeg_drivers/electromagnetics/frequency_domain/options.py @@ -22,7 +22,7 @@ LargeLoopGroundFEMReceivers, MovingLoopGroundFEMReceivers, ) -from pydantic import AliasChoices, Field, field_validator +from pydantic import field_validator from simpeg_drivers import assets_path from simpeg_drivers.options import ( From b6b4a46435d64f431c9a122aa74a84520003e0e7 Mon Sep 17 00:00:00 2001 From: domfournier Date: Mon, 16 Mar 2026 14:55:08 -0700 Subject: [PATCH 11/17] Bulk changes from copilot --- .../uijson/fdem1d_forward.ui.json | 6 ++++-- .../uijson/fdem1d_inversion.ui.json | 14 ++++++++------ simpeg_drivers-assets/uijson/fdem_forward.ui.json | 14 +++++++------- .../uijson/fdem_inversion.ui.json | 14 +++++++------- simpeg_drivers-assets/uijson/tdem_forward.ui.json | 2 +- .../uijson/tdem_inversion.ui.json | 2 +- .../oriented_airborne_tem_receiver_test.py | 8 ++++---- tests/run_tests/oriented_fem_receiver_test.py | 2 +- 8 files changed, 33 insertions(+), 29 deletions(-) diff --git a/simpeg_drivers-assets/uijson/fdem1d_forward.ui.json b/simpeg_drivers-assets/uijson/fdem1d_forward.ui.json index 8a423744..549d3ed8 100644 --- a/simpeg_drivers-assets/uijson/fdem1d_forward.ui.json +++ b/simpeg_drivers-assets/uijson/fdem1d_forward.ui.json @@ -22,13 +22,15 @@ "z_imag_channel_bool": { "group": "Survey", "main": true, - "label": "Z imag component", + "label": "Vertical (imaginary)", + "tooltip": "Vertical (w) imaginary component of the magnetic data.\nPositive up along the z-axis if no receiver orientation provided", "value": true }, "z_real_channel_bool": { "group": "Survey", "main": true, - "label": "Z real component", + "label": "Vertical (real)", + "tooltip": "Vertical (w) real component of the magnetic data.\nPositive up along the z-axis if no receiver orientation provided", "value": true }, "u_cell_size": { diff --git a/simpeg_drivers-assets/uijson/fdem1d_inversion.ui.json b/simpeg_drivers-assets/uijson/fdem1d_inversion.ui.json index cd951543..941f14da 100644 --- a/simpeg_drivers-assets/uijson/fdem1d_inversion.ui.json +++ b/simpeg_drivers-assets/uijson/fdem1d_inversion.ui.json @@ -28,7 +28,8 @@ "group": "Data", "dataGroupType": "Multi-element", "main": true, - "label": "z-imag component (ppm)", + "label": "Vertical (imaginary)", + "tooltip": "Vertical (w) imaginary component of the magnetic data.\nPositive up along the z-axis if no receiver orientation provided", "parent": "data_object", "optional": true, "enabled": true, @@ -43,10 +44,10 @@ "group": "Data", "dataGroupType": "Multi-element", "main": true, - "label": "Uncertainty (ppm)", + "label": "Uncertainty", "parent": "data_object", "dependency": "z_imag_channel", - "dependencyType": "enabled", + "dependencyType": "show", "value": "" }, "z_real_channel": { @@ -58,7 +59,8 @@ "group": "Data", "dataGroupType": "Multi-element", "main": true, - "label": "z-real component (ppm)", + "label": "Vertical (real)", + "tooltip": "Vertical (w) real component of the magnetic data.\nPositive up along the z-axis if no receiver orientation provided", "parent": "data_object", "optional": true, "enabled": true, @@ -73,10 +75,10 @@ "group": "Data", "dataGroupType": "Multi-element", "main": true, - "label": "Uncertainty (ppm)", + "label": "Uncertainty", "parent": "data_object", "dependency": "z_real_channel", - "dependencyType": "enabled", + "dependencyType": "show", "value": "" }, "u_cell_size": { diff --git a/simpeg_drivers-assets/uijson/fdem_forward.ui.json b/simpeg_drivers-assets/uijson/fdem_forward.ui.json index 29f41ec6..64e30627 100644 --- a/simpeg_drivers-assets/uijson/fdem_forward.ui.json +++ b/simpeg_drivers-assets/uijson/fdem_forward.ui.json @@ -30,7 +30,7 @@ ], "label": "Receivers orientation", "tooltip": [ - "Receivers orientation provided as 'Dip direction & dip' data group. If not provided, it is assumes:
", + "Receivers orientation provided as 'Dip direction & dip' data group. If not provided, it assumes:
", "
In-line component
Positive towards North (Y).
", "
Cross-line component
Positive towards East (X).
", "
Vertical component
Positive up (Z).
", @@ -45,42 +45,42 @@ "group": "Survey", "main": true, "label": "Vertical (imaginary)", - "tooltip": "Vertical (w) imaginary component of the magnetic data.\\Positive up along the z-axis if no receiver orientation provided", + "tooltip": "Vertical (w) imaginary component of the magnetic data.\nPositive up along the z-axis if no receiver orientation provided", "value": true }, "z_real_channel_bool": { "group": "Survey", "main": true, "label": "Vertical (real)", - "tooltip": "Vertical (w) real component of the magnetic data.\\Positive up along the z-axis if no receiver orientation provided", + "tooltip": "Vertical (w) real component of the magnetic data.\nPositive up along the z-axis if no receiver orientation provided", "value": true }, "y_imag_channel_bool": { "group": "Survey", "main": true, "label": "In-line (imaginary)", - "tooltip": "In-line (u) imaginary component of the magnetic data.\\Positive towards North if no receiver orientation provided", + "tooltip": "In-line (u) imaginary component of the magnetic data.\nPositive towards North if no receiver orientation provided", "value": true }, "y_real_channel_bool": { "group": "Survey", "main": true, "label": "In-line (real)", - "tooltip": "In-line (u) real component of the magnetic data.\\Positive towards North if no receiver orientation provided", + "tooltip": "In-line (u) real component of the magnetic data.\nPositive towards North if no receiver orientation provided", "value": true }, "x_imag_channel_bool": { "group": "Survey", "main": true, "label": "Cross-line (imaginary)", - "tooltip": "Cross-line (v) imaginary component of the magnetic data.\\Positive towards East if no receiver orientation provided", + "tooltip": "Cross-line (v) imaginary component of the magnetic data.\nPositive towards East if no receiver orientation provided", "value": true }, "x_real_channel_bool": { "group": "Survey", "main": true, "label": "Cross-line (real)", - "tooltip": "Cross-line (v) real component of the magnetic data.\\Positive towards East if no receiver orientation provided", + "tooltip": "Cross-line (v) real component of the magnetic data.\nPositive towards East if no receiver orientation provided", "value": true }, "mesh": { diff --git a/simpeg_drivers-assets/uijson/fdem_inversion.ui.json b/simpeg_drivers-assets/uijson/fdem_inversion.ui.json index 6f9d833c..94179e38 100644 --- a/simpeg_drivers-assets/uijson/fdem_inversion.ui.json +++ b/simpeg_drivers-assets/uijson/fdem_inversion.ui.json @@ -30,7 +30,7 @@ ], "label": "Receivers orientation", "tooltip": [ - "Receivers orientation provided as 'Dip direction & dip' data group. If not provided, it is assumes:
", + "Receivers orientation provided as 'Dip direction & dip' data group. If not provided, it assumes:
", "
In-line component
Positive towards North (Y).
", "
Cross-line component
Positive towards East (X).
", "
Vertical component
Positive up (Z).
", @@ -51,7 +51,7 @@ "dataGroupType": "Multi-element", "main": true, "label": "Vertical (imaginary)", - "tooltip": "Vertical (w) imaginary component of the magnetic data.\\Positive up along the z-axis if no receiver orientation provided", + "tooltip": "Vertical (w) imaginary component of the magnetic data.\nPositive up along the z-axis if no receiver orientation provided", "parent": "data_object", "optional": true, "enabled": true, @@ -82,7 +82,7 @@ "dataGroupType": "Multi-element", "main": true, "label": "Vertical (real)", - "tooltip": "Vertical (w) real component of the magnetic data.\\Positive up along the z-axis if no receiver orientation provided", + "tooltip": "Vertical (w) real component of the magnetic data.\nPositive up along the z-axis if no receiver orientation provided", "parent": "data_object", "optional": true, "enabled": true, @@ -113,7 +113,7 @@ "dataGroupType": "Multi-element", "main": true, "label": "In-line (imaginary)", - "tooltip": "In-line (u) imaginary component of the magnetic data.\\Positive towards North if no receiver orientation provided", + "tooltip": "In-line (u) imaginary component of the magnetic data.\nPositive towards North if no receiver orientation provided", "parent": "data_object", "optional": true, "enabled": false, @@ -144,7 +144,7 @@ "dataGroupType": "Multi-element", "main": true, "label": "In-line (real)", - "tooltip": "In-line (u) real component of the magnetic data.\\Positive towards North if no receiver orientation provided", + "tooltip": "In-line (u) real component of the magnetic data.\nPositive towards North if no receiver orientation provided", "parent": "data_object", "optional": true, "enabled": false, @@ -175,7 +175,7 @@ "dataGroupType": "Multi-element", "main": true, "label": "Cross-line (imaginary)", - "tooltip": "Cross-line (v) imaginary component of the magnetic data.\\Positive towards East if no receiver orientation provided", + "tooltip": "Cross-line (v) imaginary component of the magnetic data.\nPositive towards East if no receiver orientation provided", "parent": "data_object", "optional": true, "enabled": false, @@ -206,7 +206,7 @@ "dataGroupType": "Multi-element", "main": true, "label": "Cross-line (real)", - "tooltip": "Cross-line (v) real component of the magnetic data.\\Positive towards East if no receiver orientation provided", + "tooltip": "Cross-line (v) real component of the magnetic data.\nPositive towards East if no receiver orientation provided", "parent": "data_object", "optional": true, "enabled": false, diff --git a/simpeg_drivers-assets/uijson/tdem_forward.ui.json b/simpeg_drivers-assets/uijson/tdem_forward.ui.json index 6d7150d0..e9b59d4d 100644 --- a/simpeg_drivers-assets/uijson/tdem_forward.ui.json +++ b/simpeg_drivers-assets/uijson/tdem_forward.ui.json @@ -31,7 +31,7 @@ ], "label": "Receivers orientation", "tooltip": [ - "Receivers orientation provided as 'Dip direction & dip' data group. If not provided, it is assumes:
", + "Receivers orientation provided as 'Dip direction & dip' data group. If not provided, it assumes:
", "
In-line component
Positive towards North (Y).
", "
Cross-line component
Positive towards East (X).
", "
Vertical component
Positive up (Z).
", diff --git a/simpeg_drivers-assets/uijson/tdem_inversion.ui.json b/simpeg_drivers-assets/uijson/tdem_inversion.ui.json index 24383f12..e97b30fe 100644 --- a/simpeg_drivers-assets/uijson/tdem_inversion.ui.json +++ b/simpeg_drivers-assets/uijson/tdem_inversion.ui.json @@ -31,7 +31,7 @@ ], "label": "Receivers orientation", "tooltip": [ - "Receivers orientation provided as 'Dip direction & dip' data group. If not provided, it is assumes:
", + "Receivers orientation provided as 'Dip direction & dip' data group. If not provided, it assumes:
", "
In-line component
Positive towards North (Y).
", "
Cross-line component
Positive towards East (X).
", "
Vertical component
Positive up (Z).
", diff --git a/tests/run_tests/oriented_airborne_tem_receiver_test.py b/tests/run_tests/oriented_airborne_tem_receiver_test.py index bff27718..78530c6c 100644 --- a/tests/run_tests/oriented_airborne_tem_receiver_test.py +++ b/tests/run_tests/oriented_airborne_tem_receiver_test.py @@ -97,12 +97,12 @@ def test_tem_fwr_run(tmp_path: Path, azimuth, dip): survey = components.survey # Create property group with orientation - dip = np.ones(survey.n_vertices) * dip - azimuth = np.ones(survey.n_vertices) * azimuth + dip_values = np.ones(survey.n_vertices) * dip + azimuth_values = np.ones(survey.n_vertices) * azimuth data_list = survey.add_data( { - "azimuth": {"values": azimuth}, - "dip": {"values": dip}, + "azimuth": {"values": azimuth_values}, + "dip": {"values": dip_values}, } ) pg = PropertyGroup( diff --git a/tests/run_tests/oriented_fem_receiver_test.py b/tests/run_tests/oriented_fem_receiver_test.py index a40522e5..89bec0d2 100644 --- a/tests/run_tests/oriented_fem_receiver_test.py +++ b/tests/run_tests/oriented_fem_receiver_test.py @@ -117,7 +117,7 @@ def test_fem_fwr_run(tmp_path: Path, azimuth, dip): params = FDEMForwardOptions.build( geoh5=geoh5, - title="Forward: Azimuth {azimuth}, Dip {dip}", + title=f"Forward: Azimuth {azimuth}, Dip {dip}", mesh=components.mesh, topography_object=components.topography, data_object=components.survey, From 4c3b1fbd3a33ce3c3ca3a2e91e26325a386afad7 Mon Sep 17 00:00:00 2001 From: domfournier Date: Mon, 16 Mar 2026 15:14:58 -0700 Subject: [PATCH 12/17] Revert receiver orientation on Tipper until future issue --- .../uijson/tipper_forward.ui.json | 14 -------------- .../uijson/tipper_inversion.ui.json | 14 -------------- simpeg_drivers/natural_sources/tipper/options.py | 4 ++-- 3 files changed, 2 insertions(+), 30 deletions(-) diff --git a/simpeg_drivers-assets/uijson/tipper_forward.ui.json b/simpeg_drivers-assets/uijson/tipper_forward.ui.json index c92cc294..2c73996a 100644 --- a/simpeg_drivers-assets/uijson/tipper_forward.ui.json +++ b/simpeg_drivers-assets/uijson/tipper_forward.ui.json @@ -17,20 +17,6 @@ "meshType": "{0b639533-f35b-44d8-92a8-f70ecff3fd26}", "value": "" }, - "receivers_orientation": { - "group": "Data", - "association": "Vertex", - "dataType": "Float", - "dataGroupType": [ - "Dip direction & dip", - "3D vector" - ], - "label": "Receivers orientation provided as either Dip & dip direction or 3D vector components", - "optional": true, - "enabled": false, - "parent": "data_object", - "value": "" - }, "txz_imag_channel_bool": { "group": "Survey", "main": true, diff --git a/simpeg_drivers-assets/uijson/tipper_inversion.ui.json b/simpeg_drivers-assets/uijson/tipper_inversion.ui.json index ea981220..848948d1 100644 --- a/simpeg_drivers-assets/uijson/tipper_inversion.ui.json +++ b/simpeg_drivers-assets/uijson/tipper_inversion.ui.json @@ -17,20 +17,6 @@ "meshType": "{0b639533-f35b-44d8-92a8-f70ecff3fd26}", "value": "" }, - "receivers_orientation": { - "group": "Data", - "association": "Vertex", - "dataType": "Float", - "dataGroupType": [ - "Dip direction & dip", - "3D vector" - ], - "label": "Receivers orientation provided as either Dip & dip direction or 3D vector components", - "optional": true, - "enabled": false, - "parent": "data_object", - "value": "" - }, "txz_imag_channel": { "association": [ "Cell", diff --git a/simpeg_drivers/natural_sources/tipper/options.py b/simpeg_drivers/natural_sources/tipper/options.py index 59647440..d689199a 100644 --- a/simpeg_drivers/natural_sources/tipper/options.py +++ b/simpeg_drivers/natural_sources/tipper/options.py @@ -46,7 +46,7 @@ class TipperForwardOptions(EMDataMixin, BaseForwardOptions): inversion_type: str = "tipper" data_object: TipperReceivers - receivers_orientation: PropertyGroup | None = None + txz_real_channel_bool: bool | None = None txz_imag_channel_bool: bool | None = None tyz_real_channel_bool: bool | None = None @@ -76,7 +76,7 @@ class TipperInversionOptions(EMDataMixin, BaseInversionOptions): inversion_type: str = "tipper" data_object: TipperReceivers - receivers_orientation: PropertyGroup | None = None + txz_real_channel: PropertyGroup | None = None txz_real_uncertainty: PropertyGroup | None = None txz_imag_channel: PropertyGroup | None = None From 1e48070b1dd9af1682b0781b1163fd6274068245 Mon Sep 17 00:00:00 2001 From: domfournier Date: Mon, 16 Mar 2026 15:17:13 -0700 Subject: [PATCH 13/17] Fix option plate in synthetic --- simpeg_drivers/utils/synthetics/meshes/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simpeg_drivers/utils/synthetics/meshes/factory.py b/simpeg_drivers/utils/synthetics/meshes/factory.py index b8439508..d50136a4 100644 --- a/simpeg_drivers/utils/synthetics/meshes/factory.py +++ b/simpeg_drivers/utils/synthetics/meshes/factory.py @@ -23,7 +23,7 @@ def get_mesh( survey: Points, topography: Surface, options: MeshOptions, - plate: PlateModel, + plate: PlateModel | None, ) -> DrapeModel | Octree: """Factory for mesh creation with behaviour modified by the provided method.""" From 2602adce5c8a8cc07b87fa00fad399c698388bcf Mon Sep 17 00:00:00 2001 From: domfournier Date: Mon, 16 Mar 2026 15:23:34 -0700 Subject: [PATCH 14/17] Ben's comments --- .../uijson/fdem_forward.ui.json | 2 +- .../uijson/fdem_inversion.ui.json | 2 +- .../uijson/tdem_forward.ui.json | 2 +- .../uijson/tdem_inversion.ui.json | 2 +- .../frequency_domain/options.py | 36 +++++++++---------- .../electromagnetics/time_domain/options.py | 19 +++++----- 6 files changed, 32 insertions(+), 31 deletions(-) diff --git a/simpeg_drivers-assets/uijson/fdem_forward.ui.json b/simpeg_drivers-assets/uijson/fdem_forward.ui.json index 64e30627..8917d993 100644 --- a/simpeg_drivers-assets/uijson/fdem_forward.ui.json +++ b/simpeg_drivers-assets/uijson/fdem_forward.ui.json @@ -30,7 +30,7 @@ ], "label": "Receivers orientation", "tooltip": [ - "Receivers orientation provided as 'Dip direction & dip' data group. If not provided, it assumes:
", + "Receivers orientation provided as 'Dip direction & dip' or '3D vector' data group. If not provided, it assumes:
", "
In-line component
Positive towards North (Y).
", "
Cross-line component
Positive towards East (X).
", "
Vertical component
Positive up (Z).
", diff --git a/simpeg_drivers-assets/uijson/fdem_inversion.ui.json b/simpeg_drivers-assets/uijson/fdem_inversion.ui.json index 94179e38..109c7c15 100644 --- a/simpeg_drivers-assets/uijson/fdem_inversion.ui.json +++ b/simpeg_drivers-assets/uijson/fdem_inversion.ui.json @@ -30,7 +30,7 @@ ], "label": "Receivers orientation", "tooltip": [ - "Receivers orientation provided as 'Dip direction & dip' data group. If not provided, it assumes:
", + "Receivers orientation provided as 'Dip direction & dip' or '3D vector' data group. If not provided, it assumes:
", "
In-line component
Positive towards North (Y).
", "
Cross-line component
Positive towards East (X).
", "
Vertical component
Positive up (Z).
", diff --git a/simpeg_drivers-assets/uijson/tdem_forward.ui.json b/simpeg_drivers-assets/uijson/tdem_forward.ui.json index e9b59d4d..13806380 100644 --- a/simpeg_drivers-assets/uijson/tdem_forward.ui.json +++ b/simpeg_drivers-assets/uijson/tdem_forward.ui.json @@ -31,7 +31,7 @@ ], "label": "Receivers orientation", "tooltip": [ - "Receivers orientation provided as 'Dip direction & dip' data group. If not provided, it assumes:
", + "Receivers orientation provided as 'Dip direction & dip' or '3D vector' data group. If not provided, it assumes:
", "
In-line component
Positive towards North (Y).
", "
Cross-line component
Positive towards East (X).
", "
Vertical component
Positive up (Z).
", diff --git a/simpeg_drivers-assets/uijson/tdem_inversion.ui.json b/simpeg_drivers-assets/uijson/tdem_inversion.ui.json index e97b30fe..79c6411c 100644 --- a/simpeg_drivers-assets/uijson/tdem_inversion.ui.json +++ b/simpeg_drivers-assets/uijson/tdem_inversion.ui.json @@ -31,7 +31,7 @@ ], "label": "Receivers orientation", "tooltip": [ - "Receivers orientation provided as 'Dip direction & dip' data group. If not provided, it assumes:
", + "Receivers orientation provided as 'Dip direction & dip' or '3D vector' data group. If not provided, it assumes:
", "
In-line component
Positive towards North (Y).
", "
Cross-line component
Positive towards East (X).
", "
Vertical component
Positive up (Z).
", diff --git a/simpeg_drivers/electromagnetics/frequency_domain/options.py b/simpeg_drivers/electromagnetics/frequency_domain/options.py index 74ec99ad..942bac8d 100644 --- a/simpeg_drivers/electromagnetics/frequency_domain/options.py +++ b/simpeg_drivers/electromagnetics/frequency_domain/options.py @@ -88,10 +88,10 @@ class FDEMForwardOptions(BaseForwardOptions, BaseFDEMOptions): :param receivers_orientation: Orientation of the receivers provided as a group. :param z_real_channel_bool: Vertical (real) component of impedance channel boolean. :param z_imag_channel_bool: Vertical (imaginary) component of impedance channel boolean. - :param x_real_channel_bool: In-line (real) component of impedance channel boolean. - :param x_imag_channel_bool: In-line (imaginary) component of impedance channel boolean. - :param y_real_channel_bool: Cross-line (real) component of impedance channel boolean. - :param y_imag_channel_bool: Cross-line (imaginary) component of impedance channel + :param y_real_channel_bool: In-line (real) component of impedance channel boolean. + :param y_imag_channel_bool: In-line (imaginary) component of impedance channel boolean. + :param x_real_channel_bool: Cross-line (real) component of impedance channel boolean. + :param x_imag_channel_bool: Cross-line (imaginary) component of impedance channel :param models: Specify whether the models are provided in resistivity or conductivity. """ @@ -109,10 +109,10 @@ class FDEMForwardOptions(BaseForwardOptions, BaseFDEMOptions): receivers_orientation: PropertyGroup | None = None z_real_channel_bool: bool = False z_imag_channel_bool: bool = False - x_real_channel_bool: bool = False - x_imag_channel_bool: bool = False y_real_channel_bool: bool = False y_imag_channel_bool: bool = False + x_real_channel_bool: bool = False + x_imag_channel_bool: bool = False models: ConductivityModelOptions @@ -124,14 +124,14 @@ class FDEMInversionOptions(BaseFDEMOptions, BaseInversionOptions): :param z_real_uncertainty: Vertical (real) impedance uncertainty channel. :param z_imag_channel: Vertical (imaginary) impedance channel. :param z_imag_uncertainty: Vertical (imaginary) impedance uncertainty channel. - :param x_real_channel: In-line (real) impedance channel. - :param x_real_uncertainty: In-line (real) impedance uncertainty channel. - :param x_imag_channel: In-line (imaginary) impedance channel. - :param x_imag_uncertainty: In-line (imaginary) impedance uncertainty channel - :param y_real_channel: Cross-line (real) impedance channel. - :param y_real_uncertainty: Cross-line (real) impedance uncertainty channel. - :param y_imag_channel: Cross-line (imaginary) impedance channel. - :param y_imag_uncertainty: Cross-line (imaginary) impedance uncertainty channel + :param y_real_channel: In-line (real) impedance channel. + :param y_real_uncertainty: In-line (real) impedance uncertainty channel. + :param y_imag_channel: In-line (imaginary) impedance channel. + :param y_imag_uncertainty: In-line (imaginary) impedance uncertainty channel + :param x_real_channel: Cross-line (real) impedance channel. + :param x_real_uncertainty: Cross-line (real) impedance uncertainty channel. + :param x_imag_channel: Cross-line (imaginary) impedance channel. + :param x_imag_uncertainty: Cross-line (imaginary) impedance uncertainty channel :param models: Specify whether the models are provided in resistivity or conductivity. """ @@ -151,14 +151,14 @@ class FDEMInversionOptions(BaseFDEMOptions, BaseInversionOptions): z_real_uncertainty: PropertyGroup | None = None z_imag_channel: PropertyGroup | None = None z_imag_uncertainty: PropertyGroup | None = None - x_real_channel: PropertyGroup | None = None - x_real_uncertainty: PropertyGroup | None = None - x_imag_channel: PropertyGroup | None = None - x_imag_uncertainty: PropertyGroup | None = None y_real_channel: PropertyGroup | None = None y_real_uncertainty: PropertyGroup | None = None y_imag_channel: PropertyGroup | None = None y_imag_uncertainty: PropertyGroup | None = None + x_real_channel: PropertyGroup | None = None + x_real_uncertainty: PropertyGroup | None = None + x_imag_channel: PropertyGroup | None = None + x_imag_uncertainty: PropertyGroup | None = None models: ConductivityModelOptions diff --git a/simpeg_drivers/electromagnetics/time_domain/options.py b/simpeg_drivers/electromagnetics/time_domain/options.py index 591a1c21..ef52632a 100644 --- a/simpeg_drivers/electromagnetics/time_domain/options.py +++ b/simpeg_drivers/electromagnetics/time_domain/options.py @@ -76,9 +76,9 @@ class TDEMForwardOptions(BaseTDEMOptions, BaseForwardOptions): """ Time Domain Electromagnetic forward options. - :param z_channel_bool: Z-component data channel boolean. - :param x_channel_bool: X-component data channel boolean. - :param y_channel_bool: Y-component data channel boolean. + :param z_channel_bool: Vertical data channel boolean. + :param y_channel_bool: In-line data channel boolean. + :param x_channel_bool: Cross-line data channel boolean. """ name: ClassVar[str] = "Time Domain Electromagnetics Forward" @@ -105,10 +105,10 @@ class TDEMInversionOptions(BaseTDEMOptions, BaseInversionOptions): :param z_channel: Z-component data channel. :param z_uncertainty: Z-component data channel uncertainty. - :param x_channel: X-component data channel. - :param x_uncertainty: X-component data channel uncertainty. - :param y_channel: Y-component data channel. - :param y_uncertainty: Y-component data channel uncertainty. + :param y_channel: In-line data channel. + :param y_uncertainty: In-line data channel uncertainty. + :param x_channel: Cross-line data channel. + :param x_uncertainty: Cross-line data channel uncertainty. """ name: ClassVar[str] = "Time Domain Electromagnetics Inversion" @@ -125,8 +125,9 @@ class TDEMInversionOptions(BaseTDEMOptions, BaseInversionOptions): receivers_orientation: PropertyGroup | None = None z_channel: PropertyGroup | None = None z_uncertainty: PropertyGroup | None = None - x_channel: PropertyGroup | None = None - x_uncertainty: PropertyGroup | None = None y_channel: PropertyGroup | None = None y_uncertainty: PropertyGroup | None = None + x_channel: PropertyGroup | None = None + x_uncertainty: PropertyGroup | None = None + models: ConductivityModelOptions From c28aed5a93752c430022ba1ad7c41f3815d0ef49 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:24:54 +0000 Subject: [PATCH 15/17] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../utils/synthetics/surveys/frequency_domain/fdem.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/simpeg_drivers/utils/synthetics/surveys/frequency_domain/fdem.py b/simpeg_drivers/utils/synthetics/surveys/frequency_domain/fdem.py index 61510cc4..de73a84e 100644 --- a/simpeg_drivers/utils/synthetics/surveys/frequency_domain/fdem.py +++ b/simpeg_drivers/utils/synthetics/surveys/frequency_domain/fdem.py @@ -22,7 +22,11 @@ def generate_fdem_survey( - geoh5: Workspace, x_grid: np.ndarray, y_grid: np.ndarray, z_grid: np.ndarray, name: str = "survey" + geoh5: Workspace, + x_grid: np.ndarray, + y_grid: np.ndarray, + z_grid: np.ndarray, + name: str = "survey", ) -> AirborneFEMReceivers: """Create an FDEM survey object from survey grid locations.""" @@ -74,7 +78,6 @@ def generate_fdem_survey( } ) - transmitters.remove_cells(mask_large_connections(transmitters, 200.0)) return survey From ade914b304a25114a4deff6a626027e33b371fb9 Mon Sep 17 00:00:00 2001 From: domfournier Date: Mon, 16 Mar 2026 15:45:43 -0700 Subject: [PATCH 16/17] Fix 1D test --- .../electromagnetics/frequency_domain_1d/options.py | 2 +- .../utils/synthetics/surveys/frequency_domain/fdem.py | 8 ++++++-- tests/run_tests/driver_airborne_fem_1d_test.py | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/simpeg_drivers/electromagnetics/frequency_domain_1d/options.py b/simpeg_drivers/electromagnetics/frequency_domain_1d/options.py index fd9fddb8..e7b9ebb8 100644 --- a/simpeg_drivers/electromagnetics/frequency_domain_1d/options.py +++ b/simpeg_drivers/electromagnetics/frequency_domain_1d/options.py @@ -49,7 +49,7 @@ class FDEM1DForwardOptions(BaseForwardOptions, BaseFDEMOptions, Base1DOptions): models: ConductivityModelOptions -class FDEM1DInversionOptions(BaseInversionOptions, BaseFDEMOptions, Base1DOptions): +class FDEM1DInversionOptions(BaseFDEMOptions, BaseInversionOptions, Base1DOptions): """ Frequency Domain Electromagnetic Inversion options. diff --git a/simpeg_drivers/utils/synthetics/surveys/frequency_domain/fdem.py b/simpeg_drivers/utils/synthetics/surveys/frequency_domain/fdem.py index 61510cc4..2e599897 100644 --- a/simpeg_drivers/utils/synthetics/surveys/frequency_domain/fdem.py +++ b/simpeg_drivers/utils/synthetics/surveys/frequency_domain/fdem.py @@ -22,7 +22,11 @@ def generate_fdem_survey( - geoh5: Workspace, x_grid: np.ndarray, y_grid: np.ndarray, z_grid: np.ndarray, name: str = "survey" + geoh5: Workspace, + x_grid: np.ndarray, + y_grid: np.ndarray, + z_grid: np.ndarray, + name: str = "survey", ) -> AirborneFEMReceivers: """Create an FDEM survey object from survey grid locations.""" @@ -42,6 +46,7 @@ def generate_fdem_survey( for part in np.unique(survey.parts): line = survey.parts == part delta = np.diff(vertices[line, :], axis=0) + delta[:, 2] = 0 length = np.linalg.norm(delta, axis=1) if np.any(length <= 0): @@ -74,7 +79,6 @@ def generate_fdem_survey( } ) - transmitters.remove_cells(mask_large_connections(transmitters, 200.0)) return survey diff --git a/tests/run_tests/driver_airborne_fem_1d_test.py b/tests/run_tests/driver_airborne_fem_1d_test.py index d1f10f87..da501fa6 100644 --- a/tests/run_tests/driver_airborne_fem_1d_test.py +++ b/tests/run_tests/driver_airborne_fem_1d_test.py @@ -41,7 +41,7 @@ # To test the full run and validate the inversion. # Move this file out of the test directory and run. -target_run = {"data_norm": 804.9848595951983, "phi_d": 64500, "phi_m": 717} +target_run = {"data_norm": 804.9849282354428, "phi_d": 58200, "phi_m": 118} def test_fem_fwr_1d_run( From 41c1280c360cb29dda780508c5455bca90c8cb7e Mon Sep 17 00:00:00 2001 From: domfournier Date: Tue, 17 Mar 2026 08:19:17 -0700 Subject: [PATCH 17/17] Restrict orientation to airborne for now --- simpeg_drivers/components/factories/receiver_factory.py | 6 +++++- .../{driver_fem_test.py => driver_airborne_fem_test.py} | 0 2 files changed, 5 insertions(+), 1 deletion(-) rename tests/run_tests/{driver_fem_test.py => driver_airborne_fem_test.py} (100%) diff --git a/simpeg_drivers/components/factories/receiver_factory.py b/simpeg_drivers/components/factories/receiver_factory.py index 6e49bc57..202fede6 100644 --- a/simpeg_drivers/components/factories/receiver_factory.py +++ b/simpeg_drivers/components/factories/receiver_factory.py @@ -24,6 +24,7 @@ import numpy as np from geoapps_utils.utils.transformations import x_rotation_matrix, z_rotation_matrix +from geoh5py.objects.surveys.electromagnetics.base import AirborneEMSurvey from simpeg_drivers.components.factories.simpeg_factory import SimPEGFactory from simpeg_drivers.utils.regularization import direction_and_dip, get_cell_normals @@ -162,7 +163,10 @@ def assemble_keyword_arguments( kwargs["data_type"] = "ppm" # Overload orientation if provided - if self.factory_type in ["tdem", "fdem"] and local_indices is not None: + if ( + isinstance(self.params.data_object, AirborneEMSurvey) + and local_indices is not None + ): kwargs["orientation"] = self.orientations[kwargs["orientation"]][ local_indices, : ] diff --git a/tests/run_tests/driver_fem_test.py b/tests/run_tests/driver_airborne_fem_test.py similarity index 100% rename from tests/run_tests/driver_fem_test.py rename to tests/run_tests/driver_airborne_fem_test.py