[fdfd.eme] enable using leaky modes
This commit is contained in:
parent
7e6363ea04
commit
444ae49a74
2 changed files with 244 additions and 24 deletions
|
|
@ -49,6 +49,11 @@ class ModeSection:
|
|||
modes are synthesized as `(E, -H)`.
|
||||
backward_wavenumbers: Optional propagation constants for
|
||||
`backward_modes`. If omitted, they are synthesized as `-wavenumbers`.
|
||||
dual_modes: Optional forward dual / adjoint projection modes. If
|
||||
omitted, `modes` are used as their own projection basis.
|
||||
dual_backward_modes: Optional backward dual / adjoint projection modes.
|
||||
If omitted, they are synthesized from `dual_modes` when available,
|
||||
otherwise from `backward_modes`.
|
||||
"""
|
||||
|
||||
z: float
|
||||
|
|
@ -56,6 +61,8 @@ class ModeSection:
|
|||
wavenumbers: wavenumber_seq
|
||||
backward_modes: mode_seq | None = None
|
||||
backward_wavenumbers: wavenumber_seq | None = None
|
||||
dual_modes: mode_seq | None = None
|
||||
dual_backward_modes: mode_seq | None = None
|
||||
|
||||
|
||||
def _validate_port_modes(
|
||||
|
|
@ -89,6 +96,21 @@ def _validate_port_modes(
|
|||
return e_shape, h_shape
|
||||
|
||||
|
||||
def _validate_dual_modes(
|
||||
name: str,
|
||||
dual_ehs: mode_seq | None,
|
||||
reference_shape: tuple[int, ...],
|
||||
wavenumbers: wavenumber_seq,
|
||||
) -> mode_seq | None:
|
||||
if dual_ehs is None:
|
||||
return None
|
||||
|
||||
dual_e_shape, dual_h_shape = _validate_port_modes(name, dual_ehs, wavenumbers)
|
||||
if dual_e_shape != reference_shape or dual_h_shape != reference_shape:
|
||||
raise ValueError(f'{name} modal fields must share the same E/H shapes as the corresponding modes')
|
||||
return dual_ehs
|
||||
|
||||
|
||||
def _as_wavenumber_array(
|
||||
name: str,
|
||||
wavenumbers: wavenumber_seq,
|
||||
|
|
@ -140,15 +162,17 @@ def _phase_align_modes(
|
|||
previous: Sequence[tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]],
|
||||
current: Sequence[tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]],
|
||||
dxes: dx_lists2_t,
|
||||
previous_dual: Sequence[tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]] | None = None,
|
||||
) -> list[tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]]:
|
||||
aligned = []
|
||||
for index, (previous_mode, current_mode) in enumerate(zip(previous, current, strict=True)):
|
||||
overlap = _lorentz_overlap(previous_mode, current_mode, dxes)
|
||||
self_overlap = _lorentz_overlap(previous_mode, previous_mode, dxes)
|
||||
test_modes = previous if previous_dual is None else previous_dual
|
||||
for index, (previous_mode, current_mode, test_mode) in enumerate(zip(previous, current, test_modes, strict=True)):
|
||||
overlap = _lorentz_overlap(test_mode, current_mode, dxes)
|
||||
self_overlap = _lorentz_overlap(test_mode, previous_mode, dxes)
|
||||
if overlap == 0:
|
||||
raise ValueError(f'cannot phase-track mode {index}: adjacent section overlap is zero')
|
||||
if self_overlap == 0:
|
||||
raise ValueError(f'cannot phase-track mode {index}: mode self-overlap is zero')
|
||||
raise ValueError(f'cannot phase-track mode {index}: mode dual-overlap is zero')
|
||||
phase = (overlap / abs(overlap)) / (self_overlap / abs(self_overlap))
|
||||
e_field, h_field = current_mode
|
||||
aligned.append((e_field / phase, h_field / phase))
|
||||
|
|
@ -166,6 +190,8 @@ def _section_branches(
|
|||
NDArray[numpy.complex128],
|
||||
list[tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]],
|
||||
NDArray[numpy.complex128],
|
||||
list[tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]],
|
||||
list[tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]],
|
||||
tuple[int, ...],
|
||||
]:
|
||||
z_coord = float(section.z)
|
||||
|
|
@ -203,7 +229,37 @@ def _section_branches(
|
|||
if len(backward_wavenumbers) != len(wavenumbers):
|
||||
raise ValueError('forward and backward mode counts must match')
|
||||
|
||||
return z_coord, forward_modes, wavenumbers, backward_modes, backward_wavenumbers, shape
|
||||
if section.dual_modes is None:
|
||||
dual_modes = forward_modes
|
||||
else:
|
||||
dual_shape, _dual_h_shape = _validate_port_modes(
|
||||
f'sections[{index}].dual_modes',
|
||||
section.dual_modes,
|
||||
section.wavenumbers,
|
||||
)
|
||||
if dual_shape != shape:
|
||||
raise ValueError('modal fields and dual modal fields must share the same E/H shapes')
|
||||
dual_modes = _as_mode_arrays(section.dual_modes)
|
||||
|
||||
if section.dual_backward_modes is None:
|
||||
if section.dual_modes is None and section.backward_modes is not None:
|
||||
dual_backward_modes = backward_modes
|
||||
else:
|
||||
dual_backward_modes = [(e_field.copy(), -h_field.copy()) for e_field, h_field in dual_modes]
|
||||
else:
|
||||
dual_backward_shape, _dual_backward_h_shape = _validate_port_modes(
|
||||
f'sections[{index}].dual_backward_modes',
|
||||
section.dual_backward_modes,
|
||||
section.backward_wavenumbers if section.backward_wavenumbers is not None else backward_wavenumbers,
|
||||
)
|
||||
if dual_backward_shape != shape:
|
||||
raise ValueError('backward modal fields and dual backward modal fields must share the same E/H shapes')
|
||||
dual_backward_modes = _as_mode_arrays(section.dual_backward_modes)
|
||||
|
||||
if len(dual_modes) != len(forward_modes) or len(dual_backward_modes) != len(backward_modes):
|
||||
raise ValueError('dual mode counts must match the corresponding modal basis counts')
|
||||
|
||||
return z_coord, forward_modes, wavenumbers, backward_modes, backward_wavenumbers, dual_modes, dual_backward_modes, shape
|
||||
|
||||
|
||||
def _validate_taper_sections(
|
||||
|
|
@ -212,6 +268,7 @@ def _validate_taper_sections(
|
|||
) -> tuple[
|
||||
NDArray[numpy.float64],
|
||||
list[list[tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]]],
|
||||
list[list[tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]]],
|
||||
list[NDArray[numpy.complex128]],
|
||||
int,
|
||||
]:
|
||||
|
|
@ -220,12 +277,14 @@ def _validate_taper_sections(
|
|||
|
||||
z_coords: list[float] = []
|
||||
branch_modes: list[list[tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]]] = []
|
||||
branch_dual_modes: list[list[tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]]] = []
|
||||
branch_wavenumbers: list[NDArray[numpy.complex128]] = []
|
||||
explicit_duals: list[bool] = []
|
||||
expected_count: int | None = None
|
||||
expected_shape: tuple[int, ...] | None = None
|
||||
|
||||
for index, section in enumerate(sections):
|
||||
z_coord, forward_modes, forward_wavenumbers, backward_modes, backward_wavenumbers, shape = _section_branches(
|
||||
z_coord, forward_modes, forward_wavenumbers, backward_modes, backward_wavenumbers, dual_modes, dual_backward_modes, shape = _section_branches(
|
||||
section,
|
||||
index,
|
||||
expected_count,
|
||||
|
|
@ -236,21 +295,26 @@ def _validate_taper_sections(
|
|||
expected_shape = shape
|
||||
z_coords.append(z_coord)
|
||||
branch_modes.append([*forward_modes, *backward_modes])
|
||||
branch_dual_modes.append([*dual_modes, *dual_backward_modes])
|
||||
branch_wavenumbers.append(numpy.concatenate((forward_wavenumbers, backward_wavenumbers)))
|
||||
explicit_duals.append(section.dual_modes is not None or section.dual_backward_modes is not None)
|
||||
|
||||
z_array = numpy.asarray(z_coords, dtype=float)
|
||||
if not (numpy.diff(z_array) > 0).all():
|
||||
raise ValueError('taper section z coordinates must be strictly increasing')
|
||||
|
||||
for index in range(1, len(branch_modes)):
|
||||
branch_modes[index] = _phase_align_modes(branch_modes[index - 1], branch_modes[index], dxes)
|
||||
branch_modes[index] = _phase_align_modes(branch_modes[index - 1], branch_modes[index], dxes, branch_dual_modes[index - 1])
|
||||
if not explicit_duals[index]:
|
||||
branch_dual_modes[index] = branch_modes[index]
|
||||
|
||||
assert expected_count is not None
|
||||
return z_array, branch_modes, branch_wavenumbers, expected_count
|
||||
return z_array, branch_modes, branch_dual_modes, branch_wavenumbers, expected_count
|
||||
|
||||
|
||||
def _taper_interval_generator(
|
||||
left_modes: Sequence[tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]],
|
||||
left_dual_modes: Sequence[tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]],
|
||||
right_modes: Sequence[tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]],
|
||||
left_wavenumbers: NDArray[numpy.complex128],
|
||||
right_wavenumbers: NDArray[numpy.complex128],
|
||||
|
|
@ -261,7 +325,7 @@ def _taper_interval_generator(
|
|||
gram = numpy.zeros((mode_count, mode_count), dtype=complex)
|
||||
derivative_overlap = numpy.zeros((mode_count, mode_count), dtype=complex)
|
||||
|
||||
for row, left_row_mode in enumerate(left_modes):
|
||||
for row, left_row_mode in enumerate(left_dual_modes):
|
||||
for col, left_col_mode in enumerate(left_modes):
|
||||
gram[row, col] = _lorentz_overlap(left_row_mode, left_col_mode, dxes)
|
||||
for col, (left_col_mode, right_col_mode) in enumerate(zip(left_modes, right_modes, strict=True)):
|
||||
|
|
@ -294,11 +358,12 @@ def _abcd_to_s(
|
|||
|
||||
|
||||
def get_tr(
|
||||
ehLs: Sequence[Sequence[vcfdfield2]],
|
||||
ehLs: mode_seq,
|
||||
wavenumbers_L: wavenumber_seq,
|
||||
ehRs: Sequence[Sequence[vcfdfield2]],
|
||||
ehRs: mode_seq,
|
||||
wavenumbers_R: wavenumber_seq,
|
||||
dxes: dx_lists2_t,
|
||||
dual_ehLs: mode_seq | None = None,
|
||||
) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]:
|
||||
"""
|
||||
Compute left-incident transmission and reflection matrices.
|
||||
|
|
@ -309,6 +374,8 @@ def get_tr(
|
|||
ehRs: Right-port modes as `(E, H)` field pairs.
|
||||
wavenumbers_R: Propagation constants for `ehRs`.
|
||||
dxes: Two-dimensional Yee-cell edge lengths for the shared port plane.
|
||||
dual_ehLs: Optional left-port dual / adjoint projection modes. If
|
||||
omitted, `ehLs` are used as their own projection basis.
|
||||
|
||||
Returns:
|
||||
`(T12, R12)` where columns index left-incident modes and rows index
|
||||
|
|
@ -322,6 +389,8 @@ def get_tr(
|
|||
right_e_shape, right_h_shape = _validate_port_modes('ehRs', ehRs, wavenumbers_R)
|
||||
if left_e_shape != right_e_shape or left_h_shape != right_h_shape:
|
||||
raise ValueError('left and right modal fields must share the same E/H shapes')
|
||||
dual_projection_ehLs = _validate_dual_modes('dual_ehLs', dual_ehLs, left_e_shape, wavenumbers_L)
|
||||
projection_ehLs = ehLs if dual_projection_ehLs is None else dual_projection_ehLs
|
||||
|
||||
nL = len(wavenumbers_L)
|
||||
nR = len(wavenumbers_R)
|
||||
|
|
@ -330,11 +399,12 @@ def get_tr(
|
|||
B11 = numpy.zeros((nL,), dtype=complex)
|
||||
for ll in range(nL):
|
||||
eL, hL = ehLs[ll]
|
||||
B11[ll] = inner_product(eL, hL, dxes=dxes, conj_h=False)
|
||||
eP, hP = projection_ehLs[ll]
|
||||
B11[ll] = inner_product(eL, hP, dxes=dxes, conj_h=False)
|
||||
for rr in range(nR):
|
||||
eR, hR = ehRs[rr]
|
||||
A12[ll, rr] = inner_product(eL, hR, dxes=dxes, conj_h=False) # TODO optimize loop?
|
||||
A21[ll, rr] = inner_product(eR, hL, dxes=dxes, conj_h=False)
|
||||
A12[ll, rr] = inner_product(eP, hR, dxes=dxes, conj_h=False) # TODO optimize loop?
|
||||
A21[ll, rr] = inner_product(eR, hP, dxes=dxes, conj_h=False)
|
||||
|
||||
# tt0 = 2 * numpy.linalg.pinv(A21 + numpy.conj(A12))
|
||||
tt0, _resid, _rank, _sing = numpy.linalg.lstsq(A21 + A12, numpy.diag(2 * B11), rcond=None)
|
||||
|
|
@ -351,10 +421,12 @@ def get_tr(
|
|||
|
||||
|
||||
def get_abcd(
|
||||
ehLs: Sequence[Sequence[vcfdfield2]],
|
||||
ehLs: mode_seq,
|
||||
wavenumbers_L: wavenumber_seq,
|
||||
ehRs: Sequence[Sequence[vcfdfield2]],
|
||||
ehRs: mode_seq,
|
||||
wavenumbers_R: wavenumber_seq,
|
||||
dual_ehLs: mode_seq | None = None,
|
||||
dual_ehRs: mode_seq | None = None,
|
||||
**kwargs,
|
||||
) -> sparse.sparray:
|
||||
"""
|
||||
|
|
@ -367,8 +439,8 @@ def get_abcd(
|
|||
|
||||
convention.
|
||||
"""
|
||||
t12, r12 = get_tr(ehLs, wavenumbers_L, ehRs, wavenumbers_R, **kwargs)
|
||||
t21, r21 = get_tr(ehRs, wavenumbers_R, ehLs, wavenumbers_L, **kwargs)
|
||||
t12, r12 = get_tr(ehLs, wavenumbers_L, ehRs, wavenumbers_R, dual_ehLs=dual_ehLs, **kwargs)
|
||||
t21, r21 = get_tr(ehRs, wavenumbers_R, ehLs, wavenumbers_L, dual_ehLs=dual_ehRs, **kwargs)
|
||||
t21i = numpy.linalg.pinv(t21)
|
||||
A = t12 - r21 @ t21i @ r12
|
||||
B = r21 @ t21i
|
||||
|
|
@ -384,12 +456,14 @@ def get_abcd(
|
|||
|
||||
|
||||
def get_s(
|
||||
ehLs: Sequence[Sequence[vcfdfield2]],
|
||||
ehLs: mode_seq,
|
||||
wavenumbers_L: wavenumber_seq,
|
||||
ehRs: Sequence[Sequence[vcfdfield2]],
|
||||
ehRs: mode_seq,
|
||||
wavenumbers_R: wavenumber_seq,
|
||||
force_nogain: bool = False,
|
||||
force_reciprocal: bool = False,
|
||||
dual_ehLs: mode_seq | None = None,
|
||||
dual_ehRs: mode_seq | None = None,
|
||||
**kwargs,
|
||||
) -> NDArray[numpy.complex128]:
|
||||
"""
|
||||
|
|
@ -404,9 +478,11 @@ def get_s(
|
|||
scattering matrix to at most one.
|
||||
force_reciprocal: If `True`, symmetrize the assembled matrix as
|
||||
`0.5 * (S + S.T)`.
|
||||
dual_ehLs: Optional left-port dual / adjoint projection modes.
|
||||
dual_ehRs: Optional right-port dual / adjoint projection modes.
|
||||
"""
|
||||
t12, r12 = get_tr(ehLs, wavenumbers_L, ehRs, wavenumbers_R, **kwargs)
|
||||
t21, r21 = get_tr(ehRs, wavenumbers_R, ehLs, wavenumbers_L, **kwargs)
|
||||
t12, r12 = get_tr(ehLs, wavenumbers_L, ehRs, wavenumbers_R, dual_ehLs=dual_ehLs, **kwargs)
|
||||
t21, r21 = get_tr(ehRs, wavenumbers_R, ehLs, wavenumbers_L, dual_ehLs=dual_ehRs, **kwargs)
|
||||
|
||||
ss = numpy.block([[r12, t12],
|
||||
[t21, r21]])
|
||||
|
|
@ -437,6 +513,9 @@ def get_taper_abcd(
|
|||
coordinates. Adjacent modal phases are tracked with the same non-conjugated
|
||||
Lorentz/Poynting overlap used by the abrupt-interface helpers, then each
|
||||
interval is propagated with a finite-difference local-mode CMT generator.
|
||||
If a `ModeSection` supplies dual / adjoint modes, those modes are used for
|
||||
the CMT projection. This supports leaky or radiative mode sets whose natural
|
||||
projection basis is biorthogonal rather than self-projected.
|
||||
|
||||
Args:
|
||||
sections: Local modal samples ordered by increasing `z`.
|
||||
|
|
@ -459,13 +538,14 @@ def get_taper_abcd(
|
|||
if max_step is not None and max_step <= 0:
|
||||
raise ValueError('max_step must be positive')
|
||||
|
||||
z_coords, branch_modes, branch_wavenumbers, n_modes = _validate_taper_sections(sections, dxes)
|
||||
z_coords, branch_modes, branch_dual_modes, branch_wavenumbers, n_modes = _validate_taper_sections(sections, dxes)
|
||||
branch_count = 2 * n_modes
|
||||
transfer = numpy.eye(branch_count, dtype=complex)
|
||||
|
||||
for index, dz in enumerate(numpy.diff(z_coords)):
|
||||
generator = _taper_interval_generator(
|
||||
branch_modes[index],
|
||||
branch_dual_modes[index],
|
||||
branch_modes[index + 1],
|
||||
branch_wavenumbers[index],
|
||||
branch_wavenumbers[index + 1],
|
||||
|
|
@ -494,7 +574,7 @@ def get_taper_s(
|
|||
The returned matrix uses the same ordering as `get_s(...)`:
|
||||
`[[R12, T12], [T21, R21]]`.
|
||||
"""
|
||||
_z_coords, _branch_modes, _branch_wavenumbers, n_modes = _validate_taper_sections(sections, dxes)
|
||||
_z_coords, _branch_modes, _branch_dual_modes, _branch_wavenumbers, n_modes = _validate_taper_sections(sections, dxes)
|
||||
abcd = get_taper_abcd(sections, dxes, **kwargs).toarray()
|
||||
ss = _abcd_to_s(abcd, n_modes)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue