|
13 | 13 | from tidy3d import ScalarFieldDataArray |
14 | 14 | from tidy3d.components.data.monitor_data import ModeSolverData |
15 | 15 | 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 |
17 | 17 | from tidy3d.components.mode_spec import MODE_DATA_KEYS |
18 | 18 | from tidy3d.exceptions import DataError, SetupError |
19 | 19 | from tidy3d.plugins.mode import ModeSolver |
@@ -963,7 +963,16 @@ def test_mode_solver_nan_pol_fraction(): |
963 | 963 |
|
964 | 964 | md = ms.solve() |
965 | 965 | 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) |
967 | 976 | assert list(np.where(np.isnan(md.pol_fraction.te))[1]) == [9] |
968 | 977 |
|
969 | 978 |
|
@@ -1503,3 +1512,52 @@ def test_sort_spec_track_freq(): |
1503 | 1512 | assert np.allclose(modes_lowest.Ex.abs, modes_lowest_retracked.Ex.abs) |
1504 | 1513 | assert np.all(modes_lowest.n_eff == modes_lowest_retracked.n_eff) |
1505 | 1514 | 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