Skip to content

Commit 20d5df4

Browse files
committed
fix(mode): ensure degenerate modes are orthogonal from mode solver
1 parent e9b6d42 commit 20d5df4

File tree

3 files changed

+438
-2
lines changed

3 files changed

+438
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2626
- Fixed normal for `Box` shape gradient computation to always point outward from boundary which is needed for correct PEC handling.
2727
- Fixed `Box` gradients within `GeometryGroup` where the group intersection boundaries were forwarded.
2828
- Fixed `Box` gradients to use automatic permittivity detection for inside/outside permittivity.
29+
- Improved degenerate mode handling in the mode solver to ensure modes respect the bi-orthogonality condition.
2930

3031
## [2.10.0rc3] - 2025-11-26
3132

tests/test_plugins/test_mode_solver.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from tidy3d import ScalarFieldDataArray
1414
from tidy3d.components.data.monitor_data import ModeSolverData
1515
from tidy3d.components.mode.derivatives import create_sfactor_b, create_sfactor_f
16-
from tidy3d.components.mode.solver import compute_modes
16+
from tidy3d.components.mode.solver import TOL_DEGENERATE_CANDIDATE, EigSolver, compute_modes
1717
from tidy3d.components.mode_spec import MODE_DATA_KEYS
1818
from tidy3d.exceptions import DataError, SetupError
1919
from tidy3d.plugins.mode import ModeSolver
@@ -963,7 +963,16 @@ def test_mode_solver_nan_pol_fraction():
963963

964964
md = ms.solve()
965965
check_ms_reduction(ms)
966-
966+
# Inject NaN at mode_index=5 for selected field components
967+
nan_fields = {}
968+
for field_name in ["Ex", "Ez", "Hx", "Hz"]:
969+
field = getattr(md, field_name)
970+
data = field.values.copy()
971+
data[..., 5] = np.nan
972+
nan_fields[field_name] = field.copy(data=data)
973+
974+
md = md.updated_copy(**nan_fields)
975+
md = ms._filter_polarization(md)
967976
assert list(np.where(np.isnan(md.pol_fraction.te))[1]) == [9]
968977

969978

@@ -1503,3 +1512,52 @@ def test_sort_spec_track_freq():
15031512
assert np.allclose(modes_lowest.Ex.abs, modes_lowest_retracked.Ex.abs)
15041513
assert np.all(modes_lowest.n_eff == modes_lowest_retracked.n_eff)
15051514
assert np.all(modes_lowest.n_group == modes_lowest_retracked.n_group)
1515+
1516+
1517+
def test_degenerate_mode_processing():
1518+
"""Ensure degenerate modes returned by mode solver are bi-orthogonal."""
1519+
freq0 = td.C_0
1520+
sim_size = (0, 2, 2)
1521+
inf = 10
1522+
W1 = 0.3
1523+
n = 1.5
1524+
num_modes = 4
1525+
mode_spec = td.ModeSpec(num_modes=num_modes)
1526+
medium = td.Medium(permittivity=n**2)
1527+
geom1 = td.Box.from_bounds((-inf, -W1 / 2, -W1 / 2), (inf, W1 / 2, W1 / 2))
1528+
wg1 = td.Structure(geometry=geom1, medium=medium)
1529+
1530+
grid_spec = td.GridSpec.uniform(dl=0.2)
1531+
1532+
sim = td.Simulation(
1533+
size=sim_size,
1534+
structures=[wg1],
1535+
grid_spec=grid_spec,
1536+
run_time=10 / freq0,
1537+
)
1538+
1539+
ms = ModeSolver(
1540+
simulation=sim,
1541+
plane=sim.geometry,
1542+
mode_spec=mode_spec,
1543+
freqs=[freq0],
1544+
direction="+",
1545+
)
1546+
1547+
mode_data = ms.data_raw
1548+
1549+
degen_sets = EigSolver._identify_degenerate_modes(
1550+
mode_data.n_complex.values[0, :], TOL_DEGENERATE_CANDIDATE
1551+
)
1552+
assert len(degen_sets) == 1
1553+
1554+
S = mode_data.outer_dot(mode_data, conjugate=False).isel(f=0).values
1555+
threshold = 1e-7
1556+
off_diag_mask = ~np.eye(S.shape[0], dtype=bool)
1557+
large_vals = np.abs(S) > threshold
1558+
problem_mask = off_diag_mask & large_vals
1559+
1560+
indices = np.argwhere(problem_mask)
1561+
msg = f"Found {len(indices)} off-diagonal values > {threshold}:\n"
1562+
msg += "\n".join(f" |S[{i},{j}]| = {np.abs(S[i, j]):.4e}" for i, j in indices)
1563+
assert not np.any(problem_mask), msg

0 commit comments

Comments
 (0)