Skip to content

GEOPY-2779: Add Leroi as option for engine inside Plate Simulation application#378

Open
benk-mira wants to merge 11 commits intodevelopfrom
GEOPY-2779
Open

GEOPY-2779: Add Leroi as option for engine inside Plate Simulation application#378
benk-mira wants to merge 11 commits intodevelopfrom
GEOPY-2779

Conversation

@benk-mira
Copy link
Copy Markdown
Contributor

@benk-mira benk-mira commented Apr 15, 2026

GEOPY-2779 - Add Leroi as option for engine inside Plate Simulation application

Copilot AI review requested due to automatic review settings April 15, 2026 22:10
@github-actions github-actions bot changed the title Geopy 2779 GEOPY-2779: Add Leroi as option for engine inside Plate Simulation application Apr 15, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_leroi option (Python + UI JSON) intended to switch plate simulation execution to a LeroiAir driver for TEM forward runs.
  • Adds initial leroi_air options/interface/driver modules under simpeg_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_run has no function body, which will raise an IndentationError during test collection. Add a body (e.g., implement assertions or at least pass/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()
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_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.

Suggested change
self._simulation_driver = self._get_simpeg_driver()
self._get_simpeg_driver()

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +45
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,
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +55
"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]),
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"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),

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +17


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)
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +47
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",
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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",

Copilot uses AI. Check for mistakes.
Comment on lines +325 to +328
leroi_opts = LeroiAirOptions.from_plate_simulation(self.params)
driver = LeroiAirDriver(leroi_opts)
return driver

Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_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.

Copilot uses AI. Check for mistakes.
Comment on lines +122 to +123
layer_sigma = self.layer_thicknesses * (1 / np.array(self.layer_resistivities))
plate_sigma = [g.width for g in self.plate_geometries] * (
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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]) * (

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +112
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)
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +48
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)

Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
},
"use_leroi": {
"main": true,
"label": "Use leroi",
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"label": "Use leroi",
"label": "Use Leroi",

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants