Skip to content

Commit 1f3af6e

Browse files
committed
Validating all simulation mode objects by creating associated mode solver
Validating that structure rotation in the mode solver with angle_rotation=True can be done Fix adjoint tests and update changelog
1 parent 16607e9 commit 1f3af6e

File tree

5 files changed

+160
-55
lines changed

5 files changed

+160
-55
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2727
- Fixed field colocation in `EMEModeSolverMonitor`.
2828
- Solver error for EME simulations with bends, introduced when support for 2D EME simulations was added.
2929
- Internal interpolation errors with some versions of `xarray` and `numpy`.
30+
- If `ModeSpec.angle_rotation=True` for a mode object, validate that the structure rotation can be successfully done. Also, error if the medium cannot be rotated (e.g. anisotropic or custom medium), which would previously have just produced wrong results.
3031

3132
### Changed
3233
- Relaxed bounds checking of path integrals during `WavePort` validation.
@@ -36,8 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3637
- Adjoint source frequency width is adjusted to decay sufficiently before zero frequency when possible to improve accuracy of simulation normalization when using custom current sources.
3738
- Change `VisualizationSpec` validator for checking validity of user specified colors to only issue a warning if matplotlib is not installed instead of an error.
3839
- Improved performance of `tidy3d.web.delete_old()` for large folders.
39-
40-
### Changed
40+
- Upon initialization, an FDTD `Simulation` will now try to create all `ModeSolver` objects associated to `ModeSource`-s and `ModeMonitor`-s so they can be validated.
4141
- `tidy3d.plugins.autograd.interpolate_spline()` and `tidy3d.plugins.autograd.add_at()` can now be called with keyword arguments during tracing.
4242

4343
## [2.8.4] - 2025-05-15

tests/test_components/test_mode.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,92 @@ def test_angle_rotation_with_phi():
7878
td.ModeSpec(angle_phi=np.pi / 3, angle_rotation=True)
7979

8080

81+
def test_validation_from_simulation():
82+
"""Test that a ModeSolver created from a simulation ModeMonitor validates correctly."""
83+
84+
sim = td.Simulation(
85+
size=(10, 10, 10),
86+
grid_spec=td.GridSpec(wavelength=1.0),
87+
structures=[],
88+
run_time=1e-12,
89+
monitors=[],
90+
)
91+
92+
inf_geometry = td.Structure(
93+
geometry=td.Box.from_bounds((-td.inf, -1, -100), (td.inf, 1, 0)),
94+
medium=td.Medium(permittivity=4.0, conductivity=1e-4),
95+
)
96+
97+
anisotropic_geometry = td.Structure(
98+
geometry=td.Box.from_bounds((-1, -1, -100), (1, 1, 0)),
99+
medium=td.AnisotropicMedium(
100+
xx=td.Medium(permittivity=4.0, conductivity=1e-4),
101+
yy=td.Medium(permittivity=4.0, conductivity=1e-4),
102+
zz=td.Medium(permittivity=3.0, conductivity=1e-4),
103+
),
104+
)
105+
106+
rot_monitor = td.ModeMonitor(
107+
size=(0, 5, 5),
108+
name="mode_solver",
109+
mode_spec=td.ModeSpec(angle_rotation=True, angle_theta=np.pi / 4),
110+
freqs=[td.C_0],
111+
)
112+
113+
rot_source = td.ModeSource(
114+
size=(0, 5, 5),
115+
mode_spec=td.ModeSpec(angle_rotation=True, angle_theta=np.pi / 4),
116+
source_time=td.GaussianPulse(freq0=td.C_0, fwidth=td.C_0 / 10),
117+
direction="+",
118+
)
119+
120+
# Test that transforming a geometry with an infinite extent raises an error
121+
with pytest.raises(SetupError):
122+
sim.updated_copy(
123+
structures=[inf_geometry],
124+
monitors=[rot_monitor],
125+
)
126+
127+
# Test that transforming an anisotropic medium raises an error
128+
with pytest.raises(SetupError):
129+
sim.updated_copy(
130+
structures=[anisotropic_geometry],
131+
monitors=[rot_monitor],
132+
)
133+
134+
# Same thing with a ModeSource
135+
with pytest.raises(SetupError):
136+
sim.updated_copy(
137+
structures=[inf_geometry],
138+
sources=[rot_source],
139+
)
140+
141+
with pytest.raises(SetupError):
142+
sim.updated_copy(
143+
structures=[anisotropic_geometry],
144+
monitors=[rot_monitor],
145+
)
146+
147+
# Same thing with ModeSimulation
148+
with pytest.raises(SetupError):
149+
td.ModeSimulation(
150+
structures=[inf_geometry],
151+
size=(0, 5, 5),
152+
mode_spec=td.ModeSpec(angle_rotation=True, angle_theta=np.pi / 4),
153+
freqs=[td.C_0],
154+
boundary_spec=td.BoundarySpec.all_sides(td.Periodic()),
155+
)
156+
157+
with pytest.raises(SetupError):
158+
td.ModeSimulation(
159+
structures=[anisotropic_geometry],
160+
size=(0, 5, 5),
161+
mode_spec=td.ModeSpec(angle_rotation=True, angle_theta=np.pi / 4),
162+
freqs=[td.C_0],
163+
boundary_spec=td.BoundarySpec.all_sides(td.Periodic()),
164+
)
165+
166+
81167
def get_mode_sim():
82168
mode_spec = MODE_SPEC.updated_copy(filter_pol="tm")
83169
permittivity_monitor = td.PermittivityMonitor(

tests/test_plugins/test_adjoint.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,13 @@ def f(permittivity, size, vertices, base_eps_val):
574574
def test_adjoint_pipeline_2d(local, use_emulated_run, tmp_path):
575575
run_fn = run_local if local else run
576576

577-
sim = make_sim(permittivity=EPS, size=SIZE, vertices=VERTICES, base_eps_val=BASE_EPS_VAL)
577+
sim = make_sim(
578+
permittivity=EPS,
579+
size=SIZE,
580+
vertices=VERTICES,
581+
base_eps_val=BASE_EPS_VAL,
582+
custom_medium=False,
583+
)
578584

579585
sim_size_2d = list(sim.size)
580586
sim_size_2d[1] = 0
@@ -584,7 +590,11 @@ def test_adjoint_pipeline_2d(local, use_emulated_run, tmp_path):
584590

585591
def f(permittivity, size, vertices, base_eps_val):
586592
sim = make_sim(
587-
permittivity=permittivity, size=size, vertices=vertices, base_eps_val=base_eps_val
593+
permittivity=permittivity,
594+
size=size,
595+
vertices=vertices,
596+
base_eps_val=base_eps_val,
597+
custom_medium=False,
588598
)
589599
sim_size_2d = list(sim.size)
590600
sim_size_2d[1] = 0
@@ -1262,7 +1272,11 @@ def test_adjoint_run_async(local, use_emulated_run_async, tmp_path):
12621272
def make_sim_simple(permittivity: float) -> JaxSimulation:
12631273
"""Make a sim as a function of a single parameter."""
12641274
return make_sim(
1265-
permittivity=permittivity, size=SIZE, vertices=VERTICES, base_eps_val=BASE_EPS_VAL
1275+
permittivity=permittivity,
1276+
size=SIZE,
1277+
vertices=VERTICES,
1278+
base_eps_val=BASE_EPS_VAL,
1279+
custom_medium=False,
12661280
)
12671281

12681282
def f(x):
@@ -1804,10 +1818,8 @@ def test_adjoint_run_time(use_emulated_run, tmp_path, fwidth, run_time, run_time
18041818
assert sim_adj.run_time == run_time_expected
18051819

18061820

1807-
@pytest.mark.parametrize("has_adj_src, log_level_expected", [(True, None), (False, "WARNING")])
1808-
def test_no_adjoint_sources(
1809-
monkeypatch, use_emulated_run, tmp_path, has_adj_src, log_level_expected
1810-
):
1821+
@pytest.mark.parametrize("has_adj_src", [True, False])
1822+
def test_no_adjoint_sources(monkeypatch, use_emulated_run, tmp_path, has_adj_src):
18111823
"""Make sure warning (not error) if no adjoint sources."""
18121824

18131825
def make_sim(eps):
@@ -1839,8 +1851,9 @@ def make_sim(eps):
18391851
data = run(sim, task_name="test", path=str(tmp_path / RUN_FILE))
18401852

18411853
# check whether we got a warning for no sources?
1842-
with AssertLogLevel(log_level_expected, contains_str="No adjoint sources"):
1843-
data.make_adjoint_simulation(fwidth=src.source_time.fwidth, run_time=sim.run_time)
1854+
if not has_adj_src:
1855+
with AssertLogLevel("WARNING", contains_str="No adjoint sources"):
1856+
data.make_adjoint_simulation(fwidth=src.source_time.fwidth, run_time=sim.run_time)
18441857

18451858
jnp.sum(jnp.abs(jnp.array(data["mnt"].amps.values)) ** 2)
18461859

tidy3d/components/mode/mode_solver.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@
2828
from tidy3d.components.eme.simulation import EMESimulation
2929
from tidy3d.components.geometry.base import Box
3030
from tidy3d.components.grid.grid import Coords, Grid
31-
from tidy3d.components.medium import FullyAnisotropicMedium, LossyMetalMedium
31+
from tidy3d.components.medium import (
32+
FullyAnisotropicMedium,
33+
IsotropicUniformMediumType,
34+
LossyMetalMedium,
35+
)
3236
from tidy3d.components.mode_spec import ModeSpec
3337
from tidy3d.components.monitor import ModeMonitor, ModeSolverMonitor
3438
from tidy3d.components.scene import Scene
@@ -217,6 +221,7 @@ def _post_init_validators(self) -> None:
217221
msg_prefix="Mode solver",
218222
)
219223
self._warn_thick_pml(simulation=self.simulation, plane=self.plane, mode_spec=self.mode_spec)
224+
self._validate_rotate_structures()
220225

221226
@classmethod
222227
def _warn_thick_pml(
@@ -270,6 +275,18 @@ def _validate_mode_plane_radius(
270275
"along the radial axis, which can produce wrong results."
271276
)
272277

278+
def _validate_rotate_structures(self) -> None:
279+
"""Validate that structures can be rotated if angle_rotation is True."""
280+
if not self.mode_spec.angle_rotation:
281+
return
282+
try:
283+
_ = self._rotate_structures
284+
except Exception as e:
285+
raise SetupError(
286+
"Mode object defined with 'angle_rotation=True' but failed "
287+
f"to create rotated structures: {e!s}"
288+
) from e
289+
273290
@cached_property
274291
def normal_axis(self) -> Axis:
275292
"""Axis normal to the mode plane."""
@@ -578,14 +595,15 @@ def rotated_structures_copy(self):
578595
to the simulation and updates the ModeSpec to disable bend correction
579596
and reset angles to normal."""
580597

581-
rotated_structures = self._rotate_structures()
598+
rotated_structures = self._rotate_structures
582599
rotated_simulation = self.simulation.updated_copy(structures=rotated_structures)
583600
rotated_mode_spec = self.mode_spec.updated_copy(
584601
angle_rotation=False, angle_theta=0, angle_phi=0
585602
)
586603

587604
return self.updated_copy(simulation=rotated_simulation, mode_spec=rotated_mode_spec)
588605

606+
@cached_property
589607
def _rotate_structures(self) -> list[Structure]:
590608
"""Rotate the structures intersecting with modal plane by angle theta
591609
if bend_correction is enabeled for bend simulations."""
@@ -611,11 +629,14 @@ def _rotate_structures(self) -> list[Structure]:
611629
translate_coords[idx_u] = mnt_center[idx_u]
612630
translate_coords[idx_v] = mnt_center[idx_v]
613631

614-
reduced_sim_solver = self.reduced_simulation_copy
615632
rotated_structures = []
616-
for structure in Scene.intersecting_structures(
617-
self.plane, reduced_sim_solver.simulation.structures
618-
):
633+
for structure in Scene.intersecting_structures(self.plane, self.simulation.structures):
634+
if not isinstance(structure.medium, IsotropicUniformMediumType):
635+
raise NotImplementedError(
636+
"Mode solver plane intersects an unsupported "
637+
"medium. Only uniform isotropic media are supported for the plane rotation. "
638+
)
639+
619640
# Rotate and apply translations
620641
geometry = structure.geometry
621642
geometry = (

tidy3d/components/simulation.py

Lines changed: 23 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3665,8 +3665,7 @@ def _post_init_validators(self) -> None:
36653665
self._validate_tfsf_aux_sources()
36663666
self._validate_nonlinear_specs()
36673667
self._validate_custom_source_time()
3668-
self._validate_mode_object_bends()
3669-
self._warn_mode_object_pml()
3668+
self._validate_mode_objects()
36703669
self._warn_rf_license()
36713670

36723671
def _warn_rf_license(self):
@@ -3703,49 +3702,35 @@ def _warn_rf_license(self):
37033702
msg += rf_component_breakdown_msg
37043703
log.warning(msg, log_once=True)
37053704

3706-
def _warn_mode_object_pml(self) -> None:
3707-
"""Warn if any mode objects have large pml."""
3705+
def _validate_mode_objects(self) -> None:
3706+
"""Create a ModeSolver for each mode object in order to validate."""
37083707
from .mode.mode_solver import ModeSolver
37093708

37103709
for imnt, monitor in enumerate(self.monitors):
37113710
if isinstance(monitor, AbstractModeMonitor):
3712-
warn_str = f"'monitors[{imnt}]'"
3713-
ModeSolver._warn_thick_pml(
3714-
simulation=self,
3715-
plane=monitor.geometry,
3716-
mode_spec=monitor.mode_spec,
3717-
warn_str=warn_str,
3718-
)
3719-
for isrc, source in enumerate(self.sources):
3720-
if isinstance(source, ModeSource):
3721-
warn_str = f"'sources[{isrc}]'"
3722-
ModeSolver._warn_thick_pml(
3723-
simulation=self,
3724-
plane=source.geometry,
3725-
mode_spec=source.mode_spec,
3726-
warn_str=warn_str,
3727-
)
3728-
3729-
def _validate_mode_object_bends(self) -> None:
3730-
"""Error if any mode sources or monitors with bends have a radius that is too small."""
3731-
from .mode.mode_solver import ModeSolver
3711+
try:
3712+
_ = ModeSolver(
3713+
mode_spec=monitor.mode_spec,
3714+
plane=monitor.geometry,
3715+
simulation=self,
3716+
freqs=monitor.freqs,
3717+
)
3718+
except Exception as e:
3719+
raise SetupError(
3720+
f"Monitor at 'monitors[{imnt}]' failed validation: {e!s}"
3721+
) from e
37323722

3733-
for imnt, monitor in enumerate(self.monitors):
3734-
if isinstance(monitor, AbstractModeMonitor):
3735-
ModeSolver._validate_mode_plane_radius(
3736-
mode_spec=monitor.mode_spec,
3737-
plane=monitor.geometry,
3738-
sim_geom=self.geometry,
3739-
msg_prefix=f"Monitor at 'monitors[{imnt}]' ",
3740-
)
37413723
for isrc, source in enumerate(self.sources):
37423724
if isinstance(source, ModeSource):
3743-
ModeSolver._validate_mode_plane_radius(
3744-
mode_spec=source.mode_spec,
3745-
plane=source.geometry,
3746-
sim_geom=self.geometry,
3747-
msg_prefix=f"Source at 'sources[{isrc}]' ",
3748-
)
3725+
try:
3726+
_ = ModeSolver(
3727+
mode_spec=source.mode_spec,
3728+
plane=source.geometry,
3729+
simulation=self,
3730+
freqs=source.source_time.freq0,
3731+
)
3732+
except Exception as e:
3733+
raise SetupError(f"Source at 'sources[{isrc}]' failed validation: {e!s}") from e
37493734

37503735
def _validate_custom_source_time(self):
37513736
"""Warn if all simulation times are outside CustomSourceTime definition range."""

0 commit comments

Comments
 (0)