[EME] add more docs and tests
This commit is contained in:
parent
8b67696d7f
commit
1a2c6ab524
2 changed files with 201 additions and 1 deletions
|
|
@ -1,3 +1,24 @@
|
|||
"""
|
||||
Low-level mode-matching helpers for waveguide / EME workflows.
|
||||
|
||||
These helpers operate on already-solved and already-normalized port fields.
|
||||
They do not build geometries or solve modes themselves; downstream users are
|
||||
expected to supply compatible `(E, H)` modal field pairs from
|
||||
`waveguide_2d`, `waveguide_3d`, or `waveguide_cyl`.
|
||||
|
||||
The returned matrices follow the usual port ordering:
|
||||
|
||||
- `get_tr(...)` returns `(T, R)` for left-incident modes.
|
||||
- `get_abcd(...)` returns the 2-port block transfer matrix built from the two
|
||||
directional `T/R` solves.
|
||||
- `get_s(...)` returns the full block scattering matrix
|
||||
`[[R12, T12], [T21, R21]]`.
|
||||
|
||||
This module is intentionally a thin library layer rather than an integrated
|
||||
simulation suite. It provides the overlap algebra that downstream users can
|
||||
compose into larger workflows.
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
import numpy
|
||||
from numpy.typing import NDArray
|
||||
|
|
@ -7,6 +28,37 @@ from ..fdmath import dx_lists2_t, vcfdfield2
|
|||
from .waveguide_2d import inner_product
|
||||
|
||||
|
||||
def _validate_port_modes(
|
||||
name: str,
|
||||
ehs: Sequence[Sequence[vcfdfield2]],
|
||||
wavenumbers: Sequence[complex],
|
||||
) -> tuple[tuple[int, ...], tuple[int, ...]]:
|
||||
if len(ehs) != len(wavenumbers):
|
||||
raise ValueError(f'{name} mode list and wavenumber list must have the same length')
|
||||
if not ehs:
|
||||
raise ValueError(f'{name} must contain at least one mode')
|
||||
|
||||
e_shape: tuple[int, ...] | None = None
|
||||
h_shape: tuple[int, ...] | None = None
|
||||
for index, mode in enumerate(ehs):
|
||||
if len(mode) != 2:
|
||||
raise ValueError(f'{name}[{index}] must be a 2-tuple of (E, H) modal fields')
|
||||
e_field, h_field = mode
|
||||
mode_e_shape = numpy.shape(e_field)
|
||||
mode_h_shape = numpy.shape(h_field)
|
||||
if mode_e_shape != mode_h_shape:
|
||||
raise ValueError(f'{name}[{index}] has mismatched E/H field shapes')
|
||||
if e_shape is None:
|
||||
e_shape = mode_e_shape
|
||||
h_shape = mode_h_shape
|
||||
elif mode_e_shape != e_shape or mode_h_shape != h_shape:
|
||||
raise ValueError(f'{name} modal fields must all share the same shape')
|
||||
|
||||
assert e_shape is not None
|
||||
assert h_shape is not None
|
||||
return e_shape, h_shape
|
||||
|
||||
|
||||
def get_tr(
|
||||
ehLs: Sequence[Sequence[vcfdfield2]],
|
||||
wavenumbers_L: Sequence[complex],
|
||||
|
|
@ -14,6 +66,29 @@ def get_tr(
|
|||
wavenumbers_R: Sequence[complex],
|
||||
dxes: dx_lists2_t,
|
||||
) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]:
|
||||
"""
|
||||
Compute left-incident transmission and reflection matrices.
|
||||
|
||||
Args:
|
||||
ehLs: Left-port modes as `(E, H)` field pairs.
|
||||
wavenumbers_L: Propagation constants for `ehLs`.
|
||||
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.
|
||||
|
||||
Returns:
|
||||
`(T12, R12)` where columns index left-incident modes and rows index
|
||||
outgoing right-going / left-going modes respectively.
|
||||
|
||||
Raises:
|
||||
ValueError: If the port mode lists are empty, malformed, or defined on
|
||||
incompatible field shapes.
|
||||
"""
|
||||
left_e_shape, left_h_shape = _validate_port_modes('ehLs', ehLs, wavenumbers_L)
|
||||
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')
|
||||
|
||||
nL = len(wavenumbers_L)
|
||||
nR = len(wavenumbers_R)
|
||||
A12 = numpy.zeros((nL, nR), dtype=complex)
|
||||
|
|
@ -48,6 +123,16 @@ def get_abcd(
|
|||
wavenumbers_R: Sequence[complex],
|
||||
**kwargs,
|
||||
) -> sparse.sparray:
|
||||
"""
|
||||
Build the 2-port block transfer matrix for an interface.
|
||||
|
||||
The blocks are assembled from the forward and reverse `get_tr(...)`
|
||||
solutions using the standard
|
||||
|
||||
`[[A, B], [C, D]] = [[T12 - R21 T21^-1 R12, R21 T21^-1], [-T21^-1 R12, T21^-1]]`
|
||||
|
||||
convention.
|
||||
"""
|
||||
t12, r12 = get_tr(ehLs, wavenumbers_L, ehRs, wavenumbers_R, **kwargs)
|
||||
t21, r21 = get_tr(ehRs, wavenumbers_R, ehLs, wavenumbers_L, **kwargs)
|
||||
t21i = numpy.linalg.pinv(t21)
|
||||
|
|
@ -73,6 +158,19 @@ def get_s(
|
|||
force_reciprocal: bool = False,
|
||||
**kwargs,
|
||||
) -> NDArray[numpy.complex128]:
|
||||
"""
|
||||
Build the full block scattering matrix for a two-sided interface.
|
||||
|
||||
The returned matrix is ordered as `[[R12, T12], [T21, R21]]`, where the
|
||||
first block-row/column corresponds to the left port and the second to the
|
||||
right port.
|
||||
|
||||
Args:
|
||||
force_nogain: If `True`, clamp singular values of the assembled
|
||||
scattering matrix to at most one.
|
||||
force_reciprocal: If `True`, symmetrize the assembled matrix as
|
||||
`0.5 * (S + S.T)`.
|
||||
"""
|
||||
t12, r12 = get_tr(ehLs, wavenumbers_L, ehRs, wavenumbers_R, **kwargs)
|
||||
t21, r21 = get_tr(ehRs, wavenumbers_R, ehLs, wavenumbers_L, **kwargs)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue