diff --git a/Cargo.lock b/Cargo.lock index 31445755..549f7e4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -367,7 +367,6 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "rawpointer", - "rayon", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f9a31f80..feb01dde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,12 +10,12 @@ crate-type = ["cdylib", "lib"] path = "src-rust/lib.rs" [dependencies] -ndarray = { version = "0.17.2", features = ["rayon"] } +ndarray = "0.17.2" numpy = "0.29" pyo3 = "0.29" [dev-dependencies] -ndarray = { version = "0.17.2", features = ["rayon", "approx"] } +ndarray = { version = "0.17.2", features = ["approx"] } approx = "0.5.1" criterion = { version = "0.8.2", features = ["html_reports"] } proptest = "1.11.0" diff --git a/IM/im_calculation.py b/IM/im_calculation.py index 3188a67a..7ca9af11 100644 --- a/IM/im_calculation.py +++ b/IM/im_calculation.py @@ -1,10 +1,10 @@ """IM calculation script for ascii waveforms""" -import multiprocessing from pathlib import Path import numpy as np import pandas as pd +import xarray as xr from IM import ims from IM.ims import IM @@ -151,13 +151,37 @@ def frequency_label(frequency: float) -> str: return f"{frequency:.{FREQUENCY_LABEL_SIGNIFICANT_FIGURES}g}" +def _dataset_to_frame(dataset: xr.Dataset, index: list[str]) -> pd.DataFrame: + """Convert a component-per-variable IM dataset into a wide DataFrame. + + Each component (`000`, `090`, ..., `rotd100`) is already a data variable, + so the dataset's own columns are the frame's columns; this just drops + any non-dimension coordinates (e.g. `latitude`/`longitude`, when a real + DataArray is passed in) and replaces the row index with `index`. + + Parameters + ---------- + dataset : xr.Dataset + Dataset with one data variable per component. + index : list of str + Row labels to assign to the resulting DataFrame. + + Returns + ------- + pd.DataFrame + Wide-format DataFrame with component names as columns. + """ + frame = dataset.reset_coords(drop=True).to_dataframe() + frame.index = index + return frame + + def calculate_ims( waveform: np.ndarray, dt: float, ims_list: list[IM] | None = None, periods: np.ndarray = DEFAULT_PERIODS, frequencies: np.ndarray = DEFAULT_FREQUENCIES, - cores: int = multiprocessing.cpu_count(), ko_directory: Path | None = None, ): """ @@ -175,8 +199,6 @@ def calculate_ims( List of periods required for calculating the pseudo-spectral acceleration (pSA). frequencies : np.ndarray, optional List of frequencies required for calculating the Fourier amplitude spectrum (FAS). - cores : int, optional - Number of cores to use for parallel processing in pSA and FAS calculations. ko_directory : Path, optional Path to the directory containing the Konno-Ohmachi matrices. Only required if FAS is in the list of IMs. @@ -205,56 +227,51 @@ def calculate_ims( # Iterate through IMs and calculate them for im in ims_list: if im == IM.PGA: - result = ims.peak_ground_acceleration(waveform, cores) - result.index = [im.value] + dataset = ims.peak_ground_acceleration(waveform) + result = _dataset_to_frame(dataset, [im.value]) elif im == IM.PGV: - result = ims.peak_ground_velocity(waveform, dt, cores) - result.index = [im.value] + dataset = ims.peak_ground_velocity(waveform, dt) + result = _dataset_to_frame(dataset, [im.value]) elif im == IM.PGD: - result = ims.peak_ground_displacement(waveform, dt, cores) - result.index = [im.value] + dataset = ims.peak_ground_displacement(waveform, dt) + result = _dataset_to_frame(dataset, [im.value]) elif im == IM.pSA: - data_array = ims.pseudo_spectral_acceleration( - waveform, periods, np.float64(dt), cores=cores + dataset = ims.pseudo_spectral_acceleration(waveform, periods, dt) + result = _dataset_to_frame( + dataset, + [f"{im.value}_{idx}" for idx in dataset.coords["period"].values], ) - # Convert the data array to a DataFrame - result = data_array.to_dataframe().unstack(level="component") - result.index = [ - f"{im.value}_{idx}" for idx in data_array.coords["period"].values - ] - result.columns = result.columns.droplevel(0) # ty: ignore[invalid-assignment, invalid-argument-type] elif im == IM.CAV: - result = ims.cumulative_absolute_velocity(waveform, dt, cores) - result.index = [im.value] + dataset = ims.cumulative_absolute_velocity(waveform, dt) + result = _dataset_to_frame(dataset, [im.value]) elif im == IM.CAV5: - result = ims.cumulative_absolute_velocity(waveform, dt, cores, threshold=5) - result.index = [im.value] + dataset = ims.cumulative_absolute_velocity(waveform, dt, threshold=5) + result = _dataset_to_frame(dataset, [im.value]) elif im == IM.Ds575: - result = ims.ds575(waveform, dt, cores) - result.index = [im.value] + dataset = ims.ds575(waveform, dt) + result = _dataset_to_frame(dataset, [im.value]) elif im == IM.Ds595: - result = ims.ds595(waveform, dt, cores) - result.index = [im.value] + dataset = ims.ds595(waveform, dt) + result = _dataset_to_frame(dataset, [im.value]) elif im == IM.AI: - result = ims.arias_intensity(waveform, dt, cores) - result.index = [im.value] + dataset = ims.arias_intensity(waveform, dt) + result = _dataset_to_frame(dataset, [im.value]) elif im == IM.FAS: assert ko_directory - data_array = ims.fourier_amplitude_spectra( + dataset = ims.fourier_amplitude_spectra( waveform, dt, frequencies, - cores=cores, # ko_directory must be Path because of the check earlier. ko_directory=ko_directory, ) - # Convert the data array to a DataFrame - result = data_array.to_dataframe().unstack(level="component") - result.index = [ - f"{im.value}_{frequency_label(idx)}" - for idx in data_array.coords["frequency"].values - ] - result.columns = result.columns.droplevel(0) # ty: ignore[invalid-assignment, invalid-argument-type] + result = _dataset_to_frame( + dataset, + [ + f"{im.value}_{frequency_label(idx)}" + for idx in dataset.coords["frequency"].values + ], + ) else: raise ValueError( f"IM {im} not recognized. Available IMs are {IM.__members__.keys()}" diff --git a/IM/ims.py b/IM/ims.py index a597f2cb..6c428c16 100644 --- a/IM/ims.py +++ b/IM/ims.py @@ -1,19 +1,14 @@ """Intensity Measure Implementations.""" -import itertools -import multiprocessing -import os +import functools import warnings -from collections.abc import Generator, MutableMapping -from contextlib import contextmanager +from collections.abc import Callable, Mapping, Sequence from enum import IntEnum, StrEnum from pathlib import Path import numpy as np import numpy.typing as npt -import pandas as pd import scipy as sp -import tqdm import xarray as xr from pyfftw.interfaces import numpy_fft as fft @@ -22,44 +17,35 @@ ko_matrices, ) -# (n_components, n_stations, nt) +# Concrete (n_components, n_stations, nt) block, as seen inside a kernel once +# `_components` has flattened any leading (broadcast) axes into `n_stations`. ChunkedWaveformArray = np.ndarray[tuple[int, int, int], np.dtype[np.float64]] -# (n_stations, nt) -SingleWaveformArray = np.ndarray[tuple[int, int], np.dtype[np.float64]] -WaveformArray = ChunkedWaveformArray | SingleWaveformArray -Array1D = np.ndarray[tuple[int], np.dtype[np.float64]] - - -@contextmanager -def environment( - **variables: str, - # NOTE: the type here could be the os._Environ type defined in the - # os module, but this means we don't rely on any specific - # behaviour of that object which might change later down the line - # (or indeed, they may remove the os._Environ object at any time - # because it is an internal class). -) -> Generator[MutableMapping[str, str]]: - """Update an environment and revert after exit - Parameters - ---------- - **variables : str or bytes - Environment values to update inside the context manager. +# A (component, station, time) waveform. A bare ndarray is wrapped eagerly by +# `_as_waveform`; a dask-backed DataArray stays lazy end to end. +Waveform = xr.DataArray | np.ndarray + +WAVEFORM_DIMS = ("component", "station", "time") +ROTD_COMPONENTS = ( + "000", + "090", + "ver", + "geom", + "rotd0", + "rotd50", + "rotd100", + "rotd0_orientation", + "rotd50_orientation", + "rotd100_orientation", +) +GEOM_COMPONENTS = ("000", "090", "ver", "geom") +FAS_COMPONENTS = ("000", "090", "ver", "geom", "eas") - Yields - ------ - MutableMapping - The mapping object representing `os.environ`. - """ - # Code to acquire resource, e.g.: - old_environment: dict[str, str] = os.environ.copy() - try: - os.environ.update(variables) - yield os.environ - finally: - for key in set(os.environ) - set(old_environment): - del os.environ[key] - os.environ.update(old_environment) +DAMPING = 0.05 +G = 981 +# Bounds how much of a (possibly multi-gigabyte, float32, memmapped) Konno +# matrix gets promoted to float64 at once by `_konno_smooth`. +KONNO_BLOCK_BYTES = 64 * 2**20 class Component(IntEnum): @@ -88,593 +74,748 @@ class IM(StrEnum): FAS = "FAS" -def pseudo_spectral_acceleration( - waveforms: ChunkedWaveformArray, - periods: Array1D, - dt: np.float64, - cores: int = multiprocessing.cpu_count(), - step: int | None = None, - use_tqdm: bool = False, -) -> xr.DataArray: - """Compute pseudo-spectral acceleration (PSA) statistics. +def _as_waveform(waveform: Waveform) -> xr.DataArray: + """Normalise a waveform into a DataArray with `component` and `time` dims. - Calculates PSA for single-degree-of-freedom oscillators across various - periods using the Newmark-beta method and computes rotated (RotD) statistics. + A bare `(n_components, n_stations, nt)` ndarray is wrapped as an eager + DataArray with dims `("component", "station", "time")`. A dask-backed + DataArray is rechunked so that `component` and `time` -- the core + dimensions every kernel operates on -- are each a single chunk; any + `station` chunking is left untouched. Parameters ---------- - waveforms : ChunkedWaveformArray - Acceleration waveforms (g) with shape (n_components, n_stations, nt). - periods : Array1 - Natural periods of the oscillators (s). - dt : np.float64 - Timestep resolution of the waveforms (s). - cores : int, optional - Number of CPU cores for parallel processing via Rayon. - step : int, optional - Station chunk size for processing. Defaults to `cores` if None. - use_tqdm : bool, optional - Whether to display a progress bar. + waveform : Waveform + Either a bare `(n_components, n_stations, nt)` ndarray, or an + `xr.DataArray` with `component` and `time` dimensions. Returns ------- xr.DataArray - A 3D DataArray (component, period, station) containing PSA for - ['000', '090', 'ver', 'geom', 'rotd0', 'rotd50', 'rotd100']. - """ - waveforms = np.ascontiguousarray(waveforms) - angular_frequencies = 2 * np.pi / periods - - # Step size *used* to be based on the cores available but that no - # longer holds because Rayon, the rust parallel work scheduler, - # manages this on its own. So the real bound on step size is now - # how much memory we have available. - step = step or cores - n_stations = waveforms.shape[1] - n_frequencies = len(angular_frequencies) - rotd_psa = np.zeros((n_frequencies, n_stations, 3), dtype=np.float64) - - comp_0_psa = np.zeros((n_frequencies, n_stations), dtype=np.float64) - comp_90_psa = np.zeros((n_frequencies, n_stations), dtype=np.float64) - comp_ver_psa = np.zeros((n_frequencies, n_stations), dtype=np.float64) - xi = 0.05 - with environment(RAYON_NUM_THREADS=str(cores)): - station_iter = range(0, n_stations, step) - n_steps = len(station_iter) * len(angular_frequencies) - station_period_iterator = itertools.product( - range(len(angular_frequencies)), station_iter - ) - # Coverage tests don't cover this interactive usage (because - # it doesn't change the calculations). - if use_tqdm: # pragma no cover - station_period_iterator = tqdm.tqdm(station_period_iterator, total=n_steps) - j_last: int | None = None - for j, i in station_period_iterator: - w = angular_frequencies[j] - if use_tqdm and j_last != j: # pragma: no cover - assert isinstance(station_period_iterator, tqdm.tqdm) - j_last = j - t0 = periods[j] - station_period_iterator.set_description(f"Period {t0:g}") - - comp_0_chunk = waveforms[Component.COMP_0.value, i : i + step].astype( - np.float64 - ) - comp_90_chunk = waveforms[Component.COMP_90.value, i : i + step].astype( - np.float64 - ) - comp_0_response = _core._newmark_beta_method(comp_0_chunk, dt, w, xi) - comp_90_response = _core._newmark_beta_method(comp_90_chunk, dt, w, xi) - conversion_factor = w * w + The waveform as a DataArray suitable to pass to `xr.apply_ufunc`. - rotd_psa[j, i : i + step] = conversion_factor * _core._rotd_parallel( - comp_0_response, comp_90_response + Raises + ------ + TypeError + If the waveform has the wrong number of dimensions, is missing the + `component`/`time` dimensions, or does not have 3 components. + """ + if not isinstance(waveform, xr.DataArray): + array = np.asarray(waveform) + if array.ndim != 3: + raise TypeError( + "Waveform must have shape (n_components, n_stations, nt), " + f"but {array.shape=}" ) + waveform = xr.DataArray(array, dims=WAVEFORM_DIMS) - comp_0_psa[j, i : i + step] = conversion_factor * np.abs( - comp_0_response - ).max(axis=1) - comp_90_psa[j, i : i + step] = conversion_factor * np.abs( - comp_90_response - ).max(axis=1) - - z = waveforms[Component.COMP_VER.value, i : i + step].astype(np.float64) - z_response = _core._newmark_beta_method(z, dt, w, xi) - comp_ver_psa[j, i : i + step] = conversion_factor * np.abs(z_response).max( - axis=1 - ) + missing = {"component", "time"}.difference(waveform.dims) + if missing: + raise TypeError(f"Waveform is missing dimensions {sorted(missing)}") + if waveform.sizes["component"] != len(Component): + raise TypeError( + f"Waveform must have {len(Component)} components, " + f"but {waveform.sizes['component']=}" + ) + if waveform.chunks is not None: + waveform = waveform.chunk({"component": -1, "time": -1}) + return waveform - geom_psa = np.sqrt(comp_0_psa * comp_90_psa) - - return xr.DataArray( - np.stack( - [ - comp_0_psa, - comp_90_psa, - comp_ver_psa, - geom_psa, - rotd_psa[:, :, 0], - rotd_psa[:, :, 1], - rotd_psa[:, :, 2], - ], - axis=0, - ), - name=IM.pSA.value, - dims=( - "component", - "period", - "station", - ), - coords={ - "station": np.arange(waveforms.shape[1]), - "period": periods, - "component": ["000", "090", "ver", "geom", "rotd0", "rotd50", "rotd100"], - }, - ) +def _components(block: np.ndarray) -> tuple[ChunkedWaveformArray, tuple[int, ...]]: + """Split an `apply_ufunc` block into contiguous per-component matrices. -def significant_duration( - waveforms: ChunkedWaveformArray, - dt: float, - percent_low: float, - percent_high: float, - cores: int, -) -> pd.DataFrame: - """Compute significant duration based on Arias Intensity accumulation. + `apply_ufunc` moves core dimensions to the end, so `block` arrives as + `(*lead, n_components, nt)`, where `lead` is whatever loop dimensions the + input had (normally just `station`, but there may be none or several). + Moving the component axis to the front and forcing a contiguous float64 + copy collapses `lead` into a single row axis, so `components[i]` is a + contiguous `(n_rows, nt)` matrix -- what every `_core` kernel expects. + Callers restore the original leading shape with `out.reshape(lead + (...))`. Parameters ---------- - waveforms : ChunkedWaveformArray - Acceleration waveforms (g) with shape (n_components, n_stations, nt). - dt : float - Timestep resolution (s). - percent_low : float - Lower bound percentage (e.g., 5.0 for 5%). - percent_high : float - Upper bound percentage (e.g., 95.0 for 95%). - cores : int - Number of CPU cores for parallel execution. + block : ndarray + A block as received by an `apply_ufunc` kernel, of shape + `(*lead, n_components, nt)`. Returns ------- - pd.DataFrame - Significant duration (s) for components ['000', '090', 'ver', 'geom']. + ChunkedWaveformArray + The per-component matrices, contiguous float64, shape + `(n_components, prod(lead), nt)`. + tuple of int + The original leading shape, to reshape kernel output back into. """ - (_, n_stations, _) = waveforms.shape - comp_0 = waveforms[Component.COMP_0] - comp_90 = waveforms[Component.COMP_90] - comp_ver = waveforms[Component.COMP_VER] - quant_low = percent_low / 100 - quant_high = percent_high / 100 - - if ( - cores == 1 or n_stations < 1000 - ): # from benchmarks: for < 1000 stations the parallel overhead is not worth it. - significant_duration_0 = _core._significant_duration( - comp_0, dt, quant_low, quant_high - ) - significant_duration_90 = _core._significant_duration( - comp_90, dt, quant_low, quant_high - ) - significant_duration_ver = _core._significant_duration( - comp_ver, dt, quant_low, quant_high - ) - else: - # Testing is not big enough for multi-core execution so this codepath is not covered. - with environment(RAYON_NUM_THREADS=str(cores)): # pragma: no cover - significant_duration_0 = _core._parallel_significant_duration( - comp_0, dt, quant_low, quant_high - ) - significant_duration_90 = _core._parallel_significant_duration( - comp_90, dt, quant_low, quant_high - ) - significant_duration_ver = _core._parallel_significant_duration( - comp_ver, dt, quant_low, quant_high - ) - - return pd.DataFrame( - { - "000": significant_duration_0, - "090": significant_duration_90, - "ver": significant_duration_ver, - "geom": np.sqrt(significant_duration_0 * significant_duration_90), - } - ) - - -def smooth_and_interpolate( - spectrum_data: np.ndarray, - konno: np.ndarray, - freqs: npt.NDArray[np.float64], - fa_frequencies: np.ndarray, -) -> np.ndarray: - """ - Smooths and interpolates a spectrum. + n_components, nt = block.shape[-2:] + lead = block.shape[:-2] + components = np.ascontiguousarray(np.moveaxis(block, -2, 0), dtype=np.float64) + return components.reshape(n_components, -1, nt), lead + + +def _im_dataset( + kernel: Callable[..., np.ndarray], + waveform: Waveform, + components: Sequence[str], + *, + name: str, + extra_dims: Mapping[str, npt.NDArray] | None = None, + kwargs: Mapping[str, object] | None = None, +) -> xr.Dataset: + """Run a per-station kernel over a waveform, one data variable per component. + + The kernel receives a `(*lead, n_components, nt)` block (see + `_components`) and must return a `(*lead, *extra_sizes, len(components))` + array. The trailing component axis is unstacked into data variables, so + the result is a `Dataset` with one variable per component -- lazy if the + waveform was lazy. Parameters ---------- - spectrum_data : ndarray - The spectrum data to be smoothed and interpolated. - konno : ndarray - The Konno-Ohmachi smoothing matrix to apply to the spectrum data. - freqs : ndarray of float64 - The frequencies at which to interpolate the smoothed spectrum data. - fa_frequencies : ndarray - The original frequencies corresponding to the spectrum data before smoothing. + kernel : callable + Function to apply to each waveform block. + waveform : Waveform + The waveform to compute the IM for. + components : sequence of str + Names of the components the kernel produces, in output order. + name : str + Name recorded in `dataset.attrs["name"]`, identifying the IM. This is + the only place the IM name is carried, since `components` become + data variables rather than a `component` dimension. + extra_dims : mapping of str to ndarray, optional + Extra output dimensions the kernel introduces (e.g. `period` for pSA, + `frequency` for FAS), mapping dimension name to coordinate values. + kwargs : mapping, optional + Extra keyword arguments passed through to `kernel`. Returns ------- - ndarray - The smoothed and interpolated spectrum data at the specified frequencies. + xr.Dataset + One data variable per component, sharing the input's `station` + dimension and any non-dimension coordinates (e.g. real station + names, `latitude`, `longitude`). """ - smoothed = spectrum_data @ konno - interpolator = sp.interpolate.make_interp_spline( - fa_frequencies, smoothed, axis=-1, k=1 + extra_dims = extra_dims or {} + kwargs = kwargs or {} + waveform = _as_waveform(waveform) + + result = xr.apply_ufunc( + kernel, + waveform, + input_core_dims=[["component", "time"]], + output_core_dims=[[*extra_dims, "im_component"]], + kwargs=dict(kwargs), + keep_attrs=False, + dask="parallelized", + output_dtypes=[np.float64], + dask_gufunc_kwargs={ + "output_sizes": {"im_component": len(components)} + | {dim: len(values) for dim, values in extra_dims.items()} + }, ) - return interpolator(freqs) + result = result.assign_coords(im_component=list(components), **extra_dims) + dataset = result.to_dataset("im_component") + dataset.attrs = {"name": name} + return dataset -def fourier_amplitude_spectra( - waveforms: ChunkedWaveformArray, - dt: float, - freqs: npt.NDArray[np.float64], - ko_directory: Path, - cores: int = multiprocessing.cpu_count(), -) -> xr.DataArray: - """Compute Fourier Amplitude Spectrum (FAS) of seismic waveforms. - - The FAS is computed using FFT and then smoothed using the Konno-Ohmachi - smoothing algorithm. +def _rotd_kernel( + block: np.ndarray, + *, + transform: Callable[[ChunkedWaveformArray], ChunkedWaveformArray] | None = None, +) -> np.ndarray: + """Kernel for `compute_intensity_measure_rotd`. Parameters ---------- - waveforms : ndarray of float64 with shape `(n_components, n_stations, n_timesteps)` - Waveform array (g). - dt : float - Timestep resolution of the waveforms (s). - freqs : ndarray of float64 - Frequencies at which to compute FAS (Hz). - ko_directory : Path - Directory containing precomputed Konno-Ohmachi matrices. - cores : int, optional - Number of CPU cores to use, by default all available cores. + block : ndarray + A `(*lead, n_components, nt)` waveform block. + transform : callable, optional + Applied to the `(3, n_rows, nt)` component matrices before taking + peaks (e.g. integration for PGV/PGD). Must preserve the leading + `(3, n_rows, ...)` shape. Returns ------- - xr.DataArray - DataArray containing FAS values for each station, frequency and component ['000', '090', 'ver', 'eas']. + ndarray + A `(*lead, len(ROTD_COMPONENTS))` array. """ - nyquist_frequency = 1 / (2 * dt) - max_frequency = freqs.max() - if max_frequency > nyquist_frequency: - warnings.warn( - RuntimeWarning( - f"Attempting to compute FAS for frequencies above Nyquist frequency {nyquist_frequency:.2e} Hz. Results only include frequencies at or below Nyquist frequency {nyquist_frequency:.2e} Hz." - ), - ) - freqs = freqs[freqs <= nyquist_frequency] - - n_fft = 2 ** int(np.ceil(np.log2(waveforms.shape[-1]))) - # Essential! Repack the waveform array so that the rows are - # contiguous in memory. - waveforms = np.ascontiguousarray(waveforms) - fa_frequencies = np.fft.rfftfreq(n_fft, dt) - waveform_shape = list(waveforms.shape) - - waveform_shape[-1] = len(fa_frequencies) - n_components = waveform_shape[0] - fa_spectrum = np.empty(waveform_shape, dtype=waveforms.dtype) - for i in range(n_components): - fa_spectrum[i] = np.abs( - fft.rfft(waveforms[i], n=n_fft, axis=-1, threads=cores) * dt - ) - - # Get appropriate konno ohmachi matrix - konno = ko_matrices.get_konno_matrix(fa_spectrum.shape[-1], ko_directory) - # For optimal matrix-product calculation, repack the matrix in column-major order - # (i.e. Fortran order) to optimise cache efficiency and allow - # multi-threaded BLAS if enabled. - # - # NOTE: for matrices generated by new versions of - # gen_ko_matrix.py, this is a no-op because the arrays are already - # Fortran contiguous. Hence it creates no copy in memory. - konno = np.asfortranarray(konno) - fas_smooth = smooth_and_interpolate(fa_spectrum, konno, freqs, fa_frequencies) - - geom_fas = np.sqrt( - fas_smooth[Component.COMP_0.value] * fas_smooth[Component.COMP_90.value] - ) - # For EAS, we first we compute with the unsmoothed spectrum to avoid distortion of inter-frequency correlations, and then we apply the same smoothing to the EAS values. - eas_unsmoothed = np.sqrt( - 0.5 - * ( - np.square(fa_spectrum[Component.COMP_0.value]) - + np.square(fa_spectrum[Component.COMP_90.value]) - ) - ) - eas = smooth_and_interpolate(eas_unsmoothed, konno, freqs, fa_frequencies) - - return xr.DataArray( - np.stack( - [ - fas_smooth[Component.COMP_0.value], - fas_smooth[Component.COMP_90.value], - fas_smooth[Component.COMP_VER.value], - geom_fas, - eas, - ], - axis=0, - ), - name=IM.FAS.value, - dims=("component", "station", "frequency"), - coords={ - "component": ["000", "090", "ver", "geom", "eas"], - "frequency": freqs, - "station": np.arange(fas_smooth.shape[1]), - }, - ) + components, lead = _components(block) + if transform is not None: + components = transform(components) + comp_0, comp_90, comp_ver = components + peak_0 = np.abs(comp_0).max(axis=-1) + peak_90 = np.abs(comp_90).max(axis=-1) + peak_ver = np.abs(comp_ver).max(axis=-1) + # (rows, 6) = rotd0, rotd50, rotd100 then their three orientations. + stats = _core._rotd(comp_0, comp_90) + peaks = np.stack([peak_0, peak_90, peak_ver, np.sqrt(peak_0 * peak_90)], axis=-1) + out = np.concatenate([peaks, stats], axis=-1) + return out.reshape(lead + (len(ROTD_COMPONENTS),)) def compute_intensity_measure_rotd( - waveforms: ChunkedWaveformArray, cores: int -) -> pd.DataFrame: + waveforms: Waveform, + name: str, + *, + transform: Callable[[ChunkedWaveformArray], ChunkedWaveformArray] | None = None, +) -> xr.Dataset: """Generic wrapper to compute peak values and RotD statistics for IMs. Parameters ---------- - waveforms : ChunkedWaveformArray + waveforms : Waveform Waveform data with shape (n_components, n_stations, nt). - cores : int - Number of CPU cores for parallel rotation computation. + name : str + Name of the resulting dataset (recorded in `dataset.attrs["name"]`). + transform : callable, optional + Applied to the acceleration components before taking peaks (e.g. + integration to velocity/displacement for PGV/PGD). Returns ------- - pd.DataFrame - Peak ground values with columns ['000', '090', 'ver', 'geom', - 'rotd100', 'rotd50', 'rotd0']. + xr.Dataset + One data variable per component in `ROTD_COMPONENTS`: peak values for + `['000', '090', 'ver', 'geom', 'rotd0', 'rotd50', 'rotd100']`, then + `rotd0_orientation`, `rotd50_orientation` and `rotd100_orientation` + holding the angle (degrees) at which each RotD statistic occurs. """ - comp_0 = waveforms[Component.COMP_0] - comp_90 = waveforms[Component.COMP_90] - comp_ver = waveforms[Component.COMP_VER] - if cores == 1: - rotd_stats = _core._rotd(comp_0, comp_90) - else: - with environment(RAYON_NUM_THREADS=str(cores)): - rotd_stats = _core._rotd_parallel(comp_0, comp_90) - pga_comp_0 = np.abs(comp_0).max(axis=1) - pga_comp_90 = np.abs(comp_90).max(axis=1) - pga_ver = np.abs(comp_ver).max(axis=1) - rotd0 = rotd_stats[:, 0] - rotd50 = rotd_stats[:, 1] - rotd100 = rotd_stats[:, 2] - return pd.DataFrame( - { - "000": pga_comp_0, - "090": pga_comp_90, - "ver": pga_ver, - "geom": np.sqrt(pga_comp_0 * pga_comp_90), - "rotd100": rotd100, - "rotd50": rotd50, - "rotd0": rotd0, - } + return _im_dataset( + functools.partial(_rotd_kernel, transform=transform), + waveforms, + ROTD_COMPONENTS, + name=name, ) -def peak_ground_acceleration( - waveform: ChunkedWaveformArray, cores: int -) -> pd.DataFrame: +def _velocity(components: ChunkedWaveformArray, dt: float) -> ChunkedWaveformArray: + """Integrate acceleration (g) to velocity (cm/s).""" + return G * sp.integrate.cumulative_trapezoid(components, dx=dt, axis=-1) + + +def _displacement(components: ChunkedWaveformArray, dt: float) -> ChunkedWaveformArray: + """Integrate acceleration (g) to displacement (cm).""" + velocity = sp.integrate.cumulative_trapezoid(components, dx=dt, axis=-1, initial=0) + # In-place multiplication to avoid yet another allocation + np.multiply(G, velocity, out=velocity) + return sp.integrate.cumulative_trapezoid(velocity, dx=dt, axis=-1, initial=0) + + +def peak_ground_acceleration(waveform: Waveform) -> xr.Dataset: """Compute Peak Ground Acceleration (PGA) in g. Parameters ---------- - waveform : ChunkedWaveformArray + waveform : Waveform Acceleration waveforms with shape (n_components, n_stations, nt). - cores : int - Number of CPU cores for parallel processing. Returns ------- - pd.DataFrame - PGA values (g) for standard and rotated components. + xr.Dataset + One data variable per component containing PGA values (g) for + standard and rotated components. """ - if waveform.ndim != 3: - raise TypeError( - f"Waveform must have shape (n_components, n_stations, nt), but {waveform.shape=}" - ) - elif waveform.dtype != np.float64: - raise TypeError(f"Waveform must have dtype float64, but {waveform.dtype=}") - return compute_intensity_measure_rotd(waveform, cores=cores) + return compute_intensity_measure_rotd(waveform, IM.PGA.value) -def peak_ground_velocity( - waveform: ChunkedWaveformArray, dt: float, cores: int -) -> pd.DataFrame: +def peak_ground_velocity(waveform: Waveform, dt: float) -> xr.Dataset: """Compute Peak Ground Velocity (PGV) in cm/s via trapezoidal integration. Parameters ---------- - waveform : ChunkedWaveformArray + waveform : Waveform Acceleration waveforms (g) with shape (n_components, n_stations, nt). dt : float Timestep resolution (s). - cores : int - Number of CPU cores for parallel processing. Returns ------- - pd.DataFrame - PGV values (cm/s) for standard and rotated components. + xr.Dataset + One data variable per component containing PGV values (cm/s) for + standard and rotated components. """ - g = 981 return compute_intensity_measure_rotd( - g * sp.integrate.cumulative_trapezoid(waveform, dx=dt, axis=-1), cores=cores + waveform, IM.PGV.value, transform=functools.partial(_velocity, dt=dt) ) -def peak_ground_displacement( - waveform: ChunkedWaveformArray, dt: float, cores: int -) -> pd.DataFrame: +def peak_ground_displacement(waveform: Waveform, dt: float) -> xr.Dataset: """Compute Peak Ground Displacement (PGD) for waveforms. Parameters ---------- - waveform : ChunkedWaveformArray + waveform : Waveform Acceleration waveforms in g units. dt : float Timestep resolution of the waveform array. - cores : int - Number of CPU cores for parallel processing. Returns ------- - pandas.DataFrame with columns `['000', '090', 'ver', 'geom', 'rotd100', 'rotd50', 'rotd0']` - DataFrame containing PGD values with rotated components. Values are - in cm. + xr.Dataset + One data variable per component containing PGD values (cm) with + rotated components. """ - g = 981 - # Integrate twice to get displacement in cm - velocity = sp.integrate.cumulative_trapezoid(waveform, dx=dt, axis=-1, initial=0) - # In-place multiplication to avoid yet another allocation - np.multiply(g, velocity, out=velocity) - displacement = sp.integrate.cumulative_trapezoid( - velocity, dx=dt, axis=-1, initial=0 + return compute_intensity_measure_rotd( + waveform, IM.PGD.value, transform=functools.partial(_displacement, dt=dt) ) - return compute_intensity_measure_rotd(displacement, cores=cores) + + +def _cav_kernel( + block: np.ndarray, *, dt: float, threshold: float | None +) -> np.ndarray: + """Kernel for `cumulative_absolute_velocity`.""" + components, lead = _components(block) + if threshold: + components = np.where(np.abs(components) < threshold / G, 0.0, components) + comp_0, comp_90, comp_ver = components + cav_0 = _core._cav(comp_0, dt) + cav_90 = _core._cav(comp_90, dt) + cav_ver = _core._cav(comp_ver, dt) + out = np.stack([cav_0, cav_90, cav_ver, np.sqrt(cav_0 * cav_90)], axis=-1) + return out.reshape(lead + (len(GEOM_COMPONENTS),)) def cumulative_absolute_velocity( - waveform: ChunkedWaveformArray, + waveform: Waveform, dt: float, - cores: int, threshold: float | None = None, -) -> pd.DataFrame: +) -> xr.Dataset: """Compute Cumulative Absolute Velocity (CAV) in m/s. Parameters ---------- - waveform : ChunkedWaveformArray + waveform : Waveform Acceleration waveforms (g) with shape (n_components, n_stations, nt). dt : float Timestep resolution (s). - cores : int - Number of CPU cores for parallel processing. threshold : float, optional Acceleration threshold ($cm/s^2$). Values below this are ignored (e.g. 5 for CAV5). Returns ------- - pd.DataFrame - CAV values (m/s) for ['000', '090', 'ver', 'geom']. + xr.Dataset + One data variable per component (`attrs["name"]` is `CAV5` if + `threshold` is set, else `CAV`) containing CAV values (m/s) for + ['000', '090', 'ver', 'geom']. """ + name = IM.CAV5.value if threshold else IM.CAV.value + return _im_dataset( + _cav_kernel, + waveform, + GEOM_COMPONENTS, + name=name, + kwargs={"dt": dt, "threshold": threshold}, + ) - comp_0 = waveform[Component.COMP_0] - comp_90 = waveform[Component.COMP_90] - comp_ver = waveform[Component.COMP_VER] - if threshold: - g = 981 - comp_0 = np.where(np.abs(comp_0) < threshold / g, np.float64(0), comp_0) - comp_90 = np.where(np.abs(comp_90) < threshold / g, np.float64(0), comp_90) - comp_ver = np.where(np.abs(comp_ver) < threshold / g, np.float64(0), comp_ver) - - if cores == 1: - comp_0_cav = _core._cav(comp_0, dt) - comp_90_cav = _core._cav(comp_90, dt) - comp_ver_cav = _core._cav(comp_ver, dt) - else: - with environment(RAYON_NUM_THREADS=str(cores)): - comp_0_cav = _core._parallel_cav(comp_0, dt) - comp_90_cav = _core._parallel_cav(comp_90, dt) - comp_ver_cav = _core._parallel_cav(comp_ver, dt) - - return pd.DataFrame( - { - "000": comp_0_cav, - "090": comp_90_cav, - "ver": comp_ver_cav, - "geom": np.sqrt(comp_0_cav * comp_90_cav), - } - ) +def _arias_kernel(block: np.ndarray, *, dt: float) -> np.ndarray: + """Kernel for `arias_intensity`.""" + components, lead = _components(block) + comp_0, comp_90, comp_ver = components + ai_0 = _core._arias_intensity(comp_0, dt) + ai_90 = _core._arias_intensity(comp_90, dt) + ai_ver = _core._arias_intensity(comp_ver, dt) + out = np.stack([ai_0, ai_90, ai_ver, np.sqrt(ai_0 * ai_90)], axis=-1) + return out.reshape(lead + (len(GEOM_COMPONENTS),)) -def arias_intensity( - waveform: ChunkedWaveformArray, dt: float, cores: int -) -> pd.DataFrame: +def arias_intensity(waveform: Waveform, dt: float) -> xr.Dataset: """Compute Arias Intensity (AI) in m/s. Parameters ---------- - waveform : ChunkedWaveformArray + waveform : Waveform + Acceleration waveforms (g) with shape (n_components, n_stations, nt). + dt : float + Timestep resolution (s). + + Returns + ------- + xr.Dataset + One data variable per component containing AI values (m/s) for + ['000', '090', 'ver', 'geom']. + """ + return _im_dataset( + _arias_kernel, waveform, GEOM_COMPONENTS, name=IM.AI.value, kwargs={"dt": dt} + ) + + +def _duration_kernel( + block: np.ndarray, *, dt: float, quantile_low: float, quantile_high: float +) -> np.ndarray: + """Kernel for `significant_duration`.""" + components, lead = _components(block) + comp_0, comp_90, comp_ver = components + duration_0 = _core._significant_duration(comp_0, dt, quantile_low, quantile_high) + duration_90 = _core._significant_duration(comp_90, dt, quantile_low, quantile_high) + duration_ver = _core._significant_duration( + comp_ver, dt, quantile_low, quantile_high + ) + geom = np.sqrt(duration_0 * duration_90) + out = np.stack([duration_0, duration_90, duration_ver, geom], axis=-1) + return out.reshape(lead + (len(GEOM_COMPONENTS),)) + + +def significant_duration( + waveforms: Waveform, + dt: float, + percent_low: float, + percent_high: float, + name: str = "duration", +) -> xr.Dataset: + """Compute significant duration based on Arias Intensity accumulation. + + Parameters + ---------- + waveforms : Waveform Acceleration waveforms (g) with shape (n_components, n_stations, nt). dt : float Timestep resolution (s). - cores : int - Number of CPU cores for parallel processing. + percent_low : float + Lower bound percentage (e.g., 5.0 for 5%). + percent_high : float + Upper bound percentage (e.g., 95.0 for 95%). + name : str, optional + Name of the resulting dataset. Returns ------- - pd.DataFrame - AI values (m/s) for ['000', '090', 'ver', 'geom']. + xr.Dataset + One data variable per component containing the significant duration + (s) for ['000', '090', 'ver', 'geom']. """ - comp_0 = waveform[Component.COMP_0] - comp_90 = waveform[Component.COMP_90] - comp_ver = waveform[Component.COMP_VER] - - if cores == 1: - comp_0_ai = _core._arias_intensity(comp_0, dt) - comp_90_ai = _core._arias_intensity(comp_90, dt) - comp_ver_ai = _core._arias_intensity(comp_ver, dt) - else: - with environment(RAYON_NUM_THREADS=str(cores)): - comp_0_ai = _core._parallel_arias_intensity(comp_0, dt) - comp_90_ai = _core._parallel_arias_intensity(comp_90, dt) - comp_ver_ai = _core._parallel_arias_intensity(comp_ver, dt) - - return pd.DataFrame( - { - "000": comp_0_ai, - "090": comp_90_ai, - "ver": comp_ver_ai, - "geom": np.sqrt(comp_0_ai * comp_90_ai), - } + return _im_dataset( + _duration_kernel, + waveforms, + GEOM_COMPONENTS, + name=name, + kwargs={ + "dt": dt, + "quantile_low": percent_low / 100, + "quantile_high": percent_high / 100, + }, ) -def ds575(waveform: ChunkedWaveformArray, dt: float, cores: int) -> pd.DataFrame: +def ds575(waveform: Waveform, dt: float) -> xr.Dataset: """Compute 5-75% Significant Duration (DS575) in seconds. Parameters ---------- - waveform : ChunkedWaveformArray + waveform : Waveform Acceleration waveforms (g) with shape (n_components, n_stations, nt). dt : float Timestep resolution (s). - cores : int - Number of CPU cores for parallel processing. Returns ------- - pd.DataFrame - Duration values (s) for ['000', '090', 'ver', 'geom']. + xr.Dataset + One data variable per component containing duration values (s) for + ['000', '090', 'ver', 'geom']. """ - return significant_duration(waveform, dt, 5, 75, cores) + return significant_duration(waveform, dt, 5, 75, IM.Ds575.value) -def ds595(waveform: ChunkedWaveformArray, dt: float, cores: int) -> pd.DataFrame: +def ds595(waveform: Waveform, dt: float) -> xr.Dataset: """Compute 5-95% Significant Duration (DS595) in seconds. Parameters ---------- - waveform : ChunkedWaveformArray + waveform : Waveform Acceleration waveforms (g) with shape (n_components, n_stations, nt). dt : float Timestep resolution (s). - cores : int - Number of CPU cores for parallel processing. Returns ------- - pd.DataFrame - Duration values (s) for ['000', '090', 'ver', 'geom']. + xr.Dataset + One data variable per component containing duration values (s) for + ['000', '090', 'ver', 'geom']. + """ + return significant_duration(waveform, dt, 5, 95, IM.Ds595.value) + + +N_ROTD180_ANGLES = 180 +ROTD180_ANGLES = np.arange(N_ROTD180_ANGLES) + + +def _psa_kernel( + block: np.ndarray, + *, + periods: npt.NDArray[np.float64], + dt: float, + full_rotd180: bool, +) -> np.ndarray | tuple[np.ndarray, np.ndarray]: + """Kernel for `pseudo_spectral_acceleration`. + + Loops over periods internally (rather than treating `period` as a + broadcast input) so every IM kernel shares one contract: `(*lead, + n_components, nt) -> (*lead, *extra, k)`. Station-chunk parallelism is + already ample, so nothing is lost by not also parallelising over period. + + When `full_rotd180` is set, the full 180-angle RotD curve computed for + the summary statistics is also returned rather than discarded, so the + Newmark-beta solve never runs twice for the same (period, station chunk). + """ + components, lead = _components(block) + comp_0, comp_90, comp_ver = components + rows = comp_0.shape[0] + out = np.empty((rows, len(periods), len(ROTD_COMPONENTS)), dtype=np.float64) + rotd180 = ( + np.empty((rows, len(periods), N_ROTD180_ANGLES), dtype=np.float64) + if full_rotd180 + else None + ) + for index, period in enumerate(periods): + w = 2 * np.pi / period + # (rows, 182): 180 rotated peaks, then the exact 000 and 090 peaks. + psa = _core._psa_rotd180(comp_0, comp_90, dt, w, DAMPING) + # Reduced in rust, by the same code the peak ground motion RotD uses, + # so the statistics and their orientations are defined in one place. + stats = _core._rotd180_stats(psa[:, :N_ROTD180_ANGLES]) + peak_0, peak_90 = psa[:, 180], psa[:, 181] + peak_ver = _core._psa_peak(comp_ver, dt, w, DAMPING) + peaks = np.stack( + [peak_0, peak_90, peak_ver, np.sqrt(peak_0 * peak_90)], axis=-1 + ) + out[:, index] = np.concatenate([peaks, stats], axis=-1) + if rotd180 is not None: + rotd180[:, index] = psa[:, :N_ROTD180_ANGLES] + + out = out.reshape(lead + (len(periods), len(ROTD_COMPONENTS))) + if rotd180 is None: + return out + return out, rotd180.reshape(lead + (len(periods), N_ROTD180_ANGLES)) + + +def pseudo_spectral_acceleration( + waveforms: Waveform, + periods: npt.ArrayLike, + dt: float, + full_rotd180: bool = False, +) -> xr.Dataset: + """Compute pseudo-spectral acceleration (PSA) statistics. + + Calculates PSA for single-degree-of-freedom oscillators across various + periods using the Newmark-beta method and computes rotated (RotD) statistics. + + Parameters + ---------- + waveforms : Waveform + Acceleration waveforms (g) with shape (n_components, n_stations, nt). + periods : array_like + Natural periods of the oscillators (s). + dt : float + Timestep resolution of the waveforms (s). + full_rotd180 : bool, optional + If set, also include a `rotd180` data variable with an extra `angle` + dimension (0..179 degrees), holding pSA (g) at every rotation angle. + This reuses the same Newmark-beta solve already run for the summary + statistics, rather than repeating it. + + Returns + ------- + xr.Dataset + One data variable per component, each with a `period` dimension: + PSA for + ['000', '090', 'ver', 'geom', 'rotd0', 'rotd50', 'rotd100'], then + `rotd0_orientation`, `rotd50_orientation` and `rotd100_orientation` + holding the angle (degrees) at which each RotD statistic occurs. If + `full_rotd180` is set, also a `rotd180` variable with dims + (..., period, angle). """ - return significant_duration(waveform, dt, 5, 95, cores) + periods = np.asarray(periods, dtype=np.float64) + waveform = _as_waveform(waveforms) + kernel = functools.partial( + _psa_kernel, periods=periods, dt=dt, full_rotd180=full_rotd180 + ) + + output_core_dims = [["period", "im_component"]] + output_sizes = {"period": len(periods), "im_component": len(ROTD_COMPONENTS)} + if full_rotd180: + output_core_dims.append(["period", "angle"]) + output_sizes["angle"] = N_ROTD180_ANGLES + + outputs = xr.apply_ufunc( + kernel, + waveform, + input_core_dims=[["component", "time"]], + output_core_dims=output_core_dims, + keep_attrs=False, + dask="parallelized", + output_dtypes=[np.float64] * len(output_core_dims), + dask_gufunc_kwargs={"output_sizes": output_sizes}, + ) + summary, rotd180 = outputs if full_rotd180 else (outputs, None) + + summary = summary.assign_coords(im_component=list(ROTD_COMPONENTS), period=periods) + dataset = summary.to_dataset("im_component") + dataset.attrs = {"name": IM.pSA.value} + if rotd180 is not None: + dataset["rotd180"] = rotd180.assign_coords( + period=periods, angle=ROTD180_ANGLES + ) + return dataset + + +def _konno_smooth(spectrum_data: np.ndarray, konno: np.ndarray) -> np.ndarray: + """Multiply a spectrum by a Konno-Ohmachi matrix, a block of columns at a time. + + `konno` is a float32 memmap that can reach tens of gigabytes. Writing + `spectrum_data @ konno` directly makes numpy promote the *entire* matrix + to float64 before the product. Taking a block of output columns at a + time bounds the promoted array to `KONNO_BLOCK_BYTES` while leaving each + output element a single full-length float64 accumulation, so the result + is the same product, not an approximation of it -- only the contraction + axis (the matrix's rows) must never be split, and it isn't here. + + Parameters + ---------- + spectrum_data : ndarray + Spectrum values, shape `(..., n_fa)`. + konno : ndarray + Konno-Ohmachi smoothing matrix, shape `(n_fa, n_fa)`. + + Returns + ------- + ndarray + Smoothed spectrum, shape `(..., n_fa)`. + """ + n_output = konno.shape[1] + columns = max(1, KONNO_BLOCK_BYTES // (konno.shape[0] * np.float64().itemsize)) + smoothed = np.empty(spectrum_data.shape[:-1] + (n_output,), dtype=np.float64) + for start in range(0, n_output, columns): + block = slice(start, start + columns) + smoothed[..., block] = spectrum_data @ np.asarray(konno[:, block], dtype=np.float64) + return smoothed + + +def smooth_and_interpolate( + spectrum_data: np.ndarray, + konno: np.ndarray, + freqs: npt.NDArray[np.float64], + fa_frequencies: np.ndarray, +) -> np.ndarray: + """ + Smooths and interpolates a spectrum. + + Parameters + ---------- + spectrum_data : ndarray + The spectrum data to be smoothed and interpolated. + konno : ndarray + The Konno-Ohmachi smoothing matrix to apply to the spectrum data. + freqs : ndarray of float64 + The frequencies at which to interpolate the smoothed spectrum data. + fa_frequencies : ndarray + The original frequencies corresponding to the spectrum data before smoothing. + + Returns + ------- + ndarray + The smoothed and interpolated spectrum data at the specified frequencies. + """ + smoothed = _konno_smooth(spectrum_data, konno) + interpolator = sp.interpolate.make_interp_spline( + fa_frequencies, smoothed, axis=-1, k=1 + ) + return interpolator(freqs) + + +def _fas_kernel( + block: np.ndarray, + *, + dt: float, + n_fft: int, + freqs: npt.NDArray[np.float64], + fa_frequencies: npt.NDArray[np.float64], + ko_directory: Path, +) -> np.ndarray: + """Kernel for `fourier_amplitude_spectra`.""" + components, lead = _components(block) + n_components, rows, _ = components.shape + n_fa = len(fa_frequencies) + + spectra = np.empty((n_components, rows, n_fa), dtype=np.float64) + for index in range(n_components): + spectra[index] = np.abs(fft.rfft(components[index], n=n_fft, axis=-1) * dt) + + # EAS is computed from the *unsmoothed* spectrum to avoid distortion of + # inter-frequency correlations, then smoothed alongside 000/090/ver in a + # single pass over the (potentially huge) Konno matrix. + eas_unsmoothed = np.sqrt( + 0.5 + * ( + np.square(spectra[Component.COMP_0]) + + np.square(spectra[Component.COMP_90]) + ) + ) + spectra_and_eas = np.concatenate([spectra, eas_unsmoothed[np.newaxis]], axis=0) + + konno = ko_matrices.get_konno_matrix(n_fa, ko_directory) + smoothed = smooth_and_interpolate(spectra_and_eas, konno, freqs, fa_frequencies) + + geom = np.sqrt(smoothed[Component.COMP_0] * smoothed[Component.COMP_90]) + out = np.stack( + [smoothed[0], smoothed[1], smoothed[2], geom, smoothed[3]], axis=-1 + ) + return out.reshape(lead + (len(freqs), len(FAS_COMPONENTS))) + + +def fourier_amplitude_spectra( + waveforms: Waveform, + dt: float, + freqs: npt.NDArray[np.float64], + ko_directory: Path, +) -> xr.Dataset: + """Compute Fourier Amplitude Spectrum (FAS) of seismic waveforms. + + The FAS is computed using FFT and then smoothed using the Konno-Ohmachi + smoothing algorithm. + + Parameters + ---------- + waveforms : Waveform + Waveform array (g) with shape `(n_components, n_stations, n_timesteps)`. + dt : float + Timestep resolution of the waveforms (s). + freqs : ndarray of float64 + Frequencies at which to compute FAS (Hz). + ko_directory : Path + Directory containing precomputed Konno-Ohmachi matrices. + + Returns + ------- + xr.Dataset + One data variable per component, each with a `frequency` dimension, + containing FAS values for ['000', '090', 'ver', 'geom', 'eas']. + """ + waveform = _as_waveform(waveforms) + + nyquist_frequency = 1 / (2 * dt) + max_frequency = freqs.max() + if max_frequency > nyquist_frequency: + warnings.warn( + RuntimeWarning( + f"Attempting to compute FAS for frequencies above Nyquist frequency {nyquist_frequency:.2e} Hz. Results only include frequencies at or below Nyquist frequency {nyquist_frequency:.2e} Hz." + ), + ) + freqs = freqs[freqs <= nyquist_frequency] + + n_fft = 2 ** int(np.ceil(np.log2(waveform.sizes["time"]))) + fa_frequencies = np.fft.rfftfreq(n_fft, dt) + + return _im_dataset( + _fas_kernel, + waveform, + FAS_COMPONENTS, + name=IM.FAS.value, + extra_dims={"frequency": freqs}, + kwargs={ + "dt": dt, + "n_fft": n_fft, + "freqs": freqs, + "fa_frequencies": fa_frequencies, + "ko_directory": ko_directory, + }, + ) diff --git a/IM/ko_matrices.py b/IM/ko_matrices.py index 372a24f8..4d61452f 100644 --- a/IM/ko_matrices.py +++ b/IM/ko_matrices.py @@ -1,14 +1,19 @@ """KO matrix generation module""" +import functools from pathlib import Path import numpy as np +@functools.cache def get_konno_matrix(size: int, directory: Path) -> np.memmap: """ Retrieves the precomputed Konno matrix from a file. + Cached per `(size, directory)`: the memmap is opened once per process + and reused, since it may be called once per dask task/chunk. + Parameters ---------- size : int diff --git a/IM/scripts/calculate_ims.py b/IM/scripts/calculate_ims.py index a24ab58a..8eb47813 100644 --- a/IM/scripts/calculate_ims.py +++ b/IM/scripts/calculate_ims.py @@ -1,6 +1,5 @@ """ASCII IM Calculation script - entrypoint""" -import multiprocessing from pathlib import Path from typing import Annotated @@ -43,10 +42,6 @@ def calculate_ims_ascii( ], periods: Annotated[list[float] | None, typer.Option()] = None, frequencies: Annotated[list[float] | None, typer.Option()] = None, - cores: Annotated[ - int, - typer.Option(), - ] = multiprocessing.cpu_count(), ko_directory: Annotated[Path | None, typer.Option()] = None, ) -> None: """ @@ -68,8 +63,6 @@ def calculate_ims_ascii( List of periods required for calculating the pseudo-spectral acceleration (pSA). frequencies : list of float, optional List of frequencies required for calculating the Fourier amplitude spectrum (FAS). - cores : int, optional - Number of cores to use for parallel processing in pSA and FAS calculations. ko_directory : Path, optional Path to the directory containing the Konno-Ohmachi matrices. Only required if FAS is in the list of IMs. @@ -89,7 +82,6 @@ def calculate_ims_ascii( ims_list, np.array(periods), np.array(frequencies), - cores, ko_directory, ) diff --git a/IM/snr_calculation.py b/IM/snr_calculation.py index d4edd172..deb7f739 100644 --- a/IM/snr_calculation.py +++ b/IM/snr_calculation.py @@ -1,12 +1,12 @@ """Waveform SNR calculation""" -import multiprocessing from pathlib import Path from typing import NamedTuple import numpy as np import pandas as pd import scipy as sp +import xarray as xr from IM import im_calculation, ims @@ -47,7 +47,6 @@ def calculate_snr( tp: int, ko_directory: Path, frequencies: np.ndarray = im_calculation.DEFAULT_FREQUENCIES, - cores: int = multiprocessing.cpu_count(), ) -> SNRResult: """ Calculates the SNR of a waveform given a tp and common frequency vector @@ -65,8 +64,6 @@ def calculate_snr( frequencies : np.ndarray, optional The frequency vector to use for the SNR calculation, by default takes the frequencies from FAS - cores : int, optional - Number of cores to use for parallel processing in FAS calculations. Returns ------- @@ -112,37 +109,29 @@ def calculate_snr( # Generate FFT for the signal and noise fas_signal = ims.fourier_amplitude_spectra( - taper_signal_acc, dt, frequencies, ko_directory, cores + taper_signal_acc, dt, frequencies, ko_directory ) fas_noise = ims.fourier_amplitude_spectra( - taper_noise_acc, dt, frequencies, ko_directory, cores + taper_noise_acc, dt, frequencies, ko_directory ) - # Calculate the SNR + # Calculate the SNR. Dataset arithmetic aligns on variable name, so this + # produces a 5-variable (000/090/ver/geom/eas) dataset just like fas_signal + # and fas_noise. with np.errstate(divide="ignore", invalid="ignore"): snr = (fas_signal * noise_duration) / (fas_noise * signal_duration) - # Create SNR DataFrame with 000, 090 and ver - snr_df = snr.to_dataframe().unstack(level="component") - snr_df.index = snr.coords["frequency"].values - snr_df.columns = snr_df.columns.droplevel(0) - snr_df = snr_df[["000", "090", "ver"]] - - # Create FAS noise and signal DataFrames with 000, 090 and ver - fas_signal_df = fas_signal.to_dataframe().unstack(level="component") - fas_signal_df.index = fas_signal.coords["frequency"].values - fas_signal_df.columns = fas_signal_df.columns.droplevel(0) # ty: ignore[invalid-assignment, invalid-argument-type] - fas_signal_df = fas_signal_df[["000", "090", "ver"]] - - fas_noise_df = fas_noise.to_dataframe().unstack(level="component") - fas_noise_df.index = fas_noise.coords["frequency"].values - fas_noise_df.columns = fas_noise_df.columns.droplevel(0) # ty: ignore[invalid-assignment, invalid-argument-type] - fas_noise_df = fas_noise_df[["000", "090", "ver"]] - - assert isinstance(snr_df, pd.DataFrame) - assert isinstance(fas_signal_df, pd.DataFrame) - assert isinstance(fas_noise_df, pd.DataFrame) + snr_df = _component_frame(snr) + fas_signal_df = _component_frame(fas_signal) + fas_noise_df = _component_frame(fas_noise) return SNRResult( snr_df, fas_signal_df, fas_noise_df, signal_duration, noise_duration ) + + +def _component_frame(dataset: xr.Dataset) -> pd.DataFrame: + """Take the 000/090/ver components of a single-station FAS dataset as a + frequency-indexed DataFrame. + """ + return dataset[["000", "090", "ver"]].isel(station=0, drop=True).to_dataframe() diff --git a/benches/intensity_benchmarks.rs b/benches/intensity_benchmarks.rs index 9de17aad..a09317e1 100644 --- a/benches/intensity_benchmarks.rs +++ b/benches/intensity_benchmarks.rs @@ -39,13 +39,6 @@ fn bench_cav(c: &mut Criterion) { group.bench_with_input(BenchmarkId::new("Sequential", ¶m), &view, |b, &v| { b.iter(|| cav::cav(black_box(v), black_box(SAMPLING_RATE))) }); - - // Only benchmark parallel for multiple stations - if stations > 1 { - group.bench_with_input(BenchmarkId::new("Parallel", ¶m), &view, |b, &v| { - b.iter(|| cav::parallel_cav(black_box(v), black_box(SAMPLING_RATE))) - }); - } } } @@ -67,17 +60,6 @@ fn bench_arias_intensity(c: &mut Criterion) { group.bench_with_input(BenchmarkId::new("Sequential", ¶m), &view, |b, &v| { b.iter(|| arias_intensity::arias_intensity(black_box(v), black_box(SAMPLING_RATE))) }); - - if stations > 1 { - group.bench_with_input(BenchmarkId::new("Parallel", ¶m), &view, |b, &v| { - b.iter(|| { - arias_intensity::parallel_arias_intensity( - black_box(v), - black_box(SAMPLING_RATE), - ) - }) - }); - } } } @@ -105,17 +87,6 @@ fn bench_cumulative_arias(c: &mut Criterion) { ) }) }); - - if stations > 1 { - group.bench_with_input(BenchmarkId::new("Parallel", ¶m), &view, |b, &v| { - b.iter(|| { - arias_intensity::parallel_cumulative_arias_intensity( - black_box(v), - black_box(SAMPLING_RATE), - ) - }) - }); - } } } @@ -144,19 +115,6 @@ fn bench_significant_duration(c: &mut Criterion) { ) }) }); - - if stations > 1 { - group.bench_with_input(BenchmarkId::new("Parallel", ¶m), &view, |b, &v| { - b.iter(|| { - significant_duration::parallel_significant_duration( - black_box(v), - black_box(SAMPLING_RATE), - 0.05, - 0.95, - ) - }) - }); - } } } @@ -180,9 +138,9 @@ fn bench_psa(c: &mut Criterion) { group.throughput(Throughput::Bytes((stations * samples * 8) as u64)); - group.bench_with_input(BenchmarkId::new("Parallel", ¶m), &view, |b, &v| { + group.bench_with_input(BenchmarkId::new("Sequential", ¶m), &view, |b, &v| { b.iter(|| { - psa::newmark_beta_method_parallel( + psa::newmark_beta_method_batch( black_box(&v), black_box(SAMPLING_RATE), black_box(period), diff --git a/pyproject.toml b/pyproject.toml index f101bd1b..01e99cdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,14 +10,13 @@ readme = "README.md" requires-python = ">=3.12" dynamic = ["version"] dependencies = [ - "numpy", + "numpy>=2", "pandas[hdf5]", "pyfftw", "scipy", "pint-xarray", - "xarray[io]", + "xarray[io]>=2025.1", "typer>0.12.3", - "tqdm>=4.67.1", "qcore-utils>=2025.12.1", ] @@ -25,7 +24,11 @@ dependencies = [ [project.optional-dependencies] ko_matrix = ["obspy"] -test = ["pytest", "hypothesis[numpy]", "pytest-cov", "rich>=14.2.0"] +# Only needed by callers that want to pass dask-backed (lazy) waveforms in +# and get lazy datasets out; xr.apply_ufunc(..., dask="parallelized") never +# imports dask for plain numpy/eager xarray input. +lazy = ["dask[array]"] +test = ["pytest", "hypothesis[numpy]", "pytest-cov", "rich>=14.2.0", "dask[array]"] types = ["pandas-stubs", "scipy-stubs"] dev = ["deptry", "ruff", "ty", "numpydoc"] @@ -100,6 +103,9 @@ known-first-party = [ [[tool.setuptools-rust.ext-modules]] target = "IM._core" binding = "PyO3" +# Force an optimised build even for editable/develop installs, which +# setuptools-rust would otherwise compile in (much slower) debug mode. +debug = false [tool.numpydoc_validation] diff --git a/src-rust/arias_intensity.rs b/src-rust/arias_intensity.rs index 49079db3..87805fe5 100644 --- a/src-rust/arias_intensity.rs +++ b/src-rust/arias_intensity.rs @@ -8,10 +8,7 @@ //! //! Where $g$ is the acceleration due to gravity (set here to $9.81 \text{ m/s}^2$). -use crate::trapz::{ - cumulative_trapz_with_fun, parallel_cumulative_trapz_with_fun, parallel_trapz_with_fun, - trapz_with_fun, -}; +use crate::trapz::{cumulative_trapz_with_fun, trapz_with_fun}; use crate::constants::G; use ndarray::prelude::*; @@ -20,19 +17,7 @@ use std::f64::consts::PI; /// Precomputed scaling factor: $\frac{\pi}{2g}$ const ARIAS_CONSTANT: f64 = G * PI / 2.0; -/// Computes the total Arias Intensity ($I_A$) for each row in parallel. -/// -/// # Arguments -/// * `waveforms` - A 2D array view where each row is an acceleration time-series. -/// * `dt` - The time step (sampling interval) of the waveforms. -/// -/// # Returns -/// An `Array1` containing the final $I_A$ value for each station. -pub fn parallel_arias_intensity(waveforms: ArrayView2, dt: f64) -> Array1 { - ARIAS_CONSTANT * parallel_trapz_with_fun(waveforms, dt, |x| x * x) -} - -/// Computes the total Arias Intensity ($I_A$) for each row using a single thread. +/// Computes the total Arias Intensity ($I_A$) for each row. /// /// # Arguments /// * `waveforms` - A 2D array view where each row is an acceleration time-series. @@ -41,25 +26,13 @@ pub fn arias_intensity(waveforms: ArrayView2, dt: f64) -> Array1 { ARIAS_CONSTANT * trapz_with_fun(waveforms, dt, |x| x * x) } -/// Computes the cumulative Arias Intensity time-history for each row in parallel. +/// Computes the cumulative Arias Intensity time-history for each row. /// /// This returns the "Husid plot" data, showing how the intensity builds over time. /// /// # Arguments /// * `waveforms` - A 2D array view where each row is an acceleration time-series. /// * `dt` - The time step (sampling interval) of the waveforms. -/// -/// # Returns -/// An `Array2` of the same shape as `waveforms`, representing the intensity accumulated at each timestep. -pub fn parallel_cumulative_arias_intensity(waveforms: ArrayView2, dt: f64) -> Array2 { - ARIAS_CONSTANT * parallel_cumulative_trapz_with_fun(waveforms, dt, |x| x * x) -} - -/// Computes the cumulative Arias Intensity time-history for each row using a single thread. -/// -/// # Arguments -/// * `waveforms` - A 2D array view where each row is an acceleration time-series. -/// * `dt` - The time step (sampling interval) of the waveforms. pub fn cumulative_arias_intensity(waveforms: ArrayView2, dt: f64) -> Array2 { ARIAS_CONSTANT * cumulative_trapz_with_fun(waveforms, dt, |x| x * x) } @@ -111,25 +84,4 @@ mod tests { assert_eq!(cumulative.shape(), &[3, 100]); } - #[test] - fn test_parallel_equals_sequential_total_intensity() { - let waveforms = array![[0.0, 1.0, 2.0], [2.0, 1.0, 0.0]]; - let dt = 0.02; - - let seq_res = arias_intensity(waveforms.view(), dt); - let par_res = parallel_arias_intensity(waveforms.view(), dt); - - assert_abs_diff_eq!(seq_res, par_res, epsilon = 1e-10); - } - - #[test] - fn test_parallel_equals_sequential_cumulative_intensity() { - let waveforms = array![[0.0, 1.0, 2.0], [2.0, 1.0, 0.0]]; - let dt = 0.02; - - let seq_res = cumulative_arias_intensity(waveforms.view(), dt); - let par_res = parallel_cumulative_arias_intensity(waveforms.view(), dt); - - assert_abs_diff_eq!(seq_res, par_res, epsilon = 1e-10); - } } diff --git a/src-rust/cav.rs b/src-rust/cav.rs index cb6973c7..122c0f02 100644 --- a/src-rust/cav.rs +++ b/src-rust/cav.rs @@ -8,23 +8,11 @@ //! use crate::constants::G; -use crate::trapz::{parallel_trapz_with_fun, trapz_with_fun}; +use crate::trapz::trapz_with_fun; use ndarray::prelude::*; -/// Computes the total Cumulative Absolute Velocity ($CAV$) for each row in parallel. -/// -/// # Arguments -/// * `waveforms` - A 2D array view where each row is an acceleration time-series. -/// * `dt` - The time step (sampling interval) of the waveforms. -/// -/// # Returns -/// An `Array1` containing the final CAV value for each station. -pub fn parallel_cav(waveforms: ArrayView2, dt: f64) -> Array1 { - G * parallel_trapz_with_fun(waveforms, dt, |x| x.abs()) -} - -/// Computes the total Cumulative Absolute Velocity ($CAV$) for each row using a single thread. +/// Computes the total Cumulative Absolute Velocity ($CAV$) for each row. /// /// # Arguments /// * `waveforms` - A 2D array view where each row is an acceleration time-series. @@ -88,27 +76,4 @@ mod tests { let expected = 9.81 * (2.0 / 3.0) * (6.0 * 3.0f64.sqrt() + PI); assert_abs_diff_eq!(result[0], expected, epsilon = 0.1); } - - #[test] - fn test_parallel_equals_sequential() { - // This test prevents "implementation drift" where the parallel version - // gets updated but the sequential one is forgotten. - - // Shape (2 rows, 3 cols) - let waveforms = array![[0.0, 1.0, 2.0], [2.0, 1.0, 0.0]]; - let dt = 1.0; - - let seq_res = cav(waveforms.view(), dt); - let par_res = parallel_cav(waveforms.view(), dt); - - // Check if shapes match - assert_eq!( - seq_res.dim(), - par_res.dim(), - "Sequential and Parallel output shapes mismatch" - ); - - // Check if values match - assert_abs_diff_eq!(seq_res, par_res, epsilon = 1e-10); - } } diff --git a/src-rust/lib.rs b/src-rust/lib.rs index c9afb036..14ff28d6 100644 --- a/src-rust/lib.rs +++ b/src-rust/lib.rs @@ -5,7 +5,6 @@ pub mod psa; pub mod rotd; pub mod significant_duration; mod trapz; -mod utils; use pyo3::prelude::*; /// A Python module implemented in Rust. The name of this function must match @@ -32,7 +31,10 @@ mod _core { xi: f64, ) -> Bound<'py, PyArray2> { let waveforms = waveforms_py.as_array(); - let waveform_psa = psa::newmark_beta_method_parallel(&waveforms, dt, w, xi); + // Touches no Python objects, so drop the GIL for the whole solve: a + // threaded caller (e.g. Dask's threaded scheduler) can then run one + // of these per core in parallel within a single process. + let waveform_psa = py.detach(|| psa::newmark_beta_method_batch(&waveforms, dt, w, xi)); waveform_psa.into_pyarray(py) } @@ -43,86 +45,92 @@ mod _core { dt: f64, ) -> Bound<'py, PyArray1> { let waveforms = waveforms_py.as_array(); - let waveform_ai = arias_intensity::arias_intensity(waveforms, dt); + let waveform_ai = py.detach(|| arias_intensity::arias_intensity(waveforms, dt)); waveform_ai.into_pyarray(py) } #[pyfunction] - fn _parallel_arias_intensity<'py>( + fn _cav<'py>( py: Python<'py>, waveforms_py: PyReadonlyArray2, dt: f64, ) -> Bound<'py, PyArray1> { let waveforms = waveforms_py.as_array(); - let waveform_ai = arias_intensity::parallel_arias_intensity(waveforms, dt); - waveform_ai.into_pyarray(py) - } - - #[pyfunction] - fn _cumulative_arias_intensity<'py>( - py: Python<'py>, - waveforms_py: PyReadonlyArray2, - dt: f64, - ) -> Bound<'py, PyArray2> { - let waveforms = waveforms_py.as_array(); - let waveform_ai = arias_intensity::cumulative_arias_intensity(waveforms, dt); - waveform_ai.into_pyarray(py) + let waveform_cav = py.detach(|| cav::cav(waveforms, dt)); + waveform_cav.into_pyarray(py) } + /// Serial pSA at all 180 rotation angles for one period. + /// + /// Runs the Newmark-beta solver (f64) and the RotD reduction entirely in + /// Rust, one station after another, so a Dask worker holding a single core + /// gets no competing Rayon threads. Returns an `(ns, 182)` array: columns + /// 0..=179 are the rotated peaks, and columns 180/181 are the exact 000 + /// and 090 peaks (see [`psa::psa_rotd180`]). #[pyfunction] - fn _parallel_cumulative_arias_intensity<'py>( + fn _psa_rotd180<'py>( py: Python<'py>, - waveforms_py: PyReadonlyArray2, + comp_0_py: PyReadonlyArray2, + comp_90_py: PyReadonlyArray2, dt: f64, + w: f64, + xi: f64, ) -> Bound<'py, PyArray2> { - let waveforms = waveforms_py.as_array(); - let waveform_ai = arias_intensity::parallel_cumulative_arias_intensity(waveforms, dt); - waveform_ai.into_pyarray(py) - } - - #[pyfunction] - fn _cav<'py>( - py: Python<'py>, - waveforms_py: PyReadonlyArray2, - dt: f64, - ) -> Bound<'py, PyArray1> { - let waveforms = waveforms_py.as_array(); - let waveform_cav = cav::cav(waveforms, dt); - waveform_cav.into_pyarray(py) + let comp_0 = comp_0_py.as_array(); + let comp_90 = comp_90_py.as_array(); + // The solve touches no Python objects, so drop the GIL for its whole + // duration: a threaded caller (e.g. Dask's threaded scheduler) can then + // run one of these per core in parallel within a single process. + let psa_rotd = py.detach(|| psa::psa_rotd180(&comp_0, &comp_90, dt, w, xi)); + psa_rotd.into_pyarray(py) } + /// Pseudo-spectral acceleration peak for a single component, one period. + /// + /// Used for the vertical component, which never participates in RotD, so + /// only its peak response (shape `(ns,)`) is needed. #[pyfunction] - fn _parallel_cav<'py>( + fn _psa_peak<'py>( py: Python<'py>, waveforms_py: PyReadonlyArray2, dt: f64, + w: f64, + xi: f64, ) -> Bound<'py, PyArray1> { let waveforms = waveforms_py.as_array(); - let waveform_cav = cav::parallel_cav(waveforms, dt); - waveform_cav.into_pyarray(py) + let peak = py.detach(|| psa::psa_peak(&waveforms, dt, w, xi)); + peak.into_pyarray(py) } + /// RotD statistics of every `(comp_0, comp_90)` pair. + /// + /// Returns an `(ns, 6)` array: RotD00, RotD50 and RotD100, then the + /// orientation in degrees at which each of the three occurs. #[pyfunction] - fn _rotd_parallel<'py>( + fn _rotd<'py>( py: Python<'py>, comp_0_py: PyReadonlyArray2, comp_90_py: PyReadonlyArray2, ) -> Bound<'py, PyArray2> { let comp_0 = comp_0_py.as_array(); let comp_90 = comp_90_py.as_array(); - let rotd_stats = rotd::rotd_parallel(comp_0, comp_90); + let rotd_stats = py.detach(|| rotd::rotd(comp_0, comp_90)); rotd_stats.into_pyarray(py) } + /// The same RotD statistics, reduced from an already computed angle sweep. + /// + /// `curve_py` is an `(ns, 180)` array of peaks at every integer angle, as + /// the first 180 columns of [`_psa_rotd180`]. Returns the `(ns, 6)` array + /// [`_rotd`] returns, so the pSA path shares one reduction with the peak + /// ground motion path instead of repeating it in numpy. #[pyfunction] - fn _rotd<'py>( + fn _rotd180_stats<'py>( py: Python<'py>, - comp_0_py: PyReadonlyArray2, - comp_90_py: PyReadonlyArray2, + curve_py: PyReadonlyArray2, ) -> Bound<'py, PyArray2> { - let comp_0 = comp_0_py.as_array(); - let comp_90 = comp_90_py.as_array(); - let rotd_stats = rotd::rotd(comp_0, comp_90); + let curve = curve_py.as_array(); + let rotd_stats = py.detach(|| rotd::rotd180_stats(curve)); rotd_stats.into_pyarray(py) } @@ -135,27 +143,10 @@ mod _core { high: f64, ) -> Bound<'py, PyArray1> { let waveforms = waveforms_py.as_array(); - let arias_intensity = arias_intensity::cumulative_arias_intensity(waveforms, dt); - let ds = significant_duration::significant_duration(arias_intensity.view(), dt, low, high); - ds.into_pyarray(py) - } - - #[pyfunction] - fn _parallel_significant_duration<'py>( - py: Python<'py>, - waveforms_py: PyReadonlyArray2, - dt: f64, - low: f64, - high: f64, - ) -> Bound<'py, PyArray1> { - let waveforms = waveforms_py.as_array(); - let arias_intensity = arias_intensity::parallel_cumulative_arias_intensity(waveforms, dt); - let ds = significant_duration::parallel_significant_duration( - arias_intensity.view(), - dt, - low, - high, - ); + let ds = py.detach(|| { + let arias_intensity = arias_intensity::cumulative_arias_intensity(waveforms, dt); + significant_duration::significant_duration(arias_intensity.view(), dt, low, high) + }); ds.into_pyarray(py) } } diff --git a/src-rust/psa.rs b/src-rust/psa.rs index 6af08fb7..bc4d8408 100644 --- a/src-rust/psa.rs +++ b/src-rust/psa.rs @@ -1,4 +1,3 @@ -use ndarray::parallel::prelude::*; use ndarray::prelude::*; use ndarray::{Array1, Ix1, Ix2}; @@ -95,16 +94,14 @@ pub fn newmark_beta_method( newmark_beta_solver(waveform, dt, w, xi, gamma, beta, u0, dudt0) } -/// Solve the SDOF oscillator equation for an array of observations, in parallel, *in-place*. +/// Solve the SDOF oscillator equation for every row of `waveforms`, serially. /// -/// The `waveforms` array must have shape `(ns, nt)`, where `ns` is the number of stations and `nt` is the number of timesteps. -/// The solver uses the Newmark-Beta method to solve the SDOF oscillator equation for an oscillator -/// with an *angular frequency* of `w` Hz, damping coefficient of `xi`, and mass parameter `m`. -/// The `gamma` and `beta` parameters determine if the constant or linear acceleration method is implemented. -/// -/// -/// Solving is done in parallel for all `ns` stations, and in-place on the waveforms array. -pub fn newmark_beta_method_parallel( +/// The `waveforms` array has shape `(ns, nt)`, where `ns` is the number of +/// stations and `nt` the number of timesteps. Each row is solved with the +/// Newmark-Beta method for an oscillator of angular frequency `w` and damping +/// coefficient `xi`, and the resulting displacement response is written to the +/// matching row of the `(ns, nt)` output. +pub fn newmark_beta_method_batch( waveforms: &ArrayView2, dt: f64, w: f64, @@ -112,15 +109,80 @@ pub fn newmark_beta_method_parallel( ) -> Array { let mut out = Array::::zeros(waveforms.dim()); out.axis_iter_mut(Axis(0)) - .into_par_iter() - .zip(waveforms.axis_iter(Axis(0)).into_par_iter()) + .zip(waveforms.axis_iter(Axis(0))) .for_each(|(mut out_row, in_row)| { - let r = newmark_beta_method(in_row, dt, w, xi, 0.0, 0.0); - out_row.assign(&r); + out_row.assign(&newmark_beta_method(in_row, dt, w, xi, 0.0, 0.0)); }); out } +/// Pseudo-spectral acceleration at every integer rotation angle 0..=179 +/// degrees, for a single oscillator period, computed **serially**. +/// +/// For each station the two horizontal components are pushed through the +/// Newmark-beta SDOF solver (kept in f64 for accuracy over long records) and +/// the displacement responses are reduced to their peak rotated amplitude at +/// every angle by [`crate::rotd::Hull::peaks`]. Multiplying by `w^2` +/// converts the peak relative displacement of the unit-mass oscillator to a +/// pseudo-spectral acceleration. +/// +/// The loop over stations is deliberately serial: this runs one period per +/// call inside a Dask worker that already owns a core, so spawning a Rayon +/// pool here would oversubscribe the machine and fight the outer scheduler. +/// The RotD hull's work buffers are allocated once and reused for every +/// station. +/// +/// `comp_0` and `comp_90` are the 000 and 090 acceleration waveforms with +/// shape `(ns, nt)`. The result has shape `(ns, 182)`: columns 0..=179 are +/// the rotated peaks at each integer angle, and columns 180 and 181 are the +/// exact peaks of the unrotated 000 and 090 responses (`w^2 * max|response|`), +/// so a caller who only needs those two components does not have to re-derive +/// them from angle 0 / angle 90, which are off by `cos(90 deg) ~= 6.12e-17` +/// rather than being exactly zero. +pub fn psa_rotd180( + comp_0: &ArrayView2, + comp_90: &ArrayView2, + dt: f64, + w: f64, + xi: f64, +) -> Array2 { + assert_eq!( + comp_0.dim(), + comp_90.dim(), + "components must have matching shapes" + ); + let ns = comp_0.nrows(); + let conversion_factor = w * w; + let mut out = Array2::::zeros((ns, 182)); + let mut hull = crate::rotd::Hull::with_capacity(comp_0.ncols()); + for s in 0..ns { + let response_0 = newmark_beta_method(comp_0.row(s), dt, w, xi, 0.0, 0.0); + let response_90 = newmark_beta_method(comp_90.row(s), dt, w, xi, 0.0, 0.0); + let peaks = hull.peaks(response_0.view(), response_90.view()); + let mut row = out.row_mut(s); + for (angle, &peak) in peaks.iter().enumerate() { + row[angle] = conversion_factor * peak; + } + row[180] = conversion_factor * response_0.iter().fold(0.0f64, |m, &u| m.max(u.abs())); + row[181] = conversion_factor * response_90.iter().fold(0.0f64, |m, &u| m.max(u.abs())); + } + out +} + +/// Pseudo-spectral acceleration peak for a single component, one period. +/// +/// `waveforms` has shape `(ns, nt)`. Only the peak response is returned +/// (shape `(ns,)`), so a caller that needs just one component -- e.g. the +/// vertical, which never participates in RotD -- does not have to carry a +/// full `(ns, nt)` displacement response back into Python. +pub fn psa_peak(waveforms: &ArrayView2, dt: f64, w: f64, xi: f64) -> Array1 { + let conversion_factor = w * w; + Array1::from_shape_fn(waveforms.nrows(), |s| { + let response = newmark_beta_method(waveforms.row(s), dt, w, xi, 0.0, 0.0); + conversion_factor * response.iter().fold(0.0f64, |m, &u| m.max(u.abs())) + }) +} + #[cfg(test)] mod tests { @@ -265,6 +327,27 @@ mod tests { assert_abs_diff_eq!(u, analytical, epsilon = 5e-4); } + #[test] + fn test_psa_rotd180_columns_180_181_match_component_peaks() { + // Columns 180/181 must agree with an independent per-component + // psa_peak computation (which is also what column 0 / column 90 + // approximate, up to cos(90 deg) != 0 exactly in f64). + let t = Array1::::linspace(0.0, 2.0, 512); + let dt = t[1] - t[0]; + let comp_0 = t.map(|&x| (3.0 * x).sin()); + let comp_90 = t.map(|&x| 0.7 * (5.0 * x).cos()); + let comp_0_2d = comp_0.clone().insert_axis(Axis(0)); + let comp_90_2d = comp_90.clone().insert_axis(Axis(0)); + let w = 2.0 * PI; + + let combined = psa_rotd180(&comp_0_2d.view(), &comp_90_2d.view(), dt, w, XI); + let peak_0 = psa_peak(&comp_0_2d.view(), dt, w, XI); + let peak_90 = psa_peak(&comp_90_2d.view(), dt, w, XI); + + assert_abs_diff_eq!(combined[[0, 180]], peak_0[0], epsilon = 1e-12); + assert_abs_diff_eq!(combined[[0, 181]], peak_90[0], epsilon = 1e-12); + } + #[test] fn test_newmark_solves_damped_harmonic_oscillation() { let t = Array1::::linspace(0.0, 10.0, 10000); diff --git a/src-rust/rotd.rs b/src-rust/rotd.rs index dcef5ad0..0558b0ea 100644 --- a/src-rust/rotd.rs +++ b/src-rust/rotd.rs @@ -1,61 +1,263 @@ use std::f64::consts::PI; use ndarray::prelude::*; -use ndarray::Zip; const DEGREES: f64 = PI / 180.0; -/// RotD180 calculations for a single pair of components assuming the absmax -/// reduction function. +/// Integer rotation angles RotD is sampled at: 0..=179 degrees. The peak is +/// an absolute value, so angles beyond 180 degrees repeat. +pub const N_ANGLES: usize = 180; + +/// Columns in a RotD statistics row: the RotD00, RotD50 and RotD100 peak +/// amplitudes, then the orientation in degrees at which each occurs. +pub const N_ROTD_STATS: usize = 6; + +const fn cross(o: [f64; 2], u: [f64; 2], v: [f64; 2]) -> f64 { + (u[0] - o[0]) * (v[1] - o[1]) - (u[1] - o[1]) * (v[0] - o[0]) +} + +/// Extend `vertices` with one monotone chain over `points`, dropping any +/// trailing vertex that would make a non-left turn. +/// +/// `floor` is the number of vertices already in `vertices` that belong to an +/// earlier chain and must not be popped: 1 for the lower hull (its own first +/// point), and the whole lower hull for the upper one. +fn monotone_chain( + vertices: &mut Vec<[f64; 2]>, + points: impl Iterator, + floor: usize, +) { + for p in points { + while vertices.len() > floor { + let (o, u) = (vertices[vertices.len() - 2], vertices[vertices.len() - 1]); + if cross(o, u, p) > 0.0 { + break; + } + vertices.pop(); + } + vertices.push(p); + } +} + +/// The convex hull of a response trajectory, and the scratch space used to +/// find it. /// -/// Returns the (min, median, max) rotated peak amplitude, i.e. RotD00, RotD50 -/// and RotD100. -fn rotd_calculation(comp_0: ArrayView1, comp_90: ArrayView1) -> [f64; 3] { - let mut rotd_values: [f64; 180] = std::array::from_fn(|theta| { - let (sin_theta, cos_theta) = (theta as f64 * DEGREES).sin_cos(); - - Zip::from(comp_0).and(comp_90).fold(0.0f64, |peak, &x, &y| { - peak.max((cos_theta * x + sin_theta * y).abs()) +/// Every RotD figure this module produces is a peak rotated amplitude, and +/// every one of them comes from [`Hull::peaks`]. The buffers live in the +/// struct rather than in locals so a batch of stations allocates once and +/// reuses the same two vectors for every record. +#[derive(Default)] +pub struct Hull { + /// Points that survived the Akl-Toussaint cull, sorted lexicographically. + survivors: Vec<[f64; 2]>, + /// The hull vertices themselves, the only points [`Hull::peaks`] scans. + vertices: Vec<[f64; 2]>, +} + +impl Hull { + /// A hull sized for records of `nt` timesteps. + pub fn with_capacity(nt: usize) -> Self { + Self { + survivors: Vec::with_capacity(nt), + vertices: Vec::with_capacity(256), + } + } + + /// Peak rotated amplitude at every integer angle 0..=179 degrees for one + /// pair of components. + /// + /// The peak at angle theta is the support function of the response + /// trajectory `(x, y)` along the rotated axis, which is maximised at a + /// vertex of the trajectory's convex hull. The hull is found with + /// Akl-Toussaint culling followed by a monotone chain: a first O(n) pass + /// takes the four axis-extreme points, and any point strictly inside the + /// polygon they span cannot be a hull vertex and is dropped, so the sort + /// that follows sees only a few hundred of the tens of thousands of + /// timesteps. The 180 evaluations over the resulting handful of hull + /// vertices are then exact and cheap; `rotd180_matches_brute` pins the + /// result against the direct scan. + pub fn peaks(&mut self, x: ArrayView1, y: ArrayView1) -> [f64; 180] { + let n = x.len(); + // Axis extremes, in order around the trajectory: min x, max y, max x, + // min y. + let p0 = [x[0], y[0]]; + let (mut a, mut b, mut c, mut d) = (p0, p0, p0, p0); + for i in 1..n { + let p = [x[i], y[i]]; + if p[0] < a[0] { + a = p; + } + if p[0] > c[0] { + c = p; + } + if p[1] > b[1] { + b = p; + } + if p[1] < d[1] { + d = p; + } + } + // In some cases the Akl-Toussaint culling box degenerates into a + // triangle. In that case (a, b, c, d) contains repeat points which + // creates problems for the culling because then the edge = 0 and + // cross(0, u, v) == 0 for all u, v, which stops the hull doing + // anything. This loop removes the repeats. + let mut ring = [p0; 5]; + let mut corners = 0; + for corner in [a, b, c, d] { + if !ring[..corners].contains(&corner) { + ring[corners] = corner; + corners += 1; + } + } + ring[corners] = ring[0]; + + // Now we build the culling box edges. If the box is a actually a + // triangle an edge is repeated twice. This represents duplicate work in + // the culling loop below, but it is more efficient than leaving out the + // extra edge because Rust is good at optimising the predictable + // cross-products. Making this part dynamic makes calculations slower by + // a factor of 10. + let [edge_0, edge_1, edge_2, edge_3] = std::array::from_fn(|j| { + let corner = j.min(corners - 1); + [ring[corner], ring[corner + 1]] + }); + + // Now cull all points inside the box. + self.survivors.clear(); + for i in 0..n { + let p = [x[i], y[i]]; + let (e0, e1, e2, e3) = ( + cross(edge_0[0], edge_0[1], p), + cross(edge_1[0], edge_1[1], p), + cross(edge_2[0], edge_2[1], p), + cross(edge_3[0], edge_3[1], p), + ); + let inside = (e0 > 0.0 && e1 > 0.0 && e2 > 0.0 && e3 > 0.0) + || (e0 < 0.0 && e1 < 0.0 && e2 < 0.0 && e3 < 0.0); + if !inside { + self.survivors.push(p); + } + } + self.survivors + .sort_unstable_by(|p, q| p[0].total_cmp(&q[0]).then(p[1].total_cmp(&q[1]))); + self.survivors.dedup(); + + self.vertices.clear(); + if self.survivors.len() < 3 { + // Degenerate triangular case. Triangle is always its own convex hull. + self.vertices.extend_from_slice(&self.survivors); + } else { + monotone_chain(&mut self.vertices, self.survivors.iter().copied(), 1); + let lower_hull = self.vertices.len(); + monotone_chain( + &mut self.vertices, + self.survivors.iter().rev().copied(), + lower_hull, + ); + // The upper chain closes back on the lower chain's first point. + self.vertices.pop(); + } + std::array::from_fn(|theta| { + let (sin_theta, cos_theta) = (theta as f64 * DEGREES).sin_cos(); + self.vertices.iter().fold(0.0f64, |peak, &[hx, hy]| { + peak.max((cos_theta * hx + sin_theta * hy).abs()) + }) }) - }); - rotd_values.sort_unstable_by(f64::total_cmp); + } +} + +/// Reduce the 180 per-angle peaks to the (min, median, max) rotated +/// amplitude -- RotD00, RotD50 and RotD100 -- and the orientation in degrees +/// at which each of the three occurs. +/// +/// RotD00 and RotD100 each sit at a single angle -- the argmin and argmax of +/// the sweep -- and where several angles attain the same peak, the lowest of +/// them is reported. RotD50 has no single angle at all: the median of an even +/// number of samples falls between the two central ones, so its value stays +/// the average of that pair, as it has always been, and the orientation +/// reported alongside is the lower of the two. +fn rotd_stats(peaks: [f64; N_ANGLES]) -> [f64; N_ROTD_STATS] { + // Strict comparisons, so a tie leaves the lowest angle in place. Taking + // the extremes here rather than off the ranking below is what makes that + // consistent: the last element of an ascending rank is the *highest* + // angle of any tie at the maximum, not the lowest. + let (mut min_angle, mut max_angle) = (0usize, 0usize); + for theta in 1..N_ANGLES { + if peaks[theta] < peaks[min_angle] { + min_angle = theta; + } + if peaks[theta] > peaks[max_angle] { + max_angle = theta; + } + } + // The two central peaks, carrying their angles through the sort so the + // median has an orientation. The angle breaks ties, so equal peaks are + // ranked in ascending angle whatever the sort's internal order. + let mut ranked: [(f64, u8); N_ANGLES] = + std::array::from_fn(|theta| (peaks[theta], theta as u8)); + ranked.sort_unstable_by(|p, q| p.0.total_cmp(&q.0).then(p.1.cmp(&q.1))); + let (lower_median, upper_median) = (ranked[89], ranked[90]); [ - rotd_values[0], - (rotd_values[89] + rotd_values[90]) / 2.0, - rotd_values[179], + peaks[min_angle], + (lower_median.0 + upper_median.0) / 2.0, + peaks[max_angle], + min_angle as f64, + f64::from(lower_median.1), + max_angle as f64, ] } -/// Fill an (ns, 3) array with the RotD statistics of each waveform pair, -/// optionally distributing the rows across rayon's thread pool. -fn rotd_rows(comp_0: ArrayView2, comp_90: ArrayView2, parallel: bool) -> Array2 { +/// Fill an `(ns, 6)` array with the RotD statistics of each waveform pair: +/// three peak amplitudes then their three orientations, as laid out by +/// [`rotd_stats`]. +pub fn rotd(comp_0: ArrayView2, comp_90: ArrayView2) -> Array2 { assert_eq!( comp_0.nrows(), comp_90.nrows(), "Components must have the same number of waveforms" ); - let mut out = Array2::zeros((comp_0.nrows(), 3)); - - let zip = Zip::from(out.rows_mut()) - .and(comp_0.rows()) - .and(comp_90.rows()); - let stats = |mut out: ArrayViewMut1, comp_0, comp_90| { - out.assign(&ArrayView1::from(&rotd_calculation(comp_0, comp_90))); - }; - if parallel { - zip.par_for_each(stats); - } else { - zip.for_each(stats); + let mut out = Array2::zeros((comp_0.nrows(), N_ROTD_STATS)); + let mut hull = Hull::with_capacity(comp_0.ncols()); + for s in 0..comp_0.nrows() { + let stats = rotd_stats(hull.peaks(comp_0.row(s), comp_90.row(s))); + out.row_mut(s).assign(&ArrayView1::from(&stats)); } out } -pub fn rotd_parallel(comp_0: ArrayView2, comp_90: ArrayView2) -> Array2 { - rotd_rows(comp_0, comp_90, true) +/// Fill an `(ns, 6)` array with the RotD statistics of an already computed +/// `(ns, 180)` angle sweep. +/// +/// The pSA path takes its sweep from [`crate::psa::psa_rotd180`] rather than +/// from [`rotd`], so this applies the one reduction in [`rotd_stats`] to a +/// curve that has already been paid for. +pub fn rotd180_stats(curve: ArrayView2) -> Array2 { + assert_eq!( + curve.ncols(), + N_ANGLES, + "A RotD180 curve needs a peak at every integer angle" + ); + let mut out = Array2::zeros((curve.nrows(), N_ROTD_STATS)); + for s in 0..curve.nrows() { + let row = curve.row(s); + let stats = rotd_stats(std::array::from_fn(|theta| row[theta])); + out.row_mut(s).assign(&ArrayView1::from(&stats)); + } + out } -pub fn rotd(comp_0: ArrayView2, comp_90: ArrayView2) -> Array2 { - rotd_rows(comp_0, comp_90, false) +/// Fill an `(ns, 180)` array with the per-angle peaks of each response pair, +/// serially, allocating the [`Hull`] work buffers once. +pub fn rotd180_rows(comp_0: ArrayView2, comp_90: ArrayView2) -> Array2 { + let ns = comp_0.nrows(); + let mut out = Array2::zeros((ns, N_ANGLES)); + let mut hull = Hull::with_capacity(comp_0.ncols()); + for s in 0..ns { + let peaks = hull.peaks(comp_0.row(s), comp_90.row(s)); + out.row_mut(s).assign(&ArrayView1::from(&peaks)); + } + out } #[cfg(test)] @@ -63,9 +265,12 @@ mod tests { use std::f64::consts::{SQRT_2, TAU}; use ndarray::prelude::*; + use ndarray::Zip; use proptest::prelude::*; - use crate::rotd::{rotd, rotd_calculation, rotd_parallel}; + use crate::rotd::{ + rotd, rotd180_rows, rotd180_stats, rotd_stats, Hull, DEGREES, N_ANGLES, N_ROTD_STATS, + }; /// Slack allowed on the sqrt(2) bound. The bound is attained exactly by /// linearly polarised records, so only floating point error is tolerated. @@ -75,6 +280,72 @@ mod tests { /// proptest run to well under a second. const MAX_NT: usize = 128; + /// The direct scan the culled [`Hull::peaks`] must reproduce: every angle + /// against every timestep, no hull reduction. + fn brute_peaks(x: ArrayView1, y: ArrayView1) -> [f64; 180] { + std::array::from_fn(|theta| { + let (sin_theta, cos_theta) = (theta as f64 * DEGREES).sin_cos(); + Zip::from(x).and(y).fold(0.0f64, |m: f64, &a, &b| { + m.max((cos_theta * a + sin_theta * b).abs()) + }) + }) + } + + /// [`Hull::peaks`] with a fresh hull, for tests that do not exercise + /// buffer reuse themselves. + fn peaks(x: ArrayView1, y: ArrayView1) -> [f64; 180] { + Hull::default().peaks(x, y) + } + + /// The statistics the hull-based [`rotd`] must reproduce, taken from the + /// direct scan rather than from the hull. + fn brute_stats(x: ArrayView1, y: ArrayView1) -> [f64; N_ROTD_STATS] { + rotd_stats(brute_peaks(x, y)) + } + + /// Assert that each reported orientation locates its own statistic in the + /// sweep it was reduced from. + fn assert_orientations_locate_statistics(peaks: [f64; N_ANGLES], case: &str) { + let [rotd00, rotd50, rotd100, at_00, at_50, at_100] = rotd_stats(peaks); + for (angle, statistic) in [(at_00, "RotD00"), (at_50, "RotD50"), (at_100, "RotD100")] { + assert!( + (0.0..N_ANGLES as f64).contains(&angle) && angle.fract() == 0.0, + "{case}: {statistic} orientation {angle} is not an integer angle in 0..180" + ); + } + // The extremes are attained exactly at their own angle. + assert_eq!( + peaks[at_00 as usize], rotd00, + "{case}: RotD00 is not the peak at {at_00} degrees" + ); + assert_eq!( + peaks[at_100 as usize], rotd100, + "{case}: RotD100 is not the peak at {at_100} degrees" + ); + // The median falls between the two central peaks, so its angle is the + // one ranked 90th of 180: fewer than 90 angles peak below it, and at + // least 90 peak at or below it. + let median_peak = peaks[at_50 as usize]; + let below = peaks.iter().filter(|&&peak| peak < median_peak).count(); + let at_or_below = peaks.iter().filter(|&&peak| peak <= median_peak).count(); + assert!( + below <= 89 && at_or_below >= 90, + "{case}: RotD50 orientation {at_50} is not the lower median: {below} angles below it, {at_or_below} at or below" + ); + assert!( + median_peak <= rotd50 && rotd50 <= rotd100, + "{case}: RotD50 {rotd50} is not between its own peak {median_peak} and RotD100" + ); + // The lowest angle of a tie, for every statistic. + for (angle, value) in [(at_00, rotd00), (at_100, rotd100), (at_50, median_peak)] { + let first = peaks.iter().position(|&peak| peak == value).unwrap(); + assert_eq!( + angle as usize, first, + "{case}: {value} is first attained at {first} degrees, not {angle}" + ); + } + } + /// Assert RotD100 <= sqrt(2) * RotD50 for one pair of components, returning /// the ratio. /// @@ -84,7 +355,7 @@ mod tests { /// such pair -- 90 of the 180 angles -- peaks at or above RotD100 / sqrt(2), /// which puts the median there too. fn assert_ratio_bounded(comp_0: ArrayView1, comp_90: ArrayView1, case: &str) -> f64 { - let [rotd00, rotd50, rotd100] = rotd_calculation(comp_0, comp_90); + let [rotd00, rotd50, rotd100, ..] = brute_stats(comp_0, comp_90); assert!( rotd00 <= rotd50 && rotd50 <= rotd100, "{case}: RotD00 <= RotD50 <= RotD100 violated: {rotd00}, {rotd50}, {rotd100}" @@ -191,30 +462,225 @@ mod tests { #[test] fn prop_batch_api_agrees_and_is_bounded((comp_0, comp_90) in arb_batch()) { - let serial = rotd(comp_0.view(), comp_90.view()); - let parallel = rotd_parallel(comp_0.view(), comp_90.view()); - prop_assert_eq!(&serial, ¶llel, "rotd and rotd_parallel disagree"); + let stats = rotd(comp_0.view(), comp_90.view()); + let peaks = rotd180_rows(comp_0.view(), comp_90.view()); - for (i, stats) in serial.rows().into_iter().enumerate() { - let expected = rotd_calculation(comp_0.row(i), comp_90.row(i)); + for (i, row) in stats.rows().into_iter().enumerate() { + // Both entry points reduce the same hull, so the (ns, 3) + // statistics must be exactly the reduction of the (ns, 180) + // curve -- no tolerance needed to tie them together. + let row_peaks: [f64; 180] = std::array::from_fn(|theta| peaks[(i, theta)]); + let curve_stats = rotd_stats(row_peaks); prop_assert_eq!( - stats, - ArrayView1::from(&expected), - "row {} disagrees with rotd_calculation", i + row, + ArrayView1::from(&curve_stats), + "row {} disagrees with its own RotD180 curve", i ); + // And the hull must reproduce the direct scan's peaks, up + // to the floating point slack of evaluating fewer points. The + // orientations are deliberately not compared: a near-tie at + // an extreme can land on either of two angles under that + // slack, so they are pinned to their own sweep instead, by + // prop_orientations_locate_their_statistics. + let expected = brute_stats(comp_0.row(i), comp_90.row(i)); + for (stat, (&got, &want)) in row.iter().zip(expected.iter()).take(3).enumerate() { + prop_assert!( + (got - want).abs() <= 1e-9 * want.max(1.0), + "row {} stat {}: hull {} != brute {}", i, stat, got, want + ); + } assert_ratio_bounded(comp_0.row(i), comp_90.row(i), &format!("row {i}")); } } + + #[test] + fn prop_orientations_locate_their_statistics((comp_0, comp_90) in arb_record()) { + // Whatever the record, each statistic's orientation must point at + // the angle it came from. + assert_orientations_locate_statistics( + peaks(comp_0.view(), comp_90.view()), + "generated record", + ); + assert_orientations_locate_statistics( + brute_peaks(comp_0.view(), comp_90.view()), + "generated record (brute)", + ); + } + + #[test] + fn prop_culled_matches_brute((comp_0, comp_90) in arb_record()) { + // The interior culling must never change a single per-angle peak, + // across the full spread from polarised to near-circular records. + let got = peaks(comp_0.view(), comp_90.view()); + let want = brute_peaks(comp_0.view(), comp_90.view()); + for theta in 0..180 { + prop_assert!( + (got[theta] - want[theta]).abs() <= 1e-9 * want[theta].max(1.0), + "angle {}: culled {} != brute {}", theta, got[theta], want[theta] + ); + } + } + } + + #[test] + fn rotd180_matches_brute() { + // The per-angle peaks must match a direct scan at every angle, and once + // sorted must reproduce the RotD00/50/100 statistics of the reference. + let nt = 733; + let comp_0 = Array1::from_shape_fn(nt, |i| { + let t = i as f64; + (0.31 * t).sin() * (0.007 * t).cos() - 0.4 * (0.13 * t).sin() + }); + let comp_90 = Array1::from_shape_fn(nt, |i| { + let t = i as f64; + (0.17 * t).cos() + 0.6 * (0.05 * t).sin() * (0.002 * t).cos() + }); + + let got = peaks(comp_0.view(), comp_90.view()); + let want = brute_peaks(comp_0.view(), comp_90.view()); + for theta in 0..180 { + assert!( + (got[theta] - want[theta]).abs() <= 1e-9 * want[theta].max(1.0), + "angle {theta}: culled {} != brute {}", + got[theta], + want[theta] + ); + } + + let mut sorted = got; + sorted.sort_unstable_by(f64::total_cmp); + let [min, median, max, ..] = brute_stats(comp_0.view(), comp_90.view()); + assert!((sorted[0] - min).abs() <= 1e-9 * min.max(1.0)); + assert!(((sorted[89] + sorted[90]) / 2.0 - median).abs() <= 1e-9 * median.max(1.0)); + assert!((sorted[179] - max).abs() <= 1e-9 * max.max(1.0)); + } + + #[test] + fn rotd180_matches_brute_on_circular_record() { + // A near-circular trajectory is the culling's worst case: almost every + // point sits near the hull, so few are dropped. The result must still + // be exact. + let nt = 2000; + let comp_0 = Array1::from_shape_fn(nt, |i| (TAU * i as f64 / nt as f64).cos()); + let comp_90 = Array1::from_shape_fn(nt, |i| (TAU * i as f64 / nt as f64).sin()); + let got = peaks(comp_0.view(), comp_90.view()); + let want = brute_peaks(comp_0.view(), comp_90.view()); + for theta in 0..180 { + assert!( + (got[theta] - want[theta]).abs() <= 1e-9, + "circular angle {theta}" + ); + } } #[test] - fn test_rotd_calculation() { + fn rotd180_handles_degenerate_records() { + // Zero, single-sample and collinear records must not panic and must + // agree with the reference peaks (all zero, or |projection|). + let zeros = Array1::::zeros(16); + assert_eq!(peaks(zeros.view(), zeros.view()), [0.0; 180]); + + let single = array![3.5]; + let single_peaks = peaks(single.view(), array![0.0].view()); + assert!((single_peaks[0] - 3.5).abs() < 1e-12); + assert!(single_peaks[90].abs() < 1e-12); + + // A linearly polarised (collinear) record: peaks trace |cos(theta)|. + let line_0 = array![1.0, -2.0, 3.0, -4.0]; + let line_90 = &line_0 * 2.0; + let got = peaks(line_0.view(), line_90.view()); + let want = brute_peaks(line_0.view(), line_90.view()); + for theta in 0..180 { + assert!( + (got[theta] - want[theta]).abs() < 1e-9, + "collinear angle {theta}" + ); + } + } + + #[test] + fn cull_drops_the_interior_when_a_point_is_extreme_in_two_axes() { + // One timestep that is both the max in x and the min in y collapses + // the extreme quadrilateral onto a triangle. The cull must still drop + // the interior: with a zero-length quad edge every cross product is + // zero, no point counts as inside, and the whole record reaches the + // sort -- which is how 3497857_PARS_HN_20 lost 7x of its speedup. + let interior = 1000; + let mut comp_0 = Array1::zeros(interior + 3); + let mut comp_90 = Array1::zeros(interior + 3); + for i in 0..interior { + let t = TAU * i as f64 / interior as f64; + comp_0[i] = 0.01 * t.cos(); + comp_90[i] = 0.01 * t.sin(); + } + // min x, max y, and one corner that is both max x and min y. + let extremes = [[-1.0, 0.0], [0.0, 1.0], [1.0, -1.0]]; + for (i, [px, py]) in extremes.into_iter().enumerate() { + comp_0[interior + i] = px; + comp_90[interior + i] = py; + } + + let mut hull = Hull::default(); + let got = hull.peaks(comp_0.view(), comp_90.view()); + assert!( + hull.survivors.len() < interior / 10, + "cull kept {} of {} points", + hull.survivors.len(), + interior + 3 + ); + let want = brute_peaks(comp_0.view(), comp_90.view()); + for theta in 0..180 { + assert!( + (got[theta] - want[theta]).abs() <= 1e-9 * want[theta].max(1.0), + "doubly extreme angle {theta}" + ); + } + } + + #[test] + fn rotd180_buffers_reset_between_records() { + // Reusing the same buffers across records of different lengths and + // shapes must give exactly the same answer as fresh buffers each call: + // a guard against a missing clear() leaking state between stations. + let mut hull = Hull::default(); + let records = [ + (array![1.0, -2.0, 3.0], array![0.5, 0.5, -1.0]), + (Array1::zeros(5), Array1::zeros(5)), + ( + array![9.81, -3.0, 2.0, 7.0, -8.0, 1.0], + array![1.0, 4.0, -4.0, 0.0, 2.0, -2.0], + ), + (array![2.5], array![-1.5]), + ]; + for (comp_0, comp_90) in &records { + let reused = hull.peaks(comp_0.view(), comp_90.view()); + let want = brute_peaks(comp_0.view(), comp_90.view()); + for theta in 0..180 { + assert!( + (reused[theta] - want[theta]).abs() <= 1e-9 * want[theta].max(1.0), + "reused buffers diverged at angle {theta}" + ); + } + } + } + + #[test] + fn test_rotd_statistics() { let comp_0 = array![1.0f64, 0.0f64]; let comp_90 = array![0.0f64, 1.0f64]; - let [min, median, max] = rotd_calculation(comp_0.view(), comp_90.view()); + let [min, median, max, at_min, at_median, at_max] = + rotd_stats(peaks(comp_0.view(), comp_90.view())); let expected_min = 2.0f64.sqrt() / 2.0; // e.g. at pi / 4 degrees let expected_max = 1.0; // e.g. at 0 degrees - let expected_median = 0.9238443540096138; // at 23 degrees, derived independently with numpy + let expected_median = 0.9238443540096138; // derived independently with numpy + // The sweep is max(|cos theta|, |sin theta|): least at 45 degrees, and + // 1 at both 0 and 90 degrees, of which the lower is reported. The + // median falls between the peaks at 157 and 158 degrees. + assert_eq!( + [at_min, at_median, at_max], + [45.0, 157.0, 0.0], + "Orientations wrong: found {at_min}, {at_median}, {at_max} degrees" + ); assert!( (min - expected_min).abs() < 1e-6, "Minimum calculation failed: expected sqrt(2) +/- 1e-6 found: {}", @@ -232,6 +698,32 @@ mod tests { ); } + #[test] + fn rotd180_stats_reduces_a_curve_like_rotd() { + // The pSA path reduces an already computed sweep rather than a pair of + // components, and must land on exactly the same statistics. + let nt = 512; + let comp_0 = Array2::from_shape_fn((3, nt), |(s, i)| { + let t = i as f64; + (0.03 * t + s as f64).sin() * (1.0 + s as f64) + }); + let comp_90 = Array2::from_shape_fn((3, nt), |(s, i)| { + let t = i as f64; + (0.05 * t).cos() - 0.3 * (0.11 * t + s as f64).sin() + }); + let curve = rotd180_rows(comp_0.view(), comp_90.view()); + assert_eq!( + rotd180_stats(curve.view()), + rotd(comp_0.view(), comp_90.view()) + ); + } + + #[test] + #[should_panic(expected = "peak at every integer angle")] + fn rotd180_stats_rejects_a_short_curve() { + rotd180_stats(Array2::zeros((2, 179)).view()); + } + #[test] fn test_ratio_bound_degenerate_records() { // Cases proptest will not reach on its own: exact zeros, single diff --git a/src-rust/significant_duration.rs b/src-rust/significant_duration.rs index 8dcbb867..63414ad8 100644 --- a/src-rust/significant_duration.rs +++ b/src-rust/significant_duration.rs @@ -1,4 +1,3 @@ -use crate::utils::parallel_reduce_rows; use ndarray::prelude::*; fn threshold_search(normalised_intensities: ArrayView1, dt: f64, low: f64, high: f64) -> f64 { @@ -31,20 +30,6 @@ pub fn significant_duration( }) } -pub fn parallel_significant_duration( - arias_intensity: ArrayView2, - dt: f64, - low: f64, - high: f64, -) -> Array1 { - // Normalise arias intensity - // NOTE: this is subtly different to parallel_map because it does - // not create a copy of the array for output. It simply updates in-place. - parallel_reduce_rows(arias_intensity.view(), |normalised_intensity| { - threshold_search(normalised_intensity, dt, low, high) - }) -} - #[cfg(test)] mod tests { use super::*; @@ -76,19 +61,6 @@ mod tests { assert_eq!(result[0], 0.0); } - #[test] - fn test_parallel_matches_sequential() { - let arias = array![[0.0, 0.1, 0.5, 0.8, 1.0], [0.0, 0.4, 0.7, 0.9, 1.0]]; - let dt = 0.01; - let low = 0.05; - let high = 0.95; - - let seq = significant_duration(arias.view(), dt, low, high); - let par = parallel_significant_duration(arias.view(), dt, low, high); - - assert_abs_diff_eq!(seq, par, epsilon = 1e-10); - } - #[test] fn test_output_shape() { let arias = Array2::::zeros((5, 100)); diff --git a/src-rust/trapz.rs b/src-rust/trapz.rs index 1820749e..f48f00d4 100644 --- a/src-rust/trapz.rs +++ b/src-rust/trapz.rs @@ -6,7 +6,6 @@ //! A key feature of this implementation is the handling of zero-crossings, which ensures that //! rectified functions (like $f(x) = |x|$) are integrated with geometric precision. -use crate::utils::{parallel_map_rows, parallel_reduce_rows}; use ndarray::prelude::*; /// Calculates the contribution of a single step to the trapezium integral. @@ -73,22 +72,12 @@ fn cumulative_trapz_one_with_fun( out *= 0.5; } -/// Computes the total integral for each row of a 2D array in parallel. +/// Computes the total integral for each row of a 2D array. /// /// # Arguments /// * `waveforms` - 2D array where each row is a separate signal. /// * `dt` - The timestep. /// * `f` - Function to apply to values (e.g., `|x| x * x` for arias intensity). -pub fn parallel_trapz_with_fun(waveforms: ArrayView2, dt: f64, f: F) -> Array1 -where - F: Fn(f64) -> f64 + Send + Sync, -{ - parallel_reduce_rows(waveforms, |waveform| trapz_one_with_fun(waveform, dt, &f)) -} - -/// Computes the total integral for each row of a 2D array. -/// -/// This is the sequential version of [`parallel_trapz_with_fun`]. pub fn trapz_with_fun(waveforms: ArrayView2, dt: f64, f: F) -> Array1 where F: Fn(f64) -> f64, @@ -96,26 +85,7 @@ where waveforms.map_axis(Axis(1), |waveform| trapz_one_with_fun(waveform, dt, &f)) } -/// Computes the cumulative integral for each row of a 2D array in parallel. -/// -/// # Returns -/// An `Array2` of the same shape as `waveforms`. -pub fn parallel_cumulative_trapz_with_fun( - waveforms: ArrayView2, - dt: f64, - f: F, -) -> Array2 -where - F: Fn(f64) -> f64 + Send + Sync, -{ - parallel_map_rows(waveforms, |waveform, out| { - cumulative_trapz_one_with_fun(waveform, out, dt, &f) - }) -} - /// Computes the cumulative integral for each row of a 2D array. -/// -/// This is the sequential version of [`parallel_cumulative_trapz_with_fun`]. pub fn cumulative_trapz_with_fun(waveforms: ArrayView2, dt: f64, f: F) -> Array2 where F: Fn(f64) -> f64, @@ -209,29 +179,6 @@ mod tests { assert_abs_diff_eq!(area_dt2, area_dt1 * 2.0, epsilon = 1e-10); } - #[test] - fn test_parallel_equals_sequential() { - // This test prevents "implementation drift" where the parallel version - // gets updated but the sequential one is forgotten. - - // Shape (2 rows, 3 cols) - let waveforms = array![[0.0, 1.0, 2.0], [2.0, 1.0, 0.0]]; - let dt = 1.0; - - let seq_res = trapz_with_fun(waveforms.view(), dt, identity); - let par_res = parallel_trapz_with_fun(waveforms.view(), dt, identity); - - // Check if shapes match - assert_eq!( - seq_res.dim(), - par_res.dim(), - "Sequential and Parallel output shapes mismatch" - ); - - // Check if values match - assert_abs_diff_eq!(seq_res, par_res, epsilon = 1e-10); - } - #[test] fn test_orientation_consistency() { // Checks code integrates along rows (Axis 1). diff --git a/src-rust/utils.rs b/src-rust/utils.rs deleted file mode 100644 index 9b586876..00000000 --- a/src-rust/utils.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Parallel processing utilities for 2D arrays. -//! -//! This module provides generic abstractions for performing row-wise operations -//! in parallel using `ndarray` and `rayon`. - -use ndarray::parallel::prelude::*; -use ndarray::prelude::*; - -/// Processes each row of a 2D array in parallel and reduces each row to a single value. -/// -/// This is ideal for operations like row-wise sums, means, or integrations (e.g., trapz). -/// -/// # Arguments -/// * `matrix` - An `ArrayView2` of the data to process. -/// * `f` - A function or closure that takes an `ArrayView1` (a row) and returns a single value `U`. -/// -/// # Returns -/// An `Array1` containing one result per row. -/// -pub fn parallel_reduce_rows(matrix: ArrayView2, f: F) -> Array1 -where - T: Sync + Send, - U: Send + Default, - F: Fn(ArrayView1) -> U + Sync + Send, -{ - let (n_rows, _) = matrix.dim(); - let mut results = Vec::with_capacity(n_rows); - - matrix - .axis_iter(Axis(0)) - .into_par_iter() - .map(f) - .collect_into_vec(&mut results); - - Array1::from_vec(results) -} - -/// Transforms each row of a 2D array in parallel, producing a new 2D array. -/// -/// This function maps a row of length $N$ to a new row of length $M$, maintaining -/// the overall structure of the matrix. -/// -/// # Arguments -/// * `input` - An `ArrayView2` to be transformed. -/// * `f` - A function or closure that takes an `ArrayView1` and returns an `Array1`. -/// -/// # Returns -/// A new `Array2` with the same number of rows as the input. -/// -pub fn parallel_map_rows(input: ArrayView2, f: F) -> Array2 -where - T: Sync + Send + Default, - F: Fn(ArrayView1, ArrayViewMut1) + Sync + Send, -{ - let mut out = Array2::::default(input.dim()); - - out.axis_iter_mut(Axis(0)) - .into_par_iter() - .zip(input.axis_iter(Axis(0)).into_par_iter()) - .for_each(|(out_row, in_row)| { - f(in_row, out_row); - }); - - out -} diff --git a/tests/test_ims.py b/tests/test_ims.py index bf1875d4..4f0db2ba 100644 --- a/tests/test_ims.py +++ b/tests/test_ims.py @@ -1,10 +1,10 @@ """Test cases for intensity measure implementations.""" import functools -import multiprocessing from collections.abc import Callable from pathlib import Path +import dask.array as da import numpy as np import numpy.typing as npt import pandas as pd @@ -13,17 +13,17 @@ from hypothesis import given, settings from hypothesis import strategies as st from hypothesis.extra import numpy as nst -from numpy.testing import assert_array_almost_equal +from numpy.testing import assert_array_almost_equal, assert_array_equal from pytest import Metafunc, TempPathFactory from IM import im_calculation, ims, snr_calculation, waveform_reading -from IM.scripts import gen_ko_matrix -@pytest.fixture(scope="session", autouse=True) +@pytest.fixture(scope="session") def ko_matrices( request: pytest.FixtureRequest, tmp_path_factory: TempPathFactory ) -> Path: + from IM.scripts import gen_ko_matrix ko_matrix_directory = tmp_path_factory.mktemp("ko_matrices") gen_ko_matrix.main(ko_matrix_directory, num_to_gen=12) return ko_matrix_directory @@ -60,6 +60,18 @@ def sample_periods() -> npt.NDArray[np.float64]: return np.array([0.1, 0.2, 0.5, 1.0], dtype=np.float64) +def _to_dask(waveform: npt.NDArray[np.float64], station_chunk: int) -> xr.DataArray: + """Wrap a bare waveform array as a dask-backed DataArray with real station + names, chunked over `station` only (component/time as single chunks).""" + n_stations = waveform.shape[1] + return xr.DataArray( + da.from_array(waveform, chunks=(waveform.shape[0], station_chunk, waveform.shape[2])), + dims=("component", "station", "time"), + coords={"station": [f"stat_{i}" for i in range(n_stations)]}, + attrs={"units": "g"}, + ) + + # NOTE: The following unit tests PGA and PGV exist because there is no direct implementation of PGA/PGV in the rust code. @@ -75,8 +87,9 @@ def test_pga(comp_0: npt.NDArray[np.float64], expected_pga: float) -> None: # Shape (n_comp, n_stat, nt) waveforms = np.zeros((3, 1, len(comp_0)), dtype=np.float64) waveforms[ims.Component.COMP_0, 0, :] = comp_0 - result = ims.peak_ground_acceleration(waveforms, cores=1) - assert np.isclose(result["000"].iloc[0], expected_pga, atol=1e-3) + result = ims.peak_ground_acceleration(waveforms) + assert result.attrs["name"] == "PGA" + assert np.isclose(result["000"].item(), expected_pga, atol=1e-3) @pytest.mark.parametrize( @@ -97,8 +110,9 @@ def test_pgv( waveforms = np.zeros((3, 1, len(comp_0)), dtype=np.float64) waveforms[ims.Component.COMP_0, 0, :] = comp_0 dt = t_max / (len(comp_0) - 1) - result = ims.peak_ground_velocity(waveforms, dt, cores=1) - assert np.isclose(result["000"].iloc[0], expected_pgv, atol=0.1) + result = ims.peak_ground_velocity(waveforms, dt) + assert result.attrs["name"] == "PGV" + assert np.isclose(result["000"].item(), expected_pgv, atol=0.1) # CAV5 is partly a python function, so we test expected CAV5 results. CAV tests are in rust. @@ -120,16 +134,26 @@ def test_cav5( waveforms[ims.Component.COMP_0] = comp_0 dt = t_max / (len(comp_0) - 1) - assert np.isclose( - ims.cumulative_absolute_velocity(waveforms, dt, 1, threshold=5)["000"], - expected_cav5, - atol=0.1, + result = ims.cumulative_absolute_velocity(waveforms, dt, threshold=5) + assert result.attrs["name"] == "CAV5" + assert np.isclose(result["000"].item(), expected_cav5, atol=0.1) + + +def test_cav_name_depends_on_threshold(sample_waveforms: npt.NDArray[np.float64]) -> None: + """threshold=0 is falsy, so it must name the result CAV, not CAV5.""" + assert ims.cumulative_absolute_velocity(sample_waveforms, 0.01).attrs["name"] == "CAV" + assert ( + ims.cumulative_absolute_velocity(sample_waveforms, 0.01, threshold=0).attrs["name"] + == "CAV" + ) + assert ( + ims.cumulative_absolute_velocity(sample_waveforms, 0.01, threshold=5).attrs["name"] + == "CAV5" ) -@pytest.mark.parametrize("cores", [1, 2]) @pytest.mark.slow -def test_fas_benchmark(cores: int, ko_matrices: Path) -> None: +def test_fas_benchmark(ko_matrices: Path) -> None: data_array_ffp = Path(__file__).parent / "resources" / "fas_benchmark.nc" if not data_array_ffp.exists(): pytest.skip("Benchmark file missing") @@ -145,14 +169,18 @@ def test_fas_benchmark(cores: int, ko_matrices: Path) -> None: waveform = np.ascontiguousarray(np.moveaxis(waveform, -1, 0)) # Input: (n_stations, nt, n_components) as per fourier_amplitude_spectra logic fas_result_ims = ims.fourier_amplitude_spectra( - waveform, dt, data.frequency.values, ko_matrices, cores=cores + waveform, dt, data.frequency.values, ko_matrices ) - assert_array_almost_equal(data.values, fas_result_ims.values, decimal=5) + for component in data.component.values: + assert_array_almost_equal( + data.sel(component=component).values, + fas_result_ims[str(component)].values, + decimal=5, + ) -@pytest.mark.parametrize("cores", [1, multiprocessing.cpu_count()]) -def test_fas_multiple_stations_benchmark(cores: int, ko_matrices: Path) -> None: +def test_fas_multiple_stations_benchmark(ko_matrices: Path) -> None: """Compare benchmark FAS calculation with multiple stations against current implementation.""" # Load the data array data_array_ffp = Path(__file__).parent / "resources" / "fas_benchmark.nc" @@ -172,16 +200,18 @@ def test_fas_multiple_stations_benchmark(cores: int, ko_matrices: Path) -> None: # Compute the Fourier Amplitude Spectra fas_result_ims = ims.fourier_amplitude_spectra( - duplicated_array, dt, data.frequency, ko_matrices, cores=cores + duplicated_array, dt, data.frequency, ko_matrices ) # Compare the results - for i in range(fas_result_ims.shape[1]): - assert_array_almost_equal( - fas_result_ims[:, i, :], - data[:, 0, :], - decimal=5, - ) + for component in data.component.values: + expected = data.sel(component=component)[0, :] + for station in range(2): + assert_array_almost_equal( + fas_result_ims[str(component)].isel(station=station).values, + expected, + decimal=5, + ) @pytest.mark.slow @@ -237,9 +267,13 @@ def test_all_ims_benchmark(ko_matrices: Path) -> None: ko_directory=ko_matrices, ) + # The benchmark predates the RotD orientation components and has no + # reference angles to compare against; those are covered by + # test_rotd_orientations_match_a_direct_angle_sweep instead. + components = [component for component in result.index if component in data.index] for im in result.columns: - assert result[im].values == pytest.approx( - data.loc[result.index, im].values, abs=5e-4, rel=0.01, nan_ok=True + assert result.loc[components, im].values == pytest.approx( + data.loc[components, im].values, abs=5e-4, rel=0.01, nan_ok=True ), ( f"Results for {im} do not match!\n{result}" ) # 5e-6 implies rounding to five decimal places @@ -364,11 +398,8 @@ def pytest_generate_tests(metafunc: Metafunc) -> None: metafunc.parametrize("resource_dir", benchmark_cases, ids=lambda p: p.stem) -@pytest.mark.parametrize("cores", [1, multiprocessing.cpu_count()]) @pytest.mark.slow -def test_all_ims_benchmark_edge_cases( - resource_dir: Path, cores: int, ko_matrices: Path -) -> None: +def test_all_ims_benchmark_edge_cases(resource_dir: Path, ko_matrices: Path) -> None: """Compare benchmark IM calculation against current implementation for each directory in resources for edge cases.""" # Load the benchmark DataFrame benchmark_ffp = resource_dir / "im_benchmark.csv" @@ -401,11 +432,14 @@ def test_all_ims_benchmark_edge_cases( # Calculate the intensity measures result = im_calculation.calculate_ims( - waveform, dt, ims_list=im_list, ko_directory=ko_matrices, cores=cores + waveform, dt, ims_list=im_list, ko_directory=ko_matrices ) - # Align columns and indices for comparison - expected = data.loc[result.index, result.columns] + # Align columns and indices for comparison, dropping the components the + # benchmark does not carry (the RotD orientations, which postdate it). + components = [component for component in result.index if component in data.index] + result = result.loc[components] + expected = data.loc[components, result.columns] # Check for failure if not np.allclose( @@ -428,7 +462,7 @@ def test_all_ims_benchmark_edge_cases( # Perform standard assertions for im in result.columns: assert result[im].values == pytest.approx( - data.loc[result.index, im].values, abs=5e-4, rel=0.01, nan_ok=True + expected[im].values, abs=5e-4, rel=0.01, nan_ok=True ), f"Results for {im} do not match!\n{result}" @@ -445,13 +479,14 @@ def test_significant_duration( percent_high: float, ) -> None: dt = 0.01 - result = ims.significant_duration( - sample_waveforms, dt, percent_low, percent_high, cores=1 - ) + result = ims.significant_duration(sample_waveforms, dt, percent_low, percent_high) - assert result.shape == (sample_waveforms.shape[1], 4) # 4 components - assert np.all(result.values >= 0) - assert np.all(result.values <= len(sample_time) * dt) + assert result.attrs["name"] == "duration" + assert set(result.data_vars) == set(ims.GEOM_COMPONENTS) + for component in ims.GEOM_COMPONENTS: + assert result[component].shape == (sample_waveforms.shape[1],) + assert np.all(result[component].values >= 0) + assert np.all(result[component].values <= len(sample_time) * dt) def test_ds5xx() -> None: @@ -462,8 +497,12 @@ def test_ds5xx() -> None: waveforms[ims.Component.COMP_VER, 0, :] = comp_0 * 3 dt = 1.0 / len(comp_0) - assert ims.ds575(waveforms, dt, cores=1)["000"].iloc[0] == pytest.approx(0.7) - assert ims.ds595(waveforms, dt, cores=1)["000"].iloc[0] == pytest.approx(0.9) + ds575 = ims.ds575(waveforms, dt) + ds595 = ims.ds595(waveforms, dt) + assert ds575.attrs["name"] == "Ds575" + assert ds595.attrs["name"] == "Ds595" + assert ds575["000"].item() == pytest.approx(0.7) + assert ds595["000"].item() == pytest.approx(0.9) # Contract guarantee on output shapes @@ -484,13 +523,13 @@ def test_peak_ground_parameters( dt = float(sample_time[1] - sample_time[0]) if func == ims.peak_ground_acceleration: - result = func(sample_waveforms, cores=1) + result = func(sample_waveforms) else: - result = func(sample_waveforms, dt, cores=1) + result = func(sample_waveforms, dt) - assert isinstance(result, pd.DataFrame) - assert set(result.columns) >= {"000", "090", "ver", "geom"} - assert np.all(result.select_dtypes(include=[np.number]) >= 0) + assert isinstance(result, xr.Dataset) + assert set(result.data_vars) >= {"000", "090", "ver", "geom"} + assert all((variable.values >= 0).all() for variable in result.data_vars.values()) # Test cases for Fourier Amplitude Spectra @@ -504,26 +543,14 @@ def test_fourier_amplitude_spectra( """Test Fourier Amplitude Spectra calculation.""" dt = sample_time[1] - sample_time[0] freqs = np.logspace(-1, 1, n_freqs, dtype=np.float64) - # Force the multiprocessing code path if necessary. - result_mp = ims.fourier_amplitude_spectra( - sample_waveforms, - dt, - freqs, - ko_matrices, - cores=max(2, multiprocessing.cpu_count()), - ) - # Force the single core path. - result_sc = ims.fourier_amplitude_spectra( - sample_waveforms, dt, freqs, ko_matrices, cores=1 - ) + result = ims.fourier_amplitude_spectra(sample_waveforms, dt, freqs, ko_matrices) - # Check DataFrame structure - assert isinstance(result_mp, xr.DataArray) - assert list(result_mp.coords["component"]) == ["000", "090", "ver", "geom", "eas"] - assert np.allclose(result_mp.coords["frequency"], freqs) - assert np.all(result_mp.as_numpy() >= 0) - # Check that multi-core result and single-core result produce the same output. - assert np.allclose(result_mp.as_numpy(), result_sc.as_numpy()) + # Check Dataset structure + assert isinstance(result, xr.Dataset) + assert result.attrs["name"] == "FAS" + assert list(result.data_vars) == list(ims.FAS_COMPONENTS) + assert np.allclose(result.coords["frequency"], freqs) + assert all((variable.values >= 0).all() for variable in result.data_vars.values()) def test_nyquist_frequency(ko_matrices: Path) -> None: @@ -549,7 +576,11 @@ def test_nyquist_frequency(ko_matrices: Path) -> None: np.testing.assert_array_equal(fas.coords["frequency"].values, expected_freqs) # Verify the shape of the output - assert fas.shape == (5, n_stations, len(expected_freqs)), "Unexpected FAS shape." + assert len(fas.data_vars) == 5 + for component in ims.FAS_COMPONENTS: + assert fas[component].shape == (n_stations, len(expected_freqs)), ( + "Unexpected FAS shape." + ) @pytest.mark.parametrize( @@ -564,7 +595,7 @@ def test_invalid_waveform_shapes(invalid_shape: tuple[int, ...]) -> None: waveforms = np.zeros(invalid_shape, dtype=np.float64) with pytest.raises(TypeError): - ims.peak_ground_acceleration(waveforms, cores=1) # ty: ignore[invalid-argument-type] + ims.peak_ground_acceleration(waveforms) # ty: ignore[invalid-argument-type] @pytest.mark.slow @@ -574,12 +605,10 @@ def test_fourier_amplitude_spectra_shape(ko_matrices: Path) -> None: waveforms = np.random.rand(n_components, n_stations, n_timesteps).astype(np.float64) freqs = np.array([1.0, 10.0, 20.0], dtype=np.float64) - fas = ims.fourier_amplitude_spectra(waveforms, dt, freqs, ko_matrices, cores=1) - assert fas.shape == ( - 5, - n_stations, - len(freqs), - ) # 5 components: 0, 90, ver, geom, eas + fas = ims.fourier_amplitude_spectra(waveforms, dt, freqs, ko_matrices) + assert len(fas.data_vars) == 5 # 5 components: 0, 90, ver, geom, eas + for component in ims.FAS_COMPONENTS: + assert fas[component].shape == (n_stations, len(freqs)) # Asserts that the RotDx values of PGA, PGV and pSA are invariant of the order of 000 and 090. @@ -593,13 +622,12 @@ def test_fourier_amplitude_spectra_shape(ko_matrices: Path) -> None: ), im=st.sampled_from( [ - functools.partial(ims.peak_ground_acceleration, cores=1), - functools.partial(ims.peak_ground_velocity, dt=0.01, cores=1), + ims.peak_ground_acceleration, + functools.partial(ims.peak_ground_velocity, dt=0.01), functools.partial( ims.pseudo_spectral_acceleration, periods=np.array([1.0]), - dt=np.float64(0.01), - cores=1, + dt=0.01, ), ] ), @@ -608,26 +636,18 @@ def test_fourier_amplitude_spectra_shape(ko_matrices: Path) -> None: @pytest.mark.slow def test_rotational_invariance( waveform: npt.NDArray[np.float64], - im: Callable[[ims.ChunkedWaveformArray], pd.DataFrame | xr.DataArray], + im: Callable[[ims.Waveform], xr.Dataset], ) -> None: old_waveform = np.copy(waveform) waveform_ims = im(old_waveform) assert np.allclose(old_waveform, waveform) waveform_ims_transposed = im(waveform[[1, 0, 2]]) - if isinstance(waveform_ims_transposed, pd.DataFrame) and isinstance( - waveform_ims, pd.DataFrame - ): - for component in ["rotd0", "rotd50", "rotd100"]: - value = waveform_ims[component].values - value_t = waveform_ims_transposed[component].values - assert value == pytest.approx(value_t) - else: - assert isinstance(waveform_ims, xr.DataArray) - assert isinstance(waveform_ims_transposed, xr.DataArray) - for component in ["rotd0", "rotd50", "rotd100"]: - value = waveform_ims.sel(component=component).values.squeeze() - value_t = waveform_ims_transposed.sel(component=component).values.squeeze() - assert value == pytest.approx(value_t) + assert isinstance(waveform_ims, xr.Dataset) + assert isinstance(waveform_ims_transposed, xr.Dataset) + for component in ["rotd0", "rotd50", "rotd100"]: + value = waveform_ims[component].values.squeeze() + value_t = waveform_ims_transposed[component].values.squeeze() + assert value == pytest.approx(value_t) # Asserts that 090, 000, and ver components are computed for the corresponding COMP_* enum values. @@ -640,17 +660,296 @@ def test_rotational_invariance( ) @settings(deadline=None) def test_component_orientation(waveform: npt.NDArray[np.float64]) -> None: - waveform_ims = ims.peak_ground_acceleration(waveform, cores=1) + waveform_ims = ims.peak_ground_acceleration(waveform) assert_array_almost_equal( - waveform_ims["000"].values, # ty: ignore[invalid-argument-type] + waveform_ims["000"].values, np.abs(waveform[ims.Component.COMP_0]).max(axis=1), ) assert_array_almost_equal( - waveform_ims["090"].values, # ty: ignore[invalid-argument-type] + waveform_ims["090"].values, np.abs(waveform[ims.Component.COMP_90]).max(axis=1), ) assert_array_almost_equal( - waveform_ims["ver"].values, # ty: ignore[invalid-argument-type] + waveform_ims["ver"].values, np.abs(waveform[ims.Component.COMP_VER]).max(axis=1), ) + + +def test_component_orientation_with_named_components( + sample_waveforms: npt.NDArray[np.float64], +) -> None: + """Component mapping is positional: index 0/1/2 -> 000/090/ver, regardless + of how the input DataArray's `component` coordinate is labelled.""" + waveform = xr.DataArray( + sample_waveforms, + dims=("component", "station", "time"), + coords={"component": ["x", "y", "z"]}, + ) + result = ims.peak_ground_acceleration(waveform) + assert_array_almost_equal( + result["000"].values, np.abs(sample_waveforms[0]).max(axis=-1) + ) + assert_array_almost_equal( + result["090"].values, np.abs(sample_waveforms[1]).max(axis=-1) + ) + assert_array_almost_equal( + result["ver"].values, np.abs(sample_waveforms[2]).max(axis=-1) + ) + + +# Lazy (dask-backed) input must produce a lazy Dataset whose computed values +# are bit-identical to the eager result -- station chunking never mixes rows, +# so nothing about laziness should change the numbers. +LAZY_CASES = [ + pytest.param(ims.peak_ground_acceleration, {}, id="pga"), + pytest.param(ims.peak_ground_velocity, {"dt": 0.01}, id="pgv"), + pytest.param(ims.peak_ground_displacement, {"dt": 0.01}, id="pgd"), + pytest.param(ims.cumulative_absolute_velocity, {"dt": 0.01}, id="cav"), + pytest.param( + ims.cumulative_absolute_velocity, {"dt": 0.01, "threshold": 5}, id="cav5" + ), + pytest.param(ims.arias_intensity, {"dt": 0.01}, id="ai"), + pytest.param(ims.ds575, {"dt": 0.01}, id="ds575"), +] + + +@pytest.mark.parametrize("func,kwargs", LAZY_CASES) +def test_lazy_matches_eager( + sample_waveforms: npt.NDArray[np.float64], + func: Callable[..., xr.Dataset], + kwargs: dict, +) -> None: + lazy_input = _to_dask(sample_waveforms, station_chunk=1) + eager = func(sample_waveforms, **kwargs) + lazy = func(lazy_input, **kwargs) + + assert all(v.chunks is not None for v in lazy.data_vars.values()) + assert "units" not in lazy.attrs # keep_attrs=False: input attrs must not leak + + computed = lazy.compute() + for component in eager.data_vars: + assert_array_equal(eager[component].values, computed[component].values) + + +def test_lazy_matches_eager_psa(sample_waveforms: npt.NDArray[np.float64]) -> None: + periods = np.array([0.1, 0.5, 1.0]) + lazy_input = _to_dask(sample_waveforms, station_chunk=1) + eager = ims.pseudo_spectral_acceleration(sample_waveforms, periods, 0.01) + lazy = ims.pseudo_spectral_acceleration(lazy_input, periods, 0.01) + + assert all(v.chunks is not None for v in lazy.data_vars.values()) + computed = lazy.compute() + for component in eager.data_vars: + assert_array_equal(eager[component].values, computed[component].values) + + +def test_psa_full_rotd180(sample_waveforms: npt.NDArray[np.float64]) -> None: + """The full 180-angle curve must be internally consistent with the + summary statistics computed from the same solve.""" + periods = np.array([0.1, 0.5, 1.0]) + dt = 0.01 + + without = ims.pseudo_spectral_acceleration(sample_waveforms, periods, dt) + with_curve = ims.pseudo_spectral_acceleration( + sample_waveforms, periods, dt, full_rotd180=True + ) + + assert "rotd180" not in without.data_vars + assert set(with_curve.data_vars) == set(without.data_vars) | {"rotd180"} + assert with_curve["rotd180"].dims == ("station", "period", "angle") + assert with_curve["rotd180"].shape == ( + sample_waveforms.shape[1], + len(periods), + 180, + ) + assert_array_equal(with_curve.angle.values, np.arange(180)) + + # Angle 0 is exact (cos(0) == 1.0 exactly), so it must equal 000 exactly. + assert_array_equal(with_curve["rotd180"].isel(angle=0).values, with_curve["000"].values) + + # The other summary components must be unaffected by asking for the curve. + for component in without.data_vars: + assert_array_equal(without[component].values, with_curve[component].values) + + # rotd0/50/100 must be exactly the min/median/max over the angle axis. + curve = with_curve["rotd180"].values + sorted_curve = np.sort(curve, axis=-1) + assert_array_equal(sorted_curve[..., 0], with_curve["rotd0"].values) + assert_array_equal( + (sorted_curve[..., 89] + sorted_curve[..., 90]) / 2, + with_curve["rotd50"].values, + ) + assert_array_equal(sorted_curve[..., 179], with_curve["rotd100"].values) + + # And each orientation must be the angle of its own statistic in that same + # curve: the argmin and argmax for rotd0/rotd100, and the lower of the two + # central angles for rotd50, whose peak sits just below the reported + # median. + assert_array_equal(curve.argmin(axis=-1), with_curve["rotd0_orientation"].values) + assert_array_equal(curve.argmax(axis=-1), with_curve["rotd100_orientation"].values) + at_median = np.take_along_axis( + curve, + with_curve["rotd50_orientation"].values.astype(int)[..., np.newaxis], + axis=-1, + ).squeeze(-1) + assert_array_equal(at_median, sorted_curve[..., 89]) + + +def test_rotd_orientations_match_a_direct_angle_sweep( + sample_waveforms: npt.NDArray[np.float64], +) -> None: + """Each orientation must name the angle its statistic came from, against a + plain numpy sweep of the two horizontal components.""" + result = ims.peak_ground_acceleration(sample_waveforms) + comp_0 = sample_waveforms[ims.Component.COMP_0] + comp_90 = sample_waveforms[ims.Component.COMP_90] + + angles = np.deg2rad(np.arange(180)) + # (n_stations, 180): the peak rotated amplitude at every integer angle. + sweep = np.abs( + np.cos(angles)[np.newaxis, :, np.newaxis] * comp_0[:, np.newaxis, :] + + np.sin(angles)[np.newaxis, :, np.newaxis] * comp_90[:, np.newaxis, :] + ).max(axis=-1) + + assert_array_equal(sweep.argmin(axis=-1), result["rotd0_orientation"].values) + assert_array_equal(sweep.argmax(axis=-1), result["rotd100_orientation"].values) + assert_array_equal(sweep.min(axis=-1), result["rotd0"].values) + assert_array_equal(sweep.max(axis=-1), result["rotd100"].values) + + sorted_sweep = np.sort(sweep, axis=-1) + at_median = np.take_along_axis( + sweep, + result["rotd50_orientation"].values.astype(int)[..., np.newaxis], + axis=-1, + ).squeeze(-1) + assert_array_equal(at_median, sorted_sweep[..., 89]) + assert_array_equal( + (sorted_sweep[..., 89] + sorted_sweep[..., 90]) / 2, result["rotd50"].values + ) + + +@pytest.mark.parametrize("polarisation", [0, 30, 45, 100, 179]) +def test_rotd_orientation_of_a_polarised_record(polarisation: int) -> None: + """A linearly polarised record fixes the orientations exactly: it peaks + along its own direction and vanishes across it, which pins the angle + convention (degrees, anticlockwise from the 000 component).""" + time = np.arange(0, 1, 0.005) + motion = np.sin(2 * np.pi * 5 * time) * np.exp(-2 * time) + direction = np.deg2rad(polarisation) + waveform = np.stack( + [ + (motion * np.cos(direction))[np.newaxis], + (motion * np.sin(direction))[np.newaxis], + np.zeros((1, len(time))), + ] + ) + + result = ims.peak_ground_acceleration(waveform) + assert result["rotd100_orientation"].values == pytest.approx(polarisation) + assert result["rotd0_orientation"].values == pytest.approx( + (polarisation + 90) % 180 + ) + # Across the direction of motion there is nothing to see, and the sqrt(2) + # bound on RotD100 / RotD50 is attained. + assert result["rotd0"].values == pytest.approx(0, abs=1e-12) + assert result["rotd100"].values / result["rotd50"].values == pytest.approx( + np.sqrt(2), rel=1e-9 + ) + + +def test_psa_full_rotd180_does_not_duplicate_the_solve( + sample_waveforms: npt.NDArray[np.float64], monkeypatch: pytest.MonkeyPatch +) -> None: + """Requesting the full curve must reuse the same per-period solve as the + summary statistics, not run it a second time.""" + periods = np.array([0.1, 0.5, 1.0]) + dt = 0.01 + calls = [] + original = ims._core._psa_rotd180 + monkeypatch.setattr( + ims._core, + "_psa_rotd180", + lambda *args, **kwargs: (calls.append(1), original(*args, **kwargs))[1], + ) + + ims.pseudo_spectral_acceleration(sample_waveforms, periods, dt, full_rotd180=False) + n_without = len(calls) + calls.clear() + ims.pseudo_spectral_acceleration(sample_waveforms, periods, dt, full_rotd180=True) + n_with = len(calls) + + assert n_without == len(periods) + assert n_with == len(periods) + + +def test_lazy_matches_eager_psa_full_rotd180( + sample_waveforms: npt.NDArray[np.float64], +) -> None: + periods = np.array([0.1, 0.5, 1.0]) + lazy_input = _to_dask(sample_waveforms, station_chunk=1) + eager = ims.pseudo_spectral_acceleration( + sample_waveforms, periods, 0.01, full_rotd180=True + ) + lazy = ims.pseudo_spectral_acceleration( + lazy_input, periods, 0.01, full_rotd180=True + ) + + assert all(v.chunks is not None for v in lazy.data_vars.values()) + computed = lazy.compute() + for component in eager.data_vars: + assert_array_equal(eager[component].values, computed[component].values) + + +def test_lazy_matches_eager_fas( + sample_waveforms: npt.NDArray[np.float64], ko_matrices: Path +) -> None: + freqs = np.logspace(-1, 1, 16, dtype=np.float64) + lazy_input = _to_dask(sample_waveforms, station_chunk=1) + eager = ims.fourier_amplitude_spectra(sample_waveforms, 0.01, freqs, ko_matrices) + lazy = ims.fourier_amplitude_spectra(lazy_input, 0.01, freqs, ko_matrices) + + assert all(v.chunks is not None for v in lazy.data_vars.values()) + computed = lazy.compute() + # BLAS may re-block the Konno matmul differently per station chunk, so + # allow a little slack rather than requiring bit-identical results. + for component in eager.data_vars: + assert_array_almost_equal( + eager[component].values, computed[component].values, decimal=10 + ) + + +def test_lazy_preserves_station_coord_and_extra_coords( + sample_waveforms: npt.NDArray[np.float64], +) -> None: + n_stations = sample_waveforms.shape[1] + waveform = xr.DataArray( + da.from_array(sample_waveforms, chunks=(3, 1, sample_waveforms.shape[2])), + dims=("component", "station", "time"), + coords={ + "station": [f"stat_{i}" for i in range(n_stations)], + "latitude": ("station", np.arange(n_stations, dtype=np.float64)), + "longitude": ("station", -np.arange(n_stations, dtype=np.float64)), + }, + ) + result = ims.peak_ground_acceleration(waveform) + assert_array_equal(result.station.values, waveform.station.values) + assert_array_equal(result.latitude.values, waveform.latitude.values) + assert_array_equal(result.longitude.values, waveform.longitude.values) + + +def test_rechunks_component_and_time_core_dims( + sample_waveforms: npt.NDArray[np.float64], +) -> None: + """A waveform chunked across `component`/`time` (as a real broadband file + opened with `chunks={}` might be) must still work: `_as_waveform` forces + those two dims back to a single chunk before `apply_ufunc` sees them.""" + waveform = xr.DataArray( + da.from_array(sample_waveforms, chunks=(1, 1, 5)), + dims=("component", "station", "time"), + ) + result = ims.peak_ground_acceleration(waveform) + computed = result.compute() + expected = ims.peak_ground_acceleration(sample_waveforms) + for component in expected.data_vars: + assert_array_equal(expected[component].values, computed[component].values) diff --git a/uv.lock b/uv.lock index 859a2155..f57f6b73 100644 --- a/uv.lock +++ b/uv.lock @@ -46,11 +46,11 @@ wheels = [ [[package]] name = "annotated-doc" -version = "0.0.4" +version = "0.0.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, ] [[package]] @@ -291,6 +291,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl", hash = "sha256:a43e394b528d52112af599f2fc9e4b7cf3c15f94e53581f74fa6867e68c91756", size = 4273, upload-time = "2022-03-13T11:10:17.594Z" }, ] +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -368,71 +377,71 @@ wheels = [ [[package]] name = "coverage" -version = "7.15.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/d0/55fe630f4cf94e3fcba868240fad8c8cdd1f764e2a932f8926347e6ec4cd/coverage-7.15.2.tar.gz", hash = "sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d", size = 927741, upload-time = "2026-07-15T18:56:19.558Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/50/eb5bf42e531611a9f8d272556b1ed4de503f84a91413584094487cf69f8f/coverage-7.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1adac78e5abc7c5438f7a209c9ca69d06542f0bf481d728b6989ea80b813fdf9", size = 221587, upload-time = "2026-07-15T18:54:18.439Z" }, - { url = "https://files.pythonhosted.org/packages/06/d1/da99af464c335d4e023a6efcd7ec30f63b88a43c93745154ab74ffb31cea/coverage-7.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b868acc62aa5de3be7a9d05c2333bf8359ca987e43f9cb30ff8fbda6a024ab73", size = 221943, upload-time = "2026-07-15T18:54:20.062Z" }, - { url = "https://files.pythonhosted.org/packages/5b/8a/13c42723d61ca447eafa18732e8141dd6a63f2732e1c7e1502c182dd88d7/coverage-7.15.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6f6966fc30e6f06ca8f98fb0ce51eda6b111b3ee8d066a8b1ec9e77fa06ab55d", size = 253450, upload-time = "2026-07-15T18:54:21.765Z" }, - { url = "https://files.pythonhosted.org/packages/d7/29/99021303f98fbdcb63504b4d07bea4cc025b9b2dd907c4f07c85d50a0dab/coverage-7.15.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:68af907f595ab01a78f794932ff3bdf929c316d3000810d38dbc247129e26f8b", size = 256187, upload-time = "2026-07-15T18:54:23.4Z" }, - { url = "https://files.pythonhosted.org/packages/f9/a8/fd503715ed6ca9c5d742923aa5209257340b367a867b2ced0c7d4ba8a0b9/coverage-7.15.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afa29e2eff3d5729267e2cb2fd4ce9d61c952932fb2694e34ccb5d9540c6a296", size = 257301, upload-time = "2026-07-15T18:54:25.183Z" }, - { url = "https://files.pythonhosted.org/packages/da/40/3f4b8fb409810036ebc2857d36adc0498c6e957b5df0290c5036b2e143f1/coverage-7.15.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bbf44513ceb1589e31948e20eafbde9deaface90e1a1afa5f5f77b4423d17ce6", size = 259562, upload-time = "2026-07-15T18:54:27.204Z" }, - { url = "https://files.pythonhosted.org/packages/0b/8a/9bdffbef47db77cce3d6b02a28f7e919b19f0106c4b080c2c2246040f885/coverage-7.15.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9deddf09eecb717b7f980414b43d90a5b22ff3967d2949ab29cb0aa83d9e9098", size = 253841, upload-time = "2026-07-15T18:54:29.134Z" }, - { url = "https://files.pythonhosted.org/packages/1b/1e/9031efde019d31a06646261fce6dfc5c3c74e951e27a71e5c9a424563178/coverage-7.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae901f7e55ba405c84ee1cab3d3e962e4e871e4a2bcb9c90911adbd69b42ac5a", size = 255221, upload-time = "2026-07-15T18:54:31.142Z" }, - { url = "https://files.pythonhosted.org/packages/56/db/787acde872389fc84a9ef9d8cd1ccc658e391ab4cb5b28092a714426a394/coverage-7.15.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a0f47002c6eeb7c280228467a4cb0cc15ca2103a8421b986b2d3ec04a0f9bd8b", size = 253366, upload-time = "2026-07-15T18:54:32.886Z" }, - { url = "https://files.pythonhosted.org/packages/2f/9b/6f57bc4b93c842eef1695f8cdaf2318e35e7ba54f5ba80d84be213ab7858/coverage-7.15.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd7a5beb7af3e864a13b1f0fb26efd3695da43ef0daf71e586adfffaf34d5b2", size = 257434, upload-time = "2026-07-15T18:54:34.7Z" }, - { url = "https://files.pythonhosted.org/packages/88/26/b3186a21b2acc83e451118978905c81c7072c3333707804db09a78c096a2/coverage-7.15.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:97a5c5457a9fb1d6c4e06cfb5dc835871fbfb6a6a51addc9e925bdeff5ef7440", size = 252935, upload-time = "2026-07-15T18:54:36.548Z" }, - { url = "https://files.pythonhosted.org/packages/20/c2/c9f3376b2e717ea69ed7a6e9a5fcab968fb0b290db6cf4bd9a1fc7541b75/coverage-7.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0901cfe6c13bcd2302da4f83e884555d2a22bda6e4c476f09ef204ba20ca536e", size = 254807, upload-time = "2026-07-15T18:54:38.296Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e1/dfc15401f4a8aaeb486e1ba3e9e3c40522a6e38bd0ecf0b3f29cb8082957/coverage-7.15.2-cp312-cp312-win32.whl", hash = "sha256:b171bdd71cb7ff792bf32e376173b0ace7e7963e7e57c58dfc42063a6a7174cd", size = 223641, upload-time = "2026-07-15T18:54:40.103Z" }, - { url = "https://files.pythonhosted.org/packages/91/40/81b6d809d320cd366ec5bdf8176575e897dcb8efe7fb4b489ef9e93e4d13/coverage-7.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:582edc45c2040543fef83341be23c43024a3ab3ae0c2d8bc498a06282905ad40", size = 224172, upload-time = "2026-07-15T18:54:41.882Z" }, - { url = "https://files.pythonhosted.org/packages/ef/28/9f14ec438149f7de557f45518f09b4a7917b795cc37083aa7db482693f8c/coverage-7.15.2-cp312-cp312-win_arm64.whl", hash = "sha256:a638db90c61cd219aeee65e83a24fdaa57269a741ae0cf773309208ac862cee3", size = 223556, upload-time = "2026-07-15T18:54:43.674Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d5/f8c838e6b7282976f7c918884b792df7a0c42c5bba5d99c60ad2d221d56d/coverage-7.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1121caa19159a38b5463eaae4b1e1fde81e525b15ecc5e000cd5b1a108f743a8", size = 221606, upload-time = "2026-07-15T18:54:45.448Z" }, - { url = "https://files.pythonhosted.org/packages/bf/37/97c926376364f66298cc44893b89cdf17b8bc406376497c4061ae4b8a8ff/coverage-7.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a300c6934e0989c327b9e8a1e110329da4641149f872bbe9f70168be66da76c1", size = 221982, upload-time = "2026-07-15T18:54:47.341Z" }, - { url = "https://files.pythonhosted.org/packages/b7/30/a36050a6e83c2135ee0776f452ca3948224befc6d7f26acecc082d0c106a/coverage-7.15.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2617f8799d268fabdeef42a7e89ac3a23e1deee9025427db2df970f99a89a578", size = 252972, upload-time = "2026-07-15T18:54:49.2Z" }, - { url = "https://files.pythonhosted.org/packages/31/d3/06b5f1daf95f0f15ab05bd75f26ba5f3c8b33d0bb72f3aaa3cf41d1bad3a/coverage-7.15.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7dc2950a2992cd676d35c20ae63522836deeb034f08874699d14068710af3dc1", size = 255569, upload-time = "2026-07-15T18:54:51.098Z" }, - { url = "https://files.pythonhosted.org/packages/81/1c/9afb3f8de2b8d36960391c48559a2e3ff96594b58099f115921549ea8d0d/coverage-7.15.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e36686f7a442185db2400b3df171aac520869faf9deb59df687d28659eda2a6", size = 256806, upload-time = "2026-07-15T18:54:53.145Z" }, - { url = "https://files.pythonhosted.org/packages/64/d8/b989f96061a5e32d82fddd1b1b9ff48a7c8f8ae7606f0e80fd9de54b1e33/coverage-7.15.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d29ca7bd67af6e12e74632d65f026eabc1364da5c254494cd914446a28a3ef7", size = 258936, upload-time = "2026-07-15T18:54:55.015Z" }, - { url = "https://files.pythonhosted.org/packages/b8/fa/f99771f5110457c7b511c1935ca49ddf288218eaa84322e028b9334146ae/coverage-7.15.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:db9c8438057e5b0f6a22a0af99c0c1d26b57fbbdbd1be5861ddb8f897fcc3a2d", size = 253178, upload-time = "2026-07-15T18:54:57.527Z" }, - { url = "https://files.pythonhosted.org/packages/f6/96/c098a6044d119c751ceede7be91035fa8310170ec24a6523aff72f0a5793/coverage-7.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:63022c4c8dec1d0342f05c3ede99842fe3d007689acc45e86f123a1746e4a026", size = 254934, upload-time = "2026-07-15T18:54:59.41Z" }, - { url = "https://files.pythonhosted.org/packages/b2/a2/1457b3a7a50c8d77500103b97a046db863e2f59a1cf6d2f814595f349885/coverage-7.15.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6c0be82b4d4aa5b2704e08518e2252f3e3d110164bcca826816801052e48a7aa", size = 252898, upload-time = "2026-07-15T18:55:01.338Z" }, - { url = "https://files.pythonhosted.org/packages/6c/0e/76958874c471ecfcdde0d2b2747bb2c61bdbf34a40636f4ce9db9923e643/coverage-7.15.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4510fb9cdf6bb02dfa6af0be4a534b8102d086e22e4a33f8836df663da3d660d", size = 257056, upload-time = "2026-07-15T18:55:03.243Z" }, - { url = "https://files.pythonhosted.org/packages/7c/7c/3d7c4e3bf58baa40327dc7edc2272b17cf02299366d52763db1b0ca1556a/coverage-7.15.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:42ec3d989421b174a2ab607c1539f24127ad362757b7f1c0c0d7a2993f7eb37b", size = 252718, upload-time = "2026-07-15T18:55:05.029Z" }, - { url = "https://files.pythonhosted.org/packages/c8/b8/1cecffed9ce14fb25be9ba42d37b6bb61485c9a3ddd43cd3dde36b6087d8/coverage-7.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8f91bce78e32343af184c3b7fa28fcf5a9e2641f4b6623d392038f804939188", size = 254490, upload-time = "2026-07-15T18:55:06.889Z" }, - { url = "https://files.pythonhosted.org/packages/6c/2c/42984561bc7f4c045dca67516a0c50ee5ef8d84352dbeb5559dc86c4823e/coverage-7.15.2-cp313-cp313-win32.whl", hash = "sha256:434e68d531858205895eb0d74b73d20b84260de426387d53c422a5acda2cf050", size = 223647, upload-time = "2026-07-15T18:55:08.941Z" }, - { url = "https://files.pythonhosted.org/packages/41/9f/39c7c9245efc583beddf89a87683574e663ed93637f3afb6cd7b88405676/coverage-7.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:26c3b04a6377fd7c09800921fa934e3a17c0020439cd59df73e73ae1d4b6a78c", size = 224190, upload-time = "2026-07-15T18:55:10.789Z" }, - { url = "https://files.pythonhosted.org/packages/c7/de/3a2883cf8a213659280ef4b403059e17a9acaeb7fc7fd4105e1226ff2e6d/coverage-7.15.2-cp313-cp313-win_arm64.whl", hash = "sha256:3ed010aa1b69cda8e827aabfca9866216c980e2dca82ab9a78c5f83689964c8b", size = 223583, upload-time = "2026-07-15T18:55:12.678Z" }, - { url = "https://files.pythonhosted.org/packages/81/5f/aed265fd7a3551a394f36dfe41868aee709b7f95db4052205b4ad1563ac3/coverage-7.15.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a", size = 221650, upload-time = "2026-07-15T18:55:14.527Z" }, - { url = "https://files.pythonhosted.org/packages/6b/2c/222ba12a545189017120f8eddfc1a0bd4616b47d5d4a8d99421edb2fe4c6/coverage-7.15.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2", size = 221988, upload-time = "2026-07-15T18:55:16.674Z" }, - { url = "https://files.pythonhosted.org/packages/aa/38/304b5877ab46e6c290b4292cfcf3fe28245f0e5597cad7f6acc91fc7e0a4/coverage-7.15.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138", size = 253029, upload-time = "2026-07-15T18:55:18.856Z" }, - { url = "https://files.pythonhosted.org/packages/6c/58/821b533b8db9e44cf1d8a97bd525149ced40dde1d0093da02cb78e715244/coverage-7.15.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f", size = 255536, upload-time = "2026-07-15T18:55:21.027Z" }, - { url = "https://files.pythonhosted.org/packages/f1/f2/7aa06604c389d32ea7f0a6a988359a7eafc3cd3f8e7bc2e88cd2fdf0b877/coverage-7.15.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984", size = 256881, upload-time = "2026-07-15T18:55:23.125Z" }, - { url = "https://files.pythonhosted.org/packages/a2/4f/1ef342339c7916d0096bc5888cc0f653882cc7bc8f897d5cb89143287c9b/coverage-7.15.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7", size = 259196, upload-time = "2026-07-15T18:55:25.099Z" }, - { url = "https://files.pythonhosted.org/packages/fe/f4/7ed055d7a9c5ec13b161773a115a5ccc6b0081d568c31fad830806306cc7/coverage-7.15.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3", size = 253036, upload-time = "2026-07-15T18:55:27.018Z" }, - { url = "https://files.pythonhosted.org/packages/14/79/ea82cca18c242a3a38b6c017da39726aa62dcb64aa635abf79b92009975c/coverage-7.15.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee", size = 254887, upload-time = "2026-07-15T18:55:29.084Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ba/a136db3c0d9562b00e10b72540dbf3a33cd3bc5b95060c9308e247494623/coverage-7.15.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1", size = 252852, upload-time = "2026-07-15T18:55:31.184Z" }, - { url = "https://files.pythonhosted.org/packages/17/17/ea334246b16b7d059953fad6fdefa11e33c68efbd3fe37b1098120a1fac2/coverage-7.15.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a", size = 257128, upload-time = "2026-07-15T18:55:33.163Z" }, - { url = "https://files.pythonhosted.org/packages/ed/c3/074fb66d46d607855f710876b117cbda562c5ab08363528e78820449f937/coverage-7.15.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145", size = 252668, upload-time = "2026-07-15T18:55:35.063Z" }, - { url = "https://files.pythonhosted.org/packages/e1/c1/f620850ada9b36435921c9a3a8057013422b1d964eb4bf37fe138724d192/coverage-7.15.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446", size = 254325, upload-time = "2026-07-15T18:55:37.125Z" }, - { url = "https://files.pythonhosted.org/packages/cc/31/a729ca3689404493af82ef8e6ff70bd88bdda8da89aeef6ca9b387aeb2b4/coverage-7.15.2-cp314-cp314-win32.whl", hash = "sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243", size = 223844, upload-time = "2026-07-15T18:55:39.078Z" }, - { url = "https://files.pythonhosted.org/packages/c6/83/5d809dc808fb1698c671f3e372259bb9158e64b7ea526fc6ab7de64de9fe/coverage-7.15.2-cp314-cp314-win_amd64.whl", hash = "sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc", size = 224331, upload-time = "2026-07-15T18:55:41.346Z" }, - { url = "https://files.pythonhosted.org/packages/16/4e/35e488548e952795829e129995c4174df33bf432b591d1aa42c8d9e4e7ad/coverage-7.15.2-cp314-cp314-win_arm64.whl", hash = "sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635", size = 223760, upload-time = "2026-07-15T18:55:43.518Z" }, - { url = "https://files.pythonhosted.org/packages/ed/49/dd2c86cd6374038f6e415fb5bfb86db5218553209c081384a020369dee79/coverage-7.15.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be", size = 222384, upload-time = "2026-07-15T18:55:45.569Z" }, - { url = "https://files.pythonhosted.org/packages/d3/74/173ff17a1c0808e5a438f549f6f145d5ac7528f2791310b63523e3200ac7/coverage-7.15.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072", size = 222647, upload-time = "2026-07-15T18:55:47.544Z" }, - { url = "https://files.pythonhosted.org/packages/84/f8/b8cba872162356fb44ac79c10309d987206a4461e32072fc29228dad7331/coverage-7.15.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328", size = 264013, upload-time = "2026-07-15T18:55:49.768Z" }, - { url = "https://files.pythonhosted.org/packages/ee/67/a807a7586d0b8cae485308ddd55756f0806c92f8e0b411bacbf23c48edf3/coverage-7.15.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a", size = 266135, upload-time = "2026-07-15T18:55:51.941Z" }, - { url = "https://files.pythonhosted.org/packages/ce/67/cd78771dc985f7e4ebdcc82b1a96d9a932af9e806f01f2f91a89f4c72e80/coverage-7.15.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0", size = 268555, upload-time = "2026-07-15T18:55:54.065Z" }, - { url = "https://files.pythonhosted.org/packages/18/3e/10134cf81275188c58568f324fc74aedff32c63ca4d5bbc513a91944a6f0/coverage-7.15.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5", size = 269674, upload-time = "2026-07-15T18:55:56.066Z" }, - { url = "https://files.pythonhosted.org/packages/75/4a/771b77de446cba985dc414bbc5844bd21604da05dbc044286df8318a48a7/coverage-7.15.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743", size = 263101, upload-time = "2026-07-15T18:55:58.107Z" }, - { url = "https://files.pythonhosted.org/packages/5f/b5/70a7011da15f4071943361183aefa27847f3e3aec4fd335f1cb3d3a622b1/coverage-7.15.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c", size = 266007, upload-time = "2026-07-15T18:56:00.468Z" }, - { url = "https://files.pythonhosted.org/packages/b4/0d/f9547e804ce7ad49646ffeffac26699510efbe6c0f751b66fdc960c4e825/coverage-7.15.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071", size = 263611, upload-time = "2026-07-15T18:56:02.615Z" }, - { url = "https://files.pythonhosted.org/packages/ac/59/f576a396659c0efd351f5c1544f67c3560e89c7761cabf7f65e412beeda5/coverage-7.15.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a", size = 267344, upload-time = "2026-07-15T18:56:04.622Z" }, - { url = "https://files.pythonhosted.org/packages/7c/5d/c2e4fce3579c0cb635024293f1a32bbe26df101b3e3a69f22243d1352b6c/coverage-7.15.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d", size = 262456, upload-time = "2026-07-15T18:56:06.641Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/956287d69436b66094bc4b57ac2da71e43bfd2a5524e958900b9f582fcf8/coverage-7.15.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c", size = 264771, upload-time = "2026-07-15T18:56:08.795Z" }, - { url = "https://files.pythonhosted.org/packages/2c/5a/6f979530c2734c575de77cf58f5f28d51f7123a94b5030fd9156fe5f363c/coverage-7.15.2-cp314-cp314t-win32.whl", hash = "sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688", size = 224151, upload-time = "2026-07-15T18:56:10.856Z" }, - { url = "https://files.pythonhosted.org/packages/54/7e/27f6b2a74d484742f4017553e710b01e396b23d809df3e95ca0bb9a2824b/coverage-7.15.2-cp314-cp314t-win_amd64.whl", hash = "sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199", size = 224981, upload-time = "2026-07-15T18:56:12.928Z" }, - { url = "https://files.pythonhosted.org/packages/b1/48/284863423aa474240f6842bd00d680da22f4e6ea2e466618ef7c9c9e69a9/coverage-7.15.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658", size = 224294, upload-time = "2026-07-15T18:56:15.156Z" }, - { url = "https://files.pythonhosted.org/packages/ec/82/32e3bd191d498e64f6f911ad55d14006a0861e54869d2d32452326399e65/coverage-7.15.2-py3-none-any.whl", hash = "sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c", size = 213375, upload-time = "2026-07-15T18:56:17.305Z" }, +version = "7.15.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/45/78dbf9604ee5b3db24efbf26bed1cb58862fb40480cba821963c69348751/coverage-7.15.3.tar.gz", hash = "sha256:ae7ea5a4614acf399ef0483c4cb34f8f8f01df848d8fcbe7d3ce0865733f1c4d", size = 935592, upload-time = "2026-08-02T18:50:17.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/6c/bac99d9d4c6abe856e93bf3f5212982ac0bfac126dd4a042753bd53bc5af/coverage-7.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:79a3e32e83227d83d9684459ed579769b56c369ac2d7313099b2d9e031d2e10f", size = 222499, upload-time = "2026-08-02T18:48:15.018Z" }, + { url = "https://files.pythonhosted.org/packages/aa/bc/cb9a39b083bc1aa70586482dab25c9be20bab0ec6c155340e50d9066bb1e/coverage-7.15.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:767feb87c5886d781d0a69fafd450a20826ddab7b79bce1665deb64d21441b60", size = 222866, upload-time = "2026-08-02T18:48:16.884Z" }, + { url = "https://files.pythonhosted.org/packages/58/fb/beaa453d62000a0a5b39838bee2a137afe609a50a71f55e83c73461e513b/coverage-7.15.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50951e37033c40548d777b8a8454a2cd622dba1136780065678dccaec307c47f", size = 254367, upload-time = "2026-08-02T18:48:18.507Z" }, + { url = "https://files.pythonhosted.org/packages/66/64/43e72500ed6815cef189f9193f29d7af4b078830337c95ea976cd0c0d427/coverage-7.15.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:63a4ff67364afb2cac826b8bbd78a5c50ce656a7b7137436b44d7b96a9271088", size = 257103, upload-time = "2026-08-02T18:48:20.172Z" }, + { url = "https://files.pythonhosted.org/packages/66/3a/2893e2937adfe02f45fd38e4a8a0a0d8b7a02ff9e012ac3d009bee3c4f16/coverage-7.15.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e95e42856509675fe26560310313a6117640e96f9a1e19bb3d220116a27c94c", size = 258220, upload-time = "2026-08-02T18:48:21.963Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/d5e6e2eb1a62961083734291304b1f85df72e2abe95c76eb88a7f472afd0/coverage-7.15.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:abad631cba27094b4631993f4c72e89ac0ca1b3a0236c7abaf8ca79aea619851", size = 260481, upload-time = "2026-08-02T18:48:23.682Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c9/9b72c5c6a9798a9a12cf65f66e077cc1fdd396e61915c862688f9afe1cae/coverage-7.15.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b0807f1f051dd82a234ad6acdb6f1425baede60be1e84e862496c8cc9262ab9", size = 254749, upload-time = "2026-08-02T18:48:25.32Z" }, + { url = "https://files.pythonhosted.org/packages/92/20/e1c2f759e2dbce559ba85c40c0e4acfecc6cff4b740c294c88e41ccc6111/coverage-7.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8d6df7aeb5bc464040bbc9ae173d875785d3677ebc4307817997d622d74225e", size = 256138, upload-time = "2026-08-02T18:48:27.064Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ab/48cc7e760f769e86ae290a125ea6e7209dfbdbbbb7ff4f5d9d1ee7a45d57/coverage-7.15.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:974471c506c9f5758808b47c1ebf7949ecd0848f5c1020e78675fefe5ff46866", size = 254283, upload-time = "2026-08-02T18:48:29.082Z" }, + { url = "https://files.pythonhosted.org/packages/15/26/39529a68154f99b3a1829debd8b25eac384effeec890a293b5bbdcb49186/coverage-7.15.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5cba0c9c13e35c86df7998f1afaf6b1da224a3a39e4da59bdabf60c148046dcb", size = 258352, upload-time = "2026-08-02T18:48:30.892Z" }, + { url = "https://files.pythonhosted.org/packages/91/2f/55b82aa3d8d7dd8023a56e7c5c2a70e39a3c44b3353c6cf3faec9ad51566/coverage-7.15.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4d608dc36a364dce33acbf4fc3a50f9d2054c945f233bb0a2cdb4b90bfa17646", size = 253852, upload-time = "2026-08-02T18:48:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6d/839f4045124cd3518ecf2c58967e58a911202834e7c5a03cfdf2ab0b29f6/coverage-7.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2395869280554a1941da904423c12660c39f721315e1c02d076a7fe0971382f0", size = 255725, upload-time = "2026-08-02T18:48:34.848Z" }, + { url = "https://files.pythonhosted.org/packages/75/21/d25e3e2a9e327798078c877f469dfb6def860bf6e25036529046227d3e15/coverage-7.15.3-cp312-cp312-win32.whl", hash = "sha256:24f3b21840c3eb76cef3cc70b2bf6649010c64471a84a446538a39306e1ba04d", size = 224566, upload-time = "2026-08-02T18:48:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0f/df90cc1e8d095ce263968a93e04829821b2afb31ac2752c06a2e0a8e3c13/coverage-7.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:fa7b17902c3c1dd8a7adb52679b7f6340bba08443d710c8838e04db8cf62be2a", size = 225098, upload-time = "2026-08-02T18:48:38.941Z" }, + { url = "https://files.pythonhosted.org/packages/65/c7/ec49e43c58967a07163e2d1c6bbd58112b825b2772ab66784afd6a5400ba/coverage-7.15.3-cp312-cp312-win_arm64.whl", hash = "sha256:fcbe83fb7258eacd293bf5322d88807acb35ed12a5cfa99dd8215c083e3b0235", size = 224485, upload-time = "2026-08-02T18:48:40.682Z" }, + { url = "https://files.pythonhosted.org/packages/68/6e/62ae61e1fc434956bec38ed1d5b1c494f58cf579dbd998e77abffe7b3e6b/coverage-7.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1182eed05674c63d40951fae27c43e822749f04d25f75df64c2e4fa3168678de", size = 222522, upload-time = "2026-08-02T18:48:42.476Z" }, + { url = "https://files.pythonhosted.org/packages/13/ff/c74c673d81e0e77b6608c3d21331e3db42e30daeb3c8a0a8860d4c9e2e14/coverage-7.15.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c0c4b0d7c4cd56e470d0c9d8441f42e8a96cdfd95050fec027f1d4dd9f11006c", size = 222894, upload-time = "2026-08-02T18:48:44.274Z" }, + { url = "https://files.pythonhosted.org/packages/a1/91/ccb30f5ffafd7d69d0b18e5162f9b711a5654e807b7b0c13497f0826b33f/coverage-7.15.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5c9fce9f4998b0d50a753da765b9215a14decc7863822c89d72da7a89ca625b3", size = 253890, upload-time = "2026-08-02T18:48:46.097Z" }, + { url = "https://files.pythonhosted.org/packages/29/c6/e92a66cda49a2751b09826d51258f199b92aa0cb005bc5f34e9729a52a9c/coverage-7.15.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a47e2a0a0ace9241e70ee00e44520f88b843094603dd54303f1bafecd929c30", size = 256484, upload-time = "2026-08-02T18:48:47.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/7a/730929164b457cf25cf76c23898b90f9039a104a647890801b6586797b14/coverage-7.15.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:95bad94f83807ae60ed76f3ac012f69b2605ac9ea81bee959a5a483f7fa09c10", size = 257723, upload-time = "2026-08-02T18:48:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/9e/be/04cb5672cb19f5c389eda81ba22d89807699a949653d3625b0e0fda169da/coverage-7.15.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:228e172a76c428bb17d1ab78a2ff188990b0597e5dbd291f52a4edf7412de049", size = 259854, upload-time = "2026-08-02T18:48:51.413Z" }, + { url = "https://files.pythonhosted.org/packages/96/25/5e7fd6af39f6507071455944b8906dd1fe5b7b6bffb6a163ceb20afa0d13/coverage-7.15.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cea9fb33887c99349996266f1fd60abe5af3577a90633392001d27ef46b4b66e", size = 254085, upload-time = "2026-08-02T18:48:53.158Z" }, + { url = "https://files.pythonhosted.org/packages/23/c8/55e58a853f1e61163a6e755897bd14a059d78411e86560f39d9951c019b5/coverage-7.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:81760de3155d7f52c21860c4046628dc6bed182f72e3c028e2b4fd46f65aa040", size = 255850, upload-time = "2026-08-02T18:48:55.031Z" }, + { url = "https://files.pythonhosted.org/packages/be/74/8bcec66dbcf3d22bea2a0b2b77ee2fa6f766a647d0023d4eabbc4f2b2756/coverage-7.15.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b47ea0a1d3a3d089826c6cbfad8429d7d8872e28e86baa95ddef330f6875da21", size = 253818, upload-time = "2026-08-02T18:48:57.163Z" }, + { url = "https://files.pythonhosted.org/packages/ce/06/450b673fdfece0997b4e16a31d6bde6b18889c578f1013ddd34c962ac6f9/coverage-7.15.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5459ba486b2a5d58a6c05254779ecdf525e7f20174d0210ceda75ba40fdb8f2c", size = 257973, upload-time = "2026-08-02T18:48:59.098Z" }, + { url = "https://files.pythonhosted.org/packages/56/fd/3ec7409aec0ddc943132452b65672f065f043b844f1830e1fe173c98b3ab/coverage-7.15.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c59209f80a08dbfcdd5109a80dc623cd3b9d22895c85757d34f57a6e6e95570f", size = 253638, upload-time = "2026-08-02T18:49:01.199Z" }, + { url = "https://files.pythonhosted.org/packages/75/20/30a8dabb194123631c93f860fdd86401ad405d56cfb1841873afbfe4e92b/coverage-7.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f863856c1779d4a5bb6a94698a2f9073e09c6706501f76f3e7780e72df97d21c", size = 255407, upload-time = "2026-08-02T18:49:03.143Z" }, + { url = "https://files.pythonhosted.org/packages/13/4d/e14365b1953b43653341412f9088b0d752614c626a73a705ff9af400f3a3/coverage-7.15.3-cp313-cp313-win32.whl", hash = "sha256:00cbdc5e322927dc30c5e42b863819b1bb867cc66f26ab5372c585850876ab93", size = 224575, upload-time = "2026-08-02T18:49:05.011Z" }, + { url = "https://files.pythonhosted.org/packages/1c/64/88f762ea80de2070207246faef514513be874486b2773528f2cc2b4b515c/coverage-7.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:835528518a1d823cf336740324b2f335f7c01e609e74abcb5d5163b3e66661e3", size = 225116, upload-time = "2026-08-02T18:49:06.894Z" }, + { url = "https://files.pythonhosted.org/packages/ab/66/03c34c53a319f522554cd29d4f2e16c5eab61aa4cdcf55753129fd7d926c/coverage-7.15.3-cp313-cp313-win_arm64.whl", hash = "sha256:0d2e1f2cbbf36b842f3e2aff8d118c60d677adb498bc6c7fa9c6838738f82767", size = 224509, upload-time = "2026-08-02T18:49:09.129Z" }, + { url = "https://files.pythonhosted.org/packages/35/6f/8c2dc014357618b3226c90f731b8282766c3685786f422558991dc49fbf2/coverage-7.15.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1e3bb08ad574bd9fb6a991f645728f70d333c1c1958dd5fcde65e24cb862813d", size = 222571, upload-time = "2026-08-02T18:49:11.242Z" }, + { url = "https://files.pythonhosted.org/packages/07/50/d867c7ceae9d56b7e74ee61ea834f1aa4f9a1e1c7f0ce39393ba573b1c12/coverage-7.15.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9e5860eaff02a0b7f1b73304bdf846596ee62ab3a78d25c68044ebf684cb1fef", size = 222902, upload-time = "2026-08-02T18:49:13.448Z" }, + { url = "https://files.pythonhosted.org/packages/62/77/4f6dfc490c5f2bcacb2d296d9aa4d1e128c43b48e94ad313fec7f49f09ad/coverage-7.15.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:60874e5bd67f0b1bdbe42ab42c7bafa66a6fb8de88721af6df3f7a02713960cd", size = 253947, upload-time = "2026-08-02T18:49:15.304Z" }, + { url = "https://files.pythonhosted.org/packages/16/8a/6777f192af264165103e2a3d3768dbadb9894a0a2359a16877141d9ae8f5/coverage-7.15.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9147be876e9d83765e0b82176674dc248a6b9283e25e01e7462611b97e9b731", size = 256452, upload-time = "2026-08-02T18:49:17.801Z" }, + { url = "https://files.pythonhosted.org/packages/7d/7b/3d7ac46a0234bc684f41ee42be95e29b2b6525695adb04083609d5ac2149/coverage-7.15.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61a01f8c3804760fcc5a3d31c4f3cab792d660d44e17bf7adeaf0ea51e07821e", size = 257798, upload-time = "2026-08-02T18:49:19.878Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1e/c6ee59c29afcb5fdb35f936381340d1a06429a07c48f20e809646647acbe/coverage-7.15.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95bf3e7f26f792e25eb185f85a5a659d48479265176dcfe22b6f334fd0081b5c", size = 260112, upload-time = "2026-08-02T18:49:21.858Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e1/e8ea39a46e89e3a143312ee5f80336e992e3ae8fe44bf9c76b83fefeed42/coverage-7.15.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44c41eff9e413fed8740eca75d5438ebeb9d3e45e7cd37c67329213e7a72c764", size = 253944, upload-time = "2026-08-02T18:49:23.926Z" }, + { url = "https://files.pythonhosted.org/packages/95/67/31ab5f6a37fd887d1386f81f0da9306851ad2264e9baaa9c7f606e0b3e17/coverage-7.15.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54146bafb61f3ba9895b43af0dd17eba01561d586d44ce84ea221b0cbbee5a9e", size = 255805, upload-time = "2026-08-02T18:49:25.973Z" }, + { url = "https://files.pythonhosted.org/packages/fb/6a/ee505a80c8fd89620fb337c0596daecff87f33171fbb4ee3015fc3d7331f/coverage-7.15.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:af000dd1bb859ff8066fda4c79512ff938c798116540307226b373099c7b151f", size = 253769, upload-time = "2026-08-02T18:49:27.883Z" }, + { url = "https://files.pythonhosted.org/packages/b0/41/6ab0f81c9e89660230d8f3f581d4732e5ddb75a885b0a5dfc73d315dc94f/coverage-7.15.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a1b82490577f3889950b5a04f18712aef0207243e0749d60fe28c3c73ebfd5fd", size = 258045, upload-time = "2026-08-02T18:49:30.201Z" }, + { url = "https://files.pythonhosted.org/packages/bc/62/c995e91cae28cf31d6defab3bfb553dda5ac83ac7381b0f2b121264c307a/coverage-7.15.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:c4fc90a60154c3e4b8a2dc206d6dbe852f1c235c249e0dc0cef909d032c9591a", size = 253587, upload-time = "2026-08-02T18:49:32.349Z" }, + { url = "https://files.pythonhosted.org/packages/84/df/f2049980f82d6890321f2065f9e66216eabbf4b2001815db958bc543f40a/coverage-7.15.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f25bb884814a892948b4c20394db3f2364dd452d9492736479e7a493e63b0eb6", size = 255243, upload-time = "2026-08-02T18:49:34.324Z" }, + { url = "https://files.pythonhosted.org/packages/1d/82/2c841b67a978c0eb9c3707630b68f93f9e7585d78bb906bc8823ec6b07a5/coverage-7.15.3-cp314-cp314-win32.whl", hash = "sha256:722dbf8e7828fbcfe0dc8586167dc0a5ce85ad6ea171dbb21ed3f8d6581d3cb8", size = 224759, upload-time = "2026-08-02T18:49:36.326Z" }, + { url = "https://files.pythonhosted.org/packages/b3/78/5c93ec43784fd3e404ca23cd0584ae24bc1732de4a3fc194b68c3be88db0/coverage-7.15.3-cp314-cp314-win_amd64.whl", hash = "sha256:64d0845f9c3ed47302bed265c15ab4dbb64aa4ec1490839b8e328f4e7fa914d2", size = 225246, upload-time = "2026-08-02T18:49:38.366Z" }, + { url = "https://files.pythonhosted.org/packages/9d/77/813a054371f3b018cc63c6bdb46a3c35d5e95d4e3ed4f1449d4196106db5/coverage-7.15.3-cp314-cp314-win_arm64.whl", hash = "sha256:69bc14684f8fbbee9f9dbaa4fe79719b0da9725fc37956785c06ec365acf6926", size = 224673, upload-time = "2026-08-02T18:49:40.552Z" }, + { url = "https://files.pythonhosted.org/packages/8f/63/8c9f36cc71178d26db930baa03a4494abcc516d8d41bf820d0d85ef1d80b/coverage-7.15.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f92df943c24b96cb215ca26b4f6a2283e63c5db80f1635aceea7fff11311917b", size = 223298, upload-time = "2026-08-02T18:49:42.634Z" }, + { url = "https://files.pythonhosted.org/packages/54/66/211f24d058ce9f56ebf1420d55b7574fdae924f6da3836f83c8bd4793e38/coverage-7.15.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:66591c46bdd2971d3ae2bc503a5f0459c2edcaf6b7e045b292000cc95bc6cb95", size = 223568, upload-time = "2026-08-02T18:49:44.706Z" }, + { url = "https://files.pythonhosted.org/packages/dd/bb/9c2ad5574a0d6420a96c6cade4f8a683931b9e79fe609f8924d7b6964616/coverage-7.15.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa64458b81b18bfc67cdf1f6dc02b23e3edc672f2f8e11771fad75865415a43", size = 264932, upload-time = "2026-08-02T18:49:47.153Z" }, + { url = "https://files.pythonhosted.org/packages/ba/91/938c39e77bdd5a0a440412f975609ce3702dabbda6ac715719d93ca45a7b/coverage-7.15.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:447f5421ccf5475956cf516d4ca1d575f487947b6f4e11f9d80c6aefe24b3dc8", size = 267052, upload-time = "2026-08-02T18:49:49.324Z" }, + { url = "https://files.pythonhosted.org/packages/b0/a3/7b431a98af35d9cc6394e54cde9435b33b8591672fbece6a4931267d7a8e/coverage-7.15.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a0c77ef8cd483a4987a5d12d1d9d5f7ee598dfdc6c0844417d847e5768dc779", size = 269473, upload-time = "2026-08-02T18:49:51.599Z" }, + { url = "https://files.pythonhosted.org/packages/32/58/dbc9951dce46be47a732823a1c571f62bcabdd54a68d8c281489a1a55cfb/coverage-7.15.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0b273f4ff657446a06c2d85bf80e134fa869a92852ba5f87854a70e1fb44da77", size = 270591, upload-time = "2026-08-02T18:49:53.865Z" }, + { url = "https://files.pythonhosted.org/packages/71/bd/1d610772c7c0889bfe477a59c46ee66ea53e271f3f06951e9d55b317f7c6/coverage-7.15.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daea8c4fafa22488600405be2c2be525a9406fba3fc0a83acc726db3e14e2005", size = 264007, upload-time = "2026-08-02T18:49:55.875Z" }, + { url = "https://files.pythonhosted.org/packages/69/97/852eb3dcdba156b1a9078503f098499916bf889f964b61ad4a08223ac169/coverage-7.15.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:93ff57c530f3fa7aa69f92fb9b8892b8aa82712aa970842f4abf28657f42fb57", size = 266926, upload-time = "2026-08-02T18:49:57.944Z" }, + { url = "https://files.pythonhosted.org/packages/52/f8/b72cd238757fba2b587fc7dee047efe6e10b0c18343509faaaf502dd4680/coverage-7.15.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4df21bef8b800eebda9018f53d49c9ace3aeb0090c850139b27923aafcb83e91", size = 264529, upload-time = "2026-08-02T18:50:00.035Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0a/6c52ec4b7fb007cb6433d1fcfda4080cb15d75ad37ef9c31025f3427293e/coverage-7.15.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:db567b02685f26034adcbd85055f80d12cdf02111b8ed00886093d98b2874ce2", size = 268263, upload-time = "2026-08-02T18:50:02.161Z" }, + { url = "https://files.pythonhosted.org/packages/c0/4e/f1f9aa3efd109a04353563a43fb5155340c1fdcdeaa6296ebed3b6f510ea/coverage-7.15.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5318dd51b8600b947e058cf5a4fe54d183d9d13c49b97b64ca7be05a34df9bef", size = 263377, upload-time = "2026-08-02T18:50:04.243Z" }, + { url = "https://files.pythonhosted.org/packages/dd/fb/6b268a0b2728ef1c379ad656b899274477a5f6bed1bf6765b4b387fb0601/coverage-7.15.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c995bfa383c54704839b6c4c2627a1c00895597ada0e5e8190c81d8bd620555c", size = 265688, upload-time = "2026-08-02T18:50:06.428Z" }, + { url = "https://files.pythonhosted.org/packages/29/54/1a3ea96e5d5e7cd41dc432597bfc60692910e635d05e1cc25a8ccc243581/coverage-7.15.3-cp314-cp314t-win32.whl", hash = "sha256:6433fafb8da0e1d02eb53411e0ecdadb6b88f0224fdc23317e703c0e88937d42", size = 225066, upload-time = "2026-08-02T18:50:08.533Z" }, + { url = "https://files.pythonhosted.org/packages/31/9d/a7b0d9afd18ed5274dd00651a78e7810a931c70d94b79996f150bec1a30f/coverage-7.15.3-cp314-cp314t-win_amd64.whl", hash = "sha256:fe578952b1b29fe8c777f43f241d49efac4b56724a3434f5d22ebe3c208df429", size = 225897, upload-time = "2026-08-02T18:50:10.572Z" }, + { url = "https://files.pythonhosted.org/packages/ca/11/34c5ae40b945e69aa72b87dc268135b7049905f3824af573b7073acbb946/coverage-7.15.3-cp314-cp314t-win_arm64.whl", hash = "sha256:d2e1acb7aee29dfa8f3e48c23f36670898baca1209d9bdd3985a50c7f982165e", size = 225212, upload-time = "2026-08-02T18:50:12.63Z" }, + { url = "https://files.pythonhosted.org/packages/37/e7/7069b3d6c018917f49ba2e1c5fb910e498c7fefa3a1b78cb1b79e61ff45d/coverage-7.15.3-py3-none-any.whl", hash = "sha256:da78fa6fc7dafe4212839173133ee85afcf42c5cd5f3e47fa7c1c210453b445e", size = 214297, upload-time = "2026-08-02T18:50:14.709Z" }, ] [[package]] @@ -444,6 +453,29 @@ 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 = "dask" +version = "2026.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "cloudpickle" }, + { name = "fsspec" }, + { name = "packaging" }, + { name = "partd" }, + { name = "pyyaml" }, + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/39/cbd21c9133d02b4e60899ed466ad5e876553ea68ebffee6209e7dcafc8d4/dask-2026.7.1.tar.gz", hash = "sha256:5727484427665f051e86bf87d021a64d6411141cdc8a20bfe3c1ad2968cc06b7", size = 11548794, upload-time = "2026-07-14T01:06:22.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/5f/7c22733da92b3a6cc4dddcaa8731089d213c2790bbc997e51c429a4e8f8b/dask-2026.7.1-py3-none-any.whl", hash = "sha256:985ffd6c5e9d7979ede515e84ae8d39b647d6aa64f77600f15714ff65f578fe6", size = 1496882, upload-time = "2026-07-14T01:06:20.341Z" }, +] + +[package.optional-dependencies] +array = [ + { name = "numpy" }, +] + [[package]] name = "decorator" version = "5.3.1" @@ -520,11 +552,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.32.0" +version = "3.32.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c0/80/8232b582c4b318b817cf1274ba74976b07b34d35ef439b3eb948f98645a1/filelock-3.32.0.tar.gz", hash = "sha256:7be2ad23a14607ccc71808e68fe30848aeace7058ace17852f68e2a68e310402", size = 213757, upload-time = "2026-07-21T13:17:42.898Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/57/3ba6e6cb097f85b855b00163d169f35365f44277df044dcf96d55b8f62a3/filelock-3.32.2.tar.gz", hash = "sha256:c33351e1f49cae33414acbc6d56784e6ecee82514ec90795da1161fc4836b5b8", size = 217172, upload-time = "2026-07-29T22:46:04.895Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/79/b4c714bef36bc4ec2beeae1e0c124f0223888cd8c6feb1cdc56038116920/filelock-3.32.0-py3-none-any.whl", hash = "sha256:d396bea984af47333ef05e50eae7eff88c84256de6112aea0ec48a233c064fe3", size = 97732, upload-time = "2026-07-21T13:17:41.55Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e8/72f8cef9fdfeffe06213fe8508039396ee48daa0e3259457ed766173bfd6/filelock-3.32.2-py3-none-any.whl", hash = "sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82", size = 98830, upload-time = "2026-07-29T22:46:03.52Z" }, ] [[package]] @@ -594,11 +626,11 @@ wheels = [ [[package]] name = "fsspec" -version = "2026.6.0" +version = "2026.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/10/a1/ae4e3e5003468d6391d2c77b6fa1cd73bd5d13511d81c642d7b28ac90ed4/fsspec-2026.6.0.tar.gz", hash = "sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a", size = 313646, upload-time = "2026-06-16T01:57:28.105Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/78/f34251dadb8f3921264a1d9b8946f5e542014ee2614b285261b4e40e6775/fsspec-2026.7.0.tar.gz", hash = "sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88", size = 317040, upload-time = "2026-07-28T16:34:51.052Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3c/6a2bf344106328fd04963664a60b9bb6496fc25df8e962fcdc1367285fb9/fsspec-2026.7.0-py3-none-any.whl", hash = "sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279", size = 206583, upload-time = "2026-07-28T16:34:49.538Z" }, ] [[package]] @@ -690,15 +722,15 @@ wheels = [ [[package]] name = "h2" -version = "4.4.0" +version = "4.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "hpack" }, { name = "hyperframe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/d4/a7d6fb3f58be99d65cbf2d3f766896217a2921d0f3ab10711c45dc1519ee/h2-4.4.0.tar.gz", hash = "sha256:46b551bdcdc7e83cf5c04d0bf93badb8a939bd2287d9fee1abb23a445b9e0580", size = 2156691, upload-time = "2026-07-23T19:14:19.442Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/85/7c366e69d84c17bb778fe41419e1fbcce3033d5b7ce29bbffff0a98b859f/h2-4.4.1.tar.gz", hash = "sha256:4e866ffb1a869ae14dd9b5e6beb5c24a13da0495ad72b65925ded182521c1516", size = 2157281, upload-time = "2026-08-03T11:45:09.509Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/df/5b14a118322d6097cb9bb30ec6bacad268e546a8ecfcb1f6d0de618dac2f/h2-4.4.0-py3-none-any.whl", hash = "sha256:6acffe1aeab79098d7eb0f8385c1add11f2c7a94815f6fa2b7060eeddee3d87c", size = 62368, upload-time = "2026-07-23T19:14:16.143Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/e85faf23bd72a92d1921e37d674ca56eb298a3c8be31fdecef0ff2b3aaac/h2-4.4.1-py3-none-any.whl", hash = "sha256:0e25f1462b23c9cb82d9eb02e28bc706dac2a68cb457c6a0d74d63c8a2a5d0e6", size = 62636, upload-time = "2026-08-03T11:44:59.164Z" }, ] [[package]] @@ -815,51 +847,55 @@ wheels = [ [[package]] name = "hypothesis" -version = "6.161.1" +version = "6.165.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/87/1d/f5453faa2dd41890212d858f9dfa2a9e6db643c3037d6758101f38ae3f8a/hypothesis-6.161.1.tar.gz", hash = "sha256:44ee51052b560245676cacf8c88be23f312132cdf29cb08dd092b52fdcf07a5d", size = 486148, upload-time = "2026-07-23T20:37:08.065Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/23/445da79f0847e63f2acfc7ca446d18dc4ddc05a2bd88c70c2a28b3a65510/hypothesis-6.161.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c74cc6f010ac2635591891de8c16a3b5c0576f3661750509cef2487a6c1bee9a", size = 766527, upload-time = "2026-07-23T20:36:38.332Z" }, - { url = "https://files.pythonhosted.org/packages/b2/da/d7329ec56c2762553ad0f370406af4f47c1a74aed8fc7549f13c007f549c/hypothesis-6.161.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e1584bd873c68847ece7d8310f44c739a465122d94a506c44e9378278838779b", size = 762095, upload-time = "2026-07-23T20:36:14.461Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9e/7ef3091342d7b6445aec7a77b243b5fdf54ca61a6c1a3020f68c16ad52c2/hypothesis-6.161.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed29064e1c5b3062f74c8d073a3b623b33310fa4b13ce168db2c7ecd43caa922", size = 1091343, upload-time = "2026-07-23T20:35:49.161Z" }, - { url = "https://files.pythonhosted.org/packages/68/41/4b48032eaa17c59e648dfd5224fefcfe9626d7972cd99f89b3632d9dac4d/hypothesis-6.161.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7567f56527cdfe53989fcb99dca46cd805e28ca392c183886757b7f870b90046", size = 1140882, upload-time = "2026-07-23T20:36:09.431Z" }, - { url = "https://files.pythonhosted.org/packages/47/7e/9be7ae0c525a169eb6e824ea0f9b517a47887d22e379f2ad013cd0ff736f/hypothesis-6.161.1-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:730244719991ab11c1dfb6fe7a40488bdb6a632d4c856d08499d979e71e86c6b", size = 1132968, upload-time = "2026-07-23T20:36:58.457Z" }, - { url = "https://files.pythonhosted.org/packages/dd/10/f71f3fbb34cf996957d832dad6bec8a34415cd2b9145693bb082c00944b8/hypothesis-6.161.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b9956a4de0d04078791ef0b78f60e160aae205d9c8871b9a23f1db727d099dd7", size = 1265175, upload-time = "2026-07-23T20:35:52.172Z" }, - { url = "https://files.pythonhosted.org/packages/f3/28/82c4edad608931113d5993fede9fc9b6045f5d1203e538af0726d5b14a79/hypothesis-6.161.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:def1fa012d9d8fce70e8c107e2137b9ab89293884470cc6f565c42c5f07c029e", size = 1307849, upload-time = "2026-07-23T20:36:06.065Z" }, - { url = "https://files.pythonhosted.org/packages/4e/10/7ad339cdec8df2bd907e3b2a712dd5f36f59f2c1ea2d7d01417436338692/hypothesis-6.161.1-cp310-abi3-win32.whl", hash = "sha256:410e09dff0cc332b4434aa6f1a20f9d6c037ab938f3db4f6adc272b2a7d03fc4", size = 652384, upload-time = "2026-07-23T20:36:12.771Z" }, - { url = "https://files.pythonhosted.org/packages/68/65/821390df2877ea60e48d028a2659fbfa1a852b393655ce3a99dd04522002/hypothesis-6.161.1-cp310-abi3-win_amd64.whl", hash = "sha256:bba4ea9d1ba5ad6ad93c500ce4f241329de28a91489a9a318f75be8dc79f477d", size = 658547, upload-time = "2026-07-23T20:36:19.407Z" }, - { url = "https://files.pythonhosted.org/packages/92/06/c724ff585b9e61a2c40500e214f19794a02fe6a48d7587d9fcb27b75cf56/hypothesis-6.161.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2b12a6bc4f3f6071b9db444c49b387a2f005b53ecb491aa10c4bdac32abc8df4", size = 768144, upload-time = "2026-07-23T20:37:04.462Z" }, - { url = "https://files.pythonhosted.org/packages/78/3e/35ac6507910eaced5d06dc60b6e3484a718a9cc89791e067f7f17b45ec60/hypothesis-6.161.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db2d1d564c9a232007e5f1b7e9a4333c3718f2b73dc47d82f2300fb2cfce0840", size = 759714, upload-time = "2026-07-23T20:35:59.535Z" }, - { url = "https://files.pythonhosted.org/packages/94/16/3505c465f0e9f611197730561a9c787ff02ce9a7f54cdf2bd32f083af207/hypothesis-6.161.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12cf495fc49e4e3cee80f50fb0126a0ad99f9d6baa0984f1f5923c62bdd365f8", size = 1090154, upload-time = "2026-07-23T20:35:42.987Z" }, - { url = "https://files.pythonhosted.org/packages/35/09/720f80ff2daca23e164a133c4102a25d5006a5d00577bdfdf1f7df5045d1/hypothesis-6.161.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf5a709efe1eb193c340f57110df210d146967a93e7fe611213df43594c59530", size = 1140198, upload-time = "2026-07-23T20:36:33.243Z" }, - { url = "https://files.pythonhosted.org/packages/72/06/10583f6b185031e0366cdffaeb54a9fa7577799b4017e67381eb309610b2/hypothesis-6.161.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:432f832c52872369c0f9a85bd4d18198944c64a7b4d4f63a133c9837c360f951", size = 1262979, upload-time = "2026-07-23T20:36:45.745Z" }, - { url = "https://files.pythonhosted.org/packages/98/b2/abd0a1e9e05ba7ce639833eb93ab27ffdb940ffd146ab9b17ba3dcd2a4a7/hypothesis-6.161.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb8a90449d2138ef6b82cecf39311e10f30cef89ad792aeb574086193752a8c9", size = 1307159, upload-time = "2026-07-23T20:35:58.162Z" }, - { url = "https://files.pythonhosted.org/packages/50/7d/03607c3f83c2312cdab52d9ce25054216fcea2c33da483d8d1afb6356d10/hypothesis-6.161.1-cp312-cp312-win_amd64.whl", hash = "sha256:36652bce788e77ccb1bd92a21fd6491980bae5ecdf7686e8943aa70458c873a6", size = 655680, upload-time = "2026-07-23T20:36:40.063Z" }, - { url = "https://files.pythonhosted.org/packages/22/c7/8295a14fabbfa8ed90bd2c382e8f00b8726e4feb5b94236ff463d5ab2004/hypothesis-6.161.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b6f3a2eeb3b141c662b572585c903955d41ce6ac1d07ff6b927e658170fab2f8", size = 768018, upload-time = "2026-07-23T20:37:06.242Z" }, - { url = "https://files.pythonhosted.org/packages/d8/44/88f576f295aec95ff07febabb2029b64db02d392b9d0969cb70d27a5f2ef/hypothesis-6.161.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f6bc47b4100af38c740cbcd8a1cc5a69b57d67b6c910feeb964a0badad35cdf", size = 759679, upload-time = "2026-07-23T20:36:49.537Z" }, - { url = "https://files.pythonhosted.org/packages/d7/10/9d757e68d53e5ecd973aa5e09bc16ab3d045f7cc4b409ac69bf2a62c31a0/hypothesis-6.161.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ac69b3f9682b54a8dca79738805301efb6cbdc0645eacbac650992f6571b56a", size = 1090070, upload-time = "2026-07-23T20:35:50.72Z" }, - { url = "https://files.pythonhosted.org/packages/e2/eb/1ee79c1f34024738090a1033cd2e3825e590011a310922f4ff88a0730ad5/hypothesis-6.161.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b3abe32e180cef09772768ec59b2ce9320b03cbc8756a686163582c82ce5056", size = 1140012, upload-time = "2026-07-23T20:36:56.686Z" }, - { url = "https://files.pythonhosted.org/packages/43/95/cef03067e9d3249893d6c8e2527af7cd0d843729b1d5580734230c00c794/hypothesis-6.161.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3fbcf071b12d7f133f910dde2ddcb820285b4d3549f5c75aa826b8eda6e42768", size = 1263022, upload-time = "2026-07-23T20:36:11.118Z" }, - { url = "https://files.pythonhosted.org/packages/98/05/6d9732f5052a77eb119784eaf18a19e83a23f0d0aeba8358ccfd6ae53e9a/hypothesis-6.161.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f82837674382a8e03e9389cf3c3444a052420c6e697b7cab4ada2d28448e226f", size = 1306912, upload-time = "2026-07-23T20:36:47.5Z" }, - { url = "https://files.pythonhosted.org/packages/6b/9f/e1fd97cd3801782a98d642fc10d9310e4f5c852aee8933d49e50d0265c49/hypothesis-6.161.1-cp313-cp313-win_amd64.whl", hash = "sha256:e688d96fa01816c3205336fc9954c6efa561c9bfbef783b2960ab4ec714ff73e", size = 655638, upload-time = "2026-07-23T20:35:46.164Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1b/35683d3c1089354ae75d15b4cf4f00900508f8b10100f74f350d9005a7ac/hypothesis-6.161.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:36062ae4cd60fdbcc765edb47b73677c70350d6611cbcec2fdc3069be16479c1", size = 768213, upload-time = "2026-07-23T20:36:54.872Z" }, - { url = "https://files.pythonhosted.org/packages/f8/92/d85baa4241b2625f012989943bae190d21aead7b7705a0573ec61f8ca152/hypothesis-6.161.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6e59608ad96e2bfb7e4d1eb5adac633a5a2f735b9e368d8b5e0b12d976aacb2c", size = 759815, upload-time = "2026-07-23T20:35:44.516Z" }, - { url = "https://files.pythonhosted.org/packages/08/4b/08214b62fa1b85d059b71f5401130506c11d35a82d221133b415de19793d/hypothesis-6.161.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d4249baa5fa38221432318ad692c2bc34b74eee1e8fe3d828c498a27f0545a8", size = 1090571, upload-time = "2026-07-23T20:37:00.529Z" }, - { url = "https://files.pythonhosted.org/packages/13/a8/3d66569b0fbf8b7be5b2cead898295eb65d5efb0eb298f6552441292cdfc/hypothesis-6.161.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3eb24e5f115de84e089829fd6f904f7c3296d8462560f22b7d9bc04bb7828123", size = 1140198, upload-time = "2026-07-23T20:35:34.489Z" }, - { url = "https://files.pythonhosted.org/packages/aa/1c/8391c5dcb1dcbb41e193dace61092d1311718405b696207d1ba35ae5f3e4/hypothesis-6.161.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2d079ae6ae39fd602393bbf853627aa530fcb467200246ca8700a401e1c1862d", size = 1263355, upload-time = "2026-07-23T20:35:38.905Z" }, - { url = "https://files.pythonhosted.org/packages/2a/97/40c3a8dccc3a8e6cdb262b846a5de8b8f9792d47645b41b073233f435976/hypothesis-6.161.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:34a95d9684e760fa122194e721b35fe61fc4f792b6f90402fc43ce86ef1e021d", size = 1307192, upload-time = "2026-07-23T20:36:35.121Z" }, - { url = "https://files.pythonhosted.org/packages/69/7c/8919fcefd596c4fab3d89fbcced9431269edb6e7d3b0fd178949f56d9798/hypothesis-6.161.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:13b3523058a8240748f3756d443b2af6dfdca9f741b268e0e5f71fb6fe7f5934", size = 599732, upload-time = "2026-07-23T20:36:29.22Z" }, - { url = "https://files.pythonhosted.org/packages/86/04/3bda8a4d9f29c0e6225129ce394ebd75a9735f65d91fbd780d559c607acf/hypothesis-6.161.1-cp314-cp314-win_amd64.whl", hash = "sha256:3c5d057b7601801d1aa93070a38b77511e0009e1ccffe6a42cb259e846f50e31", size = 655588, upload-time = "2026-07-23T20:36:36.639Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b3/cf868c489c28cfd1c293401568a927650ff09dbab92079c322fe49f7ebe3/hypothesis-6.161.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:b79650b1b90806fc06c88ff1963c974678155770bcf488d19fd5e44b8d276893", size = 766790, upload-time = "2026-07-23T20:35:56.734Z" }, - { url = "https://files.pythonhosted.org/packages/31/a0/d743e0fbfb0f0cb58739890d3d132f605e666419c0b25d96efeffee1b5ea/hypothesis-6.161.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b7dd1963fefcfa6ac5a33abba1e401f03e25fbca57ca9c2c57b121a1bea9d14f", size = 758280, upload-time = "2026-07-23T20:36:02.819Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e9/8a13ff4ecb3582e0bd5c02d68c28ae550cf22c8bd1554ad46a4ba6919ef1/hypothesis-6.161.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84e0d123ef2996ac2025bba9c2b2d51ba44320fb744c4044acb74b8deb95c3d6", size = 1089167, upload-time = "2026-07-23T20:36:44.003Z" }, - { url = "https://files.pythonhosted.org/packages/a8/3a/e8d950dbea29184aa5863136a6e835f5fccb9ac73be13f555a62c1284b10/hypothesis-6.161.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f736f6ec2081f974cb96a14d2e05b796197e508fca0e6c84f50c163496851eac", size = 1139083, upload-time = "2026-07-23T20:37:02.593Z" }, - { url = "https://files.pythonhosted.org/packages/49/43/a55b85c185d81214849bb802714d13ca62ee5ecbbbdc6d1417efe313aaf3/hypothesis-6.161.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c2ecc445a73a77f30a7b1c95280d91ec04f674e8c61df86b3f7adbfde4a0ac1b", size = 1261591, upload-time = "2026-07-23T20:36:31.442Z" }, - { url = "https://files.pythonhosted.org/packages/8c/de/aceb9c110e2f6f995c929827ffae1f21ad85fe9c43dec7ac94619ecd8de1/hypothesis-6.161.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c9e34f122639949dfabe1c2b70c0b06287e2eaf1ce7bc49b14cd4c532a14d25", size = 1305967, upload-time = "2026-07-23T20:35:53.65Z" }, - { url = "https://files.pythonhosted.org/packages/cc/bd/06a88dfa122e549336c0b48b83f27190822a3702172b987a8448faf1d9bb/hypothesis-6.161.1-cp314-cp314t-win_amd64.whl", hash = "sha256:027367e736255b9cf3ac894bbde4815ebc000396af9d9427f16776f395697159", size = 655718, upload-time = "2026-07-23T20:35:47.721Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/5e/6f/324d76c305075036519f63f986e81e8c3f7d0a3510eb88510bc407967ab0/hypothesis-6.165.0.tar.gz", hash = "sha256:b54e86cb1d7d049eb934e7843f179889252263b24290ebd38549466c9c9351d9", size = 493017, upload-time = "2026-08-02T00:20:26.755Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/f2/c85859811281475dc3dddd628979be6683ea5012494316250d29a6d9d72a/hypothesis-6.165.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dda3707dfdfdc1397b7489ecb81534f47f22f98d468c51c92d591d6967c58b29", size = 772599, upload-time = "2026-08-02T00:19:34.099Z" }, + { url = "https://files.pythonhosted.org/packages/a0/05/a28b2ad55773b36812a1ad6f88d55b051ef324c606efa880c78ccceefbd6/hypothesis-6.165.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:a384468744915435a25a9f2dcd0a74e9f78f8d56cb51f5ea1c1c4973bd40bd5b", size = 768129, upload-time = "2026-08-02T00:19:38.326Z" }, + { url = "https://files.pythonhosted.org/packages/58/ab/3567e7f5aa16319ee9e0fb014e588149e4ef6080028fc07f2748deed9f0d/hypothesis-6.165.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d65562b9866a5a4be4c731bd4018152861997675b92ed6d0f29e925999928f9a", size = 1097411, upload-time = "2026-08-02T00:18:58.073Z" }, + { url = "https://files.pythonhosted.org/packages/bd/5d/312c3b3831183668beff6974a290390f2327846e3bd0baa44f116eecc1bc/hypothesis-6.165.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b402560b0463e643170d904a5987f61bc388718f7b48d4521b7a5b22e6a27019", size = 1125993, upload-time = "2026-08-02T00:19:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/f47f529c41853d56c0ceac5dcb69a6ffae43d1367877575b2b57c9c60fb3/hypothesis-6.165.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1e134b573c18fb7e79f915ccad3585f81c0b92ff7cf89f6c0b0d966a5ac5156", size = 1146830, upload-time = "2026-08-02T00:19:07.51Z" }, + { url = "https://files.pythonhosted.org/packages/fd/9f/4d613a35b834c602499746c0d8036aa5c0a9b2c7a23f294292a132a352db/hypothesis-6.165.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:22d3b846796593117a1227b6d9e8c0c1269ce6327fa6345fb50f24a038e1a0c1", size = 1102233, upload-time = "2026-08-02T00:20:25.053Z" }, + { url = "https://files.pythonhosted.org/packages/7f/69/f11136ed8feac210f247dcad53c9a5cbcf067ae42d4d51c9ba61640aafc1/hypothesis-6.165.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:47d419899477ed35704d4f90f82aadbaa50080d7d0ff33db6727bbc4b1c3956d", size = 1138969, upload-time = "2026-08-02T00:19:32.587Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ad/dbd5e97bf214993033ddb93f199d99410be33d7f997996c8dc2f6255edfc/hypothesis-6.165.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:71721d9de23b60f5436a1b8b8fddccd6df606efbb406f36254195089974518ee", size = 1271210, upload-time = "2026-08-02T00:20:16.318Z" }, + { url = "https://files.pythonhosted.org/packages/9f/f1/db4ecb5f01cebc19ccfe8b4b170fa211f2cfd22946ca8c9f4007eba59140/hypothesis-6.165.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:584734f6380f1965498bff2f72af307c182b3398a9c4e1b2f31e43dd1aa9c922", size = 1399053, upload-time = "2026-08-02T00:19:55.646Z" }, + { url = "https://files.pythonhosted.org/packages/68/15/205570c5f3935e074b93c7b6bda87889039d59ba39426c70e7c93bfdc939/hypothesis-6.165.0-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:61d99c493f759809e64dbe544a47948537460c9f03f916d66291bb63965fc91a", size = 1271824, upload-time = "2026-08-02T00:19:52.519Z" }, + { url = "https://files.pythonhosted.org/packages/fb/5e/d77c9cf07bd4a7b2c386cc7f8eba6c614d62723e830d0366ca2381f35cb1/hypothesis-6.165.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:68629657d49b7dba3f061d0224441df068c0c03e7a17efc278782f637d810db7", size = 1313877, upload-time = "2026-08-02T00:19:13.765Z" }, + { url = "https://files.pythonhosted.org/packages/38/46/6e7f945c465b94951bb91bbc50cb1dc847a749d32e1e5efd6e47a5a7747b/hypothesis-6.165.0-cp310-abi3-win32.whl", hash = "sha256:90633ec15635385723d29a1e9b6fe8f63e17b33641353c6b58fc7d277672ef29", size = 658400, upload-time = "2026-08-02T00:19:31.133Z" }, + { url = "https://files.pythonhosted.org/packages/06/c5/81519854c72e1f57ec44df6484f6a34f1789f1fc4c0b8647d2fd1e28d25c/hypothesis-6.165.0-cp310-abi3-win_amd64.whl", hash = "sha256:bab53228c59978c74c87dba27db4656feaf991f20db5242ea263531477227247", size = 664538, upload-time = "2026-08-02T00:20:02.436Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/dd8e7c7cfd7635f632eba381f89dfbb9af77dc55fc5a11a754f0c0fba1a0/hypothesis-6.165.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6942d6a0002092c9a1166915d26a3257c17da4b8c76859c9841b84a21a572262", size = 774175, upload-time = "2026-08-02T00:19:45.731Z" }, + { url = "https://files.pythonhosted.org/packages/62/17/84b4102defda7916512badf2741b203688cb4236e6be10bf656997b9255d/hypothesis-6.165.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:20a17a11eeb3816b3d4c9576d4d9b44a985bf062ae232d0fad9a2e5c16b35a6b", size = 765771, upload-time = "2026-08-02T00:20:19.735Z" }, + { url = "https://files.pythonhosted.org/packages/98/13/5f3a0f944286dc2913e9c84a788213e952d9e94b806e53c69bddce71eef9/hypothesis-6.165.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2d5018013480fe207408e0d7c04244c46a7113c1c2aef779cbe17e1f8750988", size = 1096190, upload-time = "2026-08-02T00:19:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/34/87/11f9386bffecf9bbc13f459f308a844a512a3f34354cb8c3087f0733190a/hypothesis-6.165.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504feb1f67bd1e6c83d42982f9f80a6d11acb164cc11519d4abf2e7479394be2", size = 1146237, upload-time = "2026-08-02T00:20:05.906Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/f24008063d2ed5eca2a8f59286849360dcf8c821a241e3553229f3d2d700/hypothesis-6.165.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:eff69d0c3ad00fe82f06420c8d13c33b86db24636c96ac457cbd9a5a588f6e88", size = 1269013, upload-time = "2026-08-02T00:19:42.717Z" }, + { url = "https://files.pythonhosted.org/packages/44/43/b5ba1be7cebe0ca1c320e188a35761050cc933b8563c9cff0ce8d2d81518/hypothesis-6.165.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db118ef2c469fe607ac617a15ccc8bc0e05f27b866c1d923c963d9edd3e4b7e1", size = 1313226, upload-time = "2026-08-02T00:20:17.954Z" }, + { url = "https://files.pythonhosted.org/packages/92/c3/70a6f3ce669616b78adfd39bcb78cbc88e68bd1a6f670eb5b88273ae7cec/hypothesis-6.165.0-cp312-cp312-win_amd64.whl", hash = "sha256:f16f8fd1ca5d1a080a360bb37eee73f3fecf3ddabdcec979ae01b76ed895e777", size = 661691, upload-time = "2026-08-02T00:19:44.18Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d4/3418ba26110b6c29e866bf1baa5b2d447827d00811e181be12ce08aebb9c/hypothesis-6.165.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:82385ff61d309b77097580e7200dc3e038f5eb78d3fe1a4aa0d110e5f1afa9df", size = 774071, upload-time = "2026-08-02T00:19:23.829Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f4/d89ab31f52ecbcc8328d25add616b9d530a5f76003ae4ac5a9bfc7080db5/hypothesis-6.165.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2e7b277f56786ec0730cd3e428ea3611b59e3ecae3382553f3fea958e8c2c25f", size = 765718, upload-time = "2026-08-02T00:19:35.595Z" }, + { url = "https://files.pythonhosted.org/packages/06/13/10a32a1e3ba78c3b66281aa1e2764269765ad058fb977d6b62dc6679e63a/hypothesis-6.165.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c73d3d4e57a00122a2657ae400b8d066fd0cfa1621c6aa3a6b38dd4b4c268035", size = 1096108, upload-time = "2026-08-02T00:20:09.316Z" }, + { url = "https://files.pythonhosted.org/packages/ef/87/f16688e373b982f6fb3bf0edd4aa42eb731e8a758582052ec1bfe7c93b30/hypothesis-6.165.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89b41b1daa93c9e544cdad636ec87d4a45169d1922c6aff80fa282cd1edeeb74", size = 1146051, upload-time = "2026-08-02T00:19:49.262Z" }, + { url = "https://files.pythonhosted.org/packages/af/2f/a560867b9be5c908e379c4a308a7e44b8082e18faf4096b5a0708497ba30/hypothesis-6.165.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b2fef387ec63e60c85db6c4fc95b07a6b7f35c971381997c862d309af9e1e4b", size = 1269056, upload-time = "2026-08-02T00:20:21.588Z" }, + { url = "https://files.pythonhosted.org/packages/0b/60/35d03cf44a7803622fa39121e8c674253175890a9b081ae0b8137a1d32ef/hypothesis-6.165.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c681bdf0036ba7923b7c4225937a48e12db7029a286b51ddc03d9b08afe2f040", size = 1312946, upload-time = "2026-08-02T00:19:12.547Z" }, + { url = "https://files.pythonhosted.org/packages/30/a9/1b587430b9ec6576fdafa102657d70d456b5694db45ee21fba88aa0c8ea1/hypothesis-6.165.0-cp313-cp313-win_amd64.whl", hash = "sha256:29edc5a08334985cea55afe6ab564c147e4b804179265eb303c9ed8e494c211b", size = 661683, upload-time = "2026-08-02T00:19:29.812Z" }, + { url = "https://files.pythonhosted.org/packages/04/b5/4aab253b312334cf2d475ff8a0a2136eef6af091f9f33ce37275864437fd/hypothesis-6.165.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:6536572500e926ec8128069fda8bbcd13019484e0d5c1b4a82b6ef50417bcfbe", size = 774311, upload-time = "2026-08-02T00:19:41.234Z" }, + { url = "https://files.pythonhosted.org/packages/fb/97/85363607d7303080d8e48277d5ff56e7fb2ad5792f5fe97e66e16f5fcce0/hypothesis-6.165.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d86c82a7796d124643d146eef00d873d336d1792a682096358bfee82dde34879", size = 765857, upload-time = "2026-08-02T00:19:08.724Z" }, + { url = "https://files.pythonhosted.org/packages/f9/80/415bd4b25fa2acc07228f7f86a438cc31d1caccf0c5ab49210b32e895990/hypothesis-6.165.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b646658e06b72b2bd2396ffe7a08a0edcfa799f649db22e1d59cc8bd5d174e6", size = 1096607, upload-time = "2026-08-02T00:20:04.23Z" }, + { url = "https://files.pythonhosted.org/packages/43/f2/95786f14932fc76273829e20d43185c4fe9a30b5930108895b0b805566ac/hypothesis-6.165.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e0fb8c29ad35fefa25f38c0bbc1b2eee28d1e4a796a977491445b05d11cb1ae", size = 1146225, upload-time = "2026-08-02T00:19:01.717Z" }, + { url = "https://files.pythonhosted.org/packages/fb/90/9284f79a7e03f8185433dfd4eb2eef64a2766e160a82f3b108d35cd5d0e9/hypothesis-6.165.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a998176a8cf1b1914c5cf8beef0b74d0065482aa2a4d0c708e63216245d968c7", size = 1269388, upload-time = "2026-08-02T00:20:12.785Z" }, + { url = "https://files.pythonhosted.org/packages/90/d6/e91cbc7e5c7b740ba8e5195d114f87281b4ee36f534c488aa06a4ef2eba4/hypothesis-6.165.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:510518cb4259916685151aa41c44a3999534c35eee921dcfbc4ea4e4631bd6d2", size = 1313262, upload-time = "2026-08-02T00:19:00.584Z" }, + { url = "https://files.pythonhosted.org/packages/1b/4f/3c5350ef157e039c1624f16495b35d7930c63946d7b07b8c2fe80537f8e8/hypothesis-6.165.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:1f786c76577ec0639390baf0b4c12491fa909131e41de76078a4a886ccd40661", size = 605819, upload-time = "2026-08-02T00:19:50.814Z" }, + { url = "https://files.pythonhosted.org/packages/66/46/59cb3bd95346ba9626b72af40cff43a95d0b4554bb5f4bdc59cc8724924b/hypothesis-6.165.0-cp314-cp314-win_amd64.whl", hash = "sha256:0bbc39067ba06514fc0c934c34863492403ffc708a07d6a506b9460058550127", size = 661568, upload-time = "2026-08-02T00:20:14.452Z" }, + { url = "https://files.pythonhosted.org/packages/30/d3/bcda5097a4535b8bb8f3ca94cce103e59149db16923732d06121c9f23a56/hypothesis-6.165.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:38222e75db0f348c8ddaab10903fc3971846f11363e64efdb4794f34aed6494f", size = 772867, upload-time = "2026-08-02T00:19:19.441Z" }, + { url = "https://files.pythonhosted.org/packages/74/06/3f275d4c2f6977133a2ad1f0c22bc75d7e3f86bc2f279c33228f4a2230e4/hypothesis-6.165.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:01c485be9970a40935b2d2f277d751d60aaa910e4dc5ec337828e80c9c8dfdc8", size = 764394, upload-time = "2026-08-02T00:19:22.276Z" }, + { url = "https://files.pythonhosted.org/packages/d2/82/3d3bb9b971946369bf4574595ed0291c49378b6ff372c169fe0da07c036a/hypothesis-6.165.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bcb354b9bfc7dcf5fbcb559cd5b3f48e0d92a4f1f6e5fbcf9fbc8cc82e51e61", size = 1095202, upload-time = "2026-08-02T00:19:25.331Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a4/93c86242ef646f798afde2f406c23b077e86c4bd39d304baa7ef7b31df81/hypothesis-6.165.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51dbf9192af532f9081c90f00afb0a7684da5543ccae8ab9507da269b6702ccf", size = 1145134, upload-time = "2026-08-02T00:20:00.737Z" }, + { url = "https://files.pythonhosted.org/packages/34/ec/b0bb70f92bea56be91e5ea431bf0af6a3fd3fd9a70b9106ccc05510d34af/hypothesis-6.165.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:70dd766e80e575580e635d6661f945ef07055e66075a93fd489d53611cb5a7a1", size = 1267624, upload-time = "2026-08-02T00:19:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/45/9b/d8bfd5410cae98b39145b817304316dc74ece187e03e273b271d1a769dfb/hypothesis-6.165.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5e811db36037576b277d8fc390d3fcb94d7924ec861d05adb0cdfd8e3ea86822", size = 1312004, upload-time = "2026-08-02T00:19:39.823Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/0e2f19c6d75d04f2604ebb36bc561a120fc9173a694130b853a248586860/hypothesis-6.165.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7a14b7905e4aad404c820bd01f8a1272042123233edf04149b847db2c5076584", size = 661693, upload-time = "2026-08-02T00:18:59.486Z" }, ] [package.optional-dependencies] @@ -886,7 +922,6 @@ dependencies = [ { name = "pyfftw" }, { name = "qcore-utils" }, { name = "scipy" }, - { name = "tqdm" }, { name = "typer" }, { name = "xarray", extra = ["io"] }, ] @@ -901,7 +936,11 @@ dev = [ ko-matrix = [ { name = "obspy" }, ] +lazy = [ + { name = "dask", extra = ["array"] }, +] test = [ + { name = "dask", extra = ["array"] }, { name = "hypothesis", extra = ["numpy"] }, { name = "pytest" }, { name = "pytest-cov" }, @@ -914,9 +953,11 @@ types = [ [package.metadata] requires-dist = [ + { name = "dask", extras = ["array"], marker = "extra == 'lazy'" }, + { name = "dask", extras = ["array"], marker = "extra == 'test'" }, { name = "deptry", marker = "extra == 'dev'" }, { name = "hypothesis", extras = ["numpy"], marker = "extra == 'test'" }, - { name = "numpy" }, + { name = "numpy", specifier = ">=2" }, { name = "numpydoc", marker = "extra == 'dev'" }, { name = "obspy", marker = "extra == 'ko-matrix'" }, { name = "pandas", extras = ["hdf5"] }, @@ -930,12 +971,11 @@ requires-dist = [ { name = "ruff", marker = "extra == 'dev'" }, { name = "scipy" }, { name = "scipy-stubs", marker = "extra == 'types'" }, - { name = "tqdm", specifier = ">=4.67.1" }, { name = "ty", marker = "extra == 'dev'" }, { name = "typer", specifier = ">0.12.3" }, - { name = "xarray", extras = ["io"] }, + { name = "xarray", extras = ["io"], specifier = ">=2025.1" }, ] -provides-extras = ["ko-matrix", "test", "types", "dev"] +provides-extras = ["ko-matrix", "lazy", "test", "types", "dev"] [[package]] name = "imagesize" @@ -1064,26 +1104,17 @@ wheels = [ [[package]] name = "llvmlite" -version = "0.48.0" +version = "0.36.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/66/6b2c49c7c68da48d17059882fdb9ad9ac9e5ac3f22b00874d7996e3c44a8/llvmlite-0.36.0.tar.gz", hash = "sha256:765128fdf5f149ed0b889ffbe2b05eb1717f8e20a5c87fa2b4018fbcce0fcfc9", size = 126219, upload-time = "2021-03-12T13:41:52.064Z" } + +[[package]] +name = "locket" +version = "1.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/a0/acc8ffcd5bdc63df0097e22c719bfcd61b604358343089313a8aebbb24ab/llvmlite-0.48.0.tar.gz", hash = "sha256:543b19f9ef8f3c7c60d1468191e4ee1b1537bf9f8a3d56f64c0ddd98de92edd2", size = 184016, upload-time = "2026-07-02T20:20:05.308Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/83/97b29fe05cb6ae28d2dbd30b81e2e402a3eed5f460c26e9eaa5895ceacf5/locket-1.0.0.tar.gz", hash = "sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632", size = 4350, upload-time = "2022-04-20T22:04:44.312Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/a2/28696a9e61e245d1a79816d29d106692a90a2b6e7d78c98b326db70827af/llvmlite-0.48.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:d66c3beb4209087ddd4cf4ed2a0856b6887e6a913bdcf1aacfec9851cf2cba4e", size = 40480651, upload-time = "2026-07-01T18:41:35.694Z" }, - { url = "https://files.pythonhosted.org/packages/80/f2/72409351db66d0a317ec5087e076f31fb7b773a640db8a90ce6b5cac9edd/llvmlite-0.48.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:416fa4c2c66c2c6dc6d0a402648c19206e548efa0aa1eff01ad5cdad0af8217d", size = 59890118, upload-time = "2026-07-01T18:41:44.886Z" }, - { url = "https://files.pythonhosted.org/packages/3a/27/5ae2f3722606360480707adb47f001ad89df8251d06b14ee80336e660b66/llvmlite-0.48.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5e5a5131045b72345c71062ea1a91910dde913792b6c9b28ebb2c1c0a712e98", size = 58343459, upload-time = "2026-07-01T18:41:40.306Z" }, - { url = "https://files.pythonhosted.org/packages/16/78/d824ffff7521cd140dc2006e44ce2bc82e64b48d1b32e90e956308c85a74/llvmlite-0.48.0-cp312-cp312-win_amd64.whl", hash = "sha256:d45c7541a80934ec6d8ab0defe67439494ecd2193cbf852a44ba827808976ac1", size = 41865022, upload-time = "2026-07-01T18:41:48.663Z" }, - { url = "https://files.pythonhosted.org/packages/9c/23/fe9316d14626b42c73ef0b502e724705a6ee9450afe53759c0a99c37c2d7/llvmlite-0.48.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a83a99ef0c05b4ccddf9b6218ed9fe84b653a0caf7c1d9dbe148d6d16c67f518", size = 40480652, upload-time = "2026-07-01T18:41:52.216Z" }, - { url = "https://files.pythonhosted.org/packages/1b/4a/90715fa12006d681270b08d881195b6fab3ec39572e048764a1f7f59fed7/llvmlite-0.48.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8761b9e522f55207e24424fcd98370289eec2710bf8e915c82d1053f642450dc", size = 59890120, upload-time = "2026-07-01T18:42:00.748Z" }, - { url = "https://files.pythonhosted.org/packages/70/5e/7b3e20d64650ca3c80af0cdb664ec4b575ec83d9d4dd05bea8bd31f9bbb6/llvmlite-0.48.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fe5cb59b2063bfa039dcb8ca6481c0181bf552f340d10dcf61d7996a665556e", size = 58343457, upload-time = "2026-07-01T18:41:56.41Z" }, - { url = "https://files.pythonhosted.org/packages/17/97/5a430055d1838cf1fb7a01cfa943300f5e4c026fc6333a522c5e4a03b0c1/llvmlite-0.48.0-cp313-cp313-win_amd64.whl", hash = "sha256:91c7e24e74cde3f02b88aa5acca678373f9e069f3b98531b3dbb3a142d9d10bb", size = 41865022, upload-time = "2026-07-01T18:42:04.57Z" }, - { url = "https://files.pythonhosted.org/packages/8d/8e/8170f2e0c217f88069c333d85bb976e536b332aecfcce606ddbdb249385f/llvmlite-0.48.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:321f1ac39b462603f0b589751aecf2d237d056f6d005749c1752b6f23ec3f074", size = 40480650, upload-time = "2026-07-01T18:42:07.935Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e1/05b50692b647cac3c18200ac485b04f342f00ed173c9cc46767274469a15/llvmlite-0.48.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:05f0103c8f2f96a37441337e3643863c01b8e83e530aff38960dcb383c54a065", size = 59890115, upload-time = "2026-07-01T18:42:17.805Z" }, - { url = "https://files.pythonhosted.org/packages/f7/c3/470b8c4ff9ae2db2f9cf5c3e73de76ed908a32788ae9eb5602d43e6a476b/llvmlite-0.48.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37d66fae72802175b0bfe1ea06e624b51e2d7aee6c3c34bbd09739b8f88e8e0b", size = 58343457, upload-time = "2026-07-01T18:42:13.217Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2d/6a5171fb7236ac0895e1a02ccba3735bf291e8597239aa6421894d3c0ba8/llvmlite-0.48.0-cp314-cp314-win_amd64.whl", hash = "sha256:966dcab0a598e2bd8fb5f2cc082cf7b07bae564fc485a3a8692393caf986facf", size = 42986372, upload-time = "2026-07-01T18:42:21.483Z" }, - { url = "https://files.pythonhosted.org/packages/94/e3/7a93e09c9f94e637ca90209ceef0334a9a1d45b0bdb7c92ff922d25d6187/llvmlite-0.48.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7a5c413317050a1d67c34708bde97707f9b2257ef1017f7532d21fe7d9a9ff30", size = 40480654, upload-time = "2026-07-01T18:42:25.076Z" }, - { url = "https://files.pythonhosted.org/packages/27/98/a29133b4728671a175f7d616fab8b1c6e1d8c269d1523581d3160697bfb1/llvmlite-0.48.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d9f952dff6c350c529997423d4fa43abae9722a884ac7ffafc37a3af676e7db", size = 59890119, upload-time = "2026-07-01T18:42:33.88Z" }, - { url = "https://files.pythonhosted.org/packages/1a/cf/7aac11a1f1c7ec54b60c7f6814e87561fb6b55b2f290455d7941eb113420/llvmlite-0.48.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:054aa7d46595935565f276cf0c1659b4f10929c996dd4a606875fae26fba2a23", size = 58343460, upload-time = "2026-07-01T18:42:29.545Z" }, - { url = "https://files.pythonhosted.org/packages/db/41/b96f440c7df5ebba07872cad4e30fbc3560387755b1ea0b629adb76d5ca8/llvmlite-0.48.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d0b3c61aac83b42fb48cc96bffbf57c81b82b2aa92276b7ed6420c814629a99a", size = 42986383, upload-time = "2026-07-01T18:42:37.544Z" }, + { url = "https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3", size = 4398, upload-time = "2022-04-20T22:04:42.23Z" }, ] [[package]] @@ -1442,31 +1473,14 @@ wheels = [ [[package]] name = "numba" -version = "0.66.0" +version = "0.53.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "llvmlite" }, { name = "numpy" }, + { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/a0/570e3dc53e5602b49108f62a13e529f1eec8bfc7ef37d49c825924dcf546/numba-0.66.0.tar.gz", hash = "sha256:b900e63a0e26c05ea9a6d5a3a5a0a177cb64c5011887bf43edb8c3ed2c38d363", size = 2806181, upload-time = "2026-07-01T23:12:46.36Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a3/70deb7f88461c1cd5d16aa990c2380604102661a427667b8950dcdccc27f/numba-0.66.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:53ca5900b7cab15109796030113a6b28576bae5ad7bb507ad6dd1360ddd81ba4", size = 2727264, upload-time = "2026-07-01T23:12:18.669Z" }, - { url = "https://files.pythonhosted.org/packages/2d/55/25c319845e9a4e08f16611ddbda56a192eb7b6ed13e1a2bff2da272ffb97/numba-0.66.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0999e3ee1b18c48e1fb51d11af35ef59852c7f4f50569c9550c25faef0616ad1", size = 3866252, upload-time = "2026-07-01T23:12:20.429Z" }, - { url = "https://files.pythonhosted.org/packages/71/ef/a82d6fd6bf1b0fe461651e924d3647eeec9ac17f8eee4896264bf7480930/numba-0.66.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efe0d2d5099790df945e0cb6e1b3104bd965d7bbfac50d62f1d5d1d6ade0825d", size = 3566974, upload-time = "2026-07-01T23:12:22.116Z" }, - { url = "https://files.pythonhosted.org/packages/fc/eb/9e6171e378822ab191c7abcfd3d8cfc8644516f6c7834c22e210e4acc070/numba-0.66.0-cp312-cp312-win_amd64.whl", hash = "sha256:b075a4e7ebc43dc6294f223e2821659656209fd5e0ce53245877c23d66d6e1a9", size = 2797403, upload-time = "2026-07-01T23:12:23.724Z" }, - { url = "https://files.pythonhosted.org/packages/03/52/176c02d005c5c5143cde10a85bbcdcb6236d9e34c3aac089380e0506cd1d/numba-0.66.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:380b2556a2019ccd1e956ae77dd257eaa39403f7520768b626d44b755112785e", size = 2727084, upload-time = "2026-07-01T23:12:25.434Z" }, - { url = "https://files.pythonhosted.org/packages/44/b5/e930010965568fe7f2c6c962fd2849d458cb9f62c3ab7584af8a19a2b40a/numba-0.66.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:939316d5d8619751207b8972a67852b5a7646665298cb4de693cd6bf135152f4", size = 3873663, upload-time = "2026-07-01T23:12:27.308Z" }, - { url = "https://files.pythonhosted.org/packages/d0/ec/5b51457cbe96e4831141d83e892e65191b23a1b78728456c62909d231ace/numba-0.66.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdf506775d9f02eb92a87bf5c5b1e0d25506fd18cafd769f4ed914a8feac73e7", size = 3573529, upload-time = "2026-07-01T23:12:28.944Z" }, - { url = "https://files.pythonhosted.org/packages/83/7e/cea7710e96913d3c7f2999f16db1b28e6c5be5171cbf40f77f98333a7243/numba-0.66.0-cp313-cp313-win_amd64.whl", hash = "sha256:c5bfe5350284509ab0474390321454c3a8627a188af5b68c910e83df3e2db4a7", size = 2797247, upload-time = "2026-07-01T23:12:30.774Z" }, - { url = "https://files.pythonhosted.org/packages/96/7a/7e0e73550eb4e41ede6e72fb5371f4539537a4d770a3b73fa9b61aea0622/numba-0.66.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:46ae5f2b19e2af3c33c2df100306a90ea2f981c8158b0390f8bf6c20eee7357e", size = 2727296, upload-time = "2026-07-01T23:12:32.39Z" }, - { url = "https://files.pythonhosted.org/packages/0f/26/885774c006de6620ed3d10f45d8e20fe0b8e6aad6d573211a2cbc8b3e528/numba-0.66.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e2b101f23b8b63978d334574d2039f27f0dccfe1d891756f33a2e2f3e4c88cf4", size = 3842720, upload-time = "2026-07-01T23:12:33.938Z" }, - { url = "https://files.pythonhosted.org/packages/93/99/edebf7de890b73973d839dd971cf73734adfb81ffa1b4504f84b9059c3e5/numba-0.66.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63b943eb2c9ba371908ce2cd6dfc643db51fc40f7966993376a1701bc922f537", size = 3543537, upload-time = "2026-07-01T23:12:35.566Z" }, - { url = "https://files.pythonhosted.org/packages/66/c5/b46ad28ac3681d035ea21365c5e052149062e1a0a9affd0563d2760ea6ff/numba-0.66.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd57790acd20f6a468e0ad333ef6b82355e309a92310fb7dff80e919f01a21a9", size = 2799250, upload-time = "2026-07-01T23:12:37.154Z" }, - { url = "https://files.pythonhosted.org/packages/10/6f/5e77a7397a37dd16f57a7b72e7e470db5227b68e3639df0d13a8e674883d/numba-0.66.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:db7735d15ea17a283d6485b9fa3504769f78fd86e5146638ad5e8da57c031b9e", size = 2730342, upload-time = "2026-07-01T23:12:38.758Z" }, - { url = "https://files.pythonhosted.org/packages/39/fd/e9c9680a3813f3d781c20e5d53c1074801b787d4feecca0472fdd7c05ce1/numba-0.66.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:651a2b53298340956db26ecbe7ab043106b50a40c2807e66d617a4917245a4ab", size = 3878695, upload-time = "2026-07-01T23:12:40.302Z" }, - { url = "https://files.pythonhosted.org/packages/61/3a/9b363287b85fcd4537ea3878793822878b2ac1008a78159d2096fea628de/numba-0.66.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c1144ba1720ea59ad79f4f488ed54d149b2613b357f7e445678b7d0739c70e9", size = 3596323, upload-time = "2026-07-01T23:12:42.805Z" }, - { url = "https://files.pythonhosted.org/packages/4c/f2/dca53d50b8f2289dd01954ace9da261e0487d5b74b188b4304e4ecc3492c/numba-0.66.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d426178fb991a85714c43112a8ea7b9d9579ea856ad8dcdb9c1c3941903ba5be", size = 2804772, upload-time = "2026-07-01T23:12:44.399Z" }, -] +sdist = { url = "https://files.pythonhosted.org/packages/e3/7d/3d61160836e49f40913741c464f119551c15ed371c1d91ea50308495b93b/numba-0.53.1.tar.gz", hash = "sha256:9cd4e5216acdc66c4e9dab2dfd22ddb5bef151185c070d4a3cd8e78638aff5b0", size = 2213956, upload-time = "2021-03-26T09:15:50.402Z" } [[package]] name = "numcodecs" @@ -1544,75 +1558,65 @@ wheels = [ [[package]] name = "numpy" -version = "2.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, - { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, - { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, - { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, - { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, - { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, - { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, - { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, - { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, - { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, - { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, - { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, - { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, - { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, - { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, - { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, - { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, - { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, - { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, - { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, - { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, - { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, - { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, - { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, - { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, - { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, - { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, - { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, - { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, - { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, - { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, - { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, - { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, - { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, - { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, - { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, - { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, - { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, - { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, - { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, - { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, - { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, - { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, - { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, - { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, +version = "2.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/7b/14687aa674250e5e546f616f486b0d56d3631cd5b2415739141ce40bdcea/numpy-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277", size = 16801574, upload-time = "2026-07-04T17:06:12.423Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/cc5bb2a3f2913d27d6dbb2c78d25921fabaedc6741d4a5a615a11f3c5bf3/numpy-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1", size = 11772250, upload-time = "2026-07-04T17:06:15.726Z" }, + { url = "https://files.pythonhosted.org/packages/42/77/fdf34a71dd30f54979b18603bee915e0aaf825b07afe79acd60b04b691e2/numpy-2.5.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0", size = 5331516, upload-time = "2026-07-04T17:06:17.913Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e2/eb7efa015b4cce41e2517bf182a7fce0d7d5b9d9ed76a29bfa0f4fe4505c/numpy-2.5.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e", size = 6664863, upload-time = "2026-07-04T17:06:20.02Z" }, + { url = "https://files.pythonhosted.org/packages/a9/4b/a2b32dd94ee9ffbeecb28152240042a3949db33b1c834d44090b80e1b3b8/numpy-2.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75", size = 15167977, upload-time = "2026-07-04T17:06:21.621Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/6e73d68500f80773f65f0654ea932019d6694329a0eb0ed0533de38df376/numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca", size = 16672469, upload-time = "2026-07-04T17:06:24.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/7d/ad3e59015135f5261c95fd4cafeff159c955febd83a99a1d9250c4233815/numpy-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3", size = 16527531, upload-time = "2026-07-04T17:06:26.69Z" }, + { url = "https://files.pythonhosted.org/packages/83/d0/a39b2fbcde9cb17a1dac678f254b33a6336298af9df338824c685425d5e8/numpy-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9", size = 18431940, upload-time = "2026-07-04T17:06:29.521Z" }, + { url = "https://files.pythonhosted.org/packages/04/12/cff070947791c1ed425ff76413189adbdc2fbe215eba7ce7fa454a03c7f8/numpy-2.5.1-cp312-cp312-win32.whl", hash = "sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2", size = 6066764, upload-time = "2026-07-04T17:06:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/65/66/53f31807a48a750f9d748da273bc3fcedd12b27ff1f3e373bfec55ef2dc0/numpy-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2", size = 12430966, upload-time = "2026-07-04T17:06:34.926Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2a/d1a88066b1c14186f5d3c0d18c94f17b064511982bab0578d49ee9d43c29/numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b", size = 10350488, upload-time = "2026-07-04T17:06:37.785Z" }, + { url = "https://files.pythonhosted.org/packages/eb/07/ec2a3f0c91761581d4b7104a740791800025983f9a4dc4e73f91a99aeac4/numpy-2.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1", size = 16796419, upload-time = "2026-07-04T17:06:40.37Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ab/ddb499fc4f8780354395face5b65c7fd107bcd6e1d667a5f07d046956f6f/numpy-2.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6", size = 11765832, upload-time = "2026-07-04T17:06:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/88/b3/3c28c558a09fc72100c646dac6d2fce8e834c471b0edca01a29996706117/numpy-2.5.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d", size = 5325143, upload-time = "2026-07-04T17:06:45.466Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0e/ce19b985bb15c596f4f05954e76cccc77c845083b3b8f938a6c68e523128/numpy-2.5.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1", size = 6659749, upload-time = "2026-07-04T17:06:47.288Z" }, + { url = "https://files.pythonhosted.org/packages/2e/20/1ee6614d64332a1bba6411f38e68cb79eec1b2459e20a623777c5c5492a2/numpy-2.5.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd", size = 15164716, upload-time = "2026-07-04T17:06:49.494Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a7/2bcd3fdbb87804755c35b729bf8709d62025c5f4cfd7d5b2415997097515/numpy-2.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a", size = 16661440, upload-time = "2026-07-04T17:06:52.061Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d7/a41e3310c886fe457d36e670bbf24fae411aca8a7b6ad92a32afd924077c/numpy-2.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7", size = 16526305, upload-time = "2026-07-04T17:06:54.605Z" }, + { url = "https://files.pythonhosted.org/packages/53/75/4333a9a707c1edd3a4e1a0c58eca52c0f31e55089fa80db02b5565b24df7/numpy-2.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6", size = 18423008, upload-time = "2026-07-04T17:06:57.54Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/e314a32b1c11a2ffe818ddad3a57b50b4b6e1b6c487192eb50cdef0415d0/numpy-2.5.1-cp313-cp313-win32.whl", hash = "sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9", size = 6063885, upload-time = "2026-07-04T17:07:00.14Z" }, + { url = "https://files.pythonhosted.org/packages/10/70/800b3fca480af32df9e8ea9f3d4a0c8feb4b32d7f195d174eabbda4829ad/numpy-2.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74", size = 12425674, upload-time = "2026-07-04T17:07:02.387Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0b/196350c122f50f6ca56846f2d71efd5e0d24b7b2e07355e019b2e2c7a11e/numpy-2.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107", size = 10350256, upload-time = "2026-07-04T17:07:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/db/f4/731b6085a83faf6ca843394cbd5e217280c214399f7e8b21b9f552af0ae2/numpy-2.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7c786fe9a5bbe360022e584c5a34cf6b54265c71bd7ec8ac3d8fec38968071f8", size = 16795063, upload-time = "2026-07-04T17:07:07.374Z" }, + { url = "https://files.pythonhosted.org/packages/bf/64/0e215f2048dd11a55bb989ed41b3585ef57452404e638d703a211a3e4157/numpy-2.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32985c896d897419ef8da6917872d80b78ad0ea26d85b23245c7366ffde76d75", size = 11776652, upload-time = "2026-07-04T17:07:09.907Z" }, + { url = "https://files.pythonhosted.org/packages/b5/59/2b844c7a6e9deff69b404a66221e1542937734f65d5e6e39411876053862/numpy-2.5.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:efd736408cc97c79b9e6917338dfc8f06013b2274f992e96b1d9a81a71e2a2c2", size = 5335944, upload-time = "2026-07-04T17:07:12.227Z" }, + { url = "https://files.pythonhosted.org/packages/86/51/9bf7cb2cabcebc9e017e4ec7e6322b378317a542c08b4cb68479c1efc716/numpy-2.5.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ab84dc6b074fa881cae55bea94cc4f68e285181ba7f32497bf7dee6b1496165b", size = 6656266, upload-time = "2026-07-04T17:07:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/83/3e/fb7615b211b82a32f44d5180a6d421b61f84d4fadd578b48ba4ac34e189f/numpy-2.5.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95", size = 15179720, upload-time = "2026-07-04T17:07:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/0f992cb24560673496c5d68de61913b57166ce530ffda07c1f280e0cc464/numpy-2.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54ad769f17bc2d833b620851989f62054fb9ab93c969d9e1dc3c8e3d56beea21", size = 16664835, upload-time = "2026-07-04T17:07:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/97d6475ee91afe2587797d09446f9d3e475ad4cb681662d824809327b75a/numpy-2.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c12afb53450fa976d4c681c50a7423729a4c51c0465ed9f32b8a9cabbc472373", size = 16539135, upload-time = "2026-07-04T17:07:22.015Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/4db81e4ba0be7e2776b1de68c82aa862c7f8ec27e1b4927d4ae075e20678/numpy-2.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c11c405efc5ff6816d5983c96cdfa215bab3428961243af3ff59b228490438", size = 18426684, upload-time = "2026-07-04T17:07:24.941Z" }, + { url = "https://files.pythonhosted.org/packages/1f/64/c0ba2d90724d450279a7df8f32057241070250a26a7e2b5337d77347f481/numpy-2.5.1-cp314-cp314-win32.whl", hash = "sha256:f2479a47f8d5932d1718168a681ad6e536a9df484c83cfcf9de365e164537ace", size = 6116103, upload-time = "2026-07-04T17:07:27.622Z" }, + { url = "https://files.pythonhosted.org/packages/c1/1a/837f9ed7405adcd7a40538792eb169eddd8fa5630c16a1ef49dae71a30f4/numpy-2.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:24d0eb82c0541d3415a33425db64ae439dffccd7b4dbcb30e7c35120205c506a", size = 12562177, upload-time = "2026-07-04T17:07:29.887Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/49707938b6dd0a78a9178dd93227dc89e4c11af47f5c798d70366e8d0483/numpy-2.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5a4c988b38d261deeeaad9954e3deb091ad905c94e8bb6708654ef1d97f286b0", size = 10627739, upload-time = "2026-07-04T17:07:32.568Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c7/bb4b882cfe7f299cbc8b66e42e7dd78cf9d14e40f9469fc5e3db7e15b3bd/numpy-2.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a33276be12fa045805f477f22482088b66bb758ffbe89a9d21457de863a32e22", size = 11894709, upload-time = "2026-07-04T17:07:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/40/3f/5af7f4a7f6224aef48017aa82bb6174c7a659d724be0c75017b7e64a55b4/numpy-2.5.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f089d7b00756190aacf1f5d34bdf38c3c430ac82b4f868f8cede73380460fce7", size = 5453810, upload-time = "2026-07-04T17:07:37.495Z" }, + { url = "https://files.pythonhosted.org/packages/20/c9/3474309bc94d634d3f9c3eddf03250ecb8c22cd948ef16fef69a77cc5d7b/numpy-2.5.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:09e9bfd8d2cf479c7d174804fb3811c53a8e9f20a37444008606b57d6b7a826d", size = 6761189, upload-time = "2026-07-04T17:07:39.563Z" }, + { url = "https://files.pythonhosted.org/packages/90/8a/558ae39fdd55d7e7f7fef9a84a6e964ac6b23edbd2a07e52bb084500507d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e68d8dd1e7eba712948f2053a29ec86917bc70ba1358df869d9f06649ef9cf09", size = 15225039, upload-time = "2026-07-04T17:07:41.682Z" }, + { url = "https://files.pythonhosted.org/packages/63/27/ca7392b2d030277bdf0273e7d23255b3ee57d57a7c170a6f4fb3981e1e5d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99d5095fa265a0c4152e7bb12759e14381ef5496152f1ce58f44bdf55c44beb4", size = 16701306, upload-time = "2026-07-04T17:07:44.611Z" }, + { url = "https://files.pythonhosted.org/packages/02/42/03d53ae7996c44d4374a8262e9dc41671fd56cbb98f7d47ef85cf5da4c6b/numpy-2.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ab87a91b3cc3382b8956095bd8f95e00cf679bb81554339be1a2ba404a1473c1", size = 16589955, upload-time = "2026-07-04T17:07:47.694Z" }, + { url = "https://files.pythonhosted.org/packages/7b/15/6c1784ae469640e65db111e9a34b3d0f14d91e8a38b9ce34810ced370dbb/numpy-2.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:224ca51130ef7da85bea2191625181cb4f337f9cb64b471f10c1a12aa8b60077", size = 18464252, upload-time = "2026-07-04T17:07:50.684Z" }, + { url = "https://files.pythonhosted.org/packages/94/a8/f98e50356cf167df656c526c2dfeec2d7dde182f2a3da4b458a5938e2776/numpy-2.5.1-cp314-cp314t-win32.whl", hash = "sha256:6eab239876581b2b3c5a242281b6007bbdbcd1c7085d7709bb57c5929b11e6bf", size = 6263298, upload-time = "2026-07-04T17:07:53.445Z" }, + { url = "https://files.pythonhosted.org/packages/72/ac/96ae880cdecad0b3275d9359fcec72667b49a4863c9f12942e43679dda02/numpy-2.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:83ce9c80d5b521b0d77ddcbe5447c218d247929b6cc056ca5351342accfff0af", size = 12748623, upload-time = "2026-07-04T17:07:55.384Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5a/4d2b1601df3602dba7a14f3348ba9bfe94a18adb428e693df6154c293831/numpy-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb", size = 10697674, upload-time = "2026-07-04T17:07:58.506Z" }, ] [[package]] name = "numpy-typing-compat" -version = "20260602.2.4" +version = "20260602.2.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/73/e331473d3db84a8e8883ac07bfd63a8ce9eb7196acbb672bda1f5b8d3294/numpy_typing_compat-20260602.2.4.tar.gz", hash = "sha256:e4eb661f312a7ad5805677967d5879e04fd7b97627fe910121ce7b1f43aa748c", size = 4603, upload-time = "2026-06-02T15:52:38.572Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/db/5cd1d99caea4bf39fd477686ded4b9b70dff3c7673b5d84ef2d96a4f5aab/numpy_typing_compat-20260602.2.5.tar.gz", hash = "sha256:1885a678e9a24564839ed5d1711c0031735fb7de7f0b5ed88d550e5d45a8d4f9", size = 4593, upload-time = "2026-06-02T15:52:39.331Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/a8/94811eedac4cef5ef7df4b24e06715fa371724782e86a4573f5d172c8473/numpy_typing_compat-20260602.2.4-py3-none-any.whl", hash = "sha256:78d33917d5f6921f8d1c549db347a5b8d9768853e36796b08208c81f5b620977", size = 5879, upload-time = "2026-06-02T15:52:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/b1/a4/9376b38b7387a0296b1f626b966e5503578625c9673777db1b45bf70acb0/numpy_typing_compat-20260602.2.5-py3-none-any.whl", hash = "sha256:21ba7757c8924d359a9ed3ab2163c282a70983ae64498fdba6d1892a6641c8b1", size = 5881, upload-time = "2026-06-02T15:52:34.167Z" }, ] [[package]] @@ -1734,14 +1738,27 @@ hdf5 = [ [[package]] name = "pandas-stubs" -version = "3.0.3.260530" +version = "3.0.5.260730" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/aa/c41a8a0ff86fd85dbb3ec0c1f3fa488ca64a8b5f82654ae1b07d84acefe5/pandas_stubs-3.0.3.260530.tar.gz", hash = "sha256:d1efe47b2e5a312c047d7feabec5cb7a55365747983420077e9fcbe9ab74f714", size = 113183, upload-time = "2026-05-30T17:47:40.34Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/d2/dea4a3a56b7b5f69c5fbca9f14625fcf28e1a39a657e9833d4a10bcac593/pandas_stubs-3.0.5.260730.tar.gz", hash = "sha256:f70a232c57d93a5a2c81f8a53953e10891a5374bc92652277deb325e2e4d0ff3", size = 114631, upload-time = "2026-07-30T14:31:42.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/c2/959caec5c46f484b5f8bb6def4b0cf7a45ba6acda26f12b75142d98cc5ae/pandas_stubs-3.0.5.260730-py3-none-any.whl", hash = "sha256:60e90e3e1eda6937e337e243cbe6217e151c11137cd7eddf832af537c7310bfd", size = 174807, upload-time = "2026-07-30T14:31:41.17Z" }, +] + +[[package]] +name = "partd" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "locket" }, + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/3a/3f06f34820a31257ddcabdfafc2672c5816be79c7e353b02c1f318daa7d4/partd-1.4.2.tar.gz", hash = "sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c", size = 21029, upload-time = "2024-05-06T19:51:41.945Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/e0/99ec5b02203c4e9ce878bc63d8caa06ac1f891e4d63bded9a5ced70fcb4f/pandas_stubs-3.0.3.260530-py3-none-any.whl", hash = "sha256:a6277eb1c8cebf48d9b2413fcd2e9a6b4ff479c934a223c29eacbc3058c4cb55", size = 173780, upload-time = "2026-05-30T17:47:39.13Z" }, + { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" }, ] [[package]] @@ -2297,27 +2314,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" }, - { url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" }, - { url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" }, - { url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" }, - { url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" }, - { url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" }, - { url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" }, - { url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" }, - { url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" }, - { url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" }, - { url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" }, - { url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" }, - { url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" }, - { url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" }, - { url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" }, - { url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" }, +version = "0.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/25/7113f6d5498888c5fb7db34081cba7d5971c4cb1bfb26819966eee68f003/ruff-0.16.1.tar.gz", hash = "sha256:fedad7c801dabd3fb9741d76aca39246e6ddd9ca446a015875207bf19f1e6bc7", size = 4877500, upload-time = "2026-07-30T19:37:01.379Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/bd/694da69368e0973de65df2ddc73ab18d43c469d5963d9b150911de6bc513/ruff-0.16.1-py3-none-linux_armv6l.whl", hash = "sha256:58edb313b88f0c5460a26adf5f39a37a3be789494a15e3e411e35fa78b89f9a0", size = 10839126, upload-time = "2026-07-30T19:36:13.697Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f0/b626e5d5bd0dd9576263658ef12885e2288afd1029a48e26ffed65ec1ac1/ruff-0.16.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fde5a99e2f97479af66edd6622c6d5a2a7592c77cf4153d9e4428f5eeb55b60c", size = 11070253, upload-time = "2026-07-30T19:36:17.14Z" }, + { url = "https://files.pythonhosted.org/packages/83/63/f40acfb6b35b88623e71684942b552c3edd96035f5d98f313815f7b277de/ruff-0.16.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e0d4c20532fca4f7fa609369161d968dd28f65d83dabbd61d8e9c7edbf7001f6", size = 10561425, upload-time = "2026-07-30T19:36:20.04Z" }, + { url = "https://files.pythonhosted.org/packages/aa/dd/14ec0e9c2b4d315547dd38765004b4863e354e1b52cb308272215d9f6f6d/ruff-0.16.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30affbcedf59ad5703d9c91f82266e02b47739f797e1a7b6e158e5526a6dae38", size = 10948879, upload-time = "2026-07-30T19:36:22.476Z" }, + { url = "https://files.pythonhosted.org/packages/33/e9/9d870cbae575030fdef595f04b4b97573c525b5497cce4f4498cf2f85446/ruff-0.16.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24e9c631573cbca9d20f1283f8f479b2afa4a8503504822bd71a293889f16743", size = 10643691, upload-time = "2026-07-30T19:36:24.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/09/12743d544e2173f53ecd27217c65f90d2bc0f8424a66a60339e56bbc0457/ruff-0.16.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b41bdd48fb420987a9b5212e4957c26ad4abce401fa9ea9d4d85843727945f4f", size = 11435354, upload-time = "2026-07-30T19:36:28.447Z" }, + { url = "https://files.pythonhosted.org/packages/7f/89/a1652b2daee52083c9554a6333b678a8b01d0400f976827bb87857f9449a/ruff-0.16.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0d1e1393b7648079e13669de1c1f4fde06d4583e84d8fd5c1551e0a77a2aa75", size = 12259033, upload-time = "2026-07-30T19:36:31.326Z" }, + { url = "https://files.pythonhosted.org/packages/16/96/ecdcb8c54ee7b123b487f807eb014e6e019155a0b81dfb669acd52f28ce3/ruff-0.16.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07bf434b1c95f4e093be4532068ef4fcf00924eb2ade8796075980902d6fd54a", size = 11667981, upload-time = "2026-07-30T19:36:34.394Z" }, + { url = "https://files.pythonhosted.org/packages/cd/90/c52e12e0d862e9572f2a33aa227409143520abe53111e9a6babbac7b4af8/ruff-0.16.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39897739f112253ee4fdd2e8aa9a4f9ded99fb2be367d5f31dfa4ded6025584c", size = 11468183, upload-time = "2026-07-30T19:36:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6b/4ffb7ad1d83eb16cf8cbb3c8815d3f11c88460fd162d4b372a2059be1c2a/ruff-0.16.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82ae3c0c0d74daf17b968a10b7b3bb3ef297ab7de0c1f749646b25e690ccb150", size = 11470071, upload-time = "2026-07-30T19:36:39.91Z" }, + { url = "https://files.pythonhosted.org/packages/9c/72/32ae7db4c0b5e32ab611787caa19d1546800676d79f7483b7100a3561bf4/ruff-0.16.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4d5f2ed10f8242d83fc08d521301089364e3375375705356f20c0e31606ef3ef", size = 10919503, upload-time = "2026-07-30T19:36:42.65Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ca/3d901ba6ad6fc38da39c3448fc6c59ac945679293a17c3ceb6d6c1cba13e/ruff-0.16.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a4665b309891f83f3e3c25447935f1213e9abbd4b5640af7a1f2def9f8d413c1", size = 10649861, upload-time = "2026-07-30T19:36:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/92/79/894ef1ced26552d5f8c9cf6d85b0687840e1128c55aeab7b9c2d54a0d880/ruff-0.16.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26e9ca5c9bc3971f20d3cf18a957f52ffd6a5f6564ff15c4912a144dcac22494", size = 11148137, upload-time = "2026-07-30T19:36:47.936Z" }, + { url = "https://files.pythonhosted.org/packages/2d/69/3609a09fa1cb46cc28b762363e440a354204e5dff01bd0c8d7437874d6b9/ruff-0.16.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:67e1e1e3fa4f0c82f0e36d4cd61e661f6e7a6196cb1aa92fe0828fa7b8f257cd", size = 11559211, upload-time = "2026-07-30T19:36:50.448Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8a/fb22af2fd78a736e241fabf67e30ce1799a64244026377a49e133af90762/ruff-0.16.1-py3-none-win32.whl", hash = "sha256:d31765e131295b8445caf301e3e8a85b34d1b9b211b4109b7ba457888b051806", size = 10838258, upload-time = "2026-07-30T19:36:53.298Z" }, + { url = "https://files.pythonhosted.org/packages/d4/35/e57fd9fb5d423961df087a00b12d42c0a830288dc2f3b45ecca299158b4f/ruff-0.16.1-py3-none-win_amd64.whl", hash = "sha256:09b05e8b90c2cb06ad63464350e7a45e8e44a2dfe52072ebfba6666ca8d3f596", size = 11961111, upload-time = "2026-07-30T19:36:56.107Z" }, + { url = "https://files.pythonhosted.org/packages/cb/46/240ea004bf6dc4feb40e9832f2205a476a47dd5b8a3f8211a5fc5f95e20e/ruff-0.16.1-py3-none-win_arm64.whl", hash = "sha256:dbaadaac38c70239f056d306b7476f246b0bf000fa6b3876402acbf5b227eaf8", size = 11309414, upload-time = "2026-07-30T19:36:58.79Z" }, ] [[package]] @@ -2691,57 +2708,54 @@ wheels = [ ] [[package]] -name = "tqdm" -version = "4.69.0" +name = "toolz" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8c/69/40407dfc835517f058b603dbf37a6df094d8582b015a51eddc988febbcb7/tqdm-4.69.0.tar.gz", hash = "sha256:700c5e85dcd5f009dd6222588a29180a193a748247a5d855b4d67db93d79a53b", size = 792569, upload-time = "2026-07-17T18:09:06.2Z" } +sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/21/99a0cdaf54eb35e77623c41b5a2c9472ee4404bba687052791fe2aba6773/tqdm-4.69.0-py3-none-any.whl", hash = "sha256:9979978912be667a6ef21fd5d8abf54e324e63d82f7f43c360792ebc2bc4e622", size = 676680, upload-time = "2026-07-17T18:09:04.172Z" }, + { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" }, ] [[package]] name = "trimesh" -version = "4.12.2" +version = "5.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/37/5cb90f04990260d2caceb6093560c6cefafca1ec522c1e43be01ca658244/trimesh-4.12.2.tar.gz", hash = "sha256:c8ca31571ac00b112e4e160e66a2d4c3491df321f056bd33806be0485d1af9d9", size = 842220, upload-time = "2026-05-01T00:57:43.333Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/45/5b9943631f265073bb96ca4cc2a9de05d151cd8c1a6896fcbaac132a1355/trimesh-5.0.0.tar.gz", hash = "sha256:0195003198baaf2550aebe612254ba2f01c385cec46c37ecc5beca8291f79a13", size = 864019, upload-time = "2026-08-01T00:05:25.042Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/98/716a473cfb24750858ddd5d14e6527539dd206583a46408d08eeb2844a75/trimesh-4.12.2-py3-none-any.whl", hash = "sha256:b5b5afa63c5272345f2858f7676bc8c217dc8a89f4fadf6193fe10a81b5ff2aa", size = 741043, upload-time = "2026-05-01T00:57:40.763Z" }, + { url = "https://files.pythonhosted.org/packages/37/82/19a03ba344ecb66ea8caab697b3059e0fbea576420f99945944c479caa78/trimesh-5.0.0-py3-none-any.whl", hash = "sha256:51ec67d7f9f74b918f2a695da5fd511b9c084e96307648ae83f45b58f13f8009", size = 745494, upload-time = "2026-08-01T00:05:23.172Z" }, ] [[package]] name = "ty" -version = "0.0.63" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/ce/cbeaa5c7576fec643609dfbf200d59493523b1cc0481d4e7a5effcbf0630/ty-0.0.63.tar.gz", hash = "sha256:c2f66439393b3acac69306c117d4ae44638ce5fffa4a20c21046e85bd473359f", size = 6280695, upload-time = "2026-07-23T11:41:39.845Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/e4/d17a8e113ab15c692fe6fb9c422112d4bee7e829d80ced35826b45d98d98/ty-0.0.63-py3-none-linux_armv6l.whl", hash = "sha256:9a4ef7782e3af314fb63d006bec5ac3025bd70fee774b4dcfb5e44e8564f1994", size = 12056302, upload-time = "2026-07-23T11:41:03.5Z" }, - { url = "https://files.pythonhosted.org/packages/be/0b/357234c815dc4bfcc88c3f860aa0983fe4228c8aa2a5bb16c35ee08a94af/ty-0.0.63-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:01eab0ab70d51ad10298aa2d4b058b387a1fe93e5ee52d2a1ee23e9c69ba8354", size = 11737674, upload-time = "2026-07-23T11:41:05.862Z" }, - { url = "https://files.pythonhosted.org/packages/1c/13/193d9aeeb6774690351cff9fafabd3ae9b54cc225d125e07fa004ce23bdc/ty-0.0.63-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a671b61eaad16178389e05b9c108c9cb75ae8d84968fe55906484f55d6268338", size = 11264191, upload-time = "2026-07-23T11:41:07.963Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b7/c5843e16759a2b25fc8a7197473474038e585ac9fdaa6fa16aeb6384bf6a/ty-0.0.63-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b255cc83d95c51bb9ee4931303fdbece8cb1d6d7c655eb77a3f2c8f349fb64d6", size = 11818890, upload-time = "2026-07-23T11:41:09.885Z" }, - { url = "https://files.pythonhosted.org/packages/06/20/adf83ae1fc570d9bb449605e1eeeaa523ad2329ca838a636698b5c135d11/ty-0.0.63-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0da1e8183aa4f893421a173904478021498f542ee41273660538da6649a9631c", size = 11853141, upload-time = "2026-07-23T11:41:12.151Z" }, - { url = "https://files.pythonhosted.org/packages/ea/90/f8effd846e3ee13486ea08257c13094d58b9f188c5641bf609d6a7d5c09f/ty-0.0.63-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:128f38eb5a67199e3811426386f7ec96f41251ddf45e04ddc0bcbb29d4853245", size = 12545184, upload-time = "2026-07-23T11:41:14.4Z" }, - { url = "https://files.pythonhosted.org/packages/b8/76/aac3a30d40431eb1d329acea016b248f5b6255e30ef3d28e19447e344be2/ty-0.0.63-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bedf34aff8b0557f2a7119b314a68a9c9e58c1d21554d0e2748f2c5fe6a1f638", size = 13062375, upload-time = "2026-07-23T11:41:16.441Z" }, - { url = "https://files.pythonhosted.org/packages/62/3d/0158733932893e17f6008dadd74c8d7aed422fefc66e47fe77888014fe8c/ty-0.0.63-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7328d63c34587606dce02935a25404f54cd0161bfe413b8f7fc21d174aead612", size = 12619461, upload-time = "2026-07-23T11:41:18.538Z" }, - { url = "https://files.pythonhosted.org/packages/f5/be/e280ad095b050778f16f493d49a073fa5f6d8f301d3e2e59be6a672ba05c/ty-0.0.63-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504c4457f3a62afe836c1f26a2c9a12549299095f9cc4146778558df51a7515c", size = 12376402, upload-time = "2026-07-23T11:41:20.482Z" }, - { url = "https://files.pythonhosted.org/packages/57/57/619788bf335cb86b090470b557ae1eed33613dc24e12142505d32b462e58/ty-0.0.63-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7394ed39424b027c89d5f0c818871c791e33958b88f5bb13e14bd4a12a3ca631", size = 12671377, upload-time = "2026-07-23T11:41:22.489Z" }, - { url = "https://files.pythonhosted.org/packages/07/a2/0aff2cdd3c98e2c4729337542b8da0ce1980790e1600e0c4a717a1f46293/ty-0.0.63-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:94c3f155230490f8fb505911655e940c630c25cc0c35a76690cb413254fb4437", size = 11768090, upload-time = "2026-07-23T11:41:24.558Z" }, - { url = "https://files.pythonhosted.org/packages/43/4a/cdb5f1d26154144dfee08e1bd671daf827238170f7cee9dad43990bb2016/ty-0.0.63-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c16e1d8b4f0ae99106d5f13a894034a202baf6cdab61e2bb1a239de82904f839", size = 11867747, upload-time = "2026-07-23T11:41:26.794Z" }, - { url = "https://files.pythonhosted.org/packages/7e/26/ecc09ecb70bc9fbfdf42fa57ff29568f173e8200df575a0d72dc2b8486f9/ty-0.0.63-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b66293dad89eaed4b9fbf3b661614dbeb85b59ba037178a3f9a0adedf264da5c", size = 12120000, upload-time = "2026-07-23T11:41:28.731Z" }, - { url = "https://files.pythonhosted.org/packages/14/07/862822f9c2c397785b69b25cf79b4dfc3c0d55684b9adf11ac194450f8e1/ty-0.0.63-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8b29cc832e2ec73502c97dd7325d6ae0e085b34a33529a0f785192317d9862fc", size = 12477862, upload-time = "2026-07-23T11:41:30.847Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c2/50b08b641578d6f48e8aa28a91d0de32c91aa24dd926ed199e8afd2d37ea/ty-0.0.63-py3-none-win32.whl", hash = "sha256:f0a8fbfd1f990c0c5d85cce018d438969ac79e49247e2192c9745394bb7d17ab", size = 11433155, upload-time = "2026-07-23T11:41:33.28Z" }, - { url = "https://files.pythonhosted.org/packages/92/2d/d422a5568f0d1f317186be22241288dc6e08b71dc24aca223f22038ac2d2/ty-0.0.63-py3-none-win_amd64.whl", hash = "sha256:2aa2370bdd6f42e9f37518c812379bef70b9162f9e5aad511495229b0b71cb93", size = 12450036, upload-time = "2026-07-23T11:41:35.559Z" }, - { url = "https://files.pythonhosted.org/packages/35/97/2c9748e28ead0650c7ad3e5f74f178832ceabd7cb5c272a882f29eb32ee4/ty-0.0.63-py3-none-win_arm64.whl", hash = "sha256:95ac1a62162c3c7ac204731e95ebf766d62a46a7bfa238a6acbe923fcb772cb1", size = 11826243, upload-time = "2026-07-23T11:41:37.749Z" }, +version = "0.0.66" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/58/4f6ab2a86589e422a3cf840bcf6114c565e4c39ddf4d0b7cd328af5b52b4/ty-0.0.66.tar.gz", hash = "sha256:24bddd4479ce445b51ac015410dd2d34af1cadd62a77f5b3cb269149ed83f9b5", size = 6520402, upload-time = "2026-08-04T01:09:47.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/4d/bbc28310d6d887ef73e5800f062c4bf54caa35a1b47d70c7b03d0515ecf1/ty-0.0.66-py3-none-linux_armv6l.whl", hash = "sha256:8b46450438b54b732338e4d7a78a7d2f5e1a012a13d77d121aacaca20fb814e2", size = 12409743, upload-time = "2026-08-04T01:09:01.229Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/c3d2eb2242fa0a2ef445ca2c7009dc9118e5b3dcb8b8a8bec70d58c8e4bc/ty-0.0.66-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8e4adbe662bc3c62b52b83d46b07f703fdc3c123bb72601606c58be5ea017ed3", size = 12078362, upload-time = "2026-08-04T01:09:04.233Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/7a4b5e45d701a8de6b714dacf6ffca91b0411baaceaa781d494037623a18/ty-0.0.66-py3-none-macosx_11_0_arm64.whl", hash = "sha256:776814351735847eb934f9a3cbea21d2278ba14aa0fc099f683da11bc2d5c90a", size = 11583289, upload-time = "2026-08-04T01:09:06.973Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/fbb1f71ee2999f981f8c4b3b139231e4bcff4ede0c3b37e858fd1334ed26/ty-0.0.66-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cca1da877f613965b954bfd22d495386e260ec767c5536c8022cca98a260ea5d", size = 12137274, upload-time = "2026-08-04T01:09:09.592Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/9e0662f6603a5ac171ae6b314396e677ead4b45695fc948efb0a4837051f/ty-0.0.66-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa523e777bc36c0fbf8ded5844096f46cbfc3712fe5a003351a94259b7e86cf4", size = 12207047, upload-time = "2026-08-04T01:09:12.533Z" }, + { url = "https://files.pythonhosted.org/packages/bb/cc/12e01bc2ea47fa7bf20fc6cdd3249d6a340be9be4edc52b1d77256245dd0/ty-0.0.66-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f7993c1f95f80a4e44056e6aaf8e46e608d778db131ae5ce59262ab59358f9a", size = 12919304, upload-time = "2026-08-04T01:09:15.175Z" }, + { url = "https://files.pythonhosted.org/packages/75/88/c6c0d3a8e71c9cdb560cc5c627a4bba6c47b5eec460b2f8c449b57c6d03a/ty-0.0.66-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b29354bcbac9f53b6952d8f46789bd81eeb9bdd7a68df7d26e654ca7498c3c", size = 13470963, upload-time = "2026-08-04T01:09:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/27/c2/2f8c18063412ad80e1b3f87afce7bd50982b4a048166607b67f80660a9fb/ty-0.0.66-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbf4e5325f7f584d9c346946e7c415b2e3cd3b8f1119d468082a90a6afd020ce", size = 13244773, upload-time = "2026-08-04T01:09:20.59Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f6/fdae2b95831116dffc055ad53a99a8f21437263651047b3ad46def4950a3/ty-0.0.66-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bd304363764bd723c22fb20b17035c345420fdf66fee856934b158ebde08a91", size = 12751343, upload-time = "2026-08-04T01:09:23.225Z" }, + { url = "https://files.pythonhosted.org/packages/14/1e/7e75f0371d11463a256dc2bee98b0df401e0fa06021e200b8b601d21949d/ty-0.0.66-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:205d8589bd957ea9718d488b731b2fbdd0d1b1cefd37c79f52c0deb7cafddfef", size = 13068057, upload-time = "2026-08-04T01:09:26.052Z" }, + { url = "https://files.pythonhosted.org/packages/b4/84/f20f24518f6f0bea936e2e668ede250b9ce0774624559931599bd1f42772/ty-0.0.66-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7304a1df54741a2343801354f41d7ad87974acb541ee3e93c26cb6a1a0b863af", size = 12082318, upload-time = "2026-08-04T01:09:28.877Z" }, + { url = "https://files.pythonhosted.org/packages/08/20/70ca0eac2427d4a58a81a3a9426b20e46fb4a5a13aefc9edc2e5172e1243/ty-0.0.66-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:cf2c062863e5da0f588b0b120fec0da2e81c0913ee4cd07b50d77aa60ffd8deb", size = 12228978, upload-time = "2026-08-04T01:09:31.905Z" }, + { url = "https://files.pythonhosted.org/packages/7c/01/5a461ab34456d788248780830aed673c9e0e796f6e9dd92d3dbf02e04d7f/ty-0.0.66-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a58d32c879d86428978332adf21e85009ec269314a23d330c3483c65b57aedc7", size = 12471917, upload-time = "2026-08-04T01:09:34.418Z" }, + { url = "https://files.pythonhosted.org/packages/46/ed/f8eb5eff7c9ee644490c6d2626425935c097acc54871adf659ea70624a2c/ty-0.0.66-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b2f810fa3516c977630d78dbe9161592b3c27029fa9bb81074366624d9b5e4b6", size = 12858086, upload-time = "2026-08-04T01:09:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/02/7f/8a78361f752274a550a7f6f07f127b246ece7d7fdde31121d22074e46eab/ty-0.0.66-py3-none-win32.whl", hash = "sha256:d28c3df565a387c1c5ea359aa452a46d19163616ef71be819058a424a62f1ae1", size = 11782494, upload-time = "2026-08-04T01:09:40.136Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e2/deed22b823ce309b8410d50414fd15afe9e90aee8b8ca04789ba55c21231/ty-0.0.66-py3-none-win_amd64.whl", hash = "sha256:e3a457f3312c078f24c47d0da6e4f73de34d0a77ed2de22571c066c80b2fd5e7", size = 12893338, upload-time = "2026-08-04T01:09:42.825Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ce/6c828f42ef1ed39f53f57f9c0cdcdf03e23666fa7a29d799042a2c34bcb4/ty-0.0.66-py3-none-win_arm64.whl", hash = "sha256:2f62ae247b9c75674fcc060635f9f00210357da7681de59234d97b50fb9e9e94", size = 12227381, upload-time = "2026-08-04T01:09:45.365Z" }, ] [[package]] name = "typer" -version = "0.27.0" +version = "0.27.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -2749,9 +2763,9 @@ dependencies = [ { name = "rich" }, { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/37/78/fda3361b56efc27944f24225f6ecd13d96d6fcfe37bd0eb34e2f4c63f9fc/typer-0.27.0.tar.gz", hash = "sha256:629bd12ea5d13a17148125d9a264f949eb171fb3f120f9b04d85873cab054fa5", size = 203430, upload-time = "2026-07-15T19:21:07.007Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/40/4a3db7990d1f62a53182aa96eaef57aeb2886a27f90a195bc66713565d31/typer-0.27.1.tar.gz", hash = "sha256:a79bef8469a79c45498e7b814ecf8d603cc7644e9acbd9e19cac0334240b18df", size = 203994, upload-time = "2026-08-03T14:41:03.438Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/03/26a383c9e58c213199d1aad1c3d353cfc22d4444ec6d2c0bf8ad02523843/typer-0.27.0-py3-none-any.whl", hash = "sha256:6f4b27631e47f077871b7dc30e933ec0131c1390fbe0e387ea5574b5bac9ccf1", size = 122716, upload-time = "2026-07-15T19:21:05.553Z" }, + { url = "https://files.pythonhosted.org/packages/43/89/9518bc0c3929bee36b3a4a8e3daddd6e03f92f9961c66d4983b837160543/typer-0.27.1-py3-none-any.whl", hash = "sha256:53150287edd11baeb4e4722c8e394fcdf8181c0ae89485cba8d25c778d5edd56", size = 122874, upload-time = "2026-08-03T14:41:04.391Z" }, ] [[package]] @@ -2807,14 +2821,14 @@ wheels = [ [[package]] name = "webob" -version = "1.8.10" +version = "1.8.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "legacy-cgi", marker = "python_full_version >= '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/f9/974eafebfd0bd442b8848899fe7d30675c93f750c313e1a6fe61acbde1e3/webob-1.8.10.tar.gz", hash = "sha256:1c963a11f307bc3f624fbab9dde737701eae255f32981b7a5486a88db1767c2b", size = 280796, upload-time = "2026-06-02T19:56:47.268Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/92/9329872e36be90e25ba616ac6e9ee8c1be4326a1ea3fdffcdabd0eb48efb/webob-1.8.11.tar.gz", hash = "sha256:aa8c27231070b135c025e567a9cd7eda03f4df71352ffaac740cb6a75f0f81a5", size = 286047, upload-time = "2026-08-02T06:26:26.063Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/21/fce134877fb6fc6ad3c464e4a07ede0ee9219f705d26a981ae58ea36ca13/webob-1.8.10-py2.py3-none-any.whl", hash = "sha256:e68ad87fda378191081965ab02a185391c26e4e926adec855c3b0286a8369d49", size = 115825, upload-time = "2026-06-02T19:56:44.765Z" }, + { url = "https://files.pythonhosted.org/packages/c1/71/370a65bc7726a1cbf478445c898c7affe907aa42e2e9e5bb38319db74115/webob-1.8.11-py2.py3-none-any.whl", hash = "sha256:4addd1d38d6a7fbe0eda22d45f25a40d74c8b290f3a99c0ac3d4023cf21f2da2", size = 118319, upload-time = "2026-08-02T06:26:24.765Z" }, ] [[package]] @@ -2845,7 +2859,7 @@ io = [ [[package]] name = "zarr" -version = "3.2.1" +version = "3.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "donfig" }, @@ -2855,7 +2869,7 @@ dependencies = [ { name = "packaging" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/8d/aeb164004f87543b06ef54f885d02c342c31ceb274e2bbec470a98927621/zarr-3.2.1.tar.gz", hash = "sha256:71565b738a0e7e8ed226f0516eba8c6bb53440ad7669a8c48ebb3534a161d035", size = 675161, upload-time = "2026-05-05T12:37:22.383Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/15/436cb1d3bbe86173bd44ce7a34ecb210d0c0416946e337858149a905ef5a/zarr-3.3.0.tar.gz", hash = "sha256:cd0c8cf738b4bb4807815bc1255acad5bdf1a7b7264b606c5a1bc0d0392a306b", size = 943626, upload-time = "2026-07-30T16:35:10.491Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/0a/469e2bd01be1490336e6c8707386845655d59261543315778a3ccc7e8019/zarr-3.2.1-py3-none-any.whl", hash = "sha256:f78cdd3d9687ad0e9f9cba2c5683b64f0c52589c19f685eeabe872e93cc0d2c7", size = 319617, upload-time = "2026-05-05T12:37:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c6/6b726ddf4c3ac5a123f285c3650fa8268902c28ea541a0282867f1336e65/zarr-3.3.0-py3-none-any.whl", hash = "sha256:323bf5366d4f909052ef6e2e03e7481a7434c3ee75d3a981eb3a71fc1ae22cef", size = 363685, upload-time = "2026-07-30T16:35:08.794Z" }, ]