Skip to content

Commit d1ada2b

Browse files
committed
improve test
1 parent 6bb0d8f commit d1ada2b

File tree

3 files changed

+71
-80
lines changed

3 files changed

+71
-80
lines changed

tests/test_plugins/test_mode_solver.py

Lines changed: 47 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1561,61 +1561,58 @@ def test_mode_solver_pec_boundary_truncation():
15611561
# Get the mode solver data
15621562
data = ms.data
15631563

1564-
# Get simulation grid boundaries
1564+
# Get simulation boundaries
15651565
sim_bounds = simulation.bounds
15661566
sim_x_min, sim_x_max = sim_bounds[0][0], sim_bounds[1][0]
15671567
sim_z_min, sim_z_max = sim_bounds[0][2], sim_bounds[1][2]
15681568

1569-
# Get solver grid boundaries (may extend beyond simulation)
1570-
solver_grid = ms._solver_grid
1571-
solver_x = solver_grid.boundaries.x
1572-
solver_z = solver_grid.boundaries.z
1573-
1574-
# Check if the solver grid extends beyond simulation bounds
1575-
grid_extends_x_min = solver_x[0] < sim_x_min - fp_eps
1576-
grid_extends_x_max = solver_x[-1] > sim_x_max + fp_eps
1577-
grid_extends_z_min = solver_z[0] < sim_z_min - fp_eps
1578-
grid_extends_z_max = solver_z[-1] > sim_z_max + fp_eps
1579-
15801569
# Test 1: Fields outside simulation boundaries should be exactly 0 (zero-padded)
15811570
for field_name in ("Ex", "Ey", "Ez", "Hx", "Hy", "Hz"):
15821571
field = data.field_components[field_name]
15831572
field_coords_x = field.coords["x"].values
15841573
field_coords_z = field.coords["z"].values
15851574

15861575
# Check fields at x positions outside simulation
1587-
if grid_extends_x_min:
1588-
x_outside_min = field_coords_x[field_coords_x < sim_x_min - fp_eps]
1589-
if len(x_outside_min) > 0:
1590-
field_outside = field.sel(x=x_outside_min)
1591-
assert np.all(field_outside.values == 0.0), (
1592-
f"{field_name} should be exactly 0 outside x_min boundary, got {np.max(np.abs(field_outside.values))}"
1593-
)
1594-
1595-
if grid_extends_x_max:
1596-
x_outside_max = field_coords_x[field_coords_x > sim_x_max + fp_eps]
1597-
if len(x_outside_max) > 0:
1598-
field_outside = field.sel(x=x_outside_max)
1599-
assert np.all(field_outside.values == 0.0), (
1600-
f"{field_name} should be exactly 0 outside x_max boundary, got {np.max(np.abs(field_outside.values))}"
1601-
)
1576+
x_outside_min = field_coords_x[
1577+
(field_coords_x < sim_x_min)
1578+
& ~np.isclose(field_coords_x, sim_x_min, rtol=fp_eps, atol=fp_eps)
1579+
]
1580+
if len(x_outside_min) > 0:
1581+
field_outside = field.sel(x=x_outside_min)
1582+
assert np.all(field_outside.values == 0.0), (
1583+
f"{field_name} should be exactly 0 outside x_min boundary, got {np.max(np.abs(field_outside.values))}"
1584+
)
1585+
1586+
x_outside_max = field_coords_x[
1587+
(field_coords_x > sim_x_max)
1588+
& ~np.isclose(field_coords_x, sim_x_max, rtol=fp_eps, atol=fp_eps)
1589+
]
1590+
if len(x_outside_max) > 0:
1591+
field_outside = field.sel(x=x_outside_max)
1592+
assert np.all(field_outside.values == 0.0), (
1593+
f"{field_name} should be exactly 0 outside x_max boundary, got {np.max(np.abs(field_outside.values))}"
1594+
)
16021595

16031596
# Check fields at z positions outside simulation
1604-
if grid_extends_z_min:
1605-
z_outside_min = field_coords_z[field_coords_z < sim_z_min - fp_eps]
1606-
if len(z_outside_min) > 0:
1607-
field_outside = field.sel(z=z_outside_min)
1608-
assert np.all(field_outside.values == 0.0), (
1609-
f"{field_name} should be exactly 0 outside z_min boundary, got {np.max(np.abs(field_outside.values))}"
1610-
)
1611-
1612-
if grid_extends_z_max:
1613-
z_outside_max = field_coords_z[field_coords_z > sim_z_max + fp_eps]
1614-
if len(z_outside_max) > 0:
1615-
field_outside = field.sel(z=z_outside_max)
1616-
assert np.all(field_outside.values == 0.0), (
1617-
f"{field_name} should be exactly 0 outside z_max boundary, got {np.max(np.abs(field_outside.values))}"
1618-
)
1597+
z_outside_min = field_coords_z[
1598+
(field_coords_z < sim_z_min)
1599+
& ~np.isclose(field_coords_z, sim_z_min, rtol=fp_eps, atol=fp_eps)
1600+
]
1601+
if len(z_outside_min) > 0:
1602+
field_outside = field.sel(z=z_outside_min)
1603+
assert np.all(field_outside.values == 0.0), (
1604+
f"{field_name} should be exactly 0 outside z_min boundary, got {np.max(np.abs(field_outside.values))}"
1605+
)
1606+
1607+
z_outside_max = field_coords_z[
1608+
(field_coords_z > sim_z_max)
1609+
& ~np.isclose(field_coords_z, sim_z_max, rtol=fp_eps, atol=fp_eps)
1610+
]
1611+
if len(z_outside_max) > 0:
1612+
field_outside = field.sel(z=z_outside_max)
1613+
assert np.all(field_outside.values == 0.0), (
1614+
f"{field_name} should be exactly 0 outside z_max boundary, got {np.max(np.abs(field_outside.values))}"
1615+
)
16191616

16201617
# Test 2: Tangential E-fields at PEC boundaries should be exactly 0
16211618
# At x boundaries: Ey and Ez are tangential
@@ -1626,8 +1623,8 @@ def test_mode_solver_pec_boundary_truncation():
16261623
field = data.field_components[field_name]
16271624
field_coords_x = field.coords["x"].values
16281625
# Find coordinates exactly at boundaries
1629-
x_at_min = field_coords_x[np.isclose(field_coords_x, sim_x_min, atol=fp_eps)]
1630-
x_at_max = field_coords_x[np.isclose(field_coords_x, sim_x_max, atol=fp_eps)]
1626+
x_at_min = field_coords_x[np.isclose(field_coords_x, sim_x_min, rtol=fp_eps, atol=fp_eps)]
1627+
x_at_max = field_coords_x[np.isclose(field_coords_x, sim_x_max, rtol=fp_eps, atol=fp_eps)]
16311628

16321629
if len(x_at_min) > 0:
16331630
field_at_boundary = field.sel(x=x_at_min[0])
@@ -1645,8 +1642,8 @@ def test_mode_solver_pec_boundary_truncation():
16451642
field = data.field_components[field_name]
16461643
field_coords_z = field.coords["z"].values
16471644
# Find coordinates exactly at boundaries
1648-
z_at_min = field_coords_z[np.isclose(field_coords_z, sim_z_min, atol=fp_eps)]
1649-
z_at_max = field_coords_z[np.isclose(field_coords_z, sim_z_max, atol=fp_eps)]
1645+
z_at_min = field_coords_z[np.isclose(field_coords_z, sim_z_min, rtol=fp_eps, atol=fp_eps)]
1646+
z_at_max = field_coords_z[np.isclose(field_coords_z, sim_z_max, rtol=fp_eps, atol=fp_eps)]
16501647

16511648
if len(z_at_min) > 0:
16521649
field_at_boundary = field.sel(z=z_at_min[0])
@@ -1665,8 +1662,8 @@ def test_mode_solver_pec_boundary_truncation():
16651662
# At z boundaries: Hz is normal
16661663
field = data.field_components["Hx"]
16671664
field_coords_x = field.coords["x"].values
1668-
x_at_min = field_coords_x[np.isclose(field_coords_x, sim_x_min, atol=fp_eps)]
1669-
x_at_max = field_coords_x[np.isclose(field_coords_x, sim_x_max, atol=fp_eps)]
1665+
x_at_min = field_coords_x[np.isclose(field_coords_x, sim_x_min, rtol=fp_eps, atol=fp_eps)]
1666+
x_at_max = field_coords_x[np.isclose(field_coords_x, sim_x_max, rtol=fp_eps, atol=fp_eps)]
16701667

16711668
if len(x_at_min) > 0:
16721669
field_at_boundary = field.sel(x=x_at_min[0])
@@ -1682,8 +1679,8 @@ def test_mode_solver_pec_boundary_truncation():
16821679

16831680
field = data.field_components["Hz"]
16841681
field_coords_z = field.coords["z"].values
1685-
z_at_min = field_coords_z[np.isclose(field_coords_z, sim_z_min, atol=fp_eps)]
1686-
z_at_max = field_coords_z[np.isclose(field_coords_z, sim_z_max, atol=fp_eps)]
1682+
z_at_min = field_coords_z[np.isclose(field_coords_z, sim_z_min, rtol=fp_eps, atol=fp_eps)]
1683+
z_at_max = field_coords_z[np.isclose(field_coords_z, sim_z_max, rtol=fp_eps, atol=fp_eps)]
16871684

16881685
if len(z_at_min) > 0:
16891686
field_at_boundary = field.sel(z=z_at_min[0])

tidy3d/components/mode/mode_solver.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1184,7 +1184,7 @@ def _reduced_simulation_copy_with_fallback(self) -> ModeSolver:
11841184
@cached_property
11851185
def _sim_boundary_positions(
11861186
self,
1187-
) -> tuple[tuple[Optional[float], Optional[float]], tuple[Optional[float], Optional[float]]]:
1187+
) -> tuple[tuple[float, float], tuple[float, float]]:
11881188
"""Get simulation boundary positions for the mode plane's tangential axes.
11891189
11901190
This method checks the simulation's boundary conditions and returns positions
@@ -1203,7 +1203,7 @@ def _sim_boundary_positions(
12031203
12041204
Returns
12051205
-------
1206-
tuple[tuple[Optional[float], Optional[float]], tuple[Optional[float], Optional[float]]]
1206+
tuple[tuple[float, float], tuple[float, float]]
12071207
(pos_min, pos_max) where:
12081208
- pos_min = (x_min, y_min) positions for supported boundaries at min edges (None otherwise)
12091209
- pos_max = (x_max, y_max) positions for supported boundaries at max edges (None otherwise)
@@ -1227,24 +1227,26 @@ def _sim_boundary_positions(
12271227
sim_max = sim_grid_list[axis][-1]
12281228
solver_min = solver_grid_list[axis][0]
12291229
solver_max = solver_grid_list[axis][-1]
1230-
1230+
pos_min[i] = sim_min
1231+
pos_max[i] = sim_max
12311232
# Check if mode solver plane intersects simulation boundaries
12321233
# Min side: intersects if solver grid extends beyond (below) sim boundary
1233-
intersects_min = solver_min < sim_min and not np.isclose(solver_min, sim_min)
1234+
intersects_min = solver_min < sim_min and not np.isclose(
1235+
solver_min, sim_min, rtol=fp_eps
1236+
)
12341237
# Max side: intersects if solver grid extends beyond (above) sim boundary
1235-
intersects_max = solver_max > sim_max and not np.isclose(solver_max, sim_max)
1238+
intersects_max = solver_max > sim_max and not np.isclose(
1239+
solver_max, sim_max, rtol=fp_eps
1240+
)
12361241

12371242
# Check minus side
12381243
bc_minus = boundary.minus
1239-
if isinstance(bc_minus, PECBoundary):
1240-
pos_min[i] = sim_min
1241-
elif isinstance(bc_minus, PMCBoundary):
1244+
if isinstance(bc_minus, PMCBoundary):
12421245
if intersects_min:
12431246
log.warning(
12441247
f"Mode solver plane intersects simulation '{axis_name}-' boundary with "
12451248
f"unsupported PMC boundary condition. The mode solver will apply PEC instead."
12461249
)
1247-
pos_min[i] = sim_min
12481250
elif isinstance(bc_minus, (Periodic, BlochBoundary)):
12491251
if intersects_min:
12501252
log.warning(
@@ -1256,15 +1258,12 @@ def _sim_boundary_positions(
12561258

12571259
# Check plus side
12581260
bc_plus = boundary.plus
1259-
if isinstance(bc_plus, PECBoundary):
1260-
pos_max[i] = sim_max
1261-
elif isinstance(bc_plus, PMCBoundary):
1261+
if isinstance(bc_plus, PMCBoundary):
12621262
if intersects_max:
12631263
log.warning(
12641264
f"Mode solver plane intersects simulation '{axis_name}+' boundary with "
12651265
f"unsupported PMC boundary condition. The mode solver will apply PEC instead."
12661266
)
1267-
pos_max[i] = sim_max
12681267
elif isinstance(bc_plus, (Periodic, BlochBoundary)):
12691268
if intersects_max:
12701269
log.warning(
@@ -1273,7 +1272,6 @@ def _sim_boundary_positions(
12731272
f"Fields will not wrap around periodically."
12741273
)
12751274
# ABC boundaries: no truncation, no warning (fields can extend)
1276-
12771275
return (tuple(pos_min), tuple(pos_max))
12781276

12791277
def _data_on_yee_grid(self) -> ModeSolverData:

tidy3d/components/mode/solver.py

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,11 @@ def compute_modes(
9595
The center of the mode plane along the tangential axes of the global simulation. Used
9696
in case of bend modes to offset the coordinates correctly w.r.t. the bend radius, which
9797
is assumed to refer to the distance from the bend center to the mode plane center.
98-
sim_pec_pos_min : Optional[tuple[Optional[float], Optional[float]]]
99-
Simulation PEC boundary positions (x_min, y_min). Each component can be None if
100-
no PEC boundary exists on that side. If the solver grid extends beyond a PEC
98+
sim_pec_pos_min : Optional[tuple[float, float]]
99+
Simulation PEC boundary positions (x_min, y_min). If the solver grid extends beyond a PEC
101100
position, it will be truncated and fields outside will be zero-padded.
102-
sim_pec_pos_max : Optional[tuple[Optional[float], Optional[float]]]
103-
Simulation PEC boundary positions (x_max, y_max). Each component can be None if
104-
no PEC boundary exists on that side. If the solver grid extends beyond a PEC
101+
sim_pec_pos_max : Optional[tuple[float, float]]
102+
Simulation PEC boundary positions (x_max, y_max). If the solver grid extends beyond a PEC
105103
position, it will be truncated and fields outside will be zero-padded.
106104
107105
Returns
@@ -133,20 +131,18 @@ def compute_modes(
133131
if sim_pec_pos_min is not None:
134132
# Find first coord index >= sim_pec_pos_min (for boundary coords)
135133
for i in range(2):
136-
if sim_pec_pos_min[i] is not None:
137-
idx = np.searchsorted(coords[i], sim_pec_pos_min[i])
138-
# Only trim if sim_pec_pos is actually inside the coord range
139-
if idx > 0 and not np.isclose(coords[i][idx - 1], sim_pec_pos_min[i]):
140-
trim_min[i] = idx
134+
idx = np.searchsorted(coords[i], sim_pec_pos_min[i])
135+
# Only trim if sim_pec_pos is actually inside the coord range
136+
if idx > 0 and not np.isclose(coords[i][idx - 1], sim_pec_pos_min[i]):
137+
trim_min[i] = idx
141138

142139
if sim_pec_pos_max is not None:
143140
# Find last coord index <= sim_pec_pos_max (for boundary coords)
144141
for i in range(2):
145-
if sim_pec_pos_max[i] is not None:
146-
idx = np.searchsorted(coords[i], sim_pec_pos_max[i], side="right")
147-
# Only trim if sim_pec_pos is actually inside the coord range
148-
if idx < len(coords[i]) and not np.isclose(coords[i][idx], sim_pec_pos_max[i]):
149-
trim_max[i] = idx
142+
idx = np.searchsorted(coords[i], sim_pec_pos_max[i], side="right")
143+
# Only trim if sim_pec_pos is actually inside the coord range
144+
if idx < len(coords[i]) and not np.isclose(coords[i][idx], sim_pec_pos_max[i]):
145+
trim_max[i] = idx
150146

151147
# Check if truncation is needed
152148
needs_truncation = trim_min != [0, 0] or trim_max != [len(coords[0]), len(coords[1])]

0 commit comments

Comments
 (0)