GEOPY-2779: Add Leroi as option for engine inside Plate Simulation application#378
GEOPY-2779: Add Leroi as option for engine inside Plate Simulation application#378
Conversation
…lder with conftest.py added to ensure the PATH variable is accessible in pytest runs).
for more information, see https://pre-commit.ci
There was a problem hiding this comment.
Pull request overview
Adds an initial (toggleable) path for running plate simulations via a LeroiAir-based workflow, alongside early interface/options scaffolding and related tests/UI wiring.
Changes:
- Introduces a
use_leroioption (Python + UI JSON) intended to switch plate simulation execution to a LeroiAir driver for TEM forward runs. - Adds initial
leroi_airoptions/interface/driver modules undersimpeg_drivers/plate_simulation/leroi_air/. - Adds new test scaffolding and a pytest fixture to help resolve executables on PATH in IDE-driven runs.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/plate_simulation/runtest/leroi_test.py | Adds Leroi-related runtime tests (currently incomplete). |
| tests/plate_simulation/runtest/conftest.py | Adds session fixture to prepend conda Scripts/ to PATH. |
| tests/plate_simulation/leroi_air/options_test.py | Adds assertions around newly introduced LeroiAir options behavior. |
| tests/plate_simulation/leroi_air/interface_test.py | Placeholder for interface tests (header-only). |
| tests/plate_simulation/leroi_air/driver_test.py | Placeholder for driver tests (currently incomplete). |
| tests/plate_simulation/leroi_air/init.py | Provides helper to generate LeroiAirOptions for tests. |
| simpeg_drivers/plate_simulation/options.py | Adds use_leroi flag to plate simulation options. |
| simpeg_drivers/plate_simulation/leroi_air/options.py | Introduces LeroiAirOptions dataclass and derived properties. |
| simpeg_drivers/plate_simulation/leroi_air/interface.py | Adds CFL formatting/writing interface for LeroiAir (scaffolding). |
| simpeg_drivers/plate_simulation/leroi_air/driver.py | Adds a LeroiAir driver stub (currently inconsistent with options/interface). |
| simpeg_drivers/plate_simulation/leroi_air/init.py | Initializes the new leroi_air package (header-only). |
| simpeg_drivers/plate_simulation/driver.py | Wires use_leroi toggle into driver selection logic. |
| simpeg_drivers-assets/uijson/plate_simulation.ui.json | Adds UI control for use_leroi. |
Comments suppressed due to low confidence (1)
tests/plate_simulation/runtest/leroi_test.py:22
test_leroi_runhas no function body, which will raise anIndentationErrorduring test collection. Add a body (e.g., implement assertions or at leastpass/pytest.skip(...)) so the test module imports cleanly.
def test_leroi_run(tmp_path):
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if self.params.use_leroi & is_tem: | ||
| self._simulation_driver = self._get_leroi_driver() | ||
| else: | ||
| self._simulation_driver = self._get_simpeg_driver() |
There was a problem hiding this comment.
_get_simpeg_driver() does not return the created driver, but simulation_driver assigns self._simulation_driver = self._get_simpeg_driver(), which will set it to None. Either return the created driver from _get_simpeg_driver() or call it without assignment and rely on it to set self._simulation_driver.
| self._simulation_driver = self._get_simpeg_driver() | |
| self._get_simpeg_driver() |
| return { | ||
| "TDFD": 1 if self.opts.domain == "time" else 2, | ||
| "DO3D": 0 if self.opts.layered_earth_only else 1, | ||
| "PRFL": 1, | ||
| "ISTOP": 0, | ||
| "ISW": 1, | ||
| "NSX": len(self.opts.ontime_waveform), | ||
| "STEP": 0 if self.opts.modelling.magnetic_field == "dBdt" else 1, | ||
| "UNITS": 1, |
There was a problem hiding this comment.
aliased_values uses attributes that don't exist on LeroiAirOptions (e.g., self.opts.modelling.magnetic_field, self.opts.plate.*). These will raise AttributeError at runtime; update to use the actual fields (self.opts.magnetic_field, self.opts.plate_geometries[...], etc.) or adjust the options model to match what the interface expects.
| "TOPN": self.opts.timing_mark + np.array([0.0] + self.opts.channels[:-1]), | ||
| "TCLS": self.opts.timing_mark + np.array(self.opts.channels), | ||
| "TMS": self.opts.timing_mark + np.array(self.opts.channels), | ||
| "WIDTH": np.array([0.3, 0.3, 0.9]), |
There was a problem hiding this comment.
TOPN uses [0.0] + self.opts.channels[:-1] which will fail if channels is a NumPy array (list/ndarray concatenation). Also WIDTH is currently hard-coded instead of using the option-derived widths, which makes the interface inconsistent with LeroiAirOptions.channel_widths.
| "TOPN": self.opts.timing_mark + np.array([0.0] + self.opts.channels[:-1]), | |
| "TCLS": self.opts.timing_mark + np.array(self.opts.channels), | |
| "TMS": self.opts.timing_mark + np.array(self.opts.channels), | |
| "WIDTH": np.array([0.3, 0.3, 0.9]), | |
| "TOPN": self.opts.timing_mark | |
| + np.concatenate(([0.0], np.asarray(self.opts.channels[:-1]))), | |
| "TCLS": self.opts.timing_mark + np.array(self.opts.channels), | |
| "TMS": self.opts.timing_mark + np.array(self.opts.channels), | |
| "WIDTH": np.array(self.opts.channel_widths), |
|
|
||
|
|
||
| def test_leroi_executable(tmp_path): | ||
| control_file = tmp_path / "test.cfl" | ||
| control_file.touch() | ||
| subprocess.run("F2.bat test output", cwd=tmp_path, shell=True, check=False) |
There was a problem hiding this comment.
This test runs a shell command with check=False and does not assert on the result, so it will pass even if the executable/batch file is missing or fails. Consider asserting on the return code (or using check=True) and skipping on unsupported platforms (e.g., non-Windows) to avoid false positives.
| def test_leroi_executable(tmp_path): | |
| control_file = tmp_path / "test.cfl" | |
| control_file.touch() | |
| subprocess.run("F2.bat test output", cwd=tmp_path, shell=True, check=False) | |
| import sys | |
| import pytest | |
| def test_leroi_executable(tmp_path): | |
| if sys.platform != "win32": | |
| pytest.skip("F2.bat is only supported on Windows.") | |
| control_file = tmp_path / "test.cfl" | |
| control_file.touch() | |
| subprocess.run("F2.bat test output", cwd=tmp_path, shell=True, check=True) |
| return cls( | ||
| survey=options.simulations_parameters().survey, | ||
| layer_resistivities=[ | ||
| options.model.overburden_options.overburden_property, | ||
| options.background, | ||
| ], | ||
| layer_thicknesses=[options.model.overburden_options.thickness, 9999], | ||
| plate_resistivities=[options.model.plate.plate_property], | ||
| plate_geometries=[options.model.plate.geometry], | ||
| magnetic_field="dBdt" if "dBdt" in options.data_units else "B", |
There was a problem hiding this comment.
from_plate_simulation_options calls options.simulations_parameters() (typo) and accesses fields that don't exist on PlateSimulationOptions/ModelOptions (e.g., options.background, options.model.plate, options.data_units). This will raise at runtime; map from options.simulation_parameters() / options.model.background / options.model.plate_options etc. consistently with the existing plate simulation option models.
| return cls( | |
| survey=options.simulations_parameters().survey, | |
| layer_resistivities=[ | |
| options.model.overburden_options.overburden_property, | |
| options.background, | |
| ], | |
| layer_thicknesses=[options.model.overburden_options.thickness, 9999], | |
| plate_resistivities=[options.model.plate.plate_property], | |
| plate_geometries=[options.model.plate.geometry], | |
| magnetic_field="dBdt" if "dBdt" in options.data_units else "B", | |
| simulation_parameters = options.simulation_parameters() | |
| return cls( | |
| survey=simulation_parameters.survey, | |
| layer_resistivities=[ | |
| options.model.overburden_options.overburden_property, | |
| options.model.background, | |
| ], | |
| layer_thicknesses=[options.model.overburden_options.thickness, 9999], | |
| plate_resistivities=[ | |
| plate_options.plate_property | |
| for plate_options in options.model.plate_options | |
| ], | |
| plate_geometries=[ | |
| plate_options.geometry for plate_options in options.model.plate_options | |
| ], | |
| magnetic_field="dBdt" if "dBdt" in simulation_parameters.data_units else "B", |
| leroi_opts = LeroiAirOptions.from_plate_simulation(self.params) | ||
| driver = LeroiAirDriver(leroi_opts) | ||
| return driver | ||
|
|
There was a problem hiding this comment.
_get_leroi_driver references LeroiAirOptions/LeroiAirDriver without importing them, and it calls LeroiAirOptions.from_plate_simulation(...) which is not defined (the new options class defines from_plate_simulation_options). Import the classes and call the correct constructor.
| layer_sigma = self.layer_thicknesses * (1 / np.array(self.layer_resistivities)) | ||
| plate_sigma = [g.width for g in self.plate_geometries] * ( |
There was a problem hiding this comment.
conductivity_thicknesses multiplies Python lists by NumPy arrays (self.layer_thicknesses * (1/array) and [g.width ...] * (1/array)), which will raise TypeError. Convert thickness/width sequences to np.asarray(...) before elementwise multiplication.
| layer_sigma = self.layer_thicknesses * (1 / np.array(self.layer_resistivities)) | |
| plate_sigma = [g.width for g in self.plate_geometries] * ( | |
| layer_sigma = np.asarray(self.layer_thicknesses) * ( | |
| 1 / np.array(self.layer_resistivities) | |
| ) | |
| plate_sigma = np.asarray([g.width for g in self.plate_geometries]) * ( |
| values = array_split(self.aliased_values[param], 10) | ||
| else: | ||
| values = np.column_stack([self.aliased_values[k] for k in params]) | ||
| return self._format_multi_line(values) + "\t ! " + ", ".join(params) |
There was a problem hiding this comment.
format_multi_line references an undefined variable param when params is a string, and when params is a string ", ".join(params) will join characters rather than the parameter name. Fix the variable name and handle the string vs list case explicitly so formatting works.
| values = array_split(self.aliased_values[param], 10) | |
| else: | |
| values = np.column_stack([self.aliased_values[k] for k in params]) | |
| return self._format_multi_line(values) + "\t ! " + ", ".join(params) | |
| values = array_split(self.aliased_values[params], 10) | |
| label = params | |
| else: | |
| values = np.column_stack([self.aliased_values[k] for k in params]) | |
| label = ", ".join(params) | |
| return self._format_multi_line(values) + "\t ! " + label |
| from .interface import LeroiAirInterface | ||
| from .options import BackgroundOptions, LeroiAirOptions, ModellingOptions, OutputOptions | ||
|
|
||
|
|
||
| class LeroiAirDriver: | ||
| def __init__(self, options: LeroiAirOptions): | ||
| self.options = options | ||
|
|
||
| def run(self): | ||
| opts = LeroiAirOptions( | ||
| title="test", | ||
| background=BackgroundOptions( | ||
| basement_thickness=5000, | ||
| basement_resistivity=1000, | ||
| ), | ||
| modelling=ModellingOptions(offtime=3.1, cell_size=10), | ||
| output=OutputOptions(channel="all"), | ||
| ) | ||
|
|
||
| with Workspace("dom_waveform_600Ohmm_bkgr_and_plate.geoh5", mode="r") as geoh5: | ||
| survey = geoh5.get_entity("survey")[1] | ||
| plate = PlateOptions( | ||
| reference=[0.0, 0.0, -20.0], | ||
| strike_length=80.0, | ||
| dip_length=100.0, | ||
| thickness=5.0, | ||
| dip_direction=90.0, | ||
| dip=90.0, | ||
| resistivity=1.0, | ||
| ) | ||
| interface = LeroiAirInterface(geoh5, survey, plate, opts) | ||
|
|
There was a problem hiding this comment.
This driver imports BackgroundOptions, ModellingOptions, and OutputOptions from .options, but they are not defined there, so importing this module will fail. Additionally, the constructed LeroiAirOptions(...) uses fields that don't exist on LeroiAirOptions, and the LeroiAirInterface(...) call does not match the interface constructor signature.
| }, | ||
| "use_leroi": { | ||
| "main": true, | ||
| "label": "Use leroi", |
There was a problem hiding this comment.
User-facing label uses inconsistent capitalization for the product name ("Use leroi"). Consider capitalizing it (e.g., "Use Leroi" or "Use LeroiAir") for clarity and consistency with other labels/tooltips.
| "label": "Use leroi", | |
| "label": "Use Leroi", |
GEOPY-2779 - Add Leroi as option for engine inside Plate Simulation application