[tests] more tests
This commit is contained in:
parent
9ac24892d6
commit
0ff23542ac
4 changed files with 392 additions and 11 deletions
|
|
@ -18,35 +18,35 @@ from .types import (
|
|||
|
||||
@overload
|
||||
def vec(f: None) -> None:
|
||||
pass
|
||||
pass # pragma: no cover
|
||||
|
||||
@overload
|
||||
def vec(f: fdfield_t) -> vfdfield_t:
|
||||
pass
|
||||
pass # pragma: no cover
|
||||
|
||||
@overload
|
||||
def vec(f: cfdfield_t) -> vcfdfield_t:
|
||||
pass
|
||||
pass # pragma: no cover
|
||||
|
||||
@overload
|
||||
def vec(f: fdfield2_t) -> vfdfield2_t:
|
||||
pass
|
||||
pass # pragma: no cover
|
||||
|
||||
@overload
|
||||
def vec(f: cfdfield2_t) -> vcfdfield2_t:
|
||||
pass
|
||||
pass # pragma: no cover
|
||||
|
||||
@overload
|
||||
def vec(f: fdslice_t) -> vfdslice_t:
|
||||
pass
|
||||
pass # pragma: no cover
|
||||
|
||||
@overload
|
||||
def vec(f: cfdslice_t) -> vcfdslice_t:
|
||||
pass
|
||||
pass # pragma: no cover
|
||||
|
||||
@overload
|
||||
def vec(f: ArrayLike) -> NDArray:
|
||||
pass
|
||||
pass # pragma: no cover
|
||||
|
||||
def vec(
|
||||
f: fdfield_t | cfdfield_t | fdfield2_t | cfdfield2_t | fdslice_t | cfdslice_t | ArrayLike | None,
|
||||
|
|
@ -70,15 +70,15 @@ def vec(
|
|||
|
||||
@overload
|
||||
def unvec(v: None, shape: Sequence[int], nvdim: int = 3) -> None:
|
||||
pass
|
||||
pass # pragma: no cover
|
||||
|
||||
@overload
|
||||
def unvec(v: vfdfield_t, shape: Sequence[int], nvdim: int = 3) -> fdfield_t:
|
||||
pass
|
||||
pass # pragma: no cover
|
||||
|
||||
@overload
|
||||
def unvec(v: vcfdfield_t, shape: Sequence[int], nvdim: int = 3) -> cfdfield_t:
|
||||
pass
|
||||
pass # pragma: no cover
|
||||
|
||||
@overload
|
||||
def unvec(v: vfdfield2_t, shape: Sequence[int], nvdim: int = 3) -> fdfield2_t:
|
||||
|
|
|
|||
45
meanas/test/test_fdmath_vectorization.py
Normal file
45
meanas/test/test_fdmath_vectorization.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import numpy
|
||||
from numpy.testing import assert_allclose, assert_array_equal
|
||||
|
||||
from ..fdmath import unvec, vec
|
||||
|
||||
|
||||
SHAPE = (2, 3, 2)
|
||||
FIELD = numpy.arange(3 * numpy.prod(SHAPE), dtype=float).reshape((3, *SHAPE), order='C')
|
||||
COMPLEX_FIELD = (FIELD + 0.5j * FIELD).astype(complex)
|
||||
|
||||
|
||||
def test_vec_and_unvec_return_none_for_none_input() -> None:
|
||||
assert vec(None) is None
|
||||
assert unvec(None, SHAPE) is None
|
||||
|
||||
|
||||
def test_real_field_round_trip_preserves_shape_and_values() -> None:
|
||||
vector = vec(FIELD)
|
||||
assert vector is not None
|
||||
restored = unvec(vector, SHAPE)
|
||||
assert restored is not None
|
||||
assert restored.shape == (3, *SHAPE)
|
||||
assert_array_equal(restored, FIELD)
|
||||
|
||||
|
||||
def test_complex_field_round_trip_preserves_shape_and_values() -> None:
|
||||
vector = vec(COMPLEX_FIELD)
|
||||
assert vector is not None
|
||||
restored = unvec(vector, SHAPE)
|
||||
assert restored is not None
|
||||
assert restored.shape == (3, *SHAPE)
|
||||
assert_allclose(restored, COMPLEX_FIELD)
|
||||
|
||||
|
||||
def test_unvec_with_two_components_round_trips_vector() -> None:
|
||||
vector = numpy.arange(2 * numpy.prod(SHAPE), dtype=float)
|
||||
field = unvec(vector, SHAPE, nvdim=2)
|
||||
assert field is not None
|
||||
assert field.shape == (2, *SHAPE)
|
||||
assert_array_equal(vec(field), vector)
|
||||
|
||||
|
||||
def test_vec_accepts_arraylike_input() -> None:
|
||||
arraylike = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
|
||||
assert_array_equal(vec(arraylike), numpy.ravel(arraylike, order='C'))
|
||||
98
meanas/test/test_fdtd_energy.py
Normal file
98
meanas/test/test_fdtd_energy.py
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import numpy
|
||||
from numpy.testing import assert_allclose
|
||||
|
||||
from .. import fdtd
|
||||
from ..fdtd import energy as fdtd_energy
|
||||
|
||||
|
||||
SHAPE = (2, 2, 2)
|
||||
DT = 0.25
|
||||
UNIT_DXES = tuple(tuple(numpy.ones(length) for length in SHAPE) for _ in range(2))
|
||||
DXES = (
|
||||
(
|
||||
numpy.array([1.0, 1.5]),
|
||||
numpy.array([0.75, 1.25]),
|
||||
numpy.array([1.1, 0.9]),
|
||||
),
|
||||
(
|
||||
numpy.array([0.8, 1.2]),
|
||||
numpy.array([1.4, 0.6]),
|
||||
numpy.array([0.7, 1.3]),
|
||||
),
|
||||
)
|
||||
E0 = numpy.arange(3 * numpy.prod(SHAPE), dtype=float).reshape((3, *SHAPE), order='C')
|
||||
E1 = E0 + 0.5
|
||||
E2 = E0 + 1.0
|
||||
E3 = E0 + 1.5
|
||||
H0 = (numpy.arange(3 * numpy.prod(SHAPE), dtype=float).reshape((3, *SHAPE), order='C') + 2.0) / 3.0
|
||||
H1 = H0 + 0.25
|
||||
H2 = H0 + 0.5
|
||||
H3 = H0 + 0.75
|
||||
J0 = (E0 + 2.0) / 5.0
|
||||
EPSILON = 1.0 + E0 / 20.0
|
||||
MU = 1.5 + H0 / 10.0
|
||||
|
||||
|
||||
def test_poynting_default_spacing_matches_explicit_unit_spacing() -> None:
|
||||
default_spacing = fdtd.poynting(E1, H1)
|
||||
explicit_spacing = fdtd.poynting(E1, H1, dxes=UNIT_DXES)
|
||||
assert_allclose(default_spacing, explicit_spacing)
|
||||
|
||||
|
||||
def test_poynting_divergence_matches_precomputed_poynting_vector() -> None:
|
||||
s = fdtd.poynting(E2, H2, dxes=DXES)
|
||||
from_fields = fdtd.poynting_divergence(e=E2, h=H2, dxes=DXES)
|
||||
from_vector = fdtd.poynting_divergence(s=s)
|
||||
assert_allclose(from_fields, from_vector)
|
||||
|
||||
|
||||
def test_delta_energy_h2e_matches_direct_dxmul_formula() -> None:
|
||||
expected = fdtd_energy.dxmul(
|
||||
E2 * (E2 - E0) / DT,
|
||||
H1 * (H3 - H1) / DT,
|
||||
EPSILON,
|
||||
MU,
|
||||
DXES,
|
||||
)
|
||||
actual = fdtd.delta_energy_h2e(
|
||||
dt=DT,
|
||||
e0=E0,
|
||||
h1=H1,
|
||||
e2=E2,
|
||||
h3=H3,
|
||||
epsilon=EPSILON,
|
||||
mu=MU,
|
||||
dxes=DXES,
|
||||
)
|
||||
assert_allclose(actual, expected)
|
||||
|
||||
|
||||
def test_delta_energy_e2h_matches_direct_dxmul_formula() -> None:
|
||||
expected = fdtd_energy.dxmul(
|
||||
E1 * (E3 - E1) / DT,
|
||||
H2 * (H2 - H0) / DT,
|
||||
EPSILON,
|
||||
MU,
|
||||
DXES,
|
||||
)
|
||||
actual = fdtd_energy.delta_energy_e2h(
|
||||
dt=DT,
|
||||
h0=H0,
|
||||
e1=E1,
|
||||
h2=H2,
|
||||
e3=E3,
|
||||
epsilon=EPSILON,
|
||||
mu=MU,
|
||||
dxes=DXES,
|
||||
)
|
||||
assert_allclose(actual, expected)
|
||||
|
||||
|
||||
def test_delta_energy_j_defaults_to_unit_cell_volume() -> None:
|
||||
expected = (J0 * E1).sum(axis=0)
|
||||
assert_allclose(fdtd.delta_energy_j(j0=J0, e1=E1), expected)
|
||||
|
||||
|
||||
def test_dxmul_defaults_to_unit_materials_and_spacing() -> None:
|
||||
expected = E1.sum(axis=0) + H1.sum(axis=0)
|
||||
assert_allclose(fdtd_energy.dxmul(E1, H1), expected)
|
||||
238
meanas/test/test_regressions.py
Normal file
238
meanas/test/test_regressions.py
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
import numpy
|
||||
import pytest # type: ignore
|
||||
from numpy.testing import assert_allclose
|
||||
from scipy import sparse
|
||||
|
||||
from ..eigensolvers import power_iteration, rayleigh_quotient_iteration, signed_eigensolve
|
||||
from ..fdfd import eme, farfield
|
||||
from ..fdtd.boundaries import conducting_boundary
|
||||
from ..fdtd.misc import gaussian_beam, gaussian_packet, ricker_pulse
|
||||
from ..fdtd.pml import cpml_params, updates_with_cpml
|
||||
|
||||
|
||||
def _axis_index(axis: int, index: int) -> tuple[slice | int, ...]:
|
||||
coords: list[slice | int] = [slice(None), slice(None), slice(None)]
|
||||
coords[axis] = index
|
||||
return tuple(coords)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('one_sided', [False, True])
|
||||
def test_gaussian_packet_accepts_array_input(one_sided: bool) -> None:
|
||||
dt = 0.01
|
||||
source, delay = gaussian_packet(1.55, 0.1, dt, one_sided=one_sided)
|
||||
steps = numpy.array([0, int(numpy.ceil(delay / dt)) + 5])
|
||||
envelope, cc, ss = source(steps)
|
||||
|
||||
assert envelope.shape == (2,)
|
||||
assert numpy.isfinite(envelope).all()
|
||||
assert numpy.isfinite(cc).all()
|
||||
assert numpy.isfinite(ss).all()
|
||||
if one_sided:
|
||||
assert envelope[-1] == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_ricker_pulse_returns_finite_values() -> None:
|
||||
source, delay = ricker_pulse(1.55, 0.01)
|
||||
envelope, cc, ss = source(numpy.array([0, 1, 2]))
|
||||
|
||||
assert numpy.isfinite(delay)
|
||||
assert numpy.isfinite(envelope).all()
|
||||
assert numpy.isfinite(cc).all()
|
||||
assert numpy.isfinite(ss).all()
|
||||
|
||||
|
||||
def test_gaussian_beam_centered_grid_is_finite_and_normalized() -> None:
|
||||
beam = gaussian_beam(
|
||||
xyz=[numpy.linspace(-1, 1, 3), numpy.linspace(-1, 1, 3), numpy.linspace(-1, 1, 3)],
|
||||
center=[0, 0, 0],
|
||||
waist_radius=1.0,
|
||||
wl=1.55,
|
||||
)
|
||||
|
||||
row = beam[:, :, beam.shape[2] // 2]
|
||||
assert numpy.isfinite(beam).all()
|
||||
assert numpy.linalg.norm(row) == pytest.approx(1.0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('direction', [0, 1, 2])
|
||||
@pytest.mark.parametrize('polarity', [-1, 1])
|
||||
def test_conducting_boundary_updates_expected_faces(direction: int, polarity: int) -> None:
|
||||
e = numpy.arange(3 * 4 * 4 * 4, dtype=float).reshape(3, 4, 4, 4)
|
||||
h = e.copy()
|
||||
e0 = e.copy()
|
||||
h0 = h.copy()
|
||||
|
||||
update_e, update_h = conducting_boundary(direction, polarity)
|
||||
update_e(e)
|
||||
update_h(h)
|
||||
|
||||
dirs = [0, 1, 2]
|
||||
dirs.remove(direction)
|
||||
u, v = dirs
|
||||
|
||||
if polarity < 0:
|
||||
boundary = _axis_index(direction, 0)
|
||||
shifted1 = _axis_index(direction, 1)
|
||||
|
||||
assert_allclose(e[direction][boundary], 0)
|
||||
assert_allclose(e[u][boundary], e0[u][shifted1])
|
||||
assert_allclose(e[v][boundary], e0[v][shifted1])
|
||||
assert_allclose(h[direction][boundary], h0[direction][shifted1])
|
||||
assert_allclose(h[u][boundary], 0)
|
||||
assert_allclose(h[v][boundary], 0)
|
||||
else:
|
||||
boundary = _axis_index(direction, -1)
|
||||
shifted1 = _axis_index(direction, -2)
|
||||
shifted2 = _axis_index(direction, -3)
|
||||
|
||||
assert_allclose(e[direction][boundary], -e0[direction][shifted2])
|
||||
assert_allclose(e[direction][shifted1], 0)
|
||||
assert_allclose(e[u][boundary], e0[u][shifted1])
|
||||
assert_allclose(e[v][boundary], e0[v][shifted1])
|
||||
assert_allclose(h[direction][boundary], h0[direction][shifted1])
|
||||
assert_allclose(h[u][boundary], -h0[u][shifted2])
|
||||
assert_allclose(h[u][shifted1], 0)
|
||||
assert_allclose(h[v][boundary], -h0[v][shifted2])
|
||||
assert_allclose(h[v][shifted1], 0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
('direction', 'polarity'),
|
||||
[(-1, 1), (3, 1), (0, 0)],
|
||||
)
|
||||
def test_conducting_boundary_rejects_invalid_arguments(direction: int, polarity: int) -> None:
|
||||
with pytest.raises(Exception):
|
||||
conducting_boundary(direction, polarity)
|
||||
|
||||
|
||||
def test_get_abcd_returns_sparse_block_matrix(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
t = numpy.array([[1.0, 0.0], [0.0, 2.0]])
|
||||
r = numpy.array([[0.0, 0.1], [0.2, 0.0]])
|
||||
monkeypatch.setattr(eme, 'get_tr', lambda *args, **kwargs: (t, r))
|
||||
|
||||
abcd = eme.get_abcd(None, None, None, None)
|
||||
|
||||
assert sparse.issparse(abcd)
|
||||
assert abcd.shape == (4, 4)
|
||||
assert numpy.isfinite(abcd.toarray()).all()
|
||||
|
||||
|
||||
def test_get_s_force_reciprocal_symmetrizes_output(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
left = object()
|
||||
right = object()
|
||||
|
||||
def fake_get_tr(_eh_l, wavenumbers_l, _eh_r, _wavenumbers_r, **kwargs):
|
||||
if wavenumbers_l is left:
|
||||
return (
|
||||
numpy.array([[1.0, 2.0], [0.5, 1.0]]),
|
||||
numpy.array([[0.0, 1.0], [2.0, 0.0]]),
|
||||
)
|
||||
return (
|
||||
numpy.array([[1.0, -1.0], [0.0, 1.0]]),
|
||||
numpy.array([[0.0, 0.5], [1.5, 0.0]]),
|
||||
)
|
||||
|
||||
monkeypatch.setattr(eme, 'get_tr', fake_get_tr)
|
||||
ss = eme.get_s(None, left, None, right, force_reciprocal=True)
|
||||
|
||||
assert_allclose(ss, ss.T)
|
||||
|
||||
|
||||
def test_farfield_roundtrip_supports_rectangular_arrays() -> None:
|
||||
e_near = [numpy.zeros((4, 8), dtype=complex), numpy.zeros((4, 8), dtype=complex)]
|
||||
h_near = [numpy.zeros((4, 8), dtype=complex), numpy.zeros((4, 8), dtype=complex)]
|
||||
e_near[0][1, 3] = 1.0 + 0.25j
|
||||
h_near[1][2, 5] = -0.5j
|
||||
|
||||
ff = farfield.near_to_farfield(e_near, h_near, dx=0.2, dy=0.3, padded_size=(4, 8))
|
||||
restored = farfield.far_to_nearfield(
|
||||
[field.copy() for field in ff['E']],
|
||||
[field.copy() for field in ff['H']],
|
||||
ff['dkx'],
|
||||
ff['dky'],
|
||||
padded_size=(4, 8),
|
||||
)
|
||||
|
||||
assert isinstance(ff['dkx'], float)
|
||||
assert isinstance(ff['dky'], float)
|
||||
assert ff['E'][0].shape == (4, 8)
|
||||
assert restored['E'][0].shape == (4, 8)
|
||||
assert restored['H'][0].shape == (4, 8)
|
||||
assert restored['dx'] == pytest.approx(0.2)
|
||||
assert restored['dy'] == pytest.approx(0.3)
|
||||
assert numpy.isfinite(restored['E'][0]).all()
|
||||
assert numpy.isfinite(restored['H'][0]).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
('axis', 'polarity', 'thickness', 'epsilon_eff'),
|
||||
[(3, 1, 4, 1.0), (0, 0, 4, 1.0), (0, 1, 2, 1.0), (0, 1, 4, 0.0)],
|
||||
)
|
||||
def test_cpml_params_reject_invalid_arguments(axis: int, polarity: int, thickness: int, epsilon_eff: float) -> None:
|
||||
with pytest.raises(Exception):
|
||||
cpml_params(axis=axis, polarity=polarity, dt=0.1, thickness=thickness, epsilon_eff=epsilon_eff)
|
||||
|
||||
|
||||
def test_cpml_params_shapes_and_region() -> None:
|
||||
params = cpml_params(axis=1, polarity=1, dt=0.1, thickness=3)
|
||||
p0e, p1e, p2e = params['param_e']
|
||||
p0h, p1h, p2h = params['param_h']
|
||||
|
||||
assert p0e.shape == (1, 3, 1)
|
||||
assert p1e.shape == (1, 3, 1)
|
||||
assert p2e.shape == (1, 3, 1)
|
||||
assert p0h.shape == (1, 3, 1)
|
||||
assert p1h.shape == (1, 3, 1)
|
||||
assert p2h.shape == (1, 3, 1)
|
||||
assert params['region'][1] == slice(-3, None)
|
||||
|
||||
|
||||
def test_updates_with_cpml_keeps_zero_fields_zero() -> None:
|
||||
shape = (3, 4, 4, 4)
|
||||
epsilon = numpy.ones(shape, dtype=float)
|
||||
e = numpy.zeros(shape, dtype=float)
|
||||
h = numpy.zeros(shape, dtype=float)
|
||||
dxes = [[numpy.ones(4), numpy.ones(4), numpy.ones(4)] for _ in range(2)]
|
||||
params = [[None, None] for _ in range(3)]
|
||||
params[0][0] = cpml_params(axis=0, polarity=-1, dt=0.1, thickness=3)
|
||||
|
||||
update_e, update_h = updates_with_cpml(params, dt=0.1, dxes=dxes, epsilon=epsilon)
|
||||
update_e(e, h, epsilon)
|
||||
update_h(e, h)
|
||||
|
||||
assert not e.any()
|
||||
assert not h.any()
|
||||
|
||||
|
||||
def test_power_iteration_finds_dominant_mode() -> None:
|
||||
operator = sparse.diags([5.0, 3.0, 1.0, -2.0]).tocsr()
|
||||
eigval, eigvec = power_iteration(operator, guess_vector=numpy.ones(4, dtype=complex), iterations=20)
|
||||
|
||||
assert eigval == pytest.approx(5.0, rel=1e-6)
|
||||
assert abs(eigvec[0]) > abs(eigvec[1])
|
||||
|
||||
|
||||
def test_rayleigh_quotient_iteration_refines_known_mode() -> None:
|
||||
operator = sparse.diags([5.0, 3.0, 1.0, -2.0]).tocsr()
|
||||
|
||||
def solver(matrix: sparse.spmatrix, rhs: numpy.ndarray) -> numpy.ndarray:
|
||||
return numpy.linalg.lstsq(matrix.toarray(), rhs, rcond=None)[0]
|
||||
|
||||
eigval, eigvec = rayleigh_quotient_iteration(
|
||||
operator,
|
||||
numpy.array([1.0, 0.1, 0.0, 0.0], dtype=complex),
|
||||
iterations=8,
|
||||
solver=solver,
|
||||
)
|
||||
|
||||
residual = numpy.linalg.norm(operator @ eigvec - eigval * eigvec)
|
||||
assert eigval == pytest.approx(3.0, rel=1e-6)
|
||||
assert residual < 1e-8
|
||||
|
||||
|
||||
def test_signed_eigensolve_returns_largest_positive_modes() -> None:
|
||||
operator = sparse.diags([5.0, 3.0, 1.0, -2.0]).tocsr()
|
||||
eigvals, eigvecs = signed_eigensolve(operator, how_many=2)
|
||||
|
||||
assert_allclose(eigvals, [3.0, 5.0], atol=1e-6)
|
||||
assert eigvecs.shape == (4, 2)
|
||||
Loading…
Add table
Add a link
Reference in a new issue