[tests] add some more tests around numerical self-consistency
This commit is contained in:
parent
bc55baf4a6
commit
38a5c1a9aa
3 changed files with 258 additions and 0 deletions
47
meanas/test/test_eigensolvers_numerics.py
Normal file
47
meanas/test/test_eigensolvers_numerics.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import numpy
|
||||||
|
from numpy.linalg import norm
|
||||||
|
from scipy import sparse
|
||||||
|
import scipy.sparse.linalg as spalg
|
||||||
|
|
||||||
|
from ..eigensolvers import rayleigh_quotient_iteration, signed_eigensolve
|
||||||
|
|
||||||
|
|
||||||
|
def test_rayleigh_quotient_iteration_with_linear_operator() -> None:
|
||||||
|
operator = sparse.diags([5.0, 3.0, 1.0, -2.0]).tocsr()
|
||||||
|
linear_operator = spalg.LinearOperator(
|
||||||
|
shape=operator.shape,
|
||||||
|
dtype=complex,
|
||||||
|
matvec=lambda vv: operator @ vv,
|
||||||
|
)
|
||||||
|
|
||||||
|
def dense_solver(
|
||||||
|
shifted_operator: spalg.LinearOperator,
|
||||||
|
rhs: numpy.ndarray,
|
||||||
|
) -> numpy.ndarray:
|
||||||
|
basis = numpy.eye(operator.shape[0], dtype=complex)
|
||||||
|
columns = [shifted_operator.matvec(basis[:, ii]) for ii in range(operator.shape[0])]
|
||||||
|
dense_matrix = numpy.column_stack(columns)
|
||||||
|
return numpy.linalg.lstsq(dense_matrix, rhs, rcond=None)[0]
|
||||||
|
|
||||||
|
guess = numpy.array([0.0, 1.0, 1e-6, 0.0], dtype=complex)
|
||||||
|
eigval, eigvec = rayleigh_quotient_iteration(
|
||||||
|
linear_operator,
|
||||||
|
guess,
|
||||||
|
iterations=8,
|
||||||
|
solver=dense_solver,
|
||||||
|
)
|
||||||
|
|
||||||
|
residual = norm(operator @ eigvec - eigval * eigvec)
|
||||||
|
assert abs(eigval - 3.0) < 1e-12
|
||||||
|
assert residual < 1e-12
|
||||||
|
|
||||||
|
|
||||||
|
def test_signed_eigensolve_negative_returns_largest_negative_mode() -> None:
|
||||||
|
operator = sparse.diags([5.0, 3.0, 1.0, -2.0]).tocsr()
|
||||||
|
|
||||||
|
eigvals, eigvecs = signed_eigensolve(operator, how_many=1, negative=True)
|
||||||
|
|
||||||
|
assert eigvals.shape == (1,)
|
||||||
|
assert eigvecs.shape == (4, 1)
|
||||||
|
assert abs(eigvals[0] + 2.0) < 1e-12
|
||||||
|
assert abs(eigvecs[3, 0]) > 0.99
|
||||||
108
meanas/test/test_waveguide_2d_numerics.py
Normal file
108
meanas/test/test_waveguide_2d_numerics.py
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
import numpy
|
||||||
|
from numpy.linalg import norm
|
||||||
|
|
||||||
|
from ..fdmath import vec
|
||||||
|
from ..fdfd import waveguide_2d
|
||||||
|
|
||||||
|
|
||||||
|
OMEGA = 1 / 1500
|
||||||
|
GRID_SHAPE = (5, 5)
|
||||||
|
DXES_2D = [[numpy.ones(GRID_SHAPE[0]), numpy.ones(GRID_SHAPE[1])] 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 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
|
||||||
|
|
||||||
|
numpy.testing.assert_allclose(
|
||||||
|
sensitivity[target_index],
|
||||||
|
finite_difference,
|
||||||
|
rtol=0.1,
|
||||||
|
atol=1e-6,
|
||||||
|
)
|
||||||
103
meanas/test/test_waveguide_mode_helpers.py
Normal file
103
meanas/test/test_waveguide_mode_helpers.py
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
import numpy
|
||||||
|
from numpy.linalg import norm
|
||||||
|
|
||||||
|
from ..fdmath import vec
|
||||||
|
from ..fdfd import waveguide_3d, waveguide_cyl
|
||||||
|
|
||||||
|
|
||||||
|
OMEGA = 1 / 1500
|
||||||
|
|
||||||
|
|
||||||
|
def test_waveguide_3d_solve_mode_and_expand_e_are_phase_consistent() -> None:
|
||||||
|
epsilon = numpy.ones((3, 5, 5, 1), dtype=float)
|
||||||
|
dxes = [[numpy.ones(5), numpy.ones(5), numpy.ones(1)] for _ in range(2)]
|
||||||
|
axis = 0
|
||||||
|
polarity = 1
|
||||||
|
slices = (slice(0, 1), slice(None), slice(None))
|
||||||
|
|
||||||
|
result = waveguide_3d.solve_mode(
|
||||||
|
0,
|
||||||
|
omega=OMEGA,
|
||||||
|
dxes=dxes,
|
||||||
|
axis=axis,
|
||||||
|
polarity=polarity,
|
||||||
|
slices=slices,
|
||||||
|
epsilon=epsilon,
|
||||||
|
)
|
||||||
|
expanded = waveguide_3d.expand_e(
|
||||||
|
E=result['E'],
|
||||||
|
wavenumber=result['wavenumber'],
|
||||||
|
dxes=dxes,
|
||||||
|
axis=axis,
|
||||||
|
polarity=polarity,
|
||||||
|
slices=slices,
|
||||||
|
)
|
||||||
|
|
||||||
|
dx_prop = 0.5 * numpy.array([dx[2][slices[2]] for dx in dxes]).sum()
|
||||||
|
expected_wavenumber = 2 / dx_prop * numpy.arcsin(result['wavenumber_2d'] * dx_prop / 2)
|
||||||
|
solved_slice = (slice(None), *slices)
|
||||||
|
|
||||||
|
assert result['E'].shape == epsilon.shape
|
||||||
|
assert result['H'].shape == epsilon.shape
|
||||||
|
assert numpy.isfinite(result['E']).all()
|
||||||
|
assert numpy.isfinite(result['H']).all()
|
||||||
|
assert abs(result['wavenumber'] - expected_wavenumber) < 1e-12
|
||||||
|
assert numpy.allclose(expanded[solved_slice], result['E'][solved_slice])
|
||||||
|
|
||||||
|
component, _x, y_index, z_index = numpy.unravel_index(
|
||||||
|
numpy.abs(result['E']).argmax(),
|
||||||
|
result['E'].shape,
|
||||||
|
)
|
||||||
|
values = expanded[component, :, y_index, z_index]
|
||||||
|
ratios = values[1:] / values[:-1]
|
||||||
|
expected_ratio = numpy.exp(-1j * result['wavenumber'])
|
||||||
|
|
||||||
|
numpy.testing.assert_allclose(ratios, expected_ratio, rtol=1e-6, atol=1e-9)
|
||||||
|
|
||||||
|
|
||||||
|
def test_waveguide_cyl_solved_modes_are_ordered_and_low_residual() -> None:
|
||||||
|
shape = (5, 5)
|
||||||
|
dxes = [[numpy.ones(shape[0]), numpy.ones(shape[1])] for _ in range(2)]
|
||||||
|
epsilon = vec(numpy.ones((3, *shape), dtype=float))
|
||||||
|
rmin = 10.0
|
||||||
|
|
||||||
|
e_xys, angular_wavenumbers = waveguide_cyl.solve_modes(
|
||||||
|
[0, 1],
|
||||||
|
omega=OMEGA,
|
||||||
|
dxes=dxes,
|
||||||
|
epsilon=epsilon,
|
||||||
|
rmin=rmin,
|
||||||
|
)
|
||||||
|
operator = waveguide_cyl.cylindrical_operator(OMEGA, dxes, epsilon, rmin=rmin)
|
||||||
|
|
||||||
|
assert numpy.all(numpy.diff(numpy.real(angular_wavenumbers)) <= 0)
|
||||||
|
|
||||||
|
for e_xy, angular_wavenumber in zip(e_xys, angular_wavenumbers, strict=True):
|
||||||
|
eigenvalue = (angular_wavenumber / rmin) ** 2
|
||||||
|
residual = norm(operator @ e_xy - eigenvalue * e_xy) / norm(e_xy)
|
||||||
|
assert residual < 1e-6
|
||||||
|
|
||||||
|
|
||||||
|
def test_waveguide_cyl_linear_wavenumbers_are_finite_and_ordered() -> None:
|
||||||
|
shape = (5, 5)
|
||||||
|
dxes = [[numpy.ones(shape[0]), numpy.ones(shape[1])] for _ in range(2)]
|
||||||
|
epsilon = vec(numpy.ones((3, *shape), dtype=float))
|
||||||
|
|
||||||
|
e_xys, angular_wavenumbers = waveguide_cyl.solve_modes(
|
||||||
|
[0, 1],
|
||||||
|
omega=OMEGA,
|
||||||
|
dxes=dxes,
|
||||||
|
epsilon=epsilon,
|
||||||
|
rmin=10.0,
|
||||||
|
)
|
||||||
|
linear_wavenumbers = waveguide_cyl.linear_wavenumbers(
|
||||||
|
e_xys,
|
||||||
|
angular_wavenumbers,
|
||||||
|
epsilon=epsilon,
|
||||||
|
dxes=dxes,
|
||||||
|
rmin=10.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert numpy.isfinite(linear_wavenumbers).all()
|
||||||
|
assert numpy.all(numpy.real(linear_wavenumbers) > 0)
|
||||||
|
assert numpy.all(numpy.diff(numpy.real(linear_wavenumbers)) <= 0)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue