Rework field types, use sparse arrays instead of matrices, rework eme arg naming, improve type annotations and linter cleanup

This commit is contained in:
jan 2025-12-10 02:14:20 -08:00
commit b486fa325b
20 changed files with 571 additions and 433 deletions

View file

@ -106,7 +106,7 @@ import scipy.optimize
from scipy.linalg import norm
import scipy.sparse.linalg as spalg
from ..fdmath import fdfield_t, cfdfield_t
from ..fdmath import fdfield, cfdfield, cfdfield_t
logger = logging.getLogger(__name__)
@ -183,8 +183,8 @@ def generate_kmn(
def maxwell_operator(
k0: ArrayLike,
G_matrix: ArrayLike,
epsilon: fdfield_t,
mu: fdfield_t | None = None
epsilon: fdfield,
mu: fdfield | None = None
) -> Callable[[NDArray[numpy.complex128]], NDArray[numpy.complex128]]:
"""
Generate the Maxwell operator
@ -276,7 +276,7 @@ def maxwell_operator(
def hmn_2_exyz(
k0: ArrayLike,
G_matrix: ArrayLike,
epsilon: fdfield_t,
epsilon: fdfield,
) -> Callable[[NDArray[numpy.complex128]], cfdfield_t]:
"""
Generate an operator which converts a vectorized spatial-frequency-space
@ -308,7 +308,8 @@ def hmn_2_exyz(
- m * hin_n) * k_mag
# divide by epsilon
return numpy.moveaxis(ifftn(d_xyz, axes=range(3)) / epsilon, 3, 0)
exyz = numpy.moveaxis(ifftn(d_xyz, axes=range(3)) / epsilon, 3, 0)
return cfdfield_t(exyz)
return operator
@ -316,7 +317,7 @@ def hmn_2_exyz(
def hmn_2_hxyz(
k0: ArrayLike,
G_matrix: ArrayLike,
epsilon: fdfield_t
epsilon: fdfield,
) -> Callable[[NDArray[numpy.complex128]], cfdfield_t]:
"""
Generate an operator which converts a vectorized spatial-frequency-space
@ -343,8 +344,8 @@ def hmn_2_hxyz(
def operator(h: NDArray[numpy.complex128]) -> cfdfield_t:
hin_m, hin_n = (hi.reshape(shape) for hi in numpy.split(h, 2))
h_xyz = (m * hin_m
+ n * hin_n) # noqa: E128
return numpy.array([ifftn(hi) for hi in numpy.moveaxis(h_xyz, 3, 0)])
+ n * hin_n)
return cfdfield_t(numpy.array([ifftn(hi) for hi in numpy.moveaxis(h_xyz, 3, 0)]))
return operator
@ -352,8 +353,8 @@ def hmn_2_hxyz(
def inverse_maxwell_operator_approx(
k0: ArrayLike,
G_matrix: ArrayLike,
epsilon: fdfield_t,
mu: fdfield_t | None = None,
epsilon: fdfield,
mu: fdfield | None = None,
) -> Callable[[NDArray[numpy.complex128]], NDArray[numpy.complex128]]:
"""
Generate an approximate inverse of the Maxwell operator,
@ -440,8 +441,8 @@ def find_k(
tolerance: float,
direction: ArrayLike,
G_matrix: ArrayLike,
epsilon: fdfield_t,
mu: fdfield_t | None = None,
epsilon: fdfield,
mu: fdfield | None = None,
band: int = 0,
k_bounds: tuple[float, float] = (0, 0.5),
k_guess: float | None = None,
@ -508,8 +509,8 @@ def eigsolve(
num_modes: int,
k0: ArrayLike,
G_matrix: ArrayLike,
epsilon: fdfield_t,
mu: fdfield_t | None = None,
epsilon: fdfield,
mu: fdfield | None = None,
tolerance: float = 1e-7,
max_iters: int = 10000,
reset_iters: int = 100,
@ -649,7 +650,7 @@ def eigsolve(
def Qi_func(theta: float, Qi_memo=Qi_memo, ZtZ=ZtZ, DtD=DtD, symZtD=symZtD) -> float: # noqa: ANN001
if Qi_memo[0] == theta:
return cast(float, Qi_memo[1])
return cast('float', Qi_memo[1])
c = numpy.cos(theta)
s = numpy.sin(theta)
@ -668,8 +669,8 @@ def eigsolve(
else:
raise Exception('Inexplicable singularity in trace_func') from err
Qi_memo[0] = theta
Qi_memo[1] = cast(float, Qi)
return cast(float, Qi)
Qi_memo[1] = cast('float', Qi)
return cast('float', Qi)
def trace_func(theta: float, ZtAZ=ZtAZ, DtAD=DtAD, symZtAD=symZtAD) -> float: # noqa: ANN001
c = numpy.cos(theta)
@ -801,7 +802,12 @@ def _symmetrize(A: NDArray[numpy.complex128]) -> NDArray[numpy.complex128]:
def inner_product(eL, hL, eR, hR) -> complex:
def inner_product(
eL: cfdfield,
hL: cfdfield,
eR: cfdfield,
hR: cfdfield,
) -> complex:
# assumes x-axis propagation
assert numpy.array_equal(eR.shape, hR.shape)
@ -829,7 +835,12 @@ def inner_product(eL, hL, eR, hR) -> complex:
return eRxhL_x.sum() - eLxhR_x.sum()
def trq(eI, hI, eO, hO) -> tuple[complex, complex]:
def trq(
eI: cfdfield,
hI: cfdfield,
eO: cfdfield,
hO: cfdfield,
) -> tuple[complex, complex]:
pp = inner_product(eO, hO, eI, hI)
pn = inner_product(eO, hO, eI, -hI)
np = inner_product(eO, -hO, eI, hI)

View file

@ -1,20 +1,29 @@
from collections.abc import Sequence
import numpy
from numpy.typing import NDArray
from scipy import sparse
from ..fdmath import vec, unvec, dx_lists_t, vfdfield_t, vcfdfield_t
from ..fdmath import dx_lists2_t, vcfdfield2
from .waveguide_2d import inner_product
def get_tr(ehL, wavenumbers_L, ehR, wavenumbers_R, dxes: dx_lists_t):
def get_tr(
ehLs: Sequence[Sequence[vcfdfield2]],
wavenumbers_L: Sequence[complex],
ehRs: Sequence[Sequence[vcfdfield2]],
wavenumbers_R: Sequence[complex],
dxes: dx_lists2_t,
) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]:
nL = len(wavenumbers_L)
nR = len(wavenumbers_R)
A12 = numpy.zeros((nL, nR), dtype=complex)
A21 = numpy.zeros((nL, nR), dtype=complex)
B11 = numpy.zeros((nL,), dtype=complex)
for ll in range(nL):
eL, hL = ehL[ll]
eL, hL = ehLs[ll]
B11[ll] = inner_product(eL, hL, dxes=dxes, conj_h=False)
for rr in range(nR):
eR, hR = ehR[rr]
eR, hR = ehRs[rr]
A12[ll, rr] = inner_product(eL, hR, dxes=dxes, conj_h=False) # TODO optimize loop?
A21[ll, rr] = inner_product(eR, hL, dxes=dxes, conj_h=False)
@ -32,9 +41,15 @@ def get_tr(ehL, wavenumbers_L, ehR, wavenumbers_R, dxes: dx_lists_t):
return tt, rr
def get_abcd(eL_xys, wavenumbers_L, eR_xys, wavenumbers_R, **kwargs):
t12, r12 = get_tr(eL_xys, wavenumbers_L, eR_xys, wavenumbers_R, **kwargs)
t21, r21 = get_tr(eR_xys, wavenumbers_R, eL_xys, wavenumbers_L, **kwargs)
def get_abcd(
ehLs: Sequence[Sequence[vcfdfield2]],
wavenumbers_L: Sequence[complex],
ehRs: Sequence[Sequence[vcfdfield2]],
wavenumbers_R: Sequence[complex],
**kwargs,
) -> sparse.sparray:
t12, r12 = get_tr(ehLs, wavenumbers_L, ehRs, wavenumbers_R, **kwargs)
t21, r21 = get_tr(ehRs, wavenumbers_R, ehLs, wavenumbers_L, **kwargs)
t21i = numpy.linalg.pinv(t21)
A = t12 - r21 @ t21i @ r12
B = r21 @ t21i
@ -44,15 +59,16 @@ def get_abcd(eL_xys, wavenumbers_L, eR_xys, wavenumbers_R, **kwargs):
def get_s(
eL_xys,
wavenumbers_L,
eR_xys,
wavenumbers_R,
ehLs: Sequence[Sequence[vcfdfield2]],
wavenumbers_L: Sequence[complex],
ehRs: Sequence[Sequence[vcfdfield2]],
wavenumbers_R: Sequence[complex],
force_nogain: bool = False,
force_reciprocal: bool = False,
**kwargs):
t12, r12 = get_tr(eL_xys, wavenumbers_L, eR_xys, wavenumbers_R, **kwargs)
t21, r21 = get_tr(eR_xys, wavenumbers_R, eL_xys, wavenumbers_L, **kwargs)
**kwargs,
) -> NDArray[numpy.complex128]:
t12, r12 = get_tr(ehLs, wavenumbers_L, ehRs, wavenumbers_R, **kwargs)
t21, r21 = get_tr(ehRs, wavenumbers_R, ehLs, wavenumbers_L, **kwargs)
ss = numpy.block([[r12, t12],
[t21, r21]])

View file

@ -1,14 +1,16 @@
"""
Functions for performing near-to-farfield transformation (and the reverse).
"""
from typing import Any, cast
from collections.abc import Sequence
from typing import Any, cast, TYPE_CHECKING
import numpy
from numpy.fft import fft2, fftshift, fftfreq, ifft2, ifftshift
from numpy import pi
from ..fdmath import cfdfield_t
if TYPE_CHECKING:
from collections.abc import Sequence
def near_to_farfield(
E_near: cfdfield_t,
@ -63,7 +65,7 @@ def near_to_farfield(
padded_size = (2**numpy.ceil(numpy.log2(s))).astype(int)
if not hasattr(padded_size, '__len__'):
padded_size = (padded_size, padded_size) # type: ignore # checked if sequence
padded_shape = cast(Sequence[int], padded_size)
padded_shape = cast('Sequence[int]', padded_size)
En_fft = [fftshift(fft2(fftshift(Eni), s=padded_shape)) for Eni in E_near]
Hn_fft = [fftshift(fft2(fftshift(Hni), s=padded_shape)) for Hni in H_near]
@ -172,7 +174,7 @@ def far_to_nearfield(
padded_size = (2 ** numpy.ceil(numpy.log2(s))).astype(int)
if not hasattr(padded_size, '__len__'):
padded_size = (padded_size, padded_size) # type: ignore # checked if sequence
padded_shape = cast(Sequence[int], padded_size)
padded_shape = cast('Sequence[int]', padded_size)
k = 2 * pi
kxs = fftshift(fftfreq(s[0], 1 / (s[0] * dkx)))

View file

@ -8,7 +8,7 @@ e.g. E = [E_x, E_y, E_z] where each (complex) component has shape (X, Y, Z)
from collections.abc import Callable
import numpy
from ..fdmath import dx_lists_t, fdfield_t, cfdfield_t, cfdfield_updater_t
from ..fdmath import dx_lists_t, cfdfield_t, fdfield, cfdfield, cfdfield_updater_t
from ..fdmath.functional import curl_forward, curl_back
@ -18,8 +18,8 @@ __author__ = 'Jan Petykiewicz'
def e_full(
omega: complex,
dxes: dx_lists_t,
epsilon: fdfield_t,
mu: fdfield_t | None = None,
epsilon: fdfield,
mu: fdfield | None = None,
) -> cfdfield_updater_t:
"""
Wave operator for use with E-field. See `operators.e_full` for details.
@ -37,13 +37,13 @@ def e_full(
ch = curl_back(dxes[1])
ce = curl_forward(dxes[0])
def op_1(e: cfdfield_t) -> cfdfield_t:
def op_1(e: cfdfield) -> cfdfield_t:
curls = ch(ce(e))
return curls - omega ** 2 * epsilon * e
return cfdfield_t(curls - omega ** 2 * epsilon * e)
def op_mu(e: cfdfield_t) -> cfdfield_t:
def op_mu(e: cfdfield) -> cfdfield_t:
curls = ch(mu * ce(e)) # type: ignore # mu = None ok because we don't return the function
return curls - omega ** 2 * epsilon * e
return cfdfield_t(curls - omega ** 2 * epsilon * e)
if mu is None:
return op_1
@ -53,9 +53,9 @@ def e_full(
def eh_full(
omega: complex,
dxes: dx_lists_t,
epsilon: fdfield_t,
mu: fdfield_t | None = None,
) -> Callable[[cfdfield_t, cfdfield_t], tuple[cfdfield_t, cfdfield_t]]:
epsilon: fdfield,
mu: fdfield | None = None,
) -> Callable[[cfdfield, cfdfield], tuple[cfdfield_t, cfdfield_t]]:
"""
Wave operator for full (both E and H) field representation.
See `operators.eh_full`.
@ -73,13 +73,13 @@ def eh_full(
ch = curl_back(dxes[1])
ce = curl_forward(dxes[0])
def op_1(e: cfdfield_t, h: cfdfield_t) -> tuple[cfdfield_t, cfdfield_t]:
return (ch(h) - 1j * omega * epsilon * e,
ce(e) + 1j * omega * h)
def op_1(e: cfdfield, h: cfdfield) -> tuple[cfdfield_t, cfdfield_t]:
return (cfdfield_t(ch(h) - 1j * omega * epsilon * e),
cfdfield_t(ce(e) + 1j * omega * h))
def op_mu(e: cfdfield_t, h: cfdfield_t) -> tuple[cfdfield_t, cfdfield_t]:
return (ch(h) - 1j * omega * epsilon * e,
ce(e) + 1j * omega * mu * h) # type: ignore # mu=None ok
def op_mu(e: cfdfield, h: cfdfield) -> tuple[cfdfield_t, cfdfield_t]:
return (cfdfield_t(ch(h) - 1j * omega * epsilon * e),
cfdfield_t(ce(e) + 1j * omega * mu * h)) # type: ignore # mu=None ok
if mu is None:
return op_1
@ -89,7 +89,7 @@ def eh_full(
def e2h(
omega: complex,
dxes: dx_lists_t,
mu: fdfield_t | None = None,
mu: fdfield | None = None,
) -> cfdfield_updater_t:
"""
Utility operator for converting the `E` field into the `H` field.
@ -106,11 +106,11 @@ def e2h(
"""
ce = curl_forward(dxes[0])
def e2h_1_1(e: cfdfield_t) -> cfdfield_t:
return ce(e) / (-1j * omega)
def e2h_1_1(e: cfdfield) -> cfdfield_t:
return cfdfield_t(ce(e) / (-1j * omega))
def e2h_mu(e: cfdfield_t) -> cfdfield_t:
return ce(e) / (-1j * omega * mu) # type: ignore # mu=None ok
def e2h_mu(e: cfdfield) -> cfdfield_t:
return cfdfield_t(ce(e) / (-1j * omega * mu)) # type: ignore # mu=None ok
if mu is None:
return e2h_1_1
@ -120,7 +120,7 @@ def e2h(
def m2j(
omega: complex,
dxes: dx_lists_t,
mu: fdfield_t | None = None,
mu: fdfield | None = None,
) -> cfdfield_updater_t:
"""
Utility operator for converting magnetic current `M` distribution
@ -138,13 +138,13 @@ def m2j(
"""
ch = curl_back(dxes[1])
def m2j_mu(m: cfdfield_t) -> cfdfield_t:
def m2j_mu(m: cfdfield) -> cfdfield_t:
J = ch(m / mu) / (-1j * omega) # type: ignore # mu=None ok
return J
return cfdfield_t(J)
def m2j_1(m: cfdfield_t) -> cfdfield_t:
def m2j_1(m: cfdfield) -> cfdfield_t:
J = ch(m) / (-1j * omega)
return J
return cfdfield_t(J)
if mu is None:
return m2j_1
@ -152,11 +152,11 @@ def m2j(
def e_tfsf_source(
TF_region: fdfield_t,
TF_region: fdfield,
omega: complex,
dxes: dx_lists_t,
epsilon: fdfield_t,
mu: fdfield_t | None = None,
epsilon: fdfield,
mu: fdfield | None = None,
) -> cfdfield_updater_t:
"""
Operator that turns an E-field distribution into a total-field/scattered-field
@ -178,13 +178,13 @@ def e_tfsf_source(
# TODO documentation
A = e_full(omega, dxes, epsilon, mu)
def op(e: cfdfield_t) -> cfdfield_t:
def op(e: cfdfield) -> cfdfield_t:
neg_iwj = A(TF_region * e) - TF_region * A(e)
return neg_iwj / (-1j * omega)
return cfdfield_t(neg_iwj / (-1j * omega))
return op
def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield_t, cfdfield_t], cfdfield_t]:
def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield, cfdfield], cfdfield_t]:
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
@ -206,7 +206,7 @@ def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield_t, cfdfield_t], c
Returns:
Function `f` that returns E x H as required for the poynting vector.
"""
def exh(e: cfdfield_t, h: cfdfield_t) -> cfdfield_t:
def exh(e: cfdfield, h: cfdfield) -> cfdfield_t:
s = numpy.empty_like(e)
ex = e[0] * dxes[0][0][:, None, None]
ey = e[1] * dxes[0][1][None, :, None]
@ -217,5 +217,5 @@ def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield_t, cfdfield_t], c
s[0] = numpy.roll(ey, -1, axis=0) * hz - numpy.roll(ez, -1, axis=0) * hy
s[1] = numpy.roll(ez, -1, axis=1) * hx - numpy.roll(ex, -1, axis=1) * hz
s[2] = numpy.roll(ex, -1, axis=2) * hy - numpy.roll(ey, -1, axis=2) * hx
return s
return cfdfield_t(s)
return exh

View file

@ -1,7 +1,7 @@
"""
Sparse matrix operators for use with electromagnetic wave equations.
These functions return sparse-matrix (`scipy.sparse.spmatrix`) representations of
These functions return sparse-matrix (`scipy.sparse.sparray`) representations of
a variety of operators, intended for use with E and H fields vectorized using the
`meanas.fdmath.vectorization.vec()` and `meanas.fdmath.vectorization.unvec()` functions.
@ -30,7 +30,7 @@ The following operators are included:
import numpy
from scipy import sparse
from ..fdmath import vec, dx_lists_t, vfdfield_t, vcfdfield_t
from ..fdmath import vec, dx_lists_t, vfdfield, vcfdfield
from ..fdmath.operators import shift_with_mirror, shift_circ, curl_forward, curl_back
@ -40,11 +40,11 @@ __author__ = 'Jan Petykiewicz'
def e_full(
omega: complex,
dxes: dx_lists_t,
epsilon: vfdfield_t | vcfdfield_t,
mu: vfdfield_t | None = None,
pec: vfdfield_t | None = None,
pmc: vfdfield_t | None = None,
) -> sparse.spmatrix:
epsilon: vfdfield | vcfdfield,
mu: vfdfield | None = None,
pec: vfdfield | None = None,
pmc: vfdfield | None = None,
) -> sparse.sparray:
r"""
Wave operator
$$ \nabla \times (\frac{1}{\mu} \nabla \times) - \Omega^2 \epsilon $$
@ -77,20 +77,20 @@ def e_full(
ce = curl_forward(dxes[0])
if pec is None:
pe = sparse.eye(epsilon.size)
pe = sparse.eye_array(epsilon.size)
else:
pe = sparse.diags(numpy.where(pec, 0, 1)) # Set pe to (not PEC)
pe = sparse.diags_array(numpy.where(pec, 0, 1)) # Set pe to (not PEC)
if pmc is None:
pm = sparse.eye(epsilon.size)
pm = sparse.eye_array(epsilon.size)
else:
pm = sparse.diags(numpy.where(pmc, 0, 1)) # set pm to (not PMC)
pm = sparse.diags_array(numpy.where(pmc, 0, 1)) # set pm to (not PMC)
e = sparse.diags(epsilon)
e = sparse.diags_array(epsilon)
if mu is None:
m_div = sparse.eye(epsilon.size)
m_div = sparse.eye_array(epsilon.size)
else:
m_div = sparse.diags(1 / mu)
m_div = sparse.diags_array(1 / mu)
op = pe @ (ch @ pm @ m_div @ ce - omega**2 * e) @ pe
return op
@ -98,7 +98,7 @@ def e_full(
def e_full_preconditioners(
dxes: dx_lists_t,
) -> tuple[sparse.spmatrix, sparse.spmatrix]:
) -> tuple[sparse.sparray, sparse.sparray]:
"""
Left and right preconditioners `(Pl, Pr)` for symmetrizing the `e_full` wave operator.
@ -118,19 +118,19 @@ def e_full_preconditioners(
dxes[1][0][:, None, None] * dxes[1][1][None, :, None] * dxes[0][2][None, None, :]]
p_vector = numpy.sqrt(vec(p_squared))
P_left = sparse.diags(p_vector)
P_right = sparse.diags(1 / p_vector)
P_left = sparse.diags_array(p_vector)
P_right = sparse.diags_array(1 / p_vector)
return P_left, P_right
def h_full(
omega: complex,
dxes: dx_lists_t,
epsilon: vfdfield_t,
mu: vfdfield_t | None = None,
pec: vfdfield_t | None = None,
pmc: vfdfield_t | None = None,
) -> sparse.spmatrix:
epsilon: vfdfield,
mu: vfdfield | None = None,
pec: vfdfield | None = None,
pmc: vfdfield | None = None,
) -> sparse.sparray:
r"""
Wave operator
$$ \nabla \times (\frac{1}{\epsilon} \nabla \times) - \omega^2 \mu $$
@ -161,20 +161,20 @@ def h_full(
ce = curl_forward(dxes[0])
if pec is None:
pe = sparse.eye(epsilon.size)
pe = sparse.eye_array(epsilon.size)
else:
pe = sparse.diags(numpy.where(pec, 0, 1)) # set pe to (not PEC)
pe = sparse.diags_array(numpy.where(pec, 0, 1)) # set pe to (not PEC)
if pmc is None:
pm = sparse.eye(epsilon.size)
pm = sparse.eye_array(epsilon.size)
else:
pm = sparse.diags(numpy.where(pmc, 0, 1)) # Set pe to (not PMC)
pm = sparse.diags_array(numpy.where(pmc, 0, 1)) # Set pe to (not PMC)
e_div = sparse.diags(1 / epsilon)
e_div = sparse.diags_array(1 / epsilon)
if mu is None:
m = sparse.eye(epsilon.size)
m = sparse.eye_array(epsilon.size)
else:
m = sparse.diags(mu)
m = sparse.diags_array(mu)
A = pm @ (ce @ pe @ e_div @ ch - omega**2 * m) @ pm
return A
@ -183,11 +183,11 @@ def h_full(
def eh_full(
omega: complex,
dxes: dx_lists_t,
epsilon: vfdfield_t,
mu: vfdfield_t | None = None,
pec: vfdfield_t | None = None,
pmc: vfdfield_t | None = None,
) -> sparse.spmatrix:
epsilon: vfdfield,
mu: vfdfield | None = None,
pec: vfdfield | None = None,
pmc: vfdfield | None = None,
) -> sparse.sparray:
r"""
Wave operator for `[E, H]` field representation. This operator implements Maxwell's
equations without cancelling out either E or H. The operator is
@ -227,35 +227,35 @@ def eh_full(
Sparse matrix containing the wave operator.
"""
if pec is None:
pe = sparse.eye(epsilon.size)
pe = sparse.eye_array(epsilon.size)
else:
pe = sparse.diags(numpy.where(pec, 0, 1)) # set pe to (not PEC)
pe = sparse.diags_array(numpy.where(pec, 0, 1)) # set pe to (not PEC)
if pmc is None:
pm = sparse.eye(epsilon.size)
pm = sparse.eye_array(epsilon.size)
else:
pm = sparse.diags(numpy.where(pmc, 0, 1)) # set pm to (not PMC)
pm = sparse.diags_array(numpy.where(pmc, 0, 1)) # set pm to (not PMC)
iwe = pe @ (1j * omega * sparse.diags(epsilon)) @ pe
iwe = pe @ (1j * omega * sparse.diags_array(epsilon)) @ pe
iwm = 1j * omega
if mu is not None:
iwm *= sparse.diags(mu)
iwm *= sparse.diags_array(mu)
iwm = pm @ iwm @ pm
A1 = pe @ curl_back(dxes[1]) @ pm
A2 = pm @ curl_forward(dxes[0]) @ pe
A = sparse.bmat([[-iwe, A1],
[A2, iwm]])
A = sparse.block_array([[-iwe, A1],
[A2, iwm]])
return A
def e2h(
omega: complex,
dxes: dx_lists_t,
mu: vfdfield_t | None = None,
pmc: vfdfield_t | None = None,
) -> sparse.spmatrix:
mu: vfdfield | None = None,
pmc: vfdfield | None = None,
) -> sparse.sparray:
"""
Utility operator for converting the E field into the H field.
For use with `e_full()` -- assumes that there is no magnetic current M.
@ -274,10 +274,10 @@ def e2h(
op = curl_forward(dxes[0]) / (-1j * omega)
if mu is not None:
op = sparse.diags(1 / mu) @ op
op = sparse.diags_array(1 / mu) @ op
if pmc is not None:
op = sparse.diags(numpy.where(pmc, 0, 1)) @ op
op = sparse.diags_array(numpy.where(pmc, 0, 1)) @ op
return op
@ -285,8 +285,8 @@ def e2h(
def m2j(
omega: complex,
dxes: dx_lists_t,
mu: vfdfield_t | None = None,
) -> sparse.spmatrix:
mu: vfdfield | None = None,
) -> sparse.sparray:
"""
Operator for converting a magnetic current M into an electric current J.
For use with eg. `e_full()`.
@ -302,12 +302,12 @@ def m2j(
op = curl_back(dxes[1]) / (1j * omega)
if mu is not None:
op = op @ sparse.diags(1 / mu)
op = op @ sparse.diags_array(1 / mu)
return op
def poynting_e_cross(e: vcfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix:
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.
@ -330,13 +330,13 @@ def poynting_e_cross(e: vcfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix:
block_diags = [[ None, fx @ -Ez, fx @ Ey],
[ fy @ Ez, None, fy @ -Ex],
[ fz @ -Ey, fz @ Ex, None]]
block_matrix = sparse.bmat([[sparse.diags(x) if x is not None else None for x in row]
for row in block_diags])
P = block_matrix @ sparse.diags(numpy.concatenate(dxbg))
block_matrix = sparse.block_array([[sparse.diags_array(x) if x is not None else None for x in row]
for row in block_diags])
P = block_matrix @ sparse.diags_array(numpy.concatenate(dxbg))
return P
def poynting_h_cross(h: vcfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix:
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.
@ -353,23 +353,23 @@ def poynting_h_cross(h: vcfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix:
dxag = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')]
dxbg = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[1], indexing='ij')]
Hx, Hy, Hz = (sparse.diags(hi * db) for hi, db in zip(numpy.split(h, 3), dxbg, strict=True))
Hx, Hy, Hz = (sparse.diags_array(hi * db) for hi, db in zip(numpy.split(h, 3), dxbg, strict=True))
P = (sparse.bmat(
P = (sparse.block_array(
[[ None, -Hz @ fx, Hy @ fx],
[ Hz @ fy, None, -Hx @ fy],
[-Hy @ fz, Hx @ fz, None]])
@ sparse.diags(numpy.concatenate(dxag)))
@ sparse.diags_array(numpy.concatenate(dxag)))
return P
def e_tfsf_source(
TF_region: vfdfield_t,
TF_region: vfdfield,
omega: complex,
dxes: dx_lists_t,
epsilon: vfdfield_t,
mu: vfdfield_t | None = None,
) -> sparse.spmatrix:
epsilon: vfdfield,
mu: vfdfield | None = None,
) -> sparse.sparray:
"""
Operator that turns a desired E-field distribution into a
total-field/scattered-field (TFSF) source.
@ -390,18 +390,18 @@ def e_tfsf_source(
"""
# TODO documentation
A = e_full(omega, dxes, epsilon, mu)
Q = sparse.diags(TF_region)
Q = sparse.diags_array(TF_region)
return (A @ Q - Q @ A) / (-1j * omega)
def e_boundary_source(
mask: vfdfield_t,
mask: vfdfield,
omega: complex,
dxes: dx_lists_t,
epsilon: vfdfield_t,
mu: vfdfield_t | None = None,
epsilon: vfdfield,
mu: vfdfield | None = None,
periodic_mask_edges: bool = False,
) -> sparse.spmatrix:
) -> sparse.sparray:
"""
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
@ -424,10 +424,10 @@ def e_boundary_source(
shape = [len(dxe) for dxe in dxes[0]]
jmask = numpy.zeros_like(mask, dtype=bool)
def shift_rot(axis: int, polarity: int) -> sparse.spmatrix:
def shift_rot(axis: int, polarity: int) -> sparse.sparray:
return shift_circ(axis=axis, shape=shape, shift_distance=polarity)
def shift_mir(axis: int, polarity: int) -> sparse.spmatrix:
def shift_mir(axis: int, polarity: int) -> sparse.sparray:
return shift_with_mirror(axis=axis, shape=shape, shift_distance=polarity)
shift = shift_rot if periodic_mask_edges else shift_mir
@ -436,7 +436,7 @@ def e_boundary_source(
if shape[axis] == 1:
continue
for polarity in (-1, +1):
r = shift(axis, polarity) - sparse.eye(numpy.prod(shape)) # shifted minus original
r = shift(axis, polarity) - sparse.eye_array(numpy.prod(shape)) # shifted minus original
r3 = sparse.block_diag((r, r, r))
jmask = numpy.logical_or(jmask, numpy.abs(r3 @ mask))
@ -447,5 +447,5 @@ def e_boundary_source(
# (numpy.roll(mask, -1, axis=2) != mask) |
# (numpy.roll(mask, +1, axis=2) != mask))
return sparse.diags(jmask.astype(int)) @ full
return sparse.diags_array(jmask.astype(int)) @ full

View file

@ -11,7 +11,7 @@ from numpy.typing import ArrayLike, NDArray
from numpy.linalg import norm
import scipy.sparse.linalg
from ..fdmath import dx_lists_t, vfdfield_t, vcfdfield_t
from ..fdmath import dx_lists_t, vfdfield, vcfdfield, vcfdfield_t
from . import operators
@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
def _scipy_qmr(
A: scipy.sparse.csr_matrix,
A: scipy.sparse.csr_array,
b: ArrayLike,
**kwargs: Any,
) -> NDArray[numpy.float64]:
@ -66,16 +66,16 @@ def _scipy_qmr(
def generic(
omega: complex,
dxes: dx_lists_t,
J: vcfdfield_t,
epsilon: vfdfield_t,
mu: vfdfield_t | None = None,
J: vcfdfield,
epsilon: vfdfield,
mu: vfdfield | None = None,
*,
pec: vfdfield_t | None = None,
pmc: vfdfield_t | None = None,
pec: vfdfield | None = None,
pmc: vfdfield | None = None,
adjoint: bool = False,
matrix_solver: Callable[..., ArrayLike] = _scipy_qmr,
matrix_solver_opts: dict[str, Any] | None = None,
E_guess: vcfdfield_t | None = None,
E_guess: vcfdfield | None = None,
) -> vcfdfield_t:
"""
Conjugate gradient FDFD solver using CSR sparse matrices.
@ -95,7 +95,7 @@ def generic(
(at H-field locations; non-zero value indicates PMC is present)
adjoint: If true, solves the adjoint problem.
matrix_solver: Called as `matrix_solver(A, b, **matrix_solver_opts) -> x`,
where `A`: `scipy.sparse.csr_matrix`;
where `A`: `scipy.sparse.csr_array`;
`b`: `ArrayLike`;
`x`: `ArrayLike`;
Default is a wrapped version of `scipy.sparse.linalg.qmr()`
@ -138,4 +138,4 @@ def generic(
else:
x0 = Pr @ x
return x0
return vcfdfield_t(x0)

View file

@ -180,12 +180,12 @@ if the result is introduced into a space with a discretized z-axis.
from typing import Any
from collections.abc import Sequence
import numpy
from numpy.typing import NDArray, ArrayLike
from numpy.typing import NDArray
from numpy.linalg import norm
from scipy import sparse
from ..fdmath.operators import deriv_forward, deriv_back, cross
from ..fdmath import vec, unvec, dx_lists_t, vfdfield_t, vcfdfield_t
from ..fdmath import vec, unvec, dx_lists2_t, vcfdfield2_t, vcfdslice_t, vcfdfield2, vfdslice, vcfdslice
from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration
@ -194,10 +194,10 @@ __author__ = 'Jan Petykiewicz'
def operator_e(
omega: complex,
dxes: dx_lists_t,
epsilon: vfdfield_t,
mu: vfdfield_t | None = None,
) -> sparse.spmatrix:
dxes: dx_lists2_t,
epsilon: vfdslice,
mu: vfdslice | None = None,
) -> sparse.sparray:
r"""
Waveguide operator of the form
@ -246,12 +246,12 @@ def operator_e(
Dbx, Dby = deriv_back(dxes[1])
eps_parts = numpy.split(epsilon, 3)
eps_xy = sparse.diags(numpy.hstack((eps_parts[0], eps_parts[1])))
eps_z_inv = sparse.diags(1 / eps_parts[2])
eps_xy = sparse.diags_array(numpy.hstack((eps_parts[0], eps_parts[1])))
eps_z_inv = sparse.diags_array(1 / eps_parts[2])
mu_parts = numpy.split(mu, 3)
mu_yx = sparse.diags(numpy.hstack((mu_parts[1], mu_parts[0])))
mu_z_inv = sparse.diags(1 / mu_parts[2])
mu_yx = sparse.diags_array(numpy.hstack((mu_parts[1], mu_parts[0])))
mu_z_inv = sparse.diags_array(1 / mu_parts[2])
op = (
omega * omega * mu_yx @ eps_xy
@ -263,10 +263,10 @@ def operator_e(
def operator_h(
omega: complex,
dxes: dx_lists_t,
epsilon: vfdfield_t,
mu: vfdfield_t | None = None,
) -> sparse.spmatrix:
dxes: dx_lists2_t,
epsilon: vfdslice,
mu: vfdslice | None = None,
) -> sparse.sparray:
r"""
Waveguide operator of the form
@ -315,12 +315,12 @@ def operator_h(
Dbx, Dby = deriv_back(dxes[1])
eps_parts = numpy.split(epsilon, 3)
eps_yx = sparse.diags(numpy.hstack((eps_parts[1], eps_parts[0])))
eps_z_inv = sparse.diags(1 / eps_parts[2])
eps_yx = sparse.diags_array(numpy.hstack((eps_parts[1], eps_parts[0])))
eps_z_inv = sparse.diags_array(1 / eps_parts[2])
mu_parts = numpy.split(mu, 3)
mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1])))
mu_z_inv = sparse.diags(1 / mu_parts[2])
mu_xy = sparse.diags_array(numpy.hstack((mu_parts[0], mu_parts[1])))
mu_z_inv = sparse.diags_array(1 / mu_parts[2])
op = (
omega * omega * eps_yx @ mu_xy
@ -331,14 +331,14 @@ def operator_h(
def normalized_fields_e(
e_xy: ArrayLike,
e_xy: vcfdfield2,
wavenumber: complex,
omega: complex,
dxes: dx_lists_t,
epsilon: vfdfield_t,
mu: vfdfield_t | None = None,
dxes: dx_lists2_t,
epsilon: vfdslice,
mu: vfdslice | None = None,
prop_phase: float = 0,
) -> tuple[vcfdfield_t, vcfdfield_t]:
) -> 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.
@ -366,14 +366,14 @@ def normalized_fields_e(
def normalized_fields_h(
h_xy: ArrayLike,
h_xy: vcfdfield2,
wavenumber: complex,
omega: complex,
dxes: dx_lists_t,
epsilon: vfdfield_t,
mu: vfdfield_t | None = None,
dxes: dx_lists2_t,
epsilon: vfdslice,
mu: vfdslice | None = None,
prop_phase: float = 0,
) -> tuple[vcfdfield_t, vcfdfield_t]:
) -> tuple[vcfdslice_t, vcfdslice_t]:
"""
Given a vector `h_xy` containing the vectorized H_x and H_y fields,
returns normalized, vectorized E and H fields for the system.
@ -401,21 +401,19 @@ def normalized_fields_h(
def _normalized_fields(
e: vcfdfield_t,
h: vcfdfield_t,
e: vcfdslice,
h: vcfdslice,
omega: complex,
dxes: dx_lists_t,
epsilon: vfdfield_t,
mu: vfdfield_t | None = None,
dxes: dx_lists2_t,
epsilon: vfdslice,
mu: vfdslice | None = None,
prop_phase: float = 0,
) -> tuple[vcfdfield_t, vcfdfield_t]:
) -> tuple[vcfdslice_t, vcfdslice_t]:
# TODO documentation
shape = [s.size for s in dxes[0]]
dxes_real = [[numpy.real(d) for d in numpy.meshgrid(*dxes[v], indexing='ij')] for v in (0, 1)]
# Find time-averaged Sz and normalize to it
# H phase is adjusted by a half-cell forward shift for Yee cell, and 1-cell reverse shift for Poynting
phase = numpy.exp(-1j * -prop_phase / 2)
Sz_tavg = inner_product(e, h, dxes=dxes, prop_phase=prop_phase, conj_h=True).real
assert Sz_tavg > 0, f'Found a mode propagating in the wrong direction! {Sz_tavg=}'
@ -436,16 +434,16 @@ def _normalized_fields(
e *= norm_factor
h *= norm_factor
return e, h
return vcfdslice_t(e), vcfdslice_t(h)
def exy2h(
wavenumber: complex,
omega: complex,
dxes: dx_lists_t,
epsilon: vfdfield_t,
mu: vfdfield_t | None = None
) -> sparse.spmatrix:
dxes: dx_lists2_t,
epsilon: vfdslice,
mu: vfdslice | None = None
) -> sparse.sparray:
"""
Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields,
into a vectorized H containing all three H components
@ -468,10 +466,10 @@ def exy2h(
def hxy2e(
wavenumber: complex,
omega: complex,
dxes: dx_lists_t,
epsilon: vfdfield_t,
mu: vfdfield_t | None = None
) -> sparse.spmatrix:
dxes: dx_lists2_t,
epsilon: vfdslice,
mu: vfdslice | None = None
) -> sparse.sparray:
"""
Operator which transforms the vector `h_xy` containing the vectorized H_x and H_y fields,
into a vectorized E containing all three E components
@ -493,9 +491,9 @@ def hxy2e(
def hxy2h(
wavenumber: complex,
dxes: dx_lists_t,
mu: vfdfield_t | None = None
) -> sparse.spmatrix:
dxes: dx_lists2_t,
mu: vfdslice | None = None
) -> sparse.sparray:
"""
Operator which transforms the vector `h_xy` containing the vectorized H_x and H_y fields,
into a vectorized H containing all three H components
@ -514,22 +512,22 @@ def hxy2h(
if mu is not None:
mu_parts = numpy.split(mu, 3)
mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1])))
mu_z_inv = sparse.diags(1 / mu_parts[2])
mu_xy = sparse.diags_array(numpy.hstack((mu_parts[0], mu_parts[1])))
mu_z_inv = sparse.diags_array(1 / mu_parts[2])
hxy2hz = mu_z_inv @ hxy2hz @ mu_xy
n_pts = dxes[1][0].size * dxes[1][1].size
op = sparse.vstack((sparse.eye(2 * n_pts),
op = sparse.vstack((sparse.eye_array(2 * n_pts),
hxy2hz))
return op
def exy2e(
wavenumber: complex,
dxes: dx_lists_t,
epsilon: vfdfield_t,
) -> sparse.spmatrix:
dxes: dx_lists2_t,
epsilon: vfdslice,
) -> sparse.sparray:
r"""
Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields,
into a vectorized E containing all three E components
@ -575,13 +573,13 @@ def exy2e(
if epsilon is not None:
epsilon_parts = numpy.split(epsilon, 3)
epsilon_xy = sparse.diags(numpy.hstack((epsilon_parts[0], epsilon_parts[1])))
epsilon_z_inv = sparse.diags(1 / epsilon_parts[2])
epsilon_xy = sparse.diags_array(numpy.hstack((epsilon_parts[0], epsilon_parts[1])))
epsilon_z_inv = sparse.diags_array(1 / epsilon_parts[2])
exy2ez = epsilon_z_inv @ exy2ez @ epsilon_xy
n_pts = dxes[0][0].size * dxes[0][1].size
op = sparse.vstack((sparse.eye(2 * n_pts),
op = sparse.vstack((sparse.eye_array(2 * n_pts),
exy2ez))
return op
@ -589,12 +587,12 @@ def exy2e(
def e2h(
wavenumber: complex,
omega: complex,
dxes: dx_lists_t,
mu: vfdfield_t | None = None
) -> sparse.spmatrix:
dxes: dx_lists2_t,
mu: vfdslice | None = None
) -> sparse.sparray:
"""
Returns an operator which, when applied to a vectorized E eigenfield, produces
the vectorized H eigenfield.
the vectorized H eigenfield slice.
Args:
wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`
@ -607,19 +605,19 @@ def e2h(
"""
op = curl_e(wavenumber, dxes) / (-1j * omega)
if mu is not None:
op = sparse.diags(1 / mu) @ op
op = sparse.diags_array(1 / mu) @ op
return op
def h2e(
wavenumber: complex,
omega: complex,
dxes: dx_lists_t,
epsilon: vfdfield_t
) -> sparse.spmatrix:
dxes: dx_lists2_t,
epsilon: vfdslice,
) -> sparse.sparray:
"""
Returns an operator which, when applied to a vectorized H eigenfield, produces
the vectorized E eigenfield.
the vectorized E eigenfield slice.
Args:
wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`
@ -630,13 +628,13 @@ def h2e(
Returns:
Sparse matrix representation of the operator.
"""
op = sparse.diags(1 / (1j * omega * epsilon)) @ curl_h(wavenumber, dxes)
op = sparse.diags_array(1 / (1j * omega * epsilon)) @ curl_h(wavenumber, dxes)
return op
def curl_e(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix:
def curl_e(wavenumber: complex, dxes: dx_lists2_t) -> sparse.sparray:
"""
Discretized curl operator for use with the waveguide E field.
Discretized curl operator for use with the waveguide E field slice.
Args:
wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`
@ -645,18 +643,18 @@ def curl_e(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix:
Returns:
Sparse matrix representation of the operator.
"""
n = 1
for d in dxes[0]:
n *= len(d)
nn = 1
for dd in dxes[0]:
nn *= len(dd)
Bz = -1j * wavenumber * sparse.eye(n)
Bz = -1j * wavenumber * sparse.eye_array(nn)
Dfx, Dfy = deriv_forward(dxes[0])
return cross([Dfx, Dfy, Bz])
def curl_h(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix:
def curl_h(wavenumber: complex, dxes: dx_lists2_t) -> sparse.sparray:
"""
Discretized curl operator for use with the waveguide H field.
Discretized curl operator for use with the waveguide H field slice.
Args:
wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`
@ -665,22 +663,22 @@ def curl_h(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix:
Returns:
Sparse matrix representation of the operator.
"""
n = 1
for d in dxes[1]:
n *= len(d)
nn = 1
for dd in dxes[1]:
nn *= len(dd)
Bz = -1j * wavenumber * sparse.eye(n)
Bz = -1j * wavenumber * sparse.eye_array(nn)
Dbx, Dby = deriv_back(dxes[1])
return cross([Dbx, Dby, Bz])
def h_err(
h: vcfdfield_t,
h: vcfdslice,
wavenumber: complex,
omega: complex,
dxes: dx_lists_t,
epsilon: vfdfield_t,
mu: vfdfield_t | None = None
dxes: dx_lists2_t,
epsilon: vfdslice,
mu: vfdslice | None = None
) -> float:
"""
Calculates the relative error in the H field
@ -699,7 +697,7 @@ def h_err(
ce = curl_e(wavenumber, dxes)
ch = curl_h(wavenumber, dxes)
eps_inv = sparse.diags(1 / epsilon)
eps_inv = sparse.diags_array(1 / epsilon)
if mu is None:
op = ce @ eps_inv @ ch @ h - omega ** 2 * h
@ -710,12 +708,12 @@ def h_err(
def e_err(
e: vcfdfield_t,
e: vcfdslice,
wavenumber: complex,
omega: complex,
dxes: dx_lists_t,
epsilon: vfdfield_t,
mu: vfdfield_t | None = None,
dxes: dx_lists2_t,
epsilon: vfdslice,
mu: vfdslice | None = None,
) -> float:
"""
Calculates the relative error in the E field
@ -737,21 +735,21 @@ def e_err(
if mu is None:
op = ch @ ce @ e - omega ** 2 * (epsilon * e)
else:
mu_inv = sparse.diags(1 / mu)
mu_inv = sparse.diags_array(1 / mu)
op = ch @ mu_inv @ ce @ e - omega ** 2 * (epsilon * e)
return float(norm(op) / norm(e))
def sensitivity(
e_norm: vcfdfield_t,
h_norm: vcfdfield_t,
e_norm: vcfdslice,
h_norm: vcfdslice,
wavenumber: complex,
omega: complex,
dxes: dx_lists_t,
epsilon: vfdfield_t,
mu: vfdfield_t | None = None,
) -> vcfdfield_t:
dxes: dx_lists2_t,
epsilon: vfdslice,
mu: vfdslice | None = None,
) -> vcfdslice_t:
r"""
Given a waveguide structure (`dxes`, `epsilon`, `mu`) and mode fields
(`e_norm`, `h_norm`, `wavenumber`, `omega`), calculates the sensitivity of the wavenumber
@ -825,11 +823,11 @@ def sensitivity(
Dbx, Dby = deriv_back(dxes[1])
eps_x, eps_y, eps_z = numpy.split(epsilon, 3)
eps_xy = sparse.diags(numpy.hstack((eps_x, eps_y)))
eps_z_inv = sparse.diags(1 / eps_z)
eps_xy = sparse.diags_array(numpy.hstack((eps_x, eps_y)))
eps_z_inv = sparse.diags_array(1 / eps_z)
mu_x, mu_y, _mu_z = numpy.split(mu, 3)
mu_yx = sparse.diags(numpy.hstack((mu_y, mu_x)))
mu_yx = sparse.diags_array(numpy.hstack((mu_y, mu_x)))
da_exxhyy = vec(dxes[1][0][:, None] * dxes[0][1][None, :])
da_eyyhxx = vec(dxes[1][1][None, :] * dxes[0][0][:, None])
@ -843,15 +841,15 @@ def sensitivity(
norm = hv_yx_conj @ ev_xy
sens_tot = numpy.concatenate([sens_xy1 + sens_xy2, sens_z]) / (2 * wavenumber * norm)
return sens_tot
return vcfdslice_t(sens_tot)
def solve_modes(
mode_numbers: Sequence[int],
omega: complex,
dxes: dx_lists_t,
epsilon: vfdfield_t,
mu: vfdfield_t | None = None,
dxes: dx_lists2_t,
epsilon: vfdslice,
mu: vfdslice | None = None,
mode_margin: int = 2,
) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]:
"""
@ -907,7 +905,7 @@ def solve_mode(
mode_number: int,
*args: Any,
**kwargs: Any,
) -> tuple[vcfdfield_t, complex]:
) -> tuple[vcfdfield2_t, complex]:
"""
Wrapper around `solve_modes()` that solves for a single mode.
@ -921,13 +919,13 @@ def solve_mode(
"""
kwargs['mode_numbers'] = [mode_number]
e_xys, wavenumbers = solve_modes(*args, **kwargs)
return e_xys[0], wavenumbers[0]
return vcfdfield2_t(e_xys[0]), wavenumbers[0]
def inner_product( # TODO documentation
e1: vcfdfield_t,
h2: vcfdfield_t,
dxes: dx_lists_t,
e1: vcfdfield2,
h2: vcfdfield2,
dxes: dx_lists2_t,
prop_phase: float = 0,
conj_h: bool = False,
trapezoid: bool = False,

View file

@ -4,13 +4,13 @@ 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 Any
from typing import Any, cast
from collections.abc import Sequence
import numpy
from numpy.typing import NDArray
from numpy import complexfloating
from ..fdmath import vec, unvec, dx_lists_t, fdfield_t, cfdfield_t
from ..fdmath import vec, unvec, dx_lists_t, cfdfield_t, fdfield, cfdfield
from . import operators, waveguide_2d
@ -21,8 +21,8 @@ def solve_mode(
axis: int,
polarity: int,
slices: Sequence[slice],
epsilon: fdfield_t,
mu: fdfield_t | None = None,
epsilon: fdfield,
mu: fdfield | None = None,
) -> dict[str, complex | NDArray[complexfloating]]:
"""
Given a 3D grid, selects a slice from the grid and attempts to
@ -95,9 +95,10 @@ def solve_mode(
# 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)
for aa, oo in enumerate(reverse_order):
iii = cast('tuple[slice | int]', (aa, *slices))
E[iii] = e[oo][:, :, None].transpose(reverse_order)
H[iii] = h[oo][:, :, None].transpose(reverse_order)
results = {
'wavenumber': wavenumber,
@ -109,15 +110,15 @@ def solve_mode(
def compute_source(
E: cfdfield_t,
E: cfdfield,
wavenumber: complex,
omega: complex,
dxes: dx_lists_t,
axis: int,
polarity: int,
slices: Sequence[slice],
epsilon: fdfield_t,
mu: fdfield_t | None = None,
epsilon: fdfield,
mu: fdfield | None = None,
) -> cfdfield_t:
"""
Given an eigenmode obtained by `solve_mode`, returns the current source distribution
@ -151,11 +152,11 @@ def compute_source(
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
return cfdfield_t(J)
def compute_overlap_e(
E: cfdfield_t,
E: cfdfield,
wavenumber: complex,
dxes: dx_lists_t,
axis: int,
@ -195,12 +196,12 @@ def compute_overlap_e(
Etgt = numpy.zeros_like(Ee)
Etgt[slices2] = Ee[slices2]
Etgt /= (Etgt.conj() * Etgt).sum()
return Etgt
Etgt /= (Etgt.conj() * Etgt).sum() # type: ignore
return cfdfield_t(Etgt)
def expand_e(
E: cfdfield_t,
E: cfdfield,
wavenumber: complex,
dxes: dx_lists_t,
axis: int,
@ -245,4 +246,4 @@ def expand_e(
slices_in = (slice(None), *slices)
E_expanded[slices_exp] = phase_E * numpy.array(E)[slices_in]
return E_expanded
return cfdfield_t(E_expanded)

View file

@ -68,7 +68,7 @@ import numpy
from numpy.typing import NDArray, ArrayLike
from scipy import sparse
from ..fdmath import vec, unvec, dx_lists_t, vfdfield_t, vcfdfield_t
from ..fdmath import vec, unvec, dx_lists2_t, vcfdslice_t, vcfdfield2_t, vfdslice, vcfdslice, vcfdfield2
from ..fdmath.operators import deriv_forward, deriv_back
from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration
from . import waveguide_2d
@ -78,10 +78,10 @@ logger = logging.getLogger(__name__)
def cylindrical_operator(
omega: float,
dxes: dx_lists_t,
epsilon: vfdfield_t,
dxes: dx_lists2_t,
epsilon: vfdslice,
rmin: float,
) -> sparse.spmatrix:
) -> sparse.sparray:
r"""
Cylindrical coordinate waveguide operator of the form
@ -143,11 +143,11 @@ def cylindrical_operator(
def solve_modes(
mode_numbers: Sequence[int],
omega: float,
dxes: dx_lists_t,
epsilon: vfdfield_t,
dxes: dx_lists2_t,
epsilon: vfdslice,
rmin: float,
mode_margin: int = 2,
) -> tuple[vcfdfield_t, NDArray[numpy.complex128]]:
) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]:
"""
Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode
of the bent waveguide with the specified mode number.
@ -191,7 +191,7 @@ def solve_modes(
# Wavenumbers assume the mode is at rmin, which is unlikely
# Instead, return the wavenumber in inverse radians
angular_wavenumbers = wavenumbers * cast(complex, rmin)
angular_wavenumbers = wavenumbers * cast('complex', rmin)
order = angular_wavenumbers.argsort()[::-1]
e_xys = e_xys[order]
@ -204,7 +204,7 @@ def solve_mode(
mode_number: int,
*args: Any,
**kwargs: Any,
) -> tuple[vcfdfield_t, complex]:
) -> tuple[vcfdslice, complex]:
"""
Wrapper around `solve_modes()` that solves for a single mode.
@ -222,10 +222,10 @@ def solve_mode(
def linear_wavenumbers(
e_xys: vcfdfield_t,
e_xys: list[vcfdfield2_t],
angular_wavenumbers: ArrayLike,
epsilon: vfdfield_t,
dxes: dx_lists_t,
epsilon: vfdslice,
dxes: dx_lists2_t,
rmin: float,
) -> NDArray[numpy.complex128]:
"""
@ -247,7 +247,6 @@ def linear_wavenumbers(
angular_wavenumbers = numpy.asarray(angular_wavenumbers)
mode_radii = numpy.empty_like(angular_wavenumbers, dtype=float)
wavenumbers = numpy.empty_like(angular_wavenumbers)
shape2d = (len(dxes[0][0]), len(dxes[0][1]))
epsilon2d = unvec(epsilon, shape2d)[:2]
grid_radii = rmin + numpy.cumsum(dxes[0][0])
@ -265,11 +264,11 @@ def linear_wavenumbers(
def exy2h(
angular_wavenumber: complex,
omega: float,
dxes: dx_lists_t,
dxes: dx_lists2_t,
rmin: float,
epsilon: vfdfield_t,
mu: vfdfield_t | None = None
) -> sparse.spmatrix:
epsilon: vfdslice,
mu: vfdslice | None = None
) -> sparse.sparray:
"""
Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields,
into a vectorized H containing all three H components
@ -294,10 +293,10 @@ def exy2h(
def exy2e(
angular_wavenumber: complex,
omega: float,
dxes: dx_lists_t,
dxes: dx_lists2_t,
rmin: float,
epsilon: vfdfield_t,
) -> sparse.spmatrix:
epsilon: vfdslice,
) -> sparse.sparray:
"""
Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields,
into a vectorized E containing all three E components
@ -323,7 +322,7 @@ def exy2e(
Ta, Tb = dxes2T(dxes=dxes, rmin=rmin)
Tai = sparse.diags_array(1 / Ta.diagonal())
Tbi = sparse.diags_array(1 / Tb.diagonal())
#Tbi = sparse.diags_array(1 / Tb.diagonal())
epsilon_parts = numpy.split(epsilon, 3)
epsilon_x, epsilon_y = (sparse.diags_array(epsi) for epsi in epsilon_parts[:2])
@ -331,8 +330,6 @@ def exy2e(
n_pts = dxes[0][0].size * dxes[0][1].size
zeros = sparse.coo_array((n_pts, n_pts))
keep_x = sparse.block_array([[sparse.eye_array(n_pts), None], [None, zeros]])
keep_y = sparse.block_array([[zeros, None], [None, sparse.eye_array(n_pts)]])
mu_z = numpy.ones(n_pts)
mu_z_inv = sparse.diags_array(1 / mu_z)
@ -352,10 +349,10 @@ def exy2e(
def e2h(
angular_wavenumber: complex,
omega: float,
dxes: dx_lists_t,
dxes: dx_lists2_t,
rmin: float,
mu: vfdfield_t | None = None
) -> sparse.spmatrix:
mu: vfdslice | None = None
) -> sparse.sparray:
r"""
Returns an operator which, when applied to a vectorized E eigenfield, produces
the vectorized H eigenfield.
@ -396,7 +393,7 @@ def e2h(
def dxes2T(
dxes: dx_lists_t,
dxes: dx_lists2_t,
rmin: float,
) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
r"""
@ -421,15 +418,15 @@ def dxes2T(
def normalized_fields_e(
e_xy: ArrayLike,
e_xy: vcfdfield2,
angular_wavenumber: complex,
omega: float,
dxes: dx_lists_t,
dxes: dx_lists2_t,
rmin: float,
epsilon: vfdfield_t,
mu: vfdfield_t | None = None,
epsilon: vfdslice,
mu: vfdslice | None = None,
prop_phase: float = 0,
) -> tuple[vcfdfield_t, vcfdfield_t]:
) -> 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.
@ -459,23 +456,21 @@ def normalized_fields_e(
def _normalized_fields(
e: vcfdfield_t,
h: vcfdfield_t,
e: vcfdslice,
h: vcfdslice,
omega: complex,
dxes: dx_lists_t,
rmin: float,
epsilon: vfdfield_t,
mu: vfdfield_t | None = None,
dxes: dx_lists2_t,
rmin: float, # Currently unused, but may want to use cylindrical poynting
epsilon: vfdslice,
mu: vfdslice | None = None,
prop_phase: float = 0,
) -> tuple[vcfdfield_t, vcfdfield_t]:
) -> tuple[vcfdslice_t, vcfdslice_t]:
h *= -1
# TODO documentation for normalized_fields
shape = [s.size for s in dxes[0]]
dxes_real = [[numpy.real(d) for d in numpy.meshgrid(*dxes[v], indexing='ij')] for v in (0, 1)]
# Find time-averaged Sz and normalize to it
# H phase is adjusted by a half-cell forward shift for Yee cell, and 1-cell reverse shift for Poynting
phase = numpy.exp(-1j * -prop_phase / 2)
Sz_tavg = waveguide_2d.inner_product(e, h, dxes=dxes, prop_phase=prop_phase, conj_h=True).real # Note, using linear poynting vector
assert Sz_tavg > 0, f'Found a mode propagating in the wrong direction! {Sz_tavg=}'
@ -495,4 +490,4 @@ def _normalized_fields(
e *= norm_factor
h *= norm_factor
return e, h
return vcfdslice_t(e), vcfdslice_t(h)