[tests] more test coverage
This commit is contained in:
parent
0afe2297b0
commit
267d161769
8 changed files with 410 additions and 4 deletions
|
|
@ -57,8 +57,6 @@ def cpml_params(
|
||||||
xh -= 0.5
|
xh -= 0.5
|
||||||
xe = xe[::-1]
|
xe = xe[::-1]
|
||||||
xh = xh[::-1]
|
xh = xh[::-1]
|
||||||
else:
|
|
||||||
raise Exception('Bad polarity!')
|
|
||||||
|
|
||||||
expand_slice_l: list[Any] = [None, None, None]
|
expand_slice_l: list[Any] = [None, None, None]
|
||||||
expand_slice_l[axis] = slice(None)
|
expand_slice_l[axis] = slice(None)
|
||||||
|
|
@ -82,8 +80,6 @@ def cpml_params(
|
||||||
region_list[axis] = slice(None, thickness)
|
region_list[axis] = slice(None, thickness)
|
||||||
elif polarity > 0:
|
elif polarity > 0:
|
||||||
region_list[axis] = slice(-thickness, None)
|
region_list[axis] = slice(-thickness, None)
|
||||||
else:
|
|
||||||
raise Exception('Bad polarity!')
|
|
||||||
region = tuple(region_list)
|
region = tuple(region_list)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import numpy
|
import numpy
|
||||||
|
import pytest
|
||||||
from numpy.testing import assert_allclose
|
from numpy.testing import assert_allclose
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
from ..fdfd import bloch
|
from ..fdfd import bloch
|
||||||
|
|
||||||
|
|
@ -10,6 +12,12 @@ EPSILON = numpy.ones((3, *SHAPE), dtype=float)
|
||||||
K0 = numpy.array([0.1, 0.0, 0.0], dtype=float)
|
K0 = numpy.array([0.1, 0.0, 0.0], dtype=float)
|
||||||
H_SIZE = 2 * numpy.prod(SHAPE)
|
H_SIZE = 2 * numpy.prod(SHAPE)
|
||||||
Y0 = (numpy.arange(H_SIZE, dtype=float) + 1j * numpy.linspace(0.1, 0.9, H_SIZE))[None, :]
|
Y0 = (numpy.arange(H_SIZE, dtype=float) + 1j * numpy.linspace(0.1, 0.9, H_SIZE))[None, :]
|
||||||
|
Y0_TWO_MODE = numpy.vstack(
|
||||||
|
[
|
||||||
|
numpy.arange(H_SIZE, dtype=float) + 1j * numpy.linspace(0.1, 0.9, H_SIZE),
|
||||||
|
numpy.linspace(2.0, 3.5, H_SIZE) - 0.5j * numpy.arange(H_SIZE, dtype=float),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_overlap_fixture() -> tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray]:
|
def build_overlap_fixture() -> tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray]:
|
||||||
|
|
@ -98,6 +106,187 @@ def test_eigsolve_returns_finite_modes_with_small_residual() -> None:
|
||||||
assert callback_count > 0
|
assert callback_count > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_eigsolve_without_initial_guess_returns_finite_modes() -> None:
|
||||||
|
eigvals, eigvecs = bloch.eigsolve(
|
||||||
|
1,
|
||||||
|
K0,
|
||||||
|
G_MATRIX,
|
||||||
|
EPSILON,
|
||||||
|
tolerance=1e-6,
|
||||||
|
max_iters=20,
|
||||||
|
y0=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
operator = bloch.maxwell_operator(K0, G_MATRIX, EPSILON)
|
||||||
|
eigvec = eigvecs[0] / numpy.linalg.norm(eigvecs[0])
|
||||||
|
residual = numpy.linalg.norm(operator(eigvec).reshape(-1) - eigvals[0] * eigvec) / numpy.linalg.norm(eigvec)
|
||||||
|
|
||||||
|
assert eigvals.shape == (1,)
|
||||||
|
assert eigvecs.shape == (1, H_SIZE)
|
||||||
|
assert numpy.isfinite(eigvals).all()
|
||||||
|
assert numpy.isfinite(eigvecs).all()
|
||||||
|
assert residual < 1e-5
|
||||||
|
|
||||||
|
|
||||||
|
def test_eigsolve_recovers_from_singular_initial_subspace(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
class FakeRng:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.calls = 0
|
||||||
|
|
||||||
|
def random(self, shape: tuple[int, ...]) -> numpy.ndarray:
|
||||||
|
self.calls += 1
|
||||||
|
return numpy.arange(numpy.prod(shape), dtype=float).reshape(shape) + self.calls
|
||||||
|
|
||||||
|
fake_rng = FakeRng()
|
||||||
|
singular_y0 = numpy.vstack([Y0_TWO_MODE[0], Y0_TWO_MODE[0]])
|
||||||
|
monkeypatch.setattr(bloch.numpy.random, 'default_rng', lambda: fake_rng)
|
||||||
|
|
||||||
|
eigvals, eigvecs = bloch.eigsolve(
|
||||||
|
2,
|
||||||
|
K0,
|
||||||
|
G_MATRIX,
|
||||||
|
EPSILON,
|
||||||
|
tolerance=1e-6,
|
||||||
|
max_iters=20,
|
||||||
|
y0=singular_y0,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert fake_rng.calls == 2
|
||||||
|
assert eigvals.shape == (2,)
|
||||||
|
assert eigvecs.shape == (2, H_SIZE)
|
||||||
|
assert numpy.isfinite(eigvals).all()
|
||||||
|
assert numpy.isfinite(eigvecs).all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_eigsolve_reconditions_large_trace_initial_subspace(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
original_inv = bloch.numpy.linalg.inv
|
||||||
|
original_sqrtm = bloch.scipy.linalg.sqrtm
|
||||||
|
sqrtm_calls = 0
|
||||||
|
inv_calls = 0
|
||||||
|
|
||||||
|
def inv_with_large_first_trace(matrix: numpy.ndarray) -> numpy.ndarray:
|
||||||
|
nonlocal inv_calls
|
||||||
|
inv_calls += 1
|
||||||
|
if inv_calls == 1:
|
||||||
|
return numpy.eye(matrix.shape[0], dtype=complex) * 1e9
|
||||||
|
return original_inv(matrix)
|
||||||
|
|
||||||
|
def sqrtm_wrapper(matrix: numpy.ndarray) -> numpy.ndarray:
|
||||||
|
nonlocal sqrtm_calls
|
||||||
|
sqrtm_calls += 1
|
||||||
|
return original_sqrtm(matrix)
|
||||||
|
|
||||||
|
monkeypatch.setattr(bloch.numpy.linalg, 'inv', inv_with_large_first_trace)
|
||||||
|
monkeypatch.setattr(bloch.scipy.linalg, 'sqrtm', sqrtm_wrapper)
|
||||||
|
|
||||||
|
eigvals, eigvecs = bloch.eigsolve(
|
||||||
|
2,
|
||||||
|
K0,
|
||||||
|
G_MATRIX,
|
||||||
|
EPSILON,
|
||||||
|
tolerance=1e-6,
|
||||||
|
max_iters=20,
|
||||||
|
y0=Y0_TWO_MODE,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert sqrtm_calls >= 2
|
||||||
|
assert eigvals.shape == (2,)
|
||||||
|
assert eigvecs.shape == (2, H_SIZE)
|
||||||
|
assert numpy.isfinite(eigvals).all()
|
||||||
|
assert numpy.isfinite(eigvecs).all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_eigsolve_qi_memoization_reuses_cached_theta(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
def fake_minimize_scalar(func, method: str, bounds: tuple[float, float], options: dict[str, float]) -> SimpleNamespace:
|
||||||
|
theta = 0.3
|
||||||
|
first = func(theta)
|
||||||
|
second = func(theta)
|
||||||
|
assert_allclose(second, first)
|
||||||
|
return SimpleNamespace(fun=second, x=theta)
|
||||||
|
|
||||||
|
monkeypatch.setattr(bloch.scipy.optimize, 'minimize_scalar', fake_minimize_scalar)
|
||||||
|
|
||||||
|
eigvals, eigvecs = bloch.eigsolve(
|
||||||
|
1,
|
||||||
|
K0,
|
||||||
|
G_MATRIX,
|
||||||
|
EPSILON,
|
||||||
|
tolerance=1e-6,
|
||||||
|
max_iters=1,
|
||||||
|
y0=Y0,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert eigvals.shape == (1,)
|
||||||
|
assert eigvecs.shape == (1, H_SIZE)
|
||||||
|
assert numpy.isfinite(eigvals).all()
|
||||||
|
assert numpy.isfinite(eigvecs).all()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('theta', [numpy.pi / 2 - 1e-8, 1e-8])
|
||||||
|
def test_eigsolve_qi_taylor_expansions_return_finite_modes(monkeypatch: pytest.MonkeyPatch, theta: float) -> None:
|
||||||
|
original_inv = bloch.numpy.linalg.inv
|
||||||
|
inv_calls = 0
|
||||||
|
|
||||||
|
def inv_raise_once_for_q(matrix: numpy.ndarray) -> numpy.ndarray:
|
||||||
|
nonlocal inv_calls
|
||||||
|
inv_calls += 1
|
||||||
|
if inv_calls == 3:
|
||||||
|
raise numpy.linalg.LinAlgError('forced singular Q')
|
||||||
|
return original_inv(matrix)
|
||||||
|
|
||||||
|
def fake_minimize_scalar(func, method: str, bounds: tuple[float, float], options: dict[str, float]) -> SimpleNamespace:
|
||||||
|
value = func(theta)
|
||||||
|
return SimpleNamespace(fun=value, x=theta)
|
||||||
|
|
||||||
|
monkeypatch.setattr(bloch.numpy.linalg, 'inv', inv_raise_once_for_q)
|
||||||
|
monkeypatch.setattr(bloch.scipy.optimize, 'minimize_scalar', fake_minimize_scalar)
|
||||||
|
|
||||||
|
eigvals, eigvecs = bloch.eigsolve(
|
||||||
|
1,
|
||||||
|
K0,
|
||||||
|
G_MATRIX,
|
||||||
|
EPSILON,
|
||||||
|
tolerance=1e-6,
|
||||||
|
max_iters=1,
|
||||||
|
y0=Y0,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert eigvals.shape == (1,)
|
||||||
|
assert eigvecs.shape == (1, H_SIZE)
|
||||||
|
assert numpy.isfinite(eigvals).all()
|
||||||
|
assert numpy.isfinite(eigvecs).all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_eigsolve_qi_inexplicable_singularity_raises(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
original_inv = bloch.numpy.linalg.inv
|
||||||
|
inv_calls = 0
|
||||||
|
|
||||||
|
def inv_raise_once_for_q(matrix: numpy.ndarray) -> numpy.ndarray:
|
||||||
|
nonlocal inv_calls
|
||||||
|
inv_calls += 1
|
||||||
|
if inv_calls == 3:
|
||||||
|
raise numpy.linalg.LinAlgError('forced singular Q')
|
||||||
|
return original_inv(matrix)
|
||||||
|
|
||||||
|
def fake_minimize_scalar(func, method: str, bounds: tuple[float, float], options: dict[str, float]) -> SimpleNamespace:
|
||||||
|
func(numpy.pi / 4)
|
||||||
|
raise AssertionError('unreachable after trace_func exception')
|
||||||
|
|
||||||
|
monkeypatch.setattr(bloch.numpy.linalg, 'inv', inv_raise_once_for_q)
|
||||||
|
monkeypatch.setattr(bloch.scipy.optimize, 'minimize_scalar', fake_minimize_scalar)
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match='Inexplicable singularity in trace_func'):
|
||||||
|
bloch.eigsolve(
|
||||||
|
1,
|
||||||
|
K0,
|
||||||
|
G_MATRIX,
|
||||||
|
EPSILON,
|
||||||
|
tolerance=1e-6,
|
||||||
|
max_iters=1,
|
||||||
|
y0=Y0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_find_k_returns_vector_frequency_and_callbacks() -> None:
|
def test_find_k_returns_vector_frequency_and_callbacks() -> None:
|
||||||
target_eigvals, _target_eigvecs = bloch.eigsolve(
|
target_eigvals, _target_eigvecs = bloch.eigsolve(
|
||||||
1,
|
1,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import numpy
|
||||||
from numpy.linalg import norm
|
from numpy.linalg import norm
|
||||||
from scipy import sparse
|
from scipy import sparse
|
||||||
import scipy.sparse.linalg as spalg
|
import scipy.sparse.linalg as spalg
|
||||||
|
from numpy.testing import assert_allclose
|
||||||
|
|
||||||
from ..eigensolvers import rayleigh_quotient_iteration, signed_eigensolve
|
from ..eigensolvers import rayleigh_quotient_iteration, signed_eigensolve
|
||||||
|
|
||||||
|
|
@ -45,3 +46,38 @@ def test_signed_eigensolve_negative_returns_largest_negative_mode() -> None:
|
||||||
assert eigvecs.shape == (4, 1)
|
assert eigvecs.shape == (4, 1)
|
||||||
assert abs(eigvals[0] + 2.0) < 1e-12
|
assert abs(eigvals[0] + 2.0) < 1e-12
|
||||||
assert abs(eigvecs[3, 0]) > 0.99
|
assert abs(eigvecs[3, 0]) > 0.99
|
||||||
|
|
||||||
|
|
||||||
|
def test_rayleigh_quotient_iteration_uses_default_linear_operator_solver() -> 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
eigval, eigvec = rayleigh_quotient_iteration(
|
||||||
|
linear_operator,
|
||||||
|
numpy.array([0.0, 1.0, 1e-6, 0.0], dtype=complex),
|
||||||
|
iterations=8,
|
||||||
|
)
|
||||||
|
|
||||||
|
residual = norm(operator @ eigvec - eigval * eigvec)
|
||||||
|
assert abs(eigval - 3.0) < 1e-12
|
||||||
|
assert residual < 1e-12
|
||||||
|
|
||||||
|
|
||||||
|
def test_signed_eigensolve_linear_operator_fallback_returns_dominant_positive_mode() -> 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
eigvals, eigvecs = signed_eigensolve(linear_operator, how_many=1)
|
||||||
|
|
||||||
|
assert eigvals.shape == (1,)
|
||||||
|
assert eigvecs.shape == (4, 1)
|
||||||
|
assert_allclose(eigvals[0], 5.0, atol=1e-12, rtol=1e-12)
|
||||||
|
assert abs(eigvecs[0, 0]) > 0.99
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,15 @@ def test_uniform_grid_scpml_matches_expected_stretch_profile() -> None:
|
||||||
assert numpy.isfinite(dxes[1][0]).all()
|
assert numpy.isfinite(dxes[1][0]).all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_uniform_grid_scpml_default_s_function_matches_explicit_default() -> None:
|
||||||
|
implicit = scpml.uniform_grid_scpml((6, 4, 3), (2, 0, 1), omega=2.0)
|
||||||
|
explicit = scpml.uniform_grid_scpml((6, 4, 3), (2, 0, 1), omega=2.0, s_function=scpml.prepare_s_function())
|
||||||
|
|
||||||
|
for implicit_group, explicit_group in zip(implicit, explicit, strict=True):
|
||||||
|
for implicit_axis, explicit_axis in zip(implicit_group, explicit_group, strict=True):
|
||||||
|
assert_allclose(implicit_axis, explicit_axis)
|
||||||
|
|
||||||
|
|
||||||
def test_stretch_with_scpml_only_modifies_requested_front_edge() -> None:
|
def test_stretch_with_scpml_only_modifies_requested_front_edge() -> None:
|
||||||
s_function = scpml.prepare_s_function(ln_R=-12.0, m=3.0)
|
s_function = scpml.prepare_s_function(ln_R=-12.0, m=3.0)
|
||||||
base = [[numpy.ones(6), numpy.ones(4), numpy.ones(3)] for _ in range(2)]
|
base = [[numpy.ones(6), numpy.ones(4), numpy.ones(3)] for _ in range(2)]
|
||||||
|
|
|
||||||
74
meanas/test/test_fdfd_farfield.py
Normal file
74
meanas/test/test_fdfd_farfield.py
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
import numpy
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ..fdfd import farfield
|
||||||
|
|
||||||
|
|
||||||
|
NEAR_SHAPE = (2, 3)
|
||||||
|
E_NEAR = [numpy.zeros(NEAR_SHAPE, dtype=complex), numpy.zeros(NEAR_SHAPE, dtype=complex)]
|
||||||
|
H_NEAR = [numpy.zeros(NEAR_SHAPE, dtype=complex), numpy.zeros(NEAR_SHAPE, dtype=complex)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_near_to_farfield_rejects_wrong_length_inputs() -> None:
|
||||||
|
with pytest.raises(Exception, match='E_near must be a length-2 list'):
|
||||||
|
farfield.near_to_farfield(E_NEAR[:1], H_NEAR, dx=0.2, dy=0.3)
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match='H_near must be a length-2 list'):
|
||||||
|
farfield.near_to_farfield(E_NEAR, H_NEAR[:1], dx=0.2, dy=0.3)
|
||||||
|
|
||||||
|
|
||||||
|
def test_near_to_farfield_rejects_mismatched_shapes() -> None:
|
||||||
|
bad_h_near = [H_NEAR[0], numpy.zeros((2, 4), dtype=complex)]
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match='All fields must be the same shape'):
|
||||||
|
farfield.near_to_farfield(E_NEAR, bad_h_near, dx=0.2, dy=0.3)
|
||||||
|
|
||||||
|
|
||||||
|
def test_near_to_farfield_uses_default_and_scalar_padding_shapes() -> None:
|
||||||
|
default_result = farfield.near_to_farfield(E_NEAR, H_NEAR, dx=0.2, dy=0.3)
|
||||||
|
scalar_result = farfield.near_to_farfield(E_NEAR, H_NEAR, dx=0.2, dy=0.3, padded_size=8)
|
||||||
|
|
||||||
|
assert default_result['E'][0].shape == (2, 4)
|
||||||
|
assert default_result['H'][0].shape == (2, 4)
|
||||||
|
assert scalar_result['E'][0].shape == (8, 8)
|
||||||
|
assert scalar_result['H'][0].shape == (8, 8)
|
||||||
|
|
||||||
|
|
||||||
|
def test_far_to_nearfield_rejects_wrong_length_inputs() -> None:
|
||||||
|
ff = farfield.near_to_farfield(E_NEAR, H_NEAR, dx=0.2, dy=0.3, padded_size=8)
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match='E_far must be a length-2 list'):
|
||||||
|
farfield.far_to_nearfield(ff['E'][:1], ff['H'], ff['dkx'], ff['dky'])
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match='H_far must be a length-2 list'):
|
||||||
|
farfield.far_to_nearfield(ff['E'], ff['H'][:1], ff['dkx'], ff['dky'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_far_to_nearfield_rejects_mismatched_shapes() -> None:
|
||||||
|
ff = farfield.near_to_farfield(E_NEAR, H_NEAR, dx=0.2, dy=0.3, padded_size=8)
|
||||||
|
bad_h_far = [ff['H'][0], numpy.zeros((8, 4), dtype=complex)]
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match='All fields must be the same shape'):
|
||||||
|
farfield.far_to_nearfield(ff['E'], bad_h_far, ff['dkx'], ff['dky'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_far_to_nearfield_uses_default_and_scalar_padding_shapes() -> None:
|
||||||
|
ff = farfield.near_to_farfield(E_NEAR, H_NEAR, dx=0.2, dy=0.3, padded_size=8)
|
||||||
|
default_result = farfield.far_to_nearfield(
|
||||||
|
[field.copy() for field in ff['E']],
|
||||||
|
[field.copy() for field in ff['H']],
|
||||||
|
ff['dkx'],
|
||||||
|
ff['dky'],
|
||||||
|
)
|
||||||
|
scalar_result = 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert default_result['E'][0].shape == (8, 8)
|
||||||
|
assert default_result['H'][0].shape == (8, 8)
|
||||||
|
assert scalar_result['E'][0].shape == (4, 4)
|
||||||
|
assert scalar_result['H'][0].shape == (4, 4)
|
||||||
44
meanas/test/test_fdtd_base.py
Normal file
44
meanas/test/test_fdtd_base.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import numpy
|
||||||
|
from numpy.testing import assert_allclose
|
||||||
|
|
||||||
|
from ..fdmath import functional as fd_functional
|
||||||
|
from ..fdtd import base
|
||||||
|
|
||||||
|
|
||||||
|
DT = 0.25
|
||||||
|
SHAPE = (3, 2, 2, 2)
|
||||||
|
E_FIELD = numpy.arange(numpy.prod(SHAPE), dtype=float).reshape(SHAPE, order='C') / 5.0
|
||||||
|
H_FIELD = (numpy.arange(numpy.prod(SHAPE), dtype=float).reshape(SHAPE, order='C') + 1.0) / 7.0
|
||||||
|
EPSILON = 1.5 + E_FIELD / 10.0
|
||||||
|
MU_FIELD = 2.0 + H_FIELD / 8.0
|
||||||
|
MU_SCALAR = 3.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_maxwell_e_without_dxes_matches_unit_spacing_update() -> None:
|
||||||
|
updater = base.maxwell_e(dt=DT)
|
||||||
|
expected = E_FIELD + DT * fd_functional.curl_back()(H_FIELD) / EPSILON
|
||||||
|
|
||||||
|
updated = updater(E_FIELD.copy(), H_FIELD.copy(), EPSILON)
|
||||||
|
|
||||||
|
assert_allclose(updated, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_maxwell_h_without_dxes_and_without_mu_matches_unit_spacing_update() -> None:
|
||||||
|
updater = base.maxwell_h(dt=DT)
|
||||||
|
expected = H_FIELD - DT * fd_functional.curl_forward()(E_FIELD)
|
||||||
|
|
||||||
|
updated = updater(E_FIELD.copy(), H_FIELD.copy())
|
||||||
|
|
||||||
|
assert_allclose(updated, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_maxwell_h_without_dxes_accepts_scalar_and_field_mu() -> None:
|
||||||
|
updater = base.maxwell_h(dt=DT)
|
||||||
|
|
||||||
|
updated_scalar = updater(E_FIELD.copy(), H_FIELD.copy(), MU_SCALAR)
|
||||||
|
expected_scalar = H_FIELD - DT * fd_functional.curl_forward()(E_FIELD) / MU_SCALAR
|
||||||
|
assert_allclose(updated_scalar, expected_scalar)
|
||||||
|
|
||||||
|
updated_field = updater(E_FIELD.copy(), H_FIELD.copy(), MU_FIELD)
|
||||||
|
expected_field = H_FIELD - DT * fd_functional.curl_forward()(E_FIELD) / MU_FIELD
|
||||||
|
assert_allclose(updated_field, expected_field)
|
||||||
43
meanas/test/test_import_fallbacks.py
Normal file
43
meanas/test/test_import_fallbacks.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import builtins
|
||||||
|
import importlib
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
import meanas
|
||||||
|
from ..fdfd import bloch
|
||||||
|
|
||||||
|
|
||||||
|
def test_meanas_import_survives_readme_open_failure(monkeypatch) -> None: # type: ignore[no-untyped-def]
|
||||||
|
original_open = pathlib.Path.open
|
||||||
|
|
||||||
|
def failing_open(self: pathlib.Path, *args, **kwargs): # type: ignore[no-untyped-def]
|
||||||
|
if self.name == 'README.md':
|
||||||
|
raise FileNotFoundError('forced README failure')
|
||||||
|
return original_open(self, *args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr(pathlib.Path, 'open', failing_open)
|
||||||
|
reloaded = importlib.reload(meanas)
|
||||||
|
|
||||||
|
assert reloaded.__version__ == '0.10'
|
||||||
|
assert reloaded.__author__ == 'Jan Petykiewicz'
|
||||||
|
assert reloaded.__doc__ is not None
|
||||||
|
|
||||||
|
monkeypatch.undo()
|
||||||
|
importlib.reload(meanas)
|
||||||
|
|
||||||
|
|
||||||
|
def test_bloch_reloads_with_numpy_fft_when_pyfftw_is_unavailable(monkeypatch) -> None: # type: ignore[no-untyped-def]
|
||||||
|
original_import = builtins.__import__
|
||||||
|
|
||||||
|
def fake_import(name: str, globals=None, locals=None, fromlist=(), level: int = 0): # type: ignore[no-untyped-def]
|
||||||
|
if name.startswith('pyfftw'):
|
||||||
|
raise ImportError('forced pyfftw failure')
|
||||||
|
return original_import(name, globals, locals, fromlist, level)
|
||||||
|
|
||||||
|
monkeypatch.setattr(builtins, '__import__', fake_import)
|
||||||
|
reloaded = importlib.reload(bloch)
|
||||||
|
|
||||||
|
assert reloaded.fftn.__module__ == 'numpy.fft'
|
||||||
|
assert reloaded.ifftn.__module__ == 'numpy.fft'
|
||||||
|
|
||||||
|
monkeypatch.undo()
|
||||||
|
importlib.reload(bloch)
|
||||||
|
|
@ -162,6 +162,21 @@ def test_waveguide_3d_compute_overlap_e_rejects_empty_overlap_window(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_waveguide_3d_compute_overlap_e_rejects_zero_support_window() -> None:
|
||||||
|
_epsilon, dxes, slices, result = build_waveguide_3d_mode(slice_start=2, polarity=1)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match='no overlap field support'):
|
||||||
|
waveguide_3d.compute_overlap_e(
|
||||||
|
E=numpy.zeros_like(result['E']),
|
||||||
|
wavenumber=result['wavenumber'],
|
||||||
|
dxes=dxes,
|
||||||
|
axis=0,
|
||||||
|
polarity=1,
|
||||||
|
slices=slices,
|
||||||
|
omega=OMEGA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_waveguide_cyl_solved_modes_are_ordered_and_low_residual() -> None:
|
def test_waveguide_cyl_solved_modes_are_ordered_and_low_residual() -> None:
|
||||||
dxes, epsilon, rmin = build_waveguide_cyl_fixture()
|
dxes, epsilon, rmin = build_waveguide_cyl_fixture()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue