[docs] expand API and derivation docs
This commit is contained in:
parent
0568e1ba50
commit
5e95d66a7e
12 changed files with 608 additions and 127 deletions
|
|
@ -9,9 +9,12 @@ Submodules:
|
|||
|
||||
- `operators`, `functional`: General FDFD problem setup.
|
||||
- `solvers`: Solver interface and reference implementation.
|
||||
- `scpml`: Stretched-coordinate perfectly matched layer (scpml) boundary conditions
|
||||
- `scpml`: Stretched-coordinate perfectly matched layer (SCPML) boundary conditions.
|
||||
- `waveguide_2d`: Operators and mode-solver for waveguides with constant cross-section.
|
||||
- `waveguide_3d`: Functions for transforming `waveguide_2d` results into 3D.
|
||||
- `waveguide_3d`: Functions for transforming `waveguide_2d` results into 3D,
|
||||
including mode-source and overlap-window construction.
|
||||
- `farfield`, `bloch`, `eme`: specialized helper modules for near/far transforms,
|
||||
Bloch-periodic problems, and eigenmode expansion.
|
||||
|
||||
|
||||
================================================================
|
||||
|
|
@ -86,10 +89,6 @@ $$
|
|||
-\omega^2 \epsilon_{\vec{r}} \cdot \tilde{E}_{\vec{r}} = -\imath \omega \tilde{J}_{\vec{r}} \\
|
||||
$$
|
||||
|
||||
# TODO FDFD?
|
||||
# TODO PML
|
||||
|
||||
|
||||
"""
|
||||
from . import (
|
||||
solvers as solvers,
|
||||
|
|
|
|||
|
|
@ -158,10 +158,23 @@ def e_tfsf_source(
|
|||
epsilon: fdfield,
|
||||
mu: fdfield | None = None,
|
||||
) -> cfdfield_updater_t:
|
||||
"""
|
||||
r"""
|
||||
Operator that turns an E-field distribution into a total-field/scattered-field
|
||||
(TFSF) source.
|
||||
|
||||
If `A` is the full wave operator from `e_full(...)` and `Q` is the diagonal
|
||||
mask selecting the total-field region, then the TFSF source is the commutator
|
||||
|
||||
$$
|
||||
\frac{A Q - Q A}{-i \omega} E.
|
||||
$$
|
||||
|
||||
This vanishes in the interior of the total-field and scattered-field regions
|
||||
and is supported only at their shared boundary, where the mask discontinuity
|
||||
makes `A` and `Q` fail to commute. The returned current is therefore the
|
||||
distributed source needed to inject the desired total field without also
|
||||
forcing the scattered-field region.
|
||||
|
||||
Args:
|
||||
TF_region: mask which is set to 1 in the total-field region, and 0 elsewhere
|
||||
(i.e. in the scattered-field region).
|
||||
|
|
@ -175,7 +188,6 @@ def e_tfsf_source(
|
|||
Function `f` which takes an E field and returns a current distribution,
|
||||
`f(E)` -> `J`
|
||||
"""
|
||||
# TODO documentation
|
||||
A = e_full(omega, dxes, epsilon, mu)
|
||||
|
||||
def op(e: cfdfield) -> cfdfield_t:
|
||||
|
|
@ -188,7 +200,13 @@ def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield, cfdfield], cfdfi
|
|||
r"""
|
||||
Generates a function that takes the single-frequency `E` and `H` fields
|
||||
and calculates the cross product `E` x `H` = $E \times H$ as required
|
||||
for the Poynting vector, $S = E \times H$
|
||||
for the Poynting vector, $S = E \times H$.
|
||||
|
||||
On the Yee grid, the electric and magnetic components are not stored at the
|
||||
same locations. This helper therefore applies the same one-cell electric-field
|
||||
shifts used by the sparse `operators.poynting_e_cross(...)` construction so
|
||||
that the discrete cross product matches the face-centered energy flux used in
|
||||
`meanas.fdtd.energy.poynting(...)`.
|
||||
|
||||
Note:
|
||||
This function also shifts the input `E` field by one cell as required
|
||||
|
|
@ -204,7 +222,8 @@ def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield, cfdfield], cfdfi
|
|||
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types`
|
||||
|
||||
Returns:
|
||||
Function `f` that returns E x H as required for the poynting vector.
|
||||
Function `f` that returns the staggered-grid cross product `E \times H`.
|
||||
For time-average power, call it as `f(E, H.conj())` and take `Re(...) / 2`.
|
||||
"""
|
||||
def exh(e: cfdfield, h: cfdfield) -> cfdfield_t:
|
||||
s = numpy.empty_like(e)
|
||||
|
|
|
|||
|
|
@ -310,16 +310,22 @@ def m2j(
|
|||
|
||||
|
||||
def poynting_e_cross(e: vcfdfield, dxes: dx_lists_t) -> sparse.sparray:
|
||||
"""
|
||||
Operator for computing the Poynting vector, containing the
|
||||
(E x) portion of the Poynting vector.
|
||||
r"""
|
||||
Operator for computing the staggered-grid `(E \times)` part of the Poynting vector.
|
||||
|
||||
On the Yee grid the E and H components live on different edges, so the
|
||||
electric field must be shifted by one cell in the appropriate direction
|
||||
before forming the discrete cross product. This sparse operator encodes that
|
||||
shifted cross product directly and is the matrix equivalent of
|
||||
`functional.poynting_e_cross_h(...)`.
|
||||
|
||||
Args:
|
||||
e: Vectorized E-field for the ExH cross product
|
||||
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types`
|
||||
|
||||
Returns:
|
||||
Sparse matrix containing (E x) portion of Poynting cross product.
|
||||
Sparse matrix containing the `(E \times)` part of the staggered Poynting
|
||||
cross product.
|
||||
"""
|
||||
shape = [len(dx) for dx in dxes[0]]
|
||||
|
||||
|
|
@ -339,15 +345,26 @@ def poynting_e_cross(e: vcfdfield, dxes: dx_lists_t) -> sparse.sparray:
|
|||
|
||||
|
||||
def poynting_h_cross(h: vcfdfield, dxes: dx_lists_t) -> sparse.sparray:
|
||||
"""
|
||||
Operator for computing the Poynting vector, containing the (H x) portion of the Poynting vector.
|
||||
r"""
|
||||
Operator for computing the staggered-grid `(H \times)` part of the Poynting vector.
|
||||
|
||||
Together with `poynting_e_cross(...)`, this provides the matrix form of the
|
||||
Yee-grid cross product used in the flux helpers. The two are related by the
|
||||
usual antisymmetry of the cross product,
|
||||
|
||||
$$
|
||||
H \times E = -(E \times H),
|
||||
$$
|
||||
|
||||
once the same staggered field placement is used on both sides.
|
||||
|
||||
Args:
|
||||
h: Vectorized H-field for the HxE cross product
|
||||
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types`
|
||||
|
||||
Returns:
|
||||
Sparse matrix containing (H x) portion of Poynting cross product.
|
||||
Sparse matrix containing the `(H \times)` part of the staggered Poynting
|
||||
cross product.
|
||||
"""
|
||||
shape = [len(dx) for dx in dxes[0]]
|
||||
|
||||
|
|
@ -372,11 +389,23 @@ def e_tfsf_source(
|
|||
epsilon: vfdfield,
|
||||
mu: vfdfield | None = None,
|
||||
) -> sparse.sparray:
|
||||
"""
|
||||
r"""
|
||||
Operator that turns a desired E-field distribution into a
|
||||
total-field/scattered-field (TFSF) source.
|
||||
|
||||
TODO: Reference Rumpf paper
|
||||
Let `A` be the full wave operator from `e_full(...)`, and let
|
||||
`Q = \mathrm{diag}(TF_region)` be the projector onto the total-field region.
|
||||
Then the TFSF current operator is the commutator
|
||||
|
||||
$$
|
||||
\frac{A Q - Q A}{-i \omega}.
|
||||
$$
|
||||
|
||||
Inside regions where `Q` is locally constant, `A` and `Q` commute and the
|
||||
source vanishes. Only cells at the TF/SF boundary contribute nonzero current,
|
||||
which is exactly the desired distributed source for injecting the chosen
|
||||
field into the total-field region without directly forcing the
|
||||
scattered-field region.
|
||||
|
||||
Args:
|
||||
TF_region: Mask, which is set to 1 inside the total-field region and 0 in the
|
||||
|
|
@ -388,9 +417,7 @@ def e_tfsf_source(
|
|||
|
||||
Returns:
|
||||
Sparse matrix that turns an E-field into a current (J) distribution.
|
||||
|
||||
"""
|
||||
# TODO documentation
|
||||
A = e_full(omega, dxes, epsilon, mu)
|
||||
Q = sparse.diags_array(TF_region)
|
||||
return (A @ Q - Q @ A) / (-1j * omega)
|
||||
|
|
@ -404,11 +431,17 @@ def e_boundary_source(
|
|||
mu: vfdfield | None = None,
|
||||
periodic_mask_edges: bool = False,
|
||||
) -> sparse.sparray:
|
||||
"""
|
||||
r"""
|
||||
Operator that turns an E-field distrubtion into a current (J) distribution
|
||||
along the edges (external and internal) of the provided mask. This is just an
|
||||
`e_tfsf_source()` with an additional masking step.
|
||||
|
||||
Equivalently, this helper first constructs the TFSF commutator source for the
|
||||
full mask and then zeroes out all cells except the mask boundary. The
|
||||
boundary is defined as the set of cells whose mask value changes under a
|
||||
one-cell shift in any Cartesian direction. With `periodic_mask_edges=False`
|
||||
the shifts mirror at the domain boundary; with `True` they wrap periodically.
|
||||
|
||||
Args:
|
||||
mask: The current distribution is generated at the edges of the mask,
|
||||
i.e. any points where shifting the mask by one cell in any direction
|
||||
|
|
|
|||
|
|
@ -175,8 +175,6 @@ if the result is introduced into a space with a discretized z-axis.
|
|||
|
||||
|
||||
"""
|
||||
# TODO update module docs
|
||||
|
||||
from typing import Any
|
||||
from collections.abc import Sequence
|
||||
import numpy
|
||||
|
|
@ -339,7 +337,7 @@ def normalized_fields_e(
|
|||
mu: vfdslice | None = None,
|
||||
prop_phase: float = 0,
|
||||
) -> tuple[vcfdslice_t, vcfdslice_t]:
|
||||
"""
|
||||
r"""
|
||||
Given a vector `e_xy` containing the vectorized E_x and E_y fields,
|
||||
returns normalized, vectorized E and H fields for the system.
|
||||
|
||||
|
|
@ -357,6 +355,21 @@ def normalized_fields_e(
|
|||
Returns:
|
||||
`(e, h)`, where each field is vectorized, normalized,
|
||||
and contains all three vector components.
|
||||
|
||||
Notes:
|
||||
`e_xy` is only the transverse electric eigenvector. This helper first
|
||||
reconstructs the full three-component `E` and `H` fields with `exy2e(...)`
|
||||
and `exy2h(...)`, then normalizes them to unit forward power using
|
||||
`_normalized_fields(...)`.
|
||||
|
||||
The normalization target is
|
||||
|
||||
$$
|
||||
\Re\left[\mathrm{inner\_product}(e, h, \mathrm{conj\_h}=True)\right] = 1,
|
||||
$$
|
||||
|
||||
so the returned fields represent a unit-power forward mode under the
|
||||
discrete Yee-grid Poynting inner product.
|
||||
"""
|
||||
e = exy2e(wavenumber=wavenumber, dxes=dxes, epsilon=epsilon) @ e_xy
|
||||
h = exy2h(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ e_xy
|
||||
|
|
@ -374,7 +387,7 @@ def normalized_fields_h(
|
|||
mu: vfdslice | None = None,
|
||||
prop_phase: float = 0,
|
||||
) -> tuple[vcfdslice_t, vcfdslice_t]:
|
||||
"""
|
||||
r"""
|
||||
Given a vector `h_xy` containing the vectorized H_x and H_y fields,
|
||||
returns normalized, vectorized E and H fields for the system.
|
||||
|
||||
|
|
@ -392,6 +405,13 @@ def normalized_fields_h(
|
|||
Returns:
|
||||
`(e, h)`, where each field is vectorized, normalized,
|
||||
and contains all three vector components.
|
||||
|
||||
Notes:
|
||||
This is the `H_x/H_y` analogue of `normalized_fields_e(...)`. The final
|
||||
normalized mode should describe the same physical solution, but because
|
||||
the overall complex phase and sign are chosen heuristically,
|
||||
`normalized_fields_e(...)` and `normalized_fields_h(...)` need not return
|
||||
identical representatives for nearly symmetric modes.
|
||||
"""
|
||||
e = hxy2e(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ h_xy
|
||||
h = hxy2h(wavenumber=wavenumber, dxes=dxes, mu=mu) @ h_xy
|
||||
|
|
@ -409,7 +429,25 @@ def _normalized_fields(
|
|||
mu: vfdslice | None = None,
|
||||
prop_phase: float = 0,
|
||||
) -> tuple[vcfdslice_t, vcfdslice_t]:
|
||||
# TODO documentation
|
||||
r"""
|
||||
Normalize a reconstructed waveguide mode to unit forward power.
|
||||
|
||||
The eigenproblem solved by `solve_mode(s)` determines only the mode shape and
|
||||
propagation constant. The overall complex amplitude and sign are still free.
|
||||
This helper fixes those remaining degrees of freedom in two steps:
|
||||
|
||||
1. Compute the discrete longitudinal Poynting flux with
|
||||
`inner_product(e, h, conj_h=True)`, including the half-cell longitudinal
|
||||
phase adjustment controlled by `prop_phase`.
|
||||
2. Multiply both fields by a scalar chosen so that the real forward power is
|
||||
`1`, then choose a reproducible phase/sign representative by making a
|
||||
dominant-energy sample real and using a weighted quadrant sum to break
|
||||
mirror-symmetry ties.
|
||||
|
||||
The sign heuristic is intentionally pragmatic rather than fundamental: it is
|
||||
only there to make downstream tests and source/overlap construction choose a
|
||||
consistent representative when the physical mode is symmetric.
|
||||
"""
|
||||
shape = [s.size for s in dxes[0]]
|
||||
|
||||
# Find time-averaged Sz and normalize to it
|
||||
|
|
@ -921,7 +959,7 @@ def solve_mode(
|
|||
return vcfdfield2_t(e_xys[0]), wavenumbers[0]
|
||||
|
||||
|
||||
def inner_product( # TODO documentation
|
||||
def inner_product(
|
||||
e1: vcfdfield2,
|
||||
h2: vcfdfield2,
|
||||
dxes: dx_lists2_t,
|
||||
|
|
@ -929,6 +967,36 @@ def inner_product( # TODO documentation
|
|||
conj_h: bool = False,
|
||||
trapezoid: bool = False,
|
||||
) -> complex:
|
||||
r"""
|
||||
Compute the discrete waveguide overlap / Poynting inner product.
|
||||
|
||||
This is the 2D transverse integral corresponding to the time-averaged
|
||||
longitudinal Poynting flux,
|
||||
|
||||
$$
|
||||
\frac{1}{2}\int (E_x H_y - E_y H_x) \, dx \, dy
|
||||
$$
|
||||
|
||||
with the Yee-grid staggering and optional propagation-phase adjustment used
|
||||
by the waveguide helpers in this module.
|
||||
|
||||
Args:
|
||||
e1: Vectorized electric field, typically from `exy2e(...)` or
|
||||
`normalized_fields_e(...)`.
|
||||
h2: Vectorized magnetic field, typically from `hxy2h(...)`,
|
||||
`exy2h(...)`, or one of the normalization helpers.
|
||||
dxes: Two-dimensional Yee-grid spacing lists `[dx_e, dx_h]`.
|
||||
prop_phase: Phase advance over one propagation cell. This is used to
|
||||
shift the H field into the same longitudinal reference plane as the
|
||||
E field.
|
||||
conj_h: Whether to conjugate `h2` before forming the overlap. Use
|
||||
`True` for the usual time-averaged power normalization.
|
||||
trapezoid: Whether to use trapezoidal quadrature instead of the default
|
||||
rectangular Yee-cell sum.
|
||||
|
||||
Returns:
|
||||
Complex overlap / longitudinal power integral.
|
||||
"""
|
||||
|
||||
shape = [s.size for s in dxes[0]]
|
||||
|
||||
|
|
@ -951,5 +1019,3 @@ def inner_product( # TODO documentation
|
|||
Sz_b = E1[1] * H2[0] * dxes_real[0][0][:, None] * dxes_real[1][1][None, :]
|
||||
Sz = 0.5 * (Sz_a.sum() - Sz_b.sum())
|
||||
return Sz
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,21 @@ Tools for working with waveguide modes in 3D domains.
|
|||
|
||||
This module relies heavily on `waveguide_2d` and mostly just transforms
|
||||
its parameters into 2D equivalents and expands the results back into 3D.
|
||||
|
||||
The intended workflow is:
|
||||
|
||||
1. Select a single-cell slice normal to the propagation axis.
|
||||
2. Solve the corresponding 2D mode problem with `solve_mode(...)`.
|
||||
3. Turn that mode into a one-sided source with `compute_source(...)`.
|
||||
4. Build an overlap window with `compute_overlap_e(...)` for port readout.
|
||||
|
||||
`polarity` is part of the public convention throughout this module:
|
||||
|
||||
- `+1` means forward propagation toward increasing index along `axis`
|
||||
- `-1` means backward propagation toward decreasing index along `axis`
|
||||
|
||||
That same convention controls which side of the selected slice is used for the
|
||||
overlap window and how the expanded field is phased.
|
||||
"""
|
||||
from typing import Any, cast
|
||||
import warnings
|
||||
|
|
@ -26,9 +41,9 @@ def solve_mode(
|
|||
epsilon: fdfield,
|
||||
mu: fdfield | None = None,
|
||||
) -> dict[str, complex | NDArray[complexfloating]]:
|
||||
"""
|
||||
r"""
|
||||
Given a 3D grid, selects a slice from the grid and attempts to
|
||||
solve for an eigenmode propagating through that slice.
|
||||
solve for an eigenmode propagating through that slice.
|
||||
|
||||
Args:
|
||||
mode_number: Number of the mode, 0-indexed
|
||||
|
|
@ -37,19 +52,22 @@ def solve_mode(
|
|||
axis: Propagation axis (0=x, 1=y, 2=z)
|
||||
polarity: Propagation direction (+1 for +ve, -1 for -ve)
|
||||
slices: `epsilon[tuple(slices)]` is used to select the portion of the grid to use
|
||||
as the waveguide cross-section. `slices[axis]` should select only one item.
|
||||
as the waveguide cross-section. `slices[axis]` must select exactly one item.
|
||||
epsilon: Dielectric constant
|
||||
mu: Magnetic permeability (default 1 everywhere)
|
||||
|
||||
Returns:
|
||||
```
|
||||
{
|
||||
'E': NDArray[complexfloating],
|
||||
'H': NDArray[complexfloating],
|
||||
'wavenumber': complex,
|
||||
'wavenumber_2d': complex,
|
||||
}
|
||||
```
|
||||
Dictionary containing:
|
||||
|
||||
- `E`: full-grid electric field for the solved mode
|
||||
- `H`: full-grid magnetic field for the solved mode
|
||||
- `wavenumber`: propagation constant corrected for the discretized
|
||||
propagation axis
|
||||
- `wavenumber_2d`: propagation constant of the reduced 2D eigenproblem
|
||||
|
||||
Notes:
|
||||
The returned fields are normalized through the `waveguide_2d`
|
||||
normalization convention before being expanded back to 3D.
|
||||
"""
|
||||
if mu is None:
|
||||
mu = numpy.ones_like(epsilon)
|
||||
|
|
@ -139,7 +157,14 @@ def compute_source(
|
|||
mu: Magnetic permeability (default 1 everywhere)
|
||||
|
||||
Returns:
|
||||
J distribution for the unidirectional source
|
||||
`J` distribution for a one-sided electric-current source.
|
||||
|
||||
Notes:
|
||||
The source is built from the expanded mode field and a boundary-source
|
||||
operator. The resulting current is intended to be injected with the
|
||||
same sign convention used elsewhere in the package:
|
||||
|
||||
`E -= dt * J / epsilon`
|
||||
"""
|
||||
E_expanded = expand_e(E=E, dxes=dxes, wavenumber=wavenumber, axis=axis,
|
||||
polarity=polarity, slices=slices)
|
||||
|
|
@ -167,10 +192,33 @@ def compute_overlap_e(
|
|||
slices: Sequence[slice],
|
||||
omega: float,
|
||||
) -> cfdfield_t:
|
||||
"""
|
||||
Given an eigenmode obtained by `solve_mode`, calculates an overlap_e for the
|
||||
mode orthogonality relation Integrate(((E x H_mode) + (E_mode x H)) dot dn)
|
||||
[assumes reflection symmetry].
|
||||
r"""
|
||||
Build an overlap field for projecting another 3D electric field onto a mode.
|
||||
|
||||
The returned field is intended for the discrete overlap expression
|
||||
|
||||
$$
|
||||
\sum \mathrm{overlap\_e} \; E_\mathrm{other}^*
|
||||
$$
|
||||
|
||||
where the sum is over the full Yee-grid field storage.
|
||||
|
||||
The construction uses a two-cell window immediately upstream of the selected
|
||||
slice:
|
||||
|
||||
- for `polarity=+1`, the two cells just before `slices[axis].start`
|
||||
- for `polarity=-1`, the two cells just after `slices[axis].stop`
|
||||
|
||||
The window is clipped to the simulation domain if necessary. A clipped but
|
||||
non-empty window raises `RuntimeWarning`; an empty window raises
|
||||
`ValueError`.
|
||||
|
||||
The derivation below assumes reflection symmetry and the standard waveguide
|
||||
overlap relation involving
|
||||
|
||||
$$
|
||||
\int ((E \times H_\mathrm{mode}) + (E_\mathrm{mode} \times H)) \cdot dn.
|
||||
$$
|
||||
|
||||
E x H_mode + E_mode x H
|
||||
-> Ex Hmy - EyHmx + Emx Hy - Emy Hx (Z-prop)
|
||||
|
|
@ -183,8 +231,6 @@ def compute_overlap_e(
|
|||
B/wu (Ex Emx + Ey Emy) - j/wu (Ex dx Emz + Ey dy Emz)
|
||||
|
||||
|
||||
TODO: add reference
|
||||
|
||||
Args:
|
||||
E: E-field of the mode
|
||||
wavenumber: Wavenumber of the mode
|
||||
|
|
@ -195,7 +241,8 @@ def compute_overlap_e(
|
|||
as the waveguide cross-section. slices[axis] should select only one item.
|
||||
|
||||
Returns:
|
||||
overlap_e such that `numpy.sum(overlap_e * other_e.conj())` computes the overlap integral
|
||||
`overlap_e` normalized so that `numpy.sum(overlap_e * E.conj()) == 1`
|
||||
over the retained overlap window.
|
||||
"""
|
||||
slices = tuple(slices)
|
||||
|
||||
|
|
@ -240,7 +287,7 @@ def expand_e(
|
|||
polarity: int,
|
||||
slices: Sequence[slice],
|
||||
) -> cfdfield_t:
|
||||
"""
|
||||
r"""
|
||||
Given an eigenmode obtained by `solve_mode`, expands the E-field from the 2D
|
||||
slice where the mode was calculated to the entire domain (along the propagation
|
||||
axis). This assumes the epsilon cross-section remains constant throughout the
|
||||
|
|
@ -258,6 +305,16 @@ def expand_e(
|
|||
|
||||
Returns:
|
||||
`E`, with the original field expanded along the specified `axis`.
|
||||
|
||||
Notes:
|
||||
This helper assumes that the waveguide cross-section remains constant
|
||||
along the propagation axis and applies the phase factor
|
||||
|
||||
$$
|
||||
e^{-i \, \mathrm{polarity} \, wavenumber \, \Delta z}
|
||||
$$
|
||||
|
||||
to each copied slice.
|
||||
"""
|
||||
slices = tuple(slices)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,63 +2,125 @@ r"""
|
|||
Operators and helper functions for cylindrical waveguides with unchanging cross-section.
|
||||
|
||||
Waveguide operator is derived according to 10.1364/OL.33.001848.
|
||||
The curl equations in the complex coordinate system become
|
||||
|
||||
As in `waveguide_2d`, the propagation dependence is separated from the
|
||||
transverse solve. Here the propagation coordinate is the bend angle `\theta`,
|
||||
and the fields are assumed to have the form
|
||||
|
||||
$$
|
||||
\vec{E}(r, y, \theta), \vec{H}(r, y, \theta) \propto e^{-\imath m \theta},
|
||||
$$
|
||||
|
||||
where `m` is the angular wavenumber returned by `solve_mode(s)`. It is often
|
||||
convenient to introduce the corresponding linear wavenumber
|
||||
|
||||
$$
|
||||
\beta = \frac{m}{r_{\min}},
|
||||
$$
|
||||
|
||||
so that the cylindrical problem resembles the straight-waveguide problem with
|
||||
additional metric factors.
|
||||
|
||||
Those metric factors live on the staggered radial Yee grids. If the left edge of
|
||||
the computational window is at `r = r_{\min}`, define the electric-grid and
|
||||
magnetic-grid radial sample locations by
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
-\imath \omega \mu_{xx} H_x &= \tilde{\partial}_y E_z + \imath \beta frac{E_y}{\tilde{t}_x} \\
|
||||
-\imath \omega \mu_{yy} H_y &= -\imath \beta E_x - \frac{1}{\hat{t}_x} \tilde{\partial}_x \tilde{t}_x E_z \\
|
||||
-\imath \omega \mu_{zz} H_z &= \tilde{\partial}_x E_y - \tilde{\partial}_y E_x \\
|
||||
\imath \omega \epsilon_{xx} E_x &= \hat{\partial}_y H_z + \imath \beta \frac{H_y}{\hat{T}} \\
|
||||
\imath \omega \epsilon_{yy} E_y &= -\imath \beta H_x - \{1}{\tilde{t}_x} \hat{\partial}_x \hat{t}_x} H_z \\
|
||||
\imath \omega \epsilon_{zz} E_z &= \hat{\partial}_x H_y - \hat{\partial}_y H_x \\
|
||||
r_a(n) &= r_{\min} + \sum_{j \le n} \Delta r_{e, j}, \\
|
||||
r_b\!\left(n + \tfrac{1}{2}\right) &= r_{\min} + \tfrac{1}{2}\Delta r_{e, n}
|
||||
+ \sum_{j < n} \Delta r_{h, j},
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
where $t_x = 1 + \frac{\Delta_{x, m}}{R_0}$ is the grid spacing adjusted by the nominal radius $R0$.
|
||||
|
||||
Rewrite the last three equations as
|
||||
and from them the diagonal metric matrices
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\imath \beta H_y &= \imath \omega \hat{t}_x \epsilon_{xx} E_x - \hat{t}_x \hat{\partial}_y H_z \\
|
||||
\imath \beta H_x &= -\imath \omega \hat{t}_x \epsilon_{yy} E_y - \hat{t}_x \hat{\partial}_x H_z \\
|
||||
\imath \omega E_z &= \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y - \frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\
|
||||
T_a &= \operatorname{diag}(r_a / r_{\min}), \\
|
||||
T_b &= \operatorname{diag}(r_b / r_{\min}).
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
The derivation then follows the same steps as the straight waveguide, leading to the eigenvalue problem
|
||||
|
||||
$$
|
||||
\beta^2 \begin{bmatrix} E_x \\
|
||||
E_y \end{bmatrix} =
|
||||
(\omega^2 \begin{bmatrix} T_b T_b \mu_{yy} \epsilon_{xx} & 0 \\
|
||||
0 & T_a T_a \mu_{xx} \epsilon_{yy} \end{bmatrix} +
|
||||
\begin{bmatrix} -T_b \mu_{yy} \hat{\partial}_y \\
|
||||
T_a \mu_{xx} \hat{\partial}_x \end{bmatrix} T_b \mu_{zz}^{-1}
|
||||
\begin{bmatrix} -\tilde{\partial}_y & \tilde{\partial}_x \end{bmatrix} +
|
||||
\begin{bmatrix} \tilde{\partial}_x \\
|
||||
\tilde{\partial}_y \end{bmatrix} T_a \epsilon_{zz}^{-1}
|
||||
\begin{bmatrix} \hat{\partial}_x T_b \epsilon_{xx} & \hat{\partial}_y T_a \epsilon_{yy} \end{bmatrix})
|
||||
\begin{bmatrix} E_x \\
|
||||
E_y \end{bmatrix}
|
||||
$$
|
||||
|
||||
which resembles the straight waveguide eigenproblem with additonal $T_a$ and $T_b$ terms. These
|
||||
are diagonal matrices containing the $t_x$ values:
|
||||
With the same forward/backward derivative notation used in `waveguide_2d`, the
|
||||
coordinate-transformed discrete curl equations used here are
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
T_a &= 1 + \frac{\Delta_{x, m }}{R_0}
|
||||
T_b &= 1 + \frac{\Delta_{x, m + \frac{1}{2} }}{R_0}
|
||||
-\imath \omega \mu_{rr} H_r &= \tilde{\partial}_y E_z + \imath \beta T_a^{-1} E_y, \\
|
||||
-\imath \omega \mu_{yy} H_y &= -\imath \beta T_b^{-1} E_r
|
||||
- T_b^{-1} \tilde{\partial}_r (T_a E_z), \\
|
||||
-\imath \omega \mu_{zz} H_z &= \tilde{\partial}_r E_y - \tilde{\partial}_y E_r, \\
|
||||
\imath \beta H_y &= -\imath \omega T_b \epsilon_{rr} E_r - T_b \hat{\partial}_y H_z, \\
|
||||
\imath \beta H_r &= \imath \omega T_a \epsilon_{yy} E_y
|
||||
- T_b T_a^{-1} \hat{\partial}_r (T_b H_z), \\
|
||||
\imath \omega E_z &= T_a \epsilon_{zz}^{-1}
|
||||
\left(\hat{\partial}_r H_y - \hat{\partial}_y H_r\right).
|
||||
\end{aligned}
|
||||
|
||||
|
||||
TODO: consider 10.1364/OE.20.021583 for an alternate approach
|
||||
$$
|
||||
|
||||
As in the straight waveguide case, all the functions in this file assume a 2D grid
|
||||
(i.e. `dxes = [[[dr_e_0, dx_e_1, ...], [dy_e_0, ...]], [[dr_h_0, ...], [dy_h_0, ...]]]`).
|
||||
The first three equations are the cylindrical analogue of the straight-guide
|
||||
relations for `H_r`, `H_y`, and `H_z`. The next two are the metric-weighted
|
||||
versions of the straight-guide identities for `\imath \beta H_y` and
|
||||
`\imath \beta H_r`, and the last equation plays the same role as the
|
||||
longitudinal `E_z` reconstruction in `waveguide_2d`.
|
||||
|
||||
Following the same elimination steps as in `waveguide_2d`, apply
|
||||
`\imath \beta \tilde{\partial}_r` and `\imath \beta \tilde{\partial}_y` to the
|
||||
equation for `E_z`, substitute for `\imath \beta H_r` and `\imath \beta H_y`,
|
||||
and then eliminate `H_z` with
|
||||
|
||||
$$
|
||||
H_z = \frac{1}{-\imath \omega \mu_{zz}}
|
||||
\left(\tilde{\partial}_r E_y - \tilde{\partial}_y E_r\right).
|
||||
$$
|
||||
|
||||
This yields the transverse electric eigenproblem implemented by
|
||||
`cylindrical_operator(...)`:
|
||||
|
||||
$$
|
||||
\beta^2
|
||||
\begin{bmatrix} E_r \\ E_y \end{bmatrix}
|
||||
=
|
||||
\left(
|
||||
\omega^2
|
||||
\begin{bmatrix}
|
||||
T_b^2 \mu_{yy} \epsilon_{xx} & 0 \\
|
||||
0 & T_a^2 \mu_{xx} \epsilon_{yy}
|
||||
\end{bmatrix}
|
||||
+
|
||||
\begin{bmatrix}
|
||||
-T_b \mu_{yy} \hat{\partial}_y \\
|
||||
T_a \mu_{xx} \hat{\partial}_x
|
||||
\end{bmatrix}
|
||||
T_b \mu_{zz}^{-1}
|
||||
\begin{bmatrix}
|
||||
-\tilde{\partial}_y & \tilde{\partial}_x
|
||||
\end{bmatrix}
|
||||
+
|
||||
\begin{bmatrix}
|
||||
\tilde{\partial}_x \\
|
||||
\tilde{\partial}_y
|
||||
\end{bmatrix}
|
||||
T_a \epsilon_{zz}^{-1}
|
||||
\begin{bmatrix}
|
||||
\hat{\partial}_x T_b \epsilon_{xx} &
|
||||
\hat{\partial}_y T_a \epsilon_{yy}
|
||||
\end{bmatrix}
|
||||
\right)
|
||||
\begin{bmatrix} E_r \\ E_y \end{bmatrix}.
|
||||
$$
|
||||
|
||||
Since `\beta = m / r_{\min}`, the solver implemented in this file returns the
|
||||
angular wavenumber `m`, while the operator itself is most naturally written in
|
||||
terms of the linear quantity `\beta`. The helpers below reconstruct the full
|
||||
field components from the solved transverse eigenvector and then normalize the
|
||||
mode to unit forward power with the same discrete longitudinal Poynting inner
|
||||
product used by `waveguide_2d`.
|
||||
|
||||
As in the straight-waveguide case, all functions here assume a 2D grid:
|
||||
|
||||
`dxes = [[[dr_e_0, dr_e_1, ...], [dy_e_0, ...]], [[dr_h_0, ...], [dy_h_0, ...]]]`.
|
||||
"""
|
||||
from typing import Any, cast
|
||||
from collections.abc import Sequence
|
||||
|
|
@ -94,17 +156,18 @@ def cylindrical_operator(
|
|||
\begin{bmatrix} \tilde{\partial}_x \\
|
||||
\tilde{\partial}_y \end{bmatrix} T_a \epsilon_{zz}^{-1}
|
||||
\begin{bmatrix} \hat{\partial}_x T_b \epsilon_{xx} & \hat{\partial}_y T_a \epsilon_{yy} \end{bmatrix})
|
||||
\begin{bmatrix} E_x \\
|
||||
\begin{bmatrix} E_r \\
|
||||
E_y \end{bmatrix}
|
||||
$$
|
||||
|
||||
for use with a field vector of the form `[E_r, E_y]`.
|
||||
|
||||
This operator can be used to form an eigenvalue problem of the form
|
||||
A @ [E_r, E_y] = wavenumber**2 * [E_r, E_y]
|
||||
A @ [E_r, E_y] = beta**2 * [E_r, E_y]
|
||||
|
||||
which can then be solved for the eigenmodes of the system
|
||||
(an `exp(-i * wavenumber * theta)` theta-dependence is assumed for the fields).
|
||||
(an `exp(-i * angular_wavenumber * theta)` theta-dependence is assumed for
|
||||
the fields, with `beta = angular_wavenumber / rmin`).
|
||||
|
||||
(NOTE: See module docs and 10.1364/OL.33.001848)
|
||||
|
||||
|
|
@ -270,7 +333,7 @@ def exy2h(
|
|||
mu: vfdslice | None = None
|
||||
) -> sparse.sparray:
|
||||
"""
|
||||
Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields,
|
||||
Operator which transforms the vector `e_xy` containing the vectorized E_r and E_y fields,
|
||||
into a vectorized H containing all three H components
|
||||
|
||||
Args:
|
||||
|
|
@ -298,11 +361,11 @@ def exy2e(
|
|||
epsilon: vfdslice,
|
||||
) -> sparse.sparray:
|
||||
"""
|
||||
Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields,
|
||||
Operator which transforms the vector `e_xy` containing the vectorized E_r and E_y fields,
|
||||
into a vectorized E containing all three E components
|
||||
|
||||
Unlike the straight waveguide case, the H_z components do not cancel and must be calculated
|
||||
from E_x and E_y in order to then calculate E_z.
|
||||
from E_r and E_y in order to then calculate E_z.
|
||||
|
||||
Args:
|
||||
angular_wavenumber: Wavenumber assuming fields have theta-dependence of
|
||||
|
|
@ -360,9 +423,10 @@ def e2h(
|
|||
This operator is created directly from the initial coordinate-transformed equations:
|
||||
$$
|
||||
\begin{aligned}
|
||||
\imath \omega \epsilon_{xx} E_x &= \hat{\partial}_y H_z + \imath \beta \frac{H_y}{\hat{T}} \\
|
||||
\imath \omega \epsilon_{yy} E_y &= -\imath \beta H_x - \{1}{\tilde{t}_x} \hat{\partial}_x \hat{t}_x} H_z \\
|
||||
\imath \omega \epsilon_{zz} E_z &= \hat{\partial}_x H_y - \hat{\partial}_y H_x \\
|
||||
-\imath \omega \mu_{rr} H_r &= \tilde{\partial}_y E_z + \imath \beta T_a^{-1} E_y, \\
|
||||
-\imath \omega \mu_{yy} H_y &= -\imath \beta T_b^{-1} E_r
|
||||
- T_b^{-1} \tilde{\partial}_r (T_a E_z), \\
|
||||
-\imath \omega \mu_{zz} H_z &= \tilde{\partial}_r E_y - \tilde{\partial}_y E_r,
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
|
|
@ -397,15 +461,18 @@ def dxes2T(
|
|||
rmin: float,
|
||||
) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
|
||||
r"""
|
||||
Returns the $T_a$ and $T_b$ diagonal matrices which are used to apply the cylindrical
|
||||
coordinate transformation in various operators.
|
||||
Construct the cylindrical metric matrices $T_a$ and $T_b$.
|
||||
|
||||
`T_a` is sampled on the E-grid radial locations, while `T_b` is sampled on
|
||||
the staggered H-grid radial locations. These are the diagonal matrices that
|
||||
convert the straight-waveguide algebra into its cylindrical counterpart.
|
||||
|
||||
Args:
|
||||
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
|
||||
rmin: Radius at the left edge of the simulation domain (at minimum 'x')
|
||||
|
||||
Returns:
|
||||
Sparse matrix representations of the operators Ta and Tb
|
||||
Sparse diagonal matrices `(T_a, T_b)`.
|
||||
"""
|
||||
ra = rmin + numpy.cumsum(dxes[0][0]) # Radius at Ey points
|
||||
rb = rmin + dxes[0][0] / 2.0 + numpy.cumsum(dxes[1][0]) # Radius at Ex points
|
||||
|
|
@ -427,12 +494,12 @@ def normalized_fields_e(
|
|||
mu: vfdslice | None = None,
|
||||
prop_phase: float = 0,
|
||||
) -> tuple[vcfdslice_t, vcfdslice_t]:
|
||||
"""
|
||||
Given a vector `e_xy` containing the vectorized E_x and E_y fields,
|
||||
returns normalized, vectorized E and H fields for the system.
|
||||
r"""
|
||||
Given a vector `e_xy` containing the vectorized E_r and E_y fields,
|
||||
returns normalized, vectorized E and H fields for the system.
|
||||
|
||||
Args:
|
||||
e_xy: Vector containing E_x and E_y fields
|
||||
e_xy: Vector containing E_r and E_y fields
|
||||
angular_wavenumber: Wavenumber assuming fields have theta-dependence of
|
||||
`exp(-i * angular_wavenumber * theta)`. It should satisfy
|
||||
`operator_e() @ e_xy == (angular_wavenumber / rmin) ** 2 * e_xy`
|
||||
|
|
@ -447,6 +514,18 @@ def normalized_fields_e(
|
|||
Returns:
|
||||
`(e, h)`, where each field is vectorized, normalized,
|
||||
and contains all three vector components.
|
||||
|
||||
Notes:
|
||||
The normalization step is delegated to `_normalized_fields(...)`, which
|
||||
enforces unit forward power under the discrete inner product
|
||||
|
||||
$$
|
||||
\frac{1}{2}\int (E_r H_y^* - E_y H_r^*) \, dr \, dy.
|
||||
$$
|
||||
|
||||
The angular wavenumber `m` is first converted into the full three-component
|
||||
fields, then the overall complex phase and sign are fixed so the result is
|
||||
reproducible for symmetric modes.
|
||||
"""
|
||||
e = exy2e(angular_wavenumber=angular_wavenumber, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon) @ e_xy
|
||||
h = exy2h(angular_wavenumber=angular_wavenumber, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon, mu=mu) @ e_xy
|
||||
|
|
@ -465,8 +544,30 @@ def _normalized_fields(
|
|||
mu: vfdslice | None = None,
|
||||
prop_phase: float = 0,
|
||||
) -> tuple[vcfdslice_t, vcfdslice_t]:
|
||||
r"""
|
||||
Normalize a cylindrical waveguide mode to unit forward power.
|
||||
|
||||
The cylindrical helpers reuse the straight-waveguide inner product after the
|
||||
field reconstruction step. The extra metric factors have already been folded
|
||||
into the reconstructed `e`/`h` fields through `dxes2T(...)` and the
|
||||
cylindrical `exy2e(...)` / `exy2h(...)` operators, so the same discrete
|
||||
longitudinal Poynting integral can be used here.
|
||||
|
||||
The normalization procedure is:
|
||||
|
||||
1. Flip the magnetic field sign so the reconstructed `(e, h)` pair follows
|
||||
the same forward-power convention as `waveguide_2d`.
|
||||
2. Compute the time-averaged forward power with
|
||||
`waveguide_2d.inner_product(..., conj_h=True)`.
|
||||
3. Scale by `1 / sqrt(S_z)` so the resulting mode has unit forward power.
|
||||
4. Remove the arbitrary complex phase and apply a quadrant-sum sign heuristic
|
||||
so symmetric modes choose a stable representative.
|
||||
|
||||
`prop_phase` has the same meaning as in `waveguide_2d`: it compensates for
|
||||
the half-cell longitudinal staggering between the E and H fields when the
|
||||
propagation direction is itself discretized.
|
||||
"""
|
||||
h *= -1
|
||||
# TODO documentation for normalized_fields
|
||||
shape = [s.size for s in dxes[0]]
|
||||
|
||||
# Find time-averaged Sz and normalize to it
|
||||
|
|
|
|||
Loading…
Reference in a new issue