Compare commits

...

12 Commits

4 changed files with 164 additions and 71 deletions

View File

@ -46,20 +46,24 @@ def test0(solver=generic_solver):
# #### Create the grid, mask, and draw the device #### # #### Create the grid, mask, and draw the device ####
grid = gridlock.Grid(edge_coords) grid = gridlock.Grid(edge_coords)
epsilon = grid.allocate(n_air**2, dtype=numpy.float32) epsilon = grid.allocate(n_air**2, dtype=numpy.float32)
grid.draw_cylinder(epsilon, grid.draw_cylinder(
surface_normal=2, epsilon,
center=center, surface_normal=2,
radius=max(radii), center=center,
thickness=th, radius=max(radii),
eps=n_ring**2, thickness=th,
num_points=24) foreground=n_ring**2,
grid.draw_cylinder(epsilon, num_points=24,
surface_normal=2, )
center=center, grid.draw_cylinder(
radius=min(radii), epsilon,
thickness=th*1.1, surface_normal=2,
eps=n_air ** 2, center=center,
num_points=24) radius=min(radii),
thickness=th*1.1,
foreground=n_air ** 2,
num_points=24,
)
dxes = [grid.dxyz, grid.autoshifted_dxyz()] dxes = [grid.dxyz, grid.autoshifted_dxyz()]
for a in (0, 1, 2): for a in (0, 1, 2):
@ -71,9 +75,9 @@ def test0(solver=generic_solver):
J[1][15, grid.shape[1]//2, grid.shape[2]//2] = 1 J[1][15, grid.shape[1]//2, grid.shape[2]//2] = 1
''' #
Solve! # Solve!
''' #
sim_args = { sim_args = {
'omega': omega, 'omega': omega,
'dxes': dxes, 'dxes': dxes,
@ -87,9 +91,9 @@ def test0(solver=generic_solver):
E = unvec(x, grid.shape) E = unvec(x, grid.shape)
''' #
Plot results # Plot results
''' #
pyplot.figure() pyplot.figure()
pyplot.pcolor(numpy.real(E[1][:, :, grid.shape[2]//2]), cmap='seismic') pyplot.pcolor(numpy.real(E[1][:, :, grid.shape[2]//2]), cmap='seismic')
pyplot.axis('equal') pyplot.axis('equal')
@ -122,7 +126,7 @@ def test1(solver=generic_solver):
# #### Create the grid and draw the device #### # #### Create the grid and draw the device ####
grid = gridlock.Grid(edge_coords) grid = gridlock.Grid(edge_coords)
epsilon = grid.allocate(n_air**2, dtype=numpy.float32) epsilon = grid.allocate(n_air**2, dtype=numpy.float32)
grid.draw_cuboid(epsilon, center=center, dimensions=[8e3, w, th], eps=n_wg**2) grid.draw_cuboid(epsilon, center=center, dimensions=[8e3, w, th], foreground=n_wg**2)
dxes = [grid.dxyz, grid.autoshifted_dxyz()] dxes = [grid.dxyz, grid.autoshifted_dxyz()]
for a in (0, 1, 2): for a in (0, 1, 2):
@ -169,9 +173,9 @@ def test1(solver=generic_solver):
# pcolor((numpy.abs(J3).sum(axis=2).sum(axis=0) > 0).astype(float).T) # pcolor((numpy.abs(J3).sum(axis=2).sum(axis=0) > 0).astype(float).T)
pyplot.show(block=True) pyplot.show(block=True)
''' #
Solve! # Solve!
''' #
sim_args = { sim_args = {
'omega': omega, 'omega': omega,
'dxes': dxes, 'dxes': dxes,
@ -188,9 +192,9 @@ def test1(solver=generic_solver):
E = unvec(x, grid.shape) E = unvec(x, grid.shape)
''' #
Plot results # Plot results
''' #
center = grid.pos2ind([0, 0, 0], None).astype(int) center = grid.pos2ind([0, 0, 0], None).astype(int)
pyplot.figure() pyplot.figure()
pyplot.subplot(2, 2, 1) pyplot.subplot(2, 2, 1)
@ -232,7 +236,7 @@ def test1(solver=generic_solver):
pyplot.grid(alpha=0.6) pyplot.grid(alpha=0.6)
pyplot.title('Overlap with mode') pyplot.title('Overlap with mode')
pyplot.show() pyplot.show()
print('Average overlap with mode:', sum(q)/len(q)) print('Average overlap with mode:', sum(q[8:32])/len(q[8:32]))
def module_available(name): def module_available(name):

View File

@ -414,18 +414,13 @@ def _normalized_fields(
shape = [s.size for s in dxes[0]] 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)] dxes_real = [[numpy.real(d) for d in numpy.meshgrid(*dxes[v], indexing='ij')] for v in (0, 1)]
E = unvec(e, shape)
H = unvec(h, shape)
# Find time-averaged Sz and normalize to it # 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 # 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) phase = numpy.exp(-1j * -prop_phase / 2)
Sz_a = E[0] * numpy.conj(H[1] * phase) * dxes_real[0][1] * dxes_real[1][0] Sz_tavg = inner_product(e, h, dxes=dxes, prop_phase=prop_phase, conj_h=True).real
Sz_b = E[1] * numpy.conj(H[0] * phase) * dxes_real[0][0] * dxes_real[1][1]
Sz_tavg = numpy.real(Sz_a.sum() - Sz_b.sum()) * 0.5 # 0.5 since E, H are assumed to be peak (not RMS) amplitudes
assert Sz_tavg > 0, f'Found a mode propagating in the wrong direction! {Sz_tavg=}' assert Sz_tavg > 0, f'Found a mode propagating in the wrong direction! {Sz_tavg=}'
energy = epsilon * e.conj() * e energy = numpy.real(epsilon * e.conj() * e)
norm_amplitude = 1 / numpy.sqrt(Sz_tavg) norm_amplitude = 1 / numpy.sqrt(Sz_tavg)
norm_angle = -numpy.angle(e[energy.argmax()]) # Will randomly add a negative sign when mode is symmetric norm_angle = -numpy.angle(e[energy.argmax()]) # Will randomly add a negative sign when mode is symmetric
@ -435,6 +430,7 @@ def _normalized_fields(
sign = numpy.sign(E_weighted[:, sign = numpy.sign(E_weighted[:,
:max(shape[0] // 2, 1), :max(shape[0] // 2, 1),
:max(shape[1] // 2, 1)].real.sum()) :max(shape[1] // 2, 1)].real.sum())
assert sign != 0
norm_factor = sign * norm_amplitude * numpy.exp(1j * norm_angle) norm_factor = sign * norm_amplitude * numpy.exp(1j * norm_angle)
@ -825,7 +821,7 @@ def sensitivity(
def solve_modes( def solve_modes(
mode_numbers: list[int], mode_numbers: Sequence[int],
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists_t,
epsilon: vfdfield_t, epsilon: vfdfield_t,
@ -858,7 +854,9 @@ def solve_modes(
A_r = operator_e(numpy.real(omega), dxes_real, numpy.real(epsilon), mu_real) A_r = operator_e(numpy.real(omega), dxes_real, numpy.real(epsilon), mu_real)
eigvals, eigvecs = signed_eigensolve(A_r, max(mode_numbers) + mode_margin) eigvals, eigvecs = signed_eigensolve(A_r, max(mode_numbers) + mode_margin)
e_xys = eigvecs[:, -(numpy.array(mode_numbers) + 1)] keep_inds = -(numpy.array(mode_numbers) + 1)
e_xys = eigvecs[:, keep_inds].T
eigvals = eigvals[keep_inds]
# #
# 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
@ -866,13 +864,17 @@ def solve_modes(
# #
A = operator_e(omega, dxes, epsilon, mu) A = operator_e(omega, dxes, epsilon, mu)
for nn in range(len(mode_numbers)): for nn in range(len(mode_numbers)):
eigvals[nn], e_xys[:, nn] = rayleigh_quotient_iteration(A, e_xys[:, nn]) 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)
wavenumbers = numpy.sqrt(eigvals) wavenumbers = numpy.sqrt(eigvals)
wavenumbers *= numpy.sign(numpy.real(wavenumbers)) wavenumbers *= numpy.sign(numpy.real(wavenumbers))
return e_xys.T, wavenumbers order = wavenumbers.argsort()[::-1]
e_xys = e_xys[order]
wavenumbers = wavenumbers[order]
return e_xys, wavenumbers
def solve_mode( def solve_mode(
@ -894,3 +896,37 @@ def solve_mode(
kwargs['mode_numbers'] = [mode_number] kwargs['mode_numbers'] = [mode_number]
e_xys, wavenumbers = solve_modes(*args, **kwargs) e_xys, wavenumbers = solve_modes(*args, **kwargs)
return e_xys[0], wavenumbers[0] return e_xys[0], wavenumbers[0]
def inner_product( # TODO documentation
e1: vcfdfield_t,
h2: vcfdfield_t,
dxes: dx_lists_t,
prop_phase: float = 0,
conj_h: bool = False,
trapezoid: bool = False,
) -> tuple[vcfdfield_t, vcfdfield_t]:
shape = [s.size for s in dxes[0]]
# 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)
E1 = unvec(e1, shape)
H2 = unvec(h2, shape) * phase
if conj_h:
H2 = numpy.conj(H2)
# Find time-averaged Sz and normalize to it
dxes_real = [[numpy.real(dxyz) for dxyz in dxeh] for dxeh in dxes]
if integrate:
Sz_a = numpy.trapezoid(numpy.trapezoid(E1[0] * H2[1], numpy.cumsum(dxes_real[0][1])), numpy.cumsum(dxes_real[1][0]))
Sz_b = numpy.trapezoid(numpy.trapezoid(E1[1] * H2[0], numpy.cumsum(dxes_real[0][0])), numpy.cumsum(dxes_real[1][1]))
else:
Sz_a = E1[0] * H2[1] * dxes_real[1][0][:, None] * dxes_real[0][1][None, :]
Sz_b = E1[1] * H2[0] * dxes_real[0][0][:, None] * dxes_real[1][1][None, :]
Sz = 0.5 * (Sz_a.sum() - Sz_b.sum())
return Sz

View File

@ -10,6 +10,7 @@ As the z-dependence is known, all the functions in this file assume a 2D grid
from typing import Any from typing import Any
from collections.abc import Sequence from collections.abc import Sequence
import logging
import numpy import numpy
from numpy.typing import NDArray, ArrayLike from numpy.typing import NDArray, ArrayLike
@ -20,11 +21,13 @@ from ..fdmath.operators import deriv_forward, deriv_back
from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration
logger = logging.getLogger(__name__)
def cylindrical_operator( def cylindrical_operator(
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists_t,
epsilon: vfdfield_t, epsilon: vfdfield_t,
r0: float,
rmin: float, rmin: float,
) -> sparse.spmatrix: ) -> sparse.spmatrix:
""" """
@ -47,8 +50,7 @@ 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 at x=0 rmin: Radius at the left edge of the simulation domain (minimum 'x')
rmin: Radius at the left edge of the simulation domain
Returns: Returns:
Sparse matrix representation of the operator Sparse matrix representation of the operator
@ -57,13 +59,7 @@ 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])
ra = rmin + dxes[0][0] / 2.0 + numpy.cumsum(dxes[1][0]) # Radius at Ex points Ta, Tb = dxes2T(dxes=dxes, rmin=rmin)
rb = rmin + numpy.cumsum(dxes[0][0]) # Radius at Ey points
ta = ra / r0
tb = rb / r0
Ta = sparse.diags(vec(ta[:, None].repeat(dxes[0][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])
@ -99,10 +95,9 @@ def solve_modes(
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists_t,
epsilon: vfdfield_t, epsilon: vfdfield_t,
r0: float,
rmin: float, rmin: float,
mode_margin: int = 2, mode_margin: int = 2,
) -> tuple[vcfdfield_t, NDArray[numpy.complex64]]: ) -> tuple[vcfdfield_t, NDArray[numpy.complex128]]:
""" """
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
@ -114,12 +109,12 @@ def solve_modes(
dxes: Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types. dxes: Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types.
The first coordinate is assumed to be r, the second is y. The first coordinate is assumed to be r, the second is y.
epsilon: Dielectric constant epsilon: Dielectric constant
r0: Radius of curvature for the simulation. This should be the minimum value of rmin: Radius of curvature for the simulation. This should be the minimum value of
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. e_xys: NDArray of vfdfield_t specifying fields. First dimension is mode number.
wavenumbers: list of wavenumbers angular_wavenumbers: list of wavenumbers in 1/rad units.
""" """
# #
@ -127,15 +122,17 @@ def solve_modes(
# #
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=r0, rmin=rmin) A_r = cylindrical_operator(numpy.real(omega), dxes_real, numpy.real(epsilon), rmin=rmin)
eigvals, eigvecs = signed_eigensolve(A_r, max(mode_numbers) + mode_margin) eigvals, eigvecs = signed_eigensolve(A_r, max(mode_numbers) + mode_margin)
e_xys = eigvecs[:, -(numpy.array(mode_numbers) + 1)].T keep_inds = -(numpy.array(mode_numbers) + 1)
e_xys = eigvecs[:, keep_inds].T
eigvals = eigvals[keep_inds]
# #
# 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=r0, rmin=rmin) A = cylindrical_operator(omega, dxes, epsilon, rmin=rmin)
for nn in range(len(mode_numbers)): for nn in range(len(mode_numbers)):
eigvals[nn], e_xys[nn, :] = rayleigh_quotient_iteration(A, e_xys[nn, :]) eigvals[nn], e_xys[nn, :] = rayleigh_quotient_iteration(A, e_xys[nn, :])
@ -143,7 +140,15 @@ def solve_modes(
wavenumbers = numpy.sqrt(eigvals) wavenumbers = numpy.sqrt(eigvals)
wavenumbers *= numpy.sign(numpy.real(wavenumbers)) wavenumbers *= numpy.sign(numpy.real(wavenumbers))
return e_xys, wavenumbers # Wavenumbers assume the mode is at rmin, which is unlikely
# Instead, return the wavenumber in inverse radians
angular_wavenumbers = wavenumbers * rmin
order = angular_wavenumbers.argsort()[::-1]
e_xys = e_xys[order]
angular_wavenumbers = angular_wavenumbers[order]
return e_xys, angular_wavenumbers
def solve_mode( def solve_mode(
@ -160,8 +165,49 @@ def solve_mode(
**kwargs: passed to `solve_modes()` **kwargs: passed to `solve_modes()`
Returns: Returns:
(e_xy, wavenumber) (e_xy, angular_wavenumber)
""" """
kwargs['mode_numbers'] = [mode_number] kwargs['mode_numbers'] = [mode_number]
e_xys, wavenumbers = solve_modes(*args, **kwargs) e_xys, wavenumbers = solve_modes(*args, **kwargs)
return e_xys[0], wavenumbers[0] return e_xys[0], angular_wavenumbers[0]
def linear_wavenumbers(
e_xys: vcfdfield_t,
angular_wavenumbers: ArrayLike,
epsilon: vfdfield_t,
dxes: dx_lists_t,
rmin: float,
) -> NDArray[numpy.complex128]:
"""
Calculate linear wavenumbers (1/distance) based on angular wavenumbers (1/rad)
and the mode's energy distribution.
Args:
e_xys: Vectorized mode fields with shape [num_modes, 2 * x *y)
angular_wavenumbers: Angular wavenumbers corresponding to the fields in `e_xys`
epsilon: Vectorized dielectric constant grid with shape (3, x, y)
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
rmin: Radius at the left edge of the simulation domain (minimum 'x')
Returns:
NDArray containing the calculated linear (1/distance) 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])
for ii in range(angular_wavenumbers.size):
efield = unvec(e_xys[ii], shape2d, 2)
energy = numpy.real((efield * efield.conj()) * epsilon2d)
energy_vs_x = energy.sum(axis=(0, 2))
mode_radii[ii] = (grid_radii * energy_vs_x).sum() / energy_vs_x.sum()
logger.info(f'{mode_radii=}')
lin_wavenumbers = angular_wavenumbers / mode_radii
return lin_wavenumbers

View File

@ -28,14 +28,16 @@ def vec(f: cfdfield_t) -> vcfdfield_t:
def vec(f: ArrayLike) -> vfdfield_t | vcfdfield_t: def vec(f: ArrayLike) -> vfdfield_t | vcfdfield_t:
pass pass
def vec(f: fdfield_t | cfdfield_t | ArrayLike | None) -> vfdfield_t | vcfdfield_t | None: def vec(
f: fdfield_t | cfdfield_t | ArrayLike | None,
) -> vfdfield_t | vcfdfield_t | None:
""" """
Create a 1D ndarray from a 3D vector field which spans a 1-3D region. Create a 1D ndarray from a vector field which spans a 1-3D region.
Returns `None` if called with `f=None`. Returns `None` if called with `f=None`.
Args: Args:
f: A vector field, `[f_x, f_y, f_z]` where each `f_` component is a 1- to f: A vector field, e.g. `[f_x, f_y, f_z]` where each `f_` component is a 1- to
3-D ndarray (`f_*` should all be the same size). Doesn't fail with `f=None`. 3-D ndarray (`f_*` should all be the same size). Doesn't fail with `f=None`.
Returns: Returns:
@ -47,33 +49,38 @@ def vec(f: fdfield_t | cfdfield_t | ArrayLike | None) -> vfdfield_t | vcfdfield_
@overload @overload
def unvec(v: None, shape: Sequence[int]) -> None: def unvec(v: None, shape: Sequence[int], nvdim: int) -> None:
pass pass
@overload @overload
def unvec(v: vfdfield_t, shape: Sequence[int]) -> fdfield_t: def unvec(v: vfdfield_t, shape: Sequence[int], nvdim: int) -> fdfield_t:
pass pass
@overload @overload
def unvec(v: vcfdfield_t, shape: Sequence[int]) -> cfdfield_t: def unvec(v: vcfdfield_t, shape: Sequence[int], nvdim: int) -> cfdfield_t:
pass pass
def unvec(v: vfdfield_t | vcfdfield_t | None, shape: Sequence[int]) -> fdfield_t | cfdfield_t | None: def unvec(
v: vfdfield_t | vcfdfield_t | None,
shape: Sequence[int],
nvdim: int = 3,
) -> fdfield_t | cfdfield_t | None:
""" """
Perform the inverse of vec(): take a 1D ndarray and output a 3D field Perform the inverse of vec(): take a 1D ndarray and output an `nvdim`-component field
of form `[f_x, f_y, f_z]` where each of `f_*` is a len(shape)-dimensional of form e.g. `[f_x, f_y, f_z]` (`nvdim=3`) where each of `f_*` is a len(shape)-dimensional
ndarray. ndarray.
Returns `None` if called with `v=None`. Returns `None` if called with `v=None`.
Args: Args:
v: 1D ndarray representing a 3D vector field of shape shape (or None) v: 1D ndarray representing a vector field of shape shape (or None)
shape: shape of the vector field shape: shape of the vector field
nvdim: Number of components in each vector
Returns: Returns:
`[f_x, f_y, f_z]` where each `f_` is a `len(shape)` dimensional ndarray (or `None`) `[f_x, f_y, f_z]` where each `f_` is a `len(shape)` dimensional ndarray (or `None`)
""" """
if v is None: if v is None:
return None return None
return v.reshape((3, *shape), order='C') return v.reshape((nvdim, *shape), order='C')