[tests] more tests

This commit is contained in:
Jan Petykiewicz 2026-04-17 22:58:16 -07:00
commit 0ff23542ac
4 changed files with 392 additions and 11 deletions

View file

@ -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:

View 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'))

View 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)

View 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)