diff --git a/build-app b/build-app deleted file mode 100755 index 033a24f..0000000 --- a/build-app +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env nu - -rm build/dispersionapp.zip -^zip -r build/dispersionapp.zip src/dispersionapp/*.py LICENSE pyproject.toml README.md -scp -O build/dispersionapp.zip fibnas:/volume1/web/ diff --git a/pyproject.toml b/pyproject.toml index 13d6e62..53a2b8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,17 +1,19 @@ [project] name = "dispersionapp" -version = "0.2.0" +version = "0.2.2" description = "Model hollow capillary and revolver fiber interactively" -authors = [{ name = "Benoît Sierro", email = "benoit.sierro@unibe.ch" }] +authors = [{ name = "Dedebenui", email = "dedebenui@protonmail.com" }] +requires-python = ">=3.13" dependencies = [ - "scgenerator @ git+https://github.com/bsierro/scgenerator.git", "click", - "pydantic < 2", + "pydantic >= 2", "tomli", "tomli_w", "PySide6 >= 6.4.0", "pyqtgraph >= 0.13.1", + "numba>=0.62.1", + "scipy>=1.16.3", ] license = { file = "LICENSE" } @@ -25,5 +27,17 @@ classifiers = [ dispersionapp = "dispersionapp.__main__:main" [build-system] -build-backend = "setuptools.build_meta" -requires = ["setuptools >= 65.0.0"] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.pyright] +exclude = [".venv"] +venvPath = "." +venv = ".venv" +typeCheckingMode = "off" + +[dependency-groups] +dev = [ + "matplotlib>=3.10.7", +] + diff --git a/src/dispersionapp/__main__.py b/src/dispersionapp/__main__.py index 92c202a..3403d6f 100644 --- a/src/dispersionapp/__main__.py +++ b/src/dispersionapp/__main__.py @@ -3,7 +3,7 @@ import sys import click -from dispersionapp.core import DEFAULT_CONFIG_FILE +from dispersionapp.core import DEFAULT_CONFIG_FILE, Config from dispersionapp.gui import app @@ -11,12 +11,17 @@ from dispersionapp.gui import app @click.option( "-c", "--config", + "config_file", default=DEFAULT_CONFIG_FILE, help="configuration file in TOML format", type=click.Path(file_okay=True, dir_okay=False, resolve_path=True), ) +@click.option("-g", "--gas", help="Gas. Must be one of ", type=str) @click.version_option() -def main(config: os.PathLike): +def main(config_file: os.PathLike, gas: str | None = None): + config = Config.load(config_file) + if gas is not None: + config.gas = gas.lower() app(config) diff --git a/src/dispersionapp/core.py b/src/dispersionapp/core.py index 28fb50d..26932c0 100644 --- a/src/dispersionapp/core.py +++ b/src/dispersionapp/core.py @@ -1,33 +1,39 @@ from __future__ import annotations +import os import sys from pathlib import Path -from typing import NamedTuple +from typing import Annotated, NamedTuple, Self import numpy as np -import scgenerator as sc import tomli import tomli_w -from pydantic import BaseModel, PrivateAttr, ValidationError, confloat, conint +from pydantic import BaseModel, PrivateAttr, ValidationError, Field -DEFAULT_CONFIG_FILE = "dispersion_config.toml" +import dispersionapp.physics.units as units +import dispersionapp.physics.materials as materials +import dispersionapp.physics.pulse as pulse +import dispersionapp.physics.fiber as fiber +import dispersionapp.math as math + +DEFAULT_CONFIG_FILE = Path.home() / "dispersion_config.toml" class CurrentState(BaseModel): core_diameter_um: float pressure_mbar: float wall_thickness_um: float - num_resonances: conint(ge=6, le=40) = 6 + num_resonances: Annotated[int, Field(ge=6, le=40)] = 6 n_tubes: int gap_um: float t_fwhm_fs: float class Config(BaseModel): - wl_min: confloat(ge=100, le=1000) = 160 - wl_max: confloat(ge=500, le=6000) = 1600 - wl_pump: confloat(ge=200, le=6000) = 800 - rep_rate: confloat(gt=0) = 8e3 + wl_min: Annotated[float, Field(ge=100, le=1000)] = 160 + wl_max: Annotated[float, Field(ge=500, le=6000)] = 1600 + wl_pump: Annotated[float, Field(ge=200, le=6000)] = 800 + rep_rate: Annotated[float, Field(gt=0)] = 8e3 gas: str = "argon" safety_factor: float = 10.0 current_state: CurrentState | None = None @@ -35,7 +41,7 @@ class Config(BaseModel): _file_name: Path = PrivateAttr() @classmethod - def load(cls, config_file: str | None = None) -> Config: + def load(cls, config_file: os.PathLike | None = None) -> Self: config_file = Path(config_file or DEFAULT_CONFIG_FILE) if not config_file.exists(): config_file.touch() @@ -54,7 +60,7 @@ class Config(BaseModel): def save(self): tmp = self._file_name.parent / f"{self._file_name.name}.tmp" with open(tmp, "wb") as file: - tomli_w.dump(self.dict(), file) + tomli_w.dump(self.model_dump(), file) tmp.replace(self._file_name) def update_current( @@ -88,12 +94,16 @@ class LimitValues(NamedTuple): def b2(w, n_eff): dw = w[1] - w[0] - beta = sc.fiber.beta(w, n_eff) - return sc.math.differentiate_arr(beta, 2, 4, dw) + beta = fiber.beta(w, n_eff) + return math.differentiate_arr(beta, 2, 4, dw) def N_sf_max( - wl: np.ndarray, t0: float, wl_zero_disp: float, gas: sc.materials.Gas, safety: float = 10.0 + wl: np.ndarray, + t0: float, + wl_zero_disp: float, + gas: materials.Gas, + safety: float = 10.0, ) -> np.ndarray: """ maximum soliton number according to self focusing @@ -105,30 +115,34 @@ def N_sf_max( def N_ion_max( - wl: np.ndarray, t0: float, wl_zero_disp: float, gas: sc.materials.Gas, safety: float = 10.0 + wl: np.ndarray, + t0: float, + wl_zero_disp: float, + gas: materials.Gas, + safety: float = 10.0, ) -> np.ndarray: """ eq. S16 in Travers2019 """ - ind = sc.math.argclosest(wl, wl_zero_disp) + ind = math.argclosest(wl, wl_zero_disp) f = np.gradient(np.gradient(gas.sellmeier.chi(wl), wl), wl) - factor = (sc.math.u_nm(1, 1) / sc.units.c) ** 2 * (0.5 * wl / np.pi) ** 3 + factor = (math.u_nm(1, 1) / units.c) ** 2 * (0.5 * wl / np.pi) ** 3 delta = factor * (f / f[ind] - 1) denom = safety * np.pi * wl * np.abs(delta) * f[ind] - return t0 * sc.math.u_nm(1, 1) * np.sqrt(gas.n2() * gas.barrier_suppression / denom) + return t0 * math.u_nm(1, 1) * np.sqrt(gas.n2() * gas.barrier_suppression / denom) def solition_num( t0: float, w0: float, beta2: float, n2: float, core_radius: float, peak_power: float ) -> float: - gamma = sc.fiber.gamma_parameter(n2, w0, sc.fiber.A_eff_marcatili(core_radius)) - ld = sc.pulse.L_D(t0, beta2) + gamma = fiber.gamma_parameter(n2, w0, fiber.effective_area_marcatili(core_radius)) + ld = pulse.dispersion_length(t0, beta2) return np.sqrt(gamma * ld * peak_power) def energy( t0: float, w0: float, beta2: float, n2: float, core_radius: float, solition_num: float ) -> float: - gamma = sc.fiber.gamma_parameter(n2, w0, sc.fiber.A_eff_marcatili(core_radius)) + gamma = fiber.gamma_parameter(n2, w0, fiber.effective_area_marcatili(core_radius)) peak_power = solition_num**2 * abs(beta2) / (t0**2 * gamma) - return sc.pulse.P0_to_E0(peak_power, t0, "sech") + return pulse.P0_to_E0(peak_power, t0, "sech") diff --git a/src/dispersionapp/data/README.md b/src/dispersionapp/data/README.md new file mode 100644 index 0000000..5993dc4 --- /dev/null +++ b/src/dispersionapp/data/README.md @@ -0,0 +1,10 @@ +# `materials.json` +Contains material property like Sellmeir coefficients, nonlinear refractive index and Van der Waals constants. + +Sources are embeded in the document and are completed as such: +Van der Waals constants : https://en.wikipedia.org/wiki/Van_der_Waals_constants_(data_page) +Chi3 : Wahlstrand 2012 + +# `raman_response.csv` +Raman impulse response recovered from measured gain spectrum. This is used then `raman_type` is set to `"measured"` (Stolen1989). + diff --git a/src/dispersionapp/data/__init__.py b/src/dispersionapp/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/dispersionapp/data/materials.json b/src/dispersionapp/data/materials.json new file mode 100644 index 0000000..ca62112 --- /dev/null +++ b/src/dispersionapp/data/materials.json @@ -0,0 +1,449 @@ +{ + "air": + { + "a": 0.1358, + "atomic_mass": 4.809e-26, + "atomic_number": 7.14, + "b": 3.64e-5, + "kerr": + { + "P0": 101325, + "T0": 293.15, + "n2": 4e-23, + "source": "Nibbering, E., Grillon, G., Franco, M.A., Prade, B., Mysyrowicz, A., 1997. JOSA B 14, 650–660." + }, + "sellmeier": + { + "B": + [ + 57921050000, + 1679170000 + ], + "C": + [ + 2.380185e+14, + 5.7362e+13 + ], + "P0": 101325, + "T0": 288.15, + "kind": 2 + } + }, + "argon": + { + "a": 0.1355, + "atomic_mass": 6.6338e-26, + "atomic_number": 18, + "b": 3.201e-5, + "ionization_energy": 2.5249661793774e-18, + "kerr": + { + "P0": 30400, + "T0": 273.15, + "n2": 9.7e-24, + "source": "Wahlstrand, J. K., Cheng, Y. H., & Milchberg, H. M. (2012). High field optical nonlinearity and the Kramers-Kronig relations. Physical review letters, 109(11), 113904." + }, + "sellmeier": + { + "B": + [ + 2501410000, + 500283000, + 52234300000 + ], + "C": + [ + 9.1012e+13, + 8.7892e+13, + 2.1402e+14 + ], + "P0": 101325, + "T0": 273.15, + "kind": 2, + "source": "A. Bideau-Mehu, Y. Guern, R. Abjean, A. Johannin-Gilles. Measurement of refractive indices of neon, argon, krypton and xenon in the 253.7-140.4 nm wavelength range. Dispersion relations and estimated oscillator strengths of the resonance lines. J. Quant. Spectrosc. Rad. Transfer 25, 395-402 (1981)" + } + }, + "argon_alt": + { + "a": 0.1355, + "atomic_mass": 6.6338e-26, + "atomic_number": 18, + "b": 3.201e-5, + "kerr": + { + "P0": 30400, + "T0": 273.15, + "n2": 9.7e-24, + "source": "Wahlstrand, J. K., Cheng, Y. H., & Milchberg, H. M. (2012). High field optical nonlinearity and the Kramers-Kronig relations. Physical review letters, 109(11), 113904." + }, + "sellmeier": + { + "B": + [ + 0.0002033229, + 0.0003445831 + ], + "C": + [ + 2.0612e-16, + 8.066e-15 + ], + "P0": 101325, + "T0": 273.15, + "kind": 1, + "source": "A. Börzsönyi, Z. Heiner, M. P. Kalashnikov, A. P. Kovács, and K. Osvay, Dispersion measurement of inert gases and gas mixtures at 800 nm, Appl. Opt. 47, 4856-4863 (2008)" + } + }, + "argon_alt2": + { + "a": 0.1355, + "b": 3.201e-5, + "kerr": + { + "P0": 30400, + "T0": 273.15, + "n2": 9.7e-24, + "source": "Wahlstrand, J. K., Cheng, Y. H., & Milchberg, H. M. (2012). High field optical nonlinearity and the Kramers-Kronig relations. Physical review letters, 109(11), 113904." + }, + "sellmeier": + { + "B": + [ + 0.030182943 + ], + "C": + [ + 1.44e+14 + ], + "P0": 101325, + "T0": 273.15, + "const": 6.7867e-5, + "kind": 2, + "source": "E. R. Peck and D. J. Fisher. Dispersion of argon, J. Opt. Soc. Am. 54, 1362-1364 (1964)" + } + }, + "d-zlaf52la": + { + "sellmeier": + { + "B": + [ + -8487502830, + 3.1753525, + 3.71301651e-14, + -1.09609062e-27, + 2.4418582e-40, + -8.94583294e-54 + ], + "C": [], + "kind": 3, + "reference": + [ + "CDGM Zemax catalog 2017-09 (obtained from http://www.cdgmgd.com)" + ] + } + }, + "e7": + { + "sellmeier": + { + "B": + [ + 0, + 7.2e-15, + 3e-28 + ], + "C": [], + "kind": 3, + "reference": + [ + "J. Li, C. H. Wen, S. Gauza, R. Lu and S. T. Wu. Refractive indices of liquid crystals for display applications, J. Disp. Technol. 1, 51-61, 2005" + ] + } + }, + "helium": + { + "a": 0.00346, + "atomic_mass": 6.646477e-27, + "atomic_number": 2, + "b": 2.38e-5, + "ionization_energy": 3.9393356074281e-18, + "kerr": + { + "P0": 30400, + "T0": 273.15, + "n2": 3.1e-25, + "source": "Wahlstrand, J. K., Cheng, Y. H., & Milchberg, H. M. (2012). High field optical nonlinearity and the Kramers-Kronig relations. Physical review letters, 109(11), 113904." + }, + "sellmeier": + { + "B": + [ + 2.16463842e-5, + 2.10561127e-7, + 4.7509272e-5 + ], + "C": + [ + -6.80769781e-16, + 5.13251289e-15, + 3.18621354e-15 + ], + "P0": 101325, + "T0": 273.15, + "kind": 1, + "source": "A. Ermolov, K. F. Mak, M. H. Frosz, J. C. Travers, P. St. J. Russell, Supercontinuum generation in the vacuum ultraviolet through dispersive-wave and soliton-plasma interaction in a noble-gas-filled hollow-core photonic crystal fiber, Phys. Rev. A 92, 033821 (2015)" + } + }, + "helium_alt": + { + "a": 0.00346, + "b": 2.38e-5, + "kerr": + { + "P0": 30400, + "T0": 273.15, + "n2": 3.1e-25 + }, + "sellmeier": + { + "B": + [ + 14755297000 + ], + "C": + [ + 4.262974e+14 + ], + "P0": 101325, + "T0": 273.15, + "kind": 2, + "source": " C. Cuthbertson and M. Cuthbertson. The refraction and dispersion of neon and helium. Proc. R. Soc. London A 135, 40-47 (1936)" + } + }, + "hydrogen": + { + "a": 0.02453, + "atomic_mass": 1.674e-27, + "b": 2.651e-5, + "kerr": + { + "P0": 30400, + "T0": 273.15, + "n2": 6.36e-24, + "source": "Shelton, D. P., & Rice, J. E. (1994). Measurements and calculations of the hyperpolarizabilities of atoms and small molecules in the gas phase. Chemical Reviews, 94(1), 3-29" + }, + "sellmeier": + { + "B": + [ + 0.0148956, + 0.0049037 + ], + "C": + [ + 1.807e-10, + 9.2e-11 + ], + "P0": 101325, + "T0": 273.15, + "kind": 2, + "source": "E. R. Peck and S. Hung. Refractivity and dispersion of hydrogen in the visible and near infrared, J. Opt. Soc. Am. 67, 1550-1554 (1977)" + } + }, + "krypton": + { + "a": 0.2349, + "atomic_mass": 1.3915e-25, + "atomic_number": 36, + "b": 3.978e-5, + "ionization_energy": 2.2429831039374e-18, + "kerr": + { + "P0": 30400, + "T0": 273.15, + "n2": 2.2e-23, + "source": "Wahlstrand, J. K., Cheng, Y. H., & Milchberg, H. M. (2012). High field optical nonlinearity and the Kramers-Kronig relations. Physical review letters, 109(11), 113904." + }, + "sellmeier": + { + "B": + [ + 2536370000, + 2736490000, + 62080200000 + ], + "C": + [ + 6.54742e+13, + 7.3698e+13, + 1.8108e+14 + ], + "P0": 101325, + "T0": 273.15, + "kind": 2 + } + }, + "nbk7": + { + "sellmeier": + { + "B": + [ + 1.03961212, + 0.231792344, + 1.01046945 + ], + "C": + [ + 6.00069867e-15, + 2.00179144e-14, + 1.03560653e-10 + ], + "kind": 1, + "reference": + [ + "SCHOTT Zemax catalog 2017-01-20b (obtained from http://www.schott.com)" + ] + } + }, + "neon": + { + "a": 0.02135, + "atomic_mass": 3.35092e-26, + "atomic_number": 10, + "b": 1.709e-5, + "ionization_energy": 3.45501365359425e-18, + "kerr": + { + "P0": 30400, + "T0": 273.15, + "n2": 8.7e-25, + "source": "Wahlstrand, J. K., Cheng, Y. H., & Milchberg, H. M. (2012). High field optical nonlinearity and the Kramers-Kronig relations. Physical review letters, 109(11), 113904." + }, + "sellmeier": + { + "B": + [ + 1281450000, + 22048600000 + ], + "C": + [ + 1.84661e+14, + 3.7684e+14 + ], + "P0": 101325, + "T0": 273.15, + "kind": 2 + } + }, + "nitrogen": + { + "a": 0.137, + "atomic_mass": 2.3259e-26, + "atomic_number": 7, + "b": 1.709e-5, + "kerr": + { + "P0": 30400, + "T0": 273.15, + "n2": 2.2e-23, + "source": "Wahlstrand, J. K., Cheng, Y. H., & Milchberg, H. M. (2012). High field optical nonlinearity and the Kramers-Kronig relations. Physical review letters, 109(11), 113904." + }, + "sellmeier": + { + "B": + [ + 32431570000 + ], + "C": + [ + 1.44e+14 + ], + "P0": 101325, + "T0": 273.15, + "const": 6.8552e-5, + "kind": 2 + } + }, + "silica": + { + "sellmeier": + { + "B": + [ + 0.6961663, + 0.4079426, + 0.8974794 + ], + "C": + [ + 4.67914826e-15, + 1.35120631e-14, + 9.79340025e-11 + ], + "kind": 1, + "reference": + [ + "I. H. Malitson. Interspecimen comparison of the refractive index of fused silica, J. Opt. Soc. Am. 55, 1205-1208 (1965)", + "C. Z. Tan. Determination of refractive index of silica glass for infrared wavelengths by IR spectroscopy, J. Non-Cryst. Solids 223, 158-163 (1998)" + ] + } + }, + "vacuum": + { + "a": 0, + "atomic_mass": 0, + "atomic_number": 1, + "b": 0, + "kerr": + { + "P0": 30400, + "T0": 273.15, + "n2": 0, + "source": "none" + }, + "sellmeier": + { + "B": [], + "C": [], + "P0": 101325, + "T0": 273.15, + "kind": 1 + } + }, + "xenon": + { + "a": 0.425, + "atomic_mass": 2.18017e-25, + "atomic_number": 54, + "b": 5.105e-5, + "ionization_energy": 1.94342415157935e-18, + "kerr": + { + "P0": 30400, + "T0": 273.15, + "n2": 5.8e-23, + "source": "Wahlstrand, J. K., Cheng, Y. H., & Milchberg, H. M. (2012). High field optical nonlinearity and the Kramers-Kronig relations. Physical review letters, 109(11), 113904." + }, + "sellmeier": + { + "B": + [ + 3228690000, + 3553930000, + 60676400000 + ], + "C": + [ + 4.6301e+13, + 5.9578e+13, + 1.1274e+14 + ], + "P0": 101325, + "T0": 273.15, + "kind": 2 + } + } +} diff --git a/src/dispersionapp/data/raman_response.npy b/src/dispersionapp/data/raman_response.npy new file mode 100644 index 0000000..a6b6ce0 Binary files /dev/null and b/src/dispersionapp/data/raman_response.npy differ diff --git a/src/dispersionapp/gui.py b/src/dispersionapp/gui.py index 7924f21..96bbb48 100644 --- a/src/dispersionapp/gui.py +++ b/src/dispersionapp/gui.py @@ -1,30 +1,31 @@ from __future__ import annotations -import os import warnings from functools import cache import numpy as np -import scgenerator as sc -from dispersionapp.core import (Config, LimitValues, N_ion_max, N_sf_max, b2, - energy) +import dispersionapp.math as math +import dispersionapp.physics.fiber as fiber +import dispersionapp.physics.materials as materials +import dispersionapp.physics.pulse as pulse +import dispersionapp.physics.units as units +from dispersionapp.core import Config, LimitValues, N_ion_max, N_sf_max, b2, energy from dispersionapp.plotapp import PlotApp, QtWidgets -def app(config_file: os.PathLike | None = None): - config = Config.load(config_file) +def app(config: Config): # grid stuff - w = sc.w_from_wl(config.wl_min, config.wl_max, 2048) - wl = sc.units.m.inv(w) - wl_ind = sc.math.argclosest(wl, config.wl_pump * 1e-9) - w0 = w[wl_ind] + w = units.w_from_wl(config.wl_min, config.wl_max, 2048) + wl = units.m_rads(w) + wl_ind = math.argclosest(wl, config.wl_pump * 1e-9) + w0: float = w[wl_ind] - gas = sc.materials.Gas(config.gas) + gas = materials.Gas(config.gas) with PlotApp( f"Dispersion design with {config.gas.title()}", - core_diameter_um=np.arange(50, 301, dtype=float), + core_diameter_um=np.arange(5, 301, dtype=float), pressure_mbar=np.geomspace(1, 2000), wall_thickness_um=np.geomspace(0.01, 10, 201), num_resonances=np.arange(6, 41), @@ -78,24 +79,28 @@ def app(config_file: os.PathLike | None = None): t_fwhm = 1e-15 * t_fwhm_fs pressure = 1e2 * pressure_mbar core_radius = 0.5e-6 * core_diameter_um - t0 = sc.pulse.fwhm_to_T0_fac["sech"] * t_fwhm + t0 = pulse.fwhm_to_T0_fac["sech"] * t_fwhm disp = compute_capillary(core_diameter_um, pressure_mbar) - wl_zero_disp = sc.math.all_zeros(wl, disp) + wl_zero_disp = math.all_zeros(wl, disp) if len(wl_zero_disp) == 0: raise ValueError("No zero dispersion found") - wl_zero_disp = wl_zero_disp[0] + single_wl_zero_disp: float = wl_zero_disp[0] with warnings.catch_warnings(): warnings.simplefilter("ignore") - ion_limit = N_ion_max(wl, t0, wl_zero_disp, gas, config.safety_factor)[wl_ind] - sf_limit = N_sf_max(wl, t0, wl_zero_disp, gas, config.safety_factor)[wl_ind] + ion_limit: float = N_ion_max( + wl, t0, single_wl_zero_disp, gas, config.safety_factor + )[wl_ind] + sf_limit: float = N_sf_max(wl, t0, single_wl_zero_disp, gas, config.safety_factor)[ + wl_ind + ] beta2 = disp[wl_ind] n2 = gas.n2(pressure=pressure) return LimitValues( - wl_zero_disp, + single_wl_zero_disp, ion_limit, sf_limit, energy(t0, w0, beta2, n2, core_radius, ion_limit), @@ -116,9 +121,9 @@ def app(config_file: os.PathLike | None = None): gap = gap_um * 1e-6 pressure = pressure_mbar * 1e2 - tr = sc.fiber.tube_radius_from_gap(core_diameter / 2, gap, n_tubes) + tr = fiber.tube_radius_from_gap(core_diameter / 2, gap, n_tubes) n_gas_2 = gas.sellmeier.n_gas_2(wl, None, pressure) - return sc.fiber.n_eff_vincetti( + return fiber.n_eff_vincetti( wl, 800e-9, n_gas_2, wall_thickness, tr, gap, n_tubes, n_terms=num_resonances ) @@ -141,7 +146,7 @@ def app(config_file: os.PathLike | None = None): pressure = pressure_mbar * 1e2 core_diameter = core_diameter_um * 1e-6 n_gas_2 = gas.sellmeier.n_gas_2(wl, None, pressure) - n_eff_cap = sc.fiber.n_eff_marcatili(wl, n_gas_2, core_diameter / 2) + n_eff_cap = fiber.n_eff_marcatili(wl, n_gas_2, core_diameter / 2) return b2(w, n_eff_cap) @app.update @@ -170,18 +175,18 @@ def app(config_file: os.PathLike | None = None): b2 = compute_vincetti_beta( wall_thickness_um, core_diameter_um, pressure_mbar, num_resonances, n_tubes, gap_um ) - beta_ax.set_line_data("Vincetti", wl * 1e9, sc.units.beta2_fs_cm_inv(b2)) + beta_ax.set_line_data("Vincetti", wl * 1e9, units.beta2_fs_cm_inv(b2)) @app.update def draw_capillary(core_diameter_um: float, pressure_mbar: float): b2 = compute_capillary(core_diameter_um, pressure_mbar) - beta_ax.set_line_data("Capillary", wl * 1e9, sc.units.beta2_fs_cm_inv(b2)) + beta_ax.set_line_data("Capillary", wl * 1e9, units.beta2_fs_cm_inv(b2)) @app.update def draw_energy_limit(core_diameter_um: float, pressure_mbar: float, t_fwhm_fs: float): info_lines = [ f"gas = {config.gas.title()}", - f"rep rate = {config.rep_rate*1e-3}kHz", + f"rep rate = {config.rep_rate * 1e-3}kHz", ] try: diff --git a/src/dispersionapp/io.py b/src/dispersionapp/io.py new file mode 100644 index 0000000..680c107 --- /dev/null +++ b/src/dispersionapp/io.py @@ -0,0 +1,432 @@ +from __future__ import annotations + +import datetime +import importlib.resources +import json +import os +import re +from dataclasses import dataclass +from functools import cache +from io import BytesIO +from pathlib import Path +from typing import Any, BinaryIO, Protocol, Sequence, overload +from zipfile import ZipFile + +import numpy as np + + +class TimedMessage: + def __init__(self, interval: float = 10.0): + self.interval = datetime.timedelta(seconds=interval) + self.next = datetime.datetime.now() + + def ready(self) -> bool: + t = datetime.datetime.now() + if self.next <= t: + self.next = t + self.interval + return True + return False + + +def data_file(path: str) -> Path: + """returns a `Path` object pointing to the desired data file included in `scgenerator`""" + return Path(importlib.resources.files("dispersionapp") / "data" / path) + + +class CustomEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, (datetime.date, datetime.datetime)): + return obj.isoformat() + elif isinstance(obj, np.ndarray): + return tuple(obj) + elif isinstance(obj, DataFile): + return obj.__json__() + + +def _decode_datetime(s: str) -> datetime.date | datetime.datetime | str: + try: + return datetime.datetime.fromisoformat(s) + except Exception: + pass + try: + return datetime.date.fromisoformat(s) + except Exception: + pass + return s + + +def custom_decode_hook(obj): + """ + Some simple, non json-compatible objects are encoded as str, this function reconstructs the + original object and sets it in the decoded json structure + """ + for k, v in obj.items(): + if isinstance(v, str): + obj[k] = _decode_datetime(v) + elif isinstance(v, list): + obj[k] = tuple(v) + return obj + + +@cache +def load_material_dico(name: str) -> dict[str, Any]: + """ + loads a material dictionary + + Parameters + ---------- + name : str + name of the material + + Returns + ------- + material_dico : dict + """ + return json.loads(data_file("materials.json").read_text())[name] + + +class PropagationIOHandler(Protocol): + def __len__(self) -> int: ... + + def keys(self) -> list[str]: ... + + def save_spectrum(self, index: int, spectrum: np.ndarray): ... + + def load_spectrum(self, index: int) -> np.ndarray: ... + + def save_data(self, name: str, data: bytes): ... + + def load_data(self, name: str) -> bytes: ... + + def clear(self): ... + + +class MemoryIOHandler: + spectra: dict[int, np.ndarray] + data: dict[str, bytes] + + def __init__(self): + self.spectra = {} + self.data = {} + + def __len__(self) -> int: + return len(self.spectra) + + def keys(self) -> list[str]: + return list(self.data.keys()) + + def save_spectrum(self, index: int, spectrum: np.ndarray): + self.spectra[index] = spectrum + + def load_spectrum(self, index: int) -> np.ndarray: + return self.spectra[index] + + def save_data(self, name: str, data: bytes): + self.data[name] = data + + def load_data(self, name: str) -> bytes: + return self.data[name] + + def clear(self): + self.spectra = {} + self.data = {} + + +class ZipFileIOHandler: + file: BinaryIO + SPECTRUM_FN = "spectra/spectrum_{}.npy" + + def __init__(self, file: os.PathLike): + """ + Create a IO handler to be used in Propagation. + This handler saves spectra as numpy `.npy` files. + + Parameters + ---------- + file : os.PathLike + path to the desired zip file. Will be created if it doesn't exist. + Passing in an already opened file-like object is not supported at the moment + """ + self.file = Path(file) + if not self.file.exists(): + ZipFile(self.file, "w").close() + + def __len__(self) -> int: + with ZipFile(self.file, "r") as zip_file: + i = 0 + while True: + try: + zip_file.open(self.SPECTRUM_FN.format(i)) + except KeyError: + return i + i += 1 + + def keys(self) -> list[str]: + with ZipFile(self.file, "r") as zip_file: + return zip_file.namelist() + + def save_spectrum(self, index: int, spectrum: np.ndarray): + with ( + ZipFile(self.file, "a") as zip_file, + zip_file.open(self.SPECTRUM_FN.format(index), "w") as file, + ): + np.lib.format.write_array(file, spectrum, allow_pickle=False) + + def load_spectrum(self, index: int) -> np.ndarray: + with ( + ZipFile(self.file, "r") as zip_file, + zip_file.open(self.SPECTRUM_FN.format(index), "r") as file, + ): + return np.load(file) + + def save_data(self, name: str, data: bytes): + with ZipFile(self.file, "a") as zip_file, zip_file.open(name, "w") as file: + file.write(data) + + def load_data(self, name: str) -> bytes: + with ZipFile(self.file, "r") as zip_file, zip_file.open(name, "r") as file: + return file.read() + + def clear(self): + self.file.unlink(missing_ok=True) + + +@dataclass +class DataFile: + """ + Holds information about external files necessary for a simulation. + In the current implementation, only reading data from the file is supported + """ + + prefix: str | None + path: str + io: PropagationIOHandler + + PREFIX_SEP = "::" + + @classmethod + def from_str(cls, s: str, io: PropagationIOHandler | None = None) -> DataFile: + if cls.PREFIX_SEP in s: + prefix, path = s.split(cls.PREFIX_SEP, 1) + else: + prefix = None + path = s + return cls(prefix, path, io) + + @classmethod + def validate(cls, name: str, data: str | DataFile) -> DataFile: + """To be used to automatically construct a DataFile when creating a Parameters obj""" + if isinstance(data, cls): + return data + elif isinstance(data, str): + return cls.from_str(data) + elif isinstance(data, Path): + return cls.from_str(os.fspath(data)) + else: + raise TypeError( + f"{name!r} must be a path or a bundled file specifier, not a {type(data)!r}" + ) + + def __json__(self) -> str: + if self.prefix is None: + return os.fspath(Path(self.path)) + return self.prefix + self.PREFIX_SEP + self.path + + def load_bytes(self) -> bytes: + if self.prefix is not None and self.io is None: + raise ValueError( + f"a bundled file prefixed with {self.prefix} " + "must have a PropagationIOHandler attached" + ) + # a DataFile obj may have a useless io obj attached to it + if self.prefix is not None: + return self.io.load_data(self.path) + else: + return Path(self.path).read_bytes() + + @overload + def load_arrays(self, labels: str | tuple[str, ...]) -> tuple[np.ndarray]: ... + @overload + def load_arrays( + self, labels: str | tuple[str, ...], labels2: str | tuple[str, ...] + ) -> tuple[np.ndarray, np.ndarray]: ... + def load_arrays(self, *labels: str | tuple[str, ...]) -> tuple[np.ndarray, ...]: + raw_data = self.load_bytes() + extension = self.path.lower().split(".")[-1] + return _parse_raw_data(raw_data, extension, *labels) + + def similar_to(self, other: DataFile) -> bool: + return Path(self.path).name == Path(other.path).name + + +def open_data_file(path: os.PathLike, *labels: str) -> tuple[np.ndarray, ...]: + path = Path(path) + raw_data = path.read_bytes() + extension = path.suffix.lower() + return _parse_raw_data(raw_data, extension, *labels) + + +def _parse_raw_data(data: bytes, extension: str, *keys) -> tuple[np.ndarray, ...]: + if extension.endswith("npz"): + df = np.load(BytesIO(data)) + return _parse_npz_data(df, *keys) + elif extension.endswith("npy"): + return tuple(np.load(BytesIO(data))) + else: + return _parse_text_data(data.decode()) + + +def _parse_text_data(s: str) -> tuple[np.ndarray, ...]: + lines = s.splitlines() + for delimiter in ", \t;": + try: + return tuple(np.loadtxt(lines, delimiter=delimiter).T) + except ValueError: + continue + raise ValueError("Could not load text data as numpy array") + + +def _parse_npz_data( + df: np.lib.npyio.NpzFile, *labels: str | tuple[str, ...] +) -> tuple[np.ndarray, ...]: + if not labels: + return tuple(df.values()) + out = [] + for key in labels: + if not isinstance(key, (list, tuple, set)): + out.append(df[key]) + continue + for possible_key in key: + try: + out.append(df[possible_key]) + except KeyError: + continue + else: + break + else: + raise KeyError(f"no key of {key!r} present in {df}") + return tuple(out) + + +def unique_name(base_name: str, existing: set[str]) -> str: + name = base_name + p = Path(base_name) + base = p.stem + ext = p.suffix + i = 0 + while name in existing: + name = f"{base}_{i}{ext}" + i += 1 + return name + + +def format_graph(left_elements: Sequence[str], middle: str, right_elements: Sequence[str]): + if len(left_elements) == 0: + left_elements = [""] + if len(right_elements) == 0: + right_elements = [""] + mid_elements = ["."[:i] + el for i, el in enumerate(middle.split("."))] + left_height = len(left_elements) + right_height = len(right_elements) + + max_left = max(len(el) for el in left_elements) + 1 + max_right = max(len(el) for el in right_elements) + 1 + max_mid = max(len(el) for el in mid_elements) + 2 + + right_elements = [fit_right(el, max_right) for el in right_elements][::-1] + left_elements = [fit_left(el, max_left) for el in left_elements][::-1] + mid_elements = ["─" * max_mid] + [fit_mid(el, max_mid) for el in mid_elements][::-1] + + total_height = max(len(left_elements), len(right_elements), len(mid_elements)) + + line_pos = total_height - len(mid_elements) + + left = left_col( + left_elements, line_pos, total_height, max_left, start=max(0, line_pos - left_height + 1) + ) + right = right_col( + right_elements, line_pos, total_height, max_right, start=max(0, line_pos - right_height + 1) + ) + middle = mid_col(mid_elements, line_pos, total_height, max_mid) + + final = "\n".join(l + m + r for l, m, r in zip(left, middle, right)) + return final + + +def fit_left(el, length, line=1, pad=1): + return f"{' ' * length}{el}{' ' * pad}{'─' * line}"[-(length + 1) :] + + +def fit_right(el, length, line=1, pad=1): + return f"{'─' * line}{' ' * pad}{el}{' ' * length}"[: length + 1] + + +def fit_mid(el, length, pad=1): + return f"{' ' * pad}{el}{' ' * length}"[:length] + + +def mid_col(mid_els, line_pos, height, pad_length): + out = [" " * pad_length] * line_pos + mid_els + while len(out) < height: + out.append(" " * pad_length) + return out[::-1] + + +def left_col(left_els, line_pos, height, pad_length, start=0): + out = [" " * (pad_length + 1)] * start + for rel_i, el in enumerate(left_els): + abs_j = rel_i + start + out.append(el + get_symb(False, rel_i, abs_j, line_pos, len(left_els) - 1)) + while len(out) < height: + out.append(" " * (pad_length + 2)) + return out[::-1] + + +def right_col(right_els, line_pos, height, pad_length, start=0): + out = [" " * (pad_length + 1)] * start + for rel_i, el in enumerate(right_els): + abs_j = rel_i + start + out.append(get_symb(True, rel_i, abs_j, line_pos, len(right_els) - 1) + el) + while len(out) < height: + out.append(" " * (pad_length + 2)) + return out[::-1] + + +def get_symb(right: bool, rel_i, abs_j, line_pos, max_ind): + if max_ind == 0: + return "─" + elif rel_i == 0: + if abs_j < line_pos: + return "╯╰"[right] + elif abs_j == line_pos: + return "┴" + else: + raise ValueError("bottom of left columns cannot be above line") + elif rel_i < max_ind: + if abs_j == line_pos: + return "┼" + else: + return "┤├"[right] + else: + if abs_j == line_pos: + return "┬" + elif abs_j > line_pos: + return "╮╭"[right] + else: + raise ValueError("top of left columns cannot be below line") + + +def _to_num(s: str) -> str | float | int: + for f in int, float: + try: + return f(s) + except ValueError: + pass + return s + + +NUM_REGEX = r"[+-]?[\d]+(?:\.?[\d]+)?(?:[eE][+-]?\d+)?" + + +def logical_sort_key(s: os.PathLike | str) -> tuple: + return tuple(_to_num(el) for el in re.split(f"({NUM_REGEX})", str(s)) if el) diff --git a/src/dispersionapp/math.py b/src/dispersionapp/math.py new file mode 100644 index 0000000..a93e8e3 --- /dev/null +++ b/src/dispersionapp/math.py @@ -0,0 +1,800 @@ +""" +collection of purely mathematical function +""" + +import math +import warnings +from dataclasses import dataclass +from functools import cache +from typing import Callable, Sequence, overload + +import numba +import numpy as np +from scipy import fft as sfft +from scipy.interpolate import interp1d, lagrange +from scipy.special import jn_zeros + +pi = np.pi +c = 299792458.0 + + +def fft_functions( + full_field: bool, +) -> tuple[Callable[[np.ndarray], np.ndarray], Callable[[np.ndarray], np.ndarray]]: + if full_field: + return sfft.rfft, sfft.irfft + else: + return sfft.fft, sfft.ifft + + +def expm1_int(y: np.ndarray, dx: float) -> np.ndarray: + """evaluates 1 - exp( -∫func(y(x))dx ) from x=-inf to x""" + return -np.expm1(-cumulative_simpson(y) * dx) + + +def span(*vec: np.ndarray) -> tuple[float, float]: + """returns the min and max of whatever array-like is given. can accept many args""" + out = (np.inf, -np.inf) + if len(vec) == 0 or len(vec[0]) == 0: + raise ValueError("did not provide any value to span") + for x in vec: + x = np.atleast_1d(x) + out = (min(np.min(x), out[0]), max(np.max(x), out[1])) + return out + + +def total_extent(*vec: np.ndarray) -> float: + """measure the distance between the min and max value of all given arrays""" + left, right = span(*vec) + return right - left + + +def span_above(arr: np.ndarray, threshold: float) -> tuple[int, int]: + """returns the first and last index where the array is above the specified threshold""" + ind = np.where(arr >= threshold)[-1] + return np.min(ind), np.max(ind) + + +@overload +def argclosest(array: np.ndarray, target: float | int) -> int: ... +@overload +def argclosest(array: np.ndarray, target: Sequence[float | int] | np.ndarray) -> np.ndarray: ... + + +def argclosest( + array: np.ndarray, target: float | int | Sequence[float | int] | np.ndarray +) -> int | np.ndarray: + """ + returns the index/indices corresponding to the closest matches of target in array + + Parameters + ---------- + array : np.ndarray, shape (n,) + array of values + target : number | np.ndarray + find the closest value to target in `array`. The index of the closest match is returned. + + Returns + ------- + int | np.ndarray + index / indices of the closest match + + """ + min_dist = np.inf + index = 0 + if isinstance(target, (list, tuple, np.ndarray)): + return np.array([argclosest(array, t) for t in target]) + for k, val in enumerate(array): + dist = abs(val - target) + if dist < min_dist: + min_dist = dist + index = k + + return index + + +def extent(x): + return np.max(x) - np.min(x) + + +def power_fact(x, n): + """ + returns x ^ n / n! + """ + if isinstance(x, (int, float)): + return _power_fact_single(x, n) + + elif isinstance(x, np.ndarray): + return _power_fact_array(x, n) + else: + raise TypeError(f"type {type(x)} of x not supported.") + + +def _power_fact_single(x, n): + result = 1.0 + for k in range(n): + result = result * x / (n - k) + return result + + +def _power_fact_array(x, n): + """returns x^2/n!""" + result = np.ones(len(x), dtype=np.float64) + for k in range(n): + result = result * x / (n - k) + return result + + +@numba.njit() +def abs2(z: np.ndarray) -> np.ndarray: + return z.real**2 + z.imag**2 + + +def normalized(z: np.ndarray) -> np.ndarray: + ab = abs2(z) + return ab / ab.max() + + +def autocrop( + x, y, z, threshold: float = 1e-4, x_only: bool = False +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + thr = z.max() + yind, xind = np.where(z > thr * threshold) + xmin, xmax = xind.min(), xind.max() + if x_only: + ymin, ymax = 0, len(y) - 1 + else: + ymin, ymax = yind.min(), yind.max() + return x[xmin : xmax + 1], y[ymin : ymax + 1], z[ymin : ymax + 1][:, xmin : xmax + 1] + + +def to_dB(arr: np.ndarray, ref=None, axis=None, default: float | None = None) -> np.ndarray: + """ + converts unitless values in dB + + Parameters + ---------- + arr : np.ndarray + array of non-negative values. Any values 0 or below will be mapped to the minimum + positive value in dB + ref : float, optional + reference value corresponding to 0dB (default : max(arr)) + axis : int | None, optional + on which axis to apply the transformation. If `ref` is given, this has no effect + + Returns + ---------- + np.ndarray + array in dB + """ + if axis is not None and arr.ndim > 1 and ref is None: + return np.apply_along_axis(to_dB, axis, arr, default=default) + + out = np.ones_like(arr) + if default is not None and math.isfinite(default): + out[:] *= default + else: + out *= np.nan + + if ref is None: + ref = np.max(arr) + above_0 = arr > 0 + if not np.any(above_0) or ref <= 0: + warnings.warn(f"invalid array to convert to dB, returning {default} instead") + if default is None: + out *= np.nan + return out + m = arr / ref + if default is None: + out *= 10 * np.log10(m[above_0].min()) + return 10 * np.log10(m, out=out, where=above_0) + + +def u_nm(n, m): + """ + returns the mth zero of the Bessel function of order n-1 + + Parameters + ---------- + n-1 : order of the Bessel function + m : order of the zero + + Returns + ---------- + float + """ + return jn_zeros(n - 1, m)[-1] + + +def all_zeros(x: np.ndarray, y: np.ndarray) -> np.ndarray: + """find all the x values such that y(x)=0 with linear interpolation""" + pos = np.argwhere(y[1:] * y[:-1] < 0)[:, 0] + m = (y[pos + 1] - y[pos]) / (x[pos + 1] - x[pos]) + return -y[pos] / m + x[pos] + + +def wspace(t: float | np.ndarray, t_num=0): + """ + frequency array such that x(t) <-> np.fft(x)(w) + + Parameters + ---------- + t : float | np.ndarray + float : total width of the time window + array : time array + t_num : int- + if t is a float, specifies the number of points + + Returns + ------- + w : array + linspace of frencies corresponding to t + """ + if isinstance(t, (np.ndarray, list, tuple)): + dt = t[1] - t[0] + t_num = len(t) + else: + dt = t / t_num + return np.fft.fftfreq(t_num, dt) * 2 * np.pi + + +def tspace(time_window: float | None = None, t_num: int | None = None, dt: float | None = None): + """ + returns a time array centered on 0 + + Parameters + ---------- + time_window : float + total time spanned + t_num : int + number of points + dt : float + time resolution + + at least 2 arguments must be given. They are prioritize as such + t_num > time_window > dt + + Returns + ------- + t : array + a linearily spaced time array + Raises + ------ + TypeError + missing at least 1 argument + """ + if t_num is not None: + if isinstance(time_window, (float, int)): + return np.linspace(-time_window / 2, time_window / 2, int(t_num)) + elif isinstance(dt, (float, int)): + time_window = (t_num - 1) * dt + return np.linspace(-time_window / 2, time_window / 2, t_num) + elif isinstance(time_window, (float, int)) and isinstance(dt, (float, int)): + t_num = int(time_window / dt) + 1 + return np.linspace(-time_window / 2, time_window / 2, t_num) + raise TypeError("not enough parameter to determine time vector") + + +def irfftfreq(freq: np.ndarray, retstep: bool = False): + """ + Given an array of positive only frequency, this returns the corresponding time array centered + around 0 that will be aligned with the `numpy.fft.irfft` of a spectrum aligned with `freq`. + if `retstep` is True, the sample spacing is returned as well + """ + df = freq[1] - freq[0] + nt = (len(freq) - 1) * 2 + period = 1 / df + dt = period / nt + + t = np.linspace(-(period - dt) / 2, (period - dt) / 2, nt) + if retstep: + return t, dt + else: + return t + + +def iwspace(w: np.ndarray, retstep: bool = False): + """invserse of wspace: recovers the (symmetric) time array corresponsding to `w`""" + df = (w[1] - w[0]) * 0.5 / np.pi + nt = len(w) + period = 1 / df + dt = period / nt + t = np.linspace(-(period - dt) / 2, (period - dt) / 2, nt) + if retstep: + return t, dt + else: + return t + + +def dt_from_min_wl(wl_min: float, wavelength: float) -> float: + return 0.5 / c / (1 / wl_min - 1 / wavelength) + + +def min_wl_from_dt(dt: float, wavelength: float) -> float: + return wavelength / (1 + wavelength / (2 * c * dt)) + + +def build_envelope_w_grid(t: np.ndarray, w0: float) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """ + convenience function to + + Parameters + ---------- + t : np.ndarray, shape (t_num,) + time array + w0 : float + center frequency + + Returns + ------- + w_c : np.ndarray, shape (n, ) + centered angular frequencies in rad/s where 0 is the pump frequency + w : np.ndarray, shape (n, ) + angular frequency grid + w_order : np.ndarray, shape (n,) + arrays of indices such that w[w_order] is sorted + """ + w_c = wspace(t) + w = w_c + w0 + w_order = np.argsort(w) + return w_c, w, w_order + + +def build_full_field_w_grid(t_num: int, dt: float) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """ + Paramters + --------- + t_num : int + number of temporal sampling points + dt : float + uniform spacing between sample points + + Returns + ------- + w : np.ndarray, shape (n, ) + angular frequency grid + w_order : np.ndarray, shape (n,) + arrays of indices such that w[w_order] is sorted + """ + w = 2 * pi * np.fft.rfftfreq(t_num, dt) + w_order = np.argsort(w) + ind = w != 0 + wl = np.zeros(len(w)) + wl[ind] = 2 * pi * c / w[ind] + if any(ind): + wl[~ind] = 2 * pi * c / (np.abs(w[ind]).min()) + return w, w_order, wl + + +def build_z_grid(z_num: int, length: float) -> np.ndarray: + return np.linspace(0, length, z_num) + + +def build_t_grid( + time_window: float | None = None, t_num: int | None = None, dt: float | None = None +) -> tuple[np.ndarray, float, float, int]: + """ + + Returns + ------- + t : np.ndarray, shape (t_num, ) + temporal points in s + time_window : float + total width of the temporal grid in s, by default None + dt : float + spacing of the temporal grid in s, by default None + t_num : int + number of temporal grid points, by default None + """ + t = tspace(time_window, t_num, dt) + time_window = t.max() - t.min() + dt = t[1] - t[0] + t_num = len(t) + return t, time_window, dt, t_num + + +def polynom_extrapolation(x: np.ndarray, y: np.ndarray, degree: int) -> np.ndarray: + """ + performs a polynomial extrapolation on both ends of the support + + Parameters + ---------- + x : np.ndarray (n,) + x values + y : np.ndarray (n,) + y values. The shape must correspond to that of x. + degree : int + degree of the polynom. + + Returns + ------- + np.ndarray, shape (n,) + y array with zero values on either side replaces with polynomial extrapolation + + Example + + + + """ + out = y.copy() + order = np.argsort(x) + left_ind, *_, right_ind = np.nonzero(out[order])[0] + return _polynom_extrapolation_in_place(out[order], left_ind, right_ind, degree)[order.argsort] + + +def _polynom_extrapolation_in_place(y: np.ndarray, left_ind: int, right_ind: int, degree: float): + """ + extrapolates IN PLACE linearily on both side of the support + + Parameters + ---------- + y : np.ndarray + array + left_ind : int + first value we want to keep (extrapolate to the left of that) + right_ind : int + last value we want to keep (extrapolate to the right of that) + """ + r_left = (1 + np.arange(left_ind))[::-1] ** degree + r_right = np.arange(len(y) - right_ind) ** degree + y[:left_ind] = r_left * (y[left_ind] - y[left_ind + 1]) + y[left_ind] + y[right_ind:] = r_right * (y[right_ind] - y[right_ind - 1]) + y[right_ind] + return y + + +def interp_2d( + old_x: np.ndarray, + old_y: np.ndarray, + z: np.ndarray, + new_x: np.ndarray | tuple, + new_y: np.ndarray | tuple, + kind="linear", +) -> np.ndarray: + if isinstance(new_x, tuple): + new_x = np.linspace(*new_x) + if isinstance(new_y, tuple): + new_y = np.linspace(*new_y) + z = interp1d(old_y, z, axis=0, kind=kind, bounds_error=False, fill_value=0)(new_y) + return interp1d(old_x, z, kind=kind, bounds_error=False, fill_value=0)(new_x) + + +@numba.jit(nopython=True) +def linear_interp_2d(old_x: np.ndarray, old_y: np.ndarray, new_x: np.ndarray): + new_vals = np.zeros((len(old_y), len(new_x))) + interpolable = (new_x > old_x[0]) & (new_x <= old_x[-1]) + equal = new_x == old_x[0] + inds = np.searchsorted(old_x, new_x[interpolable]) + for i, val in enumerate(old_y): + new_vals[i][interpolable] = val[inds - 1] + (new_x[interpolable] - old_x[inds - 1]) * ( + val[inds] - val[inds - 1] + ) / (old_x[inds] - old_x[inds - 1]) + new_vals[i][equal] = val[0] + return new_vals + + +@numba.jit(nopython=True) +def linear_interp_1d(old_x: np.ndarray, old_y: np.ndarray, new_x: np.ndarray): + new_vals = np.zeros(len(new_x)) + interpolable = (new_x > old_x[0]) & (new_x <= old_x[-1]) + inds = np.searchsorted(old_x, new_x[interpolable]) + new_vals[interpolable] = old_y[inds - 1] + (new_x[interpolable] - old_x[inds - 1]) * ( + old_y[inds] - old_y[inds - 1] + ) / (old_x[inds] - old_x[inds - 1]) + new_vals[new_x == old_x[0]] = old_y[0] + return new_vals + + +def envelope_ind( + signal: np.ndarray, dmin: int = 1, dmax: int = 1, split: bool = False +) -> tuple[np.ndarray, np.ndarray]: + """ + Attempts to separate the envolope from a periodic signal and return the indices of the + top/bottom envelope of a signal + + Parameters + ---------- + signal : np.ndarray, shape (n,) + signal array (must be sorted) + dmin, dmax : int, optional + size of chunks for lower/upper envelope + split: bool, optional + split the signal in half along its mean, might help to generate the envelope in some cases + this has the effect of forcing the envlopes to be on either side of the dc signal + by default False + + Returns + ------- + np.ndarray, shape (m,), m <= n + lower envelope indices + np.ndarray, shape (l,), l <= n + upper envelope indices + """ + + local_min = (np.diff(np.sign(np.diff(signal))) > 0).nonzero()[0] + 1 + local_max = (np.diff(np.sign(np.diff(signal))) < 0).nonzero()[0] + 1 + + if split: + dc_value = np.mean(signal) + local_min = local_min[signal[local_min] < dc_value] + local_max = local_max[signal[local_max] > dc_value] + + if dmin > 1: + local_min = local_min[ + [i + np.argmin(signal[local_min[i : i + dmin]]) for i in range(0, len(local_min), dmin)] + ] + if dmax > 1: + local_max = local_max[ + [i + np.argmax(signal[local_max[i : i + dmax]]) for i in range(0, len(local_max), dmax)] + ] + + return local_min, local_max + + +def envelope_2d(x: np.ndarray, values: np.ndarray) -> np.ndarray: + """ + returns the envelope of a 2d propagation-like array + + Parameters + ---------- + x : np.ndarray, shape (nt,) + x axis + values : np.ndarray, shape (nz, nt) + values of which to find the envelope + + Returns + ------- + np.ndarray, shape (nz, nt) + interpolated values + """ + return np.array([envelope_1d(x, y) for y in values]) + + +def envelope_1d(y: np.ndarray, x: np.ndarray | None = None) -> np.ndarray: + if x is None: + x = np.arange(len(y)) + _, hi = envelope_ind(y) + return interp1d(x[hi], y[hi], kind="cubic", fill_value=0, bounds_error=False)(x) + + +@dataclass(frozen=True) +class LobeProps: + left_pos: float + left_fwhm: float + center: float + right_fwhm: float + right_pos: float + interp: interp1d + + @property + @cache + def x(self) -> np.ndarray: + return np.array( + [self.left_pos, self.left_fwhm, self.center, self.right_fwhm, self.right_pos] + ) + + @property + @cache + def y(self) -> np.ndarray: + return self.interp(self.x) + + @property + @cache + def fwhm(self) -> float: + return abs(self.right_fwhm - self.left_fwhm) + + @property + @cache + def width(self) -> float: + return abs(self.right_pos - self.left_pos) + + +def measure_lobe(x_in, y_in, /, lobe_pos: int | None = None, thr_rel: float = 1 / 50) -> LobeProps: + """ + given a fairly smooth signal, finds the highest lobe and returns its position as well + as its fwhm points + + Parameters + ---------- + x_in : np.ndarray, shape (n,) + x values + y_in : np.ndarray, shape (n,) + y values + lobe_pos : int, optional + index of the desired lobe, by default None (take highest peak) + thr_rel : float, optional + + + Returns + ------- + np.ndarray + (left limit, left half maximum, maximum position, right half maximum, right limit) + """ + interp = interp1d(x_in, y_in) + lobe_index = lobe_pos or np.argmax(y_in) + maxi = y_in[lobe_index] + maxi2 = maxi / 2 + thr_abs = maxi * thr_rel + half_max_left: float = all_zeros(x_in[:lobe_index], y_in[:lobe_index] - maxi2)[-1] + half_max_right: float = all_zeros(x_in[lobe_index:], y_in[lobe_index:] - maxi2)[0] + + poly = lagrange((half_max_left, x_in[lobe_index], half_max_right), (maxi2, maxi2 * 2, maxi2)) + parabola_left, parabola_right = sorted(poly.roots) + + r_cand = x_in > half_max_right + x_right = x_in[r_cand] + y_right = y_in[r_cand] + + l_cand = x_in < half_max_left + x_left = x_in[l_cand][::-1] + y_left = y_in[l_cand][::-1] + + d = {} + for x, y, central_parabola_root, sign in [ + (x_left, y_left, parabola_left, 1), + (x_right, y_right, parabola_right, -1), + ]: + candidates = [] + slope = sign * np.gradient(y, x) + + for y_test, num_to_take in [ + (sign * np.gradient(slope, x), 2), + (y - thr_abs, 1), + (slope, 3), + ]: + candidates.extend(all_zeros(x, y_test)[:num_to_take]) + candidates = np.array(sorted(candidates)) + + side_parabola_root = x[0] - 2 * y[0] / (sign * slope[0]) + weights = ( + np.abs(candidates - side_parabola_root) + + np.abs(candidates - central_parabola_root) + + interp(candidates) / thr_abs + ) + d[sign] = candidates[np.argmin(weights)] + + return LobeProps(d[1], half_max_left, x_in[lobe_index], half_max_right, d[-1], interp) + + +@numba.jit(nopython=True) +def cumulative_simpson(x: np.ndarray) -> np.ndarray: + out = np.zeros_like(x) + c1 = 1 / 3 + c2 = 4 / 3 + out[1] = (x[1] + x[0]) * 0.5 + for i in range(2, len(x)): + out[i] = out[i - 2] + x[i - 2] * c1 + x[i - 1] * c2 + c1 * x[i] + return out + + +@numba.jit(nopython=True) +def cumulative_boole(x: np.ndarray) -> np.ndarray: + out = np.zeros_like(x) + c1 = 14 / 45 + c2 = 64 / 45 + c3 = 24 / 45 + c = np.array([c1, c2, c3, c2, c1]) + ind = np.arange(5) + out[ind] = cumulative_simpson(x[ind]) + for i in range(4, len(x)): + out[i] = out[i - 4] + np.sum(c * x[i - 4 : i + 1]) + return out + + +def stencil_coefficients(stencil_points: Sequence, order: int) -> np.ndarray: + """ + Reference + --------- + https://en.wikipedia.org/wiki/Finite_difference_coefficient#Arbitrary_stencil_points + """ + mat = np.power.outer(stencil_points, np.arange(len(stencil_points))).T + d = np.zeros(len(stencil_points)) + d[order] = math.factorial(order) + return np.linalg.solve(mat, d) + + +@cache +def central_stencil_coefficients(n: int, order: int) -> np.ndarray: + """ + returns the coefficients of a centered finite difference scheme + + Parameters + ---------- + n : int + number of points + order : int + order of differentiation + """ + return stencil_coefficients(np.arange(n * 2 + 1) - n, order) + + +@cache +def stencil_coefficients_set(n: int, order: int) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """ + coefficients for a forward, centered and backward finite difference differentation scheme of + order `order` that extends `n` points away from the evaluation point (`x_0`) + """ + central = central_stencil_coefficients(n, order) + left = stencil_coefficients(np.arange(n + 1), order) + right = stencil_coefficients(-np.arange(n + 1)[::-1], order) + return left, central, right + + +def differentiate_arr( + values: np.ndarray, + diff_order: int, + extent: int | None = None, + h: float = 1.0, + correct_edges=True, +) -> np.ndarray: + """ + takes a derivative of order `diff_order` using equally spaced values + + Parameters + --------- + values : ndarray + equally spaced values + diff_order : int + order of differentiation + extent : int, optional + how many points away from the center the scheme uses. This determines accuracy. + example: extent=6 means that 13 (6 on either side + center) points are used to evaluate the + derivative at each point. by default diff_order + 2 + h : float, optional + step size, by default 1.0 + correct_edges : bool, optional + avoid artifacts by using forward/backward schemes on the edges, by default True + + Reference + --------- + https://en.wikipedia.org/wiki/Finite_difference_coefficient + + """ + if extent is None: + extent = diff_order + 2 + n_points = (diff_order + extent) // 2 + + if not correct_edges: + central_coefs = central_stencil_coefficients(n_points, diff_order) + result = np.convolve(values, central_coefs[::-1], mode="same") + else: + left_coefs, central_coefs, right_coefs = stencil_coefficients_set(n_points, diff_order) + result = np.concatenate( + ( + np.convolve(values[: 2 * n_points], left_coefs[::-1], mode="valid"), + np.convolve(values, central_coefs[::-1], mode="valid"), + np.convolve(values[-2 * n_points :], right_coefs[::-1], mode="valid"), + ) + ) + return result / h**diff_order + + +def mean_angle(values: np.ndarray, axis: int = 0): + """ + computes the mean angle of an array along a given axis + + Parameters + ---------- + spectra : np.ndarray + complex values from which to compute the mean phase + axis : int, optional + axis on which to take the mean, by default 0 + + Returns + ---------- + np.ndarray + array of complex numbers of unit length representing the mean phase, decimated along the + specified axis + + Example + ---------- + >>> x = np.array([[1 + 1j, 0 + 2j, -3 - 1j], + [1 + 0j, 2 + 3j, -3 + 1j]]) + >>> mean_angle(x) + array([ 0.92387953+0.38268343j, 0.28978415+0.95709203j, -1. +0.j ]) + + """ + values = np.divide( + values, np.abs(values), out=np.zeros(values.shape, dtype=values.dtype), where=values != 0 + ) + total_phase = np.sum(values, axis=axis) + return np.divide(total_phase, np.abs(total_phase), where=total_phase != 0, out=total_phase) diff --git a/src/dispersionapp/physics/__init__.py b/src/dispersionapp/physics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/dispersionapp/physics/fiber.py b/src/dispersionapp/physics/fiber.py new file mode 100644 index 0000000..a17213b --- /dev/null +++ b/src/dispersionapp/physics/fiber.py @@ -0,0 +1,1202 @@ +import warnings +from typing import Sequence, overload + +import numpy as np +from numpy import e +from numpy.fft import fft +from numpy.polynomial.chebyshev import Chebyshev, cheb2poly + +import dispersionapp.io as io +import dispersionapp.math as math +import dispersionapp.physics.materials as materials +import dispersionapp.physics.units as units + +pipi = 2 * math.pi +pi2c = 2 * math.pi * units.c + +type Num = np.floating | np.integer | float | int + + +def valid_wavelength_window(wl: np.ndarray, dispersion_ind: np.ndarray) -> tuple[float, float]: + return wl[dispersion_ind].min(), wl[dispersion_ind].max() + + +def dispersion_indices(wl: np.ndarray, wavelength_window) -> np.ndarray: + return np.where((wl >= wavelength_window[0]) & (wl <= wavelength_window[1]))[0] + + +def is_dynamic_dispersion(pressure=None): + """ + tests if the parameter dictionary implies that the dispersion profile of + the fiber changes with z + + Parameters + ---------- + params : dict + flattened parameters dict + + Returns + ------- + bool : True if dispersion is supposed to change with z + """ + out = False + if pressure is not None: + out |= isinstance(pressure, (tuple, list)) and len(pressure) == 2 + + return out + + +@overload +def beta2_to_dispersion_parameter(beta2: float, wavelength: np.ndarray) -> np.ndarray: ... +@overload +def beta2_to_dispersion_parameter(beta2: float, wavelength: Num) -> float: ... +def beta2_to_dispersion_parameter(beta2: float, wavelength): + return -(pipi * units.c) / (wavelength**2) * beta2 + + +@overload +def dispersion_parameter_to_beta2( + dispersion_parameter: float, wavelength: np.ndarray +) -> np.ndarray: ... +@overload +def dispersion_parameter_to_beta2(dispersion_parameter: float, wavelength: Num) -> float: ... +@overload +def dispersion_parameter_to_beta2( + dispersion_parameter: np.ndarray, wavelength: np.ndarray +) -> np.ndarray: ... +def dispersion_parameter_to_beta2(dispersion_parameter: float, wavelength): + return -(wavelength**2) / (pipi * units.c) * dispersion_parameter + + +def dispersion_slope_to_beta3( + dispersion_parameter: float, dispersion_slope: float, wavelength +) -> float: + return (wavelength**2 / pi2c) ** 2 * (dispersion_slope + 2 * dispersion_parameter / wavelength) + + +def handle_dispersion_parameter( + wavelength: float, dispersion_parameter: float, dispersion_slope: float | None = None +) -> list[float]: + coeffs = [dispersion_parameter_to_beta2(dispersion_parameter, wavelength)] + if dispersion_slope is not None: + coeffs.append(dispersion_slope_to_beta3(dispersion_parameter, dispersion_slope, wavelength)) + return coeffs + + +def find_zdw(wl: np.ndarray, beta2_arr: np.ndarray) -> list[float]: + return [el for el in math.all_zeros(wl, beta2_arr) if el > 0] + + +def plasma_dispersion(wl: np.ndarray, number_density: float, simple=False) -> np.ndarray: + """ + computes dispersion (beta2) for constant plasma + + Parameters + ---------- + l : array-like + wavelengths over which to calculate the dispersion + number_density : number of ionized atoms /m^3 + + Returns + ------- + beta2 : ndarray + WL-dependent dispersion parameter + """ + + e2_me_e0 = 3182.60735 # e^2 /(m_e * epsilon_0) + w = units.m_rads(wl) + if simple: + w_pl = number_density * e2_me_e0 + return -(w_pl**2) / (units.c * w**2) + + beta2_arr = beta2(w, np.sqrt(1 - number_density * e2_me_e0 / w**2)) + return beta2_arr + + +def n_eff_marcatili(wl, n_gas_2, core_radius, he_mode=(1, 1)): + """ + computes the effective refractive index according to the Marcatili model of a capillary + + Parameters + ---------- + l : ndarray, shape (n, ) + wavelengths array (m) + n_gas_2 : ndarray, shape (n, ) + square of the refractive index of the gas as function of l + core_radius : float + inner radius of the capillary (m) + he_mode : tuple, shape (2, ), optional + n and m value of the HE_nm mode. 1 and 1 corresponds to the fundamental mode + + Returns + ------- + n_eff : ndarray, shape (n, ) + + Reference + --------- + Marcatili, E., and core_radius. Schmeltzer, 1964, Bell Syst. Tech. J. 43, 1783. + """ + u = math.u_nm(*he_mode) + + n_eff2 = n_gas_2 - (wl * u / (pipi * core_radius)) ** 2 + return np.sqrt(n_eff2) + + +def n_eff_marcatili_adjusted(wl, n_gas_2, core_radius, he_mode=(1, 1), fit_parameters=()): + """ + computes the effective refractive index according to the Marcatili model of a capillary + but adjusted at longer wavelengths + + Parameters + ---------- + l : ndarray, shape (n, ) + wavelengths array (m) + n_gas_2 : ndarray, shape (n, ) + refractive index of the gas as function of l + core_radius : float + inner radius of the capillary (m) + he_mode : tuple, shape (2, ), optional + n and m value of the HE_nm mode. 1 and 1 corresponds to the fundamental mode + fit_parameters : tuple, shape (2, ), optional + fitting parameters (s, h). See reference for more info + + Returns + ------- + n_eff : ndarray, shape (n, ) + + Reference + ---------- + Köttig, F., et al. "Novel mid-infrared dispersive wave generation in gas-filled PCF by + transient ionization-driven changes in dispersion." arXiv preprint arXiv:1701.04843 (2017). + """ + u = math.u_nm(*he_mode) + + corrected_radius = effective_core_radius(wl, core_radius, *fit_parameters) + + return np.sqrt(n_gas_2 - (wl * u / (pipi * corrected_radius)) ** 2) + + +def effective_area_marcatili(core_radius: float) -> float: + """ + Effective mode-field area for fundamental mode hollow capillaries + + Parameters + ---------- + core_radius : float + radius of the core + + Returns + ------- + float + effective mode field area + """ + return 1.5 * core_radius**2 + + +def capillary_spacing_hasan( + capillary_num: int, capillary_radius: float, core_radius: float +) -> float: + return ( + 2 * (capillary_radius + core_radius) * np.sin(np.pi / capillary_num) - 2 * capillary_radius + ) + + +def resonance_thickness( + wl: np.ndarray, order: int, n_gas_2: np.ndarray, core_radius: float +) -> float: + """ + computes at which wall thickness the specified wl is resonant + + Parameters + ---------- + l : np.ndarray + in m + order : int + 0, 1, ... + n_gas_2 : np.ndarray + as returned by materials.n_gas_2 + core_radius : float + in m + + Returns + ------- + float + thickness in m + """ + n_si_2 = materials.n_gas_2(wl, "silica", None, None) + return ( + wl + / (4 * np.sqrt(n_si_2)) + * (2 * order + 1) + * (1 - n_gas_2 / n_si_2 + wl**2 / (4 * n_si_2 * core_radius**2)) ** -0.5 + ) + + +def resonance_strength( + wl: np.ndarray, core_radius: float, capillary_thickness: float, order: int +) -> float: + a = 1.83 + (2.3 * capillary_thickness / core_radius) + n_si = np.sqrt(materials.n_gas_2(wl, "silica", None, None)) + return ( + capillary_thickness + / (n_si * core_radius) ** (2.303 * a / n_si) + * ((order + 2) / (3 * order)) ** (3.57 * a) + ) + + +def capillary_resonance_strengths( + wl: np.ndarray, + core_radius: float, + capillary_thickness: float, + capillary_resonance_max_order: int, +) -> list[float]: + return [ + resonance_strength(wl, core_radius, capillary_thickness, o) + for o in range(1, capillary_resonance_max_order + 1) + ] + + +def n_eff_hasan( + wl: np.ndarray, + n_gas_2: np.ndarray, + core_radius: float, + capillary_num: int, + capillary_nested: int, + capillary_thickness: float, + capillary_spacing: float, + capillary_resonance_strengths: list[float], +) -> np.ndarray: + """ + computes the effective refractive index of the fundamental mode according to the Hasan + model for a anti-resonance fiber + + Parameters + ---------- + l : np.ndarray, shape(n,) + wavelenghs array + n_gas_2 : ndarray, shape (n, ) + squared refractive index of the gas as a function of l + core_radius : float + radius of the core (from cented to edge of a capillary) + capillary_num : int + number of capillaries + capillary_nested : int, optional + number of levels of nested capillaries. default : 0 + capillary_thickness : float + thickness of the capillaries + capillary_spacing : float + spacing between capillaries + capillary_resonance_strengths : list or tuple + strengths of the resonance lines. may be empty + + Returns + ------- + n_eff : ndarray, shape (n, ) + the effective refractive index as function of wavelength + + Reference + ---------- + Hasan, Md Imran, Nail Akhmediev, and Wonkeun Chang. "Empirical formulae for dispersion and + effective mode area in hollow-core antiresonant fibers." Journal of Lightwave Technology 36.18 + (2018): 4060-4065. + """ + u = math.u_nm(1, 1) + alpha = 5e-12 + + Rg = core_radius / capillary_spacing + + f1 = 1.095 * np.exp(0.097041 / Rg) + f2 = 0.007584 * capillary_num * np.exp(0.76246 / Rg) - capillary_num * 0.002 + 0.012 + if capillary_nested > 0: + f2 += 0.0045 * np.exp(-4.1589 / (capillary_nested * Rg)) + + R_eff = f1 * core_radius * (1 - f2 * wl**2 / (core_radius * capillary_thickness)) + + n_eff_2 = n_gas_2 - (u * wl / (pipi * R_eff)) ** 2 + + chi_sil = materials.Sellmeier.load("silica").chi(wl) + + with np.errstate(divide="ignore", invalid="ignore"): + for m, strength in enumerate(capillary_resonance_strengths): + n_eff_2 += ( + strength + * wl**2 + / (alpha + wl**2 - chi_sil * (2 * capillary_thickness / (m + 1)) ** 2) + ) + + return np.sqrt(n_eff_2) + + +def effective_area_hasan(core_radius, capillary_num, capillary_spacing): + """ + computed the effective mode area + + Parameters + ---------- + core_radius : float + radius of the core (m) (from cented to edge of a capillary) + capillary_num : int + number of capillaries + capillary_spacing : float + spacing between capillaries (m) + + Returns + ------- + effective_area: float + """ + M_f = 1.5 / (1 - 0.5 * np.exp(-0.245 * capillary_num)) + return M_f * core_radius**2 * np.exp((capillary_spacing / 22e-6) ** 2.5) + + +@overload +def V_eff_step_index( + wl: np.ndarray, + core_radius: float, + numerical_aperture: float, + wavelength_window: tuple[float, float] | None = None, +) -> np.ndarray: ... +@overload +def V_eff_step_index( + wl: Num, + core_radius: float, + numerical_aperture: float, + wavelength_window: tuple[float, float] | None = None, +) -> float: ... +def V_eff_step_index( + wl, + core_radius: float, + numerical_aperture: float, + wavelength_window: tuple[float, float] | None = None, +): + """computes the V parameter of a step-index fiber + + Parameters + ---------- + l : T + wavelength + core_radius : float + radius of the core + numerical_aperture : float + as a decimal number + wavelength_window : tuple[float, float], optional + when provided, only computes V over this range, wavelengths outside this range will + yield V=inf, by default None + + Returns + ------- + T + V parameter + """ + pi2cn = 2 * math.pi * core_radius * numerical_aperture + if wavelength_window is not None and isinstance(wl, np.ndarray): + low, high = wavelength_window + wl = np.where((wl >= low) & (wl <= high), wl, np.inf) + return pi2cn / wl + + +def V_parameter_koshiba(wl: np.ndarray, pcf_pitch: float, pcf_pitch_ratio: float) -> np.ndarray: + """ + returns the V parameter according to Koshiba2004 + + + Parameters + ---------- + l : np.ndarray, shape (n,) + wavelength + pcf_pitch : float + distance between air holes in m + pcf_pitch_ratio : float + ratio diameter of holes / distance + w0 : float + pump angular frequency + + Returns + ------- + float + effective mode field area + """ + ratio_l = wl / pcf_pitch + n_co = 1.45 + r_eff = pcf_pitch / np.sqrt(3) + pi2a = pipi * r_eff + A, _ = saitoh_paramters(pcf_pitch_ratio) + + V = A[0] + A[1] / (1 + A[2] * np.exp(A[3] * ratio_l)) + + n_FSM2 = 1.45**2 - (wl * V / (pi2a)) ** 2 + V_eff = pi2a / wl * np.sqrt(n_co**2 - n_FSM2) + + return V_eff + + +def effective_area_pcf(pcf_pitch: float): + return math.pi / 3 * pcf_pitch**2 + + +def effective_area_from_V(core_radius: float, V_eff: np.ndarray) -> np.ndarray: + """ + According to Marcuse1978 + + Parameters + ---------- + core_radius : float + in m + V_eff : T + effective V parameter. + + Returns + ------- + T + effective_area + + """ + out = np.ones_like(V_eff) + out[V_eff > 0] = core_radius * ( + 0.65 + 1.619 / V_eff[V_eff > 0] ** 1.5 + 2.879 / V_eff[V_eff > 0] ** 6 + ) + return out + + +def beta(w: np.ndarray, n_eff: np.ndarray) -> np.ndarray: + return n_eff * w / units.c + + +def beta1(w: np.ndarray, n_eff: np.ndarray) -> np.ndarray: + return np.gradient(beta(w, n_eff), w[1] - w[0]) + + +def beta2(w: np.ndarray, n_eff: np.ndarray) -> np.ndarray: + """ + computes the dispersion parameter beta2 according to the effective refractive + index of the fiber and the frequency range + + Parameters + ---------- + w : ndarray, shape (n, ) + angular frequencies over which to calculate the dispersion + n_eff : ndarray_ shape (n, ) + effective refractive index of the fiber computed with one of the n_eff_* methods + + Returns + ------- + beta2 : ndarray, shape (n, ) + """ + dw = w[1] - w[0] + return np.gradient(np.gradient(beta(w, n_eff), dw), dw) + + +def frame_velocity(beta1_arr: np.ndarray, w0_ind: int) -> float: + return 1.0 / beta1_arr[w0_ind] + + +def HCPCF_dispersion( + wl, + gas_name: str, + model="marcatili", + pressure=None, + temperature=None, + **model_params, +): + """ + returns the dispersion profile (beta_2) of a hollow-core photonic crystal fiber. + + Parameters + ---------- + l : ndarray, shape (n, ) + wavelengths over which to calculate the dispersion + gas_name : str + name of the filling gas in lower case + model : string {"marcatili", "marcatili_adjusted", "hasan"} + which model of effective refractive index to use + model_params : tuple + to be passed on to the function in charge of computing the effective index of the fiber. + Every n_eff_* function has a signature + `n_eff_(l, n_gas_2, radius, *args)` and model_params corresponds to args + temperature : float + Temperature of the material + pressure : float + constant pressure + + Returns + ------- + out : 1D array + beta2 as function of wavelength + """ + + w = units.m_rads(wl) + n_gas_2 = materials.Sellmeier.load(gas_name).n_gas_2(wl, temperature, pressure) + + n_eff_func = dict( + marcatili=n_eff_marcatili, marcatili_adjusted=n_eff_marcatili_adjusted, hasan=n_eff_hasan + )[model] + n_eff = n_eff_func(wl, n_gas_2, **model_params) + + return beta2(w, n_eff) + + +@overload +def gamma_parameter(n2: float, w0: float, effective_area: np.ndarray) -> np.ndarray: ... +@overload +def gamma_parameter(n2: float, w0: float, effective_area: Num) -> float: ... +def gamma_parameter(n2: float, w0: float, effective_area): + if isinstance(effective_area, np.ndarray): + effective_area_term = np.sqrt(effective_area * effective_area[0]) + return np.divide( + n2 * w0, + effective_area_term * units.c, + where=effective_area_term != 0, + out=effective_area_term, + ) + + return n2 * w0 / (effective_area * units.c) + + +def constant_effective_area_arr(wl: np.ndarray, effective_area: float) -> np.ndarray: + return np.ones_like(wl) * effective_area + + +def n_eff_pcf(wl: np.ndarray, pcf_pitch: float, pcf_pitch_ratio: float) -> np.ndarray: + """ + semi-analytical computation of the dispersion profile of a triangular Index-guiding PCF + + Parameters + ---------- + l : np.ndarray, shape (n,) + wavelengths over which to calculate the dispersion + pcf_pitch : float + distance between air holes in m + pcf_pitch_ratio : float + ratio diameter of hole / pcf_pitch + + Returns + ------- + n_eff : np.ndarray, shape (n,) + effective index of refraction + + Reference + --------- + Formulas and values are from Saitoh K and Koshiba M, "Empirical relations for + simple design of photonic crystal fibers" (2005) + """ + # Check validity + if pcf_pitch_ratio < 0.2 or pcf_pitch_ratio > 0.8: + warnings.warn( + "fitted formula for PCF dispersion is valid only for pcf_pitch_ratio between" + f" 0.2 and 0.8, {pcf_pitch_ratio:0.6f} was specified" + ) + + r_eff = pcf_pitch / np.sqrt(3) + pi2a = pipi * r_eff + + ratio_l = wl / pcf_pitch + + A, B = saitoh_paramters(pcf_pitch_ratio) + + V = A[0] + A[1] / (1 + A[2] * np.exp(A[3] * ratio_l)) + W = B[0] + B[1] / (1 + B[2] * np.exp(B[3] * ratio_l)) + + n_FSM2 = 1.45**2 - (wl * V / (pi2a)) ** 2 + n_eff2 = (wl * W / (pi2a)) ** 2 + n_FSM2 + n_eff = np.sqrt(n_eff2) + + chi_mat = materials.Sellmeier.load("silica").chi(wl) + return n_eff + np.sqrt(chi_mat + 1) + + +def effective_area_from_diam(effective_mode_diameter: float) -> float: + return math.pi * (effective_mode_diameter / 2) ** 2 + + +def effective_area_from_gamma(gamma: float, n2: float, w0: float): + return n2 * w0 / (units.c * gamma) + + +def saitoh_paramters(pcf_pitch_ratio: float) -> tuple[np.ndarray, np.ndarray]: + # Table 1 and 2 in Saitoh2005 + ai0 = np.array([0.54808, 0.71041, 0.16904, -1.52736]) + ai1 = np.array([5.00401, 9.73491, 1.85765, 1.06745]) + ai2 = np.array([-10.43248, 47.41496, 18.96849, 1.93229]) + ai3 = np.array([8.22992, -437.50962, -42.4318, 3.89]) + bi1 = np.array([5, 1.8, 1.7, -0.84]) + bi2 = np.array([7, 7.32, 10, 1.02]) + bi3 = np.array([9, 22.8, 14, 13.4]) + ci0 = np.array([-0.0973, 0.53193, 0.24876, 5.29801]) + ci1 = np.array([-16.70566, 6.70858, 2.72423, 0.05142]) + ci2 = np.array([67.13845, 52.04855, 13.28649, -5.18302]) + ci3 = np.array([-50.25518, -540.66947, -36.80372, 2.7641]) + di1 = np.array([7, 1.49, 3.85, -2]) + di2 = np.array([9, 6.58, 10, 0.41]) + di3 = np.array([10, 24.8, 15, 6]) + + A = ai0 + ai1 * pcf_pitch_ratio**bi1 + ai2 * pcf_pitch_ratio**bi2 + ai3 * pcf_pitch_ratio**bi3 + B = ci0 + ci1 * pcf_pitch_ratio**di1 + ci2 * pcf_pitch_ratio**di2 + ci3 * pcf_pitch_ratio**di3 + return A, B + + +def load_custom_effective_area(effective_area_file: io.DataFile, new_wl: np.ndarray) -> np.ndarray: + """ + loads custom effective area file + + Parameters + ---------- + effective_area_file : str + relative or absolute path to the file + l : np.ndarray, shape (n,) + wavelength array of the simulation + + Returns + ------- + np.ndarray, shape (n,) + wl-dependent effective mode field area + """ + wl, effective_area = effective_area_file.load_arrays( + ("wavelength", "wl"), ("A_eff", "effective_area") + ) + return np.interp(new_wl, wl, effective_area) + + +def load_custom_dispersion( + wl: np.ndarray, dispersion_file: io.DataFile +) -> tuple[np.ndarray, np.ndarray]: + """loads a custom dispersion and returns beta2(l) where it is specified, as well as indices""" + out = np.zeros_like(wl) + + wl_file, D = dispersion_file.load_arrays(("wavelength", "wl"), "dispersion") + wl_file = units.normalize_wavelengths(wl_file) + + ind = np.where((wl >= wl_file.min()) & (wl <= wl_file.max()))[0] + out[ind] = np.interp(wl[ind], wl_file, dispersion_parameter_to_beta2(D, wl_file)) + return out, ind + + +def load_custom_loss(new_wl: np.ndarray, loss_file: io.DataFile) -> np.ndarray: + """ + loads a npz loss file that contains a wavelength and a loss entry + + Parameters + ---------- + l : np.ndarray, shape (n,) + wavelength array of the simulation + loss_file : str + relative or absolute path to the loss file + + Returns + ------- + np.ndarray, shape (n,) + loss in 1/m units + """ + wl, loss = loss_file.load_arrays(("wavelength", "wl"), "loss") + wl = units.normalize_wavelengths(wl) + return np.interp(new_wl, wl, loss, left=0, right=0) + + +def auto_dispersion_coefficients( + w_c: np.ndarray, + dispersion_ind: np.ndarray, + beta2_arr: np.ndarray, + interpolation_degree: int, +) -> np.ndarray: + return dispersion_coefficients( + w_c[dispersion_ind], beta2_arr[dispersion_ind], interpolation_degree + ) + + +def dispersion_coefficients( + w_c: np.ndarray, + beta2_arr: np.ndarray, + interpolation_degree: int, +): + """ + Computes the taylor expansion of beta2 to be used in dispersion_op + + Parameters + ---------- + w : np.ndarray, shape (n,) + angular frequencies array + beta2 : np.ndarray, shape (n,) + beta2 as function of w + w0 : float + pump angular frequency + interpolation_degree : int + degree of polynomial fit. Will return deg+1 coefficients + + Returns + ------- + beta2_coef : 1D array + Taylor coefficients in decreasing order + """ + + # we get the beta2 Taylor coeffiecients by making a fit around w0 + if interpolation_degree < 2: + raise ValueError(f"interpolation_degree must be at least 2, got {interpolation_degree}") + + fit = Chebyshev.fit(w_c, beta2_arr, interpolation_degree) + poly_coef = cheb2poly(fit.convert().coef) + beta2_coef = poly_coef * np.cumprod([1] + list(range(1, interpolation_degree + 1))) + + return beta2_coef + + +def dispersion_from_coefficients( + w_c: np.ndarray, beta2_coefficients: Sequence[float] +) -> np.ndarray: + """ + computes the dispersion profile (beta2) from the beta coefficients + + Parameters + ---------- + w_c : np.ndarray, shape (n, ) + centered angular frequency (0 <=> pump frequency) + beta2_coefficients : Iterable[float] + beta coefficients + + Returns + ------- + np.ndarray, shape (n, ) + beta2 as function of w_c + """ + + coef = np.array(beta2_coefficients) / np.cumprod([1] + list(range(1, len(beta2_coefficients)))) + beta_arr = np.zeros_like(w_c) + for k, b in reversed(list(enumerate(coef))): + beta_arr = beta_arr + b * w_c**k + return beta_arr + + +def raman_fraction(raman_type: str) -> float: + if raman_type == "agrawal": + return 0.245 + else: + return 0.18 + + +def delayed_raman_t(t: np.ndarray, raman_type: str) -> np.ndarray: + """ + computes the unnormalized temporal Raman response function applied to the array t + + Parameters + ---------- + t : 1D array + time in the co-moving frame of reference + raman_type : str {"stolen", "agrawal", "measured"} + indicates what type of Raman effect modelization to use + + Returns + ------- + hr_arr : 1D array + temporal response function + """ + tau1 = 12.2e-15 + tau2 = 32e-15 + t_ = t - t[0] + t = t_ + if raman_type == "stolen": + hr_arr = (tau1 / tau2**2 + 1 / tau1) * np.exp(-t_ / tau2) * np.sin(t_ / tau1) + + elif raman_type == "agrawal": + taub = 96e-15 + h_a = (tau1 / tau2**2 + 1 / tau1) * np.exp(-t_ / tau2) * np.sin(t_ / tau1) + h_b = (2 * taub - t_) / taub**2 * np.exp(-t_ / taub) + hr_arr = 0.79 * h_a + 0.21 * h_b + + elif raman_type == "measured": + try: + file = io.data_file("raman_response.npy") + t_stored, hr_arr_stored = np.load(file) + except FileNotFoundError: + print("Not able to find the measured Raman response function. Going with agrawal model") + return delayed_raman_t(t, raman_type="agrawal") + + hr_arr: np.ndarray = np.interp(t, t_stored, hr_arr_stored, left=0, right=0) + else: + raise ValueError("invalid raman response function, aborting") + + return hr_arr + + +def delayed_raman_w(t: np.ndarray, raman_type: str) -> tuple[np.ndarray, float]: + """ + returns the delayed raman response function as function of w + see delayed_raman_t for detailes as well as the raman fraction + """ + hr_w = fft(delayed_raman_t(t, raman_type)) * (t[1] - t[0]) + return hr_w, raman_fraction(raman_type) + + +def fast_poly_dispersion_op(w_c, beta_arr, power_fact_arr, where=slice(None)): + """ + dispersive operator + + Parameters + ---------- + w_c : 1d array + angular frequencies centered around 0 + beta_arr : 1d array + beta coefficients returned by scgenerator.physics.fiber.dispersion_coefficients + power_fact : list of arrays of len == len(w_c) + precomputed values for w_c^k / k! + where : slice-like + indices over which to apply the operator, otherwise 0 + + Returns + ------- + array of len == len(w_c) + dispersive component + """ + + dispersion = _fast_disp_loop(np.zeros_like(w_c), beta_arr, power_fact_arr) + + out = np.zeros_like(dispersion) + out[where] = dispersion[where] + return -1j * out + + +def _fast_disp_loop(dispersion: np.ndarray, beta_arr, power_fact_arr): + for k in range(len(beta_arr) - 1, -1, -1): + dispersion = dispersion + beta_arr[k] * power_fact_arr[k] + return dispersion + + +def direct_dispersion(w: np.ndarray, w0: float, n_eff: np.ndarray) -> np.ndarray: + """ + returns the dispersive operator in direct form (without polynomial interpolation) + i.e. -1j * (beta(w) - beta1 * (w - w0) - beta0) + + Parameters + ---------- + w : np.ndarray + angular frequency array + w0 : float + center frequency + n_eff : np.ndarray + effectiv refractive index + + Returns + ------- + np.ndarray + dispersive operator + """ + w0_ind = math.argclosest(w, w0) + return fast_direct_dispersion(w, w0, n_eff, w0_ind) + + +def fast_direct_dispersion(w: np.ndarray, w0: float, n_eff: np.ndarray, w0_ind: int) -> np.ndarray: + beta_arr = beta(w, n_eff) + beta1_arr = np.gradient(beta_arr, w) + return -1j * (beta_arr - beta1_arr[w0_ind] * (w - w0) - beta_arr[w0_ind]) + + +def effective_core_radius(wl, core_radius, s=0.08, h=200e-9): + """ + return the variable core radius according to Eq. S2.2 from Köttig2017 + + Parameters + ---------- + l : ndarray, shape (n, ) + array of wl over which to calculate the effective core radius + core_radius : float + physical core radius in m + s : float + s parameter from the equation S2.2 + h : float + wall thickness in m + + Returns + ------- + effective_core_radius : ndarray, shape (n, ) + """ + return core_radius / (1 + s * wl**2 / (core_radius * h)) + + +def effective_radius_HCARF(core_radius, t, f1, f2, wl): + """eq. 3 in Hasan 2018""" + return f1 * core_radius * (1 - f2 * wl**2 / (core_radius * t)) + + +def scalar_loss(alpha: float, w_num: int) -> np.ndarray: + """simple, wavelength independent scalar loss""" + return alpha * np.ones(w_num) + + +def capillary_loss(wl: np.ndarray, he_mode: tuple[int, int], core_radius: float) -> np.ndarray: + """ + computes the wavelenth dependent capillary loss according to Marcatili + + Parameters + ---------- + wl : np.ndarray, shape (n, ) + wavelength array + he_mode : tuple[int, int] + tuple of mode (n, m) + core_radius : float + in m + + Returns + ------- + np.ndarray + loss in 1/m + """ + chi_silica = abs(materials.Sellmeier.load("silica").chi(wl)) + # the real loss alpha is 2*Im(n_eff), which differs from the notation of the paper + nu_n = (chi_silica + 2) / np.sqrt(chi_silica) + return nu_n * (math.u_nm(*he_mode) * wl / pipi) ** 2 * core_radius**-3 + + +def safe_capillary_loss( + wl: np.ndarray, + dispersion_ind: np.ndarray, + w_num: int, + core_radius: float, + he_mode: tuple[int, int], +) -> np.ndarray: + alpha = capillary_loss(wl[dispersion_ind], he_mode, core_radius) + loss_arr = np.zeros(w_num) + loss_arr[dispersion_ind] = alpha + return loss_arr + + +def extinction_distance(loss, ratio=1 / e): + return np.log(ratio) / -loss + + +def L_eff(loss, length: float): + return -np.expm1(-loss * length) / loss + + +def core_radius_from_capillaries(tube_radius: float, gap: float, n_tubes: int) -> float: + k = 1 + 0.5 * gap / tube_radius + return tube_radius * (k / np.sin(np.pi / n_tubes) - 1) + + +def gap_from_capillaries(core_radius: float, tube_radius: float, n_tubes: int) -> float: + s = np.sin(np.pi / n_tubes) + return 2 * (s * (tube_radius + core_radius) - tube_radius) + + +def tube_radius_from_gap(core_radius: float, gap: float, n_tubes: int) -> float: + s = np.sin(np.pi / n_tubes) + return (core_radius * s - 0.5 * gap) / (1 - s) + + +def normalized_frequency_vincetti( + wl: np.ndarray, thickness: float, n_clad_2: np.ndarray, n_gas_2: np.ndarray +) -> np.ndarray: + """ + eq. 3 of [1] in n_eff_vincetti + + Parameters + ---------- + wl : ndarray + wavelength array + thickness : float + thickness of the capillary tube + n_clad_2 : ndarray + real refractive index of the cladding squared corresponding to wavelengths in wl + n_gas_2 : ndarray + real refractive index of the filling gas squared + """ + return 2 * thickness / wl * np.sqrt(n_clad_2 - n_gas_2) + + +def effective_core_radius_vincetti( + wl: np.ndarray, f: np.ndarray, r: float, g: float, n: int +) -> np.ndarray: + """ + Parameters + ---------- + wl : ndarray + wavelength array + f : ndarray + corresponding normalized frequency + r : float + capillary external radius + g : float + gap size bewteen capillaries + n : int + number of tubes + """ + r_co = core_radius_from_capillaries(r, g, n) + factor = 1.027 + 0.001 * (f + 2 / f**4) + # | Missing in paper + # V + inner = r_co**2 + n / np.pi * 0.046875 * r**2 * (1 + (3 + 20 * wl / r_co) * g / r) + return factor * np.sqrt(inner) + + +def li_vincetti(f_2, f0_2): + k = f0_2 - f_2 + return k / (k**2 + 9e-4 * f_2) + + +@overload +def cutoff_frequency_he_vincetti( + mu: int, nu: int, t_ratio: float, n_clad_0: np.ndarray +) -> np.ndarray: ... +@overload +def cutoff_frequency_he_vincetti(mu: int, nu: int, t_ratio: float, n_clad_0: Num) -> float: ... +def cutoff_frequency_he_vincetti(mu: int, nu: int, t_ratio: float, n_clad_0: float) -> np.ndarray: + """ + eq. (4) in [2] of n_eff_vincetti + Parameters + ---------- + mu : int + azimuthal mode number + nu : int + radial mode number + t_ratio : float + t/r_ext + n_clad_0 : float + refractive index of the cladding material, generally at the pump wavelength + """ + if nu == 1: + base = np.abs(0.21 + 0.175 * mu - 0.1 * (mu - 0.35) ** -2) + corr = 0.04 * np.sqrt(mu) * t_ratio + return base * t_ratio ** (0.55 + 5e-3 * np.sqrt(n_clad_0**4 - 1)) + corr + elif nu >= 2: + return 0.3 / n_clad_0**0.3 * (0.5 * nu) ** -1.2 * np.abs(mu - 0.8) * t_ratio + nu - 1 + else: + raise ValueError(f"nu must be a strictly positive integer, got {nu}") + + +@overload +def cutoff_frequency_eh_vincetti( + mu: int, nu: int, t_ratio: float, n_clad_0: np.ndarray +) -> np.ndarray: ... +@overload +def cutoff_frequency_eh_vincetti(mu: int, nu: int, t_ratio: float, n_clad_0: Num) -> float: ... +def cutoff_frequency_eh_vincetti(mu: int, nu: int, t_ratio: float, n_clad_0): + """ + eq. (5) in [2] of n_eff_vincetti + Parameters + ---------- + mu : int + azimuthal mode number + nu : int + radial mode number + t_ratio : float + t/r_ext + n_clad_0 : float + refractive index of the cladding material, generally at the pump wavelength + """ + if nu == 1: + base = 0.73 + 0.1425 * (mu**0.8 + 1.5) - 0.04 / (mu - 0.35) + expo = 0.5 - 0.1 * (n_clad_0 - 1) * (mu + 0.5) ** -0.1 + corr = 0 + elif nu >= 2: + base = ( + (11.5 * nu**-1.2 / (7.75 - nu)) + * (0.34 + 0.25 * mu * (n_clad_0 / 1.2) ** 1.15) + * (mu + 0.2 / n_clad_0) ** -0.15 + ) + expo = 0.75 + 0.06 * n_clad_0**-1.15 + 0.1 * np.sqrt(1.44 / n_clad_0) * (nu - 2) + corr = nu - 1 + else: + raise ValueError(f"nu must be a positive integer, got {nu}") + return base * t_ratio**expo + corr + + +def v_sum_vincetti( + f: np.ndarray, t_ratio: float, n_clad_0: float | np.ndarray, n_terms: int +) -> np.ndarray: + f_2 = f**2 + out = np.zeros_like(f) + for nu in range(1, n_terms + 1): + out[:] += li_vincetti(f_2, cutoff_frequency_he_vincetti(1, nu, t_ratio, n_clad_0) ** 2) + out[:] += li_vincetti(f_2, cutoff_frequency_eh_vincetti(1, nu, t_ratio, n_clad_0) ** 2) + out *= 2e3 + return out + + +def n_eff_correction_vincetti( + wl: np.ndarray, f: np.ndarray, t_ratio: float, r_co: float, n_clad_0: float, n_terms: int +) -> np.ndarray: + """ + eq. 6 from [1] in n_eff_vincetti + + Parameters + ---------- + wl : np.ndarray + wavelength array + f : np.ndarray + corresponding normalized frequency + t_ratio : float + t (tube thickness) / r (external tube radius) + r_co : float + core radius + n_clad_0 : float + refractive index of the cladding (usu. at pump wavelength) + n_terms : int + how many resonances to calulcate + """ + factor = 4.5e-7 / (1 - t_ratio) ** 4 * (wl / r_co) ** 2 + return factor * v_sum_vincetti(f, t_ratio, n_clad_0, n_terms) + + +def n_eff_vincetti( + wl: np.ndarray, + wavelength: float, + n_gas_2: np.ndarray, + thickness: float, + tube_radius: float, + gap: float, + n_tubes: int, + n_terms: int = 8, + n_clad_2: np.ndarray | None = None, +): + """ + Parameters + ---------- + l : ndarray + wavelength (m) array over which to compute the refractive index + wavelength : float + center wavelength / pump wavelength + n_gas_2 : ndarray + n^2 of the filling gas + thickness : float + thickness of the structural capillary tubes + tube_radius : float + external radius of the strucural capillaries + gap : float + gap between the structural capillary tubes + n_tubes : int + number of capillary tubes + n_terms : int + number of resonances to calculate, by default 8 + n_clad_2: ndarray | None + n^2 of the cladding, by default Silica + + Returns + ------- + effective refractive index according to the Vincetti model + + + + Internal symbols help + --------------------- + f: normalized frequency + r_co_eff: effective core radius + + + References + ---------- + [1] ROSA, Lorenzo, MELLI, Federico, et VINCETTI, Luca. Analytical Formulas for Dispersion and + Effective Area in Hollow-Core Tube Lattice Fibers. Fibers, 2021, vol. 9, no 10, p. 58. + [2] VINCETTI, Luca et ROSA, Lorenzo. A simple analytical model for confinement loss estimation + kin hollow-core Tube Lattice Fibers. Optics Express, 2019, vol. 27, no 4, p. 5230-5237. + + """ + + if n_clad_2 is None: + n_clad_2 = materials.Sellmeier.load("silica").n_gas_2(wl) + + n_clad_0 = np.sqrt(n_clad_2[math.argclosest(wl, wavelength)]) + + f = normalized_frequency_vincetti(wl, thickness, n_clad_2, n_gas_2) + r_co_eff = effective_core_radius_vincetti(wl, f, tube_radius, gap, n_tubes) + r_co = core_radius_from_capillaries(tube_radius, gap, n_tubes) + d_n_eff = n_eff_correction_vincetti(wl, f, thickness / tube_radius, r_co, n_clad_0, n_terms) + + n_gas = np.sqrt(n_gas_2) + + # eq. (21) in [1] + return n_gas - 0.125 / n_gas * (math.u_nm(1, 1) * wl / (np.pi * r_co_eff)) ** 2 + d_n_eff diff --git a/src/dispersionapp/physics/materials.py b/src/dispersionapp/physics/materials.py new file mode 100644 index 0000000..299245b --- /dev/null +++ b/src/dispersionapp/physics/materials.py @@ -0,0 +1,445 @@ +from __future__ import annotations + +import warnings +from dataclasses import dataclass, field +from functools import cache +from typing import Any, overload + +import numpy as np + +import dispersionapp.io as io +import dispersionapp.math as math +import dispersionapp.physics.units as units +from dispersionapp.physics.units import NA, c, epsilon0, kB + +type Num = np.floating | np.integer | float | int + + +@dataclass +class Sellmeier: + B: list[float] = field(default_factory=list) + C: list[float] = field(default_factory=list) + pressure_ref: float = 101325 + temperature_ref: float = 273.15 + kind: int = 2 + constant: float = 0 + + @classmethod + @cache + def load(cls, name: str) -> Sellmeier: + mat_dico = io.load_material_dico(name) + s = mat_dico.get("sellmeier", {}) + return cls( + **{ + newk: s.get(k, None) + for newk, k in zip( + ["B", "C", "pressure_ref", "temperature_ref", "kind", "constant"], + ["B", "C", "P0", "T0", "kind", "const"], + ) + if k in s + } + ) + + @overload + def chi( + self, wl: np.ndarray, temperature: float | None = None, pressure: float | None = None + ) -> np.ndarray: ... + @overload + def chi( + self, wl: Num, temperature: float | None = None, pressure: float | None = None + ) -> Num: ... + def chi(self, wl, temperature: float | None = None, pressure: float | None = None): + """n^2 - 1""" + if isinstance(wl, np.ndarray): + chi = np.zeros_like(wl) # = n^2 - 1 + else: + chi = 0 + if self.kind == 1: + for b, c_ in zip(self.B, self.C): + chi += wl**2 * b / (wl**2 - c_) + elif self.kind == 2: # gives n-1 + for b, c_ in zip(self.B, self.C): + chi += b / (c_ - 1 / wl**2) + chi += self.constant + chi = (chi + 1) ** 2 - 1 + elif self.kind == 3: # Schott formula + for i, b in reversed(list(enumerate(self.B))): + chi += b * wl ** (-2 * (i - 1)) + chi = chi - 1 + else: + raise ValueError(f"kind {self.kind} is not recognized.") + + if temperature is not None: + chi *= self.temperature_ref / temperature + + if pressure is not None: + chi *= pressure / self.pressure_ref + return chi + + @overload + def n_gas_2( + self, wl: np.ndarray, temperature: float | None = None, pressure: float | None = None + ) -> np.ndarray: ... + @overload + def n_gas_2( + self, wl: Num, temperature: float | None = None, pressure: float | None = None + ) -> float: ... + def n_gas_2(self, wl, temperature: float | None = None, pressure: float | None = None): + return self.chi(wl, temperature, pressure) + 1 + + @overload + def n( + self, wl: np.ndarray, temperature: float | None = None, pressure: float | None = None + ) -> np.ndarray: ... + @overload + def n( + self, wl: Num, temperature: float | None = None, pressure: float | None = None + ) -> float: ... + def n(self, wl, temperature: float | None = None, pressure: float | None = None): + return np.sqrt(self.n_gas_2(wl, temperature, pressure)) + + def delta(self, wl: np.ndarray, wl_zero_disp: float) -> np.ndarray: + """ + 'delta' quantity that describes the gas dispersion according to eq. S7 in [1] + + Parameters + ---------- + wl : ndarray + wavelength in m + wl_zero_disp : float + zero dispersion wavelength in m + + Reference + --------- + [1] TRAVERS, John C., GRIGOROVA, Teodora F., BRAHMS, Christian, et al. High-energy + pulse self-compression and ultraviolet generation through soliton dynamics in hollow + capillary fibres. Nature Photonics, 2019, vol. 13, no 8, p. 547-554. + """ + factor = (math.u_nm(1, 1) / c) ** 2 * (0.5 * wl / np.pi) ** 3 + f = np.gradient(np.gradient(self.chi(wl), wl), wl) + ind = math.argclosest(wl, wl_zero_disp) + return factor * (f / f[ind] - 1) + + +class Gas: + name: str + sellmeier: Sellmeier + atomic_number: int | float + atomic_mass: float + chi3_0: float + ionization_energy: float | None + + _raw_sellmeier: dict[str, Any] + _kerr_wl: np.ndarray + + def __init__(self, gas_name: str): + self.name = gas_name + self._raw_sellmeier = io.load_material_dico(gas_name) + self.atomic_mass = self._raw_sellmeier["atomic_mass"] + self.atomic_number = self._raw_sellmeier["atomic_number"] + self.ionization_energy = self._raw_sellmeier.get("ionization_energy") + + s = self._raw_sellmeier.get("sellmeier", {}) + self.sellmeier = Sellmeier( + **{ + newk: s.get(k, None) + for newk, k in zip( + ["B", "C", "pressure_ref", "temperature_ref", "kind", "constant"], + ["B", "C", "P0", "T0", "kind", "const"], + ) + if k in s + } + ) + kerr = self._raw_sellmeier["kerr"] + n2_0 = kerr["n2"] + self._kerr_wl = kerr.get("wavelength", 800e-9) + self.chi3_0 = ( + 4 + / 3 + * units.epsilon0 + * units.c + * self.sellmeier.n_gas_2(self._kerr_wl, kerr["T0"], kerr["P0"]) + * n2_0 + ) + + def pressure_from_relative_density( + self, density: float, temperature: float | None = None + ) -> float: + temperature = temperature or self.sellmeier.temperature_ref + return self.sellmeier.temperature_ref / temperature * density * self.sellmeier.pressure_ref + + def density_factor(self, temperature: float, pressure: float, ideal_gas: bool) -> float: + """ + returns the number density relative to reference values + + Parameters + ---------- + temperature : float + target temperature in K + pressure : float + target pressure in Pa + ideal_gas : bool + whether to use ideal gas law + + Returns + ------- + float + N / N_0 + """ + if ideal_gas: + return ( + pressure + / self.sellmeier.pressure_ref + * self.sellmeier.temperature_ref + / temperature + ) + else: + return self.number_density_van_der_waals( + pressure, temperature + ) / self.number_density_van_der_waals( + self.sellmeier.pressure_ref, + self.sellmeier.temperature_ref, + ) + + def number_density( + self, + temperature: float | None = None, + pressure: float | None = None, + ideal_gas: bool = False, + ) -> float: + """ + returns the number density in 1/m^3 using van der Waals equation + + Parameters + ---------- + temperature : float, optional + temperature in K, by default None + pressure : float, optional + pressure in Pa, by default None + + Returns + ------- + float + number density in 1/m^3 + """ + pressure = pressure or self.sellmeier.pressure_ref + temperature = temperature or self.sellmeier.temperature_ref + if ideal_gas: + return pressure / temperature / kB + else: + return self.number_density_van_der_waals(pressure, temperature) + + def number_density_van_der_waals( + self, pressure: float | None = None, temperature: float | None = None + ): + """ + returns the number density of a gas + + Parameters + ---------- + pressure : float, optional + pressure in Pa, by default the reference pressure + temperature : float, optional + temperature in K, by default the reference temperature + + Returns + ---------- + the numbers density (/m^3) + + Raises + ---------- + ValueError : Since the Van der Waals equation is a cubic one, there could be more than one + real, positive solution + """ + + if pressure == 0: + return 0 + a = self._raw_sellmeier.get("a", 0) + b = self._raw_sellmeier.get("b", 0) + pressure = ( + self._raw_sellmeier["sellmeier"].get("P0", 101325) if pressure is None else pressure + ) + temperature = ( + self._raw_sellmeier["sellmeier"].get("T0", 273.15) + if temperature is None + else temperature + ) + ap = a / NA**2 + bp = b / NA + + # setup van der Waals equation for the number density + p3 = -ap * bp + p2 = ap + p1 = -(pressure * bp + kB * temperature) + p0 = pressure + + # filter out unwanted matches + roots = np.roots([p3, p2, p1, p0]) + roots = roots[np.isreal(roots)].real + roots = roots[roots > 0] + if len(roots) != 1: + s = f"Van der Waals eq with parameters P={pressure}, T={temperature}, a={a}, b={b}" + s += f", There is more than one possible number density : {roots}." + s += f", {np.min(roots)} was returned" + warnings.warn(s) + return np.min(roots) + + def chi3(self, temperature: float | None = None, pressure: float | None = None) -> float: + """nonlinear susceptibility""" + + # if pressure and/or temperature are specified, adjustment is made + # according to number density ratio + if pressure is not None or temperature is not None: + N0 = self.number_density_van_der_waals() + N = self.number_density_van_der_waals(pressure, temperature) + ratio = N / N0 + else: + ratio = 1 + return ratio * self.chi3_0 + + def n2(self, temperature: float | None = None, pressure: float | None = None) -> float: + sellmeier = self.sellmeier.n_gas_2(self._kerr_wl, temperature, pressure) + chi3 = self.chi3(temperature, pressure) + return 0.75 * chi3 / (units.epsilon0 * units.c * sellmeier) + + @property + def ionic_charge(self): + return 1 + + @property + def barrier_suppression(self) -> float: + """ + Compute the barrier suppression threshold (W/m^2) + """ + field_max = self.barrier_suppression_field + return 0.5 * units.c * units.epsilon0 * field_max**2 + + @property + def barrier_suppression_field(self) -> float: + """ + Barrier suppression threshold electric field + + Reference + --------- + [1] KRAINOV, V. P. Theory of barrier-suppression ionization of atoms. Journal of Nonlinear + Optical Physics & Materials, 1995, vol. 4, no 04, p. 775-798. + [2] BETHE, H. A. et SALPETER, E. E. Quantum mechanics of one-and two-electron atoms. 1977. + """ + # from [1-2] : + # field_max = 0.0625 / self.ionic_charge * (self.ionization_energy / 2.1787e-18) ** 2 + # return field_max = 5.14220670712125e11 + + # from [3] : + if self.ionization_energy is None: + raise ValueError(f"no ionization_energy for {self.name}") + Z = self.ionic_charge + Ip_au = self.ionization_energy / 4.359744650021498e-18 + ns = Z / np.sqrt(2 * Ip_au) + return Z**3 / (16 * ns**4) * 5.14220670712125e11 + + def get(self, key, default=None): + return self._raw_sellmeier.get(key, default) + + def __getitem__(self, key): + return self._raw_sellmeier[key] + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.name!r})" + + +def n_gas_2( + wl: np.ndarray, gas_name: str, pressure: float | None, temperature: float | None +) -> np.ndarray: + """Returns the sqare of the index of refraction of the specified gas""" + return Sellmeier.load(gas_name).n_gas_2(wl, temperature, pressure) + + +def pressure_from_gradient(ratio: float, p0: float, p1: float) -> float: + """ + returns the pressure as function of distance with eq. 20 in Markos et al. (2017) + + Parameters + ---------- + ratio : relative position in the fiber (0 = start, 1 = end) + p0 : pressure at the start + p1 : pressure at the end + + Returns + ------- + the pressure (float) + """ + return np.sqrt(p0**2 - ratio * (p0**2 - p1**2)) + + +def delta_gas(w: np.ndarray, gas: Gas) -> np.ndarray: + """ + returns the value delta_t (eq. 24 in Markos(2017)) + + Parameters + ---------- + w : np.ndarray + angular frequency array + gas : Gas + + Returns + ------- + delta_t + since 2 gradients are computed, it is recommended to exclude the 2 extremum values + """ + chi = gas.sellmeier.chi(units.m_rads(w)) + N0 = gas.number_density_van_der_waals() + + dchi_dw = np.gradient(chi, w) + return 1 / (N0 * c) * (dchi_dw + w / 2 * np.gradient(dchi_dw, w)) + + +def gas_n2(gas_name: str, pressure: float, temperature: float) -> float: + """returns the nonlinear refractive index + + Parameters + ---------- + gas_name : str + gas name + pressure : float + pressure in Pa + temperature : float + temperature in K + + Returns + ------- + float + n2 in m2/W + """ + return Gas(gas_name).n2(temperature, pressure) + + +def gas_chi3(gas_name: str, wavelength: float, pressure: float, temperature: float) -> float: + """ + returns the chi3 of a particular material + + Parameters + ---------- + gas_name : str + [description] + pressure : float + [description] + temperature : float + [description] + + Returns + ------- + float + [description] + """ + gas = Gas(gas_name) + n = gas.sellmeier.n(wavelength, temperature, pressure) + n2 = gas.n2(temperature, pressure) + return n2_to_chi3(n2, n) + + +def n2_to_chi3(n2: float, n0: float) -> float: + return n2 * 4 * epsilon0 * n0**2 * c / 3 + + +def chi3_to_n2(chi3: float, n0: float) -> float: + return 3.0 * chi3 / (4.0 * epsilon0 * c * n0**2) diff --git a/src/dispersionapp/physics/pulse.py b/src/dispersionapp/physics/pulse.py new file mode 100644 index 0000000..1ba2546 --- /dev/null +++ b/src/dispersionapp/physics/pulse.py @@ -0,0 +1,1438 @@ +""" +This files incluedes funcitons used by the scgenerator module to compute properties of pulses. +This include computing initial pulse shape and pulse noise as well as transforming the pulse +or measuring its properties. + +NOTE +the term `sc-ordering` is used throughout this module. An array that follows sc-ordering is +of shape `([what, ever,] n, nt)` (could be just `(n, nt)` for 2D sc-ordered array) such that +n is the number of spectra at the same z position and nt is the size of the time/frequency grid +""" + +from __future__ import annotations + +import os +import warnings +from dataclasses import astuple, dataclass +from pathlib import Path +from typing import Any, Literal, Sequence, Tuple, TypeVar + +import numba +import numpy as np +from numpy import pi +from numpy.fft import fft, fftshift, ifft +from scipy.interpolate import UnivariateSpline, interp1d +from scipy.optimize import minimize_scalar +from scipy.optimize._optimize import OptimizeResult + +import dispersionapp.io as io +import dispersionapp.math as math +import dispersionapp.physics.units as units + +plt = None +try: + import matplotlib.pyplot as plt +except ModuleNotFoundError: + pass + +T = TypeVar("T") + +# +fwhm_to_T0_fac = dict( + sech=1 / (2 * np.log(1 + np.sqrt(2))), + gaussian=1 / (np.sqrt(2 * np.log(2))), +) +"""relates the fwhm of the intensity profile (amplitue^2) to the t0 parameter of the amplitude""" + +P0T0_to_E0_fac = dict( + sech=2, # int(a * sech(x / b)^2 * dx) from -inf to inf = 2 * a * b + gaussian=np.sqrt(pi / 2), # int(a * exp(-(x/b)^2)^2 * dx) from -inf to inf = sqrt(pi/2) * a * b +) +""" +relates the total energy (amplitue^2) to the t0 parameter of the +amplitude and the peak intensity (peak_amplitude^2) +""" + + +@dataclass +class PulseProperties: + quality: float + mean_coherence: float + fwhm_noise: float + mean_fwhm: float + peak_rin: float + energy_rin: float + timing_jitter: float + + @classmethod + def header(cls, delimiter: str = ",", quotechar: str = "") -> str: + return delimiter.join( + quotechar + f + quotechar + for f in [ + "quality", + "mean_coherence", + "fwhm_noise", + "mean_fwhm", + "peak_rin", + "energy_rin", + "timing_jitter", + ] + ) + + @classmethod + def save_all( + cls, + destination: os.PathLike, + *props: "PulseProperties", + delimiter: str = ",", + quotechar: str = "", + ): + out = np.zeros((len(props), 7)) + for i, p in enumerate(props): + out[i] = astuple(p) + np.savetxt( + destination, + out, + header=cls.header(delimiter=delimiter, quotechar=quotechar), + delimiter=delimiter, + ) + + @classmethod + def load(cls, path: os.PathLike, delimiter: str = ",") -> list["PulseProperties"]: + arr = np.loadtxt(path, delimiter=delimiter) + return [cls(*a) for a in arr] + + +class ShotNoiseParameter(tuple): + """ + convenience class to handle shot noise parameters when specifying it in the main Parameters + class. Refer to the `shot_noise` function for information of parameters. + """ + + __slots__ = [] + + @classmethod + def validate(cls, _: str, params: Any): + if isinstance(params, Sequence): + params = tuple(params) + if params in {(), (False,), False}: + return cls() + elif params in {(True,), True}: + return cls(False, True) + else: + return cls(*params) + + def __new__(cls, p1: tuple | bool | None = None, p2: bool | None = None): + if isinstance(p1, cls): + return p1 + elif isinstance(p1, Sequence): + return cls(*p1) + elif p1 is None and p2 is None: + return tuple.__new__(cls, ()) + elif isinstance(p1, bool) and isinstance(p2, bool): + return tuple.__new__(cls, (p1, p2)) + else: + raise TypeError( + f"{cls.__name__} can either be a 0-tuple or a 2-tuple of bool, got {p1=}, {p2=}" + ) + + def __bool__(self) -> bool: + return len(self) > 0 + + @property + def constant_amplitude(self) -> bool: + return self[0] + + @property + def phase_only(self) -> bool: + return self[1] + + +no_shot_noise = ShotNoiseParameter() + + +def initial_full_field( + t: np.ndarray, + shape: str, + effective_area: float, + t0: float, + peak_power: float, + w0: float, + n0: float, + delay: float | None = None, +) -> np.ndarray: + """ + initial field in full field simulations + + Parameters + ---------- + t : np.ndarray, shape(n, ) + time array + shape : str + gaussian or sech + t0 : float + t0 parameter + peak_power : float + peak power in W + w0 : float + center frequency + n0 : float + refractive index at center frequency + delay : float | None, optional, + if given, delays the pulse by that amount. This means that in the time domain, the pulse + won't be in the center of the window anymore. + + Returns + ------- + np.ndarray, shape (n,) + initial field + """ + return ( + initial_field_envelope(t, shape, t0, peak_power, delay) + * np.cos(w0 * t) + * units.W_to_Vm(n0, effective_area) + ) + + +def initial_field_envelope( + t: np.ndarray, + shape: str, + t0: float, + peak_power: float, + delay: float | None = None, + chirp: float = 0.0, +) -> np.ndarray: + """ + returns the initial field + + Parameters + ---------- + t : 1d array + time array + shape : str {"gaussian", "sech"} + shape of the pulse + t0 : float + time parameters. Can be obtained by dividing the FWHM by + `scgenerator.physics.pulse.fwhm_to_T0_fac[shape]` + peak_power : float + peak power + delay : float | None, optional, + if given, delays the pulse by that amount. This means that in the time domain, the pulse + won't be in the center of the window anymore. + + Returns + ------- + 1d array + field array + + Raises + ------ + ValueError + raised when shape is not recognized + """ + if delay is not None: + t = t - delay + + if shape == "gaussian": + return gaussian_pulse(t, t0, peak_power, chirp) + elif shape == "sech": + return sech_pulse(t, t0, peak_power, chirp) + else: + raise ValueError(f"shape '{shape}' not understood") + + +def modify_field_ratio( + t: np.ndarray, + pre_field_0: np.ndarray, + peak_power: float | None = None, + energy: float | None = None, +) -> float: + """ + multiply a field by this number to get the desired specifications + + Parameters + ---------- + t : np.ndarray + time (only used when target_energy is not None) + field : np.ndarray + initial field + target_power : float, optional + abs2(field).max() == target_power, by default None + intensity_noise : float, optional + intensity noise, by default None + + Returns + ------- + float + ratio (multiply field by this number) + """ + ratio = 1 + if energy is not None: + ratio *= np.sqrt(energy / np.trapezoid(math.abs2(pre_field_0), t)) + elif peak_power is not None: + ratio *= np.sqrt(peak_power / math.abs2(pre_field_0).max()) + + return ratio + + +def convert_field_units(envelope: np.ndarray, n: np.ndarray, effective_area: float) -> np.ndarray: + """ + [summary] + + Parameters + ---------- + envelope : np.ndarray, shape (n,) + complex envelope in units such that |envelope|^2 is in W + n : np.ndarray, shape (n,) + refractive index + effective_area : float + effective mode field area in m^2 + + Returns + ------- + np.ndarray, shape (n,) + real field in V/m + """ + return 2 * envelope.real / np.sqrt(2 * units.epsilon0 * units.c * n * effective_area) + + +def c_to_a_factor(effective_area_arr: np.ndarray) -> np.ndarray: + return np.where( + effective_area_arr != 0, (effective_area_arr / effective_area_arr[0]) ** (1 / 4), 1 + ) + + +def a_to_c_factor(effective_area_arr: np.ndarray) -> np.ndarray: + return (effective_area_arr / effective_area_arr[0]) ** (-1 / 4) + + +def spectrum_factor_envelope(dt: float) -> float: + return dt / np.sqrt(2 * np.pi) + + +def spectrum_factor_fullfield(dt: float) -> float: + return 2 * dt / np.sqrt(2 * np.pi) + + +def conform_pulse_params( + shape: Literal["gaussian", "sech"], + width: float | None = None, + t0: float | None = None, + peak_power: float | None = None, + energy: float | None = None, + soliton_num: float | None = None, + gamma: float | None = None, + beta2: float | None = None, +): + """ + makes sure all parameters of the pulse are set and consistent + + Parameters + ---------- + shape : str {"gaussian", "sech"} + shape of the pulse + width : float, optional + fwhm of the intensity pulse, by default None + t0 : float, optional + time parameter of the amplitude pulse, by default None + peak_power : float, optional + peak power, by default None + energy : float, optional + total energy of the pulse, by default None + soliton_num : float, optional + soliton number, by default None + gamma : float, optional + nonlinear parameter, by default None + beta2 : float, optional + second order dispersion coefficient, by default None + + if more parameters than required are specified, the order of precedence + indicated by the order in which the parameters are enumerated below holds, + meaning the superflous parameters will be overwritten. + choose one of the possible combinations : + [1 of (width, t0), 1 of (peak_power, energy)] + adding both gamma and beta2 is optional + [soliton_num, gamma, 1 of (width, peak_power, energy, t0)] + examples : + specify width, peak_power and energy -> t0 and energy will be computed + specify soliton_num, gamma, peak_power, t0 -> width, t0 and energy will be computed + + Returns + ------- + width, t0, peak_power, energy + when no gamma is specified + width, t0, peak_power, energy, soliton_num + when gamma is specified + + Raises + ------ + TypeError + [description] + """ + + if (gamma is not None and beta2 is None) or (beta2 is not None and gamma is None): + raise ValueError("when soliton number is desired, both gamma and beta2 must be specified") + + if soliton_num is not None and beta2 is not None: + if gamma is None: + raise ValueError("gamma must be specified when soliton_num is") + + if width is not None and t0 is not None: + peak_power = soliton_num**2 * abs(beta2) / (gamma * t0**2) + elif peak_power is not None: + t0 = np.sqrt(soliton_num**2 * abs(beta2) / (peak_power * gamma)) + elif energy is not None: + t0 = P0T0_to_E0_fac[shape] * soliton_num**2 * abs(beta2) / (energy * gamma) + elif t0 is not None: + width = t0 / fwhm_to_T0_fac[shape] + peak_power = soliton_num**2 * abs(beta2) / (gamma * t0**2) + + if width is not None: + t0 = width * fwhm_to_T0_fac[shape] + else: + width = t0 / fwhm_to_T0_fac[shape] + + if t0 is not None: + if peak_power is not None: + energy = P0_to_E0(peak_power, t0, shape) + elif energy is not None: + peak_power = E0_to_P0(energy, t0, shape) + + if peak_power is None or t0 is None: + raise ValueError("not enough parameters to determine pulse") + + if gamma is None: + return width, t0, peak_power, energy + else: + if soliton_num is None: + if beta2 is not None: + soliton_num = np.sqrt(peak_power * gamma * dispersion_length(t0, beta2)) + else: + raise ValueError("beta2 is needed to calculate soliton number") + return width, t0, peak_power, energy, soliton_num + + +def t0_to_width(t0: float, shape: str): + return t0 / fwhm_to_T0_fac[shape] + + +def width_to_t0(width: float, shape: str): + return width * fwhm_to_T0_fac[shape] + + +def mean_power_to_energy(mean_power: float, repetition_rate: float) -> float: + return mean_power / repetition_rate + + +def energy_to_mean_power(energy: float, repetition_rate: float) -> float: + return energy * repetition_rate + + +def soliton_num_to_peak_power(soliton_num: float, beta2: float, gamma: float, t0): + return soliton_num**2 * abs(beta2) / (gamma * t0**2) + + +def soliton_num_to_t0(soliton_num: float, beta2: float, gamma: float, peak_power: float): + return np.sqrt(soliton_num**2 * abs(beta2) / (peak_power * gamma)) + + +def soliton_num(dispersion_length: float, nonlinear_length: float): + return np.sqrt(dispersion_length / nonlinear_length) + + +def dispersion_length(t0: float, beta2: float): + return t0**2 / abs(beta2) + + +def nonlinear_length(peak_power: float, gamma: float): + return 1 / (gamma * peak_power) + + +def soliton_length(dispersion_length: float): + return pi / 2 * dispersion_length + + +def center_of_gravity(t: np.ndarray, intensity: np.ndarray): + return np.sum(intensity * t, axis=-1) / np.sum(intensity, axis=-1) + + +def adjust_custom_field( + input_time: np.ndarray, + input_field: np.ndarray, + t: np.ndarray, + energy: float | None = None, + peak_power: float | None = None, +) -> tuple[np.ndarray, float, float, float]: + """ + When loading a custom input field, use this function to conform the custom data to the + simulation parameters. + + Parameters + ---------- + input_time : np.ndarray, shape (m,) + time axis of the custom data + input_field : np.ndarray, shape (m,) + complex field values + t : np.ndarray, shape (nt,) + time axis of the simulation + delay : float | None, optional + if given, delays the pulse by that amount. This means that in the time domain, the pulse + won't be in the center of the window anymore. + intensity_noise : float | None, optional + if given, pick a value `delta` on a normal distribution such that, when considering an + ensemble of similar input fields, the relative `intensity_noise` is the specified amount. + by default None + noise_correlation : float | None, optional + if given, correlation factor between intensity noise and pulse width variations + by default None + energy : float | None, optional + if given, readjusts the amplitude of the field such that the total energy is the specified + value. Takes precedence over peak_power, by default None + peak_power : float | None, optional + if given, readjusts the amplitude of the field such that the peak power is the specified + value, by default None + """ + field_0 = interp_custom_field(input_time, input_field, t) + if energy is not None: + curr_energy = np.trapezoid(math.abs2(field_0), t) + field_0 = field_0 * np.sqrt(energy / curr_energy) + elif peak_power is not None: + ratio = np.sqrt(peak_power / math.abs2(field_0).max()) + field_0 = field_0 * ratio + else: + raise ValueError("Not enough parameters specified to load custom field correctly") + + field_0 = field_0 * modify_field_ratio(t, field_0, peak_power, energy) + width, peak_power, energy = measure_field(t, field_0) + return field_0, peak_power, energy, width + + +def interp_custom_field( + input_time: np.ndarray, input_field: np.ndarray, t: np.ndarray, delay: float | None = None +) -> np.ndarray: + if delay is not None: + t = t + delay + + field_interp = interp1d(input_time, input_field, bounds_error=False, fill_value=(0, 0)) + field_0 = field_interp(t) + return field_0 + + +def load_custom_field(field_file: io.DataFile) -> tuple[np.ndarray, np.ndarray]: + return field_file.load_arrays("time", "field") + + +def correct_wavelength(init_wavelength: float, w_c: np.ndarray, field_0: np.ndarray) -> float: + """ + finds a new wavelength parameter such that the maximum of the spectrum corresponding + to field_0 is located at init_wavelength + """ + delta_w = w_c[np.argmax(math.abs2(np.fft.fft(field_0)))] + return units.m_rads(units.m_rads(init_wavelength) - delta_w) + + +def E0_to_P0(energy: float, t0: float, shape: str): + """convert an initial total pulse energy to a pulse peak peak_power""" + return energy / (t0 * P0T0_to_E0_fac[shape]) + + +def P0_to_E0(peak_power: float, t0: float, shape: str): + """converts initial peak peak_power to pulse energy""" + return peak_power * t0 * P0T0_to_E0_fac[shape] + + +def sech_pulse(t: np.ndarray, t0: float, P0: float, chirp: float = 0.0): + if chirp != 0.0: + warnings.warn("Chirp is not implemented for `sech_pulse`") + arg = t / t0 + ind = (arg < 700) & (arg > -700) + out = np.zeros_like(t) + out[ind] = np.sqrt(P0) / np.cosh(arg[ind]) + return out + + +def gaussian_pulse(t: np.ndarray, t0: float, P0: float, chirp: float = 0.0): + """ + for unchirped pulses, the following, slower, formula is equivalent + ``` + return np.sqrt(P0) * 4 ** (-(t / fwhm) ** 2) + ``` + """ + return np.sqrt(P0) * np.exp((1j * chirp - 1.0) * ((t / t0) ** 2)) + + +def pulse_envelope( + t: np.ndarray, + fwhm: float, + peak_power: float | None = None, + energy: float | None = None, + shape: str = "gaussian", +) -> np.ndarray: + """ + convenience function to create a pulse envelope + + Parameters + ---------- + t : np.ndarray + time array + fwhm : float + full width at half maximum of intensity (although the amplitude envelope is returned) + peak_power, energy : float | None, optional + provide either peak power or energy information + shape : {'gaussian', 'sech'} + pulse shape, by default Gaussian + """ + t0 = fwhm_to_T0_fac[shape] * fwhm + if peak_power is None: + if energy is not None: + peak_power = E0_to_P0(energy, t0, shape) + else: + raise ValueError("please provide either peak power or energy data") + + if shape == "gaussian": + return gaussian_pulse(t, t0, peak_power) + elif shape == "sech": + return sech_pulse(t, t0, peak_power) + else: + raise ValueError(f"shape {shape!r} not recognized") + + +def photon_number(spec2: np.ndarray, w: np.ndarray, dw: float, gamma: float) -> float: + return np.sum(1 / gamma * spec2 / w * dw) + + +def photon_number_with_loss( + spec2: np.ndarray, w: np.ndarray, dw: float, gamma: float, alpha: float, h: float +) -> float: + return np.sum(1 / gamma * spec2 / w * dw) - h * np.sum(alpha / gamma * spec2 / w * dw) + + +def pulse_energy(spec2: np.ndarray, dw: float) -> float: + return np.sum(spec2 * dw) + + +def pulse_energy_with_loss(spec2: np.ndarray, dw: float, alpha: float, h: float) -> float: + return np.sum(spec2 * dw) - h * np.sum(alpha * spec2 * dw) + + +def technical_noise(rms_noise: float, noise_correlation: float = -0.4): + """ + To implement technical noise as described in Grenier2019, we need to know the + noise properties of the laser, summarized into the RMS amplitude noise + + Parameters + ---------- + rms_noise : float + RMS amplitude noise of the laser + relative factor : float + magnitude of the anticorrelation between peak_power and pulse width noise + + Returns + ---------- + delta_int : float + delta_T0 : float + """ + psy = np.random.normal(1, rms_noise) + return psy, 1 + noise_correlation * (psy - 1) + + +def shot_noise( + w: np.ndarray, constant_amplitude: bool = False, phase_only: bool = True +) -> np.ndarray: + """ + Parameters + ---------- + w : array, shape (n,) + angular frequencies + constant_amplitude: bool, optional + if True, the shot noise spectrum variance is a proportional to the pump angular frequency + over the entire frequency grid. Otherwise (default), it is proportional to each frequency + bin. + phase_only: bool, optional + if True (default), only the phase of the complex shot noise amplitude is random. If False, + the real parts and imaginary parts are generated independently (this doesn't change the + variance). + + Returns + ------- + np.ndarray, shape (n,) + noise spectrum, scaled such that |`ifft(shot_noise(w))`|^2 represents instantaneous power + in W. + """ + n = len(w) + dw = abs(w[1] - w[0]) + dt = 2 * pi / (dw * n) + if constant_amplitude: + std = np.sqrt(0.5 * units.hbar * np.abs(w[0]) / dw) + else: + std = np.sqrt(0.5 * units.hbar * np.abs(w) / dw) + fac = std / dt * np.sqrt(2 * pi) + if phase_only: + return fac * np.exp(2j * pi * np.random.rand(n)) + else: + real = np.random.randn(n) * np.sqrt(0.5) + imag = np.random.randn(n) * np.sqrt(0.5) * 1j + return fac * (real + imag) + + +def finalize_pulse( + pre_field_0: np.ndarray, + quantum_noise: ShotNoiseParameter, + w: np.ndarray, + input_transmission: float, + ase_level: float = 1.0, +) -> np.ndarray: + pre_field_0 *= np.sqrt(input_transmission) + if quantum_noise: + pre_field_0 = pre_field_0 + np.fft.ifft(np.sqrt(ase_level) * shot_noise(w, *quantum_noise)) + + return pre_field_0 + + +def A_to_C(A: np.ndarray, effective_area_arr: np.ndarray) -> np.ndarray: + return (effective_area_arr / effective_area_arr[0]) ** (-1 / 4) * A + + +def C_to_A(C: np.ndarray, effective_area_arr: np.ndarray) -> np.ndarray: + return (effective_area_arr / effective_area_arr[0]) ** (1 / 4) * C + + +def flatten_phase(spectra: np.ndarray): + """ + takes the mean phase out of an array of complex numbers + + Parameters + ---------- + spectra : np.ndarray, shape (..., nt) + complex spectra from which to remove the mean phase + + Returns + ---------- + np.ndarray, shape (..., nt) + array of same dimensions and amplitude, but with a flattened phase + """ + mean_theta = math.mean_angle(np.atleast_2d(spectra), axis=0) + tiled = np.tile(mean_theta, (len(spectra), 1)) + output = spectra * np.conj(tiled) + return output + + +def compress_pulse(spectra: np.ndarray): + """ + given some complex spectrum, returns the compressed pulse in the time domain + Parameters + ---------- + spectra : ND array + spectra to compress. The shape must be at least 2D. Compression occurs along + the -2th axis. This means spectra have to be of shape ([what, ever,] n, nt) where n + is the number of spectra brought together for one compression operation and nt the + resolution of the grid. + + Returns + ---------- + out : array of shape ([what, ever,] nt) + compressed inverse Fourier-transformed pulse + """ + if spectra.ndim > 2: + return np.array([compress_pulse(spec) for spec in spectra]) + else: + return fftshift(ifft(flatten_phase(spectra)), axes=1) + + +def ideal_compressed_pulse(spectra: np.ndarray): + """ + returns the ideal compressed pulse assuming flat phase + Parameters + ---------- + spectra : 2D array, sc-ordering + + Returns + ---------- + compressed : 1D array + time envelope of the compressed field + """ + return math.abs2(fftshift(ifft(np.sqrt(np.mean(math.abs2(spectra), axis=0))))) + + +def spectrogram( + time: np.ndarray, + delays: np.ndarray, + signal: np.ndarray, + gate_width=200e-15, +): + """ + returns the spectorgram of the field already interpolated along the frequency axis + + Parameters + ---------- + time : np.ndarray, shape (nt,) + time in the co-moving frame of reference + delays : np.ndarray, shape (nd,) + delays overs which to compute the spectrogram + signal : np.ndarray, shape (nt,) + signal array that matches the time array + old_w : np.ndarray, shape (nt,) + angular frequency array corresponding to the fourier space of the `time` array + new_w : np.ndarray, shape (nw,) + angural frequency grid of the spectrogram + gate_width : float, optional + full width at half maximum of the gaussian gate pulse + + Returns + ---------- + spec : np.ndarray, shape (nd, nt) + real 2D spectrogram + """ + + spec = np.zeros((len(delays), len(time))) + t_gate = gate_width / (2 * np.sqrt(np.log(2))) + for i, delay in enumerate(delays): + masked = signal * np.exp(-(((time - delay) / t_gate) ** 2)) + spec[i] = math.abs2(fft(masked)) + + return spec + + +def g12(values: np.ndarray, axis: int = 0, rel_cutoff: float = 1e-16) -> np.ndarray: + """ + computes the first order coherence function of a ensemble of values + + Parameters + ---------- + values : np.ndarray, shape (..., n, nt) + complex values following sc-ordering + axis : int, optional + axis to collapse on which to compute the coherence, by default 0 + rel_cutoff : float, optional + relative mean spectrum intensity above which to compute coherence + + Returns + ------- + np.ndarray, shape (..., nt) + coherence function + """ + + n = len(values) + mean_spec = np.mean(math.abs2(values), axis=axis) + corr = np.zeros_like(mean_spec, dtype=complex) + mask = mean_spec > rel_cutoff * mean_spec.max() + corr[mask] = _g12_fast(values[..., mask]) + corr[mask] = corr[mask] / (n * (n - 1) / 2 * mean_spec[mask]) + return np.abs(corr) + + +@numba.njit() +def _g12_fast(values: np.ndarray) -> np.ndarray: + corr = np.zeros_like(values[0]) + n = len(values) + for i in range(n - 1): + for j in range(i + 1, n): + corr += values[i].conj() * values[j] + return corr + + +def avg_g12(values: np.ndarray): + """ + computes the average of the coherence function weighted by amplitude of spectrum + + Parameters + ---------- + values : np.ndarray, shape (nd, nt) + + Returns + ---------- + (float) average g12 + """ + + if len(values.shape) > 2: + pass + + avg_values = np.mean(math.abs2(values), axis=0) + coherence = g12(values) + return np.sum(coherence * avg_values, axis=-1) / np.sum(avg_values, axis=-1) + + +def fwhm_ind(values: np.ndarray, mam=None): + """ + returns the indices where values is bigger than half its maximum + + Parameters + ---------- + values : array + real values with ideally only one smooth peak + mam : tupple (float, int) + (maximum value, index of the maximum value) + + Returns + ------- + left_ind, right_ind : int + indices of the the left and right spots where values drops below 1/2 the maximum + """ + + if mam is None: + m = np.max(values) + am = np.argmax(values) + else: + m, am = mam + + left_ind = am - np.where(values[am::-1] < m / 2)[0][0] + right_ind = am + np.where(values[am:] < m / 2)[0][0] + return left_ind - 1, right_ind + 1 + + +def peak_ind(values: np.ndarray, mam: tuple[int, int] | None = None): + """ + returns the indices that encapsulate the entire peak + Parameters + ---------- + values : array + real values with ideally only one smooth peak + mam : tupple (float, int) + (maximum value, index of the maximum value) + + Returns + ------- + left_ind, right_ind : int + indices of the the left and right spots where values starts rising again, + with a margin of 3 + """ + + if mam is None: + m = np.max(values) + am = np.argmax(values) + else: + m, am = mam + + try: + left_ind = ( + am + - np.where((values[am:0:-1] - values[am - 1 :: -1] <= 0) & (values[am:0:-1] < m / 2))[ + 0 + ][0] + ) - 3 + except IndexError: + left_ind = 0 + try: + right_ind = ( + am + np.where((values[am:-1] - values[am + 1 :] <= 0) & (values[am:-1] < m / 2))[0][0] + ) + 3 + except IndexError: + right_ind = len(values) - 1 + return max(0, left_ind), min(len(values) - 1, right_ind) + + +def setup_splines(x_axis, values, mam=None): + """ + sets up spline interpolation to better measure a peak. Different splines with different + orders are necessary because derivatives and second derivatives are computed to find extremea + and inflection points + + Parameters + ---------- + x_axis : 1D array + domain of values + values : 1D array + real values that ideally contain only one smooth peak to measure + mam : tupple (float, int) + (maximum value, index of the maximum value) + + Returns + ------- + small_spline : scipy.interpolate.UnivariateSpline + order 3 spline that interpolates `values - m/2` around the peak + spline_4 : scipy.interpolate.UnivariateSpline + order 4 spline that interpolate values around the peak + spline 5 : scipy.interpolate.UnivariateSpline + order 5 spline that interpolates values around the peak + d_spline : scipy.interpolate.UnivariateSpline + order 3 spline that interpolates the derivative of values around the peak + d_roots : list + roots of d_spline + dd_roots : list + inflection points of spline_5 + l_ind, r_ind : int + return values of peak_ind + """ + + # Isolate part thats roughly above max/2 + l_ind_h, r_ind_h = fwhm_ind(values, mam) + l_ind, r_ind = peak_ind(values, mam) + + if mam is None: + mm = np.max(values) + else: + mm, _ = mam + + # Only roots of deg=3 splines can be computed, so we need 3 splines to find + # zeros, local extrema and inflection points + small_spline = UnivariateSpline( + x_axis[l_ind_h : r_ind_h + 1], values[l_ind_h : r_ind_h + 1] - mm / 2, k=3, s=0 + ) + spline_4 = UnivariateSpline(x_axis[l_ind : r_ind + 1], values[l_ind : r_ind + 1], k=4, s=0) + spline_5 = UnivariateSpline(x_axis[l_ind : r_ind + 1], values[l_ind : r_ind + 1], k=5, s=0) + d_spline = spline_4.derivative() + d_roots = spline_4.derivative().roots() + dd_roots = spline_5.derivative(2).roots() + + return small_spline, spline_4, spline_5, d_spline, d_roots, dd_roots, l_ind, r_ind + + +def find_lobe_limits(x_axis, values, debug="", already_sorted=True): + """ + find the limits of the centra lobe given 2 derivatives of the values and + the position of the FWHM + + Parameters + ---------- + x_axis : 1D array + domain of values + values : 1D array + real values that present a peak whose properties we want to meausure + debug : str + if the peak is not distinct, a plot is made to assess the measurement + providing a debug label can help identify which plot correspond to which function call + sorted : bool + faster computation if arrays are already sorted + + Returns + ------- + peak_lim : 1D array (left_lim, right_lim, peak_pos) + values that delimit the left, right and maximum of the peak in units of x_axis + fwhm_pos : 1D array (left_pos, right_pos) + values corresponding to fwhm positions in units of x_axis + good_roots : 1D array + all candidate values that could delimit the peak position + spline_4 : scipy.interpolate.UnivariateSpline + order 4 spline that interpolate values around the peak + """ + + if not already_sorted: + x_axis, values = x_axis.copy(), values.copy() + values = values[np.argsort(x_axis)] + x_axis.sort() + + debug_str = f"debug : {debug}" if debug != "" else "" + + small_spline, spline_4, spline_5, d_spline, d_roots, dd_roots, l_ind, r_ind = setup_splines( + x_axis, values + ) + + # get premliminary values for fwhm limits and peak limits + # if the peak is distinct, it should be sufficient + fwhm_pos = np.array(small_spline.roots()) + peak_pos = d_roots[np.argmax(spline_4(d_roots))] + + # if there are more than 2 fwhm position, a detailed analysis can help + # determining the true ones. If that fails, there is no meaningful peak to measure + detailed_measurement = len(fwhm_pos) > 2 + if detailed_measurement: + print("trouble measuring the peak.{}".format(debug_str)) + ( + spline_4, + d_spline, + d_roots, + dd_roots, + fwhm_pos, + peak_pos, + out_path, + fig, + ax, + color, + ) = _detailed_find_lobe_limits( + x_axis, + values, + debug, + debug_str, + spline_4, + spline_5, + fwhm_pos, + peak_pos, + d_spline, + d_roots, + dd_roots, + l_ind, + r_ind, + ) + + good_roots, left_lim, right_lim = _select_roots(d_spline, d_roots, dd_roots, fwhm_pos) + if debug != "" and plt is not None: + ax.scatter( + [left_lim, right_lim], + spline_4([left_lim, right_lim]), + marker="|", + label="lobe pos", + c=color[5], + ) + ax.legend() + fig.savefig(out_path, bbox_inches="tight") + if fig is not None and plt is not None: + plt.close(fig) + + else: + good_roots, left_lim, right_lim = _select_roots(d_spline, d_roots, dd_roots, fwhm_pos) + + return np.array([left_lim, right_lim, peak_pos]), fwhm_pos, np.array(good_roots), spline_4 + + +def _select_roots(d_spline, d_roots, dd_roots, fwhm_pos): + """ + selects the limits of a lobe + + Parameters + ---------- + d_spline : scipy.interpolate.UnivariateSpline + spline of the first derivative of the lobe + d_roots : list + roots of the first derivarive (extrema of the original function) + dd_roots : list + roots of the second derivative (inflection points of the original function) + fwhm_pos : list + locations where the lobe is half of its maximum + + Returns + ------- + good_roots : list + valid roots + left_lim : list + location of the left limit + right_lim : list + location of the right limit + """ + # includes inflection points when slope is low + # (avoids considering the inflection points around fwhm limits) + + all_roots = np.append(d_roots, dd_roots) + good_roots = all_roots[np.abs(d_spline(all_roots)) < np.max(d_spline(all_roots)) / 10] + + try: + left_lim = np.max(good_roots[good_roots < np.min(fwhm_pos)]) + except ValueError: + left_lim = np.min(good_roots) + + try: + right_lim = np.min(good_roots[good_roots > np.max(fwhm_pos)]) + except ValueError: + right_lim = np.max(good_roots) + + return good_roots, left_lim, right_lim + + +def _detailed_find_lobe_limits( + x_axis, + values, + debug, + debug_str, + spline_4, + spline_5, + fwhm_pos, + peak_pos, + d_spline, + d_roots, + dd_roots, + l_ind, + r_ind, +): + left_pos = fwhm_pos[fwhm_pos < peak_pos] + right_pos = fwhm_pos[fwhm_pos > peak_pos] + + iterations = 0 + + # spline maximum may not be on same peak as the original one. In this + # case it means that there is no distinct peak, but we try to + # compute everything again anyway. If spline inaccuracies lead to a cycle, + # we break it by choosing two values arbitrarily + + while len(left_pos) == 0 or len(right_pos) == 0: + if iterations > 4: + left_pos, right_pos = [np.min(peak_pos)], [np.max(peak_pos)] + print( + "Cycle had to be broken. Peak measurement is probably wrong : {}".format(debug_str) + ) + break + else: + iterations += 1 + + mam = (spline_4(peak_pos), math.argclosest(x_axis, peak_pos)) + + small_spline, spline_4, spline_5, d_spline, d_roots, dd_roots, l_ind, r_ind = setup_splines( + x_axis, values, mam + ) + + fwhm_pos = np.array(small_spline.roots()) + peak_pos = d_roots[np.argmax(spline_4(d_roots))] + + left_pos = fwhm_pos[fwhm_pos < peak_pos] + right_pos = fwhm_pos[fwhm_pos > peak_pos] + + # if measurement of the peak is not straightforward, we plot the situation to see + # if the final measurement is good or not + + new_fwhm_pos = np.array([np.max(left_pos), np.min(right_pos)]) + + out_path, fig, ax = (None, None, None) + if debug != "" and plt is not None: + out_path = Path(f"measurement_errors_plots/it_{iterations}_{debug}") + fig, ax = plt.subplots(1, 1) + + color = plt.rcParams["axes.prop_cycle"].by_key()["color"] + newx = np.linspace(*math.span(x_axis[l_ind : r_ind + 1]), 1000) + ax.plot(x_axis[l_ind - 5 : r_ind + 6], values[l_ind - 5 : r_ind + 6], c=color[0]) + ax.plot(newx, spline_5(newx), c=color[1]) + ax.scatter(fwhm_pos, spline_4(fwhm_pos), marker="+", label="all fwhm", c=color[2]) + ax.scatter(peak_pos, spline_4(peak_pos), marker=".", label="peak pos", c=color[3]) + ax.scatter(new_fwhm_pos, spline_4(new_fwhm_pos), marker="_", label="2 chosen", c=color[4]) + + fwhm_pos = new_fwhm_pos + return ( + spline_4, + d_spline, + d_roots, + dd_roots, + fwhm_pos, + peak_pos, + out_path, + fig, + ax, + color, + ) + + +def measure_properties( + spectra, t, compress=True, return_limits=False, debug="" +) -> PulseProperties | tuple[PulseProperties, list[tuple[np.ndarray, np.ndarray]]]: + """ + measure the quality factor, the fwhm variation, the peak power variation, + + Parameters + ---------- + spectra : np.ndarray, shape (n, nt) + set of n spectra on a grid of nt angular frequency points + t : np.ndarray, shape (nt, ) + time axis of the simulation + compress : bool, optional + whether to perform pulse compression. Default value is True, but this + should be set to False to measure the initial pulse as output by gaussian_pulse + or sech_pulse because compressing it would result in glitches and wrong measurements + return_limits : bool, optional + return the time values of the limits + + Returns + ------- + PulseProperties object: + quality : float + quality factor of the pulse ensemble + mean_gmean_coherence12 : float + mean coherence of the spectra ensemble + fwhm_noise : float + relative noise in temporal width of the (compressed) pulse + mean_fwhm : float + width of the mean (compressed) pulse + peak_rin : float + relative noise in the (compressed) pulse peak intensity + energy_rin : float + relative noise in the (compressed) pulse total_energy + timing_jitter : float + standard deviantion in absolute temporal peak position + all_limits : list[tuple[np.ndarray, np.ndarray]], only if return_limits = True + list of tuples of the form ([left_lobe_lim, right_lobe_lim, lobe_pos], [left_hm, right_hm]) + """ + if compress: + fields = math.abs2(compress_pulse(spectra)) + else: + fields = math.abs2(ifft(spectra)) + + field = np.mean(fields, axis=0) + ideal_field = math.abs2(fftshift(ifft(np.sqrt(np.mean(math.abs2(spectra), axis=0))))) + + # Isolate whole central lobe of both mean and ideal field + lobe_lim, fwhm_lim, _, big_spline = find_lobe_limits(t, field, debug) + lobe_lim_i, _, _, big_spline_i = find_lobe_limits(t, ideal_field, debug) + + # Compute quality factor + energy_fraction = (big_spline.integral(*math.span(lobe_lim[:2]))) / np.trapezoid(field, x=t) + energy_fraction_i = (big_spline_i.integral(*math.span(lobe_lim_i[:2]))) / np.trapezoid( + ideal_field, x=t + ) + qf = energy_fraction / energy_fraction_i + + # Compute mean coherence + mean_g12 = avg_g12(spectra) + fwhm_abs = np.max(fwhm_lim) - np.min(fwhm_lim) + + # To compute amplitude and fwhm fluctuations, we need to measure every single peak + P0 = [] + fwhm = [] + t_offset = [] + energies = [] + all_lims: list[tuple[np.ndarray, np.ndarray]] = [] + for f in fields: + lobe_lim, fwhm_lim, _, big_spline = find_lobe_limits(t, f, debug) + all_lims.append((lobe_lim, fwhm_lim)) + P0.append(big_spline(lobe_lim[2])) + fwhm.append(np.max(fwhm_lim) - np.min(fwhm_lim)) + t_offset.append(lobe_lim[2]) + energies.append(np.trapezoid(fields, t)) + fwhm_var = np.std(fwhm) / np.mean(fwhm) + int_var = np.std(P0) / np.mean(P0) + en_var = np.std(energies) / np.mean(energies) + t_jitter = np.std(t_offset) + + if isinstance(mean_g12, np.ndarray) and mean_g12.ndim == 0: + mean_g12 = mean_g12[()] + pp = PulseProperties( + qf, mean_g12, float(fwhm_var), fwhm_abs, float(int_var), float(en_var), float(t_jitter) + ) + + if return_limits: + return pp, all_lims + else: + return pp + + +def rin_curve(spectra: np.ndarray) -> np.ndarray: + """ + computes the rin curve, i.e. the rin at every single point + + Parameters + ---------- + spectra : np.ndarray, shape (n, nt) + a collection of n spectra from which to compute the RIN + complex amplitude is automatically converted to intensity + + Returns + ------- + rin_curve : np.ndarray + RIN curve + """ + if np.iscomplexobj(spectra): + A2 = math.abs2(spectra) + else: + A2 = spectra + return np.std(A2, axis=-2) / np.mean(A2, axis=-2) + + +def measure_field(t: np.ndarray, field: np.ndarray) -> Tuple[float, float, float]: + """returns fwhm, peak_power, energy""" + if np.iscomplexobj(field): + intensity = math.abs2(field) + else: + intensity = field + _, fwhm_lim, _, _ = find_lobe_limits(t, intensity) + fwhm = math.total_extent(fwhm_lim) + peak_power = intensity.max() + energy = np.trapezoid(intensity, t) + return fwhm, peak_power, energy + + +def remove_2nd_order_dispersion( + spectrum: T, w_c: np.ndarray, beta2: float, max_z: float = -100.0 +) -> tuple[T, OptimizeResult]: + """ + attempts to remove 2nd order dispersion from a complex spectrum + + Parameters + ---------- + spectrum : np.ndarray or Spectrum, shape (n, ) + spectrum from which to remove 2nd order dispersion + w_c : np.ndarray, shape (n, ) + corresponding centered angular frequencies (w-w0) + beta2 : float + 2nd order dispersion coefficient + + Returns + ------- + np.ndarray, shape (n, ) + spectrum with 2nd order dispersion removed + """ + + def propagate(z): + return spectrum * np.exp(-0.5j * beta2 * w_c**2 * z) + + def score(z): + return -np.max(math.abs2(np.fft.ifft(propagate(z)))) + + opti = minimize_scalar(score, bracket=(max_z, 0)) + return propagate(opti.x), opti + + +def remove_2nd_order_dispersion2( + spectrum: T, w_c: np.ndarray, max_gdd: float = 1000e-30 +) -> tuple[T, OptimizeResult]: + """ + attempts to remove 2nd order dispersion from a complex spectrum + + Parameters + ---------- + spectrum : np.ndarray or Spectrum, shape (n, ) + spectrum from which to remove 2nd order dispersion + w_c : np.ndarray, shape (n, ) + corresponding centered angular frequencies (w-w0) + + Returns + ------- + np.ndarray, shape (n, ) + spectrum with 2nd order dispersion removed + """ + + def propagate(gdd): + return spectrum * np.exp(-0.5j * w_c**2 * 1e-30 * gdd) + + def integrate(gdd): + return math.abs2(np.fft.ifft(propagate(gdd))) + + def score(gdd): + return -np.sum(integrate(gdd) ** 6) + + # def score(gdd): + # return -np.max(integrate(gdd)) + + # to_test = np.linspace(-max_gdd, max_gdd, 200) + # scores = [score(g) for g in to_test] + # fig, ax = plt.subplots() + # ax.plot(to_test, scores / np.min(scores)) + # plt.show() + # plt.close(fig) + # ama = np.argmin(scores) + + opti = minimize_scalar(score, bounds=(-max_gdd * 1e30, max_gdd * 1e30)) + opti["x"] *= 1e-30 + return propagate(opti.x * 1e30), opti + + +def gdd(w: np.ndarray, gdd: float) -> np.ndarray: + return np.exp(0.5j * w**2 * gdd) + + +def mask_with_noise( + w: np.ndarray, + spectrum: T, + mask: np.ndarray, + sn_params: ShotNoiseParameter = ShotNoiseParameter(False, False), +) -> T: + sn = shot_noise(w, *sn_params) + return spectrum * mask + np.sqrt(1.0 - mask**2) * sn + + +def new( + t: np.ndarray, + w: np.ndarray, + shot_noise: ShotNoiseParameter, + shape: Literal["gaussian", "sech"], + delay=0.0, + input_transmission=1.0, + ase_level=1.0, + peak_power: float | None = None, + mean_power: float | None = None, + repetition_rate: float | None = None, + energy: float | None = None, + width: float | None = None, + t0: float | None = None, +) -> np.ndarray: + t0 = t0 if t0 is not None else fwhm_to_T0_fac[shape] * width + + if energy is not None and mean_power is not None and repetition_rate is not None: + energy = mean_power / repetition_rate + width, t0, peak_power, energy = conform_pulse_params(shape, width, t0, peak_power, energy) + + pre = initial_field_envelope(t, shape, t0, peak_power, delay) + return finalize_pulse(pre, ShotNoiseParameter(shot_noise), w, input_transmission, ase_level) diff --git a/src/dispersionapp/physics/units.py b/src/dispersionapp/physics/units.py new file mode 100644 index 0000000..b58e8b9 --- /dev/null +++ b/src/dispersionapp/physics/units.py @@ -0,0 +1,460 @@ +# series of functions to convert different values to angular frequencies +# For example, nm(X) means "I give the number X in nm, figure out the ang. freq." +# to be used especially when giving plotting ranges : (400, 1400, nm), (-4, 8, ps), ... + +from __future__ import annotations + +from typing import Callable, Protocol, Sequence, Union, overload + +import numpy as np +from numpy import pi + +c = 299792458.0 +h = 6.62607015e-34 +hbar = 0.5 * h / np.pi +NA = 6.02214076e23 +R = 8.31446261815324 +kB = 1.380649e-23 +epsilon0 = 8.85418781e-12 +me = 9.1093837015e-31 +e = 1.602176634e-19 + + +prefix = dict(P=1e12, G=1e9, M=1e6, k=1e3, d=1e-1, c=1e-2, m=1e-3, u=1e-6, n=1e-9, p=1e-12, f=1e-15) + +""" +Below are common units. You can define your own unit function +provided you decorate it with @unit and provide at least a type and a label +types are "WL", "FREQ", "AFREQ", "TIME", "PRESSURE", "TEMPERATURE", "OTHER" +""" +type _UT[T] = Callable[[T], T] +PRIMARIES = dict(WL="m", FREQ="Rad/s", AFREQ="Rad/s", TIME="s", PRESSURE="Pa", TEMPERATURE="K") + + +class Unit(Protocol): + @overload + def __call__(self, v: np.ndarray) -> np.ndarray: ... + @overload + def __call__(self, v: float | int | np.floating | np.integer) -> float: ... + def __call__(self, v): ... + @overload + def inv(self, v: np.ndarray) -> np.ndarray: ... + @overload + def inv(self, v: float | int | np.floating | np.integer) -> float: ... + def inv(self, v): ... + def inverse(self, f: _UT) -> Unit: ... + + +class UnitMap(dict): + def __setitem__(self, new_name, new_func): + super().__setitem__(new_name, new_func) + already_here = [name for name in self if isinstance(name, str)] + for old_name in already_here: + super().__setitem__((old_name, new_name), self._chain(old_name, new_name)) + super().__setitem__((new_name, old_name), self._chain(new_name, old_name)) + + def _chain(self, name_1: str, name_2: str) -> _UT: + c1 = self[name_1] + c2 = self[name_2] + + def chained_function[T](x: T) -> T: + return c2.inv(c1(x)) + + chained_function.name = f"{name_1}_to_{name_2}" + chained_function.__name__ = f"{name_1}_to_{name_2}" + chained_function.__doc__ = f"converts x from {name_1} to {name_2}" + + return chained_function + + +units_map: dict[Union[str, tuple[str, str]], _UT] = UnitMap() + + +class To: + def __init__(self, name: str): + self.name = name + + def __getstate__(self): + """make pickle happy""" + return self.name + + def __setstate__(self, name): + """make pickle happy""" + self.name = name + + def __getattr__(self, key: str): + try: + return units_map[self.name, key] + except KeyError: + raise KeyError(f"no registered unit named {key!r}") from None + + +def W_to_Vm(n0: float, effective_area: float) -> float: + """returns the factor to convert from [|E|²] = W to [|E|] = V/m + + Parameters + ---------- + n : float + refractive index + + Returns + ------- + float + p such that E_sqrt(W) * p = W_Vm + """ + return 1.0 / np.sqrt(effective_area * 0.5 * epsilon0 * c * n0) + + +def unit_formatter( + unit: str, decimals: int = 3, vmin: float | None = 1e-28 +) -> Callable[[float | int], str]: + if not unit: + + def formatter(val): + return f"{val:.{decimals}g}" + + elif unit == "%": + + def formatter(val): + return f"{100 * val:.{decimals}g}%" + + else: + + def formatter(val): + if vmin is not None and abs(val) < vmin: + return f"0{unit}" + base, true_exp = format(val, f".{3 + decimals}e").split("e") + true_exp = int(true_exp) + exp, mult = divmod(true_exp, 3) + if exp < -8: + mult -= -3 * (8 + exp) + exp = -8 + elif exp > 8: + mult += 3 * (exp - 8) + exp = 8 + + prefix = "yzafpnµm kMGTPEZY"[8 + exp] if exp else "" + return f"{float(base) * 10**mult:.{decimals}g}{prefix}{unit}" + + def _format(val): + if isinstance(val, (Sequence, np.ndarray)): + return f"({', '.join(_format(el) for el in val)})" + else: + return formatter(val) + + def _format_mpl(val, _): + return _format(val) + + setattr(_format, "for_mpl", _format_mpl) + + return _format + + +class unit: + inv = None + + def __init__(self, tpe: str, label: str): + self.type = tpe + self.label = label + + def __call__(self, func) -> Unit: + self.name = func.__name__ + self.func = func + self.inv = func + + units_map[self.name] = self.func + + func.to = To(self.name) + func.inv = func + func.inverse = self.inverse + func.type = self.type + func.label = self.label + func.name = self.name + func.__doc__ = f"Transform x from {self.name!r} to {PRIMARIES.get(self.type)!r}" + return func + + def inverse(self, func: _UT): + if func.__name__ == self.name: + raise ValueError( + f"inverse function of {self.name} must be named something else than {self.name}" + ) + self.inv = func + self.func.inv = func + return func + + +@unit("WL", r"Wavelength λ (m)") +def m(wl): + return wl + + +@unit("WL", r"Wavelength λ (nm)") +def nm(wl): + return wl * 1e-9 + + +@nm.inverse +def nm_inv(wl): + return wl * 1e9 + + +@unit("WL", r"Wavelength λ (μm)") +def um(wl): + return wl * 1e-6 + + +@um.inverse +def um_inv(wl): + return wl * 1e6 + + +@unit("FREQ", r"Frequency $f$ (Hz)") +def Hz(f): + return 2 * pi * f + + +@Hz.inverse +def Hz_inv(w): + return w / (2 * pi) + + +@unit("FREQ", r"Frequency $f$ (THz)") +def THz(f): + return 1e12 * 2 * pi * f + + +@THz.inverse +def THz_inv(w): + return w / (1e12 * 2 * pi) + + +@unit("FREQ", r"Frequency $f$ (PHz)") +def PHz(f): + return 1e15 * 2 * pi * f + + +@PHz.inverse +def PHz_inv(w): + return w / (1e15 * 2 * pi) + + +@unit("AFREQ", r"Angular frequency $\omega$ ($\frac{\mathrm{rad}}{\mathrm{s}}$)") +def rad_s(w): + return w + + +@unit("AFREQ", r"Angular frequency $\omega$ ($\frac{\mathrm{Prad}}{\mathrm{s}}$)") +def Prad_s(w): + return w * 1e15 + + +@Prad_s.inverse +def Prad_s_inv(w): + return w * 1e-15 + + +@unit("TIME", r"relative time ${\tau}/{\tau_\mathrm{0, FWHM}}$") +def rel_time(t): + return t + + +@unit("FREQ", r"relative angular freq. $(\omega - \omega_0)/\Delta\omega_0$") +def rel_freq(f): + return f + + +@unit("TIME", r"Time $t$ (s)") +def s(t): + return t + + +@unit("TIME", r"Time $t$ (us)") +def us(t): + return t * 1e-6 + + +@us.inverse +def us_inv(t): + return t * 1e6 + + +@unit("TIME", r"Time $t$ (ns)") +def ns(t): + return t * 1e-9 + + +@ns.inverse +def ns_inv(t): + return t * 1e9 + + +@unit("TIME", r"Time $t$ (ps)") +def ps(t): + return t * 1e-12 + + +@ps.inverse +def ps_inv(t): + return t * 1e12 + + +@unit("TIME", r"Time $t$ (fs)") +def fs(t): + return t * 1e-15 + + +@fs.inverse +def fs_inv(t): + return t * 1e15 + + +@unit("WL", "inverse") +def inv(x): + return 1 / x + + +@unit("PRESSURE", "Pressure (bar)") +def bar(p): + return 1e5 * p + + +@bar.inverse +def bar_inv(p): + return p * 1e-5 + + +@unit("PRESSURE", "Pressure (mbar)") +def mbar(p): + return 1e2 * p + + +@mbar.inverse +def mbar_inv(p): + return 1e-2 * p + + +@unit("OTHER", r"$\beta_2$ (fs$^2$/cm)") +def beta2_fs_cm(b2): + return 1e-28 * b2 + + +@beta2_fs_cm.inverse +def beta2_fs_cm_inv(b2): + return 1e28 * b2 + + +@unit("OTHER", r"$\beta_2$ (ps$^2$/km)") +def beta2_ps_km(b2): + return 1e-27 * b2 + + +@beta2_ps_km.inverse +def beta2_ps_km_inv(b2): + return 1e27 * b2 + + +@unit("OTHER", r"$D$ (ps/(nm km))") +def D_ps_nm_km(D): + return 1e-6 * D + + +@D_ps_nm_km.inverse +def D_ps_nm_km_inv(D): + return 1e6 * D + + +@unit("OTHER", r"a.u.") +def unity(x): + return x + + +@unit("TEMPERATURE", r"Temperature (K)") +def K(t): + return t + + +@unit("TEMPERATURE", r"Temperature (°C)") +def C(t_C): + return t_C + 272.15 + + +@C.inverse +def C_inv(t_K): + return t_K - 272.15 + + +@unit("OTHER", r"a.u") +def no_unit(x): + return x + + +def nm_rads(nm): + return 2e9 * np.pi * c / nm + + +def nm_hz(nm): + return 1e9 * c / nm + + +def um_rads(um): + return 2e6 * np.pi * c / um + + +def um_hz(um): + return 1e6 * c / um + + +def m_rads(m): + return 2 * np.pi * c / m + + +def m_hz(m): + return c / m + + +def get_unit(unit: Union[str, Callable]) -> Callable[[float], float]: + if isinstance(unit, str): + return units_map[unit] + return unit + + +def beta2_coef(beta2_coefficients): + fac = 1e27 + out = np.zeros_like(beta2_coefficients) + for i, b in enumerate(beta2_coefficients): + out[i] = fac * b + fac *= 1e12 + return out + + +def to_WL(spectrum: np.ndarray, lambda_: np.ndarray) -> np.ndarray: + """ + rescales the spectrum because of uneven binning when going from freq to wl + + Parameters + ---------- + spectrum : np.ndarray, shape (..., n) + intensity spectrum + lambda_ : np.ndarray, shape (n, ) + wavelength in m + + Returns + ------- + np.ndarray, shape (n, ) + intensity spectrum correctly scaled + """ + m = 2 * pi * c / (lambda_**2) * spectrum + return m + + +def w_from_wl(wl_min_nm: float, wl_max_nm: float, n: int) -> np.ndarray: + return np.linspace(nm_rads(wl_max_nm), nm_rads(wl_min_nm), n) + + +def normalize_wavelengths(wl: np.ndarray) -> np.ndarray: + """attempts to guess the units of wl and return the same array in base SI units (m)""" + if np.all((wl > 0.01) & (wl < 20)): + return wl * 1e-6 + elif np.all(wl >= 20): + return wl * 1e-9 + return wl diff --git a/src/dispersionapp/plotapp.py b/src/dispersionapp/plotapp.py index ed161e5..566240a 100644 --- a/src/dispersionapp/plotapp.py +++ b/src/dispersionapp/plotapp.py @@ -5,8 +5,7 @@ import itertools from collections.abc import MutableMapping, Sequence from functools import cache from types import MethodType -from typing import (Any, Callable, Iterable, Iterator, Optional, Type, Union, - overload) +from typing import Any, Callable, Iterable, Iterator, Optional, Type, Union, overload import numpy as np import pyqtgraph as pg @@ -80,7 +79,9 @@ class SliderField(QtWidgets.QWidget): self.value_changed.emit(self.__value) def set_values(self, values: Iterable): - self.possible_values = np.array(values) + self.possible_values = ( + np.array(values) if not isinstance(values, np.ndarray) else values.copy() + ) self.value_to_slider_map = {v: i for i, v in enumerate(self.possible_values)} self._slider_max = len(self.possible_values) - 1 self.slider.setRange(0, self._slider_max) @@ -103,7 +104,7 @@ class SliderField(QtWidgets.QWidget): self.update_slider() @property - def value(self) -> float: + def value(self) -> float | int: return self.__value @value.setter @@ -488,7 +489,7 @@ class PlotApp: opts = dict(zip(["v_min", "v_max", "v_num", "v_init"], opts)) yield p_name, dtype, opts - def update(self, *p_names: str): + def update(self, first_param: str | Callable, *p_names: str): """ use this as a decorator to connect the decorated function to the value_changed signal of some parameters @@ -521,8 +522,12 @@ class PlotApp: changes and is given its new value as argument """ - if len(p_names) == 1 and callable(p_names[0]): - return self.update(*self._get_func_args(p_names[0]))(p_names[0]) + if len(p_names) == 0 and callable(first_param) and not isinstance(first_param, str): + return self.update(*self._get_func_args(first_param))(first_param) + elif not isinstance(first_param, str): + raise TypeError() + + p_names = (first_param, *p_names) def wrapper(func): def to_call(v=None): @@ -650,12 +655,10 @@ class PlotApp: self.app.exec() @overload - def __getitem__(self, key: tuple[key_type, key_type]) -> PlotDataItem: - ... + def __getitem__(self, key: tuple[key_type, key_type]) -> PlotDataItem: ... @overload - def __getitem__(self, key: key_type) -> Plot: - ... + def __getitem__(self, key: key_type) -> Plot: ... def __getitem__(self, key: key_type) -> Plot: if isinstance(key, tuple): diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..c370f4e --- /dev/null +++ b/uv.lock @@ -0,0 +1,704 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[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/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" }, +] + +[[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 = "dispersionapp" +version = "0.2.2" +source = { editable = "." } +dependencies = [ + { name = "click" }, + { name = "numba" }, + { name = "pydantic" }, + { name = "pyqtgraph" }, + { name = "pyside6" }, + { name = "scipy" }, + { name = "tomli" }, + { name = "tomli-w" }, +] + +[package.dev-dependencies] +dev = [ + { name = "matplotlib" }, +] + +[package.metadata] +requires-dist = [ + { name = "click" }, + { name = "numba", specifier = ">=0.62.1" }, + { name = "pydantic", specifier = ">=2" }, + { name = "pyqtgraph", specifier = ">=0.13.1" }, + { name = "pyside6", specifier = ">=6.4.0" }, + { name = "scipy", specifier = ">=1.16.3" }, + { name = "tomli" }, + { name = "tomli-w" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "matplotlib", specifier = ">=3.10.7" }] + +[[package]] +name = "fonttools" +version = "4.60.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/42/97a13e47a1e51a5a7142475bbcf5107fe3a68fc34aef331c897d5fb98ad0/fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", size = 3559823, upload-time = "2025-09-29T21:13:27.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb", size = 2825777, upload-time = "2025-09-29T21:12:01.22Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8a/de9cc0540f542963ba5e8f3a1f6ad48fa211badc3177783b9d5cadf79b5d/fonttools-4.60.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4", size = 2348080, upload-time = "2025-09-29T21:12:03.785Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8b/371ab3cec97ee3fe1126b3406b7abd60c8fec8975fd79a3c75cdea0c3d83/fonttools-4.60.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c", size = 4903082, upload-time = "2025-09-29T21:12:06.382Z" }, + { url = "https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77", size = 4960125, upload-time = "2025-09-29T21:12:09.314Z" }, + { url = "https://files.pythonhosted.org/packages/8e/37/f3b840fcb2666f6cb97038793606bdd83488dca2d0b0fc542ccc20afa668/fonttools-4.60.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199", size = 4901454, upload-time = "2025-09-29T21:12:11.931Z" }, + { url = "https://files.pythonhosted.org/packages/fd/9e/eb76f77e82f8d4a46420aadff12cec6237751b0fb9ef1de373186dcffb5f/fonttools-4.60.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c", size = 5044495, upload-time = "2025-09-29T21:12:15.241Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/cede8f8235d42ff7ae891bae8d619d02c8ac9fd0cfc450c5927a6200c70d/fonttools-4.60.1-cp313-cp313-win32.whl", hash = "sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272", size = 2217028, upload-time = "2025-09-29T21:12:17.96Z" }, + { url = "https://files.pythonhosted.org/packages/75/4d/b022c1577807ce8b31ffe055306ec13a866f2337ecee96e75b24b9b753ea/fonttools-4.60.1-cp313-cp313-win_amd64.whl", hash = "sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac", size = 2266200, upload-time = "2025-09-29T21:12:20.14Z" }, + { url = "https://files.pythonhosted.org/packages/9a/83/752ca11c1aa9a899b793a130f2e466b79ea0cf7279c8d79c178fc954a07b/fonttools-4.60.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3", size = 2822830, upload-time = "2025-09-29T21:12:24.406Z" }, + { url = "https://files.pythonhosted.org/packages/57/17/bbeab391100331950a96ce55cfbbff27d781c1b85ebafb4167eae50d9fe3/fonttools-4.60.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85", size = 2345524, upload-time = "2025-09-29T21:12:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2e/d4831caa96d85a84dd0da1d9f90d81cec081f551e0ea216df684092c6c97/fonttools-4.60.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537", size = 4843490, upload-time = "2025-09-29T21:12:29.123Z" }, + { url = "https://files.pythonhosted.org/packages/49/13/5e2ea7c7a101b6fc3941be65307ef8df92cbbfa6ec4804032baf1893b434/fonttools-4.60.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003", size = 4944184, upload-time = "2025-09-29T21:12:31.414Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2b/cf9603551c525b73fc47c52ee0b82a891579a93d9651ed694e4e2cd08bb8/fonttools-4.60.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08", size = 4890218, upload-time = "2025-09-29T21:12:33.936Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2f/933d2352422e25f2376aae74f79eaa882a50fb3bfef3c0d4f50501267101/fonttools-4.60.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99", size = 4999324, upload-time = "2025-09-29T21:12:36.637Z" }, + { url = "https://files.pythonhosted.org/packages/38/99/234594c0391221f66216bc2c886923513b3399a148defaccf81dc3be6560/fonttools-4.60.1-cp314-cp314-win32.whl", hash = "sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6", size = 2220861, upload-time = "2025-09-29T21:12:39.108Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1d/edb5b23726dde50fc4068e1493e4fc7658eeefcaf75d4c5ffce067d07ae5/fonttools-4.60.1-cp314-cp314-win_amd64.whl", hash = "sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987", size = 2270934, upload-time = "2025-09-29T21:12:41.339Z" }, + { url = "https://files.pythonhosted.org/packages/fb/da/1392aaa2170adc7071fe7f9cfd181a5684a7afcde605aebddf1fb4d76df5/fonttools-4.60.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299", size = 2894340, upload-time = "2025-09-29T21:12:43.774Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a7/3b9f16e010d536ce567058b931a20b590d8f3177b2eda09edd92e392375d/fonttools-4.60.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01", size = 2375073, upload-time = "2025-09-29T21:12:46.437Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/e9bcf51980f98e59bb5bb7c382a63c6f6cac0eec5f67de6d8f2322382065/fonttools-4.60.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801", size = 4849758, upload-time = "2025-09-29T21:12:48.694Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/1d2cf7d1cba82264b2f8385db3f5960e3d8ce756b4dc65b700d2c496f7e9/fonttools-4.60.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc", size = 5085598, upload-time = "2025-09-29T21:12:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/5d/4d/279e28ba87fb20e0c69baf72b60bbf1c4d873af1476806a7b5f2b7fac1ff/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc", size = 4957603, upload-time = "2025-09-29T21:12:53.423Z" }, + { url = "https://files.pythonhosted.org/packages/78/d4/ff19976305e0c05aa3340c805475abb00224c954d3c65e82c0a69633d55d/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed", size = 4974184, upload-time = "2025-09-29T21:12:55.962Z" }, + { url = "https://files.pythonhosted.org/packages/63/22/8553ff6166f5cd21cfaa115aaacaa0dc73b91c079a8cfd54a482cbc0f4f5/fonttools-4.60.1-cp314-cp314t-win32.whl", hash = "sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259", size = 2282241, upload-time = "2025-09-29T21:12:58.179Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/fa7b4d148e11d5a72761a22e595344133e83a9507a4c231df972e657579b/fonttools-4.60.1-cp314-cp314t-win_amd64.whl", hash = "sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c", size = 2345760, upload-time = "2025-09-29T21:13:00.375Z" }, + { url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" }, +] + +[[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/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" }, +] + +[[package]] +name = "llvmlite" +version = "0.45.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/8d/5baf1cef7f9c084fb35a8afbde88074f0d6a727bc63ef764fe0e7543ba40/llvmlite-0.45.1.tar.gz", hash = "sha256:09430bb9d0bb58fc45a45a57c7eae912850bedc095cd0810a57de109c69e1c32", size = 185600, upload-time = "2025-10-01T17:59:52.046Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/e2/c185bb7e88514d5025f93c6c4092f6120c6cea8fe938974ec9860fb03bbb/llvmlite-0.45.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:d9ea9e6f17569a4253515cc01dade70aba536476e3d750b2e18d81d7e670eb15", size = 43043524, upload-time = "2025-10-01T18:03:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/09/b8/b5437b9ecb2064e89ccf67dccae0d02cd38911705112dd0dcbfa9cd9a9de/llvmlite-0.45.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:c9f3cadee1630ce4ac18ea38adebf2a4f57a89bd2740ce83746876797f6e0bfb", size = 37253121, upload-time = "2025-10-01T18:04:30.557Z" }, + { url = "https://files.pythonhosted.org/packages/f7/97/ad1a907c0173a90dd4df7228f24a3ec61058bc1a9ff8a0caec20a0cc622e/llvmlite-0.45.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:57c48bf2e1083eedbc9406fb83c4e6483017879714916fe8be8a72a9672c995a", size = 56288210, upload-time = "2025-10-01T18:01:40.26Z" }, + { url = "https://files.pythonhosted.org/packages/32/d8/c99c8ac7a326e9735401ead3116f7685a7ec652691aeb2615aa732b1fc4a/llvmlite-0.45.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3aa3dfceda4219ae39cf18806c60eeb518c1680ff834b8b311bd784160b9ce40", size = 55140957, upload-time = "2025-10-01T18:02:46.244Z" }, + { url = "https://files.pythonhosted.org/packages/09/56/ed35668130e32dbfad2eb37356793b0a95f23494ab5be7d9bf5cb75850ee/llvmlite-0.45.1-cp313-cp313-win_amd64.whl", hash = "sha256:080e6f8d0778a8239cd47686d402cb66eb165e421efa9391366a9b7e5810a38b", size = 38132232, upload-time = "2025-10-01T18:05:14.477Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.7" +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/ae/e2/d2d5295be2f44c678ebaf3544ba32d20c1f9ef08c49fe47f496180e1db15/matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7", size = 34804865, upload-time = "2025-10-09T00:28:00.669Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/9c/207547916a02c78f6bdd83448d9b21afbc42f6379ed887ecf610984f3b4e/matplotlib-3.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1d9d3713a237970569156cfb4de7533b7c4eacdd61789726f444f96a0d28f57f", size = 8273212, upload-time = "2025-10-09T00:26:56.752Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37a1fea41153dd6ee061d21ab69c9cf2cf543160b1b85d89cd3d2e2a7902ca4c", size = 8128713, upload-time = "2025-10-09T00:26:59.001Z" }, + { url = "https://files.pythonhosted.org/packages/22/ff/6425bf5c20d79aa5b959d1ce9e65f599632345391381c9a104133fe0b171/matplotlib-3.10.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b3c4ea4948d93c9c29dc01c0c23eef66f2101bf75158c291b88de6525c55c3d1", size = 8698527, upload-time = "2025-10-09T00:27:00.69Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7f/ccdca06f4c2e6c7989270ed7829b8679466682f4cfc0f8c9986241c023b6/matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22df30ffaa89f6643206cf13877191c63a50e8f800b038bc39bee9d2d4957632", size = 9529690, upload-time = "2025-10-09T00:27:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/b80fc2c1f269f21ff3d193ca697358e24408c33ce2b106a7438a45407b63/matplotlib-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b69676845a0a66f9da30e87f48be36734d6748024b525ec4710be40194282c84", size = 9593732, upload-time = "2025-10-09T00:27:04.653Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b6/23064a96308b9aeceeffa65e96bcde459a2ea4934d311dee20afde7407a0/matplotlib-3.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:744991e0cc863dd669c8dc9136ca4e6e0082be2070b9d793cbd64bec872a6815", size = 8122727, upload-time = "2025-10-09T00:27:06.814Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a6/2faaf48133b82cf3607759027f82b5c702aa99cdfcefb7f93d6ccf26a424/matplotlib-3.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:fba2974df0bf8ce3c995fa84b79cde38326e0f7b5409e7a3a481c1141340bcf7", size = 7992958, upload-time = "2025-10-09T00:27:08.567Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f0/b018fed0b599bd48d84c08794cb242227fe3341952da102ee9d9682db574/matplotlib-3.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:932c55d1fa7af4423422cb6a492a31cbcbdbe68fd1a9a3f545aa5e7a143b5355", size = 8316849, upload-time = "2025-10-09T00:27:10.254Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b7/bb4f23856197659f275e11a2a164e36e65e9b48ea3e93c4ec25b4f163198/matplotlib-3.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e38c2d581d62ee729a6e144c47a71b3f42fb4187508dbbf4fe71d5612c3433b", size = 8178225, upload-time = "2025-10-09T00:27:12.241Z" }, + { url = "https://files.pythonhosted.org/packages/62/56/0600609893ff277e6f3ab3c0cef4eafa6e61006c058e84286c467223d4d5/matplotlib-3.10.7-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:786656bb13c237bbcebcd402f65f44dd61ead60ee3deb045af429d889c8dbc67", size = 8711708, upload-time = "2025-10-09T00:27:13.879Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1a/6bfecb0cafe94d6658f2f1af22c43b76cf7a1c2f0dc34ef84cbb6809617e/matplotlib-3.10.7-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09d7945a70ea43bf9248f4b6582734c2fe726723204a76eca233f24cffc7ef67", size = 9541409, upload-time = "2025-10-09T00:27:15.684Z" }, + { url = "https://files.pythonhosted.org/packages/08/50/95122a407d7f2e446fd865e2388a232a23f2b81934960ea802f3171518e4/matplotlib-3.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0b181e9fa8daf1d9f2d4c547527b167cb8838fc587deabca7b5c01f97199e84", size = 9594054, upload-time = "2025-10-09T00:27:17.547Z" }, + { url = "https://files.pythonhosted.org/packages/13/76/75b194a43b81583478a81e78a07da8d9ca6ddf50dd0a2ccabf258059481d/matplotlib-3.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:31963603041634ce1a96053047b40961f7a29eb8f9a62e80cc2c0427aa1d22a2", size = 8200100, upload-time = "2025-10-09T00:27:20.039Z" }, + { url = "https://files.pythonhosted.org/packages/f5/9e/6aefebdc9f8235c12bdeeda44cc0383d89c1e41da2c400caf3ee2073a3ce/matplotlib-3.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:aebed7b50aa6ac698c90f60f854b47e48cd2252b30510e7a1feddaf5a3f72cbf", size = 8042131, upload-time = "2025-10-09T00:27:21.608Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4b/e5bc2c321b6a7e3a75638d937d19ea267c34bd5a90e12bee76c4d7c7a0d9/matplotlib-3.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d883460c43e8c6b173fef244a2341f7f7c0e9725c7fe68306e8e44ed9c8fb100", size = 8273787, upload-time = "2025-10-09T00:27:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/86/ad/6efae459c56c2fbc404da154e13e3a6039129f3c942b0152624f1c621f05/matplotlib-3.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07124afcf7a6504eafcb8ce94091c5898bbdd351519a1beb5c45f7a38c67e77f", size = 8131348, upload-time = "2025-10-09T00:27:24.926Z" }, + { url = "https://files.pythonhosted.org/packages/a6/5a/a4284d2958dee4116359cc05d7e19c057e64ece1b4ac986ab0f2f4d52d5a/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c17398b709a6cce3d9fdb1595c33e356d91c098cd9486cb2cc21ea2ea418e715", size = 9533949, upload-time = "2025-10-09T00:27:26.704Z" }, + { url = "https://files.pythonhosted.org/packages/de/ff/f3781b5057fa3786623ad8976fc9f7b0d02b2f28534751fd5a44240de4cf/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7146d64f561498764561e9cd0ed64fcf582e570fc519e6f521e2d0cfd43365e1", size = 9804247, upload-time = "2025-10-09T00:27:28.514Z" }, + { url = "https://files.pythonhosted.org/packages/47/5a/993a59facb8444efb0e197bf55f545ee449902dcee86a4dfc580c3b61314/matplotlib-3.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90ad854c0a435da3104c01e2c6f0028d7e719b690998a2333d7218db80950722", size = 9595497, upload-time = "2025-10-09T00:27:30.418Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a5/77c95aaa9bb32c345cbb49626ad8eb15550cba2e6d4c88081a6c2ac7b08d/matplotlib-3.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:4645fc5d9d20ffa3a39361fcdbcec731382763b623b72627806bf251b6388866", size = 8252732, upload-time = "2025-10-09T00:27:32.332Z" }, + { url = "https://files.pythonhosted.org/packages/74/04/45d269b4268d222390d7817dae77b159651909669a34ee9fdee336db5883/matplotlib-3.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:9257be2f2a03415f9105c486d304a321168e61ad450f6153d77c69504ad764bb", size = 8124240, upload-time = "2025-10-09T00:27:33.94Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c7/ca01c607bb827158b439208c153d6f14ddb9fb640768f06f7ca3488ae67b/matplotlib-3.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1e4bbad66c177a8fdfa53972e5ef8be72a5f27e6a607cec0d8579abd0f3102b1", size = 8316938, upload-time = "2025-10-09T00:27:35.534Z" }, + { url = "https://files.pythonhosted.org/packages/84/d2/5539e66e9f56d2fdec94bb8436f5e449683b4e199bcc897c44fbe3c99e28/matplotlib-3.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8eb7194b084b12feb19142262165832fc6ee879b945491d1c3d4660748020c4", size = 8178245, upload-time = "2025-10-09T00:27:37.334Z" }, + { url = "https://files.pythonhosted.org/packages/77/b5/e6ca22901fd3e4fe433a82e583436dd872f6c966fca7e63cf806b40356f8/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d41379b05528091f00e1728004f9a8d7191260f3862178b88e8fd770206318", size = 9541411, upload-time = "2025-10-09T00:27:39.387Z" }, + { url = "https://files.pythonhosted.org/packages/9e/99/a4524db57cad8fee54b7237239a8f8360bfcfa3170d37c9e71c090c0f409/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a74f79fafb2e177f240579bc83f0b60f82cc47d2f1d260f422a0627207008ca", size = 9803664, upload-time = "2025-10-09T00:27:41.492Z" }, + { url = "https://files.pythonhosted.org/packages/e6/a5/85e2edf76ea0ad4288d174926d9454ea85f3ce5390cc4e6fab196cbf250b/matplotlib-3.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:702590829c30aada1e8cef0568ddbffa77ca747b4d6e36c6d173f66e301f89cc", size = 9594066, upload-time = "2025-10-09T00:27:43.694Z" }, + { url = "https://files.pythonhosted.org/packages/39/69/9684368a314f6d83fe5c5ad2a4121a3a8e03723d2e5c8ea17b66c1bad0e7/matplotlib-3.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:f79d5de970fc90cd5591f60053aecfce1fcd736e0303d9f0bf86be649fa68fb8", size = 8342832, upload-time = "2025-10-09T00:27:45.543Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/e22e08da14bc1a0894184640d47819d2338b792732e20d292bf86e5ab785/matplotlib-3.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:cb783436e47fcf82064baca52ce748af71725d0352e1d31564cbe9c95df92b9c", size = 8172585, upload-time = "2025-10-09T00:27:47.185Z" }, +] + +[[package]] +name = "numba" +version = "0.62.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/20/33dbdbfe60e5fd8e3dbfde299d106279a33d9f8308346022316781368591/numba-0.62.1.tar.gz", hash = "sha256:7b774242aa890e34c21200a1fc62e5b5757d5286267e71103257f4e2af0d5161", size = 2749817, upload-time = "2025-09-29T10:46:31.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/76/501ea2c07c089ef1386868f33dff2978f43f51b854e34397b20fc55e0a58/numba-0.62.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:b72489ba8411cc9fdcaa2458d8f7677751e94f0109eeb53e5becfdc818c64afb", size = 2685766, upload-time = "2025-09-29T10:43:49.161Z" }, + { url = "https://files.pythonhosted.org/packages/80/68/444986ed95350c0611d5c7b46828411c222ce41a0c76707c36425d27ce29/numba-0.62.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:44a1412095534a26fb5da2717bc755b57da5f3053965128fe3dc286652cc6a92", size = 2688741, upload-time = "2025-09-29T10:44:10.07Z" }, + { url = "https://files.pythonhosted.org/packages/78/7e/bf2e3634993d57f95305c7cee4c9c6cb3c9c78404ee7b49569a0dfecfe33/numba-0.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c9460b9e936c5bd2f0570e20a0a5909ee6e8b694fd958b210e3bde3a6dba2d7", size = 3804576, upload-time = "2025-09-29T10:42:59.53Z" }, + { url = "https://files.pythonhosted.org/packages/e8/b6/8a1723fff71f63bbb1354bdc60a1513a068acc0f5322f58da6f022d20247/numba-0.62.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:728f91a874192df22d74e3fd42c12900b7ce7190b1aad3574c6c61b08313e4c5", size = 3503367, upload-time = "2025-09-29T10:43:26.326Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ec/9d414e7a80d6d1dc4af0e07c6bfe293ce0b04ea4d0ed6c45dad9bd6e72eb/numba-0.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:bbf3f88b461514287df66bc8d0307e949b09f2b6f67da92265094e8fa1282dd8", size = 2745529, upload-time = "2025-09-29T10:44:31.738Z" }, +] + +[[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/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" }, +] + +[[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 = "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/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" }, +] + +[[package]] +name = "pydantic" +version = "2.12.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, +] + +[[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 = "pyqtgraph" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/36/4c242f81fdcbfa4fb62a5645f6af79191f4097a0577bd5460c24f19cc4ef/pyqtgraph-0.14.0-py3-none-any.whl", hash = "sha256:7abb7c3e17362add64f8711b474dffac5e7b0e9245abdf992e9a44119b7aa4f5", size = 1924755, upload-time = "2025-11-16T19:43:22.251Z" }, +] + +[[package]] +name = "pyside6" +version = "6.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyside6-addons" }, + { name = "pyside6-essentials" }, + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/98/84b16f78b5d92dd234fb1eb9890a350a5b0c83d985bb8c44a92f813a2d02/pyside6-6.10.0-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:c2cbc5dc2a164e3c7c51b3435e24203e90e5edd518c865466afccbd2e5872bb0", size = 558115, upload-time = "2025-10-08T09:47:09.246Z" }, + { url = "https://files.pythonhosted.org/packages/4e/76/0961c8c5653ecb60a6881b649dcb6b71a6be5bd1c8d441ecc48ac7f50b1a/pyside6-6.10.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ae8c3c8339cd7c3c9faa7cc5c52670dcc8662ccf4b63a6fed61c6345b90c4c01", size = 557762, upload-time = "2025-10-08T09:47:11.819Z" }, + { url = "https://files.pythonhosted.org/packages/c8/73/6187502fff8b6599443d15c46dd900b2ded24be5aacb2becce33f6faf566/pyside6-6.10.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:9f402f883e640048fab246d36e298a5e16df9b18ba2e8c519877e472d3602820", size = 558299, upload-time = "2025-10-08T09:47:14.255Z" }, + { url = "https://files.pythonhosted.org/packages/43/67/94794ebaf198bbdb35cb77f19f38370f9b323b036ab149874bc33c38faab/pyside6-6.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:70a8bcc73ea8d6baab70bba311eac77b9a1d31f658d0b418e15eb6ea36c97e6f", size = 564367, upload-time = "2025-10-08T09:47:16.287Z" }, + { url = "https://files.pythonhosted.org/packages/1d/cc/552331d413c1b933d54ed45e33cc7ff29d0b239677975fe2528e7ac8bfbc/pyside6-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:4b709bdeeb89d386059343a5a706fc185cee37b517bda44c7d6b64d5fdaf3339", size = 548826, upload-time = "2025-10-08T09:47:18.399Z" }, +] + +[[package]] +name = "pyside6-addons" +version = "6.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyside6-essentials" }, + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/23/9fbdec2ce16244ac3fe28e6d44c39c70465c93a03325939a792fd00fde7f/pyside6_addons-6.10.0-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:88e61e21ee4643cdd9efb39ec52f4dc1ac74c0b45c5b7fa453d03c094f0a8a5c", size = 322248256, upload-time = "2025-10-08T09:47:37.844Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b8/d129210f2c7366b4e1bf5bb6230be42052b29e8ba1b1d7db6ef333cf5a39/pyside6_addons-6.10.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:08d4ed46c4c9a353a9eb84134678f8fdd4ce17fb8cce2b3686172a7575025464", size = 170238987, upload-time = "2025-10-08T09:47:51.446Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ae/ede1edd009395092219f3437d2ee59f9ba93739c28c040542ed47c6cc831/pyside6_addons-6.10.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:15d32229d681be0bba1b936c4a300da43d01e1917ada5b57f9e03a387c245ab0", size = 165939425, upload-time = "2025-10-08T09:48:02.073Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5d/a3c32f85ac7f905c95679967c0ddda0ba043c273b75623cc90d8185064e4/pyside6_addons-6.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:99d93a32c17c5f6d797c3b90dd58f2a8bae13abde81e85802c34ceafaee11859", size = 164814172, upload-time = "2025-10-08T09:48:12.891Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2a/4ff71b09571202c8e1320c45276fc1d0cd81ee53107dfc17bb22d4243f88/pyside6_addons-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:92536427413f3b6557cf53f1a515cd766725ee46a170aff57ad2ff1dfce0ffb1", size = 34104251, upload-time = "2025-10-08T09:48:18.287Z" }, +] + +[[package]] +name = "pyside6-essentials" +version = "6.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/55/bad02ab890c8b8101abef0db4a2e5304be78a69e23a438e4d8555b664467/pyside6_essentials-6.10.0-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:003e871effe1f3e5b876bde715c15a780d876682005a6e989d89f48b8b93e93a", size = 105034090, upload-time = "2025-10-08T09:48:24.944Z" }, + { url = "https://files.pythonhosted.org/packages/5c/75/e17efc7eb900993e0e3925885635c6cf373c817196f09bcbcc102b00ac94/pyside6_essentials-6.10.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1d5e013a8698e37ab8ef360e6960794eb5ef20832a8d562e649b8c5a0574b2d8", size = 76362150, upload-time = "2025-10-08T09:48:31.849Z" }, + { url = "https://files.pythonhosted.org/packages/06/62/fbd1e81caafcda97b147c03f5b06cfaadd8da5fa8298f527d2ec648fa5b7/pyside6_essentials-6.10.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:b1dd0864f0577a448fb44426b91cafff7ee7cccd1782ba66491e1c668033f998", size = 75454169, upload-time = "2025-10-08T09:48:38.21Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3a/d8211d17e6ca70f641c6ebd309f08ef18930acda60e74082c75875a274da/pyside6_essentials-6.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:fc167eb211dd1580e20ba90d299e74898e7a5a1306d832421e879641fc03b6fe", size = 74361794, upload-time = "2025-10-08T09:48:44.335Z" }, + { url = "https://files.pythonhosted.org/packages/61/e9/0e22e3c10325c4ff09447fadb43f7962afb82cef0b65358f5704251c6b32/pyside6_essentials-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:6dd0936394cb14da2fd8e869899f5e0925a738b1c8d74c2f22503720ea363fb1", size = 55099467, upload-time = "2025-10-08T09:48:50.902Z" }, +] + +[[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/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 = "shiboken6" +version = "6.10.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/78/3e730aea82089dd82b1e092bc265778bda329459e6ad9b7134eec5fff3f2/shiboken6-6.10.0-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:7a5f5f400ebfb3a13616030815708289c2154e701a60b9db7833b843e0bee543", size = 476535, upload-time = "2025-10-08T09:49:08Z" }, + { url = "https://files.pythonhosted.org/packages/ea/09/4ffa3284a17b6b765d45b41c9a7f1b2cde6c617c853ac6f170fb62bbbece/shiboken6-6.10.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:e612734da515d683696980107cdc0396a3ae0f07b059f0f422ec8a2333810234", size = 271098, upload-time = "2025-10-08T09:49:09.47Z" }, + { url = "https://files.pythonhosted.org/packages/31/29/00e26f33a0fb259c2edce9c761a7a438d7531ca514bdb1a4c072673bd437/shiboken6-6.10.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:b01377e68d14132360efb0f4b7233006d26aa8ae9bb50edf00960c2a5f52d148", size = 267698, upload-time = "2025-10-08T09:49:10.694Z" }, + { url = "https://files.pythonhosted.org/packages/11/30/e4624a7e3f0dc9796b701079b77defcce0d32d1afc86bb1d0df04bc3d9e2/shiboken6-6.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:0bc5631c1bf150cbef768a17f5f289aae1cb4db6c6b0c19b2421394e27783717", size = 1234227, upload-time = "2025-10-08T09:49:12.774Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e5/0ab862005ea87dc8647ba958a3099b3b0115fd6491c65da5c5a0f6364db1/shiboken6-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:dfc4beab5fec7dbbebbb418f3bf99af865d6953aa0795435563d4cbb82093b61", size = 1794775, upload-time = "2025-10-08T09:49:14.641Z" }, +] + +[[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" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +]