354 lines
12 KiB
Python
354 lines
12 KiB
Python
import numpy
|
|
import pytest
|
|
from numpy.linalg import norm
|
|
from numpy.testing import assert_allclose
|
|
from scipy import linalg
|
|
|
|
from ..fdmath import vec
|
|
from ..fdfd import waveguide_2d, scpml
|
|
|
|
|
|
OMEGA = 1 / 1500
|
|
GRID_SHAPE = (5, 5)
|
|
DXES_2D = [[numpy.ones(GRID_SHAPE[0]), numpy.ones(GRID_SHAPE[1])] for _ in range(2)]
|
|
DXES_2D_NONUNIFORM = [[
|
|
numpy.array([1.0, 1.2, 0.9, 1.1, 1.3]),
|
|
numpy.array([0.8, 1.1, 1.0, 1.2, 0.9]),
|
|
] for _ in range(2)]
|
|
|
|
|
|
def build_asymmetric_epsilon() -> numpy.ndarray:
|
|
epsilon = numpy.ones((3, *GRID_SHAPE), dtype=float)
|
|
epsilon[:, 2, 1] = 2.0
|
|
return vec(epsilon)
|
|
|
|
|
|
def build_mu_profile() -> numpy.ndarray:
|
|
return numpy.linspace(1.5, 2.2, 3 * GRID_SHAPE[0] * GRID_SHAPE[1])
|
|
|
|
|
|
def test_waveguide_2d_solved_modes_are_ordered_and_low_residual() -> None:
|
|
epsilon = build_asymmetric_epsilon()
|
|
operator_e = waveguide_2d.operator_e(OMEGA, DXES_2D, epsilon)
|
|
|
|
e_xys, wavenumbers = waveguide_2d.solve_modes(
|
|
[0, 1],
|
|
omega=OMEGA,
|
|
dxes=DXES_2D,
|
|
epsilon=epsilon,
|
|
)
|
|
|
|
assert numpy.all(numpy.diff(numpy.real(wavenumbers)) <= 0)
|
|
|
|
for e_xy, wavenumber in zip(e_xys, wavenumbers, strict=True):
|
|
residual = norm(operator_e @ e_xy - (wavenumber ** 2) * e_xy) / norm(e_xy)
|
|
assert residual < 1e-6
|
|
|
|
|
|
def test_waveguide_2d_normalized_fields_are_consistent() -> None:
|
|
epsilon = build_asymmetric_epsilon()
|
|
operator_h = waveguide_2d.operator_h(OMEGA, DXES_2D, epsilon)
|
|
|
|
e_xy, wavenumber = waveguide_2d.solve_mode(
|
|
0,
|
|
omega=OMEGA,
|
|
dxes=DXES_2D,
|
|
epsilon=epsilon,
|
|
)
|
|
e_field, h_field = waveguide_2d.normalized_fields_e(
|
|
e_xy,
|
|
wavenumber=wavenumber,
|
|
omega=OMEGA,
|
|
dxes=DXES_2D,
|
|
epsilon=epsilon,
|
|
)
|
|
h_xy = numpy.concatenate(numpy.split(h_field, 3)[:2])
|
|
|
|
overlap = waveguide_2d.inner_product(e_field, h_field, DXES_2D, conj_h=True)
|
|
h_residual = norm(operator_h @ h_xy - (wavenumber ** 2) * h_xy) / norm(h_xy)
|
|
|
|
assert abs(overlap.real - 1.0) < 1e-10
|
|
assert abs(overlap.imag) < 1e-10
|
|
assert waveguide_2d.e_err(e_field, wavenumber, OMEGA, DXES_2D, epsilon) < 1e-6
|
|
assert waveguide_2d.h_err(h_field, wavenumber, OMEGA, DXES_2D, epsilon) < 1e-6
|
|
assert h_residual < 1e-6
|
|
|
|
|
|
def test_waveguide_2d_sensitivity_matches_finite_difference() -> None:
|
|
epsilon = build_asymmetric_epsilon()
|
|
e_xy, wavenumber = waveguide_2d.solve_mode(
|
|
0,
|
|
omega=OMEGA,
|
|
dxes=DXES_2D,
|
|
epsilon=epsilon,
|
|
)
|
|
e_field, h_field = waveguide_2d.normalized_fields_e(
|
|
e_xy,
|
|
wavenumber=wavenumber,
|
|
omega=OMEGA,
|
|
dxes=DXES_2D,
|
|
epsilon=epsilon,
|
|
)
|
|
sensitivity = waveguide_2d.sensitivity(
|
|
e_field,
|
|
h_field,
|
|
wavenumber=wavenumber,
|
|
omega=OMEGA,
|
|
dxes=DXES_2D,
|
|
epsilon=epsilon,
|
|
)
|
|
|
|
target_index = int(numpy.argmax(numpy.abs(sensitivity)))
|
|
delta = 1e-4
|
|
epsilon_perturbed = epsilon.copy()
|
|
epsilon_perturbed[target_index] += delta
|
|
|
|
_, perturbed_wavenumber = waveguide_2d.solve_mode(
|
|
0,
|
|
omega=OMEGA,
|
|
dxes=DXES_2D,
|
|
epsilon=epsilon_perturbed,
|
|
)
|
|
finite_difference = (perturbed_wavenumber - wavenumber) / delta
|
|
|
|
assert numpy.isfinite(sensitivity[target_index])
|
|
assert numpy.isfinite(finite_difference)
|
|
assert abs(sensitivity[target_index].imag) < 1e-10
|
|
assert abs(finite_difference.imag) < 1e-10
|
|
|
|
ratio = abs(sensitivity[target_index] / finite_difference)
|
|
assert sensitivity[target_index].real > 0
|
|
assert finite_difference.real > 0
|
|
assert 0.4 < ratio < 1.8
|
|
|
|
|
|
@pytest.mark.parametrize('absorption', ['none', 'pml', 'material'])
|
|
def test_sensitivity_matches_central_differences_on_graded_yee_mesh(absorption) -> 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]
|
|
if absorption == 'pml':
|
|
dxes = scpml.stretch_with_scpml(dxes, axis=0, polarity=-1, omega=omega, thickness=1, max_imag_ratio=0.3)
|
|
elif absorption == 'material':
|
|
epsilon = epsilon * (1 - 0.15j * rng.uniform(0.5, 1.5, epsilon.size))
|
|
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:
|
|
epsilon = build_asymmetric_epsilon()
|
|
mu = build_mu_profile()
|
|
|
|
e_xy, wavenumber = waveguide_2d.solve_mode(
|
|
0,
|
|
omega=OMEGA,
|
|
dxes=DXES_2D,
|
|
epsilon=epsilon,
|
|
mu=mu,
|
|
)
|
|
_e_ref, h_ref = waveguide_2d.normalized_fields_e(
|
|
e_xy,
|
|
wavenumber=wavenumber,
|
|
omega=OMEGA,
|
|
dxes=DXES_2D,
|
|
epsilon=epsilon,
|
|
mu=mu,
|
|
)
|
|
h_xy = numpy.concatenate(numpy.split(h_ref, 3)[:2])
|
|
|
|
e_field, h_field = waveguide_2d.normalized_fields_h(
|
|
h_xy,
|
|
wavenumber=wavenumber,
|
|
omega=OMEGA,
|
|
dxes=DXES_2D,
|
|
epsilon=epsilon,
|
|
mu=mu,
|
|
)
|
|
overlap = waveguide_2d.inner_product(e_field, h_field, DXES_2D, conj_h=True)
|
|
|
|
assert e_field.shape == (3 * GRID_SHAPE[0] * GRID_SHAPE[1],)
|
|
assert h_field.shape == (3 * GRID_SHAPE[0] * GRID_SHAPE[1],)
|
|
assert numpy.isfinite(e_field).all()
|
|
assert numpy.isfinite(h_field).all()
|
|
assert abs(overlap.real - 1.0) < 1e-10
|
|
assert abs(overlap.imag) < 1e-10
|
|
|
|
|
|
def test_waveguide_2d_helper_operators_with_mu_return_finite_outputs() -> None:
|
|
epsilon = build_asymmetric_epsilon()
|
|
mu = build_mu_profile()
|
|
|
|
e_xy, wavenumber = waveguide_2d.solve_mode(
|
|
0,
|
|
omega=OMEGA,
|
|
dxes=DXES_2D,
|
|
epsilon=epsilon,
|
|
mu=mu,
|
|
)
|
|
_e_ref, h_ref = waveguide_2d.normalized_fields_e(
|
|
e_xy,
|
|
wavenumber=wavenumber,
|
|
omega=OMEGA,
|
|
dxes=DXES_2D,
|
|
epsilon=epsilon,
|
|
mu=mu,
|
|
)
|
|
h_xy = numpy.concatenate(numpy.split(h_ref, 3)[:2])
|
|
n_pts = GRID_SHAPE[0] * GRID_SHAPE[1]
|
|
|
|
operators = [
|
|
('exy2h', waveguide_2d.exy2h(wavenumber, OMEGA, DXES_2D, epsilon, mu), e_xy),
|
|
('hxy2e', waveguide_2d.hxy2e(wavenumber, OMEGA, DXES_2D, epsilon, mu), h_xy),
|
|
('hxy2h', waveguide_2d.hxy2h(wavenumber, DXES_2D, mu), h_xy),
|
|
]
|
|
|
|
for _name, operator, vector in operators:
|
|
result = operator @ vector
|
|
assert operator.shape == (3 * n_pts, 2 * n_pts)
|
|
assert numpy.isfinite(operator.data).all()
|
|
assert result.shape == (3 * n_pts,)
|
|
assert numpy.isfinite(result).all()
|
|
|
|
|
|
def test_waveguide_2d_error_helpers_with_mu_return_finite_values() -> None:
|
|
epsilon = build_asymmetric_epsilon()
|
|
mu = build_mu_profile()
|
|
|
|
e_xy, wavenumber = waveguide_2d.solve_mode(
|
|
0,
|
|
omega=OMEGA,
|
|
dxes=DXES_2D,
|
|
epsilon=epsilon,
|
|
mu=mu,
|
|
)
|
|
e_field, h_field = waveguide_2d.normalized_fields_e(
|
|
e_xy,
|
|
wavenumber=wavenumber,
|
|
omega=OMEGA,
|
|
dxes=DXES_2D,
|
|
epsilon=epsilon,
|
|
mu=mu,
|
|
)
|
|
|
|
h_error = waveguide_2d.h_err(h_field, wavenumber, OMEGA, DXES_2D, epsilon, mu)
|
|
e_error = waveguide_2d.e_err(e_field, wavenumber, OMEGA, DXES_2D, epsilon, mu)
|
|
|
|
assert numpy.isfinite(h_error)
|
|
assert numpy.isfinite(e_error)
|
|
assert 0 < h_error < 1e-6
|
|
assert 0 < e_error < 1e-6
|
|
|
|
|
|
def test_waveguide_2d_inner_product_phase_and_conjugation_branches() -> None:
|
|
epsilon = build_asymmetric_epsilon()
|
|
mu = build_mu_profile()
|
|
|
|
e_xy, wavenumber = waveguide_2d.solve_mode(
|
|
0,
|
|
omega=OMEGA,
|
|
dxes=DXES_2D,
|
|
epsilon=epsilon,
|
|
mu=mu,
|
|
)
|
|
e_field, h_field = waveguide_2d.normalized_fields_e(
|
|
e_xy,
|
|
wavenumber=wavenumber,
|
|
omega=OMEGA,
|
|
dxes=DXES_2D,
|
|
epsilon=epsilon,
|
|
mu=mu,
|
|
)
|
|
|
|
overlap_no_conj = waveguide_2d.inner_product(e_field, h_field, DXES_2D, conj_h=False)
|
|
overlap_conj = waveguide_2d.inner_product(e_field, h_field, DXES_2D, conj_h=True)
|
|
overlap_phase = waveguide_2d.inner_product(e_field, h_field, DXES_2D, conj_h=True, prop_phase=0.3)
|
|
|
|
assert numpy.isfinite(overlap_no_conj)
|
|
assert numpy.isfinite(overlap_phase)
|
|
assert abs(overlap_no_conj.real - 1.0) < 1e-10
|
|
assert abs(overlap_no_conj.imag) < 1e-10
|
|
assert_allclose(overlap_phase / overlap_conj, numpy.exp(-0.15j), atol=1e-12, rtol=1e-12)
|
|
|
|
|
|
def test_waveguide_2d_inner_product_trapezoid_branch_on_nonuniform_grid() -> None:
|
|
epsilon = build_asymmetric_epsilon()
|
|
|
|
e_xy, wavenumber = waveguide_2d.solve_mode(
|
|
0,
|
|
omega=OMEGA,
|
|
dxes=DXES_2D_NONUNIFORM,
|
|
epsilon=epsilon,
|
|
)
|
|
e_field, h_field = waveguide_2d.normalized_fields_e(
|
|
e_xy,
|
|
wavenumber=wavenumber,
|
|
omega=OMEGA,
|
|
dxes=DXES_2D_NONUNIFORM,
|
|
epsilon=epsilon,
|
|
)
|
|
|
|
overlap_rect = waveguide_2d.inner_product(e_field, h_field, DXES_2D_NONUNIFORM, conj_h=True)
|
|
overlap_trap = waveguide_2d.inner_product(
|
|
e_field,
|
|
h_field,
|
|
DXES_2D_NONUNIFORM,
|
|
conj_h=True,
|
|
trapezoid=True,
|
|
)
|
|
|
|
assert numpy.isfinite(overlap_rect)
|
|
assert numpy.isfinite(overlap_trap)
|
|
assert abs(overlap_rect.imag) < 1e-10
|
|
assert abs(overlap_trap.imag) < 1e-10
|
|
assert abs(overlap_rect - overlap_trap) > 0.1
|
|
|
|
|
|
def test_waveguide_2d_inner_product_uses_staggered_nonuniform_areas() -> None:
|
|
shape = (3, 4)
|
|
dxes = [
|
|
[numpy.asarray([0.4, 0.7, 1.1]), numpy.asarray([0.3, 0.5, 0.8, 1.2])],
|
|
[numpy.asarray([0.6, 0.9, 1.3]), numpy.asarray([0.2, 0.45, 0.75, 1.0])],
|
|
]
|
|
samples = numpy.arange(numpy.prod(shape), dtype=float).reshape(shape)
|
|
electric = vec(numpy.stack((1.0 + samples, 0.25 - 0.3 * samples, numpy.zeros(shape))))
|
|
magnetic = vec(numpy.stack((-0.4 + 0.2 * samples, 2.0 - 0.1 * samples, numpy.zeros(shape))))
|
|
ex, ey, _ez = numpy.split(electric, 3)
|
|
hx, hy, _hz = numpy.split(magnetic, 3)
|
|
ex = ex.reshape(shape)
|
|
ey = ey.reshape(shape)
|
|
hx = hx.reshape(shape)
|
|
hy = hy.reshape(shape)
|
|
|
|
expected_rectangular = 0.5 * (
|
|
numpy.sum(ex * hy * dxes[0][0][:, None] * dxes[1][1][None, :])
|
|
- numpy.sum(ey * hx * dxes[1][0][:, None] * dxes[0][1][None, :])
|
|
)
|
|
expected_trapezoidal = 0.5 * (
|
|
numpy.trapezoid(numpy.trapezoid(ex * hy, numpy.cumsum(dxes[1][1])), numpy.cumsum(dxes[0][0]))
|
|
- numpy.trapezoid(numpy.trapezoid(ey * hx, numpy.cumsum(dxes[0][1])), numpy.cumsum(dxes[1][0]))
|
|
)
|
|
|
|
assert_allclose(waveguide_2d.inner_product(electric, magnetic, dxes), expected_rectangular)
|
|
assert_allclose(
|
|
waveguide_2d.inner_product(electric, magnetic, dxes, trapezoid=True),
|
|
expected_trapezoidal,
|
|
)
|