Compare commits

..

64 Commits

Author SHA1 Message Date
jan
be647658d3 [eigensolvers] Increase number of lanczos vectors (ncv) based on number of requested eigenvalues 2025-12-10 23:07:28 -08:00
jan
c46bed8298 update optional deps 2025-12-10 21:15:38 -08:00
jan
fb3bef23bf [examples/fdfd] split fdfd example into two files 2025-12-10 21:14:34 -08:00
jan
d4f1008c5c [fdfd.waveguide*] comment updates 2025-12-10 19:45:26 -08:00
jan
b486fa325b Rework field types, use sparse arrays instead of matrices, rework eme arg naming, improve type annotations and linter cleanup 2025-12-10 02:14:20 -08:00
jan
b7ad5dea2b [fdfd.bloch] drop unnecessary noqas 2025-12-10 02:05:24 -08:00
jan
684b891e0f [waveguide_3d] clean up docstrings 2025-12-09 22:56:16 -08:00
jan
4a80ca8b12 [waveguide_cyl] silence some debug prints 2025-12-09 22:55:52 -08:00
e3169b9e20 bump version to v0.10 2025-04-16 22:20:16 -07:00
35ecbad15e remove old lint 2025-04-16 22:19:21 -07:00
43e01a814d examples will use new gridlock 2025-04-16 22:19:14 -07:00
9eb0e28bcb [meanas.fdtd.misc] add basic pulse and beam shapes 2025-03-12 23:40:00 -07:00
c858b20d47 Bump numpy dependency to >=2.0 2025-03-12 23:19:20 -07:00
777ecbc024 [fdfd.solvers.generic] add option to pass a guess solution 2025-02-05 00:13:46 -08:00
c4f8749941 [fdfd.solvers.generic] report residual scaled to b 2025-02-05 00:09:25 -08:00
cd5cc9eb83 [fdfd.eme] Add basic (WIP) eignmode expansion functionality 2025-01-28 22:07:19 -08:00
99e8d32eb1 [waveguide_cyl] frequency should be real 2025-01-28 22:06:32 -08:00
1cb0cb2e4f [fdfd.waveguide_cyl] Improve documentation and add auxiliary functions (e.g. exy2exyz) 2025-01-28 21:59:59 -08:00
234e8d7ac3 delete h version of operator in comment 2025-01-28 19:55:09 -08:00
83f4d87ad8 [fdfd.waveguide*] misc fixes 2025-01-28 19:54:48 -08:00
1987ee473a improve type annotations 2025-01-28 19:54:13 -08:00
4afc6cf62e cleanup latex 2025-01-14 22:34:52 -08:00
53d5812b4a [waveguide_2d] Remove \gamma from docs in favor of just using \beta 2025-01-14 22:34:35 -08:00
651e255704 add derivation for exy2e() 2025-01-14 22:15:18 -08:00
71c2bbfada Add linear_wavenumbers() for calculating 1/distance wavenumbers 2025-01-14 22:02:43 -08:00
6a56921c12 Return angular wavenumbers, and remove r0 arg (leaving only rmin) 2025-01-14 22:02:19 -08:00
006833acf2 add logger 2025-01-14 22:01:29 -08:00
155f30068f add inner_product() and use it for energy calculation 2025-01-14 22:01:10 -08:00
7987dc796f mode numbers may be any sequence 2025-01-14 22:00:21 -08:00
829007c672 Only keep the real part of the energy 2025-01-14 22:00:08 -08:00
659566750f update for new gridlock syntax 2025-01-14 21:59:46 -08:00
76701f593c Check overlap only on forward-propagating part of mode 2025-01-14 21:59:37 -08:00
4e3a163522 indentation & style 2025-01-14 21:59:12 -08:00
50f92e1cc8 [vectorization] add nvdim arg allowing unvec() on 2D fields 2025-01-14 21:58:46 -08:00
b3c2fd391b [waveguide_2d] Return modes sorted by wavenumber (descending) 2025-01-14 21:57:54 -08:00
c543868c0b check for sign=0 case 2025-01-14 21:51:32 -08:00
e54735d9c6 Fix cylindrical waveguide module
- Properly account for rmin vs r0
- Change return values to match waveguide_2d
- Change operator definition to look more like waveguide_2d

remaining TODO:
- Fix docs
- Further consolidate operators vs waveguide_2d
- Figure out E/H field conversions
2025-01-07 00:10:15 -08:00
4f2433320d fix zip(strict=True) for 2D problems 2025-01-07 00:05:19 -08:00
47415a0beb Return list-of-vectors from waveguide mode solve 2025-01-07 00:04:53 -08:00
e459b5e61f clean up comments and some types 2025-01-07 00:04:01 -08:00
36431cd0e4 enable numpy 2.0 and recent scipy 2024-07-29 02:25:16 -07:00
739e96df3d avoid a copy 2024-07-29 00:34:17 -07:00
63e7cb949f explicitly specify closed variables 2024-07-29 00:33:58 -07:00
c53a3c4d84 unused var 2024-07-29 00:33:43 -07:00
5dd9994e76 improve some type annotations 2024-07-29 00:32:52 -07:00
1021768e30 simplify indentation 2024-07-29 00:32:20 -07:00
95e923d7b7 improve error handling 2024-07-29 00:32:03 -07:00
3f8802cb5f use strict zip 2024-07-29 00:31:44 -07:00
43bb0ba379 use generators where applicable 2024-07-29 00:31:16 -07:00
e19968bb9f linter-related test updates 2024-07-29 00:30:00 -07:00
43f038d761 modernize type annotations 2024-07-29 00:29:39 -07:00
d5fca741d1 remove type:ignore from scipy imports (done at pyproject.toml level) 2024-07-29 00:27:59 -07:00
ca94ad1b25 use path.open() 2024-07-29 00:23:08 -07:00
10f26c12b4 add ruff and mypy configs 2024-07-29 00:22:54 -07:00
ee51c7db49 improve type annotations 2024-07-28 23:23:47 -07:00
36bea6a593 drop unused import 2024-07-28 23:23:21 -07:00
b16b35d84a use new numpy.random.Generator approach 2024-07-28 23:23:11 -07:00
6f3ae5a64f explicitly re-export some names 2024-07-28 23:22:21 -07:00
99c22d572f bump numpy version 2024-07-28 23:21:59 -07:00
2f00baf0c6 fixup cylindrical wg example 2024-07-18 19:31:17 -07:00
2712d96f2a add notes on references 2024-07-18 19:31:17 -07:00
dc3e733e7f flake8 fixes 2024-07-18 19:31:17 -07:00
95e3f71b40 use f-strings in place of .format() 2024-07-18 19:31:17 -07:00
639f88bba8 add sensitivity calculation 2024-07-18 19:31:17 -07:00
31 changed files with 2585 additions and 809 deletions

103
examples/fdfd0.py Normal file
View File

@ -0,0 +1,103 @@
import numpy
from numpy.linalg import norm
from matplotlib import pyplot, colors
import logging
import meanas
from meanas import fdtd
from meanas.fdmath import vec, unvec
from meanas.fdfd import waveguide_3d, functional, scpml, operators
from meanas.fdfd.solvers import generic as generic_solver
import gridlock
logging.basicConfig(level=logging.DEBUG)
logging.getLogger('matplotlib').setLevel(logging.WARNING)
__author__ = 'Jan Petykiewicz'
def pcolor(ax, v) -> None:
mappable = ax.pcolor(v, cmap='seismic', norm=colors.CenteredNorm())
ax.axis('equal')
ax.get_figure().colorbar(mappable)
def test0(solver=generic_solver):
dx = 50 # discretization (nm/cell)
pml_thickness = 10 # (number of cells)
wl = 1550 # Excitation wavelength
omega = 2 * numpy.pi / wl
# Device design parameters
radii = (1, 0.6)
th = 220
center = [0, 0, 0]
# refractive indices
n_ring = numpy.sqrt(12.6) # ~Si
n_air = 4.0 # air
# Half-dimensions of the simulation grid
xyz_max = numpy.array([1.2, 1.2, 0.3]) * 1000 + pml_thickness * dx
# Coordinates of the edges of the cells.
half_edge_coords = [numpy.arange(dx/2, m + dx, step=dx) for m in xyz_max]
edge_coords = [numpy.hstack((-h[::-1], h)) for h in half_edge_coords]
# #### Create the grid, mask, and draw the device ####
grid = gridlock.Grid(edge_coords)
epsilon = grid.allocate(n_air**2, dtype=numpy.float32)
grid.draw_cylinder(
epsilon,
h = dict(axis='z', center=center[2], span=th),
radius = max(radii),
center2d = center[:2],
foreground = n_ring ** 2,
num_points = 24,
)
grid.draw_cylinder(
epsilon,
h = dict(axis='z', center=center[2], span=th * 1.1),
radius = min(radii),
center2d = center[:2],
foreground = n_air ** 2,
num_points = 24,
)
dxes = [grid.dxyz, grid.autoshifted_dxyz()]
for a in (0, 1, 2):
for p in (-1, 1):
dxes = meanas.fdfd.scpml.stretch_with_scpml(dxes, axis=a, polarity=p, omega=omega,
thickness=pml_thickness)
J = [numpy.zeros_like(epsilon[0], dtype=complex) for _ in range(3)]
J[1][15, grid.shape[1]//2, grid.shape[2]//2] = 1
#
# Solve!
#
sim_args = dict(
omega = omega,
dxes = dxes,
epsilon = vec(epsilon),
)
x = solver(J=vec(J), **sim_args)
A = operators.e_full(omega, dxes, vec(epsilon)).tocsr()
b = -1j * omega * vec(J)
print('Norm of the residual is ', norm(A @ x - b) / norm(b))
E = unvec(x, grid.shape)
#
# Plot results
#
grid.visualize_slice(E.real, plane=dict(z=0), which_shifts=1, pcolormesh_args=dict(norm=colors.CenteredNorm(), cmap='bwr'))
if __name__ == '__main__':
test0()

View File

@ -1,6 +1,8 @@
import importlib import importlib
import numpy import numpy
from numpy.linalg import norm from numpy.linalg import norm
from matplotlib import pyplot, colors
import logging
import meanas import meanas
from meanas import fdtd from meanas import fdtd
@ -10,9 +12,6 @@ from meanas.fdfd.solvers import generic as generic_solver
import gridlock import gridlock
from matplotlib import pyplot
import logging
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
logging.getLogger('matplotlib').setLevel(logging.WARNING) logging.getLogger('matplotlib').setLevel(logging.WARNING)
@ -20,82 +19,6 @@ logging.getLogger('matplotlib').setLevel(logging.WARNING)
__author__ = 'Jan Petykiewicz' __author__ = 'Jan Petykiewicz'
def test0(solver=generic_solver):
dx = 50 # discretization (nm/cell)
pml_thickness = 10 # (number of cells)
wl = 1550 # Excitation wavelength
omega = 2 * numpy.pi / wl
# Device design parameters
radii = (1, 0.6)
th = 220
center = [0, 0, 0]
# refractive indices
n_ring = numpy.sqrt(12.6) # ~Si
n_air = 4.0 # air
# Half-dimensions of the simulation grid
xyz_max = numpy.array([1.2, 1.2, 0.3]) * 1000 + pml_thickness * dx
# Coordinates of the edges of the cells.
half_edge_coords = [numpy.arange(dx/2, m + dx, step=dx) for m in xyz_max]
edge_coords = [numpy.hstack((-h[::-1], h)) for h in half_edge_coords]
# #### Create the grid, mask, and draw the device ####
grid = gridlock.Grid(edge_coords)
epsilon = grid.allocate(n_air**2, dtype=numpy.float32)
grid.draw_cylinder(epsilon,
surface_normal=2,
center=center,
radius=max(radii),
thickness=th,
eps=n_ring**2,
num_points=24)
grid.draw_cylinder(epsilon,
surface_normal=2,
center=center,
radius=min(radii),
thickness=th*1.1,
eps=n_air ** 2,
num_points=24)
dxes = [grid.dxyz, grid.autoshifted_dxyz()]
for a in (0, 1, 2):
for p in (-1, 1):
dxes = meanas.fdfd.scpml.stretch_with_scpml(dxes, axis=a, polarity=p, omega=omega,
thickness=pml_thickness)
J = [numpy.zeros_like(epsilon[0], dtype=complex) for _ in range(3)]
J[1][15, grid.shape[1]//2, grid.shape[2]//2] = 1
'''
Solve!
'''
sim_args = {
'omega': omega,
'dxes': dxes,
'epsilon': vec(epsilon),
}
x = solver(J=vec(J), **sim_args)
A = operators.e_full(omega, dxes, vec(epsilon)).tocsr()
b = -1j * omega * vec(J)
print('Norm of the residual is ', norm(A @ x - b))
E = unvec(x, grid.shape)
'''
Plot results
'''
pyplot.figure()
pyplot.pcolor(numpy.real(E[1][:, :, grid.shape[2]//2]), cmap='seismic')
pyplot.axis('equal')
pyplot.show()
def test1(solver=generic_solver): def test1(solver=generic_solver):
dx = 40 # discretization (nm/cell) dx = 40 # discretization (nm/cell)
pml_thickness = 10 # (number of cells) pml_thickness = 10 # (number of cells)
@ -122,7 +45,7 @@ def test1(solver=generic_solver):
# #### Create the grid and draw the device #### # #### Create the grid and draw the device ####
grid = gridlock.Grid(edge_coords) grid = gridlock.Grid(edge_coords)
epsilon = grid.allocate(n_air**2, dtype=numpy.float32) epsilon = grid.allocate(n_air**2, dtype=numpy.float32)
grid.draw_cuboid(epsilon, center=center, dimensions=[8e3, w, th], eps=n_wg**2) grid.draw_cuboid(epsilon, x=dict(center=0, span=8e3), y=dict(center=0, span=w), z=dict(center=0, span=th), foreground=n_wg**2)
dxes = [grid.dxyz, grid.autoshifted_dxyz()] dxes = [grid.dxyz, grid.autoshifted_dxyz()]
for a in (0, 1, 2): for a in (0, 1, 2):
@ -156,22 +79,14 @@ def test1(solver=generic_solver):
# grid.draw_cuboid(pmcg, center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1) # grid.draw_cuboid(pmcg, center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1)
# grid.visualize_isosurface(pmcg) # grid.visualize_isosurface(pmcg)
def pcolor(v) -> None: grid.visualize_slice(J.imag, plane=dict(y=6*dx), which_shifts=1, pcolormesh_args=dict(norm=colors.CenteredNorm(), cmap='bwr'))
vmax = numpy.max(numpy.abs(v)) fig, ax = pyplot.subplots()
pyplot.pcolor(v, cmap='seismic', vmin=-vmax, vmax=vmax) ax.pcolormesh((numpy.abs(J).sum(axis=2).sum(axis=0) > 0).astype(float).T, cmap='hot')
pyplot.axis('equal')
pyplot.colorbar()
ss = (1, slice(None), J.shape[2]//2+6, slice(None))
# pyplot.figure()
# pcolor(J3[ss].T.imag)
# pyplot.figure()
# pcolor((numpy.abs(J3).sum(axis=2).sum(axis=0) > 0).astype(float).T)
pyplot.show(block=True) pyplot.show(block=True)
''' #
Solve! # Solve!
''' #
sim_args = { sim_args = {
'omega': omega, 'omega': omega,
'dxes': dxes, 'dxes': dxes,
@ -188,20 +103,18 @@ def test1(solver=generic_solver):
E = unvec(x, grid.shape) E = unvec(x, grid.shape)
''' #
Plot results # Plot results
''' #
center = grid.pos2ind([0, 0, 0], None).astype(int) center = grid.pos2ind([0, 0, 0], None).astype(int)
pyplot.figure() fig, axes = pyplot.subplots(2, 2)
pyplot.subplot(2, 2, 1) grid.visualize_slice(E.real, plane=dict(x=0), which_shifts=1, ax=axes[0, 0], finalize=False, pcolormesh_args=dict(norm=colors.CenteredNorm(), cmap='bwr'))
pcolor(numpy.real(E[1][center[0], :, :]).T) grid.visualize_slice(E.real, plane=dict(z=0), which_shifts=1, ax=axes[0, 1], finalize=False, pcolormesh_args=dict(norm=colors.CenteredNorm(), cmap='bwr'))
pyplot.subplot(2, 2, 2) # pcolor(axes[0, 0], numpy.real(E[1][center[0], :, :]).T)
pyplot.plot(numpy.log10(numpy.abs(E[1][:, center[1], center[2]]) + 1e-10)) # pcolor(axes[0, 1], numpy.real(E[1][:, :, center[2]]).T)
pyplot.grid(alpha=0.6) axes[1, 0].plot(numpy.log10(numpy.abs(E[1][:, center[1], center[2]]) + 1e-10))
pyplot.ylabel('log10 of field') axes[1, 0].grid(alpha=0.6)
pyplot.subplot(2, 2, 3) axes[1, 0].set_ylabel('log10 of field')
pcolor(numpy.real(E[1][:, :, center[2]]).T)
pyplot.subplot(2, 2, 4)
def poyntings(E): def poyntings(E):
H = functional.e2h(omega, dxes)(E) H = functional.e2h(omega, dxes)(E)
@ -215,24 +128,28 @@ def test1(solver=generic_solver):
return s0, s1, s2 return s0, s1, s2
s0x, s1x, s2x = poyntings(E) s0x, s1x, s2x = poyntings(E)
pyplot.plot(s0x[0].sum(axis=2).sum(axis=1), label='s0', marker='.') ax = axes[1, 1]
pyplot.plot(s1x[0].sum(axis=2).sum(axis=1), label='s1', marker='.') ax.plot(s0x[0].sum(axis=2).sum(axis=1), label='s0', marker='.')
pyplot.plot(s2x[0].sum(axis=2).sum(axis=1), label='s2', marker='.') ax.plot(s1x[0].sum(axis=2).sum(axis=1), label='s1', marker='.')
pyplot.plot(E[1][:, center[1], center[2]].real.T, label='Ey', marker='x') ax.plot(s2x[0].sum(axis=2).sum(axis=1), label='s2', marker='.')
pyplot.grid(alpha=0.6) ax.plot(E[1][:, center[1], center[2]].real.T, label='Ey', marker='x')
pyplot.legend() ax.grid(alpha=0.6)
pyplot.show() ax.legend()
p_in = (-E * J.conj()).sum() / 2 * (dx * dx * dx)
print(f'{p_in=}')
q = [] q = []
for i in range(-5, 30): for i in range(-5, 30):
e_ovl_rolled = numpy.roll(e_overlap, i, axis=1) e_ovl_rolled = numpy.roll(e_overlap, i, axis=1)
q += [numpy.abs(vec(E) @ vec(e_ovl_rolled).conj())] q += [numpy.abs(vec(E).conj() @ vec(e_ovl_rolled))]
pyplot.figure() fig, ax = pyplot.subplots()
pyplot.plot(q, marker='.') ax.plot(q, marker='.')
pyplot.grid(alpha=0.6) ax.grid(alpha=0.6)
pyplot.title('Overlap with mode') ax.set_title('Overlap with mode')
pyplot.show() print('Average overlap with mode:', sum(q[8:32])/len(q[8:32]))
print('Average overlap with mode:', sum(q)/len(q))
pyplot.show(block=True)
def module_available(name): def module_available(name):
@ -240,9 +157,6 @@ def module_available(name):
if __name__ == '__main__': if __name__ == '__main__':
#test0()
# test1()
if module_available('opencl_fdfd'): if module_available('opencl_fdfd'):
from opencl_fdfd import cg_solver as opencl_solver from opencl_fdfd import cg_solver as opencl_solver
test1(opencl_solver) test1(opencl_solver)
@ -253,3 +167,4 @@ if __name__ == '__main__':
# test1(magma_solver) # test1(magma_solver)
else: else:
test1() test1()

View File

@ -6,12 +6,13 @@ See the readme or `import meanas; help(meanas)` for more info.
import pathlib import pathlib
__version__ = '0.9' __version__ = '0.10'
__author__ = 'Jan Petykiewicz' __author__ = 'Jan Petykiewicz'
try: try:
with open(pathlib.Path(__file__).parent / 'README.md', 'r') as f: readme_path = pathlib.Path(__file__).parent / 'README.md'
with readme_path.open('r') as f:
__doc__ = f.read() __doc__ = f.read()
except Exception: except Exception:
pass pass

View File

@ -1,12 +1,12 @@
""" """
Solvers for eigenvalue / eigenvector problems Solvers for eigenvalue / eigenvector problems
""" """
from typing import Callable from collections.abc import Callable
import numpy import numpy
from numpy.typing import NDArray, ArrayLike from numpy.typing import NDArray, ArrayLike
from numpy.linalg import norm from numpy.linalg import norm
from scipy import sparse # type: ignore from scipy import sparse
import scipy.sparse.linalg as spalg # type: ignore import scipy.sparse.linalg as spalg
def power_iteration( def power_iteration(
@ -25,8 +25,9 @@ def power_iteration(
Returns: Returns:
(Largest-magnitude eigenvalue, Corresponding eigenvector estimate) (Largest-magnitude eigenvalue, Corresponding eigenvector estimate)
""" """
rng = numpy.random.default_rng()
if guess_vector is None: if guess_vector is None:
v = numpy.random.rand(operator.shape[0]) + 1j * numpy.random.rand(operator.shape[0]) v = rng.random(operator.shape[0]) + 1j * rng.random(operator.shape[0])
else: else:
v = guess_vector v = guess_vector
@ -63,10 +64,10 @@ def rayleigh_quotient_iteration(
(eigenvalues, eigenvectors) (eigenvalues, eigenvectors)
""" """
try: try:
(operator - sparse.eye(operator.shape[0])) (operator - sparse.eye_array(operator.shape[0]))
def shift(eigval: float) -> sparse: def shift(eigval: float) -> sparse.sparray:
return eigval * sparse.eye(operator.shape[0]) return eigval * sparse.eye_array(operator.shape[0])
if solver is None: if solver is None:
solver = spalg.spsolve solver = spalg.spsolve
@ -129,12 +130,12 @@ def signed_eigensolve(
# Try to combine, use general LinearOperator if we fail # Try to combine, use general LinearOperator if we fail
try: try:
shifted_operator = operator + shift * sparse.eye(operator.shape[0]) shifted_operator = operator + shift * sparse.eye_array(operator.shape[0])
except TypeError: except TypeError:
shifted_operator = operator + spalg.LinearOperator(shape=operator.shape, shifted_operator = operator + spalg.LinearOperator(shape=operator.shape,
matvec=lambda v: shift * v) matvec=lambda v: shift * v)
shifted_eigenvalues, eigenvectors = spalg.eigs(shifted_operator, which='LM', k=how_many, ncv=50) shifted_eigenvalues, eigenvectors = spalg.eigs(shifted_operator, which='LM', k=how_many, ncv=2 * how_many + 50)
eigenvalues = shifted_eigenvalues - shift eigenvalues = shifted_eigenvalues - shift
k = eigenvalues.argsort() k = eigenvalues.argsort()

View File

@ -91,5 +91,12 @@ $$
""" """
from . import solvers, operators, functional, scpml, waveguide_2d, waveguide_3d from . import (
solvers as solvers,
operators as operators,
functional as functional,
scpml as scpml,
waveguide_2d as waveguide_2d,
waveguide_3d as waveguide_3d,
)
# from . import farfield, bloch TODO # from . import farfield, bloch TODO

View File

@ -94,18 +94,19 @@ This module contains functions for generating and solving the
""" """
from typing import Callable, Any, cast, Sequence from typing import Any, cast
from collections.abc import Callable, Sequence
import logging import logging
import numpy import numpy
from numpy import pi, real, trace from numpy import pi, real, trace
from numpy.fft import fftfreq from numpy.fft import fftfreq
from numpy.typing import NDArray, ArrayLike from numpy.typing import NDArray, ArrayLike
import scipy # type: ignore import scipy
import scipy.optimize # type: ignore import scipy.optimize
from scipy.linalg import norm # type: ignore from scipy.linalg import norm
import scipy.sparse.linalg as spalg # type: ignore import scipy.sparse.linalg as spalg
from ..fdmath import fdfield_t, cfdfield_t from ..fdmath import fdfield, cfdfield, cfdfield_t
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -114,7 +115,6 @@ logger = logging.getLogger(__name__)
try: try:
import pyfftw.interfaces.numpy_fft # type: ignore import pyfftw.interfaces.numpy_fft # type: ignore
import pyfftw.interfaces # type: ignore import pyfftw.interfaces # type: ignore
import multiprocessing
logger.info('Using pyfftw') logger.info('Using pyfftw')
pyfftw.interfaces.cache.enable() pyfftw.interfaces.cache.enable()
@ -155,7 +155,7 @@ def generate_kmn(
All are given in the xyz basis (e.g. `|k|[0,0,0] = norm(G_matrix @ k0)`). All are given in the xyz basis (e.g. `|k|[0,0,0] = norm(G_matrix @ k0)`).
""" """
k0 = numpy.array(k0) k0 = numpy.array(k0)
G_matrix = numpy.array(G_matrix, copy=False) G_matrix = numpy.asarray(G_matrix)
Gi_grids = numpy.array(numpy.meshgrid(*(fftfreq(n, 1 / n) for n in shape[:3]), indexing='ij')) Gi_grids = numpy.array(numpy.meshgrid(*(fftfreq(n, 1 / n) for n in shape[:3]), indexing='ij'))
Gi = numpy.moveaxis(Gi_grids, 0, -1) Gi = numpy.moveaxis(Gi_grids, 0, -1)
@ -183,8 +183,8 @@ def generate_kmn(
def maxwell_operator( def maxwell_operator(
k0: ArrayLike, k0: ArrayLike,
G_matrix: ArrayLike, G_matrix: ArrayLike,
epsilon: fdfield_t, epsilon: fdfield,
mu: fdfield_t | None = None mu: fdfield | None = None
) -> Callable[[NDArray[numpy.complex128]], NDArray[numpy.complex128]]: ) -> Callable[[NDArray[numpy.complex128]], NDArray[numpy.complex128]]:
""" """
Generate the Maxwell operator Generate the Maxwell operator
@ -232,13 +232,13 @@ def maxwell_operator(
Raveled conv(1/mu_k, ik x conv(1/eps_k, ik x h_mn)), returned Raveled conv(1/mu_k, ik x conv(1/eps_k, ik x h_mn)), returned
and overwritten in-place of `h`. and overwritten in-place of `h`.
""" """
hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)] hin_m, hin_n = (hi.reshape(shape) for hi in numpy.split(h, 2))
#{d,e,h}_xyz fields are complex 3-fields in (1/x, 1/y, 1/z) basis #{d,e,h}_xyz fields are complex 3-fields in (1/x, 1/y, 1/z) basis
# cross product and transform into xyz basis # cross product and transform into xyz basis
d_xyz = (n * hin_m d_xyz = (n * hin_m
- m * hin_n) * k_mag # noqa: E128 - m * hin_n) * k_mag
# divide by epsilon # divide by epsilon
temp = ifftn(d_xyz, axes=range(3)) # reuses d_xyz if using pyfftw temp = ifftn(d_xyz, axes=range(3)) # reuses d_xyz if using pyfftw
@ -254,7 +254,7 @@ def maxwell_operator(
else: else:
# transform from mn to xyz # transform from mn to xyz
b_xyz = (m * b_m[:, :, :, None] b_xyz = (m * b_m[:, :, :, None]
+ n * b_n[:, :, :, None]) # noqa: E128 + n * b_n[:, :, :, None])
# divide by mu # divide by mu
temp = ifftn(b_xyz, axes=range(3)) temp = ifftn(b_xyz, axes=range(3))
@ -276,7 +276,7 @@ def maxwell_operator(
def hmn_2_exyz( def hmn_2_exyz(
k0: ArrayLike, k0: ArrayLike,
G_matrix: ArrayLike, G_matrix: ArrayLike,
epsilon: fdfield_t, epsilon: fdfield,
) -> Callable[[NDArray[numpy.complex128]], cfdfield_t]: ) -> Callable[[NDArray[numpy.complex128]], cfdfield_t]:
""" """
Generate an operator which converts a vectorized spatial-frequency-space Generate an operator which converts a vectorized spatial-frequency-space
@ -303,12 +303,13 @@ def hmn_2_exyz(
k_mag, m, n = generate_kmn(k0, G_matrix, shape) k_mag, m, n = generate_kmn(k0, G_matrix, shape)
def operator(h: NDArray[numpy.complex128]) -> cfdfield_t: def operator(h: NDArray[numpy.complex128]) -> cfdfield_t:
hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)] hin_m, hin_n = (hi.reshape(shape) for hi in numpy.split(h, 2))
d_xyz = (n * hin_m d_xyz = (n * hin_m
- m * hin_n) * k_mag # noqa: E128 - m * hin_n) * k_mag
# divide by epsilon # divide by epsilon
return numpy.array([ei for ei in numpy.moveaxis(ifftn(d_xyz, axes=range(3)) / epsilon, 3, 0)]) # TODO avoid copy exyz = numpy.moveaxis(ifftn(d_xyz, axes=range(3)) / epsilon, 3, 0)
return cfdfield_t(exyz)
return operator return operator
@ -316,7 +317,7 @@ def hmn_2_exyz(
def hmn_2_hxyz( def hmn_2_hxyz(
k0: ArrayLike, k0: ArrayLike,
G_matrix: ArrayLike, G_matrix: ArrayLike,
epsilon: fdfield_t epsilon: fdfield,
) -> Callable[[NDArray[numpy.complex128]], cfdfield_t]: ) -> Callable[[NDArray[numpy.complex128]], cfdfield_t]:
""" """
Generate an operator which converts a vectorized spatial-frequency-space Generate an operator which converts a vectorized spatial-frequency-space
@ -341,10 +342,10 @@ def hmn_2_hxyz(
_k_mag, m, n = generate_kmn(k0, G_matrix, shape) _k_mag, m, n = generate_kmn(k0, G_matrix, shape)
def operator(h: NDArray[numpy.complex128]) -> cfdfield_t: def operator(h: NDArray[numpy.complex128]) -> cfdfield_t:
hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)] hin_m, hin_n = (hi.reshape(shape) for hi in numpy.split(h, 2))
h_xyz = (m * hin_m h_xyz = (m * hin_m
+ n * hin_n) # noqa: E128 + n * hin_n)
return numpy.array([ifftn(hi) for hi in numpy.moveaxis(h_xyz, 3, 0)]) return cfdfield_t(numpy.array([ifftn(hi) for hi in numpy.moveaxis(h_xyz, 3, 0)]))
return operator return operator
@ -352,8 +353,8 @@ def hmn_2_hxyz(
def inverse_maxwell_operator_approx( def inverse_maxwell_operator_approx(
k0: ArrayLike, k0: ArrayLike,
G_matrix: ArrayLike, G_matrix: ArrayLike,
epsilon: fdfield_t, epsilon: fdfield,
mu: fdfield_t | None = None, mu: fdfield | None = None,
) -> Callable[[NDArray[numpy.complex128]], NDArray[numpy.complex128]]: ) -> Callable[[NDArray[numpy.complex128]], NDArray[numpy.complex128]]:
""" """
Generate an approximate inverse of the Maxwell operator, Generate an approximate inverse of the Maxwell operator,
@ -394,7 +395,7 @@ def inverse_maxwell_operator_approx(
Returns: Returns:
Raveled ik x conv(eps_k, ik x conv(mu_k, h_mn)) Raveled ik x conv(eps_k, ik x conv(mu_k, h_mn))
""" """
hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)] hin_m, hin_n = (hi.reshape(shape) for hi in numpy.split(h, 2))
#{d,e,h}_xyz fields are complex 3-fields in (1/x, 1/y, 1/z) basis #{d,e,h}_xyz fields are complex 3-fields in (1/x, 1/y, 1/z) basis
@ -403,7 +404,7 @@ def inverse_maxwell_operator_approx(
else: else:
# transform from mn to xyz # transform from mn to xyz
h_xyz = (m * hin_m[:, :, :, None] h_xyz = (m * hin_m[:, :, :, None]
+ n * hin_n[:, :, :, None]) # noqa: E128 + n * hin_n[:, :, :, None])
# multiply by mu # multiply by mu
temp = ifftn(h_xyz, axes=range(3)) temp = ifftn(h_xyz, axes=range(3))
@ -416,7 +417,7 @@ def inverse_maxwell_operator_approx(
# cross product and transform into xyz basis # cross product and transform into xyz basis
e_xyz = (n * b_m e_xyz = (n * b_m
- m * b_n) / k_mag # noqa: E128 - m * b_n) / k_mag
# multiply by epsilon # multiply by epsilon
temp = ifftn(e_xyz, axes=range(3)) temp = ifftn(e_xyz, axes=range(3))
@ -440,8 +441,8 @@ def find_k(
tolerance: float, tolerance: float,
direction: ArrayLike, direction: ArrayLike,
G_matrix: ArrayLike, G_matrix: ArrayLike,
epsilon: fdfield_t, epsilon: fdfield,
mu: fdfield_t | None = None, mu: fdfield | None = None,
band: int = 0, band: int = 0,
k_bounds: tuple[float, float] = (0, 0.5), k_bounds: tuple[float, float] = (0, 0.5),
k_guess: float | None = None, k_guess: float | None = None,
@ -508,8 +509,8 @@ def eigsolve(
num_modes: int, num_modes: int,
k0: ArrayLike, k0: ArrayLike,
G_matrix: ArrayLike, G_matrix: ArrayLike,
epsilon: fdfield_t, epsilon: fdfield,
mu: fdfield_t | None = None, mu: fdfield | None = None,
tolerance: float = 1e-7, tolerance: float = 1e-7,
max_iters: int = 10000, max_iters: int = 10000,
reset_iters: int = 100, reset_iters: int = 100,
@ -538,7 +539,7 @@ def eigsolve(
`(eigenvalues, eigenvectors)` where `eigenvalues[i]` corresponds to the `(eigenvalues, eigenvectors)` where `eigenvalues[i]` corresponds to the
vector `eigenvectors[i, :]` vector `eigenvectors[i, :]`
""" """
k0 = numpy.array(k0, copy=False) k0 = numpy.asarray(k0)
h_size = 2 * epsilon[0].size h_size = 2 * epsilon[0].size
@ -561,11 +562,12 @@ def eigsolve(
prev_theta = 0.5 prev_theta = 0.5
D = numpy.zeros(shape=y_shape, dtype=complex) D = numpy.zeros(shape=y_shape, dtype=complex)
rng = numpy.random.default_rng()
Z: NDArray[numpy.complex128] Z: NDArray[numpy.complex128]
if y0 is None: if y0 is None:
Z = numpy.random.rand(*y_shape) + 1j * numpy.random.rand(*y_shape) Z = rng.random(y_shape) + 1j * rng.random(y_shape)
else: else:
Z = numpy.array(y0, copy=False).T Z = numpy.asarray(y0).T
while True: while True:
Z *= num_modes / norm(Z) Z *= num_modes / norm(Z)
@ -573,7 +575,7 @@ def eigsolve(
try: try:
U = numpy.linalg.inv(ZtZ) U = numpy.linalg.inv(ZtZ)
except numpy.linalg.LinAlgError: except numpy.linalg.LinAlgError:
Z = numpy.random.rand(*y_shape) + 1j * numpy.random.rand(*y_shape) Z = rng.random(y_shape) + 1j * rng.random(y_shape)
continue continue
trace_U = real(trace(U)) trace_U = real(trace(U))
@ -646,17 +648,16 @@ def eigsolve(
Qi_memo: list[float | None] = [None, None] Qi_memo: list[float | None] = [None, None]
def Qi_func(theta: float) -> float: def Qi_func(theta: float, Qi_memo=Qi_memo, ZtZ=ZtZ, DtD=DtD, symZtD=symZtD) -> float: # noqa: ANN001
nonlocal Qi_memo
if Qi_memo[0] == theta: if Qi_memo[0] == theta:
return cast(float, Qi_memo[1]) return cast('float', Qi_memo[1])
c = numpy.cos(theta) c = numpy.cos(theta)
s = numpy.sin(theta) s = numpy.sin(theta)
Q = c * c * ZtZ + s * s * DtD + 2 * s * c * symZtD Q = c * c * ZtZ + s * s * DtD + 2 * s * c * symZtD
try: try:
Qi = numpy.linalg.inv(Q) Qi = numpy.linalg.inv(Q)
except numpy.linalg.LinAlgError: except numpy.linalg.LinAlgError as err:
logger.info('taylor Qi') logger.info('taylor Qi')
# if c or s small, taylor expand # if c or s small, taylor expand
if c < 1e-4 * s and c != 0: if c < 1e-4 * s and c != 0:
@ -666,12 +667,12 @@ def eigsolve(
ZtZi = numpy.linalg.inv(ZtZ) ZtZi = numpy.linalg.inv(ZtZ)
Qi = ZtZi / (c * c) - 2 * s / (c * c * c) * (ZtZi @ (ZtZi @ symZtD).conj().T) Qi = ZtZi / (c * c) - 2 * s / (c * c * c) * (ZtZi @ (ZtZi @ symZtD).conj().T)
else: else:
raise Exception('Inexplicable singularity in trace_func') raise Exception('Inexplicable singularity in trace_func') from err
Qi_memo[0] = theta Qi_memo[0] = theta
Qi_memo[1] = cast(float, Qi) Qi_memo[1] = cast('float', Qi)
return cast(float, Qi) return cast('float', Qi)
def trace_func(theta: float) -> float: def trace_func(theta: float, ZtAZ=ZtAZ, DtAD=DtAD, symZtAD=symZtAD) -> float: # noqa: ANN001
c = numpy.cos(theta) c = numpy.cos(theta)
s = numpy.sin(theta) s = numpy.sin(theta)
Qi = Qi_func(theta) Qi = Qi_func(theta)
@ -680,7 +681,7 @@ def eigsolve(
return numpy.abs(trace) return numpy.abs(trace)
if False: if False:
def trace_deriv(theta): def trace_deriv(theta, sgn: int = sgn, ZtAZ=ZtAZ, DtAD=DtAD, symZtD=symZtD, symZtAD=symZtAD, ZtZ=ZtZ, DtD=DtD): # noqa: ANN001
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)
@ -799,3 +800,62 @@ def _rtrace_AtB(
def _symmetrize(A: NDArray[numpy.complex128]) -> NDArray[numpy.complex128]: def _symmetrize(A: NDArray[numpy.complex128]) -> NDArray[numpy.complex128]:
return (A + A.conj().T) * 0.5 return (A + A.conj().T) * 0.5
def inner_product(
eL: cfdfield,
hL: cfdfield,
eR: cfdfield,
hR: cfdfield,
) -> complex:
# assumes x-axis propagation
assert numpy.array_equal(eR.shape, hR.shape)
assert numpy.array_equal(eL.shape, hL.shape)
assert numpy.array_equal(eR.shape, eL.shape)
# Cross product, times 2 since it's <p | n>, then divide by 4. # TODO might want to abs() this?
norm2R = (eR[1] * hR[2] - eR[2] * hR[1]).sum() / 2
norm2L = (eL[1] * hL[2] - eL[2] * hL[1]).sum() / 2
# eRxhR_x = numpy.cross(eR.reshape(3, -1), hR.reshape(3, -1), axis=0).reshape(eR.shape)[0] / normR
# logger.info(f'power {eRxhR_x.sum() / 2})
eR /= numpy.sqrt(norm2R)
hR /= numpy.sqrt(norm2R)
eL /= numpy.sqrt(norm2L)
hL /= numpy.sqrt(norm2L)
# (eR x hL)[0] and (eL x hR)[0]
eRxhL_x = eR[1] * hL[2] - eR[2] - hL[1]
eLxhR_x = eL[1] * hR[2] - eL[2] - hR[1]
#return 1j * (eRxhL_x - eLxhR_x).sum() / numpy.sqrt(norm2R * norm2L)
#return (eRxhL_x.sum() - eLxhR_x.sum()) / numpy.sqrt(norm2R * norm2L)
return eRxhL_x.sum() - eLxhR_x.sum()
def trq(
eI: cfdfield,
hI: cfdfield,
eO: cfdfield,
hO: cfdfield,
) -> tuple[complex, complex]:
pp = inner_product(eO, hO, eI, hI)
pn = inner_product(eO, hO, eI, -hI)
np = inner_product(eO, -hO, eI, hI)
nn = inner_product(eO, -hO, eI, -hI)
assert pp == -nn
assert pn == -np
logger.info(f'''
{pp=:4g} {pn=:4g}
{nn=:4g} {np=:4g}
{nn * pp / pn=:4g} {-np=:4g}
''')
r = -pp / pn # -<Pp|Bp>/<Pn/Bp> = -(-pp) / (-pn)
t = (np - nn * pp / pn) / 4
return t, r

84
meanas/fdfd/eme.py Normal file
View File

@ -0,0 +1,84 @@
from collections.abc import Sequence
import numpy
from numpy.typing import NDArray
from scipy import sparse
from ..fdmath import dx_lists2_t, vcfdfield2
from .waveguide_2d import inner_product
def get_tr(
ehLs: Sequence[Sequence[vcfdfield2]],
wavenumbers_L: Sequence[complex],
ehRs: Sequence[Sequence[vcfdfield2]],
wavenumbers_R: Sequence[complex],
dxes: dx_lists2_t,
) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]:
nL = len(wavenumbers_L)
nR = len(wavenumbers_R)
A12 = numpy.zeros((nL, nR), dtype=complex)
A21 = numpy.zeros((nL, nR), dtype=complex)
B11 = numpy.zeros((nL,), dtype=complex)
for ll in range(nL):
eL, hL = ehLs[ll]
B11[ll] = inner_product(eL, hL, dxes=dxes, conj_h=False)
for rr in range(nR):
eR, hR = ehRs[rr]
A12[ll, rr] = inner_product(eL, hR, dxes=dxes, conj_h=False) # TODO optimize loop?
A21[ll, rr] = inner_product(eR, hL, dxes=dxes, conj_h=False)
# tt0 = 2 * numpy.linalg.pinv(A21 + numpy.conj(A12))
tt0, _resid, _rank, _sing = numpy.linalg.lstsq(A21 + A12, numpy.diag(2 * B11), rcond=None)
U, st, V = numpy.linalg.svd(tt0)
gain = st > 1
st[gain] = 1 / st[gain]
tt = U @ numpy.diag(st) @ V
# rr = 0.5 * (A21 - numpy.conj(A12)) @ tt
rr = numpy.diag(0.5 / B11) @ (A21 - A12) @ tt
return tt, rr
def get_abcd(
ehLs: Sequence[Sequence[vcfdfield2]],
wavenumbers_L: Sequence[complex],
ehRs: Sequence[Sequence[vcfdfield2]],
wavenumbers_R: Sequence[complex],
**kwargs,
) -> sparse.sparray:
t12, r12 = get_tr(ehLs, wavenumbers_L, ehRs, wavenumbers_R, **kwargs)
t21, r21 = get_tr(ehRs, wavenumbers_R, ehLs, wavenumbers_L, **kwargs)
t21i = numpy.linalg.pinv(t21)
A = t12 - r21 @ t21i @ r12
B = r21 @ t21i
C = -t21i @ r12
D = t21i
return sparse.block_array(((A, B), (C, D)))
def get_s(
ehLs: Sequence[Sequence[vcfdfield2]],
wavenumbers_L: Sequence[complex],
ehRs: Sequence[Sequence[vcfdfield2]],
wavenumbers_R: Sequence[complex],
force_nogain: bool = False,
force_reciprocal: bool = False,
**kwargs,
) -> NDArray[numpy.complex128]:
t12, r12 = get_tr(ehLs, wavenumbers_L, ehRs, wavenumbers_R, **kwargs)
t21, r21 = get_tr(ehRs, wavenumbers_R, ehLs, wavenumbers_L, **kwargs)
ss = numpy.block([[r12, t12],
[t21, r21]])
if force_nogain:
# force S @ S.H diagonal
U, sing, V = numpy.linalg.svd(ss)
ss = numpy.diag(sing) @ U @ V
if force_reciprocal:
ss = 0.5 * (ss + ss.T)
return ss

View File

@ -1,13 +1,16 @@
""" """
Functions for performing near-to-farfield transformation (and the reverse). Functions for performing near-to-farfield transformation (and the reverse).
""" """
from typing import Any, Sequence, cast from typing import Any, cast, TYPE_CHECKING
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 ..fdmath import cfdfield_t from ..fdmath import cfdfield_t
if TYPE_CHECKING:
from collections.abc import Sequence
def near_to_farfield( def near_to_farfield(
E_near: cfdfield_t, E_near: cfdfield_t,
@ -62,7 +65,7 @@ def near_to_farfield(
padded_size = (2**numpy.ceil(numpy.log2(s))).astype(int) padded_size = (2**numpy.ceil(numpy.log2(s))).astype(int)
if not hasattr(padded_size, '__len__'): if not hasattr(padded_size, '__len__'):
padded_size = (padded_size, padded_size) # type: ignore # checked if sequence padded_size = (padded_size, padded_size) # type: ignore # checked if sequence
padded_shape = cast(Sequence[int], padded_size) padded_shape = cast('Sequence[int]', padded_size)
En_fft = [fftshift(fft2(fftshift(Eni), s=padded_shape)) for Eni in E_near] En_fft = [fftshift(fft2(fftshift(Eni), s=padded_shape)) for Eni in E_near]
Hn_fft = [fftshift(fft2(fftshift(Hni), s=padded_shape)) for Hni in H_near] Hn_fft = [fftshift(fft2(fftshift(Hni), s=padded_shape)) for Hni in H_near]
@ -171,7 +174,7 @@ def far_to_nearfield(
padded_size = (2 ** numpy.ceil(numpy.log2(s))).astype(int) padded_size = (2 ** numpy.ceil(numpy.log2(s))).astype(int)
if not hasattr(padded_size, '__len__'): if not hasattr(padded_size, '__len__'):
padded_size = (padded_size, padded_size) # type: ignore # checked if sequence padded_size = (padded_size, padded_size) # type: ignore # checked if sequence
padded_shape = cast(Sequence[int], padded_size) padded_shape = cast('Sequence[int]', padded_size)
k = 2 * pi k = 2 * pi
kxs = fftshift(fftfreq(s[0], 1 / (s[0] * dkx))) kxs = fftshift(fftfreq(s[0], 1 / (s[0] * dkx)))

View File

@ -5,10 +5,10 @@ Functional versions of many FDFD operators. These can be useful for performing
The functions generated here expect `cfdfield_t` inputs with shape (3, X, Y, Z), The functions generated here expect `cfdfield_t` inputs with shape (3, X, Y, Z),
e.g. E = [E_x, E_y, E_z] where each (complex) component has shape (X, Y, Z) e.g. E = [E_x, E_y, E_z] where each (complex) component has shape (X, Y, Z)
""" """
from typing import Callable from collections.abc import Callable
import numpy import numpy
from ..fdmath import dx_lists_t, fdfield_t, cfdfield_t, cfdfield_updater_t from ..fdmath import dx_lists_t, cfdfield_t, fdfield, cfdfield, cfdfield_updater_t
from ..fdmath.functional import curl_forward, curl_back from ..fdmath.functional import curl_forward, curl_back
@ -18,8 +18,8 @@ __author__ = 'Jan Petykiewicz'
def e_full( def e_full(
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists_t,
epsilon: fdfield_t, epsilon: fdfield,
mu: fdfield_t | None = None, mu: fdfield | None = None,
) -> cfdfield_updater_t: ) -> cfdfield_updater_t:
""" """
Wave operator for use with E-field. See `operators.e_full` for details. Wave operator for use with E-field. See `operators.e_full` for details.
@ -37,26 +37,25 @@ def e_full(
ch = curl_back(dxes[1]) ch = curl_back(dxes[1])
ce = curl_forward(dxes[0]) ce = curl_forward(dxes[0])
def op_1(e: cfdfield_t) -> cfdfield_t: def op_1(e: cfdfield) -> cfdfield_t:
curls = ch(ce(e)) curls = ch(ce(e))
return curls - omega ** 2 * epsilon * e return cfdfield_t(curls - omega ** 2 * epsilon * e)
def op_mu(e: cfdfield_t) -> cfdfield_t: def op_mu(e: cfdfield) -> cfdfield_t:
curls = ch(mu * ce(e)) # type: ignore # mu = None ok because we don't return the function curls = ch(mu * ce(e)) # type: ignore # mu = None ok because we don't return the function
return curls - omega ** 2 * epsilon * e return cfdfield_t(curls - omega ** 2 * epsilon * e)
if mu is None: if mu is None:
return op_1 return op_1
else:
return op_mu return op_mu
def eh_full( def eh_full(
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists_t,
epsilon: fdfield_t, epsilon: fdfield,
mu: fdfield_t | None = None, mu: fdfield | None = None,
) -> Callable[[cfdfield_t, cfdfield_t], tuple[cfdfield_t, cfdfield_t]]: ) -> Callable[[cfdfield, cfdfield], tuple[cfdfield_t, cfdfield_t]]:
""" """
Wave operator for full (both E and H) field representation. Wave operator for full (both E and H) field representation.
See `operators.eh_full`. See `operators.eh_full`.
@ -74,24 +73,23 @@ def eh_full(
ch = curl_back(dxes[1]) ch = curl_back(dxes[1])
ce = curl_forward(dxes[0]) ce = curl_forward(dxes[0])
def op_1(e: cfdfield_t, h: cfdfield_t) -> tuple[cfdfield_t, cfdfield_t]: def op_1(e: cfdfield, h: cfdfield) -> tuple[cfdfield_t, cfdfield_t]:
return (ch(h) - 1j * omega * epsilon * e, return (cfdfield_t(ch(h) - 1j * omega * epsilon * e),
ce(e) + 1j * omega * h) cfdfield_t(ce(e) + 1j * omega * h))
def op_mu(e: cfdfield_t, h: cfdfield_t) -> tuple[cfdfield_t, cfdfield_t]: def op_mu(e: cfdfield, h: cfdfield) -> tuple[cfdfield_t, cfdfield_t]:
return (ch(h) - 1j * omega * epsilon * e, return (cfdfield_t(ch(h) - 1j * omega * epsilon * e),
ce(e) + 1j * omega * mu * h) # type: ignore # mu=None ok cfdfield_t(ce(e) + 1j * omega * mu * h)) # type: ignore # mu=None ok
if mu is None: if mu is None:
return op_1 return op_1
else:
return op_mu return op_mu
def e2h( def e2h(
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists_t,
mu: fdfield_t | None = None, mu: fdfield | None = None,
) -> cfdfield_updater_t: ) -> cfdfield_updater_t:
""" """
Utility operator for converting the `E` field into the `H` field. Utility operator for converting the `E` field into the `H` field.
@ -108,22 +106,21 @@ def e2h(
""" """
ce = curl_forward(dxes[0]) ce = curl_forward(dxes[0])
def e2h_1_1(e: cfdfield_t) -> cfdfield_t: def e2h_1_1(e: cfdfield) -> cfdfield_t:
return ce(e) / (-1j * omega) return cfdfield_t(ce(e) / (-1j * omega))
def e2h_mu(e: cfdfield_t) -> cfdfield_t: def e2h_mu(e: cfdfield) -> cfdfield_t:
return ce(e) / (-1j * omega * mu) # type: ignore # mu=None ok return cfdfield_t(ce(e) / (-1j * omega * mu)) # type: ignore # mu=None ok
if mu is None: if mu is None:
return e2h_1_1 return e2h_1_1
else:
return e2h_mu return e2h_mu
def m2j( def m2j(
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists_t,
mu: fdfield_t | None = None, mu: fdfield | None = None,
) -> cfdfield_updater_t: ) -> cfdfield_updater_t:
""" """
Utility operator for converting magnetic current `M` distribution Utility operator for converting magnetic current `M` distribution
@ -141,26 +138,25 @@ def m2j(
""" """
ch = curl_back(dxes[1]) ch = curl_back(dxes[1])
def m2j_mu(m: cfdfield_t) -> cfdfield_t: def m2j_mu(m: cfdfield) -> cfdfield_t:
J = ch(m / mu) / (-1j * omega) # type: ignore # mu=None ok J = ch(m / mu) / (-1j * omega) # type: ignore # mu=None ok
return J return cfdfield_t(J)
def m2j_1(m: cfdfield_t) -> cfdfield_t: def m2j_1(m: cfdfield) -> cfdfield_t:
J = ch(m) / (-1j * omega) J = ch(m) / (-1j * omega)
return J return cfdfield_t(J)
if mu is None: if mu is None:
return m2j_1 return m2j_1
else:
return m2j_mu return m2j_mu
def e_tfsf_source( def e_tfsf_source(
TF_region: fdfield_t, TF_region: fdfield,
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists_t,
epsilon: fdfield_t, epsilon: fdfield,
mu: fdfield_t | None = None, mu: fdfield | None = None,
) -> cfdfield_updater_t: ) -> cfdfield_updater_t:
""" """
Operator that turns an E-field distribution into a total-field/scattered-field Operator that turns an E-field distribution into a total-field/scattered-field
@ -182,13 +178,13 @@ def e_tfsf_source(
# TODO documentation # TODO documentation
A = e_full(omega, dxes, epsilon, mu) A = e_full(omega, dxes, epsilon, mu)
def op(e: cfdfield_t) -> cfdfield_t: def op(e: cfdfield) -> cfdfield_t:
neg_iwj = A(TF_region * e) - TF_region * A(e) neg_iwj = A(TF_region * e) - TF_region * A(e)
return neg_iwj / (-1j * omega) return cfdfield_t(neg_iwj / (-1j * omega))
return op return op
def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield_t, cfdfield_t], cfdfield_t]: def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield, cfdfield], cfdfield_t]:
r""" r"""
Generates a function that takes the single-frequency `E` and `H` fields Generates a function that takes the single-frequency `E` and `H` fields
and calculates the cross product `E` x `H` = $E \times H$ as required and calculates the cross product `E` x `H` = $E \times H$ as required
@ -210,7 +206,7 @@ def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield_t, cfdfield_t], c
Returns: Returns:
Function `f` that returns E x H as required for the poynting vector. Function `f` that returns E x H as required for the poynting vector.
""" """
def exh(e: cfdfield_t, h: cfdfield_t) -> cfdfield_t: def exh(e: cfdfield, h: cfdfield) -> cfdfield_t:
s = numpy.empty_like(e) s = numpy.empty_like(e)
ex = e[0] * dxes[0][0][:, None, None] ex = e[0] * dxes[0][0][:, None, None]
ey = e[1] * dxes[0][1][None, :, None] ey = e[1] * dxes[0][1][None, :, None]
@ -221,5 +217,5 @@ def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield_t, cfdfield_t], c
s[0] = numpy.roll(ey, -1, axis=0) * hz - numpy.roll(ez, -1, axis=0) * hy s[0] = numpy.roll(ey, -1, axis=0) * hz - numpy.roll(ez, -1, axis=0) * hy
s[1] = numpy.roll(ez, -1, axis=1) * hx - numpy.roll(ex, -1, axis=1) * hz s[1] = numpy.roll(ez, -1, axis=1) * hx - numpy.roll(ex, -1, axis=1) * hz
s[2] = numpy.roll(ex, -1, axis=2) * hy - numpy.roll(ey, -1, axis=2) * hx s[2] = numpy.roll(ex, -1, axis=2) * hy - numpy.roll(ey, -1, axis=2) * hx
return s return cfdfield_t(s)
return exh return exh

View File

@ -1,7 +1,7 @@
""" """
Sparse matrix operators for use with electromagnetic wave equations. Sparse matrix operators for use with electromagnetic wave equations.
These functions return sparse-matrix (`scipy.sparse.spmatrix`) representations of These functions return sparse-matrix (`scipy.sparse.sparray`) representations of
a variety of operators, intended for use with E and H fields vectorized using the a variety of operators, intended for use with E and H fields vectorized using the
`meanas.fdmath.vectorization.vec()` and `meanas.fdmath.vectorization.unvec()` functions. `meanas.fdmath.vectorization.vec()` and `meanas.fdmath.vectorization.unvec()` functions.
@ -28,9 +28,9 @@ The following operators are included:
""" """
import numpy import numpy
import scipy.sparse as sparse # type: ignore from scipy import sparse
from ..fdmath import vec, dx_lists_t, vfdfield_t, vcfdfield_t from ..fdmath import vec, dx_lists_t, vfdfield, vcfdfield
from ..fdmath.operators import shift_with_mirror, shift_circ, curl_forward, curl_back from ..fdmath.operators import shift_with_mirror, shift_circ, curl_forward, curl_back
@ -40,11 +40,11 @@ __author__ = 'Jan Petykiewicz'
def e_full( def e_full(
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists_t,
epsilon: vfdfield_t, epsilon: vfdfield | vcfdfield,
mu: vfdfield_t | None = None, mu: vfdfield | None = None,
pec: vfdfield_t | None = None, pec: vfdfield | None = None,
pmc: vfdfield_t | None = None, pmc: vfdfield | None = None,
) -> sparse.spmatrix: ) -> sparse.sparray:
r""" r"""
Wave operator Wave operator
$$ \nabla \times (\frac{1}{\mu} \nabla \times) - \Omega^2 \epsilon $$ $$ \nabla \times (\frac{1}{\mu} \nabla \times) - \Omega^2 \epsilon $$
@ -77,20 +77,20 @@ def e_full(
ce = curl_forward(dxes[0]) ce = curl_forward(dxes[0])
if pec is None: if pec is None:
pe = sparse.eye(epsilon.size) pe = sparse.eye_array(epsilon.size)
else: else:
pe = sparse.diags(numpy.where(pec, 0, 1)) # Set pe to (not PEC) pe = sparse.diags_array(numpy.where(pec, 0, 1)) # Set pe to (not PEC)
if pmc is None: if pmc is None:
pm = sparse.eye(epsilon.size) pm = sparse.eye_array(epsilon.size)
else: else:
pm = sparse.diags(numpy.where(pmc, 0, 1)) # set pm to (not PMC) pm = sparse.diags_array(numpy.where(pmc, 0, 1)) # set pm to (not PMC)
e = sparse.diags(epsilon) e = sparse.diags_array(epsilon)
if mu is None: if mu is None:
m_div = sparse.eye(epsilon.size) m_div = sparse.eye_array(epsilon.size)
else: else:
m_div = sparse.diags(1 / mu) m_div = sparse.diags_array(1 / mu)
op = pe @ (ch @ pm @ m_div @ ce - omega**2 * e) @ pe op = pe @ (ch @ pm @ m_div @ ce - omega**2 * e) @ pe
return op return op
@ -98,7 +98,7 @@ def e_full(
def e_full_preconditioners( def e_full_preconditioners(
dxes: dx_lists_t, dxes: dx_lists_t,
) -> tuple[sparse.spmatrix, sparse.spmatrix]: ) -> tuple[sparse.sparray, sparse.sparray]:
""" """
Left and right preconditioners `(Pl, Pr)` for symmetrizing the `e_full` wave operator. Left and right preconditioners `(Pl, Pr)` for symmetrizing the `e_full` wave operator.
@ -118,19 +118,19 @@ def e_full_preconditioners(
dxes[1][0][:, None, None] * dxes[1][1][None, :, None] * dxes[0][2][None, None, :]] dxes[1][0][:, None, None] * dxes[1][1][None, :, None] * dxes[0][2][None, None, :]]
p_vector = numpy.sqrt(vec(p_squared)) p_vector = numpy.sqrt(vec(p_squared))
P_left = sparse.diags(p_vector) P_left = sparse.diags_array(p_vector)
P_right = sparse.diags(1 / p_vector) P_right = sparse.diags_array(1 / p_vector)
return P_left, P_right return P_left, P_right
def h_full( def h_full(
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists_t,
epsilon: vfdfield_t, epsilon: vfdfield,
mu: vfdfield_t | None = None, mu: vfdfield | None = None,
pec: vfdfield_t | None = None, pec: vfdfield | None = None,
pmc: vfdfield_t | None = None, pmc: vfdfield | None = None,
) -> sparse.spmatrix: ) -> sparse.sparray:
r""" r"""
Wave operator Wave operator
$$ \nabla \times (\frac{1}{\epsilon} \nabla \times) - \omega^2 \mu $$ $$ \nabla \times (\frac{1}{\epsilon} \nabla \times) - \omega^2 \mu $$
@ -161,20 +161,20 @@ def h_full(
ce = curl_forward(dxes[0]) ce = curl_forward(dxes[0])
if pec is None: if pec is None:
pe = sparse.eye(epsilon.size) pe = sparse.eye_array(epsilon.size)
else: else:
pe = sparse.diags(numpy.where(pec, 0, 1)) # set pe to (not PEC) pe = sparse.diags_array(numpy.where(pec, 0, 1)) # set pe to (not PEC)
if pmc is None: if pmc is None:
pm = sparse.eye(epsilon.size) pm = sparse.eye_array(epsilon.size)
else: else:
pm = sparse.diags(numpy.where(pmc, 0, 1)) # Set pe to (not PMC) pm = sparse.diags_array(numpy.where(pmc, 0, 1)) # Set pe to (not PMC)
e_div = sparse.diags(1 / epsilon) e_div = sparse.diags_array(1 / epsilon)
if mu is None: if mu is None:
m = sparse.eye(epsilon.size) m = sparse.eye_array(epsilon.size)
else: else:
m = sparse.diags(mu) m = sparse.diags_array(mu)
A = pm @ (ce @ pe @ e_div @ ch - omega**2 * m) @ pm A = pm @ (ce @ pe @ e_div @ ch - omega**2 * m) @ pm
return A return A
@ -183,11 +183,11 @@ def h_full(
def eh_full( def eh_full(
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists_t,
epsilon: vfdfield_t, epsilon: vfdfield,
mu: vfdfield_t | None = None, mu: vfdfield | None = None,
pec: vfdfield_t | None = None, pec: vfdfield | None = None,
pmc: vfdfield_t | None = None, pmc: vfdfield | None = None,
) -> sparse.spmatrix: ) -> sparse.sparray:
r""" r"""
Wave operator for `[E, H]` field representation. This operator implements Maxwell's Wave operator for `[E, H]` field representation. This operator implements Maxwell's
equations without cancelling out either E or H. The operator is equations without cancelling out either E or H. The operator is
@ -227,25 +227,25 @@ def eh_full(
Sparse matrix containing the wave operator. Sparse matrix containing the wave operator.
""" """
if pec is None: if pec is None:
pe = sparse.eye(epsilon.size) pe = sparse.eye_array(epsilon.size)
else: else:
pe = sparse.diags(numpy.where(pec, 0, 1)) # set pe to (not PEC) pe = sparse.diags_array(numpy.where(pec, 0, 1)) # set pe to (not PEC)
if pmc is None: if pmc is None:
pm = sparse.eye(epsilon.size) pm = sparse.eye_array(epsilon.size)
else: else:
pm = sparse.diags(numpy.where(pmc, 0, 1)) # set pm to (not PMC) pm = sparse.diags_array(numpy.where(pmc, 0, 1)) # set pm to (not PMC)
iwe = pe @ (1j * omega * sparse.diags(epsilon)) @ pe iwe = pe @ (1j * omega * sparse.diags_array(epsilon)) @ pe
iwm = 1j * omega iwm = 1j * omega
if mu is not None: if mu is not None:
iwm *= sparse.diags(mu) iwm *= sparse.diags_array(mu)
iwm = pm @ iwm @ pm iwm = pm @ iwm @ pm
A1 = pe @ curl_back(dxes[1]) @ pm A1 = pe @ curl_back(dxes[1]) @ pm
A2 = pm @ curl_forward(dxes[0]) @ pe A2 = pm @ curl_forward(dxes[0]) @ pe
A = sparse.bmat([[-iwe, A1], A = sparse.block_array([[-iwe, A1],
[A2, iwm]]) [A2, iwm]])
return A return A
@ -253,9 +253,9 @@ def eh_full(
def e2h( def e2h(
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists_t,
mu: vfdfield_t | None = None, mu: vfdfield | None = None,
pmc: vfdfield_t | None = None, pmc: vfdfield | None = None,
) -> sparse.spmatrix: ) -> sparse.sparray:
""" """
Utility operator for converting the E field into the H field. Utility operator for converting the E field into the H field.
For use with `e_full()` -- assumes that there is no magnetic current M. For use with `e_full()` -- assumes that there is no magnetic current M.
@ -274,10 +274,10 @@ def e2h(
op = curl_forward(dxes[0]) / (-1j * omega) op = curl_forward(dxes[0]) / (-1j * omega)
if mu is not None: if mu is not None:
op = sparse.diags(1 / mu) @ op op = sparse.diags_array(1 / mu) @ op
if pmc is not None: if pmc is not None:
op = sparse.diags(numpy.where(pmc, 0, 1)) @ op op = sparse.diags_array(numpy.where(pmc, 0, 1)) @ op
return op return op
@ -285,8 +285,8 @@ def e2h(
def m2j( def m2j(
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists_t,
mu: vfdfield_t | None = None, mu: vfdfield | None = None,
) -> sparse.spmatrix: ) -> sparse.sparray:
""" """
Operator for converting a magnetic current M into an electric current J. Operator for converting a magnetic current M into an electric current J.
For use with eg. `e_full()`. For use with eg. `e_full()`.
@ -302,12 +302,12 @@ def m2j(
op = curl_back(dxes[1]) / (1j * omega) op = curl_back(dxes[1]) / (1j * omega)
if mu is not None: if mu is not None:
op = op @ sparse.diags(1 / mu) op = op @ sparse.diags_array(1 / mu)
return op return op
def poynting_e_cross(e: vcfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix: def poynting_e_cross(e: vcfdfield, dxes: dx_lists_t) -> sparse.sparray:
""" """
Operator for computing the Poynting vector, containing the Operator for computing the Poynting vector, containing the
(E x) portion of the Poynting vector. (E x) portion of the Poynting vector.
@ -321,22 +321,22 @@ def poynting_e_cross(e: vcfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix:
""" """
shape = [len(dx) for dx in dxes[0]] shape = [len(dx) for dx in dxes[0]]
fx, fy, fz = [shift_circ(i, shape, 1) for i in range(3)] fx, fy, fz = (shift_circ(i, shape, 1) for i in range(3))
dxag = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')] dxag = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')]
dxbg = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[1], indexing='ij')] dxbg = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[1], indexing='ij')]
Ex, Ey, Ez = [ei * da for ei, da in zip(numpy.split(e, 3), dxag)] Ex, Ey, Ez = (ei * da for ei, da in zip(numpy.split(e, 3), dxag, strict=True))
block_diags = [[ None, fx @ -Ez, fx @ Ey], block_diags = [[ None, fx @ -Ez, fx @ Ey],
[ fy @ Ez, None, fy @ -Ex], [ fy @ Ez, None, fy @ -Ex],
[ fz @ -Ey, fz @ Ex, None]] [ fz @ -Ey, fz @ Ex, None]]
block_matrix = sparse.bmat([[sparse.diags(x) if x is not None else None for x in row] block_matrix = sparse.block_array([[sparse.diags_array(x) if x is not None else None for x in row]
for row in block_diags]) for row in block_diags])
P = block_matrix @ sparse.diags(numpy.concatenate(dxbg)) P = block_matrix @ sparse.diags_array(numpy.concatenate(dxbg))
return P return P
def poynting_h_cross(h: vcfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix: def poynting_h_cross(h: vcfdfield, dxes: dx_lists_t) -> sparse.sparray:
""" """
Operator for computing the Poynting vector, containing the (H x) portion of the Poynting vector. Operator for computing the Poynting vector, containing the (H x) portion of the Poynting vector.
@ -349,27 +349,27 @@ def poynting_h_cross(h: vcfdfield_t, dxes: dx_lists_t) -> sparse.spmatrix:
""" """
shape = [len(dx) for dx in dxes[0]] shape = [len(dx) for dx in dxes[0]]
fx, fy, fz = [shift_circ(i, shape, 1) for i in range(3)] fx, fy, fz = (shift_circ(i, shape, 1) for i in range(3))
dxag = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')] dxag = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[0], indexing='ij')]
dxbg = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[1], indexing='ij')] dxbg = [dx.ravel(order='C') for dx in numpy.meshgrid(*dxes[1], indexing='ij')]
Hx, Hy, Hz = [sparse.diags(hi * db) for hi, db in zip(numpy.split(h, 3), dxbg)] Hx, Hy, Hz = (sparse.diags_array(hi * db) for hi, db in zip(numpy.split(h, 3), dxbg, strict=True))
P = (sparse.bmat( P = (sparse.block_array(
[[ None, -Hz @ fx, Hy @ fx], [[ None, -Hz @ fx, Hy @ fx],
[ Hz @ fy, None, -Hx @ fy], [ Hz @ fy, None, -Hx @ fy],
[-Hy @ fz, Hx @ fz, None]]) [-Hy @ fz, Hx @ fz, None]])
@ sparse.diags(numpy.concatenate(dxag))) @ sparse.diags_array(numpy.concatenate(dxag)))
return P return P
def e_tfsf_source( def e_tfsf_source(
TF_region: vfdfield_t, TF_region: vfdfield,
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists_t,
epsilon: vfdfield_t, epsilon: vfdfield,
mu: vfdfield_t | None = None, mu: vfdfield | None = None,
) -> sparse.spmatrix: ) -> sparse.sparray:
""" """
Operator that turns a desired E-field distribution into a Operator that turns a desired E-field distribution into a
total-field/scattered-field (TFSF) source. total-field/scattered-field (TFSF) source.
@ -390,18 +390,18 @@ def e_tfsf_source(
""" """
# TODO documentation # TODO documentation
A = e_full(omega, dxes, epsilon, mu) A = e_full(omega, dxes, epsilon, mu)
Q = sparse.diags(TF_region) Q = sparse.diags_array(TF_region)
return (A @ Q - Q @ A) / (-1j * omega) return (A @ Q - Q @ A) / (-1j * omega)
def e_boundary_source( def e_boundary_source(
mask: vfdfield_t, mask: vfdfield,
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists_t,
epsilon: vfdfield_t, epsilon: vfdfield,
mu: vfdfield_t | None = None, mu: vfdfield | None = None,
periodic_mask_edges: bool = False, periodic_mask_edges: bool = False,
) -> sparse.spmatrix: ) -> sparse.sparray:
""" """
Operator that turns an E-field distrubtion into a current (J) distribution Operator that turns an E-field distrubtion into a current (J) distribution
along the edges (external and internal) of the provided mask. This is just an along the edges (external and internal) of the provided mask. This is just an
@ -424,10 +424,10 @@ def e_boundary_source(
shape = [len(dxe) for dxe in dxes[0]] shape = [len(dxe) for dxe in dxes[0]]
jmask = numpy.zeros_like(mask, dtype=bool) jmask = numpy.zeros_like(mask, dtype=bool)
def shift_rot(axis: int, polarity: int) -> sparse.spmatrix: def shift_rot(axis: int, polarity: int) -> sparse.sparray:
return shift_circ(axis=axis, shape=shape, shift_distance=polarity) return shift_circ(axis=axis, shape=shape, shift_distance=polarity)
def shift_mir(axis: int, polarity: int) -> sparse.spmatrix: def shift_mir(axis: int, polarity: int) -> sparse.sparray:
return shift_with_mirror(axis=axis, shape=shape, shift_distance=polarity) return shift_with_mirror(axis=axis, shape=shape, shift_distance=polarity)
shift = shift_rot if periodic_mask_edges else shift_mir shift = shift_rot if periodic_mask_edges else shift_mir
@ -436,7 +436,7 @@ def e_boundary_source(
if shape[axis] == 1: if shape[axis] == 1:
continue continue
for polarity in (-1, +1): for polarity in (-1, +1):
r = shift(axis, polarity) - sparse.eye(numpy.prod(shape)) # shifted minus original r = shift(axis, polarity) - sparse.eye_array(numpy.prod(shape)) # shifted minus original
r3 = sparse.block_diag((r, r, r)) r3 = sparse.block_diag((r, r, r))
jmask = numpy.logical_or(jmask, numpy.abs(r3 @ mask)) jmask = numpy.logical_or(jmask, numpy.abs(r3 @ mask))
@ -447,5 +447,5 @@ def e_boundary_source(
# (numpy.roll(mask, -1, axis=2) != mask) | # (numpy.roll(mask, -1, axis=2) != mask) |
# (numpy.roll(mask, +1, axis=2) != mask)) # (numpy.roll(mask, +1, axis=2) != mask))
return sparse.diags(jmask.astype(int)) @ full return sparse.diags_array(jmask.astype(int)) @ full

View File

@ -2,7 +2,7 @@
Functions for creating stretched coordinate perfectly matched layer (PML) absorbers. Functions for creating stretched coordinate perfectly matched layer (PML) absorbers.
""" """
from typing import Sequence, Callable from collections.abc import Sequence, Callable
import numpy import numpy
from numpy.typing import NDArray from numpy.typing import NDArray

View File

@ -2,15 +2,16 @@
Solvers and solver interface for FDFD problems. Solvers and solver interface for FDFD problems.
""" """
from typing import Callable, Dict, Any, Optional from typing import Any
from collections.abc import Callable
import logging import logging
import numpy import numpy
from numpy.typing import ArrayLike, NDArray from numpy.typing import ArrayLike, NDArray
from numpy.linalg import norm from numpy.linalg import norm
import scipy.sparse.linalg # type: ignore import scipy.sparse.linalg
from ..fdmath import dx_lists_t, vfdfield_t, vcfdfield_t from ..fdmath import dx_lists_t, vfdfield, vcfdfield, vcfdfield_t
from . import operators from . import operators
@ -18,7 +19,7 @@ logger = logging.getLogger(__name__)
def _scipy_qmr( def _scipy_qmr(
A: scipy.sparse.csr_matrix, A: scipy.sparse.csr_array,
b: ArrayLike, b: ArrayLike,
**kwargs: Any, **kwargs: Any,
) -> NDArray[numpy.float64]: ) -> NDArray[numpy.float64]:
@ -34,16 +35,16 @@ def _scipy_qmr(
Guess for solution (returned even if didn't converge) Guess for solution (returned even if didn't converge)
""" """
''' #
Report on our progress #Report on our progress
''' #
ii = 0 ii = 0
def log_residual(xk: ArrayLike) -> None: def log_residual(xk: ArrayLike) -> None:
nonlocal ii nonlocal ii
ii += 1 ii += 1
if ii % 100 == 0: if ii % 100 == 0:
cur_norm = norm(A @ xk - b) cur_norm = norm(A @ xk - b) / norm(b)
logger.info(f'Solver residual at iteration {ii} : {cur_norm}') logger.info(f'Solver residual at iteration {ii} : {cur_norm}')
if 'callback' in kwargs: if 'callback' in kwargs:
@ -55,10 +56,9 @@ def _scipy_qmr(
else: else:
kwargs['callback'] = log_residual kwargs['callback'] = log_residual
''' #
Run the actual solve # Run the actual solve
''' #
x, _ = scipy.sparse.linalg.qmr(A, b, **kwargs) x, _ = scipy.sparse.linalg.qmr(A, b, **kwargs)
return x return x
@ -66,14 +66,16 @@ def _scipy_qmr(
def generic( def generic(
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists_t,
J: vcfdfield_t, J: vcfdfield,
epsilon: vfdfield_t, epsilon: vfdfield,
mu: Optional[vfdfield_t] = None, mu: vfdfield | None = None,
pec: Optional[vfdfield_t] = None, *,
pmc: Optional[vfdfield_t] = None, pec: vfdfield | None = None,
pmc: vfdfield | None = None,
adjoint: bool = False, adjoint: bool = False,
matrix_solver: Callable[..., ArrayLike] = _scipy_qmr, matrix_solver: Callable[..., ArrayLike] = _scipy_qmr,
matrix_solver_opts: Optional[Dict[str, Any]] = None, matrix_solver_opts: dict[str, Any] | None = None,
E_guess: vcfdfield | None = None,
) -> vcfdfield_t: ) -> vcfdfield_t:
""" """
Conjugate gradient FDFD solver using CSR sparse matrices. Conjugate gradient FDFD solver using CSR sparse matrices.
@ -93,13 +95,15 @@ def generic(
(at H-field locations; non-zero value indicates PMC is present) (at H-field locations; non-zero value indicates PMC is present)
adjoint: If true, solves the adjoint problem. adjoint: If true, solves the adjoint problem.
matrix_solver: Called as `matrix_solver(A, b, **matrix_solver_opts) -> x`, matrix_solver: Called as `matrix_solver(A, b, **matrix_solver_opts) -> x`,
where `A`: `scipy.sparse.csr_matrix`; where `A`: `scipy.sparse.csr_array`;
`b`: `ArrayLike`; `b`: `ArrayLike`;
`x`: `ArrayLike`; `x`: `ArrayLike`;
Default is a wrapped version of `scipy.sparse.linalg.qmr()` Default is a wrapped version of `scipy.sparse.linalg.qmr()`
which doesn't return convergence info and logs the residual which doesn't return convergence info and logs the residual
every 100 iterations. every 100 iterations.
matrix_solver_opts: Passed as kwargs to `matrix_solver(...)` matrix_solver_opts: Passed as kwargs to `matrix_solver(...)`
E_guess: Guess at the solution E-field. `matrix_solver` must accept an
`x0` argument with the same purpose.
Returns: Returns:
E-field which solves the system. E-field which solves the system.
@ -120,6 +124,13 @@ def generic(
A = Pl @ A0 @ Pr A = Pl @ A0 @ Pr
b = Pl @ b0 b = Pl @ b0
if E_guess is not None:
if adjoint:
x0 = Pr.H @ E_guess
else:
x0 = Pl @ E_guess
matrix_solver_opts['x0'] = x0
x = matrix_solver(A.tocsr(), b, **matrix_solver_opts) x = matrix_solver(A.tocsr(), b, **matrix_solver_opts)
if adjoint: if adjoint:
@ -127,4 +138,4 @@ def generic(
else: else:
x0 = Pr @ x x0 = Pr @ x
return x0 return vcfdfield_t(x0)

View File

@ -18,8 +18,8 @@ $$
\begin{aligned} \begin{aligned}
\nabla \times \vec{E}(x, y, z) &= -\imath \omega \mu \vec{H} \\ \nabla \times \vec{E}(x, y, z) &= -\imath \omega \mu \vec{H} \\
\nabla \times \vec{H}(x, y, z) &= \imath \omega \epsilon \vec{E} \\ \nabla \times \vec{H}(x, y, z) &= \imath \omega \epsilon \vec{E} \\
\vec{E}(x,y,z) &= (\vec{E}_t(x, y) + E_z(x, y)\vec{z}) e^{-\gamma z} \\ \vec{E}(x,y,z) &= (\vec{E}_t(x, y) + E_z(x, y)\vec{z}) e^{-\imath \beta z} \\
\vec{H}(x,y,z) &= (\vec{H}_t(x, y) + H_z(x, y)\vec{z}) e^{-\gamma z} \\ \vec{H}(x,y,z) &= (\vec{H}_t(x, y) + H_z(x, y)\vec{z}) e^{-\imath \beta z} \\
\end{aligned} \end{aligned}
$$ $$
@ -40,56 +40,57 @@ Substituting in our expressions for $\vec{E}$, $\vec{H}$ and discretizing:
$$ $$
\begin{aligned} \begin{aligned}
-\imath \omega \mu_{xx} H_x &= \tilde{\partial}_y E_z + \gamma E_y \\ -\imath \omega \mu_{xx} H_x &= \tilde{\partial}_y E_z + \imath \beta E_y \\
-\imath \omega \mu_{yy} H_y &= -\gamma E_x - \tilde{\partial}_x E_z \\ -\imath \omega \mu_{yy} H_y &= -\imath \beta E_x - \tilde{\partial}_x E_z \\
-\imath \omega \mu_{zz} H_z &= \tilde{\partial}_x E_y - \tilde{\partial}_y E_x \\ -\imath \omega \mu_{zz} H_z &= \tilde{\partial}_x E_y - \tilde{\partial}_y E_x \\
\imath \omega \epsilon_{xx} E_x &= \hat{\partial}_y H_z + \gamma H_y \\ \imath \omega \epsilon_{xx} E_x &= \hat{\partial}_y H_z + \imath \beta H_y \\
\imath \omega \epsilon_{yy} E_y &= -\gamma H_x - \hat{\partial}_x H_z \\ \imath \omega \epsilon_{yy} E_y &= -\imath \beta H_x - \hat{\partial}_x H_z \\
\imath \omega \epsilon_{zz} E_z &= \hat{\partial}_x H_y - \hat{\partial}_y H_x \\ \imath \omega \epsilon_{zz} E_z &= \hat{\partial}_x H_y - \hat{\partial}_y H_x \\
\end{aligned} \end{aligned}
$$ $$
Rewrite the last three equations as Rewrite the last three equations as
$$ $$
\begin{aligned} \begin{aligned}
\gamma H_y &= \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z \\ \imath \beta H_y &= \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z \\
\gamma H_x &= -\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z \\ \imath \beta H_x &= -\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z \\
\imath \omega E_z &= \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y - \frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\ \imath \omega E_z &= \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y - \frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\
\end{aligned} \end{aligned}
$$ $$
Now apply $\gamma \tilde{\partial}_x$ to the last equation, Now apply $\imath \beta \tilde{\partial}_x$ to the last equation,
then substitute in for $\gamma H_x$ and $\gamma H_y$: then substitute in for $\imath \beta H_x$ and $\imath \beta H_y$:
$$ $$
\begin{aligned} \begin{aligned}
\gamma \tilde{\partial}_x \imath \omega E_z &= \gamma \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y \imath \beta \tilde{\partial}_x \imath \omega E_z &= \imath \beta \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y
- \gamma \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\ - \imath \beta \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\
&= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x ( \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z) &= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x ( \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z)
- \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (-\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z) \\ - \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (-\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z) \\
&= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x ( \imath \omega \epsilon_{xx} E_x) &= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x ( \imath \omega \epsilon_{xx} E_x)
- \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (-\imath \omega \epsilon_{yy} E_y) \\ - \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (-\imath \omega \epsilon_{yy} E_y) \\
\gamma \tilde{\partial}_x E_z &= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x) \imath \beta \tilde{\partial}_x E_z &= \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
+ \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) \\ + \tilde{\partial}_x \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) \\
\end{aligned} \end{aligned}
$$ $$
With a similar approach (but using $\gamma \tilde{\partial}_y$ instead), we can get With a similar approach (but using $\imath \beta \tilde{\partial}_y$ instead), we can get
$$ $$
\begin{aligned} \begin{aligned}
\gamma \tilde{\partial}_y E_z &= \tilde{\partial}_y \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x) \imath \beta \tilde{\partial}_y E_z &= \tilde{\partial}_y \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
+ \tilde{\partial}_y \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) \\ + \tilde{\partial}_y \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) \\
\end{aligned} \end{aligned}
$$ $$
We can combine this equation for $\gamma \tilde{\partial}_y E_z$ with We can combine this equation for $\imath \beta \tilde{\partial}_y E_z$ with
the unused $\imath \omega \mu_{xx} H_x$ and $\imath \omega \mu_{yy} H_y$ equations to get the unused $\imath \omega \mu_{xx} H_x$ and $\imath \omega \mu_{yy} H_y$ equations to get
$$ $$
\begin{aligned} \begin{aligned}
-\imath \omega \mu_{xx} \gamma H_x &= \gamma^2 E_y + \gamma \tilde{\partial}_y E_z \\ -\imath \omega \mu_{xx} \imath \beta H_x &= -\beta^2 E_y + \imath \beta \tilde{\partial}_y E_z \\
-\imath \omega \mu_{xx} \gamma H_x &= \gamma^2 E_y + \tilde{\partial}_y ( -\imath \omega \mu_{xx} \imath \beta H_x &= -\beta^2 E_y + \tilde{\partial}_y (
\frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x) \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
+ \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) + \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y)
)\\ )\\
@ -100,22 +101,21 @@ and
$$ $$
\begin{aligned} \begin{aligned}
-\imath \omega \mu_{yy} \gamma H_y &= -\gamma^2 E_x - \gamma \tilde{\partial}_x E_z \\ -\imath \omega \mu_{yy} \imath \beta H_y &= \beta^2 E_x - \imath \beta \tilde{\partial}_x E_z \\
-\imath \omega \mu_{yy} \gamma H_y &= -\gamma^2 E_x - \tilde{\partial}_x ( -\imath \omega \mu_{yy} \imath \beta H_y &= \beta^2 E_x - \tilde{\partial}_x (
\frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x) \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
+ \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) + \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y)
)\\ )\\
\end{aligned} \end{aligned}
$$ $$
However, based on our rewritten equation for $\gamma H_x$ and the so-far unused However, based on our rewritten equation for $\imath \beta H_x$ and the so-far unused
equation for $\imath \omega \mu_{zz} H_z$ we can also write equation for $\imath \omega \mu_{zz} H_z$ we can also write
$$ $$
\begin{aligned} \begin{aligned}
-\imath \omega \mu_{xx} (\gamma H_x) &= -\imath \omega \mu_{xx} (-\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z) \\ -\imath \omega \mu_{xx} (\imath \beta H_x) &= -\imath \omega \mu_{xx} (-\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z) \\
&= -\omega^2 \mu_{xx} \epsilon_{yy} E_y &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y + \imath \omega \mu_{xx} \hat{\partial}_x (
+\imath \omega \mu_{xx} \hat{\partial}_x (
\frac{1}{-\imath \omega \mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x)) \\ \frac{1}{-\imath \omega \mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x)) \\
&= -\omega^2 \mu_{xx} \epsilon_{yy} E_y &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y
-\mu_{xx} \hat{\partial}_x \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\ -\mu_{xx} \hat{\partial}_x \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\
@ -126,7 +126,7 @@ and, similarly,
$$ $$
\begin{aligned} \begin{aligned}
-\imath \omega \mu_{yy} (\gamma H_y) &= \omega^2 \mu_{yy} \epsilon_{xx} E_x -\imath \omega \mu_{yy} (\imath \beta H_y) &= \omega^2 \mu_{yy} \epsilon_{xx} E_x
+\mu_{yy} \hat{\partial}_y \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\ +\mu_{yy} \hat{\partial}_y \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\
\end{aligned} \end{aligned}
$$ $$
@ -135,12 +135,12 @@ By combining both pairs of expressions, we get
$$ $$
\begin{aligned} \begin{aligned}
-\gamma^2 E_x - \tilde{\partial}_x ( \beta^2 E_x - \tilde{\partial}_x (
\frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x) \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
+ \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) + \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y)
) &= \omega^2 \mu_{yy} \epsilon_{xx} E_x ) &= \omega^2 \mu_{yy} \epsilon_{xx} E_x
+\mu_{yy} \hat{\partial}_y \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\ +\mu_{yy} \hat{\partial}_y \frac{1}{\mu_{zz}} (\tilde{\partial}_x E_y - \tilde{\partial}_y E_x) \\
\gamma^2 E_y + \tilde{\partial}_y ( -\beta^2 E_y + \tilde{\partial}_y (
\frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x) \frac{1}{\epsilon_{zz}} \hat{\partial}_x (\epsilon_{xx} E_x)
+ \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y) + \frac{1}{\epsilon_{zz}} \hat{\partial}_y (\epsilon_{yy} E_y)
) &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y ) &= -\omega^2 \mu_{xx} \epsilon_{yy} E_y
@ -165,27 +165,27 @@ $$
E_y \end{bmatrix} E_y \end{bmatrix}
$$ $$
where $\gamma = \imath\beta$. In the literature, $\beta$ is usually used to denote In the literature, $\beta$ is usually used to denote the lossless/real part of the propagation constant,
the lossless/real part of the propagation constant, but in `meanas` it is allowed to but in `meanas` it is allowed to be complex.
be complex.
An equivalent eigenvalue problem can be formed using the $H_x$ and $H_y$ fields, if those are more convenient. An equivalent eigenvalue problem can be formed using the $H_x$ and $H_y$ fields, if those are more convenient.
Note that $E_z$ was never discretized, so $\gamma$ and $\beta$ will need adjustment Note that $E_z$ was never discretized, so $\beta$ will need adjustment to account for numerical dispersion
to account for numerical dispersion if the result is introduced into a space with a discretized z-axis. if the result is introduced into a space with a discretized z-axis.
""" """
# TODO update module docs # TODO update module docs
from typing import Any from typing import Any
from collections.abc import Sequence
import numpy import numpy
from numpy.typing import NDArray, ArrayLike from numpy.typing import NDArray
from numpy.linalg import norm from numpy.linalg import norm
import scipy.sparse as sparse # type: ignore from scipy import sparse
from ..fdmath.operators import deriv_forward, deriv_back, cross from ..fdmath.operators import deriv_forward, deriv_back, cross
from ..fdmath import vec, unvec, dx_lists_t, vfdfield_t, vcfdfield_t from ..fdmath import vec, unvec, dx_lists2_t, vcfdfield2_t, vcfdslice_t, vcfdfield2, vfdslice, vcfdslice
from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration
@ -194,10 +194,10 @@ __author__ = 'Jan Petykiewicz'
def operator_e( def operator_e(
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists2_t,
epsilon: vfdfield_t, epsilon: vfdslice,
mu: vfdfield_t | None = None, mu: vfdslice | None = None,
) -> sparse.spmatrix: ) -> sparse.sparray:
r""" r"""
Waveguide operator of the form Waveguide operator of the form
@ -246,12 +246,12 @@ def operator_e(
Dbx, Dby = deriv_back(dxes[1]) Dbx, Dby = deriv_back(dxes[1])
eps_parts = numpy.split(epsilon, 3) eps_parts = numpy.split(epsilon, 3)
eps_xy = sparse.diags(numpy.hstack((eps_parts[0], eps_parts[1]))) eps_xy = sparse.diags_array(numpy.hstack((eps_parts[0], eps_parts[1])))
eps_z_inv = sparse.diags(1 / eps_parts[2]) eps_z_inv = sparse.diags_array(1 / eps_parts[2])
mu_parts = numpy.split(mu, 3) mu_parts = numpy.split(mu, 3)
mu_yx = sparse.diags(numpy.hstack((mu_parts[1], mu_parts[0]))) mu_yx = sparse.diags_array(numpy.hstack((mu_parts[1], mu_parts[0])))
mu_z_inv = sparse.diags(1 / mu_parts[2]) mu_z_inv = sparse.diags_array(1 / mu_parts[2])
op = ( op = (
omega * omega * mu_yx @ eps_xy omega * omega * mu_yx @ eps_xy
@ -263,10 +263,10 @@ def operator_e(
def operator_h( def operator_h(
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists2_t,
epsilon: vfdfield_t, epsilon: vfdslice,
mu: vfdfield_t | None = None, mu: vfdslice | None = None,
) -> sparse.spmatrix: ) -> sparse.sparray:
r""" r"""
Waveguide operator of the form Waveguide operator of the form
@ -315,12 +315,12 @@ def operator_h(
Dbx, Dby = deriv_back(dxes[1]) Dbx, Dby = deriv_back(dxes[1])
eps_parts = numpy.split(epsilon, 3) eps_parts = numpy.split(epsilon, 3)
eps_yx = sparse.diags(numpy.hstack((eps_parts[1], eps_parts[0]))) eps_yx = sparse.diags_array(numpy.hstack((eps_parts[1], eps_parts[0])))
eps_z_inv = sparse.diags(1 / eps_parts[2]) eps_z_inv = sparse.diags_array(1 / eps_parts[2])
mu_parts = numpy.split(mu, 3) mu_parts = numpy.split(mu, 3)
mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1]))) mu_xy = sparse.diags_array(numpy.hstack((mu_parts[0], mu_parts[1])))
mu_z_inv = sparse.diags(1 / mu_parts[2]) mu_z_inv = sparse.diags_array(1 / mu_parts[2])
op = ( op = (
omega * omega * eps_yx @ mu_xy omega * omega * eps_yx @ mu_xy
@ -331,14 +331,14 @@ def operator_h(
def normalized_fields_e( def normalized_fields_e(
e_xy: ArrayLike, e_xy: vcfdfield2,
wavenumber: complex, wavenumber: complex,
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists2_t,
epsilon: vfdfield_t, epsilon: vfdslice,
mu: vfdfield_t | None = None, mu: vfdslice | None = None,
prop_phase: float = 0, prop_phase: float = 0,
) -> tuple[vcfdfield_t, vcfdfield_t]: ) -> tuple[vcfdslice_t, vcfdslice_t]:
""" """
Given a vector `e_xy` containing the vectorized E_x and E_y fields, Given a vector `e_xy` containing the vectorized E_x and E_y fields,
returns normalized, vectorized E and H fields for the system. returns normalized, vectorized E and H fields for the system.
@ -366,14 +366,14 @@ def normalized_fields_e(
def normalized_fields_h( def normalized_fields_h(
h_xy: ArrayLike, h_xy: vcfdfield2,
wavenumber: complex, wavenumber: complex,
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists2_t,
epsilon: vfdfield_t, epsilon: vfdslice,
mu: vfdfield_t | None = None, mu: vfdslice | None = None,
prop_phase: float = 0, prop_phase: float = 0,
) -> tuple[vcfdfield_t, vcfdfield_t]: ) -> tuple[vcfdslice_t, vcfdslice_t]:
""" """
Given a vector `h_xy` containing the vectorized H_x and H_y fields, Given a vector `h_xy` containing the vectorized H_x and H_y fields,
returns normalized, vectorized E and H fields for the system. returns normalized, vectorized E and H fields for the system.
@ -401,30 +401,22 @@ def normalized_fields_h(
def _normalized_fields( def _normalized_fields(
e: vcfdfield_t, e: vcfdslice,
h: vcfdfield_t, h: vcfdslice,
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists2_t,
epsilon: vfdfield_t, epsilon: vfdslice,
mu: vfdfield_t | None = None, mu: vfdslice | None = None,
prop_phase: float = 0, prop_phase: float = 0,
) -> tuple[vcfdfield_t, vcfdfield_t]: ) -> tuple[vcfdslice_t, vcfdslice_t]:
# TODO documentation # TODO documentation
shape = [s.size for s in dxes[0]] shape = [s.size for s in dxes[0]]
dxes_real = [[numpy.real(d) for d in numpy.meshgrid(*dxes[v], indexing='ij')] for v in (0, 1)]
E = unvec(e, shape)
H = unvec(h, shape)
# Find time-averaged Sz and normalize to it # Find time-averaged Sz and normalize to it
# H phase is adjusted by a half-cell forward shift for Yee cell, and 1-cell reverse shift for Poynting Sz_tavg = inner_product(e, h, dxes=dxes, prop_phase=prop_phase, conj_h=True).real
phase = numpy.exp(-1j * -prop_phase / 2)
Sz_a = E[0] * numpy.conj(H[1] * phase) * dxes_real[0][1] * dxes_real[1][0]
Sz_b = E[1] * numpy.conj(H[0] * phase) * dxes_real[0][0] * dxes_real[1][1]
Sz_tavg = numpy.real(Sz_a.sum() - Sz_b.sum()) * 0.5 # 0.5 since E, H are assumed to be peak (not RMS) amplitudes
assert Sz_tavg > 0, f'Found a mode propagating in the wrong direction! {Sz_tavg=}' assert Sz_tavg > 0, f'Found a mode propagating in the wrong direction! {Sz_tavg=}'
energy = epsilon * e.conj() * e energy = numpy.real(epsilon * e.conj() * e)
norm_amplitude = 1 / numpy.sqrt(Sz_tavg) norm_amplitude = 1 / numpy.sqrt(Sz_tavg)
norm_angle = -numpy.angle(e[energy.argmax()]) # Will randomly add a negative sign when mode is symmetric norm_angle = -numpy.angle(e[energy.argmax()]) # Will randomly add a negative sign when mode is symmetric
@ -434,22 +426,23 @@ def _normalized_fields(
sign = numpy.sign(E_weighted[:, sign = numpy.sign(E_weighted[:,
:max(shape[0] // 2, 1), :max(shape[0] // 2, 1),
:max(shape[1] // 2, 1)].real.sum()) :max(shape[1] // 2, 1)].real.sum())
assert sign != 0
norm_factor = sign * norm_amplitude * numpy.exp(1j * norm_angle) norm_factor = sign * norm_amplitude * numpy.exp(1j * norm_angle)
e *= norm_factor e *= norm_factor
h *= norm_factor h *= norm_factor
return e, h return vcfdslice_t(e), vcfdslice_t(h)
def exy2h( def exy2h(
wavenumber: complex, wavenumber: complex,
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists2_t,
epsilon: vfdfield_t, epsilon: vfdslice,
mu: vfdfield_t | None = None mu: vfdslice | None = None
) -> sparse.spmatrix: ) -> sparse.sparray:
""" """
Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields, Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields,
into a vectorized H containing all three H components into a vectorized H containing all three H components
@ -472,10 +465,10 @@ def exy2h(
def hxy2e( def hxy2e(
wavenumber: complex, wavenumber: complex,
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists2_t,
epsilon: vfdfield_t, epsilon: vfdslice,
mu: vfdfield_t | None = None mu: vfdslice | None = None
) -> sparse.spmatrix: ) -> sparse.sparray:
""" """
Operator which transforms the vector `h_xy` containing the vectorized H_x and H_y fields, Operator which transforms the vector `h_xy` containing the vectorized H_x and H_y fields,
into a vectorized E containing all three E components into a vectorized E containing all three E components
@ -497,9 +490,9 @@ def hxy2e(
def hxy2h( def hxy2h(
wavenumber: complex, wavenumber: complex,
dxes: dx_lists_t, dxes: dx_lists2_t,
mu: vfdfield_t | None = None mu: vfdslice | None = None
) -> sparse.spmatrix: ) -> sparse.sparray:
""" """
Operator which transforms the vector `h_xy` containing the vectorized H_x and H_y fields, Operator which transforms the vector `h_xy` containing the vectorized H_x and H_y fields,
into a vectorized H containing all three H components into a vectorized H containing all three H components
@ -518,26 +511,53 @@ def hxy2h(
if mu is not None: if mu is not None:
mu_parts = numpy.split(mu, 3) mu_parts = numpy.split(mu, 3)
mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1]))) mu_xy = sparse.diags_array(numpy.hstack((mu_parts[0], mu_parts[1])))
mu_z_inv = sparse.diags(1 / mu_parts[2]) mu_z_inv = sparse.diags_array(1 / mu_parts[2])
hxy2hz = mu_z_inv @ hxy2hz @ mu_xy hxy2hz = mu_z_inv @ hxy2hz @ mu_xy
n_pts = dxes[1][0].size * dxes[1][1].size n_pts = dxes[1][0].size * dxes[1][1].size
op = sparse.vstack((sparse.eye(2 * n_pts), op = sparse.vstack((sparse.eye_array(2 * n_pts),
hxy2hz)) hxy2hz))
return op return op
def exy2e( def exy2e(
wavenumber: complex, wavenumber: complex,
dxes: dx_lists_t, dxes: dx_lists2_t,
epsilon: vfdfield_t, epsilon: vfdslice,
) -> sparse.spmatrix: ) -> sparse.sparray:
""" r"""
Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields, Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields,
into a vectorized E containing all three E components into a vectorized E containing all three E components
From the operator derivation (see module docs), we have
$$
\imath \omega \epsilon_{zz} E_z = \hat{\partial}_x H_y - \hat{\partial}_y H_x \\
$$
as well as the intermediate equations
$$
\begin{aligned}
\imath \beta H_y &= \imath \omega \epsilon_{xx} E_x - \hat{\partial}_y H_z \\
\imath \beta H_x &= -\imath \omega \epsilon_{yy} E_y - \hat{\partial}_x H_z \\
\end{aligned}
$$
Combining these, we get
$$
\begin{aligned}
E_z &= \frac{1}{- \omega \beta \epsilon_{zz}} ((
\hat{\partial}_y \hat{\partial}_x H_z
-\hat{\partial}_x \hat{\partial}_y H_z)
+ \imath \omega (\hat{\partial}_x \epsilon_{xx} E_x + \hat{\partial}_y \epsilon{yy} E_y))
&= \frac{1}{\imath \beta \epsilon_{zz}} (\hat{\partial}_x \epsilon_{xx} E_x + \hat{\partial}_y \epsilon{yy} E_y)
\end{aligned}
$$
Args: Args:
wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`
It should satisfy `operator_e() @ e_xy == wavenumber**2 * e_xy` It should satisfy `operator_e() @ e_xy == wavenumber**2 * e_xy`
@ -552,13 +572,13 @@ def exy2e(
if epsilon is not None: if epsilon is not None:
epsilon_parts = numpy.split(epsilon, 3) epsilon_parts = numpy.split(epsilon, 3)
epsilon_xy = sparse.diags(numpy.hstack((epsilon_parts[0], epsilon_parts[1]))) epsilon_xy = sparse.diags_array(numpy.hstack((epsilon_parts[0], epsilon_parts[1])))
epsilon_z_inv = sparse.diags(1 / epsilon_parts[2]) epsilon_z_inv = sparse.diags_array(1 / epsilon_parts[2])
exy2ez = epsilon_z_inv @ exy2ez @ epsilon_xy exy2ez = epsilon_z_inv @ exy2ez @ epsilon_xy
n_pts = dxes[0][0].size * dxes[0][1].size n_pts = dxes[0][0].size * dxes[0][1].size
op = sparse.vstack((sparse.eye(2 * n_pts), op = sparse.vstack((sparse.eye_array(2 * n_pts),
exy2ez)) exy2ez))
return op return op
@ -566,12 +586,12 @@ def exy2e(
def e2h( def e2h(
wavenumber: complex, wavenumber: complex,
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists2_t,
mu: vfdfield_t | None = None mu: vfdslice | None = None
) -> sparse.spmatrix: ) -> sparse.sparray:
""" """
Returns an operator which, when applied to a vectorized E eigenfield, produces Returns an operator which, when applied to a vectorized E eigenfield, produces
the vectorized H eigenfield. the vectorized H eigenfield slice.
Args: Args:
wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`
@ -584,19 +604,19 @@ def e2h(
""" """
op = curl_e(wavenumber, dxes) / (-1j * omega) op = curl_e(wavenumber, dxes) / (-1j * omega)
if mu is not None: if mu is not None:
op = sparse.diags(1 / mu) @ op op = sparse.diags_array(1 / mu) @ op
return op return op
def h2e( def h2e(
wavenumber: complex, wavenumber: complex,
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists2_t,
epsilon: vfdfield_t epsilon: vfdslice,
) -> sparse.spmatrix: ) -> sparse.sparray:
""" """
Returns an operator which, when applied to a vectorized H eigenfield, produces Returns an operator which, when applied to a vectorized H eigenfield, produces
the vectorized E eigenfield. the vectorized E eigenfield slice.
Args: Args:
wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`
@ -607,13 +627,13 @@ def h2e(
Returns: Returns:
Sparse matrix representation of the operator. Sparse matrix representation of the operator.
""" """
op = sparse.diags(1 / (1j * omega * epsilon)) @ curl_h(wavenumber, dxes) op = sparse.diags_array(1 / (1j * omega * epsilon)) @ curl_h(wavenumber, dxes)
return op return op
def curl_e(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: def curl_e(wavenumber: complex, dxes: dx_lists2_t) -> sparse.sparray:
""" """
Discretized curl operator for use with the waveguide E field. Discretized curl operator for use with the waveguide E field slice.
Args: Args:
wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`
@ -622,18 +642,18 @@ def curl_e(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix:
Returns: Returns:
Sparse matrix representation of the operator. Sparse matrix representation of the operator.
""" """
n = 1 nn = 1
for d in dxes[0]: for dd in dxes[0]:
n *= len(d) nn *= len(dd)
Bz = -1j * wavenumber * sparse.eye(n) Bz = -1j * wavenumber * sparse.eye_array(nn)
Dfx, Dfy = deriv_forward(dxes[0]) Dfx, Dfy = deriv_forward(dxes[0])
return cross([Dfx, Dfy, Bz]) return cross([Dfx, Dfy, Bz])
def curl_h(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: def curl_h(wavenumber: complex, dxes: dx_lists2_t) -> sparse.sparray:
""" """
Discretized curl operator for use with the waveguide H field. Discretized curl operator for use with the waveguide H field slice.
Args: Args:
wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)` wavenumber: Wavenumber assuming fields have z-dependence of `exp(-i * wavenumber * z)`
@ -642,22 +662,22 @@ def curl_h(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix:
Returns: Returns:
Sparse matrix representation of the operator. Sparse matrix representation of the operator.
""" """
n = 1 nn = 1
for d in dxes[1]: for dd in dxes[1]:
n *= len(d) nn *= len(dd)
Bz = -1j * wavenumber * sparse.eye(n) Bz = -1j * wavenumber * sparse.eye_array(nn)
Dbx, Dby = deriv_back(dxes[1]) Dbx, Dby = deriv_back(dxes[1])
return cross([Dbx, Dby, Bz]) return cross([Dbx, Dby, Bz])
def h_err( def h_err(
h: vcfdfield_t, h: vcfdslice,
wavenumber: complex, wavenumber: complex,
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists2_t,
epsilon: vfdfield_t, epsilon: vfdslice,
mu: vfdfield_t | None = None mu: vfdslice | None = None
) -> float: ) -> float:
""" """
Calculates the relative error in the H field Calculates the relative error in the H field
@ -676,7 +696,7 @@ def h_err(
ce = curl_e(wavenumber, dxes) ce = curl_e(wavenumber, dxes)
ch = curl_h(wavenumber, dxes) ch = curl_h(wavenumber, dxes)
eps_inv = sparse.diags(1 / epsilon) eps_inv = sparse.diags_array(1 / epsilon)
if mu is None: if mu is None:
op = ce @ eps_inv @ ch @ h - omega ** 2 * h op = ce @ eps_inv @ ch @ h - omega ** 2 * h
@ -687,12 +707,12 @@ def h_err(
def e_err( def e_err(
e: vcfdfield_t, e: vcfdslice,
wavenumber: complex, wavenumber: complex,
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists2_t,
epsilon: vfdfield_t, epsilon: vfdslice,
mu: vfdfield_t | None = None, mu: vfdslice | None = None,
) -> float: ) -> float:
""" """
Calculates the relative error in the E field Calculates the relative error in the E field
@ -714,21 +734,21 @@ def e_err(
if mu is None: if mu is None:
op = ch @ ce @ e - omega ** 2 * (epsilon * e) op = ch @ ce @ e - omega ** 2 * (epsilon * e)
else: else:
mu_inv = sparse.diags(1 / mu) mu_inv = sparse.diags_array(1 / mu)
op = ch @ mu_inv @ ce @ e - omega ** 2 * (epsilon * e) op = ch @ mu_inv @ ce @ e - omega ** 2 * (epsilon * e)
return float(norm(op) / norm(e)) return float(norm(op) / norm(e))
def sensitivity( def sensitivity(
e_norm: vcfdfield_t, e_norm: vcfdslice,
h_norm: vcfdfield_t, h_norm: vcfdslice,
wavenumber: complex, wavenumber: complex,
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists2_t,
epsilon: vfdfield_t, epsilon: vfdslice,
mu: vfdfield_t | None = None, mu: vfdslice | None = None,
) -> vcfdfield_t: ) -> vcfdslice_t:
r""" r"""
Given a waveguide structure (`dxes`, `epsilon`, `mu`) and mode fields Given a waveguide structure (`dxes`, `epsilon`, `mu`) and mode fields
(`e_norm`, `h_norm`, `wavenumber`, `omega`), calculates the sensitivity of the wavenumber (`e_norm`, `h_norm`, `wavenumber`, `omega`), calculates the sensitivity of the wavenumber
@ -802,11 +822,11 @@ def sensitivity(
Dbx, Dby = deriv_back(dxes[1]) Dbx, Dby = deriv_back(dxes[1])
eps_x, eps_y, eps_z = numpy.split(epsilon, 3) eps_x, eps_y, eps_z = numpy.split(epsilon, 3)
eps_xy = sparse.diags(numpy.hstack((eps_x, eps_y))) eps_xy = sparse.diags_array(numpy.hstack((eps_x, eps_y)))
eps_z_inv = sparse.diags(1 / eps_z) eps_z_inv = sparse.diags_array(1 / eps_z)
mu_x, mu_y, _mu_z = numpy.split(mu, 3) mu_x, mu_y, _mu_z = numpy.split(mu, 3)
mu_yx = sparse.diags(numpy.hstack((mu_y, mu_x))) mu_yx = sparse.diags_array(numpy.hstack((mu_y, mu_x)))
da_exxhyy = vec(dxes[1][0][:, None] * dxes[0][1][None, :]) da_exxhyy = vec(dxes[1][0][:, None] * dxes[0][1][None, :])
da_eyyhxx = vec(dxes[1][1][None, :] * dxes[0][0][:, None]) da_eyyhxx = vec(dxes[1][1][None, :] * dxes[0][0][:, None])
@ -820,15 +840,15 @@ def sensitivity(
norm = hv_yx_conj @ ev_xy norm = hv_yx_conj @ ev_xy
sens_tot = numpy.concatenate([sens_xy1 + sens_xy2, sens_z]) / (2 * wavenumber * norm) sens_tot = numpy.concatenate([sens_xy1 + sens_xy2, sens_z]) / (2 * wavenumber * norm)
return sens_tot return vcfdslice_t(sens_tot)
def solve_modes( def solve_modes(
mode_numbers: list[int], mode_numbers: Sequence[int],
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists2_t,
epsilon: vfdfield_t, epsilon: vfdslice,
mu: vfdfield_t | None = None, mu: vfdslice | None = None,
mode_margin: int = 2, mode_margin: int = 2,
) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]: ) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]:
""" """
@ -845,32 +865,38 @@ def solve_modes(
ability to find the correct mode. Default 2. ability to find the correct mode. Default 2.
Returns: Returns:
e_xys: list of vfdfield_t specifying fields e_xys: NDArray of vfdfield_t specifying fields. First dimension is mode number.
wavenumbers: list of wavenumbers wavenumbers: list of wavenumbers
""" """
''' #
Solve for the largest-magnitude eigenvalue of the real operator # Solve for the largest-magnitude eigenvalue of the real operator
''' #
dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes] dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes]
mu_real = None if mu is None else numpy.real(mu) mu_real = None if mu is None else numpy.real(mu)
A_r = operator_e(numpy.real(omega), dxes_real, numpy.real(epsilon), mu_real) A_r = operator_e(numpy.real(omega), dxes_real, numpy.real(epsilon), mu_real)
eigvals, eigvecs = signed_eigensolve(A_r, max(mode_numbers) + mode_margin) eigvals, eigvecs = signed_eigensolve(A_r, max(mode_numbers) + mode_margin)
e_xys = eigvecs[:, -(numpy.array(mode_numbers) + 1)] keep_inds = -(numpy.array(mode_numbers) + 1)
e_xys = eigvecs[:, keep_inds].T
eigvals = eigvals[keep_inds]
''' #
Now solve for the eigenvector of the full operator, using the real operator's # Now solve for the eigenvector of the full operator, using the real operator's
eigenvector as an initial guess for Rayleigh quotient iteration. # eigenvector as an initial guess for Rayleigh quotient iteration.
''' #
A = operator_e(omega, dxes, epsilon, mu) A = operator_e(omega, dxes, epsilon, mu)
for nn in range(len(mode_numbers)): for nn in range(len(mode_numbers)):
eigvals[nn], e_xys[:, nn] = rayleigh_quotient_iteration(A, e_xys[:, nn]) eigvals[nn], e_xys[nn, :] = rayleigh_quotient_iteration(A, e_xys[nn, :])
# Calculate the wave-vector (force the real part to be positive) # Calculate the wave-vector (force the real part to be positive)
wavenumbers = numpy.sqrt(eigvals) wavenumbers = numpy.sqrt(eigvals)
wavenumbers *= numpy.sign(numpy.real(wavenumbers)) wavenumbers *= numpy.sign(numpy.real(wavenumbers))
order = wavenumbers.argsort()[::-1]
e_xys = e_xys[order]
wavenumbers = wavenumbers[order]
return e_xys, wavenumbers return e_xys, wavenumbers
@ -878,7 +904,7 @@ def solve_mode(
mode_number: int, mode_number: int,
*args: Any, *args: Any,
**kwargs: Any, **kwargs: Any,
) -> tuple[vcfdfield_t, complex]: ) -> tuple[vcfdfield2_t, complex]:
""" """
Wrapper around `solve_modes()` that solves for a single mode. Wrapper around `solve_modes()` that solves for a single mode.
@ -892,4 +918,38 @@ def solve_mode(
""" """
kwargs['mode_numbers'] = [mode_number] kwargs['mode_numbers'] = [mode_number]
e_xys, wavenumbers = solve_modes(*args, **kwargs) e_xys, wavenumbers = solve_modes(*args, **kwargs)
return e_xys[:, 0], wavenumbers[0] return vcfdfield2_t(e_xys[0]), wavenumbers[0]
def inner_product( # TODO documentation
e1: vcfdfield2,
h2: vcfdfield2,
dxes: dx_lists2_t,
prop_phase: float = 0,
conj_h: bool = False,
trapezoid: bool = False,
) -> complex:
shape = [s.size for s in dxes[0]]
# H phase is adjusted by a half-cell forward shift for Yee cell, and 1-cell reverse shift for Poynting
phase = numpy.exp(-1j * -prop_phase / 2)
E1 = unvec(e1, shape)
H2 = unvec(h2, shape) * phase
if conj_h:
H2 = numpy.conj(H2)
# Find time-averaged Sz and normalize to it
dxes_real = [[numpy.real(dxyz) for dxyz in dxeh] for dxeh in dxes]
if trapezoid:
Sz_a = numpy.trapezoid(numpy.trapezoid(E1[0] * H2[1], numpy.cumsum(dxes_real[0][1])), numpy.cumsum(dxes_real[1][0]))
Sz_b = numpy.trapezoid(numpy.trapezoid(E1[1] * H2[0], numpy.cumsum(dxes_real[0][0])), numpy.cumsum(dxes_real[1][1]))
else:
Sz_a = E1[0] * H2[1] * dxes_real[1][0][:, None] * dxes_real[0][1][None, :]
Sz_b = E1[1] * H2[0] * dxes_real[0][0][:, None] * dxes_real[1][1][None, :]
Sz = 0.5 * (Sz_a.sum() - Sz_b.sum())
return Sz

View File

@ -4,11 +4,13 @@ Tools for working with waveguide modes in 3D domains.
This module relies heavily on `waveguide_2d` and mostly just transforms This module relies heavily on `waveguide_2d` and mostly just transforms
its parameters into 2D equivalents and expands the results back into 3D. its parameters into 2D equivalents and expands the results back into 3D.
""" """
from typing import Sequence, Any from typing import Any, cast
from collections.abc import Sequence
import numpy import numpy
from numpy.typing import NDArray from numpy.typing import NDArray
from numpy import complexfloating
from ..fdmath import vec, unvec, dx_lists_t, fdfield_t, cfdfield_t from ..fdmath import vec, unvec, dx_lists_t, cfdfield_t, fdfield, cfdfield
from . import operators, waveguide_2d from . import operators, waveguide_2d
@ -19,9 +21,9 @@ def solve_mode(
axis: int, axis: int,
polarity: int, polarity: int,
slices: Sequence[slice], slices: Sequence[slice],
epsilon: fdfield_t, epsilon: fdfield,
mu: fdfield_t | None = None, mu: fdfield | None = None,
) -> dict[str, complex | NDArray[numpy.float_]]: ) -> dict[str, complex | NDArray[complexfloating]]:
""" """
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.
@ -40,9 +42,10 @@ def solve_mode(
Returns: Returns:
``` ```
{ {
'E': list[NDArray[numpy.float_]], 'E': NDArray[complexfloating],
'H': list[NDArray[numpy.float_]], 'H': NDArray[complexfloating],
'wavenumber': complex, 'wavenumber': complex,
'wavenumber_2d': complex,
} }
``` ```
""" """
@ -51,9 +54,9 @@ def solve_mode(
slices = tuple(slices) slices = tuple(slices)
''' #
Solve the 2D problem in the specified plane # Solve the 2D problem in the specified plane
''' #
# Define rotation to set z as propagation direction # Define rotation to set z as propagation direction
order = numpy.roll(range(3), 2 - axis) order = numpy.roll(range(3), 2 - axis)
reverse_order = numpy.roll(range(3), axis - 2) reverse_order = numpy.roll(range(3), axis - 2)
@ -71,9 +74,10 @@ def solve_mode(
} }
e_xy, wavenumber_2d = waveguide_2d.solve_mode(mode_number, **args_2d) e_xy, wavenumber_2d = waveguide_2d.solve_mode(mode_number, **args_2d)
''' #
Apply corrections and expand to 3D # Apply corrections and expand to 3D
''' #
# Correct wavenumber to account for numerical dispersion. # Correct wavenumber to account for numerical dispersion.
wavenumber = 2 / dx_prop * numpy.arcsin(wavenumber_2d * dx_prop / 2) wavenumber = 2 / dx_prop * numpy.arcsin(wavenumber_2d * dx_prop / 2)
@ -92,9 +96,10 @@ def solve_mode(
# Expand E, H to full epsilon space we were given # Expand E, H to full epsilon space we were given
E = numpy.zeros_like(epsilon, dtype=complex) E = numpy.zeros_like(epsilon, dtype=complex)
H = numpy.zeros_like(epsilon, dtype=complex) H = numpy.zeros_like(epsilon, dtype=complex)
for a, o in enumerate(reverse_order): for aa, oo in enumerate(reverse_order):
E[(a, *slices)] = e[o][:, :, None].transpose(reverse_order) iii = cast('tuple[slice | int]', (aa, *slices))
H[(a, *slices)] = h[o][:, :, None].transpose(reverse_order) E[iii] = e[oo][:, :, None].transpose(reverse_order)
H[iii] = h[oo][:, :, None].transpose(reverse_order)
results = { results = {
'wavenumber': wavenumber, 'wavenumber': wavenumber,
@ -106,15 +111,15 @@ def solve_mode(
def compute_source( def compute_source(
E: cfdfield_t, E: cfdfield,
wavenumber: complex, wavenumber: complex,
omega: complex, omega: complex,
dxes: dx_lists_t, dxes: dx_lists_t,
axis: int, axis: int,
polarity: int, polarity: int,
slices: Sequence[slice], slices: Sequence[slice],
epsilon: fdfield_t, epsilon: fdfield,
mu: fdfield_t | None = None, mu: fdfield | None = None,
) -> cfdfield_t: ) -> cfdfield_t:
""" """
Given an eigenmode obtained by `solve_mode`, returns the current source distribution Given an eigenmode obtained by `solve_mode`, returns the current source distribution
@ -148,35 +153,32 @@ def compute_source(
masked_e2j = operators.e_boundary_source(mask=vec(mask), omega=omega, dxes=dxes, epsilon=vec(epsilon), mu=vec(mu)) masked_e2j = operators.e_boundary_source(mask=vec(mask), omega=omega, dxes=dxes, epsilon=vec(epsilon), mu=vec(mu))
J = unvec(masked_e2j @ vec(E_expanded), E.shape[1:]) J = unvec(masked_e2j @ vec(E_expanded), E.shape[1:])
return J return cfdfield_t(J)
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],
) -> cfdfield_t: # TODO DOCS ) -> cfdfield_t:
""" """
Given an eigenmode obtained by `solve_mode`, calculates an overlap_e for the Given an eigenmode obtained by `solve_mode`, calculates an overlap_e for the
mode orthogonality relation Integrate(((E x H_mode) + (E_mode x H)) dot dn) mode orthogonality relation Integrate(((E x H_mode) + (E_mode x H)) dot dn)
[assumes reflection symmetry]. [assumes reflection symmetry].
TODO: add reference TODO: add reference or derivation for compute_overlap_e
Args: Args:
E: E-field of the mode E: E-field of the mode
H: H-field of the mode (advanced by half of a Yee cell from E)
wavenumber: Wavenumber of the mode wavenumber: Wavenumber of the mode
omega: Angular frequency of the simulation
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types`
axis: Propagation axis (0=x, 1=y, 2=z) axis: Propagation axis (0=x, 1=y, 2=z)
polarity: Propagation direction (+1 for +ve, -1 for -ve) polarity: Propagation direction (+1 for +ve, -1 for -ve)
slices: `epsilon[tuple(slices)]` is used to select the portion of the grid to use slices: `epsilon[tuple(slices)]` is used to select the portion of the grid to use
as the waveguide cross-section. slices[axis] should select only one item. as the waveguide cross-section. slices[axis] should select only one item.
mu: Magnetic permeability (default 1 everywhere)
Returns: Returns:
overlap_e such that `numpy.sum(overlap_e * other_e.conj())` computes the overlap integral overlap_e such that `numpy.sum(overlap_e * other_e.conj())` computes the overlap integral
@ -195,12 +197,14 @@ def compute_overlap_e(
Etgt = numpy.zeros_like(Ee) Etgt = numpy.zeros_like(Ee)
Etgt[slices2] = Ee[slices2] Etgt[slices2] = Ee[slices2]
Etgt /= (Etgt.conj() * Etgt).sum() # note no sqrt() when normalizing below since we want to get 1.0 after overlapping with the
return Etgt # original field, not the normalized one
Etgt /= (Etgt.conj() * Etgt).sum() # type: ignore
return cfdfield_t(Etgt)
def expand_e( def expand_e(
E: cfdfield_t, E: cfdfield,
wavenumber: complex, wavenumber: complex,
dxes: dx_lists_t, dxes: dx_lists_t,
axis: int, axis: int,
@ -245,4 +249,4 @@ def expand_e(
slices_in = (slice(None), *slices) slices_in = (slice(None), *slices)
E_expanded[slices_exp] = phase_E * numpy.array(E)[slices_in] E_expanded[slices_exp] = phase_E * numpy.array(E)[slices_in]
return E_expanded return cfdfield_t(E_expanded)

View File

@ -1,34 +1,102 @@
""" r"""
Operators and helper functions for cylindrical waveguides with unchanging cross-section. Operators and helper functions for cylindrical waveguides with unchanging cross-section.
WORK IN PROGRESS, CURRENTLY BROKEN Waveguide operator is derived according to 10.1364/OL.33.001848.
The curl equations in the complex coordinate system become
As the z-dependence is known, all the functions in this file assume a 2D grid $$
\begin{aligned}
-\imath \omega \mu_{xx} H_x &= \tilde{\partial}_y E_z + \imath \beta frac{E_y}{\tilde{t}_x} \\
-\imath \omega \mu_{yy} H_y &= -\imath \beta E_x - \frac{1}{\hat{t}_x} \tilde{\partial}_x \tilde{t}_x E_z \\
-\imath \omega \mu_{zz} H_z &= \tilde{\partial}_x E_y - \tilde{\partial}_y E_x \\
\imath \omega \epsilon_{xx} E_x &= \hat{\partial}_y H_z + \imath \beta \frac{H_y}{\hat{T}} \\
\imath \omega \epsilon_{yy} E_y &= -\imath \beta H_x - \{1}{\tilde{t}_x} \hat{\partial}_x \hat{t}_x} H_z \\
\imath \omega \epsilon_{zz} E_z &= \hat{\partial}_x H_y - \hat{\partial}_y H_x \\
\end{aligned}
$$
where $t_x = 1 + \frac{\Delta_{x, m}}{R_0}$ is the grid spacing adjusted by the nominal radius $R0$.
Rewrite the last three equations as
$$
\begin{aligned}
\imath \beta H_y &= \imath \omega \hat{t}_x \epsilon_{xx} E_x - \hat{t}_x \hat{\partial}_y H_z \\
\imath \beta H_x &= -\imath \omega \hat{t}_x \epsilon_{yy} E_y - \hat{t}_x \hat{\partial}_x H_z \\
\imath \omega E_z &= \frac{1}{\epsilon_{zz}} \hat{\partial}_x H_y - \frac{1}{\epsilon_{zz}} \hat{\partial}_y H_x \\
\end{aligned}
$$
The derivation then follows the same steps as the straight waveguide, leading to the eigenvalue problem
$$
\beta^2 \begin{bmatrix} E_x \\
E_y \end{bmatrix} =
(\omega^2 \begin{bmatrix} T_b T_b \mu_{yy} \epsilon_{xx} & 0 \\
0 & T_a T_a \mu_{xx} \epsilon_{yy} \end{bmatrix} +
\begin{bmatrix} -T_b \mu_{yy} \hat{\partial}_y \\
T_a \mu_{xx} \hat{\partial}_x \end{bmatrix} T_b \mu_{zz}^{-1}
\begin{bmatrix} -\tilde{\partial}_y & \tilde{\partial}_x \end{bmatrix} +
\begin{bmatrix} \tilde{\partial}_x \\
\tilde{\partial}_y \end{bmatrix} T_a \epsilon_{zz}^{-1}
\begin{bmatrix} \hat{\partial}_x T_b \epsilon_{xx} & \hat{\partial}_y T_a \epsilon_{yy} \end{bmatrix})
\begin{bmatrix} E_x \\
E_y \end{bmatrix}
$$
which resembles the straight waveguide eigenproblem with additonal $T_a$ and $T_b$ terms. These
are diagonal matrices containing the $t_x$ values:
$$
\begin{aligned}
T_a &= 1 + \frac{\Delta_{x, m }}{R_0}
T_b &= 1 + \frac{\Delta_{x, m + \frac{1}{2} }}{R_0}
\end{aligned}
TODO: consider 10.1364/OE.20.021583 for an alternate approach
$$
As in the straight waveguide case, all the functions in this file assume a 2D grid
(i.e. `dxes = [[[dr_e_0, dx_e_1, ...], [dy_e_0, ...]], [[dr_h_0, ...], [dy_h_0, ...]]]`). (i.e. `dxes = [[[dr_e_0, dx_e_1, ...], [dy_e_0, ...]], [[dr_h_0, ...], [dy_h_0, ...]]]`).
""" """
# TODO update module docs from typing import Any, cast
from collections.abc import Sequence
import logging
import numpy import numpy
import scipy.sparse as sparse # type: ignore from numpy.typing import NDArray, ArrayLike
from scipy import sparse
from ..fdmath import vec, unvec, dx_lists_t, fdfield_t, vfdfield_t, cfdfield_t from ..fdmath import vec, unvec, dx_lists2_t, vcfdslice_t, vcfdfield2_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
logger = logging.getLogger(__name__)
def cylindrical_operator( def cylindrical_operator(
omega: complex, omega: float,
dxes: dx_lists_t, dxes: dx_lists2_t,
epsilon: vfdfield_t, epsilon: vfdslice,
r0: float, rmin: float,
) -> sparse.spmatrix: ) -> sparse.sparray:
""" r"""
Cylindrical coordinate waveguide operator of the form Cylindrical coordinate waveguide operator of the form
(NOTE: See 10.1364/OL.33.001848) $$
TODO: consider 10.1364/OE.20.021583 (\omega^2 \begin{bmatrix} T_b T_b \mu_{yy} \epsilon_{xx} & 0 \\
0 & T_a T_a \mu_{xx} \epsilon_{yy} \end{bmatrix} +
TODO \begin{bmatrix} -T_b \mu_{yy} \hat{\partial}_y \\
T_a \mu_{xx} \hat{\partial}_x \end{bmatrix} T_b \mu_{zz}^{-1}
\begin{bmatrix} -\tilde{\partial}_y & \tilde{\partial}_x \end{bmatrix} +
\begin{bmatrix} \tilde{\partial}_x \\
\tilde{\partial}_y \end{bmatrix} T_a \epsilon_{zz}^{-1}
\begin{bmatrix} \hat{\partial}_x T_b \epsilon_{xx} & \hat{\partial}_y T_a \epsilon_{yy} \end{bmatrix})
\begin{bmatrix} E_x \\
E_y \end{bmatrix}
$$
for use with a field vector of the form `[E_r, E_y]`. for use with a field vector of the form `[E_r, E_y]`.
@ -38,12 +106,13 @@ def cylindrical_operator(
which can then be solved for the eigenmodes of the system which can then be solved for the eigenmodes of the system
(an `exp(-i * wavenumber * theta)` theta-dependence is assumed for the fields). (an `exp(-i * wavenumber * theta)` theta-dependence is assumed for the fields).
(NOTE: See module docs and 10.1364/OL.33.001848)
Args: Args:
omega: The angular frequency of the system omega: The angular frequency of the system
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D) dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
epsilon: Vectorized dielectric constant grid epsilon: Vectorized dielectric constant grid
r0: Radius of curvature for the simulation. This should be the minimum value of rmin: Radius at the left edge of the simulation domain (at minimum 'x')
r within the simulation domain.
Returns: Returns:
Sparse matrix representation of the operator Sparse matrix representation of the operator
@ -52,46 +121,34 @@ def cylindrical_operator(
Dfx, Dfy = deriv_forward(dxes[0]) Dfx, Dfy = deriv_forward(dxes[0])
Dbx, Dby = deriv_back(dxes[1]) Dbx, Dby = deriv_back(dxes[1])
rx = r0 + numpy.cumsum(dxes[0][0]) Ta, Tb = dxes2T(dxes=dxes, rmin=rmin)
ry = r0 + dxes[0][0] / 2.0 + numpy.cumsum(dxes[1][0])
tx = rx / r0
ty = ry / r0
Tx = sparse.diags(vec(tx[:, None].repeat(dxes[0][1].size, axis=1)))
Ty = sparse.diags(vec(ty[:, None].repeat(dxes[1][1].size, axis=1)))
eps_parts = numpy.split(epsilon, 3) eps_parts = numpy.split(epsilon, 3)
eps_x = sparse.diags(eps_parts[0]) eps_x = sparse.diags_array(eps_parts[0])
eps_y = sparse.diags(eps_parts[1]) eps_y = sparse.diags_array(eps_parts[1])
eps_z_inv = sparse.diags(1 / eps_parts[2]) eps_z_inv = sparse.diags_array(1 / eps_parts[2])
pa = sparse.vstack((Dfx, Dfy)) @ Tx @ eps_z_inv @ sparse.hstack((Dbx, Dby))
pb = sparse.vstack((Dfx, Dfy)) @ Tx @ eps_z_inv @ sparse.hstack((Dby, Dbx))
a0 = Ty @ eps_x + omega**-2 * Dby @ Ty @ Dfy
a1 = Tx @ eps_y + omega**-2 * Dbx @ Ty @ Dfx
b0 = Dbx @ Ty @ Dfy
b1 = Dby @ Ty @ Dfx
diag = sparse.block_diag
omega2 = omega * omega omega2 = omega * omega
diag = sparse.block_diag
op = ( sq0 = omega2 * diag((Tb @ Tb @ eps_x,
(omega2 * diag((Tx, Ty)) + pa) @ diag((a0, a1)) Ta @ Ta @ eps_y))
- (sparse.bmat(((None, Ty), (Tx, None))) + pb / omega2) @ diag((b0, b1)) lin0 = sparse.vstack((-Tb @ Dby, Ta @ Dbx)) @ Tb @ sparse.hstack((-Dfy, Dfx))
) lin1 = sparse.vstack((Dfx, Dfy)) @ Ta @ eps_z_inv @ sparse.hstack((Dbx @ Tb @ eps_x,
Dby @ Ta @ eps_y))
op = sq0 + lin0 + lin1
return op return op
def solve_mode( def solve_modes(
mode_number: int, mode_numbers: Sequence[int],
omega: complex, omega: float,
dxes: dx_lists_t, dxes: dx_lists2_t,
epsilon: vfdfield_t, epsilon: vfdslice,
r0: float, rmin: float,
) -> dict[str, complex | cfdfield_t]: mode_margin: int = 2,
) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]:
""" """
TODO: fixup
Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode
of the bent waveguide with the specified mode number. of the bent waveguide with the specified mode number.
@ -101,48 +158,336 @@ def solve_mode(
dxes: Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types. dxes: Grid parameters [dx_e, dx_h] as described in meanas.fdmath.types.
The first coordinate is assumed to be r, the second is y. The first coordinate is assumed to be r, the second is y.
epsilon: Dielectric constant epsilon: Dielectric constant
r0: Radius of curvature for the simulation. This should be the minimum value of rmin: Radius of curvature for the simulation. This should be the minimum value of
r within the simulation domain. r within the simulation domain.
Returns: Returns:
``` e_xys: NDArray of vfdfield_t specifying fields. First dimension is mode number.
{ angular_wavenumbers: list of wavenumbers in 1/rad units.
'E': list[NDArray[numpy.complex_]],
'H': list[NDArray[numpy.complex_]],
'wavenumber': complex,
}
```
""" """
''' #
Solve for the largest-magnitude eigenvalue of the real operator # Solve for the largest-magnitude eigenvalue of the real operator
''' #
dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes] dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes]
A_r = cylindrical_operator(numpy.real(omega), dxes_real, numpy.real(epsilon), r0) A_r = cylindrical_operator(numpy.real(omega), dxes_real, numpy.real(epsilon), rmin=rmin)
eigvals, eigvecs = signed_eigensolve(A_r, mode_number + 3) eigvals, eigvecs = signed_eigensolve(A_r, max(mode_numbers) + mode_margin)
e_xy = eigvecs[:, -(mode_number + 1)] keep_inds = -(numpy.array(mode_numbers) + 1)
e_xys = eigvecs[:, keep_inds].T
eigvals = eigvals[keep_inds]
''' #
Now solve for the eigenvector of the full operator, using the real operator's # Now solve for the eigenvector of the full operator, using the real operator's
eigenvector as an initial guess for Rayleigh quotient iteration. # eigenvector as an initial guess for Rayleigh quotient iteration.
''' #
A = cylindrical_operator(omega, dxes, epsilon, r0) A = cylindrical_operator(omega, dxes, epsilon, rmin=rmin)
eigval, e_xy = rayleigh_quotient_iteration(A, e_xy) for nn in range(len(mode_numbers)):
eigvals[nn], e_xys[nn, :] = rayleigh_quotient_iteration(A, e_xys[nn, :])
# Calculate the wave-vector (force the real part to be positive) # Calculate the wave-vector (force the real part to be positive)
wavenumber = numpy.sqrt(eigval) wavenumbers = numpy.sqrt(eigvals)
wavenumber *= numpy.sign(numpy.real(wavenumber)) wavenumbers *= numpy.sign(numpy.real(wavenumbers))
# TODO: Perform correction on wavenumber to account for numerical dispersion. # Wavenumbers assume the mode is at rmin, which is unlikely
# Instead, return the wavenumber in inverse radians
angular_wavenumbers = wavenumbers * cast('complex', rmin)
shape = [d.size for d in dxes[0]] order = angular_wavenumbers.argsort()[::-1]
e_xy = numpy.hstack((e_xy, numpy.zeros(shape[0] * shape[1]))) e_xys = e_xys[order]
fields = { angular_wavenumbers = angular_wavenumbers[order]
'wavenumber': wavenumber,
'E': unvec(e_xy, shape),
# 'E': unvec(e, shape),
# 'H': unvec(h, shape),
}
return fields return e_xys, angular_wavenumbers
def solve_mode(
mode_number: int,
*args: Any,
**kwargs: Any,
) -> tuple[vcfdslice, complex]:
"""
Wrapper around `solve_modes()` that solves for a single mode.
Args:
mode_number: 0-indexed mode number to solve for
*args: passed to `solve_modes()`
**kwargs: passed to `solve_modes()`
Returns:
(e_xy, angular_wavenumber)
"""
kwargs['mode_numbers'] = [mode_number]
e_xys, angular_wavenumbers = solve_modes(*args, **kwargs)
return e_xys[0], angular_wavenumbers[0]
def linear_wavenumbers(
e_xys: list[vcfdfield2_t],
angular_wavenumbers: ArrayLike,
epsilon: vfdslice,
dxes: dx_lists2_t,
rmin: float,
) -> NDArray[numpy.complex128]:
"""
Calculate linear wavenumbers (1/distance) based on angular wavenumbers (1/rad)
and the mode's energy distribution.
Args:
e_xys: Vectorized mode fields with shape (num_modes, 2 * x *y)
angular_wavenumbers: Wavenumbers assuming fields have theta-dependence of
`exp(-i * angular_wavenumber * theta)`. They should satisfy
`operator_e() @ e_xy == (angular_wavenumber / rmin) ** 2 * e_xy`
epsilon: Vectorized dielectric constant grid with shape (3, x, y)
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
rmin: Radius at the left edge of the simulation domain (at minimum 'x')
Returns:
NDArray containing the calculated linear (1/distance) wavenumbers
"""
angular_wavenumbers = numpy.asarray(angular_wavenumbers)
mode_radii = numpy.empty_like(angular_wavenumbers, dtype=float)
shape2d = (len(dxes[0][0]), len(dxes[0][1]))
epsilon2d = unvec(epsilon, shape2d)[:2]
grid_radii = rmin + numpy.cumsum(dxes[0][0])
for ii in range(angular_wavenumbers.size):
efield = unvec(e_xys[ii], shape2d, 2)
energy = numpy.real((efield * efield.conj()) * epsilon2d)
energy_vs_x = energy.sum(axis=(0, 2))
mode_radii[ii] = (grid_radii * energy_vs_x).sum() / energy_vs_x.sum()
logger.info(f'{mode_radii=}')
lin_wavenumbers = angular_wavenumbers / mode_radii
return lin_wavenumbers
def exy2h(
angular_wavenumber: complex,
omega: float,
dxes: dx_lists2_t,
rmin: float,
epsilon: vfdslice,
mu: vfdslice | None = None
) -> sparse.sparray:
"""
Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields,
into a vectorized H containing all three H components
Args:
angular_wavenumber: Wavenumber assuming fields have theta-dependence of
`exp(-i * angular_wavenumber * theta)`. It should satisfy
`operator_e() @ e_xy == (angular_wavenumber / rmin) ** 2 * e_xy`
omega: The angular frequency of the system
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
rmin: Radius at the left edge of the simulation domain (at minimum 'x')
epsilon: Vectorized dielectric constant grid
mu: Vectorized magnetic permeability grid (default 1 everywhere)
Returns:
Sparse matrix representing the operator.
"""
e2hop = e2h(angular_wavenumber=angular_wavenumber, omega=omega, dxes=dxes, rmin=rmin, mu=mu)
return e2hop @ exy2e(angular_wavenumber=angular_wavenumber, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon)
def exy2e(
angular_wavenumber: complex,
omega: float,
dxes: dx_lists2_t,
rmin: float,
epsilon: vfdslice,
) -> sparse.sparray:
"""
Operator which transforms the vector `e_xy` containing the vectorized E_x and E_y fields,
into a vectorized E containing all three E components
Unlike the straight waveguide case, the H_z components do not cancel and must be calculated
from E_x and E_y in order to then calculate E_z.
Args:
angular_wavenumber: Wavenumber assuming fields have theta-dependence of
`exp(-i * angular_wavenumber * theta)`. It should satisfy
`operator_e() @ e_xy == (angular_wavenumber / rmin) ** 2 * e_xy`
omega: The angular frequency of the system
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
rmin: Radius at the left edge of the simulation domain (at minimum 'x')
epsilon: Vectorized dielectric constant grid
Returns:
Sparse matrix representing the operator.
"""
Dfx, Dfy = deriv_forward(dxes[0])
Dbx, Dby = deriv_back(dxes[1])
wavenumber = angular_wavenumber / rmin
Ta, Tb = dxes2T(dxes=dxes, rmin=rmin)
Tai = sparse.diags_array(1 / Ta.diagonal())
#Tbi = sparse.diags_array(1 / Tb.diagonal())
epsilon_parts = numpy.split(epsilon, 3)
epsilon_x, epsilon_y = (sparse.diags_array(epsi) for epsi in epsilon_parts[:2])
epsilon_z_inv = sparse.diags_array(1 / epsilon_parts[2])
n_pts = dxes[0][0].size * dxes[0][1].size
zeros = sparse.coo_array((n_pts, n_pts))
mu_z = numpy.ones(n_pts)
mu_z_inv = sparse.diags_array(1 / mu_z)
exy2hz = 1 / (-1j * omega) * mu_z_inv @ sparse.hstack((Dfy, -Dfx))
hxy2ez = 1 / (1j * omega) * epsilon_z_inv @ sparse.hstack((Dby, -Dbx))
exy2hy = Tb / (1j * wavenumber) @ (-1j * omega * sparse.hstack((epsilon_x, zeros)) - Dby @ exy2hz)
exy2hx = Tb / (1j * wavenumber) @ ( 1j * omega * sparse.hstack((zeros, epsilon_y)) - Tai @ Dbx @ Tb @ exy2hz)
exy2ez = hxy2ez @ sparse.vstack((exy2hx, exy2hy))
op = sparse.vstack((sparse.eye_array(2 * n_pts),
exy2ez))
return op
def e2h(
angular_wavenumber: complex,
omega: float,
dxes: dx_lists2_t,
rmin: float,
mu: vfdslice | None = None
) -> sparse.sparray:
r"""
Returns an operator which, when applied to a vectorized E eigenfield, produces
the vectorized H eigenfield.
This operator is created directly from the initial coordinate-transformed equations:
$$
\begin{aligned}
\imath \omega \epsilon_{xx} E_x &= \hat{\partial}_y H_z + \imath \beta \frac{H_y}{\hat{T}} \\
\imath \omega \epsilon_{yy} E_y &= -\imath \beta H_x - \{1}{\tilde{t}_x} \hat{\partial}_x \hat{t}_x} H_z \\
\imath \omega \epsilon_{zz} E_z &= \hat{\partial}_x H_y - \hat{\partial}_y H_x \\
\end{aligned}
$$
Args:
angular_wavenumber: Wavenumber assuming fields have theta-dependence of
`exp(-i * angular_wavenumber * theta)`. It should satisfy
`operator_e() @ e_xy == (angular_wavenumber / rmin) ** 2 * e_xy`
omega: The angular frequency of the system
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
rmin: Radius at the left edge of the simulation domain (at minimum 'x')
mu: Vectorized magnetic permeability grid (default 1 everywhere)
Returns:
Sparse matrix representation of the operator.
"""
Dfx, Dfy = deriv_forward(dxes[0])
Ta, Tb = dxes2T(dxes=dxes, rmin=rmin)
Tai = sparse.diags_array(1 / Ta.diagonal())
Tbi = sparse.diags_array(1 / Tb.diagonal())
jB = 1j * angular_wavenumber / rmin
op = sparse.block_array([[ None, -jB * Tai, -Dfy],
[jB * Tbi, None, Tbi @ Dfx @ Ta],
[ Dfy, -Dfx, None]]) / (-1j * omega)
if mu is not None:
op = sparse.diags_array(1 / mu) @ op
return op
def dxes2T(
dxes: dx_lists2_t,
rmin: float,
) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
r"""
Returns the $T_a$ and $T_b$ diagonal matrices which are used to apply the cylindrical
coordinate transformation in various operators.
Args:
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
rmin: Radius at the left edge of the simulation domain (at minimum 'x')
Returns:
Sparse matrix representations of the operators Ta and Tb
"""
ra = rmin + numpy.cumsum(dxes[0][0]) # Radius at Ey points
rb = rmin + dxes[0][0] / 2.0 + numpy.cumsum(dxes[1][0]) # Radius at Ex points
ta = ra / rmin
tb = rb / rmin
Ta = sparse.diags_array(vec(ta[:, None].repeat(dxes[0][1].size, axis=1)))
Tb = sparse.diags_array(vec(tb[:, None].repeat(dxes[1][1].size, axis=1)))
return Ta, Tb
def normalized_fields_e(
e_xy: vcfdfield2,
angular_wavenumber: complex,
omega: float,
dxes: dx_lists2_t,
rmin: float,
epsilon: vfdslice,
mu: vfdslice | None = None,
prop_phase: float = 0,
) -> tuple[vcfdslice_t, vcfdslice_t]:
"""
Given a vector `e_xy` containing the vectorized E_x and E_y fields,
returns normalized, vectorized E and H fields for the system.
Args:
e_xy: Vector containing E_x and E_y fields
angular_wavenumber: Wavenumber assuming fields have theta-dependence of
`exp(-i * angular_wavenumber * theta)`. It should satisfy
`operator_e() @ e_xy == (angular_wavenumber / rmin) ** 2 * e_xy`
omega: The angular frequency of the system
dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` (2D)
rmin: Radius at the left edge of the simulation domain (at minimum 'x')
epsilon: Vectorized dielectric constant grid
mu: Vectorized magnetic permeability grid (default 1 everywhere)
prop_phase: Phase shift `(dz * corrected_wavenumber)` over 1 cell in propagation direction.
Default 0 (continuous propagation direction, i.e. dz->0).
Returns:
`(e, h)`, where each field is vectorized, normalized,
and contains all three vector components.
"""
e = exy2e(angular_wavenumber=angular_wavenumber, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon) @ e_xy
h = exy2h(angular_wavenumber=angular_wavenumber, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon, mu=mu) @ e_xy
e_norm, h_norm = _normalized_fields(e=e, h=h, omega=omega, dxes=dxes, rmin=rmin, epsilon=epsilon,
mu=mu, prop_phase=prop_phase)
return e_norm, h_norm
def _normalized_fields(
e: vcfdslice,
h: vcfdslice,
omega: complex,
dxes: dx_lists2_t,
rmin: float, # Currently unused, but may want to use cylindrical poynting
epsilon: vfdslice,
mu: vfdslice | None = None,
prop_phase: float = 0,
) -> tuple[vcfdslice_t, vcfdslice_t]:
h *= -1
# TODO documentation for normalized_fields
shape = [s.size for s in dxes[0]]
# Find time-averaged Sz and normalize to it
# H phase is adjusted by a half-cell forward shift for Yee cell, and 1-cell reverse shift for Poynting
Sz_tavg = waveguide_2d.inner_product(e, h, dxes=dxes, prop_phase=prop_phase, conj_h=True).real # Note, using linear poynting vector
assert Sz_tavg > 0, f'Found a mode propagating in the wrong direction! {Sz_tavg=}'
energy = numpy.real(epsilon * e.conj() * e)
norm_amplitude = 1 / numpy.sqrt(Sz_tavg)
norm_angle = -numpy.angle(e[energy.argmax()]) # Will randomly add a negative sign when mode is symmetric
# Try to break symmetry to assign a consistent sign [experimental]
E_weighted = unvec(e * energy * numpy.exp(1j * norm_angle), shape)
sign = numpy.sign(E_weighted[:,
:max(shape[0] // 2, 1),
:max(shape[1] // 2, 1)].real.sum())
assert sign != 0
norm_factor = sign * norm_amplitude * numpy.exp(1j * norm_angle)
e *= norm_factor
h *= norm_factor
return vcfdslice_t(e), vcfdslice_t(h)

View File

@ -741,8 +741,46 @@ the true values can be multiplied back in after the simulation is complete if no
normalized results are needed. normalized results are needed.
""" """
from .types import fdfield_t, vfdfield_t, cfdfield_t, vcfdfield_t, dx_lists_t, dx_lists_mut from .types import (
from .types import fdfield_updater_t, cfdfield_updater_t fdfield_t as fdfield_t,
from .vectorization import vec, unvec vfdfield_t as vfdfield_t,
from . import operators, functional, types, vectorization cfdfield_t as cfdfield_t,
vcfdfield_t as vcfdfield_t,
fdfield2_t as fdfield2_t,
vfdfield2_t as vfdfield2_t,
cfdfield2_t as cfdfield2_t,
vcfdfield2_t as vcfdfield2_t,
fdfield as fdfield,
vfdfield as vfdfield,
cfdfield as cfdfield,
vcfdfield as vcfdfield,
fdfield2 as fdfield2,
vfdfield2 as vfdfield2,
cfdfield2 as cfdfield2,
vcfdfield2 as vcfdfield2,
fdslice_t as fdslice_t,
vfdslice_t as vfdslice_t,
cfdslice_t as cfdslice_t,
vcfdslice_t as vcfdslice_t,
fdslice as fdslice,
vfdslice as vfdslice,
cfdslice as cfdslice,
vcfdslice as vcfdslice,
dx_lists_t as dx_lists_t,
dx_lists2_t as dx_lists2_t,
dx_lists_mut as dx_lists_mut,
dx_lists2_mut as dx_lists2_mut,
fdfield_updater_t as fdfield_updater_t,
cfdfield_updater_t as cfdfield_updater_t,
)
from .vectorization import (
vec as vec,
unvec as unvec,
)
from . import (
operators as operators,
functional as functional,
types as types,
vectorization as vectorization,
)

View File

@ -3,16 +3,18 @@ Math functions for finite difference simulations
Basic discrete calculus etc. Basic discrete calculus etc.
""" """
from typing import Sequence, Callable from typing import TypeVar
from collections.abc import Sequence, Callable
import numpy import numpy
from numpy.typing import NDArray from numpy.typing import NDArray
from numpy import floating, complexfloating
from .types import fdfield_t, fdfield_updater_t from .types import fdfield_t, fdfield_updater_t
def deriv_forward( def deriv_forward(
dx_e: Sequence[NDArray[numpy.float_]] | None = None, dx_e: Sequence[NDArray[floating | complexfloating]] | None = None,
) -> tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t]: ) -> tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t]:
""" """
Utility operators for taking discretized derivatives (backward variant). Utility operators for taking discretized derivatives (backward variant).
@ -36,7 +38,7 @@ def deriv_forward(
def deriv_back( def deriv_back(
dx_h: Sequence[NDArray[numpy.float_]] | None = None, dx_h: Sequence[NDArray[floating | complexfloating]] | None = None,
) -> tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t]: ) -> tuple[fdfield_updater_t, fdfield_updater_t, fdfield_updater_t]:
""" """
Utility operators for taking discretized derivatives (forward variant). Utility operators for taking discretized derivatives (forward variant).
@ -59,9 +61,12 @@ def deriv_back(
return derivs return derivs
TT = TypeVar('TT', bound='NDArray[floating | complexfloating]')
def curl_forward( def curl_forward(
dx_e: Sequence[NDArray[numpy.float_]] | None = None, dx_e: Sequence[NDArray[floating | complexfloating]] | None = None,
) -> fdfield_updater_t: ) -> Callable[[TT], TT]:
r""" r"""
Curl operator for use with the E field. Curl operator for use with the E field.
@ -75,7 +80,7 @@ def curl_forward(
""" """
Dx, Dy, Dz = deriv_forward(dx_e) Dx, Dy, Dz = deriv_forward(dx_e)
def ce_fun(e: fdfield_t) -> fdfield_t: def ce_fun(e: TT) -> TT:
output = numpy.empty_like(e) output = numpy.empty_like(e)
output[0] = Dy(e[2]) output[0] = Dy(e[2])
output[1] = Dz(e[0]) output[1] = Dz(e[0])
@ -89,8 +94,8 @@ def curl_forward(
def curl_back( def curl_back(
dx_h: Sequence[NDArray[numpy.float_]] | None = None, dx_h: Sequence[NDArray[floating | complexfloating]] | None = None,
) -> fdfield_updater_t: ) -> Callable[[TT], TT]:
r""" r"""
Create a function which takes the backward curl of a field. Create a function which takes the backward curl of a field.
@ -104,7 +109,7 @@ def curl_back(
""" """
Dx, Dy, Dz = deriv_back(dx_h) Dx, Dy, Dz = deriv_back(dx_h)
def ch_fun(h: fdfield_t) -> fdfield_t: def ch_fun(h: TT) -> TT:
output = numpy.empty_like(h) output = numpy.empty_like(h)
output[0] = Dy(h[2]) output[0] = Dy(h[2])
output[1] = Dz(h[0]) output[1] = Dz(h[0])
@ -118,7 +123,7 @@ def curl_back(
def curl_forward_parts( def curl_forward_parts(
dx_e: Sequence[NDArray[numpy.float_]] | None = None, dx_e: Sequence[NDArray[floating | complexfloating]] | None = None,
) -> Callable: ) -> Callable:
Dx, Dy, Dz = deriv_forward(dx_e) Dx, Dy, Dz = deriv_forward(dx_e)
@ -131,7 +136,7 @@ def curl_forward_parts(
def curl_back_parts( def curl_back_parts(
dx_h: Sequence[NDArray[numpy.float_]] | None = None, dx_h: Sequence[NDArray[floating | complexfloating]] | None = None,
) -> Callable: ) -> Callable:
Dx, Dy, Dz = deriv_back(dx_h) Dx, Dy, Dz = deriv_back(dx_h)

View File

@ -3,10 +3,11 @@ Matrix operators for finite difference simulations
Basic discrete calculus etc. Basic discrete calculus etc.
""" """
from typing import Sequence from collections.abc import Sequence
import numpy import numpy
from numpy.typing import NDArray from numpy.typing import NDArray
import scipy.sparse as sparse # type: ignore from numpy import floating, complexfloating
from scipy import sparse
from .types import vfdfield_t from .types import vfdfield_t
@ -15,7 +16,7 @@ def shift_circ(
axis: int, axis: int,
shape: Sequence[int], shape: Sequence[int],
shift_distance: int = 1, shift_distance: int = 1,
) -> sparse.spmatrix: ) -> sparse.sparray:
""" """
Utility operator for performing a circular shift along a specified axis by a Utility operator for performing a circular shift along a specified axis by a
specified number of elements. specified number of elements.
@ -33,8 +34,8 @@ def shift_circ(
if axis not in range(len(shape)): if axis not in range(len(shape)):
raise Exception(f'Invalid direction: {axis}, shape is {shape}') raise Exception(f'Invalid direction: {axis}, shape is {shape}')
shifts = [abs(shift_distance) if a == axis else 0 for a in range(3)] shifts = [abs(shift_distance) if a == axis else 0 for a in range(len(shape))]
shifted_diags = [(numpy.arange(n) + s) % n for n, s in zip(shape, shifts)] shifted_diags = [(numpy.arange(n) + s) % n for n, s in zip(shape, shifts, strict=True)]
ijk = numpy.meshgrid(*shifted_diags, indexing='ij') ijk = numpy.meshgrid(*shifted_diags, indexing='ij')
n = numpy.prod(shape) n = numpy.prod(shape)
@ -43,7 +44,7 @@ def shift_circ(
vij = (numpy.ones(n), (i_ind, j_ind.ravel(order='C'))) vij = (numpy.ones(n), (i_ind, j_ind.ravel(order='C')))
d = sparse.csr_matrix(vij, shape=(n, n)) d = sparse.csr_array(vij, shape=(n, n))
if shift_distance < 0: if shift_distance < 0:
d = d.T d = d.T
@ -55,7 +56,7 @@ def shift_with_mirror(
axis: int, axis: int,
shape: Sequence[int], shape: Sequence[int],
shift_distance: int = 1, shift_distance: int = 1,
) -> sparse.spmatrix: ) -> sparse.sparray:
""" """
Utility operator for performing an n-element shift along a specified axis, with mirror Utility operator for performing an n-element shift along a specified axis, with mirror
boundary conditions applied to the cells beyond the receding edge. boundary conditions applied to the cells beyond the receding edge.
@ -81,8 +82,8 @@ def shift_with_mirror(
v = numpy.where(v < 0, - 1 - v, v) v = numpy.where(v < 0, - 1 - v, v)
return v return v
shifts = [shift_distance if a == axis else 0 for a in range(3)] shifts = [shift_distance if a == axis else 0 for a in range(len(shape))]
shifted_diags = [mirrored_range(n, s) for n, s in zip(shape, shifts)] shifted_diags = [mirrored_range(n, s) for n, s in zip(shape, shifts, strict=True)]
ijk = numpy.meshgrid(*shifted_diags, indexing='ij') ijk = numpy.meshgrid(*shifted_diags, indexing='ij')
n = numpy.prod(shape) n = numpy.prod(shape)
@ -91,13 +92,13 @@ def shift_with_mirror(
vij = (numpy.ones(n), (i_ind, j_ind.ravel(order='C'))) vij = (numpy.ones(n), (i_ind, j_ind.ravel(order='C')))
d = sparse.csr_matrix(vij, shape=(n, n)) d = sparse.csr_array(vij, shape=(n, n))
return d return d
def deriv_forward( def deriv_forward(
dx_e: Sequence[NDArray[numpy.float_]], dx_e: Sequence[NDArray[floating | complexfloating]],
) -> list[sparse.spmatrix]: ) -> list[sparse.sparray]:
""" """
Utility operators for taking discretized derivatives (forward variant). Utility operators for taking discretized derivatives (forward variant).
@ -113,18 +114,18 @@ def deriv_forward(
dx_e_expanded = numpy.meshgrid(*dx_e, indexing='ij') dx_e_expanded = numpy.meshgrid(*dx_e, indexing='ij')
def deriv(axis: int) -> sparse.spmatrix: def deriv(axis: int) -> sparse.sparray:
return shift_circ(axis, shape, 1) - sparse.eye(n) return shift_circ(axis, shape, 1) - sparse.eye_array(n)
Ds = [sparse.diags(+1 / dx.ravel(order='C')) @ deriv(a) Ds = [sparse.diags_array(+1 / dx.ravel(order='C')) @ deriv(a)
for a, dx in enumerate(dx_e_expanded)] for a, dx in enumerate(dx_e_expanded)]
return Ds return Ds
def deriv_back( def deriv_back(
dx_h: Sequence[NDArray[numpy.float_]], dx_h: Sequence[NDArray[floating | complexfloating]],
) -> list[sparse.spmatrix]: ) -> list[sparse.sparray]:
""" """
Utility operators for taking discretized derivatives (backward variant). Utility operators for taking discretized derivatives (backward variant).
@ -140,18 +141,18 @@ def deriv_back(
dx_h_expanded = numpy.meshgrid(*dx_h, indexing='ij') dx_h_expanded = numpy.meshgrid(*dx_h, indexing='ij')
def deriv(axis: int) -> sparse.spmatrix: def deriv(axis: int) -> sparse.sparray:
return shift_circ(axis, shape, -1) - sparse.eye(n) return shift_circ(axis, shape, -1) - sparse.eye_array(n)
Ds = [sparse.diags(-1 / dx.ravel(order='C')) @ deriv(a) Ds = [sparse.diags_array(-1 / dx.ravel(order='C')) @ deriv(a)
for a, dx in enumerate(dx_h_expanded)] for a, dx in enumerate(dx_h_expanded)]
return Ds return Ds
def cross( def cross(
B: Sequence[sparse.spmatrix], B: Sequence[sparse.sparray],
) -> sparse.spmatrix: ) -> sparse.sparray:
""" """
Cross product operator Cross product operator
@ -163,13 +164,14 @@ def cross(
Sparse matrix corresponding to (B x), where x is the cross product. Sparse matrix corresponding to (B x), where x is the cross product.
""" """
n = B[0].shape[0] n = B[0].shape[0]
zero = sparse.csr_matrix((n, n)) zero = sparse.csr_array((n, n))
return sparse.bmat([[zero, -B[2], B[1]], return sparse.block_array([
[zero, -B[2], B[1]],
[B[2], zero, -B[0]], [B[2], zero, -B[0]],
[-B[1], B[0], zero]]) [-B[1], B[0], zero]])
def vec_cross(b: vfdfield_t) -> sparse.spmatrix: def vec_cross(b: vfdfield_t) -> sparse.sparray:
""" """
Vector cross product operator Vector cross product operator
@ -181,11 +183,11 @@ def vec_cross(b: vfdfield_t) -> sparse.spmatrix:
Sparse matrix corresponding to (b x), where x is the cross product. Sparse matrix corresponding to (b x), where x is the cross product.
""" """
B = [sparse.diags(c) for c in numpy.split(b, 3)] B = [sparse.diags_array(c) for c in numpy.split(b, 3)]
return cross(B) return cross(B)
def avg_forward(axis: int, shape: Sequence[int]) -> sparse.spmatrix: def avg_forward(axis: int, shape: Sequence[int]) -> sparse.sparray:
""" """
Forward average operator `(x4 = (x4 + x5) / 2)` Forward average operator `(x4 = (x4 + x5) / 2)`
@ -200,10 +202,10 @@ def avg_forward(axis: int, shape: Sequence[int]) -> sparse.spmatrix:
raise Exception(f'Invalid shape: {shape}') raise Exception(f'Invalid shape: {shape}')
n = numpy.prod(shape) n = numpy.prod(shape)
return 0.5 * (sparse.eye(n) + shift_circ(axis, shape)) return 0.5 * (sparse.eye_array(n) + shift_circ(axis, shape))
def avg_back(axis: int, shape: Sequence[int]) -> sparse.spmatrix: def avg_back(axis: int, shape: Sequence[int]) -> sparse.sparray:
""" """
Backward average operator `(x4 = (x4 + x3) / 2)` Backward average operator `(x4 = (x4 + x3) / 2)`
@ -218,8 +220,8 @@ def avg_back(axis: int, shape: Sequence[int]) -> sparse.spmatrix:
def curl_forward( def curl_forward(
dx_e: Sequence[NDArray[numpy.float_]], dx_e: Sequence[NDArray[floating | complexfloating]],
) -> sparse.spmatrix: ) -> sparse.sparray:
""" """
Curl operator for use with the E field. Curl operator for use with the E field.
@ -234,8 +236,8 @@ def curl_forward(
def curl_back( def curl_back(
dx_h: Sequence[NDArray[numpy.float_]], dx_h: Sequence[NDArray[floating | complexfloating]],
) -> sparse.spmatrix: ) -> sparse.sparray:
""" """
Curl operator for use with the H field. Curl operator for use with the H field.

View File

@ -1,26 +1,65 @@
""" """
Types shared across multiple submodules Types shared across multiple submodules
""" """
from typing import Sequence, Callable, MutableSequence from typing import NewType
import numpy from collections.abc import Sequence, Callable, MutableSequence
from numpy.typing import NDArray from numpy.typing import NDArray
from numpy import floating, complexfloating
# Field types # Field types
fdfield_t = NDArray[numpy.float_] fdfield_t = NewType('fdfield_t', NDArray[floating])
type fdfield = fdfield_t | NDArray[floating]
"""Vector field with shape (3, X, Y, Z) (e.g. `[E_x, E_y, E_z]`)""" """Vector field with shape (3, X, Y, Z) (e.g. `[E_x, E_y, E_z]`)"""
vfdfield_t = NDArray[numpy.float_] vfdfield_t = NewType('vfdfield_t', NDArray[floating])
type vfdfield = vfdfield_t | NDArray[floating]
"""Linearized vector field (single vector of length 3*X*Y*Z)""" """Linearized vector field (single vector of length 3*X*Y*Z)"""
cfdfield_t = NDArray[numpy.complex_] cfdfield_t = NewType('cfdfield_t', NDArray[complexfloating])
type cfdfield = cfdfield_t | NDArray[complexfloating]
"""Complex vector field with shape (3, X, Y, Z) (e.g. `[E_x, E_y, E_z]`)""" """Complex vector field with shape (3, X, Y, Z) (e.g. `[E_x, E_y, E_z]`)"""
vcfdfield_t = NDArray[numpy.complex_] vcfdfield_t = NewType('vcfdfield_t', NDArray[complexfloating])
type vcfdfield = vcfdfield_t | NDArray[complexfloating]
"""Linearized complex vector field (single vector of length 3*X*Y*Z)""" """Linearized complex vector field (single vector of length 3*X*Y*Z)"""
dx_lists_t = Sequence[Sequence[NDArray[numpy.float_]]] fdslice_t = NewType('fdslice_t', NDArray[floating])
type fdslice = fdslice_t | NDArray[floating]
"""Vector field slice with shape (3, X, Y) (e.g. `[E_x, E_y, E_z]` at a single Z position)"""
vfdslice_t = NewType('vfdslice_t', NDArray[floating])
type vfdslice = vfdslice_t | NDArray[floating]
"""Linearized vector field slice (single vector of length 3*X*Y)"""
cfdslice_t = NewType('cfdslice_t', NDArray[complexfloating])
type cfdslice = cfdslice_t | NDArray[complexfloating]
"""Complex vector field slice with shape (3, X, Y) (e.g. `[E_x, E_y, E_z]` at a single Z position)"""
vcfdslice_t = NewType('vcfdslice_t', NDArray[complexfloating])
type vcfdslice = vcfdslice_t | NDArray[complexfloating]
"""Linearized complex vector field slice (single vector of length 3*X*Y)"""
fdfield2_t = NewType('fdfield2_t', NDArray[floating])
type fdfield2 = fdfield2_t | NDArray[floating]
"""2D Vector field with shape (2, X, Y) (e.g. `[E_x, E_y]`)"""
vfdfield2_t = NewType('vfdfield2_t', NDArray[floating])
type vfdfield2 = vfdfield2_t | NDArray[floating]
"""2D Linearized vector field (single vector of length 2*X*Y)"""
cfdfield2_t = NewType('cfdfield2_t', NDArray[complexfloating])
type cfdfield2 = cfdfield2_t | NDArray[complexfloating]
"""2D Complex vector field with shape (2, X, Y) (e.g. `[E_x, E_y]`)"""
vcfdfield2_t = NewType('vcfdfield2_t', NDArray[complexfloating])
type vcfdfield2 = vcfdfield2_t | NDArray[complexfloating]
"""2D Linearized complex vector field (single vector of length 2*X*Y)"""
dx_lists_t = Sequence[Sequence[NDArray[floating | complexfloating]]]
""" """
'dxes' datastructure which contains grid cell width information in the following format: 'dxes' datastructure which contains grid cell width information in the following format:
@ -31,9 +70,23 @@ dx_lists_t = Sequence[Sequence[NDArray[numpy.float_]]]
and `dy_h[0]` is the y-width of the `y=0` cells, as used when calculating dH/dy, etc. and `dy_h[0]` is the y-width of the `y=0` cells, as used when calculating dH/dy, etc.
""" """
dx_lists_mut = MutableSequence[MutableSequence[NDArray[numpy.float_]]] dx_lists2_t = Sequence[Sequence[NDArray[floating | complexfloating]]]
"""
2D 'dxes' datastructure which contains grid cell width information in the following format:
[[[dx_e[0], dx_e[1], ...], [dy_e[0], ...]],
[[dx_h[0], dx_h[1], ...], [dy_h[0], ...]]]
where `dx_e[0]` is the x-width of the `x=0` cells, as used when calculating dE/dx,
and `dy_h[0]` is the y-width of the `y=0` cells, as used when calculating dH/dy, etc.
"""
dx_lists_mut = MutableSequence[MutableSequence[NDArray[floating | complexfloating]]]
"""Mutable version of `dx_lists_t`""" """Mutable version of `dx_lists_t`"""
dx_lists2_mut = MutableSequence[MutableSequence[NDArray[floating | complexfloating]]]
"""Mutable version of `dx_lists2_t`"""
fdfield_updater_t = Callable[..., fdfield_t] fdfield_updater_t = Callable[..., fdfield_t]
"""Convenience type for functions which take and return an fdfield_t""" """Convenience type for functions which take and return an fdfield_t"""

View File

@ -4,11 +4,16 @@ and a 1D array representation of that field `[f_x0, f_x1, f_x2,... f_y0,... f_z0
Vectorized versions of the field use row-major (ie., C-style) ordering. Vectorized versions of the field use row-major (ie., C-style) ordering.
""" """
from typing import overload, Sequence from typing import overload
from collections.abc import Sequence
import numpy import numpy
from numpy.typing import ArrayLike from numpy.typing import ArrayLike, NDArray
from .types import fdfield_t, vfdfield_t, cfdfield_t, vcfdfield_t from .types import (
fdfield_t, vfdfield_t, cfdfield_t, vcfdfield_t,
fdslice_t, vfdslice_t, cfdslice_t, vcfdslice_t,
fdfield2_t, vfdfield2_t, cfdfield2_t, vcfdfield2_t,
)
@overload @overload
@ -24,17 +29,35 @@ def vec(f: cfdfield_t) -> vcfdfield_t:
pass pass
@overload @overload
def vec(f: ArrayLike) -> vfdfield_t | vcfdfield_t: def vec(f: fdfield2_t) -> vfdfield2_t:
pass pass
def vec(f: fdfield_t | cfdfield_t | ArrayLike | None) -> vfdfield_t | vcfdfield_t | None: @overload
def vec(f: cfdfield2_t) -> vcfdfield2_t:
pass
@overload
def vec(f: fdslice_t) -> vfdslice_t:
pass
@overload
def vec(f: cfdslice_t) -> vcfdslice_t:
pass
@overload
def vec(f: ArrayLike) -> NDArray:
pass
def vec(
f: fdfield_t | cfdfield_t | fdfield2_t | cfdfield2_t | fdslice_t | cfdslice_t | ArrayLike | None,
) -> vfdfield_t | vcfdfield_t | vfdfield2_t | vcfdfield2_t | vfdslice_t | vcfdslice_t | NDArray | None:
""" """
Create a 1D ndarray from a 3D vector field which spans a 1-3D region. Create a 1D ndarray from a vector field which spans a 1-3D region.
Returns `None` if called with `f=None`. Returns `None` if called with `f=None`.
Args: Args:
f: A vector field, `[f_x, f_y, f_z]` where each `f_` component is a 1- to f: A vector field, e.g. `[f_x, f_y, f_z]` where each `f_` component is a 1- to
3-D ndarray (`f_*` should all be the same size). Doesn't fail with `f=None`. 3-D ndarray (`f_*` should all be the same size). Doesn't fail with `f=None`.
Returns: Returns:
@ -42,37 +65,62 @@ def vec(f: fdfield_t | cfdfield_t | ArrayLike | None) -> vfdfield_t | vcfdfield_
""" """
if f is None: if f is None:
return None return None
return numpy.ravel(f, order='C') return numpy.ravel(f, order='C') # type: ignore
@overload @overload
def unvec(v: None, shape: Sequence[int]) -> None: def unvec(v: None, shape: Sequence[int], nvdim: int = 3) -> None:
pass pass
@overload @overload
def unvec(v: vfdfield_t, shape: Sequence[int]) -> fdfield_t: def unvec(v: vfdfield_t, shape: Sequence[int], nvdim: int = 3) -> fdfield_t:
pass pass
@overload @overload
def unvec(v: vcfdfield_t, shape: Sequence[int]) -> cfdfield_t: def unvec(v: vcfdfield_t, shape: Sequence[int], nvdim: int = 3) -> cfdfield_t:
pass pass
def unvec(v: vfdfield_t | vcfdfield_t | None, shape: Sequence[int]) -> fdfield_t | cfdfield_t | None: @overload
def unvec(v: vfdfield2_t, shape: Sequence[int], nvdim: int = 3) -> fdfield2_t:
pass
@overload
def unvec(v: vcfdfield2_t, shape: Sequence[int], nvdim: int = 3) -> cfdfield2_t:
pass
@overload
def unvec(v: vfdslice_t, shape: Sequence[int], nvdim: int = 3) -> fdslice_t:
pass
@overload
def unvec(v: vcfdslice_t, shape: Sequence[int], nvdim: int = 3) -> cfdslice_t:
pass
@overload
def unvec(v: ArrayLike, shape: Sequence[int], nvdim: int = 3) -> NDArray:
pass
def unvec(
v: vfdfield_t | vcfdfield_t | vfdfield2_t | vcfdfield2_t | vfdslice_t | vcfdslice_t | ArrayLike | None,
shape: Sequence[int],
nvdim: int = 3,
) -> fdfield_t | cfdfield_t | fdfield2_t | cfdfield2_t | fdslice_t | cfdslice_t | NDArray | None:
""" """
Perform the inverse of vec(): take a 1D ndarray and output a 3D field Perform the inverse of vec(): take a 1D ndarray and output an `nvdim`-component field
of form `[f_x, f_y, f_z]` where each of `f_*` is a len(shape)-dimensional of form e.g. `[f_x, f_y, f_z]` (`nvdim=3`) where each of `f_*` is a len(shape)-dimensional
ndarray. ndarray.
Returns `None` if called with `v=None`. Returns `None` if called with `v=None`.
Args: Args:
v: 1D ndarray representing a 3D vector field of shape shape (or None) v: 1D ndarray representing a vector field of shape shape (or None)
shape: shape of the vector field shape: shape of the vector field
nvdim: Number of components in each vector
Returns: Returns:
`[f_x, f_y, f_z]` where each `f_` is a `len(shape)` dimensional ndarray (or `None`) `[f_x, f_y, f_z]` where each `f_` is a `len(shape)` dimensional ndarray (or `None`)
""" """
if v is None: if v is None:
return None return None
return v.reshape((3, *shape), order='C') return v.reshape((nvdim, *shape), order='C') # type: ignore

View File

@ -159,8 +159,22 @@ Boundary conditions
# TODO notes about boundaries / PMLs # TODO notes about boundaries / PMLs
""" """
from .base import maxwell_e, maxwell_h from .base import (
from .pml import cpml_params, updates_with_cpml maxwell_e as maxwell_e,
from .energy import (poynting, poynting_divergence, energy_hstep, energy_estep, maxwell_h as maxwell_h,
delta_energy_h2e, delta_energy_j) )
from .boundaries import conducting_boundary from .pml import (
cpml_params as cpml_params,
updates_with_cpml as updates_with_cpml,
)
from .energy import (
poynting as poynting,
poynting_divergence as poynting_divergence,
energy_hstep as energy_hstep,
energy_estep as energy_estep,
delta_energy_h2e as delta_energy_h2e,
delta_energy_j as delta_energy_j,
)
from .boundaries import (
conducting_boundary as conducting_boundary,
)

View File

@ -1,6 +1,6 @@
import numpy import numpy
from ..fdmath import dx_lists_t, fdfield_t from ..fdmath import dx_lists_t, fdfield_t, fdfield
from ..fdmath.functional import deriv_back from ..fdmath.functional import deriv_back
@ -8,8 +8,8 @@ from ..fdmath.functional import deriv_back
def poynting( def poynting(
e: fdfield_t, e: fdfield,
h: fdfield_t, h: fdfield,
dxes: dx_lists_t | None = None, dxes: dx_lists_t | None = None,
) -> fdfield_t: ) -> fdfield_t:
r""" r"""
@ -84,14 +84,14 @@ def poynting(
s[0] = numpy.roll(ey, -1, axis=0) * hz - numpy.roll(ez, -1, axis=0) * hy s[0] = numpy.roll(ey, -1, axis=0) * hz - numpy.roll(ez, -1, axis=0) * hy
s[1] = numpy.roll(ez, -1, axis=1) * hx - numpy.roll(ex, -1, axis=1) * hz s[1] = numpy.roll(ez, -1, axis=1) * hx - numpy.roll(ex, -1, axis=1) * hz
s[2] = numpy.roll(ex, -1, axis=2) * hy - numpy.roll(ey, -1, axis=2) * hx s[2] = numpy.roll(ex, -1, axis=2) * hy - numpy.roll(ey, -1, axis=2) * hx
return s return fdfield_t(s)
def poynting_divergence( def poynting_divergence(
s: fdfield_t | None = None, s: fdfield | None = None,
*, *,
e: fdfield_t | None = None, e: fdfield | None = None,
h: fdfield_t | None = None, h: fdfield | None = None,
dxes: dx_lists_t | None = None, dxes: dx_lists_t | None = None,
) -> fdfield_t: ) -> fdfield_t:
""" """
@ -122,15 +122,15 @@ def poynting_divergence(
Dx, Dy, Dz = deriv_back() Dx, Dy, Dz = deriv_back()
ds = Dx(s[0]) + Dy(s[1]) + Dz(s[2]) ds = Dx(s[0]) + Dy(s[1]) + Dz(s[2])
return ds return fdfield_t(ds)
def energy_hstep( def energy_hstep(
e0: fdfield_t, e0: fdfield,
h1: fdfield_t, h1: fdfield,
e2: fdfield_t, e2: fdfield,
epsilon: fdfield_t | None = None, epsilon: fdfield | None = None,
mu: fdfield_t | None = None, mu: fdfield | None = None,
dxes: dx_lists_t | None = None, dxes: dx_lists_t | None = None,
) -> fdfield_t: ) -> fdfield_t:
""" """
@ -150,15 +150,15 @@ def energy_hstep(
Energy, at the time of the H-field `h1`. Energy, at the time of the H-field `h1`.
""" """
u = dxmul(e0 * e2, h1 * h1, epsilon, mu, dxes) u = dxmul(e0 * e2, h1 * h1, epsilon, mu, dxes)
return u return fdfield_t(u)
def energy_estep( def energy_estep(
h0: fdfield_t, h0: fdfield,
e1: fdfield_t, e1: fdfield,
h2: fdfield_t, h2: fdfield,
epsilon: fdfield_t | None = None, epsilon: fdfield | None = None,
mu: fdfield_t | None = None, mu: fdfield | None = None,
dxes: dx_lists_t | None = None, dxes: dx_lists_t | None = None,
) -> fdfield_t: ) -> fdfield_t:
""" """
@ -178,17 +178,17 @@ def energy_estep(
Energy, at the time of the E-field `e1`. Energy, at the time of the E-field `e1`.
""" """
u = dxmul(e1 * e1, h0 * h2, epsilon, mu, dxes) u = dxmul(e1 * e1, h0 * h2, epsilon, mu, dxes)
return u return fdfield_t(u)
def delta_energy_h2e( def delta_energy_h2e(
dt: float, dt: float,
e0: fdfield_t, e0: fdfield,
h1: fdfield_t, h1: fdfield,
e2: fdfield_t, e2: fdfield,
h3: fdfield_t, h3: fdfield,
epsilon: fdfield_t | None = None, epsilon: fdfield | None = None,
mu: fdfield_t | None = None, mu: fdfield | None = None,
dxes: dx_lists_t | None = None, dxes: dx_lists_t | None = None,
) -> fdfield_t: ) -> fdfield_t:
""" """
@ -211,17 +211,17 @@ def delta_energy_h2e(
de = e2 * (e2 - e0) / dt de = e2 * (e2 - e0) / dt
dh = h1 * (h3 - h1) / dt dh = h1 * (h3 - h1) / dt
du = dxmul(de, dh, epsilon, mu, dxes) du = dxmul(de, dh, epsilon, mu, dxes)
return du return fdfield_t(du)
def delta_energy_e2h( def delta_energy_e2h(
dt: float, dt: float,
h0: fdfield_t, h0: fdfield,
e1: fdfield_t, e1: fdfield,
h2: fdfield_t, h2: fdfield,
e3: fdfield_t, e3: fdfield,
epsilon: fdfield_t | None = None, epsilon: fdfield | None = None,
mu: fdfield_t | None = None, mu: fdfield | None = None,
dxes: dx_lists_t | None = None, dxes: dx_lists_t | None = None,
) -> fdfield_t: ) -> fdfield_t:
""" """
@ -244,12 +244,12 @@ def delta_energy_e2h(
de = e1 * (e3 - e1) / dt de = e1 * (e3 - e1) / dt
dh = h2 * (h2 - h0) / dt dh = h2 * (h2 - h0) / dt
du = dxmul(de, dh, epsilon, mu, dxes) du = dxmul(de, dh, epsilon, mu, dxes)
return du return fdfield_t(du)
def delta_energy_j( def delta_energy_j(
j0: fdfield_t, j0: fdfield,
e1: fdfield_t, e1: fdfield,
dxes: dx_lists_t | None = None, dxes: dx_lists_t | None = None,
) -> fdfield_t: ) -> fdfield_t:
""" """
@ -267,14 +267,14 @@ def delta_energy_j(
* dxes[0][0][:, None, None] * dxes[0][0][:, None, None]
* dxes[0][1][None, :, None] * dxes[0][1][None, :, None]
* dxes[0][2][None, None, :]) * dxes[0][2][None, None, :])
return du return fdfield_t(du)
def dxmul( def dxmul(
ee: fdfield_t, ee: fdfield,
hh: fdfield_t, hh: fdfield,
epsilon: fdfield_t | float | None = None, epsilon: fdfield | float | None = None,
mu: fdfield_t | float | None = None, mu: fdfield | float | None = None,
dxes: dx_lists_t | None = None, dxes: dx_lists_t | None = None,
) -> fdfield_t: ) -> fdfield_t:
if epsilon is None: if epsilon is None:
@ -292,4 +292,4 @@ def dxmul(
* dxes[1][0][:, None, None] * dxes[1][0][:, None, None]
* dxes[1][1][None, :, None] * dxes[1][1][None, :, None]
* dxes[1][2][None, None, :]) * dxes[1][2][None, None, :])
return result return fdfield_t(result)

166
meanas/fdtd/misc.py Normal file
View File

@ -0,0 +1,166 @@
from collections.abc import Callable
import logging
import numpy
from numpy.typing import NDArray, ArrayLike
from numpy import pi
logger = logging.getLogger(__name__)
pulse_fn_t = Callable[[int | NDArray], tuple[float, float, float]]
def gaussian_packet(
wl: float,
dwl: float,
dt: float,
turn_on: float = 1e-10,
one_sided: bool = False,
) -> tuple[pulse_fn_t, float]:
"""
Gaussian pulse (or gaussian ramp) for FDTD excitation
exp(-a*t*t) ==> exp(-omega * omega / (4 * a)) [fourier, ignoring leading const.]
FWHM_time is 2 * sqrt(2 * log(2)) * sqrt(2 / a)
FWHM_omega is 2 * sqrt(2 * log(2)) * sqrt(2 * a) = 4 * sqrt(log(2) * a)
Args:
wl: wavelength
dwl: Gaussian's FWHM in wavelength space
dt: Timestep
turn_on: Max allowable amplitude at t=0
one_sided: If `True`, source amplitude never decreases after reaching max
Returns:
Source function: src(timestep) -> (envelope[tt], cos[... * tt], sin[... * tt])
Delay: number of initial timesteps for which envelope[tt] will be 0
"""
logger.warning('meanas.fdtd.misc functions are still very WIP!') # TODO
# dt * dw = 4 * ln(2)
omega = 2 * pi / wl
freq = 1 / wl
fwhm_omega = dwl * omega * omega / (2 * pi) # dwl -> d_omega (approx)
alpha = (fwhm_omega * fwhm_omega) * numpy.log(2) / 8
delay = numpy.sqrt(-numpy.log(turn_on) / alpha)
delay = numpy.ceil(delay * freq) / freq # force delay to integer number of periods to maintain phase
logger.info(f'src_time {2 * delay / dt}')
def source_phasor(ii: int | NDArray) -> tuple[float, float, float]:
t0 = ii * dt - delay
envelope = numpy.sqrt(numpy.sqrt(2 * alpha / pi)) * numpy.exp(-alpha * t0 * t0)
if one_sided and t0 > 0:
envelope = 1
cc = numpy.cos(omega * t0)
ss = numpy.sin(omega * t0)
return envelope, cc, ss
# nrm = numpy.exp(-omega * omega / alpha) / 2
return source_phasor, delay
def ricker_pulse(
wl: float,
dt: float,
turn_on: float = 1e-10,
) -> tuple[pulse_fn_t, float]:
"""
Ricker wavelet (second derivative of a gaussian pulse)
t0 = ii * dt - delay
R = w_peak * t0 / 2
f(t) = (1 - 2 * (pi * f_peak * t0) ** 2) * exp(-(pi * f_peak * t0)**2
= (1 - (w_peak * t0)**2 / 2 exp(-(w_peak * t0 / 2) **2)
= (1 - 2 * R * R) * exp(-R * R)
# NOTE: don't use cosine/sine for J, just for phasor readout
Args:
wl: wavelength
dt: Timestep
turn_on: Max allowable amplitude at t=0
Returns:
Source function: src(timestep) -> (envelope[tt], cos[... * tt], sin[... * tt])
Delay: number of initial timesteps for which envelope[tt] will be 0
"""
logger.warning('meanas.fdtd.misc functions are still very WIP!') # TODO
omega = 2 * pi / wl
freq = 1 / wl
# r0 = omega / 2
from scipy.optimize import root_scalar
delay_results = root_scalar(lambda tt: (1 - omega * omega * tt * tt / 2) * numpy.exp(-omega * omega / 4 * tt * tt) - turn_on, x0=0, x1=-2 / omega)
delay = delay_results.root
delay = numpy.ceil(delay * freq) / freq # force delay to integer number of periods to maintain phase
def source_phasor(ii: int | NDArray) -> tuple[float, float, float]:
t0 = ii * dt - delay
rr = omega * t0 / 2
ff = (1 - 2 * rr * rr) * numpy.exp(-rr * rr)
cc = numpy.cos(omega * t0)
ss = numpy.sin(omega * t0)
return ff, cc, ss
return source_phasor, delay
def gaussian_beam(
xyz: list[NDArray],
center: ArrayLike,
waist_radius: float,
wl: float,
tilt: float = 0,
) -> NDArray[numpy.complex128]:
"""
Gaussian beam
(solution to paraxial Helmholtz equation)
Default (no tilt) corresponds to a beam propagating in the -z direction.
Args:
xyz: List of [[x0, x1, ...], [y0, ...], [z0, ...]] positions specifying grid
locations at which the field will be sampled.
center: [x, y, z] location of beam waist
waist_radius: Beam radius at the waist
wl: Wavelength
tilt: Rotation around y axis. Default (0) has beam propagating in -z direction.
"""
logger.warning('meanas.fdtd.misc functions are still very WIP!') # TODO
w0 = waist_radius
grids = numpy.asarray(numpy.meshgrid(*xyz, indexing='ij'))
grids -= numpy.asarray(center)[:, None, None, None]
rot = numpy.array([
[ numpy.cos(tilt), 0, numpy.sin(tilt)],
[ 0, 1, 0],
[-numpy.sin(tilt), 0, numpy.cos(tilt)],
])
xx, yy, zz = numpy.einsum('ij,jxyz->ixyz', rot, grids)
r2 = xx * xx + yy * yy
z2 = zz * zz
zr = pi * w0 * w0 / wl
zr2 = zr * zr
wz2 = w0 * w0 * (1 + z2 / zr2)
wz = numpy.sqrt(wz2) # == fwhm(z) / sqrt(2 * ln(2))
kk = 2 * pi / wl
Rz = zz * (1 + zr2 / z2)
gouy = numpy.arctan(zz / zr)
gaussian = w0 / wz * numpy.exp(-r2 / wz2) * numpy.exp(1j * (kk * zz + kk * r2 / 2 / Rz - gouy))
row = gaussian[:, :, gaussian.shape[2] // 2]
norm = numpy.sqrt((row * row.conj()).sum())
return gaussian / norm

View File

@ -7,12 +7,13 @@ PML implementations
""" """
# TODO retest pmls! # TODO retest pmls!
from typing import Callable, Sequence, Any from typing import Any
from collections.abc import Callable, Sequence
from copy import deepcopy from copy import deepcopy
import numpy import numpy
from numpy.typing import NDArray, DTypeLike from numpy.typing import NDArray, DTypeLike
from ..fdmath import fdfield_t, dx_lists_t from ..fdmath import fdfield, fdfield_t, dx_lists_t
from ..fdmath.functional import deriv_forward, deriv_back from ..fdmath.functional import deriv_forward, deriv_back
@ -96,7 +97,7 @@ def updates_with_cpml(
cpml_params: Sequence[Sequence[dict[str, Any] | None]], cpml_params: Sequence[Sequence[dict[str, Any] | None]],
dt: float, dt: float,
dxes: dx_lists_t, dxes: dx_lists_t,
epsilon: fdfield_t, epsilon: fdfield,
*, *,
dtype: DTypeLike = numpy.float32, dtype: DTypeLike = numpy.float32,
) -> tuple[Callable[[fdfield_t, fdfield_t, fdfield_t], None], ) -> tuple[Callable[[fdfield_t, fdfield_t, fdfield_t], None],
@ -111,7 +112,7 @@ def updates_with_cpml(
params_H: list[list[tuple[Any, Any, Any, Any]]] = deepcopy(params_E) params_H: list[list[tuple[Any, Any, Any, Any]]] = deepcopy(params_E)
for axis in range(3): for axis in range(3):
for pp, polarity in enumerate((-1, 1)): for pp, _polarity in enumerate((-1, 1)):
cpml_param = cpml_params[axis][pp] cpml_param = cpml_params[axis][pp]
if cpml_param is None: if cpml_param is None:
psi_E[axis][pp] = (None, None) psi_E[axis][pp] = (None, None)
@ -184,7 +185,7 @@ def updates_with_cpml(
def update_H( def update_H(
e: fdfield_t, e: fdfield_t,
h: fdfield_t, h: fdfield_t,
mu: fdfield_t = numpy.ones(3), mu: fdfield_t | 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

@ -3,7 +3,8 @@
Test fixtures Test fixtures
""" """
from typing import Iterable, Any # ruff: noqa: ARG001
from typing import Any
import numpy import numpy
from numpy.typing import NDArray from numpy.typing import NDArray
import pytest # type: ignore import pytest # type: ignore
@ -20,18 +21,18 @@ FixtureRequest = Any
(5, 5, 5), (5, 5, 5),
# (7, 7, 7), # (7, 7, 7),
]) ])
def shape(request: FixtureRequest) -> Iterable[tuple[int, ...]]: def shape(request: FixtureRequest) -> tuple[int, ...]:
yield (3, *request.param) return (3, *request.param)
@pytest.fixture(scope='module', params=[1.0, 1.5]) @pytest.fixture(scope='module', params=[1.0, 1.5])
def epsilon_bg(request: FixtureRequest) -> Iterable[float]: def epsilon_bg(request: FixtureRequest) -> float:
yield request.param return request.param
@pytest.fixture(scope='module', params=[1.0, 2.5]) @pytest.fixture(scope='module', params=[1.0, 2.5])
def epsilon_fg(request: FixtureRequest) -> Iterable[float]: def epsilon_fg(request: FixtureRequest) -> float:
yield request.param return request.param
@pytest.fixture(scope='module', params=['center', '000', 'random']) @pytest.fixture(scope='module', params=['center', '000', 'random'])
@ -40,7 +41,7 @@ def epsilon(
shape: tuple[int, ...], shape: tuple[int, ...],
epsilon_bg: float, epsilon_bg: float,
epsilon_fg: float, epsilon_fg: float,
) -> Iterable[NDArray[numpy.float64]]: ) -> NDArray[numpy.float64]:
is3d = (numpy.array(shape) == 1).sum() == 0 is3d = (numpy.array(shape) == 1).sum() == 0
if is3d: if is3d:
if request.param == '000': if request.param == '000':
@ -60,17 +61,17 @@ def epsilon(
high=max(epsilon_bg, epsilon_fg), high=max(epsilon_bg, epsilon_fg),
size=shape) size=shape)
yield epsilon return epsilon
@pytest.fixture(scope='module', params=[1.0]) # 1.5 @pytest.fixture(scope='module', params=[1.0]) # 1.5
def j_mag(request: FixtureRequest) -> Iterable[float]: def j_mag(request: FixtureRequest) -> float:
yield request.param return request.param
@pytest.fixture(scope='module', params=[1.0, 1.5]) @pytest.fixture(scope='module', params=[1.0, 1.5])
def dx(request: FixtureRequest) -> Iterable[float]: def dx(request: FixtureRequest) -> float:
yield request.param return request.param
@pytest.fixture(scope='module', params=['uniform', 'centerbig']) @pytest.fixture(scope='module', params=['uniform', 'centerbig'])
@ -78,7 +79,7 @@ def dxes(
request: FixtureRequest, request: FixtureRequest,
shape: tuple[int, ...], shape: tuple[int, ...],
dx: float, dx: float,
) -> Iterable[list[list[NDArray[numpy.float64]]]]: ) -> list[list[NDArray[numpy.float64]]]:
if request.param == 'uniform': if request.param == 'uniform':
dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)] dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)]
elif request.param == 'centerbig': elif request.param == 'centerbig':
@ -90,5 +91,5 @@ def dxes(
dxe = [PRNG.uniform(low=1.0 * dx, high=1.1 * dx, size=s) for s in shape[1:]] dxe = [PRNG.uniform(low=1.0 * dx, high=1.1 * dx, size=s) for s in shape[1:]]
dxh = [(d + numpy.roll(d, -1)) / 2 for d in dxe] dxh = [(d + numpy.roll(d, -1)) / 2 for d in dxe]
dxes = [dxe, dxh] dxes = [dxe, dxh]
yield dxes return dxes

View File

@ -1,4 +1,4 @@
from typing import Iterable # ruff: noqa: ARG001
import dataclasses import dataclasses
import pytest # type: ignore import pytest # type: ignore
import numpy import numpy
@ -6,7 +6,7 @@ from numpy.typing import NDArray
#from numpy.testing import assert_allclose, assert_array_equal #from numpy.testing import assert_allclose, assert_array_equal
from .. import fdfd from .. import fdfd
from ..fdmath import vec, unvec from ..fdmath import vec, unvec, vcfdfield, vfdfield, dx_lists_t
from .utils import assert_close # , assert_fields_close from .utils import assert_close # , assert_fields_close
from .conftest import FixtureRequest from .conftest import FixtureRequest
@ -61,24 +61,24 @@ def test_poynting_planes(sim: 'FDResult') -> None:
# Also see conftest.py # Also see conftest.py
@pytest.fixture(params=[1 / 1500]) @pytest.fixture(params=[1 / 1500])
def omega(request: FixtureRequest) -> Iterable[float]: def omega(request: FixtureRequest) -> float:
yield request.param return request.param
@pytest.fixture(params=[None]) @pytest.fixture(params=[None])
def pec(request: FixtureRequest) -> Iterable[NDArray[numpy.float64] | None]: def pec(request: FixtureRequest) -> NDArray[numpy.float64] | None:
yield request.param return request.param
@pytest.fixture(params=[None]) @pytest.fixture(params=[None])
def pmc(request: FixtureRequest) -> Iterable[NDArray[numpy.float64] | None]: def pmc(request: FixtureRequest) -> NDArray[numpy.float64] | None:
yield request.param return request.param
#@pytest.fixture(scope='module', #@pytest.fixture(scope='module',
# params=[(25, 5, 5)]) # params=[(25, 5, 5)])
#def shape(request): #def shape(request: FixtureRequest):
# yield (3, *request.param) # return (3, *request.param)
@pytest.fixture(params=['diag']) # 'center' @pytest.fixture(params=['diag']) # 'center'
@ -86,7 +86,7 @@ def j_distribution(
request: FixtureRequest, request: FixtureRequest,
shape: tuple[int, ...], shape: tuple[int, ...],
j_mag: float, j_mag: float,
) -> Iterable[NDArray[numpy.float64]]: ) -> NDArray[numpy.float64]:
j = numpy.zeros(shape, dtype=complex) j = numpy.zeros(shape, dtype=complex)
center_mask = numpy.zeros(shape, dtype=bool) center_mask = numpy.zeros(shape, dtype=bool)
center_mask[:, shape[1] // 2, shape[2] // 2, shape[3] // 2] = True center_mask[:, shape[1] // 2, shape[2] // 2, shape[3] // 2] = True
@ -96,22 +96,22 @@ def j_distribution(
elif request.param == 'diag': elif request.param == 'diag':
j[numpy.roll(center_mask, [1, 1, 1], axis=(1, 2, 3))] = (1 + 1j) * j_mag j[numpy.roll(center_mask, [1, 1, 1], axis=(1, 2, 3))] = (1 + 1j) * j_mag
j[numpy.roll(center_mask, [-1, -1, -1], axis=(1, 2, 3))] = (1 - 1j) * j_mag j[numpy.roll(center_mask, [-1, -1, -1], axis=(1, 2, 3))] = (1 - 1j) * j_mag
yield j return j
@dataclasses.dataclass() @dataclasses.dataclass()
class FDResult: class FDResult:
shape: tuple[int, ...] shape: tuple[int, ...]
dxes: list[list[NDArray[numpy.float64]]] dxes: dx_lists_t
epsilon: NDArray[numpy.float64] epsilon: vfdfield
omega: complex omega: complex
j: NDArray[numpy.complex128] j: vcfdfield
e: NDArray[numpy.complex128] e: vcfdfield
pmc: NDArray[numpy.float64] | None pmc: vfdfield | None
pec: NDArray[numpy.float64] | None pec: vfdfield | None
@pytest.fixture() @pytest.fixture
def sim( def sim(
request: FixtureRequest, request: FixtureRequest,
shape: tuple[int, ...], shape: tuple[int, ...],
@ -141,11 +141,11 @@ def sim(
j_vec = vec(j_distribution) j_vec = vec(j_distribution)
eps_vec = vec(epsilon) eps_vec = vec(epsilon)
e_vec = fdfd.solvers.generic( e_vec = fdfd.solvers.generic(
J=j_vec, J = j_vec,
omega=omega, omega = omega,
dxes=dxes, dxes = dxes,
epsilon=eps_vec, epsilon = eps_vec,
matrix_solver_opts={'atol': 1e-15, 'tol': 1e-11}, matrix_solver_opts = dict(atol=1e-15, rtol=1e-11),
) )
e = unvec(e_vec, shape[1:]) e = unvec(e_vec, shape[1:])

View File

@ -1,11 +1,11 @@
from typing import Iterable # ruff: noqa: ARG001
import pytest # type: ignore import pytest # type: ignore
import numpy import numpy
from numpy.typing import NDArray from numpy.typing import NDArray
from numpy.testing import assert_allclose from numpy.testing import assert_allclose
from .. import fdfd from .. import fdfd
from ..fdmath import vec, unvec, dx_lists_mut from ..fdmath import vec, unvec, dx_lists_mut, vfdfield, cfdfield_t
#from .utils import assert_close, assert_fields_close #from .utils import assert_close, assert_fields_close
from .test_fdfd import FDResult from .test_fdfd import FDResult
from .conftest import FixtureRequest from .conftest import FixtureRequest
@ -44,41 +44,41 @@ def test_pml(sim: FDResult, src_polarity: int) -> None:
# Also see conftest.py # Also see conftest.py
@pytest.fixture(params=[1 / 1500]) @pytest.fixture(params=[1 / 1500])
def omega(request: FixtureRequest) -> Iterable[float]: def omega(request: FixtureRequest) -> float:
yield request.param return request.param
@pytest.fixture(params=[None]) @pytest.fixture(params=[None])
def pec(request: FixtureRequest) -> Iterable[NDArray[numpy.float64] | None]: def pec(request: FixtureRequest) -> NDArray[numpy.float64] | None:
yield request.param return request.param
@pytest.fixture(params=[None]) @pytest.fixture(params=[None])
def pmc(request: FixtureRequest) -> Iterable[NDArray[numpy.float64] | None]: def pmc(request: FixtureRequest) -> NDArray[numpy.float64] | None:
yield request.param return request.param
@pytest.fixture(params=[(30, 1, 1), @pytest.fixture(params=[(30, 1, 1),
(1, 30, 1), (1, 30, 1),
(1, 1, 30)]) (1, 1, 30)])
def shape(request: FixtureRequest) -> Iterable[tuple[int, ...]]: def shape(request: FixtureRequest) -> tuple[int, int, int]:
yield (3, *request.param) return (3, *request.param)
@pytest.fixture(params=[+1, -1]) @pytest.fixture(params=[+1, -1])
def src_polarity(request: FixtureRequest) -> Iterable[int]: def src_polarity(request: FixtureRequest) -> int:
yield request.param return request.param
@pytest.fixture() @pytest.fixture
def j_distribution( def j_distribution(
request: FixtureRequest, request: FixtureRequest,
shape: tuple[int, ...], shape: tuple[int, ...],
epsilon: NDArray[numpy.float64], epsilon: vfdfield,
dxes: dx_lists_mut, dxes: dx_lists_mut,
omega: float, omega: float,
src_polarity: int, src_polarity: int,
) -> Iterable[NDArray[numpy.complex128]]: ) -> cfdfield_t:
j = numpy.zeros(shape, dtype=complex) j = numpy.zeros(shape, dtype=complex)
dim = numpy.where(numpy.array(shape[1:]) > 1)[0][0] # Propagation axis dim = numpy.where(numpy.array(shape[1:]) > 1)[0][0] # Propagation axis
@ -106,18 +106,18 @@ def j_distribution(
j = fdfd.waveguide_3d.compute_source(E=e, wavenumber=wavenumber_corrected, omega=omega, dxes=dxes, j = fdfd.waveguide_3d.compute_source(E=e, wavenumber=wavenumber_corrected, omega=omega, dxes=dxes,
axis=dim, polarity=src_polarity, slices=slices, epsilon=epsilon) axis=dim, polarity=src_polarity, slices=slices, epsilon=epsilon)
yield j return j
@pytest.fixture() @pytest.fixture
def epsilon( def epsilon(
request: FixtureRequest, request: FixtureRequest,
shape: tuple[int, ...], shape: tuple[int, ...],
epsilon_bg: float, epsilon_bg: float,
epsilon_fg: float, epsilon_fg: float,
) -> Iterable[NDArray[numpy.float64]]: ) -> NDArray[numpy.float64]:
epsilon = numpy.full(shape, epsilon_fg, dtype=float) epsilon = numpy.full(shape, epsilon_fg, dtype=float)
yield epsilon return epsilon
@pytest.fixture(params=['uniform']) @pytest.fixture(params=['uniform'])
@ -127,7 +127,7 @@ def dxes(
dx: float, dx: float,
omega: float, omega: float,
epsilon_fg: float, epsilon_fg: float,
) -> Iterable[list[list[NDArray[numpy.float64]]]]: ) -> list[list[NDArray[numpy.float64]]]:
if request.param == 'uniform': if request.param == 'uniform':
dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)] dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)]
dim = numpy.where(numpy.array(shape[1:]) > 1)[0][0] # Propagation axis dim = numpy.where(numpy.array(shape[1:]) > 1)[0][0] # Propagation axis
@ -141,10 +141,10 @@ def dxes(
epsilon_effective=epsilon_fg, epsilon_effective=epsilon_fg,
thickness=10, thickness=10,
) )
yield dxes return dxes
@pytest.fixture() @pytest.fixture
def sim( def sim(
request: FixtureRequest, request: FixtureRequest,
shape: tuple[int, ...], shape: tuple[int, ...],
@ -162,7 +162,7 @@ def sim(
omega=omega, omega=omega,
dxes=dxes, dxes=dxes,
epsilon=eps_vec, epsilon=eps_vec,
matrix_solver_opts={'atol': 1e-15, 'tol': 1e-11}, matrix_solver_opts={'atol': 1e-15, 'rtol': 1e-11},
) )
e = unvec(e_vec, shape[1:]) e = unvec(e_vec, shape[1:])

View File

@ -1,4 +1,5 @@
from typing import Iterable, Any # ruff: noqa: ARG001
from typing import Any
import dataclasses import dataclasses
import pytest # type: ignore import pytest # type: ignore
import numpy import numpy
@ -150,8 +151,8 @@ def test_poynting_planes(sim: 'TDResult') -> None:
@pytest.fixture(params=[0.3]) @pytest.fixture(params=[0.3])
def dt(request: FixtureRequest) -> Iterable[float]: def dt(request: FixtureRequest) -> float:
yield request.param return request.param
@dataclasses.dataclass() @dataclasses.dataclass()
@ -168,8 +169,8 @@ class TDResult:
@pytest.fixture(params=[(0, 4, 8)]) # (0,) @pytest.fixture(params=[(0, 4, 8)]) # (0,)
def j_steps(request: FixtureRequest) -> Iterable[tuple[int, ...]]: def j_steps(request: FixtureRequest) -> tuple[int, ...]:
yield request.param return request.param
@pytest.fixture(params=['center', 'random']) @pytest.fixture(params=['center', 'random'])
@ -177,7 +178,7 @@ def j_distribution(
request: FixtureRequest, request: FixtureRequest,
shape: tuple[int, ...], shape: tuple[int, ...],
j_mag: float, j_mag: float,
) -> Iterable[NDArray[numpy.float64]]: ) -> NDArray[numpy.float64]:
j = numpy.zeros(shape) j = numpy.zeros(shape)
if request.param == 'center': if request.param == 'center':
j[:, shape[1] // 2, shape[2] // 2, shape[3] // 2] = j_mag j[:, shape[1] // 2, shape[2] // 2, shape[3] // 2] = j_mag
@ -185,10 +186,10 @@ def j_distribution(
j[:, 0, 0, 0] = j_mag j[:, 0, 0, 0] = j_mag
elif request.param == 'random': elif request.param == 'random':
j[:] = PRNG.uniform(low=-j_mag, high=j_mag, size=shape) j[:] = PRNG.uniform(low=-j_mag, high=j_mag, size=shape)
yield j return j
@pytest.fixture() @pytest.fixture
def sim( def sim(
request: FixtureRequest, request: FixtureRequest,
shape: tuple[int, ...], shape: tuple[int, ...],
@ -199,8 +200,7 @@ def sim(
j_steps: tuple[int, ...], j_steps: tuple[int, ...],
) -> TDResult: ) -> TDResult:
is3d = (numpy.array(shape) == 1).sum() == 0 is3d = (numpy.array(shape) == 1).sum() == 0
if is3d: if is3d and dt != 0.3:
if dt != 0.3:
pytest.skip('Skipping dt != 0.3 because test is 3D (for speed)') pytest.skip('Skipping dt != 0.3 because test is 3D (for speed)')
sim = TDResult( sim = TDResult(

View File

@ -1,5 +1,3 @@
from typing import Any
import numpy import numpy
from numpy.typing import NDArray from numpy.typing import NDArray
@ -10,22 +8,25 @@ PRNG = numpy.random.RandomState(12345)
def assert_fields_close( def assert_fields_close(
x: NDArray, x: NDArray,
y: NDArray, y: NDArray,
*args: Any,
**kwargs: Any,
) -> None:
numpy.testing.assert_allclose(
x, y, verbose=False, # type: ignore
err_msg='Fields did not match:\n{}\n{}'.format(numpy.moveaxis(x, -1, 0),
numpy.moveaxis(y, -1, 0)),
*args, *args,
**kwargs, **kwargs,
) -> None:
x_disp = numpy.moveaxis(x, -1, 0)
y_disp = numpy.moveaxis(y, -1, 0)
numpy.testing.assert_allclose(
x, # type: ignore
y, # type: ignore
*args,
verbose=False,
err_msg=f'Fields did not match:\n{x_disp}\n{y_disp}',
**kwargs,
) )
def assert_close( def assert_close(
x: NDArray, x: NDArray,
y: NDArray, y: NDArray,
*args: Any, *args,
**kwargs: Any, **kwargs,
) -> None: ) -> None:
numpy.testing.assert_allclose(x, y, *args, **kwargs) numpy.testing.assert_allclose(x, y, *args, **kwargs)

View File

@ -39,9 +39,10 @@ include = [
] ]
dynamic = ["version"] dynamic = ["version"]
dependencies = [ dependencies = [
"numpy~=1.21", "gridlock",
"scipy", "numpy>=2.0",
] "scipy~=1.14",
]
[tool.hatch.version] [tool.hatch.version]
@ -49,5 +50,55 @@ path = "meanas/__init__.py"
[project.optional-dependencies] [project.optional-dependencies]
dev = ["pytest", "pdoc", "gridlock"] dev = ["pytest", "pdoc", "gridlock"]
examples = ["gridlock"] examples = [
"gridlock>=2.1",
"matplotlib>=3.10.8",
]
test = ["pytest"] test = ["pytest"]
[tool.ruff]
exclude = [
".git",
"dist",
]
line-length = 245
indent-width = 4
lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
lint.select = [
"NPY", "E", "F", "W", "B", "ANN", "UP", "SLOT", "SIM", "LOG",
"C4", "ISC", "PIE", "PT", "RET", "TCH", "PTH", "INT",
"ARG", "PL", "R", "TRY",
"G010", "G101", "G201", "G202",
"Q002", "Q003", "Q004",
]
lint.ignore = [
#"ANN001", # No annotation
"ANN002", # *args
"ANN003", # **kwargs
"ANN401", # Any
"SIM108", # single-line if / else assignment
"RET504", # x=y+z; return x
"PIE790", # unnecessary pass
"ISC003", # non-implicit string concatenation
"C408", # dict(x=y) instead of {'x': y}
"PLR09", # Too many xxx
"PLR2004", # magic number
"PLC0414", # import x as x
"TRY003", # Long exception message
"TRY002", # Exception()
]
[[tool.mypy.overrides]]
module = [
"scipy",
"scipy.optimize",
"scipy.linalg",
"scipy.sparse",
"scipy.sparse.linalg",
]
ignore_missing_imports = true
[tool.uv.sources]
gridlock = { path = "../gridlock", editable = true }

806
uv.lock generated Normal file
View File

@ -0,0 +1,806 @@
version = 1
revision = 3
requires-python = ">=3.11"
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "contourpy"
version = "1.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" },
{ url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" },
{ url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" },
{ url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" },
{ url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" },
{ url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" },
{ url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" },
{ url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" },
{ url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" },
{ url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" },
{ url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" },
{ url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" },
{ url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" },
{ url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" },
{ url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" },
{ url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" },
{ url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" },
{ url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" },
{ url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" },
{ url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" },
{ url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" },
{ url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" },
{ url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" },
{ url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" },
{ url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" },
{ url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" },
{ url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" },
{ url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" },
{ url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" },
{ url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" },
{ url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" },
{ url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" },
{ url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" },
{ url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" },
{ url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" },
{ url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" },
{ url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" },
{ url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" },
{ url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" },
{ url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" },
{ url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" },
{ url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" },
{ url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" },
{ url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" },
{ url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" },
{ url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" },
{ url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" },
{ url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" },
{ url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" },
{ url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" },
{ url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" },
{ url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" },
{ url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" },
{ url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" },
{ url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" },
{ url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" },
{ url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" },
{ url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" },
{ url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" },
{ url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" },
{ url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" },
{ url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" },
{ url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" },
{ url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" },
{ url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" },
{ url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" },
{ url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" },
{ url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" },
{ url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" },
{ url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" },
{ url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" },
]
[[package]]
name = "cycler"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
]
[[package]]
name = "float-raster"
version = "0.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "scipy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0d/7e/57d91306c966fc5ce8e068650741b599011a14e96ff0b8ec226668094287/float_raster-0.8.tar.gz", hash = "sha256:90e9c00d3908a8e0d50cd97c6df42055ac0fcf900302a504a4becaa024c60c22", size = 29233, upload-time = "2024-07-29T09:10:12.007Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/69/bcdcf1b52420d4b97ab6a18798a4ebb92292948b555a9a6782cb628821e6/float_raster-0.8-py3-none-any.whl", hash = "sha256:7e4ce9ffaf972e3ee788f16b06ee0eb07488b74634ee6f3db2402bf10ef29be7", size = 42469, upload-time = "2024-07-29T09:10:09.91Z" },
]
[[package]]
name = "fonttools"
version = "4.61.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/33/f9/0e84d593c0e12244150280a630999835a64f2852276161b62a0f98318de0/fonttools-4.61.0.tar.gz", hash = "sha256:ec520a1f0c7758d7a858a00f090c1745f6cde6a7c5e76fb70ea4044a15f712e7", size = 3561884, upload-time = "2025-11-28T17:05:49.491Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/be/5aa89cdddf2863d8afbdc19eb8ec5d8d35d40eeeb8e6cf52c5ff1c2dbd33/fonttools-4.61.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a32a16951cbf113d38f1dd8551b277b6e06e0f6f776fece0f99f746d739e1be3", size = 2847553, upload-time = "2025-11-28T17:04:30.539Z" },
{ url = "https://files.pythonhosted.org/packages/0d/3e/6ff643b07cead1236a534f51291ae2981721cf419135af5b740c002a66dd/fonttools-4.61.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:328a9c227984bebaf69f3ac9062265f8f6acc7ddf2e4e344c63358579af0aa3d", size = 2388298, upload-time = "2025-11-28T17:04:32.161Z" },
{ url = "https://files.pythonhosted.org/packages/c3/15/fca8dfbe7b482e6f240b1aad0ed7c6e2e75e7a28efa3d3a03b570617b5e5/fonttools-4.61.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f0bafc8a3b3749c69cc610e5aa3da832d39c2a37a68f03d18ec9a02ecaac04a", size = 5054133, upload-time = "2025-11-28T17:04:34.035Z" },
{ url = "https://files.pythonhosted.org/packages/6a/a2/821c61c691b21fd09e07528a9a499cc2b075ac83ddb644aa16c9875a64bc/fonttools-4.61.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b5ca59b7417d149cf24e4c1933c9f44b2957424fc03536f132346d5242e0ebe5", size = 5031410, upload-time = "2025-11-28T17:04:36.141Z" },
{ url = "https://files.pythonhosted.org/packages/e8/f6/8b16339e93d03c732c8a23edefe3061b17a5f9107ddc47a3215ecd054cac/fonttools-4.61.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:df8cbce85cf482eb01f4551edca978c719f099c623277bda8332e5dbe7dba09d", size = 5030005, upload-time = "2025-11-28T17:04:38.314Z" },
{ url = "https://files.pythonhosted.org/packages/ac/eb/d4e150427bdaa147755239c931bbce829a88149ade5bfd8a327afe565567/fonttools-4.61.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7fb5b84f48a6a733ca3d7f41aa9551908ccabe8669ffe79586560abcc00a9cfd", size = 5154026, upload-time = "2025-11-28T17:04:40.34Z" },
{ url = "https://files.pythonhosted.org/packages/7f/5f/3dd00ce0dba6759943c707b1830af8c0bcf6f8f1a9fe46cb82e7ac2aaa74/fonttools-4.61.0-cp311-cp311-win32.whl", hash = "sha256:787ef9dfd1ea9fe49573c272412ae5f479d78e671981819538143bec65863865", size = 2276035, upload-time = "2025-11-28T17:04:42.59Z" },
{ url = "https://files.pythonhosted.org/packages/4e/44/798c472f096ddf12955eddb98f4f7c906e7497695d04ce073ddf7161d134/fonttools-4.61.0-cp311-cp311-win_amd64.whl", hash = "sha256:14fafda386377b6131d9e448af42d0926bad47e038de0e5ba1d58c25d621f028", size = 2327290, upload-time = "2025-11-28T17:04:44.57Z" },
{ url = "https://files.pythonhosted.org/packages/00/5d/19e5939f773c7cb05480fe2e881d63870b63ee2b4bdb9a77d55b1d36c7b9/fonttools-4.61.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e24a1565c4e57111ec7f4915f8981ecbb61adf66a55f378fdc00e206059fcfef", size = 2846930, upload-time = "2025-11-28T17:04:46.639Z" },
{ url = "https://files.pythonhosted.org/packages/25/b2/0658faf66f705293bd7e739a4f038302d188d424926be9c59bdad945664b/fonttools-4.61.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2bfacb5351303cae9f072ccf3fc6ecb437a6f359c0606bae4b1ab6715201d87", size = 2383016, upload-time = "2025-11-28T17:04:48.525Z" },
{ url = "https://files.pythonhosted.org/packages/29/a3/1fa90b95b690f0d7541f48850adc40e9019374d896c1b8148d15012b2458/fonttools-4.61.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0bdcf2e29d65c26299cc3d502f4612365e8b90a939f46cd92d037b6cb7bb544a", size = 4949425, upload-time = "2025-11-28T17:04:50.482Z" },
{ url = "https://files.pythonhosted.org/packages/af/00/acf18c00f6c501bd6e05ee930f926186f8a8e268265407065688820f1c94/fonttools-4.61.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e6cd0d9051b8ddaf7385f99dd82ec2a058e2b46cf1f1961e68e1ff20fcbb61af", size = 4999632, upload-time = "2025-11-28T17:04:52.508Z" },
{ url = "https://files.pythonhosted.org/packages/5f/e0/19a2b86e54109b1d2ee8743c96a1d297238ae03243897bc5345c0365f34d/fonttools-4.61.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e074bc07c31406f45c418e17c1722e83560f181d122c412fa9e815df0ff74810", size = 4939438, upload-time = "2025-11-28T17:04:54.437Z" },
{ url = "https://files.pythonhosted.org/packages/04/35/7b57a5f57d46286360355eff8d6b88c64ab6331107f37a273a71c803798d/fonttools-4.61.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5a9b78da5d5faa17e63b2404b77feeae105c1b7e75f26020ab7a27b76e02039f", size = 5088960, upload-time = "2025-11-28T17:04:56.348Z" },
{ url = "https://files.pythonhosted.org/packages/3e/0e/6c5023eb2e0fe5d1ababc7e221e44acd3ff668781489cc1937a6f83d620a/fonttools-4.61.0-cp312-cp312-win32.whl", hash = "sha256:9821ed77bb676736b88fa87a737c97b6af06e8109667e625a4f00158540ce044", size = 2264404, upload-time = "2025-11-28T17:04:58.149Z" },
{ url = "https://files.pythonhosted.org/packages/36/0b/63273128c7c5df19b1e4cd92e0a1e6ea5bb74a400c4905054c96ad60a675/fonttools-4.61.0-cp312-cp312-win_amd64.whl", hash = "sha256:0011d640afa61053bc6590f9a3394bd222de7cfde19346588beabac374e9d8ac", size = 2314427, upload-time = "2025-11-28T17:04:59.812Z" },
{ url = "https://files.pythonhosted.org/packages/17/45/334f0d7f181e5473cfb757e1b60f4e60e7fc64f28d406e5d364a952718c0/fonttools-4.61.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba774b8cbd8754f54b8eb58124e8bd45f736b2743325ab1a5229698942b9b433", size = 2841801, upload-time = "2025-11-28T17:05:01.621Z" },
{ url = "https://files.pythonhosted.org/packages/cc/63/97b9c78e1f79bc741d4efe6e51f13872d8edb2b36e1b9fb2bab0d4491bb7/fonttools-4.61.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c84b430616ed73ce46e9cafd0bf0800e366a3e02fb7e1ad7c1e214dbe3862b1f", size = 2379024, upload-time = "2025-11-28T17:05:03.668Z" },
{ url = "https://files.pythonhosted.org/packages/4e/80/c87bc524a90dbeb2a390eea23eae448286983da59b7e02c67fa0ca96a8c5/fonttools-4.61.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b2b734d8391afe3c682320840c8191de9bd24e7eb85768dd4dc06ed1b63dbb1b", size = 4923706, upload-time = "2025-11-28T17:05:05.494Z" },
{ url = "https://files.pythonhosted.org/packages/6d/f6/a3b0374811a1de8c3f9207ec88f61ad1bb96f938ed89babae26c065c2e46/fonttools-4.61.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5c5fff72bf31b0e558ed085e4fd7ed96eb85881404ecc39ed2a779e7cf724eb", size = 4979751, upload-time = "2025-11-28T17:05:07.665Z" },
{ url = "https://files.pythonhosted.org/packages/a5/3b/30f63b4308b449091573285f9d27619563a84f399946bca3eadc9554afbe/fonttools-4.61.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:14a290c5c93fcab76b7f451e6a4b7721b712d90b3b5ed6908f1abcf794e90d6d", size = 4921113, upload-time = "2025-11-28T17:05:09.551Z" },
{ url = "https://files.pythonhosted.org/packages/41/6c/58e6e9b7d9d8bf2d7010bd7bb493060b39b02a12d1cda64a8bfb116ce760/fonttools-4.61.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:13e3e20a5463bfeb77b3557d04b30bd6a96a6bb5c15c7b2e7908903e69d437a0", size = 5063183, upload-time = "2025-11-28T17:05:11.677Z" },
{ url = "https://files.pythonhosted.org/packages/3f/e3/52c790ab2b07492df059947a1fd7778e105aac5848c0473029a4d20481a2/fonttools-4.61.0-cp313-cp313-win32.whl", hash = "sha256:6781e7a4bb010be1cd69a29927b0305c86b843395f2613bdabe115f7d6ea7f34", size = 2263159, upload-time = "2025-11-28T17:05:13.292Z" },
{ url = "https://files.pythonhosted.org/packages/e9/1f/116013b200fbeba871046554d5d2a45fefa69a05c40e9cdfd0d4fff53edc/fonttools-4.61.0-cp313-cp313-win_amd64.whl", hash = "sha256:c53b47834ae41e8e4829171cc44fec0fdf125545a15f6da41776b926b9645a9a", size = 2313530, upload-time = "2025-11-28T17:05:14.848Z" },
{ url = "https://files.pythonhosted.org/packages/d3/99/59b1e25987787cb714aa9457cee4c9301b7c2153f0b673e2b8679d37669d/fonttools-4.61.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:96dfc9bc1f2302224e48e6ee37e656eddbab810b724b52e9d9c13a57a6abad01", size = 2841429, upload-time = "2025-11-28T17:05:16.671Z" },
{ url = "https://files.pythonhosted.org/packages/2b/b2/4c1911d4332c8a144bb3b44416e274ccca0e297157c971ea1b3fbb855590/fonttools-4.61.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3b2065d94e5d63aafc2591c8b6ccbdb511001d9619f1bca8ad39b745ebeb5efa", size = 2378987, upload-time = "2025-11-28T17:05:18.69Z" },
{ url = "https://files.pythonhosted.org/packages/24/b0/f442e90fde5d2af2ae0cb54008ab6411edc557ee33b824e13e1d04925ac9/fonttools-4.61.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e0d87e81e4d869549585ba0beb3f033718501c1095004f5e6aef598d13ebc216", size = 4873270, upload-time = "2025-11-28T17:05:20.625Z" },
{ url = "https://files.pythonhosted.org/packages/bb/04/f5d5990e33053c8a59b90b1d7e10ad9b97a73f42c745304da0e709635fab/fonttools-4.61.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cfa2eb9bae650e58f0e8ad53c49d19a844d6034d6b259f30f197238abc1ccee", size = 4968270, upload-time = "2025-11-28T17:05:22.515Z" },
{ url = "https://files.pythonhosted.org/packages/94/9f/2091402e0d27c9c8c4bab5de0e5cd146d9609a2d7d1c666bbb75c0011c1a/fonttools-4.61.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4238120002e68296d55e091411c09eab94e111c8ce64716d17df53fd0eb3bb3d", size = 4919799, upload-time = "2025-11-28T17:05:24.437Z" },
{ url = "https://files.pythonhosted.org/packages/a8/72/86adab22fde710b829f8ffbc8f264df01928e5b7a8f6177fa29979ebf256/fonttools-4.61.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b6ceac262cc62bec01b3bb59abccf41b24ef6580869e306a4e88b7e56bb4bdda", size = 5030966, upload-time = "2025-11-28T17:05:26.115Z" },
{ url = "https://files.pythonhosted.org/packages/e8/a7/7c8e31b003349e845b853f5e0a67b95ff6b052fa4f5224f8b72624f5ac69/fonttools-4.61.0-cp314-cp314-win32.whl", hash = "sha256:adbb4ecee1a779469a77377bbe490565effe8fce6fb2e6f95f064de58f8bac85", size = 2267243, upload-time = "2025-11-28T17:05:27.807Z" },
{ url = "https://files.pythonhosted.org/packages/20/ee/f434fe7749360497c52b7dcbcfdbccdaab0a71c59f19d572576066717122/fonttools-4.61.0-cp314-cp314-win_amd64.whl", hash = "sha256:02bdf8e04d1a70476564b8640380f04bb4ac74edc1fc71f1bacb840b3e398ee9", size = 2318822, upload-time = "2025-11-28T17:05:29.882Z" },
{ url = "https://files.pythonhosted.org/packages/33/b3/c16255320255e5c1863ca2b2599bb61a46e2f566db0bbb9948615a8fe692/fonttools-4.61.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:627216062d90ab0d98215176d8b9562c4dd5b61271d35f130bcd30f6a8aaa33a", size = 2924917, upload-time = "2025-11-28T17:05:31.46Z" },
{ url = "https://files.pythonhosted.org/packages/e2/b8/08067ae21de705a817777c02ef36ab0b953cbe91d8adf134f9c2da75ed6d/fonttools-4.61.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7b446623c9cd5f14a59493818eaa80255eec2468c27d2c01b56e05357c263195", size = 2413576, upload-time = "2025-11-28T17:05:33.343Z" },
{ url = "https://files.pythonhosted.org/packages/42/f1/96ff43f92addce2356780fdc203f2966206f3d22ea20e242c27826fd7442/fonttools-4.61.0-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:70e2a0c0182ee75e493ef33061bfebf140ea57e035481d2f95aa03b66c7a0e05", size = 4877447, upload-time = "2025-11-28T17:05:35.278Z" },
{ url = "https://files.pythonhosted.org/packages/d0/1e/a3d8e51ed9ccfd7385e239ae374b78d258a0fb82d82cab99160a014a45d1/fonttools-4.61.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9064b0f55b947e929ac669af5311ab1f26f750214db6dd9a0c97e091e918f486", size = 5095681, upload-time = "2025-11-28T17:05:37.142Z" },
{ url = "https://files.pythonhosted.org/packages/eb/f6/d256bd6c1065c146a0bdddf1c62f542e08ae5b3405dbf3fcc52be272f674/fonttools-4.61.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cb5e45a824ce14b90510024d0d39dae51bd4fbb54c42a9334ea8c8cf4d95cbe", size = 4974140, upload-time = "2025-11-28T17:05:39.5Z" },
{ url = "https://files.pythonhosted.org/packages/5d/0c/96633eb4b26f138cc48561c6e0c44b4ea48acea56b20b507d6b14f8e80ce/fonttools-4.61.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e5ca8c62efdec7972dfdfd454415c4db49b89aeaefaaacada432f3b7eea9866", size = 5001741, upload-time = "2025-11-28T17:05:41.424Z" },
{ url = "https://files.pythonhosted.org/packages/6f/9a/3b536bad3be4f26186f296e749ff17bad3e6d57232c104d752d24b2e265b/fonttools-4.61.0-cp314-cp314t-win32.whl", hash = "sha256:63c7125d31abe3e61d7bb917329b5543c5b3448db95f24081a13aaf064360fc8", size = 2330707, upload-time = "2025-11-28T17:05:43.548Z" },
{ url = "https://files.pythonhosted.org/packages/18/ea/e6b9ac610451ee9f04477c311ad126de971f6112cb579fa391d2a8edb00b/fonttools-4.61.0-cp314-cp314t-win_amd64.whl", hash = "sha256:67d841aa272be5500de7f447c40d1d8452783af33b4c3599899319f6ef9ad3c1", size = 2395950, upload-time = "2025-11-28T17:05:45.638Z" },
{ url = "https://files.pythonhosted.org/packages/0c/14/634f7daea5ffe6a5f7a0322ba8e1a0e23c9257b80aa91458107896d1dfc7/fonttools-4.61.0-py3-none-any.whl", hash = "sha256:276f14c560e6f98d24ef7f5f44438e55ff5a67f78fa85236b218462c9f5d0635", size = 1144485, upload-time = "2025-11-28T17:05:47.573Z" },
]
[[package]]
name = "gridlock"
source = { editable = "../gridlock" }
dependencies = [
{ name = "float-raster" },
{ name = "numpy" },
]
[package.metadata]
requires-dist = [
{ name = "float-raster", specifier = ">=0.8" },
{ name = "matplotlib", marker = "extra == 'visualization'" },
{ name = "matplotlib", marker = "extra == 'visualization-isosurface'" },
{ name = "mpl-toolkits", marker = "extra == 'visualization-isosurface'" },
{ name = "numpy", specifier = ">=1.26" },
{ name = "skimage", marker = "extra == 'visualization-isosurface'", specifier = ">=0.13" },
]
provides-extras = ["visualization", "visualization-isosurface"]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]]
name = "kiwisolver"
version = "1.4.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" },
{ url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" },
{ url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" },
{ url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" },
{ url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" },
{ url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" },
{ url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" },
{ url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" },
{ url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" },
{ url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" },
{ url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" },
{ url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" },
{ url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" },
{ url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" },
{ url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" },
{ url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" },
{ url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" },
{ url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" },
{ url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" },
{ url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" },
{ url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" },
{ url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" },
{ url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" },
{ url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" },
{ url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" },
{ url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" },
{ url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" },
{ url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" },
{ url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" },
{ url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" },
{ url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" },
{ url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" },
{ url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" },
{ url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" },
{ url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" },
{ url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" },
{ url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" },
{ url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" },
{ url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" },
{ url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" },
{ url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" },
{ url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" },
{ url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" },
{ url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" },
{ url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" },
{ url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" },
{ url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" },
{ url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" },
{ url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" },
{ url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" },
{ url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" },
{ url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" },
{ url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" },
{ url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" },
{ url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" },
{ url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" },
{ url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" },
{ url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" },
{ url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" },
{ url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" },
{ url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" },
{ url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" },
{ url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" },
{ url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" },
{ url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" },
{ url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" },
{ url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" },
{ url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" },
{ url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" },
{ url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" },
{ url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" },
{ url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" },
{ url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" },
{ url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" },
{ url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" },
{ url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" },
{ url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" },
{ url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" },
{ url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" },
{ url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" },
{ url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" },
{ url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" },
]
[[package]]
name = "markdown2"
version = "2.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/42/f8/b2ae8bf5f28f9b510ae097415e6e4cb63226bb28d7ee01aec03a755ba03b/markdown2-2.5.4.tar.gz", hash = "sha256:a09873f0b3c23dbfae589b0080587df52ad75bb09a5fa6559147554736676889", size = 145652, upload-time = "2025-07-27T16:16:24.307Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/06/2697b5043c3ecb720ce0d243fc7cf5024c0b5b1e450506e9b21939019963/markdown2-2.5.4-py3-none-any.whl", hash = "sha256:3c4b2934e677be7fec0e6f2de4410e116681f4ad50ec8e5ba7557be506d3f439", size = 49954, upload-time = "2025-07-27T16:16:23.026Z" },
]
[[package]]
name = "markupsafe"
version = "3.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
{ url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
{ url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
{ url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
{ url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
{ url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
{ url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
{ url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
{ url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
{ url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
{ url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
{ url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
{ url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
{ url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
{ url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
{ url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
{ url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
{ url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
]
[[package]]
name = "matplotlib"
version = "3.10.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "contourpy" },
{ name = "cycler" },
{ name = "fonttools" },
{ name = "kiwisolver" },
{ name = "numpy" },
{ name = "packaging" },
{ name = "pillow" },
{ name = "pyparsing" },
{ name = "python-dateutil" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" },
{ url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" },
{ url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" },
{ url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" },
{ url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" },
{ url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" },
{ url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" },
{ url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" },
{ url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" },
{ url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" },
{ url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" },
{ url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" },
{ url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" },
{ url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" },
{ url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" },
{ url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" },
{ url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" },
{ url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" },
{ url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" },
{ url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" },
{ url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" },
{ url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" },
{ url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" },
{ url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" },
{ url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" },
{ url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" },
{ url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" },
{ url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" },
{ url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" },
{ url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" },
{ url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" },
{ url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" },
{ url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" },
{ url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" },
{ url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" },
{ url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" },
{ url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" },
{ url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" },
{ url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" },
{ url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" },
{ url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" },
{ url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" },
{ url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" },
{ url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" },
{ url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" },
]
[[package]]
name = "meanas"
source = { editable = "." }
dependencies = [
{ name = "gridlock" },
{ name = "numpy" },
{ name = "scipy" },
]
[package.optional-dependencies]
dev = [
{ name = "gridlock" },
{ name = "pdoc" },
{ name = "pytest" },
]
examples = [
{ name = "gridlock" },
{ name = "matplotlib" },
]
test = [
{ name = "pytest" },
]
[package.metadata]
requires-dist = [
{ name = "gridlock", editable = "../gridlock" },
{ name = "gridlock", marker = "extra == 'dev'", editable = "../gridlock" },
{ name = "gridlock", marker = "extra == 'examples'", editable = "../gridlock" },
{ name = "matplotlib", marker = "extra == 'examples'", specifier = ">=3.10.8" },
{ name = "numpy", specifier = ">=2.0" },
{ name = "pdoc", marker = "extra == 'dev'" },
{ name = "pytest", marker = "extra == 'dev'" },
{ name = "pytest", marker = "extra == 'test'" },
{ name = "scipy", specifier = "~=1.14" },
]
provides-extras = ["dev", "examples", "test"]
[[package]]
name = "numpy"
version = "2.3.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" },
{ url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" },
{ url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" },
{ url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" },
{ url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" },
{ url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" },
{ url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" },
{ url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" },
{ url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" },
{ url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" },
{ url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" },
{ url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" },
{ url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" },
{ url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" },
{ url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" },
{ url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" },
{ url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" },
{ url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" },
{ url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" },
{ url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" },
{ url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" },
{ url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" },
{ url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" },
{ url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" },
{ url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" },
{ url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" },
{ url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" },
{ url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" },
{ url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" },
{ url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" },
{ url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" },
{ url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" },
{ url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" },
{ url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" },
{ url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" },
{ url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" },
{ url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" },
{ url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" },
{ url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" },
{ url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" },
{ url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" },
{ url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" },
{ url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" },
{ url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" },
{ url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" },
{ url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" },
{ url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" },
{ url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" },
{ url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" },
{ url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" },
{ url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" },
{ url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" },
{ url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" },
{ url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" },
{ url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" },
{ url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" },
{ url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" },
{ url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" },
{ url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" },
{ url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" },
{ url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" },
{ url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" },
{ url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" },
{ url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" },
{ url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" },
{ url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" },
{ url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" },
{ url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" },
{ url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" },
{ url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" },
{ url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" },
{ url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" },
{ url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "pdoc"
version = "16.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jinja2" },
{ name = "markdown2" },
{ name = "markupsafe" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ac/fe/ab3f34a5fb08c6b698439a2c2643caf8fef0d61a86dd3fdcd5501c670ab8/pdoc-16.0.0.tar.gz", hash = "sha256:fdadc40cc717ec53919e3cd720390d4e3bcd40405cb51c4918c119447f913514", size = 111890, upload-time = "2025-10-27T16:02:16.345Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/16/a1/56a17b7f9e18c2bb8df73f3833345d97083b344708b97bab148fdd7e0b82/pdoc-16.0.0-py3-none-any.whl", hash = "sha256:070b51de2743b9b1a4e0ab193a06c9e6c12cf4151cf9137656eebb16e8556628", size = 100014, upload-time = "2025-10-27T16:02:15.007Z" },
]
[[package]]
name = "pillow"
version = "12.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/5a/a2f6773b64edb921a756eb0729068acad9fc5208a53f4a349396e9436721/pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", size = 5289798, upload-time = "2025-10-15T18:21:47.763Z" },
{ url = "https://files.pythonhosted.org/packages/2e/05/069b1f8a2e4b5a37493da6c5868531c3f77b85e716ad7a590ef87d58730d/pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", size = 4650589, upload-time = "2025-10-15T18:21:49.515Z" },
{ url = "https://files.pythonhosted.org/packages/61/e3/2c820d6e9a36432503ead175ae294f96861b07600a7156154a086ba7111a/pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", size = 6230472, upload-time = "2025-10-15T18:21:51.052Z" },
{ url = "https://files.pythonhosted.org/packages/4f/89/63427f51c64209c5e23d4d52071c8d0f21024d3a8a487737caaf614a5795/pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", size = 8033887, upload-time = "2025-10-15T18:21:52.604Z" },
{ url = "https://files.pythonhosted.org/packages/f6/1b/c9711318d4901093c15840f268ad649459cd81984c9ec9887756cca049a5/pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", size = 6343964, upload-time = "2025-10-15T18:21:54.619Z" },
{ url = "https://files.pythonhosted.org/packages/41/1e/db9470f2d030b4995083044cd8738cdd1bf773106819f6d8ba12597d5352/pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", size = 7034756, upload-time = "2025-10-15T18:21:56.151Z" },
{ url = "https://files.pythonhosted.org/packages/cc/b0/6177a8bdd5ee4ed87cba2de5a3cc1db55ffbbec6176784ce5bb75aa96798/pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", size = 6458075, upload-time = "2025-10-15T18:21:57.759Z" },
{ url = "https://files.pythonhosted.org/packages/bc/5e/61537aa6fa977922c6a03253a0e727e6e4a72381a80d63ad8eec350684f2/pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", size = 7125955, upload-time = "2025-10-15T18:21:59.372Z" },
{ url = "https://files.pythonhosted.org/packages/1f/3d/d5033539344ee3cbd9a4d69e12e63ca3a44a739eb2d4c8da350a3d38edd7/pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", size = 6298440, upload-time = "2025-10-15T18:22:00.982Z" },
{ url = "https://files.pythonhosted.org/packages/4d/42/aaca386de5cc8bd8a0254516957c1f265e3521c91515b16e286c662854c4/pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", size = 6999256, upload-time = "2025-10-15T18:22:02.617Z" },
{ url = "https://files.pythonhosted.org/packages/ba/f1/9197c9c2d5708b785f631a6dfbfa8eb3fb9672837cb92ae9af812c13b4ed/pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", size = 2436025, upload-time = "2025-10-15T18:22:04.598Z" },
{ url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" },
{ url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" },
{ url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" },
{ url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" },
{ url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" },
{ url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" },
{ url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" },
{ url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" },
{ url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" },
{ url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" },
{ url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" },
{ url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" },
{ url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" },
{ url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" },
{ url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" },
{ url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" },
{ url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" },
{ url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" },
{ url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" },
{ url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" },
{ url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" },
{ url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" },
{ url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" },
{ url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" },
{ url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" },
{ url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" },
{ url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" },
{ url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" },
{ url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" },
{ url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" },
{ url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" },
{ url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" },
{ url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" },
{ url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" },
{ url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" },
{ url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" },
{ url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" },
{ url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" },
{ url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" },
{ url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" },
{ url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" },
{ url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" },
{ url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" },
{ url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" },
{ url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" },
{ url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" },
{ url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" },
{ url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" },
{ url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" },
{ url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" },
{ url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" },
{ url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" },
{ url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" },
{ url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" },
{ url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" },
{ url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" },
{ url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" },
{ url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" },
{ url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" },
{ url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" },
{ url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" },
{ url = "https://files.pythonhosted.org/packages/1d/b3/582327e6c9f86d037b63beebe981425d6811104cb443e8193824ef1a2f27/pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", size = 5215068, upload-time = "2025-10-15T18:23:59.594Z" },
{ url = "https://files.pythonhosted.org/packages/fd/d6/67748211d119f3b6540baf90f92fae73ae51d5217b171b0e8b5f7e5d558f/pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", size = 4614994, upload-time = "2025-10-15T18:24:01.669Z" },
{ url = "https://files.pythonhosted.org/packages/2d/e1/f8281e5d844c41872b273b9f2c34a4bf64ca08905668c8ae730eedc7c9fa/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", size = 5246639, upload-time = "2025-10-15T18:24:03.403Z" },
{ url = "https://files.pythonhosted.org/packages/94/5a/0d8ab8ffe8a102ff5df60d0de5af309015163bf710c7bb3e8311dd3b3ad0/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", size = 6986839, upload-time = "2025-10-15T18:24:05.344Z" },
{ url = "https://files.pythonhosted.org/packages/20/2e/3434380e8110b76cd9eb00a363c484b050f949b4bbe84ba770bb8508a02c/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", size = 5313505, upload-time = "2025-10-15T18:24:07.137Z" },
{ url = "https://files.pythonhosted.org/packages/57/ca/5a9d38900d9d74785141d6580950fe705de68af735ff6e727cb911b64740/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", size = 5963654, upload-time = "2025-10-15T18:24:09.579Z" },
{ url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pyparsing"
version = "3.2.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" },
]
[[package]]
name = "pytest"
version = "9.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]]
name = "scipy"
version = "1.16.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/5f/6f37d7439de1455ce9c5a556b8d1db0979f03a796c030bafdf08d35b7bf9/scipy-1.16.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:40be6cf99e68b6c4321e9f8782e7d5ff8265af28ef2cd56e9c9b2638fa08ad97", size = 36630881, upload-time = "2025-10-28T17:31:47.104Z" },
{ url = "https://files.pythonhosted.org/packages/7c/89/d70e9f628749b7e4db2aa4cd89735502ff3f08f7b9b27d2e799485987cd9/scipy-1.16.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:8be1ca9170fcb6223cc7c27f4305d680ded114a1567c0bd2bfcbf947d1b17511", size = 28941012, upload-time = "2025-10-28T17:31:53.411Z" },
{ url = "https://files.pythonhosted.org/packages/a8/a8/0e7a9a6872a923505dbdf6bb93451edcac120363131c19013044a1e7cb0c/scipy-1.16.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bea0a62734d20d67608660f69dcda23e7f90fb4ca20974ab80b6ed40df87a005", size = 20931935, upload-time = "2025-10-28T17:31:57.361Z" },
{ url = "https://files.pythonhosted.org/packages/bd/c7/020fb72bd79ad798e4dbe53938543ecb96b3a9ac3fe274b7189e23e27353/scipy-1.16.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:2a207a6ce9c24f1951241f4693ede2d393f59c07abc159b2cb2be980820e01fb", size = 23534466, upload-time = "2025-10-28T17:32:01.875Z" },
{ url = "https://files.pythonhosted.org/packages/be/a0/668c4609ce6dbf2f948e167836ccaf897f95fb63fa231c87da7558a374cd/scipy-1.16.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:532fb5ad6a87e9e9cd9c959b106b73145a03f04c7d57ea3e6f6bb60b86ab0876", size = 33593618, upload-time = "2025-10-28T17:32:06.902Z" },
{ url = "https://files.pythonhosted.org/packages/ca/6e/8942461cf2636cdae083e3eb72622a7fbbfa5cf559c7d13ab250a5dbdc01/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2", size = 35899798, upload-time = "2025-10-28T17:32:12.665Z" },
{ url = "https://files.pythonhosted.org/packages/79/e8/d0f33590364cdbd67f28ce79368b373889faa4ee959588beddf6daef9abe/scipy-1.16.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7180967113560cca57418a7bc719e30366b47959dd845a93206fbed693c867e", size = 36226154, upload-time = "2025-10-28T17:32:17.961Z" },
{ url = "https://files.pythonhosted.org/packages/39/c1/1903de608c0c924a1749c590064e65810f8046e437aba6be365abc4f7557/scipy-1.16.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:deb3841c925eeddb6afc1e4e4a45e418d19ec7b87c5df177695224078e8ec733", size = 38878540, upload-time = "2025-10-28T17:32:23.907Z" },
{ url = "https://files.pythonhosted.org/packages/f1/d0/22ec7036ba0b0a35bccb7f25ab407382ed34af0b111475eb301c16f8a2e5/scipy-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:53c3844d527213631e886621df5695d35e4f6a75f620dca412bcd292f6b87d78", size = 38722107, upload-time = "2025-10-28T17:32:29.921Z" },
{ url = "https://files.pythonhosted.org/packages/7b/60/8a00e5a524bb3bf8898db1650d350f50e6cffb9d7a491c561dc9826c7515/scipy-1.16.3-cp311-cp311-win_arm64.whl", hash = "sha256:9452781bd879b14b6f055b26643703551320aa8d79ae064a71df55c00286a184", size = 25506272, upload-time = "2025-10-28T17:32:34.577Z" },
{ url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" },
{ url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" },
{ url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" },
{ url = "https://files.pythonhosted.org/packages/80/35/178d9d0c35394d5d5211bbff7ac4f2986c5488b59506fef9e1de13ea28d3/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795, upload-time = "2025-10-28T17:32:53.337Z" },
{ url = "https://files.pythonhosted.org/packages/fa/46/d1146ff536d034d02f83c8afc3c4bab2eddb634624d6529a8512f3afc9da/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476, upload-time = "2025-10-28T17:32:58.353Z" },
{ url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" },
{ url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" },
{ url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" },
{ url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" },
{ url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" },
{ url = "https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:d2ec56337675e61b312179a1ad124f5f570c00f920cc75e1000025451b88241c", size = 36617856, upload-time = "2025-10-28T17:33:31.375Z" },
{ url = "https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d", size = 28874306, upload-time = "2025-10-28T17:33:36.516Z" },
{ url = "https://files.pythonhosted.org/packages/15/65/3a9400efd0228a176e6ec3454b1fa998fbbb5a8defa1672c3f65706987db/scipy-1.16.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5803c5fadd29de0cf27fa08ccbfe7a9e5d741bf63e4ab1085437266f12460ff9", size = 20865371, upload-time = "2025-10-28T17:33:42.094Z" },
{ url = "https://files.pythonhosted.org/packages/33/d7/eda09adf009a9fb81827194d4dd02d2e4bc752cef16737cc4ef065234031/scipy-1.16.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b81c27fc41954319a943d43b20e07c40bdcd3ff7cf013f4fb86286faefe546c4", size = 23524877, upload-time = "2025-10-28T17:33:48.483Z" },
{ url = "https://files.pythonhosted.org/packages/7d/6b/3f911e1ebc364cb81320223a3422aab7d26c9c7973109a9cd0f27c64c6c0/scipy-1.16.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0c3b4dd3d9b08dbce0f3440032c52e9e2ab9f96ade2d3943313dfe51a7056959", size = 33342103, upload-time = "2025-10-28T17:33:56.495Z" },
{ url = "https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88", size = 35697297, upload-time = "2025-10-28T17:34:04.722Z" },
{ url = "https://files.pythonhosted.org/packages/04/e1/6496dadbc80d8d896ff72511ecfe2316b50313bfc3ebf07a3f580f08bd8c/scipy-1.16.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:663b8d66a8748051c3ee9c96465fb417509315b99c71550fda2591d7dd634234", size = 36021756, upload-time = "2025-10-28T17:34:13.482Z" },
{ url = "https://files.pythonhosted.org/packages/fe/bd/a8c7799e0136b987bda3e1b23d155bcb31aec68a4a472554df5f0937eef7/scipy-1.16.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eab43fae33a0c39006a88096cd7b4f4ef545ea0447d250d5ac18202d40b6611d", size = 38696566, upload-time = "2025-10-28T17:34:22.384Z" },
{ url = "https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", size = 38529877, upload-time = "2025-10-28T17:35:51.076Z" },
{ url = "https://files.pythonhosted.org/packages/7f/14/9d9fbcaa1260a94f4bb5b64ba9213ceb5d03cd88841fe9fd1ffd47a45b73/scipy-1.16.3-cp313-cp313-win_arm64.whl", hash = "sha256:50a3dbf286dbc7d84f176f9a1574c705f277cb6565069f88f60db9eafdbe3ee2", size = 25455366, upload-time = "2025-10-28T17:35:59.014Z" },
{ url = "https://files.pythonhosted.org/packages/e2/a3/9ec205bd49f42d45d77f1730dbad9ccf146244c1647605cf834b3a8c4f36/scipy-1.16.3-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b", size = 37027931, upload-time = "2025-10-28T17:34:31.451Z" },
{ url = "https://files.pythonhosted.org/packages/25/06/ca9fd1f3a4589cbd825b1447e5db3a8ebb969c1eaf22c8579bd286f51b6d/scipy-1.16.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:8d09d72dc92742988b0e7750bddb8060b0c7079606c0d24a8cc8e9c9c11f9079", size = 29400081, upload-time = "2025-10-28T17:34:39.087Z" },
{ url = "https://files.pythonhosted.org/packages/6a/56/933e68210d92657d93fb0e381683bc0e53a965048d7358ff5fbf9e6a1b17/scipy-1.16.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:03192a35e661470197556de24e7cb1330d84b35b94ead65c46ad6f16f6b28f2a", size = 21391244, upload-time = "2025-10-28T17:34:45.234Z" },
{ url = "https://files.pythonhosted.org/packages/a8/7e/779845db03dc1418e215726329674b40576879b91814568757ff0014ad65/scipy-1.16.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:57d01cb6f85e34f0946b33caa66e892aae072b64b034183f3d87c4025802a119", size = 23929753, upload-time = "2025-10-28T17:34:51.793Z" },
{ url = "https://files.pythonhosted.org/packages/4c/4b/f756cf8161d5365dcdef9e5f460ab226c068211030a175d2fc7f3f41ca64/scipy-1.16.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96491a6a54e995f00a28a3c3badfff58fd093bf26cd5fb34a2188c8c756a3a2c", size = 33496912, upload-time = "2025-10-28T17:34:59.8Z" },
{ url = "https://files.pythonhosted.org/packages/09/b5/222b1e49a58668f23839ca1542a6322bb095ab8d6590d4f71723869a6c2c/scipy-1.16.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cd13e354df9938598af2be05822c323e97132d5e6306b83a3b4ee6724c6e522e", size = 35802371, upload-time = "2025-10-28T17:35:08.173Z" },
{ url = "https://files.pythonhosted.org/packages/c1/8d/5964ef68bb31829bde27611f8c9deeac13764589fe74a75390242b64ca44/scipy-1.16.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63d3cdacb8a824a295191a723ee5e4ea7768ca5ca5f2838532d9f2e2b3ce2135", size = 36190477, upload-time = "2025-10-28T17:35:16.7Z" },
{ url = "https://files.pythonhosted.org/packages/ab/f2/b31d75cb9b5fa4dd39a0a931ee9b33e7f6f36f23be5ef560bf72e0f92f32/scipy-1.16.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e7efa2681ea410b10dde31a52b18b0154d66f2485328830e45fdf183af5aefc6", size = 38796678, upload-time = "2025-10-28T17:35:26.354Z" },
{ url = "https://files.pythonhosted.org/packages/b4/1e/b3723d8ff64ab548c38d87055483714fefe6ee20e0189b62352b5e015bb1/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178, upload-time = "2025-10-28T17:35:35.304Z" },
{ url = "https://files.pythonhosted.org/packages/8e/f3/d854ff38789aca9b0cc23008d607ced9de4f7ab14fa1ca4329f86b3758ca/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246, upload-time = "2025-10-28T17:35:42.155Z" },
{ url = "https://files.pythonhosted.org/packages/99/f6/99b10fd70f2d864c1e29a28bbcaa0c6340f9d8518396542d9ea3b4aaae15/scipy-1.16.3-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:875555ce62743e1d54f06cdf22c1e0bc47b91130ac40fe5d783b6dfa114beeb6", size = 36606469, upload-time = "2025-10-28T17:36:08.741Z" },
{ url = "https://files.pythonhosted.org/packages/4d/74/043b54f2319f48ea940dd025779fa28ee360e6b95acb7cd188fad4391c6b/scipy-1.16.3-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bb61878c18a470021fb515a843dc7a76961a8daceaaaa8bad1332f1bf4b54657", size = 28872043, upload-time = "2025-10-28T17:36:16.599Z" },
{ url = "https://files.pythonhosted.org/packages/4d/e1/24b7e50cc1c4ee6ffbcb1f27fe9f4c8b40e7911675f6d2d20955f41c6348/scipy-1.16.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2622206f5559784fa5c4b53a950c3c7c1cf3e84ca1b9c4b6c03f062f289ca26", size = 20862952, upload-time = "2025-10-28T17:36:22.966Z" },
{ url = "https://files.pythonhosted.org/packages/dd/3a/3e8c01a4d742b730df368e063787c6808597ccb38636ed821d10b39ca51b/scipy-1.16.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7f68154688c515cdb541a31ef8eb66d8cd1050605be9dcd74199cbd22ac739bc", size = 23508512, upload-time = "2025-10-28T17:36:29.731Z" },
{ url = "https://files.pythonhosted.org/packages/1f/60/c45a12b98ad591536bfe5330cb3cfe1850d7570259303563b1721564d458/scipy-1.16.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3c820ddb80029fe9f43d61b81d8b488d3ef8ca010d15122b152db77dc94c22", size = 33413639, upload-time = "2025-10-28T17:36:37.982Z" },
{ url = "https://files.pythonhosted.org/packages/71/bc/35957d88645476307e4839712642896689df442f3e53b0fa016ecf8a3357/scipy-1.16.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3837938ae715fc0fe3c39c0202de3a8853aff22ca66781ddc2ade7554b7e2cc", size = 35704729, upload-time = "2025-10-28T17:36:46.547Z" },
{ url = "https://files.pythonhosted.org/packages/3b/15/89105e659041b1ca11c386e9995aefacd513a78493656e57789f9d9eab61/scipy-1.16.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aadd23f98f9cb069b3bd64ddc900c4d277778242e961751f77a8cb5c4b946fb0", size = 36086251, upload-time = "2025-10-28T17:36:55.161Z" },
{ url = "https://files.pythonhosted.org/packages/1a/87/c0ea673ac9c6cc50b3da2196d860273bc7389aa69b64efa8493bdd25b093/scipy-1.16.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b7c5f1bda1354d6a19bc6af73a649f8285ca63ac6b52e64e658a5a11d4d69800", size = 38716681, upload-time = "2025-10-28T17:37:04.1Z" },
{ url = "https://files.pythonhosted.org/packages/91/06/837893227b043fb9b0d13e4bd7586982d8136cb249ffb3492930dab905b8/scipy-1.16.3-cp314-cp314-win_amd64.whl", hash = "sha256:e5d42a9472e7579e473879a1990327830493a7047506d58d73fc429b84c1d49d", size = 39358423, upload-time = "2025-10-28T17:38:20.005Z" },
{ url = "https://files.pythonhosted.org/packages/95/03/28bce0355e4d34a7c034727505a02d19548549e190bedd13a721e35380b7/scipy-1.16.3-cp314-cp314-win_arm64.whl", hash = "sha256:6020470b9d00245926f2d5bb93b119ca0340f0d564eb6fbaad843eaebf9d690f", size = 26135027, upload-time = "2025-10-28T17:38:24.966Z" },
{ url = "https://files.pythonhosted.org/packages/b2/6f/69f1e2b682efe9de8fe9f91040f0cd32f13cfccba690512ba4c582b0bc29/scipy-1.16.3-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:e1d27cbcb4602680a49d787d90664fa4974063ac9d4134813332a8c53dbe667c", size = 37028379, upload-time = "2025-10-28T17:37:14.061Z" },
{ url = "https://files.pythonhosted.org/packages/7c/2d/e826f31624a5ebbab1cd93d30fd74349914753076ed0593e1d56a98c4fb4/scipy-1.16.3-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9b9c9c07b6d56a35777a1b4cc8966118fb16cfd8daf6743867d17d36cfad2d40", size = 29400052, upload-time = "2025-10-28T17:37:21.709Z" },
{ url = "https://files.pythonhosted.org/packages/69/27/d24feb80155f41fd1f156bf144e7e049b4e2b9dd06261a242905e3bc7a03/scipy-1.16.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:3a4c460301fb2cffb7f88528f30b3127742cff583603aa7dc964a52c463b385d", size = 21391183, upload-time = "2025-10-28T17:37:29.559Z" },
{ url = "https://files.pythonhosted.org/packages/f8/d3/1b229e433074c5738a24277eca520a2319aac7465eea7310ea6ae0e98ae2/scipy-1.16.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f667a4542cc8917af1db06366d3f78a5c8e83badd56409f94d1eac8d8d9133fa", size = 23930174, upload-time = "2025-10-28T17:37:36.306Z" },
{ url = "https://files.pythonhosted.org/packages/16/9d/d9e148b0ec680c0f042581a2be79a28a7ab66c0c4946697f9e7553ead337/scipy-1.16.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f379b54b77a597aa7ee5e697df0d66903e41b9c85a6dd7946159e356319158e8", size = 33497852, upload-time = "2025-10-28T17:37:42.228Z" },
{ url = "https://files.pythonhosted.org/packages/2f/22/4e5f7561e4f98b7bea63cf3fd7934bff1e3182e9f1626b089a679914d5c8/scipy-1.16.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4aff59800a3b7f786b70bfd6ab551001cb553244988d7d6b8299cb1ea653b353", size = 35798595, upload-time = "2025-10-28T17:37:48.102Z" },
{ url = "https://files.pythonhosted.org/packages/83/42/6644d714c179429fc7196857866f219fef25238319b650bb32dde7bf7a48/scipy-1.16.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:da7763f55885045036fabcebd80144b757d3db06ab0861415d1c3b7c69042146", size = 36186269, upload-time = "2025-10-28T17:37:53.72Z" },
{ url = "https://files.pythonhosted.org/packages/ac/70/64b4d7ca92f9cf2e6fc6aaa2eecf80bb9b6b985043a9583f32f8177ea122/scipy-1.16.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa6eea95283b2b8079b821dc11f50a17d0571c92b43e2b5b12764dc5f9b285d", size = 38802779, upload-time = "2025-10-28T17:37:59.393Z" },
{ url = "https://files.pythonhosted.org/packages/61/82/8d0e39f62764cce5ffd5284131e109f07cf8955aef9ab8ed4e3aa5e30539/scipy-1.16.3-cp314-cp314t-win_amd64.whl", hash = "sha256:d9f48cafc7ce94cf9b15c6bffdc443a81a27bf7075cf2dcd5c8b40f85d10c4e7", size = 39471128, upload-time = "2025-10-28T17:38:05.259Z" },
{ url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127, upload-time = "2025-10-28T17:38:11.34Z" },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]