Skip to content

Mode solving

Eigenmode solvers for waveguide cross-sections: the general ModeSolver, the full-vector solver, and the grid-consistent family whose modes are grid-consistent with the FDTD fields they will drive.

Scalar and semi-vector

ModeSolver

ModeSolver(
    eps: ndarray,
    dl_x_um: float,
    dl_y_um: float,
    wlen_um: float,
)

Finite-difference eigenmode solver for a straight waveguide cross-section.

Construct from a raw permittivity cross-section and the transverse grid, then call solve. See the module docstring for the physics, the operator definitions, and, importantly, the straight-waveguide-only scope with the bent-mode error bound.

Parameters

eps: Real relative-permittivity cross-section, a 2-D array indexed [iy, ix] (row = y, column = x). Must be finite and >= 1 everywhere. Not copied defensively beyond an asarray(float); do not mutate it after construction. dl_x_um, dl_y_um: Uniform transverse grid spacings along x and y (microns, > 0). (Non-uniform transverse meshing is not supported by this solver.) wavelength_um: Free-space wavelength (microns, > 0). Use wavelength_um = c0 / freq_hz * 1e6 to go from frequency.

C0 class-attribute instance-attribute

C0: float = _C0

MAX_UNKNOWNS class-attribute instance-attribute

MAX_UNKNOWNS: int = 8100

eps instance-attribute

eps: ndarray = eps_arr

dl_x_um instance-attribute

dl_x_um: float = float(dl_x_um)

dl_y_um instance-attribute

dl_y_um: float = float(dl_y_um)

wavelength_um instance-attribute

wavelength_um: float = float(wlen_um)

at_wlen

at_wlen(wlen_um: float) -> 'ModeSolver'

A sibling solver on the SAME cross-section (eps, dl) at a new free-space wavelength. Use this to re-solve a mode across a frequency band, wavelength_um = C0 / freq_hz * 1e6, without repeating mesh sampling of the geometry (the basis for the broadband num_freqs mode solves on both the source and monitor sides). eps is shared by reference (the operator never mutates it).

at_wavelength

at_wavelength(wavelength_um: float) -> 'ModeSolver'

Deprecated spelling of at_wlen.

from_rectangular_core classmethod

from_rectangular_core(
    *,
    wlen_um: float,
    dl_um: float,
    core_w_um: float,
    core_h_um: float,
    n_core: float,
    n_clad: float,
    window_w_um: Optional[float] = None,
    window_h_um: Optional[float] = None,
    clad_pad_um: float = 0.5,
) -> "ModeSolver"

Build a solver for a centered rectangular core in a uniform cladding.

Samples the canonical strip-waveguide cross-section (the validated SOI case) onto a square uniform grid of spacing dl_um. This keeps the plugin decoupled from the photonhub.components spec models, feed it plain numbers (or read them off a Box + two Medium permittivities yourself and call the raw constructor).

Parameters

wlen_um: Free-space wavelength (microns). dl_um: Uniform grid spacing for both x and y (microns). core_w_um, core_h_um: Core full width (x) and height (y) in microns. n_core, n_clad: Core and cladding refractive indices (eps = n**2). window_w_um, window_h_um: Total computational window extents (microns). If omitted, default to the core plus clad_pad_um of cladding on each side so the field decays before the Dirichlet wall. (Padding is additive, not a core multiple, to keep the dense operator a tractable size.) clad_pad_um: Per-side cladding padding (microns) used only when a window extent is omitted. ~0.5 um is several evanescent decay lengths for a typical SOI strip; enlarge it for weakly-guided (low-contrast) modes.

solve

solve(
    num_modes: int = 1,
    polarization: PolarizationName = "TE",
    n_guess: Optional[float] = None,
    bend_radius_um: Optional[float] = None,
) -> Tuple[Mode, ...]

Compute the num_modes best-confined guided modes.

Parameters

num_modes: Number of modes to return, ordered by descending n_eff (the first is the fundamental). Must be >= 1. polarization: "TE" (default, validated quasi-TE Ex-major), "TM" (quasi-TM Ey-major), or "scalar" (no vectorial correction). n_guess: Optional starting index hint. Modes are filtered to n_eff below sqrt(max(eps)) (no mode can be more confined than the core) regardless; n_guess is currently advisory and does not change the returned set (the dense solve finds the true spectrum). Kept in the signature so a future inverse-iteration fast path can use it without an API break. bend_radius_um: Must be None (or non-finite/<=0 is rejected). Bent waveguides are out of scope, see the module docstring's error bound. A finite positive value raises NotImplementedError rather than silently returning a wrong (curvature-free) result.

Returns

tuple[Mode, ...] Up to num_modes Mode objects, highest n_eff first. Fewer are returned if the window supports fewer guided modes.

Mode dataclass

Mode(
    n_eff: float,
    field: ndarray,
    wavelength_um: float,
    polarization: PolarizationName,
    dl_x_um: float,
    dl_y_um: float,
)

One guided eigenmode of a straight-waveguide cross-section.

Attributes

n_eff: Modal effective index beta / k0 (dimensionless, real). For a properly confined guided mode this lies between the cladding and core refractive indices. field: The dominant transverse field component on the cross-section, a real float64 array of shape (ny, nx) indexed [iy, ix] (row = y, column = x), L2-normalized (sum(field**2) == 1) and sign-fixed so the lobe is positive. This is Ex for "TE"/"scalar" and Ey for "TM". wavelength_um: Free-space wavelength the mode was solved at (microns). polarization: Which operator branch produced this mode ("TE"/"TM"/"scalar"). dl_x_um, dl_y_um: Transverse grid spacings (microns), carried for real-space helpers.

n_eff instance-attribute

n_eff: float

field instance-attribute

field: ndarray

wavelength_um instance-attribute

wavelength_um: float

polarization instance-attribute

polarization: PolarizationName

dl_x_um instance-attribute

dl_x_um: float

dl_y_um instance-attribute

dl_y_um: float

shape property

shape: Tuple[int, int]

(ny, nx) of the field profile.

field_dataarray

field_dataarray() -> DataArray

The field profile as an xarray.DataArray with real-space x/y coordinates in microns (origin at the cross-section center, matching ModeSolver.from_rectangular_core). Dims ("y", "x").

core_fraction

core_fraction(core_w_um: float, core_h_um: float) -> float

Fraction of |field|^2 inside a centered core_w_um x core_h_um bounding box, a simple confinement metric (1.0 = fully confined). The box is centered on the cross-section, matching ModeSolver.from_rectangular_core.

Full vector

VectorModeSolver

VectorModeSolver(
    eps: ndarray,
    dl_x_um: float,
    dl_y_um: float,
    wlen_um: float,
    x_symmetry: XSymmetry = "none",
    *,
    eps_tensor: Optional[
        Tuple[ndarray, ndarray, ndarray]
    ] = None,
    x_min_symmetry: Optional[MirrorSymmetry] = None,
    method: ModeMethod = "flm",
    yee_eps_tensor: Optional[
        Tuple[ndarray, ndarray, ndarray]
    ] = None,
    yee_eps_outer: Optional[Tuple[ndarray, ndarray]] = None,
    yee_periodic: tuple[bool, bool] = (False, False),
)

Full-vectorial finite-difference eigenmode solver for a straight-waveguide cross-section.

Construct from a raw permittivity cross-section and the transverse grid, then call solve. See the module docstring for the physics (the Fallahkhair–Li–Murphy transverse-H operator), the boundary conditions, and the straight-waveguide-only scope.

Parameters

eps: Real relative-permittivity cross-section, a 2-D array indexed [iy, ix] (row = y, column = x). Must be finite and >= 1 everywhere. Stored as asarray(float); do not mutate after construction. dl_x_um, dl_y_um: Uniform transverse grid spacings along x and y (microns, > 0). wavelength_um: Free-space wavelength (microns, > 0). x_symmetry: "none" (default) for electric (PEC) walls on every edge, or "pmc" for a magnetic wall on the x-axis edges. On a y-only (x-invariant) ε profile each wall gives an x-uniform (kx = 0) slab fundamental, but of a DIFFERENT polarization, the operator is in H, so the electric wall ("none") yields the slab TE0 (Eₓ-major) and the magnetic wall ("pmc") the slab TM0 (E_y-major). Choose the wall to match the polarization you want (the wrong one returns a too-low n_eff; see from_rectangular_core). With x_min_symmetry set it governs the x-max wall only. x_min_symmetry: None (default) or a symmetry plane on the x-min edge: "pec" (odd / electric: Hₓ odd, H_y even, so Eₓ even) or "pmc" (even / magnetic: the complementary parity). The plane passes through the centre of ε column 0. So eps is the half of a mirror-symmetric cross-section that keeps the column straddling the plane. The plane is the x origin of the returned modes: column j reports x = j*dl_x_um through VectorMode.center_offset_um. A caller whose plane sits elsewhere in its own frame centres the mode on the plane when placing it, as with any solve. Overlaps of such a mode cover the half window only (see VectorMode.x_min_symmetry). Bent solves and an x PML are rejected: a bend has no mirror symmetry, and the PML would cover the plane. With method="yee" the plane is the node line through the centre of ε column 0 and the parities keep the same families: "pec" pins Ey and Ez to zero on the plane (Ex even; TE0), "pmc" keeps the complementary family (TM0, TE1). method: The cross-section discretization. "flm" (default) is the transverse-H operator described above. "yee" solves the transverse electric field on a staggered Yee lattice (Z. Zhu and T. G. Brown, Opt. Express 10, 853 (2002)). The centre of ε cell [iy, ix] is lattice node (ix, iy) and carries Ez; Ex sits on the centre of the cell's +x face and Ey on the centre of its +y face, and each component sees its own permittivity sample. Prefer it for high-contrast cores with corners, such as a silicon strip, where it converges faster under mesh refinement than the default, which loses accuracy at the corners. Its walls are electric (PEC) and lie on node lines half a cell outside the ε grid on every side, so opposite walls are mirror images of each other; a half-window solve (x_min_symmetry) therefore reproduces the full solve's index of that parity to eigensolver precision. Returned modes hold one entry per ε cell for every component, co-located on the cell centres: a component half a cell off takes the mean of its two neighbours (Hz of its four). That mean is second order where the field is smooth and first order across a material face, where the normal E component jumps; it leaves n_eff and n_group unchanged and moves VectorMode.te_fraction at first order (2.1e-4 for a 500 x 220 nm Si strip's TE0 at 5 nm cells). They carry yee_staggered=False like the default method's, so every consumer reads them as node-collocated fields and none mistakes them for modes solved on a simulation's own grid. Bends and absorbing x layers (bend_radius_um, num_pml) work as in the default method, by transformation optics on the same lattice (see solve). Limits: solve_eme_basis raises NotImplementedError (it is planned), square cells (dl_x_um == dl_y_um) and x_symmetry="none". From a cell-centred eps (or eps_tensor) the staggered values are derived from the cells: εzz is the cell's value and εxx at a face the harmonic mean of the two cells sharing that x face, εyy likewise across a y face (the outermost faces take their one cell's value). This re-averages an already averaged grid and is less accurate than sampling the geometry at each component, which from_rectangular_core does and yee_eps_tensor accepts. yee_eps_tensor: Optional (εxx, εyy, εzz) sampled at the Yee positions of each cell, each shaped like eps: εxx at the centre of the cell's +x face, εyy at the centre of its +y face, εzz at its centre. Only with method="yee"; it replaces the values derived from eps. yee_eps_outer: Optional (εxx, εyy) for the faces that are no cell's +x or +y face: εxx on the -x face of column 0 (shape (ny,)) and εyy on the -y face of row 0 (shape (nx,)). Both lie on the low edge of the eps grid, half a cell inside the low walls; a mirror plane or a periodic axis does not use the x one. Only with yee_eps_tensor. Without it those faces repeat the sample of the same cell's +x (+y) face, which is exact only when the material is uniform across both cells, so pass it when geometry touches the window's low x or y edge. yee_periodic: (periodic_x, periodic_y), default (False, False): close that axis periodically across the window (k = 0) instead of meeting electric walls, as from_rectangular_core does for a core as wide (tall) as the window. The structure must then be invariant along that axis (a ValueError names the array that is not); its modes come out uniform along it, both slab polarizations included, so a slab whose two claddings differ can be built from an array. A periodic x axis takes no x_min_symmetry, bend or absorbing x layers. Only with method="yee".

C0 class-attribute instance-attribute

C0: float = _C0

MAX_UNKNOWNS class-attribute instance-attribute

MAX_UNKNOWNS: int = 40000

eps instance-attribute

eps: ndarray = eps_arr

dl_x_um instance-attribute

dl_x_um: float = float(dl_x_um)

dl_y_um instance-attribute

dl_y_um: float = float(dl_y_um)

wavelength_um instance-attribute

wavelength_um: float = float(wlen_um)

x_symmetry instance-attribute

x_symmetry: XSymmetry = x_symmetry

x_min_symmetry instance-attribute

x_min_symmetry: Optional[MirrorSymmetry] = x_min_symmetry

method instance-attribute

method: ModeMethod = method

at_wlen

at_wlen(wlen_um: float) -> 'VectorModeSolver'

A sibling solver on the SAME cross-section (eps / tensor, dl, symmetry) at a new free-space wavelength, for re-solving a full-vector mode across a frequency band (wavelength_um = C0 / freq_hz * 1e6) without repeating mesh sampling. Mirrors ModeSolver.at_wavelength; the permittivity (and any KFJ tensor) is shared by reference.

at_wavelength

at_wavelength(wavelength_um: float) -> 'VectorModeSolver'

Deprecated spelling of at_wlen.

from_rectangular_core classmethod

from_rectangular_core(
    *,
    wlen_um: float,
    dl_um: float,
    core_w_um: float,
    core_h_um: float,
    n_core: float,
    n_clad: float,
    window_w_um: Optional[float] = None,
    window_h_um: Optional[float] = None,
    clad_pad_um: float = 0.5,
    x_symmetry: XSymmetry = "none",
    x_min_symmetry: Optional[str] = None,
    subpixel: bool = True,
    subpixel_method: SubpixelMethod = "tensor",
    method: ModeMethod = "flm",
) -> "VectorModeSolver"

Build a solver for a centered rectangular core in a uniform cladding.

Samples the canonical strip-waveguide cross-section onto a square uniform grid of spacing dl_um (the same mesh sampling as photonhub.analysis.modes.ModeSolver.from_rectangular_core, so the full-vector and semi-vec solvers are drop-in comparable).

Parameters

wlen_um: Free-space wavelength (microns). dl_um: Uniform grid spacing for both x and y (microns). core_w_um, core_h_um: Core full width (x) and height (y) in microns. n_core, n_clad: Core and cladding refractive indices (eps = n**2). window_w_um, window_h_um: Total computational window extents (microns). If omitted, default to the core plus clad_pad_um of cladding on each side so the field decays before the wall. clad_pad_um: Per-side cladding padding (microns) used only when a window extent is omitted. x_symmetry: x-wall boundary, "none" (default, electric / PEC walls) or "pmc" (magnetic x-wall). With core_w_um == window_w_um this builds a y-only slab cross-section whose fundamental is exactly x-uniform (kx = 0), the 1-D slab limit. The wall selects the polarization, because the operator is in H: an electric wall ("none") makes the major-H component Neumann (and Hₓ=0), so its x-uniform fundamental is the slab TE0 (Eₓ-major); a magnetic wall ("pmc") flips the parity, so its x-uniform fundamental is the slab TM0 (E_y-major). Picking the wrong wall for the polarization you want does not error, it returns a mode of that polarization forced to zero at the walls (a spurious kx ≈ π/window_w), whose n_eff is too low. So: "none" for a TE slab, "pmc" for a TM slab. x_min_symmetry: None (default, full cross-section), "pec", or "pmc" to exploit a mirror symmetry about the width center. The solver keeps the ε columns from the centre column, the one straddling the plane, to the +x edge. The eigenproblem is about half the size, and every kept H node is a node of the full grid. "pec" makes the x-min wall an odd / electric plane: it keeps the modes whose Eₓ is even and E_y odd in x, such as TE0. "pmc" makes it an even / magnetic plane with the complementary parity, such as TM0 and the width-odd TE1. The half solve reproduces the full solve's modes of that parity at the same dl, to eigensolver precision: equal n_eff, and field arrays equal to the full ones from the centre column on, up to normalization. Its modes report x from the plane, the core centre, so column j sits at x = j*dl_um as in the full solve. Their overlaps cover the half window only, so an overlap with a full-width partner (a fibre mode, say) returns about half the full coupling; see VectorMode.x_min_symmetry. x_symmetry still sets the far (x-max) wall, so the default matches the full solve. subpixel: False hard-samples ε (staircase). True (default) smooths the high-contrast walls; the kind is set by subpixel_method. subpixel_method: "tensor" (default) applies a diagonalized Kottke–Farjadpour–Johnson subpixel tensor, the interface-normal ε-component gets the harmonic mean and tangential components the arithmetic mean. This is exact on the rectangle's axis-aligned face cells; at a partially filled corner the diagonal-only FLM solver omits KFJ's off-diagonal xy term. "volume" uses the scalar area-average. Ignored when subpixel=False. method: "flm" (default) or "yee", the discretization described on VectorModeSolver. With "yee" each field component's permittivity is sampled from the exact rectangle over a cell centred on that component: εxx at Ex is the arithmetic mean along y of the harmonic means along x, εyy at Ey the same with x and y exchanged, and εzz at Ez the area mean. On a cell cut by one core face this is the diagonal of the Kottke, Farjadpour and Johnson subpixel tensor (Phys. Rev. E 77, 036611 (2008)); a corner cell takes the same nested mean and, as in the default, no off-diagonal term. subpixel_method="volume" takes the area mean for every component and subpixel=False the value at each component's position. The core centre is a lattice node, the centre of the middle ε cell, where the default method centres it, so both methods solve the same geometry; the electric walls sit half a cell outside the default method's window on every side. x_min_symmetry puts the mirror plane on that centre node line, and the half solve reproduces the full solve's index of its parity to eigensolver precision. A core as wide (tall) as the window is an x-invariant (y-invariant) slab: that axis closes periodically across the window instead of meeting walls, so the slab modes of both polarizations are returned uniform along it, without x_symmetry (x_symmetry="pmc" is rejected). x_min_symmetry is refused on an x-invariant slab only; a y-invariant slab keeps the x mirror plane.

solve

solve(
    num_modes: int = 1,
    n_guess: Optional[float] = None,
    group_index: bool = False,
    bend_radius_um: Optional[float] = None,
    num_pml: int = 0,
    pml_strength: float = 30.0,
) -> Tuple[VectorMode, ...]

Compute up to num_modes guided modes (full-vector).

Parameters

num_modes: Number of modes to return, ordered by descending Re(n_eff) (the first is the fundamental). Must be >= 1. n_guess: Shift-invert target index. The solve returns the modes whose n_eff is nearest n_guess. Layer-free straight solves default to sqrt(max(eps)); layered solves anchor on the layer-free fundamental so weak guides remain in the search. group_index: If True, also compute each returned mode's group index n_g by central difference over two extra solves at lambda(1 +/- 1e-3) (the same mode is tracked by maximum transverse-field overlap). Triples the cost. Default False (mode.n_group is None). bend_radius_um: If set (nonzero), solve the bent-waveguide mode of this radius (microns) via the physical cylindrical/centrifugal generalized eigenproblem A h = β₀² B h, B = diag((R/r)²) (see _centrifugal_weight); the reported n_eff follows the engine convention (field ~ exp(+i n k0 R φ), so R→∞ recovers the straight n_eff). Index, field and attenuation come from one eigenpair of the solve with absorbing layers. The returned arrays are conjugated into the EME phasor. If num_pml is left at 0 a default PML at strength 30 is added with a warning. A radius with magnitude below half the full x window width plus one x mesh cell is refused before the eigensolve. None is the straight, lossless solve (k_eff == 0). num_pml: Number of complex-coordinate-stretched PML cells on each in-plane (x) window edge (_pml_stretch) so leaky/radiated fields are absorbed and the eigenvalue becomes complex. 0 (default) on the straight path keeps the operator real and lossless; a bend solve with num_pml == 0 adds a default thickness and warns. pml_strength: PML conductivity scale σ_max/k0 (dimensionless); the polynomial profile peaks at the wall. Default 30.0, strong enough to absorb the bend radiation in a handful of cells.

Returns

tuple[VectorMode, ...] Up to num_modes VectorMode objects, highest Re(n_eff) first. Fewer are returned if the window supports fewer guided modes. Any complex mode follows the split convention documented on VectorMode.n_eff_complex: positive-loss metadata remains in n_eff + i*k_eff, while the field arrays use its EME conjugate.

Notes

Finite windows and absorbing layers affect leaky bend loss. The solver rejects layer-localized eigenpairs by their energy outside the layers. Straight guides check the real-index response to half layer strength; bend pairs are assigned one to one to the selected straight modes by interior overlap times the square root of interior energy share. loss_unresolved flags a bend whose caustic margin is under 0.75 Airy lengths or whose loss changes by over 10% at half layer strength. Negative attenuation is clamped to zero; it can reach a few hundred dB/cm on a narrow window, so check a zero bend loss on a wider window like any other. Widen the window and vary layer thickness when quantitative loss matters.

With method="yee" the solve is the transverse-E eigenproblem on the Yee lattice. Without a bend or layers, n_guess sets its shift (default the largest sampled index), modes above the smallest sampled index are returned with k_eff == 0, and group_index uses the same central difference with the permittivity samples unchanged.

A bend or num_pml > 0 keeps every argument's meaning, default, check and the selection rules above, on a transformed operator. The bend becomes a straight guide along w = R φ with ε' = ε diag(q, q, 1/q) and μ' = diag(q, q, 1/q) in (x, y, w), q = (R + x)/R, each factor taken at its own component's x: the cell's x face for Ex, Hy, Hz, its centre for Ey, Ez, Hx. The layers stretch x by s = 1 - i·pml_strength·ξ³ (ξ the depth into the num_pml-cell layer, 0 on its inner face, 1 on the outer edge of the ε grid), entering as diag(1/s, s, s). In the outer layer, where the bend radiates, q is continued to the stretched radius, (R + x̃)/R with x̃ = ∫₀ˣ s dx'; the inner layer keeps the real q. Outside the layers s = 1 and q is real, exactly. The returned fields are the physical ones: ex/hx radial and ey/hy vertical as transformed (divided by s inside a layer), ez = E_φ = E'w / q and hz = H_φ = H'w / q with q at the component's own position, all then co-located on the cell centres; they keep the eigenpair's complex β. Without a bend the map is the layers alone (q = 1). Interior energy shares use the transverse E of the eigenvector (Ex / s, Ey) in place of the transverse H.

solve_eme_basis

solve_eme_basis(
    *,
    num_guided: int = 1,
    num_radiation: int = 0,
    num_evanescent: int = 0,
    n_guess: Optional[float] = None,
    pml_cells_xy: Optional[Tuple[int, int]] = None,
    pml_imaginary_thickness_um: Tuple[float, float] = (
        0.1,
        0.1,
    ),
    residual_tolerance: float = 1e-07,
) -> Tuple[VectorMode, ...]

Return a common-operator EME basis containing guided, radiation and evanescent modes.

Warning

Radiation/evanescent and PML requests are experimental. Analytic spectrum/Maxwell checks pass, but the current high-contrast device basis does not stabilize under nested radiation-mode-group refinement. Require independent mesh, window, PML, basis, and FDTD convergence before interpreting a result as quantitative radiation loss.

Unlike solve, which deliberately filters to bound guided modes, this method samples the complex spectrum at guided, radiation and negative-beta^2 shifts. When continuum modes are requested it uses a four-sided complex-coordinate PML, so the open radiation continuum is represented by a finite set of passive box/PML modes. All returned modes come from that same operator and therefore retain diagonal propagation. Before return, each PML eigenfield, beta, and complex contour is conjugated together into the EME convention. mode.n_eff_complex deliberately retains the positive-loss engine eigenvalue as metadata. Thus an evanescent channel has approximately n_eff=0, k_eff>0, beta_eme=-i*k0*k_eff, and the EME propagation factor exp(-i*beta_eme*L)=exp(-k0*k_eff*L) decays.

pml_cells_xy=(0, 0) is an explicit hard-wall box-spectrum mode useful for analytic verification. Otherwise the default PML occupies roughly one eighth of each transverse window. Its strength is parameterised by integrated imaginary contour thickness, not a grid-dependent peak. The current classifier requires one homogeneous scalar cladding surrounding all four PML strips; asymmetric air/substrate exteriors need a multi-exterior light-line treatment and are rejected.

Not available with method="yee" yet (planned): it raises NotImplementedError.

VectorMode dataclass

VectorMode(
    n_eff: float,
    n_group: Optional[float],
    ex: ndarray,
    ey: ndarray,
    ez: ndarray,
    hx: ndarray,
    hy: ndarray,
    hz: ndarray,
    wavelength_um: float,
    dl_x_um: float,
    dl_y_um: float,
    k_eff: float = 0.0,
    bend_radius_um: Optional[float] = None,
    center_offset_um: Optional[Tuple[float, float]] = None,
    yee_staggered: bool = False,
    x_coords_um: Optional[ndarray] = None,
    y_coords_um: Optional[ndarray] = None,
    solve_params: Optional[Mapping[str, Any]] = None,
    mode_type: ModeType = "guided",
    overlap_weights: Optional[ndarray] = None,
    physical_mask: Optional[ndarray] = None,
    pml_cells_xy: Tuple[int, int] = (0, 0),
    eigen_residual: Optional[float] = None,
    yee_eme_compatible: bool = False,
    yee_eme_axis: Optional[str] = None,
    yee_eme_origin_um: Optional[Tuple[float, float]] = None,
    x_min_symmetry: Optional[MirrorSymmetry] = None,
    window_limited: bool = False,
    loss_unresolved: bool = False,
    x_min_plane_on_node: bool = False,
)

One full-vector waveguide mode (guided, radiation, evanescent, or bent) carrying all six field components.

Attributes

n_eff: Real phase index (dimensionless). For a properly confined guided mode this lies between the cladding and core indices. k_eff: Nonnegative passive attenuation metadata. The engine/eigensolver beta is beta_store = k0*(n_eff + i*k_eff); k_eff > 0 decays under its exp(-i*omega*t + i*beta_store*z) convention. Returned field arrays use the conjugate EME beta beta_store.conjugate(). n_group: Group index n_g = n_eff - lambda dn_eff/dlambda (waveguide dispersion), or None if not requested (solve(group_index=False)). window_limited: For a straight mode, true when less than 95% of transverse magnetic energy lies outside the absorbing layers. A bend inherits this flag from its straight partner. Widen the window before using its index. loss_unresolved: True for a bend when the radiation caustic is too close to the outer layer or its attenuation changes by over 10% at half layer strength. Widen the window or strengthen the layers before using its loss. ex, ey, ez, hx, hy, hz: The six field components on the cross-section, complex (ny, nx) arrays indexed [iy, ix] (row = y, column = x). The transverse pair (ex, ey) is jointly L2-normalized (sum |ex|^2 + |ey|^2 == 1) and phase-fixed so the dominant transverse-E component is real-positive at its peak; the other components share that scale. All returned arrays use exp(+i*omega*t - i*beta_eme*z) with beta_eme = k0*n_eff_complex.conjugate(). For complex PML/bend modes this is intentionally the conjugate of the positive-loss metadata. wavelength_um: Free-space wavelength the mode was solved at (microns). dl_x_um, dl_y_um: Transverse grid spacings (microns), carried for the real-space helpers. center_offset_um: Optional (x, y) placement metadata (microns, mode frame: x = width, y = height): the ACTUAL center of the solver's sample window minus the center the caller REQUESTED. The cross-section mesh samplers deliberately snap their eps window to the simulation grid (so dielectric walls land at the same sub-cell positions the FDTD saw), which displaces the array center from the requested center by up to ~a cell; consumers that reconstruct real-space coordinates as (i - (n-1)/2)*dl + center (vector_modal_fields and everything built on it) shift by this offset so the mode lands where its window truly was. A half-window solve (x_min_symmetry) sets ((nx-1)/2*dl_x_um, 0): its window starts on the symmetry plane, which is the requested centre, so column j lands at x = j*dl_x_um. A further placement shift adds to that offset rather than replacing it. None (default, e.g. a hand-built or idealized-rectangle mode) means "centered as requested", the legacy behavior. x_min_symmetry: "pec" or "pmc" for a mode solved on a half window (the parity of its symmetry plane), else None. The arrays hold the columns from the plane to the +x edge. core_fraction describes the full mode. The arrays themselves, te_fraction, modal_power and the overlap helpers describe the stored half window (with x_min_plane_on_node, column 0 straddles the plane and the first two count it at half weight, so te_fraction equals the full window's and modal_power is half of it): an overlap with a full-width partner, such as a full-window mode, a Gaussian beam or a recorded plane, returns about half the full coupling. Solve the full window for coupling to such a partner.

n_eff instance-attribute

n_eff: float

n_group instance-attribute

n_group: Optional[float]

ex instance-attribute

ex: ndarray

ey instance-attribute

ey: ndarray

ez instance-attribute

ez: ndarray

hx instance-attribute

hx: ndarray

hy instance-attribute

hy: ndarray

hz instance-attribute

hz: ndarray

wavelength_um instance-attribute

wavelength_um: float

dl_x_um instance-attribute

dl_x_um: float

dl_y_um instance-attribute

dl_y_um: float

k_eff class-attribute instance-attribute

k_eff: float = 0.0

bend_radius_um class-attribute instance-attribute

bend_radius_um: Optional[float] = None

center_offset_um class-attribute instance-attribute

center_offset_um: Optional[Tuple[float, float]] = None

yee_staggered class-attribute instance-attribute

yee_staggered: bool = False

x_coords_um class-attribute instance-attribute

x_coords_um: Optional[ndarray] = None

y_coords_um class-attribute instance-attribute

y_coords_um: Optional[ndarray] = None

solve_params class-attribute instance-attribute

solve_params: Optional[Mapping[str, Any]] = None

mode_type class-attribute instance-attribute

mode_type: ModeType = 'guided'

overlap_weights class-attribute instance-attribute

overlap_weights: Optional[ndarray] = None

physical_mask class-attribute instance-attribute

physical_mask: Optional[ndarray] = None

pml_cells_xy class-attribute instance-attribute

pml_cells_xy: Tuple[int, int] = (0, 0)

eigen_residual class-attribute instance-attribute

eigen_residual: Optional[float] = None

yee_eme_compatible class-attribute instance-attribute

yee_eme_compatible: bool = False

yee_eme_axis class-attribute instance-attribute

yee_eme_axis: Optional[str] = None

yee_eme_origin_um class-attribute instance-attribute

yee_eme_origin_um: Optional[Tuple[float, float]] = None

x_min_symmetry class-attribute instance-attribute

x_min_symmetry: Optional[MirrorSymmetry] = None

window_limited class-attribute instance-attribute

window_limited: bool = False

loss_unresolved class-attribute instance-attribute

loss_unresolved: bool = False

x_min_plane_on_node class-attribute instance-attribute

x_min_plane_on_node: bool = False

shape property

shape: Tuple[int, int]

(ny, nx) of the field arrays.

n_eff_complex property

n_eff_complex: complex

Positive-loss complex-index metadata for the engine/eigensolver.

beta_store = k0*(n_eff + i*k_eff) decays in exp(-i*omega*t + i*beta_store*z) when k_eff > 0. The returned field arrays use the conjugate EME phasor, so their curl/propagation constant is beta_eme = beta_store.conjugate() and EME propagates by exp(-i*beta_eme*L). Do not insert this property directly into exp(-i*k0*n*L); that would turn passive loss into growth.

loss_db_per_cm property

loss_db_per_cm: float

Propagation/bend loss in dB/cm from the imaginary modal index.

alpha [1/m] = 2 k0 k_eff (field ~ exp(-alpha/2 z) ⇒ power ~ exp(-alpha z)), and loss[dB/m] = 10 alpha / ln 10; converted to dB/cm. For a bent mode this is the radiation (bend) loss; 0 for a straight lossless mode. Solver-produced passive modes have k_eff >= 0. A negative user-supplied value denotes gain metadata; it is not the negative imaginary part of a passive EME field beta.

te_fraction property

te_fraction: float

Transverse-E TE fraction ∫|Ex|² / (∫|Ex|² + ∫|Ey|²), the polarization purity. ~1 for a clean TE (Ex-major) mode, ~0 for a clean TM (Ey-major) mode, ~0.5 for a strongly hybrid mode.

On a half window whose mirror plane passes through column 0 (x_min_plane_on_node), that column's cell straddles the plane, so it counts at half weight and the fraction equals the full window's.

polarization property

polarization: str

"TE" if te_fraction >= 0.5 (Ex-major), else "TM" (Ey-major). A label for the dominant transverse-E component; strongly hybrid modes near 0.5 are classified by the majority component.

field_dataarray

field_dataarray(component: str = 'Ex') -> DataArray

One field component as an xarray.DataArray with real-space x/y coordinates in microns (origin at the REQUESTED cross-section centre; a grid-snapped or half-window solve's center_offset_um and a graded solve's node ladders are honoured, see _node_coords_um). Dims ("y", "x"). A half-window mode's x starts at 0 on its symmetry plane and equals the full solve's x from the centre column on.

component is one of "Ex", "Ey", "Ez", "Hx", "Hy", "Hz" (default "Ex"). The data is complex; plot_mode and other consumers take .real / np.abs as needed.

core_fraction

core_fraction(core_w_um: float, core_h_um: float) -> float

Fraction of the transverse-E energy |Ex|² + |Ey|² inside a centered core_w_um x core_h_um bounding box, a confinement metric (1.0 = fully confined). The box is centered on the REQUESTED cross-section centre, matching VectorModeSolver.from_rectangular_core; the mode's own node coordinates honour a grid-snapped or half-window solve's center_offset_um and a graded solve's ladders (see _node_coords_um), so the box lands on the real core instead of being displaced by the window placement.

On a half-window mode (x_min_symmetry set) the fraction is the full mode's, laid out as the full solve's arrays hold it. Those arrays report each sample half a cell before its node, so the mirror image of stored column j sits at x = -(j+1)*dl_x_um from the plane, one column further out than the column itself. Every stored column but the last, on the far wall, therefore counts twice, and the box is tested at both positions. A full solve's H arrays are exact mirror images in this layout. Its E arrays are not beside a vertical core wall, where the two samples of a pair divide by the permittivity of different cells, so the result can differ from the full solve's own value by O(dl) there.

A method="yee" half mode (x_min_plane_on_node) has its plane on node 0 and every component co-located on the nodes, so its column 0 is its own image and stored column j images to x = -j*dl_x_um. Rebuilt that way, the fraction equals the full Yee solve's to eigensolver precision.

modal_power

modal_power() -> float

Time-averaged modal power (watts) carried across the cross-section by this full-vector mode, the physical z-Poynting flux

P = (1/2) integral Re( E x H* ) . z_hat dA
  = (1/2) integral Re( Ex Hy* - Ey Hx* ) dA ,

evaluated on the mode's own transverse grid (dA = dl_x * dl_y in m², midpoint quadrature). For a PML-discretized EME mode, physical_mask excludes the artificial complex-contour cells from this physical observable. The six components are in SI units (E in V/m, H in A/m), so the integral is in watts. Used by the mode-source builder to scale the injected profiles to 1 W (profile /= sqrt(P)), and by any consumer that wants the absolute (not just relative) modal power.

For the dominant-forward (+z) guided mode this is positive; a sign flip indicates a predominantly backward eigenpair.

On a half window whose mirror plane passes through column 0 (x_min_plane_on_node), that column counts at half weight, so the result is the power through the stored half window, half the full window's.

Grid-consistent modes

solve_yee_mode

solve_yee_mode(
    sim,
    axis: str,
    plane_value_um: float,
    wlen_um: float,
    pol: str,
    mode_index: int,
    *,
    h_center_um: float,
    v_center_um: float,
    half_w_um: float,
    half_v_um: float,
    dl_um: float,
    supersample: int = 8,
    num_modes: Optional[int] = None,
    eps_of_medium: Optional[Mapping[int, float]] = None,
) -> VectorMode

Solve the engine-consistent Yee-grid discrete eigenmode and return it as a VectorMode (mode-frame [iy=height, ix=width]). Dispersive media are anchored at the solve frequency (Medium.permittivity_at_hz), matching the eps the engine's ADE realizes there, NOT the eps_inf that bare permittivity carries; eps_of_medium overrides per medium.

Symmetry planes are honored AUTOMATICALLY: when sim.symmetry puts a plane on an in-plane axis' min face and the window reaches it, the window is clipped at the plane and the matching parity BC applied (PEC -1 / PMC +1, window_min_face_bcs), the half-window mode is the engine's half-domain field, and mode_index counts within the matching-parity family only.

A plain PERIODIC in-plane axis is honored the same way: the window is the whole period with the k = 0 periodic closure instead of walls, whatever half-extent was asked for (_periodic_window). On a one-cell axis (the quasi-2D reduction) the window is that single cell and the solve returns the cross-section's SLAB modes, so a Port on a quasi-2D device resolves. There "TE" is the family with E along the periodic axis (te_fraction ~1).

solve_yee_mode_bank

solve_yee_mode_bank(
    sim,
    axis: str,
    plane_value_um: float,
    freqs_hz,
    pol: str,
    mode_index: int,
    *,
    h_center_um: float,
    v_center_um: float,
    half_w_um: float,
    half_v_um: float,
    dl_um: float,
    supersample: int = 8,
    num_modes: Optional[int] = None,
    eps_of_medium: Optional[Mapping[int, float]] = None,
)

{freq_hz: VectorMode} per-frequency Yee-grid readout mode mapping, the engine- consistent analogue of mode_bank_on_cross_section (which uses the node-collocated FLM VectorModeSolver). The window geometry is sampled on the mesh ONCE; a non-dispersive cross-section shares one Yee-staggered ε for every frequency (λ-independent at constant n), while dispersive media are re-anchored at each frequency (Medium.permittivity_at_hz), then the discrete-Yee eigenproblem is re-solved per frequency, so the readout reference mode matches the FDTD field's discretization at every λ, the same discrete operator the launch used (solve_yee_mode).

Each solve fixes its mode's phase so the dominant transverse E is real and positive at its peak; for a two-lobe mode (TE1, TM1) the winning lobe, and so the sign, changes with frequency. The mode mapping is therefore phase-aligned along the band from its lowest frequency, whose mode keeps its own phase (the reference solve_yee_port_mode_bank uses too): the complex amplitude read against it, and every S-parameter phase, is continuous across the band instead of jumping by pi. Neighbouring picks that overlap below 0.5 warn.

solve_yee_multimode_bank

solve_yee_multimode_bank(
    sim,
    axis: str,
    plane_value_um: float,
    freqs_hz,
    *,
    mode_indices=(0,),
    h_center_um: float,
    v_center_um: float,
    half_w_um: float,
    half_v_um: float,
    dl_um: float,
    supersample: int = 8,
    num_modes: Optional[int] = None,
    eps_of_medium: Optional[Mapping[int, float]] = None,
)

{freq_hz: {mode_index: VectorMode}} MULTI-mode per-frequency Yee mode mapping, the engine-consistent analogue of solve_mode_bank (which needs an FLM/scalar solver object), ready for ModeMonitor.mode_decomposition.

Indexing follows solve_mode_bank's convention: mode_indices count the guided modes in descending-n_eff order ACROSS polarizations (0 = the fundamental), NOT the per-polarization (pol, mode_index) selection of solve_yee_mode_bank. Geometry is sampled on the mesh once; dispersive media are re-anchored per frequency (Medium.permittivity_at_hz).

The indices are counted at the band's middle frequency, and each mode is followed across the band by field overlap with its neighbouring frequency and phase-aligned to it: a mode keeps its index through a crossing in n_eff where the solver's own order swaps. Where two modes mix near a crossing (common for TE1 and TM0 on a Yee window), no index names one physical mode across it; the mode mapping warns when a link's overlap falls below 0.9 or a mode's TE fraction changes by more than 0.5 over the band.

solve_yee_port_mode_bank

solve_yee_port_mode_bank(
    sim,
    axis: str,
    plane_value_um: float,
    freqs_hz,
    *,
    modes=(("TE", 0),),
    h_center_um: float,
    v_center_um: float,
    half_w_um: float,
    half_v_um: float,
    dl_um: float,
    supersample: int = 8,
    num_modes: Optional[int] = None,
    thickness_axis: Optional[str] = None,
    eps_of_medium: Optional[Mapping[int, float]] = None,
)

Solve several polarization-family modes with one eigensolve per frequency.

Returns {freq_hz: {(polarization, mode_index): VectorMode}}. Unlike solve_yee_multimode_bank, each mode_index is counted within its requested TE/TM family. This is the identity exposed by a modal-port editor (TE0, TE1, TM0) and the one used by solve_yee_mode_bank for a single channel.

The cross-section is sampled on the mesh once and every requested family/index is selected from the same guided-mode frame at each frequency. That avoids repeating the sparse Yee eigensolve when one recorded plane is decomposed into, for example, both TE0 and TE1.

solve_yee_eme_basis

solve_yee_eme_basis(
    sim,
    axis: str,
    plane_value_um: float,
    wlen_um: float,
    num_modes: Optional[int] = None,
    *,
    h_center_um: float,
    v_center_um: float,
    half_w_um: float,
    half_v_um: float,
    dl_um: float,
    supersample: int = 8,
    eps_of_medium: Optional[Mapping[int, float]] = None,
    residual_tolerance: float = 1e-07,
    reaction_tolerance: float = 1e-07,
    neff_cutoff: Optional[float] = None,
    max_modes: int = 64,
) -> Tuple[VectorMode, ...]

Return an experimental propagating Yee hard-wall basis for EME.

The solve uses the same per-component KFJ permittivity and staggered curl operator as solve_yee_mode, but retains both bound modes and the positive-beta hard-wall discretization of the radiation continuum. Modes are ordered by decreasing real n_eff and labelled "guided" above the homogeneous exterior light line, otherwise "radiation". Every returned mode is lossless, yee_staggered=True, and safe under the unconjugated reaction product used by EME.

Warning

This is a propagating-only basis: it excludes beta≈0 and pure-imaginary-beta roots because the current Yee field reconstruction divides by real beta. It therefore contains guided and box-radiation modes, not the evanescent channels required for a complete EME basis. Passing the reaction guard is a numerical precondition, not evidence of quantitative device-radiation accuracy.

This first public continuum path deliberately supports only a uniform transverse lattice and a homogeneous scalar exterior. It has no modal PML: the requested window is the discretization box, so radiation modes are box modes whose quantitative use requires independent window and complete-mode-group convergence sweeps. Holding num_modes fixed while changing the window is not a valid continuum control because it changes the represented beta band and modal density. For a window sweep, omit num_modes and pass neff_cutoff: the solver returns every complete propagating mode group above that spectral cutoff, subject to the explicit max_modes safety cap.

sample_staggered_eps

sample_staggered_eps(
    sim,
    axis,
    plane_value_um,
    *,
    h_center,
    v_center,
    half_w,
    half_v,
    dl,
    supersample=8,
    eps_of_medium=None,
    freq_hz=None,
)

Diagonal subpixel eps at the Yee E-component locations, snapped to the sim grid. Returns (eps_xx, eps_yy, eps_zz, nh, nv, h_lo, v_lo): the three eps components each a flat [ihnv+iv] vector of length nhnv (eps_xx at the Ex location (+1/2 in h), eps_yy at Ey (+1/2 in v), eps_zz at the node), the grid extents, and the snapped window origin (microns), node (ih, iv) sits at (h_lo + ih*dl, v_lo + iv*dl) on a uniform grid (on a graded one the nodes are the sim's own ladder; use staggered_eps_sampler for the full geometry). h = mode-x (width), v = mode-y (height). freq_hz anchors dispersive media at that frequency (see staggered_eps_sampler); None keeps the legacy bare permittivity (= eps_inf for a Lorentz medium, wrong for a dispersive solve, so pass the solve frequency).

window_min_face_bcs

window_min_face_bcs(
    sim, axis, *, h_center, half_w, v_center, half_v, dl
)

The shared window-registration + symmetry rule for every consumer of a cross-section window (the eps sampler, the eigensolve, and the source plane MUST agree bit-for-bit on this). Returns (h_lo, v_lo, h_bc, v_bc):

  • lo = the grid-snapped window origin floor((center-half)/dl)*dl, CLIPPED to 0 on any in-plane axis carrying a symmetry plane (the plane sits on the domain min face at coordinate 0; the below-plane half of a requested window is the mirror image the boundary supplies, it must not be solved or stamped).
  • bc = the axis' symmetry parity when the (clipped) window edge lands exactly ON the plane, else None (an interior window keeps the legacy far-from-edge behavior; its tails must not reach the plane, the same immaterial-wall assumption every window edge already makes).

Cross-sections

rectangular_waveguide

rectangular_waveguide(
    *,
    wlen_um: float,
    core_w_um: float,
    core_h_um: float,
    core: Union[float, str, object],
    clad: Union[float, str, object],
    slab_h_um: float = 0.0,
    dl_um: Optional[float] = None,
    num_modes: int = 2,
    clad_pad_um: Optional[float] = None,
    window_w_um: Optional[float] = None,
    window_h_um: Optional[float] = None,
    group_index: bool = False,
    group_index_step: float = 0.005,
    bend_radius_um: Optional[float] = None,
    num_pml: int = 0,
    pml_strength: float = 1.0,
) -> WaveguideModes

Solve the guided modes of a strip or rib waveguide in one call.

Parameters

wlen_um: Free-space wavelength. core_w_um, core_h_um: Core width and total height (rib: measured from the slab bottom). core, clad: Refractive indices as floats, or photonhub.materials entries / names ("Si", "SiO2") evaluated at wlen_um. slab_h_um: Rib slab thickness (0 = strip). dl_um: Transverse grid step; default min(core_w, core_h) / 24, capped at wavelength / (12 * n_core). num_modes: Eigenpairs to return. clad_pad_um / window_w_um / window_h_um: Window sizing: explicit extents win; otherwise the core plus clad_pad_um (default max(0.5, wavelength/2)) per side. group_index: Also compute n_g per mode. With material core/clad this re-solves at wavelength * (1 +/- group_index_step) with each material's own n(lambda), waveguide AND material dispersion; with float indices it defers to the solver's built-in (waveguide-dispersion-only) central difference. bend_radius_um, num_pml, pml_strength: Bend analysis passthrough to VectorModeSolver.solve. When num_pml=0, the solver adds layers at strength 30 and warns; the default pml_strength=1 applies only with explicit layers.

Returns

WaveguideModes

WaveguideModes dataclass

WaveguideModes(
    modes: Tuple[VectorMode, ...],
    wavelength_um: float,
    n_core: float,
    n_clad: float,
    solver: VectorModeSolver,
)

Solved modes of rectangular_waveguide, sorted by descending n_eff. n_group / loss_db_per_cm are None when not requested / not applicable.

modes instance-attribute

modes: Tuple[VectorMode, ...]

wavelength_um instance-attribute

wavelength_um: float

n_core instance-attribute

n_core: float

n_clad instance-attribute

n_clad: float

solver instance-attribute

n_eff property

n_eff: ndarray

Effective indices in the stored mode order.

te_fractions property

te_fractions: ndarray

Transverse electric polarization fractions in the stored mode order.

polarizations property

polarizations: List[str]

TE/TM labels in the stored mode order.

n_group property

n_group: Optional[ndarray]

Group indices, or None when none are recorded.

If only some modes have group indices, missing values are NaN.

loss_db_per_cm property

loss_db_per_cm: Optional[ndarray]

Modal loss in dB/cm, or None when every mode has zero loss.

fundamental

fundamental(polarization: str = 'TE') -> VectorMode

The highest-n_eff mode of the requested TE/TM family (by te_fraction majority).

summary

summary() -> str

Format wavelength, material indices, and each mode's recorded properties.

Include group index and loss when available. This does not solve modes or establish convergence.

solve_mode_on_cross_section

solve_mode_on_cross_section(
    sim,
    axis: str,
    plane_value_um: float,
    wlen_um: float,
    pol: str,
    mode_index: int,
    *,
    h_center_um: float,
    v_center_um: float,
    half_w_um: float,
    half_v_um: float,
    dl_um: float,
    supersample: int = 8,
    x_symmetry: str = "none",
    num_modes: Optional[int] = None,
    eps_of_medium: Optional[Mapping[int, float]] = None,
    use_yee: bool = True,
)

Full-vector mode of the ACTUAL cross-section at axis = plane_value_um, windowed to (h_center +/- half_w, v_center +/- half_v). The engine-wide default reference-mode builder for both launch and readout.

use_yee (default True) solves the engine-consistent discrete-Yee eigenmode (solve_yee_mode): the mode on the FDTD's own Yee-staggered grid, so source and readout match the propagated field's discretization exactly (the discrete form of that projection). use_yee=False falls back to the node-collocated Fallahkhair–Li–Murphy VectorModeSolver (the prior default; kept for A/B and backward comparison). Both read the real geometry + diagonal KFJ subpixel and return the mode_index-th pol mode (n_eff-descending).

Symmetry: the Yee path honors sim.symmetry planes AUTOMATICALLY (window clipped at the plane + the matching parity BC; see window_min_face_bcs), nothing to pass. A plain periodic in-plane axis is honored too: the window is the whole period with the k = 0 periodic closure (the half-extent asked for on that axis is not used), and a one-cell (quasi-2D) axis yields the cross-section's slab modes (see solve_yee_mode). The FLM path knows neither. x_symmetry is the FLM path's MANUAL width-axis wall control only ("none" = electric walls, "pmc" = magnetic; it is NOT read from the sim and does not affect the Yee path).

Window size on a half domain. half_w_um/half_v_um mean the same physical half-extent either way (on an axis with a symmetry plane the mirror supplies the other half), but they do NOT buy the same accuracy. Every window truncates the mode's tail against a hard wall, and the two walls of a whole-domain window bias n_eff in OPPOSITE directions, so their errors partly cancel; a symmetry plane replaces one of those walls with the exact mirror and removes the cancellation. On a 500 x 220 nm Si strip at 37 nm cells, half_v_um 0.31 reads 2.4443 on the whole domain and 2.4281 on the half domain, against 2.4467 for both once the window is large enough. Size the window until the reading stops moving; the solve warns when the mode is still large on a wall. Note also that a half domain must put the guide's mirror plane on coordinate 0, which is a grid NODE: a whole domain that centres the same guide on a half-node is a different discretization of that guide and legitimately reads a different n_eff.

Dispersive media are anchored at the solve frequency automatically (Medium.permittivity_at_hz); an explicit eps_of_medium overrides that per medium. Its keys are id() of media: the one you passed in, or the one sim.structures holds. A materials-library entry such as ph.materials.Si is not a medium: the simulation holds a Medium made from it, so key that one (sim.structures[i].medium). A key that matches no medium of the simulation warns and is ignored; a key on a structure outside the window is fine. A domain fit (domain=ph.Domain()) makes media of its own: a medium placed by node coordinates (ph.perturbed_medium) moves into the wire frame as a copy, and the guide extension the fit adds behind a port owns the port plane with the guide's medium, or with a box-spanning grid (Medium.from_eps_array) placed by node coordinates. Each records the media it stands for, so an anchor keyed on the guide's medium, as you passed it or as sim.structures holds it, holds on the moved copy and on the extensions too; a key on a structure's own medium wins. That record is not on the wire: on a simulation read back from a file every structure holds a medium of its own, so key each one. Unless eps_of_medium is given, the returned mode carries its solve_params provenance, which lets ModeMonitor re-solve the SAME mode identity at every monitor frequency (the automatic per-frequency readout mode mapping).

mode_bank_on_cross_section

mode_bank_on_cross_section(
    sim,
    axis: str,
    plane_value_um: float,
    freqs_hz,
    pol: str,
    mode_index: int,
    *,
    h_center_um: float,
    v_center_um: float,
    half_w_um: float,
    half_v_um: float,
    dl_um: float,
    supersample: int = 8,
    x_symmetry: str = "none",
    num_modes: Optional[int] = None,
    eps_of_medium=None,
    use_yee: bool = True,
)

{freq_hz: VectorMode} per-frequency readout mode mapping for a port. The window geometry is sampled on the mesh once; a NON-dispersive cross-section shares one ε for every frequency (λ-independent at constant n), while dispersive media are re-anchored at each mode mapping frequency (Medium.permittivity_at_hz) so both the material AND waveguide dispersion land in the per-λ modes.

use_yee (default True) uses the engine-consistent discrete-Yee solver (solve_yee_mode_bank) so the readout reference mode matches the FDTD field's discretization at every λ, the same operator the launch used. use_yee=False re-solves via the node-collocated FLM VectorModeSolver.at_wavelength (the prior default; kept for A/B).

sample_cross_section_kfj

sample_cross_section_kfj(
    sim,
    axis: str,
    plane_value_um: float,
    *,
    h_lo_um: float,
    h_hi_um: float,
    v_lo_um: float,
    v_hi_um: float,
    dl_um: float,
    supersample: int = 8,
    eps_of_medium: Optional[Mapping[int, float]] = None,
    freq_hz: Optional[float] = None,
) -> Tuple[ndarray, Tuple[ndarray, ndarray, ndarray]]

Diagonal KFJ tensor of the real cross-section over a window, on the sim grid.

Returns (eps_scalar = eps_par, (exx, eyy, ezz)) indexed [iy=height, ix=width], a drop-in for VectorModeSolver._kfj_tensor_rect's output but built from the actual sim geometry. The window [h_lo,h_hi] x [v_lo,v_hi] (in the cut's (h, v) in-plane axes) is sampled at dl_um and supersampled supersamplex to get per-cell fill fractions. Material values: eps_of_medium ({id(medium): eps_value}) overrides win; otherwise each medium is anchored at freq_hz when given (Medium.permittivity_at_hz, REQUIRED for a correct dispersive solve; bare permittivity is only eps_inf there), else bare permittivity (legacy).