[fdfd] correct backward mode signs, graded sensitivities, and flux precision
This commit is contained in:
parent
b9aa06355b
commit
b52ee4f622
6 changed files with 89 additions and 8 deletions
|
|
@ -226,6 +226,8 @@ def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield, cfdfield], cfdfi
|
||||||
For time-average power, call it as `f(E, H.conj())` and take `Re(...) / 2`.
|
For time-average power, call it as `f(E, H.conj())` and take `Re(...) / 2`.
|
||||||
"""
|
"""
|
||||||
def exh(e: cfdfield, h: cfdfield) -> cfdfield_t:
|
def exh(e: cfdfield, h: cfdfield) -> cfdfield_t:
|
||||||
|
dtype = numpy.result_type(e, h, *dxes[0], *dxes[1])
|
||||||
|
e, h = numpy.asarray(e, dtype=dtype), numpy.asarray(h, dtype=dtype)
|
||||||
s = numpy.empty_like(e)
|
s = numpy.empty_like(e)
|
||||||
ex = e[0] * dxes[0][0][:, None, None]
|
ex = e[0] * dxes[0][0][:, None, None]
|
||||||
ey = e[1] * dxes[0][1][None, :, None]
|
ey = e[1] * dxes[0][1][None, :, None]
|
||||||
|
|
|
||||||
|
|
@ -836,7 +836,10 @@ def sensitivity(
|
||||||
= H_{yx}^\star \partial_{\epsilon_i}(A_E) E_{xy} + H_{yx}^\star A_E \partial_{\epsilon_i} E_{xy}
|
= H_{yx}^\star \partial_{\epsilon_i}(A_E) E_{xy} + H_{yx}^\star A_E \partial_{\epsilon_i} E_{xy}
|
||||||
$$
|
$$
|
||||||
|
|
||||||
However, $H_{yx}^\star$ is actually a left-eigenvector of $A_E$. This can be verified by inspecting
|
On a graded Yee grid, the left projection includes the quadrature weights:
|
||||||
|
$H_y^\star \Delta x_e \Delta y_h$ and $-H_x^\star \Delta x_h \Delta y_e$.
|
||||||
|
These weights multiply the left vector, before it acts on any derivative operators.
|
||||||
|
With these weights understood, $H_{yx}^\star$ is a left-eigenvector of $A_E$. This can be verified by inspecting
|
||||||
the form of `operator_h` ($A_H$) and comparing its conjugate transpose to `operator_e` ($A_E$). Also, note
|
the form of `operator_h` ($A_H$) and comparing its conjugate transpose to `operator_e` ($A_E$). Also, note
|
||||||
$H_{yx}^\star \cdot E_{xy} = H^\star \times E$ recalls the mode orthogonality relation. See doi:10.5194/ars-9-85-201
|
$H_{yx}^\star \cdot E_{xy} = H^\star \times E$ recalls the mode orthogonality relation. See doi:10.5194/ars-9-85-201
|
||||||
for a similar approach. Therefore,
|
for a similar approach. Therefore,
|
||||||
|
|
@ -883,11 +886,12 @@ def sensitivity(
|
||||||
mu_x, mu_y, _mu_z = numpy.split(mu, 3)
|
mu_x, mu_y, _mu_z = numpy.split(mu, 3)
|
||||||
mu_yx = sparse.diags_array(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_exxhyy = vec(dxes[0][0][:, None] * dxes[1][1][None, :])
|
||||||
da_eyyhxx = vec(dxes[1][1][None, :] * dxes[0][0][:, None])
|
da_eyyhxx = vec(dxes[1][0][:, None] * dxes[0][1][None, :])
|
||||||
ev_xy = numpy.concatenate(numpy.split(e_norm, 3)[:2]) * numpy.concatenate([da_exxhyy, da_eyyhxx])
|
ev_xy = numpy.concatenate(numpy.split(e_norm, 3)[:2])
|
||||||
hx, hy, hz = numpy.split(h_norm, 3)
|
hx, hy, _hz = numpy.split(h_norm, 3)
|
||||||
hv_yx_conj = numpy.conj(numpy.concatenate([hy, -hx]))
|
# The Yee-weighted H projection is the left eigenvector on a graded grid.
|
||||||
|
hv_yx_conj = numpy.conj(numpy.concatenate([hy, -hx])) * numpy.concatenate([da_exxhyy, da_eyyhxx])
|
||||||
|
|
||||||
sens_xy1 = (hv_yx_conj @ (omega * omega * mu_yx)) * ev_xy
|
sens_xy1 = (hv_yx_conj @ (omega * omega * mu_yx)) * ev_xy
|
||||||
sens_xy2 = (hv_yx_conj @ sparse.vstack((Dfx, Dfy)) @ eps_z_inv @ sparse.hstack((Dbx, Dby))) * ev_xy
|
sens_xy2 = (hv_yx_conj @ sparse.vstack((Dfx, Dfy)) @ eps_z_inv @ sparse.hstack((Dbx, Dby))) * ev_xy
|
||||||
|
|
|
||||||
|
|
@ -161,8 +161,9 @@ def mode_from_2d(
|
||||||
e = unvec(ve, shape)
|
e = unvec(ve, shape)
|
||||||
h = unvec(vh, shape)
|
h = unvec(vh, shape)
|
||||||
|
|
||||||
# Adjust for propagation direction
|
# Reversing beta changes Ez and transverse H, while Hz is unchanged.
|
||||||
h *= polarity # type: ignore # mypy issue with numpy
|
e[2] *= polarity
|
||||||
|
h[:2] *= polarity
|
||||||
|
|
||||||
# Apply phase shift to H-field
|
# Apply phase shift to H-field
|
||||||
h[:2] *= numpy.exp(-1j * polarity * 0.5 * wavenumber * dx_prop)
|
h[:2] *= numpy.exp(-1j * polarity * 0.5 * wavenumber * dx_prop)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import numpy
|
import numpy
|
||||||
|
import pytest
|
||||||
|
|
||||||
from ..fdmath import unvec, vec
|
from ..fdmath import unvec, vec
|
||||||
from ..fdfd import functional, operators
|
from ..fdfd import functional, operators
|
||||||
|
|
@ -120,3 +121,17 @@ def test_poynting_e_cross_h_matches_sparse_operator() -> None:
|
||||||
functional_result = functional.poynting_e_cross_h(DXES)(E_FIELD, H_FIELD)
|
functional_result = functional.poynting_e_cross_h(DXES)(E_FIELD, H_FIELD)
|
||||||
|
|
||||||
assert_fields_match(functional_result, matrix_result)
|
assert_fields_match(functional_result, matrix_result)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('wide_input', ['electric', 'magnetic', 'widths'])
|
||||||
|
def test_poynting_uses_promoted_precision_before_products(wide_input) -> None:
|
||||||
|
shape = (2, 2, 2)
|
||||||
|
e = numpy.zeros((3, *shape), dtype=numpy.complex128 if wide_input == 'electric' else numpy.complex64)
|
||||||
|
h = numpy.zeros((3, *shape), dtype=numpy.complex128 if wide_input == 'magnetic' else numpy.complex64)
|
||||||
|
e[1], h[2] = 1e20, 1e20j
|
||||||
|
widths = [[numpy.ones(n, dtype=numpy.float64 if wide_input == 'widths' else numpy.float32) for n in shape] for _ in range(2)]
|
||||||
|
with numpy.errstate(over='raise', invalid='raise'):
|
||||||
|
result = functional.poynting_e_cross_h(widths)(e, h)
|
||||||
|
expected = unvec(operators.poynting_e_cross(vec(e.astype(complex)), widths) @ vec(h.astype(complex)), shape)
|
||||||
|
assert result.dtype == numpy.complex128
|
||||||
|
assert_fields_close(result, expected, atol=0, rtol=1e-14)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.linalg import norm
|
from numpy.linalg import norm
|
||||||
from numpy.testing import assert_allclose
|
from numpy.testing import assert_allclose
|
||||||
|
from scipy import linalg
|
||||||
|
|
||||||
from ..fdmath import vec
|
from ..fdmath import vec
|
||||||
from ..fdfd import waveguide_2d
|
from ..fdfd import waveguide_2d
|
||||||
|
|
@ -120,6 +121,33 @@ def test_waveguide_2d_sensitivity_matches_finite_difference() -> None:
|
||||||
assert 0.4 < ratio < 1.8
|
assert 0.4 < ratio < 1.8
|
||||||
|
|
||||||
|
|
||||||
|
def test_sensitivity_matches_central_differences_on_graded_yee_mesh() -> None:
|
||||||
|
rng = numpy.random.default_rng(4)
|
||||||
|
shape = (4, 3)
|
||||||
|
omega = 0.7
|
||||||
|
epsilon = vec(1 + rng.uniform(0, 3, (3, *shape)))
|
||||||
|
magnetic_widths = [rng.uniform(0.2, 1.2, n) for n in shape]
|
||||||
|
electric_widths = [0.5 * (dx + numpy.roll(dx, -1)) for dx in magnetic_widths]
|
||||||
|
dxes = [electric_widths, magnetic_widths]
|
||||||
|
values, vectors = linalg.eig(waveguide_2d.operator_e(omega, dxes, epsilon).toarray())
|
||||||
|
selected = numpy.argmax(values.real)
|
||||||
|
beta = numpy.sqrt(values[selected])
|
||||||
|
e, h = waveguide_2d.normalized_fields_e(vectors[:, selected], beta, omega, dxes, epsilon)
|
||||||
|
sensitivity = waveguide_2d.sensitivity(e, h, beta, omega, dxes, epsilon)
|
||||||
|
delta = 1e-5
|
||||||
|
finite_difference = numpy.empty(epsilon.size, dtype=complex)
|
||||||
|
for index in range(epsilon.size):
|
||||||
|
roots = []
|
||||||
|
for direction in (-1, 1):
|
||||||
|
perturbed = epsilon.copy()
|
||||||
|
perturbed[index] += direction * delta
|
||||||
|
roots_squared = linalg.eigvals(waveguide_2d.operator_e(omega, dxes, perturbed).toarray())
|
||||||
|
candidates = numpy.sqrt(roots_squared)
|
||||||
|
roots.append(candidates[numpy.argmin(numpy.abs(candidates - beta))])
|
||||||
|
finite_difference[index] = (roots[1] - roots[0]) / (2 * delta)
|
||||||
|
assert_allclose(sensitivity, finite_difference, atol=1e-8, rtol=1e-5)
|
||||||
|
|
||||||
|
|
||||||
def test_waveguide_2d_normalized_fields_h_are_finite_and_unit_normalized_with_mu() -> None:
|
def test_waveguide_2d_normalized_fields_h_are_finite_and_unit_normalized_with_mu() -> None:
|
||||||
epsilon = build_asymmetric_epsilon()
|
epsilon = build_asymmetric_epsilon()
|
||||||
mu = build_mu_profile()
|
mu = build_mu_profile()
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,48 @@ import contextlib
|
||||||
import io
|
import io
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.linalg import norm
|
from numpy.linalg import norm
|
||||||
|
from scipy import linalg
|
||||||
import pytest
|
import pytest
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from ..fdmath import vec, unvec
|
from ..fdmath import vec, unvec
|
||||||
from ..fdmath.operators import deriv_forward, deriv_back
|
from ..fdmath.operators import deriv_forward, deriv_back
|
||||||
|
from ..fdmath.functional import curl_forward, curl_back
|
||||||
from ..fdfd import waveguide_2d, waveguide_3d, waveguide_cyl
|
from ..fdfd import waveguide_2d, waveguide_3d, waveguide_cyl
|
||||||
|
|
||||||
|
|
||||||
OMEGA = 1 / 1500
|
OMEGA = 1 / 1500
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('axis', [0, 1, 2])
|
||||||
|
@pytest.mark.parametrize('polarity', [-1, 1])
|
||||||
|
def test_embedded_hybrid_mode_obeys_both_maxwell_curls(axis, polarity) -> None:
|
||||||
|
omega = 0.7
|
||||||
|
# Cyclic rotation preserves the orientation of the vector cross product.
|
||||||
|
permutation = numpy.roll(numpy.arange(3), axis + 1)
|
||||||
|
canonical = numpy.ones((3, 4, 3, 9))
|
||||||
|
canonical[:, 1:3, :, :] = 4
|
||||||
|
epsilon = canonical[permutation].transpose(0, *(permutation + 1))
|
||||||
|
shape = epsilon.shape[1:]
|
||||||
|
dxes = [[numpy.full(n, 0.4) for n in shape] for _ in range(2)]
|
||||||
|
cross_dxes = [[numpy.full(4, 0.4), numpy.full(3, 0.4)] for _ in range(2)]
|
||||||
|
values, vectors = linalg.eig(waveguide_2d.operator_e(omega, cross_dxes, vec(canonical[:, :, :, 0])).toarray())
|
||||||
|
selected = numpy.argmax(values.real)
|
||||||
|
slices = [slice(None)] * 3
|
||||||
|
slices[axis] = slice(4, 5)
|
||||||
|
mode = waveguide_3d.mode_from_2d(
|
||||||
|
vectors[:, selected], numpy.sqrt(values[selected]), omega=omega, dxes=dxes,
|
||||||
|
axis=axis, polarity=polarity, slices=slices, epsilon=epsilon,
|
||||||
|
)
|
||||||
|
e, h = [waveguide_3d.expand_e(mode[field], mode['wavenumber'], dxes, axis, polarity, slices) for field in ('E', 'H')]
|
||||||
|
assert norm(h[axis]) > 0.01 * norm(h)
|
||||||
|
region = [slice(None)] * 4
|
||||||
|
region[axis + 1] = slice(1, -1)
|
||||||
|
ce, ch = curl_forward(dxes[0])(e), curl_back(dxes[1])(h)
|
||||||
|
assert relative_residual(ce[tuple(region)], (-1j * omega * h)[tuple(region)]) < 1e-11
|
||||||
|
assert relative_residual(ch[tuple(region)], (1j * omega * epsilon * e)[tuple(region)]) < 1e-11
|
||||||
|
|
||||||
|
|
||||||
def build_waveguide_3d_mode(
|
def build_waveguide_3d_mode(
|
||||||
*,
|
*,
|
||||||
slice_start: int,
|
slice_start: int,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue