613 lines
21 KiB
Python
613 lines
21 KiB
Python
import contextlib
|
|
import io
|
|
import numpy
|
|
from numpy.linalg import norm
|
|
from scipy import linalg
|
|
import pytest
|
|
import warnings
|
|
|
|
from ..fdmath import vec, unvec
|
|
from ..fdmath.operators import deriv_forward, deriv_back
|
|
from ..fdmath.functional import curl_forward, curl_back
|
|
from ..fdfd import waveguide_2d, waveguide_3d, waveguide_cyl
|
|
|
|
|
|
OMEGA = 1 / 1500
|
|
|
|
|
|
@pytest.mark.parametrize('axis', [0, 1, 2])
|
|
@pytest.mark.parametrize('polarity', [-1, 1])
|
|
def test_embedded_hybrid_mode_obeys_both_maxwell_curls(axis, polarity) -> None:
|
|
omega = 0.7
|
|
# Cyclic rotation preserves the orientation of the vector cross product.
|
|
permutation = numpy.roll(numpy.arange(3), axis + 1)
|
|
rng = numpy.random.default_rng(4)
|
|
canonical = numpy.repeat((1 + rng.uniform(0, 3, (3, 4, 3)))[..., None], 9, axis=3)
|
|
epsilon = canonical[permutation].transpose(0, *(permutation + 1))
|
|
shape = epsilon.shape[1:]
|
|
dxes = [[numpy.full(n, 0.4) for n in shape] for _ in range(2)]
|
|
cross_dxes = [[numpy.full(4, 0.4), numpy.full(3, 0.4)] for _ in range(2)]
|
|
values, vectors = linalg.eig(waveguide_2d.operator_e(omega, cross_dxes, vec(canonical[:, :, :, 0])).toarray())
|
|
selected = numpy.argmax(values.real)
|
|
slices = [slice(None)] * 3
|
|
slices[axis] = slice(4, 5)
|
|
mode = waveguide_3d.mode_from_2d(
|
|
vectors[:, selected], numpy.sqrt(values[selected]), omega=omega, dxes=dxes,
|
|
axis=axis, polarity=polarity, slices=slices, epsilon=epsilon,
|
|
)
|
|
e, h = [waveguide_3d.expand_e(mode[field], mode['wavenumber'], dxes, axis, polarity, slices) for field in ('E', 'H')]
|
|
assert norm(e[axis]) > 0.01 * norm(e)
|
|
assert norm(h[axis]) > 0.01 * norm(h)
|
|
region = [slice(None)] * 4
|
|
region[axis + 1] = slice(1, -1)
|
|
ce, ch = curl_forward(dxes[0])(e), curl_back(dxes[1])(h)
|
|
assert relative_residual(ce[tuple(region)], (-1j * omega * h)[tuple(region)]) < 1e-11
|
|
assert relative_residual(ch[tuple(region)], (1j * omega * epsilon * e)[tuple(region)]) < 1e-11
|
|
|
|
|
|
def build_waveguide_3d_mode(
|
|
*,
|
|
slice_start: int,
|
|
polarity: int,
|
|
axis: int = 0,
|
|
shape: tuple[int, int, int] = (5, 5, 1),
|
|
dxes: list[list[numpy.ndarray]] | None = None,
|
|
) -> tuple[numpy.ndarray, list[list[numpy.ndarray]], tuple[slice, slice, slice], waveguide_3d.Waveguide3DMode]:
|
|
epsilon = numpy.ones((3, *shape), dtype=float)
|
|
if dxes is None:
|
|
dxes = [[numpy.ones(size) for size in shape] for _ in range(2)]
|
|
slices_l = [slice(None), slice(None), slice(None)]
|
|
slices_l[axis] = slice(slice_start, slice_start + 1)
|
|
slices = tuple(slices_l)
|
|
result = waveguide_3d.solve_mode(
|
|
0,
|
|
omega=OMEGA,
|
|
dxes=dxes,
|
|
axis=axis,
|
|
polarity=polarity,
|
|
slices=slices,
|
|
epsilon=epsilon,
|
|
)
|
|
return epsilon, dxes, slices, result
|
|
|
|
|
|
def build_waveguide_cyl_fixture(
|
|
*,
|
|
nonuniform: bool = False,
|
|
asymmetric: bool = False,
|
|
) -> tuple[list[list[numpy.ndarray]], numpy.ndarray, float]:
|
|
if nonuniform:
|
|
dxes = [
|
|
[numpy.array([1.0, 1.5, 1.2, 0.8, 1.1]), numpy.ones(5)],
|
|
[numpy.array([0.9, 1.4, 1.0, 0.7, 1.2]), numpy.ones(5)],
|
|
]
|
|
else:
|
|
dxes = [[numpy.ones(5), numpy.ones(5)] for _ in range(2)]
|
|
epsilon_3d = numpy.ones((3, 5, 5), dtype=float)
|
|
if asymmetric:
|
|
epsilon_3d[:, 2, 1] = 2.0
|
|
epsilon = vec(epsilon_3d)
|
|
return dxes, epsilon, 10.0
|
|
|
|
|
|
def build_waveguide_cyl_mu_profile() -> numpy.ndarray:
|
|
return numpy.linspace(1.5, 2.2, 3 * 5 * 5)
|
|
|
|
|
|
def relative_residual(lhs: numpy.ndarray, rhs: numpy.ndarray) -> float:
|
|
return float(norm(lhs - rhs) / (norm(lhs) + norm(rhs)))
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
('axis', 'shape'),
|
|
[
|
|
(0, (5, 5, 1)),
|
|
(1, (5, 5, 1)),
|
|
(2, (5, 5, 5)),
|
|
],
|
|
)
|
|
def test_waveguide_3d_solve_mode_and_expand_e_are_phase_consistent(
|
|
axis: int,
|
|
shape: tuple[int, int, int],
|
|
) -> None:
|
|
polarity = 1
|
|
dxes = [
|
|
[
|
|
numpy.linspace(0.9 + 0.1 * eh + 0.05 * aa, 1.2 + 0.1 * eh + 0.05 * aa, size)
|
|
for aa, size in enumerate(shape)
|
|
]
|
|
for eh in range(2)
|
|
]
|
|
epsilon, dxes, slices, result = build_waveguide_3d_mode(
|
|
slice_start=2,
|
|
polarity=polarity,
|
|
axis=axis,
|
|
shape=shape,
|
|
dxes=dxes,
|
|
)
|
|
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[axis][slices[axis]] 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, *indices = numpy.unravel_index(
|
|
numpy.abs(result['E']).argmax(),
|
|
result['E'].shape,
|
|
)
|
|
line_selection: list[int | slice] = list(indices)
|
|
line_selection[axis] = slice(None)
|
|
values = expanded[component][tuple(line_selection)]
|
|
ratios = values[1:] / values[:-1]
|
|
intervals = numpy.real(dxes[1][axis][1:] if component == axis else dxes[0][axis][:-1])
|
|
expected_ratios = numpy.exp(-1j * result['wavenumber'] * intervals)
|
|
|
|
numpy.testing.assert_allclose(ratios, expected_ratios, rtol=1e-6, atol=1e-9)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
('polarity', 'expected_range'),
|
|
[(1, (0, 1)), (-1, (3, 4))],
|
|
)
|
|
def test_waveguide_3d_compute_overlap_e_uses_adjacent_window(
|
|
polarity: int,
|
|
expected_range: tuple[int, int],
|
|
) -> None:
|
|
_epsilon, dxes, slices, result = build_waveguide_3d_mode(slice_start=2, polarity=polarity)
|
|
|
|
with warnings.catch_warnings(record=True) as caught:
|
|
overlap = waveguide_3d.compute_overlap_e(
|
|
E=result['E'],
|
|
wavenumber=result['wavenumber'],
|
|
dxes=dxes,
|
|
axis=0,
|
|
polarity=polarity,
|
|
slices=slices,
|
|
)
|
|
|
|
nonzero = numpy.argwhere(numpy.abs(overlap) > 0)
|
|
|
|
assert not caught
|
|
assert numpy.isfinite(overlap).all()
|
|
assert nonzero[:, 1].min() == expected_range[0]
|
|
assert nonzero[:, 1].max() == expected_range[1]
|
|
|
|
expanded = waveguide_3d.expand_e(
|
|
E=result['E'],
|
|
wavenumber=result['wavenumber'],
|
|
dxes=dxes,
|
|
axis=0,
|
|
polarity=polarity,
|
|
slices=slices,
|
|
)
|
|
numpy.testing.assert_allclose(numpy.sum(overlap * expanded.conj()), 1.0, atol=1e-12)
|
|
numpy.testing.assert_allclose(vec(expanded) @ vec(overlap).conj(), 1.0, atol=1e-12)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
('polarity', 'slice_start', 'expected_index'),
|
|
[(1, 1, 0), (-1, 3, 4)],
|
|
)
|
|
def test_waveguide_3d_compute_overlap_e_warns_when_window_is_clipped(
|
|
polarity: int,
|
|
slice_start: int,
|
|
expected_index: int,
|
|
) -> None:
|
|
_epsilon, dxes, slices, result = build_waveguide_3d_mode(slice_start=slice_start, polarity=polarity)
|
|
|
|
with pytest.warns(RuntimeWarning, match='clipped'):
|
|
overlap = waveguide_3d.compute_overlap_e(
|
|
E=result['E'],
|
|
wavenumber=result['wavenumber'],
|
|
dxes=dxes,
|
|
axis=0,
|
|
polarity=polarity,
|
|
slices=slices,
|
|
)
|
|
|
|
nonzero = numpy.argwhere(numpy.abs(overlap) > 0)
|
|
|
|
assert numpy.isfinite(overlap).all()
|
|
assert nonzero[:, 1].min() == expected_index
|
|
assert nonzero[:, 1].max() == expected_index
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
('polarity', 'slice_start'),
|
|
[(1, 0), (-1, 4)],
|
|
)
|
|
def test_waveguide_3d_compute_overlap_e_rejects_empty_overlap_window(
|
|
polarity: int,
|
|
slice_start: int,
|
|
) -> None:
|
|
_epsilon, dxes, slices, result = build_waveguide_3d_mode(slice_start=slice_start, polarity=polarity)
|
|
|
|
with pytest.raises(ValueError, match='outside the domain'):
|
|
waveguide_3d.compute_overlap_e(
|
|
E=result['E'],
|
|
wavenumber=result['wavenumber'],
|
|
dxes=dxes,
|
|
axis=0,
|
|
polarity=polarity,
|
|
slices=slices,
|
|
)
|
|
|
|
|
|
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,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize('use_mu', [False, True])
|
|
def test_waveguide_cyl_solved_modes_are_ordered_and_low_residual(use_mu: bool) -> None:
|
|
dxes, epsilon, rmin = build_waveguide_cyl_fixture(asymmetric=use_mu)
|
|
mu = build_waveguide_cyl_mu_profile() if use_mu else None
|
|
|
|
e_xys, angular_wavenumbers = waveguide_cyl.solve_modes(
|
|
[0, 1],
|
|
omega=OMEGA,
|
|
dxes=dxes,
|
|
epsilon=epsilon,
|
|
rmin=rmin,
|
|
mu=mu,
|
|
)
|
|
operator = waveguide_cyl.cylindrical_operator(OMEGA, dxes, epsilon, rmin=rmin, mu=mu)
|
|
|
|
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:
|
|
dxes, epsilon, rmin = build_waveguide_cyl_fixture()
|
|
|
|
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=rmin,
|
|
)
|
|
|
|
assert numpy.isfinite(linear_wavenumbers).all()
|
|
assert numpy.all(numpy.real(linear_wavenumbers) > 0)
|
|
|
|
|
|
def test_waveguide_cyl_dxes2t_matches_expected_radius_scaling() -> None:
|
|
dxes, _epsilon, rmin = build_waveguide_cyl_fixture(nonuniform=True)
|
|
Ta, Tb = waveguide_cyl.dxes2T(dxes, rmin)
|
|
|
|
ta = (rmin + numpy.cumsum(dxes[0][0])) / rmin
|
|
tb = (
|
|
rmin
|
|
+ dxes[0][0] / 2
|
|
+ numpy.concatenate((numpy.zeros(1), numpy.cumsum(dxes[1][0][:-1])))
|
|
) / rmin
|
|
|
|
numpy.testing.assert_allclose(Ta.diagonal(), numpy.repeat(ta, dxes[0][1].size))
|
|
numpy.testing.assert_allclose(Tb.diagonal(), numpy.repeat(tb, dxes[1][1].size))
|
|
|
|
|
|
@pytest.mark.parametrize('use_mu', [False, True])
|
|
def test_waveguide_cyl_operator_matches_2d_limit(use_mu: bool) -> None:
|
|
dxes, epsilon, _rmin = build_waveguide_cyl_fixture(asymmetric=True)
|
|
mu = build_waveguide_cyl_mu_profile() if use_mu else None
|
|
rmin = 1e15
|
|
|
|
cyl_operator = waveguide_cyl.cylindrical_operator(OMEGA, dxes, epsilon, rmin=rmin, mu=mu)
|
|
straight_operator = waveguide_2d.operator_e(OMEGA, dxes, epsilon, mu=mu)
|
|
|
|
numpy.testing.assert_allclose(
|
|
cyl_operator.toarray(),
|
|
straight_operator.toarray(),
|
|
rtol=1e-9,
|
|
atol=1e-10,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize('use_mu', [False, True])
|
|
def test_waveguide_cyl_reconstruction_matches_2d_limit(use_mu: bool) -> None:
|
|
dxes, epsilon, _rmin = build_waveguide_cyl_fixture(asymmetric=True)
|
|
mu = build_waveguide_cyl_mu_profile() if use_mu else None
|
|
rmin = 1e15
|
|
e_xy, wavenumber = waveguide_2d.solve_mode(
|
|
0,
|
|
omega=OMEGA,
|
|
dxes=dxes,
|
|
epsilon=epsilon,
|
|
mu=mu,
|
|
)
|
|
angular_wavenumber = wavenumber * rmin
|
|
|
|
cyl_e = waveguide_cyl.exy2e(
|
|
angular_wavenumber=angular_wavenumber,
|
|
dxes=dxes,
|
|
rmin=rmin,
|
|
epsilon=epsilon,
|
|
) @ e_xy
|
|
straight_e = waveguide_2d.exy2e(
|
|
wavenumber=wavenumber,
|
|
dxes=dxes,
|
|
epsilon=epsilon,
|
|
) @ e_xy
|
|
cyl_h = waveguide_cyl.e2h(
|
|
angular_wavenumber=angular_wavenumber,
|
|
omega=OMEGA,
|
|
dxes=dxes,
|
|
rmin=rmin,
|
|
mu=mu,
|
|
) @ cyl_e
|
|
straight_h = waveguide_2d.e2h(
|
|
wavenumber=wavenumber,
|
|
omega=OMEGA,
|
|
dxes=dxes,
|
|
mu=mu,
|
|
) @ straight_e
|
|
|
|
numpy.testing.assert_allclose(cyl_e, straight_e, rtol=1e-8, atol=1e-8)
|
|
numpy.testing.assert_allclose(cyl_h, straight_h, rtol=1e-8, atol=1e-8)
|
|
|
|
|
|
@pytest.mark.parametrize('use_mu', [False, True])
|
|
def test_waveguide_cyl_reconstruction_is_finite_radius_consistent(use_mu: bool) -> None:
|
|
dxes, epsilon, rmin = build_waveguide_cyl_fixture(nonuniform=True, asymmetric=True)
|
|
mu = build_waveguide_cyl_mu_profile() if use_mu else None
|
|
mu_full = numpy.ones_like(epsilon) if mu is None else mu
|
|
|
|
e_xy, angular_wavenumber = waveguide_cyl.solve_mode(
|
|
0,
|
|
omega=OMEGA,
|
|
dxes=dxes,
|
|
epsilon=epsilon,
|
|
rmin=rmin,
|
|
mu=mu,
|
|
)
|
|
e_field = waveguide_cyl.exy2e(
|
|
angular_wavenumber=angular_wavenumber,
|
|
dxes=dxes,
|
|
rmin=rmin,
|
|
epsilon=epsilon,
|
|
) @ e_xy
|
|
h_field = waveguide_cyl.exy2h(
|
|
angular_wavenumber=angular_wavenumber,
|
|
omega=OMEGA,
|
|
dxes=dxes,
|
|
rmin=rmin,
|
|
epsilon=epsilon,
|
|
mu=mu,
|
|
) @ e_xy
|
|
|
|
Dfx, Dfy = deriv_forward(dxes[0])
|
|
Dbx, Dby = deriv_back(dxes[1])
|
|
Ta, Tb = waveguide_cyl.dxes2T(dxes, rmin)
|
|
ta = Ta.diagonal()
|
|
tb = Tb.diagonal()
|
|
beta = angular_wavenumber / rmin
|
|
|
|
er, ey, ez = numpy.split(e_field, 3)
|
|
hr, hy, hz = numpy.split(h_field, 3)
|
|
eps_r, eps_y, eps_z = numpy.split(epsilon, 3)
|
|
mu_r, mu_y, mu_z = numpy.split(mu_full, 3)
|
|
|
|
ez_rhs = (
|
|
(Dbx @ (tb * eps_r * er))
|
|
+ (Dby @ (ta * eps_y * ey))
|
|
) / eps_z
|
|
hr_rhs = 1j * beta * ey / ta + Dfy @ ez
|
|
hy_rhs = -1j * beta * er / tb - (Dfx @ (ta * ez)) / tb
|
|
hz_rhs = -Dfy @ er + Dfx @ ey
|
|
|
|
assert relative_residual(1j * beta * ez, ez_rhs) < 1e-7
|
|
assert relative_residual(-1j * OMEGA * mu_r * hr, hr_rhs) < 1e-7
|
|
assert relative_residual(-1j * OMEGA * mu_y * hy, hy_rhs) < 1e-7
|
|
assert relative_residual(-1j * OMEGA * mu_z * hz, hz_rhs) < 1e-7
|
|
|
|
|
|
def test_waveguide_cyl_linear_wavenumbers_use_component_radii() -> None:
|
|
dxes, epsilon, rmin = build_waveguide_cyl_fixture(nonuniform=True)
|
|
nx = dxes[0][0].size
|
|
ny = dxes[0][1].size
|
|
angular_wavenumber = numpy.array([2.0])
|
|
|
|
ra = rmin + numpy.cumsum(dxes[0][0])
|
|
rb = (
|
|
rmin
|
|
+ dxes[0][0] / 2
|
|
+ numpy.concatenate((numpy.zeros(1), numpy.cumsum(dxes[1][0][:-1])))
|
|
)
|
|
|
|
er_only = numpy.zeros((1, 2 * nx * ny), dtype=complex)
|
|
er_only[0] = vec(numpy.array([numpy.ones((nx, ny)), numpy.zeros((nx, ny))]))
|
|
ey_only = numpy.zeros_like(er_only)
|
|
ey_only[0] = vec(numpy.array([numpy.zeros((nx, ny)), numpy.ones((nx, ny))]))
|
|
|
|
er_linear = waveguide_cyl.linear_wavenumbers(
|
|
er_only,
|
|
angular_wavenumber,
|
|
epsilon=epsilon,
|
|
dxes=dxes,
|
|
rmin=rmin,
|
|
)
|
|
ey_linear = waveguide_cyl.linear_wavenumbers(
|
|
ey_only,
|
|
angular_wavenumber,
|
|
epsilon=epsilon,
|
|
dxes=dxes,
|
|
rmin=rmin,
|
|
)
|
|
|
|
numpy.testing.assert_allclose(er_linear[0], angular_wavenumber[0] / numpy.average(rb, weights=dxes[0][0]))
|
|
numpy.testing.assert_allclose(ey_linear[0], angular_wavenumber[0] / numpy.average(ra, weights=dxes[1][0]))
|
|
|
|
|
|
@pytest.mark.parametrize('use_mu', [False, True])
|
|
def test_cylindrical_modes_obey_the_full_ampere_curl(use_mu) -> None:
|
|
shape, omega, rmin = (4, 3), 0.7, 2.0
|
|
dxes = [[numpy.full(n, 0.4) for n in shape] for _ in range(2)]
|
|
rng = numpy.random.default_rng(4)
|
|
epsilon = vec(1 + rng.uniform(0, 3, (3, *shape)))
|
|
mu = vec(1 + rng.uniform(0, 2, (3, *shape))) if use_mu else numpy.ones_like(epsilon)
|
|
matrix = waveguide_cyl.cylindrical_operator(omega, dxes, epsilon, rmin, mu).toarray()
|
|
values, vectors = linalg.eig(matrix)
|
|
selected = numpy.argmax(values.real)
|
|
beta = numpy.sqrt(values[selected])
|
|
electric = waveguide_cyl.exy2e(beta * rmin, dxes, rmin, epsilon) @ vectors[:, selected]
|
|
magnetic = waveguide_cyl.e2h(beta * rmin, omega, dxes, rmin, mu) @ electric
|
|
ta, tb = (metric.diagonal() for metric in waveguide_cyl.dxes2T(dxes, rmin))
|
|
dbx, dby = deriv_back(dxes[1])
|
|
hr, hy, hz = numpy.split(magnetic, 3)
|
|
# Physical cylindrical Ampere curl, independently of electric reconstruction.
|
|
curl_h = numpy.concatenate((
|
|
1j * beta * hy / tb + dby @ hz,
|
|
-1j * beta * hr / ta - (dbx @ (tb * hz)) / ta,
|
|
-dby @ hr + dbx @ hy,
|
|
))
|
|
numpy.testing.assert_allclose(curl_h, 1j * omega * epsilon * electric, atol=1e-11, rtol=1e-10)
|
|
|
|
|
|
def test_cylindrical_mean_radius_is_invariant_under_constant_density_refinement() -> None:
|
|
for widths in ([1.0, 3.0], [1.0, 1.0, 1.0, 1.0]):
|
|
n = len(widths)
|
|
dxes = [[numpy.array(widths), numpy.ones(1)] for _ in range(2)]
|
|
electric_xy = numpy.concatenate((numpy.ones(n), numpy.zeros(n)))[None, :].astype(complex)
|
|
beta = waveguide_cyl.linear_wavenumbers(electric_xy, [12.0], numpy.ones(3 * n), dxes, rmin=10.0)
|
|
numpy.testing.assert_allclose(beta, 1.0)
|
|
|
|
|
|
@pytest.mark.parametrize('axis', [0, 1, 2])
|
|
@pytest.mark.parametrize('polarity', [-1, 1])
|
|
def test_graded_expansion_uses_each_components_actual_yee_intervals(axis, polarity) -> None:
|
|
shape = [1, 1, 1]
|
|
shape[axis] = 4
|
|
dxes = [[numpy.ones(n) for n in shape] for _ in range(2)]
|
|
dxes[0][axis] = numpy.array([1.0, 2.0, 3.0, 4.0])
|
|
dxes[1][axis] = numpy.array([0.6, 0.8, 1.0, 1.2])
|
|
source = [slice(None)] * 3
|
|
source[axis] = slice(1, 2)
|
|
field = numpy.zeros((3, *shape), dtype=complex)
|
|
field[(slice(None), *source)] = 1 + 0.5j
|
|
expanded = waveguide_3d.expand_e(field, 0.2, dxes, axis, polarity, source)
|
|
for component in range(3):
|
|
line = expanded[component].ravel()
|
|
intervals = dxes[1][axis][1:] if component == axis else dxes[0][axis][:-1]
|
|
numpy.testing.assert_allclose(line[1:] / line[:-1], numpy.exp(-1j * polarity * 0.2 * intervals))
|
|
numpy.testing.assert_array_equal(expanded[(slice(None), *source)], field[(slice(None), *source)])
|
|
|
|
|
|
@pytest.mark.parametrize('beta', [2.0, 3.0])
|
|
def test_grid_band_edge_rejects_power_normalization_and_selects_decay(beta) -> None:
|
|
shape = (1, 1, 5)
|
|
dxes = [[numpy.ones(n) for n in shape] for _ in range(2)]
|
|
kwargs = dict(omega=1.0, dxes=dxes, axis=2, polarity=1,
|
|
slices=(slice(None), slice(None), slice(0, 1)), epsilon=numpy.full((3, *shape), beta ** 2))
|
|
with pytest.raises(ValueError, match='grid band edge'):
|
|
waveguide_3d.mode_from_2d(numpy.array([1, 0], dtype=complex), beta, **kwargs)
|
|
mode = waveguide_3d.mode_from_2d(numpy.array([1, 0], dtype=complex), beta, normalization='lorentz', **kwargs)
|
|
assert mode['wavenumber'].imag <= 0
|
|
expanded = waveguide_3d.expand_e(mode['E'], mode['wavenumber'], dxes, 2, 1, kwargs['slices'])
|
|
ratio = abs(expanded[0, 0, 0, 1] / expanded[0, 0, 0, 0])
|
|
assert ratio <= 1 + 1e-14
|
|
if beta > 2:
|
|
assert ratio < 1
|
|
|
|
|
|
def test_waveguide_cyl_exy2e_and_exy2h_return_finite_full_fields() -> None:
|
|
dxes, epsilon, rmin = build_waveguide_cyl_fixture()
|
|
mu = build_waveguide_cyl_mu_profile()
|
|
e_xy, angular_wavenumber = waveguide_cyl.solve_mode(
|
|
0,
|
|
omega=OMEGA,
|
|
dxes=dxes,
|
|
epsilon=epsilon,
|
|
rmin=rmin,
|
|
mu=mu,
|
|
)
|
|
|
|
e_field = waveguide_cyl.exy2e(
|
|
angular_wavenumber=angular_wavenumber,
|
|
dxes=dxes,
|
|
rmin=rmin,
|
|
epsilon=epsilon,
|
|
) @ e_xy
|
|
h_field = waveguide_cyl.exy2h(
|
|
angular_wavenumber=angular_wavenumber,
|
|
omega=OMEGA,
|
|
dxes=dxes,
|
|
rmin=rmin,
|
|
epsilon=epsilon,
|
|
mu=mu,
|
|
) @ e_xy
|
|
|
|
assert e_field.shape == (3 * 25,)
|
|
assert h_field.shape == (3 * 25,)
|
|
assert numpy.isfinite(e_field).all()
|
|
assert numpy.isfinite(h_field).all()
|
|
assert unvec(e_field, (5, 5)).shape == (3, 5, 5)
|
|
assert unvec(h_field, (5, 5)).shape == (3, 5, 5)
|
|
|
|
|
|
@pytest.mark.parametrize('use_mu', [False, True])
|
|
def test_waveguide_cyl_normalized_fields_are_unit_norm_and_silent(use_mu: bool) -> None:
|
|
dxes, epsilon, rmin = build_waveguide_cyl_fixture()
|
|
mu = build_waveguide_cyl_mu_profile() if use_mu else None
|
|
e_xy, angular_wavenumber = waveguide_cyl.solve_mode(
|
|
0,
|
|
omega=OMEGA,
|
|
dxes=dxes,
|
|
epsilon=epsilon,
|
|
rmin=rmin,
|
|
mu=mu,
|
|
)
|
|
|
|
output = io.StringIO()
|
|
with contextlib.redirect_stdout(output):
|
|
e_field, h_field = waveguide_cyl.normalized_fields_e(
|
|
e_xy,
|
|
angular_wavenumber=angular_wavenumber,
|
|
omega=OMEGA,
|
|
dxes=dxes,
|
|
rmin=rmin,
|
|
epsilon=epsilon,
|
|
mu=mu,
|
|
)
|
|
|
|
overlap = waveguide_2d.inner_product(e_field, h_field, dxes, conj_h=True)
|
|
|
|
assert output.getvalue() == ''
|
|
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
|