You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
meanas/meanas/fdfd/waveguide_3d.py

236 lines
8.7 KiB
Python

"""
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.
"""
from typing import Dict, Optional, Sequence, Union, Any
import numpy # type: ignore
from ..fdmath import vec, unvec, dx_lists_t, fdfield_t
from . import operators, waveguide_2d
def solve_mode(mode_number: int,
omega: complex,
dxes: dx_lists_t,
axis: int,
polarity: int,
slices: Sequence[slice],
epsilon: fdfield_t,
mu: Optional[fdfield_t] = None,
) -> Dict[str, Union[complex, numpy.ndarray]]:
"""
Given a 3D grid, selects a slice from the grid and attempts to
solve for an eigenmode propagating through that slice.
Args:
mode_number: Number of the mode, 0-indexed
omega: Angular frequency of the simulation
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types`
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.
epsilon: Dielectric constant
mu: Magnetic permeability (default 1 everywhere)
Returns:
`{'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex}`
"""
if mu is None:
mu = numpy.ones_like(epsilon)
slices = tuple(slices)
'''
Solve the 2D problem in the specified plane
'''
# Define rotation to set z as propagation direction
order = numpy.roll(range(3), 2 - axis)
reverse_order = numpy.roll(range(3), axis - 2)
# Find dx in propagation direction
dxab_forward = numpy.array([dx[order[2]][slices[order[2]]] for dx in dxes])
dx_prop = 0.5 * dxab_forward.sum()
# Reduce to 2D and solve the 2D problem
args_2d: Dict[str, Any] = {
'omega': omega,
'dxes': [[dx[i][slices[i]] for i in order[:2]] for dx in dxes],
'epsilon': vec([epsilon[i][slices].transpose(order) for i in order]),
'mu': vec([mu[i][slices].transpose(order) for i in order]),
}
e_xy, wavenumber_2d = waveguide_2d.solve_mode(mode_number, **args_2d)
'''
Apply corrections and expand to 3D
'''
# Correct wavenumber to account for numerical dispersion.
wavenumber = 2 / dx_prop * numpy.arcsin(wavenumber_2d * dx_prop / 2)
shape = [d.size for d in args_2d['dxes'][0]]
ve, vh = waveguide_2d.normalized_fields_e(e_xy, wavenumber=wavenumber_2d, prop_phase=dx_prop * wavenumber, **args_2d)
e = unvec(ve, shape)
h = unvec(vh, shape)
# Adjust for propagation direction
h *= polarity # type: ignore # mypy issue with numpy
# Apply phase shift to H-field
h[:2] *= numpy.exp(-1j * polarity * 0.5 * wavenumber * dx_prop)
e[2] *= numpy.exp(-1j * polarity * 0.5 * wavenumber * dx_prop)
# Expand E, H to full epsilon space we were given
E = numpy.zeros_like(epsilon, dtype=complex)
H = numpy.zeros_like(epsilon, dtype=complex)
for a, o in enumerate(reverse_order):
E[(a, *slices)] = e[o][:, :, None].transpose(reverse_order)
H[(a, *slices)] = h[o][:, :, None].transpose(reverse_order)
results = {
'wavenumber': wavenumber,
'wavenumber_2d': wavenumber_2d,
'H': H,
'E': E,
}
return results
def compute_source(E: fdfield_t,
wavenumber: complex,
omega: complex,
dxes: dx_lists_t,
axis: int,
polarity: int,
slices: Sequence[slice],
epsilon: fdfield_t,
mu: Optional[fdfield_t] = None,
) -> fdfield_t:
"""
Given an eigenmode obtained by `solve_mode`, returns the current source distribution
necessary to position a unidirectional source at the slice location.
Args:
E: E-field of the mode
wavenumber: Wavenumber of the mode
omega: Angular frequency of the simulation
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types`
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.
mu: Magnetic permeability (default 1 everywhere)
Returns:
J distribution for the unidirectional source
"""
E_expanded = expand_e(E=E, dxes=dxes, wavenumber=wavenumber, axis=axis,
polarity=polarity, slices=slices)
smask = [slice(None)] * 4
if polarity > 0:
smask[axis + 1] = slice(slices[axis].start, None)
else:
smask[axis + 1] = slice(None, slices[axis].stop)
mask = numpy.zeros_like(E_expanded, dtype=int)
mask[tuple(smask)] = 1
masked_e2j = operators.e_boundary_source(mask=vec(mask), omega=omega, dxes=dxes, epsilon=vec(epsilon), mu=vec(mu))
J = unvec(masked_e2j @ vec(E_expanded), E.shape[1:])
return J
def compute_overlap_e(E: fdfield_t,
wavenumber: complex,
dxes: dx_lists_t,
axis: int,
polarity: int,
slices: Sequence[slice],
) -> fdfield_t: # TODO DOCS
"""
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].
Args:
E: E-field of the mode
H: H-field of the mode (advanced by half of a Yee cell from E)
wavenumber: Wavenumber of the mode
omega: Angular frequency of the simulation
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types`
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.
mu: Magnetic permeability (default 1 everywhere)
Returns:
overlap_e such that `numpy.sum(overlap_e * other_e)` computes the overlap integral
"""
slices = tuple(slices)
Ee = expand_e(E=E, wavenumber=wavenumber, dxes=dxes,
axis=axis, polarity=polarity, slices=slices)
start, stop = sorted((slices[axis].start, slices[axis].start - 2 * polarity))
slices2_l = list(slices)
slices2_l[axis] = slice(start, stop)
slices2 = (slice(None), *slices2_l)
Etgt = numpy.zeros_like(Ee)
Etgt[slices2] = Ee[slices2]
Etgt /= (Etgt.conj() * Etgt).sum()
return Etgt
def expand_e(E: fdfield_t,
wavenumber: complex,
dxes: dx_lists_t,
axis: int,
polarity: int,
slices: Sequence[slice],
) -> fdfield_t:
"""
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
entire domain; it is up to the caller to truncate the expansion to any regions
where it is valid.
Args:
E: E-field of the mode
wavenumber: Wavenumber of the mode
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types`
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.
Returns:
`E`, with the original field expanded along the specified `axis`.
"""
slices = tuple(slices)
# Determine phase factors for parallel slices
a_shape = numpy.roll([1, -1, 1, 1], axis)
a_E = numpy.real(dxes[0][axis]).cumsum()
r_E = a_E - a_E[slices[axis]]
iphi = polarity * -1j * wavenumber
phase_E = numpy.exp(iphi * r_E).reshape(a_shape)
# Expand our slice to the entire grid using the phase factors
E_expanded = numpy.zeros_like(E)
slices_exp_l = list(slices)
slices_exp_l[axis] = slice(E.shape[axis + 1])
slices_exp = (slice(None), *slices_exp_l)
slices_in = (slice(None), *slices)
E_expanded[slices_exp] = phase_E * numpy.array(E)[slices_in]
return E_expanded