Skip to content

Commit 629fc8b

Browse files
committed
Add support for 2D EME simulations
1 parent 99d0fec commit 629fc8b

File tree

7 files changed

+100
-114
lines changed

7 files changed

+100
-114
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
- Improved error message and handling when attempting to load a non-existent task ID.
1919
- `ClipOperation` now fails validation if traced fields are detected.
2020
- Warn if more than 20 frequencies are used in EME, as this may lead to slower or more expensive simulations.
21+
- EME now supports 2D simulations.
2122

2223
## [2.8.3] - 2025-04-24
2324

tests/test_components/test_eme.py

+19-24
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ def test_eme_grid():
166166
assert mode_plane.size[dim] == 0
167167
else:
168168
assert mode_plane.center[dim] == sim_geom.center[dim]
169-
assert mode_plane.size[dim] == sim_geom.size[dim]
169+
assert mode_plane.size[dim] == td.inf
170170

171171
# test that boundary planes span sim and lie at cell boundaries
172172
for grid in grids:
@@ -296,22 +296,6 @@ def test_eme_simulation():
296296
_ = sim2.plot(y=0, ax=AX)
297297
_ = sim2.plot(z=0, ax=AX)
298298

299-
# must be 3D
300-
with pytest.raises(pd.ValidationError):
301-
_ = td.EMESimulation(
302-
size=(0, 2, 2),
303-
freqs=[td.C_0],
304-
axis=2,
305-
eme_grid_spec=td.EMEUniformGrid(num_cells=2, mode_spec=td.EMEModeSpec()),
306-
)
307-
with pytest.raises(pd.ValidationError):
308-
_ = td.EMESimulation(
309-
size=(2, 2, 0),
310-
freqs=[td.C_0],
311-
axis=2,
312-
eme_grid_spec=td.EMEUniformGrid(num_cells=2, mode_spec=td.EMEModeSpec()),
313-
)
314-
315299
# need at least one freq
316300
with pytest.raises(pd.ValidationError):
317301
_ = sim.updated_copy(freqs=[])
@@ -422,10 +406,6 @@ def test_eme_simulation():
422406
with pytest.raises(pd.ValidationError):
423407
_ = sim.updated_copy(monitors=[monitor])
424408

425-
# test boundary and source validation
426-
with pytest.raises(SetupError):
427-
_ = sim.updated_copy(boundary_spec=td.BoundarySpec.all_sides(td.Periodic()))
428-
429409
# test max sim size and freqs
430410
sim_bad = sim.updated_copy(size=(1000, 1000, 1000))
431411
with pytest.raises(SetupError):
@@ -1244,10 +1224,10 @@ def test_eme_sim_subsection():
12441224
subsection = eme_sim.subsection(region=region, eme_grid_spec="identical")
12451225
assert subsection.size[2] == 1
12461226

1247-
# 2d subsection errors
1227+
# 2d subsection
12481228
region = td.Box(size=(2, 2, 0))
1249-
with pytest.raises(pd.ValidationError):
1250-
subsection = eme_sim.subsection(region=region)
1229+
subsection = eme_sim.subsection(region=region)
1230+
assert subsection.size[2] == 0
12511231

12521232

12531233
def test_eme_periodicity():
@@ -1365,3 +1345,18 @@ def test_eme_grid_from_structures():
13651345
names=[None, "wg", None],
13661346
num_reps=[1, 2, 1],
13671347
)
1348+
1349+
1350+
def test_eme_sim_2d():
1351+
freq0 = td.C_0 / 1.55
1352+
sim_size = (3, 0, 3)
1353+
eme_grid_spec = td.EMEUniformGrid(num_cells=5, mode_spec=td.EMEModeSpec())
1354+
monitor = td.EMEFieldMonitor(size=(td.inf, td.inf, td.inf), name="field")
1355+
eme_sim = td.EMESimulation(
1356+
size=sim_size,
1357+
axis=2,
1358+
freqs=[freq0],
1359+
eme_grid_spec=eme_grid_spec,
1360+
monitors=[monitor],
1361+
port_offsets=(0.5, 0),
1362+
)

tests/test_components/test_mode.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,16 @@ def get_mode_sim():
8080
permittivity_monitor = td.PermittivityMonitor(
8181
size=(1, 1, 0), center=(0, 0, 0), name="eps", freqs=FS
8282
)
83+
boundary_spec = td.BoundarySpec(
84+
x=td.Boundary.pml(), y=td.Boundary.periodic(), z=td.Boundary.pml()
85+
)
8386
sim = td.ModeSimulation(
8487
size=SIZE_2D,
8588
freqs=FS,
8689
mode_spec=mode_spec,
8790
grid_spec=td.GridSpec.auto(wavelength=td.C_0 / FS[0]),
8891
monitors=[permittivity_monitor],
92+
boundary_spec=boundary_spec,
8993
)
9094
return sim
9195

@@ -122,12 +126,12 @@ def test_mode_sim():
122126
_ = sim.updated_copy(freqs=FS[0], grid_spec=grid_spec)
123127
# multiple freqs are ok
124128
_ = sim.updated_copy(grid_spec=td.GridSpec.uniform(dl=0.2), freqs=[1e10] + list(sim.freqs))
125-
_ = td.ModeSimulation(
129+
_ = sim.updated_copy(
126130
size=sim.size, freqs=list(sim.freqs) + [1e10], grid_spec=grid_spec, mode_spec=MODE_SPEC
127131
)
128132

129133
# size limit
130-
sim_too_large = sim.updated_copy(size=(2000, 2000, 0), plane=None)
134+
sim_too_large = sim.updated_copy(size=(2000, 0, 2000), plane=None)
131135
with pytest.raises(SetupError):
132136
sim_too_large.validate_pre_upload()
133137

tidy3d/components/eme/grid.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import numpy as np
99
import pydantic.v1 as pd
1010

11-
from ...constants import RADIAN, fp_eps
11+
from ...constants import RADIAN, fp_eps, inf
1212
from ...exceptions import SetupError, ValidationError
1313
from ..base import Tidy3dBaseModel, skip_if_fields_missing
1414
from ..geometry.base import Box
@@ -762,7 +762,7 @@ def num_cells(self) -> pd.NonNegativeInteger:
762762
@property
763763
def mode_planes(self) -> List[Box]:
764764
"""Planes for mode solving, aligned with cell centers."""
765-
size = list(self.size)
765+
size = [inf, inf, inf]
766766
center = list(self.center)
767767
axis = self.axis
768768
size[axis] = 0

tidy3d/components/eme/simulation.py

+14-38
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,13 @@
2222
from ..medium import FullyAnisotropicMedium
2323
from ..monitor import AbstractModeMonitor, ModeSolverMonitor, Monitor, MonitorType
2424
from ..scene import Scene
25-
from ..simulation import AbstractYeeGridSimulation, Simulation
25+
from ..simulation import AbstractYeeGridSimulation, Simulation, validate_boundaries_for_zero_dims
2626
from ..types import Ax, Axis, FreqArray, Symmetry, annotate_type
27-
from ..validators import MIN_FREQUENCY, validate_freqs_min, validate_freqs_not_empty
27+
from ..validators import (
28+
MIN_FREQUENCY,
29+
validate_freqs_min,
30+
validate_freqs_not_empty,
31+
)
2832
from ..viz import add_ax_if_none, equal_aspect
2933
from .grid import EMECompositeGrid, EMEExplicitGrid, EMEGrid, EMEGridSpec, EMEGridSpecType
3034
from .monitor import (
@@ -187,12 +191,12 @@ class EMESimulation(AbstractYeeGridSimulation):
187191
boundary_spec: BoundarySpec = pd.Field(
188192
BoundarySpec.all_sides(PECBoundary()),
189193
title="Boundaries",
190-
description="Specification of boundary conditions along each dimension. If ``None``, "
191-
"PML boundary conditions are applied on all sides. NOTE: for EME simulations, this "
192-
"is required to be PECBoundary on all sides. To capture radiative effects, "
193-
"move the boundary farther away from the waveguide in the tangential directions, "
194-
"and increase the number of modes. The 'ModeSpec' can also be used to try "
195-
"different boundary conditions.",
194+
description="Specification of boundary conditions along each dimension. "
195+
"By default, PEC boundary conditions are applied on all sides. "
196+
"This field is for consistency with FDTD simulations; however, please note that "
197+
"regardless of the 'boundary_spec', the mode solver terminates the mode plane "
198+
"with PEC boundary. The 'EMEModeSpec' can be used to "
199+
"apply PML layers in the mode solver.",
196200
)
197201

198202
sources: Tuple[None, ...] = pd.Field(
@@ -256,16 +260,6 @@ class EMESimulation(AbstractYeeGridSimulation):
256260
_freqs_not_empty = validate_freqs_not_empty()
257261
_freqs_lower_bound = validate_freqs_min()
258262

259-
@pd.validator("size", always=True)
260-
def _validate_fully_3d(cls, val):
261-
"""An EME simulation must be fully 3D."""
262-
if val.count(0.0) != 0:
263-
raise ValidationError(
264-
"'EMESimulation' cannot have any component of 'size' equal to "
265-
f"zero, given 'size={val}'."
266-
)
267-
return val
268-
269263
@pd.validator("grid_spec", always=True)
270264
def _validate_auto_grid_wavelength(cls, val, values):
271265
"""Handle the case where grid_spec is auto and wavelength is not provided."""
@@ -587,7 +581,6 @@ def _post_init_validators(self) -> None:
587581
self._validate_sweep_spec()
588582
self._validate_symmetry()
589583
self._validate_monitor_setup()
590-
self._validate_sources_and_boundary()
591584

592585
def validate_pre_upload(self) -> None:
593586
"""Validate the fully initialized EME simulation is ok for upload to our servers."""
@@ -795,25 +788,6 @@ def _validate_monitor_setup(self):
795788
"('num_reps != 1' in any 'EMEGridSpec'.)"
796789
)
797790

798-
def _validate_sources_and_boundary(self):
799-
"""Disallow sources and boundary."""
800-
if self.boundary_spec != BoundarySpec.all_sides(PECBoundary()):
801-
raise SetupError(
802-
"In an EME simulation, the 'boundary_spec' must be `PECBoundary` "
803-
"on all sides (the default value). The boundary condition along "
804-
"the propagation axis is always transparent; boundary conditions "
805-
"in the tangential directions are imposed in 'mode_spec' in the "
806-
"EME grid."
807-
)
808-
# commented because sources has type Tuple[None, ...]
809-
# if self.sources != ():
810-
# raise SetupError(
811-
# "EME simulations do not currently support sources. "
812-
# "The simulation performs full bidirectional propagation in the "
813-
# "'port_mode' basis. After running the simulation, use "
814-
# "'smatrix_in_basis' to use another set of modes. "
815-
# )
816-
817791
def _validate_size(self) -> None:
818792
"""Ensures the simulation is within size limits before simulation is uploaded."""
819793

@@ -1206,3 +1180,5 @@ def _cell_index_pairs(self) -> List[pd.NonNegativeInt]:
12061180
else:
12071181
pairs = set(self.eme_grid_spec._cell_index_pairs)
12081182
return list(pairs)
1183+
1184+
_boundaries_for_zero_dims = validate_boundaries_for_zero_dims()

tidy3d/components/mode/simulation.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from ..grid.grid_spec import GridSpec
1818
from ..mode_spec import ModeSpec
1919
from ..monitor import ModeMonitor, ModeSolverMonitor, PermittivityMonitor
20-
from ..simulation import AbstractYeeGridSimulation, Simulation
20+
from ..simulation import AbstractYeeGridSimulation, Simulation, validate_boundaries_for_zero_dims
2121
from ..source.field import ModeSource
2222
from ..types import (
2323
TYPE_TAG_STR,
@@ -513,3 +513,5 @@ def plot_pml_mode_plane(
513513

514514
def validate_pre_upload(self, source_required: bool = False):
515515
self._mode_solver.validate_pre_upload(source_required=source_required)
516+
517+
_boundaries_for_zero_dims = validate_boundaries_for_zero_dims()

tidy3d/components/simulation.py

+55-47
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,59 @@
175175
FIXED_ANGLE_DT_SAFETY_FACTOR = 0.9
176176

177177

178+
def validate_boundaries_for_zero_dims():
179+
"""Error if absorbing boundaries, bloch boundaries, unmatching pec/pmc, or symmetry is used along a zero dimension."""
180+
181+
@pydantic.validator("boundary_spec", allow_reuse=True, always=True)
182+
@skip_if_fields_missing(["size", "symmetry"])
183+
def boundaries_for_zero_dims(cls, val, values):
184+
"""Error if absorbing boundaries, bloch boundaries, unmatching pec/pmc, or symmetry is used along a zero dimension."""
185+
boundaries = val.to_list
186+
size = values.get("size")
187+
symmetry = values.get("symmetry")
188+
axis_names = "xyz"
189+
190+
for dim, (boundary, symmetry_dim, size_dim) in enumerate(zip(boundaries, symmetry, size)):
191+
if size_dim == 0:
192+
axis = axis_names[dim]
193+
num_absorbing_bdries = sum(isinstance(bnd, AbsorberSpec) for bnd in boundary)
194+
num_bloch_bdries = sum(isinstance(bnd, BlochBoundary) for bnd in boundary)
195+
196+
if num_absorbing_bdries > 0:
197+
raise SetupError(
198+
f"The simulation has zero size along the {axis} axis, so "
199+
"using a PML or absorbing boundary along that axis is incorrect. "
200+
f"Use either 'Periodic' or 'BlochBoundary' along {axis}."
201+
)
202+
203+
if num_bloch_bdries > 0:
204+
raise SetupError(
205+
f"The simulation has zero size along the {axis} axis, "
206+
"using a Bloch boundary along such an axis is not supported because of "
207+
"the Bloch vector definition in units of '2 * pi / (size along dimension)'. Use a small "
208+
"but nonzero size along the dimension instead."
209+
)
210+
211+
if symmetry_dim != 0:
212+
raise SetupError(
213+
f"The simulation has zero size along the {axis} axis, so "
214+
"using symmetry along that axis is incorrect. Use 'PECBoundary' "
215+
"or 'PMCBoundary' to select source polarization if needed and set "
216+
f"Simulation.symmetry to 0 along {axis}."
217+
)
218+
219+
if boundary[0] != boundary[1]:
220+
raise SetupError(
221+
f"The simulation has zero size along the {axis} axis. "
222+
f"The boundary condition for {axis} plus and {axis} "
223+
"minus must be the same."
224+
)
225+
226+
return val
227+
228+
return boundaries_for_zero_dims
229+
230+
178231
class AbstractYeeGridSimulation(AbstractSimulation, ABC):
179232
"""
180233
Abstract class for a simulation involving electromagnetic fields defined on a Yee grid.
@@ -2803,53 +2856,6 @@ def check_fixed_angle_components(cls, values):
28032856

28042857
return values
28052858

2806-
@pydantic.validator("boundary_spec", always=True)
2807-
@skip_if_fields_missing(["size", "symmetry"])
2808-
def boundaries_for_zero_dims(cls, val, values):
2809-
"""Error if absorbing boundaries, bloch boundaries, unmatching pec/pmc, or symmetry is used along a zero dimension."""
2810-
boundaries = val.to_list
2811-
size = values.get("size")
2812-
symmetry = values.get("symmetry")
2813-
axis_names = "xyz"
2814-
2815-
for dim, (boundary, symmetry_dim, size_dim) in enumerate(zip(boundaries, symmetry, size)):
2816-
if size_dim == 0:
2817-
axis = axis_names[dim]
2818-
num_absorbing_bdries = sum(isinstance(bnd, AbsorberSpec) for bnd in boundary)
2819-
num_bloch_bdries = sum(isinstance(bnd, BlochBoundary) for bnd in boundary)
2820-
2821-
if num_absorbing_bdries > 0:
2822-
raise SetupError(
2823-
f"The simulation has zero size along the {axis} axis, so "
2824-
"using a PML or absorbing boundary along that axis is incorrect. "
2825-
f"Use either 'Periodic' or 'BlochBoundary' along {axis}."
2826-
)
2827-
2828-
if num_bloch_bdries > 0:
2829-
raise SetupError(
2830-
f"The simulation has zero size along the {axis} axis, "
2831-
"using a Bloch boundary along such an axis is not supported because of "
2832-
"the Bloch vector definition in units of '2 * pi / (size along dimension)'. Use a small "
2833-
"but nonzero size along the dimension instead."
2834-
)
2835-
2836-
if symmetry_dim != 0:
2837-
raise SetupError(
2838-
f"The simulation has zero size along the {axis} axis, so "
2839-
"using symmetry along that axis is incorrect. Use 'PECBoundary' "
2840-
"or 'PMCBoundary' to select source polarization if needed and set "
2841-
f"Simulation.symmetry to 0 along {axis}."
2842-
)
2843-
2844-
if boundary[0] != boundary[1]:
2845-
raise SetupError(
2846-
f"The simulation has zero size along the {axis} axis. "
2847-
f"The boundary condition for {axis} plus and {axis} "
2848-
"minus must be the same."
2849-
)
2850-
2851-
return val
2852-
28532859
@pydantic.validator("sources", always=True)
28542860
def _validate_num_sources(cls, val):
28552861
"""Error if too many sources present."""
@@ -5201,3 +5207,5 @@ def from_scene(cls, scene: Scene, **kwargs) -> Simulation:
52015207
medium=scene.medium,
52025208
**kwargs,
52035209
)
5210+
5211+
_boundaries_for_zero_dims = validate_boundaries_for_zero_dims()

0 commit comments

Comments
 (0)