Integrate targeted complex mode bases, solve both interface incidences together, and cascade sampled taper S matrices. Add frozen-basis evaluation, explicit overlap measures, reproducible starts, shared 2D-to-3D conversion, PML controls, and lower-allocation derivatives. Validation: 1629 tests passed, 584 skipped.
868 lines
31 KiB
Python
868 lines
31 KiB
Python
from typing import cast
|
|
|
|
import numpy
|
|
import pytest
|
|
|
|
from ..fdmath import vec
|
|
from ..fdfd import eme, waveguide_2d, waveguide_3d, waveguide_cyl
|
|
from ._test_builders import complex_ramp, unit_dxes
|
|
from .utils import assert_close
|
|
|
|
|
|
SHAPE = (3, 2, 2)
|
|
DXES = unit_dxes((2, 2))
|
|
WAVENUMBERS_L = numpy.array([1.0, 0.8])
|
|
WAVENUMBERS_R = numpy.array([0.9, 0.7])
|
|
OMEGA = 1 / 1500
|
|
FDFD_OMEGA = 2 * numpy.pi / 16
|
|
REAL_DXES = unit_dxes((5, 5))
|
|
FDFD_SHAPE_2D = (7, 7)
|
|
FDFD_DXES_2D = unit_dxes(FDFD_SHAPE_2D)
|
|
|
|
|
|
def _mode(scale: float) -> tuple[numpy.ndarray, numpy.ndarray]:
|
|
e_field = complex_ramp(SHAPE, offset=1.0 + scale)
|
|
h_field = complex_ramp(SHAPE, scale=0.2, offset=2.0, imag_offset=0.05 * scale)
|
|
return vec(e_field), vec(h_field)
|
|
|
|
|
|
def _mode_sets() -> tuple[list[tuple[numpy.ndarray, numpy.ndarray]], list[tuple[numpy.ndarray, numpy.ndarray]]]:
|
|
left_modes = [_mode(0.0), _mode(0.7)]
|
|
right_modes = [_mode(1.4), _mode(2.1)]
|
|
return left_modes, right_modes
|
|
|
|
|
|
def _dummy_modes() -> tuple[list[tuple[numpy.ndarray, numpy.ndarray]], numpy.ndarray]:
|
|
return [_mode(0.0), _mode(0.7)], numpy.array([1.0, 0.5])
|
|
|
|
|
|
def test_get_tr_returns_finite_coordinate_transfer_matrices() -> None:
|
|
left_modes, right_modes = _mode_sets()
|
|
|
|
transmission, reflection = eme.get_tr(
|
|
left_modes,
|
|
WAVENUMBERS_L,
|
|
right_modes,
|
|
WAVENUMBERS_R,
|
|
dxes=DXES,
|
|
)
|
|
|
|
singular_values = numpy.linalg.svd(transmission, compute_uv=False)
|
|
|
|
assert transmission.shape == (2, 2)
|
|
assert reflection.shape == (2, 2)
|
|
assert numpy.isfinite(transmission).all()
|
|
assert numpy.isfinite(reflection).all()
|
|
# These arbitrary fields are not power-normalized: coefficients may exceed one.
|
|
assert singular_values.max() > 1.0
|
|
|
|
|
|
def test_get_tr_accepts_scaled_dual_projection_modes() -> None:
|
|
left_modes, right_modes = _mode_sets()
|
|
dual_left_modes = [
|
|
(mode[0] * (0.5 + 0.25j), mode[1] * (0.5 + 0.25j))
|
|
for mode in left_modes
|
|
]
|
|
|
|
plain_t, plain_r = eme.get_tr(left_modes, WAVENUMBERS_L, right_modes, WAVENUMBERS_R, dxes=DXES)
|
|
dual_t, dual_r = eme.get_tr(
|
|
left_modes,
|
|
WAVENUMBERS_L,
|
|
right_modes,
|
|
WAVENUMBERS_R,
|
|
dxes=DXES,
|
|
dual_ehLs=dual_left_modes,
|
|
)
|
|
|
|
assert_close(dual_t, plain_t)
|
|
assert_close(dual_r, plain_r, atol=1e-11)
|
|
|
|
|
|
def test_get_abcd_maps_left_state_to_right_state() -> None:
|
|
left_modes, right_modes = _mode_sets()
|
|
ss = eme.get_s(left_modes, WAVENUMBERS_L, right_modes, WAVENUMBERS_R, DXES)
|
|
transfer = eme.get_abcd(left_modes, WAVENUMBERS_L, right_modes, WAVENUMBERS_R, DXES).toarray()
|
|
incoming = numpy.array([1.0, .3j, -.4j, .2])
|
|
outgoing = ss @ incoming
|
|
state_left = numpy.concatenate((incoming[:2], outgoing[:2]))
|
|
state_right = numpy.concatenate((outgoing[2:], incoming[2:]))
|
|
assert_close(transfer @ state_left, state_right, atol=1e-11)
|
|
|
|
|
|
def test_get_s_left_incidence_matches_get_tr() -> None:
|
|
left_modes, right_modes = _mode_sets()
|
|
transmission, reflection = eme.get_tr(left_modes, WAVENUMBERS_L, right_modes, WAVENUMBERS_R, DXES)
|
|
ss = eme.get_s(left_modes, WAVENUMBERS_L, right_modes, WAVENUMBERS_R, DXES)
|
|
assert ss.shape == (4, 4)
|
|
assert_close(ss[:2, :2], reflection, atol=1e-11)
|
|
assert_close(ss[2:, :2], transmission, atol=1e-11)
|
|
|
|
|
|
def test_get_s_force_nogain_caps_singular_values(monkeypatch) -> None:
|
|
raw = numpy.diag([2., .5, 1.2, .3])
|
|
monkeypatch.setattr(eme, '_interface_s', lambda *args, **kwargs: raw.copy())
|
|
modes, wavenumbers = _dummy_modes()
|
|
plain_s = eme.get_s(modes, wavenumbers, modes, wavenumbers, DXES)
|
|
clipped_s = eme.get_s(modes, wavenumbers, modes, wavenumbers, DXES, force_nogain=True)
|
|
assert numpy.linalg.svd(plain_s, compute_uv=False).max() > 1
|
|
assert_close(numpy.linalg.svd(clipped_s, compute_uv=False), [1., 1., .5, .3])
|
|
|
|
|
|
def test_get_s_force_reciprocal_symmetrizes_output(monkeypatch) -> None:
|
|
raw = numpy.arange(16, dtype=float).reshape(4, 4)
|
|
monkeypatch.setattr(eme, '_interface_s', lambda *args, **kwargs: raw.copy())
|
|
modes, wavenumbers = _dummy_modes()
|
|
ss = eme.get_s(modes, wavenumbers, modes, wavenumbers, DXES, force_reciprocal=True)
|
|
assert_close(ss, .5 * (raw + raw.T))
|
|
|
|
|
|
def test_get_s_force_nogain_and_reciprocal_returns_finite_output(monkeypatch) -> None:
|
|
raw = numpy.arange(16, dtype=float).reshape(4, 4)
|
|
monkeypatch.setattr(eme, '_interface_s', lambda *args, **kwargs: raw.copy())
|
|
modes, wavenumbers = _dummy_modes()
|
|
ss = eme.get_s(modes, wavenumbers, modes, wavenumbers, DXES, force_nogain=True, force_reciprocal=True)
|
|
assert_close(ss, ss.T, atol=1e-14)
|
|
assert (numpy.linalg.svd(ss, compute_uv=False) <= 1.0 + 1e-12).all()
|
|
|
|
|
|
def test_get_tr_rejects_length_mismatches() -> None:
|
|
left_modes, right_modes = _mode_sets()
|
|
|
|
with pytest.raises(ValueError, match='same length'):
|
|
eme.get_tr(left_modes[:1], WAVENUMBERS_L, right_modes, WAVENUMBERS_R, dxes=DXES)
|
|
|
|
|
|
def test_get_tr_rejects_malformed_mode_tuples() -> None:
|
|
bad_modes = cast(list[tuple[numpy.ndarray, numpy.ndarray]], [(numpy.ones(4, dtype=complex),)])
|
|
|
|
with pytest.raises(ValueError, match='2-tuple'):
|
|
eme.get_tr(bad_modes, [1.0], bad_modes, [1.0], dxes=DXES)
|
|
|
|
|
|
def test_get_tr_rejects_incompatible_field_shapes() -> None:
|
|
left_modes = [(numpy.ones(4, dtype=complex), numpy.ones(4, dtype=complex))]
|
|
right_modes = [(numpy.ones(6, dtype=complex), numpy.ones(6, dtype=complex))]
|
|
|
|
with pytest.raises(ValueError, match='same E/H shapes'):
|
|
eme.get_tr(left_modes, [1.0], right_modes, [1.0], dxes=DXES)
|
|
|
|
|
|
def test_get_tr_rejects_dual_mode_length_mismatches() -> None:
|
|
left_modes, right_modes = _mode_sets()
|
|
|
|
with pytest.raises(ValueError, match='same length'):
|
|
eme.get_tr(
|
|
left_modes,
|
|
WAVENUMBERS_L,
|
|
right_modes,
|
|
WAVENUMBERS_R,
|
|
dxes=DXES,
|
|
dual_ehLs=left_modes[:1],
|
|
)
|
|
|
|
|
|
def _build_real_epsilon() -> numpy.ndarray:
|
|
epsilon = numpy.ones((3, 5, 5), dtype=float)
|
|
epsilon[:, 2, 1] = 2.0
|
|
return vec(epsilon)
|
|
|
|
|
|
def _build_straight_mode() -> tuple[tuple[numpy.ndarray, numpy.ndarray], complex, numpy.ndarray]:
|
|
epsilon = _build_real_epsilon()
|
|
e_xy, wavenumber = waveguide_2d.solve_mode(
|
|
0,
|
|
omega=OMEGA,
|
|
dxes=REAL_DXES,
|
|
epsilon=epsilon,
|
|
)
|
|
e_field, h_field = waveguide_2d.normalized_fields_e(
|
|
e_xy,
|
|
wavenumber=wavenumber,
|
|
omega=OMEGA,
|
|
dxes=REAL_DXES,
|
|
epsilon=epsilon,
|
|
)
|
|
return (e_field, h_field), wavenumber, epsilon
|
|
|
|
|
|
def _build_bend_mode() -> tuple[tuple[numpy.ndarray, numpy.ndarray], complex]:
|
|
epsilon = vec(numpy.ones((3, 5, 5), dtype=float))
|
|
rmin = 10.0
|
|
e_xy, angular_wavenumber = waveguide_cyl.solve_mode(
|
|
0,
|
|
omega=OMEGA,
|
|
dxes=REAL_DXES,
|
|
epsilon=epsilon,
|
|
rmin=rmin,
|
|
)
|
|
linear_wavenumber = waveguide_cyl.linear_wavenumbers(
|
|
[e_xy],
|
|
[angular_wavenumber],
|
|
epsilon=epsilon,
|
|
dxes=REAL_DXES,
|
|
rmin=rmin,
|
|
)[0]
|
|
e_field, h_field = waveguide_cyl.normalized_fields_e(
|
|
e_xy,
|
|
angular_wavenumber=angular_wavenumber,
|
|
omega=OMEGA,
|
|
dxes=REAL_DXES,
|
|
epsilon=epsilon,
|
|
rmin=rmin,
|
|
)
|
|
return (e_field, h_field), linear_wavenumber
|
|
|
|
|
|
def _build_uniform_mode(index: float) -> tuple[tuple[numpy.ndarray, numpy.ndarray], complex]:
|
|
area = 25.0
|
|
e_field = numpy.zeros((3, 5, 5), dtype=complex)
|
|
h_field = numpy.zeros((3, 5, 5), dtype=complex)
|
|
e_field[0] = numpy.sqrt(2.0 / (index * area))
|
|
h_field[1] = numpy.sqrt(2.0 * index / area)
|
|
return (vec(e_field), vec(h_field)), complex(index * OMEGA)
|
|
|
|
|
|
def test_get_taper_abcd_constant_section_is_phase_only() -> None:
|
|
mode, beta = _build_uniform_mode(1.5)
|
|
length = 11.0
|
|
|
|
abcd = eme.get_taper_abcd(
|
|
[
|
|
eme.ModeSection(0.0, [mode], [beta]),
|
|
eme.ModeSection(length, [mode], [beta]),
|
|
],
|
|
dxes=REAL_DXES,
|
|
).toarray()
|
|
|
|
assert_close(abcd, _propagation_abcd(beta, length), atol=1e-12, rtol=1e-12)
|
|
|
|
|
|
def test_get_taper_s_constant_section_has_no_reflection() -> None:
|
|
mode, beta = _build_uniform_mode(1.5)
|
|
length = 11.0
|
|
phase = numpy.exp(-1j * beta * length)
|
|
|
|
ss = eme.get_taper_s(
|
|
[
|
|
eme.ModeSection(0.0, [mode], [beta]),
|
|
eme.ModeSection(length, [mode], [beta]),
|
|
],
|
|
dxes=REAL_DXES,
|
|
)
|
|
|
|
assert_close(ss, numpy.array([[0.0, phase], [phase, 0.0]], dtype=complex), atol=1e-12, rtol=1e-12)
|
|
|
|
|
|
def test_get_taper_abcd_is_invariant_to_adjacent_modal_phase() -> None:
|
|
mode, beta = _build_uniform_mode(1.5)
|
|
shifted_mode = (mode[0] * numpy.exp(0.73j), mode[1] * numpy.exp(0.73j))
|
|
length = 11.0
|
|
base_sections = [
|
|
eme.ModeSection(0.0, [mode], [beta]),
|
|
eme.ModeSection(length, [mode], [beta]),
|
|
]
|
|
shifted_sections = [
|
|
eme.ModeSection(0.0, [mode], [beta]),
|
|
eme.ModeSection(length, [shifted_mode], [beta]),
|
|
]
|
|
|
|
base = eme.get_taper_abcd(base_sections, dxes=REAL_DXES).toarray()
|
|
shifted = eme.get_taper_abcd(shifted_sections, dxes=REAL_DXES).toarray()
|
|
|
|
assert_close(shifted, base, atol=1e-12, rtol=1e-12)
|
|
|
|
|
|
def test_get_taper_abcd_is_invariant_to_modal_phase_across_multiple_sections() -> None:
|
|
mode, beta = _build_uniform_mode(1.5)
|
|
mid_length = 5.0
|
|
length = 11.0
|
|
base_sections = [
|
|
eme.ModeSection(0.0, [mode], [beta]),
|
|
eme.ModeSection(mid_length, [mode], [beta]),
|
|
eme.ModeSection(length, [mode], [beta]),
|
|
]
|
|
shifted_sections = [
|
|
eme.ModeSection(0.0, [mode], [beta]),
|
|
eme.ModeSection(mid_length, [(mode[0] * numpy.exp(0.41j), mode[1] * numpy.exp(0.41j))], [beta]),
|
|
eme.ModeSection(length, [(mode[0] * numpy.exp(-0.28j), mode[1] * numpy.exp(-0.28j))], [beta]),
|
|
]
|
|
|
|
base = eme.get_taper_abcd(base_sections, dxes=REAL_DXES).toarray()
|
|
shifted = eme.get_taper_abcd(shifted_sections, dxes=REAL_DXES).toarray()
|
|
|
|
assert_close(shifted, base, atol=1e-12, rtol=1e-12)
|
|
|
|
|
|
def test_get_taper_accepts_complex_leaky_wavenumber() -> None:
|
|
mode, beta = _build_uniform_mode(1.5)
|
|
leaky_beta = beta - 0.02j
|
|
length = 3.0
|
|
|
|
abcd = eme.get_taper_abcd(
|
|
[
|
|
eme.ModeSection(0.0, [mode], [leaky_beta]),
|
|
eme.ModeSection(length, [mode], [leaky_beta]),
|
|
],
|
|
dxes=REAL_DXES,
|
|
).toarray()
|
|
|
|
assert_close(abcd, _propagation_abcd(leaky_beta, length), atol=1e-12, rtol=1e-12)
|
|
|
|
|
|
def test_get_taper_uses_supplied_dual_modes_for_phase_tracking() -> None:
|
|
mode, beta = _build_uniform_mode(1.5)
|
|
primal_scale = numpy.exp(0.42j)
|
|
dual_scale = 0.31 * numpy.exp(-0.77j)
|
|
dual_mode = (mode[0] * dual_scale, mode[1] * dual_scale)
|
|
shifted_mode = (mode[0] * primal_scale, mode[1] * primal_scale)
|
|
shifted_dual_mode = (dual_mode[0] * 2.3j, dual_mode[1] * 2.3j)
|
|
length = 11.0
|
|
|
|
base = eme.get_taper_abcd(
|
|
[
|
|
eme.ModeSection(0.0, [mode], [beta], dual_modes=[dual_mode]),
|
|
eme.ModeSection(length, [mode], [beta], dual_modes=[dual_mode]),
|
|
],
|
|
dxes=REAL_DXES,
|
|
).toarray()
|
|
shifted = eme.get_taper_abcd(
|
|
[
|
|
eme.ModeSection(0.0, [mode], [beta], dual_modes=[dual_mode]),
|
|
eme.ModeSection(length, [shifted_mode], [beta], dual_modes=[shifted_dual_mode]),
|
|
],
|
|
dxes=REAL_DXES,
|
|
).toarray()
|
|
|
|
assert_close(shifted, base, atol=1e-12, rtol=1e-12)
|
|
|
|
|
|
def test_get_taper_rejects_nonmonotonic_sections() -> None:
|
|
mode, beta = _build_uniform_mode(1.5)
|
|
|
|
with pytest.raises(ValueError, match='strictly increasing'):
|
|
eme.get_taper_abcd(
|
|
[
|
|
eme.ModeSection(0.0, [mode], [beta]),
|
|
eme.ModeSection(0.0, [mode], [beta]),
|
|
],
|
|
dxes=REAL_DXES,
|
|
)
|
|
|
|
|
|
def test_get_taper_rejects_mode_count_changes() -> None:
|
|
mode, beta = _build_uniform_mode(1.5)
|
|
|
|
with pytest.raises(ValueError, match='same number of modes'):
|
|
eme.get_taper_abcd(
|
|
[
|
|
eme.ModeSection(0.0, [mode], [beta]),
|
|
eme.ModeSection(1.0, [mode, mode], [beta, beta]),
|
|
],
|
|
dxes=REAL_DXES,
|
|
)
|
|
|
|
|
|
def test_get_taper_rejects_dual_mode_count_changes() -> None:
|
|
mode, beta = _build_uniform_mode(1.5)
|
|
|
|
with pytest.raises(ValueError, match='same length'):
|
|
eme.get_taper_abcd(
|
|
[
|
|
eme.ModeSection(0.0, [mode], [beta], dual_modes=[mode]),
|
|
eme.ModeSection(1.0, [mode], [beta], dual_modes=[mode, mode]),
|
|
],
|
|
dxes=REAL_DXES,
|
|
)
|
|
|
|
|
|
def _interface_s(n_left: float, n_right: float) -> numpy.ndarray:
|
|
left_mode, left_beta = _build_uniform_mode(n_left)
|
|
right_mode, right_beta = _build_uniform_mode(n_right)
|
|
return eme.get_s([left_mode], [left_beta], [right_mode], [right_beta], dxes=REAL_DXES)
|
|
|
|
|
|
def _interface_abcd(n_left: float, n_right: float) -> numpy.ndarray:
|
|
left_mode, left_beta = _build_uniform_mode(n_left)
|
|
right_mode, right_beta = _build_uniform_mode(n_right)
|
|
return eme.get_abcd([left_mode], [left_beta], [right_mode], [right_beta], dxes=REAL_DXES).toarray()
|
|
|
|
|
|
def _expected_interface_s(n_left: float, n_right: float) -> numpy.ndarray:
|
|
reflection = (n_left - n_right) / (n_left + n_right)
|
|
transmission = 2 * numpy.sqrt(n_left * n_right) / (n_left + n_right)
|
|
return numpy.array(
|
|
[
|
|
[reflection, transmission],
|
|
[transmission, -reflection],
|
|
],
|
|
dtype=complex,
|
|
)
|
|
|
|
|
|
def _propagation_abcd(beta: complex, length: float) -> numpy.ndarray:
|
|
phase = numpy.exp(-1j * beta * length)
|
|
return numpy.array(
|
|
[
|
|
[phase, 0.0],
|
|
[0.0, phase ** -1],
|
|
],
|
|
dtype=complex,
|
|
)
|
|
|
|
|
|
def _abcd_to_s(abcd: numpy.ndarray) -> numpy.ndarray:
|
|
aa = abcd[0, 0]
|
|
bb = abcd[0, 1]
|
|
cc = abcd[1, 0]
|
|
dd = abcd[1, 1]
|
|
t21 = 1.0 / dd
|
|
r21 = bb / dd
|
|
r12 = -cc / dd
|
|
t12 = aa - bb * cc / dd
|
|
return numpy.array(
|
|
[
|
|
[r12, t21],
|
|
[t12, r21],
|
|
],
|
|
dtype=complex,
|
|
)
|
|
|
|
|
|
def _expected_bragg_reflector_s(n_low: float, n_high: float, periods: int) -> numpy.ndarray:
|
|
ratio = n_high / n_low
|
|
reflection = (1 - ratio ** (2 * periods)) / (1 + ratio ** (2 * periods))
|
|
transmission = ((-1) ** periods) * 2 * ratio ** periods / (1 + ratio ** (2 * periods))
|
|
return numpy.array(
|
|
[
|
|
[reflection, transmission],
|
|
[transmission, -reflection],
|
|
],
|
|
dtype=complex,
|
|
)
|
|
|
|
|
|
def _fdfd_cross_section(core_epsilon: float) -> numpy.ndarray:
|
|
epsilon = numpy.ones((3, *FDFD_SHAPE_2D), dtype=float)
|
|
epsilon[:, 2:5, 2:5] = core_epsilon
|
|
return epsilon
|
|
|
|
|
|
def _small_fdfd_kwargs() -> dict:
|
|
return {
|
|
'omega': FDFD_OMEGA,
|
|
'dxes_2d': FDFD_DXES_2D,
|
|
'mode_numbers': [0],
|
|
'pml_cells': 4,
|
|
'pml_buffer_cells': 2,
|
|
'source_monitor_cells': 5,
|
|
'monitor_interface_cells': 5,
|
|
'matrix_solver_opts': {'atol': 1e-10, 'rtol': 1e-8},
|
|
}
|
|
|
|
|
|
def test_get_s_is_near_identity_for_identical_solved_straight_modes() -> None:
|
|
mode, wavenumber, _epsilon = _build_straight_mode()
|
|
|
|
ss = eme.get_s([mode], [wavenumber], [mode], [wavenumber], dxes=REAL_DXES)
|
|
|
|
assert ss.shape == (2, 2)
|
|
assert numpy.isfinite(ss).all()
|
|
assert abs(ss[0, 0]) < 1e-6
|
|
assert abs(ss[1, 1]) < 1e-6
|
|
assert abs(abs(ss[0, 1]) - 1.0) < 1e-6
|
|
assert abs(abs(ss[1, 0]) - 1.0) < 1e-6
|
|
assert numpy.linalg.svd(ss, compute_uv=False).max() <= 1.0 + 1e-10
|
|
|
|
|
|
def test_get_s_returns_finite_passive_output_for_small_straight_to_bend_fixture() -> None:
|
|
straight_mode, straight_wavenumber, _epsilon = _build_straight_mode()
|
|
bend_mode, bend_wavenumber = _build_bend_mode()
|
|
|
|
ss = eme.get_s([straight_mode], [straight_wavenumber], [bend_mode], [bend_wavenumber], dxes=REAL_DXES)
|
|
|
|
assert ss.shape == (2, 2)
|
|
assert numpy.isfinite(ss).all()
|
|
assert numpy.linalg.svd(ss, compute_uv=False).max() <= 1.0 + 1e-10
|
|
|
|
|
|
def test_get_s_matches_analytic_fresnel_interface_for_uniform_one_mode_ports() -> None:
|
|
"""
|
|
For power-normalized one-mode ports at normal incidence, the interface matrix is
|
|
|
|
r12 = (n_left - n_right) / (n_left + n_right)
|
|
r21 = -r12
|
|
t12 = t21 = 2 * sqrt(n_left * n_right) / (n_left + n_right)
|
|
|
|
so
|
|
|
|
S = [[r12, t12], [t21, r21]].
|
|
"""
|
|
ss = _interface_s(1.0, 2.0)
|
|
expected = _expected_interface_s(1.0, 2.0)
|
|
|
|
assert ss.shape == (2, 2)
|
|
assert numpy.isfinite(ss).all()
|
|
assert_close(ss, expected, atol=1e-6, rtol=1e-6)
|
|
assert numpy.linalg.svd(ss, compute_uv=False).max() <= 1.0 + 1e-10
|
|
|
|
|
|
def test_get_s_with_dual_modes_matches_analytic_fresnel_interface() -> None:
|
|
left_mode, left_beta = _build_uniform_mode(1.0)
|
|
right_mode, right_beta = _build_uniform_mode(2.0)
|
|
left_dual = (left_mode[0] * (0.25 + 0.5j), left_mode[1] * (0.25 + 0.5j))
|
|
right_dual = (right_mode[0] * (-0.75 + 0.125j), right_mode[1] * (-0.75 + 0.125j))
|
|
|
|
ss = eme.get_s(
|
|
[left_mode],
|
|
[left_beta],
|
|
[right_mode],
|
|
[right_beta],
|
|
dxes=REAL_DXES,
|
|
dual_ehLs=[left_dual],
|
|
dual_ehRs=[right_dual],
|
|
)
|
|
expected = _expected_interface_s(1.0, 2.0)
|
|
|
|
assert_close(ss, expected, atol=1e-6, rtol=1e-6)
|
|
|
|
|
|
def test_get_s_accepts_complex_leaky_wavenumbers_for_abrupt_interface() -> None:
|
|
mode, beta = _build_uniform_mode(1.5)
|
|
|
|
ss = eme.get_s([mode], [beta - 0.02j], [mode], [beta - 0.03j], dxes=REAL_DXES)
|
|
|
|
assert_close(ss, numpy.array([[0.0, 1.0], [1.0, 0.0]], dtype=complex), atol=1e-12, rtol=1e-12)
|
|
|
|
|
|
def test_get_s_fdfd_from_2d_ports_identical_guides_are_nearly_transmissive() -> None:
|
|
epsilon = _fdfd_cross_section(2.25)
|
|
|
|
ss = waveguide_3d.get_s_fdfd_from_2d_ports(epsilon, epsilon, **_small_fdfd_kwargs())
|
|
|
|
assert ss.shape == (2, 2)
|
|
assert numpy.isfinite(ss).all()
|
|
assert abs(ss[0, 0]) < 0.03
|
|
assert abs(ss[1, 1]) < 0.04
|
|
assert abs(abs(ss[1, 0]) - 1.0) < 0.02
|
|
assert abs(abs(ss[0, 1]) - 1.0) < 0.03
|
|
|
|
|
|
def test_get_s_fdfd_from_2d_ports_mismatch_returns_finite_diagnostics() -> None:
|
|
left = _fdfd_cross_section(2.25)
|
|
right = _fdfd_cross_section(1.5)
|
|
|
|
ss, diagnostics = waveguide_3d.get_s_fdfd_from_2d_ports(left, right, return_diagnostics=True, **_small_fdfd_kwargs())
|
|
|
|
assert ss.shape == (2, 2)
|
|
assert numpy.isfinite(ss).all()
|
|
assert len(diagnostics['electric_fields']) == 2
|
|
assert len(diagnostics['magnetic_fields']) == 2
|
|
assert diagnostics['electric_fields'][0].shape == (3, 33, *FDFD_SHAPE_2D)
|
|
assert numpy.isfinite(diagnostics['electric_fields'][0]).all()
|
|
assert numpy.isfinite(diagnostics['magnetic_fields'][0]).all()
|
|
assert numpy.isfinite(diagnostics['guided_powers']).all()
|
|
assert numpy.isfinite(diagnostics['unresolved_powers']).all()
|
|
assert numpy.isfinite(diagnostics['raw_outgoing_amplitudes']).all()
|
|
assert numpy.isfinite(diagnostics['incident_amplitudes']).all()
|
|
assert (numpy.abs(diagnostics['incident_amplitudes']) > 1e-3).all()
|
|
assert_close(diagnostics['unresolved_powers'], 1.0 - diagnostics['guided_powers'])
|
|
assert diagnostics['guided_powers'][0] < 1.05
|
|
|
|
|
|
def test_get_s_fdfd_normalization_is_source_scale_invariant() -> None:
|
|
left = _fdfd_cross_section(2.25)
|
|
right = _fdfd_cross_section(1.5)
|
|
kwargs = _small_fdfd_kwargs()
|
|
|
|
base, base_diagnostics = waveguide_3d.get_s_fdfd_from_2d_ports(left, right, return_diagnostics=True, **kwargs)
|
|
scaled, scaled_diagnostics = waveguide_3d.get_s_fdfd_from_2d_ports(left, right, source_scales=[2.0, 0.3], return_diagnostics=True, **kwargs)
|
|
|
|
assert_close(numpy.abs(scaled), numpy.abs(base), atol=1e-6, rtol=1e-6)
|
|
assert abs(abs(scaled_diagnostics['incident_amplitudes'][0] / base_diagnostics['incident_amplitudes'][0]) - 2.0) < 1e-6
|
|
assert abs(abs(scaled_diagnostics['incident_amplitudes'][1] / base_diagnostics['incident_amplitudes'][1]) - 0.3) < 1e-6
|
|
|
|
|
|
def test_get_s_fdfd_from_2d_ports_diagnostics_order_generated_slices() -> None:
|
|
epsilon = _fdfd_cross_section(2.25)
|
|
|
|
_ss, diagnostics = waveguide_3d.get_s_fdfd_from_2d_ports(epsilon, epsilon, return_diagnostics=True, **_small_fdfd_kwargs())
|
|
|
|
left_source = diagnostics['left_source_slice'][0].start
|
|
left_monitor = diagnostics['left_monitor_slice'][0].start
|
|
interface_index = diagnostics['interface_index']
|
|
right_monitor = diagnostics['right_monitor_slice'][0].start
|
|
right_source = diagnostics['right_source_slice'][0].start
|
|
|
|
assert left_source == 6
|
|
assert left_monitor == 11
|
|
assert interface_index == 16
|
|
assert right_monitor == 21
|
|
assert right_source == 26
|
|
assert left_source < left_monitor < interface_index < right_monitor < right_source
|
|
|
|
|
|
def test_get_s_fdfd_from_2d_ports_transverse_padding_and_pml_updates_diagnostics() -> None:
|
|
epsilon = _fdfd_cross_section(2.25)
|
|
|
|
ss, diagnostics = waveguide_3d.get_s_fdfd_from_2d_ports(
|
|
epsilon,
|
|
epsilon,
|
|
transverse_padding_cells=(2, 3),
|
|
transverse_pml_cells=(1, 2),
|
|
return_diagnostics=True,
|
|
**_small_fdfd_kwargs(),
|
|
)
|
|
|
|
assert ss.shape == (2, 2)
|
|
assert diagnostics['epsilon'].shape == (3, 33, 11, 13)
|
|
assert diagnostics['transverse_padding_cells'] == (2, 3)
|
|
assert diagnostics['transverse_pml_cells'] == (1, 2)
|
|
assert numpy.iscomplexobj(diagnostics['solve_dxes'][0][1])
|
|
assert numpy.iscomplexobj(diagnostics['solve_dxes'][0][2])
|
|
assert numpy.abs(numpy.imag(diagnostics['solve_dxes'][0][1][0])) > 0
|
|
assert numpy.abs(numpy.imag(diagnostics['solve_dxes'][0][2][0])) > 0
|
|
|
|
|
|
def test_get_s_fdfd_from_2d_ports_supports_nondefault_axis() -> None:
|
|
epsilon = _fdfd_cross_section(2.25)
|
|
|
|
ss, diagnostics = waveguide_3d.get_s_fdfd_from_2d_ports(epsilon, epsilon, axis=1, return_diagnostics=True, **_small_fdfd_kwargs())
|
|
|
|
assert ss.shape == (2, 2)
|
|
assert diagnostics['epsilon'].shape == (3, FDFD_SHAPE_2D[0], 33, FDFD_SHAPE_2D[1])
|
|
assert diagnostics['left_source_slice'][1].start == 6
|
|
assert numpy.isfinite(ss).all()
|
|
|
|
|
|
def test_get_s_fdfd_rejects_bad_port_slice_order() -> None:
|
|
epsilon = numpy.ones((3, 12, 5, 5), dtype=float)
|
|
dxes = [[numpy.ones(12), numpy.ones(5), numpy.ones(5)] for _ in range(2)]
|
|
|
|
with pytest.raises(ValueError, match='ordered'):
|
|
waveguide_3d.get_s_fdfd(
|
|
FDFD_OMEGA,
|
|
dxes,
|
|
epsilon,
|
|
left_mode_numbers=[0],
|
|
left_source_slice=(slice(5, 6), slice(None), slice(None)),
|
|
left_monitor_slice=(slice(4, 5), slice(None), slice(None)),
|
|
right_source_slice=(slice(10, 11), slice(None), slice(None)),
|
|
right_monitor_slice=(slice(8, 9), slice(None), slice(None)),
|
|
)
|
|
|
|
|
|
def test_get_s_fdfd_rejects_bad_source_scale_count_before_solving() -> None:
|
|
epsilon = numpy.ones((3, 12, 5, 5), dtype=float)
|
|
dxes = [[numpy.ones(12), numpy.ones(5), numpy.ones(5)] for _ in range(2)]
|
|
|
|
with pytest.raises(ValueError, match='source_scales'):
|
|
waveguide_3d.get_s_fdfd(
|
|
FDFD_OMEGA,
|
|
dxes,
|
|
epsilon,
|
|
left_mode_numbers=[0],
|
|
left_source_slice=(slice(2, 3), slice(None), slice(None)),
|
|
left_monitor_slice=(slice(4, 5), slice(None), slice(None)),
|
|
right_mode_numbers=[0, 1],
|
|
right_source_slice=(slice(10, 11), slice(None), slice(None)),
|
|
right_monitor_slice=(slice(8, 9), slice(None), slice(None)),
|
|
source_scales=[1.0, 1.0],
|
|
)
|
|
|
|
|
|
def test_get_s_fdfd_from_2d_ports_rejects_invalid_generated_geometry() -> None:
|
|
epsilon = _fdfd_cross_section(2.25)
|
|
|
|
with pytest.raises(ValueError, match='must be positive'):
|
|
waveguide_3d.get_s_fdfd_from_2d_ports(epsilon, epsilon, **{**_small_fdfd_kwargs(), 'pml_cells': 0})
|
|
|
|
|
|
def test_get_s_fdfd_rejects_noninteger_mode_numbers_before_solving() -> None:
|
|
epsilon = numpy.ones((3, 12, 5, 5), dtype=float)
|
|
dxes = [[numpy.ones(12), numpy.ones(5), numpy.ones(5)] for _ in range(2)]
|
|
|
|
with pytest.raises(ValueError, match='mode_numbers'):
|
|
waveguide_3d.get_s_fdfd(
|
|
FDFD_OMEGA,
|
|
dxes,
|
|
epsilon,
|
|
left_mode_numbers=[0.5], # type: ignore[list-item]
|
|
left_source_slice=(slice(2, 3), slice(None), slice(None)),
|
|
left_monitor_slice=(slice(4, 5), slice(None), slice(None)),
|
|
right_source_slice=(slice(10, 11), slice(None), slice(None)),
|
|
right_monitor_slice=(slice(8, 9), slice(None), slice(None)),
|
|
)
|
|
|
|
|
|
def test_get_s_fdfd_from_2d_ports_preserves_complex_generated_inputs(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
left = _fdfd_cross_section(2.25)
|
|
right = _fdfd_cross_section(1.5).astype(complex)
|
|
right += 0.1j
|
|
dxes_2d = [
|
|
[FDFD_DXES_2D[0][0].astype(complex) + 0.2j, FDFD_DXES_2D[0][1].astype(complex) + 0.3j],
|
|
[FDFD_DXES_2D[1][0].astype(complex) + 0.4j, FDFD_DXES_2D[1][1].astype(complex) + 0.5j],
|
|
]
|
|
|
|
def fake_get_s_fdfd(*args, **kwargs) -> tuple[numpy.ndarray, dict]:
|
|
return numpy.eye(2, dtype=complex), {}
|
|
|
|
monkeypatch.setattr(waveguide_3d, 'get_s_fdfd', fake_get_s_fdfd)
|
|
|
|
kwargs = {**_small_fdfd_kwargs(), 'dxes_2d': dxes_2d}
|
|
_ss, diagnostics = waveguide_3d.get_s_fdfd_from_2d_ports(left, right, return_diagnostics=True, **kwargs)
|
|
|
|
assert numpy.iscomplexobj(diagnostics['epsilon'])
|
|
assert diagnostics['epsilon'][0, diagnostics['interface_index'], 0, 0].imag == pytest.approx(0.1)
|
|
assert numpy.iscomplexobj(diagnostics['dxes'][0][1])
|
|
assert diagnostics['dxes'][0][1][0].imag == pytest.approx(0.2)
|
|
|
|
|
|
def test_quarter_wave_matching_layer_is_nearly_reflectionless_at_design_frequency() -> None:
|
|
"""
|
|
A single quarter-wave matching layer with
|
|
|
|
n1 = sqrt(n0 * n2), beta1 * L = pi / 2
|
|
|
|
cancels the two interface reflections at the design wavelength, so the
|
|
normal-incidence stack should satisfy `r = 0` and `|t| = 1`.
|
|
"""
|
|
n0 = 1.0
|
|
n1 = numpy.sqrt(2.0)
|
|
n2 = 2.0
|
|
interface_01 = _interface_abcd(n0, n1)
|
|
interface_12 = _interface_abcd(n1, n2)
|
|
_mode_1, beta_1 = _build_uniform_mode(float(n1))
|
|
quarter_wave_length = numpy.pi / (2 * numpy.real(beta_1))
|
|
|
|
stack_abcd = interface_12 @ _propagation_abcd(beta_1, quarter_wave_length) @ interface_01
|
|
ss = _abcd_to_s(stack_abcd)
|
|
|
|
assert ss.shape == (2, 2)
|
|
assert numpy.isfinite(ss).all()
|
|
assert abs(ss[0, 0]) < 1e-12
|
|
assert abs(ss[1, 1]) < 1e-12
|
|
assert abs(abs(ss[0, 1]) - 1.0) < 1e-12
|
|
assert abs(abs(ss[1, 0]) - 1.0) < 1e-12
|
|
assert numpy.linalg.svd(ss, compute_uv=False).max() <= 1.0 + 1e-10
|
|
|
|
|
|
def test_quarter_wave_ar_layer_reduces_reflection_relative_to_abrupt_interface() -> None:
|
|
"""
|
|
Compare the abrupt interface `n0 -> n2` against the quarter-wave matching-layer
|
|
stack `n0 -> sqrt(n0 n2) -> n2` at the same design wavelength.
|
|
|
|
For the canonical `n0 = 1`, `n2 = 2` case, the abrupt interface has
|
|
|
|
|r_abrupt| = |(n0 - n2) / (n0 + n2)| = 1 / 3,
|
|
|
|
while the quarter-wave matching layer should cancel the interface reflections
|
|
so that `|r_ar|` is essentially zero and `|t_ar|` is correspondingly larger.
|
|
"""
|
|
n0 = 1.0
|
|
n2 = 2.0
|
|
abrupt = _interface_s(n0, n2)
|
|
|
|
n1 = numpy.sqrt(n0 * n2)
|
|
interface_01 = _interface_abcd(n0, n1)
|
|
interface_12 = _interface_abcd(n1, n2)
|
|
_mode_1, beta_1 = _build_uniform_mode(float(n1))
|
|
quarter_wave_length = numpy.pi / (2 * numpy.real(beta_1))
|
|
ar_stack = _abcd_to_s(interface_12 @ _propagation_abcd(beta_1, quarter_wave_length) @ interface_01)
|
|
|
|
abrupt_reflection = abs(abrupt[0, 0])
|
|
abrupt_transmission = abs(abrupt[1, 0])
|
|
ar_reflection = abs(ar_stack[0, 0])
|
|
ar_transmission = abs(ar_stack[1, 0])
|
|
|
|
assert numpy.linalg.svd(abrupt, compute_uv=False).max() <= 1.0 + 1e-10
|
|
assert numpy.linalg.svd(ar_stack, compute_uv=False).max() <= 1.0 + 1e-10
|
|
assert ar_reflection < abrupt_reflection
|
|
assert ar_transmission > abrupt_transmission
|
|
assert ar_reflection < 1e-12
|
|
assert abs(abrupt_reflection - (1.0 / 3.0)) < 1e-12
|
|
|
|
|
|
def test_half_wave_uniform_slab_restores_unit_transmission_between_matched_media() -> None:
|
|
"""
|
|
For matched exterior media `n0 = n2`, a half-wave slab with
|
|
|
|
beta1 * L = pi
|
|
|
|
contributes only a global phase, so the stack returns to `r = 0` and
|
|
`|t| = 1` at the design wavelength.
|
|
"""
|
|
n0 = 1.0
|
|
n1 = 2.0
|
|
interface_01 = _interface_abcd(n0, n1)
|
|
interface_10 = _interface_abcd(n1, n0)
|
|
_mode_1, beta_1 = _build_uniform_mode(n1)
|
|
half_wave_length = numpy.pi / numpy.real(beta_1)
|
|
|
|
stack_abcd = interface_10 @ _propagation_abcd(beta_1, half_wave_length) @ interface_01
|
|
ss = _abcd_to_s(stack_abcd)
|
|
|
|
assert ss.shape == (2, 2)
|
|
assert numpy.isfinite(ss).all()
|
|
assert abs(ss[0, 0]) < 1e-12
|
|
assert abs(ss[1, 1]) < 1e-12
|
|
assert abs(abs(ss[0, 1]) - 1.0) < 1e-12
|
|
assert abs(abs(ss[1, 0]) - 1.0) < 1e-12
|
|
assert numpy.linalg.svd(ss, compute_uv=False).max() <= 1.0 + 1e-10
|
|
|
|
|
|
def test_strong_uniform_index_mismatch_behaves_like_near_termination() -> None:
|
|
"""
|
|
In the large-index-ratio limit, the same Fresnel formulas approach a hard-wall
|
|
reflector:
|
|
|
|
|r| -> 1, |t| -> 0 as n_right / n_left -> infinity.
|
|
"""
|
|
ss = _interface_s(1.0, 100.0)
|
|
expected = _expected_interface_s(1.0, 100.0)
|
|
|
|
assert ss.shape == (2, 2)
|
|
assert numpy.isfinite(ss).all()
|
|
assert_close(ss, expected, atol=1e-6, rtol=1e-6)
|
|
assert abs(ss[0, 0]) > 0.9
|
|
assert abs(ss[1, 0]) < 0.25
|
|
assert numpy.linalg.svd(ss, compute_uv=False).max() <= 1.0 + 1e-10
|
|
|
|
|
|
def test_quarter_wave_bragg_reflector_matches_closed_form_stopband_response() -> None:
|
|
"""
|
|
For `N` quarter-wave high/low periods at the Bragg wavelength with identical
|
|
low-index incident and exit media (`n0 = ns = n_low`),
|
|
|
|
M_pair = diag(-(n_low / n_high), -(n_high / n_low))
|
|
M_stack = M_pair ** N
|
|
|
|
which yields the closed-form scattering amplitudes
|
|
|
|
r = (1 - (n_high / n_low) ** (2N)) / (1 + (n_high / n_low) ** (2N))
|
|
t = 2 * (n_high / n_low) ** N / (1 + (n_high / n_low) ** (2N)).
|
|
|
|
The reflector should therefore sit deep in the stopband with `|r|` near 1 and
|
|
`|t|` correspondingly small.
|
|
"""
|
|
n_low = 1.0
|
|
n_high = 2.0
|
|
periods = 5
|
|
interface_lh = _interface_abcd(n_low, n_high)
|
|
interface_hl = _interface_abcd(n_high, n_low)
|
|
_mode_h, beta_h = _build_uniform_mode(n_high)
|
|
_mode_l, beta_l = _build_uniform_mode(n_low)
|
|
quarter_wave_high = numpy.pi / (2 * numpy.real(beta_h))
|
|
quarter_wave_low = numpy.pi / (2 * numpy.real(beta_l))
|
|
|
|
stack_abcd = numpy.eye(2, dtype=complex)
|
|
for _ in range(periods):
|
|
stack_abcd = _propagation_abcd(beta_l, quarter_wave_low) @ interface_hl @ _propagation_abcd(beta_h, quarter_wave_high) @ interface_lh @ stack_abcd
|
|
ss = _abcd_to_s(stack_abcd)
|
|
expected = _expected_bragg_reflector_s(n_low, n_high, periods)
|
|
|
|
assert ss.shape == (2, 2)
|
|
assert numpy.isfinite(ss).all()
|
|
assert_close(ss, expected, atol=1e-12, rtol=1e-12)
|
|
assert abs(ss[0, 0]) > 0.99
|
|
assert abs(ss[1, 0]) < 0.1
|
|
assert numpy.linalg.svd(ss, compute_uv=False).max() <= 1.0 + 1e-10
|