Compare commits

...

9 commits

Author SHA1 Message Date
Forgejo Actions
bf99f35f9b bump version to v0.12 2026-04-22 21:11:05 -07:00
Forgejo Actions
39291a8314 [eme] add analytic tests 2026-04-22 21:10:18 -07:00
Forgejo Actions
061c3f2e90 [_normalized_fields] remove unused args 2026-04-22 21:09:59 -07:00
Forgejo Actions
35fc67faa3 [compute_overlap_e] remove omega arg (unused) 2026-04-22 21:08:12 -07:00
Forgejo Actions
a1568a6f16 ignore some lint 2026-04-21 21:20:34 -07:00
Forgejo Actions
c6c9159b13 type hints and lint 2026-04-21 21:13:34 -07:00
Forgejo Actions
eec3fc28a7 [docs] update colors 2026-04-21 19:51:57 -07:00
Forgejo Actions
010da1ccf5 [tests] add some slow tests 2026-04-21 19:40:49 -07:00
Forgejo Actions
1a2c6ab524 [EME] add more docs and tests 2026-04-21 19:40:32 -07:00
36 changed files with 837 additions and 172 deletions

View file

@ -95,9 +95,13 @@ source my_venv/bin/activate
# Install in-place (-e, editable) from ./meanas, including development dependencies ([dev]) # Install in-place (-e, editable) from ./meanas, including development dependencies ([dev])
pip3 install --user -e './meanas[dev]' pip3 install --user -e './meanas[dev]'
# Run tests # Fast local iteration: excludes slower 3D/integration/example-smoke checks
cd meanas 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: #### See also:

View file

@ -13,19 +13,31 @@
} }
[data-md-color-scheme="slate"] { [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: #e8eef7;
--md-default-fg-color--light: #b3bfd1; --md-default-fg-color--light: #b3bfd1;
--md-default-fg-color--lighter: #7f8ba0; --md-default-fg-color--lighter: #7f8ba0;
--md-default-fg-color--lightest: #5d6880; --md-default-fg-color--lightest: #5d6880;
--md-code-bg-color: #111923; --md-code-bg-color: #050505;
--md-code-fg-color: #e4edf8; --md-code-fg-color: #e4edf8;
--md-accent-fg-color: #7dd3fc; --md-accent-fg-color: #7dd3fc;
} }
[data-md-color-scheme="slate"] .md-header, [data-md-color-scheme="slate"] .md-header,
[data-md-color-scheme="slate"] .md-tabs { [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, [data-md-color-scheme="slate"] .md-typeset pre > code,
@ -34,7 +46,7 @@
} }
[data-md-color-scheme="slate"] .md-typeset table:not([class]) { [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 { [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 .admonition,
[data-md-color-scheme="slate"] .md-typeset details { [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); border-color: rgba(125, 211, 252, 0.2);
} }

View file

@ -14,6 +14,7 @@ simple straight interface:
from __future__ import annotations from __future__ import annotations
import importlib import importlib
from typing import TYPE_CHECKING
import numpy import numpy
from numpy import pi from numpy import pi
@ -24,6 +25,9 @@ from gridlock import Extent
from meanas.fdfd import eme, waveguide_2d from meanas.fdfd import eme, waveguide_2d
from meanas.fdmath import unvec from meanas.fdmath import unvec
if TYPE_CHECKING:
from types import ModuleType
WL = 1310.0 WL = 1310.0
DX = 40.0 DX = 40.0
@ -35,7 +39,7 @@ EPS_OX = 1.453 ** 2
MODE_NUMBERS = numpy.array([0]) 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 package_name = package_name or name
try: try:
return importlib.import_module(name) return importlib.import_module(name)
@ -159,7 +163,7 @@ def print_summary(ss: numpy.ndarray, wavenumbers_left: numpy.ndarray, wavenumber
def plot_results( def plot_results(
*, *,
pyplot, pyplot: ModuleType,
ss: numpy.ndarray, ss: numpy.ndarray,
left_mode: tuple[numpy.ndarray, numpy.ndarray], left_mode: tuple[numpy.ndarray, numpy.ndarray],
right_mode: tuple[numpy.ndarray, numpy.ndarray], right_mode: tuple[numpy.ndarray, numpy.ndarray],

View file

@ -15,6 +15,7 @@ This example demonstrates a cylindrical-waveguide EME workflow:
from __future__ import annotations from __future__ import annotations
import importlib import importlib
from typing import TYPE_CHECKING
import numpy import numpy
from numpy import pi from numpy import pi
@ -26,6 +27,9 @@ from gridlock import Extent
from meanas.fdfd import eme, waveguide_2d, waveguide_cyl from meanas.fdfd import eme, waveguide_2d, waveguide_cyl
from meanas.fdmath import unvec from meanas.fdmath import unvec
if TYPE_CHECKING:
from types import ModuleType
WL = 1310.0 WL = 1310.0
DX = 40.0 DX = 40.0
@ -40,7 +44,7 @@ STRAIGHT_SECTION_LENGTH = 12e3
BEND_ANGLE = pi / 2 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 package_name = package_name or name
try: try:
return importlib.import_module(name) return importlib.import_module(name)
@ -163,7 +167,7 @@ def solve_bend_modes(
def build_cascaded_network( def build_cascaded_network(
skrf, skrf: ModuleType,
*, *,
interface_s: numpy.ndarray, interface_s: numpy.ndarray,
straight_wavenumbers: numpy.ndarray, straight_wavenumbers: numpy.ndarray,
@ -216,7 +220,7 @@ def print_summary(
def plot_results( def plot_results(
*, *,
pyplot, pyplot: ModuleType,
interface_s: numpy.ndarray, interface_s: numpy.ndarray,
cascaded_s: numpy.ndarray, cascaded_s: numpy.ndarray,
straight_mode: tuple[numpy.ndarray, numpy.ndarray], straight_mode: tuple[numpy.ndarray, numpy.ndarray],

View file

@ -89,7 +89,7 @@ def perturbed_l3(a: float, radius: float, **kwargs) -> Pattern:
return pat return pat
def main(): def main() -> None:
dtype = numpy.float32 dtype = numpy.float32
max_t = 3600 # number of timesteps max_t = 3600 # number of timesteps
@ -97,7 +97,6 @@ def main():
pml_thickness = 8 # (number of cells) pml_thickness = 8 # (number of cells)
wl = 1550 # Excitation wavelength and fwhm wl = 1550 # Excitation wavelength and fwhm
dwl = 100
# Device design parameters # Device design parameters
xy_size = numpy.array([10, 10]) xy_size = numpy.array([10, 10])

View file

@ -100,7 +100,7 @@ def get_waveguide_mode(
# compute_overlap_e() returns the normalized upstream overlap window used to # compute_overlap_e() returns the normalized upstream overlap window used to
# project another field onto this same guided mode. # 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 return J, e_overlap

View file

@ -7,7 +7,7 @@ toolbox overview and API derivations.
import pathlib import pathlib
__version__ = '0.11' __version__ = '0.12'
__author__ = 'Jan Petykiewicz' __author__ = 'Jan Petykiewicz'

View file

@ -262,7 +262,7 @@ def maxwell_operator(
else: else:
# transform from mn to xyz # transform from mn to xyz
b_xyz = (m * b_m b_xyz = (m * b_m
+ n * b_n) # noqa: E128 + n * b_n) # noqa
# divide by mu # divide by mu
temp = ifftn(b_xyz, axes=range(3)) temp = ifftn(b_xyz, axes=range(3))
@ -409,7 +409,7 @@ def inverse_maxwell_operator_approx(
else: else:
# transform from mn to xyz # transform from mn to xyz
h_xyz = (m * hin_m h_xyz = (m * hin_m
+ n * hin_n) # noqa: E128 + n * hin_n) # noqa
# multiply by mu # multiply by mu
temp = ifftn(h_xyz, axes=range(3)) temp = ifftn(h_xyz, axes=range(3))
@ -474,7 +474,7 @@ def find_k(
`(k, actual_frequency, eigenvalues, eigenvectors)` `(k, actual_frequency, eigenvalues, eigenvectors)`
The found k-vector and its frequency, along with all eigenvalues and 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... k_bounds = tuple(sorted(k_bounds)) # type: ignore # we know the length already...
assert len(k_bounds) == 2 assert len(k_bounds) == 2
@ -504,7 +504,7 @@ def find_k(
assert n is not None assert n is not None
assert v is not None assert v is not None
actual_frequency = get_f(float(res.x), band) 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( def eigsolve(
@ -683,7 +683,16 @@ def eigsolve(
return numpy.abs(trace) return numpy.abs(trace)
if False: 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) Qi = Qi_func(theta)
c2 = numpy.cos(2 * theta) c2 = numpy.cos(2 * theta)
s2 = numpy.sin(2 * theta) s2 = numpy.sin(2 * theta)

View file

@ -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 from collections.abc import Sequence
import numpy import numpy
from numpy.typing import NDArray from numpy.typing import NDArray
@ -6,14 +27,70 @@ from scipy import sparse
from ..fdmath import dx_lists2_t, vcfdfield2 from ..fdmath import dx_lists2_t, vcfdfield2
from .waveguide_2d import inner_product 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: 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')
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( def get_tr(
ehLs: Sequence[Sequence[vcfdfield2]], ehLs: Sequence[Sequence[vcfdfield2]],
wavenumbers_L: Sequence[complex], wavenumbers_L: wavenumber_seq,
ehRs: Sequence[Sequence[vcfdfield2]], ehRs: Sequence[Sequence[vcfdfield2]],
wavenumbers_R: Sequence[complex], wavenumbers_R: wavenumber_seq,
dxes: dx_lists2_t, dxes: dx_lists2_t,
) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]: ) -> 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) nL = len(wavenumbers_L)
nR = len(wavenumbers_R) nR = len(wavenumbers_R)
A12 = numpy.zeros((nL, nR), dtype=complex) A12 = numpy.zeros((nL, nR), dtype=complex)
@ -43,11 +120,21 @@ def get_tr(
def get_abcd( def get_abcd(
ehLs: Sequence[Sequence[vcfdfield2]], ehLs: Sequence[Sequence[vcfdfield2]],
wavenumbers_L: Sequence[complex], wavenumbers_L: wavenumber_seq,
ehRs: Sequence[Sequence[vcfdfield2]], ehRs: Sequence[Sequence[vcfdfield2]],
wavenumbers_R: Sequence[complex], wavenumbers_R: wavenumber_seq,
**kwargs, **kwargs,
) -> sparse.sparray: ) -> 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) t12, r12 = get_tr(ehLs, wavenumbers_L, ehRs, wavenumbers_R, **kwargs)
t21, r21 = get_tr(ehRs, wavenumbers_R, ehLs, wavenumbers_L, **kwargs) t21, r21 = get_tr(ehRs, wavenumbers_R, ehLs, wavenumbers_L, **kwargs)
t21i = numpy.linalg.pinv(t21) t21i = numpy.linalg.pinv(t21)
@ -66,13 +153,26 @@ def get_abcd(
def get_s( def get_s(
ehLs: Sequence[Sequence[vcfdfield2]], ehLs: Sequence[Sequence[vcfdfield2]],
wavenumbers_L: Sequence[complex], wavenumbers_L: wavenumber_seq,
ehRs: Sequence[Sequence[vcfdfield2]], ehRs: Sequence[Sequence[vcfdfield2]],
wavenumbers_R: Sequence[complex], wavenumbers_R: wavenumber_seq,
force_nogain: bool = False, force_nogain: bool = False,
force_reciprocal: bool = False, force_reciprocal: bool = False,
**kwargs, **kwargs,
) -> NDArray[numpy.complex128]: ) -> 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) t12, r12 = get_tr(ehLs, wavenumbers_L, ehRs, wavenumbers_R, **kwargs)
t21, r21 = get_tr(ehRs, wavenumbers_R, ehLs, wavenumbers_L, **kwargs) t21, r21 = get_tr(ehRs, wavenumbers_R, ehLs, wavenumbers_L, **kwargs)

View file

@ -1,23 +1,24 @@
""" """
Functions for performing near-to-farfield transformation (and the reverse). 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 import numpy
from numpy.fft import fft2, fftshift, fftfreq, ifft2, ifftshift from numpy.fft import fft2, fftshift, fftfreq, ifft2, ifftshift
from numpy import pi from numpy import pi
from numpy.typing import NDArray
from numpy import complexfloating
from ..fdmath import cfdfield_t type farfield_slice = NDArray[complexfloating]
type transverse_slice_pair = Sequence[farfield_slice]
if TYPE_CHECKING:
from collections.abc import Sequence
def near_to_farfield( def near_to_farfield(
E_near: cfdfield_t, E_near: transverse_slice_pair,
H_near: cfdfield_t, H_near: transverse_slice_pair,
dx: float, dx: float,
dy: float, dy: float,
padded_size: list[int] | int | None = None padded_size: Sequence[int] | int | None = None
) -> dict[str, Any]: ) -> dict[str, Any]:
""" """
Compute the farfield, i.e. the distribution of the fields after propagation 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') raise Exception('H_near must be a length-2 list of ndarrays')
s = E_near[0].shape 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!') raise Exception('All fields must be the same shape!')
if padded_size is None: if padded_size is None:
@ -86,14 +87,14 @@ def near_to_farfield(
# Normalized vector potentials N, L # Normalized vector potentials N, L
N = [-Hn_fft[1] * cos_phi * cos_th + Hn_fft[0] * cos_phi * sin_th, 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, 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], E_far = [-L[1] - N[0],
L[0] - N[1]] # noqa: E127 L[0] - N[1]] # noqa
H_far = [-E_far[1], H_far = [-E_far[1],
E_far[0]] # noqa: E127 E_far[0]] # noqa
theta = numpy.arctan2(ky, kx) theta = numpy.arctan2(ky, kx)
phi = numpy.arccos(cos_phi) phi = numpy.arccos(cos_phi)
@ -123,11 +124,11 @@ def near_to_farfield(
def far_to_nearfield( def far_to_nearfield(
E_far: cfdfield_t, E_far: transverse_slice_pair,
H_far: cfdfield_t, H_far: transverse_slice_pair,
dkx: float, dkx: float,
dky: float, dky: float,
padded_size: list[int] | int | None = None padded_size: Sequence[int] | int | None = None
) -> dict[str, Any]: ) -> dict[str, Any]:
""" """
Compute the farfield, i.e. the distribution of the fields after propagation 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') raise Exception('H_far must be a length-2 list of ndarrays')
s = E_far[0].shape 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!') raise Exception('All fields must be the same shape!')
if padded_size is None: if padded_size is None:
@ -202,9 +203,9 @@ def far_to_nearfield(
# Normalized vector potentials N, L # Normalized vector potentials N, L
L = [0.5 * E_far[1], L = [0.5 * E_far[1],
-0.5 * E_far[0]] # noqa: E128 -0.5 * E_far[0]] # noqa
N = [L[1], N = [L[1],
-L[0]] # noqa: E128 -L[0]] # noqa
En_fft = [ En_fft = [
numpy.divide( numpy.divide(

View file

@ -373,8 +373,9 @@ def normalized_fields_e(
""" """
e = exy2e(wavenumber=wavenumber, dxes=dxes, epsilon=epsilon) @ e_xy e = exy2e(wavenumber=wavenumber, dxes=dxes, epsilon=epsilon) @ e_xy
h = exy2h(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ 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, e_norm, h_norm = _normalized_fields(
mu=mu, prop_phase=prop_phase) e=e, h=h, dxes=dxes, epsilon=epsilon, prop_phase=prop_phase,
)
return e_norm, h_norm return e_norm, h_norm
@ -415,18 +416,17 @@ def normalized_fields_h(
""" """
e = hxy2e(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ h_xy e = hxy2e(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ h_xy
h = hxy2h(wavenumber=wavenumber, dxes=dxes, 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, e_norm, h_norm = _normalized_fields(
mu=mu, prop_phase=prop_phase) e=e, h=h, dxes=dxes, epsilon=epsilon, prop_phase=prop_phase,
)
return e_norm, h_norm return e_norm, h_norm
def _normalized_fields( def _normalized_fields(
e: vcfdslice, e: vcfdslice,
h: vcfdslice, h: vcfdslice,
omega: complex,
dxes: dx_lists2_t, dxes: dx_lists2_t,
epsilon: vfdslice, epsilon: vfdslice,
mu: vfdslice | None = None,
prop_phase: float = 0, prop_phase: float = 0,
) -> tuple[vcfdslice_t, vcfdslice_t]: ) -> tuple[vcfdslice_t, vcfdslice_t]:
r""" r"""

View file

@ -19,9 +19,8 @@ The intended workflow is:
That same convention controls which side of the selected slice is used for the That same convention controls which side of the selected slice is used for the
overlap window and how the expanded field is phased. overlap window and how the expanded field is phased.
""" """
from typing import Any, cast from typing import Any, TypedDict, cast
import warnings import warnings
from typing import Any
from collections.abc import Sequence from collections.abc import Sequence
import numpy import numpy
from numpy.typing import NDArray 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 from . import operators, waveguide_2d
class Waveguide3DMode(TypedDict):
wavenumber: complex
wavenumber_2d: complex
H: NDArray[complexfloating]
E: NDArray[complexfloating]
def solve_mode( def solve_mode(
mode_number: int, mode_number: int,
omega: complex, omega: complex,
@ -40,7 +46,7 @@ def solve_mode(
slices: Sequence[slice], slices: Sequence[slice],
epsilon: fdfield, epsilon: fdfield,
mu: fdfield | None = None, mu: fdfield | None = None,
) -> dict[str, complex | NDArray[complexfloating]]: ) -> Waveguide3DMode:
r""" r"""
Given a 3D grid, selects a slice from the grid and attempts to Given a 3D grid, selects a slice from the grid and attempts to
solve for an eigenmode propagating through that slice. solve for an eigenmode propagating through that slice.
@ -121,7 +127,7 @@ def solve_mode(
E[iii] = e[oo][:, :, None].transpose(reverse_order) E[iii] = e[oo][:, :, None].transpose(reverse_order)
H[iii] = h[oo][:, :, None].transpose(reverse_order) H[iii] = h[oo][:, :, None].transpose(reverse_order)
results = { results: Waveguide3DMode = {
'wavenumber': wavenumber, 'wavenumber': wavenumber,
'wavenumber_2d': wavenumber_2d, 'wavenumber_2d': wavenumber_2d,
'H': H, 'H': H,
@ -184,13 +190,12 @@ def compute_source(
def compute_overlap_e( def compute_overlap_e(
E: cfdfield_t, E: cfdfield,
wavenumber: complex, wavenumber: complex,
dxes: dx_lists_t, dxes: dx_lists_t,
axis: int, axis: int,
polarity: int, polarity: int,
slices: Sequence[slice], slices: Sequence[slice],
omega: float,
) -> cfdfield_t: ) -> cfdfield_t:
r""" r"""
Build an overlap field for projecting another 3D electric field onto a mode. Build an overlap field for projecting another 3D electric field onto a mode.
@ -262,7 +267,7 @@ def compute_overlap_e(
if clipped_start >= clipped_stop: if clipped_start >= clipped_stop:
raise ValueError('Requested overlap window lies outside the domain') raise ValueError('Requested overlap window lies outside the domain')
if clipped_start != start or clipped_stop != stop: 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 = list(slices)
slices2_l[axis] = slice(clipped_start, clipped_stop) slices2_l[axis] = slice(clipped_start, clipped_stop)
@ -275,7 +280,7 @@ def compute_overlap_e(
norm = (Etgt.conj() * Etgt).sum() norm = (Etgt.conj() * Etgt).sum()
if norm == 0: if norm == 0:
raise ValueError('Requested overlap window contains no overlap field support') raise ValueError('Requested overlap window contains no overlap field support')
Etgt /= norm Etgt = Etgt / norm
return cfdfield_t(Etgt) return cfdfield_t(Etgt)

View file

@ -130,7 +130,7 @@ import numpy
from numpy.typing import NDArray, ArrayLike from numpy.typing import NDArray, ArrayLike
from scipy import sparse 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 ..fdmath.operators import deriv_forward, deriv_back
from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration
from . import waveguide_2d from . import waveguide_2d
@ -267,7 +267,7 @@ def solve_mode(
mode_number: int, mode_number: int,
*args: Any, *args: Any,
**kwargs: Any, **kwargs: Any,
) -> tuple[vcfdslice, complex]: ) -> tuple[vcfdfield2, complex]:
""" """
Wrapper around `solve_modes()` that solves for a single mode. Wrapper around `solve_modes()` that solves for a single mode.
@ -285,7 +285,7 @@ def solve_mode(
def linear_wavenumbers( def linear_wavenumbers(
e_xys: list[vcfdfield2_t], e_xys: Sequence[vcfdfield2] | NDArray[numpy.complex128],
angular_wavenumbers: ArrayLike, angular_wavenumbers: ArrayLike,
epsilon: vfdslice, epsilon: vfdslice,
dxes: dx_lists2_t, dxes: dx_lists2_t,
@ -529,19 +529,17 @@ def normalized_fields_e(
""" """
e = exy2e(angular_wavenumber=angular_wavenumber, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon) @ e_xy 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 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, e_norm, h_norm = _normalized_fields(
mu=mu, prop_phase=prop_phase) e=e, h=h, dxes=dxes, epsilon=epsilon, prop_phase=prop_phase,
)
return e_norm, h_norm return e_norm, h_norm
def _normalized_fields( def _normalized_fields(
e: vcfdslice, e: vcfdslice,
h: vcfdslice, h: vcfdslice,
omega: complex,
dxes: dx_lists2_t, dxes: dx_lists2_t,
rmin: float, # Currently unused, but may want to use cylindrical poynting
epsilon: vfdslice, epsilon: vfdslice,
mu: vfdslice | None = None,
prop_phase: float = 0, prop_phase: float = 0,
) -> tuple[vcfdslice_t, vcfdslice_t]: ) -> tuple[vcfdslice_t, vcfdslice_t]:
r""" r"""

View file

@ -10,7 +10,7 @@ import numpy
from numpy.typing import NDArray from numpy.typing import NDArray
from numpy import floating, complexfloating from numpy import floating, complexfloating
from .types import fdfield_t, fdfield_updater_t from .types import fdfield, fdfield_updater_t
def deriv_forward( def deriv_forward(
@ -127,7 +127,7 @@ def curl_forward_parts(
) -> Callable: ) -> Callable:
Dx, Dy, Dz = deriv_forward(dx_e) 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])), return ((-Dz(e[1]), Dy(e[2])),
( Dz(e[0]), -Dx(e[2])), ( Dz(e[0]), -Dx(e[2])),
(-Dy(e[0]), Dx(e[1]))) (-Dy(e[0]), Dx(e[1])))
@ -140,7 +140,7 @@ def curl_back_parts(
) -> Callable: ) -> Callable:
Dx, Dy, Dz = deriv_back(dx_h) 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])), return ((-Dz(h[1]), Dy(h[2])),
( Dz(h[0]), -Dx(h[2])), ( Dz(h[0]), -Dx(h[2])),
(-Dy(h[0]), Dx(h[1]))) (-Dy(h[0]), Dx(h[1])))

View file

@ -9,7 +9,7 @@ from numpy.typing import NDArray
from numpy import floating, complexfloating from numpy import floating, complexfloating
from scipy import sparse from scipy import sparse
from .types import vfdfield_t from .types import vfdfield
def shift_circ( def shift_circ(
@ -171,7 +171,7 @@ def cross(
[-B[1], B[0], zero]]) [-B[1], B[0], zero]])
def vec_cross(b: vfdfield_t) -> sparse.sparray: def vec_cross(b: vfdfield) -> sparse.sparray:
""" """
Vector cross product operator Vector cross product operator

View file

@ -88,8 +88,8 @@ dx_lists2_mut = MutableSequence[MutableSequence[NDArray[floating | complexfloati
"""Mutable version of `dx_lists2_t`""" """Mutable version of `dx_lists2_t`"""
fdfield_updater_t = Callable[..., fdfield_t] fdfield_updater_t = Callable[..., fdfield]
"""Convenience type for functions which take and return an fdfield_t""" """Convenience type for functions which take and return a real `fdfield`"""
cfdfield_updater_t = Callable[..., cfdfield_t] cfdfield_updater_t = Callable[..., cfdfield]
"""Convenience type for functions which take and return an cfdfield_t""" """Convenience type for functions which take and return a complex `cfdfield`"""

View file

@ -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 from ..fdmath.functional import curl_forward, curl_back
@ -47,7 +47,7 @@ def maxwell_e(
else: else:
curl_h_fun = curl_back() 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. Update the E-field.
@ -103,7 +103,7 @@ def maxwell_h(
else: else:
curl_e_fun = curl_forward() 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. Update the H-field.

View file

@ -6,7 +6,7 @@ Boundary conditions
from typing import Any from typing import Any
from ..fdmath import fdfield_t, fdfield_updater_t from ..fdmath import fdfield, fdfield_updater_t
def conducting_boundary( def conducting_boundary(
@ -15,7 +15,7 @@ def conducting_boundary(
) -> tuple[fdfield_updater_t, fdfield_updater_t]: ) -> tuple[fdfield_updater_t, fdfield_updater_t]:
dirs = [0, 1, 2] dirs = [0, 1, 2]
if direction not in dirs: if direction not in dirs:
raise Exception(f'Invalid direction: {direction}') raise ValueError(f'Invalid direction: {direction}')
dirs.remove(direction) dirs.remove(direction)
u, v = dirs u, v = dirs
@ -31,13 +31,13 @@ def conducting_boundary(
boundary = tuple(boundary_slice) boundary = tuple(boundary_slice)
shifted1 = tuple(shifted1_slice) shifted1 = tuple(shifted1_slice)
def en(e: fdfield_t) -> fdfield_t: def en(e: fdfield) -> fdfield:
e[direction][boundary] = 0 e[direction][boundary] = 0
e[u][boundary] = e[u][shifted1] e[u][boundary] = e[u][shifted1]
e[v][boundary] = e[v][shifted1] e[v][boundary] = e[v][shifted1]
return e return e
def hn(h: fdfield_t) -> fdfield_t: def hn(h: fdfield) -> fdfield:
h[direction][boundary] = h[direction][shifted1] h[direction][boundary] = h[direction][shifted1]
h[u][boundary] = 0 h[u][boundary] = 0
h[v][boundary] = 0 h[v][boundary] = 0
@ -56,14 +56,14 @@ def conducting_boundary(
shifted1 = tuple(shifted1_slice) shifted1 = tuple(shifted1_slice)
shifted2 = tuple(shifted2_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][boundary] = -e[direction][shifted2]
e[direction][shifted1] = 0 e[direction][shifted1] = 0
e[u][boundary] = e[u][shifted1] e[u][boundary] = e[u][shifted1]
e[v][boundary] = e[v][shifted1] e[v][boundary] = e[v][shifted1]
return e return e
def hp(h: fdfield_t) -> fdfield_t: def hp(h: fdfield) -> fdfield:
h[direction][boundary] = h[direction][shifted1] h[direction][boundary] = h[direction][shifted1]
h[u][boundary] = -h[u][shifted2] h[u][boundary] = -h[u][shifted2]
h[u][shifted1] = 0 h[u][shifted1] = 0
@ -73,4 +73,4 @@ def conducting_boundary(
return ep, hp return ep, hp
raise Exception(f'Bad polarity: {polarity}') raise ValueError(f'Bad polarity: {polarity}')

View file

@ -1,5 +1,6 @@
from collections.abc import Callable from collections.abc import Callable
import logging import logging
from typing import cast
import numpy import numpy
from numpy.typing import NDArray, ArrayLike from numpy.typing import NDArray, ArrayLike
@ -9,7 +10,14 @@ from numpy import pi
logger = logging.getLogger(__name__) 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( 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 delay = numpy.ceil(delay * freq) / freq # force delay to integer number of periods to maintain phase
logger.info(f'src_time {2 * delay / dt}') logger.info(f'src_time {2 * delay / dt}')
def source_phasor(ii: int | NDArray) -> tuple[float, float, float]: def source_phasor(ii: ArrayLike) -> tuple[pulse_scalar_t, pulse_scalar_t, pulse_scalar_t]:
t0 = ii * dt - delay 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) envelope = numpy.sqrt(numpy.sqrt(2 * alpha / pi)) * numpy.exp(-alpha * t0 * t0)
if one_sided: if one_sided:
@ -59,7 +68,7 @@ def gaussian_packet(
cc = numpy.cos(omega * t0) cc = numpy.cos(omega * t0)
ss = numpy.sin(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 # nrm = numpy.exp(-omega * omega / alpha) / 2
@ -105,15 +114,16 @@ def ricker_pulse(
delay = delay_results.root delay = delay_results.root
delay = numpy.ceil(delay * freq) / freq # force delay to integer number of periods to maintain phase 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]: def source_phasor(ii: ArrayLike) -> tuple[pulse_scalar_t, pulse_scalar_t, pulse_scalar_t]:
t0 = ii * dt - delay ii_array = numpy.asarray(ii, dtype=float)
t0 = ii_array * dt - delay
rr = omega * t0 / 2 rr = omega * t0 / 2
ff = (1 - 2 * rr * rr) * numpy.exp(-rr * rr) ff = (1 - 2 * rr * rr) * numpy.exp(-rr * rr)
cc = numpy.cos(omega * t0) cc = numpy.cos(omega * t0)
ss = numpy.sin(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 return source_phasor, delay

View file

@ -23,7 +23,7 @@ from copy import deepcopy
import numpy import numpy
from numpy.typing import NDArray, DTypeLike 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 from ..fdmath.functional import deriv_forward, deriv_back
@ -67,16 +67,16 @@ def cpml_params(
""" """
if axis not in range(3): if axis not in range(3):
raise Exception(f'Invalid axis: {axis}') raise ValueError(f'Invalid axis: {axis}')
if polarity not in (-1, 1): if polarity not in (-1, 1):
raise Exception(f'Invalid polarity: {polarity}') raise ValueError(f'Invalid polarity: {polarity}')
if thickness <= 2: 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: 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) sigma_max = -ln_R_per_layer / 2 * (m + 1)
kappa_max = numpy.sqrt(epsilon_eff * mu_eff) kappa_max = numpy.sqrt(epsilon_eff * mu_eff)
@ -129,8 +129,7 @@ def updates_with_cpml(
epsilon: fdfield, epsilon: fdfield,
*, *,
dtype: DTypeLike = numpy.float32, dtype: DTypeLike = numpy.float32,
) -> tuple[Callable[[fdfield_t, fdfield_t, fdfield_t], None], ) -> tuple[Callable[..., None], Callable[..., None]]:
Callable[[fdfield_t, fdfield_t, fdfield_t], None]]:
""" """
Build Yee-step update closures augmented with CPML terms. Build Yee-step update closures augmented with CPML terms.
@ -187,9 +186,9 @@ def updates_with_cpml(
pH = numpy.empty_like(epsilon, dtype=dtype) pH = numpy.empty_like(epsilon, dtype=dtype)
def update_E( def update_E(
e: fdfield_t, e: fdfield,
h: fdfield_t, h: fdfield,
epsilon: fdfield_t, epsilon: fdfield,
) -> None: ) -> None:
dyHx = Dby(h[0]) dyHx = Dby(h[0])
dzHx = Dbz(h[0]) dzHx = Dbz(h[0])
@ -233,9 +232,9 @@ def updates_with_cpml(
e[2] += dt / epsilon[2] * (dxHy - dyHx + pE[2]) e[2] += dt / epsilon[2] * (dxHy - dyHx + pE[2])
def update_H( def update_H(
e: fdfield_t, e: fdfield,
h: fdfield_t, h: fdfield,
mu: fdfield_t | tuple[int, int, int] = (1, 1, 1), mu: fdfield | tuple[int, int, int] = (1, 1, 1),
) -> None: ) -> None:
dyEx = Dfy(e[0]) dyEx = Dfy(e[0])
dzEx = Dfz(e[0]) dzEx = Dfz(e[0])

View file

@ -4,7 +4,7 @@ from numpy.testing import assert_allclose
from types import SimpleNamespace from types import SimpleNamespace
from ..fdfd import bloch 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 from .utils import assert_close

View file

@ -1,8 +1,11 @@
from typing import cast
import numpy import numpy
import pytest
from scipy import sparse from scipy import sparse
from ..fdmath import vec 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 ._test_builders import complex_ramp, unit_dxes
from .utils import assert_close from .utils import assert_close
@ -11,6 +14,8 @@ SHAPE = (3, 2, 2)
DXES = unit_dxes((2, 2)) DXES = unit_dxes((2, 2))
WAVENUMBERS_L = numpy.array([1.0, 0.8]) WAVENUMBERS_L = numpy.array([1.0, 0.8])
WAVENUMBERS_R = numpy.array([0.9, 0.7]) 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]: def _mode(scale: float) -> tuple[numpy.ndarray, numpy.ndarray]:
@ -48,6 +53,10 @@ def _nonsymmetric_tr(left_marker: object):
return fake_get_tr 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: def test_get_tr_returns_finite_bounded_transfer_matrices() -> None:
left_modes, right_modes = _mode_sets() left_modes, right_modes = _mode_sets()
@ -100,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: def test_get_s_force_nogain_caps_singular_values(monkeypatch) -> None:
monkeypatch.setattr(eme, 'get_tr', _gain_only_tr) monkeypatch.setattr(eme, 'get_tr', _gain_only_tr)
modes, wavenumbers = _dummy_modes()
plain_s = eme.get_s(None, None, None, None) plain_s = eme.get_s(modes, wavenumbers, modes, wavenumbers)
clipped_s = eme.get_s(None, None, None, None, force_nogain=True) clipped_s = eme.get_s(modes, wavenumbers, modes, wavenumbers, force_nogain=True)
plain_singular_values = numpy.linalg.svd(plain_s, compute_uv=False) plain_singular_values = numpy.linalg.svd(plain_s, compute_uv=False)
clipped_singular_values = numpy.linalg.svd(clipped_s, compute_uv=False) clipped_singular_values = numpy.linalg.svd(clipped_s, compute_uv=False)
@ -113,20 +123,369 @@ def test_get_s_force_nogain_caps_singular_values(monkeypatch) -> None:
def test_get_s_force_reciprocal_symmetrizes_output(monkeypatch) -> None: def test_get_s_force_reciprocal_symmetrizes_output(monkeypatch) -> None:
left = object() left = numpy.array([1.0, 0.5])
right = object() right = numpy.array([0.9, 0.4])
modes, _wavenumbers = _dummy_modes()
monkeypatch.setattr(eme, 'get_tr', _nonsymmetric_tr(left)) 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) assert_close(ss, ss.T)
def test_get_s_force_nogain_and_reciprocal_returns_finite_output(monkeypatch) -> None: def test_get_s_force_nogain_and_reciprocal_returns_finite_output(monkeypatch) -> None:
monkeypatch.setattr(eme, 'get_tr', _gain_and_reflection_tr) 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 ss.shape == (4, 4)
assert numpy.isfinite(ss).all() assert numpy.isfinite(ss).all()
assert_close(ss, ss.T) assert_close(ss, ss.T)
assert (numpy.linalg.svd(ss, compute_uv=False) <= 1.0 + 1e-12).all() 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 = 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, 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)
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 _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()
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
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

View file

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

View file

@ -85,8 +85,10 @@ def j_distribution(
other_dims = [0, 1, 2] other_dims = [0, 1, 2]
other_dims.remove(dim) other_dims.remove(dim)
dx_prop = (dxes[0][dim][shape[dim + 1] // 2] dx_prop = (
+ dxes[1][dim][shape[dim + 1] // 2]) / 2 # noqa: E128 # TODO is this right for nonuniform dxes? 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 # Mask only contains components orthogonal to propagation direction
center_mask = numpy.zeros(shape, dtype=bool) center_mask = numpy.zeros(shape, dtype=bool)

View file

@ -1,3 +1,5 @@
from typing import cast
import numpy import numpy
from ..fdfd import solvers 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: def test_generic_forward_preconditions_system_and_guess(monkeypatch) -> None:
case = solver_plumbing_case() 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', lambda *args, **kwargs: case.a0)
monkeypatch.setattr(solvers.operators, 'e_full_preconditioners', lambda dxes: (case.pl, case.pr)) 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, E_guess=case.guess,
) )
assert_close(captured['a'].toarray(), (case.pl @ case.a0 @ case.pr).toarray()) assert_close(cast(object, captured['a']).toarray(), (case.pl @ case.a0 @ case.pr).toarray()) # type: ignore[attr-defined]
assert_close(captured['b'], case.pl @ (-1j * case.omega * case.j)) assert_close(cast(numpy.ndarray, captured['b']), case.pl @ (-1j * case.omega * case.j))
assert_close(captured['x0'], case.pl @ case.guess) assert_close(cast(numpy.ndarray, captured['x0']), case.pl @ case.guess)
assert captured['atol'] == 1e-12 assert captured['atol'] == 1e-12
assert_close(result, case.pr @ case.solver_result) assert_close(result, case.pr @ case.solver_result)
def test_generic_adjoint_preconditions_system_and_guess(monkeypatch) -> None: def test_generic_adjoint_preconditions_system_and_guess(monkeypatch) -> None:
case = solver_plumbing_case() 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', lambda *args, **kwargs: case.a0)
monkeypatch.setattr(solvers.operators, 'e_full_preconditioners', lambda dxes: (case.pl, case.pr)) 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() expected_matrix = (case.pl @ case.a0 @ case.pr).T.conjugate()
assert_close(captured['a'].toarray(), expected_matrix.toarray()) assert_close(cast(object, captured['a']).toarray(), expected_matrix.toarray()) # type: ignore[attr-defined]
assert_close(captured['b'], case.pr.T.conjugate() @ (-1j * case.omega * case.j)) assert_close(cast(numpy.ndarray, captured['b']), case.pr.T.conjugate() @ (-1j * case.omega * case.j))
assert_close(captured['x0'], case.pr.T.conjugate() @ case.guess) assert_close(cast(numpy.ndarray, captured['x0']), case.pr.T.conjugate() @ case.guess)
assert captured['rtol'] == 1e-9 assert captured['rtol'] == 1e-9
assert_close(result, case.pl.T.conjugate() @ case.solver_result) 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, 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])) assert_close(result, case.pr @ numpy.array([1.0, -1.0]))

View file

@ -1,5 +1,3 @@
import numpy
from ..fdmath import functional as fd_functional from ..fdmath import functional as fd_functional
from ..fdtd import base from ..fdtd import base
from ._test_builders import real_ramp from ._test_builders import real_ramp

View file

@ -58,5 +58,5 @@ def test_conducting_boundary_updates_expected_faces(direction: int, polarity: in
[(-1, 1), (3, 1), (0, 0)], [(-1, 1), (3, 1), (0, 0)],
) )
def test_conducting_boundary_rejects_invalid_arguments(direction: int, polarity: int) -> None: 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) conducting_boundary(direction, polarity)

View file

@ -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) source, delay = gaussian_packet(1.55, 0.1, dt, one_sided=one_sided)
steps = numpy.array([0, int(numpy.ceil(delay / dt)) + 5]) steps = numpy.array([0, int(numpy.ceil(delay / dt)) + 5])
envelope, cc, ss = source(steps) 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 envelope.shape == (2,)
assert numpy.isfinite(envelope).all() assert numpy.isfinite(envelope).all()

View file

@ -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) 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) 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] scale = fdtd.real_injection_scale(waveform, omega, dt, offset_steps=0.5)[0]
j_accumulator = numpy.zeros((1, *full_shape), dtype=complex) j_accumulator = numpy.zeros((1, *full_shape), dtype=complex)

View file

@ -1,3 +1,5 @@
from typing import Any
import numpy import numpy
import pytest 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)], [(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: 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) 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) e = numpy.zeros(shape, dtype=float)
h = 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)] 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) params[0][0] = cpml_params(axis=0, polarity=-1, dt=0.1, thickness=3)
update_e, update_h = updates_with_cpml(params, dt=0.1, dxes=dxes, epsilon=epsilon) update_e, 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) e = _real_field(shape, 10.0)
h = _real_field(shape, 100.0) h = _real_field(shape, 100.0)
dxes = _unit_dxes(shape) 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_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) 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) e = _complex_field(shape, 10.0)
h = _complex_field(shape, 100.0) h = _complex_field(shape, 100.0)
dxes = _unit_dxes(shape) 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_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) 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) dxes = _unit_dxes(shape)
thickness = 3 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) 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) 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) epsilon = numpy.ones(shape, dtype=float)
dxes = _unit_dxes(shape) 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)): for polarity_index, polarity in enumerate((-1, 1)):
params[0][polarity_index] = cpml_params(axis=0, polarity=polarity, dt=dt, thickness=thickness) params[0][polarity_index] = cpml_params(axis=0, polarity=polarity, dt=dt, thickness=thickness)
@ -198,6 +200,7 @@ def test_cpml_plane_wave_phasor_decays_monotonically_through_outgoing_pml() -> N
assert numpy.all(numpy.diff(right_pml) <= interior_level * 1e-3) 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: def test_cpml_point_source_total_energy_reaches_late_time_plateau() -> None:
dt = 0.3 dt = 0.3
period_steps = 24 period_steps = 24
@ -211,7 +214,7 @@ def test_cpml_point_source_total_energy_reaches_late_time_plateau() -> None:
epsilon = numpy.ones(shape, dtype=float) epsilon = numpy.ones(shape, dtype=float)
dxes = _unit_dxes(shape) 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 axis in range(3):
for polarity_index, polarity in enumerate((-1, 1)): for polarity_index, polarity in enumerate((-1, 1)):
params[axis][polarity_index] = cpml_params(axis=axis, polarity=polarity, dt=dt, thickness=thickness) params[axis][polarity_index] = cpml_params(axis=axis, polarity=polarity, dt=dt, thickness=thickness)

View file

@ -1,25 +1,28 @@
import builtins import builtins
import importlib import importlib
import pathlib import pathlib
from types import ModuleType
from typing import Any
import pytest
import meanas import meanas
from ..fdfd import bloch from ..fdfd import bloch
from .utils import assert_close
def _reload(module): def _reload(module: ModuleType) -> ModuleType:
return importlib.reload(module) return importlib.reload(module)
def _restore_reloaded(monkeypatch, module): def _restore_reloaded(monkeypatch: pytest.MonkeyPatch, module: ModuleType) -> ModuleType:
monkeypatch.undo() monkeypatch.undo()
return _reload(module) 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 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': if self.name == 'README.md':
raise FileNotFoundError('forced README failure') raise FileNotFoundError('forced README failure')
return original_open(self, *args, **kwargs) return original_open(self, *args, **kwargs)
@ -27,17 +30,23 @@ def test_meanas_import_survives_readme_open_failure(monkeypatch) -> None: # typ
monkeypatch.setattr(pathlib.Path, 'open', failing_open) monkeypatch.setattr(pathlib.Path, 'open', failing_open)
reloaded = _reload(meanas) reloaded = _reload(meanas)
assert reloaded.__version__ == '0.10' assert reloaded.__version__ == expected_version
assert reloaded.__author__ == 'Jan Petykiewicz' assert reloaded.__author__ == 'Jan Petykiewicz'
assert reloaded.__doc__ is not None assert reloaded.__doc__ is not None
_restore_reloaded(monkeypatch, meanas) _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__ 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'): if name.startswith('pyfftw'):
raise ImportError('forced pyfftw failure') raise ImportError('forced pyfftw failure')
return original_import(name, globals, locals, fromlist, level) return original_import(name, globals, locals, fromlist, level)

View file

@ -2,6 +2,7 @@ import dataclasses
from functools import lru_cache from functools import lru_cache
import numpy import numpy
import pytest
from .. import fdfd, fdtd from .. import fdfd, fdtd
from ..fdtd.misc import gaussian_packet from ..fdtd.misc import gaussian_packet
@ -9,6 +10,9 @@ from ..fdmath import vec, unvec
from ..fdfd import functional, scpml, waveguide_3d from ..fdfd import functional, scpml, waveguide_3d
pytestmark = pytest.mark.complete
DT = 0.25 DT = 0.25
PERIOD_STEPS = 64 PERIOD_STEPS = 64
OMEGA = 2 * numpy.pi / (PERIOD_STEPS * DT) OMEGA = 2 * numpy.pi / (PERIOD_STEPS * DT)
@ -220,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]: 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) 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) 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] scale = fdtd.temporal_phasor_scale(waveform, OMEGA, DT, offset_steps=0.5)[0]
return waveform, scale return waveform, scale
@ -268,7 +272,7 @@ def _run_real_field_straight_waveguide_case() -> RealFieldWaveguideResult:
slices=REAL_FIELD_SOURCE_SLICES, slices=REAL_FIELD_SOURCE_SLICES,
epsilon=epsilon, 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( monitor_mode = waveguide_3d.solve_mode(
0, 0,
omega=OMEGA, omega=OMEGA,
@ -376,7 +380,6 @@ def _run_straight_waveguide_case(variant: str) -> WaveguideCalibrationResult:
axis=0, axis=0,
polarity=1, polarity=1,
slices=MONITOR_SLICES, slices=MONITOR_SLICES,
omega=OMEGA,
) )
update_e, update_h = fdtd.updates_with_cpml(cpml_params=_build_cpml_params(), dt=DT, dxes=base_dxes, epsilon=epsilon) update_e, update_h = fdtd.updates_with_cpml(cpml_params=_build_cpml_params(), dt=DT, dxes=base_dxes, epsilon=epsilon)
@ -421,8 +424,8 @@ def _run_straight_waveguide_case(variant: str) -> WaveguideCalibrationResult:
) )
h_fdfd = functional.e2h(OMEGA, stretched_dxes)(e_fdfd) h_fdfd = functional.e2h(OMEGA, stretched_dxes)(e_fdfd)
overlap_td = vec(e_ph) @ vec(overlap_e).conj() overlap_td = complex(vec(e_ph) @ vec(overlap_e).conj())
overlap_fd = vec(e_fdfd) @ 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_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()) poynting_fd = functional.poynting_e_cross_h(stretched_dxes)(e_fdfd, h_fdfd.conj())
@ -484,7 +487,6 @@ def _run_width_step_scattering_case() -> WaveguideScatteringResult:
axis=0, axis=0,
polarity=-1, polarity=-1,
slices=SCATTERING_REFLECT_SLICES, slices=SCATTERING_REFLECT_SLICES,
omega=OMEGA,
) )
transmitted_mode = waveguide_3d.solve_mode( transmitted_mode = waveguide_3d.solve_mode(
0, 0,
@ -502,7 +504,6 @@ def _run_width_step_scattering_case() -> WaveguideScatteringResult:
axis=0, axis=0,
polarity=1, polarity=1,
slices=SCATTERING_TRANSMIT_SLICES, slices=SCATTERING_TRANSMIT_SLICES,
omega=OMEGA,
) )
update_e, update_h = fdtd.updates_with_cpml(cpml_params=_build_cpml_params(), dt=DT, dxes=base_dxes, epsilon=epsilon) update_e, update_h = fdtd.updates_with_cpml(cpml_params=_build_cpml_params(), dt=DT, dxes=base_dxes, epsilon=epsilon)
@ -547,10 +548,10 @@ def _run_width_step_scattering_case() -> WaveguideScatteringResult:
) )
h_fdfd = functional.e2h(OMEGA, stretched_dxes)(e_fdfd) h_fdfd = functional.e2h(OMEGA, stretched_dxes)(e_fdfd)
reflected_td = vec(e_ph) @ vec(reflected_overlap).conj() reflected_td = complex(vec(e_ph) @ vec(reflected_overlap).conj())
reflected_fd = vec(e_fdfd) @ vec(reflected_overlap).conj() reflected_fd = complex(vec(e_fdfd) @ vec(reflected_overlap).conj())
transmitted_td = vec(e_ph) @ vec(transmitted_overlap).conj() transmitted_td = complex(vec(e_ph) @ vec(transmitted_overlap).conj())
transmitted_fd = vec(e_fdfd) @ 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_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()) poynting_fd = functional.poynting_e_cross_h(stretched_dxes)(e_fdfd, h_fdfd.conj())
@ -617,7 +618,6 @@ def _run_pulsed_straight_waveguide_case() -> PulsedWaveguideCalibrationResult:
axis=0, axis=0,
polarity=1, polarity=1,
slices=MONITOR_SLICES, slices=MONITOR_SLICES,
omega=OMEGA,
) )
update_e, update_h = fdtd.updates_with_cpml(cpml_params=_build_cpml_params(), dt=DT, dxes=base_dxes, epsilon=epsilon, dtype=complex) update_e, update_h = fdtd.updates_with_cpml(cpml_params=_build_cpml_params(), dt=DT, dxes=base_dxes, epsilon=epsilon, dtype=complex)
@ -660,8 +660,8 @@ def _run_pulsed_straight_waveguide_case() -> PulsedWaveguideCalibrationResult:
) )
h_fdfd = functional.e2h(OMEGA, stretched_dxes)(e_fdfd) h_fdfd = functional.e2h(OMEGA, stretched_dxes)(e_fdfd)
overlap_td = vec(e_ph) @ vec(overlap_e).conj() overlap_td = complex(vec(e_ph) @ vec(overlap_e).conj())
overlap_fd = vec(e_fdfd) @ 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_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()) poynting_fd = functional.poynting_e_cross_h(stretched_dxes)(e_fdfd, h_fdfd.conj())

View file

@ -16,7 +16,7 @@ def build_waveguide_3d_mode(
*, *,
slice_start: int, slice_start: int,
polarity: 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) epsilon = numpy.ones((3, 5, 5, 1), dtype=float)
dxes = [[numpy.ones(5), numpy.ones(5), numpy.ones(1)] for _ in range(2)] 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)) slices = (slice(slice_start, slice_start + 1), slice(None), slice(None))
@ -100,7 +100,6 @@ def test_waveguide_3d_compute_overlap_e_uses_adjacent_window(
axis=0, axis=0,
polarity=polarity, polarity=polarity,
slices=slices, slices=slices,
omega=OMEGA,
) )
nonzero = numpy.argwhere(numpy.abs(overlap) > 0) 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, axis=0,
polarity=polarity, polarity=polarity,
slices=slices, slices=slices,
omega=OMEGA,
) )
nonzero = numpy.argwhere(numpy.abs(overlap) > 0) nonzero = numpy.argwhere(numpy.abs(overlap) > 0)
@ -158,7 +156,6 @@ def test_waveguide_3d_compute_overlap_e_rejects_empty_overlap_window(
axis=0, axis=0,
polarity=polarity, polarity=polarity,
slices=slices, slices=slices,
omega=OMEGA,
) )
@ -173,7 +170,6 @@ def test_waveguide_3d_compute_overlap_e_rejects_zero_support_window() -> None:
axis=0, axis=0,
polarity=1, polarity=1,
slices=slices, slices=slices,
omega=OMEGA,
) )

View file

@ -1,5 +1,6 @@
import numpy import numpy
from numpy.typing import NDArray from numpy.typing import NDArray
from numpy.typing import ArrayLike
def make_prng(seed: int = 12345) -> numpy.random.RandomState: def make_prng(seed: int = 12345) -> numpy.random.RandomState:
@ -24,9 +25,9 @@ def assert_fields_close(
) )
def assert_close( def assert_close(
x: NDArray, x: ArrayLike,
y: NDArray, y: ArrayLike,
*args, *args,
**kwargs, **kwargs,
) -> None: ) -> None:
numpy.testing.assert_allclose(x, y, *args, **kwargs) numpy.testing.assert_allclose(numpy.asarray(x), numpy.asarray(y), *args, **kwargs)

View file

@ -104,6 +104,9 @@ lint.ignore = [
"TRY002", # Exception() "TRY002", # Exception()
] ]
[tool.ruff.lint.per-file-ignores]
"meanas/test/**/*.py" = ["ANN", "ARG", "TC006"]
[[tool.mypy.overrides]] [[tool.mypy.overrides]]
module = [ module = [
@ -121,6 +124,9 @@ ignore_missing_imports = true
[tool.pytest.ini_options] [tool.pytest.ini_options]
addopts = "-rsXx" addopts = "-rsXx"
testpaths = ["meanas"] testpaths = ["meanas"]
markers = [
"complete: slower integration and smoke tests intended for full pre-commit confidence runs",
]
[tool.coverage.run] [tool.coverage.run]
source = ["meanas"] source = ["meanas"]

104
uv.lock generated
View file

@ -1,5 +1,13 @@
version = 1 version = 1
requires-python = ">=3.11" 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]] [[package]]
name = "babel" name = "babel"
@ -730,8 +738,8 @@ dependencies = [
[package.optional-dependencies] [package.optional-dependencies]
dev = [ dev = [
{ name = "coverage" }, { name = "coverage" },
{ name = "gridlock" },
{ name = "htmlark" }, { name = "htmlark" },
{ name = "matplotlib" },
{ name = "mkdocs" }, { name = "mkdocs" },
{ name = "mkdocs-material" }, { name = "mkdocs-material" },
{ name = "mkdocs-print-site-plugin" }, { name = "mkdocs-print-site-plugin" },
@ -739,6 +747,7 @@ dev = [
{ name = "pymdown-extensions" }, { name = "pymdown-extensions" },
{ name = "pytest" }, { name = "pytest" },
{ name = "ruff" }, { name = "ruff" },
{ name = "scikit-rf" },
] ]
docs = [ docs = [
{ name = "htmlark" }, { name = "htmlark" },
@ -750,8 +759,8 @@ docs = [
{ name = "ruff" }, { name = "ruff" },
] ]
examples = [ examples = [
{ name = "gridlock" },
{ name = "matplotlib" }, { name = "matplotlib" },
{ name = "scikit-rf" },
] ]
test = [ test = [
{ name = "coverage" }, { name = "coverage" },
@ -762,11 +771,10 @@ test = [
requires-dist = [ requires-dist = [
{ name = "coverage", marker = "extra == 'dev'" }, { name = "coverage", marker = "extra == 'dev'" },
{ name = "coverage", marker = "extra == 'test'" }, { name = "coverage", marker = "extra == 'test'" },
{ name = "gridlock" }, { name = "gridlock", specifier = ">=2.1" },
{ name = "gridlock", marker = "extra == 'dev'" },
{ name = "gridlock", marker = "extra == 'examples'", specifier = ">=2.1" },
{ name = "htmlark", marker = "extra == 'dev'", specifier = ">=1.0" }, { name = "htmlark", marker = "extra == 'dev'", specifier = ">=1.0" },
{ name = "htmlark", marker = "extra == 'docs'", 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 = "matplotlib", marker = "extra == 'examples'", specifier = ">=3.10.8" },
{ name = "mkdocs", marker = "extra == 'dev'", specifier = ">=1.6" }, { name = "mkdocs", marker = "extra == 'dev'", specifier = ">=1.6" },
{ name = "mkdocs", marker = "extra == 'docs'", specifier = ">=1.6" }, { name = "mkdocs", marker = "extra == 'docs'", specifier = ">=1.6" },
@ -783,6 +791,8 @@ requires-dist = [
{ name = "pytest", marker = "extra == 'test'" }, { name = "pytest", marker = "extra == 'test'" },
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6" },
{ name = "ruff", marker = "extra == 'docs'", 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" }, { 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 }, { 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]] [[package]]
name = "pathspec" name = "pathspec"
version = "1.0.4" 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 }, { 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]] [[package]]
name = "scipy" name = "scipy"
version = "1.16.3" 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 }, { 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]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.6.3" version = "2.6.3"