Fix cylindrical waveguide module

- Properly account for rmin vs r0
- Change return values to match waveguide_2d
- Change operator definition to look more like waveguide_2d

remaining TODO:
- Fix docs
- Further consolidate operators vs waveguide_2d
- Figure out E/H field conversions
This commit is contained in:
Jan Petykiewicz 2025-01-07 00:10:15 -08:00
parent 4f2433320d
commit e54735d9c6

View File

@ -8,10 +8,14 @@ As the z-dependence is known, all the functions in this file assume a 2D grid
""" """
# TODO update module docs # TODO update module docs
from typing import Any
from collections.abc import Sequence
import numpy import numpy
from numpy.typing import NDArray, ArrayLike
from scipy import sparse from scipy import sparse
from ..fdmath import vec, unvec, dx_lists_t, vfdfield_t, cfdfield_t from ..fdmath import vec, unvec, dx_lists_t, vfdfield_t, vcfdfield_t
from ..fdmath.operators import deriv_forward, deriv_back from ..fdmath.operators import deriv_forward, deriv_back
from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration
@ -21,6 +25,7 @@ def cylindrical_operator(
dxes: dx_lists_t, dxes: dx_lists_t,
epsilon: vfdfield_t, epsilon: vfdfield_t,
r0: float, r0: float,
rmin: float,
) -> sparse.spmatrix: ) -> sparse.spmatrix:
""" """
Cylindrical coordinate waveguide operator of the form Cylindrical coordinate waveguide operator of the form
@ -42,8 +47,8 @@ def cylindrical_operator(
omega: The angular frequency of the system omega: The angular frequency of the system
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D) dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
epsilon: Vectorized dielectric constant grid epsilon: Vectorized dielectric constant grid
r0: Radius of curvature for the simulation. This should be the minimum value of r0: Radius of curvature at x=0
r within the simulation domain. rmin: Radius at the left edge of the simulation domain
Returns: Returns:
Sparse matrix representation of the operator Sparse matrix representation of the operator
@ -52,44 +57,52 @@ def cylindrical_operator(
Dfx, Dfy = deriv_forward(dxes[0]) Dfx, Dfy = deriv_forward(dxes[0])
Dbx, Dby = deriv_back(dxes[1]) Dbx, Dby = deriv_back(dxes[1])
rx = r0 + numpy.cumsum(dxes[0][0]) ra = rmin + dxes[0][0] / 2.0 + numpy.cumsum(dxes[1][0]) # Radius at Ex points
ry = r0 + dxes[0][0] / 2.0 + numpy.cumsum(dxes[1][0]) rb = rmin + numpy.cumsum(dxes[0][0]) # Radius at Ey points
tx = rx / r0 ta = ra / r0
ty = ry / r0 tb = rb / r0
Tx = sparse.diags(vec(tx[:, None].repeat(dxes[0][1].size, axis=1))) Ta = sparse.diags(vec(ta[:, None].repeat(dxes[0][1].size, axis=1)))
Ty = sparse.diags(vec(ty[:, None].repeat(dxes[1][1].size, axis=1))) Tb = sparse.diags(vec(tb[:, None].repeat(dxes[1][1].size, axis=1)))
eps_parts = numpy.split(epsilon, 3) eps_parts = numpy.split(epsilon, 3)
eps_x = sparse.diags(eps_parts[0]) eps_x = sparse.diags(eps_parts[0])
eps_y = sparse.diags(eps_parts[1]) eps_y = sparse.diags(eps_parts[1])
eps_z_inv = sparse.diags(1 / eps_parts[2]) eps_z_inv = sparse.diags(1 / eps_parts[2])
pa = sparse.vstack((Dfx, Dfy)) @ Tx @ eps_z_inv @ sparse.hstack((Dbx, Dby)) omega2 = omega * omega
pb = sparse.vstack((Dfx, Dfy)) @ Tx @ eps_z_inv @ sparse.hstack((Dby, Dbx))
a0 = Ty @ eps_x + omega**-2 * Dby @ Ty @ Dfy
a1 = Tx @ eps_y + omega**-2 * Dbx @ Ty @ Dfx
b0 = Dbx @ Ty @ Dfy
b1 = Dby @ Ty @ Dfx
diag = sparse.block_diag diag = sparse.block_diag
omega2 = omega * omega sq0 = omega2 * diag((Tb @ Tb @ eps_x,
Ta @ Ta @ eps_y))
lin0 = sparse.vstack((-Tb @ Dby, Ta @ Dbx)) @ Tb @ sparse.hstack((-Dfy, Dfx))
lin1 = sparse.vstack((Dfx, Dfy)) @ Ta @ eps_z_inv @ sparse.hstack((Dbx @ Tb @ eps_x,
Dby @ Ta @ eps_y))
# op = (
# # E
# omega * omega * mu_yx @ eps_xy
# + mu_yx @ sparse.vstack((-Dby, Dbx)) @ mu_z_inv @ sparse.hstack((-Dfy, Dfx))
# + sparse.vstack((Dfx, Dfy)) @ eps_z_inv @ sparse.hstack((Dbx, Dby)) @ eps_xy
op = ( # # H
(omega2 * diag((Tx, Ty)) + pa) @ diag((a0, a1)) # omega * omega * eps_yx @ mu_xy
- (sparse.bmat(((None, Ty), (Tx, None))) + pb / omega2) @ diag((b0, b1)) # + eps_yx @ sparse.vstack((-Dfy, Dfx)) @ eps_z_inv @ sparse.hstack((-Dby, Dbx))
) # + sparse.vstack((Dbx, Dby)) @ mu_z_inv @ sparse.hstack((Dfx, Dfy)) @ mu_xy
# )
op = sq0 + lin0 + lin1
return op return op
def solve_mode( def solve_modes(
mode_number: int, mode_numbers: Sequence[int],
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists_t,
epsilon: vfdfield_t, epsilon: vfdfield_t,
r0: float, r0: float,
) -> dict[str, complex | cfdfield_t]: rmin: float,
mode_margin: int = 2,
) -> tuple[vcfdfield_t, NDArray[numpy.complex64]]:
""" """
TODO: fixup TODO: fixup
Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode
@ -105,44 +118,50 @@ def solve_mode(
r within the simulation domain. r within the simulation domain.
Returns: Returns:
``` e_xys: NDArray of vfdfield_t specifying fields. First dimension is mode number.
{ wavenumbers: list of wavenumbers
'E': list[NDArray[numpy.complex_]],
'H': list[NDArray[numpy.complex_]],
'wavenumber': complex,
}
```
""" """
''' #
Solve for the largest-magnitude eigenvalue of the real operator # Solve for the largest-magnitude eigenvalue of the real operator
''' #
dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes] dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes]
A_r = cylindrical_operator(numpy.real(omega), dxes_real, numpy.real(epsilon), r0) A_r = cylindrical_operator(numpy.real(omega), dxes_real, numpy.real(epsilon), r0=r0, rmin=rmin)
eigvals, eigvecs = signed_eigensolve(A_r, mode_number + 3) eigvals, eigvecs = signed_eigensolve(A_r, max(mode_numbers) + mode_margin)
e_xy = eigvecs[:, -(mode_number + 1)] e_xys = eigvecs[:, -(numpy.array(mode_numbers) + 1)].T
''' #
Now solve for the eigenvector of the full operator, using the real operator's # Now solve for the eigenvector of the full operator, using the real operator's
eigenvector as an initial guess for Rayleigh quotient iteration. # eigenvector as an initial guess for Rayleigh quotient iteration.
''' #
A = cylindrical_operator(omega, dxes, epsilon, r0) A = cylindrical_operator(omega, dxes, epsilon, r0=r0, rmin=rmin)
eigval, e_xy = rayleigh_quotient_iteration(A, e_xy) for nn in range(len(mode_numbers)):
eigvals[nn], e_xys[nn, :] = rayleigh_quotient_iteration(A, e_xys[nn, :])
# Calculate the wave-vector (force the real part to be positive) # Calculate the wave-vector (force the real part to be positive)
wavenumber = numpy.sqrt(eigval) wavenumbers = numpy.sqrt(eigvals)
wavenumber *= numpy.sign(numpy.real(wavenumber)) wavenumbers *= numpy.sign(numpy.real(wavenumbers))
# TODO: Perform correction on wavenumber to account for numerical dispersion. return e_xys, wavenumbers
shape = [d.size for d in dxes[0]]
e_xy = numpy.hstack((e_xy, numpy.zeros(shape[0] * shape[1])))
fields = {
'wavenumber': wavenumber,
'E': unvec(e_xy, shape),
# 'E': unvec(e, shape),
# 'H': unvec(h, shape),
}
return fields def solve_mode(
mode_number: int,
*args: Any,
**kwargs: Any,
) -> tuple[vcfdfield_t, complex]:
"""
Wrapper around `solve_modes()` that solves for a single mode.
Args:
mode_number: 0-indexed mode number to solve for
*args: passed to `solve_modes()`
**kwargs: passed to `solve_modes()`
Returns:
(e_xy, wavenumber)
"""
kwargs['mode_numbers'] = [mode_number]
e_xys, wavenumbers = solve_modes(*args, **kwargs)
return e_xys[0], wavenumbers[0]