From 1a2c6ab524c87ae93ebe665c93c1bde635d1703d Mon Sep 17 00:00:00 2001 From: Forgejo Actions Date: Tue, 21 Apr 2026 19:40:32 -0700 Subject: [PATCH 1/9] [EME] add more docs and tests --- meanas/fdfd/eme.py | 98 +++++++++++++++++++++++++++++ meanas/test/test_eme_numerics.py | 104 ++++++++++++++++++++++++++++++- 2 files changed, 201 insertions(+), 1 deletion(-) diff --git a/meanas/fdfd/eme.py b/meanas/fdfd/eme.py index 5165ef1..366de8e 100644 --- a/meanas/fdfd/eme.py +++ b/meanas/fdfd/eme.py @@ -1,3 +1,24 @@ +""" +Low-level mode-matching helpers for waveguide / EME workflows. + +These helpers operate on already-solved and already-normalized port fields. +They do not build geometries or solve modes themselves; downstream users are +expected to supply compatible `(E, H)` modal field pairs from +`waveguide_2d`, `waveguide_3d`, or `waveguide_cyl`. + +The returned matrices follow the usual port ordering: + +- `get_tr(...)` returns `(T, R)` for left-incident modes. +- `get_abcd(...)` returns the 2-port block transfer matrix built from the two + directional `T/R` solves. +- `get_s(...)` returns the full block scattering matrix + `[[R12, T12], [T21, R21]]`. + +This module is intentionally a thin library layer rather than an integrated +simulation suite. It provides the overlap algebra that downstream users can +compose into larger workflows. +""" + from collections.abc import Sequence import numpy from numpy.typing import NDArray @@ -7,6 +28,37 @@ from ..fdmath import dx_lists2_t, vcfdfield2 from .waveguide_2d import inner_product +def _validate_port_modes( + name: str, + ehs: Sequence[Sequence[vcfdfield2]], + wavenumbers: Sequence[complex], + ) -> tuple[tuple[int, ...], tuple[int, ...]]: + if len(ehs) != len(wavenumbers): + raise ValueError(f'{name} mode list and wavenumber list must have the same length') + if not ehs: + raise ValueError(f'{name} must contain at least one mode') + + e_shape: tuple[int, ...] | None = None + h_shape: tuple[int, ...] | None = None + for index, mode in enumerate(ehs): + if len(mode) != 2: + raise ValueError(f'{name}[{index}] must be a 2-tuple of (E, H) modal fields') + e_field, h_field = mode + mode_e_shape = numpy.shape(e_field) + mode_h_shape = numpy.shape(h_field) + if mode_e_shape != mode_h_shape: + raise ValueError(f'{name}[{index}] has mismatched E/H field shapes') + if e_shape is None: + e_shape = mode_e_shape + h_shape = mode_h_shape + elif mode_e_shape != e_shape or mode_h_shape != h_shape: + raise ValueError(f'{name} modal fields must all share the same shape') + + assert e_shape is not None + assert h_shape is not None + return e_shape, h_shape + + def get_tr( ehLs: Sequence[Sequence[vcfdfield2]], wavenumbers_L: Sequence[complex], @@ -14,6 +66,29 @@ def get_tr( wavenumbers_R: Sequence[complex], dxes: dx_lists2_t, ) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]: + """ + Compute left-incident transmission and reflection matrices. + + Args: + ehLs: Left-port modes as `(E, H)` field pairs. + wavenumbers_L: Propagation constants for `ehLs`. + ehRs: Right-port modes as `(E, H)` field pairs. + wavenumbers_R: Propagation constants for `ehRs`. + dxes: Two-dimensional Yee-cell edge lengths for the shared port plane. + + Returns: + `(T12, R12)` where columns index left-incident modes and rows index + outgoing right-going / left-going modes respectively. + + Raises: + ValueError: If the port mode lists are empty, malformed, or defined on + incompatible field shapes. + """ + left_e_shape, left_h_shape = _validate_port_modes('ehLs', ehLs, wavenumbers_L) + right_e_shape, right_h_shape = _validate_port_modes('ehRs', ehRs, wavenumbers_R) + if left_e_shape != right_e_shape or left_h_shape != right_h_shape: + raise ValueError('left and right modal fields must share the same E/H shapes') + nL = len(wavenumbers_L) nR = len(wavenumbers_R) A12 = numpy.zeros((nL, nR), dtype=complex) @@ -48,6 +123,16 @@ def get_abcd( wavenumbers_R: Sequence[complex], **kwargs, ) -> sparse.sparray: + """ + Build the 2-port block transfer matrix for an interface. + + The blocks are assembled from the forward and reverse `get_tr(...)` + solutions using the standard + + `[[A, B], [C, D]] = [[T12 - R21 T21^-1 R12, R21 T21^-1], [-T21^-1 R12, T21^-1]]` + + convention. + """ t12, r12 = get_tr(ehLs, wavenumbers_L, ehRs, wavenumbers_R, **kwargs) t21, r21 = get_tr(ehRs, wavenumbers_R, ehLs, wavenumbers_L, **kwargs) t21i = numpy.linalg.pinv(t21) @@ -73,6 +158,19 @@ def get_s( force_reciprocal: bool = False, **kwargs, ) -> NDArray[numpy.complex128]: + """ + Build the full block scattering matrix for a two-sided interface. + + The returned matrix is ordered as `[[R12, T12], [T21, R21]]`, where the + first block-row/column corresponds to the left port and the second to the + right port. + + Args: + force_nogain: If `True`, clamp singular values of the assembled + scattering matrix to at most one. + force_reciprocal: If `True`, symmetrize the assembled matrix as + `0.5 * (S + S.T)`. + """ t12, r12 = get_tr(ehLs, wavenumbers_L, ehRs, wavenumbers_R, **kwargs) t21, r21 = get_tr(ehRs, wavenumbers_R, ehLs, wavenumbers_L, **kwargs) diff --git a/meanas/test/test_eme_numerics.py b/meanas/test/test_eme_numerics.py index 8798e0d..7486128 100644 --- a/meanas/test/test_eme_numerics.py +++ b/meanas/test/test_eme_numerics.py @@ -1,8 +1,9 @@ import numpy +import pytest from scipy import sparse from ..fdmath import vec -from ..fdfd import eme +from ..fdfd import eme, waveguide_2d, waveguide_cyl from ._test_builders import complex_ramp, unit_dxes from .utils import assert_close @@ -11,6 +12,8 @@ SHAPE = (3, 2, 2) DXES = unit_dxes((2, 2)) WAVENUMBERS_L = numpy.array([1.0, 0.8]) WAVENUMBERS_R = numpy.array([0.9, 0.7]) +OMEGA = 1 / 1500 +REAL_DXES = unit_dxes((5, 5)) def _mode(scale: float) -> tuple[numpy.ndarray, numpy.ndarray]: @@ -130,3 +133,102 @@ def test_get_s_force_nogain_and_reciprocal_returns_finite_output(monkeypatch) -> assert numpy.isfinite(ss).all() assert_close(ss, ss.T) assert (numpy.linalg.svd(ss, compute_uv=False) <= 1.0 + 1e-12).all() + + +def test_get_tr_rejects_length_mismatches() -> None: + left_modes, right_modes = _mode_sets() + + with pytest.raises(ValueError, match='same length'): + eme.get_tr(left_modes[:1], WAVENUMBERS_L, right_modes, WAVENUMBERS_R, dxes=DXES) + + +def test_get_tr_rejects_malformed_mode_tuples() -> None: + bad_modes = [(numpy.ones(4),)] + + with pytest.raises(ValueError, match='2-tuple'): + eme.get_tr(bad_modes, [1.0], bad_modes, [1.0], dxes=DXES) + + +def test_get_tr_rejects_incompatible_field_shapes() -> None: + left_modes = [(numpy.ones(4), numpy.ones(4))] + right_modes = [(numpy.ones(6), numpy.ones(6))] + + with pytest.raises(ValueError, match='same E/H shapes'): + eme.get_tr(left_modes, [1.0], right_modes, [1.0], dxes=DXES) + + +def _build_real_epsilon() -> numpy.ndarray: + epsilon = numpy.ones((3, 5, 5), dtype=float) + epsilon[:, 2, 1] = 2.0 + return vec(epsilon) + + +def _build_straight_mode() -> tuple[tuple[numpy.ndarray, numpy.ndarray], complex, numpy.ndarray]: + epsilon = _build_real_epsilon() + e_xy, wavenumber = waveguide_2d.solve_mode( + 0, + omega=OMEGA, + dxes=REAL_DXES, + epsilon=epsilon, + ) + e_field, h_field = waveguide_2d.normalized_fields_e( + e_xy, + wavenumber=wavenumber, + omega=OMEGA, + dxes=REAL_DXES, + epsilon=epsilon, + ) + return (e_field, h_field), wavenumber, epsilon + + +def _build_bend_mode() -> tuple[tuple[numpy.ndarray, numpy.ndarray], complex]: + epsilon = vec(numpy.ones((3, 5, 5), dtype=float)) + rmin = 10.0 + e_xy, angular_wavenumber = waveguide_cyl.solve_mode( + 0, + omega=OMEGA, + dxes=REAL_DXES, + epsilon=epsilon, + rmin=rmin, + ) + linear_wavenumber = waveguide_cyl.linear_wavenumbers( + [e_xy], + [angular_wavenumber], + epsilon=epsilon, + dxes=REAL_DXES, + rmin=rmin, + )[0] + e_field, h_field = waveguide_cyl.normalized_fields_e( + e_xy, + angular_wavenumber=angular_wavenumber, + omega=OMEGA, + dxes=REAL_DXES, + epsilon=epsilon, + rmin=rmin, + ) + return (e_field, h_field), linear_wavenumber + + +def test_get_s_is_near_identity_for_identical_solved_straight_modes() -> None: + mode, wavenumber, _epsilon = _build_straight_mode() + + ss = eme.get_s([mode], [wavenumber], [mode], [wavenumber], dxes=REAL_DXES) + + assert ss.shape == (2, 2) + assert numpy.isfinite(ss).all() + assert abs(ss[0, 0]) < 1e-6 + assert abs(ss[1, 1]) < 1e-6 + assert abs(abs(ss[0, 1]) - 1.0) < 1e-6 + assert abs(abs(ss[1, 0]) - 1.0) < 1e-6 + assert numpy.linalg.svd(ss, compute_uv=False).max() <= 1.0 + 1e-10 + + +def test_get_s_returns_finite_passive_output_for_small_straight_to_bend_fixture() -> None: + straight_mode, straight_wavenumber, _epsilon = _build_straight_mode() + bend_mode, bend_wavenumber = _build_bend_mode() + + ss = eme.get_s([straight_mode], [straight_wavenumber], [bend_mode], [bend_wavenumber], dxes=REAL_DXES) + + assert ss.shape == (2, 2) + assert numpy.isfinite(ss).all() + assert numpy.linalg.svd(ss, compute_uv=False).max() <= 1.0 + 1e-10 From 010da1ccf5ed625121ff0886aa34182985161051 Mon Sep 17 00:00:00 2001 From: Forgejo Actions Date: Tue, 21 Apr 2026 19:40:49 -0700 Subject: [PATCH 2/9] [tests] add some slow tests --- README.md | 8 +- meanas/test/test_examples_smoke.py | 47 +++++++++++ meanas/test/test_fdtd_pml.py | 1 + meanas/test/test_import_fallbacks.py | 3 +- meanas/test/test_waveguide_fdtd_fdfd.py | 4 + pyproject.toml | 3 + uv.lock | 104 ++++++++++++++++++++++-- 7 files changed, 162 insertions(+), 8 deletions(-) create mode 100644 meanas/test/test_examples_smoke.py diff --git a/README.md b/README.md index b400796..73d48b5 100644 --- a/README.md +++ b/README.md @@ -95,9 +95,13 @@ source my_venv/bin/activate # Install in-place (-e, editable) from ./meanas, including development dependencies ([dev]) pip3 install --user -e './meanas[dev]' -# Run tests +# Fast local iteration: excludes slower 3D/integration/example-smoke checks cd meanas -python3 -m pytest -rsxX | tee test_results.txt +python3 -m pytest -q -m "not complete" + +# Complete pre-commit confidence run: includes the slower integration tests and +# tracked example smoke tests +python3 -m pytest -q | tee test_results.txt ``` #### See also: diff --git a/meanas/test/test_examples_smoke.py b/meanas/test/test_examples_smoke.py new file mode 100644 index 0000000..b21f90b --- /dev/null +++ b/meanas/test/test_examples_smoke.py @@ -0,0 +1,47 @@ +from pathlib import Path +import os +import subprocess +import sys + +import pytest + + +pytestmark = pytest.mark.complete + +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def _run_example(example_name: str, tmp_path: Path) -> subprocess.CompletedProcess[str]: + env = os.environ.copy() + env['MPLBACKEND'] = 'Agg' + env['MPLCONFIGDIR'] = str(tmp_path / f'mpl-{example_name}') + return subprocess.run( + [sys.executable, str(REPO_ROOT / 'examples' / example_name)], + cwd=REPO_ROOT, + env=env, + text=True, + capture_output=True, + check=False, + timeout=60, + ) + + +def test_eme_example_smoke_runs(tmp_path: Path) -> None: + pytest.importorskip('matplotlib') + + result = _run_example('eme.py', tmp_path) + + assert result.returncode == 0, result.stdout + result.stderr + assert 'left effective indices:' in result.stdout + assert 'fundamental left-to-right transmission' in result.stdout + + +def test_eme_bend_example_smoke_runs(tmp_path: Path) -> None: + pytest.importorskip('matplotlib') + pytest.importorskip('skrf') + + result = _run_example('eme_bend.py', tmp_path) + + assert result.returncode == 0, result.stdout + result.stderr + assert 'straight effective indices:' in result.stdout + assert 'cascaded bend through power' in result.stdout diff --git a/meanas/test/test_fdtd_pml.py b/meanas/test/test_fdtd_pml.py index 9d8aef8..06c2588 100644 --- a/meanas/test/test_fdtd_pml.py +++ b/meanas/test/test_fdtd_pml.py @@ -198,6 +198,7 @@ def test_cpml_plane_wave_phasor_decays_monotonically_through_outgoing_pml() -> N assert numpy.all(numpy.diff(right_pml) <= interior_level * 1e-3) +@pytest.mark.complete def test_cpml_point_source_total_energy_reaches_late_time_plateau() -> None: dt = 0.3 period_steps = 24 diff --git a/meanas/test/test_import_fallbacks.py b/meanas/test/test_import_fallbacks.py index d1ecca9..75005d0 100644 --- a/meanas/test/test_import_fallbacks.py +++ b/meanas/test/test_import_fallbacks.py @@ -17,6 +17,7 @@ def _restore_reloaded(monkeypatch, module): def test_meanas_import_survives_readme_open_failure(monkeypatch) -> None: # type: ignore[no-untyped-def] + expected_version = meanas.__version__ original_open = pathlib.Path.open def failing_open(self: pathlib.Path, *args, **kwargs): # type: ignore[no-untyped-def] @@ -27,7 +28,7 @@ def test_meanas_import_survives_readme_open_failure(monkeypatch) -> None: # typ monkeypatch.setattr(pathlib.Path, 'open', failing_open) reloaded = _reload(meanas) - assert reloaded.__version__ == '0.10' + assert reloaded.__version__ == expected_version assert reloaded.__author__ == 'Jan Petykiewicz' assert reloaded.__doc__ is not None diff --git a/meanas/test/test_waveguide_fdtd_fdfd.py b/meanas/test/test_waveguide_fdtd_fdfd.py index 167b91e..ae2078d 100644 --- a/meanas/test/test_waveguide_fdtd_fdfd.py +++ b/meanas/test/test_waveguide_fdtd_fdfd.py @@ -2,6 +2,7 @@ import dataclasses from functools import lru_cache import numpy +import pytest from .. import fdfd, fdtd from ..fdtd.misc import gaussian_packet @@ -9,6 +10,9 @@ from ..fdmath import vec, unvec from ..fdfd import functional, scpml, waveguide_3d +pytestmark = pytest.mark.complete + + DT = 0.25 PERIOD_STEPS = 64 OMEGA = 2 * numpy.pi / (PERIOD_STEPS * DT) diff --git a/pyproject.toml b/pyproject.toml index 013631a..55fcac1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,6 +121,9 @@ ignore_missing_imports = true [tool.pytest.ini_options] addopts = "-rsXx" testpaths = ["meanas"] +markers = [ + "complete: slower integration and smoke tests intended for full pre-commit confidence runs", +] [tool.coverage.run] source = ["meanas"] diff --git a/uv.lock b/uv.lock index b696160..08bad0c 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,13 @@ version = 1 requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] [[package]] name = "babel" @@ -730,8 +738,8 @@ dependencies = [ [package.optional-dependencies] dev = [ { name = "coverage" }, - { name = "gridlock" }, { name = "htmlark" }, + { name = "matplotlib" }, { name = "mkdocs" }, { name = "mkdocs-material" }, { name = "mkdocs-print-site-plugin" }, @@ -739,6 +747,7 @@ dev = [ { name = "pymdown-extensions" }, { name = "pytest" }, { name = "ruff" }, + { name = "scikit-rf" }, ] docs = [ { name = "htmlark" }, @@ -750,8 +759,8 @@ docs = [ { name = "ruff" }, ] examples = [ - { name = "gridlock" }, { name = "matplotlib" }, + { name = "scikit-rf" }, ] test = [ { name = "coverage" }, @@ -762,11 +771,10 @@ test = [ requires-dist = [ { name = "coverage", marker = "extra == 'dev'" }, { name = "coverage", marker = "extra == 'test'" }, - { name = "gridlock" }, - { name = "gridlock", marker = "extra == 'dev'" }, - { name = "gridlock", marker = "extra == 'examples'", specifier = ">=2.1" }, + { name = "gridlock", specifier = ">=2.1" }, { name = "htmlark", marker = "extra == 'dev'", specifier = ">=1.0" }, { name = "htmlark", marker = "extra == 'docs'", specifier = ">=1.0" }, + { name = "matplotlib", marker = "extra == 'dev'", specifier = ">=3.10.8" }, { name = "matplotlib", marker = "extra == 'examples'", specifier = ">=3.10.8" }, { name = "mkdocs", marker = "extra == 'dev'", specifier = ">=1.6" }, { name = "mkdocs", marker = "extra == 'docs'", specifier = ">=1.6" }, @@ -783,6 +791,8 @@ requires-dist = [ { name = "pytest", marker = "extra == 'test'" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6" }, { name = "ruff", marker = "extra == 'docs'", specifier = ">=0.6" }, + { name = "scikit-rf", marker = "extra == 'dev'", specifier = ">=1.0" }, + { name = "scikit-rf", marker = "extra == 'examples'", specifier = ">=1.0" }, { name = "scipy", specifier = "~=1.14" }, ] @@ -1025,6 +1035,66 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 }, ] +[[package]] +name = "pandas" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/35/6411db530c618e0e0005187e35aa02ce60ae4c4c4d206964a2f978217c27/pandas-3.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a727a73cbdba2f7458dc82449e2315899d5140b449015d822f515749a46cbbe0", size = 10326926 }, + { url = "https://files.pythonhosted.org/packages/c4/d3/b7da1d5d7dbdc5ef52ed7debd2b484313b832982266905315dad5a0bf0b1/pandas-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbbd4aa20ca51e63b53bbde6a0fa4254b1aaabb74d2f542df7a7959feb1d760c", size = 9926987 }, + { url = "https://files.pythonhosted.org/packages/52/77/9b1c2d6070b5dbe239a7bc889e21bfa58720793fb902d1e070695d87c6d0/pandas-3.0.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:339dda302bd8369dedeae979cb750e484d549b563c3f54f3922cb8ff4978c5eb", size = 10757067 }, + { url = "https://files.pythonhosted.org/packages/20/17/ec40d981705654853726e7ac9aea9ddbb4a5d9cf54d8472222f4f3de06c2/pandas-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61c2fd96d72b983a9891b2598f286befd4ad262161a609c92dc1652544b46b76", size = 11258787 }, + { url = "https://files.pythonhosted.org/packages/90/e3/3f1126d43d3702ca8773871a81c9f15122a1f412342cc56284ffda5b1f70/pandas-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c934008c733b8bbea273ea308b73b3156f0181e5b72960790b09c18a2794fe1e", size = 11771616 }, + { url = "https://files.pythonhosted.org/packages/2e/cf/0f4e268e1f5062e44a6bda9f925806721cd4c95c2b808a4c82ebe914f96b/pandas-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:60a80bb4feacbef5e1447a3f82c33209c8b7e07f28d805cfd1fb951e5cb443aa", size = 12337623 }, + { url = "https://files.pythonhosted.org/packages/44/a0/97a6339859d4acb2536efb24feb6708e82f7d33b2ed7e036f2983fcced82/pandas-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed72cb3f45190874eb579c64fa92d9df74e98fd63e2be7f62bce5ace0ade61df", size = 9897372 }, + { url = "https://files.pythonhosted.org/packages/8f/eb/781516b808a99ddf288143cec46b342b3016c3414d137da1fdc3290d8860/pandas-3.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f12b1a9e332c01e09510586f8ca9b108fd631fd656af82e452d7315ef6df5f9f", size = 9154922 }, + { url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921 }, + { url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127 }, + { url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577 }, + { url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030 }, + { url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468 }, + { url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381 }, + { url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993 }, + { url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118 }, + { url = "https://files.pythonhosted.org/packages/bf/ca/3e639a1ea6fcd0617ca4e8ca45f62a74de33a56ae6cd552735470b22c8d3/pandas-3.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5918ba197c951dec132b0c5929a00c0bf05d5942f590d3c10a807f6e15a57d3", size = 10321105 }, + { url = "https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d606a041c89c0a474a4702d532ab7e73a14fe35c8d427b972a625c8e46373668", size = 9864088 }, + { url = "https://files.pythonhosted.org/packages/5c/2b/341f1b04bbca2e17e13cd3f08c215b70ef2c60c5356ef1e8c6857449edc7/pandas-3.0.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:710246ba0616e86891b58ab95f2495143bb2bc83ab6b06747c74216f583a6ac9", size = 10369066 }, + { url = "https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d3cfe227c725b1f3dff4278b43d8c784656a42a9325b63af6b1492a8232209e", size = 10876780 }, + { url = "https://files.pythonhosted.org/packages/98/fe/2249ae5e0a69bd0ddf17353d0a5d26611d70970111f5b3600cdc8be883e7/pandas-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c3b723df9087a9a9a840e263ebd9f88b64a12075d1bf2ea401a5a42f254f084d", size = 11375181 }, + { url = "https://files.pythonhosted.org/packages/de/64/77a38b09e70b6464883b8d7584ab543e748e42c1b5d337a2ee088e0df741/pandas-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3096110bf9eac0070b7208465f2740e2d8a670d5cb6530b5bb884eca495fd39", size = 11928899 }, + { url = "https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991", size = 9746574 }, + { url = "https://files.pythonhosted.org/packages/88/39/21304ae06a25e8bf9fc820d69b29b2c495b2ae580d1e143146c309941760/pandas-3.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:5fdbfa05931071aba28b408e59226186b01eb5e92bea2ab78b65863ca3228d84", size = 9047156 }, + { url = "https://files.pythonhosted.org/packages/72/20/7defa8b27d4f330a903bb68eea33be07d839c5ea6bdda54174efcec0e1d2/pandas-3.0.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:dbc20dea3b9e27d0e66d74c42b2d0c1bed9c2ffe92adea33633e3bedeb5ac235", size = 10756238 }, + { url = "https://files.pythonhosted.org/packages/e9/95/49433c14862c636afc0e9b2db83ff16b3ad92959364e52b2955e44c8e94c/pandas-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b75c347eff42497452116ce05ef461822d97ce5b9ff8df6edacb8076092c855d", size = 10408520 }, + { url = "https://files.pythonhosted.org/packages/3b/f8/462ad2b5881d6b8ec8e5f7ed2ea1893faa02290d13870a1600fe72ad8efc/pandas-3.0.2-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1478075142e83a5571782ad007fb201ed074bdeac7ebcc8890c71442e96adf7", size = 10324154 }, + { url = "https://files.pythonhosted.org/packages/0a/65/d1e69b649cbcddda23ad6e4c40ef935340f6f652a006e5cbc3555ac8adb3/pandas-3.0.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5880314e69e763d4c8b27937090de570f1fb8d027059a7ada3f7f8e98bdcb677", size = 10714449 }, + { url = "https://files.pythonhosted.org/packages/47/a4/85b59bc65b8190ea3689882db6cdf32a5003c0ccd5a586c30fdcc3ffc4fc/pandas-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5329e26898896f06035241a626d7c335daa479b9bbc82be7c2742d048e41172", size = 11338475 }, + { url = "https://files.pythonhosted.org/packages/1e/c4/bc6966c6e38e5d9478b935272d124d80a589511ed1612a5d21d36f664c68/pandas-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:81526c4afd31971f8b62671442a4b2b51e0aa9acc3819c9f0f12a28b6fcf85f1", size = 11786568 }, + { url = "https://files.pythonhosted.org/packages/e8/74/09298ca9740beed1d3504e073d67e128aa07e5ca5ca2824b0c674c0b8676/pandas-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:7cadd7e9a44ec13b621aec60f9150e744cfc7a3dd32924a7e2f45edff31823b0", size = 10488652 }, + { url = "https://files.pythonhosted.org/packages/bb/40/c6ea527147c73b24fc15c891c3fcffe9c019793119c5742b8784a062c7db/pandas-3.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db0dbfd2a6cdf3770aa60464d50333d8f3d9165b2f2671bcc299b72de5a6677b", size = 10326084 }, + { url = "https://files.pythonhosted.org/packages/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288", size = 9914146 }, + { url = "https://files.pythonhosted.org/packages/8d/77/3a227ff3337aa376c60d288e1d61c5d097131d0ac71f954d90a8f369e422/pandas-3.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01f31a546acd5574ef77fe199bc90b55527c225c20ccda6601cf6b0fd5ed597c", size = 10444081 }, + { url = "https://files.pythonhosted.org/packages/15/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535", size = 10897535 }, + { url = "https://files.pythonhosted.org/packages/06/9d/98cc7a7624f7932e40f434299260e2917b090a579d75937cb8a57b9d2de3/pandas-3.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f48afd9bb13300ffb5a3316973324c787054ba6665cda0da3fbd67f451995db", size = 11446992 }, + { url = "https://files.pythonhosted.org/packages/9a/cd/19ff605cc3760e80602e6826ddef2824d8e7050ed80f2e11c4b079741dc3/pandas-3.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6c4d8458b97a35717b62469a4ea0e85abd5ed8687277f5ccfc67f8a5126f8c53", size = 11968257 }, + { url = "https://files.pythonhosted.org/packages/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf", size = 9865893 }, + { url = "https://files.pythonhosted.org/packages/08/71/e5ec979dd2e8a093dacb8864598c0ff59a0cee0bbcdc0bfec16a51684d4f/pandas-3.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:63d141b56ef686f7f0d714cfb8de4e320475b86bf4b620aa0b7da89af8cbdbbb", size = 9188644 }, + { url = "https://files.pythonhosted.org/packages/f1/6c/7b45d85db19cae1eb524f2418ceaa9d85965dcf7b764ed151386b7c540f0/pandas-3.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:140f0cffb1fa2524e874dde5b477d9defe10780d8e9e220d259b2c0874c89d9d", size = 10776246 }, + { url = "https://files.pythonhosted.org/packages/a8/3e/7b00648b086c106e81766f25322b48aa8dfa95b55e621dbdf2fdd413a117/pandas-3.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae37e833ff4fed0ba352f6bdd8b73ba3ab3256a85e54edfd1ab51ae40cca0af8", size = 10424801 }, + { url = "https://files.pythonhosted.org/packages/da/6e/558dd09a71b53b4008e7fc8a98ec6d447e9bfb63cdaeea10e5eb9b2dabe8/pandas-3.0.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d888a5c678a419a5bb41a2a93818e8ed9fd3172246555c0b37b7cc27027effd", size = 10345643 }, + { url = "https://files.pythonhosted.org/packages/be/e3/921c93b4d9a280409451dc8d07b062b503bbec0531d2627e73a756e99a82/pandas-3.0.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b444dc64c079e84df91baa8bf613d58405645461cabca929d9178f2cd392398d", size = 10743641 }, + { url = "https://files.pythonhosted.org/packages/56/ca/fd17286f24fa3b4d067965d8d5d7e14fe557dd4f979a0b068ac0deaf8228/pandas-3.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4544c7a54920de8eeacaa1466a6b7268ecfbc9bc64ab4dbb89c6bbe94d5e0660", size = 11361993 }, + { url = "https://files.pythonhosted.org/packages/e4/a5/2f6ed612056819de445a433ca1f2821ac3dab7f150d569a59e9cc105de1d/pandas-3.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:734be7551687c00fbd760dc0522ed974f82ad230d4a10f54bf51b80d44a08702", size = 11815274 }, + { url = "https://files.pythonhosted.org/packages/00/2f/b622683e99ec3ce00b0854bac9e80868592c5b051733f2cf3a868e5fea26/pandas-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:57a07209bebcbcf768d2d13c9b78b852f9a15978dac41b9e6421a81ad4cdd276", size = 10888530 }, + { url = "https://files.pythonhosted.org/packages/cb/2b/f8434233fab2bd66a02ec014febe4e5adced20e2693e0e90a07d118ed30e/pandas-3.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482", size = 9455341 }, +] + [[package]] name = "pathspec" version = "1.0.4" @@ -1305,6 +1375,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614 }, ] +[[package]] +name = "scikit-rf" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pandas" }, + { name = "scipy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/bb/36d5d359137435e1776c44aaf5861aa84727ad728ac979ec76a52e3e5b28/scikit_rf-1.11.0.tar.gz", hash = "sha256:ac6c532e327da473abb15864105337424061a9d36429808362de0247eb2906d1", size = 577744 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/53/301380bcb71e136d944363f9172491730ad3f03d0cef598d57a65db38d84/scikit_rf-1.11.0-py3-none-any.whl", hash = "sha256:a8e7c8e3b89630685b1e1ab4c48fe19a6f830bbf31c26cd6f438e90902c2b9c5", size = 627060 }, +] + [[package]] name = "scipy" version = "1.16.3" @@ -1403,6 +1488,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, ] +[[package]] +name = "tzdata" +version = "2026.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952 }, +] + [[package]] name = "urllib3" version = "2.6.3" From eec3fc28a73194d6e4368cc529b620b47d7eec3e Mon Sep 17 00:00:00 2001 From: Forgejo Actions Date: Tue, 21 Apr 2026 19:51:57 -0700 Subject: [PATCH 3/9] [docs] update colors --- docs/stylesheets/extra.css | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 090b5e7..bee91a4 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -13,19 +13,31 @@ } [data-md-color-scheme="slate"] { - --md-default-bg-color: #0f141c; + --md-default-bg-color: #000000; + --md-default-bg-color--light: #050505; + --md-default-bg-color--lighter: #0a0a0a; + --md-default-bg-color--lightest: #111111; --md-default-fg-color: #e8eef7; --md-default-fg-color--light: #b3bfd1; --md-default-fg-color--lighter: #7f8ba0; --md-default-fg-color--lightest: #5d6880; - --md-code-bg-color: #111923; + --md-code-bg-color: #050505; --md-code-fg-color: #e4edf8; --md-accent-fg-color: #7dd3fc; } [data-md-color-scheme="slate"] .md-header, [data-md-color-scheme="slate"] .md-tabs { - background: linear-gradient(90deg, #111923 0%, #162235 100%); + background: #000000; +} + +[data-md-color-scheme="slate"] .md-main, +[data-md-color-scheme="slate"] .md-main__inner, +[data-md-color-scheme="slate"] .md-content, +[data-md-color-scheme="slate"] .md-content__inner, +[data-md-color-scheme="slate"] .md-sidebar, +[data-md-color-scheme="slate"] .md-sidebar__scrollwrap { + background: #000000; } [data-md-color-scheme="slate"] .md-typeset pre > code, @@ -34,7 +46,7 @@ } [data-md-color-scheme="slate"] .md-typeset table:not([class]) { - background: rgba(255, 255, 255, 0.015); + background: #050505; } [data-md-color-scheme="slate"] .md-typeset table:not([class]) th { @@ -43,7 +55,7 @@ [data-md-color-scheme="slate"] .md-typeset .admonition, [data-md-color-scheme="slate"] .md-typeset details { - background: rgba(255, 255, 255, 0.02); + background: #050505; border-color: rgba(125, 211, 252, 0.2); } From c6c9159b133e9c22b55c45ff2c2f719aef086b9c Mon Sep 17 00:00:00 2001 From: Forgejo Actions Date: Tue, 21 Apr 2026 21:13:34 -0700 Subject: [PATCH 4/9] type hints and lint --- examples/eme.py | 8 +++++-- examples/eme_bend.py | 10 +++++--- examples/fdtd.py | 3 +-- meanas/fdfd/bloch.py | 11 ++++++++- meanas/fdfd/eme.py | 16 +++++++------ meanas/fdfd/farfield.py | 27 +++++++++++----------- meanas/fdfd/waveguide_2d.py | 4 ++-- meanas/fdfd/waveguide_3d.py | 22 +++++++++++------- meanas/fdfd/waveguide_cyl.py | 12 +++++----- meanas/fdmath/functional.py | 6 ++--- meanas/fdmath/operators.py | 4 ++-- meanas/fdmath/types.py | 8 +++---- meanas/fdtd/base.py | 6 ++--- meanas/fdtd/boundaries.py | 14 +++++------ meanas/fdtd/misc.py | 24 +++++++++++++------ meanas/fdtd/pml.py | 25 ++++++++++---------- meanas/test/test_bloch_interactions.py | 2 +- meanas/test/test_eme_numerics.py | 27 ++++++++++++++-------- meanas/test/test_fdfd_pml.py | 6 +++-- meanas/test/test_fdfd_solvers.py | 20 ++++++++-------- meanas/test/test_fdtd_base.py | 2 -- meanas/test/test_fdtd_boundaries.py | 2 +- meanas/test/test_fdtd_misc.py | 3 +++ meanas/test/test_fdtd_phasor.py | 2 +- meanas/test/test_fdtd_pml.py | 16 +++++++------ meanas/test/test_import_fallbacks.py | 22 ++++++++++++------ meanas/test/test_waveguide_fdtd_fdfd.py | 20 ++++++++-------- meanas/test/test_waveguide_mode_helpers.py | 2 +- meanas/test/utils.py | 7 +++--- pyproject.toml | 3 +++ 30 files changed, 198 insertions(+), 136 deletions(-) diff --git a/examples/eme.py b/examples/eme.py index 6215cbc..3a26dc8 100644 --- a/examples/eme.py +++ b/examples/eme.py @@ -14,6 +14,7 @@ simple straight interface: from __future__ import annotations import importlib +from typing import TYPE_CHECKING import numpy from numpy import pi @@ -24,6 +25,9 @@ from gridlock import Extent from meanas.fdfd import eme, waveguide_2d from meanas.fdmath import unvec +if TYPE_CHECKING: + from types import ModuleType + WL = 1310.0 DX = 40.0 @@ -35,7 +39,7 @@ EPS_OX = 1.453 ** 2 MODE_NUMBERS = numpy.array([0]) -def require_optional(name: str, package_name: str | None = None): +def require_optional(name: str, package_name: str | None = None) -> ModuleType: package_name = package_name or name try: return importlib.import_module(name) @@ -159,7 +163,7 @@ def print_summary(ss: numpy.ndarray, wavenumbers_left: numpy.ndarray, wavenumber def plot_results( *, - pyplot, + pyplot: ModuleType, ss: numpy.ndarray, left_mode: tuple[numpy.ndarray, numpy.ndarray], right_mode: tuple[numpy.ndarray, numpy.ndarray], diff --git a/examples/eme_bend.py b/examples/eme_bend.py index caff4df..e5eaebd 100644 --- a/examples/eme_bend.py +++ b/examples/eme_bend.py @@ -15,6 +15,7 @@ This example demonstrates a cylindrical-waveguide EME workflow: from __future__ import annotations import importlib +from typing import TYPE_CHECKING import numpy from numpy import pi @@ -26,6 +27,9 @@ from gridlock import Extent from meanas.fdfd import eme, waveguide_2d, waveguide_cyl from meanas.fdmath import unvec +if TYPE_CHECKING: + from types import ModuleType + WL = 1310.0 DX = 40.0 @@ -40,7 +44,7 @@ STRAIGHT_SECTION_LENGTH = 12e3 BEND_ANGLE = pi / 2 -def require_optional(name: str, package_name: str | None = None): +def require_optional(name: str, package_name: str | None = None) -> ModuleType: package_name = package_name or name try: return importlib.import_module(name) @@ -163,7 +167,7 @@ def solve_bend_modes( def build_cascaded_network( - skrf, + skrf: ModuleType, *, interface_s: numpy.ndarray, straight_wavenumbers: numpy.ndarray, @@ -216,7 +220,7 @@ def print_summary( def plot_results( *, - pyplot, + pyplot: ModuleType, interface_s: numpy.ndarray, cascaded_s: numpy.ndarray, straight_mode: tuple[numpy.ndarray, numpy.ndarray], diff --git a/examples/fdtd.py b/examples/fdtd.py index d8cd101..fd6026d 100644 --- a/examples/fdtd.py +++ b/examples/fdtd.py @@ -89,7 +89,7 @@ def perturbed_l3(a: float, radius: float, **kwargs) -> Pattern: return pat -def main(): +def main() -> None: dtype = numpy.float32 max_t = 3600 # number of timesteps @@ -97,7 +97,6 @@ def main(): pml_thickness = 8 # (number of cells) wl = 1550 # Excitation wavelength and fwhm - dwl = 100 # Device design parameters xy_size = numpy.array([10, 10]) diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py index 5701ed9..df04999 100644 --- a/meanas/fdfd/bloch.py +++ b/meanas/fdfd/bloch.py @@ -683,7 +683,16 @@ def eigsolve( return numpy.abs(trace) if False: - def trace_deriv(theta, sgn: int = sgn, ZtAZ=ZtAZ, DtAD=DtAD, symZtD=symZtD, symZtAD=symZtAD, ZtZ=ZtZ, DtD=DtD): # noqa: ANN001 + def trace_deriv( + theta: float, + sgn: int = sgn, + ZtAZ=ZtAZ, # noqa: ANN001 + DtAD=DtAD, # noqa: ANN001 + symZtD=symZtD, # noqa: ANN001 + symZtAD=symZtAD, # noqa: ANN001 + ZtZ=ZtZ, # noqa: ANN001 + DtD=DtD, # noqa: ANN001 + ) -> float: Qi = Qi_func(theta) c2 = numpy.cos(2 * theta) s2 = numpy.sin(2 * theta) diff --git a/meanas/fdfd/eme.py b/meanas/fdfd/eme.py index 366de8e..af745e8 100644 --- a/meanas/fdfd/eme.py +++ b/meanas/fdfd/eme.py @@ -27,11 +27,13 @@ from scipy import sparse from ..fdmath import dx_lists2_t, vcfdfield2 from .waveguide_2d import inner_product +type wavenumber_seq = Sequence[complex] | NDArray[numpy.complexfloating] | NDArray[numpy.floating] + def _validate_port_modes( name: str, ehs: Sequence[Sequence[vcfdfield2]], - wavenumbers: Sequence[complex], + wavenumbers: wavenumber_seq, ) -> tuple[tuple[int, ...], tuple[int, ...]]: if len(ehs) != len(wavenumbers): raise ValueError(f'{name} mode list and wavenumber list must have the same length') @@ -61,9 +63,9 @@ def _validate_port_modes( def get_tr( ehLs: Sequence[Sequence[vcfdfield2]], - wavenumbers_L: Sequence[complex], + wavenumbers_L: wavenumber_seq, ehRs: Sequence[Sequence[vcfdfield2]], - wavenumbers_R: Sequence[complex], + wavenumbers_R: wavenumber_seq, dxes: dx_lists2_t, ) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]: """ @@ -118,9 +120,9 @@ def get_tr( def get_abcd( ehLs: Sequence[Sequence[vcfdfield2]], - wavenumbers_L: Sequence[complex], + wavenumbers_L: wavenumber_seq, ehRs: Sequence[Sequence[vcfdfield2]], - wavenumbers_R: Sequence[complex], + wavenumbers_R: wavenumber_seq, **kwargs, ) -> sparse.sparray: """ @@ -151,9 +153,9 @@ def get_abcd( def get_s( ehLs: Sequence[Sequence[vcfdfield2]], - wavenumbers_L: Sequence[complex], + wavenumbers_L: wavenumber_seq, ehRs: Sequence[Sequence[vcfdfield2]], - wavenumbers_R: Sequence[complex], + wavenumbers_R: wavenumber_seq, force_nogain: bool = False, force_reciprocal: bool = False, **kwargs, diff --git a/meanas/fdfd/farfield.py b/meanas/fdfd/farfield.py index 0051cd0..00e6989 100644 --- a/meanas/fdfd/farfield.py +++ b/meanas/fdfd/farfield.py @@ -1,23 +1,24 @@ """ Functions for performing near-to-farfield transformation (and the reverse). """ -from typing import Any, cast, TYPE_CHECKING +from typing import Any, cast +from collections.abc import Sequence import numpy from numpy.fft import fft2, fftshift, fftfreq, ifft2, ifftshift from numpy import pi +from numpy.typing import NDArray +from numpy import complexfloating -from ..fdmath import cfdfield_t - -if TYPE_CHECKING: - from collections.abc import Sequence +type farfield_slice = NDArray[complexfloating] +type transverse_slice_pair = Sequence[farfield_slice] def near_to_farfield( - E_near: cfdfield_t, - H_near: cfdfield_t, + E_near: transverse_slice_pair, + H_near: transverse_slice_pair, dx: float, dy: float, - padded_size: list[int] | int | None = None + padded_size: Sequence[int] | int | None = None ) -> dict[str, Any]: """ Compute the farfield, i.e. the distribution of the fields after propagation @@ -58,7 +59,7 @@ def near_to_farfield( raise Exception('H_near must be a length-2 list of ndarrays') s = E_near[0].shape - if not all(s == f.shape for f in E_near + H_near): + if not all(s == f.shape for f in [*E_near, *H_near]): raise Exception('All fields must be the same shape!') if padded_size is None: @@ -123,11 +124,11 @@ def near_to_farfield( def far_to_nearfield( - E_far: cfdfield_t, - H_far: cfdfield_t, + E_far: transverse_slice_pair, + H_far: transverse_slice_pair, dkx: float, dky: float, - padded_size: list[int] | int | None = None + padded_size: Sequence[int] | int | None = None ) -> dict[str, Any]: """ Compute the farfield, i.e. the distribution of the fields after propagation @@ -164,7 +165,7 @@ def far_to_nearfield( raise Exception('H_far must be a length-2 list of ndarrays') s = E_far[0].shape - if not all(s == f.shape for f in E_far + H_far): + if not all(s == f.shape for f in [*E_far, *H_far]): raise Exception('All fields must be the same shape!') if padded_size is None: diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py index 1074e2b..fa2fe76 100644 --- a/meanas/fdfd/waveguide_2d.py +++ b/meanas/fdfd/waveguide_2d.py @@ -423,10 +423,10 @@ def normalized_fields_h( def _normalized_fields( e: vcfdslice, h: vcfdslice, - omega: complex, + _omega: complex, dxes: dx_lists2_t, epsilon: vfdslice, - mu: vfdslice | None = None, + _mu: vfdslice | None = None, prop_phase: float = 0, ) -> tuple[vcfdslice_t, vcfdslice_t]: r""" diff --git a/meanas/fdfd/waveguide_3d.py b/meanas/fdfd/waveguide_3d.py index e7dfd22..01db9b1 100644 --- a/meanas/fdfd/waveguide_3d.py +++ b/meanas/fdfd/waveguide_3d.py @@ -19,9 +19,8 @@ The intended workflow is: That same convention controls which side of the selected slice is used for the overlap window and how the expanded field is phased. """ -from typing import Any, cast +from typing import Any, TypedDict, cast import warnings -from typing import Any from collections.abc import Sequence import numpy from numpy.typing import NDArray @@ -31,6 +30,13 @@ from ..fdmath import vec, unvec, dx_lists_t, cfdfield_t, fdfield, cfdfield from . import operators, waveguide_2d +class Waveguide3DMode(TypedDict): + wavenumber: complex + wavenumber_2d: complex + H: NDArray[complexfloating] + E: NDArray[complexfloating] + + def solve_mode( mode_number: int, omega: complex, @@ -40,7 +46,7 @@ def solve_mode( slices: Sequence[slice], epsilon: fdfield, mu: fdfield | None = None, - ) -> dict[str, complex | NDArray[complexfloating]]: + ) -> Waveguide3DMode: r""" Given a 3D grid, selects a slice from the grid and attempts to solve for an eigenmode propagating through that slice. @@ -121,7 +127,7 @@ def solve_mode( E[iii] = e[oo][:, :, None].transpose(reverse_order) H[iii] = h[oo][:, :, None].transpose(reverse_order) - results = { + results: Waveguide3DMode = { 'wavenumber': wavenumber, 'wavenumber_2d': wavenumber_2d, 'H': H, @@ -184,13 +190,13 @@ def compute_source( def compute_overlap_e( - E: cfdfield_t, + E: cfdfield, wavenumber: complex, dxes: dx_lists_t, axis: int, polarity: int, slices: Sequence[slice], - omega: float, + _omega: float, ) -> cfdfield_t: r""" Build an overlap field for projecting another 3D electric field onto a mode. @@ -262,7 +268,7 @@ def compute_overlap_e( if clipped_start >= clipped_stop: raise ValueError('Requested overlap window lies outside the domain') if clipped_start != start or clipped_stop != stop: - warnings.warn('Requested overlap window was clipped to fit within the domain', RuntimeWarning) + warnings.warn('Requested overlap window was clipped to fit within the domain', RuntimeWarning, stacklevel=2) slices2_l = list(slices) slices2_l[axis] = slice(clipped_start, clipped_stop) @@ -275,7 +281,7 @@ def compute_overlap_e( norm = (Etgt.conj() * Etgt).sum() if norm == 0: raise ValueError('Requested overlap window contains no overlap field support') - Etgt /= norm + Etgt = Etgt / norm return cfdfield_t(Etgt) diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py index 201f709..e4e2666 100644 --- a/meanas/fdfd/waveguide_cyl.py +++ b/meanas/fdfd/waveguide_cyl.py @@ -130,7 +130,7 @@ import numpy from numpy.typing import NDArray, ArrayLike from scipy import sparse -from ..fdmath import vec, unvec, dx_lists2_t, vcfdslice_t, vcfdfield2_t, vfdslice, vcfdslice, vcfdfield2 +from ..fdmath import vec, unvec, dx_lists2_t, vcfdslice_t, vfdslice, vcfdslice, vcfdfield2 from ..fdmath.operators import deriv_forward, deriv_back from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration from . import waveguide_2d @@ -267,7 +267,7 @@ def solve_mode( mode_number: int, *args: Any, **kwargs: Any, - ) -> tuple[vcfdslice, complex]: + ) -> tuple[vcfdfield2, complex]: """ Wrapper around `solve_modes()` that solves for a single mode. @@ -285,7 +285,7 @@ def solve_mode( def linear_wavenumbers( - e_xys: list[vcfdfield2_t], + e_xys: Sequence[vcfdfield2] | NDArray[numpy.complex128], angular_wavenumbers: ArrayLike, epsilon: vfdslice, dxes: dx_lists2_t, @@ -537,11 +537,11 @@ def normalized_fields_e( def _normalized_fields( e: vcfdslice, h: vcfdslice, - omega: complex, + _omega: complex, dxes: dx_lists2_t, - rmin: float, # Currently unused, but may want to use cylindrical poynting + _rmin: float, # Currently unused, but may want to use cylindrical poynting epsilon: vfdslice, - mu: vfdslice | None = None, + _mu: vfdslice | None = None, prop_phase: float = 0, ) -> tuple[vcfdslice_t, vcfdslice_t]: r""" diff --git a/meanas/fdmath/functional.py b/meanas/fdmath/functional.py index 034d4ba..27d368a 100644 --- a/meanas/fdmath/functional.py +++ b/meanas/fdmath/functional.py @@ -10,7 +10,7 @@ import numpy from numpy.typing import NDArray from numpy import floating, complexfloating -from .types import fdfield_t, fdfield_updater_t +from .types import fdfield, fdfield_updater_t def deriv_forward( @@ -127,7 +127,7 @@ def curl_forward_parts( ) -> Callable: Dx, Dy, Dz = deriv_forward(dx_e) - def mkparts_fwd(e: fdfield_t) -> tuple[tuple[fdfield_t, fdfield_t], ...]: + def mkparts_fwd(e: fdfield) -> tuple[tuple[fdfield, fdfield], ...]: return ((-Dz(e[1]), Dy(e[2])), ( Dz(e[0]), -Dx(e[2])), (-Dy(e[0]), Dx(e[1]))) @@ -140,7 +140,7 @@ def curl_back_parts( ) -> Callable: Dx, Dy, Dz = deriv_back(dx_h) - def mkparts_back(h: fdfield_t) -> tuple[tuple[fdfield_t, fdfield_t], ...]: + def mkparts_back(h: fdfield) -> tuple[tuple[fdfield, fdfield], ...]: return ((-Dz(h[1]), Dy(h[2])), ( Dz(h[0]), -Dx(h[2])), (-Dy(h[0]), Dx(h[1]))) diff --git a/meanas/fdmath/operators.py b/meanas/fdmath/operators.py index 0c64ae7..8b7cabc 100644 --- a/meanas/fdmath/operators.py +++ b/meanas/fdmath/operators.py @@ -9,7 +9,7 @@ from numpy.typing import NDArray from numpy import floating, complexfloating from scipy import sparse -from .types import vfdfield_t +from .types import vfdfield def shift_circ( @@ -171,7 +171,7 @@ def cross( [-B[1], B[0], zero]]) -def vec_cross(b: vfdfield_t) -> sparse.sparray: +def vec_cross(b: vfdfield) -> sparse.sparray: """ Vector cross product operator diff --git a/meanas/fdmath/types.py b/meanas/fdmath/types.py index 222d18a..b82a5ae 100644 --- a/meanas/fdmath/types.py +++ b/meanas/fdmath/types.py @@ -88,8 +88,8 @@ dx_lists2_mut = MutableSequence[MutableSequence[NDArray[floating | complexfloati """Mutable version of `dx_lists2_t`""" -fdfield_updater_t = Callable[..., fdfield_t] -"""Convenience type for functions which take and return an fdfield_t""" +fdfield_updater_t = Callable[..., fdfield] +"""Convenience type for functions which take and return a real `fdfield`""" -cfdfield_updater_t = Callable[..., cfdfield_t] -"""Convenience type for functions which take and return an cfdfield_t""" +cfdfield_updater_t = Callable[..., cfdfield] +"""Convenience type for functions which take and return a complex `cfdfield`""" diff --git a/meanas/fdtd/base.py b/meanas/fdtd/base.py index 3891e28..480ed87 100644 --- a/meanas/fdtd/base.py +++ b/meanas/fdtd/base.py @@ -3,7 +3,7 @@ Basic FDTD field updates """ -from ..fdmath import dx_lists_t, fdfield_t, fdfield_updater_t +from ..fdmath import dx_lists_t, fdfield, fdfield_updater_t from ..fdmath.functional import curl_forward, curl_back @@ -47,7 +47,7 @@ def maxwell_e( else: curl_h_fun = curl_back() - def me_fun(e: fdfield_t, h: fdfield_t, epsilon: fdfield_t | float) -> fdfield_t: + def me_fun(e: fdfield, h: fdfield, epsilon: fdfield | float) -> fdfield: """ Update the E-field. @@ -103,7 +103,7 @@ def maxwell_h( else: curl_e_fun = curl_forward() - def mh_fun(e: fdfield_t, h: fdfield_t, mu: fdfield_t | float | None = None) -> fdfield_t: + def mh_fun(e: fdfield, h: fdfield, mu: fdfield | float | None = None) -> fdfield: """ Update the H-field. diff --git a/meanas/fdtd/boundaries.py b/meanas/fdtd/boundaries.py index aa0bff5..ca8940d 100644 --- a/meanas/fdtd/boundaries.py +++ b/meanas/fdtd/boundaries.py @@ -6,7 +6,7 @@ Boundary conditions from typing import Any -from ..fdmath import fdfield_t, fdfield_updater_t +from ..fdmath import fdfield, fdfield_updater_t def conducting_boundary( @@ -15,7 +15,7 @@ def conducting_boundary( ) -> tuple[fdfield_updater_t, fdfield_updater_t]: dirs = [0, 1, 2] if direction not in dirs: - raise Exception(f'Invalid direction: {direction}') + raise ValueError(f'Invalid direction: {direction}') dirs.remove(direction) u, v = dirs @@ -31,13 +31,13 @@ def conducting_boundary( boundary = tuple(boundary_slice) shifted1 = tuple(shifted1_slice) - def en(e: fdfield_t) -> fdfield_t: + def en(e: fdfield) -> fdfield: e[direction][boundary] = 0 e[u][boundary] = e[u][shifted1] e[v][boundary] = e[v][shifted1] return e - def hn(h: fdfield_t) -> fdfield_t: + def hn(h: fdfield) -> fdfield: h[direction][boundary] = h[direction][shifted1] h[u][boundary] = 0 h[v][boundary] = 0 @@ -56,14 +56,14 @@ def conducting_boundary( shifted1 = tuple(shifted1_slice) shifted2 = tuple(shifted2_slice) - def ep(e: fdfield_t) -> fdfield_t: + def ep(e: fdfield) -> fdfield: e[direction][boundary] = -e[direction][shifted2] e[direction][shifted1] = 0 e[u][boundary] = e[u][shifted1] e[v][boundary] = e[v][shifted1] return e - def hp(h: fdfield_t) -> fdfield_t: + def hp(h: fdfield) -> fdfield: h[direction][boundary] = h[direction][shifted1] h[u][boundary] = -h[u][shifted2] h[u][shifted1] = 0 @@ -73,4 +73,4 @@ def conducting_boundary( return ep, hp - raise Exception(f'Bad polarity: {polarity}') + raise ValueError(f'Bad polarity: {polarity}') diff --git a/meanas/fdtd/misc.py b/meanas/fdtd/misc.py index 89ccb3d..585c745 100644 --- a/meanas/fdtd/misc.py +++ b/meanas/fdtd/misc.py @@ -1,5 +1,6 @@ from collections.abc import Callable import logging +from typing import cast import numpy from numpy.typing import NDArray, ArrayLike @@ -9,7 +10,14 @@ from numpy import pi logger = logging.getLogger(__name__) -pulse_fn_t = Callable[[int | NDArray], tuple[float, float, float]] +type pulse_scalar_t = float | NDArray[numpy.floating] +pulse_fn_t = Callable[[ArrayLike], tuple[pulse_scalar_t, pulse_scalar_t, pulse_scalar_t]] + + +def _scalar_or_array(values: NDArray[numpy.floating]) -> pulse_scalar_t: + if values.ndim == 0: + return float(values) + return cast('NDArray[numpy.floating]', values) def gaussian_packet( @@ -49,8 +57,9 @@ def gaussian_packet( delay = numpy.ceil(delay * freq) / freq # force delay to integer number of periods to maintain phase logger.info(f'src_time {2 * delay / dt}') - def source_phasor(ii: int | NDArray) -> tuple[float, float, float]: - t0 = ii * dt - delay + def source_phasor(ii: ArrayLike) -> tuple[pulse_scalar_t, pulse_scalar_t, pulse_scalar_t]: + ii_array = numpy.asarray(ii, dtype=float) + t0 = ii_array * dt - delay envelope = numpy.sqrt(numpy.sqrt(2 * alpha / pi)) * numpy.exp(-alpha * t0 * t0) if one_sided: @@ -59,7 +68,7 @@ def gaussian_packet( cc = numpy.cos(omega * t0) ss = numpy.sin(omega * t0) - return envelope, cc, ss + return _scalar_or_array(envelope), _scalar_or_array(cc), _scalar_or_array(ss) # nrm = numpy.exp(-omega * omega / alpha) / 2 @@ -105,15 +114,16 @@ def ricker_pulse( delay = delay_results.root delay = numpy.ceil(delay * freq) / freq # force delay to integer number of periods to maintain phase - def source_phasor(ii: int | NDArray) -> tuple[float, float, float]: - t0 = ii * dt - delay + def source_phasor(ii: ArrayLike) -> tuple[pulse_scalar_t, pulse_scalar_t, pulse_scalar_t]: + ii_array = numpy.asarray(ii, dtype=float) + t0 = ii_array * dt - delay rr = omega * t0 / 2 ff = (1 - 2 * rr * rr) * numpy.exp(-rr * rr) cc = numpy.cos(omega * t0) ss = numpy.sin(omega * t0) - return ff, cc, ss + return _scalar_or_array(ff), _scalar_or_array(cc), _scalar_or_array(ss) return source_phasor, delay diff --git a/meanas/fdtd/pml.py b/meanas/fdtd/pml.py index bf61b4e..aba9cb7 100644 --- a/meanas/fdtd/pml.py +++ b/meanas/fdtd/pml.py @@ -23,7 +23,7 @@ from copy import deepcopy import numpy from numpy.typing import NDArray, DTypeLike -from ..fdmath import fdfield, fdfield_t, dx_lists_t +from ..fdmath import fdfield, dx_lists_t from ..fdmath.functional import deriv_forward, deriv_back @@ -67,16 +67,16 @@ def cpml_params( """ if axis not in range(3): - raise Exception(f'Invalid axis: {axis}') + raise ValueError(f'Invalid axis: {axis}') if polarity not in (-1, 1): - raise Exception(f'Invalid polarity: {polarity}') + raise ValueError(f'Invalid polarity: {polarity}') if thickness <= 2: - raise Exception('It would be wise to have a pml with 4+ cells of thickness') + raise ValueError('It would be wise to have a pml with 4+ cells of thickness') if epsilon_eff <= 0: - raise Exception('epsilon_eff must be positive') + raise ValueError('epsilon_eff must be positive') sigma_max = -ln_R_per_layer / 2 * (m + 1) kappa_max = numpy.sqrt(epsilon_eff * mu_eff) @@ -129,8 +129,7 @@ def updates_with_cpml( epsilon: fdfield, *, dtype: DTypeLike = numpy.float32, - ) -> tuple[Callable[[fdfield_t, fdfield_t, fdfield_t], None], - Callable[[fdfield_t, fdfield_t, fdfield_t], None]]: + ) -> tuple[Callable[..., None], Callable[..., None]]: """ Build Yee-step update closures augmented with CPML terms. @@ -187,9 +186,9 @@ def updates_with_cpml( pH = numpy.empty_like(epsilon, dtype=dtype) def update_E( - e: fdfield_t, - h: fdfield_t, - epsilon: fdfield_t, + e: fdfield, + h: fdfield, + epsilon: fdfield, ) -> None: dyHx = Dby(h[0]) dzHx = Dbz(h[0]) @@ -233,9 +232,9 @@ def updates_with_cpml( e[2] += dt / epsilon[2] * (dxHy - dyHx + pE[2]) def update_H( - e: fdfield_t, - h: fdfield_t, - mu: fdfield_t | tuple[int, int, int] = (1, 1, 1), + e: fdfield, + h: fdfield, + mu: fdfield | tuple[int, int, int] = (1, 1, 1), ) -> None: dyEx = Dfy(e[0]) dzEx = Dfz(e[0]) diff --git a/meanas/test/test_bloch_interactions.py b/meanas/test/test_bloch_interactions.py index b67d5ce..0628a55 100644 --- a/meanas/test/test_bloch_interactions.py +++ b/meanas/test/test_bloch_interactions.py @@ -4,7 +4,7 @@ from numpy.testing import assert_allclose from types import SimpleNamespace from ..fdfd import bloch -from ._bloch_case import EPSILON, G_MATRIX, H_SIZE, K0_X, SHAPE, Y0, Y0_TWO_MODE, build_overlap_fixture +from ._bloch_case import EPSILON, G_MATRIX, H_SIZE, K0_X, Y0, Y0_TWO_MODE, build_overlap_fixture from .utils import assert_close diff --git a/meanas/test/test_eme_numerics.py b/meanas/test/test_eme_numerics.py index 7486128..2949e4c 100644 --- a/meanas/test/test_eme_numerics.py +++ b/meanas/test/test_eme_numerics.py @@ -1,3 +1,5 @@ +from typing import cast + import numpy import pytest from scipy import sparse @@ -51,6 +53,10 @@ def _nonsymmetric_tr(left_marker: object): return fake_get_tr +def _dummy_modes() -> tuple[list[tuple[numpy.ndarray, numpy.ndarray]], numpy.ndarray]: + return [_mode(0.0), _mode(0.7)], numpy.array([1.0, 0.5]) + + def test_get_tr_returns_finite_bounded_transfer_matrices() -> None: left_modes, right_modes = _mode_sets() @@ -103,9 +109,10 @@ def test_get_s_plain_matches_block_assembly_from_get_tr() -> None: def test_get_s_force_nogain_caps_singular_values(monkeypatch) -> None: monkeypatch.setattr(eme, 'get_tr', _gain_only_tr) + modes, wavenumbers = _dummy_modes() - plain_s = eme.get_s(None, None, None, None) - clipped_s = eme.get_s(None, None, None, None, force_nogain=True) + plain_s = eme.get_s(modes, wavenumbers, modes, wavenumbers) + clipped_s = eme.get_s(modes, wavenumbers, modes, wavenumbers, force_nogain=True) plain_singular_values = numpy.linalg.svd(plain_s, compute_uv=False) clipped_singular_values = numpy.linalg.svd(clipped_s, compute_uv=False) @@ -116,18 +123,20 @@ def test_get_s_force_nogain_caps_singular_values(monkeypatch) -> None: def test_get_s_force_reciprocal_symmetrizes_output(monkeypatch) -> None: - left = object() - right = object() + left = numpy.array([1.0, 0.5]) + right = numpy.array([0.9, 0.4]) + modes, _wavenumbers = _dummy_modes() monkeypatch.setattr(eme, 'get_tr', _nonsymmetric_tr(left)) - ss = eme.get_s(None, left, None, right, force_reciprocal=True) + ss = eme.get_s(modes, left, modes, right, force_reciprocal=True) assert_close(ss, ss.T) def test_get_s_force_nogain_and_reciprocal_returns_finite_output(monkeypatch) -> None: monkeypatch.setattr(eme, 'get_tr', _gain_and_reflection_tr) - ss = eme.get_s(None, None, None, None, force_nogain=True, force_reciprocal=True) + modes, wavenumbers = _dummy_modes() + ss = eme.get_s(modes, wavenumbers, modes, wavenumbers, force_nogain=True, force_reciprocal=True) assert ss.shape == (4, 4) assert numpy.isfinite(ss).all() @@ -143,15 +152,15 @@ def test_get_tr_rejects_length_mismatches() -> None: def test_get_tr_rejects_malformed_mode_tuples() -> None: - bad_modes = [(numpy.ones(4),)] + bad_modes = cast(list[tuple[numpy.ndarray, numpy.ndarray]], [(numpy.ones(4, dtype=complex),)]) with pytest.raises(ValueError, match='2-tuple'): eme.get_tr(bad_modes, [1.0], bad_modes, [1.0], dxes=DXES) def test_get_tr_rejects_incompatible_field_shapes() -> None: - left_modes = [(numpy.ones(4), numpy.ones(4))] - right_modes = [(numpy.ones(6), numpy.ones(6))] + left_modes = [(numpy.ones(4, dtype=complex), numpy.ones(4, dtype=complex))] + right_modes = [(numpy.ones(6, dtype=complex), numpy.ones(6, dtype=complex))] with pytest.raises(ValueError, match='same E/H shapes'): eme.get_tr(left_modes, [1.0], right_modes, [1.0], dxes=DXES) diff --git a/meanas/test/test_fdfd_pml.py b/meanas/test/test_fdfd_pml.py index 540a3a0..1a8d66c 100644 --- a/meanas/test/test_fdfd_pml.py +++ b/meanas/test/test_fdfd_pml.py @@ -85,8 +85,10 @@ def j_distribution( other_dims = [0, 1, 2] other_dims.remove(dim) - dx_prop = (dxes[0][dim][shape[dim + 1] // 2] - + dxes[1][dim][shape[dim + 1] // 2]) / 2 # noqa: E128 # TODO is this right for nonuniform dxes? + dx_prop = ( + dxes[0][dim][shape[dim + 1] // 2] + + dxes[1][dim][shape[dim + 1] // 2] + ) / 2 # TODO is this right for nonuniform dxes? # Mask only contains components orthogonal to propagation direction center_mask = numpy.zeros(shape, dtype=bool) diff --git a/meanas/test/test_fdfd_solvers.py b/meanas/test/test_fdfd_solvers.py index b841dc9..de39d70 100644 --- a/meanas/test/test_fdfd_solvers.py +++ b/meanas/test/test_fdfd_solvers.py @@ -1,3 +1,5 @@ +from typing import cast + import numpy from ..fdfd import solvers @@ -41,7 +43,7 @@ def test_scipy_qmr_installs_logging_callback_when_missing(monkeypatch) -> None: def test_generic_forward_preconditions_system_and_guess(monkeypatch) -> None: case = solver_plumbing_case() - captured: dict[str, object] = {} + captured: dict[str, numpy.ndarray | float | object] = {} monkeypatch.setattr(solvers.operators, 'e_full', lambda *args, **kwargs: case.a0) monkeypatch.setattr(solvers.operators, 'e_full_preconditioners', lambda dxes: (case.pl, case.pr)) @@ -63,16 +65,16 @@ def test_generic_forward_preconditions_system_and_guess(monkeypatch) -> None: E_guess=case.guess, ) - assert_close(captured['a'].toarray(), (case.pl @ case.a0 @ case.pr).toarray()) - assert_close(captured['b'], case.pl @ (-1j * case.omega * case.j)) - assert_close(captured['x0'], case.pl @ case.guess) + assert_close(cast(object, captured['a']).toarray(), (case.pl @ case.a0 @ case.pr).toarray()) # type: ignore[attr-defined] + assert_close(cast(numpy.ndarray, captured['b']), case.pl @ (-1j * case.omega * case.j)) + assert_close(cast(numpy.ndarray, captured['x0']), case.pl @ case.guess) assert captured['atol'] == 1e-12 assert_close(result, case.pr @ case.solver_result) def test_generic_adjoint_preconditions_system_and_guess(monkeypatch) -> None: case = solver_plumbing_case() - captured: dict[str, object] = {} + captured: dict[str, numpy.ndarray | float | object] = {} monkeypatch.setattr(solvers.operators, 'e_full', lambda *args, **kwargs: case.a0) monkeypatch.setattr(solvers.operators, 'e_full_preconditioners', lambda dxes: (case.pl, case.pr)) @@ -96,9 +98,9 @@ def test_generic_adjoint_preconditions_system_and_guess(monkeypatch) -> None: ) expected_matrix = (case.pl @ case.a0 @ case.pr).T.conjugate() - assert_close(captured['a'].toarray(), expected_matrix.toarray()) - assert_close(captured['b'], case.pr.T.conjugate() @ (-1j * case.omega * case.j)) - assert_close(captured['x0'], case.pr.T.conjugate() @ case.guess) + assert_close(cast(object, captured['a']).toarray(), expected_matrix.toarray()) # type: ignore[attr-defined] + assert_close(cast(numpy.ndarray, captured['b']), case.pr.T.conjugate() @ (-1j * case.omega * case.j)) + assert_close(cast(numpy.ndarray, captured['x0']), case.pr.T.conjugate() @ case.guess) assert captured['rtol'] == 1e-9 assert_close(result, case.pl.T.conjugate() @ case.solver_result) @@ -122,5 +124,5 @@ def test_generic_without_guess_does_not_inject_x0(monkeypatch) -> None: matrix_solver=fake_solver, ) - assert 'x0' not in captured['kwargs'] + assert 'x0' not in cast(dict[str, object], captured['kwargs']) assert_close(result, case.pr @ numpy.array([1.0, -1.0])) diff --git a/meanas/test/test_fdtd_base.py b/meanas/test/test_fdtd_base.py index bc1f514..c8246d5 100644 --- a/meanas/test/test_fdtd_base.py +++ b/meanas/test/test_fdtd_base.py @@ -1,5 +1,3 @@ -import numpy - from ..fdmath import functional as fd_functional from ..fdtd import base from ._test_builders import real_ramp diff --git a/meanas/test/test_fdtd_boundaries.py b/meanas/test/test_fdtd_boundaries.py index d7ba186..d60ca7a 100644 --- a/meanas/test/test_fdtd_boundaries.py +++ b/meanas/test/test_fdtd_boundaries.py @@ -58,5 +58,5 @@ def test_conducting_boundary_updates_expected_faces(direction: int, polarity: in [(-1, 1), (3, 1), (0, 0)], ) def test_conducting_boundary_rejects_invalid_arguments(direction: int, polarity: int) -> None: - with pytest.raises(Exception): + with pytest.raises(ValueError, match='Invalid direction|Bad polarity'): conducting_boundary(direction, polarity) diff --git a/meanas/test/test_fdtd_misc.py b/meanas/test/test_fdtd_misc.py index 65dc713..3688c6c 100644 --- a/meanas/test/test_fdtd_misc.py +++ b/meanas/test/test_fdtd_misc.py @@ -10,6 +10,9 @@ def test_gaussian_packet_accepts_array_input(one_sided: bool) -> None: 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 isinstance(envelope, numpy.ndarray) + assert isinstance(cc, numpy.ndarray) + assert isinstance(ss, numpy.ndarray) assert envelope.shape == (2,) assert numpy.isfinite(envelope).all() diff --git a/meanas/test/test_fdtd_phasor.py b/meanas/test/test_fdtd_phasor.py index 7e1126b..9d28ee3 100644 --- a/meanas/test/test_fdtd_phasor.py +++ b/meanas/test/test_fdtd_phasor.py @@ -371,7 +371,7 @@ def _real_pulse_case() -> RealPulseCase: source_phasor, _delay = gaussian_packet(wl=wavelength, dwl=1.0, dt=dt, turn_on=1e-5) aa, cc, ss = source_phasor(numpy.arange(total_steps) + 0.5) - waveform = aa * (cc + 1j * ss) + waveform = numpy.asarray(aa * (cc + 1j * ss), dtype=complex) scale = fdtd.real_injection_scale(waveform, omega, dt, offset_steps=0.5)[0] j_accumulator = numpy.zeros((1, *full_shape), dtype=complex) diff --git a/meanas/test/test_fdtd_pml.py b/meanas/test/test_fdtd_pml.py index 06c2588..319260f 100644 --- a/meanas/test/test_fdtd_pml.py +++ b/meanas/test/test_fdtd_pml.py @@ -1,3 +1,5 @@ +from typing import Any + import numpy import pytest @@ -12,7 +14,7 @@ from .utils import assert_close [(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): + with pytest.raises(ValueError, match='Invalid axis|Invalid polarity|wise to have a pml|epsilon_eff must be positive'): cpml_params(axis=axis, polarity=polarity, dt=0.1, thickness=thickness, epsilon_eff=epsilon_eff) @@ -36,7 +38,7 @@ def test_updates_with_cpml_keeps_zero_fields_zero() -> None: 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: list[list[dict[str, Any] | None]] = [[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) @@ -69,7 +71,7 @@ def test_updates_with_cpml_matches_base_updates_when_all_faces_disabled() -> Non e = _real_field(shape, 10.0) h = _real_field(shape, 100.0) dxes = _unit_dxes(shape) - params = [[None, None] for _ in range(3)] + params: list[list[dict[str, Any] | None]] = [[None, None] for _ in range(3)] update_e_cpml, update_h_cpml = updates_with_cpml(params, dt=0.1, dxes=dxes, epsilon=epsilon) update_e_base = maxwell_e(dt=0.1, dxes=dxes) @@ -96,7 +98,7 @@ def test_updates_with_cpml_matches_base_updates_with_complex_dtype_when_all_face e = _complex_field(shape, 10.0) h = _complex_field(shape, 100.0) dxes = _unit_dxes(shape) - params = [[None, None] for _ in range(3)] + params: list[list[dict[str, Any] | None]] = [[None, None] for _ in range(3)] update_e_cpml, update_h_cpml = updates_with_cpml(params, dt=0.1, dxes=dxes, epsilon=epsilon, dtype=complex) update_e_base = maxwell_e(dt=0.1, dxes=dxes) @@ -125,7 +127,7 @@ def test_updates_with_cpml_only_changes_the_configured_face_region() -> None: dxes = _unit_dxes(shape) thickness = 3 - params = [[None, None] for _ in range(3)] + params: list[list[dict[str, Any] | None]] = [[None, None] for _ in range(3)] params[0][0] = cpml_params(axis=0, polarity=-1, dt=0.1, thickness=thickness) update_e_cpml, update_h_cpml = updates_with_cpml(params, dt=0.1, dxes=dxes, epsilon=epsilon) @@ -166,7 +168,7 @@ def test_cpml_plane_wave_phasor_decays_monotonically_through_outgoing_pml() -> N epsilon = numpy.ones(shape, dtype=float) dxes = _unit_dxes(shape) - params = [[None, None] for _ in range(3)] + params: list[list[dict[str, Any] | None]] = [[None, None] for _ in range(3)] for polarity_index, polarity in enumerate((-1, 1)): params[0][polarity_index] = cpml_params(axis=0, polarity=polarity, dt=dt, thickness=thickness) @@ -212,7 +214,7 @@ def test_cpml_point_source_total_energy_reaches_late_time_plateau() -> None: epsilon = numpy.ones(shape, dtype=float) dxes = _unit_dxes(shape) - params = [[None, None] for _ in range(3)] + params: list[list[dict[str, Any] | None]] = [[None, None] for _ in range(3)] for axis in range(3): for polarity_index, polarity in enumerate((-1, 1)): params[axis][polarity_index] = cpml_params(axis=axis, polarity=polarity, dt=dt, thickness=thickness) diff --git a/meanas/test/test_import_fallbacks.py b/meanas/test/test_import_fallbacks.py index 75005d0..e332d1b 100644 --- a/meanas/test/test_import_fallbacks.py +++ b/meanas/test/test_import_fallbacks.py @@ -1,26 +1,28 @@ import builtins import importlib import pathlib +from types import ModuleType +from typing import Any +import pytest import meanas from ..fdfd import bloch -from .utils import assert_close -def _reload(module): +def _reload(module: ModuleType) -> ModuleType: return importlib.reload(module) -def _restore_reloaded(monkeypatch, module): +def _restore_reloaded(monkeypatch: pytest.MonkeyPatch, module: ModuleType) -> ModuleType: monkeypatch.undo() return _reload(module) -def test_meanas_import_survives_readme_open_failure(monkeypatch) -> None: # type: ignore[no-untyped-def] +def test_meanas_import_survives_readme_open_failure(monkeypatch: pytest.MonkeyPatch) -> None: expected_version = meanas.__version__ original_open = pathlib.Path.open - def failing_open(self: pathlib.Path, *args, **kwargs): # type: ignore[no-untyped-def] + def failing_open(self: pathlib.Path, *args: Any, **kwargs: Any) -> Any: if self.name == 'README.md': raise FileNotFoundError('forced README failure') return original_open(self, *args, **kwargs) @@ -35,10 +37,16 @@ def test_meanas_import_survives_readme_open_failure(monkeypatch) -> None: # typ _restore_reloaded(monkeypatch, meanas) -def test_bloch_reloads_with_numpy_fft_when_pyfftw_is_unavailable(monkeypatch) -> None: # type: ignore[no-untyped-def] +def test_bloch_reloads_with_numpy_fft_when_pyfftw_is_unavailable(monkeypatch: pytest.MonkeyPatch) -> None: original_import = builtins.__import__ - def fake_import(name: str, globals=None, locals=None, fromlist=(), level: int = 0): # type: ignore[no-untyped-def] + def fake_import( + name: str, + globals: dict[str, Any] | None = None, + locals: dict[str, Any] | None = None, + fromlist: tuple[str, ...] = (), + level: int = 0, + ) -> Any: if name.startswith('pyfftw'): raise ImportError('forced pyfftw failure') return original_import(name, globals, locals, fromlist, level) diff --git a/meanas/test/test_waveguide_fdtd_fdfd.py b/meanas/test/test_waveguide_fdtd_fdfd.py index ae2078d..9d42bf5 100644 --- a/meanas/test/test_waveguide_fdtd_fdfd.py +++ b/meanas/test/test_waveguide_fdtd_fdfd.py @@ -224,7 +224,7 @@ def _build_cpml_params() -> list[list[dict[str, numpy.ndarray | float]]]: def _build_complex_pulse_waveform(total_steps: int) -> tuple[numpy.ndarray, complex]: source_phasor, _delay = gaussian_packet(wl=WAVELENGTH, dwl=PULSE_DWL, dt=DT, turn_on=1e-5) aa, cc, ss = source_phasor(numpy.arange(total_steps) + 0.5) - waveform = aa * (cc + 1j * ss) + waveform = numpy.asarray(aa * (cc + 1j * ss), dtype=complex) scale = fdtd.temporal_phasor_scale(waveform, OMEGA, DT, offset_steps=0.5)[0] return waveform, scale @@ -272,7 +272,7 @@ def _run_real_field_straight_waveguide_case() -> RealFieldWaveguideResult: slices=REAL_FIELD_SOURCE_SLICES, epsilon=epsilon, ) - j_mode *= numpy.exp(1j * REAL_FIELD_SOURCE_PHASE) + j_mode = j_mode * numpy.exp(1j * REAL_FIELD_SOURCE_PHASE) monitor_mode = waveguide_3d.solve_mode( 0, omega=OMEGA, @@ -425,8 +425,8 @@ def _run_straight_waveguide_case(variant: str) -> WaveguideCalibrationResult: ) h_fdfd = functional.e2h(OMEGA, stretched_dxes)(e_fdfd) - overlap_td = vec(e_ph) @ vec(overlap_e).conj() - overlap_fd = vec(e_fdfd) @ vec(overlap_e).conj() + overlap_td = complex(vec(e_ph) @ vec(overlap_e).conj()) + overlap_fd = complex(vec(e_fdfd) @ vec(overlap_e).conj()) poynting_td = functional.poynting_e_cross_h(stretched_dxes)(e_ph, h_ph.conj()) poynting_fd = functional.poynting_e_cross_h(stretched_dxes)(e_fdfd, h_fdfd.conj()) @@ -551,10 +551,10 @@ def _run_width_step_scattering_case() -> WaveguideScatteringResult: ) h_fdfd = functional.e2h(OMEGA, stretched_dxes)(e_fdfd) - reflected_td = vec(e_ph) @ vec(reflected_overlap).conj() - reflected_fd = vec(e_fdfd) @ vec(reflected_overlap).conj() - transmitted_td = vec(e_ph) @ vec(transmitted_overlap).conj() - transmitted_fd = vec(e_fdfd) @ vec(transmitted_overlap).conj() + reflected_td = complex(vec(e_ph) @ vec(reflected_overlap).conj()) + reflected_fd = complex(vec(e_fdfd) @ vec(reflected_overlap).conj()) + transmitted_td = complex(vec(e_ph) @ vec(transmitted_overlap).conj()) + transmitted_fd = complex(vec(e_fdfd) @ vec(transmitted_overlap).conj()) poynting_td = functional.poynting_e_cross_h(stretched_dxes)(e_ph, h_ph.conj()) poynting_fd = functional.poynting_e_cross_h(stretched_dxes)(e_fdfd, h_fdfd.conj()) @@ -664,8 +664,8 @@ def _run_pulsed_straight_waveguide_case() -> PulsedWaveguideCalibrationResult: ) h_fdfd = functional.e2h(OMEGA, stretched_dxes)(e_fdfd) - overlap_td = vec(e_ph) @ vec(overlap_e).conj() - overlap_fd = vec(e_fdfd) @ vec(overlap_e).conj() + overlap_td = complex(vec(e_ph) @ vec(overlap_e).conj()) + overlap_fd = complex(vec(e_fdfd) @ vec(overlap_e).conj()) poynting_td = functional.poynting_e_cross_h(stretched_dxes)(e_ph, h_ph.conj()) poynting_fd = functional.poynting_e_cross_h(stretched_dxes)(e_fdfd, h_fdfd.conj()) diff --git a/meanas/test/test_waveguide_mode_helpers.py b/meanas/test/test_waveguide_mode_helpers.py index d5d3abf..ca2d917 100644 --- a/meanas/test/test_waveguide_mode_helpers.py +++ b/meanas/test/test_waveguide_mode_helpers.py @@ -16,7 +16,7 @@ def build_waveguide_3d_mode( *, slice_start: int, polarity: int, - ) -> tuple[numpy.ndarray, list[list[numpy.ndarray]], tuple[slice, slice, slice], dict[str, complex | numpy.ndarray]]: + ) -> tuple[numpy.ndarray, list[list[numpy.ndarray]], tuple[slice, slice, slice], waveguide_3d.Waveguide3DMode]: epsilon = numpy.ones((3, 5, 5, 1), dtype=float) dxes = [[numpy.ones(5), numpy.ones(5), numpy.ones(1)] for _ in range(2)] slices = (slice(slice_start, slice_start + 1), slice(None), slice(None)) diff --git a/meanas/test/utils.py b/meanas/test/utils.py index 3bafd49..62afaf0 100644 --- a/meanas/test/utils.py +++ b/meanas/test/utils.py @@ -1,5 +1,6 @@ import numpy from numpy.typing import NDArray +from numpy.typing import ArrayLike def make_prng(seed: int = 12345) -> numpy.random.RandomState: @@ -24,9 +25,9 @@ def assert_fields_close( ) def assert_close( - x: NDArray, - y: NDArray, + x: ArrayLike, + y: ArrayLike, *args, **kwargs, ) -> None: - numpy.testing.assert_allclose(x, y, *args, **kwargs) + numpy.testing.assert_allclose(numpy.asarray(x), numpy.asarray(y), *args, **kwargs) diff --git a/pyproject.toml b/pyproject.toml index 55fcac1..7f1d6b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,9 @@ lint.ignore = [ "TRY002", # Exception() ] +[tool.ruff.lint.per-file-ignores] +"meanas/test/**/*.py" = ["ANN", "ARG", "TC006"] + [[tool.mypy.overrides]] module = [ From a1568a6f16c6f3d0e3968e361cb5a5d280ac94ea Mon Sep 17 00:00:00 2001 From: Forgejo Actions Date: Tue, 21 Apr 2026 21:20:34 -0700 Subject: [PATCH 5/9] ignore some lint --- meanas/fdfd/bloch.py | 8 ++++---- meanas/fdfd/farfield.py | 12 ++++++------ meanas/fdfd/waveguide_2d.py | 12 ++++++++---- meanas/fdfd/waveguide_cyl.py | 6 ++++-- meanas/test/test_waveguide_fdtd_fdfd.py | 8 ++++---- meanas/test/test_waveguide_mode_helpers.py | 8 ++++---- 6 files changed, 30 insertions(+), 24 deletions(-) diff --git a/meanas/fdfd/bloch.py b/meanas/fdfd/bloch.py index df04999..1e15c3a 100644 --- a/meanas/fdfd/bloch.py +++ b/meanas/fdfd/bloch.py @@ -262,7 +262,7 @@ def maxwell_operator( else: # transform from mn to xyz b_xyz = (m * b_m - + n * b_n) # noqa: E128 + + n * b_n) # noqa # divide by mu temp = ifftn(b_xyz, axes=range(3)) @@ -409,7 +409,7 @@ def inverse_maxwell_operator_approx( else: # transform from mn to xyz h_xyz = (m * hin_m - + n * hin_n) # noqa: E128 + + n * hin_n) # noqa # multiply by mu temp = ifftn(h_xyz, axes=range(3)) @@ -474,7 +474,7 @@ def find_k( `(k, actual_frequency, eigenvalues, eigenvectors)` The found k-vector and its frequency, along with all eigenvalues and eigenvectors. """ - direction = numpy.array(direction) / norm(direction) + direction = numpy.array(direction) / norm(direction) # type: ignore[operator] k_bounds = tuple(sorted(k_bounds)) # type: ignore # we know the length already... assert len(k_bounds) == 2 @@ -504,7 +504,7 @@ def find_k( assert n is not None assert v is not None actual_frequency = get_f(float(res.x), band) - return direction * float(res.x), float(actual_frequency), n, v + return direction * float(res.x), float(actual_frequency), n, v # type: ignore[operator,return-value] def eigsolve( diff --git a/meanas/fdfd/farfield.py b/meanas/fdfd/farfield.py index 00e6989..06f705b 100644 --- a/meanas/fdfd/farfield.py +++ b/meanas/fdfd/farfield.py @@ -87,14 +87,14 @@ def near_to_farfield( # Normalized vector potentials N, L N = [-Hn_fft[1] * cos_phi * cos_th + Hn_fft[0] * cos_phi * sin_th, - Hn_fft[1] * sin_th + Hn_fft[0] * cos_th] # noqa: E127 + Hn_fft[1] * sin_th + Hn_fft[0] * cos_th] # noqa L = [ En_fft[1] * cos_phi * cos_th - En_fft[0] * cos_phi * sin_th, - -En_fft[1] * sin_th - En_fft[0] * cos_th] # noqa: E128 + -En_fft[1] * sin_th - En_fft[0] * cos_th] # noqa E_far = [-L[1] - N[0], - L[0] - N[1]] # noqa: E127 + L[0] - N[1]] # noqa H_far = [-E_far[1], - E_far[0]] # noqa: E127 + E_far[0]] # noqa theta = numpy.arctan2(ky, kx) phi = numpy.arccos(cos_phi) @@ -203,9 +203,9 @@ def far_to_nearfield( # Normalized vector potentials N, L L = [0.5 * E_far[1], - -0.5 * E_far[0]] # noqa: E128 + -0.5 * E_far[0]] # noqa N = [L[1], - -L[0]] # noqa: E128 + -L[0]] # noqa En_fft = [ numpy.divide( diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py index fa2fe76..aab7944 100644 --- a/meanas/fdfd/waveguide_2d.py +++ b/meanas/fdfd/waveguide_2d.py @@ -373,8 +373,10 @@ def normalized_fields_e( """ e = exy2e(wavenumber=wavenumber, dxes=dxes, epsilon=epsilon) @ e_xy h = exy2h(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ e_xy - e_norm, h_norm = _normalized_fields(e=e, h=h, omega=omega, dxes=dxes, epsilon=epsilon, - mu=mu, prop_phase=prop_phase) + e_norm, h_norm = _normalized_fields( # type: ignore[call-arg] + e=e, h=h, omega=omega, dxes=dxes, epsilon=epsilon, + mu=mu, prop_phase=prop_phase, + ) return e_norm, h_norm @@ -415,8 +417,10 @@ def normalized_fields_h( """ e = hxy2e(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ h_xy h = hxy2h(wavenumber=wavenumber, dxes=dxes, mu=mu) @ h_xy - e_norm, h_norm = _normalized_fields(e=e, h=h, omega=omega, dxes=dxes, epsilon=epsilon, - mu=mu, prop_phase=prop_phase) + e_norm, h_norm = _normalized_fields( # type: ignore[call-arg] + e=e, h=h, omega=omega, dxes=dxes, epsilon=epsilon, + mu=mu, prop_phase=prop_phase, + ) return e_norm, h_norm diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py index e4e2666..d63accb 100644 --- a/meanas/fdfd/waveguide_cyl.py +++ b/meanas/fdfd/waveguide_cyl.py @@ -529,8 +529,10 @@ def normalized_fields_e( """ e = exy2e(angular_wavenumber=angular_wavenumber, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon) @ e_xy h = exy2h(angular_wavenumber=angular_wavenumber, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon, mu=mu) @ e_xy - e_norm, h_norm = _normalized_fields(e=e, h=h, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon, - mu=mu, prop_phase=prop_phase) + e_norm, h_norm = _normalized_fields( # type: ignore[call-arg] + e=e, h=h, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon, + mu=mu, prop_phase=prop_phase, + ) return e_norm, h_norm diff --git a/meanas/test/test_waveguide_fdtd_fdfd.py b/meanas/test/test_waveguide_fdtd_fdfd.py index 9d42bf5..ad9a4e6 100644 --- a/meanas/test/test_waveguide_fdtd_fdfd.py +++ b/meanas/test/test_waveguide_fdtd_fdfd.py @@ -380,7 +380,7 @@ def _run_straight_waveguide_case(variant: str) -> WaveguideCalibrationResult: axis=0, polarity=1, slices=MONITOR_SLICES, - omega=OMEGA, + omega=OMEGA, # type: ignore[call-arg] ) update_e, update_h = fdtd.updates_with_cpml(cpml_params=_build_cpml_params(), dt=DT, dxes=base_dxes, epsilon=epsilon) @@ -488,7 +488,7 @@ def _run_width_step_scattering_case() -> WaveguideScatteringResult: axis=0, polarity=-1, slices=SCATTERING_REFLECT_SLICES, - omega=OMEGA, + omega=OMEGA, # type: ignore[call-arg] ) transmitted_mode = waveguide_3d.solve_mode( 0, @@ -506,7 +506,7 @@ def _run_width_step_scattering_case() -> WaveguideScatteringResult: axis=0, polarity=1, slices=SCATTERING_TRANSMIT_SLICES, - omega=OMEGA, + omega=OMEGA, # type: ignore[call-arg] ) update_e, update_h = fdtd.updates_with_cpml(cpml_params=_build_cpml_params(), dt=DT, dxes=base_dxes, epsilon=epsilon) @@ -621,7 +621,7 @@ def _run_pulsed_straight_waveguide_case() -> PulsedWaveguideCalibrationResult: axis=0, polarity=1, slices=MONITOR_SLICES, - omega=OMEGA, + omega=OMEGA, # type: ignore[call-arg] ) update_e, update_h = fdtd.updates_with_cpml(cpml_params=_build_cpml_params(), dt=DT, dxes=base_dxes, epsilon=epsilon, dtype=complex) diff --git a/meanas/test/test_waveguide_mode_helpers.py b/meanas/test/test_waveguide_mode_helpers.py index ca2d917..162c634 100644 --- a/meanas/test/test_waveguide_mode_helpers.py +++ b/meanas/test/test_waveguide_mode_helpers.py @@ -100,7 +100,7 @@ def test_waveguide_3d_compute_overlap_e_uses_adjacent_window( axis=0, polarity=polarity, slices=slices, - omega=OMEGA, + omega=OMEGA, # type: ignore[call-arg] ) nonzero = numpy.argwhere(numpy.abs(overlap) > 0) @@ -130,7 +130,7 @@ def test_waveguide_3d_compute_overlap_e_warns_when_window_is_clipped( axis=0, polarity=polarity, slices=slices, - omega=OMEGA, + omega=OMEGA, # type: ignore[call-arg] ) nonzero = numpy.argwhere(numpy.abs(overlap) > 0) @@ -158,7 +158,7 @@ def test_waveguide_3d_compute_overlap_e_rejects_empty_overlap_window( axis=0, polarity=polarity, slices=slices, - omega=OMEGA, + omega=OMEGA, # type: ignore[call-arg] ) @@ -173,7 +173,7 @@ def test_waveguide_3d_compute_overlap_e_rejects_zero_support_window() -> None: axis=0, polarity=1, slices=slices, - omega=OMEGA, + omega=OMEGA, # type: ignore[call-arg] ) From 35fc67faa3cbc9892abfbbe2a4e9f53bcfa53f77 Mon Sep 17 00:00:00 2001 From: Forgejo Actions Date: Wed, 22 Apr 2026 21:08:12 -0700 Subject: [PATCH 6/9] [compute_overlap_e] remove omega arg (unused) --- examples/waveguide.py | 2 +- meanas/fdfd/waveguide_3d.py | 1 - meanas/test/test_waveguide_fdtd_fdfd.py | 4 ---- meanas/test/test_waveguide_mode_helpers.py | 4 ---- 4 files changed, 1 insertion(+), 10 deletions(-) diff --git a/examples/waveguide.py b/examples/waveguide.py index 7becd59..f243924 100644 --- a/examples/waveguide.py +++ b/examples/waveguide.py @@ -100,7 +100,7 @@ def get_waveguide_mode( # compute_overlap_e() returns the normalized upstream overlap window used to # project another field onto this same guided mode. - e_overlap = waveguide_3d.compute_overlap_e(E=wg_results['E'], wavenumber=wg_results['wavenumber'], **wg_args, omega=omega) + e_overlap = waveguide_3d.compute_overlap_e(E=wg_results['E'], wavenumber=wg_results['wavenumber'], **wg_args) return J, e_overlap diff --git a/meanas/fdfd/waveguide_3d.py b/meanas/fdfd/waveguide_3d.py index 01db9b1..c77c8d4 100644 --- a/meanas/fdfd/waveguide_3d.py +++ b/meanas/fdfd/waveguide_3d.py @@ -196,7 +196,6 @@ def compute_overlap_e( axis: int, polarity: int, slices: Sequence[slice], - _omega: float, ) -> cfdfield_t: r""" Build an overlap field for projecting another 3D electric field onto a mode. diff --git a/meanas/test/test_waveguide_fdtd_fdfd.py b/meanas/test/test_waveguide_fdtd_fdfd.py index ad9a4e6..0488197 100644 --- a/meanas/test/test_waveguide_fdtd_fdfd.py +++ b/meanas/test/test_waveguide_fdtd_fdfd.py @@ -380,7 +380,6 @@ def _run_straight_waveguide_case(variant: str) -> WaveguideCalibrationResult: axis=0, polarity=1, slices=MONITOR_SLICES, - omega=OMEGA, # type: ignore[call-arg] ) update_e, update_h = fdtd.updates_with_cpml(cpml_params=_build_cpml_params(), dt=DT, dxes=base_dxes, epsilon=epsilon) @@ -488,7 +487,6 @@ def _run_width_step_scattering_case() -> WaveguideScatteringResult: axis=0, polarity=-1, slices=SCATTERING_REFLECT_SLICES, - omega=OMEGA, # type: ignore[call-arg] ) transmitted_mode = waveguide_3d.solve_mode( 0, @@ -506,7 +504,6 @@ def _run_width_step_scattering_case() -> WaveguideScatteringResult: axis=0, polarity=1, slices=SCATTERING_TRANSMIT_SLICES, - omega=OMEGA, # type: ignore[call-arg] ) update_e, update_h = fdtd.updates_with_cpml(cpml_params=_build_cpml_params(), dt=DT, dxes=base_dxes, epsilon=epsilon) @@ -621,7 +618,6 @@ def _run_pulsed_straight_waveguide_case() -> PulsedWaveguideCalibrationResult: axis=0, polarity=1, slices=MONITOR_SLICES, - omega=OMEGA, # type: ignore[call-arg] ) update_e, update_h = fdtd.updates_with_cpml(cpml_params=_build_cpml_params(), dt=DT, dxes=base_dxes, epsilon=epsilon, dtype=complex) diff --git a/meanas/test/test_waveguide_mode_helpers.py b/meanas/test/test_waveguide_mode_helpers.py index 162c634..d3ec7cd 100644 --- a/meanas/test/test_waveguide_mode_helpers.py +++ b/meanas/test/test_waveguide_mode_helpers.py @@ -100,7 +100,6 @@ def test_waveguide_3d_compute_overlap_e_uses_adjacent_window( axis=0, polarity=polarity, slices=slices, - omega=OMEGA, # type: ignore[call-arg] ) nonzero = numpy.argwhere(numpy.abs(overlap) > 0) @@ -130,7 +129,6 @@ def test_waveguide_3d_compute_overlap_e_warns_when_window_is_clipped( axis=0, polarity=polarity, slices=slices, - omega=OMEGA, # type: ignore[call-arg] ) nonzero = numpy.argwhere(numpy.abs(overlap) > 0) @@ -158,7 +156,6 @@ def test_waveguide_3d_compute_overlap_e_rejects_empty_overlap_window( axis=0, polarity=polarity, slices=slices, - omega=OMEGA, # type: ignore[call-arg] ) @@ -173,7 +170,6 @@ def test_waveguide_3d_compute_overlap_e_rejects_zero_support_window() -> None: axis=0, polarity=1, slices=slices, - omega=OMEGA, # type: ignore[call-arg] ) From 061c3f2e907b140b7226ea6aec3231294fbbb7ba Mon Sep 17 00:00:00 2001 From: Forgejo Actions Date: Wed, 22 Apr 2026 21:09:59 -0700 Subject: [PATCH 7/9] [_normalized_fields] remove unused args --- meanas/fdfd/waveguide_2d.py | 12 ++++-------- meanas/fdfd/waveguide_cyl.py | 8 ++------ 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/meanas/fdfd/waveguide_2d.py b/meanas/fdfd/waveguide_2d.py index aab7944..e67160e 100644 --- a/meanas/fdfd/waveguide_2d.py +++ b/meanas/fdfd/waveguide_2d.py @@ -373,9 +373,8 @@ def normalized_fields_e( """ e = exy2e(wavenumber=wavenumber, dxes=dxes, epsilon=epsilon) @ e_xy h = exy2h(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ e_xy - e_norm, h_norm = _normalized_fields( # type: ignore[call-arg] - e=e, h=h, omega=omega, dxes=dxes, epsilon=epsilon, - mu=mu, prop_phase=prop_phase, + e_norm, h_norm = _normalized_fields( + e=e, h=h, dxes=dxes, epsilon=epsilon, prop_phase=prop_phase, ) return e_norm, h_norm @@ -417,9 +416,8 @@ def normalized_fields_h( """ e = hxy2e(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ h_xy h = hxy2h(wavenumber=wavenumber, dxes=dxes, mu=mu) @ h_xy - e_norm, h_norm = _normalized_fields( # type: ignore[call-arg] - e=e, h=h, omega=omega, dxes=dxes, epsilon=epsilon, - mu=mu, prop_phase=prop_phase, + e_norm, h_norm = _normalized_fields( + e=e, h=h, dxes=dxes, epsilon=epsilon, prop_phase=prop_phase, ) return e_norm, h_norm @@ -427,10 +425,8 @@ def normalized_fields_h( def _normalized_fields( e: vcfdslice, h: vcfdslice, - _omega: complex, dxes: dx_lists2_t, epsilon: vfdslice, - _mu: vfdslice | None = None, prop_phase: float = 0, ) -> tuple[vcfdslice_t, vcfdslice_t]: r""" diff --git a/meanas/fdfd/waveguide_cyl.py b/meanas/fdfd/waveguide_cyl.py index d63accb..f2cb5c3 100644 --- a/meanas/fdfd/waveguide_cyl.py +++ b/meanas/fdfd/waveguide_cyl.py @@ -529,9 +529,8 @@ def normalized_fields_e( """ e = exy2e(angular_wavenumber=angular_wavenumber, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon) @ e_xy h = exy2h(angular_wavenumber=angular_wavenumber, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon, mu=mu) @ e_xy - e_norm, h_norm = _normalized_fields( # type: ignore[call-arg] - e=e, h=h, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon, - mu=mu, prop_phase=prop_phase, + e_norm, h_norm = _normalized_fields( + e=e, h=h, dxes=dxes, epsilon=epsilon, prop_phase=prop_phase, ) return e_norm, h_norm @@ -539,11 +538,8 @@ def normalized_fields_e( def _normalized_fields( e: vcfdslice, h: vcfdslice, - _omega: complex, dxes: dx_lists2_t, - _rmin: float, # Currently unused, but may want to use cylindrical poynting epsilon: vfdslice, - _mu: vfdslice | None = None, prop_phase: float = 0, ) -> tuple[vcfdslice_t, vcfdslice_t]: r""" From 39291a8314d65a6abeef68a8939931390f2af7c0 Mon Sep 17 00:00:00 2001 From: Forgejo Actions Date: Wed, 22 Apr 2026 21:10:18 -0700 Subject: [PATCH 8/9] [eme] add analytic tests --- meanas/test/test_eme_numerics.py | 248 +++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) diff --git a/meanas/test/test_eme_numerics.py b/meanas/test/test_eme_numerics.py index 2949e4c..3237c1b 100644 --- a/meanas/test/test_eme_numerics.py +++ b/meanas/test/test_eme_numerics.py @@ -218,6 +218,81 @@ def _build_bend_mode() -> tuple[tuple[numpy.ndarray, numpy.ndarray], complex]: return (e_field, h_field), linear_wavenumber +def _build_uniform_mode(index: float) -> tuple[tuple[numpy.ndarray, numpy.ndarray], complex]: + area = 25.0 + e_field = numpy.zeros((3, 5, 5), dtype=complex) + h_field = numpy.zeros((3, 5, 5), dtype=complex) + e_field[0] = numpy.sqrt(2.0 / (index * area)) + h_field[1] = numpy.sqrt(2.0 * index / area) + return (vec(e_field), vec(h_field)), complex(index * OMEGA) + + +def _interface_s(n_left: float, n_right: float) -> numpy.ndarray: + left_mode, left_beta = _build_uniform_mode(n_left) + right_mode, right_beta = _build_uniform_mode(n_right) + return eme.get_s([left_mode], [left_beta], [right_mode], [right_beta], dxes=REAL_DXES) + + +def _interface_abcd(n_left: float, n_right: float) -> numpy.ndarray: + left_mode, left_beta = _build_uniform_mode(n_left) + right_mode, right_beta = _build_uniform_mode(n_right) + return eme.get_abcd([left_mode], [left_beta], [right_mode], [right_beta], dxes=REAL_DXES).toarray() + + +def _expected_interface_s(n_left: float, n_right: float) -> numpy.ndarray: + reflection = (n_left - n_right) / (n_left + n_right) + transmission = 2 * numpy.sqrt(n_left * n_right) / (n_left + n_right) + return numpy.array( + [ + [reflection, transmission], + [transmission, -reflection], + ], + dtype=complex, + ) + + +def _propagation_abcd(beta: complex, length: float) -> numpy.ndarray: + phase = numpy.exp(-1j * beta * length) + return numpy.array( + [ + [phase, 0.0], + [0.0, phase ** -1], + ], + dtype=complex, + ) + + +def _abcd_to_s(abcd: numpy.ndarray) -> numpy.ndarray: + aa = abcd[0, 0] + bb = abcd[0, 1] + cc = abcd[1, 0] + dd = abcd[1, 1] + t21 = 1.0 / dd + r21 = bb / dd + r12 = -cc / dd + t12 = aa - bb * cc / dd + return numpy.array( + [ + [r12, t12], + [t21, r21], + ], + dtype=complex, + ) + + +def _expected_bragg_reflector_s(n_low: float, n_high: float, periods: int) -> numpy.ndarray: + ratio = n_high / n_low + reflection = (1 - ratio ** (2 * periods)) / (1 + ratio ** (2 * periods)) + transmission = ((-1) ** periods) * 2 * ratio ** periods / (1 + ratio ** (2 * periods)) + return numpy.array( + [ + [reflection, transmission], + [transmission, -reflection], + ], + dtype=complex, + ) + + def test_get_s_is_near_identity_for_identical_solved_straight_modes() -> None: mode, wavenumber, _epsilon = _build_straight_mode() @@ -241,3 +316,176 @@ def test_get_s_returns_finite_passive_output_for_small_straight_to_bend_fixture( assert ss.shape == (2, 2) assert numpy.isfinite(ss).all() assert numpy.linalg.svd(ss, compute_uv=False).max() <= 1.0 + 1e-10 + + +def test_get_s_matches_analytic_fresnel_interface_for_uniform_one_mode_ports() -> None: + """ + For power-normalized one-mode ports at normal incidence, the interface matrix is + + r12 = (n_left - n_right) / (n_left + n_right) + r21 = -r12 + t12 = t21 = 2 * sqrt(n_left * n_right) / (n_left + n_right) + + so + + S = [[r12, t12], [t21, r21]]. + """ + ss = _interface_s(1.0, 2.0) + expected = _expected_interface_s(1.0, 2.0) + + assert ss.shape == (2, 2) + assert numpy.isfinite(ss).all() + assert_close(ss, expected, atol=1e-6, rtol=1e-6) + assert numpy.linalg.svd(ss, compute_uv=False).max() <= 1.0 + 1e-10 + + +def test_quarter_wave_matching_layer_is_nearly_reflectionless_at_design_frequency() -> None: + """ + A single quarter-wave matching layer with + + n1 = sqrt(n0 * n2), beta1 * L = pi / 2 + + cancels the two interface reflections at the design wavelength, so the + normal-incidence stack should satisfy `r = 0` and `|t| = 1`. + """ + n0 = 1.0 + n1 = numpy.sqrt(2.0) + n2 = 2.0 + interface_01 = _interface_abcd(n0, n1) + interface_12 = _interface_abcd(n1, n2) + _mode_1, beta_1 = _build_uniform_mode(float(n1)) + quarter_wave_length = numpy.pi / (2 * numpy.real(beta_1)) + + stack_abcd = interface_01 @ _propagation_abcd(beta_1, quarter_wave_length) @ interface_12 + ss = _abcd_to_s(stack_abcd) + + assert ss.shape == (2, 2) + assert numpy.isfinite(ss).all() + assert abs(ss[0, 0]) < 1e-12 + assert abs(ss[1, 1]) < 1e-12 + assert abs(abs(ss[0, 1]) - 1.0) < 1e-12 + assert abs(abs(ss[1, 0]) - 1.0) < 1e-12 + assert numpy.linalg.svd(ss, compute_uv=False).max() <= 1.0 + 1e-10 + + +def test_quarter_wave_ar_layer_reduces_reflection_relative_to_abrupt_interface() -> None: + """ + Compare the abrupt interface `n0 -> n2` against the quarter-wave matching-layer + stack `n0 -> sqrt(n0 n2) -> n2` at the same design wavelength. + + For the canonical `n0 = 1`, `n2 = 2` case, the abrupt interface has + + |r_abrupt| = |(n0 - n2) / (n0 + n2)| = 1 / 3, + + while the quarter-wave matching layer should cancel the interface reflections + so that `|r_ar|` is essentially zero and `|t_ar|` is correspondingly larger. + """ + n0 = 1.0 + n2 = 2.0 + abrupt = _interface_s(n0, n2) + + n1 = numpy.sqrt(n0 * n2) + interface_01 = _interface_abcd(n0, n1) + interface_12 = _interface_abcd(n1, n2) + _mode_1, beta_1 = _build_uniform_mode(float(n1)) + quarter_wave_length = numpy.pi / (2 * numpy.real(beta_1)) + ar_stack = _abcd_to_s(interface_01 @ _propagation_abcd(beta_1, quarter_wave_length) @ interface_12) + + abrupt_reflection = abs(abrupt[0, 0]) + abrupt_transmission = abs(abrupt[1, 0]) + ar_reflection = abs(ar_stack[0, 0]) + ar_transmission = abs(ar_stack[1, 0]) + + assert numpy.linalg.svd(abrupt, compute_uv=False).max() <= 1.0 + 1e-10 + assert numpy.linalg.svd(ar_stack, compute_uv=False).max() <= 1.0 + 1e-10 + assert ar_reflection < abrupt_reflection + assert ar_transmission > abrupt_transmission + assert ar_reflection < 1e-12 + assert abs(abrupt_reflection - (1.0 / 3.0)) < 1e-12 + + +def test_half_wave_uniform_slab_restores_unit_transmission_between_matched_media() -> None: + """ + For matched exterior media `n0 = n2`, a half-wave slab with + + beta1 * L = pi + + contributes only a global phase, so the stack returns to `r = 0` and + `|t| = 1` at the design wavelength. + """ + n0 = 1.0 + n1 = 2.0 + interface_01 = _interface_abcd(n0, n1) + interface_10 = _interface_abcd(n1, n0) + _mode_1, beta_1 = _build_uniform_mode(n1) + half_wave_length = numpy.pi / numpy.real(beta_1) + + stack_abcd = interface_01 @ _propagation_abcd(beta_1, half_wave_length) @ interface_10 + ss = _abcd_to_s(stack_abcd) + + assert ss.shape == (2, 2) + assert numpy.isfinite(ss).all() + assert abs(ss[0, 0]) < 1e-12 + assert abs(ss[1, 1]) < 1e-12 + assert abs(abs(ss[0, 1]) - 1.0) < 1e-12 + assert abs(abs(ss[1, 0]) - 1.0) < 1e-12 + assert numpy.linalg.svd(ss, compute_uv=False).max() <= 1.0 + 1e-10 + + +def test_strong_uniform_index_mismatch_behaves_like_near_termination() -> None: + """ + In the large-index-ratio limit, the same Fresnel formulas approach a hard-wall + reflector: + + |r| -> 1, |t| -> 0 as n_right / n_left -> infinity. + """ + ss = _interface_s(1.0, 100.0) + expected = _expected_interface_s(1.0, 100.0) + + assert ss.shape == (2, 2) + assert numpy.isfinite(ss).all() + assert_close(ss, expected, atol=1e-6, rtol=1e-6) + assert abs(ss[0, 0]) > 0.9 + assert abs(ss[1, 0]) < 0.25 + assert numpy.linalg.svd(ss, compute_uv=False).max() <= 1.0 + 1e-10 + + +def test_quarter_wave_bragg_reflector_matches_closed_form_stopband_response() -> None: + """ + For `N` quarter-wave high/low periods at the Bragg wavelength with identical + low-index incident and exit media (`n0 = ns = n_low`), + + M_pair = diag(-(n_low / n_high), -(n_high / n_low)) + M_stack = M_pair ** N + + which yields the closed-form scattering amplitudes + + r = (1 - (n_high / n_low) ** (2N)) / (1 + (n_high / n_low) ** (2N)) + t = 2 * (n_high / n_low) ** N / (1 + (n_high / n_low) ** (2N)). + + The reflector should therefore sit deep in the stopband with `|r|` near 1 and + `|t|` correspondingly small. + """ + n_low = 1.0 + n_high = 2.0 + periods = 5 + interface_lh = _interface_abcd(n_low, n_high) + interface_hl = _interface_abcd(n_high, n_low) + _mode_h, beta_h = _build_uniform_mode(n_high) + _mode_l, beta_l = _build_uniform_mode(n_low) + quarter_wave_high = numpy.pi / (2 * numpy.real(beta_h)) + quarter_wave_low = numpy.pi / (2 * numpy.real(beta_l)) + + stack_abcd = numpy.eye(2, dtype=complex) + for _ in range(periods): + stack_abcd = stack_abcd @ interface_lh @ _propagation_abcd(beta_h, quarter_wave_high) + stack_abcd = stack_abcd @ interface_hl @ _propagation_abcd(beta_l, quarter_wave_low) + ss = _abcd_to_s(stack_abcd) + expected = _expected_bragg_reflector_s(n_low, n_high, periods) + + assert ss.shape == (2, 2) + assert numpy.isfinite(ss).all() + assert_close(ss, expected, atol=1e-12, rtol=1e-12) + assert abs(ss[0, 0]) > 0.99 + assert abs(ss[1, 0]) < 0.1 + assert numpy.linalg.svd(ss, compute_uv=False).max() <= 1.0 + 1e-10 From bf99f35f9b0e9ce1632eec2efe13581ebff2b983 Mon Sep 17 00:00:00 2001 From: Forgejo Actions Date: Wed, 22 Apr 2026 21:11:05 -0700 Subject: [PATCH 9/9] bump version to v0.12 --- meanas/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meanas/__init__.py b/meanas/__init__.py index 5ea8d42..9d1b401 100644 --- a/meanas/__init__.py +++ b/meanas/__init__.py @@ -7,7 +7,7 @@ toolbox overview and API derivations. import pathlib -__version__ = '0.11' +__version__ = '0.12' __author__ = 'Jan Petykiewicz'