diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 616044d9a..9ae69745d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -91,6 +91,33 @@ jobs: run: uv run --frozen pytest --cov=viscy_data --cov-report=term-missing working-directory: packages/viscy-data + test-utils: + name: Test Utils (Python ${{ matrix.python-version }}, ubuntu-latest) + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + python-version: ["3.12", "3.13"] + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Set up uv with Python ${{ matrix.python-version }} + uses: astral-sh/setup-uv@v7 + with: + python-version: ${{ matrix.python-version }} + enable-cache: true + cache-suffix: ubuntu-latest-${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync --frozen --all-extras --dev + working-directory: packages/viscy-utils + + - name: Run tests with coverage + run: uv run --frozen pytest --cov=viscy_utils --cov-report=term-missing + working-directory: packages/viscy-utils + test-applications: name: Test (${{ matrix.application }}, Python 3.13, ubuntu-latest) runs-on: ubuntu-latest @@ -119,7 +146,7 @@ jobs: working-directory: applications/${{ matrix.application }} test-dynacell-configs: - name: Test dynacell benchmark configs (Python 3.13, ubuntu-latest) + name: Test dynacell configs and regressions (Python 3.13, ubuntu-latest) runs-on: ubuntu-latest steps: @@ -139,10 +166,10 @@ jobs: # (falls back to numpy/scikit-image without CUDA), so the eval extra # installs and runs on a GPU-less runner. eval_gpu (cupy/cucim) is NOT # installed — those are the only CUDA-only deps. - run: uv sync --frozen --extra eval --group test + run: uv sync --frozen --package dynacell --extra eval --group test working-directory: applications/dynacell - - name: Run benchmark-schema + submit-tool + eval-runtime tests + - name: Run benchmark configs and pipeline regressions # test_benchmark_config_composition + test_submit_benchmark_job cover # config composition + the launcher. test_runtime + the two # test_evaluation_pipeline_parallel* suites cover the runtime module, @@ -153,20 +180,23 @@ jobs: # needs the eval stack). test_evaluation_grouped drives the # multi-condition driver against the same cache-only fixture. run: | - uv run --frozen pytest \ + uv run --frozen --no-sync pytest \ tests/test_benchmark_config_composition.py \ tests/test_submit_benchmark_job.py \ tests/test_runtime.py \ tests/test_evaluation_pipeline_parallel.py \ tests/test_evaluation_pipeline_parallel_cpu.py \ tests/test_evaluation_grouped.py \ + tests/test_engine.py \ + tests/test_pipeline_cache.py \ + src/dynacell/evaluation/feature_metrics_test.py \ -v working-directory: applications/dynacell check: name: All tests pass if: always() - needs: [test, test-data, test-data-extras, test-applications, test-dynacell-configs] + needs: [test, test-data, test-data-extras, test-utils, test-applications, test-dynacell-configs] runs-on: ubuntu-latest steps: - name: Verify all test jobs succeeded diff --git a/CLAUDE.md b/CLAUDE.md index 6801f8eba..cb0afc6bf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,19 +4,7 @@ VisCy is a **uv workspace monorepo** for virtual staining and computational microscopy. Sub-packages live under `packages/`. -## Repo Layout - -``` -pyproject.toml # Root config (ruff, pytest, uv workspace) -packages/ - viscy-data/ # Data loading and Lightning DataModules - viscy-models/ # Neural network architectures - viscy-transforms/ # Image transforms -src/viscy/ # Umbrella package (re-exports) -applications/ # Self-contained research applications -``` - -### Packages vs Applications +## Packages vs Applications - **Shared code belongs in `packages/`**, not in applications. - **Applications must not import from each other.** If two applications need the same logic, move it to an existing package or create a new one. @@ -30,16 +18,6 @@ applications/ # Self-contained research applications Use `uv` package manager. Run commands with `uv run `. Edit `pyproject.toml` to modify dependencies and sync to update `uv.lock`. -```sh -uv venv -p 3.13 -uv sync --all-packages --all-extras -``` - -If `uv` is not installed: -```sh -curl -LsSf https://astral.sh/uv/install.sh | sh -``` - On HPC, symlink the uv cache out of your home directory first: ```sh mkdir -p /hpc/mydata/firstname.lastname/.cache/uv && ln -s /hpc/mydata/firstname.lastname/.cache/uv ~/.cache/uv @@ -98,25 +76,12 @@ Examples (verified against the `applications/dynacell/configs/benchmarks/virtual When in doubt, read both `train_dataloader` overrides directly — they are short. Don't infer from comments alone. -### Common Commands - -```sh -uvx ruff check packages/ # lint -uvx ruff check --fix packages/ # lint + auto-fix -uvx ruff format packages/ # format -uv run pytest # all tests -``` - ### Testing -```sh -uv run pytest # all tests -uv run pytest packages/viscy-data/ # single package (data) -uv run pytest packages/viscy-models/ # single package (models) -``` - Prefer `{file}_test.py` in the same directory as `{file}.py`, unless there are import issues, in which case use `tests/`. +Run the suite in parallel with `pytest -n ` (pytest-xdist, in the `test` group). The root `conftest.py` pins each worker to one OpenMP/BLAS thread. Without that pin, torch in every worker grabs every core and the parallel run is 10x slower than it should be. `-n auto` counts the cores the shell may use, so inside a 1-CPU `srun` step it runs a single worker; give the step more CPUs (`srun --overlap -c N`) first. + --- ## Project Conventions @@ -137,10 +102,6 @@ Prefer `{file}_test.py` in the same directory as `{file}.py`, unless there are i ### Code Style -- Docstrings use **numpy style** (`convention = "numpy"`). -- Lint rules: `D, E, F, I, NPY, PD, W`. -- `D` rules are ignored in `**/tests/**` and notebooks. -- Format: double quotes, spaces, 120 char line length. - Use a subagent to run tests and complex bash commands, especially those expected to return complex output. - Run independent tasks (multi-file edits across separate concerns, cross-cutting verifications, distinct review angles) in parallel via concurrent subagents in a single message. Subagent startup overhead is negligible relative to sequential blocking. Only sequence subagents when a later task needs an earlier task's output. @@ -187,17 +148,7 @@ Ask yourself if your test is actually covering the true function. ### Coding Philosophy -#### 1. Think Before Coding - -Don't assume. Don't hide confusion. Surface tradeoffs. - -Before implementing: -- State your assumptions explicitly. If uncertain, ask. -- If multiple interpretations exist, present them — don't pick silently. -- If a simpler approach exists, say so. Push back when warranted. -- If something is unclear, stop. Name what's confusing. Ask. - -#### 2. Simplicity First +#### 1. Simplicity First Minimum code that solves the problem. Nothing speculative. @@ -208,7 +159,7 @@ Minimum code that solves the problem. Nothing speculative. - If you write 200 lines and it could be 50, rewrite it. - Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. -#### 3. Surgical Changes +#### 2. Surgical Changes Touch only what you must. Clean up only your own mess. @@ -223,18 +174,3 @@ When your changes create orphans: - Don't remove pre-existing dead code unless asked. The test: every changed line should trace directly to the user's request. - -#### 4. Goal-Driven Execution - -Define success criteria. Loop until verified. - -Transform tasks into verifiable goals: -- "Add validation" → "Write tests for invalid inputs, then make them pass" -- "Fix the bug" → "Write a test that reproduces it, then make it pass" -- "Refactor X" → "Ensure tests pass before and after" - -For multi-step tasks, state a brief plan: -1. [Step] → verify: [check] -2. [Step] → verify: [check] - -Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. diff --git a/applications/cytoland/examples/configs/recipes/modes/spotlight.yml b/applications/cytoland/examples/configs/recipes/modes/spotlight.yml index be9b5d295..cbe4e51d0 100644 --- a/applications/cytoland/examples/configs/recipes/modes/spotlight.yml +++ b/applications/cytoland/examples/configs/recipes/modes/spotlight.yml @@ -1,9 +1,10 @@ # Mode recipe: Spotlight foreground-aware loss (Kalinin et al. 2025). # Cross-cutting: sets loss, fg_mask_key, nonzero filtering, AND -# Otsu-centered normalization. fg_threshold: 0.0 is valid ONLY -# because normalizations use subtrahend: otsu_threshold. +# Otsu-centered normalization. The Otsu-centred target is what makes 0 the +# FG/BG boundary in the model's output space -- which the Dice term needs +# regardless of fg_threshold, since that field is inert here (see below). # -# Requires: viscy preprocess --compute_otsu --compute_fg_masks +# Requires: viscy preprocess --compute_otsu true --compute_fg_masks true # # Base ordering: this mode must come AFTER the data recipe in the # base: list so that its normalizations replace the data default. @@ -14,6 +15,10 @@ model: init_args: lambda_mse: 0.5 sigmoid_k: -0.95 + # INERT while data.fg_mask_key is set below: forward() short-circuits + # on `if fg_mask is not None`, so the mask from the store defines + # foreground and this threshold is never consulted. It applies only if + # fg_mask_key is removed. Same for data.nonzero_threshold. fg_threshold: 0.0 data: init_args: diff --git a/applications/cytoland/examples/configs/vscyto3d/train_spotlight.yml b/applications/cytoland/examples/configs/vscyto3d/train_spotlight.yml index a5cbdd25c..7a423f970 100644 --- a/applications/cytoland/examples/configs/vscyto3d/train_spotlight.yml +++ b/applications/cytoland/examples/configs/vscyto3d/train_spotlight.yml @@ -1,5 +1,5 @@ # VSCyto3D: supervised training with Spotlight foreground-aware loss. -# Requires: viscy preprocess --compute_otsu --compute_fg_masks +# Requires: viscy preprocess --compute_otsu true --compute_fg_masks true # Usage: python -m cytoland fit --config vscyto3d/train_spotlight.yml base: - ../recipes/trainer/fit.yml diff --git a/applications/cytoland/examples/vcp_tutorials/hek293t.ipynb b/applications/cytoland/examples/vcp_tutorials/hek293t.ipynb new file mode 100644 index 000000000..35e5b4401 --- /dev/null +++ b/applications/cytoland/examples/vcp_tutorials/hek293t.ipynb @@ -0,0 +1,368 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d0990bbc", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Cytoland Tutorial: Virtual Staining of HEK293T Cells with VSCyto3D\n", + "\n", + "**Estimated time to complete:** 15 minutes" + ] + }, + { + "cell_type": "markdown", + "id": "02b87aa0", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Learning Goals\n", + "\n", + "* Download the VSCyto3D model and an example dataset containing HEK293T cell images.\n", + "* Pre-compute normalization statistics for the images using the `viscy preprocess` command line interface (CLI).\n", + "* Run inference for joint virtual staining of cell nuclei and plasma membrane via the `viscy predict` CLI.\n", + "* Compare virtually and experimentally stained cells and see how virtual staining can rescue missing labels." + ] + }, + { + "cell_type": "markdown", + "id": "dc3ac201", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Prerequisites\n", + "\n", + "Python>=3.12" + ] + }, + { + "cell_type": "markdown", + "id": "ff9bed71", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Introduction\n", + "\n", + "See the [model card](https://virtualcellmodels.cziscience.com/paper/cytoland2025)\n", + "for more details about the Cytoland models.\n", + "\n", + "VSCyto3D is a 3D UNeXt2 model that has been trained on A549, HEK293T, and hiPSC cells using the Cytoland approach.\n", + "This model enables users to jointly stain cell nuclei and plasma membranes from 3D label-free images\n", + "for downstream analysis such as cell segmentation and tracking without the need for human annotation of volumetric data." + ] + }, + { + "cell_type": "markdown", + "id": "e96a3709", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Setup\n", + "\n", + "The commands below will install the required packages and download the example dataset and model checkpoint.\n", + "It may take a **few minutes** to download all the files.\n", + "\n", + "## Setup Google Colab\n", + "\n", + "To run this quick-start guide using Google Colab,\n", + "choose the 'T4' GPU runtime from the \"Connect\" dropdown menu\n", + "in the upper-right corner of this notebook for faster execution.\n", + "Using a GPU significantly speeds up running model inference, but CPU compute can also be used.\n", + "\n", + "## Setup Local Environment\n", + "\n", + "The commands below assume a Unix-like shell with `wget` installed.\n", + "On Windows, the files can be downloaded manually from the URLs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ca08f540", + "metadata": {}, + "outputs": [], + "source": [ + "# Install the modular Cytoland/VisCy packages required for this example\n", + "# `cytoland`, `viscy-utils` and `viscy-data` are installed from the GitHub\n", + "# monorepo: the prediction writer needs a viscy-data newer than its PyPI release.\n", + "# Here stackview and ipycanvas are installed for visualization\n", + "!pip install \"viscy-utils @ git+https://github.com/mehta-lab/VisCy.git#subdirectory=packages/viscy-utils\" \"viscy-data @ git+https://github.com/mehta-lab/VisCy.git#subdirectory=packages/viscy-data\" \"cytoland @ git+https://github.com/mehta-lab/VisCy.git#subdirectory=applications/cytoland\" stackview ipycanvas==0.11" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5673cf4", + "metadata": {}, + "outputs": [], + "source": [ + "# Restart kernel if running in Google Colab\n", + "# This is required to use the packages installed above\n", + "# The 'kernel crashed' message is expected here\n", + "if \"get_ipython\" in globals():\n", + " session = get_ipython() # noqa: F821\n", + " if \"google.colab\" in str(session):\n", + " print(\"Shutting down colab session.\")\n", + " session.kernel.do_shutdown(restart=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c70d5e2", + "metadata": {}, + "outputs": [], + "source": [ + "# Download the example dataset\n", + "!wget -m -np -nH --cut-dirs=5 -R \"index.html*\" \"https://public.czbiohub.org/comp.micro/viscy/VS_datasets/VSCyto3D/test/HEK293T-Phase3D-H2B-CAAX-example.zarr/\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fc9db7a0", + "metadata": {}, + "outputs": [], + "source": [ + "# Rename the downloaded dataset to what the example prediction config expects (`input.ome.zarr`)\n", + "# And validate the OME-Zarr metadata with iohub\n", + "!mv HEK293T-Phase3D-H2B-CAAX-example.zarr input.ome.zarr\n", + "!iohub info -v input.ome.zarr" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d2a2d1e7", + "metadata": {}, + "outputs": [], + "source": [ + "# Download the VSCyto3D model checkpoint and prediction config\n", + "!wget \"https://public.czbiohub.org/comp.micro/viscy/VS_models/VSCyto3D/epoch=83-step=14532-loss=0.492.ckpt\"\n", + "!wget \"https://public.czbiohub.org/comp.micro/viscy/VS_models/VSCyto3D/predict.yml\"" + ] + }, + { + "cell_type": "markdown", + "id": "46fdb6cb", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Use Case\n", + "\n", + "## Example Dataset\n", + "\n", + "The HEK293T example dataset used in this quick-start guide contains\n", + "quantitative phase and paired fluorescence images of cell nuclei and plasma membrane.\n", + "It is a subset (one cropped region of interest) from a test set used to evaluate the VSCyto3D model.\n", + "The full dataset can be downloaded from the\n", + "[BioImage Archive](https://www.ebi.ac.uk/biostudies/BioImages/studies/S-BIAD1702).\n", + "\n", + "Refer to our [preprint](https://doi.org/10.1101/2024.05.31.596901) for more details\n", + "about how the dataset and model were generated.\n", + "\n", + "## Using Custom Data\n", + "\n", + "The model only requires label-free images for inference.\n", + "To run inference on your own data,\n", + "convert them into the OME-Zarr data format using iohub or other\n", + "[tools](https://ngff.openmicroscopy.org/tools/index.html#file-conversion),\n", + "and edit the `predict.yml` file to specify the input data path.\n", + "Specifically, the `data.init_args.data_path` field should be updated:\n", + "\n", + "```diff\n", + "- data_path: input.ome.zarr\n", + "+ data_path: /path/to/your.ome.zarr\n", + "```\n", + "\n", + "The image may need to be resampled to roughly match the voxel size of the example dataset\n", + "(0.2x0.1x0.1 µm, ZYX)." + ] + }, + { + "cell_type": "markdown", + "id": "2d84aad9", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Run Model Inference\n", + "\n", + "On Google Colab, the preprocessing step takes about **1 minute**,\n", + "and the inference step takes about **2 minutes** (T4 GPU)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "383fb379", + "metadata": {}, + "outputs": [], + "source": [ + "# Run the CLI command to pre-compute normalization statistics\n", + "# This includes the median and interquartile range (IQR)\n", + "# Used to shift and scale the intensity distribution of the input images\n", + "!viscy preprocess --data_path=input.ome.zarr" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9c324ee5", + "metadata": {}, + "outputs": [], + "source": [ + "# Run the CLI command to run inference\n", + "!viscy predict -c predict.yml" + ] + }, + { + "cell_type": "markdown", + "id": "12de2060", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Analysis of Model Outputs\n", + "\n", + "Visualize the experimental and virtually stained images using the `stackview` package." + ] + }, + { + "cell_type": "markdown", + "id": "a2761744", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "Visualizing large 3D multichannel images in a Jupyter notebook\n", + "**is prone to performance issues and may crash the notebook** if the images are too large\n", + "(the free Colab instances have limited CPU cores and memory).\n", + "The visualization code below is only intended for demonstration.\n", + "We strongly recommend downloading the images (from the 'files' bar in Colab)\n", + "and using a standalone viewer such as [napari](https://napari.org/)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64176a04", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np # noqa: E402\n", + "import stackview # noqa: E402\n", + "from iohub import open_ome_zarr # noqa: E402\n", + "from skimage.exposure import rescale_intensity # noqa: E402\n", + "\n", + "try:\n", + " from google.colab import output\n", + "\n", + " output.enable_custom_widget_manager()\n", + "except ImportError:\n", + " pass" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bbb0b4a2", + "metadata": {}, + "outputs": [], + "source": [ + "# open the images\n", + "def split_and_rescale_channels(timepoint: np.ndarray) -> tuple[np.ndarray, ...]:\n", + " return (rescale_intensity(channel, out_range=(0, 1)) for channel in timepoint)\n", + "\n", + "\n", + "fov_name = \"plate/0/11\"\n", + "input_image = open_ome_zarr(\"input.ome.zarr\")[fov_name][\"0\"]\n", + "prediction_image = open_ome_zarr(\"prediction.ome.zarr\")[fov_name][\"0\"]\n", + "\n", + "phase, fluor_nucleus, fluor_membrane = split_and_rescale_channels(input_image[0])\n", + "vs_nucleus, vs_membrane = split_and_rescale_channels(prediction_image[0])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c6d3cb4", + "metadata": {}, + "outputs": [], + "source": [ + "# Drag the slider to start rendering\n", + "# Click on the numbered buttons to toggle the channels\n", + "stackview.switch(\n", + " # the 0, 1, 2, 3, 4 buttons will correspond to these 5 channels\n", + " # We apply a gamma adjustment to the phase channel to improve visibility in the overlay\n", + " images=[phase**2.5, fluor_nucleus, fluor_membrane, vs_nucleus, vs_membrane],\n", + " colormap=[\"gray\", \"pure_green\", \"pure_magenta\", \"pure_blue\", \"pure_yellow\"],\n", + " toggleable=True,\n", + " zoom_factor=0.5,\n", + " display_min=0.0,\n", + " display_max=0.9,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "2733926b", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "Note how the experimental fluorescence is missing for a subset of cells.\n", + "This is due to loss of genetic labeling.\n", + "The virtually stained images is not affected by this issue and can robustly label all cells." + ] + }, + { + "cell_type": "markdown", + "id": "a8c347af", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Summary\n", + "\n", + "In the above example, we demonstrated how to use the VSCyto3D model\n", + "for virtual staining of cell nuclei and plasma membranes, which can rescue missing labels." + ] + }, + { + "cell_type": "markdown", + "id": "9063adf6", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "## Contact & Feedback\n", + "\n", + "For issues or feedback about this tutorial please contact Ziwen Liu at [ziwen.liu@czbiohub.org](mailto:ziwen.liu@czbiohub.org).\n", + "\n", + "## Responsible Use\n", + "\n", + "We are committed to advancing the responsible development and use of artificial intelligence.\n", + "Please follow our [Acceptable Use Policy](https://virtualcellmodels.cziscience.com/acceptable-use-policy) when engaging with our services.\n", + "\n", + "Should you have any security or privacy issues or questions related to the services,\n", + "please reach out to our team at [security@chanzuckerberg.com](mailto:security@chanzuckerberg.com) or [privacy@chanzuckerberg.com](mailto:privacy@chanzuckerberg.com) respectively." + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "all", + "main_language": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/applications/cytoland/examples/vcp_tutorials/hek293t.py b/applications/cytoland/examples/vcp_tutorials/hek293t.py index d318ff895..2ef77b901 100644 --- a/applications/cytoland/examples/vcp_tutorials/hek293t.py +++ b/applications/cytoland/examples/vcp_tutorials/hek293t.py @@ -56,9 +56,10 @@ # %% # Install the modular Cytoland/VisCy packages required for this example -# See the [repository](https://github.com/mehta-lab/VisCy) for more details +# `cytoland`, `viscy-utils` and `viscy-data` are installed from the GitHub +# monorepo: the prediction writer needs a viscy-data newer than its PyPI release. # Here stackview and ipycanvas are installed for visualization -# !pip install -U cytoland viscy stackview ipycanvas==0.11 +# !pip install "viscy-utils @ git+https://github.com/mehta-lab/VisCy.git#subdirectory=packages/viscy-utils" "viscy-data @ git+https://github.com/mehta-lab/VisCy.git#subdirectory=packages/viscy-data" "cytoland @ git+https://github.com/mehta-lab/VisCy.git#subdirectory=applications/cytoland" stackview ipycanvas==0.11 # %% # Restart kernel if running in Google Colab diff --git a/applications/cytoland/examples/vcp_tutorials/neuromast.ipynb b/applications/cytoland/examples/vcp_tutorials/neuromast.ipynb new file mode 100644 index 000000000..ec3c526c6 --- /dev/null +++ b/applications/cytoland/examples/vcp_tutorials/neuromast.ipynb @@ -0,0 +1,465 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2dd42bb7", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Cytoland Tutorial: Virtual Staining of Zebrafish Neuromasts with VSNeuromast\n", + "\n", + "**Estimated time to complete:** 15 minutes" + ] + }, + { + "cell_type": "markdown", + "id": "e1c38b9f", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Learning Goals\n", + "\n", + "* Download the VSNeuromast model and an example dataset containing time-lapse images of zebrafish neuromasts.\n", + "* Pre-compute normalization statistics for the images using the `viscy preprocess` command-line interface (CLI).\n", + "* Run inference for joint virtual staining of cell nuclei and plasma membrane via the `viscy predict` CLI.\n", + "* Visualize the effect of photobleaching in fluorescence imaging and how virtual staining can mitigate this issue." + ] + }, + { + "cell_type": "markdown", + "id": "c1aa6808", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Prerequisites\n", + "\n", + "Python>=3.12" + ] + }, + { + "cell_type": "markdown", + "id": "bd3b93d4", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Introduction\n", + "\n", + "The zebrafish neuromasts are sensory organs on the lateral lines.\n", + "Given their relatively simple structure and high accessibility to live imaging,\n", + "they are used as a model system to study organogenesis _in vivo_.\n", + "However, multiplexed long-term fluorescence imaging at high spatial-temporal resolution\n", + "is often limited by photobleaching and phototoxicity.\n", + "Also, engineering fish lines with a combination of landmark fluorescent labels\n", + "(e.g. nuclei and plasma membrane) and functional reporters increases experimental complexity.\n", + "\\\n", + "VSNeuromast is a 3D UNeXt2 model that has been trained on images of\n", + "zebrafish neuromasts using the Cytoland approach.\n", + "(See the [model card](https://virtualcellmodels.cziscience.com/paper/cytoland2025)\n", + "for more details about the Cytoland models.)\n", + "This model enables users to jointly stain cell nuclei and plasma membranes from 3D label-free images\n", + "for downstream analysis such as cell segmentation and tracking." + ] + }, + { + "cell_type": "markdown", + "id": "f2a6edfe", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Setup\n", + "\n", + "The commands below will install the required packages and download the example dataset and model checkpoint.\n", + "It may take a **few minutes** to download all the files.\n", + "\n", + "## Setup Google Colab\n", + "\n", + "To run this quick-start guide using Google Colab,\n", + "choose the 'T4' GPU runtime from the \"Connect\" dropdown menu\n", + "in the upper-right corner of this notebook for faster execution.\n", + "Using a GPU significantly speeds up running model inference, but CPU compute can also be used.\n", + "\n", + "## Setup Local Environment\n", + "\n", + "The commands below assume a Unix-like shell with `wget` installed.\n", + "On Windows, the files can be downloaded manually from the URLs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3f0a34a", + "metadata": {}, + "outputs": [], + "source": [ + "# Install the modular Cytoland/VisCy packages required for this example\n", + "# `cytoland`, `viscy-utils` and `viscy-data` are installed from the GitHub\n", + "# monorepo: the prediction writer needs a viscy-data newer than its PyPI release.\n", + "# Here stackview and ipycanvas are installed for visualization, and iohub provides OME-Zarr access\n", + "!pip install \"viscy-utils @ git+https://github.com/mehta-lab/VisCy.git#subdirectory=packages/viscy-utils\" \"viscy-data @ git+https://github.com/mehta-lab/VisCy.git#subdirectory=packages/viscy-data\" \"cytoland @ git+https://github.com/mehta-lab/VisCy.git#subdirectory=applications/cytoland\" iohub stackview ipycanvas" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "540ab976", + "metadata": {}, + "outputs": [], + "source": [ + "# Restart kernel if running in Google Colab\n", + "# This is required to use the packages installed above\n", + "# The 'kernel crashed' message is expected here\n", + "if \"get_ipython\" in globals():\n", + " session = get_ipython()\n", + " if \"google.colab\" in str(session):\n", + " print(\"Shutting down colab session.\")\n", + " session.kernel.do_shutdown(restart=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51cf0bab", + "metadata": {}, + "outputs": [], + "source": [ + "# Download the example dataset\n", + "!wget -m -np -nH --cut-dirs=5 -R \"index.html*\" \"https://public.czbiohub.org/comp.micro/viscy/VS_datasets/VSNeuromast/test/isim-bleaching-example.zarr/\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "250def65", + "metadata": {}, + "outputs": [], + "source": [ + "# Rename the downloaded dataset to what the example prediction config expects (`input.ome.zarr`)\n", + "# And validate the OME-Zarr metadata with iohub\n", + "!mv isim-bleaching-example.zarr input.ome.zarr\n", + "!iohub info -v input.ome.zarr" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88326661", + "metadata": {}, + "outputs": [], + "source": [ + "# Download the VSNeuromast model checkpoint and prediction config\n", + "!wget \"https://public.czbiohub.org/comp.micro/viscy/VS_models/VSNeuromast/epoch=64-step=24960.ckpt\"\n", + "!wget \"https://public.czbiohub.org/comp.micro/viscy/VS_models/VSNeuromast/predict.yml\"" + ] + }, + { + "cell_type": "markdown", + "id": "1022e0e7", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Use Case\n", + "\n", + "## Example Dataset\n", + "\n", + "The neuromast example dataset used in this tutorial contains\n", + "quantitative phase and paired fluorescence images of the cell nuclei and the plasma membrane.\n", + "\\\n", + "**It is a subsampled time-lapse from a test set used to evaluate the VSNeuromast model.**\n", + "\\\n", + "The full dataset can be downloaded from the\n", + "[BioImage Archive](https://www.ebi.ac.uk/biostudies/BioImages/studies/S-BIAD1702).\n", + "\n", + "Refer to our [preprint](https://doi.org/10.1101/2024.05.31.596901) for more details\n", + "about how the dataset and model were generated.\n", + "\n", + "## Using Custom Data\n", + "\n", + "The model only requires label-free images for inference.\n", + "To run inference on your own data,\n", + "convert them into the [OME-Zarr](https://ngff.openmicroscopy.org/)\n", + "data format using iohub or other\n", + "[tools](https://ngff.openmicroscopy.org/tools/index.html#file-conversion),\n", + "and edit the `predict.yml` file to specify the input data path.\n", + "Specifically, the `data.init_args.data_path` field should be updated:\n", + "\n", + "```diff\n", + "- data_path: input.ome.zarr\n", + "+ data_path: /path/to/your.ome.zarr\n", + "```\n", + "\n", + "The image may need to be resampled to roughly match the voxel size of the example dataset\n", + "(0.25x0.108x0.108 µm, ZYX)." + ] + }, + { + "cell_type": "markdown", + "id": "a7482dd5", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Run Model Inference\n", + "\n", + "On Google Colab, the preprocessing step takes about **1 minute**,\n", + "and the inference step takes about **2 minutes** (T4 GPU)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2df952e4", + "metadata": {}, + "outputs": [], + "source": [ + "# Run the CLI command to pre-compute normalization statistics\n", + "# This includes the median and interquartile range (IQR)\n", + "# Used to shift and scale the intensity distribution of the input images\n", + "!viscy preprocess --data_path=input.ome.zarr" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05021fa5", + "metadata": {}, + "outputs": [], + "source": [ + "# Run the CLI command to run inference\n", + "!viscy predict -c predict.yml" + ] + }, + { + "cell_type": "markdown", + "id": "11adee29", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Analysis of Model Outputs\n", + "\n", + "1. Visualize predicted images over time and compare with the fluorescence images.\n", + "2. Measure photobleaching in the fluorescence images\n", + "and how virtual staining can mitigate this issue.\n", + "Since most pixels in the images are background,\n", + "we will use the 99th percentile (brightest 1%)\n", + "of the intensity distribution as a proxy for foreground signal." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3dab72b1", + "metadata": {}, + "outputs": [], + "source": [ + "# imports\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "from cmap import Colormap\n", + "from iohub import open_ome_zarr\n", + "from numpy.typing import NDArray\n", + "from skimage.exposure import rescale_intensity\n", + "\n", + "\n", + "def render_rgb(image: np.ndarray, colormap: Colormap) -> tuple[NDArray, plt.cm.ScalarMappable]:\n", + " \"\"\"Render a 2D grayscale image as RGB using a colormap.\n", + "\n", + " Parameters\n", + " ----------\n", + " image : np.ndarray\n", + " intensity image\n", + " colormap : Colormap\n", + " colormap\n", + "\n", + " Returns\n", + " -------\n", + " tuple[NDArray, plt.cm.ScalarMappable]\n", + " rendered RGB image and the color mapping\n", + " \"\"\"\n", + " image = rescale_intensity(image, out_range=(0, 1))\n", + " image = colormap(image)\n", + " mappable = plt.cm.ScalarMappable(norm=plt.Normalize(0, 1), cmap=colormap.to_matplotlib())\n", + " return image, mappable" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "108cfd0c", + "metadata": {}, + "outputs": [], + "source": [ + "# read a single Z-slice for visualization\n", + "z_slice = 30\n", + "\n", + "with open_ome_zarr(\"input.ome.zarr/0/3/0\") as fluor_store:\n", + " fluor_nucleus = fluor_store[0][:, 1, z_slice]\n", + " fluor_membrane = fluor_store[0][:, 0, z_slice]\n", + "\n", + "with open_ome_zarr(\"prediction.ome.zarr/0/3/0\") as vs_store:\n", + " vs_nucleus = vs_store[0][:, 0, z_slice]\n", + " vs_membrane = vs_store[0][:, 1, z_slice]\n", + "\n", + "\n", + "# Render the images as RGB in false colors\n", + "vs_nucleus_rgb, vs_nucleus_mappable = render_rgb(vs_nucleus, Colormap(\"bop_blue\"))\n", + "vs_membrane_rgb, vs_membrane_mappable = render_rgb(vs_membrane, Colormap(\"bop_orange\"))\n", + "merged_vs = (vs_nucleus_rgb + vs_membrane_rgb).clip(0, 1)\n", + "\n", + "fluor_nucleus_rgb, fluor_nucleus_mappable = render_rgb(fluor_nucleus, Colormap(\"green\"))\n", + "fluor_membrane_rgb, fluor_membrane_mappable = render_rgb(fluor_membrane, Colormap(\"magenta\"))\n", + "merged_fluor = (fluor_nucleus_rgb + fluor_membrane_rgb).clip(0, 1)\n", + "\n", + "# Plot\n", + "fig = plt.figure(figsize=(12, 7), layout=\"constrained\")\n", + "\n", + "images = {\"fluorescence\": merged_fluor, \"virtual staining\": merged_vs}\n", + "\n", + "for row, (subfig, (name, img)) in enumerate(zip(fig.subfigures(nrows=2, ncols=1), images.items())):\n", + " subfig.suptitle(name)\n", + " cax_nuc = subfig.add_axes([1, 0.55, 0.02, 0.3])\n", + " cax_mem = subfig.add_axes([1, 0.15, 0.02, 0.3])\n", + " axes = subfig.subplots(ncols=len(merged_vs))\n", + " for t, ax in enumerate(axes):\n", + " if row == 1:\n", + " ax.set_title(f\"{t * 30} min\", y=-0.1)\n", + " ax.imshow(img[t])\n", + " ax.axis(\"off\")\n", + " if row == 0:\n", + " subfig.colorbar(fluor_nucleus_mappable, cax=cax_nuc, label=\"Nuclei (GFP)\")\n", + " subfig.colorbar(fluor_membrane_mappable, cax=cax_mem, label=\"Membrane (mScarlett)\")\n", + " elif row == 1:\n", + " subfig.colorbar(vs_nucleus_mappable, cax=cax_nuc, label=\"Nuclei (VS)\")\n", + " subfig.colorbar(vs_membrane_mappable, cax=cax_mem, label=\"Membrane (VS)\")\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "5fcd7a29", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "The plasma membrane fluorescence decreases over time,\n", + "while the virtual staining remains stable.\n", + "How significant is this effect? Is it consistent with photobleaching?\n", + "Analysis below will answer these questions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "452ba89e", + "metadata": {}, + "outputs": [], + "source": [ + "def highlight_intensity_normalized(fov_path: str, channel_name: str) -> list[float]:\n", + " \"\"\"\n", + " Compute highlight (99th percentile) intensity of each timepoint,\n", + " normalized to the first timepoint.\n", + "\n", + " Parameters\n", + " ----------\n", + " fov_path : str\n", + " Path to the field of view (FOV).\n", + " channel_name : str\n", + " Name of the channel to compute highlight intensity for.\n", + "\n", + " Returns\n", + " -------\n", + " NDArray\n", + " List of intensity values.\n", + " \"\"\"\n", + " with open_ome_zarr(fov_path) as fov:\n", + " channel_index = fov.get_channel_index(channel_name)\n", + " channel = fov[\"0\"].dask_array()[:, channel_index]\n", + " highlights = []\n", + " for t, volume in enumerate(channel):\n", + " highlights.append(np.percentile(volume.compute(), 99))\n", + " return [h / highlights[0] for h in highlights]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "837be184", + "metadata": {}, + "outputs": [], + "source": [ + "# Plot intensity over time\n", + "mean_fl = highlight_intensity_normalized(\"input.ome.zarr/0/3/0\", \"mScarlett\")\n", + "mean_vs = highlight_intensity_normalized(\"prediction.ome.zarr/0/3/0\", \"membrane_prediction\")\n", + "time = np.arange(0, 100, 30)\n", + "\n", + "plt.plot(time, mean_fl, label=\"membrane fluorescence\")\n", + "plt.plot(time, mean_vs, label=\"membrane virtual staining\")\n", + "plt.xlabel(\"time / min\")\n", + "plt.ylabel(\"normalized highlight intensity\")\n", + "plt.legend()" + ] + }, + { + "cell_type": "markdown", + "id": "00d41be6", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "Here the highlight intensity of the fluorescence images decreases over time,\n", + "following a exponential decay pattern, indicating photobleaching.\n", + "The virtual staining is not affected by this issue.\n", + "(The object drifts slightly over time, so some inherent noise is expected.)" + ] + }, + { + "cell_type": "markdown", + "id": "19bb6b01", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Summary\n", + "\n", + "In the above example, we demonstrated how to use the VSNeuromast model\n", + "for virtual staining of cell nuclei and plasma membranes of the zebrafish neuromast _in vivo_,\n", + "which can avoid photobleaching in long-term live imaging." + ] + }, + { + "cell_type": "markdown", + "id": "8bda5ac2", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "## Contact & Feedback\n", + "\n", + "For issues or feedback about this tutorial please contact Ziwen Liu at [ziwen.liu@czbiohub.org](mailto:ziwen.liu@czbiohub.org).\n", + "\n", + "## Responsible Use\n", + "\n", + "We are committed to advancing the responsible development and use of artificial intelligence.\n", + "Please follow our [Acceptable Use Policy](https://virtualcellmodels.cziscience.com/acceptable-use-policy) when engaging with our services.\n", + "\n", + "Should you have any security or privacy issues or questions related to the services,\n", + "please reach out to our team at [security@chanzuckerberg.com](mailto:security@chanzuckerberg.com) or [privacy@chanzuckerberg.com](mailto:privacy@chanzuckerberg.com) respectively." + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "all", + "main_language": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/applications/cytoland/examples/vcp_tutorials/neuromast.py b/applications/cytoland/examples/vcp_tutorials/neuromast.py index a65d305ea..9a11678a8 100644 --- a/applications/cytoland/examples/vcp_tutorials/neuromast.py +++ b/applications/cytoland/examples/vcp_tutorials/neuromast.py @@ -64,9 +64,10 @@ # %% # Install the modular Cytoland/VisCy packages required for this example -# See the [repository](https://github.com/mehta-lab/VisCy) for more details +# `cytoland`, `viscy-utils` and `viscy-data` are installed from the GitHub +# monorepo: the prediction writer needs a viscy-data newer than its PyPI release. # Here stackview and ipycanvas are installed for visualization, and iohub provides OME-Zarr access -# !pip install -U cytoland viscy iohub stackview ipycanvas +# !pip install "viscy-utils @ git+https://github.com/mehta-lab/VisCy.git#subdirectory=packages/viscy-utils" "viscy-data @ git+https://github.com/mehta-lab/VisCy.git#subdirectory=packages/viscy-data" "cytoland @ git+https://github.com/mehta-lab/VisCy.git#subdirectory=applications/cytoland" iohub stackview ipycanvas # %% # Restart kernel if running in Google Colab diff --git a/applications/cytoland/examples/vcp_tutorials/quick_start.ipynb b/applications/cytoland/examples/vcp_tutorials/quick_start.ipynb new file mode 100644 index 000000000..81f58a6da --- /dev/null +++ b/applications/cytoland/examples/vcp_tutorials/quick_start.ipynb @@ -0,0 +1,492 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9421d8c7", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Quick Start: Cytoland\n", + "\n", + "**Estimated time to complete:** 15 minutes" + ] + }, + { + "cell_type": "markdown", + "id": "7dae3dc1", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Learning Goals\n", + "\n", + "* Download the VSCyto2D model and an example dataset containing A549 cell images.\n", + "* Run VSCyto2D model inference for joint virtual staining of cell nuclei and plasma membrane.\n", + "* Visualize and compare virtually and experimentally stained cells." + ] + }, + { + "cell_type": "markdown", + "id": "b86f7719", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Prerequisites\n", + "Python>=3.12" + ] + }, + { + "cell_type": "markdown", + "id": "23c78bc5", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Introduction\n", + "\n", + "## Model\n", + "\n", + "The Cytoland virtual staining models are a collection of models (VSCyto2D, VSCyto3D, and VSNeuromast)\n", + "used to predict cellular landmarks (e.g., nuclei and plasma membranes)\n", + "from label-free images (e.g. quantitative phase, Zernike phase contrast, and brightfield).\n", + "This quick-start guide focuses on the VSCyto2D model.\n", + "\n", + "VSCyto2D is a 2D UNeXt2 model that has been trained on A549, HEK293T, and BJ-5ta cells.\n", + "This model enables users to jointly stain cell nuclei and plasma membranes from 2D label-free images\n", + "that are commonly generated for image-based screens.\n", + "\n", + "Alternative models are optimized for different sample types and imaging conditions:\n", + "\n", + "* [VSCyto3D](https://public.czbiohub.org/comp.micro/viscy/VS_models/VSCyto3D):\n", + "3D UNeXt2 model for joint virtual staining of cell nuclei and plasma membrane\n", + "from high-resolution volumetric images.\n", + "* [VSNeuromast](https://public.czbiohub.org/comp.micro/viscy/VS_models/VSNeuromast):\n", + "3D UNeXt2 model for joint virtual staining of nuclei and plasma membrane in zebrafish neuromasts.\n", + "\n", + "## Example Dataset\n", + "\n", + "The A549 example dataset used in this quick-start guide contains\n", + "quantitative phase and paired fluorescence images of cell nuclei and plasma membrane.\n", + "It is stored in OME-Zarr format and can be downloaded from\n", + "[here](https://public.czbiohub.org/comp.micro/viscy/VS_datasets/VSCyto2D/test/a549_hoechst_cellmask_test.zarr).\n", + "It has pre-computed statistics for normalization, generated using the `viscy preprocess` CLI.\n", + "\n", + "Refer to our [preprint](https://doi.org/10.1101/2024.05.31.596901) for more details\n", + "about how the dataset and model were generated.\n", + "\n", + "## User Data\n", + "\n", + "The VSCyto2D model only requires label-free images for inference.\n", + "To run inference on your own data,\n", + "convert them into the OME-Zarr data format using iohub or other\n", + "[tools](https://ngff.openmicroscopy.org/tools/index.html#file-conversion),\n", + "and run [pre-processing](https://github.com/mehta-lab/VisCy/blob/main/docs/usage.md#preprocessing)\n", + "with the `viscy preprocess` CLI." + ] + }, + { + "cell_type": "markdown", + "id": "aba74cd8", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Setup\n", + "\n", + "The commands below will install the required packages and download the example dataset and model checkpoint.\n", + "It may take a few minutes to download all the files.\n", + "\n", + "## Setup Google Colab\n", + "\n", + "To run this quick-start guide using Google Colab,\n", + "choose the 'T4' GPU runtime from the \"Connect\" dropdown menu\n", + "in the upper-right corner of this notebook for faster execution.\n", + "Using a GPU significantly speeds up running model inference, but CPU compute can also be used.\n", + "\n", + "## Setup Local Environment\n", + "\n", + "The commands below assume a Unix-like shell with `wget` installed.\n", + "On Windows, the files can be downloaded manually from the URLs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "069fbda0", + "metadata": {}, + "outputs": [], + "source": [ + "# Install the modular packages required for this example\n", + "# `cytoland`, `viscy-utils` and `viscy-data` are installed from the GitHub\n", + "# monorepo: the prediction writer needs a viscy-data newer than its PyPI\n", + "# release. viscy-models and viscy-transforms resolve from PyPI.\n", + "!pip install \"viscy-utils @ git+https://github.com/mehta-lab/VisCy.git#subdirectory=packages/viscy-utils\" \"viscy-data @ git+https://github.com/mehta-lab/VisCy.git#subdirectory=packages/viscy-data\" \"cytoland @ git+https://github.com/mehta-lab/VisCy.git#subdirectory=applications/cytoland\" torchview cmap iohub" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44132e97", + "metadata": {}, + "outputs": [], + "source": [ + "# restart kernel if running in Google Colab\n", + "if \"get_ipython\" in globals():\n", + " session = get_ipython() # noqa: F821\n", + " if \"google.colab\" in str(session):\n", + " print(\"Shutting down colab session.\")\n", + " session.kernel.do_shutdown(restart=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e4c898fe", + "metadata": {}, + "outputs": [], + "source": [ + "# Validate installation\n", + "!viscy --help" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e4e695f9", + "metadata": {}, + "outputs": [], + "source": [ + "# Download the example dataset\n", + "!wget -m -np -nH --cut-dirs=5 -R \"index.html*\" \"https://public.czbiohub.org/comp.micro/viscy/VS_datasets/VSCyto2D/test/a549_hoechst_cellmask_test.zarr/\"\n", + "# Download the model checkpoint\n", + "!wget https://public.czbiohub.org/comp.micro/viscy/VS_models/VSCyto2D/VSCyto2D/epoch=399-step=23200.ckpt" + ] + }, + { + "cell_type": "markdown", + "id": "dc8ccda1", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Run Model Inference\n", + "\n", + "The following code will run inference on a single field of view (FOV) of the example dataset.\n", + "This can also be achieved by using the VisCy CLI." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8ad5bdd8", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path # noqa: E402\n", + "\n", + "from iohub import open_ome_zarr # noqa: E402\n", + "from torchview import draw_graph # noqa: E402\n", + "\n", + "from cytoland.engine import FcmaeUNet # noqa: E402\n", + "from viscy_data.hcs import HCSDataModule # noqa: E402\n", + "from viscy_transforms import NormalizeSampled # noqa: E402\n", + "from viscy_utils.callbacks import HCSPredictionWriter # noqa: E402\n", + "from viscy_utils.trainer import VisCyTrainer # noqa: E402" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34663450", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "# NOTE: Nothing needs to be changed in this code block for the example to work.\n", + "# If using your own data, please modify the paths below.\n", + "\n", + "# TODO: Set download paths, by default the working directory is used\n", + "root_dir = Path()\n", + "# TODO: modify the path to the input dataset\n", + "input_data_path = root_dir / \"a549_hoechst_cellmask_test.zarr\"\n", + "# TODO: modify the path to the model checkpoint\n", + "model_ckpt_path = root_dir / \"epoch=399-step=23200.ckpt\"\n", + "# TODO: modify the path to save the predictions\n", + "output_path = root_dir / \"a549_prediction.zarr\"\n", + "# TODO: Choose an FOV\n", + "fov = \"0/0/0\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ff5bd62", + "metadata": {}, + "outputs": [], + "source": [ + "# Configure the data module for loading example images in prediction mode.\n", + "# See API documentation for how to use it with a different dataset.\n", + "# For example, View the documentation for the HCSDataModule class by running:\n", + "?HCSDataModule" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4bb5e84f", + "metadata": {}, + "outputs": [], + "source": [ + "# Setup the data module to use the example dataset\n", + "data_module = HCSDataModule(\n", + " # Path to HCS or Single-FOV OME-Zarr dataset\n", + " data_path=input_data_path / fov,\n", + " # Name of the input phase channel\n", + " source_channel=\"Phase3D\",\n", + " # Desired name of the output channels\n", + " target_channel=[\"Nuclei\", \"Membrane\"],\n", + " # Axial input size, 1 for 2D models\n", + " z_window_size=1,\n", + " # Batch size\n", + " # Adjust based on available memory (reduce if seeing OOM errors)\n", + " batch_size=8,\n", + " # Number of workers for data loading\n", + " # Set to 0 for Windows and macOS if running in a notebook,\n", + " # since multiprocessing only works with a `if __name__ == '__main__':` guard.\n", + " # On Linux, set it based on available CPU cores to maximize performance.\n", + " num_workers=4,\n", + " # Normalization strategy\n", + " # This one uses pre-computed statistics from `viscy preprocess`\n", + " # to subtract the median and divide by the interquartile range (IQR).\n", + " # It can also be replaced by other MONAI transforms.\n", + " normalizations=[\n", + " NormalizeSampled(\n", + " [\"Phase3D\"],\n", + " level=\"fov_statistics\",\n", + " subtrahend=\"median\",\n", + " divisor=\"iqr\",\n", + " )\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a5b229a", + "metadata": {}, + "outputs": [], + "source": [ + "# Load the VSCyto2D model from the downloaded checkpoint\n", + "# VSCyto2D is fine-tuned from a FCMAE-pretrained UNeXt2 model.\n", + "# See this module for options to configure the model:\n", + "\n", + "?FullyConvolutionalMAE" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10674ebf", + "metadata": {}, + "outputs": [], + "source": [ + "vs_cyto_2d = FcmaeUNet.load_from_checkpoint(\n", + " # checkpoint path\n", + " model_ckpt_path,\n", + " model_config={\n", + " # number of input channels\n", + " # must match the number of channels in the input data\n", + " \"in_channels\": 1,\n", + " # number of output channels\n", + " # must match the number of target channels in the data module\n", + " \"out_channels\": 2,\n", + " # number of ConvNeXt v2 blocks in each stage of the encoder\n", + " \"encoder_blocks\": [3, 3, 9, 3],\n", + " # feature map channels in each stage of the encoder\n", + " \"dims\": [96, 192, 384, 768],\n", + " # number of ConvNeXt v2 blocks in each stage of the decoder\n", + " \"decoder_conv_blocks\": 2,\n", + " # kernel size in the stem layer\n", + " \"stem_kernel_size\": [1, 2, 2],\n", + " # axial size of the input image\n", + " # must match the Z-window size in the data module\n", + " \"in_stack_depth\": 1,\n", + " # whether to perform masking (for FCMAE pre-training)\n", + " \"pretraining\": False,\n", + " },\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b9cbece7", + "metadata": {}, + "outputs": [], + "source": [ + "# Visualize the model graph\n", + "model_graph = draw_graph(\n", + " vs_cyto_2d,\n", + " (vs_cyto_2d.example_input_array),\n", + " graph_name=\"VSCyto2D\",\n", + " roll=True,\n", + " depth=3,\n", + " expand_nested=True,\n", + ")\n", + "\n", + "model_graph.visual_graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e284524b", + "metadata": {}, + "outputs": [], + "source": [ + "# Setup the trainer for prediction\n", + "# The trainer can be further configured to better utilize the available hardware,\n", + "# For example using GPUs and half precision.\n", + "# Callbacks can also be used to customize logging and prediction writing.\n", + "# See the API documentation for more details:\n", + "?VisCyTrainer" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bffe587d", + "metadata": {}, + "outputs": [], + "source": [ + "# Initialize the trainer\n", + "# The prediction writer callback will save the predictions to an OME-Zarr store\n", + "trainer = VisCyTrainer(callbacks=[HCSPredictionWriter(output_path)])\n", + "\n", + "# Run prediction\n", + "trainer.predict(model=vs_cyto_2d, datamodule=data_module, return_predictions=False)" + ] + }, + { + "cell_type": "markdown", + "id": "92396f37", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Model Outputs\n", + "\n", + "The model outputs are also stored in an OME-Zarr store.\n", + "It can be visualized in an image viewer such as [napari](https://napari.org/).\n", + "Below we show a snapshot in the notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19e90a39", + "metadata": {}, + "outputs": [], + "source": [ + "# Read images from Zarr stores\n", + "# Choose the ROI for better visualization\n", + "y_slice = slice(0, 512)\n", + "x_slice = slice(0, 512)\n", + "\n", + "# Open the prediction store and get the 2D images from 5D arrays (t,c,z,y,x)\n", + "with open_ome_zarr(output_path / fov) as vs_store:\n", + " vs_nucleus = vs_store[0][0, 0, 0, y_slice, x_slice]\n", + " vs_membrane = vs_store[0][0, 1, 0, y_slice, x_slice]\n", + "\n", + "# Open the experimental fluorescence dataset\n", + "with open_ome_zarr(input_data_path / fov) as fluor_store:\n", + " fluor_nucleus = fluor_store[0][0, 1, 0, y_slice, x_slice]\n", + " fluor_membrane = fluor_store[0][0, 2, 0, y_slice, x_slice]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41ef52ea", + "metadata": {}, + "outputs": [], + "source": [ + "# Plot\n", + "import matplotlib.pyplot as plt # noqa: E402\n", + "import numpy as np # noqa: E402\n", + "from cmap import Colormap # noqa: E402\n", + "from skimage.exposure import rescale_intensity # noqa: E402\n", + "\n", + "\n", + "def render_rgb(image: np.ndarray, colormap: Colormap):\n", + " image = rescale_intensity(image, out_range=(0, 1))\n", + " image = colormap(image)\n", + " return image\n", + "\n", + "\n", + "# Render the images as RGB in false colors\n", + "vs_nucleus_rgb = render_rgb(vs_nucleus, Colormap(\"bop_blue\"))\n", + "vs_membrane_rgb = render_rgb(vs_membrane, Colormap(\"bop_orange\"))\n", + "merged_vs = (vs_nucleus_rgb + vs_membrane_rgb).clip(0, 1)\n", + "\n", + "fluor_nucleus_rgb = render_rgb(fluor_nucleus, Colormap(\"green\"))\n", + "fluor_membrane_rgb = render_rgb(fluor_membrane, Colormap(\"magenta\"))\n", + "merged_fluor = (fluor_nucleus_rgb + fluor_membrane_rgb).clip(0, 1)\n", + "\n", + "# Plot\n", + "# Show the individual channels and then fused in a grid\n", + "fig, ax = plt.subplots(2, 3, figsize=(15, 10))\n", + "\n", + "# Virtual staining plots\n", + "ax[0, 0].imshow(vs_nucleus_rgb)\n", + "ax[0, 0].set_title(\"VS Nuclei\")\n", + "ax[0, 1].imshow(vs_membrane_rgb)\n", + "ax[0, 1].set_title(\"VS Membrane\")\n", + "ax[0, 2].imshow(merged_vs)\n", + "ax[0, 2].set_title(\"VS Nuclei+Membrane\")\n", + "\n", + "# Experimental fluorescence plots\n", + "ax[1, 0].imshow(fluor_nucleus_rgb)\n", + "ax[1, 0].set_title(\"Experimental Fluorescence Nuclei\")\n", + "ax[1, 1].imshow(fluor_membrane_rgb)\n", + "ax[1, 1].set_title(\"Experimental Fluorescence Membrane\")\n", + "ax[1, 2].imshow(merged_fluor)\n", + "ax[1, 2].set_title(\"Experimental Fluorescence Nuclei+Membrane\")\n", + "\n", + "# turnoff axis\n", + "for a in ax.flatten():\n", + " a.axis(\"off\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "bc8778c1", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "## Responsible Use\n", + "\n", + "We are committed to advancing the responsible development and use of artificial intelligence.\n", + "Please follow our [Acceptable Use Policy](https://virtualcellmodels.cziscience.com/acceptable-use-policy) when engaging with our services.\n", + "\n", + "Should you have any security or privacy issues or questions related to the services,\n", + "please reach out to our team at [security@chanzuckerberg.com](mailto:security@chanzuckerberg.com) or [privacy@chanzuckerberg.com](mailto:privacy@chanzuckerberg.com) respectively." + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "all", + "main_language": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/applications/cytoland/examples/vcp_tutorials/quick_start.py b/applications/cytoland/examples/vcp_tutorials/quick_start.py index d67a3545f..a40e45d9e 100644 --- a/applications/cytoland/examples/vcp_tutorials/quick_start.py +++ b/applications/cytoland/examples/vcp_tutorials/quick_start.py @@ -87,8 +87,10 @@ # %% # Install the modular packages required for this example -# See the repositories/package indexes for the latest compatibility details -# !pip install cytoland viscy_data viscy_transforms viscy_utils torchview cmap iohub +# `cytoland`, `viscy-utils` and `viscy-data` are installed from the GitHub +# monorepo: the prediction writer needs a viscy-data newer than its PyPI +# release. viscy-models and viscy-transforms resolve from PyPI. +# !pip install "viscy-utils @ git+https://github.com/mehta-lab/VisCy.git#subdirectory=packages/viscy-utils" "viscy-data @ git+https://github.com/mehta-lab/VisCy.git#subdirectory=packages/viscy-data" "cytoland @ git+https://github.com/mehta-lab/VisCy.git#subdirectory=applications/cytoland" torchview cmap iohub # %% # restart kernel if running in Google Colab diff --git a/applications/cytoland/tests/conftest.py b/applications/cytoland/tests/conftest.py index 36530291d..28f6ae67b 100644 --- a/applications/cytoland/tests/conftest.py +++ b/applications/cytoland/tests/conftest.py @@ -54,11 +54,6 @@ ) -def pytest_configure(config): - """Register custom markers.""" - config.addinivalue_line("markers", "hpc_integration: requires HPC paths and GPU") - - @pytest.fixture def checkpoint_path(): """Return path to vscyto3d checkpoint.""" diff --git a/applications/dynaclr/pyproject.toml b/applications/dynaclr/pyproject.toml index 4cce8f180..76ed2e3f7 100644 --- a/applications/dynaclr/pyproject.toml +++ b/applications/dynaclr/pyproject.toml @@ -96,9 +96,6 @@ source = "uv-dynamic-versioning" [tool.hatch.build.targets.wheel] packages = [ "src/dynaclr" ] -[tool.pytest.ini_options] -testpaths = [ "tests" ] - [tool.uv-dynamic-versioning] vcs = "git" style = "pep440" diff --git a/applications/dynaclr/src/dynaclr/evaluation/linear_classifiers/orchestrated_test.py b/applications/dynaclr/src/dynaclr/evaluation/linear_classifiers/orchestrated_test.py index a28db7569..398258857 100644 --- a/applications/dynaclr/src/dynaclr/evaluation/linear_classifiers/orchestrated_test.py +++ b/applications/dynaclr/src/dynaclr/evaluation/linear_classifiers/orchestrated_test.py @@ -9,6 +9,7 @@ from dynaclr.evaluation.evaluate_config import AnnotationSource, LinearClassifiersStepConfig, TaskSpec from dynaclr.evaluation.linear_classifiers.orchestrated import run_linear_classifiers +from viscy_utils.evaluation.zarr_utils import write_anndata_zarr def _make_embeddings_zarr( @@ -58,7 +59,7 @@ def _make_embeddings_zarr( df.index = pd.Index([str(i) for i in range(total)], dtype=object) var = pd.DataFrame(index=pd.Index([str(i) for i in range(n_features)], dtype=object)) adata = ad.AnnData(X=X, obs=df, var=var) - adata.write_zarr(path) + write_anndata_zarr(adata, path) return adata @@ -76,12 +77,12 @@ def _make_annotations( ) -> Path: """Create a synthetic annotation CSV with infection_state and organelle_state labels. - fov_name is stored as the first path component only (e.g. "A/1/FOV0" → "A"), - matching what load_annotation_anndata extracts from obs via .str.split("/").str[0]. + fov_name is stored in full (e.g. "A/1/FOV0"), matching the join key + load_annotation_anndata actually builds: it normalizes both sides with + .str.strip("/") and joins on the whole path, never a split component. """ labels = ["uninfected" if i % 3 != 0 else "infected" for i in range(len(fov_names))] - # Extract first path component to match the join key in load_annotation_anndata - fov_first = [str(f).split("/")[0] for f in fov_names] + fov_first = [str(f) for f in fov_names] data: dict = { "fov_name": fov_first, "t": ts, @@ -248,7 +249,7 @@ def test_run_linear_classifiers_missing_metadata_raises(tmp_path): obs.index = pd.Index([str(i) for i in range(50)], dtype=object) var = pd.DataFrame(index=pd.Index([str(i) for i in range(8)], dtype=object)) zarr_path = tmp_path / "embeddings.zarr" - ad.AnnData(X=X, obs=obs, var=var).write_zarr(zarr_path) + write_anndata_zarr(ad.AnnData(X=X, obs=obs, var=var), zarr_path) config = LinearClassifiersStepConfig( annotations=[AnnotationSource(experiment="exp_A", path=str(tmp_path / "ann.csv"))], diff --git a/applications/dynaclr/src/dynaclr/evaluation/split_embeddings.py b/applications/dynaclr/src/dynaclr/evaluation/split_embeddings.py index c55aedbcd..06704c09d 100644 --- a/applications/dynaclr/src/dynaclr/evaluation/split_embeddings.py +++ b/applications/dynaclr/src/dynaclr/evaluation/split_embeddings.py @@ -39,11 +39,10 @@ def split_embeddings(input_path: Path, output_dir: Path) -> list[Path]: """ import anndata as ad + from viscy_utils.evaluation.zarr_utils import write_anndata_zarr + if hasattr(ad, "settings") and hasattr(ad.settings, "allow_write_nullable_strings"): ad.settings.allow_write_nullable_strings = True - import pandas as pd - - pd.options.future.infer_string = False click.echo(f"Loading embeddings from {input_path}") adata = ad.read_zarr(input_path) @@ -66,7 +65,7 @@ def split_embeddings(input_path: Path, output_dir: Path) -> list[Path]: adata_exp = adata[mask].copy() out_path = output_dir / f"{exp}.zarr" click.echo(f" Writing {exp}: {adata_exp.n_obs} cells → {out_path}") - adata_exp.write_zarr(out_path) + write_anndata_zarr(adata_exp, out_path) written.append(out_path) click.echo(f"\nRemoving combined zarr: {input_path}") diff --git a/applications/dynaclr/tests/conftest.py b/applications/dynaclr/tests/conftest.py index 855efe84f..c0b54bcbd 100644 --- a/applications/dynaclr/tests/conftest.py +++ b/applications/dynaclr/tests/conftest.py @@ -13,6 +13,7 @@ from viscy_data._typing import TripletSample from viscy_data.collection import Collection, ExperimentEntry, save_collection +from viscy_utils.evaluation.zarr_utils import write_anndata_zarr # --------------------------------------------------------------------------- # HPC / GPU skip markers @@ -329,16 +330,6 @@ def val_dataloader(self) -> DataLoader: # --------------------------------------------------------------------------- -def pytest_configure(config): - # anndata zarr writer does not support pandas ArrowStringArray (default in - # pandas 2.x when PyArrow is installed). Verified still broken in - # anndata==0.12.6 + zarr==3.x: IORegistryError on write_zarr with an - # ArrowStringArray index. Remove when anndata fixes zarr 3 support. - # See: https://github.com/scverse/anndata/issues/1510 - pd.options.future.infer_string = False - config.addinivalue_line("markers", "hpc_integration: requires HPC paths and GPU") - - def pytest_collection_modifyitems(config, items): """Auto-skip tests marked ``hpc_integration`` when HPC paths or GPU are unavailable.""" if _HPC_PATHS_AVAILABLE and _GPU_AVAILABLE: @@ -387,7 +378,7 @@ def annotated_adata() -> ad.AnnData: def annotated_adata_zarr(annotated_adata, tmp_path) -> dict: """Write annotated_adata to zarr + CSV and return dataset dict.""" zarr_path = tmp_path / "emb.zarr" - annotated_adata.write_zarr(zarr_path) + write_anndata_zarr(annotated_adata, zarr_path) csv_path = tmp_path / "ann.csv" annotated_adata.obs[["fov_name", "id", "cell_death_state"]].to_csv(csv_path, index=False) diff --git a/applications/dynaclr/tests/test_mmd.py b/applications/dynaclr/tests/test_mmd.py index 1b02196f2..359d3e7c5 100644 --- a/applications/dynaclr/tests/test_mmd.py +++ b/applications/dynaclr/tests/test_mmd.py @@ -10,6 +10,7 @@ from dynaclr.evaluation.mmd.compute_mmd import run_mmd_analysis, run_mmd_pooled from dynaclr.evaluation.mmd.config import ComparisonSpec, MMDEvalConfig, MMDPooledConfig, MMDSettings from viscy_utils.evaluation.mmd import compute_mmd_unbiased, median_heuristic, mmd_permutation_test +from viscy_utils.evaluation.zarr_utils import write_anndata_zarr # --------------------------------------------------------------------------- # Helpers @@ -424,7 +425,7 @@ def _save_adata_zarr(adata: ad.AnnData, path: str) -> None: if os.path.exists(path): shutil.rmtree(path) - adata.write_zarr(path) + write_anndata_zarr(adata, path) def test_run_mmd_pooled_columns(tmp_path): diff --git a/applications/dynaclr/tests/test_reduce_dimensionality.py b/applications/dynaclr/tests/test_reduce_dimensionality.py index fbfd4c56a..dd1ea7bf4 100644 --- a/applications/dynaclr/tests/test_reduce_dimensionality.py +++ b/applications/dynaclr/tests/test_reduce_dimensionality.py @@ -18,6 +18,7 @@ _run_phate, _run_umap, ) +from viscy_utils.evaluation.zarr_utils import write_anndata_zarr @pytest.fixture @@ -30,7 +31,7 @@ def synthetic_zarr(tmp_path): adata = ad.AnnData(X=X) zarr_path = tmp_path / "embeddings.zarr" ad.settings.allow_write_nullable_strings = True - adata.write_zarr(zarr_path) + write_anndata_zarr(adata, zarr_path) return str(zarr_path) @@ -183,7 +184,7 @@ def test_overwrite_keys_protection(self, synthetic_zarr, tmp_path): # Pre-populate X_pca adata = ad.read_zarr(synthetic_zarr) adata.obsm["X_pca"] = np.zeros((100, 5)) - adata.write_zarr(synthetic_zarr) + write_anndata_zarr(adata, synthetic_zarr) config_content = f"input_path: {synthetic_zarr}\npca:\n n_components: 10\n" config_path = tmp_path / "test_config.yaml" @@ -204,7 +205,7 @@ def test_overwrite_keys_allowed(self, synthetic_zarr, tmp_path): # Pre-populate X_pca adata = ad.read_zarr(synthetic_zarr) adata.obsm["X_pca"] = np.zeros((100, 5)) - adata.write_zarr(synthetic_zarr) + write_anndata_zarr(adata, synthetic_zarr) config_content = f"input_path: {synthetic_zarr}\noverwrite_keys: true\npca:\n n_components: 10\n" config_path = tmp_path / "test_config.yaml" @@ -249,7 +250,7 @@ def test_uns_per_key_preserves_existing(self, tmp_path): adata.uns["existing_list"] = ["a", "b"] zarr_path = tmp_path / "test.zarr" ad.settings.allow_write_nullable_strings = True - adata.write_zarr(zarr_path) + write_anndata_zarr(adata, zarr_path) append_to_anndata_zarr(zarr_path, uns={"new_key": ["path1", "path2"]}) @@ -267,7 +268,7 @@ def test_uns_overwrites_specific_key(self, tmp_path): adata.uns["other_key"] = "untouched" zarr_path = tmp_path / "test.zarr" ad.settings.allow_write_nullable_strings = True - adata.write_zarr(zarr_path) + write_anndata_zarr(adata, zarr_path) append_to_anndata_zarr(zarr_path, uns={"my_key": "new_value"}) @@ -317,7 +318,7 @@ def two_synthetic_zarrs(self, tmp_path): adata.uns["classifier_version"] = f"v{i}" adata.uns["predicted_classes"] = ["alive", "dead"] zarr_path = tmp_path / f"store_{i}.zarr" - adata.write_zarr(zarr_path) + write_anndata_zarr(adata, zarr_path) paths.append(str(zarr_path)) return paths diff --git a/conftest.py b/conftest.py new file mode 100644 index 000000000..c8b776b03 --- /dev/null +++ b/conftest.py @@ -0,0 +1,14 @@ +"""Repo-wide pytest setup, loaded before every package's own conftest.""" + +import os + +# Under pytest-xdist every worker is a separate process, and torch/OpenMP size +# their thread pools to all visible cores by default, so `-n N` runs ~N x cores +# threads. On 11 cores that made the suite 10x slower (1242 s vs 117 s) and +# timed out subprocess tests. xdist sets PYTEST_XDIST_WORKER before this module +# is imported, which is before any test module imports torch. Assigned, not +# setdefault: an inherited limit (a job script exporting OMP_NUM_THREADS=16) +# would otherwise oversubscribe the cores the same way. +if "PYTEST_XDIST_WORKER" in os.environ: + for var in ("OMP_NUM_THREADS", "MKL_NUM_THREADS", "OPENBLAS_NUM_THREADS"): + os.environ[var] = "1" diff --git a/packages/viscy-data/src/viscy_data/foreground_masks.py b/packages/viscy-data/src/viscy_data/foreground_masks.py index 2489f72f3..58404fd22 100644 --- a/packages/viscy-data/src/viscy_data/foreground_masks.py +++ b/packages/viscy-data/src/viscy_data/foreground_masks.py @@ -159,11 +159,14 @@ def read_window( Returns ------- list[Tensor] - Per-channel mask tensors with shape ``(1, Z, Y, X)``. + Per-channel mask tensors with shape ``(1, Z, Y, X)``, in the + stored dtype (not upcast: a float32 mask quadruples the mask's + share of every worker buffer and pinned host copy, which + OOM-killed the 4-GPU 3D seg-aux fits at their first validation). """ - kwargs = {} + kwargs = {"as_float": False} if self._preloaded_masks is not None: - kwargs = {"arr_idx": arr_idx, "_preloaded": self._preloaded_masks} + kwargs.update(arr_idx=arr_idx, _preloaded=self._preloaded_masks) mask_images, _ = read_fn( self._mask_arrays[arr_idx], self._mask_ch_indices[arr_idx], diff --git a/packages/viscy-data/src/viscy_data/hcs.py b/packages/viscy-data/src/viscy_data/hcs.py index eb7bc7502..28df3869e 100644 --- a/packages/viscy-data/src/viscy_data/hcs.py +++ b/packages/viscy-data/src/viscy_data/hcs.py @@ -28,7 +28,7 @@ from viscy_data._typing import Sample from viscy_data._utils import _collate_samples, _ensure_channel_list from viscy_data.foreground_masks import ForegroundMaskSupport -from viscy_data.sliding_window import MaskTestDataset, SlidingWindowDataset +from viscy_data.sliding_window import MaskTestDataset, SlidingWindowDataset, read_z_planes _logger = logging.getLogger("lightning.pytorch") @@ -119,6 +119,12 @@ class HCSDataModule(LightningDataModule): ``include_fov_names``. Stored as a ``set`` for O(1) lookup. Honored during fit/validate (including mmap staging) and predict. Test stage uses all plate positions. + predict_z_planes : str or Path or None, optional + Plane file (see :func:`viscy_data.sliding_window.read_z_planes`) that + restricts predict to the listed Z planes per position and timepoint, e.g. + the in-focus slab of a 2D model. Requires ``z_window_size == 1``. The + prediction writer allocates the full source shape and records the planes + on each position. Default None predicts every plane. Predict only. """ def __init__( @@ -151,6 +157,7 @@ def __init__( val_gpu_augmentations: list[MapTransform] | None = None, include_fov_names: Iterable[str] | None = None, exclude_fov_names: Iterable[str] | None = None, + predict_z_planes: str | Path | None = None, ): super().__init__() self.data_path = Path(data_path) @@ -185,6 +192,7 @@ def __init__( self.val_augmentations = val_augmentations or [] self.include_fov_names = set(include_fov_names) if include_fov_names is not None else None self.exclude_fov_names = set(exclude_fov_names) if exclude_fov_names is not None else None + self.predict_z_planes = Path(predict_z_planes) if predict_z_planes is not None else None if gpu_augmentations and self.fg_mask_key is not None: ForegroundMaskSupport.patch_spatial_transforms(gpu_augmentations, ("target",), ("fg_mask",)) if val_gpu_augmentations and self.fg_mask_key is not None: @@ -670,9 +678,16 @@ def _setup_predict( # never reads it, and inference datasets may not have the array. dataset_settings.pop("fg_mask_key", None) predict_transform = Compose(self.normalizations) + z_planes = read_z_planes(self.predict_z_planes) if self.predict_z_planes is not None else None self.predict_dataset = SlidingWindowDataset( positions=self._positions_maybe_single(), transform=predict_transform, + z_planes=z_planes, + # Read each volume once and cut its depth windows from memory. A plane + # selection reads single planes, where a whole-volume read would only cost, + # and loader workers each take every Nth batch, so each would read the whole + # volume. + cache_volume=z_planes is None and self.num_workers == 0, **dataset_settings, ) @@ -691,6 +706,10 @@ def on_after_batch_transfer(self, batch: Sample, dataloader_idx: int) -> Sample: """ if isinstance(batch, Tensor): return batch + # Masks travel host-side in their stored dtype (uint8) to keep worker + # and pinned buffers small; augmentations and losses expect float. + if "fg_mask" in batch and not batch["fg_mask"].is_floating_point(): + batch["fg_mask"] = batch["fg_mask"].float() if self.trainer and self.trainer.training and self._gpu_augmentations is not None: batch = self._gpu_augmentations(batch) elif ( diff --git a/packages/viscy-data/src/viscy_data/sliding_window.py b/packages/viscy-data/src/viscy_data/sliding_window.py index 7e109f555..0978aed66 100644 --- a/packages/viscy-data/src/viscy_data/sliding_window.py +++ b/packages/viscy-data/src/viscy_data/sliding_window.py @@ -1,7 +1,10 @@ """Sliding window datasets for HCS NGFF stores.""" import bisect +import json import logging +from collections.abc import Mapping, Sequence +from os import PathLike from pathlib import Path import numpy as np @@ -18,6 +21,29 @@ _logger = logging.getLogger("lightning.pytorch") +def read_z_planes(path: str | PathLike) -> dict[str, list[list[int]]]: + """Read a predict plane file: position name -> one Z-plane list per timepoint. + + The file is JSON with a ``positions`` mapping (``{"A/1/0": [[11, 12, 13], ...]}``); + any other top-level keys are provenance (how the planes were chosen) and are + ignored here. + + Parameters + ---------- + path : str or PathLike + Plane file written by the producer of the planes (dynacell writes them with + ``precompute-gt build.focus_planes``). + + Returns + ------- + dict[str, list[list[int]]] + Plate-relative position name to per-timepoint plane lists. + """ + with open(path) as f: + payload = json.load(f) + return {name: [[int(z) for z in planes] for planes in per_t] for name, per_t in payload["positions"].items()} + + class SlidingWindowDataset(Dataset): """Sliding window dataset over HCS NGFF positions. @@ -61,6 +87,24 @@ class SlidingWindowDataset(Dataset): ``(T, C, Z, Y, X)``. Channels are ``source + target`` in order. When set, bypasses zarr reads via ``.clone()`` copies from the preloaded data. Default None reads from zarr. + z_planes : Mapping[str, Sequence[Sequence[int]]] or None + Restrict the windows to these Z planes: plate-relative position name + (``"A/1/0"``) to one plane list per timepoint (see :func:`read_z_planes`). + Requires ``z_window_size == 1``; every position needs an entry with one list + per timepoint. Windows stay in increasing ``z`` order within a timepoint. + Default None takes every plane. + cache_volume : bool + Read each (position, timepoint) volume from zarr once and cut its windows + from that copy, instead of re-reading every window. Stride-1 depth windows + otherwise read each plane once per window covering it. Pays off when the + windows of a volume are requested consecutively (the sequential predict + loader); random access (training) would read a whole volume per window. + Each DataLoader worker would keep its own copy and read the whole volume, + so :class:`HCSDataModule` enables it only with ``num_workers == 0``. Not + combinable with ``fg_mask_key`` or ``min_nonzero_fraction``. When every + transform is ``pointwise`` (each voxel mapped through precomputed + statistics alone, e.g. ``NormalizeSampled``), the cached volume is + transformed once instead of every window. Default False. """ def __init__( @@ -77,8 +121,29 @@ def __init__( max_nonzero_retries: int = 100, fg_mask_key: str | None = None, preloaded_fovs: list[Tensor] | None = None, + z_planes: Mapping[str, Sequence[Sequence[int]]] | None = None, + cache_volume: bool = False, ) -> None: super().__init__() + if cache_volume and fg_mask_key is not None: + raise ValueError("cache_volume caches one array per (position, t); it cannot serve fg_mask reads too") + if cache_volume and min_nonzero_fraction > 0: + # The check would also see transformed values when the cached volume is transformed once. + raise ValueError("cache_volume serves windows in order; min_nonzero_fraction retries random windows") + self.cache_volume = cache_volume + transforms = getattr(transform, "transforms", [transform]) + self._transform_volume = ( + cache_volume + and preloaded_fovs is None # preloaded windows bypass the cache + and bool(transform) # an empty Compose has nothing to apply + and all(getattr(t, "pointwise", False) for t in transforms) + ) + self._transform_windows = bool(transform) and not self._transform_volume + self._volume_key: tuple | None = None + self._volume: np.ndarray | None = None + if z_planes is not None and z_window_size != 1: + raise ValueError(f"z_planes selects single planes and needs z_window_size=1, got {z_window_size}") + self.z_planes = z_planes if not 0.0 <= min_nonzero_fraction <= 1.0: raise ValueError(f"min_nonzero_fraction must be in [0, 1], got {min_nonzero_fraction}") if max_nonzero_retries < 0: @@ -138,9 +203,35 @@ def _get_windows(self) -> None: if self.fg_mask_support is not None: self.fg_mask_support.validate_and_store(fov, img_arr, self.target_ch_idx) self._max_window = w + self._planned: list[tuple[int, int]] | None = None + if self.z_planes is not None: + self._planned = self._planned_windows() + self._max_window = len(self._planned) + + def _planned_windows(self) -> list[tuple[int, int]]: + """``(array index, window index)`` of every ``z_planes`` plane, in dataset order.""" + planned = [] + for arr_idx, img_arr in enumerate(self.window_arrays): + name = img_arr.path.rsplit("/", 1)[0] + if name not in self.z_planes: + raise KeyError(f"z_planes has no entry for position {name!r}") + per_t = self.z_planes[name] + if len(per_t) != img_arr.frames: + raise ValueError(f"z_planes[{name!r}] has {len(per_t)} timepoints; the array has {img_arr.frames}") + for t, planes in enumerate(per_t): + zs = sorted(set(int(z) for z in planes)) + if not zs or zs[0] < 0 or zs[-1] >= img_arr.slices: + raise ValueError( + f"z_planes[{name!r}][{t}] = {list(planes)} is empty or outside [0, {img_arr.slices})" + ) + planned.extend((arr_idx, t * img_arr.slices + z) for z in zs) + return planned def _find_window(self, index: int) -> tuple[ImageArray, int, NormMeta | None, int]: """Look up window given index.""" + if self._planned is not None: + arr_idx, tz = self._planned[index] + return (self.window_arrays[arr_idx], tz, self.window_norm_meta[arr_idx], arr_idx) arr_idx = bisect.bisect_right(self.window_keys, index) tz = index - self.window_keys[arr_idx - 1] if arr_idx > 0 else index return (self.window_arrays[arr_idx], tz, self.window_norm_meta[arr_idx], arr_idx) @@ -170,6 +261,7 @@ def _read_img_window( tz: int, arr_idx: int = -1, _preloaded: list[Tensor] | None = None, + as_float: bool = True, ) -> tuple[list[Tensor], HCSStackIndex]: """Read image window as tensor. @@ -188,6 +280,10 @@ def _read_img_window( _preloaded : list[Tensor] or None Override for ``self._preloaded``. When set, uses this list instead of the dataset-level preloaded data. + as_float : bool + Cast the window to float32 (images). When False, keep the stored + dtype (foreground masks: uint8 is 4x smaller in every worker + buffer and pinned host copy; the datamodule casts on GPU). Returns ------- @@ -200,15 +296,49 @@ def _read_img_window( z = tz - t * zs preloaded = _preloaded if _preloaded is not None else self._preloaded if preloaded is not None and arr_idx >= 0: - data = preloaded[arr_idx][t : t + 1, :, z : z + self.z_window_size].to(torch.float32, copy=True) + window = preloaded[arr_idx][t : t + 1, :, z : z + self.z_window_size] + data = window.to(torch.float32 if as_float else window.dtype, copy=True) return data.unbind(dim=1), (f"/{img.path}", t, z) + if self.cache_volume: + # Keyed on the array object: positions of different plates can share a path. + key = (id(img), t, tuple(ch_idx), as_float) + if self._volume_key != key: + # Release the previous volume first, so two never coexist; windows own copies. + self._volume, self._volume_key = None, None + # Per-channel basic slices, not oindex: the coordinate indexer peaks at ~7x + # the output size, which a whole volume makes large (see HCSDataModule's + # mmap preload). Filling one preallocated array keeps the peak at its size. + volume = np.empty((1, len(ch_idx), *img.shape[-3:]), dtype=np.float32 if as_float else img.dtype) + for j, c in enumerate(ch_idx): + volume[:, j] = img[t : t + 1, int(c)] + if self._transform_volume: + self._transform_cached(volume, arr_idx, t) + self._volume = volume + self._volume_key = key + data = self._volume[:, :, z : z + self.z_window_size] + if self._transform_windows: + # Copy: transforms must not reach the cached volume the next window is cut from. + data = data.copy() + return torch.from_numpy(data).unbind(dim=1), (f"/{img.path}", t, z) data = img.oindex[ slice(t, t + 1), [int(i) for i in ch_idx], slice(z, z + self.z_window_size), - ].astype(np.float32) + ] + if as_float: + data = data.astype(np.float32) return torch.from_numpy(data).unbind(dim=1), (f"/{img.path}", t, z) + def _transform_cached(self, volume: np.ndarray, arr_idx: int, t: int) -> None: + """Apply the pointwise transforms to a cached ``(1, C, Z, Y, X)`` volume in place.""" + images = {name: torch.from_numpy(volume[:, j]) for j, name in enumerate(self._all_ch_names)} + norm_meta = self._resolve_timepoint_norm_meta(self.window_norm_meta[arr_idx], t) + if norm_meta is not None: + images["norm_meta"] = norm_meta + images = self.transform(images) + for j, name in enumerate(self._all_ch_names): + volume[:, j] = np.asarray(images[name]) + def __len__(self) -> int: """Return total number of windows.""" return self._max_window @@ -269,7 +399,7 @@ def __getitem__(self, index: int) -> Sample: if norm_meta is not None: norm_meta = self._resolve_timepoint_norm_meta(norm_meta, sample_index[1]) sample_images["norm_meta"] = norm_meta - if self.transform: + if self._transform_windows: sample_images = self.transform(sample_images) if "weight" in sample_images: del sample_images["weight"] diff --git a/packages/viscy-data/tests/test_hcs.py b/packages/viscy-data/tests/test_hcs.py index 1242e1d81..72d487192 100644 --- a/packages/viscy-data/tests/test_hcs.py +++ b/packages/viscy-data/tests/test_hcs.py @@ -11,7 +11,7 @@ from viscy_data import HCSDataModule from viscy_data.sliding_window import SlidingWindowDataset -from viscy_transforms import BatchedRandFlipd, NormalizeSampled, RandSpatialCropd +from viscy_transforms import BatchedRandFlipd, MinMaxSampled, NormalizeSampled, RandSpatialCropd @mark.parametrize("multi_sample_augmentation", [True, False]) @@ -1061,7 +1061,7 @@ def test_mmap_preload_preserves_native_dtype_and_casts_on_read(tmp_path): def test_mmap_preload_fg_mask_preserves_native_dtype(hcs_with_fg_mask, tmp_path): - """fg_mask.mmap preserves native uint8 dtype; sampled masks cast to float32.""" + """fg_mask.mmap preserves native uint8 dtype, and so do the sampled masks.""" importorskip("tensordict") from tensordict.memmap import MemoryMappedTensor @@ -1093,7 +1093,50 @@ def test_mmap_preload_fg_mask_preserves_native_dtype(hcs_with_fg_mask, tmp_path) dm.setup(stage="fit") batch = next(iter(dm.train_dataloader())) - assert batch["fg_mask"].dtype == torch.float32 + assert batch["fg_mask"].dtype == torch.uint8 + + +@mark.parametrize("mmap_preload", [False, True]) +def test_fg_mask_stays_uint8_on_host_and_is_float_after_transfer(hcs_with_fg_mask, tmp_path, mmap_preload): + """Host-side fg_mask batches keep the stored uint8; the GPU hook casts to float32. + + Regression: masks were upcast to float32 in every sample, so full-FOV + validation batches carried a mask as large as the target through the + worker buffers and the power-of-two pinned-memory cache, and the 4-GPU + 3D seg-aux fits were OOM-killed at their first validation. + """ + if mmap_preload: + importorskip("tensordict") + from viscy_transforms import BatchedCenterSpatialCropd + + dm = HCSDataModule( + data_path=hcs_with_fg_mask, + source_channel="Phase", + target_channel="Fluorescence", + fg_mask_key="fg_mask", + z_window_size=4, + batch_size=2, + num_workers=0, + yx_patch_size=[16, 16], + split_ratio=0.5, + mmap_preload=mmap_preload, + scratch_dir=tmp_path, + val_gpu_augmentations=[BatchedCenterSpatialCropd(keys=["source", "target"], roi_size=[4, 16, 16])], + ) + dm.prepare_data() + dm.setup(stage="fit") + for loader in (dm.train_dataloader(), dm.val_dataloader()): + host = next(iter(loader)) + assert host["fg_mask"].dtype == torch.uint8 + assert host["fg_mask"].nbytes * 4 == host["target"].nbytes + assert set(host["fg_mask"].unique().tolist()).issubset({0, 1}) + + host_mask = host["fg_mask"].clone() + dm.trainer = MagicMock(training=False, validating=True, sanity_checking=False) + result = dm.on_after_batch_transfer(host, 0) + assert result["fg_mask"].dtype == torch.float32 + assert result["fg_mask"].shape == result["target"].shape == (2, 1, 4, 16, 16) + assert torch.equal(result["fg_mask"], host_mask[..., 8:24, 8:24].float()) @fixture(scope="function") @@ -1226,3 +1269,76 @@ def test_mmap_preload_matches_oindex(preprocessed_hcs_dataset, tmp_path): expected = np.asarray(pos["0"].oindex[:, ch_idx, :]) assert staged.shape == expected.shape np.testing.assert_array_equal(staged, expected) + + +class _WindowMeanCenter: + """Subtract each window's own mean: per-window statistics, so not pointwise.""" + + def __init__(self, key: str) -> None: + self.key = key + + def __call__(self, sample: dict) -> dict: + sample[self.key] = sample[self.key] - sample[self.key].mean() + return sample + + +@mark.parametrize("level", ["fov_statistics", "timepoint_statistics", "min_max", "per_window"]) +def test_cached_volume_transformed_once_equals_per_window_transforms(tmp_path, level): + """A pointwise normalization of the cached volume gives every window the bytes per-window normalization gives. + + Statistics differ per FOV and timepoint, so a volume normalized with another FOV's or frame's statistics shows. + A transform with per-window statistics is not pointwise and must keep running per window. + """ + path = tmp_path / "norm.zarr" + rng = np.random.default_rng(3) + with open_ome_zarr(path, layout="hcs", mode="w", channel_names=["Phase", "Fluorescence"]) as plate: + for i, fov in enumerate(("0", "1")): + pos = plate.create_position("A", "1", fov) + pos.create_image("0", (rng.normal(size=(2, 2, 7, 16, 16)) * 3 + 10).astype(np.float32)) + stats = [ + {"mean": 10.0 + i + 0.37 * t, "std": 2.9 + 0.11 * i - 0.05 * t, "p1": 5.0 + i, "p99": 15.0 - t} + for t in range(2) + ] + pos.zattrs["normalization"] = { + ch: {"fov_statistics": stats[0], "timepoint_statistics": {str(t): s for t, s in enumerate(stats)}} + for ch in ("Phase", "Fluorescence") + } + if level == "per_window": + transform = Compose([_WindowMeanCenter("Phase")]) + elif level == "min_max": + transform = Compose([MinMaxSampled(keys=["Phase"], level="timepoint_statistics")]) + else: + transform = Compose([NormalizeSampled(keys=["Phase", "Fluorescence"], level=level)]) + samples = {} + with open_ome_zarr(path, mode="r") as plate: + positions = [pos for _, pos in plate.positions()] + for cache_volume in (False, True): + dataset = SlidingWindowDataset( + positions=positions, + channels={"source": ["Phase"], "target": ["Fluorescence"]}, + z_window_size=4, + transform=transform, + cache_volume=cache_volume, + ) + assert dataset._transform_volume is (cache_volume and level != "per_window") + samples[cache_volume] = [dataset[i] for i in range(len(dataset))] + assert len(samples[True]) == 2 * 2 * 4 + for reference, cached in zip(samples[False], samples[True]): + assert reference["index"] == cached["index"] + for key in ("source", "target"): + assert reference[key].dtype == cached[key].dtype == torch.float32 + assert reference[key].numpy().tobytes() == cached[key].numpy().tobytes() + + +def test_cached_volume_refuses_nonzero_retries(hcs_with_fg_mask): + """Retries jump to random windows and would judge transformed values, so the volume cache refuses them.""" + with open_ome_zarr(hcs_with_fg_mask, mode="r") as ds: + positions = [pos for _, pos in ds.positions()] + with raises(ValueError, match="min_nonzero_fraction"): + SlidingWindowDataset( + positions=positions, + channels={"source": ["Phase"], "target": ["Fluorescence"]}, + z_window_size=4, + min_nonzero_fraction=0.1, + cache_volume=True, + ) diff --git a/packages/viscy-models/pyproject.toml b/packages/viscy-models/pyproject.toml index 9a71806d9..fe563943e 100644 --- a/packages/viscy-models/pyproject.toml +++ b/packages/viscy-models/pyproject.toml @@ -39,7 +39,7 @@ dependencies = [ "transformers>=4.40", ] -optional-dependencies.celldiff = [ "diffusers", "torchdiffeq" ] +optional-dependencies.celldiff = [ "diffusers", "scipy", "torchdiffeq" ] urls.Homepage = "https://github.com/mehta-lab/VisCy" urls.Issues = "https://github.com/mehta-lab/VisCy/issues" diff --git a/packages/viscy-models/src/viscy_models/__init__.py b/packages/viscy-models/src/viscy_models/__init__.py index 436054985..0b66cee5a 100644 --- a/packages/viscy-models/src/viscy_models/__init__.py +++ b/packages/viscy-models/src/viscy_models/__init__.py @@ -6,7 +6,7 @@ from viscy_models.components.heads import MLP, CosineClassifier from viscy_models.contrastive import ContrastiveEncoder, NTXentHCL, ResNet3dEncoder -from viscy_models.foundation import DINOv3Model, OpenPhenomModel +from viscy_models.foundation import DINOv3Model, MorphEmModel, OpenPhenomModel from viscy_models.unet import FullyConvolutionalMAE, Unet2d, Unet3d, Unet25d, UNeXt2 from viscy_models.vae import BetaVae25D, BetaVaeMonai @@ -16,6 +16,7 @@ "ContrastiveEncoder", "CosineClassifier", "DINOv3Model", + "MorphEmModel", "NTXentHCL", "OpenPhenomModel", "FullyConvolutionalMAE", diff --git a/packages/viscy-models/src/viscy_models/celldiff/celldiff_2d_test.py b/packages/viscy-models/src/viscy_models/celldiff/celldiff_2d_test.py new file mode 100644 index 000000000..1a9767455 --- /dev/null +++ b/packages/viscy-models/src/viscy_models/celldiff/celldiff_2d_test.py @@ -0,0 +1,82 @@ +"""CELLDiffNet at Z=1 with a per-axis patch (the CellDiff-2D configuration).""" + +import pytest +import torch + +from viscy_models.celldiff.celldiff_net import CELLDiffNet +from viscy_models.celldiff.modules.patch_embed_3d import normalize_patch_size + +#: Small enough to build and run on CPU in a test; same topology as the recipe. +_NET_KWARGS = dict( + in_channels=1, + dims=[8, 16, 32, 32], + num_res_block=[2, 2, 2], + hidden_size=64, + num_heads=4, + dim_head=16, + num_hidden_layers=1, +) + + +def test_normalize_patch_size_expands_int_and_validates() -> None: + """An int stays cubic; a triple passes through; anything else raises.""" + assert normalize_patch_size(4) == (4, 4, 4) + assert normalize_patch_size([1, 4, 4]) == (1, 4, 4) + assert normalize_patch_size((2, 3, 4)) == (2, 3, 4) + for bad in (0, -1, [1, 4], [0, 4, 4], [1, 4, 4, 4]): + with pytest.raises(ValueError): + normalize_patch_size(bad) + + +@pytest.mark.parametrize( + ("spatial", "patch"), + [([1, 64, 64], [1, 4, 4]), ([1, 32, 32], [1, 2, 2]), ([8, 64, 64], 4)], +) +def test_celldiff_net_roundtrips_spatial_shape(spatial: list[int], patch: int | list[int]) -> None: + """Z=1 with a (1, p, p) patch runs, and the cubic 3D path is unchanged.""" + net = CELLDiffNet(input_spatial_size=spatial, patch_size=patch, **_NET_KWARGS) + x = torch.randn(2, 1, *spatial) + out = net(x, cond=torch.randn(2, 1, *spatial), t=torch.rand(2)) + assert out.shape == (2, 1, *spatial) + assert torch.isfinite(out).all() + + +def test_celldiff_net_z1_backward_reaches_every_parameter() -> None: + """A frozen/disconnected patch path would silently starve gradients at Z=1.""" + spatial = [1, 64, 64] + net = CELLDiffNet(input_spatial_size=spatial, patch_size=[1, 4, 4], **_NET_KWARGS) + out = net(torch.randn(1, 1, *spatial), cond=torch.randn(1, 1, *spatial), t=torch.rand(1)) + out.square().mean().backward() + starved = [ + name + for name, p in net.named_parameters() + if p.requires_grad and (p.grad is None or not torch.isfinite(p.grad).all()) + ] + assert not starved, f"parameters with missing/non-finite grads: {starved[:5]}" + + +def test_cubic_patch_is_rejected_at_z1() -> None: + """The failure that motivated per-axis patches must stay a loud error.""" + with pytest.raises(ValueError, match="not divisible by its patch extent"): + CELLDiffNet(input_spatial_size=[1, 64, 64], patch_size=4, **_NET_KWARGS) + + +def test_z1_patch_grid_drops_the_z_axis() -> None: + """The token grid must be (1, H/2**n/p, W/2**n/p) — no phantom Z tokens.""" + net = CELLDiffNet(input_spatial_size=[1, 64, 64], patch_size=[1, 4, 4], **_NET_KWARGS) + # 3 downsamples of stride (1, 2, 2): 64 -> 8, then patch 4 -> 2 + assert net.bottleneck.latent_grid_size == [1, 2, 2] + assert net.bottleneck.img_pos_embed.shape[1] == 4 + + +def test_cond_channels_widens_the_conditioning_conv_only() -> None: + """Extra conditioning channels (C-cond) are accepted and every other shape is unchanged.""" + spatial = [1, 32, 32] + base = CELLDiffNet(input_spatial_size=spatial, patch_size=[1, 4, 4], **_NET_KWARGS) + net = CELLDiffNet(input_spatial_size=spatial, patch_size=[1, 4, 4], cond_channels=2, **_NET_KWARGS) + widened = {name for name, p in net.state_dict().items() if p.shape != base.state_dict()[name].shape} + assert widened == {"_cond_inconv.weight"} + out = net(torch.randn(2, 1, *spatial), cond=torch.randn(2, 2, *spatial), t=torch.rand(2)) + assert out.shape == (2, 1, *spatial) + with pytest.raises(ValueError, match="cond must have 2 channel"): + net(torch.randn(2, 1, *spatial), cond=torch.randn(2, 1, *spatial), t=torch.rand(2)) diff --git a/packages/viscy-models/src/viscy_models/celldiff/celldiff_net.py b/packages/viscy-models/src/viscy_models/celldiff/celldiff_net.py index 537cdc360..b478e279b 100644 --- a/packages/viscy-models/src/viscy_models/celldiff/celldiff_net.py +++ b/packages/viscy-models/src/viscy_models/celldiff/celldiff_net.py @@ -9,6 +9,8 @@ application layer and is not part of this package. """ +from collections.abc import Sequence + import torch from torch import Tensor @@ -51,8 +53,14 @@ class CELLDiffNet(UNet3DBase): Feed-forward output dropout rate. num_hidden_layers : int Number of transformer blocks in the bottleneck. - patch_size : int - Cubic patch size for the 3D patch embedding. + patch_size : int | Sequence[int] + Cubic patch size for the 3D patch embedding, or per-axis + ``(D, H, W)`` extents. Use ``(1, p, p)`` at ``D=1`` for the + Z-preserving 2D configuration. + cond_channels : int + Number of conditioning channels, fused additively with the noisy + target after the first convolution. ``1`` is phase alone; extra + channels carry further pixel-aligned conditions (e.g. a mask). """ def __init__( @@ -67,8 +75,11 @@ def __init__( dropout: float = 0.0, final_dropout: float = 0.0, num_hidden_layers: int = 2, - patch_size: int = 4, + patch_size: int | Sequence[int] = 4, + cond_channels: int = 1, ) -> None: + if cond_channels < 1: + raise ValueError(f"cond_channels must be >= 1, got {cond_channels}") if input_spatial_size is None: input_spatial_size = [8, 512, 512] if dims is None: @@ -97,9 +108,10 @@ def __init__( bottleneck=bottleneck, downsample_z=False, time_embed_dim=hidden_size, - cond_channels=1, + cond_channels=cond_channels, ) self.input_spatial_size = input_spatial_size + self.cond_channels = cond_channels def forward(self, x: Tensor, cond: Tensor, t: Tensor) -> Tensor: """Predict velocity field for flow-matching. @@ -109,7 +121,8 @@ def forward(self, x: Tensor, cond: Tensor, t: Tensor) -> Tensor: x : Tensor Noisy target volume of shape ``(B, in_channels, D, H, W)``. cond : Tensor - Phase contrast conditioning of shape ``(B, 1, D, H, W)``. + Conditioning of shape ``(B, cond_channels, D, H, W)``; channel 0 + is phase. t : Tensor Diffusion timesteps of shape ``(B,)``. @@ -124,6 +137,6 @@ def forward(self, x: Tensor, cond: Tensor, t: Tensor) -> Tensor: raise ValueError( f"cond spatial size {list(cond.shape[2:])} does not match expected {self.input_spatial_size}" ) - if cond.shape[1] != 1: - raise ValueError(f"cond must have 1 channel, got {cond.shape[1]}") + if cond.shape[1] != self.cond_channels: + raise ValueError(f"cond must have {self.cond_channels} channel(s), got {cond.shape[1]}") return super().forward(x, cond=cond, t=t) diff --git a/packages/viscy-models/src/viscy_models/celldiff/modules/patch_embed_3d.py b/packages/viscy-models/src/viscy_models/celldiff/modules/patch_embed_3d.py index 507a6f3f2..7a01d6a64 100644 --- a/packages/viscy-models/src/viscy_models/celldiff/modules/patch_embed_3d.py +++ b/packages/viscy-models/src/viscy_models/celldiff/modules/patch_embed_3d.py @@ -1,19 +1,49 @@ """3D patch embedding for vision transformers.""" +from collections.abc import Sequence + import torch import torch.nn as nn +__all__ = ["PatchEmbed3D", "normalize_patch_size"] + + +def normalize_patch_size(patch_size: int | Sequence[int]) -> tuple[int, int, int]: + """Expand a patch size to a per-axis ``(D, H, W)`` triple. + + An ``int`` means a cubic patch, matching the original CELL-Diff recipe. A + 3-sequence gives per-axis control, which is what lets the same network run + Z-preserving at ``D=1`` — a cubic ``patch_size=4`` would need a Conv3d with + a size-4 kernel over a size-1 axis. + + Parameters + ---------- + patch_size : int | Sequence[int] + Cubic side length, or ``(D, H, W)`` patch extents. + + Returns + ------- + tuple[int, int, int] + Per-axis patch extents. + """ + dims = (patch_size,) * 3 if isinstance(patch_size, int) else tuple(int(p) for p in patch_size) + if len(dims) != 3: + raise ValueError(f"patch_size must be an int or a length-3 sequence, got {patch_size!r}") + if any(p < 1 for p in dims): + raise ValueError(f"patch_size entries must be >= 1, got {patch_size!r}") + return dims + class PatchEmbed3D(nn.Module): """Convert a 3D volume into a sequence of patch embeddings. Uses a single strided 3D convolution to project non-overlapping - cubic patches into an embedding space. + patches into an embedding space. Parameters ---------- - patch_size : int - Cubic patch side length. + patch_size : int | Sequence[int] + Cubic patch side length, or per-axis ``(D, H, W)`` extents. in_chans : int Number of input channels. embed_dim : int @@ -22,9 +52,10 @@ class PatchEmbed3D(nn.Module): Whether to include bias in the projection convolution. """ - def __init__(self, patch_size: int, in_chans: int, embed_dim: int, bias: bool = True) -> None: + def __init__(self, patch_size: int | Sequence[int], in_chans: int, embed_dim: int, bias: bool = True) -> None: super().__init__() - self.proj = nn.Conv3d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size, bias=bias) + patch = normalize_patch_size(patch_size) + self.proj = nn.Conv3d(in_chans, embed_dim, kernel_size=patch, stride=patch, bias=bias) def forward(self, x: torch.Tensor) -> torch.Tensor: """Project input volume into patch token sequence. diff --git a/packages/viscy-models/src/viscy_models/celldiff/modules/transformer.py b/packages/viscy-models/src/viscy_models/celldiff/modules/transformer.py index 40e096704..2d4211c35 100644 --- a/packages/viscy-models/src/viscy_models/celldiff/modules/transformer.py +++ b/packages/viscy-models/src/viscy_models/celldiff/modules/transformer.py @@ -1,10 +1,14 @@ """Transformer blocks and output layers for CellDiff models.""" +from collections.abc import Sequence + import torch import torch.nn as nn from diffusers.models.attention import FeedForward from diffusers.models.attention_processor import Attention +from viscy_models.celldiff.modules.patch_embed_3d import normalize_patch_size + def modulate(x: torch.Tensor, shift: torch.Tensor, scale: torch.Tensor) -> torch.Tensor: """Apply adaptive layer norm modulation: ``x * (1 + scale) + shift``.""" @@ -151,35 +155,35 @@ def unpatchify( x: torch.Tensor, out_channels: int, latent_grid_size: list[int], - patch_size: int, + patch_size: int | Sequence[int], ) -> torch.Tensor: """Reconstruct a 3D volume from patch tokens. Parameters ---------- x : torch.Tensor - Patch tokens of shape ``(N, T, patch_size**3 * out_channels)``. + Patch tokens of shape ``(N, T, prod(patch_size) * out_channels)``. out_channels : int Number of output channels. latent_grid_size : list[int] Latent grid dimensions ``[D, H, W]`` such that ``D*H*W == T``. - patch_size : int - Cubic patch side length. + patch_size : int | Sequence[int] + Cubic patch side length, or per-axis ``(D, H, W)`` extents. Returns ------- torch.Tensor - Reconstructed volume of shape ``(N, out_channels, D*p, H*p, W*p)``. + Reconstructed volume of shape ``(N, out_channels, D*pd, H*ph, W*pw)``. """ c = out_channels - p = patch_size + pd, ph, pw = normalize_patch_size(patch_size) d, h, w = latent_grid_size if d * h * w != x.shape[1]: raise ValueError(f"Expected {d * h * w} tokens (grid {d}x{h}x{w}), got {x.shape[1]}") - x = x.reshape(x.shape[0], d, h, w, p, p, p, c) + x = x.reshape(x.shape[0], d, h, w, pd, ph, pw, c) x = x.permute(0, 7, 1, 4, 2, 5, 3, 6) - imgs = x.reshape(x.shape[0], c, d * p, h * p, w * p) + imgs = x.reshape(x.shape[0], c, d * pd, h * ph, w * pw) return imgs @@ -190,8 +194,9 @@ class FinalLayer(nn.Module): ---------- hidden_size : int Transformer hidden dimension. - patch_size : int - Cubic patch size (output is ``patch_size**3 * out_channels`` per token). + patch_size : int | Sequence[int] + Cubic patch size, or per-axis ``(D, H, W)`` extents (output is + ``prod(patch_size) * out_channels`` per token). out_channels : int Number of output channels after unpatchifying. time_embed_dim : int | None @@ -202,13 +207,14 @@ class FinalLayer(nn.Module): def __init__( self, hidden_size: int, - patch_size: int, + patch_size: int | Sequence[int], out_channels: int, time_embed_dim: int | None = None, ) -> None: super().__init__() + pd, ph, pw = normalize_patch_size(patch_size) self.norm_final = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6) - self.linear = nn.Linear(hidden_size, patch_size * patch_size * patch_size * out_channels, bias=True) + self.linear = nn.Linear(hidden_size, pd * ph * pw * out_channels, bias=True) if time_embed_dim is not None: self.adaLN = nn.Sequential( nn.SiLU(), diff --git a/packages/viscy-models/src/viscy_models/celldiff/modules/transport/integrators.py b/packages/viscy-models/src/viscy_models/celldiff/modules/transport/integrators.py index db3962d28..6e16e1917 100644 --- a/packages/viscy-models/src/viscy_models/celldiff/modules/transport/integrators.py +++ b/packages/viscy-models/src/viscy_models/celldiff/modules/transport/integrators.py @@ -6,7 +6,9 @@ from collections.abc import Callable +import numpy as np import torch +from scipy.special import betaincinv from torch import Tensor from torchdiffeq import odeint @@ -143,12 +145,20 @@ class ODESolver: sampler_type : str ODE solver method (e.g. ``"dopri5"``, ``"euler"``). num_steps : int - Number of time points (for fixed-step methods, this is the step count; - for adaptive methods, this controls the output grid). + Number of time points (for fixed-step methods, the solver takes + ``num_steps - 1`` steps on them; for adaptive methods, this controls + the output grid). atol : float Absolute error tolerance. rtol : float Relative error tolerance. + time_schedule : str or tuple of float + ``"uniform"`` (linearly spaced time points), ``"cosine"`` + (``t0 + (t1 - t0) * (1 - cos(pi * s)) / 2`` over uniform ``s``, which + clusters the points at both ends of the interval), or ``(p, q)``: the + Beta(p, q) quantiles of uniform ``s``. Cosine is Beta(0.5, 0.5) and + uniform is Beta(1, 1); ``p`` or ``q`` below 1 packs the points toward + ``t0`` or ``t1``. """ def __init__( @@ -161,11 +171,32 @@ def __init__( num_steps: int, atol: float, rtol: float, + time_schedule: str | tuple[float, float] = "uniform", ) -> None: if t0 >= t1: raise ValueError("ODE solver requires t0 < t1") self.drift = drift - self.t = torch.linspace(t0, t1, num_steps) + if time_schedule == "uniform": + self.t = torch.linspace(t0, t1, num_steps) + elif time_schedule == "cosine": + s = torch.linspace(0.0, 1.0, num_steps, dtype=torch.float64) + self.t = (t0 + (t1 - t0) * (1 - torch.cos(torch.pi * s)) / 2).float() + elif ( + isinstance(time_schedule, tuple | list) + and len(time_schedule) == 2 + and all(0 < v < np.inf for v in time_schedule) + ): + p, q = time_schedule + self.t = torch.from_numpy(t0 + (t1 - t0) * betaincinv(p, q, np.linspace(0.0, 1.0, num_steps))).float() + else: + raise ValueError( + f"time_schedule must be 'uniform', 'cosine' or finite positive (p, q), got {time_schedule!r}" + ) + if not bool((self.t.diff() > 0).all()): + raise ValueError( + f"time_schedule {time_schedule!r} puts {num_steps} time points on [{t0}, {t1}] that are not" + " strictly increasing in float32; use fewer points or a less extreme shape" + ) self.atol = atol self.rtol = rtol self.sampler_type = sampler_type diff --git a/packages/viscy-models/src/viscy_models/celldiff/modules/transport/transport.py b/packages/viscy-models/src/viscy_models/celldiff/modules/transport/transport.py index 24eefbcbd..961a58411 100644 --- a/packages/viscy-models/src/viscy_models/celldiff/modules/transport/transport.py +++ b/packages/viscy-models/src/viscy_models/celldiff/modules/transport/transport.py @@ -450,6 +450,7 @@ def sample_ode( atol: float = 1e-6, rtol: float = 1e-3, reverse: bool = False, + time_schedule: str | tuple[float, float] = "uniform", ) -> Callable: """Return an ODE sampling function. @@ -465,6 +466,11 @@ def sample_ode( Relative error tolerance. reverse : bool Whether to integrate in reverse (data to noise). + time_schedule : str or tuple of float + Spacing of the ``num_steps`` time points: ``"uniform"``, + ``"cosine"`` (``(1 - cos(pi * s)) / 2`` over uniform ``s``, dense at + both ends of the interval) or ``(p, q)`` for Beta(p, q) quantiles + (see :class:`ODESolver`). Fixed-grid solvers step on these points. Returns ------- @@ -496,6 +502,7 @@ def drift(x, t, model, **kwargs): num_steps=num_steps, atol=atol, rtol=rtol, + time_schedule=time_schedule, ) return _ode.sample diff --git a/packages/viscy-models/src/viscy_models/celldiff/unet_vit_3d.py b/packages/viscy-models/src/viscy_models/celldiff/unet_vit_3d.py index 301f0d168..9f115b903 100644 --- a/packages/viscy-models/src/viscy_models/celldiff/unet_vit_3d.py +++ b/packages/viscy-models/src/viscy_models/celldiff/unet_vit_3d.py @@ -5,6 +5,8 @@ transformer bottleneck, CNN decoder with skip connections. """ +from collections.abc import Sequence + import torch from torch import Tensor @@ -50,8 +52,10 @@ class UNetViT3D(UNet3DBase): Feed-forward output dropout rate. num_hidden_layers : int Number of transformer blocks in the bottleneck. - patch_size : int - Cubic patch size for the 3D patch embedding. + patch_size : int | Sequence[int] + Cubic patch size for the 3D patch embedding, or per-axis + ``(D, H, W)`` extents. Use ``(1, p, p)`` at ``D=1`` for the + Z-preserving 2D configuration. """ def __init__( @@ -67,7 +71,7 @@ def __init__( dropout: float = 0.0, final_dropout: float = 0.0, num_hidden_layers: int = 2, - patch_size: int = 4, + patch_size: int | Sequence[int] = 4, ) -> None: if input_spatial_size is None: input_spatial_size = [8, 512, 512] diff --git a/packages/viscy-models/src/viscy_models/celldiff/vit_bottleneck.py b/packages/viscy-models/src/viscy_models/celldiff/vit_bottleneck.py index 6d4fc6e9f..c92f03cbe 100644 --- a/packages/viscy-models/src/viscy_models/celldiff/vit_bottleneck.py +++ b/packages/viscy-models/src/viscy_models/celldiff/vit_bottleneck.py @@ -8,11 +8,13 @@ Requires ``diffusers`` (for Attention and FeedForward). """ +from collections.abc import Sequence + import torch import torch.nn as nn from torch import Tensor -from viscy_models.celldiff.modules.patch_embed_3d import PatchEmbed3D +from viscy_models.celldiff.modules.patch_embed_3d import PatchEmbed3D, normalize_patch_size from viscy_models.celldiff.modules.positional_embedding import get_3d_sincos_pos_embed from viscy_models.celldiff.modules.transformer import ( FinalLayer, @@ -52,8 +54,10 @@ class ViTBottleneck3D(nn.Module): Feed-forward output dropout rate. num_hidden_layers : int Number of transformer blocks. - patch_size : int - Cubic patch size for the 3D patch embedding. + patch_size : int | Sequence[int] + Cubic patch size for the 3D patch embedding, or per-axis + ``(D, H, W)`` extents. Use ``(1, p, p)`` for a Z-preserving 2D + configuration at ``D=1``. time_embed_dim : int | None Timestep embedding dimension for adaLN-Zero conditioning. Pass None for unconditional (deterministic) models. @@ -71,13 +75,13 @@ def __init__( dropout: float = 0.0, final_dropout: float = 0.0, num_hidden_layers: int = 2, - patch_size: int = 4, + patch_size: int | Sequence[int] = 4, time_embed_dim: int | None = None, ) -> None: super().__init__() self._in_channels = in_channels - self._patch_size = patch_size + self._patch_size = normalize_patch_size(patch_size) # ── Compute latent spatial size after encoder downsamples ──────── # Stride-2 convolutions require exact divisibility; floor division @@ -98,20 +102,23 @@ def __init__( latent_size = input_spatial_size[:1] + [s // factor for s in input_spatial_size[1:]] # ── Validate patch divisibility ───────────────────────────────── - for dim_val, name, orig in zip(latent_size, dim_names, input_spatial_size): - if dim_val % patch_size != 0: + # Checked per axis: a Z-preserving 2D configuration runs D=1 with a + # patch of (1, p, p), where a cubic p would be neither divisible nor + # convolvable. + for dim_val, name, orig, p in zip(latent_size, dim_names, input_spatial_size, self._patch_size): + if dim_val % p != 0: raise ValueError( f"Latent {name} dimension {dim_val} (from input {name}={orig}) " - f"is not divisible by patch_size={patch_size}. " + f"is not divisible by its patch extent {p} (patch_size={patch_size}). " f"Each spatial dimension after {num_downsamples} encoder downsamples " - f"must be divisible by patch_size." + f"must be divisible by the patch extent on that axis." ) - self.latent_grid_size = [s // patch_size for s in latent_size] + self.latent_grid_size = [s // p for s, p in zip(latent_size, self._patch_size)] # ── Patch embedding ───────────────────────────────────────────── self.img_embedding = PatchEmbed3D( - patch_size=patch_size, + patch_size=self._patch_size, in_chans=in_channels, embed_dim=hidden_size, bias=True, @@ -141,7 +148,7 @@ def __init__( # ── Output projection ─────────────────────────────────────────── self.proj_out = FinalLayer( hidden_size=hidden_size, - patch_size=patch_size, + patch_size=self._patch_size, out_channels=in_channels, time_embed_dim=time_embed_dim, ) diff --git a/packages/viscy-models/src/viscy_models/foundation/__init__.py b/packages/viscy-models/src/viscy_models/foundation/__init__.py index 12f1da3ee..fa52ba881 100644 --- a/packages/viscy-models/src/viscy_models/foundation/__init__.py +++ b/packages/viscy-models/src/viscy_models/foundation/__init__.py @@ -2,6 +2,7 @@ from viscy_models.foundation.cell_dino import CellDinoModel from viscy_models.foundation.dinov3 import DINOv3Model +from viscy_models.foundation.morphem import MorphEmModel from viscy_models.foundation.openphenom import OpenPhenomModel -__all__ = ["CellDinoModel", "DINOv3Model", "OpenPhenomModel"] +__all__ = ["CellDinoModel", "DINOv3Model", "MorphEmModel", "OpenPhenomModel"] diff --git a/packages/viscy-models/src/viscy_models/foundation/morphem.py b/packages/viscy-models/src/viscy_models/foundation/morphem.py new file mode 100644 index 000000000..f94cfe253 --- /dev/null +++ b/packages/viscy-models/src/viscy_models/foundation/morphem.py @@ -0,0 +1,160 @@ +"""MorphEm foundation model wrapper for frozen feature extraction.""" + +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch import Tensor + + +class MorphEmModel(nn.Module): + """Wrap CaicedoLab/MorphEm (DINO-pretrained ViT-S/16 on microscopy). + + MorphEm is a single-channel ViT: each microscopy channel is encoded + independently and the CLS tokens are reduced to a fixed embedding. + :meth:`preprocess_2d` handles Z-squeeze, resize to 224, and per-image + per-channel spatial z-score (the published ``PerImageNormalize`` recipe). + + Parameters + ---------- + model_name : str + HuggingFace id (``"CaicedoLab/MorphEm"``) or a local snapshot dir. + revision : str | None + Hub commit SHA (or tag) to load. MorphEm ships its ViT as + ``trust_remote_code`` Python, so an unpinned id would execute + whatever is at the repository head on a cache miss. Ignored for a + local snapshot dir. + img_size : int + Spatial size inputs are resized to, by default 224. + freeze : bool + If ``True`` (default), backbone params are frozen and kept in eval mode. + projection : nn.Module | None + Optional projection head applied to the pooled embedding. + """ + + def __init__( + self, + model_name: str, + revision: str | None = None, + img_size: int = 224, + freeze: bool = True, + projection: nn.Module | None = None, + ) -> None: + super().__init__() + + import transformers + from transformers import AutoModel + + # transformers 5.x compatibility shim. MorphEm's trust_remote_code + # VisionTransformer (authored against transformers 4.x) + # never sets `all_tied_weights_keys`, which 5.x's meta-device loader + # (_move_missing_keys_from_meta_to_device) requires -> AttributeError on load. + # Provide a no-tie class-level default; models whose tie_weights() runs set + # their own instance attr, which shadows this, so other loads are unaffected. + # Idempotent: only set when absent. Verified to load CaicedoLab/MorphEm on 5.9.0. + if not isinstance(getattr(transformers.PreTrainedModel, "all_tied_weights_keys", None), dict): + transformers.PreTrainedModel.all_tied_weights_keys = {} + + self.model = AutoModel.from_pretrained(model_name, revision=revision, trust_remote_code=True) + self.target_size = (img_size, img_size) + self.projection = projection + # Expose embed dim for foundation-wrapper parity (dynaclr foundation_engine + # may read it pre-forward). Mirror the katamari reference's 3-tier + # `_infer_embed_dim` (embed_dim -> num_features -> patch_embed.proj.out_channels); + # raise loud if none resolve rather than silently defaulting. + # Verified at load: CaicedoLab/MorphEm self-reports embed_dim == num_features == 384. + self.embed_dim = self._infer_embed_dim(self.model) + + self.freeze = freeze + if freeze: + self.model.requires_grad_(False) + self.model.eval() + # Single-channel contract: MorphEm is a 1-channel ViT (CHAMMI feeds one + # channel at a time). Assert at load so a 3-channel patch_embed surfaces + # immediately instead of erroring deep in forward(). + proj = getattr(getattr(self.model, "patch_embed", None), "proj", None) + in_ch = getattr(proj, "in_channels", None) + if in_ch is not None and in_ch != 1: + raise ValueError( + f"MorphEm expected a single-channel patch_embed (in_channels=1), got {in_ch}. " + "The single-channel feed in forward() would error; revisit channel handling." + ) + + @staticmethod + def _infer_embed_dim(model: nn.Module) -> int: + """Resolve the backbone embedding dimension from common ViT attributes.""" + for attr in ("embed_dim", "num_features"): + value = getattr(model, attr, None) + if value is not None: + return int(value) + proj = getattr(getattr(model, "patch_embed", None), "proj", None) + out_channels = getattr(proj, "out_channels", None) + if out_channels is not None: + return int(out_channels) + raise ValueError("Unable to infer embed_dim from the MorphEm backbone.") + + def train(self, mode: bool = True) -> "MorphEmModel": + """Keep the backbone in eval when frozen.""" + super().train(mode) + if self.freeze: + self.model.eval() + return self + + def preprocess_2d(self, x: Tensor) -> Tensor: + """Convert a raw dataloader tensor to MorphEm input. + + Squeezes a singleton Z (middle slice if Z>1), applies per-image + per-channel spatial z-score (zero mean, unit std), THEN resizes to + ``target_size`` — matching the published recipe ordering + ``Compose([PerImageNormalize(), Resize(224)])`` (normalize before + resize; reversing the order shifts the post-interp mean/scale). + + Parameters + ---------- + x : Tensor + ``(B, C, D, H, W)`` or ``(B, C, H, W)``. + + Returns + ------- + Tensor + ``(B, C, target, target)`` float ready for :meth:`forward`. + """ + if x.ndim == 5: + x = x[:, :, 0] if x.shape[2] == 1 else x[:, :, x.shape[2] // 2] + x = x.to(torch.float32) + # PerImageNormalize: per-image per-channel spatial z-score. Uses the + # (x-m)/(std+eps) form matching CellDinoModel.preprocess_2d (the VisCy + # self_normalize convention) — numerically equivalent to the reference + # InstanceNorm2d's (x-m)/sqrt(var+eps) for any non-degenerate crop. + m = x.mean(dim=(-2, -1), keepdim=True) + s = x.std(dim=(-2, -1), unbiased=False, keepdim=True) + x = (x - m) / (s + 1e-7) + # Resize AFTER normalize (published Compose order). antialias=True matches + # the reference v2.Resize(antialias=True); inert on upscale, correct on downscale. + if x.shape[-2:] != self.target_size: + x = F.interpolate(x, size=self.target_size, mode="bilinear", align_corners=False, antialias=True) + return x + + def forward(self, x: Tensor) -> tuple[Tensor, Tensor]: + """Encode a (preprocessed) image batch to a channel-mean CLS embedding. + + Each channel is encoded independently (MorphEm is single-channel), + and the CLS tokens are mean-pooled over channels to a fixed ``(B, D)``. + + Parameters + ---------- + x : Tensor + ``(B, C, H, W)``. + + Returns + ------- + tuple[Tensor, Tensor] + ``(features, projection(features) or features)``, each ``(B, D)``. + """ + b, c = x.shape[0], x.shape[1] + x = x.reshape(b * c, 1, x.shape[-2], x.shape[-1]) + out = self.model.forward_features(x) + cls = out["x_norm_clstoken"] # (B*C, D) + cls = cls.reshape(b, c, -1).mean(dim=1) # (B, D) + if self.projection is not None: + return cls, self.projection(cls) + return cls, cls diff --git a/packages/viscy-models/src/viscy_models/gan/patchgan3d.py b/packages/viscy-models/src/viscy_models/gan/patchgan3d.py index de6ff00a1..e53c420d2 100644 --- a/packages/viscy-models/src/viscy_models/gan/patchgan3d.py +++ b/packages/viscy-models/src/viscy_models/gan/patchgan3d.py @@ -45,6 +45,13 @@ class PatchGAN3D(nn.Module): channel. Default is 64. use_spectral_norm : bool, optional If True, apply ``spectral_norm`` to every conv. Default is True. + preserve_z : bool, optional + If True, layers 1-4 take ``kernel_size=(1, 4, 4)``, ``padding=(0, 1, 1)`` + and unit Z stride, so the Z extent passes through untouched. Required + at ``Z=1``: the default cubic ``kernel_size=4`` with ``padding=1`` + gives a padded Z extent of 3 against a size-4 kernel, which raises. + Leave False for 3D, where it is the published topology. Default is + False. Returns ------- @@ -59,6 +66,7 @@ def __init__( in_channels: int = 2, base_channels: int = 64, use_spectral_norm: bool = True, + preserve_z: bool = False, ) -> None: super().__init__() c1 = base_channels @@ -66,9 +74,16 @@ def __init__( c3 = base_channels * 4 c4 = base_channels * 8 + # Z geometry of layers 1-4. The 3D branch reproduces the published + # cubic kernel exactly; the Z-preserving branch collapses the kernel + # and padding on that axis and drops the Z stride of layers 3-4. + kernel = (1, 4, 4) if preserve_z else 4 + padding = (0, 1, 1) if preserve_z else 1 + stride_z = 1 if preserve_z else 2 + self.layer1 = nn.Sequential( _maybe_spectral_norm( - nn.Conv3d(in_channels, c1, kernel_size=4, stride=(1, 2, 2), padding=1), + nn.Conv3d(in_channels, c1, kernel_size=kernel, stride=(1, 2, 2), padding=padding), use_spectral_norm, ), nn.LeakyReLU(0.2, inplace=True), @@ -76,7 +91,7 @@ def __init__( self.layer2 = nn.Sequential( _maybe_spectral_norm( - nn.Conv3d(c1, c2, kernel_size=4, stride=(1, 2, 2), padding=1), + nn.Conv3d(c1, c2, kernel_size=kernel, stride=(1, 2, 2), padding=padding), use_spectral_norm, ), nn.InstanceNorm3d(c2, affine=True), @@ -85,7 +100,7 @@ def __init__( self.layer3 = nn.Sequential( _maybe_spectral_norm( - nn.Conv3d(c2, c3, kernel_size=4, stride=(2, 2, 2), padding=1), + nn.Conv3d(c2, c3, kernel_size=kernel, stride=(stride_z, 2, 2), padding=padding), use_spectral_norm, ), nn.InstanceNorm3d(c3, affine=True), @@ -94,7 +109,7 @@ def __init__( self.layer4 = nn.Sequential( _maybe_spectral_norm( - nn.Conv3d(c3, c4, kernel_size=4, stride=(2, 2, 2), padding=1), + nn.Conv3d(c3, c4, kernel_size=kernel, stride=(stride_z, 2, 2), padding=padding), use_spectral_norm, ), nn.InstanceNorm3d(c4, affine=True), @@ -153,6 +168,10 @@ class MultiScalePatchGAN3D(nn.Module): use_spectral_norm : bool, optional If True, apply spectral normalization to every conv in each scale. Default is True. + preserve_z : bool, optional + Forwarded to every :class:`PatchGAN3D`. Set True for a Z-preserving + (``Z=1``) discriminator. The inter-scale pooling is already + YX-only, so no other change is needed. Default is False. Returns ------- @@ -166,6 +185,7 @@ def __init__( base_channels: int = 64, num_scales: int = 2, use_spectral_norm: bool = True, + preserve_z: bool = False, ) -> None: super().__init__() if num_scales < 1: @@ -177,6 +197,7 @@ def __init__( in_channels=in_channels, base_channels=base_channels, use_spectral_norm=use_spectral_norm, + preserve_z=preserve_z, ) for _ in range(num_scales) ] diff --git a/packages/viscy-models/src/viscy_models/unet/unet3d.py b/packages/viscy-models/src/viscy_models/unet/unet3d.py index ed32e1396..a190afbb1 100644 --- a/packages/viscy-models/src/viscy_models/unet/unet3d.py +++ b/packages/viscy-models/src/viscy_models/unet/unet3d.py @@ -39,9 +39,10 @@ class Unet3d(UNet3DBase): FNet-configured preset of the unified 3D U-Net base with BatchNorm, ReLU activations, non-residual double-conv blocks, and a convolutional - bottleneck. Downsamples all three spatial dimensions. + bottleneck. Downsamples Y and X, and (when ``downsample_z=True``) Z. - All spatial dimensions (Z, Y, X) must be divisible by ``2**depth``. + Each downsampled spatial dimension must be divisible by ``2**depth``: Y and + X always, and Z only when ``downsample_z=True``. Parameters ---------- @@ -56,8 +57,14 @@ class Unet3d(UNet3DBase): in_stack_depth : int or None Z-window size. Stored for engine compatibility (``example_input_array``, ``DivisiblePad``, sliding window prediction). - The model itself handles arbitrary Z as long as it is divisible - by ``2**depth``. + The model itself handles arbitrary Z; when ``downsample_z=True`` the Z + size must be divisible by ``2**depth`` (no constraint when ``False``). + downsample_z : bool + Whether to downsample the Z axis in the encoder (``True`` preserves the + FNet3D behavior). Set ``False`` for a Z-preserving 2D-style network + (stride ``(1, 2, 2)``, ``ConvTranspose3d`` kernel ``(1, 3, 3)``), which + is required to run at ``Z=1`` without tripping the base + ``D % 2**depth`` divisibility check. """ def __init__( @@ -67,6 +74,7 @@ def __init__( depth: int = 4, mult_chan: int = 32, in_stack_depth: int | None = None, + downsample_z: bool = True, ) -> None: dims = [mult_chan * (2**i) for i in range(depth + 1)] bottleneck = ConvBottleneck3D(dims[-1], residual=False, norm="batch", activation="relu") @@ -76,7 +84,7 @@ def __init__( dims=dims, num_res_block=[1] * depth, bottleneck=bottleneck, - downsample_z=True, + downsample_z=downsample_z, residual=False, norm="batch", activation="relu", diff --git a/packages/viscy-models/src/viscy_models/unet/unet3d_base.py b/packages/viscy-models/src/viscy_models/unet/unet3d_base.py index d5aae7ef5..2be04af97 100644 --- a/packages/viscy-models/src/viscy_models/unet/unet3d_base.py +++ b/packages/viscy-models/src/viscy_models/unet/unet3d_base.py @@ -93,6 +93,27 @@ def __init__( self._cond_inconv = nn.Conv3d(cond_channels, dims[0], kernel_size=3, stride=1, padding=1) # ── Stride configuration ──────────────────────────────────────── + # KNOWN ARTIFACT: kernel 3 with stride 2 is the checkerboard condition + # (Odena et al., "Deconvolution and Checkerboard Artifacts", 2016). + # 3 is not divisible by 2, so kernel placements overlap unevenly and + # each ConvTranspose imprints a 2-periodic modulation; three stacked + # upsamples put energy at 2, 4 and 8 px. It is present IN distribution, + # not only out of it: on an iPSC-trained membrane prediction of an iPSC + # FOV the period-2 component measured 216x its ground-truth amplitude + # and period-4 67x. Out of distribution it grows further -- on an + # A549-trained ER prediction of an iPSC FOV the 4 px peak reached 16x + # its spectral background. It is visible in every + # model built on this base (UNetViT3D and FNet3D alike), which is how + # it was traced here -- a shared artifact across two otherwise + # unrelated architectures. + # + # The clean fix is kernel_size 4 with output_padding 0 (kernel + # divisible by stride -> uniform overlap), or nearest-neighbour + # Upsample + Conv3d. Both change weight shapes and so invalidate every + # existing checkpoint, which is why this is documented rather than + # changed: the published benchmark numbers are tied to these weights. + # For inference-time suppression on existing checkpoints see + # ``dynacell.engine._phase_shift_average``. if downsample_z: down_stride = (2, 2, 2) up_kwargs = dict(kernel_size=3, stride=(2, 2, 2), padding=1, output_padding=1) @@ -158,7 +179,15 @@ def forward(self, x: Tensor, cond: Tensor | None = None, t: Tensor | None = None ------- Tensor Output tensor of shape ``(B, out_channels, D, H, W)``. + + Raises + ------ + ValueError + If ``x`` is not a 5D tensor, or a downsampled spatial dimension is + not divisible by ``2**num_blocks``. """ + if x.ndim != 5: + raise ValueError(f"Expected 5D input (B, C, D, H, W), got {x.ndim}D.") for dim_name, size in zip(("D", "H", "W"), x.shape[2:]): if self.downsamples_z or dim_name != "D": if size % self._divisor != 0: diff --git a/packages/viscy-models/tests/test_celldiff/test_celldiff_net.py b/packages/viscy-models/tests/test_celldiff/test_celldiff_net.py index 225150911..8b84c9c1b 100644 --- a/packages/viscy-models/tests/test_celldiff/test_celldiff_net.py +++ b/packages/viscy-models/tests/test_celldiff/test_celldiff_net.py @@ -56,5 +56,5 @@ def test_wrong_cond_channels_raises(small_config): def test_indivisible_patch_size_raises(small_config): """Constructor rejects spatial sizes not divisible by patch_size after downsampling.""" - with pytest.raises(ValueError, match="not divisible by patch_size"): + with pytest.raises(ValueError, match="not divisible by its patch extent"): CELLDiffNet(**{**small_config, "input_spatial_size": [10, 64, 64]}) diff --git a/packages/viscy-models/tests/test_celldiff/test_transport.py b/packages/viscy-models/tests/test_celldiff/test_transport.py index d92fa635b..0c7e35b0d 100644 --- a/packages/viscy-models/tests/test_celldiff/test_transport.py +++ b/packages/viscy-models/tests/test_celldiff/test_transport.py @@ -110,3 +110,76 @@ def dummy_model(x, t): # Result is a trajectory tensor from odeint. assert result.shape[0] == 5 assert result.shape[1:] == x_init.shape + + +def test_ode_solver_cosine_schedule_packs_points_at_both_ends(): + """The cosine grid keeps the endpoints and spaces points densest next to t0 and t1.""" + kwargs = dict(drift=lambda x, t, model: x, t0=0.0, t1=1.0, sampler_type="euler", num_steps=9, atol=1e-5, rtol=1e-3) + uniform = ODESolver(**kwargs).t + cosine = ODESolver(**kwargs, time_schedule="cosine").t + torch.testing.assert_close(uniform, torch.linspace(0, 1, 9)) + assert cosine[0] == 0.0 and cosine[-1] == 1.0 + gaps = cosine.diff() + assert gaps[0] < gaps[4] and gaps[-1] < gaps[4] + torch.testing.assert_close(gaps, gaps.flip(0)) + + +def test_ode_solver_beta_schedule_spans_cosine_and_uniform(): + """Beta(0.5, 0.5) is the cosine grid, Beta(1, 1) the uniform one, and p < q packs points toward t0.""" + kwargs = dict(drift=lambda x, t, model: x, t0=0.0, t1=1.0, sampler_type="euler", num_steps=8, atol=1e-5, rtol=1e-3) + torch.testing.assert_close( + ODESolver(**kwargs, time_schedule=(0.5, 0.5)).t, ODESolver(**kwargs, time_schedule="cosine").t + ) + torch.testing.assert_close(ODESolver(**kwargs, time_schedule=(1.0, 1.0)).t, ODESolver(**kwargs).t) + skewed = ODESolver(**kwargs, time_schedule=(0.3, 0.6)).t + assert skewed[0] == 0.0 and skewed[-1] == 1.0 + gaps = skewed.diff() + assert (gaps > 0).all() and gaps[0] < gaps[-1] + inner = ODESolver(**{**kwargs, "t0": 0.1, "t1": 0.9}, time_schedule=(0.3, 0.6)).t + torch.testing.assert_close(inner, 0.1 + 0.8 * skewed) + + +def test_ode_solver_rejects_a_beta_grid_that_collapses_in_float32(): + """Beta(1, 0.1) at 8 points puts its last two quantiles within float32 rounding of 1.0, which odeint + would only reject mid-predict.""" + with pytest.raises(ValueError, match="strictly increasing"): + ODESolver( + drift=lambda x, t, model: x, + t0=0.0, + t1=1.0, + sampler_type="euler", + num_steps=8, + atol=1e-5, + rtol=1e-3, + time_schedule=(1.0, 0.1), + ) + + +@pytest.mark.parametrize("schedule", ["cos", (0.0, 0.5), (0.5,), (0.5, float("nan")), (0.5, float("inf"))]) +def test_ode_solver_rejects_unknown_schedule(schedule): + """A misspelled schedule or a non-positive or non-finite Beta shape raises instead of building a bad grid.""" + with pytest.raises(ValueError, match="time_schedule"): + ODESolver( + drift=lambda x, t, model: x, + t0=0.0, + t1=1.0, + sampler_type="euler", + num_steps=5, + atol=1e-5, + rtol=1e-3, + time_schedule=schedule, + ) + + +def test_sampler_euler_with_cosine_schedule_steps_on_the_grid(): + """A fixed-grid sampler evaluates the drift once per step, at the cosine grid's left points.""" + sampler = Sampler(create_transport()) + seen = [] + + def model(x, t): + seen.append(float(t[0])) + return torch.zeros_like(x) + + sampler.sample_ode(sampling_method="euler", num_steps=5, time_schedule="cosine")(torch.randn(1, 1, 2, 2, 2), model) + expected = (1 - torch.cos(torch.pi * torch.linspace(0, 1, 5, dtype=torch.float64))) / 2 + assert seen == pytest.approx(expected[:-1].tolist(), abs=1e-6) diff --git a/packages/viscy-models/tests/test_celldiff/test_unet_vit_3d.py b/packages/viscy-models/tests/test_celldiff/test_unet_vit_3d.py index 3260305bc..be65bc242 100644 --- a/packages/viscy-models/tests/test_celldiff/test_unet_vit_3d.py +++ b/packages/viscy-models/tests/test_celldiff/test_unet_vit_3d.py @@ -52,5 +52,5 @@ def test_wrong_spatial_raises(small_config): def test_indivisible_patch_size_raises(small_config): """Constructor rejects spatial sizes not divisible by patch_size after downsampling.""" # D=10: after 2 downsamples at stride (1,2,2), latent D=10, not divisible by patch_size=4 - with pytest.raises(ValueError, match="not divisible by patch_size"): + with pytest.raises(ValueError, match="not divisible by its patch extent"): UNetViT3D(**{**small_config, "input_spatial_size": [10, 64, 64]}) diff --git a/packages/viscy-models/tests/test_foundation/test_morphem.py b/packages/viscy-models/tests/test_foundation/test_morphem.py new file mode 100644 index 000000000..a403c17ce --- /dev/null +++ b/packages/viscy-models/tests/test_foundation/test_morphem.py @@ -0,0 +1,110 @@ +"""Tests for the MorphEm foundation wrapper. + +``transformers.AutoModel`` is stubbed so the test never touches the network or +the HF cache; it exercises the real :class:`MorphEmModel` preprocessing and +channel-mean forward contract against a fake ViT-S backbone. +""" + +import pytest +import torch + +from viscy_models.foundation import MorphEmModel + +_EMBED_DIM = 384 +_N_PATCHES = 196 # (224 / 16) ** 2 + + +class _StubBackbone(torch.nn.Module): + """Minimal MorphEm stand-in: single-channel patch embed + forward_features.""" + + def __init__(self, embed_dim: int = _EMBED_DIM) -> None: + super().__init__() + self.embed_dim = embed_dim + self.num_features = embed_dim + self.patch_embed = torch.nn.Module() + self.patch_embed.proj = torch.nn.Conv2d(1, embed_dim, kernel_size=16, stride=16) + + def forward_features(self, x: torch.Tensor) -> dict[str, torch.Tensor]: + n = x.shape[0] + # Make the CLS token depend on the input so embeddings are non-constant. + cls = x.flatten(1).mean(dim=1, keepdim=True).expand(n, self.embed_dim).clone() + return { + "x_norm_clstoken": cls, + "x_norm_patchtokens": torch.zeros(n, _N_PATCHES, self.embed_dim), + } + + +@pytest.fixture +def captured_load(monkeypatch): + """Stub ``AutoModel.from_pretrained`` and capture its call args.""" + captured: dict = {} + + def _fake_from_pretrained(name, **kwargs): + captured["name"] = name + captured["kwargs"] = kwargs + return _StubBackbone() + + monkeypatch.setattr("transformers.AutoModel.from_pretrained", _fake_from_pretrained) + return captured + + +def test_load_passes_trust_remote_code(captured_load): + """The wrapper loads via trust_remote_code and infers the embed dim.""" + model = MorphEmModel(model_name="CaicedoLab/MorphEm") + assert captured_load["name"] == "CaicedoLab/MorphEm" + assert captured_load["kwargs"]["trust_remote_code"] is True + assert captured_load["kwargs"]["revision"] is None + assert model.embed_dim == _EMBED_DIM + + +def test_load_forwards_pinned_revision(captured_load): + """A pinned Hub commit reaches ``from_pretrained`` so the remote code is frozen.""" + MorphEmModel(model_name="CaicedoLab/MorphEm", revision="0e8d58787421f83f975634d72420d85c5dfc9c2c") + assert captured_load["kwargs"]["revision"] == "0e8d58787421f83f975634d72420d85c5dfc9c2c" + assert captured_load["kwargs"]["trust_remote_code"] is True + + +def test_preprocess_2d_normalizes_and_resizes(captured_load): + """5-D input is Z-squeezed, per-image z-scored, then resized to 224.""" + model = MorphEmModel(model_name="CaicedoLab/MorphEm") + x = torch.randn(2, 1, 1, 40, 40) + out = model.preprocess_2d(x) + assert out.shape == (2, 1, 224, 224) + # Per-image z-score happens before resize; upscale bilinear keeps mean ~0. + per_image_mean = out.mean(dim=(-2, -1)) + assert torch.all(per_image_mean.abs() < 0.05) + assert torch.all(out.std(dim=(-2, -1)) > 0.1) + + +def test_forward_returns_features_projection_tuple(captured_load): + """Forward on a single-channel batch returns a (features, proj) (B, D) tuple.""" + model = MorphEmModel(model_name="CaicedoLab/MorphEm") + x = torch.randn(2, 1, 224, 224) + features, projection = model(x) + assert features.shape == (2, _EMBED_DIM) + assert projection.shape == (2, _EMBED_DIM) + assert torch.equal(features, projection) # no projection head -> identity + + +def test_forward_channel_mean(captured_load): + """Multi-channel input is encoded per-channel and mean-pooled to (B, D).""" + model = MorphEmModel(model_name="CaicedoLab/MorphEm") + x = torch.randn(2, 3, 224, 224) + features, _ = model(x) + assert features.shape == (2, _EMBED_DIM) + + +def test_single_channel_assert(monkeypatch): + """A 3-channel patch_embed is rejected at load time.""" + + class _ThreeChannel(_StubBackbone): + def __init__(self) -> None: + super().__init__() + self.patch_embed.proj = torch.nn.Conv2d(3, _EMBED_DIM, kernel_size=16, stride=16) + + monkeypatch.setattr( + "transformers.AutoModel.from_pretrained", + lambda name, **kwargs: _ThreeChannel(), + ) + with pytest.raises(ValueError, match="single-channel"): + MorphEmModel(model_name="CaicedoLab/MorphEm") diff --git a/packages/viscy-models/tests/test_gan/test_patchgan3d.py b/packages/viscy-models/tests/test_gan/test_patchgan3d.py index b2bc8af9d..d7bf48915 100644 --- a/packages/viscy-models/tests/test_gan/test_patchgan3d.py +++ b/packages/viscy-models/tests/test_gan/test_patchgan3d.py @@ -1,5 +1,6 @@ """Tests for 3D PatchGAN discriminators.""" +import pytest import torch from viscy_models.gan import MultiScalePatchGAN3D, PatchGAN3D @@ -63,3 +64,78 @@ def test_multi_scale_patchgan3d_single_scale_ablation(): out = model(x) assert isinstance(out, list) assert len(out) == 1 + + +def test_patchgan3d_default_raises_at_unit_depth(): + """The cubic default cannot run at Z=1 — this is what preserve_z exists for.""" + model = PatchGAN3D(in_channels=2) + with pytest.raises(RuntimeError, match="Kernel size can't be greater than actual input size"): + model(torch.randn(1, 2, 1, 64, 64)) + + +def test_patchgan3d_preserve_z_runs_at_unit_depth(): + """preserve_z=True accepts Z=1 and keeps the Z extent at 1.""" + model = PatchGAN3D(in_channels=2, preserve_z=True) + y = model(torch.randn(2, 2, 1, 64, 64)) + assert y.ndim == 5 + assert y.shape[:2] == (2, 1) + assert y.shape[2] == 1, f"Z must pass through, got {y.shape}" + assert y.shape[-1] == y.shape[-2], f"Expected square YX, got {y.shape}" + + +def test_patchgan3d_preserve_z_keeps_depth_for_a_slab() -> None: + """Z is preserved, not merely tolerated: a 5-plane slab stays 5 planes. + + Layers 3-4 stride Z by 2 in the 3D topology, which would halve a slab + twice. ``preserve_z`` drops that stride, so the discriminator never mixes + planes the Z-preserving generator kept separate. + """ + model = PatchGAN3D(in_channels=2, preserve_z=True) + y = model(torch.randn(1, 2, 5, 64, 64)) + assert y.shape[2] == 5, f"expected Z=5 through the discriminator, got {y.shape}" + + +def test_patchgan3d_preserve_z_leaves_the_3d_topology_untouched() -> None: + """preserve_z=False must reproduce the published 3D discriminator exactly. + + Guards the shared code path: the 2D arm was added by parameterizing the + same convs the 14 pix2pix3d leaves train, so an accidental change to the + default would silently alter every published GAN number. + """ + torch.manual_seed(0) + default = PatchGAN3D(in_channels=2) + torch.manual_seed(0) + explicit = PatchGAN3D(in_channels=2, preserve_z=False) + for (name, a), (_, b) in zip(default.named_parameters(), explicit.named_parameters(), strict=True): + assert torch.equal(a, b), f"{name} differs" + for layer in (default.layer1, default.layer2): + assert layer[0].kernel_size == (4, 4, 4) + assert layer[0].padding == (1, 1, 1) + assert layer[0].stride == (1, 2, 2) + for layer in (default.layer3, default.layer4): + assert layer[0].kernel_size == (4, 4, 4) + assert layer[0].stride == (2, 2, 2) + x = torch.randn(1, 2, 8, 64, 64) + with torch.no_grad(): + assert torch.equal(default(x), explicit(x)) + + +def test_multi_scale_patchgan3d_preserve_z_at_unit_depth() -> None: + """The multi-scale wrapper forwards preserve_z and pools YX only.""" + model = MultiScalePatchGAN3D(in_channels=2, num_scales=2, preserve_z=True) + out = model(torch.randn(1, 2, 1, 128, 128)) + assert len(out) == 2 + assert all(o.shape[2] == 1 for o in out), [tuple(o.shape) for o in out] + assert out[1].shape[-1] < out[0].shape[-1] + + +def test_patchgan3d_preserve_z_gradient_flow() -> None: + """Every conv weight receives a gradient at Z=1.""" + model = PatchGAN3D(in_channels=2, preserve_z=True) + model(torch.randn(2, 2, 1, 64, 64)).sum().backward() + missing = [ + name + for name, param in model.named_parameters() + if "weight" in name and (param.grad is None or param.grad.abs().sum().item() == 0.0) + ] + assert not missing, f"no gradient reached {missing}" diff --git a/packages/viscy-models/tests/test_unet/test_unet3d.py b/packages/viscy-models/tests/test_unet/test_unet3d.py index 1e1ed76e5..58bf4e50c 100644 --- a/packages/viscy-models/tests/test_unet/test_unet3d.py +++ b/packages/viscy-models/tests/test_unet/test_unet3d.py @@ -67,6 +67,33 @@ def test_engine_attributes(): assert model.downsamples_z is True +def test_downsample_z_default_preserves_fnet3d(): + """Default downsample_z keeps the FNet3D Z-downsampling behavior.""" + model = Unet3d(depth=4, mult_chan=32, in_stack_depth=32) + assert model.downsamples_z is True + + +def test_downsample_z_false_z1_forward(): + """downsample_z=False enables the FNet2D use case: a Z=1 forward runs and preserves Z. + + With downsample_z=True (the FNet3D default) a Z=1 input trips the base + ``D % 2**depth`` divisor check; the Z-preserving path must skip that check. + """ + model = Unet3d(in_channels=1, out_channels=1, depth=4, mult_chan=32, downsample_z=False) + assert model.downsamples_z is False + x = torch.randn(2, 1, 1, 64, 64) # Y,X divisible by 2**4=16; Z=1 + y = model(x) + assert y.shape == (2, 1, 1, 64, 64) + + +def test_downsample_z_false_skips_z_divisor_check(): + """With downsample_z=False, Z need not be divisible by 2**depth (only Y/X are checked).""" + model = Unet3d(in_channels=1, out_channels=1, depth=2, mult_chan=16, downsample_z=False) + x = torch.randn(1, 1, 3, 16, 16) # Z=3 not divisible by 2^2=4, but Z is not downsampled + y = model(x) + assert y.shape == (1, 1, 3, 16, 16) + + def test_state_dict_keys(): """State dict uses iterative encoder-decoder key structure.""" model = Unet3d(in_channels=1, out_channels=1, depth=2, mult_chan=16) diff --git a/packages/viscy-models/tests/test_unet/test_unet3d_base.py b/packages/viscy-models/tests/test_unet/test_unet3d_base.py index cd54eb260..aa6c59dd2 100644 --- a/packages/viscy-models/tests/test_unet/test_unet3d_base.py +++ b/packages/viscy-models/tests/test_unet/test_unet3d_base.py @@ -173,3 +173,10 @@ def test_dims_mismatch_raises(): """len(dims) != len(num_res_block) + 1 raises ValueError.""" with pytest.raises(ValueError, match="len\\(dims\\)"): _make_base(dims=(16, 32, 64), num_res_block=(1,)) + + +def test_forward_non_5d_input_raises(): + """A non-5D input raises a clear ValueError, not a confusing Conv3d channel error.""" + model = _make_base() + with pytest.raises(ValueError, match="Expected 5D input"): + model(torch.randn(2, 1, 16, 16)) # 4D: a squeezed Z dim diff --git a/packages/viscy-transforms/src/viscy_transforms/_affine.py b/packages/viscy-transforms/src/viscy_transforms/_affine.py index 34f51100d..8c97524d2 100644 --- a/packages/viscy-transforms/src/viscy_transforms/_affine.py +++ b/packages/viscy-transforms/src/viscy_transforms/_affine.py @@ -20,10 +20,17 @@ class _PaddedRandomAffine3D(RandomAffine3D): - """RandomAffine3D with configurable padding_mode. + """RandomAffine3D with configurable padding_mode and depth-1 safety. Kornia 0.8.x hard-codes ``padding_mode='zeros'`` in apply_transform. - This subclass overrides that call to forward the user-specified mode. + This subclass overrides that call to forward the user-specified mode, + and forces the matrix row of any singleton spatial axis to identity. + + Note the neutralized row is applied to a copy, so + ``self._transform_matrix`` (and therefore + :meth:`RandomAffine3D.inverse`) still records the un-neutralized matrix + on a singleton axis. Nothing in VisCy reads those, but do not rely on + them for such inputs without re-applying the same neutralization. """ def __init__(self, *args: object, padding_mode: str = "zeros", **kwargs: object) -> None: @@ -37,9 +44,29 @@ def apply_transform( flags: dict, transform: Tensor | None = None, ) -> Tensor: + # clone() is load-bearing: transform[:, :3] is a VIEW into the same + # tensor kornia stores as self._transform_matrix right after this + # returns, so mutating it in place would corrupt the recorded + # transform (and hence RandomAffine3D.inverse()). It is not about + # contiguity -- warp_affine3d accepts the non-contiguous view -- and + # not about the source/target pairing, which kornia rebuilds per key. + matrix = transform[:, :3].clone() + # Kornia normalizes pixel coordinates with a 2 / (size - 1) scale and + # guards a singleton axis by dividing by eps=1e-14 instead, so that + # axis' scale blows up to ~2e14. Any non-identity entry in the + # corresponding matrix row then pushes every sampled coordinate out of + # bounds and padding_mode="zeros" blanks the volume. A singleton axis + # has no extent to rotate, shear, scale or translate along, so identity + # is the correct row. Other rows are left alone and still warp + # normally; their terms for this axis are divided by the same 2e14 and + # vanish. Rows are ordered (x, y, z), i.e. (W, H, D). + for row, size in enumerate((input.shape[-1], input.shape[-2], input.shape[-3])): + if size == 1: + matrix[:, row] = 0.0 + matrix[:, row, row] = 1.0 return warp_affine3d( input, - transform[:, :3, :], + matrix, (input.shape[-3], input.shape[-2], input.shape[-1]), flags["resample"].name.lower(), padding_mode=self._padding_mode, @@ -289,6 +316,12 @@ def _scale_z_shear_facets(params: dict[str, Tensor], shape: torch.Size) -> dict[ produces ~64x more displacement than the same angle across Z=8. This method reduces Z-related facets so the pixel displacement in Z is proportional to the Z depth. + + This is augmentation *policy* and is opt-out via ``scale_z_shear``. + Depth-1 safety is a correctness invariant handled separately, in + ``_PaddedRandomAffine3D.apply_transform`` — do not add depth-1 + handling here, where it would cover shear only and could be + switched off. """ z_depth = shape[2] yx_size = max(shape[3], shape[4]) diff --git a/packages/viscy-transforms/src/viscy_transforms/_normalize.py b/packages/viscy-transforms/src/viscy_transforms/_normalize.py index 50457dd66..1de096fa0 100644 --- a/packages/viscy-transforms/src/viscy_transforms/_normalize.py +++ b/packages/viscy-transforms/src/viscy_transforms/_normalize.py @@ -26,6 +26,9 @@ def _match_image(tensor: Tensor, target: Tensor) -> Tensor: class NormalizeSampled(MapTransform): is_spatial = False + # Each voxel maps through precomputed statistics alone, so a whole volume normalized + # once equals each window normalized on its own (``SlidingWindowDataset.cache_volume``). + pointwise = True """Normalize using precomputed statistics stored in ``sample["norm_meta"]``. @@ -82,6 +85,8 @@ def __call__(self, sample: Sample) -> Sample: class MinMaxSampled(MapTransform): is_spatial = False + # See NormalizeSampled.pointwise. + pointwise = True """Normalize to [-1, 1] by clipping then rescaling with precomputed range statistics. diff --git a/packages/viscy-transforms/tests/test_affine.py b/packages/viscy-transforms/tests/test_affine.py index c9852ca3f..08a5195f4 100644 --- a/packages/viscy-transforms/tests/test_affine.py +++ b/packages/viscy-transforms/tests/test_affine.py @@ -1,5 +1,6 @@ import math +import pytest import torch from viscy_transforms import BatchedRandAffined @@ -302,3 +303,123 @@ def test_safe_crop_size_preserves_key_consistency(): torch.manual_seed(42) out = t(sample) assert torch.equal(out["source"], out["target"]) + + +def test_affine_depth_one_preserves_data(): + """A depth-1 volume must not be blanked by the affine. + + Kornia normalizes pixel coordinates with a ``2 / (D - 1)`` scale, which + at ``D == 1`` becomes ~2e14. Any non-identity entry in the transform's Z + row is then multiplied by that factor, pushing every sampled coordinate + out of bounds so ``padding_mode="zeros"`` returns an all-zero tensor. + + The 2D training overlays (``z_window_size: 1``) use exactly this shear + configuration, so a regression here silently blanks training data. + """ + t = BatchedRandAffined( + keys=["source", "target"], + prob=1.0, + rotate_range=[0, 0, 0], + shear_range=[0.0, 0.05, 0.05], + scale_range=[[1, 1], [0.5, 1.5], [0.5, 1.5]], + ) + base = torch.ones(4, 1, 1, 64, 64) + sample = {"source": base.clone(), "target": base.clone()} + + torch.manual_seed(0) + out = t(sample) + + # The regression signature is exactly 0.0. A fractional threshold would be + # seed-fragile: scale_range lets YX shrink to 0.5, legitimately taking a + # sample to ~0.30 nonzero on some seeds. + nonzero = (out["source"] != 0).float().mean(dim=(1, 2, 3, 4)) + assert (nonzero > 0).all(), f"depth-1 volume was blanked: per-sample nonzero {nonzero.tolist()}" + assert torch.equal(out["source"], out["target"]) + + +def test_affine_depth_one_still_transforms_in_plane(): + """Neutralizing the Z row must not disable the YX transform at D=1.""" + t = BatchedRandAffined( + keys=["source"], + prob=1.0, + rotate_range=[3.14, 0, 0], + scale_range=[[1, 1], [0.5, 1.5], [0.5, 1.5]], + ) + base = torch.zeros(2, 1, 1, 64, 64) + base[:, :, :, 16:48, 16:48] = 1.0 + + torch.manual_seed(0) + out = t({"source": base.clone()})["source"] + + assert (out != 0).any(), "depth-1 volume was blanked" + assert not torch.equal(out, base), "YX transform was not applied at D=1" + + +@pytest.mark.parametrize( + "index,name", + list(enumerate(["sxy", "sxz", "syx", "syz", "szx", "szy"])), +) +def test_affine_depth_one_all_shear_facets(index: int, name: str): + """Every individual shear facet is safe at depth 1.""" + shear = [(0.0, 0.0)] * 6 + shear[index] = (-0.05, 0.05) + t = BatchedRandAffined(keys=["source"], prob=1.0, shear_range=shear) + + torch.manual_seed(0) + out = t({"source": torch.ones(2, 1, 1, 64, 64)})["source"] + + assert float(out.mean()) > 0.5, f"facet {name} blanked the depth-1 volume" + + +@pytest.mark.parametrize( + "shape,label", + [ + ((2, 1, 1, 64, 64), "depth"), + ((2, 1, 8, 1, 64), "height"), + ((2, 1, 8, 64, 1), "width"), + ], +) +def test_affine_singleton_axis_preserves_data(shape: tuple[int, ...], label: str): + """Any singleton spatial axis is safe, not just depth. + + Kornia's ``2 / (size - 1)`` normalization degenerates identically for a + singleton W or H, so the guard covers every spatial axis. + """ + t = BatchedRandAffined(keys=["source"], prob=1.0, shear_range=[0.0, 0.05, 0.05]) + + torch.manual_seed(0) + out = t({"source": torch.ones(*shape)})["source"] + + assert (out != 0).any(), f"singleton {label} axis blanked the volume" + + +def test_affine_depth_one_partial_batch(): + """prob < 1 at depth 1 -- the only configuration production runs. + + The 2D overlays all use ``prob: 0.8``, which takes Kornia's mixed + ``to_apply`` branch: ``apply_transform`` is called with a *sliced* batch + and the result is merged back. The ``prob=1.0`` tests take a different + branch and would not catch a guard that read a batch-level shape. + """ + t = BatchedRandAffined( + keys=["source", "target"], + prob=0.5, + rotate_range=[0, 0, 0], + shear_range=[0.0, 0.05, 0.05], + scale_range=[[1, 1], [0.5, 1.5], [0.5, 1.5]], + ) + base = torch.ones(8, 1, 1, 64, 64) + + torch.manual_seed(0) + out = t({"source": base.clone(), "target": base.clone()}) + + # Pin that the batch really was partial, or this silently degrades into + # another prob=1 (or prob=0) test and stops covering the sliced branch. + transformed = [not torch.equal(out["source"][i], base[i]) for i in range(base.shape[0])] + assert 0 < sum(transformed) < base.shape[0], ( + f"batch was not partial: {sum(transformed)}/{base.shape[0]} transformed" + ) + + nonzero = (out["source"] != 0).float().mean(dim=(1, 2, 3, 4)) + assert (nonzero > 0).all(), f"partial batch had blanked samples: {nonzero.tolist()}" + assert torch.equal(out["source"], out["target"]) diff --git a/packages/viscy-transforms/tests/test_noise.py b/packages/viscy-transforms/tests/test_noise.py index 64c2f1940..0a480d510 100644 --- a/packages/viscy-transforms/tests/test_noise.py +++ b/packages/viscy-transforms/tests/test_noise.py @@ -1,3 +1,5 @@ +import math + import pytest import torch from monai.transforms import Compose @@ -34,11 +36,14 @@ def test_batched_gaussian_noise(device, ndim, prob, compose): @pytest.mark.parametrize("std", [2.0, 4.0]) @pytest.mark.parametrize("sample_std", [True, False]) def test_batched_gaussian_noise_statistics(mean, std, sample_std): + torch.manual_seed(0) img = torch.zeros(64, 8, 8, 8, 8) transform = BatchedRandGaussianNoise(prob=1.0, mean=mean, std=std, sample_std=sample_std) result = transform(img) assert (result.mean() - mean).abs() < 0.5 - expected_std = std / 2.0 if sample_std else std + # sample_std draws each sample's std from U(0, std); the pooled std is then + # sqrt(E[s^2]) = std / sqrt(3), not the mean draw std / 2. + expected_std = std / math.sqrt(3) if sample_std else std assert (result.std() - expected_std).abs() < 0.6 diff --git a/packages/viscy-utils/pyproject.toml b/packages/viscy-utils/pyproject.toml index 10e46f85f..5ea32d75b 100644 --- a/packages/viscy-utils/pyproject.toml +++ b/packages/viscy-utils/pyproject.toml @@ -43,6 +43,7 @@ dependencies = [ "tensorboard", "tensorstore", "torch>=2.10", + "torchvision", "viscy-data", "wandb>=0.12.10", "xarray", @@ -50,8 +51,8 @@ dependencies = [ optional-dependencies.all = [ "viscy-utils[anndata,eval]" ] # Cap anndata to <0.12.9 — 0.12.9 hard-requires pandas<3, but the rest of -# the stack runs on pandas 3 (see ArrowStringArray downcast workaround in -# callbacks/embedding_writer.py). Lift this cap once anndata 0.13 lands +# the stack runs on pandas 3 (see the ArrowStringArray downcast in +# evaluation/zarr_utils.py). Lift this cap once anndata 0.13 lands # with native pandas-3 support. optional-dependencies.anndata = [ "anndata<0.12.9", "natsort" ] optional-dependencies.eval = [ diff --git a/packages/viscy-utils/src/viscy_utils/__init__.py b/packages/viscy-utils/src/viscy_utils/__init__.py index c1ad90903..266c0387e 100644 --- a/packages/viscy-utils/src/viscy_utils/__init__.py +++ b/packages/viscy-utils/src/viscy_utils/__init__.py @@ -1,17 +1,27 @@ -from viscy_utils.log_images import detach_sample, render_images -from viscy_utils.mp_utils import get_val_stats, mp_wrapper -from viscy_utils.normalize import hist_clipping, unzscore, zscore -from viscy_utils.optimizers import configure_adamw_scheduler -from viscy_utils.tensor_utils import to_numpy +import importlib -__all__ = [ - "configure_adamw_scheduler", - "detach_sample", - "get_val_stats", - "hist_clipping", - "mp_wrapper", - "render_images", - "to_numpy", - "unzscore", - "zscore", -] +# Resolved lazily (PEP 562) so importing a light submodule such as +# ``viscy_utils.prediction_metadata`` does not pull in torch and lightning. +_LAZY_ATTRS = { + "configure_adamw_scheduler": "viscy_utils.optimizers", + "detach_sample": "viscy_utils.log_images", + "get_val_stats": "viscy_utils.mp_utils", + "hist_clipping": "viscy_utils.normalize", + "mp_wrapper": "viscy_utils.mp_utils", + "render_images": "viscy_utils.log_images", + "to_numpy": "viscy_utils.tensor_utils", + "unzscore": "viscy_utils.normalize", + "zscore": "viscy_utils.normalize", +} + +__all__ = list(_LAZY_ATTRS) + + +def __getattr__(name: str): + if name not in _LAZY_ATTRS: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + return getattr(importlib.import_module(_LAZY_ATTRS[name]), name) + + +def __dir__() -> list[str]: + return sorted([*globals(), *__all__]) diff --git a/packages/viscy-utils/src/viscy_utils/callbacks/__init__.py b/packages/viscy-utils/src/viscy_utils/callbacks/__init__.py index 6e41540e4..14eef220b 100644 --- a/packages/viscy-utils/src/viscy_utils/callbacks/__init__.py +++ b/packages/viscy-utils/src/viscy_utils/callbacks/__init__.py @@ -1,11 +1,28 @@ -from viscy_utils.callbacks.embedding_snapshot import EmbeddingSnapshotCallback -from viscy_utils.callbacks.embedding_writer import EmbeddingWriter -from viscy_utils.callbacks.online_eval import OnlineEvalCallback -from viscy_utils.callbacks.prediction_writer import HCSPredictionWriter - -__all__ = [ - "EmbeddingSnapshotCallback", - "EmbeddingWriter", - "OnlineEvalCallback", - "HCSPredictionWriter", -] +import importlib + +# Resolved lazily (PEP 562). VisCyCLI injects the health callbacks into every fit +# through this package, and OnlineEvalCallback needs scikit-learn, which only the +# ``eval`` extra installs, so an eager import would break fits on a base install. +_LAZY_ATTRS = { + "EmbeddingSnapshotCallback": "viscy_utils.callbacks.embedding_snapshot", + "EmbeddingWriter": "viscy_utils.callbacks.embedding_writer", + "LinearLRCooldown": "viscy_utils.callbacks.linear_lr_cooldown", + "MonitorHealthCheck": "viscy_utils.callbacks.monitor_health", + "MonitorHealthError": "viscy_utils.callbacks.monitor_health", + "OnlineEvalCallback": "viscy_utils.callbacks.online_eval", + "OptimizerHealthCheck": "viscy_utils.callbacks.optimizer_health", + "OptimizerHealthError": "viscy_utils.callbacks.optimizer_health", + "HCSPredictionWriter": "viscy_utils.callbacks.prediction_writer", +} + +__all__ = list(_LAZY_ATTRS) + + +def __getattr__(name: str): + if name not in _LAZY_ATTRS: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + return getattr(importlib.import_module(_LAZY_ATTRS[name]), name) + + +def __dir__() -> list[str]: + return sorted([*globals(), *__all__]) diff --git a/packages/viscy-utils/src/viscy_utils/callbacks/embedding_snapshot.py b/packages/viscy-utils/src/viscy_utils/callbacks/embedding_snapshot.py index c6ebf7645..bc0a45374 100644 --- a/packages/viscy-utils/src/viscy_utils/callbacks/embedding_snapshot.py +++ b/packages/viscy-utils/src/viscy_utils/callbacks/embedding_snapshot.py @@ -153,12 +153,14 @@ def on_validation_epoch_end(self, trainer: Trainer, pl_module: LightningModule) if self.store_images and self._images is not None: import anndata as ad + from viscy_utils.evaluation.zarr_utils import write_anndata_zarr + adata = ad.read_zarr(output_path) b = self._images.shape[0] adata.obsm["X_images"] = self._images.reshape(b, -1) # Mid-Z extraction produces (C, Y, X) per sample adata.uns["image_shape_cyx"] = list(self._images.shape[1:]) - adata.write_zarr(output_path) + write_anndata_zarr(adata, output_path) _logger.info(f"Embedding snapshot saved: {output_path} ({features_np.shape[0]} samples)") self._reset() diff --git a/packages/viscy-utils/src/viscy_utils/callbacks/embedding_writer.py b/packages/viscy-utils/src/viscy_utils/callbacks/embedding_writer.py index 373507b8f..f5346808a 100644 --- a/packages/viscy-utils/src/viscy_utils/callbacks/embedding_writer.py +++ b/packages/viscy-utils/src/viscy_utils/callbacks/embedding_writer.py @@ -146,8 +146,7 @@ def write_embedding_dataset( """ import anndata as ad - # pandas 2.x with PyArrow defaults ArrowStringArray; anndata zarr writer requires numpy-backed strings - pd.options.future.infer_string = False + from viscy_utils.evaluation.zarr_utils import write_anndata_zarr output_path = Path(output_path) @@ -156,15 +155,6 @@ def write_embedding_dataset( ultrack_indices = index_df.copy() ultrack_indices["fov_name"] = ultrack_indices["fov_name"].str.strip("/") - # TODO: remove once anndata 0.13 supports pandas 3 Arrow-backed strings natively. - # anndata 0.12.9+ requires pandas <3, so we stay on 0.12.6 + pandas 3 and - # must manually downcast ArrowStringArray columns to object dtype before writing. - for col in ultrack_indices.columns: - s = ultrack_indices[col] - if isinstance(s.dtype, pd.StringDtype): - ultrack_indices[col] = s.astype(object) - elif hasattr(s, "cat") and isinstance(s.cat.categories.dtype, pd.StringDtype): - ultrack_indices[col] = s.cat.rename_categories(s.cat.categories.astype(object)) if embedding_key == "projections": if projections is None: @@ -213,7 +203,7 @@ def write_embedding_dataset( adata.uns.update(uns_metadata) _logger.debug(f"Writing dataset to {output_path}") - adata.write_zarr(output_path) + write_anndata_zarr(adata, output_path) class EmbeddingWriter(BasePredictionWriter): diff --git a/packages/viscy-utils/src/viscy_utils/callbacks/linear_lr_cooldown.py b/packages/viscy-utils/src/viscy_utils/callbacks/linear_lr_cooldown.py new file mode 100644 index 000000000..92ba9641b --- /dev/null +++ b/packages/viscy-utils/src/viscy_utils/callbacks/linear_lr_cooldown.py @@ -0,0 +1,86 @@ +"""Linear learning-rate cooldown over a fixed range of optimizer steps.""" + +from lightning.pytorch import Callback, LightningModule, Trainer +from torch.optim.lr_scheduler import ConstantLR + + +class LinearLRCooldown(Callback): + """Ramp every param group's LR linearly from its initial value to a final fraction. + + Before each training batch, sets ``lr = initial_lr * factor`` with + ``factor`` equal to 1 before ``start_step``, decreasing linearly to + ``final_factor`` at ``end_step``, and ``final_factor`` afterwards. The + factor depends only on ``trainer.global_step``, so the callback holds no + state and a resumed fit continues the ramp where the checkpoint left it. + + Only valid on top of a constant schedule: the module must have exactly one + optimizer whose scheduler is ``ConstantLR`` with ``factor=1`` (the + ``"Constant"`` schedule of + :func:`viscy_utils.optimizers.configure_adamw_scheduler`), which never + changes the LR. Any other scheduler would overwrite or compound the + cooldown. List this callback before ``LearningRateMonitor`` so the logged + LR is the applied one. + + Parameters + ---------- + start_step : int + Global step at which the cooldown begins (factor 1 at this step). + end_step : int + Global step at which the factor reaches ``final_factor``. + final_factor : float, default 0.0 + LR multiplier at and after ``end_step``, in ``[0, 1]``. + """ + + def __init__(self, start_step: int, end_step: int, final_factor: float = 0.0) -> None: + if not 0 <= start_step < end_step: + raise ValueError(f"Need 0 <= start_step < end_step, got start_step={start_step}, end_step={end_step}") + if not 0.0 <= final_factor <= 1.0: + raise ValueError(f"final_factor must be in [0, 1], got {final_factor}") + self.start_step = start_step + self.end_step = end_step + self.final_factor = final_factor + + def factor(self, step: int) -> float: + """Return the LR multiplier for a global step. + + Parameters + ---------- + step : int + Global optimizer step. + + Returns + ------- + float + LR multiplier. + """ + if step <= self.start_step: + return 1.0 + if step >= self.end_step: + return self.final_factor + progress = (step - self.start_step) / (self.end_step - self.start_step) + return 1.0 - (1.0 - self.final_factor) * progress + + def on_fit_start(self, trainer: Trainer, pl_module: LightningModule) -> None: + """Check that the LR is otherwise constant, so the cooldown is the only LR change.""" + if len(trainer.optimizers) != 1: + raise ValueError(f"LinearLRCooldown needs exactly one optimizer, got {len(trainer.optimizers)}") + configs = trainer.lr_scheduler_configs + if len(configs) != 1: + raise ValueError(f"LinearLRCooldown needs exactly one ConstantLR scheduler, got {len(configs)} schedulers") + scheduler = configs[0].scheduler + if not isinstance(scheduler, ConstantLR) or scheduler.factor != 1: + raise ValueError( + "LinearLRCooldown needs the constant schedule (ConstantLR with factor=1, schedule='Constant'); " + f"got {type(scheduler).__name__}, which would overwrite or compound the cooldown" + ) + + def on_train_batch_start(self, trainer: Trainer, pl_module: LightningModule, batch, batch_idx: int) -> None: + """Set each param group's LR for the step about to run.""" + factor = self.factor(trainer.global_step) + for group in trainer.optimizers[0].param_groups: + if "initial_lr" not in group: + raise KeyError( + "LinearLRCooldown needs param_group['initial_lr'], which torch LR schedulers set; " + "the optimizer has no scheduler attached" + ) + group["lr"] = group["initial_lr"] * factor diff --git a/packages/viscy-utils/src/viscy_utils/callbacks/linear_lr_cooldown_test.py b/packages/viscy-utils/src/viscy_utils/callbacks/linear_lr_cooldown_test.py new file mode 100644 index 000000000..eb48565ec --- /dev/null +++ b/packages/viscy-utils/src/viscy_utils/callbacks/linear_lr_cooldown_test.py @@ -0,0 +1,112 @@ +"""Tests for the linear LR cooldown callback. + +Runs a real ``Trainer`` with the shared ``configure_adamw_scheduler`` so the +epoch-interval ``ConstantLR`` steps between epochs exactly as in a Constant- +schedule fit; the module records the LR its optimizer step actually uses. +""" + +import pytest +import torch +from lightning.pytorch import LightningModule, Trainer +from torch.utils.data import DataLoader, TensorDataset + +from viscy_utils.callbacks import LinearLRCooldown +from viscy_utils.optimizers import configure_adamw_scheduler + +LR = 0.1 +START, END, FINAL = 4, 12, 0.25 +MAX_STEPS = 16 + + +class _RecordingModule(LightningModule): + """Tiny regression module that records the LR applied at each global step.""" + + def __init__(self, schedule: str = "Constant") -> None: + super().__init__() + self.layer = torch.nn.Linear(2, 1) + self.schedule = schedule + self.applied_lr: dict[int, float] = {} + + def training_step(self, batch, batch_idx): + self.applied_lr[self.trainer.global_step] = self.trainer.optimizers[0].param_groups[0]["lr"] + x, y = batch + return torch.nn.functional.mse_loss(self.layer(x), y) + + def configure_optimizers(self): + return configure_adamw_scheduler(self, self.layer, LR, self.schedule, warmup_steps=2) + + +def _loader() -> DataLoader: + # 4 batches per epoch, so the ramp crosses several epoch-end scheduler steps. + return DataLoader(TensorDataset(torch.randn(4, 2), torch.randn(4, 1)), batch_size=1) + + +def _trainer(max_steps: int) -> Trainer: + return Trainer( + max_steps=max_steps, + accelerator="cpu", + devices=1, + logger=False, + enable_progress_bar=False, + enable_model_summary=False, + enable_checkpointing=False, + callbacks=[LinearLRCooldown(START, END, FINAL)], + ) + + +def _expected(step: int) -> float: + if step <= START: + return LR + if step >= END: + return LR * FINAL + return LR * (1 - (1 - FINAL) * (step - START) / (END - START)) + + +def test_cooldown_applies_the_linear_ramp(): + """Applied LR is initial_lr up to start, linear to its final fraction at end, constant after.""" + module = _RecordingModule() + _trainer(MAX_STEPS).fit(module, _loader()) + assert sorted(module.applied_lr) == list(range(MAX_STEPS)) + mid = (START + END) // 2 + assert module.applied_lr[START - 1] == pytest.approx(LR) + assert module.applied_lr[START] == pytest.approx(LR) + assert module.applied_lr[mid] == pytest.approx(LR * (1 + FINAL) / 2) + assert module.applied_lr[END] == pytest.approx(LR * FINAL) + assert module.applied_lr[MAX_STEPS - 1] == pytest.approx(LR * FINAL) + for step, lr in module.applied_lr.items(): + assert lr == pytest.approx(_expected(step)), step + + +def test_cooldown_continues_after_resume_from_mid_ramp(tmp_path): + """A fit resumed mid-cooldown continues the ramp from the restored global step.""" + first = _RecordingModule() + resume_step = 7 + trainer = _trainer(resume_step) + trainer.fit(first, _loader()) + ckpt = tmp_path / "mid.ckpt" + trainer.save_checkpoint(ckpt) + + resumed = _RecordingModule() + _trainer(MAX_STEPS).fit(resumed, _loader(), ckpt_path=ckpt) + # The resumed fit starts at the checkpoint's global step, not at 0, and + # the ramp picks up from there rather than restarting. + assert sorted(resumed.applied_lr) == list(range(resume_step, MAX_STEPS)) + for step, lr in resumed.applied_lr.items(): + assert lr == pytest.approx(_expected(step)), step + assert resumed.applied_lr[resume_step] < LR + + +def test_cooldown_rejects_a_stepwise_scheduler(): + """A step-wise scheduler would overwrite the cooldown, so fit refuses to start.""" + with pytest.raises(ValueError, match="ConstantLR"): + _trainer(MAX_STEPS).fit(_RecordingModule(schedule="WarmupCosine"), _loader()) + + +@pytest.mark.parametrize( + ("start", "end", "final"), + [(5, 5, 0.0), (6, 5, 0.0), (-1, 5, 0.0), (0, 5, -0.1), (0, 5, 1.5)], +) +def test_cooldown_rejects_invalid_arguments(start, end, final): + """Out-of-range steps or final_factor raise at construction.""" + with pytest.raises(ValueError): + LinearLRCooldown(start, end, final) diff --git a/packages/viscy-utils/src/viscy_utils/callbacks/monitor_health.py b/packages/viscy-utils/src/viscy_utils/callbacks/monitor_health.py new file mode 100644 index 000000000..c4b9479cf --- /dev/null +++ b/packages/viscy-utils/src/viscy_utils/callbacks/monitor_health.py @@ -0,0 +1,97 @@ +"""Fail loud when a checkpoint monitor stops carrying signal. + +``ModelCheckpoint(monitor=..., save_top_k=k)`` only writes a checkpoint while +``save_top_k`` is unfilled or when the monitored metric improves. A monitor that +goes non-finite or freezes therefore *silently* starves checkpointing: the first +``k`` epochs save, and nothing after that — including ``last.ckpt``, which +Lightning writes on the same cadence. A 200-epoch run can complete with +``Trainer.fit stopped: max_epochs=200 reached``, exit code 0, and no weights +newer than epoch ``k``. + +This callback converts that silent waste into an early crash, in keeping with +the repo's prefer-raising philosophy. It observes only ``trainer.callback_metrics`` +and never touches checkpoint selection, so adding it cannot change which +checkpoint a run considers "best". +""" + +import math +from collections import deque + +import torch +from lightning.pytorch import Callback, LightningModule, Trainer + +__all__ = ["MonitorHealthCheck", "MonitorHealthError"] + + +class MonitorHealthError(RuntimeError): + """Raised when a checkpoint monitor is non-finite or frozen.""" + + +class MonitorHealthCheck(Callback): + """Raise when the checkpoint monitor goes non-finite or stops varying. + + Parameters + ---------- + monitor : str + Metric key to watch, matching the ``monitor`` of the run's monitored + ``ModelCheckpoint`` (e.g. ``loss/validate``). + patience : int + Number of consecutive validation epochs that must share one identical + value before the run is declared frozen. Must be >= 2. + + Raises + ------ + MonitorHealthError + When the monitored metric is non-finite, or has been bit-identical for + ``patience`` consecutive validation epochs. + + Notes + ----- + Sanity-check validation is skipped: it runs before training and would + contribute a value from untrained weights. + + A missing monitor key is *not* an error here. Lightning's own + ``ModelCheckpoint`` already raises for that case, and the metric can be + legitimately absent on the first epochs of some schedules. + """ + + def __init__(self, monitor: str = "loss/validate", patience: int = 10) -> None: + if patience < 2: + raise ValueError(f"patience must be >= 2 to detect a frozen metric, got {patience}") + self.monitor = monitor + self.patience = patience + self._recent: deque[float] = deque(maxlen=patience) + + def on_validation_end(self, trainer: Trainer, pl_module: LightningModule) -> None: + """Inspect metrics after the module's ``on_validation_epoch_end`` logs them.""" + if trainer.sanity_checking: + return + value = trainer.callback_metrics.get(self.monitor) + if value is None: + return + scalar = value.item() if isinstance(value, torch.Tensor) else float(value) + + if not math.isfinite(scalar): + raise MonitorHealthError( + f"checkpoint monitor {self.monitor!r} is non-finite ({scalar}) at epoch {trainer.current_epoch}. " + "ModelCheckpoint cannot rank a non-finite metric, so no further checkpoint will be written " + "and this run would finish with no usable weights." + ) + + self._recent.append(scalar) + if len(self._recent) == self.patience and len(set(self._recent)) == 1: + raise MonitorHealthError( + f"checkpoint monitor {self.monitor!r} has been identical ({scalar}) for {self.patience} " + f"consecutive validation epochs, through epoch {trainer.current_epoch}. A metric that does not " + "respond to the weights starves ModelCheckpoint: save_top_k fills once and nothing is saved " + "afterwards, so the run would end with no weights newer than the first few epochs. " + "Check that the validation loss actually depends on the model output." + ) + + def state_dict(self) -> dict: + """Persist the recent-value window so a resumed run keeps its history.""" + return {"recent": list(self._recent)} + + def load_state_dict(self, state_dict: dict) -> None: + """Restore the recent-value window from a checkpoint.""" + self._recent = deque(state_dict.get("recent", []), maxlen=self.patience) diff --git a/packages/viscy-utils/src/viscy_utils/callbacks/monitor_health_test.py b/packages/viscy-utils/src/viscy_utils/callbacks/monitor_health_test.py new file mode 100644 index 000000000..31ed3df4b --- /dev/null +++ b/packages/viscy-utils/src/viscy_utils/callbacks/monitor_health_test.py @@ -0,0 +1,327 @@ +"""Tests for the checkpoint-monitor guardrails. + +Runs a real ``Trainer`` so the interaction with Lightning's ``ModelCheckpoint`` +is exercised, not mocked — the failure this guards against (top-k fills, then +nothing is saved) only appears through the real callback. +""" + +import pytest +import torch +from jsonargparse import Namespace +from lightning.pytorch import LightningModule, Trainer +from lightning.pytorch.callbacks import ModelCheckpoint +from torch.utils.data import DataLoader, TensorDataset + +from viscy_utils.callbacks import MonitorHealthCheck, MonitorHealthError +from viscy_utils.cli import _inject_checkpoint_guardrails + + +class _ValLossModule(LightningModule): + """Minimal module that logs a scripted ``loss/validate`` sequence.""" + + def __init__(self, val_values: list[float]): + super().__init__() + self.val_values = val_values + self.layer = torch.nn.Linear(2, 1) + + def training_step(self, batch, batch_idx): + x, y = batch + return torch.nn.functional.mse_loss(self.layer(x), y) + + def on_validation_epoch_end(self): + # Index defensively: sanity-check validation also fires this hook. + idx = min(self.current_epoch, len(self.val_values) - 1) + self.log("loss/validate", torch.tensor(self.val_values[idx])) + + def validation_step(self, batch, batch_idx): + return None + + def configure_optimizers(self): + return torch.optim.SGD(self.parameters(), lr=0.01) + + +def _loaders() -> tuple[DataLoader, DataLoader]: + ds = TensorDataset(torch.randn(8, 2), torch.randn(8, 1)) + return DataLoader(ds, batch_size=4), DataLoader(ds, batch_size=4) + + +def _run(val_values: list[float], tmp_path, patience: int = 3, max_epochs: int = 8) -> Trainer: + train_dl, val_dl = _loaders() + trainer = Trainer( + max_epochs=max_epochs, + accelerator="cpu", + logger=False, + enable_progress_bar=False, + enable_model_summary=False, + num_sanity_val_steps=0, + callbacks=[MonitorHealthCheck(monitor="loss/validate", patience=patience)], + default_root_dir=tmp_path, + ) + trainer.fit(_ValLossModule(val_values), train_dl, val_dl) + return trainer + + +def test_frozen_monitor_raises(tmp_path): + """A monitor that never changes must raise rather than train silently.""" + with pytest.raises(MonitorHealthError, match="identical"): + _run([1.024275] * 3, tmp_path, patience=3, max_epochs=3) + + +def test_non_finite_monitor_raises(tmp_path): + """A NaN monitor must raise immediately.""" + with pytest.raises(MonitorHealthError, match="non-finite"): + _run([0.9, float("nan")], tmp_path, patience=3, max_epochs=2) + + +def test_improving_monitor_does_not_raise(tmp_path): + """A normally-varying monitor must train to completion untouched.""" + trainer = _run([0.9, 0.8, 0.7, 0.65, 0.6, 0.55, 0.5, 0.45], tmp_path, patience=3) + assert trainer.current_epoch == 8 + health = next(callback for callback in trainer.callbacks if isinstance(callback, MonitorHealthCheck)) + assert health.state_dict()["recent"] == pytest.approx([0.55, 0.5, 0.45]) + + +def test_brief_plateau_below_patience_does_not_raise(tmp_path): + """A plateau shorter than ``patience`` is normal training, not a dead metric.""" + trainer = _run([0.9, 0.8, 0.8, 0.7, 0.7, 0.6, 0.55, 0.5], tmp_path, patience=3) + assert trainer.current_epoch == 8 + + +def test_frozen_monitor_starves_real_model_checkpoint(tmp_path): + """Reproduce the underlying failure: a frozen monitor stops all saving. + + Without the guardrail a constant monitor fills ``save_top_k`` and then writes + nothing more — including ``last.ckpt``. This is the regression the injected + unmonitored checkpoint exists to prevent. + """ + train_dl, val_dl = _loaders() + ckpt_dir = tmp_path / "checkpoints" + monitored = ModelCheckpoint( + dirpath=ckpt_dir, + monitor="loss/validate", + filename="epoch={epoch}-step={step}", + auto_insert_metric_name=False, + every_n_epochs=1, + save_top_k=2, + save_last=True, + ) + trainer = Trainer( + max_epochs=8, + accelerator="cpu", + logger=False, + enable_progress_bar=False, + enable_model_summary=False, + num_sanity_val_steps=0, + callbacks=[monitored], + default_root_dir=tmp_path, + ) + trainer.fit(_ValLossModule([1.024275] * 8), train_dl, val_dl) + + saved = sorted(p.name for p in ckpt_dir.glob("epoch=*.ckpt")) + # save_top_k=2 fills at epochs 0 and 1; the constant metric never improves after. + assert len(saved) == 2, saved + assert saved == ["epoch=0-step=2.ckpt", "epoch=1-step=4.ckpt"], saved + # 8 epochs ran, yet nothing newer than epoch 1 exists. + assert trainer.current_epoch == 8 + + +def test_injection_adds_guardrails_and_preserves_selection(): + """Injection must add the safety net without touching selection semantics.""" + monitored = Namespace( + class_path="lightning.pytorch.callbacks.ModelCheckpoint", + init_args=Namespace( + monitor="loss/validate", + mode="min", + filename="epoch={epoch}-step={step}-loss={loss/validate:.3f}", + save_top_k=5, + every_n_epochs=1, + save_last=True, + dirpath="/models/run/checkpoints", + ), + ) + config = Namespace(fit=Namespace(trainer=Namespace(callbacks=[monitored]))) + _inject_checkpoint_guardrails(config, "fit") + callbacks = config["fit"]["trainer"]["callbacks"] + + assert len(callbacks) == 3 + # Selection semantics of the monitored callback are untouched... + assert monitored["init_args"]["monitor"] == "loss/validate" + assert monitored["init_args"]["mode"] == "min" + assert monitored["init_args"]["save_top_k"] == 5 + assert monitored["init_args"]["filename"] == "epoch={epoch}-step={step}-loss={loss/validate:.3f}" + # ...except that it no longer owns `last.ckpt`. + assert monitored["init_args"]["save_last"] is False + + latest = callbacks[1] + assert latest["class_path"] == "lightning.pytorch.callbacks.ModelCheckpoint" + assert latest["init_args"]["monitor"] is None + assert latest["init_args"]["save_last"] is True + assert latest["init_args"]["save_top_k"] == 1 + assert latest["init_args"]["dirpath"] == "/models/run/checkpoints" + + health = callbacks[2] + assert health["class_path"] == "viscy_utils.callbacks.MonitorHealthCheck" + assert health["init_args"]["monitor"] == "loss/validate" + + +def test_injection_is_idempotent(): + """Re-running injection must not stack duplicate callbacks.""" + monitored = Namespace( + class_path="lightning.pytorch.callbacks.ModelCheckpoint", + init_args=Namespace(monitor="loss/validate", save_top_k=5, save_last=True, dirpath="/d"), + ) + config = Namespace(fit=Namespace(trainer=Namespace(callbacks=[monitored]))) + _inject_checkpoint_guardrails(config, "fit") + _inject_checkpoint_guardrails(config, "fit") + assert len(config["fit"]["trainer"]["callbacks"]) == 3 + + +def test_injection_skips_non_fit_subcommands(): + """predict/test/validate write no checkpoints, so nothing is injected.""" + monitored = Namespace( + class_path="lightning.pytorch.callbacks.ModelCheckpoint", + init_args=Namespace(monitor="loss/validate", save_last=True, dirpath="/d"), + ) + config = Namespace(predict=Namespace(trainer=Namespace(callbacks=[monitored]))) + _inject_checkpoint_guardrails(config, "predict") + assert len(config["predict"]["trainer"]["callbacks"]) == 1 + assert monitored["init_args"]["save_last"] is True + + +def test_injection_noop_without_monitored_checkpoint(): + """A config with no monitored checkpoint gets no health check to attach to.""" + unmonitored = Namespace( + class_path="lightning.pytorch.callbacks.ModelCheckpoint", + init_args=Namespace(monitor=None, save_last=True, dirpath="/d"), + ) + config = Namespace(fit=Namespace(trainer=Namespace(callbacks=[unmonitored]))) + _inject_checkpoint_guardrails(config, "fit") + assert len(config["fit"]["trainer"]["callbacks"]) == 1 + + +def test_patience_below_two_rejected(): + """patience<2 cannot distinguish a frozen metric from a single sample.""" + with pytest.raises(ValueError, match="patience must be >= 2"): + MonitorHealthCheck(patience=1) + + +def test_injection_inherits_the_leaf_write_cadence(): + """The latest-weights callback must not force ``every_n_epochs: 1``. + + A leaf that sets ``every_n_epochs`` is making a deliberate write-amplification + decision. The Phase 17 arms run ~1470 epochs with a 424 MB checkpoint and set + cadence 10 for exactly that reason; forcing 1 here rewrote both ``latest-*`` + and ``last.ckpt`` every epoch (~1.25 TB of NFS traffic per arm) and silently + overrode the leaf. + """ + monitored = Namespace( + class_path="lightning.pytorch.callbacks.ModelCheckpoint", + init_args=Namespace( + monitor="loss/validate", + save_top_k=4, + every_n_epochs=10, + save_last=True, + dirpath="/models/run/checkpoints", + ), + ) + config = Namespace(fit=Namespace(trainer=Namespace(callbacks=[monitored]))) + _inject_checkpoint_guardrails(config, "fit") + + latest = config["fit"]["trainer"]["callbacks"][1] + assert latest["init_args"]["every_n_epochs"] == 10 + # The monitored callback's own cadence is still its own. + assert monitored["init_args"]["every_n_epochs"] == 10 + + +def test_injection_defaults_cadence_to_every_epoch(): + """A leaf that sets no cadence keeps the original every-epoch behaviour.""" + monitored = Namespace( + class_path="lightning.pytorch.callbacks.ModelCheckpoint", + init_args=Namespace(monitor="loss/validate", save_top_k=4, save_last=True, dirpath="/d"), + ) + config = Namespace(fit=Namespace(trainer=Namespace(callbacks=[monitored]))) + _inject_checkpoint_guardrails(config, "fit") + + assert config["fit"]["trainer"]["callbacks"][1]["init_args"]["every_n_epochs"] == 1 + + +def test_injection_takes_last_ckpt_from_every_monitored_checkpoint(): + """With two monitored checkpoints, only the injected latest-weights callback writes ``last.ckpt``.""" + monitored = [ + Namespace( + class_path="lightning.pytorch.callbacks.ModelCheckpoint", + init_args=Namespace(monitor=monitor, save_last=True, dirpath="/d"), + ) + for monitor in ("loss/validate", "metrics/dice") + ] + config = Namespace(fit=Namespace(trainer=Namespace(callbacks=list(monitored)))) + _inject_checkpoint_guardrails(config, "fit") + + callbacks = config["fit"]["trainer"]["callbacks"] + writers = [cb["init_args"].get("monitor") for cb in callbacks if cb["init_args"].get("save_last")] + assert writers == [None] + + +def test_injection_inherits_a_step_cadence(): + """A leaf that saves every N steps keeps a step-fresh ``last.ckpt``, not an epoch one.""" + monitored = Namespace( + class_path="lightning.pytorch.callbacks.ModelCheckpoint", + init_args=Namespace(monitor="loss/validate", every_n_train_steps=1000, save_last=True, dirpath="/d"), + ) + config = Namespace(fit=Namespace(trainer=Namespace(callbacks=[monitored]))) + _inject_checkpoint_guardrails(config, "fit") + + latest = config["fit"]["trainer"]["callbacks"][1]["init_args"] + assert latest["every_n_train_steps"] == 1000 + assert "every_n_epochs" not in latest + + +def test_inherited_cadence_still_saves_when_monitor_is_frozen(tmp_path): + """End-to-end: cadence 4 writes latest weights without writing every epoch. + + Runs the real Trainer with a frozen monitor, which starves the monitored + callback after ``save_top_k`` fills. The unmonitored callback must still + produce weights from the run's final epochs -- the guardrail's whole purpose -- + while writing 2 times over 8 epochs rather than 8. + """ + train_dl, val_dl = _loaders() + ckpt_dir = tmp_path / "checkpoints" + monitored = Namespace( + class_path="lightning.pytorch.callbacks.ModelCheckpoint", + init_args=Namespace( + monitor="loss/validate", + filename="epoch={epoch}-step={step}", + auto_insert_metric_name=False, + save_top_k=2, + every_n_epochs=4, + save_last=True, + dirpath=str(ckpt_dir), + ), + ) + config = Namespace(fit=Namespace(trainer=Namespace(callbacks=[monitored]))) + _inject_checkpoint_guardrails(config, "fit") + specs = config["fit"]["trainer"]["callbacks"] + + latest_cb = ModelCheckpoint(**{k: v for k, v in specs[1]["init_args"].items()}) + monitored_cb = ModelCheckpoint(**{k: v for k, v in specs[0]["init_args"].items()}) + trainer = Trainer( + max_epochs=8, + accelerator="cpu", + logger=False, + enable_progress_bar=False, + enable_model_summary=False, + num_sanity_val_steps=0, + callbacks=[monitored_cb, latest_cb], + default_root_dir=tmp_path, + ) + trainer.fit(_ValLossModule([1.024275] * 8), train_dl, val_dl) + + # Lightning fires every_n_epochs when (epoch + 1) % n == 0, so this writes at + # epochs 3 and 7 only -- 2 of 8 epochs -- and save_top_k=1 keeps just the last. + latest = sorted(p.name for p in ckpt_dir.glob("latest-*.ckpt")) + assert latest == ["latest-epoch=7-step=16.ckpt"], latest + assert (ckpt_dir / "last.ckpt").is_file() + # The point of the guardrail: weights exist from the END of the run (epoch 7 is + # the last of 8, zero-indexed), not just from where the frozen monitor stopped + # improving -- the monitored callback's top-k filled at epochs 3 and never moved. + assert torch.load(ckpt_dir / "last.ckpt", map_location="cpu", weights_only=False)["epoch"] == 7 diff --git a/packages/viscy-utils/src/viscy_utils/callbacks/optimizer_health.py b/packages/viscy-utils/src/viscy_utils/callbacks/optimizer_health.py new file mode 100644 index 000000000..a0b7a8dda --- /dev/null +++ b/packages/viscy-utils/src/viscy_utils/callbacks/optimizer_health.py @@ -0,0 +1,163 @@ +"""Fail loud when the optimizer stops making progress at the step level. + +Two failure modes hide from every loss-based monitor, and one of them cost 24 +GPU-days of FCMAE-2D fits that ran to ``epoch 199/200`` with the weights still at +their initialization: + +**Under ``16-mixed``, a NaN *gradient* is silent.** ``GradScaler`` inspects the +gradients, skips the optimizer step when any is non-finite, and halves its scale. +The forward pass — and therefore the logged loss — stays perfectly finite. From +``2**16`` it takes ~166 halvings to reach exactly ``0.0``, after which *every* +step is skipped forever: the weights never move again, and the run still exits 0. +The trigger there was a ``sqrt`` at 0 (infinite derivative) inside an SSIM +Cauchy-Schwarz bound, but any NaN-producing backward does the same thing. + +**Under ``bf16-mixed`` there is no scaler at all**, so a NaN gradient instead +lands in the weights and the loss goes non-finite on the next forward. That is +loud in a log, but nothing *stops* the run, so it burns the rest of its wall +allocation. + +This callback covers both: it watches the scaler's scale where there is one, and +the training loss everywhere. It only reads state — it never rescales, skips, or +touches optimization — so enabling it cannot change a healthy run's numerics. + +Related but distinct: :class:`~viscy_utils.callbacks.MonitorHealthCheck` watches +the *checkpoint monitor* once per validation epoch, so it catches the same freeze +only after ``patience`` epochs and cannot say why. This one trips within tens of +steps and names the mechanism. +""" + +from collections.abc import Mapping + +import torch +from lightning.pytorch import Callback, LightningModule, Trainer + +__all__ = ["OptimizerHealthCheck", "OptimizerHealthError"] + + +class OptimizerHealthError(RuntimeError): + """Raised when the optimizer can no longer be making progress.""" + + +class OptimizerHealthCheck(Callback): + """Raise when the AMP scale collapses or the training loss goes non-finite. + + Parameters + ---------- + min_scale : float + Raise once ``GradScaler``'s scale falls below this value. fp16 AMP + normally operates between ``2**10`` and ``2**16``; below ``1.0`` the + scaler is shrinking gradients rather than protecting them, which no + healthy run needs. Ignored when the run has no scaler. + max_consecutive_skips : int + Raise after this many scale *reductions* with no growth in between. Each + reduction is one skipped optimizer step; successful steps may separate + them, but the scale only grows after ``growth_interval`` of those in a + row, so a run of reductions means the backward keeps overflowing faster + than the scale can recover. A handful during AMP warmup is normal and + expected; a couple of dozen is not. + max_consecutive_nonfinite : int + Raise after this many consecutive training batches whose loss is NaN or + infinite. Small but non-zero so a single transient overflow does not kill + a run. + + Raises + ------ + OptimizerHealthError + When the scale falls below ``min_scale``, when ``max_consecutive_skips`` + steps are skipped in a row, or when the loss is non-finite for + ``max_consecutive_nonfinite`` batches in a row. + + Notes + ----- + Skips are counted only when the scale actually *changes*, so gradient + accumulation — which fires ``on_train_batch_end`` on batches where no + optimizer step happened, leaving the scale flat — cannot reset the counter + and mask a genuine run of skipped steps. + + The loss arm reads the value ``training_step`` returned rather than a logged + metric, so it also covers modules that do not log a training loss. Manual- + optimization modules that return ``None`` (e.g. the GAN engine) are skipped + by the loss arm; their scaler arm still applies. + """ + + def __init__( + self, + min_scale: float = 1.0, + max_consecutive_skips: int = 25, + max_consecutive_nonfinite: int = 5, + ) -> None: + if max_consecutive_skips < 1: + raise ValueError(f"max_consecutive_skips must be >= 1, got {max_consecutive_skips}") + if max_consecutive_nonfinite < 1: + raise ValueError(f"max_consecutive_nonfinite must be >= 1, got {max_consecutive_nonfinite}") + self.min_scale = min_scale + self.max_consecutive_skips = max_consecutive_skips + self.max_consecutive_nonfinite = max_consecutive_nonfinite + self._prev_scale: float | None = None + self._consecutive_skips = 0 + self._consecutive_nonfinite = 0 + + def on_train_batch_end(self, trainer: Trainer, pl_module: LightningModule, outputs, batch, batch_idx: int) -> None: + """Check the batch loss, then the AMP scale, after every training batch.""" + self._check_loss(outputs, trainer) + self._check_scale(trainer) + + def _check_loss(self, outputs, trainer: Trainer) -> None: + """Count consecutive non-finite training losses and raise past the limit.""" + loss = outputs.get("loss") if isinstance(outputs, Mapping) else outputs + if not isinstance(loss, torch.Tensor): + return + if torch.isfinite(loss).all(): + self._consecutive_nonfinite = 0 + return + self._consecutive_nonfinite += 1 + if self._consecutive_nonfinite >= self.max_consecutive_nonfinite: + raise OptimizerHealthError( + f"training loss has been non-finite for {self._consecutive_nonfinite} consecutive batches " + f"(step {trainer.global_step}, last value {loss.detach().float().mean().item()}). Without a " + "GradScaler to skip the step -- bf16-mixed and 32-true have none -- a NaN gradient lands in " + "the weights and every later forward is non-finite, so this run cannot recover." + ) + + def _check_scale(self, trainer: Trainer) -> None: + """Track the ``GradScaler`` scale and raise on collapse or sustained skips.""" + scaler = getattr(trainer.precision_plugin, "scaler", None) + if scaler is None: + return + scale = float(scaler.get_scale()) + prev = self._prev_scale + self._prev_scale = scale + if prev is not None and scale != prev: + # A reduction means GradScaler found a non-finite gradient and skipped + # the step; growth means a run of successful steps cleared the counter. + self._consecutive_skips = self._consecutive_skips + 1 if scale < prev else 0 + + if scale < self.min_scale: + raise OptimizerHealthError( + f"GradScaler scale collapsed to {scale} (floor {self.min_scale}) at step {trainer.global_step}. " + "The scale only halves when a gradient is non-finite, so the backward pass is producing NaN " + "or inf while the forward stays finite. At scale 0 every optimizer step is skipped forever " + "and the weights stay at their last value for the rest of the run." + ) + if self._consecutive_skips >= self.max_consecutive_skips: + raise OptimizerHealthError( + f"GradScaler halved its scale {self._consecutive_skips} times without growing in between " + f"(now {scale}, step {trainer.global_step}). Each halving skipped an optimizer step for a " + "non-finite gradient, and the scale never recovered, so the backward keeps overflowing even " + "though the loss is finite." + ) + + def state_dict(self) -> dict: + """Persist the counters so a resumed run does not restart mid-collapse.""" + return { + "prev_scale": self._prev_scale, + "consecutive_skips": self._consecutive_skips, + "consecutive_nonfinite": self._consecutive_nonfinite, + } + + def load_state_dict(self, state_dict: dict) -> None: + """Restore the counters from a checkpoint.""" + self._prev_scale = state_dict.get("prev_scale") + self._consecutive_skips = state_dict.get("consecutive_skips", 0) + self._consecutive_nonfinite = state_dict.get("consecutive_nonfinite", 0) diff --git a/packages/viscy-utils/src/viscy_utils/callbacks/optimizer_health_test.py b/packages/viscy-utils/src/viscy_utils/callbacks/optimizer_health_test.py new file mode 100644 index 000000000..e2524e4f6 --- /dev/null +++ b/packages/viscy-utils/src/viscy_utils/callbacks/optimizer_health_test.py @@ -0,0 +1,240 @@ +"""Tests for the step-level optimizer guardrail. + +Runs a real ``Trainer`` throughout: the failure being guarded against is produced +by the interaction of a NaN backward with Lightning's precision plugin, so a +mocked scaler would prove nothing. The NaN-gradient module reproduces the actual +mechanism that froze the 24 FCMAE-2D fits — a ``sqrt`` evaluated at exactly 0, +whose derivative is infinite, multiplied by a zero incoming gradient — rather +than asserting on a hand-set scale. +""" + +import pytest +import torch +from jsonargparse import Namespace +from lightning.pytorch import LightningModule, Trainer +from torch.utils.data import DataLoader, TensorDataset + +from viscy_utils.callbacks import OptimizerHealthCheck, OptimizerHealthError +from viscy_utils.cli import _inject_optimizer_health_guard + +_needs_cuda = pytest.mark.skipif( + not torch.cuda.is_available(), + reason="fp16 AMP has no GradScaler on CPU -- Lightning silently downgrades 16-mixed to bf16-mixed", +) + + +class _HealthyModule(LightningModule): + """Ordinary regression module with finite gradients everywhere.""" + + def __init__(self) -> None: + super().__init__() + self.layer = torch.nn.Linear(2, 1) + + def training_step(self, batch, batch_idx): + x, y = batch + return torch.nn.functional.mse_loss(self.layer(x), y) + + def configure_optimizers(self): + return torch.optim.SGD(self.parameters(), lr=0.01) + + +class _NaNGradientModule(_HealthyModule): + """Finite forward, NaN backward — the FCMAE-2D freeze, minimally reproduced. + + ``sqrt`` at 0 has an infinite derivative, and the incoming gradient of the + zeroed product is 0, so autograd evaluates ``0 * inf = nan`` for the whole + term while its forward contribution is exactly 0. The loss therefore stays + finite and only the gradient is poisoned. + """ + + def training_step(self, batch, batch_idx): + x, y = batch + pred = self.layer(x) + bound = torch.sqrt(pred * torch.zeros_like(pred)) + loss = torch.nn.functional.mse_loss(pred, y) + bound.mean() + assert torch.isfinite(loss).all(), "forward must stay finite for this test to mean anything" + return loss + + +class _NaNLossModule(_HealthyModule): + """Loss itself goes non-finite, as it does under bf16 with no scaler.""" + + def training_step(self, batch, batch_idx): + x, y = batch + return torch.nn.functional.mse_loss(self.layer(x), y) * float("nan") + + +def _loader() -> DataLoader: + return DataLoader(TensorDataset(torch.randn(32, 2), torch.randn(32, 1)), batch_size=4) + + +def _run(module: LightningModule, tmp_path, precision: str, accelerator: str, **guard_kwargs) -> Trainer: + trainer = Trainer( + max_epochs=4, + accelerator=accelerator, + devices=1, + precision=precision, + logger=False, + enable_progress_bar=False, + enable_model_summary=False, + enable_checkpointing=False, + num_sanity_val_steps=0, + callbacks=[OptimizerHealthCheck(**guard_kwargs)], + default_root_dir=tmp_path, + ) + trainer.fit(module, _loader()) + return trainer + + +@_needs_cuda +def test_nan_gradient_under_fp16_raises_while_the_loss_stays_finite(tmp_path): + """The silent case: finite loss, skipped steps, scale walking to zero.""" + with pytest.raises(OptimizerHealthError, match="halved its scale|scale collapsed"): + _run( + _NaNGradientModule(), + tmp_path, + precision="16-mixed", + accelerator="gpu", + max_consecutive_skips=3, + ) + + +@_needs_cuda +def test_healthy_fp16_run_completes_untouched(tmp_path): + """A normal fp16 run must not trip the guard, and must actually move weights.""" + module = _HealthyModule() + before = module.layer.weight.detach().clone() + trainer = _run(module, tmp_path, precision="16-mixed", accelerator="gpu", max_consecutive_skips=3) + assert trainer.current_epoch == 4 + assert not torch.allclose(before, module.layer.weight.detach().cpu()) + + +def test_non_finite_loss_raises_without_a_scaler(tmp_path): + """bf16 has no GradScaler, so the loss arm is the only thing that can catch it.""" + with pytest.raises(OptimizerHealthError, match="non-finite for"): + _run( + _NaNLossModule(), + tmp_path, + precision="bf16-mixed", + accelerator="cpu", + max_consecutive_nonfinite=2, + ) + + +def test_healthy_bf16_run_completes_untouched(tmp_path): + """The precision the 2D FCMAE fits actually use must train to completion.""" + module = _HealthyModule() + before = module.layer.weight.detach().clone() + trainer = _run(module, tmp_path, precision="bf16-mixed", accelerator="cpu", max_consecutive_nonfinite=2) + assert trainer.current_epoch == 4 + assert not torch.allclose(before, module.layer.weight.detach()) + + +def test_transient_non_finite_loss_is_tolerated(tmp_path): + """One overflow must not kill a run — only a sustained streak may. + + The NaN is added as a *constant*, so its gradient contribution is zero and the + weights stay healthy. Multiplying the loss by NaN instead would send NaN into + the weights and every later batch would be non-finite too — which is precisely + why bf16 needs this guard, and which made an earlier version of this test fail + for the right reason. + """ + + class _OneNaNModule(_HealthyModule): + def training_step(self, batch, batch_idx): + x, y = batch + loss = torch.nn.functional.mse_loss(self.layer(x), y) + if self.global_step == 1: + return loss + torch.tensor(float("nan")) + return loss + + module = _OneNaNModule() + trainer = _run(module, tmp_path, precision="bf16-mixed", accelerator="cpu", max_consecutive_nonfinite=3) + assert trainer.current_epoch == 4 + assert torch.isfinite(module.layer.weight).all() + + +def test_accumulation_flat_scale_does_not_reset_the_skip_counter(): + """Batches with no optimizer step leave the scale flat; that must not clear skips. + + Under ``accumulate_grad_batches>1`` the scale only changes on real steps, so a + counter that reset on every unchanged sample would never reach its limit and + the guard would be dead exactly where the bug lives. + """ + guard = OptimizerHealthCheck(max_consecutive_skips=3, min_scale=0.0) + + class _FakeScaler: + def __init__(self) -> None: + self.scale = 65536.0 + + def get_scale(self) -> float: + return self.scale + + scaler = _FakeScaler() + trainer = Namespace(precision_plugin=Namespace(scaler=scaler), global_step=0) + + guard._check_scale(trainer) # first observation, nothing to compare against + for _ in range(2): + scaler.scale /= 2 # a skipped step + guard._check_scale(trainer) + guard._check_scale(trainer) # accumulation batch: scale unchanged + assert guard._consecutive_skips == 2 + + scaler.scale /= 2 + with pytest.raises(OptimizerHealthError, match="halved its scale 3 times"): + guard._check_scale(trainer) + + +def test_scale_growth_clears_the_skip_counter(): + """A successful growth interval means the run recovered; forget the earlier skips.""" + guard = OptimizerHealthCheck(max_consecutive_skips=3, min_scale=0.0) + + class _FakeScaler: + def __init__(self) -> None: + self.scale = 1024.0 + + def get_scale(self) -> float: + return self.scale + + scaler = _FakeScaler() + trainer = Namespace(precision_plugin=Namespace(scaler=scaler), global_step=0) + guard._check_scale(trainer) + scaler.scale /= 2 + guard._check_scale(trainer) + assert guard._consecutive_skips == 1 + scaler.scale *= 2 + guard._check_scale(trainer) + assert guard._consecutive_skips == 0 + + +def test_guard_is_injected_into_a_fit_config(): + """``fit`` configs get the guard even though leaves override ``trainer.callbacks``.""" + config = Namespace(fit=Namespace(trainer=Namespace(callbacks=[]))) + _inject_optimizer_health_guard(config, "fit") + paths = [callback.class_path for callback in config.fit.trainer.callbacks] + assert paths == ["viscy_utils.callbacks.OptimizerHealthCheck"] + + +def test_guard_is_injected_into_a_fit_without_callbacks(): + """A fit that leaves ``trainer.callbacks`` unset still gets the guard.""" + config = Namespace(fit=Namespace(trainer=Namespace(callbacks=None))) + _inject_optimizer_health_guard(config, "fit") + paths = [callback.class_path for callback in config.fit.trainer.callbacks] + assert paths == ["viscy_utils.callbacks.OptimizerHealthCheck"] + + +def test_guard_injection_is_idempotent(): + """Re-running the injector (e.g. a resumed resolved config) must not duplicate it.""" + config = Namespace(fit=Namespace(trainer=Namespace(callbacks=[]))) + _inject_optimizer_health_guard(config, "fit") + _inject_optimizer_health_guard(config, "fit") + assert len(config.fit.trainer.callbacks) == 1 + + +@pytest.mark.parametrize("subcommand", ["predict", "validate", "test", None]) +def test_guard_is_not_injected_outside_fit(subcommand): + """Only training can stall this way; predict/validate configs stay untouched.""" + config = Namespace(**{str(subcommand): Namespace(trainer=Namespace(callbacks=[]))}) + _inject_optimizer_health_guard(config, subcommand) + root = config.get(str(subcommand)) + assert root.trainer.callbacks == [] diff --git a/packages/viscy-utils/src/viscy_utils/callbacks/prediction_writer.py b/packages/viscy-utils/src/viscy_utils/callbacks/prediction_writer.py index f7f778b91..6716851b5 100644 --- a/packages/viscy-utils/src/viscy_utils/callbacks/prediction_writer.py +++ b/packages/viscy-utils/src/viscy_utils/callbacks/prediction_writer.py @@ -6,8 +6,10 @@ from __future__ import annotations +import functools import logging import os +from concurrent.futures import Future, ThreadPoolExecutor from typing import TYPE_CHECKING, Literal, Optional, Sequence import numpy as np @@ -17,6 +19,21 @@ from lightning.pytorch.callbacks import BasePredictionWriter from numpy.typing import DTypeLike, NDArray +from viscy_utils.prediction_metadata import ( + PREDICTION_COMPLETE_KEY, + canonical_z_planes, + checkpoint_signature, + completion_marker, + mark_complete, + mark_started, + outruns, + predicted_z_planes, + prediction_run, + record_z_planes, + same_run, + tzyx_shape, + z_planes_sha256_12, +) from viscy_utils.tensor_utils import to_numpy if TYPE_CHECKING: @@ -78,8 +95,8 @@ def _blend_in( ) -> torch.Tensor | NDArray: """Blend a new stack into an old stack over a Z range with linear feathering. - Supports both torch tensors (5D: B,C,Z,Y,X) for in-memory sliding window - prediction and numpy arrays (4D: C,Z,Y,X) for out-of-core HCSPredictionWriter. + Supports torch tensors (B,C,Z,Y,X or C,Z,Y,X) for in-memory prediction and + numpy arrays (C,Z,Y,X) for HCSPredictionWriter's per-window writes. Parameters ---------- @@ -104,16 +121,60 @@ def _blend_in( factors.append(min(i + 1, samples)) _logger.debug(f"Blending with factors {factors}.") if isinstance(old_stack, torch.Tensor): - factors = torch.tensor(factors, dtype=old_stack.dtype, device=old_stack.device) - factors = factors.view(1, 1, -1, 1, 1) + factors = _torch_factors(tuple(factors), old_stack.dtype, old_stack.device) else: factors = np.array(factors)[np.newaxis, :, np.newaxis, np.newaxis] return old_stack * (factors - 1) / factors + new_stack / factors +@functools.lru_cache(maxsize=64) +def _torch_factors(factors: tuple[int, ...], dtype: torch.dtype, device: torch.device) -> torch.Tensor: + """Blend factors as a ``(Z, 1, 1)`` tensor, cached: a per-window host-to-device copy would sync every window.""" + # A normal tensor even when first built under predict's inference mode, so a later + # grad-enabled caller can still save it for backward. + with torch.inference_mode(False): + return torch.tensor(factors, dtype=dtype, device=device).view(-1, 1, 1) + + +def _store_into(volume: torch.Tensor, z_slice: slice, planes: torch.Tensor) -> None: + """Write ``planes`` into ``volume[:, z_slice]``, rounding to the volume dtype as numpy does. + + Torch rounds to float32 and float64 exactly like numpy. It converts float64 to + float16 through float32, though, which can round twice, so other dtypes are cast + by numpy on the host. + + Parameters + ---------- + volume : torch.Tensor + ``(C, Z, Y, X)`` volume buffer. + z_slice : slice + Planes of ``volume`` to write. + planes : torch.Tensor + ``(C, Z, Y, X)`` planes, on the volume's device. + """ + if volume.dtype not in (torch.float32, torch.float64) and planes.dtype != volume.dtype: + cast = planes.cpu().numpy().astype(torch.empty(0, dtype=volume.dtype).numpy().dtype) + planes = torch.from_numpy(cast).to(volume.device) + volume[:, z_slice] = planes + + class HCSPredictionWriter(BasePredictionWriter): """Callback to store virtual staining predictions as HCS OME-Zarr. + Single-process only: completion is tracked per process and channels are + appended to the store without cross-rank coordination, so a multi-device + predict is refused before the store is opened. + + Each (position, timepoint) volume is assembled in memory from its depth + windows and written once, when its last window arrives (or at predict end), + instead of a zarr read-modify-write per window; the stored bytes are the + same. The write runs on a background thread while the next volume is + predicted, one at a time and in order; a FOV's completion marker is written + on that thread after its last volume, and a write error is raised at the + next batch or at predict end. ``write_input`` and plane-restricted predicts (``predict_z_planes``) keep + the per-window writes: the first stores slices beside each window, and the + second writes single planes, which a volume flush would fill in between. + Parameters ---------- output_store : str @@ -126,6 +187,52 @@ class HCSPredictionWriter(BasePredictionWriter): (must be writing to a new store), by default False. write_interval : {'batch', 'epoch', 'batch_and_epoch'}, optional When to write, by default "batch". + z_reduction : {'blend', 'center'}, optional + How to combine the depth windows that cover one output plane, by + default "blend". + + ``'blend'`` feathers each window in with :func:`_blend_in`. With the + usual stride-1 depth windows that recurrence is the incremental mean, + so every plane ends up the *unweighted mean of ``z_window_size`` + forward passes*. That is a real sharpness cost for a generative model: + on an iPSC-trained pix2pix3d membrane prediction the mean over 8 + windows halves the mid-band power relative to a single pass + (0.045 -> 0.021 of ground truth), and because all 8 share one XY grid + it cancels none of the decoder's XY lattice. + + ``'center'`` writes each window from its center plane onward, so a + later window overwrites everything but that center plane and the + volume is assembled one plane per forward pass with no averaging. + The leading ``z_window_size // 2`` planes come from the first window + and the trailing ``z_window_size - 1 - z_window_size // 2`` from the + last, since no window is centered on them. For odd ``z_window_size`` + that trailing count equals ``z_window_size // 2`` -- at ``w=5`` it is + 2, not 1. Every dynacell leaf that sets ``z_window_size`` uses an odd + value. + + Both modes assume windows arrive in increasing ``z`` order within a + ``(position, timepoint)`` -- ``'blend'`` because its factors are + derived from ``z_slice.start``, ``'center'`` because it relies on + last-write-wins. The predict dataloader is sequential and iterates + ``z`` innermost, which is what makes that hold. + checkpoint_path : str or None, optional + Checkpoint the model predicts with. Its path and content hash are + recorded in every FOV's completion marker, so a resume can tell + predictions made with different weights apart and refuses to mix + them in one store. The file's size and mtime are taken when the + writer is constructed -- the same instantiation pass that loads the + model's weights -- and ``on_predict_start`` refuses a file changed + since, so the recorded hash names the bytes the model loaded rather + than whatever sits at the path later. Default None records no + checkpoint. + settings_sha256_12 : str or None, optional + Hash of the other settings that shape the predicted voxels (model + inference arguments, input normalization, precision), recorded in the + marker alongside the checkpoint so a resume with the same weights but + other settings is refused as well. The submitter computes it from the + resolved config (``submit_benchmark_job.prediction_settings_sha256_12``); + the writer cannot derive it from live objects. Default None records + none. """ def __init__( @@ -134,12 +241,24 @@ def __init__( overwrite: bool = False, write_input: bool = False, write_interval: Literal["batch", "epoch", "batch_and_epoch"] = "batch", + z_reduction: Literal["blend", "center"] = "blend", + checkpoint_path: str | None = None, + settings_sha256_12: str | None = None, ) -> None: super().__init__(write_interval) + if z_reduction not in ("blend", "center"): + raise ValueError(f"z_reduction must be 'blend' or 'center', got {z_reduction!r}") self.output_store = output_store self.overwrite = overwrite self.write_input = write_input + self.z_reduction = z_reduction + self.checkpoint_path = checkpoint_path + self._checkpoint_signature = None if checkpoint_path is None else checkpoint_signature(checkpoint_path) + self.settings_sha256_12 = settings_sha256_12 self._dataset_scale = None + # Set by on_predict_start; None until then, so a failure during setup closes nothing. + self.plate: Plate | None = None + self._flush_pool: ThreadPoolExecutor | None = None def _get_scale_metadata(self, metadata_store: os.PathLike | None) -> None: """Read scale metadata from an existing zarr store. @@ -169,6 +288,11 @@ def on_predict_start(self, trainer: Trainer, pl_module: LightningModule) -> None pl_module : LightningModule The Lightning module being used for prediction. """ + if trainer.world_size > 1: + raise NotImplementedError( + f"HCSPredictionWriter tracks completion per process and appends channels without " + f"cross-rank coordination; run predict on a single device (got world_size={trainer.world_size})." + ) dm: HCSDataModule = trainer.datamodule self._get_scale_metadata(dm.data_path) self.z_padding = dm.z_window_size // 2 if dm.target_2d else 0 @@ -176,16 +300,68 @@ def on_predict_start(self, trainer: Trainer, pl_module: LightningModule) -> None source_channel = dm.source_channel target_channel = dm.target_channel prediction_channel = [ch + "_prediction" for ch in target_channel] - if os.path.exists(self.output_store): + self._prediction_channels = prediction_channel + if ( + self.checkpoint_path is not None + and checkpoint_signature(self.checkpoint_path) != self._checkpoint_signature + ): + raise RuntimeError( + f"Checkpoint '{self.checkpoint_path}' changed since this writer was constructed alongside the " + "model, so its hash would not name the weights actually loaded. Restart the predict." + ) + # Plane-restricted predict (HCSDataModule.predict_z_planes): only the listed + # planes are predicted, the rest keep the fill value, and the planes are part + # of the run identity so a resume never mixes them with another selection. + self._z_planes = dm.predict_dataset.z_planes + # write_input stores source/target slices beside each window, and a plane selection + # writes single planes that a [first, last] flush would fill in between: both write + # per window. + self._write_once = not self.write_input and self._z_planes is None + self._volumes: dict[tuple[str, int], tuple[torch.Tensor, int, int]] = {} + # Volume writes (and the markers after them) run here, in submission order. + self._flush_pool = ThreadPoolExecutor(max_workers=1, thread_name_prefix="prediction-flush") + self._pending: Future | None = None + # Hashes the checkpoint once; every FOV's marker records this identity. + self._run = prediction_run( + array_key=dm.array_key, + z_window_size=dm.z_window_size, + z_reduction=self.z_reduction, + checkpoint_path=self.checkpoint_path, + settings_sha256_12=self.settings_sha256_12, + z_planes_sha256_12=None if self._z_planes is None else z_planes_sha256_12(self._z_planes), + ) + window_arrays = dm.predict_dataset.window_arrays + self._source_shapes = {f"/{array.path}": tzyx_shape(array) for array in window_arrays} + # Array dimensions grow before writes, so only a successful write of every + # distinct (T, Z-window) establishes completion, including overlapping windows. + # A plane-restricted run marks the windows it skips as written up front. + self._written_windows: dict[str, NDArray[np.bool_]] = {} + for array in window_arrays: + written = np.zeros((array.frames, array.slices - dm.z_window_size + 1), dtype=bool) + if self._z_planes is not None: + written[:] = True + for t, planes in enumerate(self._z_planes[array.path.rsplit("/", 1)[0]]): + written[t, list(planes)] = False + self._written_windows[f"/{array.path}"] = written + run_positions = {array.path.rsplit("/", 1)[0] for array in window_arrays} + existed = os.path.exists(self.output_store) + if existed: if self.write_input: raise FileExistsError("Cannot write input to an existing store. Aborting.") else: self.plate = open_ome_zarr(self.output_store, mode="r+") # Validate all positions before mutating any. - needs_append: list[tuple[Position, list[str]]] = [] - for _, pos in self.plate.positions(): + positions = dict(self.plate.positions()) + needs_append: list[tuple[str, list[str]]] = [] + overwritten: list[str] = [] + mixed: list[str] = [] + oversized: list[str] = [] + foreign: list[str] = [] + channel_orders: set[tuple[str, ...]] = set() + for name, pos in positions.items(): existing = set(pos.channel_names) missing = [ch for ch in prediction_channel if ch not in existing] + channel_orders.add((*pos.channel_names, *missing)) for ch in prediction_channel: if ch in existing and not self.overwrite: self.plate.close() @@ -201,10 +377,67 @@ def on_predict_start(self, trainer: Trainer, pl_module: LightningModule) -> None self.output_store, ) if missing: - needs_append.append((pos, missing)) - for pos, channels in needs_append: + needs_append.append((name, missing)) + if name in run_positions: + overwritten.append(name) + if self._outruns_source(pos, name, dm.array_key): + oversized.append(name) + if self._holds_other_planes(pos, name, prediction_channel): + foreign.append(name) + elif self._cannot_share(pos, prediction_channel): + mixed.append(name) + if oversized: + self.plate.close() + raise ValueError( + f"{len(oversized)} FOVs in '{self.output_store}' hold more timepoints or depth " + f"slices than their source (e.g. {oversized[:3]}); arrays only grow, so the stale " + "planes would survive every rewrite yet the FOVs would be marked complete. " + "Predict into a new output store." + ) + if foreign: + self.plate.close() + raise ValueError( + f"{len(foreign)} FOVs in '{self.output_store}' hold {prediction_channel} that another run " + f"predicted on other planes (e.g. {foreign[:3]}); a plane-restricted predict rewrites only " + "its own planes, so remove that store (or predict into a new one) first." + ) + if mixed: + self.plate.close() + raise ValueError( + f"{len(mixed)} FOVs outside this run already hold {prediction_channel} " + f"in '{self.output_store}' that were predicted before completion markers " + f"existed or with a different checkpoint or settings (e.g. {mixed[:3]}); " + "predict into a new output store instead of mixing them." + ) + if len(channel_orders) > 1: + self.plate.close() + raise ValueError( + f"Positions of '{self.output_store}' would disagree on channel order after " + f"appending {prediction_channel} ({sorted(channel_orders)}); the writer addresses " + "channels by one plate-wide index. Predict into a new output store." + ) + # A started marker names this run as the writer of a channel's voxels. + # Run positions get one for every prediction channel: this run + # replaces their voxels, so an interrupted run must never reuse an + # old completion, and the next resume sees unfinished FOVs, not + # legacy ones. FOVs outside the run (e.g. excluded on resume) keep + # their markers, except for the channels appended below: those hold + # zeros this run allocated, and without a marker a later resume + # could not tell them from data predicted before markers existed. + started = {name: prediction_channel for name in overwritten} + for name, channels in needs_append: + started.setdefault(name, channels) for ch in channels: - pos.append_channel(ch, resize_arrays=True) + positions[name].append_channel(ch, resize_arrays=True) + if needs_append: + # Plate.channel_names is cached from the first position when the + # store is opened; reopen so channel indices and positions created + # later in this run see the appended channel. + self.plate.close() + self.plate = open_ome_zarr(self.output_store, mode="r+") + positions = {name: self.plate[name] for name in started} + for name, channels in started.items(): + mark_started(positions[name], channels, self._run) else: channel_names = prediction_channel if self.write_input: @@ -220,6 +453,15 @@ def on_predict_start(self, trainer: Trainer, pl_module: LightningModule) -> None self.source_index = self._get_channel_indices(source_channel) self.target_index = self._get_channel_indices(target_channel) self.prediction_index = self._get_channel_indices(prediction_channel) + # Read now: once volume writes run on the flush thread, this thread leaves the store alone. + self._store_dtypes: dict[str, torch.dtype] = {} + if existed: + for img_name in self._source_shapes: + try: + array = self.plate[img_name] + except KeyError: + continue # created by this run, in the prediction's dtype + self._store_dtypes[img_name] = torch.from_numpy(np.empty(0, dtype=array.dtype)).dtype def _get_channel_indices(self, channel_names: list[str]) -> list[int]: """Get channel indices from the plate for given channel names. @@ -266,6 +508,8 @@ def write_on_batch_end( Index of the current dataloader. """ _logger.debug(f"Writing batch {batch_idx}.") + if self._pending is not None and self._pending.done(): + self._pending.result() # raise a failed volume write here, not volumes later for sample_index, _ in enumerate(batch["index"][0]): self.write_sample(batch, prediction[sample_index], sample_index) @@ -279,7 +523,35 @@ def on_predict_end(self, trainer: Trainer, pl_module: LightningModule) -> None: pl_module : LightningModule The Lightning module. """ - self.plate.close() + try: + for img_name, t_index in list(self._volumes): + self._flush_volume(img_name, t_index, complete=False) + finally: + # Even when a write failed. + self._close() + if self._pending is not None: + self._pending.result() + + def on_exception(self, trainer: Trainer, pl_module: LightningModule, exception: BaseException) -> None: + """Close the store when predict fails, e.g. on a volume write that failed mid-run. + + Parameters + ---------- + trainer : Trainer + The Lightning trainer instance. + pl_module : LightningModule + The Lightning module. + exception : BaseException + The exception that stopped predict. + """ + self._close() + + def _close(self) -> None: + """Let the last volume write finish, then close the store; safe after a partial setup or a second call.""" + if self._flush_pool is not None: + self._flush_pool.shutdown(wait=True) + if self.plate is not None: + self.plate.close() def write_sample( self, @@ -299,15 +571,29 @@ def write_sample( Index of this sample within the batch. """ _logger.debug(f"Writing sample {sample_index}.") - sample_prediction = to_numpy(sample_prediction) img_name, t_index, z_index = [batch["index"][i][sample_index] for i in range(3)] t_index = int(t_index) z_index = int(z_index) + window_z_index = z_index # account for lost slices in 2.5D z_index += self.z_padding z_slice = slice(z_index, z_index + sample_prediction.shape[-3]) - image = self._create_image(img_name, sample_prediction.shape, sample_prediction.dtype) - _resize_image(image, t_index, z_slice) + written = self._written_windows[img_name] + if written[t_index, window_z_index]: + # A repeat would blend into a volume already flushed (a zero buffer) or blend twice. + raise RuntimeError(f"Window z={window_z_index} of {img_name} t={t_index} arrived twice.") + # Blend reads and the write go to the in-memory volume, or to the store per window. + if self._write_once: + # The volume stays on the prediction's device and reaches the host once, at its flush. + sample_prediction = sample_prediction.detach() + if sample_prediction.dtype == torch.bfloat16: + sample_prediction = sample_prediction.float() # to_numpy's cast + dest, lead = self._buffer_volume(img_name, t_index, sample_prediction, z_slice), (slice(None),) + else: + sample_prediction = to_numpy(sample_prediction) + image = self._create_image(img_name, sample_prediction.shape, sample_prediction.dtype) + _resize_image(image, t_index, z_slice) + dest, lead = image.oindex, (t_index, self.prediction_index) if self.write_input: source_stack = batch["source"][sample_index].cpu() center_slice_index = source_stack.shape[-3] // 2 @@ -316,9 +602,172 @@ def write_sample( image[t_index, self.target_index, z_index] = batch["target"][sample_index][:, center_slice_index].cpu() # write CZYX if self.z_padding == 0 and sample_prediction.shape[-3] > 1: - old_stack = image.oindex[t_index, self.prediction_index, z_slice] - sample_prediction = _blend_in(old_stack, sample_prediction, z_slice) - image.oindex[t_index, self.prediction_index, z_slice] = sample_prediction + if self.z_reduction == "blend": + old_stack = dest[(*lead, z_slice)] + if self._write_once and z_slice.start != 0: + # The numpy blend promotes to float64 (integer factors); so does this one. + # The first window is stored as predicted, so it needs no float64 copies. + old_stack, sample_prediction = old_stack.double(), sample_prediction.double() + sample_prediction = _blend_in(old_stack, sample_prediction, z_slice) + else: + # Drop the planes before this window's center: the window centered on + # each of them writes it later and wins. The first window keeps all of + # them because nothing is centered on the leading planes. + keep = 0 if z_index == 0 else sample_prediction.shape[-3] // 2 + sample_prediction = sample_prediction[..., keep:, :, :] + z_slice = slice(z_slice.start + keep, z_slice.stop) + # The buffer has the store's dtype, so this casts like the zarr write and blending + # reads back what the store would hold. + if self._write_once: + _store_into(dest, z_slice, sample_prediction) + else: + dest[(*lead, z_slice)] = sample_prediction + written[t_index, window_z_index] = True + if self._write_once: + if written[t_index].all(): + self._flush_volume(img_name, t_index, complete=written.all()) + elif written.all(): + self._mark_complete(img_name) + + def _mark_complete(self, img_name: str) -> None: + """Write the completion marker of the FOV holding ``img_name``; every window is in the store.""" + name = img_name.strip("/").rsplit("/", 1)[0] + position = self.plate[name] + planes = None if self._z_planes is None else self._z_planes[name] + record_z_planes(position, self._prediction_channels, planes) + marker = completion_marker(self._source_shapes[img_name], self._run) + mark_complete(position, self._prediction_channels, marker) + + def _buffer_volume( + self, img_name: str, t_index: int, sample_prediction: torch.Tensor, z_slice: slice + ) -> torch.Tensor: + """Return the in-memory CZYX volume of ``(img_name, t_index)``, allocating it on first use. + + Parameters + ---------- + img_name : str + Hierarchical path name of the source image. + t_index : int + Timepoint of the volume. + sample_prediction : torch.Tensor + First window of the volume; sets channel count, YX shape, device, and the dtype + unless the store already holds this array (then its dtype, as the per-window + writes would cast to it). + z_slice : slice + Planes of the incoming window. The flush writes from the first plane any + window reached to the last, so planes no window predicts (``target_2d`` + padding, unselected planes) keep what the store holds, as with per-window + writes. + + Returns + ------- + torch.Tensor + Zero-filled ``(C, Z, Y, X)`` buffer spanning the source depth, like a new zarr array. + """ + key = (img_name, t_index) + if key not in self._volumes: + depth = self._source_shapes[img_name][1] + shape = (sample_prediction.shape[0], depth, *sample_prediction.shape[-2:]) + dtype = self._store_dtypes.get(img_name, sample_prediction.dtype) + volume = torch.zeros(shape, dtype=dtype, device=sample_prediction.device) + self._volumes[key] = (volume, z_slice.start, z_slice.stop) + volume, start, stop = self._volumes[key] + if z_slice.stop > volume.shape[1]: + # A prediction deeper than its window: grow like the per-window resize does. + extra = volume.new_zeros((volume.shape[0], z_slice.stop - volume.shape[1], *volume.shape[2:])) + volume = torch.cat([volume, extra], dim=1) + self._volumes[key] = (volume, min(start, z_slice.start), max(stop, z_slice.stop)) + return volume + + def _flush_volume(self, img_name: str, t_index: int, complete: bool) -> None: + """Copy one buffered volume to the host and queue its write, over the planes its windows reached. + + Parameters + ---------- + img_name : str + Hierarchical path name of the source image. + t_index : int + Timepoint of the volume. + complete : bool + The volume is the FOV's last: mark the FOV complete once it is written. + """ + volume, start, stop = self._volumes.pop((img_name, t_index)) + planes = volume[:, start:stop].cpu().numpy() + if self._pending is not None: + # One write in flight bounds the host copies held to two volumes. + self._pending.result() + self._pending = self._flush_pool.submit(self._write_volume, img_name, t_index, planes, start, complete) + + def _write_volume(self, img_name: str, t_index: int, planes: NDArray, start: int, complete: bool) -> None: + """Write planes ``start:`` of one volume to the store, then the FOV's marker if ``complete`` (flush thread).""" + stop = start + planes.shape[1] + image = self._create_image(img_name, (planes.shape[0], stop, *planes.shape[2:]), planes.dtype) + _resize_image(image, t_index, slice(start, stop)) + image.oindex[t_index, self.prediction_index, slice(start, stop)] = planes + if complete: + self._mark_complete(img_name) + + def _outruns_source(self, position: Position, name: str, array_key: str) -> bool: + """Return whether the existing output array outruns this run's source in T or Z. + + Parameters + ---------- + position : Position + Existing output position that this run rewrites. + name : str + Plate-relative position name. + array_key : str + Array level this run writes. + + Returns + ------- + bool + True when the array exists and :func:`outruns` the source. + """ + try: + output = position[array_key] + except KeyError: + return False + return outruns(output, self._source_shapes[f"/{name}/{array_key}"]) + + def _cannot_share(self, position: Position, channels: list[str]) -> bool: + """Return whether ``position`` holds any of ``channels`` that this run cannot vouch for. + + Parameters + ---------- + position : Position + Existing output position outside this run. + channels : list of str + This run's prediction channels. + + Returns + ------- + bool + True when a channel present in the position has no marker of its + own (written before markers existed, so unverifiable even if other + channels are marked), or a marker written with other weights or + settings, or in a layout this version cannot read. + """ + completed = position.zattrs.get(PREDICTION_COMPLETE_KEY, {}) + return any( + channel in position.channel_names and not same_run(completed.get(channel), self._run) + for channel in channels + ) + + def _holds_other_planes(self, position: Position, name: str, channels: list[str]) -> bool: + """Return whether a plane-restricted run would leave another run's planes in ``position``. + + Overwrite rewrites in place and this run writes only its own planes, so a + channel another run wrote survives off them unless that run predicted exactly + these planes (e.g. the same slab with a new checkpoint). A full-volume run + rewrites every plane and never holds any. + """ + if self._z_planes is None or not self._cannot_share(position, channels): + return False + planes = canonical_z_planes(self._z_planes[name]) + return any( + predicted_z_planes(position, channel) != planes for channel in channels if channel in position.channel_names + ) def _create_image(self, img_name: str, shape: tuple[int, ...], dtype: DTypeLike): """Create or retrieve an image in the zarr store. @@ -351,7 +800,15 @@ def _create_image(self, img_name: str, shape: tuple[int, ...], dtype: DTypeLike) _logger.debug(f"Creating image '{img_name}'") _, row_name, col_name, pos_name, arr_name = img_name.split("/") position = self.plate.create_position(row_name, col_name, pos_name) + # A started marker from the first write on distinguishes an interrupted FOV + # from one written before completion markers existed. + mark_started(position, self._prediction_channels, self._run) shape = [1] + list(shape) + if self._z_planes is not None: + # Allocate the full source extent: the planes this run skips must still + # exist (at the fill value) so the store aligns plane by plane with its + # source, instead of ending at the last predicted plane. + shape[0], shape[2] = self._source_shapes[img_name][:2] shape[1] = len(position.channel_names) return position.create_zeros( arr_name, diff --git a/packages/viscy-utils/src/viscy_utils/cli.py b/packages/viscy-utils/src/viscy_utils/cli.py index 8d9e6b2ad..3c2620708 100644 --- a/packages/viscy-utils/src/viscy_utils/cli.py +++ b/packages/viscy-utils/src/viscy_utils/cli.py @@ -69,6 +69,161 @@ def _configure_wandb_logger( init_args["group"] = base_name +_MODEL_CHECKPOINT_CLASS_PATH = "lightning.pytorch.callbacks.ModelCheckpoint" +_MONITOR_HEALTH_CLASS_PATH = "viscy_utils.callbacks.MonitorHealthCheck" +_OPTIMIZER_HEALTH_CLASS_PATH = "viscy_utils.callbacks.OptimizerHealthCheck" +_LATEST_CKPT_FILENAME = "latest-epoch={epoch}-step={step}" + + +def _callback_field(callback, key: str): + """Return ``key`` from a callback config entry (Namespace or dict), else None.""" + return callback.get(key) if isinstance(callback, (Namespace, dict)) else None + + +def _init_args_of(callback) -> Namespace | dict | None: + """Return the mutable ``init_args`` mapping of a callback config entry.""" + init_args = _callback_field(callback, "init_args") + return init_args if isinstance(init_args, (Namespace, dict)) else None + + +def _fit_callbacks_without(config: Namespace, subcommand: str | None, class_path: str) -> list | None: + """Return the ``fit`` trainer's mutable callback list, or None to skip injecting. + + A fit whose ``trainer.callbacks`` is unset (``None``) gets an empty list to + append to. None when the subcommand is not ``fit``, when the config has no + ``trainer`` section, or when a ``class_path`` callback is already there, which + keeps each injection idempotent. + """ + if subcommand != "fit": + return None + root = config.get(subcommand) + if not isinstance(root, Namespace): + return None + trainer = root.get("trainer") + if not isinstance(trainer, Namespace): + return None + callbacks = trainer.get("callbacks") + if callbacks is None: + callbacks = trainer["callbacks"] = [] + if not isinstance(callbacks, list): + return None + if any(_callback_field(callback, "class_path") == class_path for callback in callbacks): + return None + return callbacks + + +def _inject_checkpoint_guardrails(config: Namespace, subcommand: str | None) -> None: + """Guarantee latest-weights checkpoints and fail loud on a dead monitor. + + A monitored ``ModelCheckpoint`` writes nothing once ``save_top_k`` is filled + and the metric stops improving — ``last.ckpt`` included, because Lightning + writes it on the same cadence. A frozen or non-finite monitor therefore ends + a run with no weights newer than the first few epochs. + + For ``fit`` only, this appends two callbacks to the resolved config: + + 1. an **unmonitored** ``ModelCheckpoint`` sharing the monitored callback's + ``dirpath`` and **inheriting its ``every_n_epochs``**, which saves on that + cadence unconditionally and owns ``save_last``; and + 2. a :class:`~viscy_utils.callbacks.MonitorHealthCheck` on the same metric. + + ``save_last`` is turned off on the monitored callback so exactly one callback + writes ``last.ckpt`` and that file means "latest weights" rather than "weights + at the last improvement". + + The monitored callback's ``monitor``, ``mode``, ``save_top_k`` and ``filename`` + are deliberately left untouched: those determine which checkpoint a run calls + "best", so changing them would alter model selection and invalidate + comparisons against already-published results. + + This runs on the resolved config rather than the shared trainer recipe because + leaves override ``trainer.callbacks`` wholesale to set their own ``dirpath`` — + a recipe-level default would be silently dropped by every real leaf. + """ + # Idempotent: a config that already carries the guardrails is left alone. + callbacks = _fit_callbacks_without(config, subcommand, _MONITOR_HEALTH_CLASS_PATH) + if callbacks is None: + return + + monitored = [ + init_args + for callback in callbacks + if _callback_field(callback, "class_path") == _MODEL_CHECKPOINT_CLASS_PATH + and (init_args := _init_args_of(callback)) is not None + and init_args.get("monitor") + ] + if not monitored: + return + + # The first monitored callback names the monitor, directory and cadence. + monitored_args = monitored[0] + monitor = monitored_args.get("monitor") + dirpath = monitored_args.get("dirpath") + # Hand `last.ckpt` to the unconditional callback so it tracks the newest epoch. + # Every monitored callback gives it up, or a second one would keep writing it. + for init_args in monitored: + init_args["save_last"] = False + + # Inherit the leaf's write cadence instead of forcing every epoch. A leaf that + # sets `every_n_epochs` is making a deliberate write-amplification decision: + # the Phase 17 arms run ~1470 epochs, so `every_n_epochs: 10` is the difference + # between 147 and 1470 write cycles of a 424 MB checkpoint, and this callback + # writes *two* files (`latest-*` and `last.ckpt`) each time -- ~1.25 TB of NFS + # traffic per arm at cadence 1. Forcing 1 here silently overrode that choice. + # + # The guarantee is unaffected: an *unmonitored* callback writes on its cadence + # unconditionally, so the newest weights are always within `every_n_epochs` + # epochs of where the run stopped, however dead the monitor is. Defaults to 1 + # when the leaf is silent, preserving the original behaviour. A leaf that saves + # on a step or time trigger keeps that trigger instead: once `save_last` moves + # here, an epoch cadence would leave a max_steps run with long epochs no recent + # `last.ckpt` to resume from. + cadence = {key: monitored_args.get(key) for key in ("every_n_train_steps", "train_time_interval")} + cadence = {key: value for key, value in cadence.items() if value} or { + "every_n_epochs": monitored_args.get("every_n_epochs") or 1 + } + + latest_args = { + "monitor": None, + "filename": _LATEST_CKPT_FILENAME, + "auto_insert_metric_name": False, + **cadence, + "save_top_k": 1, + "save_last": True, + } + if dirpath is not None: + latest_args["dirpath"] = dirpath + callbacks.append(Namespace(class_path=_MODEL_CHECKPOINT_CLASS_PATH, init_args=Namespace(**latest_args))) + callbacks.append( + Namespace(class_path=_MONITOR_HEALTH_CLASS_PATH, init_args=Namespace(monitor=monitor, patience=10)) + ) + + +def _inject_optimizer_health_guard(config: Namespace, subcommand: str | None) -> None: + """Attach the step-level optimizer guard to every ``fit`` run. + + A NaN *gradient* is invisible to loss-based monitoring. Under ``16-mixed``, + ``GradScaler`` skips the step and halves its scale while the forward — and the + logged loss — stays finite; once the scale reaches ``0.0`` every step is + skipped for the rest of the run and the weights never move again. Under + ``bf16-mixed`` there is no scaler, so the NaN lands in the weights instead. + :class:`~viscy_utils.callbacks.OptimizerHealthCheck` covers both. + + Injected here rather than in the shared trainer recipe for the same reason as + the checkpoint guardrails: leaves override ``trainer.callbacks`` wholesale to + set their own ``dirpath``, so a recipe-level default is silently dropped by + every real leaf. Injecting on the resolved config makes it unforgettable. + + Unlike :func:`_inject_checkpoint_guardrails` this does not depend on the leaf + declaring a monitored ``ModelCheckpoint`` — a run with no checkpointing at all + can still stall — so it also covers a fit that sets no callbacks. + """ + callbacks = _fit_callbacks_without(config, subcommand, _OPTIMIZER_HEALTH_CLASS_PATH) + if callbacks is None: + return + callbacks.append(Namespace(class_path=_OPTIMIZER_HEALTH_CLASS_PATH)) + + class VisCyCLI(LightningCLI): """Extending lightning CLI arguments and defaults.""" @@ -127,6 +282,8 @@ def _parse_ckpt_path(self) -> None: def before_instantiate_classes(self) -> None: """Apply shared config rewrites before Lightning object creation.""" _configure_wandb_logger(self.config, self.subcommand) + _inject_checkpoint_guardrails(self.config, self.subcommand) + _inject_optimizer_health_guard(self.config, self.subcommand) def _setup_environment() -> None: diff --git a/packages/viscy-utils/src/viscy_utils/evaluation/annotation.py b/packages/viscy-utils/src/viscy_utils/evaluation/annotation.py index fe106b5e8..d215027ad 100644 --- a/packages/viscy-utils/src/viscy_utils/evaluation/annotation.py +++ b/packages/viscy-utils/src/viscy_utils/evaluation/annotation.py @@ -57,6 +57,8 @@ def convert( try: import anndata as ad from natsort import natsorted + + from viscy_utils.evaluation.zarr_utils import write_anndata_zarr except ImportError as e: raise ImportError( "anndata and natsort are required for embedding conversion. " @@ -101,7 +103,7 @@ def convert( adata = ad.AnnData(X=X, obs=tracking_df, obsm=obsm) - adata.write_zarr(output_path) + write_anndata_zarr(adata, output_path) if return_anndata: return adata diff --git a/packages/viscy-utils/src/viscy_utils/evaluation/metrics.py b/packages/viscy-utils/src/viscy_utils/evaluation/metrics.py index de293b5c8..4148db300 100644 --- a/packages/viscy-utils/src/viscy_utils/evaluation/metrics.py +++ b/packages/viscy-utils/src/viscy_utils/evaluation/metrics.py @@ -171,6 +171,42 @@ def mean_average_precision( return map_metric.compute() +# Floors the SSIM/CS denominators so a zero data_range (c1=c2=0) on a flat window +# cannot produce 0/0; negligible relative to the c1/c2 stability constants otherwise. +_SSIM_DENOM_EPS: float = 1e-8 + + +def _safe_sqrt(x: torch.Tensor) -> torch.Tensor: + """Square root whose derivative at exactly zero is 0 rather than infinite. + + ``torch.sqrt`` backward is ``grad_out * 0.5 / sqrt(x)``, which is ``inf`` at + ``x == 0``. For the elements whose incoming ``grad_out`` is *itself* zero that + evaluates to ``0 * inf == nan``, so a single zero entry poisons the gradient + of every element in its convolution window — the forward pass stays perfectly + finite while the backward pass silently fills with NaN. + + Guarding with ``clamp_min(eps)`` or ``sqrt(x + eps)`` does not fix this: both + perturb the forward value and still leave a ``1 / (2 * sqrt(eps))`` gradient + spike. Instead substitute a dummy positive value *before* the square root and + restore the true zero after, so the ``sqrt`` node never sees zero and its + backward never divides by it. The forward result is bit-identical to + ``x.sqrt()`` and the gradient is bit-identical wherever ``x > 0``; only the + ``x == 0`` entries change, from NaN to 0. + + Parameters + ---------- + x : torch.Tensor + Non-negative input tensor. + + Returns + ------- + torch.Tensor + ``sqrt(x)``, with a zero subgradient at the exact zeros of ``x``. + """ + positive = x > 0 + return torch.where(positive, torch.where(positive, x, torch.ones_like(x)).sqrt(), torch.zeros_like(x)) + + def _compute_ssim_and_cs_bf16( y_pred: torch.Tensor, y: torch.Tensor, @@ -259,12 +295,64 @@ def _compute_ssim_and_cs_bf16( c1 = (k1 * data_range) ** 2 c2 = (k2 * data_range) ** 2 - sigma_x = mu_xx - mu_x * mu_x - sigma_y = mu_yy - mu_y * mu_y - sigma_xy = mu_xy - mu_x * mu_y - - contrast_sensitivity = (2 * sigma_xy + c2) / (sigma_x + sigma_y + c2) - ssim_full = ((2 * mu_x * mu_y + c1) / (mu_x * mu_x + mu_y * mu_y + c1)) * contrast_sensitivity + # Repair bf16 catastrophic cancellation in the second moments. With bf16 + # mean / mean-of-squares convolutions, ``mu_xx - mu_x**2`` cancels badly for + # windows over large-magnitude inputs (e.g. z-scored FCMAE targets with a tiny + # iqr → normalized values in the hundreds) and can round to a slightly *negative* + # variance. At depth=1 (2D) each window has only ``1*H_k*W_k`` samples, so the + # cancellation is far more likely than at depth=15 (3D, where the extra Z samples + # keep the estimate stable) — a near-flat window then drives the SSIM denominator + # to ~0 and produces intermittent NaN loss that trains fine for a while then + # diverges. + # + # A variance is non-negative by definition, so clamp both diagonal terms to >=0. + # The covariance must then be bounded by Cauchy-Schwarz (|sigma_xy| <= + # sqrt(sigma_x*sigma_y)): exact fp32 recovers cs=1 on a flat window because the + # equally-negative sigma_x/sigma_y/sigma_xy cancel in the ratio, so clamping only + # the diagonal terms would leave an unbalanced negative sigma_xy and inflate |cs| + # to >10 on background windows. With the bound a flat window gives + # sigma_x=sigma_y=sigma_xy=0 -> cs = c2 / c2 = 1, matching exact fp32; on + # well-conditioned windows the bound is slack and both clamps are no-ops. + # + # The bound goes through ``_safe_sqrt``, not ``sqrt``: the same cancellation that + # motivates the clamp also drives ``sigma_x * sigma_y`` to *exactly* zero (either + # term rounds to 0), and ``sqrt`` has an infinite derivative there. Autograd then + # produces ``0 * inf == nan`` for the clamp-inactive elements, which spreads + # through the conv backward and NaNs most of the input gradient while the forward + # loss stays finite. + # + # That guards the fp32 path only. A NaN also reaches this line's ``MulBackward0`` + # when an enclosing 16-mixed autocast is left enabled, which ``ssim_25d`` now + # prevents -- see the note at its call site. Do not "fix" that by detaching the + # bound: the clamp binds often enough on ordinary inputs that the bound carries + # real gradient, and detaching drops the helper's gradient agreement with the + # fp32 monai reference from cosine 0.99 to 0.79 + # (``test_ssim_helper_gradient_flow``). + # + # Exposure depends on the per-window mean-to-variance ratio, so it is a latent + # trap rather than a constant hazard. Synthetic z-scored fields with a flat + # background at a nonzero offset reach 41-48% of ``pred.grad``; real z-scored + # iPSC FOVs measured 0% at depth=15 and 0-6484 NaN entries at depth=1, and a + # 60-step run of the real FCMAE-2D model on a real batch did not trip + # ``GradScaler`` at all. Treat this as a correctness fix for a reachable + # gradient path, not as a diagnosis of any particular run. + sigma_x = (mu_xx - mu_x * mu_x).clamp_min(0.0) + sigma_y = (mu_yy - mu_y * mu_y).clamp_min(0.0) + sigma_xy_bound = _safe_sqrt(sigma_x * sigma_y) + sigma_xy = torch.clamp(mu_xy - mu_x * mu_y, min=-sigma_xy_bound, max=sigma_xy_bound) + + # ``_SSIM_DENOM_EPS`` floors the stability constants so a zero data_range (→ c1=c2=0) + # on a flat window cannot yield 0/0. It goes in the NUMERATOR as well as the + # denominator -- exactly c1 -> c1 + eps, c2 -> c2 + eps -- because SSIM(x, x) == 1 + # only holds when the same constant appears in both. Flooring the denominator alone + # gave SSIM(y, y) = 0.082 at data_range 1e-3 and 0.0 at data_range 0, and + # ``ms_ssim_25d`` recomputes data_range from a downsampled target per scale, so an + # all-zero crop scored loss 1.0 for a numerically perfect prediction. Negligible vs + # c1/c2 otherwise: max 2.2e-4 relative on ordinary inputs. + contrast_sensitivity = (2 * sigma_xy + c2 + _SSIM_DENOM_EPS) / (sigma_x + sigma_y + c2 + _SSIM_DENOM_EPS) + ssim_full = ( + (2 * mu_x * mu_y + c1 + _SSIM_DENOM_EPS) / (mu_x * mu_x + mu_y * mu_y + c1 + _SSIM_DENOM_EPS) + ) * contrast_sensitivity return ssim_full, contrast_sensitivity @@ -291,12 +379,19 @@ def ssim_25d( depth = preds.shape[2] if depth > 15: warn(f"Input depth {depth} is potentially too large for 2.5D SSIM.") - ssim_img, cs_img = _compute_ssim_and_cs_bf16( - preds, - target, - kernel_size=(depth, *in_plane_window_size), - data_range=target.max(), - ) + # Outside autocast: this helper picks its own precision on purpose (bf16 convs, + # fp32 for everything after), and letting an enclosing 16-mixed autocast re-enter + # it puts backward intermediates in fp16, where the ~1e8 gradients the floored + # SSIM denominator produces overflow to inf and a later multiply turns them into + # NaN. Measured on the captured failing batch: 0.02% NaN under autocast, 0.00% in + # fp32, with the forward differing by ~3e-6. + with torch.amp.autocast(device_type=preds.device.type, enabled=False): + ssim_img, cs_img = _compute_ssim_and_cs_bf16( + preds, + target, + kernel_size=(depth, *in_plane_window_size), + data_range=target.max(), + ) # aggregate to one scalar per batch ssim = ssim_img.view(ssim_img.shape[0], -1).mean(1) if return_contrast_sensitivity: diff --git a/packages/viscy-utils/src/viscy_utils/evaluation/zarr_utils.py b/packages/viscy-utils/src/viscy_utils/evaluation/zarr_utils.py index e4566b029..065b7c176 100644 --- a/packages/viscy-utils/src/viscy_utils/evaluation/zarr_utils.py +++ b/packages/viscy-utils/src/viscy_utils/evaluation/zarr_utils.py @@ -10,6 +10,44 @@ from pandas.arrays import ArrowStringArray +def _object_strings(df: pd.DataFrame) -> pd.DataFrame: + """Copy of ``df`` with Arrow-backed strings (columns, categories, index) as object. + + TODO: remove once anndata 0.13 supports pandas 3 Arrow-backed strings natively. + anndata 0.12.9+ requires pandas <3, so we stay on 0.12.6 + pandas 3, whose zarr + writer has no method for ``ArrowStringArray``. + """ + df = df.copy() + for col in df.columns: + arr = df[col].array + if isinstance(arr, ArrowStringArray): + df[col] = df[col].astype(object) + elif isinstance(arr, pd.Categorical) and isinstance(arr.categories._values, ArrowStringArray): + df[col] = df[col].cat.rename_categories(arr.categories.astype(object)) + if isinstance(df.index._values, ArrowStringArray): + df.index = df.index.astype(object) + return df + + +def write_anndata_zarr(adata: ad.AnnData, zarr_path: str | Path) -> None: + """Write ``adata`` to a zarr store under pandas' default string inference. + + Replaces ``adata.obs`` and ``adata.var`` with copies whose Arrow-backed strings + are object arrays (see :func:`_object_strings`), then writes. Use this instead of + ``adata.write_zarr`` or setting ``pd.options.future.infer_string`` globally. + + Parameters + ---------- + adata : ad.AnnData + AnnData to write; its obs and var frames are replaced in place. + zarr_path : str | Path + Output zarr store. + """ + adata.obs = _object_strings(adata.obs) + adata.var = _object_strings(adata.var) + adata.write_zarr(zarr_path) + + def append_to_anndata_zarr( zarr_path: str | Path, *, @@ -39,18 +77,7 @@ def append_to_anndata_zarr( ad.settings.allow_write_nullable_strings = True if obs is not None: - # TODO: remove once anndata 0.13 supports pandas 3 Arrow-backed strings natively. - # anndata 0.12.9+ requires pandas <3, so we stay on 0.12.6 + pandas 3 and - # must manually downcast ArrowStringArray columns to object dtype before writing. - obs = obs.copy() - for col in obs.columns: - arr = obs[col].array - if isinstance(arr, ArrowStringArray): - obs[col] = obs[col].astype(object) - elif isinstance(arr, pd.Categorical) and isinstance(arr.categories._values, ArrowStringArray): - obs[col] = obs[col].cat.rename_categories(arr.categories.astype(object)) - if isinstance(obs.index._values, ArrowStringArray): - obs.index = obs.index.astype(object) + obs = _object_strings(obs) if "obs" in store: del store["obs"] write_elem(store, "obs", obs) diff --git a/packages/viscy-utils/src/viscy_utils/losses/__init__.py b/packages/viscy-utils/src/viscy_utils/losses/__init__.py index 7bf90b536..9b6b89c25 100644 --- a/packages/viscy-utils/src/viscy_utils/losses/__init__.py +++ b/packages/viscy-utils/src/viscy_utils/losses/__init__.py @@ -1,4 +1,7 @@ +from viscy_utils.losses.background_lowpass import BackgroundLowPass from viscy_utils.losses.mixed_loss import MixedLoss +from viscy_utils.losses.seg_aux import SegAuxDice +from viscy_utils.losses.seg_consistency import SegConsistencyLoss from viscy_utils.losses.spotlight import SpotlightLoss -__all__ = ["MixedLoss", "SpotlightLoss"] +__all__ = ["BackgroundLowPass", "MixedLoss", "SegAuxDice", "SegConsistencyLoss", "SpotlightLoss"] diff --git a/packages/viscy-utils/src/viscy_utils/losses/background_lowpass.py b/packages/viscy-utils/src/viscy_utils/losses/background_lowpass.py new file mode 100644 index 000000000..7dbd40ed4 --- /dev/null +++ b/packages/viscy-utils/src/viscy_utils/losses/background_lowpass.py @@ -0,0 +1,258 @@ +"""Background low-pass of a training target, foreground interior kept unchanged. + +``x' = w * x + (1 - w) * B`` with ``m = dilate(fg_mask, r)``, +``w = clamp(G_f(m), 0, 1)`` and the background estimate +``B = G_lp(x * (1 - m)) / G_lp(1 - m)``: where the feather kernel lies inside +``m`` the target is unchanged, where it misses ``m`` the target is a Gaussian +low-pass of the background pixels alone, and in between the feather blends the +two without a seam. The blend reaches the feather radius inside ``m``, so a +feather radius above ``r`` also mixes a little ``B`` into the rim of the +foreground itself. + +``B`` is a normalized convolution: a plain ``G_lp(x)`` would bleed bright +foreground into the background as a halo, while ``B`` averages background +pixels only, so a foreground blob never reaches it. With ``sigma_lp=None`` (its +``sigma_lp -> inf`` limit) ``B`` is flat: the mean of each plane's background +pixels, which also removes autofluorescence and uneven illumination. + +It changes the target, never the loss: the background stays supervised toward a +smooth field. In flow matching, masking the background out of the loss would +leave the initial Gaussian noise there unremoved (the Spotlight v1 collapse). + +The Gaussians are normalized, edge-replicated (``scipy.ndimage`` ``mode="nearest"``, +``truncate=4.0``) and separable, applied as shifted multiply-adds in float32, so +no convolution backend (cuDNN TF32, autocast) can lower their precision. ``B`` is a +weighted mean of background pixels, so it keeps the local background level and +the op needs no change to the target's normalization. +""" + +import math + +import torch +import torch.nn.functional as F +from torch import Tensor, nn + +__all__ = ["BackgroundLowPass"] + +# At or below this fraction of the low-pass kernel's mass on background pixels the +# normalized convolution has no background in reach; B falls back to the plane's flat mean. +_MIN_BACKGROUND_WEIGHT = 1e-6 + + +def _gaussian_kernel(sigma: float, device: torch.device) -> Tensor: + """Return a normalized float32 1D Gaussian of radius ``int(4 * sigma + 0.5)``, as scipy does.""" + radius = int(4.0 * sigma + 0.5) + x = torch.arange(-radius, radius + 1, device=device, dtype=torch.float32) + k = torch.exp(-0.5 * (x / sigma) ** 2) + return k / k.sum() + + +def _gaussian_blur(x: Tensor, sigmas: tuple[float, float, float]) -> Tensor: + """Separable Gaussian blur of ``(N, D, H, W)`` with edge-replicated borders. + + Parameters + ---------- + x : Tensor + Float32, shape ``(N, D, H, W)``. + sigmas : tuple of float + Standard deviation in voxels along ``(D, H, W)``; 0 leaves that axis alone. + + Returns + ------- + Tensor + Blurred tensor, shape of ``x``. + """ + for dim, sigma in zip((1, 2, 3), sigmas): + length = x.shape[dim] + # A size-1 axis replicates its one value, so the blur along it is the identity. + if sigma <= 0 or length == 1: + continue + k = _gaussian_kernel(sigma, x.device) + r = k.numel() // 2 + # F.pad lists the last dim first; replicate supports any pad width. + pad = [0] * 6 + pad[2 * (3 - dim)] = pad[2 * (3 - dim) + 1] = r + padded = F.pad(x.unsqueeze(1), pad, mode="replicate").squeeze(1) + out = k[0] * padded.narrow(dim, 0, length) + for i in range(1, k.numel()): + out = out + k[i] * padded.narrow(dim, i, length) + x = out + return x + + +class BackgroundLowPass(nn.Module): + """Replace a target's background by a low-pass estimate from background pixels. + + ``x' = w * x + (1 - w) * B``, ``m = dilate(fg_mask > 0.5, r)``, + ``w = clamp(G_f(m), 0, 1)``, ``B = G_lp(x * (1 - m)) / G_lp(1 - m)``. + ``w`` is exactly 1 where the feather kernel lies entirely inside ``m`` (the + dilated mask shrunk by its radius, ``int(4 * sigma_feather + 0.5)``), so + ``x' = x`` exactly there, and exactly 0 where the kernel misses ``m``, so + ``x' = B`` exactly there. + + ``sigma_lp=None`` makes ``B`` flat: per sample, channel and Z plane, the mean + of ``x`` over the plane's background pixels, the ``sigma_lp -> inf`` limit of + the normalized convolution. Where the normalized convolution has at most + ``1e-6`` of its kernel mass on background (a thin background ring narrower + than its support, say), ``B`` falls back to that flat mean. A plane with no + background pixel at all has no estimate: ``B = x`` there, and with the + per-plane feather ``w == 1``, so the plane is returned unchanged. + + By default every operation acts in XY, per Z plane: Z is anisotropic, and + 2D models have ``D = 1``. The ``*_z`` parameters extend each to Z. + + Computed in float32 regardless of autocast; returned in the target's dtype. + + Parameters + ---------- + sigma_lp : float or None + Gaussian sigma in XY, in voxels, of the background estimate (> 0); + ``None`` (or ``inf``) for a flat background. + sigma_feather : float + Gaussian sigma in XY, in voxels, that feathers the dilated mask into + ``w`` (>= 0; 0 keeps the hard dilated mask). + dilate_radius : int + Dilation radius in XY, in voxels (>= 0): a ``(2r + 1)``-wide square. + sigma_lp_z : float + Background-estimate sigma along Z, in voxels (>= 0; 0 = per plane). + Must be 0 with a flat background, which is per plane. + sigma_feather_z : float + Feather sigma along Z, in voxels (>= 0; 0 = per plane). + dilate_radius_z : int + Dilation radius along Z, in voxels (>= 0; 0 = per plane). + """ + + def __init__( + self, + sigma_lp: float | None, + sigma_feather: float, + dilate_radius: int, + sigma_lp_z: float = 0.0, + sigma_feather_z: float = 0.0, + dilate_radius_z: int = 0, + ) -> None: + super().__init__() + if sigma_lp is not None and math.isinf(sigma_lp): + sigma_lp = None + if sigma_lp is None: + if sigma_lp_z != 0: + raise ValueError(f"sigma_lp_z={sigma_lp_z} has no effect with a flat background (sigma_lp=None)") + elif sigma_lp <= 0: + raise ValueError(f"sigma_lp must be > 0 or None, got {sigma_lp}") + for name, value in ( + ("sigma_feather", sigma_feather), + ("sigma_lp_z", sigma_lp_z), + ("sigma_feather_z", sigma_feather_z), + ("dilate_radius", dilate_radius), + ("dilate_radius_z", dilate_radius_z), + ): + if value < 0: + raise ValueError(f"{name} must be >= 0, got {value}") + self.sigma_lp = None if sigma_lp is None else float(sigma_lp) + self.sigma_feather = float(sigma_feather) + self.dilate_radius = int(dilate_radius) + self.sigma_lp_z = float(sigma_lp_z) + self.sigma_feather_z = float(sigma_feather_z) + self.dilate_radius_z = int(dilate_radius_z) + + def dilated_mask(self, fg_mask: Tensor) -> Tensor: + """Return ``m = dilate(fg_mask > 0.5, r)`` as float32 in {0, 1}. + + Parameters + ---------- + fg_mask : Tensor + Foreground mask, shape ``(B, C, D, H, W)``; binarized at 0.5. + + Returns + ------- + Tensor + Float32, shape of ``fg_mask``. + """ + b, c = fg_mask.shape[:2] + rz, r = self.dilate_radius_z, self.dilate_radius + mask = (fg_mask > 0.5).float().reshape(b * c, 1, *fg_mask.shape[2:]) + if rz or r: + # max_pool pads with -inf, so the border never dilates the mask. + mask = F.max_pool3d(mask, (2 * rz + 1, 2 * r + 1, 2 * r + 1), stride=1, padding=(rz, r, r)) + return mask.reshape(fg_mask.shape) + + def blend_weight(self, dilated: Tensor) -> Tensor: + """Return ``w = clamp(G_f(m), 0, 1)`` in float32, exactly 1 under an all-``m`` kernel. + + Parameters + ---------- + dilated : Tensor + Dilated mask ``m`` from :meth:`dilated_mask`, shape ``(B, C, D, H, W)``. + + Returns + ------- + Tensor + Float32 weights in [0, 1], shape of ``dilated``. + """ + b, c = dilated.shape[:2] + spatial = dilated.shape[2:] + with torch.autocast(device_type=dilated.device.type, enabled=False): + sigmas = (self.sigma_feather_z, self.sigma_feather, self.sigma_feather) + blurred = _gaussian_blur(dilated.float().reshape(b * c, *spatial), sigmas) + # The normalized kernel sums to 1 only up to rounding (0.99999982 at sigma 0.5). + # Edge replication makes the blur of ones the same value at every voxel, computed + # by the same operations as any all-ones window of m: dividing by it gives exactly + # 1 there. Size-2 axes reproduce it (size 1 is skipped, like in the full tensor). + ones = torch.ones((1, *(min(n, 2) for n in spatial)), device=dilated.device) + unit = _gaussian_blur(ones, sigmas)[0, 0, 0, 0] + return (blurred / unit).clamp(0.0, 1.0).reshape(dilated.shape) + + def background(self, target: Tensor, dilated: Tensor) -> Tensor: + """Return the background estimate ``B`` in float32; ``target`` on planes without background. + + Parameters + ---------- + target : Tensor + Target, shape ``(B, C, D, H, W)``. + dilated : Tensor + Dilated mask ``m`` from :meth:`dilated_mask`, same shape. + + Returns + ------- + Tensor + Float32, shape of ``target``. + """ + b, c = target.shape[:2] + spatial = target.shape[2:] + with torch.autocast(device_type=target.device.type, enabled=False): + x = target.float() + bg = 1.0 - dilated.float() + count = bg.sum(dim=(-2, -1), keepdim=True) + plane_mean = (x * bg).sum(dim=(-2, -1), keepdim=True) / count.clamp(min=1.0) + flat = torch.where(count > 0, plane_mean, x) + if self.sigma_lp is None: + return flat + sigmas = (self.sigma_lp_z, self.sigma_lp, self.sigma_lp) + num = _gaussian_blur((x * bg).reshape(b * c, *spatial), sigmas).reshape(x.shape) + den = _gaussian_blur(bg.reshape(b * c, *spatial), sigmas).reshape(x.shape) + defined = den > _MIN_BACKGROUND_WEIGHT + return torch.where(defined, num / den.clamp(min=_MIN_BACKGROUND_WEIGHT), flat) + + def forward(self, target: Tensor, fg_mask: Tensor) -> Tensor: + """Return the target with its background replaced by ``B``. + + Parameters + ---------- + target : Tensor + Target, shape ``(B, C, D, H, W)``. + fg_mask : Tensor + Foreground mask, same shape; binarized at 0.5. + + Returns + ------- + Tensor + ``w * target + (1 - w) * B``, shape and dtype of ``target``. + """ + if target.ndim != 5: + raise ValueError(f"target must be (B, C, D, H, W), got shape {tuple(target.shape)}") + if fg_mask.shape != target.shape: + raise ValueError(f"fg_mask {tuple(fg_mask.shape)} must match target {tuple(target.shape)}") + dilated = self.dilated_mask(fg_mask) + w = self.blend_weight(dilated) + x = target.float() + return (w * x + (1.0 - w) * self.background(target, dilated)).to(target.dtype) diff --git a/packages/viscy-utils/src/viscy_utils/losses/seg_aux.py b/packages/viscy-utils/src/viscy_utils/losses/seg_aux.py new file mode 100644 index 000000000..350e66664 --- /dev/null +++ b/packages/viscy-utils/src/viscy_utils/losses/seg_aux.py @@ -0,0 +1,310 @@ +"""Self-calibrating soft-Dice auxiliary loss for virtual staining. + +The prediction is soft-thresholded at a level derived per patch from the target +and the foreground mask it is scored against, so the loss needs no knowledge of +the normalization applied upstream (z-score, median/IQR, MinMax to [-1, 1]). + +For every (sample, channel) patch: + +- ``tau = quantile(target, 1 - mean(fg_mask))``: the level at which the target's + own foreground fraction equals the mask's. +- ``s = c * (median(target | fg) - median(target | bg))``: the knee width + relative to the patch's own foreground/background contrast. +- ``p = sigmoid((pred - tau) / s)``: bounded, with non-zero gradient everywhere. +- ``dice = 1 - 2 * sum(p * r) / (sum(p**2) + sum(r**2) + eps)``, where the + reference ``r`` is the binary mask (``label="mask"``) or the target through + the same sigmoid, ``sigmoid((target - tau) / s)`` (``label="target"``). + +``tau``, ``s`` and ``r`` are computed from the target without gradient. + +With ``label="mask"`` the loss is not zero at ``pred == target`` wherever the +mask is not a threshold of the target (0.07-0.25 on the iPSC baselines' +validation patches, 2026-09-24), so it also pulls the prediction toward the +mask. ``label="target"`` keeps the mask's role in setting ``tau``, ``s`` and +patch validity but is minimised exactly at ``pred == target``: it isolates a +segmentation-shaped penalty from that pull. + +Two optional, orthogonal extensions (both off by default, which leaves the loss +bit-identical to the plain soft Dice above): + +- ``weighting="sauna"`` weights every voxel's terms in both Dice sums by + ``|y~|``, the magnitude of SAUNA's combined boundary/thickness uncertainty map + of the patch's mask (:func:`~viscy_utils.losses.seg_aux_maps.sauna_weight_map`), + which down-weights the boundary of thick structures and keeps thin ones and + deep interiors/backgrounds at full weight. This is a SAUNA-map-weighted + binary soft Dice adapted from SAUNA's map, not SAUNA's loss: published SAUNA + (Oulu-IMEDS/SAUNA, ``SoftJaccardLoss``) regresses a tanh head toward the + signed ``y~`` with separate foreground (``relu(y~)``) and background + (``relu(-y~)``) soft-Jaccard terms. +- ``topology="cldice"`` replaces the per-patch loss by + ``(1 - alpha) * dice + alpha * (1 - clDice)`` with the soft-skeleton clDice. +""" + +import torch +from torch import Tensor, nn + +from viscy_utils.losses.seg_aux_maps import sauna_weight_map, soft_skeleton + +__all__ = ["SegAuxDice"] + + +def _quantile_sorted(sorted_rows: Tensor, q: Tensor) -> Tensor: + """Linearly interpolated per-row quantile of pre-sorted rows. + + Matches ``torch.quantile(..., interpolation="linear")`` but takes one + quantile level per row and has no input-size limit, which + ``torch.quantile`` imposes (2**24 elements) and large 3D patches exceed. + + Parameters + ---------- + sorted_rows : Tensor + Rows sorted ascending along the last dim, shape ``(R, N)``. + q : Tensor + Quantile level per row in ``[0, 1]``, shape ``(R,)``. + + Returns + ------- + Tensor + Quantile per row, shape ``(R,)``. + """ + n = sorted_rows.shape[-1] + pos = q.clamp(0.0, 1.0) * (n - 1) + lo = pos.floor().long() + hi = (lo + 1).clamp(max=n - 1) + frac = pos - lo.to(pos.dtype) + v_lo = sorted_rows.gather(-1, lo.unsqueeze(-1)).squeeze(-1) + v_hi = sorted_rows.gather(-1, hi.unsqueeze(-1)).squeeze(-1) + return v_lo + frac * (v_hi - v_lo) + + +def _masked_median(values: Tensor, keep: Tensor) -> Tensor: + """Linearly interpolated per-row median over the entries where ``keep`` is set. + + Parameters + ---------- + values : Tensor + Values, shape ``(R, N)``. + keep : Tensor + Boolean selection, shape ``(R, N)``. + + Returns + ------- + Tensor + Median per row, shape ``(R,)``; meaningless for rows with no kept entry. + """ + k = keep.sum(-1) + # Dropped entries sort to the end, so the first k sorted values are the kept ones. + sorted_rows = torch.where(keep, values, torch.full_like(values, float("inf"))).sort(dim=-1).values + pos = 0.5 * (k - 1).clamp(min=0).to(values.dtype) + lo = pos.floor().long() + hi = torch.minimum(lo + 1, (k - 1).clamp(min=0)) + frac = pos - lo.to(pos.dtype) + v_lo = sorted_rows.gather(-1, lo.unsqueeze(-1)).squeeze(-1) + v_hi = sorted_rows.gather(-1, hi.unsqueeze(-1)).squeeze(-1) + return v_lo + frac * (v_hi - v_lo) + + +class SegAuxDice(nn.Module): + """Squared-denominator soft Dice on a self-calibrated sigmoid of the prediction. + + Patches whose mask is empty or full, or whose target is not brighter inside + the mask than outside it (no contrast to set a knee width from), carry no + segmentation signal and are excluded from the mean. + + The knee width is set from the foreground/background contrast rather than + the patch IQR: on thin-structure patches (membrane) the IQR is ~0.65x the + contrast, which left 11-25% of foreground voxels with a vanishing sigmoid + gradient, against 0.5-7.5% with the contrast knee (measured on the iPSC + baselines' validation patches, 2026-09-24). If every patch is excluded the loss is a zero + that stays connected to ``pred``'s graph. + + Computed in float32 regardless of autocast. + + Parameters + ---------- + c : float + Knee width as a fraction of the patch's foreground/background contrast. + eps : float + Dice denominator stabilizer, and the floor on the knee width. + label : {"mask", "target"} + Dice reference: the binary foreground mask, or the target soft-thresholded + like the prediction (self-consistent; zero loss at ``pred == target``). + weighting : {"none", "sauna"} + ``"sauna"`` computes ``sum(w * p * r) / (sum(w * p**2) + sum(w * r**2))`` + with ``w = |y~|`` from the binarized mask, without gradient. + spacing : tuple of float or None + Physical voxel size per spatial dim, e.g. ``(z, y, x)`` in um; required + with ``weighting="sauna"`` (and only then), one entry per spatial dim. + topology : {"none", "cldice"} + ``"cldice"`` mixes in ``1 - clDice`` with weight ``cldice_alpha``. + cldice_alpha : float + Weight of ``1 - clDice`` in [0, 1]; the Dice term gets ``1 - cldice_alpha``. + cldice_iters : int + Soft-skeleton erosion iterations (>= 1). + """ + + def __init__( + self, + c: float = 0.1, + eps: float = 1e-6, + label: str = "mask", + weighting: str = "none", + spacing: tuple[float, ...] | None = None, + topology: str = "none", + cldice_alpha: float = 0.5, + cldice_iters: int = 10, + ) -> None: + super().__init__() + if c <= 0: + raise ValueError(f"c must be > 0, got {c}") + if eps <= 0: + raise ValueError(f"eps must be > 0, got {eps}") + if label not in ("mask", "target"): + raise ValueError(f"label must be 'mask' or 'target', got {label!r}") + if weighting not in ("none", "sauna"): + raise ValueError(f"weighting must be 'none' or 'sauna', got {weighting!r}") + if weighting == "sauna": + if spacing is None: + raise ValueError("weighting='sauna' needs spacing (physical voxel size per spatial dim)") + if len(spacing) == 0 or any(s <= 0 for s in spacing): + raise ValueError(f"spacing must be non-empty and positive, got {spacing}") + elif spacing is not None: + raise ValueError(f"spacing={spacing} has no effect without weighting='sauna'") + if topology not in ("none", "cldice"): + raise ValueError(f"topology must be 'none' or 'cldice', got {topology!r}") + if not 0.0 <= cldice_alpha <= 1.0: + raise ValueError(f"cldice_alpha must be in [0, 1], got {cldice_alpha}") + if cldice_iters < 1: + raise ValueError(f"cldice_iters must be >= 1, got {cldice_iters}") + self.c = c + self.eps = eps + self.label = label + self.weighting = weighting + self.spacing = None if spacing is None else tuple(float(s) for s in spacing) + self.topology = topology + self.cldice_alpha = cldice_alpha + self.cldice_iters = cldice_iters + + def per_channel(self, pred: Tensor, target: Tensor, fg_mask: Tensor) -> tuple[Tensor, Tensor]: + """Compute the Dice loss per (sample, channel) and which entries are valid. + + Parameters + ---------- + pred : Tensor + Prediction, shape ``(B, C, *spatial)``. + target : Tensor + Ground truth, same shape. + fg_mask : Tensor + Foreground mask, same shape; binarized at 0.5. + + Returns + ------- + tuple of (Tensor, Tensor) + Dice loss ``(B, C)`` in float32 (entries for invalid patches are + finite but meaningless) and the boolean validity mask ``(B, C)``. + """ + if pred.shape != target.shape or fg_mask.shape != target.shape: + raise ValueError( + f"pred, target and fg_mask must share a shape; got " + f"{tuple(pred.shape)}, {tuple(target.shape)}, {tuple(fg_mask.shape)}" + ) + b, c = target.shape[:2] + spatial = tuple(target.shape[2:]) + if self.spacing is not None and len(self.spacing) != len(spatial): + raise ValueError(f"spacing {self.spacing} has {len(self.spacing)} entries for {len(spatial)} spatial dims") + with torch.autocast(device_type=pred.device.type, enabled=False): + pred_f = pred.float().reshape(b * c, -1) + mask = (fg_mask.reshape(b * c, -1) > 0.5).float() + n = mask.shape[-1] + fg_count = mask.sum(-1) + with torch.no_grad(): + sorted_t = target.detach().float().reshape(b * c, -1).sort(dim=-1).values + q = 1.0 - fg_count / n + tau = _quantile_sorted(sorted_t, q) + flat_t = target.detach().float().reshape(b * c, -1) + fg = mask > 0.5 + contrast = _masked_median(flat_t, fg) - _masked_median(flat_t, ~fg) + valid = (fg_count > 0) & (fg_count < n) & (contrast > 0) + # An empty or full mask makes one median inf; substitute a finite + # width so invalid entries stay finite and the mask-multiply in + # forward() can zero them (inf * 0 would be NaN). + s = torch.where(valid, self.c * contrast, torch.ones_like(contrast)).clamp(min=self.eps) + if self.label == "target": + ref = torch.sigmoid((flat_t - tau.unsqueeze(-1)) / s.unsqueeze(-1)) + else: + ref = mask + p = torch.sigmoid((pred_f - tau.unsqueeze(-1)) / s.unsqueeze(-1)) + if self.weighting == "sauna": + with torch.no_grad(): + w = sauna_weight_map(fg.reshape(b * c, *spatial), self.spacing).reshape(b * c, -1) + inter = (w * p * ref).sum(-1) + denom = (w * p * p).sum(-1) + (w * ref * ref).sum(-1) + self.eps + else: + inter = (p * ref).sum(-1) + denom = (p * p).sum(-1) + (ref * ref).sum(-1) + self.eps + dice = 1.0 - 2.0 * inter / denom + if self.topology == "cldice": + dice = (1.0 - self.cldice_alpha) * dice + self.cldice_alpha * ( + 1.0 - self._cldice(p, ref, (b * c, 1, *spatial)) + ) + return dice.reshape(b, c), valid.reshape(b, c) + + def _cldice(self, p: Tensor, ref: Tensor, shape: tuple[int, ...]) -> Tensor: + """Soft clDice per row; gradient flows through ``p`` and its skeleton only. + + Parameters + ---------- + p : Tensor + Soft prediction, shape ``(R, N)``. + ref : Tensor + Reference, shape ``(R, N)``. + shape : tuple of int + ``(R, 1, *spatial)`` to restore the spatial layout. + + Returns + ------- + Tensor + clDice per row, shape ``(R,)``. + """ + skel_p = soft_skeleton(p.reshape(shape), self.cldice_iters).reshape(p.shape) + with torch.no_grad(): + skel_r = soft_skeleton(ref.reshape(shape), self.cldice_iters).reshape(ref.shape) + # eps on both sides (clDice's "smooth"): an empty skeleton reads as perfect, not 0/0. + t_prec = ((skel_p * ref).sum(-1) + self.eps) / (skel_p.sum(-1) + self.eps) + t_sens = ((skel_r * p).sum(-1) + self.eps) / (skel_r.sum(-1) + self.eps) + return 2.0 * t_prec * t_sens / (t_prec + t_sens + self.eps) + + def forward( + self, + pred: Tensor, + target: Tensor, + fg_mask: Tensor, + return_components: bool = False, + ) -> Tensor | tuple[Tensor, dict[str, Tensor]]: + """Compute the mean Dice loss over valid (sample, channel) patches. + + Parameters + ---------- + pred : Tensor + Prediction, shape ``(B, C, *spatial)``. + target : Tensor + Ground truth, same shape. + fg_mask : Tensor + Foreground mask, same shape; binarized at 0.5. + return_components : bool + When ``True``, also return ``{"dice": loss, "n_valid": count}``. + + Returns + ------- + Tensor or tuple of (Tensor, dict of str to Tensor) + Scalar float32 loss, optionally with its components. + """ + dice, valid = self.per_channel(pred, target, fg_mask) + n_valid = valid.sum() + # Invalid entries are finite, so multiplying by the mask zeroes them while + # keeping the graph: with no valid patch this is a connected 0 / 1, and no + # host sync is needed to branch on n_valid. + loss = (dice * valid).sum() / n_valid.clamp(min=1) + if return_components: + return loss, {"dice": loss, "n_valid": n_valid.float()} + return loss diff --git a/packages/viscy-utils/src/viscy_utils/losses/seg_aux_maps.py b/packages/viscy-utils/src/viscy_utils/losses/seg_aux_maps.py new file mode 100644 index 000000000..f2bebb88c --- /dev/null +++ b/packages/viscy-utils/src/viscy_utils/losses/seg_aux_maps.py @@ -0,0 +1,363 @@ +"""Mask-derived maps for :class:`~viscy_utils.losses.SegAuxDice`. + +- :func:`squared_edt`: exact, separable, anisotropic squared Euclidean distance + transform in torch (no cupy/cucim needed), on CPU or GPU. +- :func:`sauna_weight_map`: the SAUNA combined boundary/thickness uncertainty + map ("h" variant) of a binary mask, as a per-voxel Dice weight. +- :func:`soft_skeleton`: the clDice soft skeleton. + +SAUNA: Oulu-IMEDS/SAUNA, ``mlpipeline/utils/generate_uncertainty_masks.py`` +(``extract_boundary_uncertainty_map``, ``extract_thickness_uncertainty_map`` +with ``target_c_label="h"``, ``extract_combined_uncertainty_map``). +clDice: jocpae/clDice, ``soft_skeleton.py``. +""" + +import math +from collections.abc import Sequence + +import torch +import torch.nn.functional as F +from torch import Tensor +from torch.utils.checkpoint import checkpoint + +__all__ = ["box_max", "sauna_weight_map", "soft_skeleton", "squared_edt"] + +# Elements in one (lines, L, L) block of the brute-force EDT pass: 2**26 float32 = 256 MiB. +_EDT_BLOCK_ELEMENTS = 2**26 + + +def _nearest_seed_1d_squared(seeds: Tensor, spacing: float, dim: int) -> Tensor: + """Squared distance to the nearest seed along ``dim``, by two scans. + + Parameters + ---------- + seeds : Tensor + Boolean, any shape. + spacing : float + Physical size of one step along ``dim``. + dim : int + Axis to scan. + + Returns + ------- + Tensor + Float32 squared distance, shape of ``seeds``; ``inf`` on lines with no seed. + """ + shape = [1] * seeds.ndim + shape[dim] = seeds.shape[dim] + idx = torch.arange(seeds.shape[dim], device=seeds.device, dtype=torch.float32).reshape(shape) + left = torch.where(seeds, idx, float("-inf")).cummax(dim=dim).values + right = torch.where(seeds, idx, float("inf")).flip(dim).cummin(dim=dim).values.flip(dim) + dist = torch.minimum(idx - left, right - idx) * spacing + return dist * dist + + +def _lower_envelope_1d(f: Tensor, spacing: float, band: int | None) -> Tensor: + """``out[x] = min_y f[y] + (spacing * (x - y))**2`` along the last dim, brute force. + + Parameters + ---------- + f : Tensor + Float32, shape ``(M, L)``; may hold ``inf``. + spacing : float + Physical size of one step along the last dim. + band : int or None + Only consider ``|x - y| <= band``; ``None`` searches the whole line. + + Returns + ------- + Tensor + Shape ``(M, L)``. + """ + m, length = f.shape + out = torch.empty_like(f) + if band is None or 2 * band + 1 >= length: + pos = torch.arange(length, device=f.device, dtype=torch.float32) * spacing + offsets = (pos[:, None] - pos[None, :]) ** 2 # (x, y) + rows = max(1, _EDT_BLOCK_ELEMENTS // (length * length)) + for start in range(0, m, rows): + out[start : start + rows] = (f[start : start + rows, None, :] + offsets).amin(dim=-1) + return out + steps = torch.arange(-band, band + 1, device=f.device, dtype=torch.float32) * spacing + offsets = steps * steps # (2 * band + 1,) + rows = max(1, _EDT_BLOCK_ELEMENTS // (length * (2 * band + 1))) + for start in range(0, m, rows): + padded = F.pad(f[start : start + rows], (band, band), value=float("inf")) + out[start : start + rows] = (padded.unfold(-1, 2 * band + 1, 1) + offsets).amin(dim=-1) + return out + + +def _along_axis(x: Tensor, axis: int, fn) -> Tensor: + """Apply a ``(M, L) -> (M, L)`` function along ``axis`` of ``x``.""" + moved = x.movedim(axis, -1) + shape = moved.shape + return fn(moved.reshape(-1, shape[-1])).reshape(shape).movedim(-1, axis) + + +def squared_edt(seeds: Tensor, spacing: Sequence[float], max_distance: float | None = None) -> Tensor: + """Exact squared Euclidean distance from every voxel to the nearest seed. + + Separable (Saito-Toriwaki): an exact two-scan 1D pass along the largest + axis, then a brute-force lower-envelope pass along each other axis, blocked + to bound memory. Size-1 axes are skipped. Matches + ``scipy.ndimage.distance_transform_edt(~seeds, sampling=spacing) ** 2``. + + With ``max_distance`` the result is ``min(d**2, max_distance**2)``, still + exact: every value is capped at ``max_distance**2`` before each pass, so no + minimizer that matters lies further than ``max_distance`` along any line, + and each pass only searches that band. + + Parameters + ---------- + seeds : Tensor + Boolean, shape ``(N, *spatial)``; each of the ``N`` rows is independent. + spacing : Sequence[float] + Physical voxel size per spatial dim. + max_distance : float or None + Cap on the returned distance; ``None`` for none. + + Returns + ------- + Tensor + Float32 squared distances, shape of ``seeds``; 0 on seeds and, without + a cap, ``inf`` everywhere in a row without any seed. + """ + spatial = seeds.shape[1:] + if len(spacing) != len(spatial): + raise ValueError(f"spacing has {len(spacing)} entries for {len(spatial)} spatial dims") + cap2 = None if max_distance is None else float(max_distance) ** 2 + axes = [a for a in range(len(spatial)) if spatial[a] > 1] + first = max(axes, key=lambda a: spatial[a]) if axes else None + if first is None: + d2 = torch.where(seeds, 0.0, float("inf")).float() + else: + d2 = _nearest_seed_1d_squared(seeds, float(spacing[first]), first + 1) + if cap2 is not None: + d2 = d2.clamp(max=cap2) + for a in axes: + if a != first: + band = None if max_distance is None else math.ceil(max_distance / spacing[a]) + d2 = _along_axis(d2, a + 1, lambda f, a=a, band=band: _lower_envelope_1d(f, float(spacing[a]), band)) + # Passes keep values exact where the true distance is within the cap and >= the cap elsewhere. + return d2 if cap2 is None else d2.clamp(max=cap2) + + +def _axis_distance_bound(seeds: Tensor, spacing: Sequence[float]) -> Tensor: + """Per-voxel upper bound on the distance to the nearest seed. + + The smallest distance to a seed along any single axis: 1D scans only, cheap. + """ + bound2 = torch.full(seeds.shape, float("inf"), device=seeds.device) + for a in range(seeds.ndim - 1): + if seeds.shape[a + 1] > 1: + d2 = _nearest_seed_1d_squared(seeds, float(spacing[a]), a + 1) + bound2 = torch.minimum(bound2, d2) + return bound2.sqrt() + + +def _window_max(x: Tensor, k: int, dim: int) -> Tensor: + """Max over a centred odd window ``k`` along ``dim``, ``-inf`` beyond the edges. + + Doubling (sparse-table) maxima: ``log2(k)`` elementwise passes, no pooling + indices. Equal to ``max_pool1d(k, stride=1, padding=k // 2)`` along ``dim``. + """ + length = x.shape[dim] + pad = [0, 0] * (x.ndim - 1 - dim) + [k // 2, k // 2] + m = F.pad(x, pad, value=float("-inf")) # length + k - 1 along dim + span = 1 + while 2 * span <= k: + n = m.shape[dim] - span + m = torch.maximum(m.narrow(dim, 0, n), m.narrow(dim, span, n)) + span *= 2 + # m[j] = max over [j, j + span); combine two overlapping spans to cover k. + return torch.maximum(m.narrow(dim, 0, length), m.narrow(dim, k - span, length)) + + +def box_max(x: Tensor, windows: Sequence[int]) -> Tensor: + """Box max filter, stride 1, same size, as sequential 1D window maxima. + + Parameters + ---------- + x : Tensor + Float, shape ``(N, *spatial)``. + windows : Sequence[int] + Odd window length per spatial dim; 1 leaves that dim alone. + + Returns + ------- + Tensor + Filtered tensor, shape of ``x``. + """ + for a, k in enumerate(windows): + length = x.shape[a + 1] + # A window wider than 2L - 1 already covers the whole line from every voxel. + k = min(k, 2 * length - 1) + if k <= 1: + continue + x = _window_max(x, k, a + 1) + return x + + +def _odd_windows(radius: float, spacing: Sequence[float]) -> list[int]: + """SAUNA's ``do_max_pooling`` kernel per axis: ``ceil(radius)`` voxels, made odd.""" + windows = [] + for s in spacing: + k = math.ceil(radius / s) + windows.append(k + 1 if k % 2 == 0 else k) + return windows + + +def sauna_weight_map(mask: Tensor, spacing: Sequence[float]) -> Tensor: + """Per-voxel Dice weight ``|y~|`` from SAUNA's combined "h" uncertainty map. + + With ``fg_dist`` the distance of each voxel to the nearest background voxel + and ``bg_dist`` to the nearest foreground voxel (physical units), + ``fg_max = max(fg_dist)``: + + - boundary map ``gt_b = (fg_dist - min(bg_dist, fg_max)) / fg_max``; + - thickness map ``gt_t``: box max of ``fg_dist`` over a window of + ``fg_max`` per axis, over ``fg_max`` on foreground; box max of + ``min(bg_dist, fg_max)`` (window from that map's own maximum, as SAUNA + does) over ``fg_max``, clipped to [0, 1], on background; + - ``y~ = clip(gt_b + (1 - gt_t), -1, 1)`` on foreground and + ``clip(gt_b - (1 - gt_t), -1, 1)`` on background. + + Rows with no foreground or no background get weight 1. + + Using ``|y~|`` as a Dice weight is this package's adaptation of the map, + not part of SAUNA, which regresses a tanh head toward the signed ``y~`` + (see :func:`sauna_signed_map`). + + Parameters + ---------- + mask : Tensor + Boolean foreground, shape ``(N, *spatial)``. + spacing : Sequence[float] + Physical voxel size per spatial dim. + + Returns + ------- + Tensor + Float32 weights in [0, 1], shape of ``mask``. + """ + y = sauna_signed_map(mask, spacing) + ok = _has_fg_and_bg(mask) + return torch.where(_expand_rows(ok, mask), y.abs(), torch.ones_like(y)) + + +def _has_fg_and_bg(mask: Tensor) -> Tensor: + flat = mask.reshape(mask.shape[0], -1) + return flat.any(-1) & (~flat).any(-1) + + +def _expand_rows(row_values: Tensor, like: Tensor) -> Tensor: + return row_values.reshape(-1, *([1] * (like.ndim - 1))) + + +def sauna_signed_map(mask: Tensor, spacing: Sequence[float]) -> Tensor: + """SAUNA's signed combined map ``y~`` in [-1, 1] (see :func:`sauna_weight_map`). + + Parameters + ---------- + mask : Tensor + Boolean foreground, shape ``(N, *spatial)``. + spacing : Sequence[float] + Physical voxel size per spatial dim. + + Returns + ------- + Tensor + Float32, shape of ``mask``; 0 in rows with no foreground or no background. + """ + n = mask.shape[0] + ok_rows = _has_fg_and_bg(mask) + ok = _expand_rows(ok_rows, mask) + if not ok_rows.any(): + return torch.zeros(mask.shape, device=mask.device) + # Both EDTs only need to be exact up to the largest foreground distance: + # fg_dist never exceeds it and bg_dist is clipped to it. The fg cap is a + # cheap upper bound on fg_max, taken over rows that have background. + fg_bound = _axis_distance_bound(~mask, spacing).reshape(n, -1).amax(-1)[ok_rows].amax().item() + fg_cap = fg_bound if math.isfinite(fg_bound) else None + # Rows lacking fg or bg have inf / capped distances; zero them so everything stays finite. + fg_dist = torch.where(ok, squared_edt(~mask, spacing, max_distance=fg_cap).sqrt(), 0.0) + fg_max = fg_dist.reshape(n, -1).amax(-1) + fg_max = torch.where(fg_max > 0, fg_max, torch.ones_like(fg_max)) + bg_dist = torch.where(ok, squared_edt(mask, spacing, max_distance=fg_max.max().item()).sqrt(), 0.0) + bg_clip = torch.minimum(bg_dist, _expand_rows(fg_max, mask)) + bg_max = bg_clip.reshape(n, -1).amax(-1) + # Windows differ per row (each patch has its own fg_max): filter row by row. + fg_pool = torch.empty_like(fg_dist) + bg_pool = torch.empty_like(bg_clip) + for i, (fg_r, bg_r) in enumerate(zip(fg_max.tolist(), bg_max.tolist())): + fg_pool[i : i + 1] = box_max(fg_dist[i : i + 1], _odd_windows(fg_r, spacing)) + bg_pool[i : i + 1] = box_max(bg_clip[i : i + 1], _odd_windows(bg_r, spacing)) + fg_max_b = _expand_rows(fg_max, mask) + gt_b = (fg_dist - bg_clip) / fg_max_b + gt_t = torch.where(mask, fg_pool / fg_max_b, (bg_pool / fg_max_b).clamp(0.0, 1.0)) + y = torch.where(mask, gt_b + (1.0 - gt_t), gt_b - (1.0 - gt_t)).clamp(-1.0, 1.0) + return torch.where(ok, y, 0.0) + + +def _max_pool(x: Tensor, kernel: tuple[int, ...]) -> Tensor: + """Stride-1, same-size max-pool of ``(N, 1, *spatial)`` over 1-3 spatial dims.""" + pool = {1: F.max_pool1d, 2: F.max_pool2d, 3: F.max_pool3d}[len(kernel)] + return pool(x, kernel, stride=1, padding=tuple(k // 2 for k in kernel)) + + +def _soft_erode(x: Tensor) -> Tensor: + """ClDice ``soft_erode``: min of the 1D 3-voxel min-pools along each axis (the cross).""" + nd = x.ndim - 2 + out = None + for a in range(nd): + kernel = tuple(3 if i == a else 1 for i in range(nd)) + eroded = -_max_pool(-x, kernel) + out = eroded if out is None else torch.minimum(out, eroded) + return out + + +def _soft_dilate(x: Tensor) -> Tensor: + """ClDice ``soft_dilate``: 3^n box max-pool.""" + return _max_pool(x, (3,) * (x.ndim - 2)) + + +def _soft_open(x: Tensor) -> Tensor: + return _soft_dilate(_soft_erode(x)) + + +def _skeleton_step(img: Tensor, skel: Tensor) -> tuple[Tensor, Tensor]: + img = _soft_erode(img) + delta = F.relu(img - _soft_open(img)) + return img, skel + F.relu(delta - skel * delta) + + +def soft_skeleton(img: Tensor, iters: int) -> Tensor: + """ClDice soft skeleton (jocpae/clDice ``soft_skel``), in 1-3 spatial dims. + + When a gradient is needed every iteration is activation-checkpointed: each + one holds ~a dozen full-size intermediates (pool values and int64 indices), + which for a + ``(8, 1, 32, 384, 384)`` batch and 10 iterations exceeded a 48 GB GPU. + Checkpointing keeps two tensors per iteration and recomputes the rest in + backward; values and gradients are unchanged by it. + + Parameters + ---------- + img : Tensor + Soft mask in [0, 1], shape ``(N, 1, *spatial)``. + iters : int + Erosion iterations; should exceed the largest foreground radius in voxels. + + Returns + ------- + Tensor + Soft skeleton, shape of ``img``. + """ + checkpointed = img.requires_grad and torch.is_grad_enabled() + skel = F.relu(img - _soft_open(img)) + for _ in range(iters): + if checkpointed: + img, skel = checkpoint(_skeleton_step, img, skel, use_reentrant=False) + else: + img, skel = _skeleton_step(img, skel) + return skel diff --git a/packages/viscy-utils/src/viscy_utils/losses/seg_consistency.py b/packages/viscy-utils/src/viscy_utils/losses/seg_consistency.py new file mode 100644 index 000000000..58e658c10 --- /dev/null +++ b/packages/viscy-utils/src/viscy_utils/losses/seg_consistency.py @@ -0,0 +1,230 @@ +"""Segmenter-consistency loss for virtual staining (ControlNet++ style). + +A frozen, differentiable segmenter ``S`` is run on the prediction and on the target, +and their dense, pre-threshold outputs are matched: + +``L = mean_px ( mean_c (flow_pred - flow_target)**2 + KL(q_target || q_pred) )`` + +- ``flow`` are the segmenter's two flow channels and ``q = sigmoid(cellprob_logit)``. +- The probability term is the binary KL, i.e. BCE-with-logits against the target's + soft probability minus that probability's entropy: the gradient is BCE's, and the + loss is exactly 0 at ``pred == target``. The loss equals the G4 gate's + ``D_total = D_flow + D_prob``. +- ``S(target)`` is computed under ``no_grad`` on the same augmented crop, every step; + ``S(pred)`` carries the gradient into the prediction. ``S`` itself gets none. + +The segmenter sees 2-D images. A crop ``(B, 1, Z, Y, X)`` is reduced to the +``2 * slab_halfwidth + 1``-plane max projection around its nuclear-area focus plane: +the plane of largest Otsu foreground fraction of the target crop, with the outer 15% +of planes excluded and the crop center as the fallback, as +``dynacell.evaluation.focus.nucleus_area_plane`` picks the plane the eval segments +(slab halfwidth 1 there). The same planes are projected from the prediction. One +segmenter pass per sample is the cheapest geometry the G4 bench measured, and it is +the geometry the eval scores. ``Z = 1`` crops (2-D models) give their single plane. +The projection is then center-cropped to ``crop_size``: 240 px, which Cellpose pads to +exactly one 256-px tile (a 256-px crop pads to 272 and costs 2 x 2 tiles). + +The segmenter is held outside the module tree, so its weights never enter a +``state_dict`` (checkpoints stay the backbone's, and ``ckpt_path`` loads of baseline +checkpoints stay strict) and the module's ``train()`` cannot switch it out of eval +mode. It follows the prediction's device on first use. It runs with autocast +disabled, in its own dtype, as stock inference does. +""" + +import torch +import torch.nn.functional as F +from torch import Tensor, nn + +__all__ = ["SegConsistencyLoss", "nucleus_area_planes"] + +_GUARD_FRAC = 0.15 +_MIN_FOREGROUND = 0.01 +_OTSU_BINS = 256 + + +def _otsu_thresholds(flat: Tensor) -> Tensor: + """Per-row Otsu threshold, as ``skimage.filters.threshold_otsu`` (256 bins over each row's range). + + Parameters + ---------- + flat : Tensor + Values, shape ``(R, N)``. A constant row gets a meaningless threshold. + + Returns + ------- + Tensor + Threshold per row, shape ``(R,)``: the center of the bin maximizing the + between-class variance. + """ + lo = flat.amin(1, keepdim=True) + width = ((flat.amax(1, keepdim=True) - lo) / _OTSU_BINS).clamp_min(torch.finfo(flat.dtype).tiny) + idx = ((flat - lo) / width).floor().long().clamp(0, _OTSU_BINS - 1) + # scatter_add_ rather than bincount: bincount sizes its output on the host (a sync). + hist = torch.zeros(flat.shape[0], _OTSU_BINS, device=flat.device, dtype=torch.long) + hist = hist.scatter_add_(1, idx, torch.ones((), device=flat.device, dtype=torch.long).expand_as(idx)).double() + centers = lo.double() + width.double() * (torch.arange(_OTSU_BINS, device=flat.device) + 0.5) + w1 = hist.cumsum(1) + w2 = hist.flip(1).cumsum(1).flip(1) + m1 = (hist * centers).cumsum(1) / w1 + m2 = (hist * centers).flip(1).cumsum(1).flip(1) / w2 + var = w1[:, :-1] * w2[:, 1:] * (m1[:, :-1] - m2[:, 1:]) ** 2 + return centers.gather(1, var.argmax(1, keepdim=True)).squeeze(1).to(flat.dtype) + + +@torch.no_grad() +def nucleus_area_planes(vol: Tensor) -> Tensor: + """Return each volume's plane of largest nuclear foreground area, without a host sync. + + Mirrors ``dynacell.evaluation.focus.nucleus_area_plane``: Otsu over the whole + volume, per-plane foreground fraction, argmax inside a band that drops the outer + 15% of planes; a constant volume, or one whose band peak is below 1% foreground, + falls back to the center plane. + + Parameters + ---------- + vol : Tensor + Volumes, shape ``(B, Z, Y, X)``. + + Returns + ------- + Tensor + Plane index per volume, shape ``(B,)``, int64. + """ + b, z = vol.shape[:2] + center = torch.full((b,), z // 2, device=vol.device, dtype=torch.long) + flat = vol.reshape(b, -1).float() + # A constant row has no threshold; its NaN/arbitrary one is discarded below. + constant = flat.amax(1) <= flat.amin(1) + thr = _otsu_thresholds(flat) + frac = (flat > thr.unsqueeze(1)).reshape(b, z, -1).float().mean(2) + lo = int(round(_GUARD_FRAC * z)) + hi = max(lo + 1, int(round((1.0 - _GUARD_FRAC) * z))) + band = frac[:, lo:hi] + peak, arg = band.max(1) + use_center = constant | (peak < _MIN_FOREGROUND) + return torch.where(use_center, center, lo + arg) + + +def _slab_mip(vol: Tensor, planes: Tensor, halfwidth: int) -> Tensor: + """Max-project ``(B, Z, Y, X)`` onto the planes ``[p - h, p + h]`` clipped to the stack. + + Clipping repeats an edge plane, which leaves the maximum unchanged, so this equals + ``dynacell.evaluation.focus.slab_mip``. Returns ``(B, Y, X)``. + """ + z = vol.shape[1] + offsets = torch.arange(-halfwidth, halfwidth + 1, device=vol.device) + idx = (planes.unsqueeze(1) + offsets).clamp(0, z - 1) + idx = idx[:, :, None, None].expand(-1, -1, *vol.shape[2:]) + return vol.gather(1, idx).amax(1) + + +def _center_crop(img: Tensor, size: int | None) -> Tensor: + """Center-crop ``(..., Y, X)`` to at most ``size`` per side (no-op when ``None``).""" + if size is None: + return img + y, x = img.shape[-2:] + oy, ox = max(0, (y - size) // 2), max(0, (x - size) // 2) + return img[..., oy : oy + min(y, size), ox : ox + min(x, size)] + + +def _binary_kl(p_logit: Tensor, q_logit: Tensor) -> Tensor: + """Per-element binary KL(q || p) for ``q = sigmoid(q_logit)``; 0 iff the logits match.""" + q = torch.sigmoid(q_logit) + bce = F.binary_cross_entropy_with_logits(p_logit, q, reduction="none") + ent = F.binary_cross_entropy_with_logits(q_logit, q, reduction="none") + return bce - ent + + +class SegConsistencyLoss(nn.Module): + """Match a frozen segmenter's flows and cell probability on prediction and target. + + Parameters + ---------- + segmenter : nn.Module + Frozen differentiable segmenter mapping ``(N, 1, Y, X)`` images in any + positive-affine units to ``{"flow": (N, 2, Y, X), "cellprob_logit": (N, 1, Y, X)}`` + (e.g. ``dynacell.cellpose_dense.CellposeDense``). Put in eval mode with + ``requires_grad=False`` here; never the evaluation segmenter. + slab_halfwidth : int + Half-width of the max projection around a 3-D crop's focus plane. + crop_size : int or None + Center-crop side of the projected image, ``None`` for the whole image. + """ + + def __init__( + self, + segmenter: nn.Module, + slab_halfwidth: int = 1, + crop_size: int | None = 240, + ) -> None: + super().__init__() + if slab_halfwidth < 0: + raise ValueError(f"slab_halfwidth must be >= 0, got {slab_halfwidth}") + if crop_size is not None and crop_size < 1: + raise ValueError(f"crop_size must be >= 1 or None, got {crop_size}") + segmenter.eval() + segmenter.requires_grad_(False) + # Bypass nn.Module.__setattr__ so the segmenter is not a registered submodule. + self.__dict__["segmenter"] = segmenter + self.slab_halfwidth = slab_halfwidth + self.crop_size = crop_size + + def project(self, pred: Tensor, target: Tensor) -> tuple[Tensor, Tensor]: + """Reduce ``pred`` and ``target`` to the 2-D images the segmenter scores. + + Parameters + ---------- + pred, target : Tensor + Shape ``(B, 1, Z, Y, X)``. + + Returns + ------- + tuple of (Tensor, Tensor) + Projected ``pred`` and ``target``, each ``(B, 1, Y', X')``. + """ + if pred.shape != target.shape: + raise ValueError(f"pred and target must share a shape; got {tuple(pred.shape)}, {tuple(target.shape)}") + if pred.ndim != 5 or pred.shape[1] != 1: + raise ValueError(f"expected (B, 1, Z, Y, X), got {tuple(pred.shape)}") + planes = nucleus_area_planes(target[:, 0].detach()) + pred = _slab_mip(pred[:, 0], planes, self.slab_halfwidth).unsqueeze(1) + target = _slab_mip(target[:, 0], planes, self.slab_halfwidth).unsqueeze(1) + return _center_crop(pred, self.crop_size), _center_crop(target, self.crop_size) + + def _segment(self, img: Tensor) -> dict[str, Tensor]: + seg = self.segmenter + if next(seg.parameters()).device != img.device: + seg.to(img.device) + with torch.autocast(device_type=img.device.type, enabled=False): + return seg(img.to(torch.promote_types(img.dtype, torch.float32))) + + def forward( + self, pred: Tensor, target: Tensor, return_components: bool = False + ) -> Tensor | tuple[Tensor, dict[str, Tensor]]: + """Compute the consistency loss. + + Parameters + ---------- + pred : Tensor + Prediction, ``(B, 1, Z, Y, X)``. + target : Tensor + Ground truth, same shape. + return_components : bool + Also return ``{"segcons", "segcons_flow", "segcons_prob"}``. + + Returns + ------- + Tensor or tuple of (Tensor, dict of str to Tensor) + Scalar float32 loss, optionally with its flow and probability terms. + """ + pred2d, target2d = self.project(pred, target.detach()) + with torch.no_grad(): + ref = self._segment(target2d) + out = self._segment(pred2d) + flow_sq = ((out["flow"] - ref["flow"]) ** 2).mean(1) + kl = _binary_kl(out["cellprob_logit"], ref["cellprob_logit"])[:, 0] + flow_term, prob_term = flow_sq.mean(), kl.mean() + loss = flow_term + prob_term + if return_components: + return loss, {"segcons": loss, "segcons_flow": flow_term, "segcons_prob": prob_term} + return loss diff --git a/packages/viscy-utils/src/viscy_utils/losses/spotlight.py b/packages/viscy-utils/src/viscy_utils/losses/spotlight.py index 1d74a52a3..c059255b1 100644 --- a/packages/viscy-utils/src/viscy_utils/losses/spotlight.py +++ b/packages/viscy-utils/src/viscy_utils/losses/spotlight.py @@ -158,7 +158,13 @@ def __init__( self.fg_threshold = fg_threshold self._warned_no_real_mask = False - def forward(self, pred: Tensor, target: Tensor, fg_mask: Tensor | None = None) -> Tensor: + def forward( + self, + pred: Tensor, + target: Tensor, + fg_mask: Tensor | None = None, + return_components: bool = False, + ) -> Tensor | tuple[Tensor, dict[str, Tensor]]: """Compute the Spotlight loss. Loss is computed per (batch, channel) pair and averaged. Channels @@ -176,11 +182,18 @@ def forward(self, pred: Tensor, target: Tensor, fg_mask: Tensor | None = None) - When provided, used directly instead of computing from target. When None (default), mask is computed at runtime using ``fg_threshold`` or Otsu thresholding. + return_components : bool + When ``True``, also return the two terms that make up the loss. + The terms are the ones actually summed here, so logging them is a + faithful record of the objective being optimized — re-deriving them + in the caller would silently fork from it. Returns ------- - Tensor - Scalar loss value. + Tensor or tuple of (Tensor, dict of str to Tensor) + Scalar loss value, or ``(loss, {"masked_mse": ..., "dice": ...})`` + when ``return_components`` is ``True``. The components are the + unweighted terms, before ``lambda_mse`` is applied. """ # Foreground mask: precomputed > fixed threshold > per-(sample, channel) Otsu if fg_mask is not None: @@ -222,4 +235,7 @@ def forward(self, pred: Tensor, target: Tensor, fg_mask: Tensor | None = None) - self._warned_no_real_mask = True dice = pred.new_tensor(0.0) - return self.lambda_mse * masked_mse + (1 - self.lambda_mse) * dice + total = self.lambda_mse * masked_mse + (1 - self.lambda_mse) * dice + if return_components: + return total, {"masked_mse": masked_mse, "dice": dice} + return total diff --git a/packages/viscy-utils/src/viscy_utils/meta_utils.py b/packages/viscy-utils/src/viscy_utils/meta_utils.py index 2dbe70e92..d7f924acb 100644 --- a/packages/viscy-utils/src/viscy_utils/meta_utils.py +++ b/packages/viscy-utils/src/viscy_utils/meta_utils.py @@ -1,14 +1,33 @@ """Normalization metadata generation for OME-Zarr datasets.""" +import logging + import iohub.ngff as ngff import numpy as np from iohub.core.config import TensorStoreConfig from scipy.ndimage import median_filter -from skimage.filters import threshold_otsu +from skimage.exposure import equalize_adapthist +from skimage.filters import gaussian, threshold_otsu from tqdm import tqdm from viscy_utils.mp_utils import get_val_stats +try: + # cubic is how the rest of the codebase reaches the GPU (see + # dynacell.evaluation.metrics): its proxies dispatch on the INPUT ARRAY's + # device, so the same call runs cupyx on a cupy array and scipy on a numpy + # one. Optional here because viscy-utils does not depend on cubic; it ships + # in dynacell's `eval` and `preprocess` extras. + from cubic.cuda import ascupy, asnumpy + from cubic.scipy import ndimage as _cubic_ndimage + from cubic.skimage import exposure as _cubic_exposure + from cubic.skimage import filters as _cubic_filters +except ImportError: # pragma: no cover - exercised by the CPU-only environments + ascupy = None + +_logger = logging.getLogger(__name__) +_BACKEND: dict[str, str] = {} + def write_meta_field(position, metadata, field_name, subfield_name): """Write metadata to position's .zattrs. @@ -58,9 +77,261 @@ def _grid_sample(position, grid_spacing, channel_index): return position["0"].native[:, channel_index, :, ::grid_spacing, ::grid_spacing].read().result() -def generate_normalization_metadata( - zarr_dir, num_workers=4, channel_ids=-1, grid_spacing=32, compute_otsu=False, otsu_grid_spacing=8 -): +def _select_backend(op: str) -> str: + """Pick the array backend for ``op``, logging the choice once per operation.""" + # torch is imported lazily: it costs seconds, and dynacell.evaluation.focus + # imports this module only for write_meta_field. + import torch + + if ascupy is None: + chosen = "scipy" + # Gate the upload on an actual device, as metrics.py does: cubic imports + # cleanly without cupy/cucim and ascupy would raise "GPU requested but not + # available". Falling through to numpy keeps cubic's own CPU path. + elif not torch.cuda.is_available(): + chosen = "cubic-cpu" + else: + chosen = "cubic-gpu" + if _BACKEND.get(op) != chosen: + _BACKEND[op] = chosen + _logger.info("%s: using the %s backend", op, chosen) + return chosen + + +def smooth_median(array, size): + """Median-filter ``array``, on the GPU when cubic and a device are available. + + The mask pass in :func:`generate_fg_masks` spends essentially all of its + time here. Measured on a (44, 624, 924) float32 volume with the (1, 3, 3) + footprint cell.zarr actually uses, on an A40: + + scipy.ndimage.median_filter 5.44 s + cubic.scipy.ndimage.median_filter via ascupy 0.03 s + + Routing through ``cubic.scipy.ndimage`` rather than calling cupyx directly + is the codebase convention (``dynacell.evaluation.metrics`` does the same): + the proxy dispatches on the input array's device, so uploading with + ``ascupy`` selects the GPU implementation and passing plain numpy selects + SciPy, with one call site either way. + + A median filter selects an existing element rather than computing a new + one, so there is no floating-point reassociation and the backends agree + exactly -- pinned by a test on both footprints used here. + + Parameters + ---------- + array : numpy.ndarray + Input array. + size : tuple of int + Filter footprint, one entry per axis of ``array``. + + Returns + ------- + numpy.ndarray + Filtered array, on the host, with the input's dtype and shape. + """ + backend = _select_backend("median filter") + if backend == "scipy": + return median_filter(array, size=size) + if backend == "cubic-cpu": + return asnumpy(_cubic_ndimage.median_filter(array, size=size)) + return asnumpy(_cubic_ndimage.median_filter(ascupy(array), size=size)) + + +def smooth_gaussian(array, sigma): + """Gaussian-blur ``array``, on the GPU when cubic and a device are available. + + Used only to pick the Otsu threshold, never to build the mask: blurring + this hard makes the intensity histogram separable but smears object + boundaries, so the threshold it yields is applied to the median-filtered + image instead. Measured against binarized cpdino segmentations on the 100 + ``dataset_v4/test_cropped`` positions (nucleus, mean over positions): + + threshold from median 5^3 alone, applied to median 5^3 IoU 0.7292 + threshold from median 5^3 + this blur, applied to median IoU 0.7647 + same threshold, applied to the blurred image instead IoU 0.7288 + + Unlike :func:`smooth_median`, a Gaussian computes new values by weighted + summation, so GPU and CPU results are close but not bit-identical; the + tests assert a tolerance rather than equality. + + Parameters + ---------- + array : numpy.ndarray + Input array. + sigma : float or tuple of float + Standard deviation, scalar for all axes or one entry per axis. + + Returns + ------- + numpy.ndarray + Blurred array, on the host, with the input's shape. + """ + backend = _select_backend("gaussian filter") + if backend == "scipy": + return gaussian(array, sigma=sigma, preserve_range=True) + if backend == "cubic-cpu": + return asnumpy(_cubic_filters.gaussian(array, sigma=sigma, preserve_range=True)) + return asnumpy(_cubic_filters.gaussian(ascupy(array), sigma=sigma, preserve_range=True)) + + +_CLAHE_TILE = (50, 120, 120) +_CLAHE_CLIP_LIMIT = 0.015 +_CLAHE_PERCENTILE = 0.1 + + +def equalize_clahe(array, tile_shape=_CLAHE_TILE, clip_limit=_CLAHE_CLIP_LIMIT, nbins=256): + """Contrast-limited adaptive histogram equalization, on the GPU when available. + + ``equalize_adapthist`` takes a tile SIZE, but the number of tiles is what has + to adapt: the established config computes ``shape // (50, 120, 120)`` so tiles + stay roughly constant in pixels across differently-shaped stacks. That floors + to zero in Z for a 44-plane stack, so it is clamped to at least one tile. + + The input is percentile-clipped to ``[0, 1]`` first because + ``equalize_adapthist`` rejects float input outside ``[-1, 1]``. Clipping at + ``_CLAHE_PERCENTILE`` rather than min-max keeps one bright speck from + compressing the rest of the volume toward zero. + + Parameters + ---------- + array : numpy.ndarray + Input ``(Z, Y, X)`` volume. + tile_shape : tuple of int + Target tile size in voxels; the tile COUNT is derived from it. + clip_limit : float + Contrast-limiting threshold. + nbins : int + Histogram bins. + + Returns + ------- + numpy.ndarray + Equalized volume in ``[0, 1]`` -- a different intensity space from the + input, so a threshold taken here is not comparable to a raw one. + """ + lo, hi = np.percentile(array, (_CLAHE_PERCENTILE, 100 - _CLAHE_PERCENTILE)) + if hi <= lo: + # No contrast between the clip percentiles (e.g. a blank timepoint with a + # few hot pixels): nothing to equalize, and the rescale would divide by 0. + return np.zeros(array.shape, dtype=np.float32) + scaled = np.clip((array - lo) / (hi - lo), 0.0, 1.0).astype(np.float32) + n_tiles = tuple(max(int(dim // tile), 1) for dim, tile in zip(array.shape, tile_shape)) + kernel = tuple(int(dim // n) for dim, n in zip(array.shape, n_tiles)) + backend = _select_backend("clahe") + if backend == "scipy": + return equalize_adapthist(scaled, kernel_size=kernel, clip_limit=clip_limit, nbins=nbins) + device = scaled if backend == "cubic-cpu" else ascupy(scaled) + return asnumpy(_cubic_exposure.equalize_adapthist(device, kernel_size=kernel, clip_limit=clip_limit, nbins=nbins)) + + +def foreground_mask_from_volume(volume_zyx): + """Binary foreground mask for one ``(Z, Y, X)`` volume. + + The binarization path of the original Spotlight runs + (``scripts/blastoid/preprocess.py::read_preprocess`` in the fnet_astr_vpa + repo): CLAHE, then ``otsu_binarize(filter_size=5)`` -- a 5x5x5 median cube and + ``threshold_otsu``, with no Gaussian, since CLAHE already separates the + histogram. Foreground extraction is a preprocessing choice and not part of + the Spotlight loss, so this is free to differ from :func:`otsu_threshold_from_volume`, + which still supplies the raw-space ``otsu_threshold`` that ``NormalizeSampled`` + subtracts. + + Measured against binarized cpdino segmentations, nucleus channel, on each + sample's own ``nucleus_area`` focus plane (mean IoU / recall): + + iPSC A549 + median 5^3 + gaussian, raw space 0.7637 / 0.7851 0.7713 / 0.7916 + this recipe 0.8262 / 0.8539 0.8898 / 0.9654 + + A Gaussian on top of CLAHE was measured and rejected: it helps iPSC (0.8644) + but hurts A549 (0.8807), while no-blur improves both. The repo's cleanup step + (``min_obj_size=100``, ``max_hole_size=25``) was measured as a no-op here + (+0.0003 and +0.0000) and is omitted. + + Parameters + ---------- + volume_zyx : numpy.ndarray + Full-resolution single-channel volume. + + Returns + ------- + numpy.ndarray + Boolean mask of the input's shape. + """ + # A constant volume (e.g. a blank timepoint) has no structure to segment, and + # its percentile range is zero, which CLAHE's rescale would divide by. + if volume_zyx.min() == volume_zyx.max(): + return np.zeros(volume_zyx.shape, dtype=bool) + equalized = smooth_median(equalize_clahe(volume_zyx), size=_OTSU_MEDIAN_SIZE) + flat = equalized.ravel() + if flat.min() == flat.max(): + return np.zeros(volume_zyx.shape, dtype=bool) + return equalized >= float(threshold_otsu(flat, nbins=256)) + + +_OTSU_MEDIAN_SIZE = 5 +_OTSU_BLUR_SIGMA = 5.0 + + +def otsu_threshold_from_volume(volume_tzyx): + """Compute one FOV's Otsu threshold from a full-resolution ``(T, Z, Y, X)`` volume. + + This is the recipe of the original Spotlight implementation + (``norm_threshold`` in the fnet_astr_vpa notebooks, which resolves to + ``cubic``'s ``downscale_and_filter(downscale_factor=1, filter_size=5)`` then + ``get_threshold_otsu``): a 5x5x5 median cube at full resolution, a sigma=5 + Gaussian, then Otsu. Both filters span Z. On the strongly anisotropic iPSC + stacks that matters a lot -- restricting them to the plane scores worse + than the decimated recipe they replace -- so the footprints are kept + isotropic in voxels rather than matched to physical spacing. + + Measured against binarized cpdino segmentations, nucleus channel, scored on + each sample's own ``nucleus_area`` focus plane (mean IoU / recall), on two + datasets with very different voxel geometry -- iPSC ``test_cropped`` (100 + positions, z=0.290 / xy=0.108) and A549 ``dual_nucl_memb_mock`` (36 + position-timepoint samples, z=0.174 / xy=0.1494): + + iPSC A549 + decimate ::8 + median (1, 3, 3), previous 0.7086 / 0.7309 0.7406 / 0.7558 + this recipe, applied to the median volume 0.7647 / 0.7862 0.7713 / 0.7916 + both footprints restricted to XY 0.6967 / 0.7135 -- + + Otsu erodes on both: precision stays at 0.94-0.97 while recall sits near + 0.76, so the masks are shrunken versions of the truth rather than + mislocated ones, and this recipe buys back recall at near-flat precision. + + ``_OTSU_BLUR_SIGMA`` is the published value, deliberately NOT tuned. A + larger blur scores substantially better on both datasets (iPSC 0.8713 at + sigma=30 and still climbing, A549 0.8649 peaking at sigma=20), but the + optimum is dataset-specific and does not follow from voxel spacing -- a + spacing-derived sigma was measured and refuted. Tuning it would also mean + fitting a training-time parameter on the split the models are scored on, + which biases the Spotlight arms specifically, since the baselines never + read ``fg_mask``. Tune it on a held-out non-eval split or not at all. + + The threshold is pooled over every timepoint and Z plane, so a position + gets one scalar per channel. + + Parameters + ---------- + volume_tzyx : numpy.ndarray + Full-resolution ``(T, Z, Y, X)`` volume for a single channel. + + Returns + ------- + float + The Otsu threshold, or the constant value itself for a constant input, + for which Otsu is undefined. + """ + median = smooth_median(volume_tzyx, size=(1,) + (_OTSU_MEDIAN_SIZE,) * 3) + flat = smooth_gaussian(median, sigma=(0,) + (_OTSU_BLUR_SIGMA,) * 3).ravel() + if flat.min() == flat.max(): + return float(flat.min()) + return float(threshold_otsu(flat)) + + +def generate_normalization_metadata(zarr_dir, num_workers=4, channel_ids=-1, grid_spacing=32, compute_otsu=False): """Generate pixel intensity metadata for normalization. Normalization values are recorded in the image-level metadata in the @@ -79,10 +350,6 @@ def generate_normalization_metadata( compute_otsu : bool, optional Whether to compute Otsu thresholds for foreground estimation, by default False. Required for Spotlight loss. - otsu_grid_spacing : int, optional - Grid spacing for Otsu sampling, by default 8. Denser than the - default ``grid_spacing=32`` to capture inter-cell gaps. A median - filter is applied before thresholding to smooth noise. """ with ngff.open_ome_zarr( zarr_dir, @@ -113,16 +380,10 @@ def generate_normalization_metadata( dataset_sample_values.append(samples) fov_stats = get_val_stats(samples) if compute_otsu: - otsu_samples = _grid_sample(pos, otsu_grid_spacing, channel_index) - smoothed = median_filter(otsu_samples, size=(1, 1, 3, 3)) - flat = smoothed.ravel() - # Otsu's method is undefined for constant-valued inputs. - # Use the constant value itself so generate_fg_masks marks - # nothing as foreground (no meaningful structure to supervise). - if flat.min() == flat.max(): - fov_stats["otsu_threshold"] = float(flat.min()) - else: - fov_stats["otsu_threshold"] = float(threshold_otsu(flat)) + # Full resolution: the sigma=5 blur below is defined in source + # pixels, so thresholding a decimated grid would scale it by the + # stride and erase the effect. + fov_stats["otsu_threshold"] = otsu_threshold_from_volume(_grid_sample(pos, 1, channel_index)) fov_statistics = {"fov_statistics": fov_stats} fov_timepoint_statistics = {} for t in range(num_timepoints): @@ -161,15 +422,19 @@ def generate_fg_masks( channel_names, fg_mask_key="fg_mask", ): - """Precompute binary foreground masks from Otsu thresholds. + """Precompute binary foreground masks with the CLAHE, median and Otsu recipe. For each FOV and specified channel, loads the full-resolution image one - timepoint at a time, smooths with the same median filter used for Otsu - thresholding, and writes the binary mask as a zarr array alongside the - image data. + timepoint at a time and binarizes it with + :func:`foreground_mask_from_volume`, writing the result as a zarr array + alongside the image data. - Requires ``generate_normalization_metadata`` with ``compute_otsu=True`` - to have been run first (Otsu thresholds must be stored in zattrs). + Does NOT read ``otsu_threshold``. That value is computed in raw intensity + units for ``NormalizeSampled`` to subtract, whereas this pass thresholds in + CLAHE's equalized ``[0, 1]`` space, so the two are not interchangeable and + the mask is deliberately not derivable from the stored scalar. Running + ``generate_normalization_metadata`` with ``compute_otsu=True`` is still + required for Spotlight, but for normalization rather than for masking. Parameters ---------- @@ -217,11 +482,7 @@ def generate_fg_masks( mask_arr[:, c] = 1 # Compute and write target channel masks per timepoint - for ch_name, ch_idx in zip(channel_names, channel_indices): - norm = pos.zattrs["normalization"][ch_name]["fov_statistics"] - otsu_threshold = norm["otsu_threshold"] - + for ch_idx in channel_indices: for t in range(t_total): data = img_arr[t, ch_idx].astype(np.float32) - smoothed = median_filter(data, size=(1, 3, 3)) - mask_arr[t, ch_idx] = (smoothed >= otsu_threshold).astype(np.uint8) + mask_arr[t, ch_idx] = foreground_mask_from_volume(data).astype(np.uint8) diff --git a/packages/viscy-utils/src/viscy_utils/prediction_metadata.py b/packages/viscy-utils/src/viscy_utils/prediction_metadata.py new file mode 100644 index 000000000..f274a17a6 --- /dev/null +++ b/packages/viscy-utils/src/viscy_utils/prediction_metadata.py @@ -0,0 +1,483 @@ +"""Prediction completion metadata shared by writers and submission tools.""" + +import hashlib +import json +import os +from collections.abc import Iterable, Mapping, Sequence +from pathlib import Path +from typing import Any + +from iohub.ngff import ImageArray, Position + +__all__ = [ + "PREDICTED_Z_PLANES_KEY", + "PREDICTION_COMPLETE_KEY", + "checkpoint_sha256_12", + "canonical_z_planes", + "checkpoint_signature", + "completion_marker", + "mark_complete", + "mark_started", + "marker_identity", + "outruns", + "plane_predict_unfinished", + "predicted_z_planes", + "prediction_complete", + "prediction_run", + "record_z_planes", + "same_marker", + "same_run", + "started_marker", + "tzyx_shape", + "z_planes_sha256_12", +] + +# Position attribute mapping each prediction channel to its marker: the run +# identity from ``prediction_run`` while a run is writing the channel +# (``started_marker``), and that identity plus the source TZYX once every +# window was written (``completion_marker``). A channel with no entry of its +# own was never recorded by any run -- written before markers existed -- so +# nothing can vouch for it, even when other channels of the FOV are marked. +PREDICTION_COMPLETE_KEY = "viscy_prediction_complete" + +# Position attribute mapping each prediction channel to the Z planes a plane-restricted +# run (``HCSDataModule.predict_z_planes``) predicted, one list per timepoint. Every other +# plane holds the array's fill value, so a reader must score only these. Absent for a +# channel predicted on every plane; a full run clears a stale entry. +PREDICTED_Z_PLANES_KEY = "viscy_predicted_z_planes" + +# Marker fields kept for provenance only; two markers that differ solely in +# these still describe the same prediction (a checkpoint may be moved). +_PROVENANCE_KEYS = frozenset({"checkpoint_path"}) + + +def tzyx_shape(array: ImageArray) -> list[int]: + """Return the TZYX extent of a 5D OME-Zarr array. + + Parameters + ---------- + array : ImageArray + Array whose ``(T, C, Z, Y, X)`` shape to project. + + Returns + ------- + list of int + ``[T, Z, Y, X]``, the source identity a completion marker records. + """ + return [array.frames, array.slices, array.height, array.width] + + +def outruns(array: ImageArray, source_shape: list[int]) -> bool: + """Return whether an output array holds more frames or depth slices than its source. + + Arrays only grow and no run writes beyond its source's extent, so the + excess would keep stale voxels under a fresh completion marker. + + Parameters + ---------- + array : ImageArray + Existing output array. + source_shape : list of int + TZYX shape of the source, from :func:`tzyx_shape`. + + Returns + ------- + bool + True when ``array`` exceeds the source in T or Z. + """ + return array.frames > source_shape[0] or array.slices > source_shape[1] + + +_SIGNATURE_FIELDS = ("size", "mtime_ns", "ctime_ns", "ino") + + +def checkpoint_signature(path: str | os.PathLike) -> tuple[int, int, int, int]: + """Return the size, ``st_mtime_ns``, ``st_ctime_ns`` and inode of the file at *path*. + + Overwriting a file or replacing it by rename changes at least one of these: + a copy can preserve size and mtime, but the kernel sets ctime itself and a + rename swaps the inode. Equal signatures therefore mean the same bytes for + the hash memo in :func:`checkpoint_sha256_12` and for the writer's check + that a checkpoint did not change between loading its weights and hashing + it. The one blind spot is a same-size, mtime-preserving overwrite within + the same clock tick as the previous change; identity-critical callers hash + the bytes instead (:func:`prediction_run`). + + Parameters + ---------- + path : str or PathLike + Checkpoint file to describe. + + Returns + ------- + tuple of int + ``(size, st_mtime_ns, st_ctime_ns, st_ino)``. + """ + stat = Path(path).stat() + return stat.st_size, stat.st_mtime_ns, stat.st_ctime_ns, stat.st_ino + + +def _sha256_hex(path: Path) -> str: + """Return the full sha256 hex digest of the file at *path*, read in 1 MiB chunks.""" + hasher = hashlib.sha256() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(1 << 20), b""): + hasher.update(chunk) + return hasher.hexdigest() + + +def checkpoint_sha256_12(path: str | os.PathLike, *, memoize: bool = True) -> str: + """Return the first 12 hex chars of the sha256 of the file at *path*. + + On repeated calls for the same checkpoint, reads the digest from a + ``.sha256`` sidecar file, avoiding a multi-GB re-read. The sidecar + records the :func:`checkpoint_signature` of the file it hashed and is only + trusted when every field still matches exactly: a checkpoint overwritten + or replaced changes at least one of them even when it was copied with a + preserved or older mtime, so the digest never certifies stale weights (a + sidecar that is merely newer than the checkpoint proves nothing). Writes + the sidecar after a fresh hash; silently tolerates read-only parent + directories and NFS flakes by falling back to recompute. + + Parameters + ---------- + path : str or PathLike + Checkpoint file to hash. + memoize : bool, optional + Consult and maintain the sidecar (default). Pass False to hash the + bytes as they are now and leave the checkpoint's directory untouched: + file metadata cannot prove byte equality, so a digest that certifies + which weights a run loaded must come from the bytes themselves. + + Returns + ------- + str + First 12 hex characters of the file's sha256 digest. + """ + ckpt = Path(path) + if not memoize: + return _sha256_hex(ckpt)[:12] + sidecar = ckpt.with_suffix(ckpt.suffix + ".sha256") + signature = dict(zip(_SIGNATURE_FIELDS, checkpoint_signature(ckpt), strict=True)) + try: + recorded = json.loads(sidecar.read_text()) + if ( + isinstance(recorded, dict) + and all(recorded.get(field) == value for field, value in signature.items()) + and isinstance(recorded.get("sha256"), str) + and len(recorded["sha256"]) == 64 + ): + return recorded["sha256"][:12] + except (OSError, ValueError): + pass + digest = _sha256_hex(ckpt) + try: + tmp = sidecar.with_suffix(sidecar.suffix + ".tmp") + tmp.write_text(json.dumps({"sha256": digest, **signature}) + "\n") + tmp.replace(sidecar) + except OSError: + pass + return digest[:12] + + +def canonical_z_planes(per_t: Sequence[Sequence[int]]) -> list[list[int]]: + """Sorted, de-duplicated ``int`` planes of each timepoint, as :func:`record_z_planes` stores them.""" + return [sorted({int(z) for z in planes}) for planes in per_t] + + +def z_planes_sha256_12(z_planes: Mapping[str, Sequence[Sequence[int]]]) -> str: + """Content hash of a plane selection, independent of key order and plane order. + + Parameters + ---------- + z_planes : Mapping[str, Sequence[Sequence[int]]] + Position name to one plane list per timepoint. + + Returns + ------- + str + First 12 hex digits of the SHA-256 of the canonical JSON. + """ + canonical = {name: canonical_z_planes(per_t) for name, per_t in z_planes.items()} + return hashlib.sha256(json.dumps(canonical, sort_keys=True).encode()).hexdigest()[:12] + + +def prediction_run( + *, + array_key: str, + z_window_size: int, + z_reduction: str, + checkpoint_path: str | os.PathLike | None, + settings_sha256_12: str | None = None, + z_planes_sha256_12: str | None = None, +) -> dict[str, Any]: + """Describe what determines a run's voxels: the weights, the depth handling and the other settings. + + Call once per run; the checkpoint's bytes are hashed here, not per FOV, + and never taken from a ``.sha256`` sidecar: the marker certifies + which weights produced the voxels, and a memo keyed on file metadata + cannot prove that a same-size copy did not replace them. + + Parameters + ---------- + array_key : str + Name of the source (and output) array level that is predicted. + z_window_size : int + Depth of the model's input window. + z_reduction : str + How overlapping depth windows are combined by the writer. + checkpoint_path : str or PathLike or None + Checkpoint the model predicts with; ``None`` when the run has none. + settings_sha256_12 : str or None, optional + Hash of the remaining settings that shape the predicted voxels (model + inference arguments, input normalization, precision), computed by the + submitter from the resolved config; ``None`` when the run records none. + z_planes_sha256_12 : str or None, optional + :func:`z_planes_sha256_12` of the planes a plane-restricted run predicts. + ``None`` (every plane) leaves the key out, so a full run's identity is + the one recorded before plane selection existed. + + Returns + ------- + dict + JSON-serializable identity, including ``checkpoint_sha256_12`` of the + file's content and its path for provenance. + """ + run = { + "array_key": array_key, + "z_window_size": int(z_window_size), + "z_reduction": z_reduction, + "checkpoint_path": None if checkpoint_path is None else str(checkpoint_path), + "checkpoint_sha256_12": None + if checkpoint_path is None + else checkpoint_sha256_12(checkpoint_path, memoize=False), + "settings_sha256_12": settings_sha256_12, + } + if z_planes_sha256_12 is not None: + run["z_planes_sha256_12"] = z_planes_sha256_12 + return run + + +def started_marker(run: dict[str, Any]) -> dict[str, Any]: + """Return the marker a run records while it is writing a channel: its identity alone. + + Parameters + ---------- + run : dict + Run identity from :func:`prediction_run`. + + Returns + ------- + dict + ``dict(run)``; :func:`completion_marker` adds ``source_shape`` once the + channel is fully written. + """ + return dict(run) + + +def completion_marker(source_shape: list[int], run: dict[str, Any]) -> dict[str, Any]: + """Return the marker a run records for a channel predicted from ``source_shape``. + + Parameters + ---------- + source_shape : list of int + TZYX shape of the source array, from :func:`tzyx_shape`. + run : dict + Run identity from :func:`prediction_run`. + + Returns + ------- + dict + ``{"source_shape": [T, Z, Y, X], **run}``. + """ + return {"source_shape": list(source_shape), **run} + + +def marker_identity(marker: Any) -> Any: + """Strip provenance-only fields so markers compare on what was predicted. + + Parameters + ---------- + marker : Any + Recorded marker value, or None when the channel has no entry. + + Returns + ------- + Any + ``marker`` without its provenance-only keys when it is a dict, else + ``marker`` unchanged. + """ + if isinstance(marker, dict): + return {key: value for key, value in marker.items() if key not in _PROVENANCE_KEYS} + return marker + + +def same_marker(recorded: Any, marker: Any) -> bool: + """Return whether a recorded marker equals ``marker`` up to provenance fields. + + Parameters + ---------- + recorded : Any + Recorded marker value, or None when the channel has no entry. + marker : Any + Marker from :func:`started_marker` or :func:`completion_marker`. + + Returns + ------- + bool + True when both describe the same prediction state. + """ + return marker_identity(recorded) == marker_identity(marker) + + +def same_run(marker: Any, run: dict[str, Any]) -> bool: + """Return whether a recorded marker came from ``run``, started or complete, whatever its source shape. + + Parameters + ---------- + marker : Any + Recorded marker value; markers from other layouts never match. + run : dict + Run identity from :func:`prediction_run`. + + Returns + ------- + bool + True when the marker's run fields equal ``run`` up to provenance. + """ + if not isinstance(marker, dict): + return False + recorded = {key: value for key, value in marker.items() if key != "source_shape"} + return marker_identity(recorded) == marker_identity(run) + + +def mark_complete(position: Position, channels: Iterable[str], marker: dict[str, Any]) -> None: + """Record ``marker`` as the completion of ``channels``, keeping other channels' markers. + + Parameters + ---------- + position : Position + Output position whose completion attribute to update. + channels : iterable of str + Prediction channels that were fully written. + marker : dict + Marker from :func:`completion_marker`. + """ + completed = dict(position.zattrs.get(PREDICTION_COMPLETE_KEY, {})) + for channel in channels: + completed[channel] = marker + position.zattrs[PREDICTION_COMPLETE_KEY] = completed + + +def mark_started(position: Position, channels: Iterable[str], run: dict[str, Any]) -> None: + """Record that ``run`` is writing ``channels``, keeping other channels' markers. + + Replaces any completion of ``channels``, whose voxels are about to change, + so an interrupted run never leaves an old completion behind; unlike a + channel with no entry, a started channel is known to be this run's + unfinished work rather than an unverifiable legacy prediction. + + Parameters + ---------- + position : Position + Output position whose completion attribute to update. + channels : iterable of str + Prediction channels about to be (re)written. + run : dict + Run identity from :func:`prediction_run`. + """ + completed = dict(position.zattrs.get(PREDICTION_COMPLETE_KEY, {})) + for channel in channels: + completed[channel] = started_marker(run) + position.zattrs[PREDICTION_COMPLETE_KEY] = completed + + +def prediction_complete(position: Position, channels: Iterable[str], marker: dict[str, Any]) -> bool: + """Return whether every channel of ``position`` was completed with ``marker``. + + Parameters + ---------- + position : Position + Output position to inspect. + channels : iterable of str + Prediction channels that must all be complete. + marker : dict + Marker the current run would record; see :func:`completion_marker`. + + Returns + ------- + bool + True only when each channel's recorded marker equals ``marker`` up to + provenance fields. + """ + completed = position.zattrs.get(PREDICTION_COMPLETE_KEY, {}) + return all(same_marker(completed.get(channel), marker) for channel in channels) + + +def record_z_planes(position: Position, channels: Iterable[str], planes: Sequence[Sequence[int]] | None) -> None: + """Record the planes ``channels`` were predicted on, or clear them for a full prediction. + + Parameters + ---------- + position : Position + Output position whose attribute to update. + channels : iterable of str + Prediction channels that were just fully written. + planes : Sequence[Sequence[int]] or None + One plane list per timepoint, or ``None`` when every plane was predicted. + """ + recorded = dict(position.zattrs.get(PREDICTED_Z_PLANES_KEY, {})) + for channel in channels: + if planes is None: + recorded.pop(channel, None) + else: + recorded[channel] = canonical_z_planes(planes) + if recorded: + position.zattrs[PREDICTED_Z_PLANES_KEY] = recorded + elif PREDICTED_Z_PLANES_KEY in position.zattrs: + del position.zattrs[PREDICTED_Z_PLANES_KEY] + + +def predicted_z_planes(position: Position, channel: str) -> list[list[int]] | None: + """Planes ``channel`` was predicted on (one list per timepoint), or ``None`` for every plane. + + Parameters + ---------- + position : Position + Prediction position to inspect. + channel : str + Prediction channel name. + + Returns + ------- + list[list[int]] or None + The recorded per-timepoint planes, or ``None`` when the channel holds a + prediction on every plane. + """ + return position.zattrs.get(PREDICTED_Z_PLANES_KEY, {}).get(channel) + + +def plane_predict_unfinished(position: Position, channel: str) -> bool: + """Return whether ``channel`` holds a plane-restricted predict that has not finished. + + Such a run replaces the channel's marker with its started marker (which names the + plane selection, ``z_planes_sha256_12``) before its first write, and + :func:`completion_marker` adds ``source_shape`` only once every plane is written. + Until then the planes may be unwritten or another run's, while + :func:`predicted_z_planes` returns ``None`` (a fresh store) or the previous run's + planes (an interrupted re-predict of the same slab), so neither says "unfinished". + + Parameters + ---------- + position : Position + Prediction position to inspect. + channel : str + Prediction channel name. + + Returns + ------- + bool + True when the channel's marker names a plane selection but no ``source_shape``. + """ + marker = position.zattrs.get(PREDICTION_COMPLETE_KEY, {}).get(channel) + return isinstance(marker, dict) and "z_planes_sha256_12" in marker and "source_shape" not in marker diff --git a/packages/viscy-utils/src/viscy_utils/trainer.py b/packages/viscy-utils/src/viscy_utils/trainer.py index be5bcb653..1139dab53 100644 --- a/packages/viscy-utils/src/viscy_utils/trainer.py +++ b/packages/viscy-utils/src/viscy_utils/trainer.py @@ -26,7 +26,6 @@ def preprocess( num_workers: int = 1, block_size: int = 32, compute_otsu: bool = False, - otsu_grid_spacing: int = 8, compute_fg_masks: bool = False, fg_mask_channels: list[str] | None = None, fg_mask_key: str = "fg_mask", @@ -47,11 +46,10 @@ def preprocess( compute_otsu : bool, optional Whether to compute Otsu thresholds for Spotlight loss, by default False. - otsu_grid_spacing : int, optional - Grid spacing for Otsu sampling (denser than default), by default 8. compute_fg_masks : bool, optional - Whether to precompute binary foreground masks from Otsu - thresholds, by default False. Requires ``compute_otsu=True``. + Whether to precompute binary foreground masks (see + :func:`~viscy_utils.meta_utils.generate_fg_masks`), by default + False. Requires ``compute_otsu=True``. fg_mask_channels : list[str] or None, optional Channel names to compute FG masks for. Defaults to all channels that had Otsu thresholds computed (``channel_names``). @@ -75,7 +73,6 @@ def preprocess( channel_ids=channel_indices, grid_spacing=block_size, compute_otsu=compute_otsu, - otsu_grid_spacing=otsu_grid_spacing, ) if compute_fg_masks: if not compute_otsu: diff --git a/packages/viscy-utils/tests/conftest.py b/packages/viscy-utils/tests/conftest.py index c9e8a58d8..38e63a7a9 100644 --- a/packages/viscy-utils/tests/conftest.py +++ b/packages/viscy-utils/tests/conftest.py @@ -1,16 +1,14 @@ from __future__ import annotations -import pandas as pd - -# anndata 0.12.x zarr writer does not support pandas ArrowStringArray (default in pandas 2.x with PyArrow installed) -pd.options.future.infer_string = False +from pathlib import Path -from pathlib import Path # noqa: E402 +import anndata as ad +import numpy as np +import pandas as pd +from iohub import open_ome_zarr +from pytest import TempPathFactory, fixture -import anndata as ad # noqa: E402 -import numpy as np # noqa: E402 -from iohub import open_ome_zarr # noqa: E402 -from pytest import TempPathFactory, fixture # noqa: E402 +from viscy_utils.evaluation.zarr_utils import write_anndata_zarr channel_names = ["Phase", "GFP"] @@ -53,7 +51,7 @@ def annotated_adata() -> ad.AnnData: def annotated_adata_zarr(annotated_adata, tmp_path) -> dict: """Write annotated_adata to zarr + CSV and return dataset dict.""" zarr_path = tmp_path / "emb.zarr" - annotated_adata.write_zarr(zarr_path) + write_anndata_zarr(annotated_adata, zarr_path) csv_path = tmp_path / "ann.csv" annotated_adata.obs[["fov_name", "id", "cell_death_state"]].to_csv(csv_path, index=False) diff --git a/packages/viscy-utils/tests/test_background_lowpass.py b/packages/viscy-utils/tests/test_background_lowpass.py new file mode 100644 index 000000000..d5012ffe5 --- /dev/null +++ b/packages/viscy-utils/tests/test_background_lowpass.py @@ -0,0 +1,254 @@ +"""Unit tests for BackgroundLowPass, checked against scipy.ndimage references.""" + +import numpy as np +import pytest +import torch +from scipy import ndimage + +from viscy_utils.losses import BackgroundLowPass + + +def _square_batch(shape: tuple[int, ...], lo: int, hi: int, seed: int = 0) -> tuple[torch.Tensor, torch.Tensor]: + """Noisy target with a bright square; mask = the square ``[lo, hi)`` in YX of every plane.""" + g = torch.Generator().manual_seed(seed) + mask = torch.zeros(shape) + mask[..., lo:hi, lo:hi] = 1.0 + target = torch.randn(shape, generator=g) + 2.0 * mask + return target, mask + + +def _gaussian(a: np.ndarray, sigma_z: float, sigma: float) -> np.ndarray: + return ndimage.gaussian_filter(a, sigma=(0, 0, sigma_z, sigma, sigma), mode="nearest", truncate=4.0) + + +def _reference(op: BackgroundLowPass, target: torch.Tensor, mask: torch.Tensor) -> tuple[np.ndarray, np.ndarray]: + """``(w, x')`` from scipy: square max filter, Gaussians with ``mode="nearest"``, in float64.""" + x = target.double().numpy() + rz, r = op.dilate_radius_z, op.dilate_radius + dilated = ndimage.maximum_filter( + (mask.numpy() > 0.5).astype(np.float64), size=(1, 1, 2 * rz + 1, 2 * r + 1, 2 * r + 1), mode="constant" + ) + w = _gaussian(dilated, op.sigma_feather_z, op.sigma_feather).clip(0.0, 1.0) + flat = x.copy() # planes without background keep x + for i, j, d in np.ndindex(*x.shape[:3]): + if (dilated[i, j, d] == 0).any(): + flat[i, j, d] = x[i, j, d][dilated[i, j, d] == 0].mean() + if op.sigma_lp is None: + bg = flat + else: + num = _gaussian(x * (1.0 - dilated), op.sigma_lp_z, op.sigma_lp) + den = _gaussian(1.0 - dilated, op.sigma_lp_z, op.sigma_lp) + bg = np.where(den > 1e-6, num / np.maximum(den, 1e-6), flat) + return w, w * x + (1.0 - w) * bg + + +@pytest.mark.parametrize( + ("shape", "sigma_lp", "z_args"), + [ + ((2, 1, 1, 64, 64), 2.0, {}), + ((2, 2, 5, 48, 40), 2.0, {}), + ((1, 1, 6, 40, 40), 2.0, {"sigma_lp_z": 1.0, "sigma_feather_z": 0.5, "dilate_radius_z": 1}), + ((2, 1, 1, 64, 64), None, {}), + ((2, 2, 5, 48, 40), None, {"sigma_feather_z": 0.5, "dilate_radius_z": 1}), + ], + ids=["d1", "per_plane", "with_z", "flat_d1", "flat_with_z"], +) +def test_matches_scipy_reference(shape, sigma_lp, z_args): + """The blend weight and the output equal the scipy.ndimage pipeline, D=1 and D>1, smooth and flat.""" + op = BackgroundLowPass(sigma_lp=sigma_lp, sigma_feather=1.5, dilate_radius=3, **z_args) + target, mask = _square_batch(shape, 12, 28) + mask[:, :, : shape[2] // 2] = 0.0 # planes differ, so Z handling matters + w_ref, out_ref = _reference(op, target, mask) + np.testing.assert_allclose(op.blend_weight(op.dilated_mask(mask)).numpy(), w_ref, atol=1e-6) + np.testing.assert_allclose(op(target, mask).numpy(), out_ref, atol=1e-5) + + +def test_identity_where_w_is_one_and_background_where_w_is_zero(): + """Inside the dilated mask shrunk by the feather radius x' = x; beyond its reach x' = B.""" + op = BackgroundLowPass(sigma_lp=2.0, sigma_feather=2.0, dilate_radius=4) + target, mask = _square_batch((2, 1, 1, 64, 64), 16, 48) + out = op(target, mask) + dilated = op.dilated_mask(mask) + # Dilated square [12, 52); the feather kernel's radius is int(4 * 2 + 0.5) = 8. + inner = (..., slice(20, 44), slice(20, 44)) + torch.testing.assert_close(out[inner], target[inner], rtol=0.0, atol=1e-6) + # Rows 0-3 are > 8 rows from the dilated square: w is exactly 0 there. + assert torch.equal(op.blend_weight(dilated)[..., :4, :], torch.zeros(2, 1, 1, 4, 64)) + assert torch.equal(out[..., :4, :], op.background(target, dilated)[..., :4, :]) + # ... and the background estimate is not the identity there. + assert (out[..., :4, :] - target[..., :4, :]).abs().mean() > 0.3 + + +def test_background_estimate_does_not_leak_the_foreground(): + """On a constant background B is that constant everywhere, even next to a bright blob a plain blur halos.""" + op = BackgroundLowPass(sigma_lp=4.0, sigma_feather=1.0, dilate_radius=2) + mask = torch.zeros((1, 1, 1, 64, 64)) + mask[..., 16:48, 16:48] = 1.0 + target = 0.7 + 10.0 * mask + dilated = op.dilated_mask(mask) + ring = (..., slice(50, 54), slice(16, 48)) # just outside the dilated square [14, 50) + assert torch.equal(dilated[ring], torch.zeros(1, 1, 1, 4, 32)) + plain = _gaussian(target.double().numpy(), 0.0, op.sigma_lp) + assert plain[ring].max() > 2.7 # the halo a plain G_lp(x) would teach + background = op.background(target, dilated) + torch.testing.assert_close(background, torch.full_like(target, 0.7), rtol=0.0, atol=1e-6) + out = op(target, mask) + torch.testing.assert_close(out[dilated == 0], torch.full_like(out[dilated == 0], 0.7), rtol=0.0, atol=1e-6) + + +def test_flat_background_is_the_masked_mean_per_plane(): + """sigma_lp=None: wherever w == 0, x' is one constant per (sample, channel, plane), its background mean.""" + op = BackgroundLowPass(sigma_lp=None, sigma_feather=2.0, dilate_radius=4) + target, mask = _square_batch((2, 2, 3, 64, 64), 16, 48) + mask[0, :, 1] = 0.0 # planes differ + target = target + torch.arange(3.0).reshape(3, 1, 1) + torch.linspace(-1.0, 1.0, 64) # per-plane offset + ramp + out = op(target, mask) + dilated = op.dilated_mask(mask) + zero_w = op.blend_weight(dilated) == 0 + for i, j, d in np.ndindex(2, 2, 3): + expected = target[i, j, d][dilated[i, j, d] == 0].double().mean().item() + values = out[i, j, d][zero_w[i, j, d]] + assert values.numel() > 300 + torch.testing.assert_close(values, torch.full_like(values, expected), rtol=0.0, atol=1e-5) + # An empty mask flattens every plane to its own mean. + flat = op(target, torch.zeros_like(target)) + torch.testing.assert_close(flat, target.mean(dim=(3, 4), keepdim=True).expand_as(target), rtol=0.0, atol=1e-5) + + +@pytest.mark.parametrize("sigma_lp", [None, 8.0], ids=["bgflat", "bglp"]) +def test_backlight_ramp_is_removed_by_flat_and_kept_by_smooth(sigma_lp): + """A linear backlight ramp: bgflat flattens it, bglp (even at a large sigma) keeps it.""" + op = BackgroundLowPass(sigma_lp=sigma_lp, sigma_feather=2.0, dilate_radius=4) + slope = 0.02 + mask = torch.zeros((1, 1, 1, 96, 96)) + mask[..., 40:56, 40:56] = 1.0 + target = slope * torch.arange(96.0) + 5.0 * mask + out = op(target, mask) + zero_w = op.blend_weight(op.dilated_mask(mask)) == 0 + values = out[zero_w] + if sigma_lp is None: + assert torch.unique(values).numel() == 1 + else: + cols = torch.arange(96.0).expand_as(target)[zero_w] + fitted = np.polyfit(cols.numpy(), values.numpy(), 1)[0] + assert 0.8 * slope < fitted < 1.2 * slope + + +def test_background_keeps_the_local_mean(): + """Background = constant + checkerboard: B removes the checkerboard and keeps the constant.""" + op = BackgroundLowPass(sigma_lp=2.0, sigma_feather=2.0, dilate_radius=4) + yy, xx = torch.meshgrid(torch.arange(48), torch.arange(48), indexing="ij") + target = (3.0 + 0.5 * (-1.0) ** (yy + xx)).float().expand(1, 1, 1, 48, 48) + out = op(target, torch.zeros_like(target)) + # Away from the border, where edge replication breaks the checkerboard's alternation. + torch.testing.assert_close(out[..., 8:-8, 8:-8], torch.full((1, 1, 1, 32, 32), 3.0), rtol=0.0, atol=1e-4) + # A constant passes through unchanged whatever the mask. + const = torch.full((2, 1, 3, 32, 32), -0.7) + _, mask = _square_batch(const.shape, 8, 20) + torch.testing.assert_close(op(const, mask), const, rtol=0.0, atol=1e-6) + + +def test_empty_mask_blurs_everything(): + """An empty (or sub-threshold) mask makes B a plain blur of the whole target, and x' = B.""" + op = BackgroundLowPass(sigma_lp=2.0, sigma_feather=2.0, dilate_radius=4) + target, _ = _square_batch((2, 1, 1, 32, 32), 0, 0) + blurred = _gaussian(target.double().numpy(), 0.0, op.sigma_lp) + for mask in (torch.zeros_like(target), torch.full_like(target, 0.4)): + np.testing.assert_allclose(op(target, mask).numpy(), blurred, atol=1e-5) + + +@pytest.mark.parametrize("sigma_feather", [0.5, 1.0, 2.0, 3.0]) +def test_all_foreground_plane_has_w_exactly_one_and_is_returned_unchanged(sigma_feather): + """Under an all-foreground plane w == 1 exactly (not 1 - rounding), so the plane comes back bit-identical.""" + target, mask = _square_batch((2, 1, 3, 32, 32), 8, 20) + mask[:, :, 1] = 1.0 + for sigma_lp in (2.0, None): + op = BackgroundLowPass(sigma_lp=sigma_lp, sigma_feather=sigma_feather, dilate_radius=4) + assert torch.equal(op.blend_weight(op.dilated_mask(mask))[:, :, 1], torch.ones(2, 1, 32, 32)) + out = op(target, mask) + assert torch.equal(out[:, :, 1], target[:, :, 1]) + assert not torch.equal(out[:, :, 0], target[:, :, 0]) + + +@pytest.mark.parametrize("sigma_lp", [2.0, None], ids=["smooth", "flat"]) +def test_patch_without_background_is_returned_unchanged(sigma_lp): + """Where the dilated mask covers the patch, B = x and w == 1: x' = x exactly; other patches are unaffected.""" + op = BackgroundLowPass(sigma_lp=sigma_lp, sigma_feather=2.0, dilate_radius=4) + target, mask = _square_batch((2, 1, 1, 32, 32), 8, 20) + mask[0] = 1.0 + mask[0, ..., 5:7, 9:11] = 0.0 # a hole the dilation fills + dilated = op.dilated_mask(mask) + assert torch.equal(dilated[0], torch.ones_like(dilated[0])) + assert torch.equal(op.blend_weight(dilated)[0], torch.ones_like(dilated[0])) + assert torch.equal(op.background(target, dilated)[0], target[0]) + out = op(target, mask) + assert torch.equal(out[0], target[0]) + assert torch.equal(out[1:], op(target[1:], mask[1:])) + + +def test_thin_background_ring_falls_back_to_the_plane_mean(): + """Pixels the normalized convolution cannot reach take the plane's flat mean; nothing is NaN or Inf.""" + op = BackgroundLowPass(sigma_lp=2.0, sigma_feather=2.0, dilate_radius=1) + target, mask = _square_batch((1, 1, 1, 48, 48), 3, 45) # dilated: [2, 46), a 2-px background ring + dilated = op.dilated_mask(mask) + background = op.background(target, dilated) + den = _gaussian(1.0 - dilated.double().numpy(), 0.0, op.sigma_lp) + unreached = torch.from_numpy(den <= 1e-6) + assert unreached.sum() > 100 # the kernel's support (radius 8) misses the ring from the centre + ring_mean = target[dilated == 0].mean() + torch.testing.assert_close(background[unreached], torch.full_like(background[unreached], ring_mean.item())) + assert torch.isfinite(background).all() + assert torch.isfinite(op(target, mask)).all() + + +def test_planes_are_independent_by_default(): + """With every ``*_z`` at 0, each Z plane is the op applied to that plane alone.""" + op = BackgroundLowPass(sigma_lp=2.0, sigma_feather=1.0, dilate_radius=2) + target, mask = _square_batch((2, 1, 4, 32, 32), 8, 20) + mask[:, :, 1] = 0.0 # planes differ, so a leak across Z would show + out = op(target, mask) + for d in range(4): + plane = op(target[:, :, d : d + 1], mask[:, :, d : d + 1]) + torch.testing.assert_close(out[:, :, d : d + 1], plane, rtol=0.0, atol=1e-6) + + +def test_dtype_and_autocast(): + """Low-precision input comes back in its dtype, computed in float32; autocast changes nothing.""" + op = BackgroundLowPass(sigma_lp=2.0, sigma_feather=2.0, dilate_radius=4) + target, mask = _square_batch((2, 1, 1, 32, 32), 8, 20) + bf16 = target.bfloat16() + out = op(bf16, mask) + assert out.dtype == torch.bfloat16 + assert torch.equal(out, op(bf16.float(), mask).bfloat16()) + with torch.autocast(device_type="cpu", dtype=torch.bfloat16): + autocast_out = op(target, mask) + assert autocast_out.dtype == torch.float32 + assert torch.equal(autocast_out, op(target, mask)) + + +@pytest.mark.skipif(not torch.cuda.is_available(), reason="needs a CUDA device") +@pytest.mark.parametrize("sigma_lp", [2.0, None], ids=["smooth", "flat"]) +def test_cuda_matches_cpu(sigma_lp): + """The GPU path agrees with the CPU path (no convolution backend, so no TF32).""" + op = BackgroundLowPass(sigma_lp=sigma_lp, sigma_feather=2.0, dilate_radius=4) + target, mask = _square_batch((2, 1, 1, 64, 64), 16, 48) + out = op(target.cuda(), mask.cuda()) + assert out.device.type == "cuda" + torch.testing.assert_close(out.cpu(), op(target, mask), rtol=0.0, atol=1e-5) + + +def test_rejects_bad_arguments_and_shapes(): + with pytest.raises(ValueError, match="sigma_lp must be > 0 or None"): + BackgroundLowPass(sigma_lp=0.0, sigma_feather=2.0, dilate_radius=4) + with pytest.raises(ValueError, match="no effect with a flat background"): + BackgroundLowPass(sigma_lp=None, sigma_feather=2.0, dilate_radius=4, sigma_lp_z=1.0) + assert BackgroundLowPass(sigma_lp=float("inf"), sigma_feather=2.0, dilate_radius=4).sigma_lp is None + with pytest.raises(ValueError, match="dilate_radius must be >= 0"): + BackgroundLowPass(sigma_lp=2.0, sigma_feather=2.0, dilate_radius=-1) + with pytest.raises(ValueError, match="sigma_feather_z must be >= 0"): + BackgroundLowPass(sigma_lp=2.0, sigma_feather=2.0, dilate_radius=4, sigma_feather_z=-1.0) + op = BackgroundLowPass(sigma_lp=2.0, sigma_feather=2.0, dilate_radius=4) + with pytest.raises(ValueError, match="must match target"): + op(torch.zeros(1, 1, 1, 8, 8), torch.zeros(1, 1, 1, 8, 4)) + with pytest.raises(ValueError, match=r"\(B, C, D, H, W\)"): + op(torch.zeros(1, 1, 8, 8), torch.zeros(1, 1, 8, 8)) diff --git a/packages/viscy-utils/tests/test_cli.py b/packages/viscy-utils/tests/test_cli.py index e8835f983..a086b535c 100644 --- a/packages/viscy-utils/tests/test_cli.py +++ b/packages/viscy-utils/tests/test_cli.py @@ -19,7 +19,9 @@ def _run(*args): [sys.executable, "-m", "viscy_utils.cli", *args], capture_output=True, text=True, - timeout=30, + # Hang guard only. A cold `--help` imports torch/lightning from NFS: + # 12-22 s serially, and past 30 s under `pytest -n 11`. + timeout=120, ) return _run diff --git a/packages/viscy-utils/tests/test_lazy_imports.py b/packages/viscy-utils/tests/test_lazy_imports.py new file mode 100644 index 000000000..16628b2db --- /dev/null +++ b/packages/viscy-utils/tests/test_lazy_imports.py @@ -0,0 +1,57 @@ +import subprocess +import sys + +import pytest + +import viscy_utils +import viscy_utils.callbacks + +# Submodules that evaluation code imports without training anything. +LIGHT_SUBMODULES = [ + "viscy_utils.compose", + "viscy_utils.mp_utils", + "viscy_utils.normalize", + "viscy_utils.prediction_metadata", +] + + +def test_light_submodules_skip_torch_and_lightning(): + """Importing light submodules must not load torch or lightning via the package.""" + code = ( + f"import sys, {', '.join(LIGHT_SUBMODULES)}; " + "print(sorted(m for m in ('torch', 'lightning') if m in sys.modules))" + ) + result = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, check=True) + assert result.stdout.strip() == "[]" + + +def test_health_callbacks_import_without_the_eval_extra(): + """The callbacks VisCyCLI injects into every fit must import without scikit-learn. + + ``sys.modules["sklearn"] = None`` makes every sklearn import fail, as on a base + install; OnlineEvalCallback imports it at module level. + """ + code = ( + "import sys; sys.modules['sklearn'] = None; " + "from viscy_utils.callbacks import MonitorHealthCheck, OptimizerHealthCheck" + ) + subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, check=True) + + +@pytest.mark.parametrize("name", viscy_utils.__all__) +def test_public_names_importable(name): + exec(f"from viscy_utils import {name}", {}) + + +@pytest.mark.parametrize("name", viscy_utils.callbacks.__all__) +def test_public_callbacks_importable(name): + exec(f"from viscy_utils.callbacks import {name}", {}) + + +def test_unknown_attribute_raises(): + with pytest.raises(AttributeError): + viscy_utils.not_a_name # noqa: B018 + + +def test_dir_lists_public_names(): + assert set(viscy_utils.__all__) <= set(dir(viscy_utils)) diff --git a/packages/viscy-utils/tests/test_linear_classifier.py b/packages/viscy-utils/tests/test_linear_classifier.py index efcd356b8..4bd685f5d 100644 --- a/packages/viscy-utils/tests/test_linear_classifier.py +++ b/packages/viscy-utils/tests/test_linear_classifier.py @@ -17,6 +17,7 @@ LinearClassifierInferenceConfig, LinearClassifierTrainConfig, ) +from viscy_utils.evaluation.zarr_utils import write_anndata_zarr @pytest.fixture @@ -228,7 +229,7 @@ def test_filters_unknown_labels(self, tmp_path): adata = ad.AnnData(X=X, obs=obs) zarr_path = tmp_path / "emb.zarr" - adata.write_zarr(zarr_path) + write_anndata_zarr(adata, zarr_path) csv_path = tmp_path / "ann.csv" ann_df = pd.DataFrame({"fov_name": ["A/1/0"] * n, "id": range(n), "cell_death_state": labels}) @@ -249,7 +250,7 @@ def test_filters_nan_labels(self, tmp_path): adata = ad.AnnData(X=X, obs=obs) zarr_path = tmp_path / "emb.zarr" - adata.write_zarr(zarr_path) + write_anndata_zarr(adata, zarr_path) csv_path = tmp_path / "ann.csv" ann_df = pd.DataFrame({"fov_name": ["A/1/0"] * n, "id": range(n), "cell_death_state": labels}) @@ -269,7 +270,7 @@ def test_raises_on_empty(self, tmp_path): adata = ad.AnnData(X=X, obs=obs) zarr_path = tmp_path / "emb.zarr" - adata.write_zarr(zarr_path) + write_anndata_zarr(adata, zarr_path) csv_path = tmp_path / "ann.csv" ann_df = pd.DataFrame( @@ -296,7 +297,7 @@ def test_multiple_datasets(self, annotated_adata_zarr, tmp_path): adata = ad.AnnData(X=X, obs=obs) zarr_path = tmp_path / "emb2.zarr" - adata.write_zarr(zarr_path) + write_anndata_zarr(adata, zarr_path) csv_path = tmp_path / "ann2.csv" ann_df = pd.DataFrame({"fov_name": ["B/1/0"] * n, "id": range(n), "cell_death_state": labels}) diff --git a/packages/viscy-utils/tests/test_meta_utils.py b/packages/viscy-utils/tests/test_meta_utils.py index 08ed7e5b2..6a56ddd83 100644 --- a/packages/viscy-utils/tests/test_meta_utils.py +++ b/packages/viscy-utils/tests/test_meta_utils.py @@ -1,8 +1,10 @@ +import warnings + import numpy as np import pytest from iohub import open_ome_zarr -from viscy_utils.meta_utils import generate_fg_masks, generate_normalization_metadata +from viscy_utils.meta_utils import foreground_mask_from_volume, generate_fg_masks, generate_normalization_metadata GRID_SPACING = 8 @@ -103,9 +105,7 @@ def test_normalization_metadata_keys(small_hcs_dataset): def test_compute_otsu_stores_threshold(bimodal_hcs_dataset): """compute_otsu=True stores otsu_threshold in fov_statistics.""" - generate_normalization_metadata( - bimodal_hcs_dataset, num_workers=1, grid_spacing=GRID_SPACING, compute_otsu=True, otsu_grid_spacing=4 - ) + generate_normalization_metadata(bimodal_hcs_dataset, num_workers=1, grid_spacing=GRID_SPACING, compute_otsu=True) with open_ome_zarr(bimodal_hcs_dataset, mode="r") as plate: for _, fov in plate.positions(): @@ -117,9 +117,7 @@ def test_compute_otsu_stores_threshold(bimodal_hcs_dataset): def test_compute_otsu_threshold_separates_bimodal(bimodal_hcs_dataset): """Otsu threshold on bimodal fluorescence falls between the two modes.""" - generate_normalization_metadata( - bimodal_hcs_dataset, num_workers=1, grid_spacing=GRID_SPACING, compute_otsu=True, otsu_grid_spacing=4 - ) + generate_normalization_metadata(bimodal_hcs_dataset, num_workers=1, grid_spacing=GRID_SPACING, compute_otsu=True) with open_ome_zarr(bimodal_hcs_dataset, mode="r") as plate: for _, fov in plate.positions(): @@ -141,9 +139,7 @@ def test_compute_otsu_false_omits_threshold(small_hcs_dataset): def test_generate_fg_masks_stores_mask(bimodal_hcs_dataset): """generate_fg_masks stores a uint8 binary mask array per position.""" - generate_normalization_metadata( - bimodal_hcs_dataset, num_workers=1, grid_spacing=GRID_SPACING, compute_otsu=True, otsu_grid_spacing=4 - ) + generate_normalization_metadata(bimodal_hcs_dataset, num_workers=1, grid_spacing=GRID_SPACING, compute_otsu=True) generate_fg_masks(bimodal_hcs_dataset, channel_names=["Fluorescence"]) with open_ome_zarr(bimodal_hcs_dataset, mode="r") as plate: @@ -158,9 +154,7 @@ def test_generate_fg_masks_stores_mask(bimodal_hcs_dataset): def test_generate_fg_masks_separates_bimodal(bimodal_hcs_dataset): """Fluorescence mask marks the bright half as foreground.""" - generate_normalization_metadata( - bimodal_hcs_dataset, num_workers=1, grid_spacing=GRID_SPACING, compute_otsu=True, otsu_grid_spacing=4 - ) + generate_normalization_metadata(bimodal_hcs_dataset, num_workers=1, grid_spacing=GRID_SPACING, compute_otsu=True) generate_fg_masks(bimodal_hcs_dataset, channel_names=["Fluorescence"]) with open_ome_zarr(bimodal_hcs_dataset, mode="r") as plate: @@ -173,18 +167,65 @@ def test_generate_fg_masks_separates_bimodal(bimodal_hcs_dataset): assert (fov["fg_mask"][:, phase_idx] == 1).all() -def test_generate_fg_masks_requires_otsu(bimodal_hcs_dataset): - """generate_fg_masks raises KeyError without prior Otsu computation.""" - with pytest.raises(KeyError): - generate_fg_masks(bimodal_hcs_dataset, channel_names=["Fluorescence"]) +def test_generate_fg_masks_does_not_need_otsu(bimodal_hcs_dataset): + """Masking no longer reads otsu_threshold, so it runs without one. + + The mask is thresholded in CLAHE's equalized [0, 1] space while + otsu_threshold is in raw intensity units for NormalizeSampled to subtract. + Deriving the mask from that scalar would be a unit error, so the dependency + is gone -- and this pins it gone, because the previous contract (raise + KeyError) was the opposite assertion. + """ + generate_fg_masks(bimodal_hcs_dataset, channel_names=["Fluorescence"]) + + with open_ome_zarr(bimodal_hcs_dataset, mode="r") as plate: + ch_idx = plate.channel_names.index("Fluorescence") + for _, fov in plate.positions(): + # The stats pass never ran, so there is no normalization block at + # all -- masking cannot have consulted a threshold that is absent. + assert "normalization" not in fov.zattrs + assert fov["fg_mask"][:, ch_idx].sum() > 0 + + +def test_fg_mask_separates_the_bimodal_channel(bimodal_hcs_dataset): + """The mask must track the bright half, not merely be non-empty. + + The fixture's Fluorescence channel is bright on y >= 32 only, so a mask that + covers everything (or the wrong half) still passes a `sum() > 0` check. + """ + generate_fg_masks(bimodal_hcs_dataset, channel_names=["Fluorescence"]) + + with open_ome_zarr(bimodal_hcs_dataset, mode="r") as plate: + ch_idx = plate.channel_names.index("Fluorescence") + for _, fov in plate.positions(): + mask = np.asarray(fov["fg_mask"][0, ch_idx]) + bright, dark = mask[:, 32:, :].mean(), mask[:, :32, :].mean() + assert bright > 0.9, f"bright half under-covered: {bright}" + assert dark < 0.1, f"dark half over-covered: {dark}" def test_generate_fg_masks_no_overwrite(bimodal_hcs_dataset): """generate_fg_masks raises FileExistsError on re-run.""" - generate_normalization_metadata( - bimodal_hcs_dataset, num_workers=1, grid_spacing=GRID_SPACING, compute_otsu=True, otsu_grid_spacing=4 - ) + generate_normalization_metadata(bimodal_hcs_dataset, num_workers=1, grid_spacing=GRID_SPACING, compute_otsu=True) generate_fg_masks(bimodal_hcs_dataset, channel_names=["Fluorescence"]) with pytest.raises(FileExistsError): generate_fg_masks(bimodal_hcs_dataset, channel_names=["Fluorescence"]) + + +def test_constant_volume_gives_an_empty_mask_without_warnings(): + """A blank timepoint is all background, reached by the explicit guard rather than a NaN.""" + with warnings.catch_warnings(): + warnings.simplefilter("error") + mask = foreground_mask_from_volume(np.full((4, 64, 64), 3.0, dtype=np.float32)) + assert mask.dtype == bool and mask.shape == (4, 64, 64) and not mask.any() + + +def test_near_constant_volume_gives_an_empty_mask_without_warnings(): + """A few hot pixels in a blank volume leave the clip percentiles equal: still empty, no NaN.""" + volume = np.zeros((20, 64, 64), dtype=np.float32) + volume.flat[::5000] = 100.0 + with warnings.catch_warnings(): + warnings.simplefilter("error") + mask = foreground_mask_from_volume(volume) + assert not mask.any() diff --git a/packages/viscy-utils/tests/test_metrics.py b/packages/viscy-utils/tests/test_metrics.py index 6f1d3eb51..0dfc6688b 100644 --- a/packages/viscy-utils/tests/test_metrics.py +++ b/packages/viscy-utils/tests/test_metrics.py @@ -7,13 +7,27 @@ - aggregate SSIM equivalence on correlated-pair inputs (closer to training) - gradient-flow correctness via cosine similarity and sign-flip fraction - output dtype invariance to input dtype +- finiteness at zero data_range (the eps denominator floor) +- flat-window contrast-sensitivity ≈ 1 (the Cauchy-Schwarz covariance bound) +- finite gradients on flat-background windows (sqrt-at-zero inside that same bound) +- MS-SSIM computes outside autocast, so fp16 backward intermediates cannot overflow + to inf and become NaN in a later multiply + +The two regression tests guard the deterministic, hardware-independent halves of +the bf16 NaN fix. The remaining half — the negative-variance denominator collapse +that produced intermittent NaN loss in 2D FCMAE training — is a bf16 rounding +knife-edge (the denominator must round to exactly 0) that only surfaces over +millions of windows across many steps and varies by GPU architecture, so it is not +unit-reproducible; it is validated end-to-end (the exact failing fit ran clean +after the clamp). """ import pytest import torch import torch.nn.functional as F -from viscy_utils.evaluation.metrics import _compute_ssim_and_cs_bf16 +from viscy_utils.evaluation import metrics as metrics_module +from viscy_utils.evaluation.metrics import _compute_ssim_and_cs_bf16, _safe_sqrt # monai is not a hard dep of viscy-utils — skip the suite if absent rather # than failing at import time. @@ -167,3 +181,187 @@ def test_ssim_helper_dtypes(input_dtype): assert ssim_helper.dtype == torch.float32 assert cs_helper.dtype == torch.float32 + + +@_skip_no_bf16 +def test_ssim_helper_finite_on_zero_data_range(): + """Regression: a zero ``data_range`` (→ c1=c2=0) must not yield 0/0 NaN. + + ``ms_ssim_25d`` recomputes ``data_range = target.max()`` per scale; a flat or + all-nonpositive target zeroes ``data_range`` and with it the c1/c2 stability + constants, so the SSIM/CS ratios become 0/0. ``clamp=True`` in the loss cannot + repair that — ``clamp`` bounds magnitude, not NaN — so the poisoned gradient + silently corrupts every weight. The ``_SSIM_DENOM_EPS`` denominator floor keeps + the helper finite. This case NaNs on the unfloored implementation on every device + (deterministic, hardware-independent). + """ + kernel_1 = (1, 11, 11) + shape = (2, 1, 1, 64, 64) + y = torch.zeros(*shape, device="cuda") + y_pred = torch.zeros_like(y) + + ssim_full, cs = _compute_ssim_and_cs_bf16(y_pred, y, kernel_size=kernel_1, data_range=y.max()) + + assert torch.isfinite(ssim_full).all(), "SSIM NaN/Inf at data_range=0 (denominator not floored)" + assert torch.isfinite(cs).all(), "contrast-sensitivity NaN/Inf at data_range=0" + + +@_skip_no_bf16 +def test_ssim_helper_identity_holds_at_small_data_range(): + """SSIM(y, y) == 1 for every ``data_range``, including 0. + + ``_SSIM_DENOM_EPS`` must floor c1/c2 SYMMETRICALLY -- numerator and denominator. + Flooring only the denominator keeps the helper finite (the sibling test above) + but breaks the identity: measured SSIM(y, y) = 0.082158 at data_range 1e-3 and + exactly 0.0 at data_range 0. Since ``ms_ssim_25d`` recomputes + ``data_range = target.max()`` per scale from a downsampled target, an all-zero + or masked-out crop then scored loss 1.0 for a numerically perfect prediction. + """ + shape = (2, 1, 1, 64, 64) + for value in (0.0, 1e-4, 1e-3, 3e-3, 1e-2, 1.0): + y = torch.full(shape, value, device="cuda") + ssim_full, cs = _compute_ssim_and_cs_bf16(y, y, kernel_size=(1, 11, 11), data_range=y.max()) + assert torch.isfinite(ssim_full).all() + assert abs(ssim_full.mean().item() - 1.0) < 1e-6, f"SSIM(y, y) != 1 at data_range={value}" + assert abs(cs.mean().item() - 1.0) < 1e-6, f"cs(y, y) != 1 at data_range={value}" + + zeros = torch.zeros(1, 1, 5, 256, 256, device="cuda") + assert abs(metrics_module.ms_ssim_25d(zeros, zeros, clamp=True).item() - 1.0) < 1e-6 + + +@_skip_no_bf16 +def test_ssim_helper_flat_window_contrast_is_unity(): + """Regression: near-flat windows over large-magnitude inputs keep cs≈1. + + FCMAE targets are z-scored (``NormalizeSampled``); a channel with a tiny iqr + (e.g. iPSC/A549 nucleus, iqr~1.8) normalizes to values in the hundreds. At + depth=1 (2D FCMAE) each SSIM window has only ``1*H_k*W_k`` samples, so the bf16 + ``mu_xx - mu_x**2`` variance estimate catastrophically cancels and rounds + *negative* (empirically ~-100 at magnitude 150). Exact fp32 recovers cs=1 on a + flat window because the equally-negative sigma_x/sigma_y/sigma_xy cancel in the + ratio; clamping only the diagonal variances to >=0 breaks that cancellation and + inflates |cs| past 10 on background windows (which dominate a VS image). The + Cauchy-Schwarz bound on sigma_xy restores cs=1. This guards that balance — the + unbounded-covariance variant fails it with |cs-1| > 10, deterministically. + """ + torch.manual_seed(5) + kernel_1 = (1, 11, 11) + shape = (1, 1, 1, 64, 64) + worst = 0.0 + for magnitude in (150.0, 300.0): + y = torch.full(shape, magnitude, device="cuda") + 0.03 * torch.randn(*shape, device="cuda") + y_pred = torch.full(shape, magnitude, device="cuda") + 0.03 * torch.randn(*shape, device="cuda") + _, cs = _compute_ssim_and_cs_bf16( + y_pred, y, kernel_size=kernel_1, data_range=torch.tensor(magnitude, device="cuda") + ) + assert torch.isfinite(cs).all() + worst = max(worst, (cs - 1.0).abs().max().item()) + assert worst < 0.5, f"near-flat contrast-sensitivity deviates from 1 by {worst:.3f} (unbounded covariance?)" + + +@_skip_no_bf16 +def test_ssim_helper_gradient_finite_on_flat_background(): + """Regression: the Cauchy-Schwarz bound must not NaN the gradient at zero variance. + + ``sigma_xy_bound = sqrt(sigma_x * sigma_y)`` is zero wherever either clamped + variance rounds to zero, and ``sqrt`` has an infinite derivative at zero. The + clamp-inactive elements contribute ``0 * inf == nan``, which the conv backward + then spreads across each window — so the forward loss stays finite while most of + the input gradient becomes NaN. On the unguarded ``sqrt`` this fires on ordinary + normalized batches: 41-48% of ``pred.grad`` for a z-scored field with a flat + background at a nonzero offset, at both depth=15 and depth=1. + + Uses an offset flat region rather than ``torch.rand``: the trigger is a window + whose mean dominates its variance, so ``mu_xx`` and ``mu_x**2`` round to the same + bf16 value and cancel exactly. + """ + torch.manual_seed(6) + shape = (2, 1, 15, 192, 192) + half = shape[-2] // 2 + + def _field(seed: int) -> torch.Tensor: + torch.manual_seed(seed) + field = torch.randn(*shape, device="cuda") * 0.1 - 0.5 + # Bottom half carries structure; the top half stays near-flat background. + field[..., half:, :] = torch.randn(shape[0], shape[1], shape[2], shape[-2] - half, shape[-1], device="cuda") + return field + + y_pred = _field(6).detach().requires_grad_(True) + y = _field(7) + + ssim_full, _ = _compute_ssim_and_cs_bf16(y_pred, y, kernel_size=_KERNEL, data_range=y.max()) + (1 - ssim_full.mean()).backward() + + assert y_pred.grad is not None + n_nan = torch.isnan(y_pred.grad).sum().item() + assert n_nan == 0, f"{n_nan}/{y_pred.grad.numel()} NaN gradients (sqrt of zero variance not guarded)" + + +@_skip_no_bf16 +def test_ms_ssim_runs_outside_autocast(): + """``ms_ssim_25d`` must compute in fp32 even inside a 16-mixed autocast region. + + The helper chooses its own precision deliberately -- bf16 convolutions, fp32 for + everything after. Letting an enclosing autocast re-enter it puts *backward* + intermediates in fp16, where the ~1e8 gradients produced by the floored SSIM + denominator overflow to inf and a later multiply turns them into NaN. That is the + failure that stalled three joint FCMAE resumes: ``detect_anomaly`` blamed + ``MulBackward0`` on the Cauchy-Schwarz bound, the forward loss stayed finite, and + every one of the model's 32.1 M parameter gradients came back NaN. + + Pins the structural invariant, because reproducing the NaN itself needs one + specific real batch (z-scored targets ranging to ~264) far too large to ship as a + fixture: on that batch the rate was 0.02% of the input gradient under autocast and + exactly 0% in fp32. Hardening ``sqrt`` does not cover this and neither does + splitting the product -- both were measured and both still NaN'd. + """ + seen = [] + real = metrics_module._compute_ssim_and_cs_bf16 + + def spy(*args, **kwargs): + seen.append(torch.is_autocast_enabled()) + return real(*args, **kwargs) + + y = torch.rand(*_BATCH, device="cuda") + y_pred = (y + 0.05 * torch.randn_like(y)).detach().requires_grad_(True) + + monkey = pytest.MonkeyPatch() + monkey.setattr("viscy_utils.evaluation.metrics._compute_ssim_and_cs_bf16", spy) + try: + with torch.autocast("cuda", dtype=torch.float16): + ms_ssim = metrics_module.ms_ssim_25d(y_pred, y, clamp=True) + (1 - ms_ssim).backward() + finally: + monkey.undo() + + assert seen, "ms_ssim_25d did not reach the bf16 helper" + assert not any(seen), ( + f"autocast was enabled for {sum(seen)}/{len(seen)} MS-SSIM scales; fp16 backward intermediates are the NaN path" + ) + assert torch.isfinite(y_pred.grad).all(), "non-finite gradient under 16-mixed autocast" + + +def test_safe_sqrt_matches_sqrt_forward_and_zeroes_gradient_at_zero(): + """``_safe_sqrt`` is bit-exact vs ``sqrt`` forward, and finite in backward at 0. + + Runs on CPU — the sqrt-at-zero derivative is a pure autograd property with no + bf16 or CUDA involvement. Pins both halves of the contract the SSIM bound relies + on: identical forward values, identical gradients where the input is positive, + and a zero (not NaN) gradient at the exact zeros. + """ + values = torch.tensor([0.0, 1e-12, 0.25, 1.0, 4.0]) + + plain = values.detach().clone().requires_grad_(True) + plain.sqrt().sum().backward() + + safe = values.detach().clone().requires_grad_(True) + safe_out = _safe_sqrt(safe) + safe_out.sum().backward() + + torch.testing.assert_close(safe_out, values.sqrt(), rtol=0, atol=0) + assert torch.isfinite(safe.grad).all(), f"safe grad not finite: {safe.grad}" + assert safe.grad[0] == 0.0, f"expected zero gradient at x=0, got {safe.grad[0]}" + # Unguarded sqrt is the failure being guarded against. + assert not torch.isfinite(plain.grad[0]), "expected unguarded sqrt to blow up at x=0" + # Everywhere the input is positive the two must agree bit-for-bit. + torch.testing.assert_close(safe.grad[1:], plain.grad[1:], rtol=0, atol=0) diff --git a/packages/viscy-utils/tests/test_prediction_metadata.py b/packages/viscy-utils/tests/test_prediction_metadata.py new file mode 100644 index 000000000..6046c4ff1 --- /dev/null +++ b/packages/viscy-utils/tests/test_prediction_metadata.py @@ -0,0 +1,202 @@ +"""Tests for the checkpoint content hash shared by prediction writers and caches.""" + +import hashlib +import json +import os +import shutil +import time +from pathlib import Path + +import pytest + +from viscy_utils import prediction_metadata +from viscy_utils.prediction_metadata import checkpoint_sha256_12, prediction_run + + +def test_checkpoint_sha256_12(tmp_path: Path) -> None: + """Returns the first 12 hex chars of sha256; differs for different content.""" + file_a = tmp_path / "a.ckpt" + file_b = tmp_path / "b.ckpt" + file_a.write_bytes(b"model-weights-a") + file_b.write_bytes(b"model-weights-b") + + h_a = checkpoint_sha256_12(file_a) + h_b = checkpoint_sha256_12(file_b) + assert len(h_a) == 12 + assert len(h_b) == 12 + assert h_a != h_b + assert checkpoint_sha256_12(file_a) == h_a # deterministic + + +def test_checkpoint_sha256_12_writes_and_reuses_sidecar(tmp_path: Path, monkeypatch) -> None: + """First call writes ``.sha256`` with the file's size and mtime; second call skips the hash.""" + ckpt = tmp_path / "last.ckpt" + ckpt.write_bytes(b"weights") + h1 = checkpoint_sha256_12(ckpt) + sidecar = tmp_path / "last.ckpt.sha256" + recorded = json.loads(sidecar.read_text()) + assert recorded["sha256"][:12] == h1 + assert len(recorded["sha256"]) == 64 + stat = ckpt.stat() + assert recorded["size"] == stat.st_size + assert recorded["mtime_ns"] == stat.st_mtime_ns + assert recorded["ctime_ns"] == stat.st_ctime_ns + assert recorded["ino"] == stat.st_ino + + calls = {"n": 0} + real_sha256 = hashlib.sha256 + + def tracking_sha256(*args, **kwargs): + calls["n"] += 1 + return real_sha256(*args, **kwargs) + + monkeypatch.setattr(prediction_metadata.hashlib, "sha256", tracking_sha256) + h2 = checkpoint_sha256_12(ckpt) + assert h2 == h1 + assert calls["n"] == 0 + + +def test_checkpoint_sha256_12_recomputes_when_sidecar_older(tmp_path: Path) -> None: + """Newer ckpt mtime invalidates the sidecar and forces a recompute.""" + ckpt = tmp_path / "last.ckpt" + ckpt.write_bytes(b"weights-v1") + h1 = checkpoint_sha256_12(ckpt) + + ckpt.write_bytes(b"weights-v2") + sidecar = tmp_path / "last.ckpt.sha256" + old = sidecar.stat().st_mtime + os.utime(ckpt, (old + 10, old + 10)) + + h2 = checkpoint_sha256_12(ckpt) + assert h2 != h1 + assert json.loads(sidecar.read_text())["sha256"][:12] == h2 + + +def test_checkpoint_sha256_12_recomputes_after_replacement_with_older_mtime(tmp_path: Path) -> None: + """A same-size checkpoint copied in with a preserved, older mtime must not reuse the old digest. + + Only exact size and ``st_mtime_ns`` equality qualifies the sidecar; being + newer than the checkpoint is not evidence the content is unchanged. + """ + ckpt = tmp_path / "last.ckpt" + ckpt.write_bytes(b"weights-v1") + h1 = checkpoint_sha256_12(ckpt) + sidecar = tmp_path / "last.ckpt.sha256" + + ckpt.write_bytes(b"weights-v2") # same size as v1 + older = sidecar.stat().st_mtime - 3600 + os.utime(ckpt, (older, older)) + + h2 = checkpoint_sha256_12(ckpt) + assert h2 == hashlib.sha256(b"weights-v2").hexdigest()[:12] + assert h2 != h1 + + +def test_checkpoint_sha256_12_recomputes_after_replacement_preserving_size_and_mtime(tmp_path: Path) -> None: + """A same-size copy that carries the old mtime along still gets a new ctime, so the sidecar is dropped.""" + ckpt = tmp_path / "last.ckpt" + ckpt.write_bytes(b"weights-v1") + h1 = checkpoint_sha256_12(ckpt) + before = ckpt.stat() + replacement = tmp_path / "replacement.ckpt" + replacement.write_bytes(b"weights-v2") + os.utime(replacement, ns=(before.st_atime_ns, before.st_mtime_ns)) + time.sleep(0.05) # ctime advances per kernel tick, not per write + + shutil.copy2(replacement, ckpt) + + assert ckpt.stat().st_mtime_ns == before.st_mtime_ns + assert ckpt.stat().st_size == before.st_size + h2 = checkpoint_sha256_12(ckpt) + assert h2 == hashlib.sha256(b"weights-v2").hexdigest()[:12] + assert h2 != h1 + + +@pytest.mark.parametrize("content", ["not-a-hex-digest\n", "a" * 64 + "\n", '{"sha256": "abc"}\n']) +def test_checkpoint_sha256_12_ignores_unusable_sidecars(tmp_path: Path, content: str) -> None: + """Corrupt, legacy plain-hex, and incomplete sidecars are treated as missing and recomputed.""" + ckpt = tmp_path / "last.ckpt" + ckpt.write_bytes(b"weights") + sidecar = tmp_path / "last.ckpt.sha256" + sidecar.write_text(content) + + h = checkpoint_sha256_12(ckpt) + assert h == hashlib.sha256(b"weights").hexdigest()[:12] + assert json.loads(sidecar.read_text())["sha256"][:12] == h + + +def test_checkpoint_sha256_12_without_memo_ignores_the_sidecar(tmp_path: Path) -> None: + """``memoize=False`` hashes the bytes: it neither trusts a sidecar that matches the file nor leaves one.""" + ckpt = tmp_path / "model.ckpt" + ckpt.write_bytes(b"weights") + sidecar = tmp_path / "model.ckpt.sha256" + expected = hashlib.sha256(b"weights").hexdigest()[:12] + + assert checkpoint_sha256_12(ckpt, memoize=False) == expected + assert not sidecar.exists() + + stat = ckpt.stat() + forged = json.dumps( + { + "sha256": "0" * 64, + "size": stat.st_size, + "mtime_ns": stat.st_mtime_ns, + "ctime_ns": stat.st_ctime_ns, + "ino": stat.st_ino, + } + ) + sidecar.write_text(forged + "\n") + assert checkpoint_sha256_12(ckpt) == "0" * 12 + assert checkpoint_sha256_12(ckpt, memoize=False) == expected + assert sidecar.read_text() == forged + "\n" + + +def test_prediction_run_hashes_the_checkpoint_bytes_not_a_sidecar(tmp_path: Path) -> None: + """The reviewer's probe: a same-size copy carrying the old mtime, made right after hashing, gets its own digest.""" + ckpt = tmp_path / "model.ckpt" + replacement = tmp_path / "replacement.ckpt" + ckpt.write_bytes(b"weights-a") + replacement.write_bytes(b"weights-b") + before = ckpt.stat() + os.utime(replacement, ns=(before.st_atime_ns, before.st_mtime_ns)) + + def identity() -> dict: + return prediction_run(array_key="0", z_window_size=4, z_reduction="blend", checkpoint_path=ckpt) + + first = identity() + shutil.copy2(replacement, ckpt) + second = identity() + + assert first["checkpoint_sha256_12"] == hashlib.sha256(b"weights-a").hexdigest()[:12] + assert second["checkpoint_sha256_12"] == hashlib.sha256(b"weights-b").hexdigest()[:12] + assert not (tmp_path / "model.ckpt.sha256").exists() + + +def test_checkpoint_sha256_12_read_only_dir(tmp_path: Path) -> None: + """Read-only parent dir does not raise; digest still returned.""" + ckpt_dir = tmp_path / "frozen" + ckpt_dir.mkdir() + ckpt = ckpt_dir / "last.ckpt" + ckpt.write_bytes(b"weights") + os.chmod(ckpt_dir, 0o555) + try: + h = checkpoint_sha256_12(ckpt) + assert len(h) == 12 + assert not (ckpt_dir / "last.ckpt.sha256").exists() + finally: + os.chmod(ckpt_dir, 0o755) + + +def test_marker_identity_strips_only_provenance_fields() -> None: + """A moved checkpoint keeps its identity; any field that shapes the voxels stays in it.""" + run = {"array_key": "0", "checkpoint_path": "/a/model.ckpt", "checkpoint_sha256_12": "abc"} + marker = prediction_metadata.completion_marker([1, 2, 3, 4], run) + assert prediction_metadata.marker_identity(marker) == { + "source_shape": [1, 2, 3, 4], + "array_key": "0", + "checkpoint_sha256_12": "abc", + } + assert prediction_metadata.marker_identity({**marker, "checkpoint_path": "/b/model.ckpt"}) == ( + prediction_metadata.marker_identity(marker) + ) + assert prediction_metadata.marker_identity(None) is None diff --git a/packages/viscy-utils/tests/test_prediction_writer.py b/packages/viscy-utils/tests/test_prediction_writer.py index 82aa56b6e..292a41986 100644 --- a/packages/viscy-utils/tests/test_prediction_writer.py +++ b/packages/viscy-utils/tests/test_prediction_writer.py @@ -1,9 +1,954 @@ """Tests for prediction writer blending utilities.""" +import hashlib +import json +import threading +import time +from types import SimpleNamespace + import numpy as np +import pytest import torch +from iohub import open_ome_zarr +from lightning.pytorch import LightningModule, Trainer + +from viscy_data import HCSDataModule +from viscy_data.sliding_window import SlidingWindowDataset +from viscy_utils.callbacks import prediction_writer +from viscy_utils.callbacks.prediction_writer import HCSPredictionWriter, _blend_in, _store_into, _torch_factors +from viscy_utils.prediction_metadata import ( + PREDICTED_Z_PLANES_KEY, + PREDICTION_COMPLETE_KEY, + completion_marker, + mark_complete, + mark_started, + plane_predict_unfinished, + predicted_z_planes, + prediction_complete, + prediction_run, + record_z_planes, + same_run, + started_marker, + z_planes_sha256_12, +) + +Z_SIZE = 16 +Z_WINDOW = 8 +#: The buffered writer blends on the prediction's device, so its bytes are checked on every device present. +ACCELERATORS = [ + "cpu", + pytest.param("gpu", marks=pytest.mark.skipif(not torch.cuda.is_available(), reason="no CUDA device")), +] + + +class _ZStampModule(LightningModule): + """Predict a per-plane stamp ``10 * z_window_start + offset_in_window``. + + Every plane of the assembled volume then names the forward pass and the + within-window position it came from, so the two ``z_reduction`` modes are + distinguishable from the store alone. + """ + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return x + + def predict_step(self, batch, batch_idx: int, dataloader_idx: int = 0) -> torch.Tensor: + source = batch["source"] + depth = source.shape[-3] + offsets = torch.arange(depth, dtype=torch.float32, device=source.device) + starts = torch.as_tensor([int(z) for z in batch["index"][2]], dtype=torch.float32, device=source.device) + stamp = 10.0 * starts.view(-1, 1) + offsets.view(1, -1) + return stamp.view(-1, 1, depth, 1, 1).expand(-1, 1, depth, *source.shape[-2:]).clone() + + +def _run_predict(tmp_path, z_reduction): + """Predict a synthetic single-position plate and return the written CZYX volume.""" + source_store = tmp_path / f"source_{z_reduction}.zarr" + with open_ome_zarr(source_store, layout="hcs", mode="w-", channel_names=["Phase3D", "Nuclei"]) as plate: + position = plate.create_position("0", "0", "0") + position.create_zeros("0", (1, 2, Z_SIZE, 8, 8), dtype=np.float32) + data_module = HCSDataModule( + data_path=str(source_store), + source_channel=["Phase3D"], + target_channel=["Nuclei"], + z_window_size=Z_WINDOW, + batch_size=1, + num_workers=0, + yx_patch_size=[8, 8], + normalizations=[], + augmentations=[], + ) + output_store = tmp_path / f"prediction_{z_reduction}.zarr" + Trainer( + accelerator="cpu", + logger=False, + enable_progress_bar=False, + callbacks=[HCSPredictionWriter(str(output_store), z_reduction=z_reduction)], + ).predict(_ZStampModule(), datamodule=data_module, return_predictions=False) + with open_ome_zarr(output_store, mode="r") as plate: + return np.asarray(plate["0/0/0"]["0"][0, 0, :, 0, 0]) + + +def test_predict_z_reduction_blend_averages_every_covering_window(tmp_path): + """``blend`` makes each plane the unweighted mean of the windows covering it.""" + written = _run_predict(tmp_path, "blend") + n_windows = Z_SIZE - Z_WINDOW + 1 + expected = [ + np.mean([10 * s + (p - s) for s in range(max(0, p - Z_WINDOW + 1), min(n_windows - 1, p) + 1)]) + for p in range(Z_SIZE) + ] + np.testing.assert_allclose(written, expected, rtol=1e-5) + + +def test_predict_z_reduction_center_takes_one_pass_per_plane(tmp_path): + """``center`` gives each plane the window centered on it, and covers the edges.""" + written = _run_predict(tmp_path, "center") + center = Z_WINDOW // 2 + last = Z_SIZE - Z_WINDOW + expected = [] + for p in range(Z_SIZE): + # Interior planes come from their own window's center; the leading planes + # from window 0 and the trailing ones from the last window, since no + # window is centered on them. + start = min(max(0, p - center), last) + expected.append(10 * start + (p - start)) + np.testing.assert_array_equal(written, expected) + interior = range(center, last + center + 1) + assert all(written[p] % 10 == center for p in interior) + + +def test_completion_marker_layout(tmp_path): + """One position attribute maps each prediction channel to what produced it. + + The launcher's resume check and test fixtures read this literal layout, so a + change here is a contract change, not a refactor. + """ + _run_predict(tmp_path, "blend") + run = prediction_run(array_key="0", z_window_size=Z_WINDOW, z_reduction="blend", checkpoint_path=None) + with open_ome_zarr(tmp_path / "prediction_blend.zarr", mode="r") as plate: + position = plate["0/0/0"] + assert position.zattrs[PREDICTION_COMPLETE_KEY] == { + "Nuclei_prediction": { + "source_shape": [1, Z_SIZE, 8, 8], + "array_key": "0", + "z_window_size": Z_WINDOW, + "z_reduction": "blend", + "checkpoint_path": None, + "checkpoint_sha256_12": None, + "settings_sha256_12": None, + } + } + assert prediction_complete(position, ["Nuclei_prediction"], completion_marker([1, Z_SIZE, 8, 8], run)) + assert not prediction_complete(position, ["Nuclei_prediction"], completion_marker([2, Z_SIZE, 8, 8], run)) + + +def test_completion_helpers_keep_other_channels(tmp_path): + """Completing or restarting one channel must leave the other channels' markers intact.""" + run = prediction_run(array_key="0", z_window_size=1, z_reduction="blend", checkpoint_path=None) + marker = completion_marker([1, 2, 3, 4], run) + with open_ome_zarr(tmp_path / "plate.zarr", layout="hcs", mode="w-", channel_names=["A", "B"]) as plate: + position = plate.create_position("0", "0", "0") + mark_complete(position, ["A"], marker) + mark_complete(position, ["B"], marker) + mark_started(position, ["A"], run) + assert position.zattrs[PREDICTION_COMPLETE_KEY] == {"A": started_marker(run), "B": marker} + assert prediction_complete(position, ["B"], marker) + assert not prediction_complete(position, ["A", "B"], marker) + + +class _WindowFunctionModule(LightningModule): + """Predict a nonlinear function of each window's input and position, so blending and window order matter. + + ``center_plane`` returns only the window's center plane (a 2D model, ``target_2d``); ``dtype`` sets the + prediction dtype. + """ + + def __init__(self, center_plane: bool = False, dtype: torch.dtype = torch.float32) -> None: + super().__init__() + self.center_plane = center_plane + self.out_dtype = dtype + + def predict_step(self, batch, batch_idx: int, dataloader_idx: int = 0) -> torch.Tensor: + source = batch["source"] + offsets = torch.arange(source.shape[-3], dtype=source.dtype, device=source.device).view(1, 1, -1, 1, 1) + starts = torch.as_tensor([int(z) for z in batch["index"][2]], dtype=source.dtype, device=source.device) + out = torch.sin(source * (1.0 + 0.1 * offsets)) + 0.01 * starts.view(-1, 1, 1, 1, 1) + if self.center_plane: + mid = out.shape[-3] // 2 + out = out[:, :, mid : mid + 1] + return out.to(self.out_dtype) + + +class _PerWindowWriter(HCSPredictionWriter): + """Reference: the per-window zarr read-modify-write (the ``write_input`` path) in every mode.""" + + def on_predict_start(self, trainer: Trainer, pl_module: LightningModule) -> None: + super().on_predict_start(trainer, pl_module) + self._write_once = False + + +#: Per-timepoint planes of the two FOVs of ``_write_random_source``. +RANDOM_SOURCE_PLANES = {"0/0/0": [[0, 1, 2], [4, 5, 6]], "0/0/1": [[8, 9, 10], [3, 4, 5]]} + + +def _write_random_source(path) -> None: + """Two FOVs of random ``(2, 2, 11, 8, 8)`` float32 Phase3D/Nuclei.""" + rng = np.random.default_rng(0) + with open_ome_zarr(path, layout="hcs", mode="w-", channel_names=["Phase3D", "Nuclei"]) as plate: + for fov in RANDOM_SOURCE_PLANES: + plate.create_position(*fov.split("/")).create_image( + "0", rng.normal(size=(2, 2, 11, 8, 8)).astype(np.float32) + ) + + +def _read_store(path) -> dict: + with open_ome_zarr(path, mode="r") as plate: + return {name: (np.asarray(pos["0"]), pos["0"].chunks, dict(pos.zattrs)) for name, pos in plate.positions()} + + +def _assert_same_bytes(reference: dict, buffered: dict) -> None: + assert buffered.keys() == reference.keys() + for name, (voxels, chunks, attrs) in reference.items(): + new_voxels, new_chunks, new_attrs = buffered[name] + assert (new_voxels.shape, new_voxels.dtype, new_chunks) == (voxels.shape, voxels.dtype, chunks) + assert new_voxels.tobytes() == voxels.tobytes() + assert new_attrs == attrs + + +@pytest.mark.parametrize("accelerator", ACCELERATORS) +@pytest.mark.parametrize("mode", ["blend", "center", "target_2d", "z_planes"]) +@pytest.mark.parametrize("batch_size", [1, 3]) +@pytest.mark.parametrize("limit_batches", [None, 5]) +def test_buffered_volumes_store_the_same_bytes_as_per_window_writes( + tmp_path, accelerator, mode, batch_size, limit_batches +): + """Assembling each volume in memory and writing it once stores the bytes and markers per-window writes store. + + Covers 3D windows (blend, center) and a 2D model's padded center plane (``target_2d``); a plane selection + takes the per-window path on both sides. The buffered volume lives on the prediction's device, so on a GPU + the float64 blend runs there against the reference's numpy blend. + ``limit_batches=5`` stops inside a volume, so the partial volume flushed at predict end is compared too. The + reference reads per window too (``num_workers=1`` disables the volume cache). + """ + source = tmp_path / "source.zarr" + _write_random_source(source) + planes_file = None + if mode == "z_planes": + planes_file = tmp_path / "planes.json" + planes_file.write_text(json.dumps({"positions": RANDOM_SOURCE_PLANES})) + stores = {} + for writer_cls in (_PerWindowWriter, HCSPredictionWriter): + data_module = HCSDataModule( + data_path=str(source), + source_channel=["Phase3D"], + target_channel=["Nuclei"], + z_window_size=1 if mode == "z_planes" else 4, + target_2d=mode == "target_2d", + batch_size=batch_size, + num_workers=1 if writer_cls is _PerWindowWriter else 0, + yx_patch_size=[8, 8], + normalizations=[], + augmentations=[], + predict_z_planes=planes_file, + ) + output = tmp_path / f"pred_{writer_cls.__name__}.zarr" + writer = writer_cls(str(output), z_reduction="center" if mode == "center" else "blend") + Trainer( + accelerator=accelerator, + devices=1, + logger=False, + enable_progress_bar=False, + limit_predict_batches=limit_batches, + callbacks=[writer], + ).predict( + _WindowFunctionModule(center_plane=mode == "target_2d"), datamodule=data_module, return_predictions=False + ) + assert writer._write_once is (writer_cls is HCSPredictionWriter and mode != "z_planes") + assert data_module.predict_dataset.cache_volume is (writer_cls is HCSPredictionWriter and mode != "z_planes") + stores[writer_cls] = _read_store(output) + _assert_same_bytes(stores[_PerWindowWriter], stores[HCSPredictionWriter]) + + +def test_overwrite_keeps_the_planes_a_padded_run_does_not_predict(tmp_path): + """A ``target_2d`` run over a 3D store leaves its padding planes as stored, as per-window writes do.""" + source = tmp_path / "source.zarr" + _write_random_source(source) + + def predict(output, writer, z_window_size: int, target_2d: bool) -> None: + data_module = HCSDataModule( + data_path=str(source), + source_channel=["Phase3D"], + target_channel=["Nuclei"], + z_window_size=z_window_size, + target_2d=target_2d, + batch_size=1, + num_workers=0, + yx_patch_size=[8, 8], + normalizations=[], + augmentations=[], + ) + Trainer(accelerator="cpu", logger=False, enable_progress_bar=False, callbacks=[writer]).predict( + _WindowFunctionModule(center_plane=target_2d), datamodule=data_module, return_predictions=False + ) + + stores = {} + for writer_cls in (_PerWindowWriter, HCSPredictionWriter): + output = tmp_path / f"pred_{writer_cls.__name__}.zarr" + predict(output, HCSPredictionWriter(str(output)), z_window_size=4, target_2d=False) + predict(output, writer_cls(str(output), overwrite=True), z_window_size=5, target_2d=True) + stores[writer_cls] = _read_store(output) + voxels = next(iter(stores[_PerWindowWriter].values()))[0] + assert np.abs(voxels[:, -1, :2]).sum() > 0, "the 3D run's planes below the padding must survive" + _assert_same_bytes(stores[_PerWindowWriter], stores[HCSPredictionWriter]) + + +class _DeeperModule(_WindowFunctionModule): + """Predict one plane more than the window, so the last windows reach past the source depth.""" + + def predict_step(self, batch, batch_idx: int, dataloader_idx: int = 0) -> torch.Tensor: + out = super().predict_step(batch, batch_idx, dataloader_idx) + return torch.cat([out, out[:, :, -1:] + 1.0], dim=2) + + +def test_buffered_volume_grows_for_a_prediction_deeper_than_its_window(tmp_path): + """Planes past the source depth extend the volume, as the per-window resize does.""" + source = tmp_path / "source.zarr" + _write_random_source(source) + stores = {} + for writer_cls in (_PerWindowWriter, HCSPredictionWriter): + output = tmp_path / f"pred_{writer_cls.__name__}.zarr" + data_module = HCSDataModule( + data_path=str(source), + source_channel=["Phase3D"], + target_channel=["Nuclei"], + z_window_size=4, + batch_size=1, + num_workers=0, + yx_patch_size=[8, 8], + normalizations=[], + augmentations=[], + ) + Trainer( + accelerator="cpu", logger=False, enable_progress_bar=False, callbacks=[writer_cls(str(output))] + ).predict(_DeeperModule(), datamodule=data_module, return_predictions=False) + stores[writer_cls] = _read_store(output) + assert {voxels.shape[2] for voxels, _, _ in stores[HCSPredictionWriter].values()} == {12} + _assert_same_bytes(stores[_PerWindowWriter], stores[HCSPredictionWriter]) + + +def test_buffered_volume_takes_the_dtype_of_an_existing_array(tmp_path): + """Overwriting a float32 store with float16 predictions blends in float32, as per-window writes do.""" + source = tmp_path / "source.zarr" + _write_random_source(source) + stores = {} + for writer_cls in (_PerWindowWriter, HCSPredictionWriter): + for dtype, overwrite in ((torch.float32, False), (torch.float16, True)): + output = tmp_path / f"pred_{writer_cls.__name__}.zarr" + data_module = HCSDataModule( + data_path=str(source), + source_channel=["Phase3D"], + target_channel=["Nuclei"], + z_window_size=4, + batch_size=1, + num_workers=0, + yx_patch_size=[8, 8], + normalizations=[], + augmentations=[], + ) + Trainer( + accelerator="cpu", + logger=False, + enable_progress_bar=False, + callbacks=[writer_cls(str(output), overwrite=overwrite)], + ).predict(_WindowFunctionModule(dtype=dtype), datamodule=data_module, return_predictions=False) + stores[writer_cls] = _read_store(output) + assert {voxels.dtype for voxels, _, _ in stores[HCSPredictionWriter].values()} == {np.dtype(np.float32)} + _assert_same_bytes(stores[_PerWindowWriter], stores[HCSPredictionWriter]) + + +@pytest.mark.parametrize("accelerator", ACCELERATORS) +def test_buffered_float16_volume_rounds_the_blend_like_numpy(tmp_path, accelerator): + """A float16 store from float16 predictions rounds each float64 blend to float16 as the numpy write does.""" + source = tmp_path / "source.zarr" + _write_random_source(source) + stores = {} + for writer_cls in (_PerWindowWriter, HCSPredictionWriter): + output = tmp_path / f"pred_{writer_cls.__name__}.zarr" + Trainer( + accelerator=accelerator, + devices=1, + logger=False, + enable_progress_bar=False, + callbacks=[writer_cls(str(output))], + ).predict( + _WindowFunctionModule(dtype=torch.float16), datamodule=_ones_data_module(source), return_predictions=False + ) + stores[writer_cls] = _read_store(output) + assert {voxels.dtype for voxels, _, _ in stores[HCSPredictionWriter].values()} == {np.dtype(np.float16)} + _assert_same_bytes(stores[_PerWindowWriter], stores[HCSPredictionWriter]) + + +def test_store_into_rounds_float64_to_float16_once(): + """Torch rounds float64 to float16 through float32; the volume must hold numpy's single rounding.""" + # Just above the midpoint of 1 and the next float16: float32 rounds it onto the midpoint, then a tie to even. + value = 1 + 2.0**-11 + 2.0**-40 + assert torch.tensor(value, dtype=torch.float64).to(torch.float16).item() == 1.0 + volume = torch.zeros(1, 2, 1, 1, dtype=torch.float16) + _store_into(volume, slice(1, 2), torch.full((1, 1, 1, 1), value, dtype=torch.float64)) + expected = np.float16(value) + assert expected == 1 + 2.0**-10 + assert volume[0, 1, 0, 0].item() == expected + assert volume[0, 0, 0, 0].item() == 0 + + +class _SlowWriter(HCSPredictionWriter): + """Hold every volume write on the flush thread for a while, so a marker that does not wait for it shows.""" + + def _write_volume(self, *args, **kwargs) -> None: + time.sleep(0.2) + super()._write_volume(*args, **kwargs) + + +def test_completion_marker_follows_the_flushed_voxels(tmp_path, monkeypatch): + """A FOV is marked complete only after every one of its volumes is in the store, and the writes run off-thread.""" + source = tmp_path / "source.zarr" + _write_random_source(source) + output = tmp_path / "pred.zarr" + marked = [] + real_mark_complete = prediction_writer.mark_complete + + def checked_mark_complete(position, channels, marker): + # Every voxel of this run is nonzero, so a zero names a volume not yet written. + with open_ome_zarr(output, mode="r") as plate: + voxels = np.asarray(plate[position.zgroup.path]["0"]) + marked.append((position.zgroup.path, voxels.shape, bool((voxels != 0).all()), threading.current_thread())) + real_mark_complete(position, channels, marker) + + monkeypatch.setattr(prediction_writer, "mark_complete", checked_mark_complete) + data_module = _ones_data_module(source, batch_size=3) + Trainer(accelerator="cpu", logger=False, enable_progress_bar=False, callbacks=[_SlowWriter(str(output))]).predict( + _OnesModule(), datamodule=data_module, return_predictions=False + ) + assert [(name, shape, full) for name, shape, full, _ in marked] == [ + (fov, (2, 1, 11, 8, 8), True) for fov in RANDOM_SOURCE_PLANES + ] + assert all(thread is not threading.main_thread() for *_, thread in marked) + + +class _FailingWriter(HCSPredictionWriter): + """Fail the write of one volume on the flush thread.""" + + def __init__(self, *args, fail_volume: int, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.fail_volume = fail_volume + self.n_volumes = 0 + self.closed = False + + def on_predict_start(self, trainer: Trainer, pl_module: LightningModule) -> None: + super().on_predict_start(trainer, pl_module) + close = self.plate.close + + def recording_close() -> None: + self.closed = True + close() + + self.plate.close = recording_close + + def _write_volume(self, *args, **kwargs) -> None: + self.n_volumes += 1 + if self.n_volumes == self.fail_volume: + raise OSError("disk full") + super()._write_volume(*args, **kwargs) + + +@pytest.mark.parametrize("fail_volume", [2, 4]) +def test_a_failed_volume_write_fails_the_predict(tmp_path, fail_volume): + """A write error on the flush thread stops the predict, and its FOV is never marked complete. + + Volume 2 is FOV 0's last: the error stops the run before FOV 1 is written. Volume 4 is FOV 1's last, so the + error surfaces at predict end, after FOV 0 completed. Either way the store is closed. + """ + source = tmp_path / "source.zarr" + _write_random_source(source) + writer = _FailingWriter(str(tmp_path / "pred.zarr"), fail_volume=fail_volume) + with pytest.raises(OSError, match="disk full"): + Trainer(accelerator="cpu", logger=False, enable_progress_bar=False, callbacks=[writer]).predict( + _OnesModule(), datamodule=_ones_data_module(source), return_predictions=False + ) + # Raised mid-run or at predict end, the last write finishes and the store is closed all the same. + assert writer.closed + assert writer._flush_pool._shutdown + with open_ome_zarr(tmp_path / "pred.zarr", mode="r") as plate: + markers = [pos.zattrs[PREDICTION_COMPLETE_KEY]["Nuclei_prediction"] for _, pos in plate.positions()] + assert ["source_shape" in marker for marker in markers] == ([False] if fail_volume == 2 else [True, False]) + + +class _RepeatFirstWindowDataModule(HCSDataModule): + """Serve the first window again after the first volume is done, as a stray retry would.""" + + def predict_dataloader(self): + # FOV 0, t=0 has 8 windows; its first comes back after the volume was flushed. + return torch.utils.data.DataLoader(torch.utils.data.Subset(self.predict_dataset, [*range(8), 0]), batch_size=1) + + +def test_a_window_arriving_twice_is_refused(tmp_path): + """A repeated window would blend into a flushed volume's zero buffer and re-mark the FOV; it must raise.""" + source = tmp_path / "source.zarr" + _write_random_source(source) + data_module = _RepeatFirstWindowDataModule( + data_path=str(source), + source_channel=["Phase3D"], + target_channel=["Nuclei"], + z_window_size=4, + batch_size=1, + num_workers=0, + yx_patch_size=[8, 8], + normalizations=[], + augmentations=[], + ) + with pytest.raises(RuntimeError, match="arrived twice"): + _predict_ones_with(HCSPredictionWriter(str(tmp_path / "pred.zarr")), data_module) + -from viscy_utils.callbacks.prediction_writer import _blend_in +def test_cached_volume_windows_equal_per_window_reads(tmp_path): + """Windows cut from a cached (position, t) volume equal windows read from zarr one by one.""" + source = tmp_path / "source.zarr" + _write_random_source(source) + with open_ome_zarr(source, mode="r") as plate: + positions = [position for _, position in plate.positions()] + datasets = [ + SlidingWindowDataset( + positions=positions, + channels={"source": ["Phase3D", "Nuclei"]}, + z_window_size=4, + load_normalization_metadata=False, + cache_volume=cache_volume, + ) + for cache_volume in (False, True) + ] + assert len(datasets[0]) == len(datasets[1]) == 2 * 2 * 8 + for index in range(len(datasets[0])): + reference, cached = datasets[0][index], datasets[1][index] + assert reference["index"] == cached["index"] + assert torch.equal(reference["source"], cached["source"]) + assert datasets[1]._volume_key is not None + + +class _OnesModule(LightningModule): + def predict_step(self, batch, batch_idx: int, dataloader_idx: int = 0) -> torch.Tensor: + return torch.ones_like(batch["source"]) + + +def _write_source(path, fovs: list[str], z_size: int = 4) -> None: + """Create a two-channel source plate with one ``(1, 2, Z, 8, 8)`` array per FOV.""" + with open_ome_zarr(path, layout="hcs", mode="w-", channel_names=["Phase3D", "Nuclei"]) as plate: + for fov in fovs: + plate.create_position(*fov.split("/")).create_zeros("0", (1, 2, z_size, 8, 8), dtype=np.float32) + + +def _ones_data_module(source, *, exclude=None, target="Nuclei", batch_size=1) -> HCSDataModule: + """Depth-4 windows over every FOV of ``source``, predicting ``target``.""" + return HCSDataModule( + data_path=str(source), + source_channel=["Phase3D"], + target_channel=[target], + z_window_size=4, + batch_size=batch_size, + num_workers=0, + yx_patch_size=[8, 8], + normalizations=[], + augmentations=[], + exclude_fov_names=exclude, + ) + + +def _predict_ones_with(writer: HCSPredictionWriter, data_module: HCSDataModule, *, limit_batches=None) -> None: + """Run ``_OnesModule`` through ``writer`` on the CPU.""" + Trainer( + accelerator="cpu", + logger=False, + enable_progress_bar=False, + limit_predict_batches=limit_batches, + callbacks=[writer], + ).predict(_OnesModule(), datamodule=data_module, return_predictions=False) + + +def _predict_ones( + source, output, *, checkpoint_path=None, exclude=None, overwrite=False, limit_batches=None, target="Nuclei" +) -> None: + """Predict all-ones for every FOV of ``source`` with depth-4 windows into ``output``.""" + writer = HCSPredictionWriter(str(output), overwrite=overwrite, checkpoint_path=checkpoint_path) + _predict_ones_with(writer, _ones_data_module(source, exclude=exclude, target=target), limit_batches=limit_batches) + + +def test_appending_a_channel_keeps_existing_predictions(tmp_path): + """A store holding another model's channel gains ours; its voxels and marker are untouched.""" + source = tmp_path / "source.zarr" + output = tmp_path / "pred.zarr" + _write_source(source, ["0/0/0", "0/0/1"]) + other_run = prediction_run(array_key="0", z_window_size=4, z_reduction="blend", checkpoint_path=None) + with open_ome_zarr(output, layout="hcs", mode="w-", channel_names=["Other_prediction"]) as plate: + for fov in ("0/0/0", "0/0/1"): + position = plate.create_position(*fov.split("/")) + position.create_zeros("0", (1, 1, 4, 8, 8), dtype=np.float32)[:] = 7 + mark_complete(position, ["Other_prediction"], completion_marker([1, 4, 8, 8], other_run)) + + _predict_ones(source, output) + + with open_ome_zarr(output, mode="r") as plate: + assert plate.channel_names == ["Other_prediction", "Nuclei_prediction"] + for fov in ("0/0/0", "0/0/1"): + position = plate[fov] + assert position.channel_names == ["Other_prediction", "Nuclei_prediction"] + np.testing.assert_array_equal(position["0"][:, 0], 7) + np.testing.assert_array_equal(position["0"][:, 1], 1) + marker = position.zattrs[PREDICTION_COMPLETE_KEY] + assert marker["Other_prediction"] == completion_marker([1, 4, 8, 8], other_run) + assert "Nuclei_prediction" in marker + + +def test_writer_refuses_positions_that_would_disagree_on_channel_order(tmp_path): + """Channel indices are plate-wide, so positions must end up with one channel order; check before writing.""" + source = tmp_path / "source.zarr" + output = tmp_path / "pred.zarr" + _write_source(source, ["0/0/0", "0/0/1"]) + with open_ome_zarr(output, layout="hcs", mode="w-", channel_names=["Other_prediction"]) as plate: + for fov in ("0/0/0", "0/0/1"): + plate.create_position(*fov.split("/")).create_zeros("0", (1, 1, 4, 8, 8), dtype=np.float32) + # Separate handles: positions created in one session share one channel list in memory. + for fov, channel in (("0/0/0", "Nuclei_prediction"), ("0/0/1", "Third")): + with open_ome_zarr(output, mode="r+") as plate: + plate[fov].append_channel(channel, resize_arrays=True) + + with pytest.raises(ValueError, match="channel order"): + _predict_ones(source, output, overwrite=True) + + with open_ome_zarr(output, mode="r") as plate: + assert plate["0/0/1"].channel_names == ["Other_prediction", "Third"] + + +def test_interrupted_fov_carries_a_started_marker(tmp_path): + """A FOV the writer created but never finished names the run writing it, unlike pre-marker outputs.""" + source = tmp_path / "source.zarr" + _write_source(source, ["0/0/0"], z_size=8) + _predict_ones(source, tmp_path / "pred.zarr", limit_batches=2) # 2 of 5 depth windows + + run = prediction_run(array_key="0", z_window_size=4, z_reduction="blend", checkpoint_path=None) + with open_ome_zarr(tmp_path / "pred.zarr", mode="r") as plate: + assert plate["0/0/0"].zattrs[PREDICTION_COMPLETE_KEY] == {"Nuclei_prediction": started_marker(run)} + + +def test_writer_refuses_fovs_predicted_before_markers_existed(tmp_path): + """Channels without any completion attribute cannot be verified, so the store is off limits.""" + source = tmp_path / "source.zarr" + output = tmp_path / "pred.zarr" + _write_source(source, ["0/0/0", "0/0/1"]) + with open_ome_zarr(output, layout="hcs", mode="w-", channel_names=["Nuclei_prediction"]) as plate: + for fov in ("0/0/0", "0/0/1"): + plate.create_position(*fov.split("/")).create_zeros("0", (1, 1, 4, 8, 8), dtype=np.float32) + + with pytest.raises(ValueError, match="before completion markers"): + _predict_ones(source, output, exclude=["0/0/0"], overwrite=True) + + with open_ome_zarr(output, mode="r") as plate: + assert all(PREDICTION_COMPLETE_KEY not in pos.zattrs for _, pos in plate.positions()) + np.testing.assert_array_equal(plate["0/0/1/0"][:], 0) + + +def test_writer_refuses_a_legacy_channel_after_another_channel_was_marked(tmp_path): + """Markers are per channel: predicting a new channel into a legacy store vouches for nothing else.""" + source = tmp_path / "source.zarr" + output = tmp_path / "pred.zarr" + _write_source(source, ["0/0/0", "0/0/1"]) + with open_ome_zarr(output, layout="hcs", mode="w-", channel_names=["Nuclei_prediction"]) as plate: + for fov in ("0/0/0", "0/0/1"): + plate.create_position(*fov.split("/")).create_zeros("0", (1, 1, 4, 8, 8), dtype=np.float32)[:] = 999 + _predict_ones(source, output, target="Other") + with open_ome_zarr(output, mode="r") as plate: + assert set(plate["0/0/0"].zattrs[PREDICTION_COMPLETE_KEY]) == {"Other_prediction"} + + with pytest.raises(ValueError, match="before completion markers"): + _predict_ones(source, output, exclude=["0/0/0"], overwrite=True) + + with open_ome_zarr(output, mode="r") as plate: + for fov in ("0/0/0", "0/0/1"): + np.testing.assert_array_equal(plate[fov]["0"][:, 0], 999) + assert set(plate[fov].zattrs[PREDICTION_COMPLETE_KEY]) == {"Other_prediction"} + + +def test_channels_appended_to_excluded_fovs_carry_this_runs_started_marker(tmp_path): + """Appending a channel allocates zeros in every FOV; excluded FOVs record who did, so a resume can finish them.""" + source = tmp_path / "source.zarr" + output = tmp_path / "pred.zarr" + _write_source(source, ["0/0/0", "0/0/1"]) + _predict_ones(source, output) + _predict_ones(source, output, exclude=["0/0/1"], target="Other") + + run = prediction_run(array_key="0", z_window_size=4, z_reduction="blend", checkpoint_path=None) + complete = completion_marker([1, 4, 8, 8], run) + with open_ome_zarr(output, mode="r") as plate: + assert plate["0/0/0"].zattrs[PREDICTION_COMPLETE_KEY] == { + "Nuclei_prediction": complete, + "Other_prediction": complete, + } + assert plate["0/0/1"].zattrs[PREDICTION_COMPLETE_KEY] == { + "Nuclei_prediction": complete, + "Other_prediction": started_marker(run), + } + np.testing.assert_array_equal(plate["0/0/1/0"][:, 1], 0) + + _predict_ones(source, output, exclude=["0/0/0"], target="Other", overwrite=True) + + with open_ome_zarr(output, mode="r") as plate: + for fov in ("0/0/0", "0/0/1"): + assert plate[fov].zattrs[PREDICTION_COMPLETE_KEY] == { + "Nuclei_prediction": complete, + "Other_prediction": complete, + } + np.testing.assert_array_equal(plate[fov]["0"][:], 1) + + +def test_marker_records_the_checkpoint_content(tmp_path): + """The marker names the checkpoint and hashes its content, so a moved copy still matches.""" + source = tmp_path / "source.zarr" + _write_source(source, ["0/0/0"]) + ckpt = tmp_path / "a.ckpt" + ckpt.write_bytes(b"weights-a") + _predict_ones(source, tmp_path / "pred.zarr", checkpoint_path=str(ckpt)) + + with open_ome_zarr(tmp_path / "pred.zarr", mode="r") as plate: + marker = plate["0/0/0"].zattrs[PREDICTION_COMPLETE_KEY]["Nuclei_prediction"] + assert marker["checkpoint_path"] == str(ckpt) + assert marker["checkpoint_sha256_12"] == hashlib.sha256(b"weights-a").hexdigest()[:12] + moved = tmp_path / "moved.ckpt" + moved.write_bytes(b"weights-a") + other = tmp_path / "b.ckpt" + other.write_bytes(b"weights-b") + assert same_run(marker, prediction_run(array_key="0", z_window_size=4, z_reduction="blend", checkpoint_path=moved)) + assert not same_run( + marker, prediction_run(array_key="0", z_window_size=4, z_reduction="blend", checkpoint_path=other) + ) + + +def test_writer_refuses_a_checkpoint_replaced_after_construction(tmp_path): + """The hash must name the weights the model loaded, so a file swapped after the writer saw it is refused.""" + source = tmp_path / "source.zarr" + output = tmp_path / "pred.zarr" + _write_source(source, ["0/0/0"]) + ckpt = tmp_path / "a.ckpt" + ckpt.write_bytes(b"weights-a") + writer = HCSPredictionWriter(str(output), checkpoint_path=str(ckpt)) + ckpt.write_bytes(b"weights-bb") + + with pytest.raises(RuntimeError, match="changed since this writer was constructed"): + _predict_ones_with(writer, _ones_data_module(source)) + assert not output.exists() + + +def test_writer_refuses_to_mix_checkpoints_across_fovs(tmp_path): + """FOVs outside the run that other weights completed make the store off limits, before any write.""" + source = tmp_path / "source.zarr" + output = tmp_path / "pred.zarr" + _write_source(source, ["0/0/0", "0/0/1"]) + ckpt_a = tmp_path / "a.ckpt" + ckpt_a.write_bytes(b"weights-a") + ckpt_b = tmp_path / "b.ckpt" + ckpt_b.write_bytes(b"weights-b") + _predict_ones(source, output, checkpoint_path=str(ckpt_a)) + with open_ome_zarr(output, mode="r") as plate: + before = {name: pos.zattrs[PREDICTION_COMPLETE_KEY] for name, pos in plate.positions()} + + with pytest.raises(ValueError, match="different checkpoint"): + _predict_ones(source, output, checkpoint_path=str(ckpt_b), exclude=["0/0/0"], overwrite=True) + + with open_ome_zarr(output, mode="r") as plate: + assert {name: pos.zattrs[PREDICTION_COMPLETE_KEY] for name, pos in plate.positions()} == before + + +def test_writer_refuses_multi_device_predict(tmp_path): + """Per-process completion bitmaps cannot certify a FOV split across ranks; fail before opening the store.""" + writer = HCSPredictionWriter(str(tmp_path / "pred.zarr")) + with pytest.raises(NotImplementedError, match="single device"): + writer.on_predict_start(SimpleNamespace(world_size=2), None) + assert not (tmp_path / "pred.zarr").exists() + + +class _SourcePlusOneModule(LightningModule): + """Predict ``source + 1``, so every written plane names the source plane it came from.""" + + def predict_step(self, batch, batch_idx: int, dataloader_idx: int = 0) -> torch.Tensor: + return batch["source"] + 1.0 + + +#: Per-timepoint planes for two FOVs of a (T=2, Z=7) source; caps and an interior slab. +SLAB_PLANES = {"0/0/0": [[0, 1], [3, 4, 5]], "0/0/1": [[5, 6], [2, 3, 4]]} + + +def _write_stamped_source(path) -> None: + """Two FOVs of ``(2, 2, 7, 8, 8)`` whose Phase3D plane ``(t, z)`` holds ``100 * t + z + 1``.""" + stamp = (100 * np.arange(2)[:, None] + np.arange(7)[None, :] + 1).astype(np.float32) + with open_ome_zarr(path, layout="hcs", mode="w-", channel_names=["Phase3D", "Nuclei"]) as plate: + for fov in SLAB_PLANES: + data = np.zeros((2, 2, 7, 8, 8), np.float32) + data[:, 0] = stamp[:, :, None, None] + plate.create_position(*fov.split("/")).create_image("0", data) + + +def _predict_planes(source, output, planes_file=None, settings=None) -> HCSDataModule: + data_module = HCSDataModule( + data_path=str(source), + source_channel=["Phase3D"], + target_channel=["Nuclei"], + z_window_size=1, + batch_size=2, + num_workers=0, + yx_patch_size=[8, 8], + normalizations=[], + augmentations=[], + predict_z_planes=planes_file, + ) + Trainer( + accelerator="cpu", + logger=False, + enable_progress_bar=False, + callbacks=[HCSPredictionWriter(str(output), overwrite=True, settings_sha256_12=settings)], + ).predict(_SourcePlusOneModule(), datamodule=data_module, return_predictions=False) + return data_module + + +def test_plane_restricted_predict_writes_only_the_listed_planes(tmp_path): + """Listed planes equal a full predict's; the rest stay at the fill value; the store keeps the full extent.""" + source = tmp_path / "source.zarr" + _write_stamped_source(source) + planes_file = tmp_path / "planes.json" + planes_file.write_text(json.dumps({"positions": SLAB_PLANES, "focus_anchor": "test"})) + + full, slab = tmp_path / "full.zarr", tmp_path / "slab.zarr" + _predict_planes(source, full) + data_module = _predict_planes(source, slab, planes_file) + assert len(data_module.predict_dataset) == sum(len(p) for per_t in SLAB_PLANES.values() for p in per_t) + + with open_ome_zarr(full, mode="r") as full_plate, open_ome_zarr(slab, mode="r") as slab_plate: + for fov, per_t in SLAB_PLANES.items(): + full_arr = np.asarray(full_plate[fov]["0"][:, 0]) + slab_arr = np.asarray(slab_plate[fov]["0"][:, 0]) + assert slab_arr.shape == full_arr.shape == (2, 7, 8, 8) + for t, planes in enumerate(per_t): + others = [z for z in range(7) if z not in planes] + np.testing.assert_array_equal(slab_arr[t, planes], full_arr[t, planes]) + assert (slab_arr[t, others] == 0).all() + # Discriminates: the full store holds a prediction on those planes. + assert (full_arr[t, others] > 0).all() + position = slab_plate[fov] + assert predicted_z_planes(position, "Nuclei_prediction") == per_t + marker = position.zattrs[PREDICTION_COMPLETE_KEY]["Nuclei_prediction"] + assert marker["z_planes_sha256_12"] == z_planes_sha256_12(SLAB_PLANES) + assert marker["source_shape"] == [2, 7, 8, 8] + assert PREDICTED_Z_PLANES_KEY not in full_plate[fov].zattrs + assert "z_planes_sha256_12" not in full_plate[fov].zattrs[PREDICTION_COMPLETE_KEY]["Nuclei_prediction"] + + +def test_full_predict_over_a_plane_store_clears_the_recorded_planes(tmp_path): + """Re-predicting every plane into a plane-restricted store drops its plane record.""" + source = tmp_path / "source.zarr" + _write_stamped_source(source) + planes_file = tmp_path / "planes.json" + planes_file.write_text(json.dumps({"positions": SLAB_PLANES})) + output = tmp_path / "pred.zarr" + _predict_planes(source, output, planes_file) + _predict_planes(source, output) + with open_ome_zarr(output, mode="r") as plate: + for fov in SLAB_PLANES: + assert predicted_z_planes(plate[fov], "Nuclei_prediction") is None + assert (np.asarray(plate[fov]["0"][:, 0]) > 0).all() + + +def test_plane_restricted_predict_refuses_a_store_holding_other_planes(tmp_path): + """A slab predict over another run's planes would leave them around the slab; over its own slab it may replace.""" + source = tmp_path / "source.zarr" + _write_stamped_source(source) + planes_file = tmp_path / "planes.json" + planes_file.write_text(json.dumps({"positions": SLAB_PLANES})) + full = tmp_path / "full.zarr" + _predict_planes(source, full) + with pytest.raises(ValueError, match="other planes"): + _predict_planes(source, full, planes_file) + slab = tmp_path / "slab.zarr" + _predict_planes(source, slab, planes_file, settings="aaaaaaaaaaaa") + # The same slab from other weights or settings (e.g. --ckpt best) rewrites every plane it holds. + _predict_planes(source, slab, planes_file, settings="bbbbbbbbbbbb") + other_planes = tmp_path / "other_planes.json" + other_planes.write_text(json.dumps({"positions": {fov: [[2], [6]] for fov in SLAB_PLANES}})) + with pytest.raises(ValueError, match="other planes"): + _predict_planes(source, slab, other_planes, settings="cccccccccccc") + + +def test_an_interrupted_plane_predict_is_unfinished(tmp_path): + """Until a plane run completes its marker has no source_shape, whatever planes are recorded.""" + source = tmp_path / "source.zarr" + _write_stamped_source(source) + planes_file = tmp_path / "planes.json" + planes_file.write_text(json.dumps({"positions": SLAB_PLANES})) + full, slab = tmp_path / "full.zarr", tmp_path / "slab.zarr" + _predict_planes(source, full) + _predict_planes(source, slab, planes_file) + with open_ome_zarr(full, mode="r") as full_plate, open_ome_zarr(slab, mode="r+") as slab_plate: + assert not plane_predict_unfinished(full_plate["0/0/0"], "Nuclei_prediction") + position = slab_plate["0/0/0"] + assert not plane_predict_unfinished(position, "Nuclei_prediction") + marker = position.zattrs[PREDICTION_COMPLETE_KEY]["Nuclei_prediction"] + # An interrupted re-predict of the same slab: started marker, the old planes still recorded. + mark_started(position, ["Nuclei_prediction"], {k: v for k, v in marker.items() if k != "source_shape"}) + assert predicted_z_planes(position, "Nuclei_prediction") == SLAB_PLANES["0/0/0"] + assert plane_predict_unfinished(position, "Nuclei_prediction") + # An interrupted first predict: started marker, no planes yet. + record_z_planes(position, ["Nuclei_prediction"], None) + assert plane_predict_unfinished(position, "Nuclei_prediction") + + +def test_record_z_planes_keeps_other_channels(tmp_path): + with open_ome_zarr(tmp_path / "p.zarr", layout="hcs", mode="w-", channel_names=["A", "B"]) as plate: + position = plate.create_position("0", "0", "0") + record_z_planes(position, ["A", "B"], [[3, 2, 2]]) + record_z_planes(position, ["A"], None) + assert predicted_z_planes(position, "A") is None + assert predicted_z_planes(position, "B") == [[2, 3]] + record_z_planes(position, ["B"], None) + assert PREDICTED_Z_PLANES_KEY not in position.zattrs + + +@pytest.mark.parametrize( + ("planes", "match"), + [ + ({"0/0/0": [[0]]}, "timepoints"), + ({"0/0/0": [[0], [7]], "0/0/1": [[0], [0]]}, "outside"), + ({"0/0/0": [[0], [0]]}, "no entry"), + ], +) +def test_plane_file_must_cover_every_position_and_timepoint(tmp_path, planes, match): + source = tmp_path / "source.zarr" + _write_stamped_source(source) + planes_file = tmp_path / "planes.json" + planes_file.write_text(json.dumps({"positions": planes})) + with pytest.raises((ValueError, KeyError), match=match): + _predict_planes(source, tmp_path / "pred.zarr", planes_file) + + +def test_plane_restriction_needs_single_plane_windows(tmp_path): + source = tmp_path / "source.zarr" + _write_stamped_source(source) + planes_file = tmp_path / "planes.json" + planes_file.write_text(json.dumps({"positions": SLAB_PLANES})) + data_module = _ones_data_module(source) + data_module.predict_z_planes = planes_file + with pytest.raises(ValueError, match="z_window_size=1"): + data_module.setup("predict") + + +def test_writer_rejects_unknown_z_reduction(): + """An unknown reduction fails at construction, not silently mid-predict.""" + with pytest.raises(ValueError, match="z_reduction"): + HCSPredictionWriter("unused.zarr", z_reduction="mean") # type: ignore[arg-type] def test_blend_in_consistency(): @@ -25,6 +970,17 @@ def test_blend_in_consistency(): np.testing.assert_allclose(result_np, result_torch.squeeze(0).numpy(), rtol=1e-5, atol=1e-5) +def test_blend_factors_cached_under_inference_mode_serve_autograd(): + """Factors first cached inside inference mode (predict) still work in a later blend that records gradients.""" + _torch_factors.cache_clear() # an earlier test may have cached these factors outside inference mode + old = torch.ones(1, 2, 5, 8, 8, dtype=torch.float64) + with torch.inference_mode(): + _blend_in(old, old, slice(3, 8)) + new = torch.zeros(1, 2, 5, 8, 8, dtype=torch.float64, requires_grad=True) + _blend_in(old, new, slice(3, 8)).sum().backward() + assert new.grad is not None + + def test_blend_in_zero_start(): """Verify _blend_in returns new_stack unchanged when z_slice starts at 0.""" old = np.ones((2, 5, 8, 8), dtype=np.float32) diff --git a/packages/viscy-utils/tests/test_seg_aux_loss.py b/packages/viscy-utils/tests/test_seg_aux_loss.py new file mode 100644 index 000000000..fe986826f --- /dev/null +++ b/packages/viscy-utils/tests/test_seg_aux_loss.py @@ -0,0 +1,619 @@ +"""Unit tests for SegAuxDice.""" + +import numpy as np +import pytest +import torch +from scipy import ndimage + +from viscy_utils.losses import SegAuxDice +from viscy_utils.losses.seg_aux import _masked_median, _quantile_sorted +from viscy_utils.losses.seg_aux_maps import box_max, sauna_signed_map, sauna_weight_map, soft_skeleton, squared_edt + + +def _blob_batch(shape: tuple[int, ...], seed: int = 0) -> tuple[torch.Tensor, torch.Tensor]: + """Target = bright box on a noisy background; mask = the box.""" + g = torch.Generator().manual_seed(seed) + target = 0.1 * torch.randn(shape, generator=g) + mask = torch.zeros(shape) + mask[..., shape[-2] // 4 : shape[-2] // 2, shape[-1] // 4 : shape[-1] // 2] = 1.0 + target = target + 2.0 * mask + return target, mask + + +@pytest.mark.parametrize("shape", [(2, 1, 1, 32, 32), (2, 2, 4, 16, 16)], ids=["2d", "3d"]) +def test_perfect_prediction_beats_perturbed(shape): + target, mask = _blob_batch(shape) + loss_fn = SegAuxDice() + perfect = loss_fn(target.clone(), target, mask) + g = torch.Generator().manual_seed(1) + perturbed = loss_fn(target + 0.5 * torch.randn(shape, generator=g), target, mask) + assert perfect.ndim == 0 + assert perfect < perturbed + blurred_out = loss_fn(torch.zeros(shape), target, mask) + assert perfect < blurred_out + + +def test_gradient_nonzero_for_foreground_far_below_tau(): + """A foreground voxel predicted several knee widths below tau still gets a + gradient pushing it up; v1's clamped sigmoid gave exactly zero there.""" + target, mask = _blob_batch((1, 1, 1, 32, 32)) + flat = target.flatten() + tau = torch.quantile(flat, 1 - mask.mean()) + fg = mask.flatten() > 0.5 + s = 0.1 * (flat[fg].median() - flat[~fg].median()) + pred = target.clone() + fg_idx = (0, 0, 0, 10, 10) + assert mask[fg_idx] == 1 + pred[fg_idx] = tau - 8 * s + pred.requires_grad_(True) + SegAuxDice(c=0.1)(pred, target, mask).backward() + assert pred.grad[fg_idx] < 0 # increasing pred there lowers the loss + + +@pytest.mark.parametrize("a,b", [(3.0, -5.0), (0.01, 1.0), (1.0, 0.7)]) +def test_affine_invariance(a, b): + target, mask = _blob_batch((2, 1, 4, 16, 16)) + pred = target + 0.3 * torch.randn(target.shape, generator=torch.Generator().manual_seed(2)) + loss_fn = SegAuxDice(c=0.1) + ref = loss_fn(pred, target, mask) + moved = loss_fn(a * pred + b, a * target + b, mask) + torch.testing.assert_close(moved, ref, rtol=1e-4, atol=1e-5) + + +def test_masked_median_matches_torch_on_selection(): + g = torch.Generator().manual_seed(7) + values = torch.randn(4, 101, generator=g) + keep = torch.rand(4, 101, generator=g) > 0.6 + got = _masked_median(values, keep) + want = torch.stack([torch.quantile(v[k], 0.5) for v, k in zip(values, keep)]) + torch.testing.assert_close(got, want) + + +def test_mask_darker_than_background_is_excluded(): + target, mask = _blob_batch((1, 1, 1, 32, 32)) + loss, comps = SegAuxDice()(target.clone(), -target, mask, return_components=True) + assert comps["n_valid"] == 0 + + +def test_empty_and_full_masks_are_excluded(): + target, mask = _blob_batch((3, 1, 1, 32, 32)) + mask[1] = 0.0 + mask[2] = 1.0 + pred = target + 0.3 * torch.randn(target.shape, generator=torch.Generator().manual_seed(3)) + loss_fn = SegAuxDice() + loss, comps = loss_fn(pred, target, mask, return_components=True) + only_valid = loss_fn(pred[:1], target[:1], mask[:1]) + assert comps["n_valid"] == 1 + torch.testing.assert_close(loss, only_valid) + torch.testing.assert_close(comps["dice"], loss) + + +def test_all_invalid_is_zero_with_graph(): + target = torch.randn(2, 1, 1, 16, 16) + pred = torch.randn(2, 1, 1, 16, 16, requires_grad=True) + loss, comps = SegAuxDice()(pred, target, torch.zeros_like(target), return_components=True) + assert loss.item() == 0.0 + assert comps["n_valid"] == 0 + assert loss.requires_grad + loss.backward() + assert torch.equal(pred.grad, torch.zeros_like(pred)) + + +def test_constant_target_is_excluded(): + target = torch.ones(1, 1, 1, 16, 16) + mask = torch.zeros_like(target) + mask[..., :8, :] = 1.0 + loss, comps = SegAuxDice()(torch.randn_like(target), target, mask, return_components=True) + assert comps["n_valid"] == 0 + assert loss.item() == 0.0 + + +def test_fractional_mask_is_binarized(): + target, mask = _blob_batch((1, 1, 1, 32, 32)) + pred = target + 0.2 + loss_fn = SegAuxDice() + torch.testing.assert_close(loss_fn(pred, target, mask * 0.8), loss_fn(pred, target, mask)) + + +def test_quantile_matches_torch_quantile(): + rows = torch.randn(4, 1001) + q = torch.tensor([0.0, 0.25, 0.731, 1.0]) + got = _quantile_sorted(rows.sort(-1).values, q) + expected = torch.stack([torch.quantile(rows[i], q[i]) for i in range(4)]) + torch.testing.assert_close(got, expected) + + +def test_quantile_beyond_torch_quantile_size_limit(): + """torch.quantile raises above 2**24 elements; the sort-based path does not.""" + n = 2**24 + 16 + row = torch.arange(n, dtype=torch.float32).unsqueeze(0) + got = _quantile_sorted(row, torch.tensor([0.5])) + torch.testing.assert_close(got, torch.tensor([(n - 1) / 2])) + + +@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16]) +def test_autocast_computes_in_float32(dtype): + target, mask = _blob_batch((2, 1, 4, 16, 16)) + pred = (target + 0.3).requires_grad_(True) + ref = SegAuxDice()(pred, target, mask) + with torch.autocast(device_type="cpu", dtype=dtype): + low = SegAuxDice()(pred.to(dtype), target.to(dtype), mask.to(dtype)) + assert low.dtype == torch.float32 + assert torch.isfinite(low) + torch.testing.assert_close(low, ref, rtol=0.05, atol=0.02) + low.backward() + assert torch.isfinite(pred.grad).all() + + +def test_shape_mismatch_raises(): + with pytest.raises(ValueError, match="share a shape"): + SegAuxDice()(torch.zeros(1, 1, 4, 4), torch.zeros(1, 1, 4, 4), torch.zeros(1, 1, 4, 5)) + + +def _mask_disagrees_with_target(shape: tuple[int, ...] = (2, 1, 1, 32, 32)) -> tuple[torch.Tensor, torch.Tensor]: + """Box target plus a bright blob outside the mask: the mask is not a threshold of the target.""" + target, mask = _blob_batch(shape) + target[..., -6:, -6:] += 2.0 + return target, mask + + +def test_target_label_is_minimised_at_pred_equals_target(): + """label='target' is ~0 with zero gradient at pred == target, where label='mask' has a floor.""" + target, mask = _mask_disagrees_with_target() + floor = SegAuxDice(label="mask")(target.clone(), target, mask) + pred = target.clone().requires_grad_(True) + loss = SegAuxDice(label="target")(pred, target, mask) + loss.backward() + assert floor > 0.05 + assert loss < 1e-4 + assert pred.grad.abs().max() < 1e-4 + g = torch.Generator().manual_seed(1) + perturbed = target + 0.5 * torch.randn(target.shape, generator=g) + assert SegAuxDice(label="target")(perturbed, target, mask) > 10 * loss + + +def test_target_label_keeps_the_mask_validity_rule(): + """The mask still decides which patches count: an empty mask is excluded under either label.""" + target, mask = _blob_batch((2, 1, 1, 16, 16)) + mask[0] = 0.0 + for label in ("mask", "target"): + _, valid = SegAuxDice(label=label).per_channel(target, target, mask) + assert valid.tolist() == [[False], [True]] + + +def test_unknown_label_raises(): + with pytest.raises(ValueError, match="label"): + SegAuxDice(label="soft") + + +# ---- Defaults are bit-identical to the pre-extension implementation ---- + +# per_channel(...).dice on _reference_inputs(), computed before weighting/topology existed. +_PRE_EXTENSION_DICE = { + "mask": ["0x1.71c02p-3", "0x1.73a7cp-3", "0x1.37d6bp-3", "0x1.66fff8p-3", "0x1.530968p-3", "0x1.0p+0"], + "target": ["0x1.9c865p-4", "0x1.b3d3ap-4", "0x1.57231p-4", "0x1.a811fp-4", "0x1.80088p-4", "0x1.740c6p-5"], +} + + +def _reference_inputs() -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + g = torch.Generator().manual_seed(123) + shape = (3, 2, 3, 12, 12) + target = torch.randn(shape, generator=g) + mask = (target + 0.3 * torch.randn(shape, generator=g) > 0.5).float() + mask[2, 1] = 0.0 + pred = target + 0.4 * torch.randn(shape, generator=g) + return pred, target, mask + + +@pytest.mark.parametrize("label", ["mask", "target"]) +def test_defaults_are_bit_identical_to_pre_extension(label): + pred, target, mask = _reference_inputs() + loss_fn = SegAuxDice(label=label, weighting="none", topology="none") + dice, valid = loss_fn.per_channel(pred, target, mask) + assert dice.dtype == torch.float32 + assert dice.flatten().tolist() == [float.fromhex(h) for h in _PRE_EXTENSION_DICE[label]] + assert valid.flatten().tolist() == [True] * 5 + [False] + + +# ---- Exact torch EDT ---- + + +def _random_mask(shape: tuple[int, ...], fraction: float, seed: int) -> np.ndarray: + rng = np.random.default_rng(seed) + return rng.random(shape) < fraction + + +@pytest.mark.parametrize( + "shape,spacing", + [ + ((40, 37), (1.0, 1.0)), + ((40, 37), (0.3, 1.7)), + ((9, 21, 18), (1.0, 1.0, 1.0)), + ((9, 21, 18), (0.29, 0.108, 0.108)), + ((1, 30, 25), (0.29, 0.108, 0.108)), + ], + ids=["2d-iso", "2d-aniso", "3d-iso", "3d-aniso", "3d-z1"], +) +@pytest.mark.parametrize("fraction", [0.02, 0.5]) +def test_squared_edt_matches_scipy(shape, spacing, fraction): + seeds = np.stack([_random_mask(shape, fraction, seed) for seed in range(3)]) + seeds[:, (0,) * len(shape)] = True # at least one seed per row + got = squared_edt(torch.from_numpy(seeds), spacing).sqrt().numpy() + want = np.stack([ndimage.distance_transform_edt(~s, sampling=spacing) for s in seeds]) + np.testing.assert_allclose(got, want, rtol=1e-4, atol=1e-6) + + +@pytest.mark.parametrize("max_distance", [0.5, 2.0, 7.5]) +def test_squared_edt_capped_is_exact_below_the_cap(max_distance): + shape, spacing = (9, 41, 38), (0.29, 0.108, 0.108) + seeds = np.stack([_random_mask(shape, 0.01, seed) for seed in range(2)]) + seeds[:, 0, 0, 0] = True + got = squared_edt(torch.from_numpy(seeds), spacing, max_distance=max_distance).sqrt().numpy() + want = np.stack([ndimage.distance_transform_edt(~s, sampling=spacing) for s in seeds]) + np.testing.assert_allclose(got, np.minimum(want, max_distance), rtol=1e-4, atol=1e-6) + + +@pytest.mark.parametrize("shape", [(2, 1, 5, 20, 20), (2, 1, 1, 20, 20), (2, 1, 24, 20)], ids=["3d", "3d-z1", "2d"]) +def test_soft_skeleton_matches_official_values_and_gradients(shape): + """Max-pool morphology (F.max_pool{1,2,3}d) + checkpointing reproduce jocpae/clDice soft_skel.""" + img = torch.rand(shape, generator=torch.Generator().manual_seed(8)) + weights = torch.rand(img.shape, generator=torch.Generator().manual_seed(9)) + a = img.clone().requires_grad_(True) + (soft_skeleton(a, 4) * weights).sum().backward() + b = img.clone().requires_grad_(True) + official = _official_soft_skel(b, 4) + (official * weights).sum().backward() + with torch.no_grad(): + torch.testing.assert_close(soft_skeleton(img, 4), official) + torch.testing.assert_close(a.grad, b.grad) + + +def _official_soft_skel(img: torch.Tensor, iters: int) -> torch.Tensor: + """jocpae/clDice soft_skeleton.py, verbatim for 4D and 5D input.""" + F = torch.nn.functional + + def soft_erode(img): + if len(img.shape) == 4: + p1 = -F.max_pool2d(-img, (3, 1), (1, 1), (1, 0)) + p2 = -F.max_pool2d(-img, (1, 3), (1, 1), (0, 1)) + return torch.min(p1, p2) + p1 = -F.max_pool3d(-img, (3, 1, 1), (1, 1, 1), (1, 0, 0)) + p2 = -F.max_pool3d(-img, (1, 3, 1), (1, 1, 1), (0, 1, 0)) + p3 = -F.max_pool3d(-img, (1, 1, 3), (1, 1, 1), (0, 0, 1)) + return torch.min(torch.min(p1, p2), p3) + + def soft_dilate(img): + if len(img.shape) == 4: + return F.max_pool2d(img, (3, 3), (1, 1), (1, 1)) + return F.max_pool3d(img, (3, 3, 3), (1, 1, 1), (1, 1, 1)) + + def soft_open(img): + return soft_dilate(soft_erode(img)) + + img1 = soft_open(img) + skel = F.relu(img - img1) + for _ in range(iters): + img = soft_erode(img) + img1 = soft_open(img) + delta = F.relu(img - img1) + skel = skel + F.relu(delta - skel * delta) + return skel + + +@pytest.mark.parametrize("windows", [(1, 3, 5), (3, 1, 7), (5, 9, 31), (9, 3, 1)]) +def test_box_max_matches_scipy_maximum_filter(windows): + x = torch.rand(2, 4, 13, 11, generator=torch.Generator().manual_seed(3)) + want = np.stack([ndimage.maximum_filter(r, size=windows, mode="constant", cval=-np.inf) for r in x.numpy()]) + np.testing.assert_array_equal(box_max(x, windows).numpy(), want) + + +def test_squared_edt_without_seeds_is_inf(): + d2 = squared_edt(torch.zeros(2, 4, 5, dtype=torch.bool), (1.0, 1.0)) + assert torch.isinf(d2).all() + + +# ---- SAUNA map ---- + + +def _sauna_reference(gt: np.ndarray) -> np.ndarray: + """SAUNA's combined "h" map, line by line from Oulu-IMEDS/SAUNA + ``generate_uncertainty_masks.py``, with scipy's exact EDT in place of + ``cv2.distanceTransform(..., DIST_L2, DIST_MASK_5)``. ``gt`` is uint8 0/255.""" + + def compute_distance_transform(image): + return ndimage.distance_transform_edt(image).astype(np.float32) + + def do_max_pooling(img_dist): + kernel_size = int(np.ceil(img_dist.max())) + kernel_size = kernel_size + 1 if kernel_size % 2 == 0 else kernel_size + maxpool = torch.nn.MaxPool2d(kernel_size=kernel_size, stride=1, padding=kernel_size // 2) + return maxpool(torch.tensor(img_dist).unsqueeze(0).unsqueeze(0))[0, 0].numpy() + + # extract_boundary_uncertainty_map + img_fg_dist = compute_distance_transform(gt) + fg_max = img_fg_dist.max() + img_bg_dist = -compute_distance_transform(255 - gt) + img_bg_dist[img_bg_dist <= -fg_max] = -fg_max + gt_b = (img_fg_dist + img_bg_dist) / (fg_max + 1e-6) + # extract_thickness_uncertainty_map, target_c_label="h" + img_dist = compute_distance_transform(gt) + thickness_max = img_dist.max() + img_thick_pos = (gt > 0) * do_max_pooling(img_dist) / (thickness_max + 1e-6) + img_bg_dist = compute_distance_transform(255 - gt) + img_bg_dist[img_bg_dist >= fg_max] = fg_max + img_thick_neg = (gt <= 0) * do_max_pooling(img_bg_dist) / (thickness_max + 1e-6) + img_thick_neg = np.clip(img_thick_neg, a_min=0.0, a_max=1.0) + gt_t = np.where(gt > 0, img_thick_pos, img_thick_neg) + # extract_combined_uncertainty_map, target_c_label="h" + gt_c = gt_b.copy() + gt_t_abs = np.abs(gt_t) + fg = (gt_t_abs > 0) & (gt_b > 0) + gt_c[fg] = gt_b[fg] + (1.0 - gt_t[fg]) + bg = (gt_t_abs > 0) & (gt_b < 0) + gt_c[bg] = gt_b[bg] - (1.0 - gt_t[bg]) + return np.clip(gt_c, a_min=-1.0, a_max=1.0) + + +def _disk(canvas: np.ndarray, cy: int, cx: int, r: float) -> None: + yy, xx = np.ogrid[: canvas.shape[0], : canvas.shape[1]] + canvas[(yy - cy) ** 2 + (xx - cx) ** 2 <= r * r] = True + + +def _synthetic_shapes() -> tuple[np.ndarray, dict[str, tuple[int, int]]]: + """Fat disk, 2-px line, two disks across a 2-px gap and a 2-px slit; probe voxels.""" + m = np.zeros((320, 320), dtype=bool) + _disk(m, 50, 50, 20) # rows/cols 30..70 + m[120:122, 20:230] = True # 2-px-wide line + _disk(m, 200, 100, 20) # cols 80..120 + _disk(m, 200, 143, 20) # cols 123..163: gap = cols 121, 122 + m[250:291, 20:100] = True # slit = cols 100, 101 between two 41-px-tall slabs + m[250:291, 102:180] = True + probes = { + "fat_disk_boundary_fg": (50, 70), + "fat_disk_boundary_bg": (50, 71), + "disk_center": (50, 50), + "thin_line": (120, 120), + "gap_center": (200, 121), + "slit_center": (270, 100), + "deep_background": (310, 310), + } + return m, probes + + +def _blob_masks(n: int, size: int, seed: int) -> np.ndarray: + rng = np.random.default_rng(seed) + masks = np.zeros((n, size, size), dtype=bool) + for m in masks: + for _ in range(4): + _disk(m, *rng.integers(0, size, 2), rng.uniform(2, 9)) + row = rng.integers(0, size - 1) + m[row : row + 1, :] = True # 1-px line + return masks + + +def test_sauna_map_matches_sauna_reference(): + synthetic, _ = _synthetic_shapes() + masks = [synthetic, *_blob_masks(4, 64, seed=0)] + for m in masks: + want = _sauna_reference(m.astype(np.uint8) * 255) + got = sauna_signed_map(torch.from_numpy(m)[None], (1.0, 1.0))[0].numpy() + np.testing.assert_allclose(got, want, rtol=1e-4, atol=1e-4) + + +def test_sauna_map_on_synthetic_shapes(capsys): + m, probes = _synthetic_shapes() + y = sauna_signed_map(torch.from_numpy(m)[None], (1.0, 1.0))[0] + values = {name: y[idx].item() for name, idx in probes.items()} + with capsys.disabled(): + print("\nSAUNA y~ on synthetic shapes:", {k: round(v, 4) for k, v in values.items()}) + assert values["disk_center"] == pytest.approx(1.0, abs=1e-4) + assert values["deep_background"] == -1.0 + assert values["thin_line"] > 0.9 + assert values["fat_disk_boundary_fg"] > 0 > values["fat_disk_boundary_bg"] + assert values["gap_center"] < 0 and values["slit_center"] < 0 + # Thick-structure boundaries are the uncertain, down-weighted region: the + # ceil(fg_max)-voxel window reaches ~fg_max / 2 inward, so |y~| ~ 0.5 there. + assert 0.35 < abs(values["fat_disk_boundary_fg"]) < 0.65 + assert 0.35 < abs(values["fat_disk_boundary_bg"]) < 0.65 + assert values["gap_center"] < -0.75 and values["slit_center"] < -0.75 + + +def test_sauna_map_z1_equals_2d_and_degenerate_rows_weigh_one(): + masks = torch.from_numpy(_blob_masks(2, 48, seed=1)) + flat = sauna_signed_map(masks, (1.0, 1.0)) + z1 = sauna_signed_map(masks[:, None], (0.29, 1.0, 1.0))[:, 0] + torch.testing.assert_close(z1, flat) + degenerate = torch.stack([torch.zeros(8, 8, dtype=torch.bool), torch.ones(8, 8, dtype=torch.bool)]) + torch.testing.assert_close(sauna_weight_map(degenerate, (1.0, 1.0)), torch.ones(2, 8, 8)) + + +def test_sauna_weighting_changes_dice_and_stays_finite(): + target, mask = _blob_batch((3, 1, 4, 32, 32)) + mask[1] = 0.0 # invalid patch: weights 1, entry finite and excluded + pred = (target + 0.5 * torch.randn(target.shape, generator=torch.Generator().manual_seed(4))).requires_grad_(True) + plain = SegAuxDice() + sauna = SegAuxDice(weighting="sauna", spacing=(0.29, 0.108, 0.108)) + d_plain, v_plain = plain.per_channel(pred, target, mask) + d_sauna, v_sauna = sauna.per_channel(pred, target, mask) + assert torch.equal(v_plain, v_sauna) + assert torch.isfinite(d_sauna).all() + assert not torch.allclose(d_plain[v_plain], d_sauna[v_sauna]) + loss, comps = sauna(pred, target, mask, return_components=True) + assert comps["n_valid"] == 2 + loss.backward() + assert torch.isfinite(pred.grad).all() + + +# A 1x7 row with a 3-voxel object, spacing 1. By hand: fg_dist = [0,0,1,2,1,0,0], +# bg_dist = [2,1,0,0,0,1,2], fg_max = 2, so gt_b = (fg_dist - bg_dist) / 2. Both +# max-pool windows are ceil(2) -> 3 voxels, which put fg_pool = 2 on every fg voxel +# and bg_pool = 2 on every bg voxel, so gt_t = 1 everywhere and y~ = gt_b. +_ROW_MASK = [0, 0, 1, 1, 1, 0, 0] +_ROW_SIGNED = [-1.0, -0.5, 0.5, 1.0, 0.5, -0.5, -1.0] + + +def test_sauna_weight_map_is_abs_of_hand_computed_map(): + mask = torch.tensor(_ROW_MASK, dtype=torch.bool).reshape(1, 1, 7) + w = sauna_weight_map(mask, (1.0, 1.0))[0, 0] + torch.testing.assert_close(w, torch.tensor([abs(v) for v in _ROW_SIGNED])) + # Background next to the object is uncertain (y~ = -0.5) but still weighted. + assert w[1].item() == pytest.approx(0.5) + + +def test_sauna_weighted_dice_matches_hand_computation(): + target = np.array([0.0, 0.2, 2.0, 3.0, 1.8, 0.4, 0.1]) + pred = np.array([0.5, 1.2, 1.1, 2.5, 0.8, 0.9, -0.3]) + mask = np.array(_ROW_MASK, dtype=float) + w = np.abs(np.array(_ROW_SIGNED)) + c, eps = 0.1, 1e-6 + # tau: target quantile at 1 - 3/7 -> sorted position 3.43, 0.4 + 0.43 * (1.8 - 0.4) = 1.0. + tau = np.quantile(target, 1.0 - mask.mean()) + # s: c * (median fg 2.0 - median bg 0.15) = 0.185. + s = c * (np.median(target[mask > 0]) - np.median(target[mask == 0])) + p = 1.0 / (1.0 + np.exp(-(pred - tau) / s)) + want = 1.0 - 2.0 * (w * p * mask).sum() / ((w * p * p).sum() + (w * mask * mask).sum() + eps) + + def as_input(a: np.ndarray) -> torch.Tensor: + return torch.tensor(a, dtype=torch.float32).reshape(1, 1, 1, 7) + + loss_fn = SegAuxDice(c=c, eps=eps, weighting="sauna", spacing=(1.0, 1.0)) + dice, valid = loss_fn.per_channel(as_input(pred), as_input(target), as_input(mask)) + assert valid.item() + assert dice.item() == pytest.approx(want, rel=1e-5) + + +# ---- clDice ---- + + +def _line_mask(shape: tuple[int, ...] = (1, 1, 1, 48, 48)) -> torch.Tensor: + mask = torch.zeros(shape) + mask[..., 23:25, 4:44] = 1.0 # 2-px-wide line + return mask + + +def _crisp(mask: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + """Target = mask + noise; a prediction that is a high-contrast copy of a mask.""" + noise = 0.05 * torch.randn(mask.shape, generator=torch.Generator().manual_seed(5)) + return mask + noise, 3.0 * mask - 1.0 + + +def _skeleton_precision_sensitivity(loss_fn: SegAuxDice, pred, target, mask) -> tuple[float, float]: + """Tprec/Tsens on the loss's own soft prediction, via the module's soft_skeleton.""" + flat_t = target.flatten() + fg = mask.flatten() > 0.5 + tau = torch.quantile(flat_t, 1 - mask.mean()) + s = loss_fn.c * (flat_t[fg].median() - flat_t[~fg].median()) + p = torch.sigmoid((pred - tau) / s)[:, 0] + r = mask[:, 0] + sp = soft_skeleton(p, loss_fn.cldice_iters) + sr = soft_skeleton(r, loss_fn.cldice_iters) + return ((sp * r).sum() / sp.sum()).item(), ((sr * p).sum() / sr.sum()).item() + + +def _line_and_disk_mask() -> torch.Tensor: + mask = _line_mask() + _disk(mask[0, 0, 0].numpy(), 10, 10, 6) # writes through the numpy view + return mask + + +def test_cldice_near_one_for_crisp_copy_of_mask(): + mask = _line_and_disk_mask() + target, pred = _crisp(mask) + pure = SegAuxDice(label="mask", topology="cldice", cldice_alpha=1.0) + one_minus_cl, valid = pure.per_channel(pred, target, mask) + assert valid.all() + assert one_minus_cl.item() < 0.02 + + +def test_cldice_target_label_prefers_pred_equal_target(): + """With label='target' the reference is soft, so clDice < 1 even at pred == target, + but a broken prediction still scores worse.""" + mask = _line_and_disk_mask() + target, _ = _crisp(mask) + pure = SegAuxDice(label="target", topology="cldice", cldice_alpha=1.0) + broken = target.clone() + broken[..., 18:30] = target.min() + assert pure(broken, target, mask) > pure(target.clone(), target, mask) + + +def test_cldice_bridge_lowers_precision_and_break_lowers_sensitivity(): + loss_fn = SegAuxDice(topology="cldice", cldice_alpha=1.0) + two = torch.zeros(1, 1, 1, 48, 64) + _disk(two[0, 0, 0].numpy(), 24, 18, 8) + _disk(two[0, 0, 0].numpy(), 24, 45, 8) # disjoint, gap at cols 27..36 + target, perfect = _crisp(two) + bridged = perfect.clone() + bridged[..., 22:27, 18:46] = 2.0 + prec_ok, sens_ok = _skeleton_precision_sensitivity(loss_fn, perfect, target, two) + prec_br, sens_br = _skeleton_precision_sensitivity(loss_fn, bridged, target, two) + assert prec_br < prec_ok - 0.1 + assert loss_fn(bridged, target, two) > loss_fn(perfect, target, two) + + line = _line_mask() + target, perfect = _crisp(line) + broken = perfect.clone() + broken[..., 18:30] = -1.0 # 12-px break + prec_ok, sens_ok = _skeleton_precision_sensitivity(loss_fn, perfect, target, line) + prec_bk, sens_bk = _skeleton_precision_sensitivity(loss_fn, broken, target, line) + assert sens_bk < sens_ok - 0.1 + assert prec_bk == pytest.approx(prec_ok, abs=0.05) + assert loss_fn(broken, target, line) > loss_fn(perfect, target, line) + + +@pytest.mark.parametrize("weighting", ["none", "sauna"]) +def test_cldice_gradients_finite_and_invalid_excluded(weighting): + target, mask = _blob_batch((3, 1, 4, 32, 32)) + mask[1] = 0.0 + mask[2] = 1.0 + pred = (target + 0.3 * torch.randn(target.shape, generator=torch.Generator().manual_seed(6))).requires_grad_(True) + spacing = (0.29, 0.108, 0.108) if weighting == "sauna" else None + loss_fn = SegAuxDice(topology="cldice", cldice_alpha=0.5, weighting=weighting, spacing=spacing) + loss, comps = loss_fn(pred, target, mask, return_components=True) + assert comps["n_valid"] == 1 + torch.testing.assert_close(loss, loss_fn(pred[:1], target[:1], mask[:1])) + loss.backward() + assert torch.isfinite(pred.grad).all() + assert pred.grad.abs().sum() > 0 + + +def test_cldice_alpha_zero_equals_dice(): + pred, target, mask = _reference_inputs() + plain, _ = SegAuxDice().per_channel(pred, target, mask) + mixed, _ = SegAuxDice(topology="cldice", cldice_alpha=0.0).per_channel(pred, target, mask) + torch.testing.assert_close(mixed, plain) + + +@pytest.mark.parametrize("label", ["mask", "target"]) +def test_extensions_run_under_bf16_autocast(label): + target, mask = _blob_batch((2, 1, 4, 16, 16)) + pred = (target + 0.3).requires_grad_(True) + loss_fn = SegAuxDice(label=label, weighting="sauna", spacing=(0.29, 0.108, 0.108), topology="cldice") + with torch.autocast(device_type="cpu", dtype=torch.bfloat16): + loss = loss_fn(pred.to(torch.bfloat16), target.to(torch.bfloat16), mask.to(torch.bfloat16)) + assert loss.dtype == torch.float32 + assert torch.isfinite(loss) + loss.backward() + assert torch.isfinite(pred.grad).all() + + +@pytest.mark.parametrize( + "kwargs,match", + [ + ({"weighting": "boundary"}, "weighting"), + ({"weighting": "sauna"}, "needs spacing"), + ({"weighting": "sauna", "spacing": (1.0, -1.0)}, "positive"), + ({"spacing": (1.0, 1.0, 1.0)}, "no effect"), + ({"topology": "skeleton"}, "topology"), + ({"topology": "cldice", "cldice_alpha": 1.5}, "cldice_alpha"), + ({"topology": "cldice", "cldice_iters": 0}, "cldice_iters"), + ], +) +def test_invalid_extension_args_raise(kwargs, match): + with pytest.raises(ValueError, match=match): + SegAuxDice(**kwargs) + + +def test_spacing_length_must_match_spatial_dims(): + target, mask = _blob_batch((1, 1, 4, 16, 16)) + with pytest.raises(ValueError, match="spatial dims"): + SegAuxDice(weighting="sauna", spacing=(1.0, 1.0)).per_channel(target, target, mask) diff --git a/packages/viscy-utils/tests/test_seg_consistency_loss.py b/packages/viscy-utils/tests/test_seg_consistency_loss.py new file mode 100644 index 000000000..6217d02b1 --- /dev/null +++ b/packages/viscy-utils/tests/test_seg_consistency_loss.py @@ -0,0 +1,171 @@ +"""Tests for SegConsistencyLoss on a tiny stand-in segmenter (the Cellpose-SAM one is in dynacell).""" + +import pytest +import torch +from torch import Tensor, nn + +from viscy_utils.losses import SegConsistencyLoss +from viscy_utils.losses.seg_consistency import _OTSU_BINS, _binary_kl, _otsu_thresholds, nucleus_area_planes + + +class TinySegmenter(nn.Module): + """Two conv layers to flow (2 ch) + cell-probability logit (1 ch), per-image min-max normalized.""" + + def __init__(self, seed: int = 0) -> None: + super().__init__() + torch.manual_seed(seed) + self.net = nn.Sequential(nn.Conv2d(1, 8, 3, padding=1), nn.Tanh(), nn.Conv2d(8, 3, 3, padding=1)) + + def forward(self, x: Tensor) -> dict[str, Tensor]: + with torch.no_grad(): + lo = x.amin((1, 2, 3), keepdim=True) + hi = x.amax((1, 2, 3), keepdim=True) + y = self.net((x - lo) / (hi - lo + 1e-6)) + return {"flow": y[:, :2], "cellprob_logit": y[:, 2:3]} + + +def _nuclei(shape: tuple[int, ...], seed: int = 0) -> Tensor: + """Bright blobs on noise, peaked in z around a random plane per sample.""" + g = torch.Generator().manual_seed(seed) + b, z, y, x = shape[0], shape[2], shape[3], shape[4] + yy, xx = torch.meshgrid(torch.arange(y), torch.arange(x), indexing="ij") + out = 0.05 * torch.randn(shape, generator=g) + for i in range(b): + zc = float(torch.randint(z // 4, max(z // 4 + 1, 3 * z // 4), (1,), generator=g)) + for _ in range(4): + cy, cx = (torch.rand(2, generator=g) * torch.tensor([y, x])).tolist() + disk = ((yy - cy) ** 2 + (xx - cx) ** 2 < 36).float() + prof = torch.exp(-(((torch.arange(z) - zc) / 2.0) ** 2)) + out[i, 0] += prof[:, None, None] * disk + return out + + +@pytest.mark.parametrize("shape", [(2, 1, 8, 32, 32), (2, 1, 1, 32, 32)], ids=["3d", "z1"]) +def test_zero_at_pred_equals_target_and_positive_otherwise(shape): + target = _nuclei(shape) + loss_fn = SegConsistencyLoss(TinySegmenter()) + assert float(loss_fn(target.clone(), target)) == pytest.approx(0.0, abs=1e-6) + g = torch.Generator().manual_seed(1) + assert float(loss_fn(target + 0.3 * torch.randn(shape, generator=g), target)) > 1e-4 + + +def test_invariant_to_positive_affine_units_of_both_inputs(): + """The segmenter normalizes per image, so a z-scored pred vs a raw target loses nothing.""" + target = _nuclei((2, 1, 6, 32, 32)) + loss_fn = SegConsistencyLoss(TinySegmenter()) + assert float(loss_fn(3.0 * target + 7.0, target)) == pytest.approx(0.0, abs=1e-5) + + +def test_components_match_their_definition(): + target = _nuclei((2, 1, 6, 32, 32)) + pred = target + 0.2 * torch.randn_like(target) + seg = TinySegmenter() + loss_fn = SegConsistencyLoss(seg, crop_size=None) + loss, comps = loss_fn(pred, target, return_components=True) + pred2d, target2d = loss_fn.project(pred, target) + out, ref = seg(pred2d), seg(target2d) + d_flow = ((out["flow"] - ref["flow"]) ** 2).mean() + d_prob = _binary_kl(out["cellprob_logit"], ref["cellprob_logit"]).mean() + torch.testing.assert_close(comps["segcons_flow"], d_flow) + torch.testing.assert_close(comps["segcons_prob"], d_prob) + torch.testing.assert_close(loss, d_flow + d_prob) + assert comps["segcons"] is loss + + +def test_gradient_reaches_pred_only_and_matches_finite_difference(): + target = _nuclei((1, 1, 6, 16, 16)).double() + seg = TinySegmenter().double() + loss_fn = SegConsistencyLoss(seg) + pred = (target + 0.2 * torch.randn_like(target)).requires_grad_(True) + loss_fn(pred, target).backward() + assert torch.isfinite(pred.grad).all() and pred.grad.abs().sum() > 0 + assert all(p.grad is None for p in seg.parameters()) + # Only the 3 focus-slab planes feed the segmenter. + plane = int(nucleus_area_planes(target[:, 0])[0]) + touched = (pred.grad[0, 0].abs().sum((1, 2)) > 0).nonzero().flatten().tolist() + assert set(touched) <= {plane - 1, plane, plane + 1} + # Directional central difference. The segmenter's min/max statistics are stop-gradient, + # so perturb away from the projected image's extreme pixels, which leaves them fixed. + v = torch.zeros_like(pred) + v[0, 0, plane] = torch.randn(16, 16, dtype=torch.float64) + pred2d, _ = loss_fn.project(pred.detach(), target) + for flat_idx in (pred2d.argmax(), pred2d.argmin()): + y, x = divmod(int(flat_idx), 16) + v[0, 0, :, y, x] = 0.0 + eps = 1e-6 + with torch.no_grad(): + num = (loss_fn(pred + eps * v, target) - loss_fn(pred - eps * v, target)) / (2 * eps) + ana = (pred.grad * v).sum() + assert float(num) == pytest.approx(float(ana), rel=1e-3) + + +def test_segmenter_is_frozen_unregistered_and_stays_in_eval(): + seg = TinySegmenter() + loss_fn = SegConsistencyLoss(seg) + parent = nn.Sequential(nn.Linear(2, 2)) + parent.loss = loss_fn + assert all(not k.startswith("loss") for k in parent.state_dict()) + assert list(loss_fn.parameters()) == [] + parent.train() + assert not seg.training + assert not any(p.requires_grad for p in seg.parameters()) + + +def test_crop_size_shapes_what_the_segmenter_sees(): + target = _nuclei((4, 1, 6, 40, 40)) + loss_fn = SegConsistencyLoss(TinySegmenter(), crop_size=24) + pred2d, target2d = loss_fn.project(target, target) + assert pred2d.shape == target2d.shape == (4, 1, 24, 24) + + +def test_rejects_bad_arguments_and_shapes(): + with pytest.raises(ValueError, match="slab_halfwidth"): + SegConsistencyLoss(TinySegmenter(), slab_halfwidth=-1) + with pytest.raises(ValueError, match="crop_size"): + SegConsistencyLoss(TinySegmenter(), crop_size=0) + loss_fn = SegConsistencyLoss(TinySegmenter()) + with pytest.raises(ValueError, match="share a shape"): + loss_fn(torch.zeros(1, 1, 4, 8, 8), torch.zeros(1, 1, 4, 8, 9)) + with pytest.raises(ValueError, match="expected"): + loss_fn(torch.zeros(1, 2, 4, 8, 8), torch.zeros(1, 2, 4, 8, 8)) + with pytest.raises(ValueError, match="expected"): + loss_fn(torch.zeros(1, 1, 8, 8), torch.zeros(1, 1, 8, 8)) + + +def test_nucleus_area_planes_fallbacks(): + assert nucleus_area_planes(torch.zeros(2, 7, 8, 8)).tolist() == [3, 3] + assert nucleus_area_planes(torch.zeros(1, 1, 8, 8)).tolist() == [0] + + +def _otsu_thresholds_bincount(flat: Tensor) -> Tensor: + """The first (bincount-histogram) implementation of ``_otsu_thresholds``, kept as the reference.""" + r = flat.shape[0] + lo = flat.amin(1, keepdim=True) + width = ((flat.amax(1, keepdim=True) - lo) / _OTSU_BINS).clamp_min(torch.finfo(flat.dtype).tiny) + idx = ((flat - lo) / width).floor().long().clamp(0, _OTSU_BINS - 1) + idx += torch.arange(r, device=flat.device).unsqueeze(1) * _OTSU_BINS + hist = torch.bincount(idx.reshape(-1), minlength=r * _OTSU_BINS).reshape(r, _OTSU_BINS).double() + centers = lo.double() + width.double() * (torch.arange(_OTSU_BINS, device=flat.device) + 0.5) + w1 = hist.cumsum(1) + w2 = hist.flip(1).cumsum(1).flip(1) + m1 = (hist * centers).cumsum(1) / w1 + m2 = (hist * centers).flip(1).cumsum(1).flip(1) / w2 + var = w1[:, :-1] * w2[:, 1:] * (m1[:, :-1] - m2[:, 1:]) ** 2 + return centers.gather(1, var.argmax(1, keepdim=True)).squeeze(1).to(flat.dtype) + + +def test_otsu_thresholds_equal_the_bincount_implementation(): + vols = [_nuclei((3, 1, z, 24, 24), seed=z).reshape(3, -1) for z in (1, 5, 16)] + vols.append(torch.rand(2, 1000) ** 3 * 50 - 7) + for flat in vols: + assert torch.equal(_otsu_thresholds(flat), _otsu_thresholds_bincount(flat)) + + +@pytest.mark.skipif(not torch.cuda.is_available(), reason="host-sync detection needs CUDA") +def test_focus_planes_make_no_host_sync(): + vol = _nuclei((2, 1, 8, 32, 32))[:, 0].cuda() + torch.cuda.set_sync_debug_mode("error") + try: + nucleus_area_planes(vol) + finally: + torch.cuda.set_sync_debug_mode("default") diff --git a/packages/viscy-utils/tests/test_smooth_gaussian.py b/packages/viscy-utils/tests/test_smooth_gaussian.py new file mode 100644 index 000000000..22b6b5f94 --- /dev/null +++ b/packages/viscy-utils/tests/test_smooth_gaussian.py @@ -0,0 +1,78 @@ +"""Tests for the GPU-dispatching Gaussian blur behind the Otsu threshold. + +Unlike the median, a Gaussian computes new values by weighted summation, so +the backends reassociate floating-point additions differently and agree only +to a tolerance -- these tests assert closeness, not equality. The blur only +ever picks a threshold, so a last-bit difference cannot change a mask except +for a voxel sitting exactly on the threshold. + +Dispatch goes through ``cubic.skimage.filters``, whose proxies select the +backend from the input array's device. The GPU tests are skipped, not failed, +when cubic or a device is absent: viscy-utils does not depend on cubic, which +ships in dynacell's eval/preprocess extras. +""" + +import numpy as np +import pytest +from skimage.filters import gaussian + +from viscy_utils.meta_utils import _OTSU_BLUR_SIGMA, otsu_threshold_from_volume, smooth_gaussian + +# The footprint otsu_threshold_from_volume uses: no blur across T, sigma in ZYX. +_SIGMA = (0,) + (_OTSU_BLUR_SIGMA,) * 3 +_SHAPE = (2, 8, 16, 24) + + +def _volume(seed: int) -> np.ndarray: + """Bimodal float32 array, so Otsu has two populations to separate.""" + rng = np.random.default_rng(seed) + arr = rng.normal(400.0, 20.0, size=_SHAPE).astype(np.float32) + arr[..., 4:12, 6:18] += 300.0 + return arr + + +def test_matches_skimage_on_the_cpu_path() -> None: + """Whichever backend runs, the result matches skimage's to float32 tolerance.""" + arr = _volume(seed=0) + expected = gaussian(arr, sigma=_SIGMA, preserve_range=True) + assert np.allclose(smooth_gaussian(arr, sigma=_SIGMA), expected, rtol=1e-5, atol=1e-4) + + +def test_preserve_range_is_honoured() -> None: + """Without preserve_range skimage rescales via img_as_float, moving the threshold. + + The blurred values must stay in the input's intensity units, because the + threshold Otsu picks from them is written to zattrs and later compared + against raw voxel values. + """ + arr = _volume(seed=1) + out = smooth_gaussian(arr, sigma=_SIGMA) + assert out.min() > 300.0 + assert out.max() < 800.0 + + +def test_cubic_gpu_and_cpu_agree() -> None: + """Pin cubic's GPU blur against skimage, the assumption the speedup rests on.""" + pytest.importorskip("cubic") + torch = pytest.importorskip("torch") + from cubic.cuda import ascupy, asnumpy + from cubic.skimage import filters as cubic_filters + + if not torch.cuda.is_available(): + pytest.skip("no CUDA device available") + arr = _volume(seed=2) + on_gpu = asnumpy(cubic_filters.gaussian(ascupy(arr), sigma=_SIGMA, preserve_range=True)) + assert np.allclose(on_gpu, gaussian(arr, sigma=_SIGMA, preserve_range=True), rtol=1e-4, atol=1e-3) + + +def test_threshold_separates_a_bimodal_volume() -> None: + """The whole recipe, end to end: the threshold must land between the two modes.""" + arr = _volume(seed=3) + threshold = otsu_threshold_from_volume(arr) + assert 400.0 < threshold < 700.0 + + +def test_constant_volume_returns_its_constant() -> None: + """Otsu is undefined for a constant input; the recipe returns the value itself.""" + arr = np.full(_SHAPE, 42.0, dtype=np.float32) + assert otsu_threshold_from_volume(arr) == pytest.approx(42.0) diff --git a/packages/viscy-utils/tests/test_smooth_median.py b/packages/viscy-utils/tests/test_smooth_median.py new file mode 100644 index 000000000..1cd5dba97 --- /dev/null +++ b/packages/viscy-utils/tests/test_smooth_median.py @@ -0,0 +1,80 @@ +"""Tests for the GPU-dispatching median filter behind the fg_mask pass. + +The filter is the whole cost of both Otsu passes -- 42.8 s of the 43.9 s a +(44, 624, 924) float32 volume takes on the CPU with the 5x5x5 cube, against +~0.1 s on an A40 including both transfers. That only makes it a safe swap if +the two backends agree exactly, which they should: a median filter SELECTS an +existing element rather than computing a new one, so there is no +floating-point reassociation for the backends to disagree about. + +Dispatch goes through ``cubic.scipy.ndimage``, whose proxies select the +backend from the input array's device -- the same route +``dynacell.evaluation.metrics`` takes. The GPU test is skipped, not failed, +when cubic or a device is absent: viscy-utils does not depend on cubic, which +ships in dynacell's eval/preprocess extras. +""" + +import numpy as np +import pytest +from scipy.ndimage import median_filter + +from viscy_utils.meta_utils import smooth_median + +# The two footprints the callers actually use: the 4-D (1, 5, 5, 5) of +# otsu_threshold_from_volume over a (T, Z, Y, X) volume, and the bare 5 cube +# generate_fg_masks applies per timepoint to (Z, Y, X). Both span Z, which is +# where the accuracy comes from -- restricting them to the plane scores worse +# than the decimated (1, 3, 3) recipe they replaced. +_SIZES = [(1, 5, 5, 5), 5] +_SHAPES = {(1, 5, 5, 5): (2, 8, 16, 24), 5: (8, 16, 24)} + + +def _volume(size, seed: int) -> np.ndarray: + """Bimodal float32 array of matching rank, so the filter has structure to act on.""" + rng = np.random.default_rng(seed) + arr = rng.normal(400.0, 20.0, size=_SHAPES[size]).astype(np.float32) + arr[..., 4:12, 6:18] += 300.0 + return arr + + +@pytest.mark.parametrize("size", _SIZES) +def test_matches_scipy_on_the_cpu_path(size) -> None: + """Whichever backend runs, the result equals SciPy's exactly.""" + arr = _volume(size, seed=0) + assert np.array_equal(smooth_median(arr, size=size), median_filter(arr, size=size)) + + +@pytest.mark.parametrize("size", _SIZES) +def test_cubic_gpu_and_cpu_agree_bitwise(size) -> None: + """Pin cubic's GPU path == SciPy, the assumption the speedup rests on.""" + pytest.importorskip("cubic") + torch = pytest.importorskip("torch") + from cubic.cuda import ascupy, asnumpy + from cubic.scipy import ndimage as cubic_ndimage + + if not torch.cuda.is_available(): + pytest.skip("no CUDA device available") + arr = _volume(size, seed=1) + on_gpu = asnumpy(cubic_ndimage.median_filter(ascupy(arr), size=size)) + assert np.array_equal(on_gpu, median_filter(arr, size=size)) + + +@pytest.mark.parametrize("size", _SIZES) +def test_cubic_cpu_path_agrees_bitwise(size) -> None: + """A numpy input must route to cubic's CPU path and still match SciPy.""" + pytest.importorskip("cubic") + from cubic.cuda import asnumpy + from cubic.scipy import ndimage as cubic_ndimage + + arr = _volume(size, seed=3) + on_cpu = asnumpy(cubic_ndimage.median_filter(arr, size=size)) + assert np.array_equal(on_cpu, median_filter(arr, size=size)) + + +def test_preserves_dtype_and_shape() -> None: + """The mask pass casts the result to uint8, so dtype must not drift to float64.""" + arr = _volume(5, seed=2) + out = smooth_median(arr, size=5) + assert out.dtype == arr.dtype + assert out.shape == arr.shape + assert isinstance(out, np.ndarray) diff --git a/packages/viscy-utils/tests/test_spotlight_loss.py b/packages/viscy-utils/tests/test_spotlight_loss.py index 5055ac79f..24214da4c 100644 --- a/packages/viscy-utils/tests/test_spotlight_loss.py +++ b/packages/viscy-utils/tests/test_spotlight_loss.py @@ -251,6 +251,40 @@ def test_partial_mask_ignores_placeholder_channel_in_dice(): assert loss.item() > 0 +def test_return_components_matches_default_with_precomputed_mask(): + """return_components=True: total is bit-identical to the default call, + and the components recombine to the total under lambda_mse.""" + torch.manual_seed(0) + pred = torch.randn(2, 1, 4, 8, 8, requires_grad=True) + target = torch.randn(2, 1, 4, 8, 8).abs() + fg_mask = (target > target.median()).float() + lambda_mse = 0.5 + loss_fn = SpotlightLoss(lambda_mse=lambda_mse, sigmoid_k=-0.95) + + total_default = loss_fn(pred, target, fg_mask=fg_mask) + total, components = loss_fn(pred, target, fg_mask=fg_mask, return_components=True) + + assert torch.equal(total, total_default) + recombined = lambda_mse * components["masked_mse"] + (1 - lambda_mse) * components["dice"] + assert torch.equal(recombined, total) + + +def test_return_components_matches_default_with_otsu_mask(): + """Same guarantee on the runtime-Otsu branch (no fg_mask passed).""" + torch.manual_seed(1) + pred = torch.randn(2, 1, 4, 8, 8, requires_grad=True) + target = torch.randn(2, 1, 4, 8, 8).abs() + lambda_mse = 0.3 + loss_fn = SpotlightLoss(lambda_mse=lambda_mse, sigmoid_k=-0.95, fg_threshold=None) + + total_default = loss_fn(pred, target) + total, components = loss_fn(pred, target, return_components=True) + + assert torch.equal(total, total_default) + recombined = lambda_mse * components["masked_mse"] + (1 - lambda_mse) * components["dice"] + assert torch.equal(recombined, total) + + def test_otsu_per_channel(): """Otsu computes per-(sample, channel) thresholds.""" from viscy_utils.losses.spotlight import _otsu_threshold_batch diff --git a/packages/viscy-utils/tests/test_zarr_utils.py b/packages/viscy-utils/tests/test_zarr_utils.py new file mode 100644 index 000000000..b43891930 --- /dev/null +++ b/packages/viscy-utils/tests/test_zarr_utils.py @@ -0,0 +1,51 @@ +"""Writing AnnData zarr stores under pandas 3's default Arrow-backed strings.""" + +import anndata as ad +import numpy as np +import pandas as pd +import pytest +from anndata._io.specs.registry import IORegistryError + +from viscy_utils.callbacks.embedding_writer import write_embedding_dataset +from viscy_utils.evaluation.zarr_utils import write_anndata_zarr + + +@pytest.fixture +def arrow_adata() -> ad.AnnData: + """AnnData built under pandas' default, so its strings are Arrow-backed.""" + assert pd.options.future.infer_string, "pandas 3 default must be on for this test" + obs = pd.DataFrame( + { + "fov_name": ["A/1/0", "B/2/0", "A/1/0"], + "state": pd.Categorical(["alive", "dead", "alive"]), + "id": [0, 1, 2], + } + ) + return ad.AnnData(X=np.zeros((3, 2), dtype=np.float32), obs=obs) + + +def test_plain_write_zarr_still_rejects_arrow_strings(arrow_adata, tmp_path) -> None: + """Why the helper exists; fails once anndata writes Arrow strings natively.""" + with pytest.raises(IORegistryError, match="ArrowStringArray"): + arrow_adata.write_zarr(tmp_path / "plain.zarr") + + +def test_write_anndata_zarr_round_trips_arrow_strings(arrow_adata, tmp_path) -> None: + """Strings survive the write, and pandas' global option is left as it was.""" + write_anndata_zarr(arrow_adata, tmp_path / "emb.zarr") + + back = ad.read_zarr(tmp_path / "emb.zarr") + assert list(back.obs["fov_name"]) == ["A/1/0", "B/2/0", "A/1/0"] + assert list(back.obs["state"]) == ["alive", "dead", "alive"] + assert list(back.obs_names) == ["0", "1", "2"] + assert pd.options.future.infer_string + + +def test_write_embedding_dataset_leaves_pandas_default(tmp_path) -> None: + """The embedding writer no longer switches string inference off process-wide.""" + index_df = pd.DataFrame({"fov_name": ["/A/1/0", "/B/2/0"], "track_id": [1, 2], "t": [0, 0], "id": [0, 1]}) + write_embedding_dataset(tmp_path / "emb.zarr", np.ones((2, 4), dtype=np.float32), index_df) + + back = ad.read_zarr(tmp_path / "emb.zarr") + assert list(back.obs["fov_name"]) == ["A/1/0", "B/2/0"] + assert pd.options.future.infer_string diff --git a/pyproject.toml b/pyproject.toml index efde37d5a..134d60ef5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ urls.Repository = "https://github.com/mehta-lab/VisCy" [dependency-groups] dev = [ { include-group = "jupyter" }, { include-group = "test" } ] -test = [ "pytest>=9.0.2", "pytest-cov>=7" ] +test = [ "pytest>=9.0.2", "pytest-cov>=7", "pytest-xdist>=3.8" ] jupyter = [ "ipykernel>=7.1", "jupyterlab>=4.5.3" ] doc = [ "mike @ git+https://github.com/squidfunk/mike.git", @@ -101,8 +101,24 @@ minversion = "9.0" testpaths = [ "packages/*/tests", "applications/*/tests" ] addopts = [ "-ra", "-q", "--import-mode=importlib", "-m", "not slow" ] pythonpath = [ "applications/dynacell/tools" ] +# Set as an ini option, NOT as --strict-markers in addopts. pytest >= 9 backs the +# flag with this ini option and only honours the flag from the command line — in +# addopts it parses (config.option.strict_markers reads True) but never enforces, +# so an unregistered marker still merely warns. Verified on pytest 9.0.3. +strict_markers = true markers = [ - "slow: marks tests requiring GPU/HPC resources (deselect with -m 'not slow')", + # strict_markers above turns an unregistered marker into a collection error. + # Without it a typo'd or invented marker silently decorates nothing, so the + # test it was meant to gate runs anyway (and asserts on whatever the shared + # HPC tree happens to hold). + # + # Register every marker HERE, not via addinivalue_line in an application + # conftest: that only registers when the conftest is in the invocation's + # ancestry, so a marked test outside that app collects fine from the repo root + # and errors when run as a single file. + "slow: marks tests requiring GPU/HPC resources or live HPC data (deselect with -m 'not slow')", + "hpc_integration: requires HPC data paths and a CUDA GPU", + "requires_data: reads the live dynacell training tree on disk (deselect with -m 'not requires_data')", ] [tool.uv-dynamic-versioning] diff --git a/uv.lock b/uv.lock index 9b52e7d2d..bfd8cdd5a 100644 --- a/uv.lock +++ b/uv.lock @@ -1,18 +1,24 @@ version = 1 -revision = 3 -requires-python = ">=3.12, <3.14" +revision = 5 +requires-python = ">=3.12" resolution-markers = [ - "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version >= '3.14' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version >= '3.14' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version >= '3.14' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.13' and platform_machine != 's390x' and sys_platform == 'win32'", - "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'win32'", "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'win32'", - "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'", "python_full_version < '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'", - "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'", "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'", - "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] @@ -22,7 +28,6 @@ members = [ "cytoland", "dynacell", "dynaclr", - "eet-tracking", "viscy", "viscy-data", "viscy-models", @@ -85,24 +90,6 @@ dependencies = [ { name = "tifffile" }, ] -[[package]] -name = "aiobotocore" -version = "3.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "aioitertools" }, - { name = "botocore" }, - { name = "jmespath" }, - { name = "multidict" }, - { name = "python-dateutil" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e7/75/42cce839c2ec263ff74b10b650fe36b066fbb124cbee6f247eac0983e1ab/aiobotocore-3.7.0.tar.gz", hash = "sha256:c64d871ed5491a6571948dd48eabd185b46c6c23b64e3afd0c059fc7593ada30", size = 127054, upload-time = "2026-05-09T10:02:52.332Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/5f/85535dfb3cfd6442d66d1df1694062c5d6df02f895329e7e120b2a3d2b8b/aiobotocore-3.7.0-py3-none-any.whl", hash = "sha256:680bde7c64679a821a9312641b759d9497f790ba8b2e88c6959e6273ee765b8e", size = 89539, upload-time = "2026-05-09T10:02:50.389Z" }, -] - [[package]] name = "aiohappyeyeballs" version = "2.6.2" @@ -169,15 +156,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/cc/c122eabd7a1b7e0c9bbdd6be60e4715905b858399145d9df872bb94f1427/aiohttp-3.14.0-cp313-cp313-win32.whl", hash = "sha256:23f094a1ef64823fd35854ddf5c7a80a078162f37f9d2f7c6142b51a6affa456", size = 448656, upload-time = "2026-06-01T19:39:11.171Z" }, { url = "https://files.pythonhosted.org/packages/41/a5/bab07d79848a00eedd8ed979ccb302aaea3ac6eb9fa16bd0ed87135869b4/aiohttp-3.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:e03abdaa17d553f17e1d1d06bb266b3970106c78051d06795723e748d8e49d11", size = 475803, upload-time = "2026-06-01T19:39:13.439Z" }, { url = "https://files.pythonhosted.org/packages/d1/a0/f03ade8566c153666a3871afccbedf6d99911da006325e1fc6cf72a2de99/aiohttp-3.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:acdb400538cf4769543548bb5d1eb23d39bed4f96554a6078cb728c7cb2c268b", size = 443889, upload-time = "2026-06-01T19:39:15.945Z" }, -] - -[[package]] -name = "aioitertools" -version = "0.13.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/53c4a17a05fb9ea2313ee1777ff53f5e001aefd5cc85aa2f4c2d982e1e38/aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c", size = 19322, upload-time = "2025-11-06T22:17:07.609Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be", size = 24182, upload-time = "2025-11-06T22:17:06.502Z" }, + { url = "https://files.pythonhosted.org/packages/28/03/5f36ab196a88ba5e9648ae5643e6531e67a3a8c0e96f9c6510ff41540fec/aiohttp-3.14.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:363ef9e91014e7891679bfb2ac0a7c6ea93435dbbfd10ecf41b9f06fcf506c5f", size = 503330, upload-time = "2026-06-01T19:39:18.195Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ce/8b49ec2f30f68e02f314f4832186cd45e583360a5a386058be36855d23b6/aiohttp-3.14.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:884a4edbdad77be9d0ef36142c8b504351b170df0bf62b51e784fadabf311c42", size = 509822, upload-time = "2026-06-01T19:39:20.396Z" }, + { url = "https://files.pythonhosted.org/packages/1a/fe/6edbf5d39bf29322b6816365b17ed8ede4dace164a3aea1abcd30110eb78/aiohttp-3.14.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:70ea956f6cc4a37620966b56c2e205d88ca3e6d85ec063277e414b1035cddad3", size = 483329, upload-time = "2026-06-01T19:39:22.607Z" }, + { url = "https://files.pythonhosted.org/packages/1b/5a/fae531bdbc6456fb6241f46b7b81e4d8a0dd3fc09118a0055dc7141ac1ec/aiohttp-3.14.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:ea3b9806c89f61da22fddf1f12dd524fb368e5e28f1261fbdafe5c3cd8ce893b", size = 489502, upload-time = "2026-06-01T19:39:24.881Z" }, + { url = "https://files.pythonhosted.org/packages/36/f4/48a7b0414db7fed77a03d5dde34508c026afd83510ab6bca08c313855776/aiohttp-3.14.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a071be341c2bd9b0188e62d173509f024e0a35b1c342c53c50f8daaeda8c3bd8", size = 497357, upload-time = "2026-06-01T19:39:27.197Z" }, + { url = "https://files.pythonhosted.org/packages/75/75/e85a13a370acc007fca5feb1fd1b88ac2d8426e6dadd625479b7cadd55a3/aiohttp-3.14.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:198cfe61bf253b19da1fb3e0fa122249dc4f14c12709493fed8054aa0411cc76", size = 750898, upload-time = "2026-06-01T19:39:29.563Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e4/3d637f800c724eff0e2bed64df72557444482366fd0a35b0cec0e6968f6c/aiohttp-3.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc203d6ce6b9106d54e2a93f41dfdfebfbca2d99962ba503bfd3e5921a6549e", size = 506986, upload-time = "2026-06-01T19:39:31.872Z" }, + { url = "https://files.pythonhosted.org/packages/1d/df/35161f3598bf7501d2b2a805b41ab4f45a2e34150c421bcb4ef8c0d281a7/aiohttp-3.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9e19d17ab02bf16832a2c8c0d55a486792c5b1645665652ee9531aebcc30cb72", size = 508033, upload-time = "2026-06-01T19:39:34.137Z" }, + { url = "https://files.pythonhosted.org/packages/e5/39/b36e5d3d31e850fb4691dd3e941684ac490a2559249f6fa634b6b0fdf020/aiohttp-3.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d925fba0c14d5b498a8028b0107beebdfd16c5d48d702ff54f879cb017aaaca3", size = 1746213, upload-time = "2026-06-01T19:39:36.654Z" }, + { url = "https://files.pythonhosted.org/packages/b1/28/24e1409e605a9aa5d84abe0e2acb365354b70ae56d40948101cabe3341ab/aiohttp-3.14.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d33e61021222ce7f9792bcac870d6f58d8adfceda33ab857b01264f4560f2c5f", size = 1705862, upload-time = "2026-06-01T19:39:38.968Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d0/e5eb3ff1daeaf644c7e36a957517672494122628e067c38b263fa04eda77/aiohttp-3.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:44eca38755d0105bb32f47d085f5dd449846a449e1245fc105889e3279dcf8e3", size = 1798909, upload-time = "2026-06-01T19:39:41.334Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ba/8943f906f0570342886ababb9a722a44e360f786a028c5e0b0e29e3f735b/aiohttp-3.14.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f13087e06f68fea4941c21a0c541c00553aa16e4f8fd7bbe2b198df761e964d6", size = 1868892, upload-time = "2026-06-01T19:39:43.807Z" }, + { url = "https://files.pythonhosted.org/packages/3a/05/27df32c844b2156e1675a8d8ec22d963e3c8ba469ed7ceb1863320c7b521/aiohttp-3.14.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ff82be7f1ef73634cb77890a770743239bc3d487b848669be1c599889336dc0a", size = 1751659, upload-time = "2026-06-01T19:39:46.398Z" }, + { url = "https://files.pythonhosted.org/packages/7f/62/da182e5910ab912b2e88aa919b61a16046a37a95714a5795b02eb57b2d18/aiohttp-3.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a150c0875ac8fd87f1c398650841308a30d65facf7416b12dbdb9cfdcbe5a48c", size = 1578775, upload-time = "2026-06-01T19:39:48.902Z" }, + { url = "https://files.pythonhosted.org/packages/66/e3/53c67097e8a5ce98625e91e3fa7f43c9c6940de680345d03b3509a72a078/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:edc01ea4e1ec5a1649a28866262bf24195889ff7b27bdd947029a6086741de9b", size = 1710090, upload-time = "2026-06-01T19:39:51.392Z" }, + { url = "https://files.pythonhosted.org/packages/dd/55/0e2732ca598c7a4dfe8a775662376d0ca2977cb1030e48386d4da5d9a456/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:540632bf882ff8fc88f2e1697be0761578e89e0d79fb4a8a6d65dc5da7e729d4", size = 1715016, upload-time = "2026-06-01T19:39:53.807Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/f0b73730798c9ca525afc30b39f1f81bbe24e245d9654c54d3b39d63212d/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:860a86bc2c80237f5dff52edcf427e10a8d8352271fd84845429a3e60199e02c", size = 1763810, upload-time = "2026-06-01T19:39:56.31Z" }, + { url = "https://files.pythonhosted.org/packages/71/cc/11acb6c4518f448323405a7312b6f255d0f974a34373ad1db7633c4aadc8/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5cbd50e6a50d6b99283a826b18cbdebf65b0797689a7535cb0e9dd37be0f63c3", size = 1573064, upload-time = "2026-06-01T19:39:58.718Z" }, + { url = "https://files.pythonhosted.org/packages/de/2d/28c31dde0a7dc98c0ee7d0da2ddcec3f7688c4fc131e5989e278d0c03c0a/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:20144819e99db593e22bbd2f3f2691a5e149f879142d6b8670254708853ff4fb", size = 1775765, upload-time = "2026-06-01T19:40:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/b8/69/155c4ef3aec96417d47024800472b33b16c5d8a665371dcd044c2afdf25d/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:26b6d79aa54cb4ed50cc7d41ed14e99e0f1fc8e7c2d42f2e05b37aea897b2b52", size = 1733716, upload-time = "2026-06-01T19:40:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/5f/44/6126116fd8a316b712bb615660b855c78466bb67ba1bb1742427eafcf7ac/aiohttp-3.14.0-cp314-cp314-win32.whl", hash = "sha256:106ed074a856f3e21d186b8579e2c8afb6da598e267cdaab01059e13db2fc44d", size = 453684, upload-time = "2026-06-01T19:40:06.277Z" }, + { url = "https://files.pythonhosted.org/packages/a2/d7/eff4c58a88c5cac5e38b55f44fb8a6d3929c3cbd77356e383e094d3220bd/aiohttp-3.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f770846edae8f00ecc57af825bce811f787f87a7dcf0e90d191790efe5b31f7", size = 481758, upload-time = "2026-06-01T19:40:08.653Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ed/17b5bd9fbcb46e688f02e572f517754a9a75831e7b54702f027761dc4fa5/aiohttp-3.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:acf1581c4f21ed4b80a2dded504d87b055a071a84d5737ea966435f768275ac6", size = 450557, upload-time = "2026-06-01T19:40:11.03Z" }, + { url = "https://files.pythonhosted.org/packages/12/34/6180103ce9aabc8ebff3f7bb55a1228ffe60f61042823031d9692cb7b101/aiohttp-3.14.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6aa1a40f9cbb3da9f80714c5966b8946c21e6a2530d809b9498b33161e3c8733", size = 787878, upload-time = "2026-06-01T19:40:13.401Z" }, + { url = "https://files.pythonhosted.org/packages/92/e9/08954a40e8b7baa3d8beadd2b074b186e9b1e9c8ddabc288678a6265de50/aiohttp-3.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b62af5a8cc96a194eaa01a9ed7b34a3ffa58d3d8daaa1a0d7a749353ad12d228", size = 524400, upload-time = "2026-06-01T19:40:15.972Z" }, + { url = "https://files.pythonhosted.org/packages/08/6a/b5965a634ac4d5ba99a463314cf4ab214ca073fcdc38a15e0294273701fc/aiohttp-3.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6eb63b1417efaf7d1002a6ad034a40d44376afcc16508a57f8e74b49ad26a095", size = 527904, upload-time = "2026-06-01T19:40:18.28Z" }, + { url = "https://files.pythonhosted.org/packages/06/b4/932bcdd850c354d9bcca30f360e475d7852e30413fbbd44b182782ed5432/aiohttp-3.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c20b9ad156a79eb97be5cf9e069eec01d2f0dc8472ffbd75299a8b2d4c2cbbde", size = 1912162, upload-time = "2026-06-01T19:40:20.825Z" }, + { url = "https://files.pythonhosted.org/packages/c6/85/ce79bab0310d2e3fd2d7bc7e44412abeff7c8338f8a21dd0f2f1714989e5/aiohttp-3.14.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:40ae7b0642c25632c7eabc4a04754012691864d2a1b93becf7cddb76027b838a", size = 1778813, upload-time = "2026-06-01T19:40:23.726Z" }, + { url = "https://files.pythonhosted.org/packages/05/54/ba62ac2d1bc87e010aad23751e383b8794e45d931df67677313a2da78823/aiohttp-3.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95f5217e76a046b9f228a101717ef8d42b1eb3d9d196d15202db5bf41df88936", size = 1899969, upload-time = "2026-06-01T19:40:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/dc/82/7cc7907725d83a19f31551334061e1ab8e108b1d7ac52632a2a844a4acb5/aiohttp-3.14.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1a4a9f17e85b80878c176695c1998c790e83731d8271881e5d356488652a1f9e", size = 1991771, upload-time = "2026-06-01T19:40:29.061Z" }, + { url = "https://files.pythonhosted.org/packages/d0/1c/a57de71a4508c93a830b77c28af3d08cd97f606dedfc6b94275347744508/aiohttp-3.14.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:145262119b07d7f95abc1839add35ba2bfc84551d4b4660ca11542c0b215455b", size = 1868606, upload-time = "2026-06-01T19:40:31.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ae/3839726cd49150a53ed340cc24ce5ba09d4c2117020ef9d45542bec5eb2f/aiohttp-3.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:49a33ded29b0b2fa7a367a02cf0fb89af602bb87542a16177ec8ce1c9c51d12a", size = 1665437, upload-time = "2026-06-01T19:40:35.01Z" }, + { url = "https://files.pythonhosted.org/packages/35/1e/c237923232c7da7f0392ea25d89fc5e60c0e93f685f4ebca8e7bcdd5271c/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cc736a9c9fc2bc4dd71fd404815741b6573df27c3f985948ec4076989ac57de", size = 1834090, upload-time = "2026-06-01T19:40:37.733Z" }, + { url = "https://files.pythonhosted.org/packages/98/02/a5a7a2524f92d3911761b405a7c067c751891942144adc13e2ad79611e39/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4141a3e5342ee3053a9cab54d25b64ed28289c1041e4c54b3d99839314d90ce", size = 1816907, upload-time = "2026-06-01T19:40:40.46Z" }, + { url = "https://files.pythonhosted.org/packages/fa/76/a8b9f0d09234d516af9f2d7dd715557f33b5da3b0b56ead41d1170e86e3c/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e30871b2d58996cb81aac52d2b1d15ac05257131ef0f90f18c2115a380fbfe7c", size = 1840382, upload-time = "2026-06-01T19:40:43.48Z" }, + { url = "https://files.pythonhosted.org/packages/c9/8e/140e715a0a4bbc211979ea30ec8396ad2ed5bf90ab87d8058fc4668b1923/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:667b881d083ccae3900ea5a241e17e5007ca78844c53ed389bb63d48f729d9c7", size = 1659497, upload-time = "2026-06-01T19:40:46.265Z" }, + { url = "https://files.pythonhosted.org/packages/10/c7/7ba5de8af9650b9767b063c675427b8685f43fa7ce563673a7bc3af60f08/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:b584dfe615d151e9b8f0a8ecb3aee6147f2927ec5b95ba25fe621f5377510928", size = 1870829, upload-time = "2026-06-01T19:40:49.583Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bc/2aaab2f85cadb26ea59c091fa2b8e370d625154b5c14b478f1b489d07551/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6199707cc40e0e9cd39c36fbc97bec416c704e1d0ddce03412bb3b3e6a90ccd0", size = 1832281, upload-time = "2026-06-01T19:40:52.303Z" }, + { url = "https://files.pythonhosted.org/packages/39/98/31b9ad9fbc01f0075ee7221002df5fd2d10b647f451ca5f30edc802d9dd6/aiohttp-3.14.0-cp314-cp314t-win32.whl", hash = "sha256:a8d93334d4961c9d566b1f046c81dee475b7c21eb730728d38237bfa70d1c8e6", size = 490597, upload-time = "2026-06-01T19:40:54.937Z" }, + { url = "https://files.pythonhosted.org/packages/59/1f/299b21441c8de42ff70fddc7cfe65e92f810abcf740739a09b56f7835364/aiohttp-3.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d2ffe9b614f50f069068b3b52e73414e4107fc10b7efc939a76acff9251fdd2", size = 525789, upload-time = "2026-06-01T19:40:57.306Z" }, + { url = "https://files.pythonhosted.org/packages/70/11/7f83fcba9ee05d4c54d61b3f8104da0d43a59adac44dd28effc0c9a10422/aiohttp-3.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7a3fc4358e65826c515350f199c210de747cf669998211b1ee6c2e46de364b24", size = 467399, upload-time = "2026-06-01T19:40:59.993Z" }, ] [[package]] @@ -224,15 +243,6 @@ requires-dist = [ ] provides-extras = ["dev"] -[[package]] -name = "alabaster" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, -] - [[package]] name = "anndata" version = "0.12.6" @@ -290,31 +300,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, ] -[[package]] -name = "app-model" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "in-n-out" }, - { name = "psygnal" }, - { name = "pydantic" }, - { name = "pydantic-compat" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8d/23/a9537fd7021e8b6ea7db96b73e66fe61e809a3b23338de07d1176e69e4ee/app_model-0.4.0.tar.gz", hash = "sha256:ccf667999f6c659e921ca3490b6da176971e67cf2f41abc34e33caa8cfa18573", size = 120529, upload-time = "2025-06-20T17:41:08.943Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/fa/d7f4ae02a3b115abdf561d05ba8d5cdf9e3b2b5724b45d91e6cf08c163c3/app_model-0.4.0-py3-none-any.whl", hash = "sha256:5b1d69ee023d955b0c7a525771fd2fc02ff9d82cd2f12a982949423c3a901210", size = 65710, upload-time = "2025-06-20T17:41:07.696Z" }, -] - -[[package]] -name = "appdirs" -version = "1.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470, upload-time = "2020-05-11T07:59:51.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566, upload-time = "2020-05-11T07:59:49.499Z" }, -] - [[package]] name = "appnope" version = "0.1.4" @@ -345,6 +330,16 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, @@ -433,6 +428,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/aa/e8d7dc17054815fd4a33032a73824d51129c607db3cc720e34e11b684994/awscrt-0.33.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:b99351915b0830eb89e997409970a90d164991ab765f357c9719eba9e1e45406", size = 4195537, upload-time = "2026-05-22T19:42:35.824Z" }, { url = "https://files.pythonhosted.org/packages/5e/b0/58e6a1945223610b5866eecb5978bf86dcb28eef85a47813c9dbd646c1de/awscrt-0.33.0-cp313-cp313t-win32.whl", hash = "sha256:ebf12555563e14f8d6d89a23456cb7b907481905efc3a2c1ae74928863685ab5", size = 4138190, upload-time = "2026-05-22T19:42:37.34Z" }, { url = "https://files.pythonhosted.org/packages/c3/be/fc020b8488f0d0e0b48d3d331ef4a3fca175e08894556b8f8eb7db586ad9/awscrt-0.33.0-cp313-cp313t-win_amd64.whl", hash = "sha256:7ac4afb54890343335807e9e25277030a429f069112146418ade98c02a2a211f", size = 4296418, upload-time = "2026-05-22T19:42:38.762Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f1/fcb5d4bb2c7f8bcb28de35046e4b57f5b95f493305c95e7720ab0103df90/awscrt-0.33.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:3911132882a6687712b27541b55f19554a0a2e61e184f95ea35eddce38201d37", size = 3430854, upload-time = "2026-05-22T19:42:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/a7/26/2c866f0bb59a8cd72e4149369bd3aaefa1636390def7d2503125ee9ad076/awscrt-0.33.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e5627e05708ed036d092d01480e99eba92c370b4823c0f88a2389f68735cfcd", size = 4037329, upload-time = "2026-05-22T19:42:42.603Z" }, + { url = "https://files.pythonhosted.org/packages/57/e3/f7667f059a0162e8b5fdd3967fc36f5efdea5410edfcb91dc9774ddd0d5e/awscrt-0.33.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:26de77d818810074c84de5e3819c14ecba208da51faa3c16350c8c760384520d", size = 4325550, upload-time = "2026-05-22T19:42:44.393Z" }, + { url = "https://files.pythonhosted.org/packages/59/1c/7361ff0706189c6e08ee86bf311d75a68aefea7cf3e13884f02a0515d743/awscrt-0.33.0-cp314-cp314t-win32.whl", hash = "sha256:f460bf5d16580baa88c44eb73a08d46d0125146743b7849a2d338f39a8951c08", size = 4219307, upload-time = "2026-05-22T19:42:45.972Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2c/409e529c5ef528b09ddf0ef116d23448b417a301c25f4cfda4d2b0451eae/awscrt-0.33.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8d4d506da517475c935cfa89a7626d8d1610616c593b1c32b2dd5a4286f7e07d", size = 4396689, upload-time = "2026-05-22T19:42:47.878Z" }, ] [[package]] @@ -508,77 +508,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/89/70d22710fe3eb5eaad18fd74277ca970f1622bdd67279643268c9f489cf8/blosc2-4.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5fe4e26efde57e5fafb17518aaa8825d6ce1da6cb1a87785594edf8cc0474154", size = 6353541, upload-time = "2026-06-02T18:03:36.692Z" }, { url = "https://files.pythonhosted.org/packages/71/c6/054e8499bb2f61fcdaaff258408d0deb859fc077e50fa9389983cd98bbaf/blosc2-4.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2edede1fed53a485cb6153a8bc325d05bbc388cec737419fe556a1edf7e3b5e0", size = 6647836, upload-time = "2026-06-02T18:03:38.588Z" }, { url = "https://files.pythonhosted.org/packages/2e/be/89a8805247e8de0fba12a4f6ac254bb156fd0576742afb87714e58fb7a7e/blosc2-4.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:51d7537d97a50f0f8336550f0a5ec66e93599c8951a4144453a7c5ba60aef15b", size = 4203672, upload-time = "2026-06-02T18:03:40.491Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/ccc8fcedc661bd8697d7fb601c5d7f1170cf981662e2d8270239dc5c339a/blosc2-4.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f4ec2fd1f82d0f75eaa91c263d999ceda713d779a1d2ddb918719490825e8d76", size = 5954517, upload-time = "2026-06-02T18:03:42.576Z" }, + { url = "https://files.pythonhosted.org/packages/0f/12/e809357c8f8c617bec5ac88d6a982e3e32ea7dd9067bc3a47ebeffdde844/blosc2-4.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:016db2f3926d7d39e262ead711fc0980f0061ab7899aa27a724f335521345a90", size = 5110449, upload-time = "2026-06-02T18:03:44.739Z" }, + { url = "https://files.pythonhosted.org/packages/f0/09/37eeb0aa926bbe0d112c6ec1a6d7ce20fa3cf8f431fd774e52c96fcdb1b5/blosc2-4.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:30d2da937c886a1efa7b93bb08a23cdbd067f10308a6a329094a8e10f4cc44c3", size = 6365820, upload-time = "2026-06-02T18:03:47.531Z" }, + { url = "https://files.pythonhosted.org/packages/d0/36/d5ca09221aca7ec497b63635cb6260cb0ff9b997bc6cc195e648407ea40e/blosc2-4.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f803cecd7f69062a64fa287367dbad6989ad201278a94bf2a5ddf56d6edab965", size = 6647422, upload-time = "2026-06-02T18:03:50.414Z" }, + { url = "https://files.pythonhosted.org/packages/ec/3b/91e10bfe3cdee2708d17508c4d8fdbc0f8e328ad53eec21b8d024e0403f2/blosc2-4.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:2e9e0febd93bb4d7ecff1c5a0465da9c6e3b81fb262d655234732bccaf3caad3", size = 4286622, upload-time = "2026-06-02T18:03:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/6e/3e/cb9719a9730109fe2d6dbabf253277e0a0e56d0805c5d6c6c6b0cf8e5d10/blosc2-4.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cd8dbf99e0f30f6315037720bb2731b964ba84961eab5911d90ee8e80af046f0", size = 5989949, upload-time = "2026-06-02T18:03:54.804Z" }, + { url = "https://files.pythonhosted.org/packages/7f/cb/695999125111bb17ca81d54ea06425982095fffab2fcc3cbeac5e01e5dae/blosc2-4.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:eb2ea1089d15b8f91201df028f7e9c428065acd4f7721f8878a6f25caaac228c", size = 5157341, upload-time = "2026-06-02T18:03:56.85Z" }, + { url = "https://files.pythonhosted.org/packages/95/32/935022910ccd525c93cbd47ff257cc38a3f415ef7d170ca61ee47e8a37e0/blosc2-4.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7bfb7ab86b13f8cefe7ea08740e08df66d8fd63cb180c81a3a1321d17073f00d", size = 6317857, upload-time = "2026-06-02T18:03:58.846Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f3/2fcf50f0331fc80f4dcaa9199dba278609b3e7d0ae8decbcea4ce54de8b2/blosc2-4.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae547c72b9cc4ecbd6278e0f68306a00664ba66c8b3176b45d2c8426c28bd09", size = 6612822, upload-time = "2026-06-02T18:04:01.206Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ff/71e766357b50d11a232e233cfd16caf85cc550e6ccea0b45bb4425b22357/blosc2-4.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5bbdf3bf5746e18e6ec1bc4f33e758e39a01c3df98b127ffc30f173455229362", size = 4346450, upload-time = "2026-06-02T18:04:03.344Z" }, ] [[package]] name = "boto3" -version = "1.43.0" +version = "1.43.20" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/65/47670987f2f9e181397872c7ee6415b7b95156d711b7eab6c55f66e575bc/boto3-1.43.0.tar.gz", hash = "sha256:80d44a943ef90aba7958ab31d30c155c198acc8a9581b5846b3878b2c8951086", size = 113143, upload-time = "2026-04-29T22:07:49.084Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/7a/a3605b814fedd052fec798932f8d2b0459e61fd60ab2f652de1ce95815df/boto3-1.43.20.tar.gz", hash = "sha256:536d023d9f72dcec04254f71b216b78c6ee644be56ac61e780a6c73c1ddea445", size = 113210, upload-time = "2026-06-02T19:43:36.98Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/a0/3e6a0b1c1ea6bec76f71473727ef27abf3cd40e9709b3ebcbfbcfaae6f79/boto3-1.43.0-py3-none-any.whl", hash = "sha256:8ebe03754a4b73a5cb6ec2f14cca03ac33bd4760d0adea53da4724845130258b", size = 140497, upload-time = "2026-04-29T22:07:46.216Z" }, + { url = "https://files.pythonhosted.org/packages/5d/af/05411a9dc841d6e2344f2b8101338991bb32248e1bfa7cf8e2288327c025/boto3-1.43.20-py3-none-any.whl", hash = "sha256:ee878fa5cd739f38e68fbad535ff9bc3bf7c409290943a62f5e8bddad486aad4", size = 140536, upload-time = "2026-06-02T19:43:34.21Z" }, ] [[package]] name = "botocore" -version = "1.43.0" +version = "1.43.20" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/79/2f4be1896db3db7ccf44504253a175d56b6bd6b669619edc5147d1aa21ea/botocore-1.43.0.tar.gz", hash = "sha256:e933b31a2d644253e1d029d7d39e99ba41b87e29300534f189744cc438cdf928", size = 15286817, upload-time = "2026-04-29T22:07:31.723Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/4b/afc1fef8a43bafb139f57f73bbd70df82807af5934321e8112ae50668827/botocore-1.43.0-py3-none-any.whl", hash = "sha256:cc5b15eaec3c6eac05d8012cb5ef17ebe891beb88a16ca13c374bfaece1241e6", size = 14970102, upload-time = "2026-04-29T22:07:27Z" }, -] - -[[package]] -name = "build" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "os_name == 'nt'" }, - { name = "packaging" }, - { name = "pyproject-hooks" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796, upload-time = "2026-04-30T03:18:25.17Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", size = 26018, upload-time = "2026-04-30T03:18:23.644Z" }, -] - -[[package]] -name = "cachey" -version = "0.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "heapdict" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c6/9c/e3c959c1601013bf8a72e8bf91ea1ebc6fe8a2305bd2324b039ee0403277/cachey-0.2.1.tar.gz", hash = "sha256:0310ba8afe52729fa7626325c8d8356a8421c434bf887ac851e58dcf7cf056a6", size = 6461, upload-time = "2020-03-11T15:34:08.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/f0/e24f3e5d5d539abeb783087b87c26cfb99c259f1126700569e000243745a/cachey-0.2.1-py3-none-any.whl", hash = "sha256:49cf8528496ce3f99d47f1bd136b7c88237e55347a15d880f47cefc0615a83c3", size = 6415, upload-time = "2020-03-11T15:34:07.347Z" }, -] - -[[package]] -name = "cbcbox" -version = "2.929" -source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/04/ab59a00e6f003a5bee49464a14fa801b1498ecf1fb1ec32d5d847ab0e639/botocore-1.43.20.tar.gz", hash = "sha256:84fec90a8f25d11bb98176a3fa31c8b8e031b5be37d56f9a1a16f825b483ad4b", size = 15453850, upload-time = "2026-06-02T19:43:25.572Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/9e/c41844f3a746500b88982817233f3f2f4fa6d3a554cedb9222bac9efcbea/cbcbox-2.929-py3-none-macosx_15_0_arm64.whl", hash = "sha256:610fd250f737b19f599d56cca299682a4519472b3ff6a13044502eb71e03931e", size = 59626733, upload-time = "2026-03-23T15:28:20.758Z" }, - { url = "https://files.pythonhosted.org/packages/3f/08/172af0d618ede8668862ae9c83ffcd929bc966610c1ce357b268edb33645/cbcbox-2.929-py3-none-macosx_15_0_x86_64.whl", hash = "sha256:b369071081176fd55ba9d104c2f2330133c36f0264ebe0215c50a366f290a050", size = 87883726, upload-time = "2026-03-23T15:28:24.874Z" }, - { url = "https://files.pythonhosted.org/packages/d4/c7/a57493dd959a57dcb8dc31aeef468c0e95d066d4bbf8fc59f9dda3a1fd97/cbcbox-2.929-py3-none-manylinux2014_aarch64.whl", hash = "sha256:62841ab4ed4e1c368bcba8d62c66cd7dd03f3dfaf1ecf4a01c6385a122d1946e", size = 145740150, upload-time = "2026-03-23T15:28:29.617Z" }, - { url = "https://files.pythonhosted.org/packages/4c/86/502b81d060603e1016a8cb471064e4e65d8009c2feed60ea878b8513a728/cbcbox-2.929-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a57a403cc44fb2af0aa53cb790a7642390f1836a5ff6a7a0b31b30eb6597e3e0", size = 181208771, upload-time = "2026-03-23T15:28:35.544Z" }, - { url = "https://files.pythonhosted.org/packages/0c/85/2fef9c7c3054ebfb9b0e84fabbeac776a18514b025efa5abd768c889f560/cbcbox-2.929-py3-none-win_amd64.whl", hash = "sha256:0a91420befb965ec1763aab62d7908d3a89a51af845e2a4e05c09d99a67f1151", size = 135305901, upload-time = "2026-03-23T15:28:42.021Z" }, + { url = "https://files.pythonhosted.org/packages/94/97/0bf341f86389a02cd3c4e6e2c597d15c7bf6c033a2abf1352473fc2e8354/botocore-1.43.20-py3-none-any.whl", hash = "sha256:1ee28afba70210f726c24f4bed1bf238a33542937fa2cf1b8cf2107287b85e04", size = 15135023, upload-time = "2026-06-02T19:43:20.939Z" }, ] [[package]] name = "cellpose" -version = "4.1.1" +version = "4.2.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastremap" }, @@ -595,9 +567,9 @@ dependencies = [ { name = "torchvision" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/b0/3dde4f82033a4b8e35a48d7419aa9c4f3e7c1100055a135790a41c47f19b/cellpose-4.1.1.tar.gz", hash = "sha256:859c732c8edede78ec7ee6537c5cfb18ab54f3dc01846adc10edca0cec124094", size = 4252891, upload-time = "2026-03-31T16:40:23.181Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/e4/3d4606883a5489b86fa6ab5f65b9d0a243810f64efb934033fe425089d5a/cellpose-4.2.1.1.tar.gz", hash = "sha256:229eedea3977184202b2c87b3bd41c609facd742c7203bb4ad3cebd7b6bc6839", size = 4441350, upload-time = "2026-06-14T14:25:15.082Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/33/c2fbe5d074e2bb95bacdada02ebfa05915d211b67ab590419be1449d2115/cellpose-4.1.1-py3-none-any.whl", hash = "sha256:52d33d3c890df5572bec029ec4211388611b313c58f7b7073f47622b9befc32a", size = 213394, upload-time = "2026-03-31T16:40:21.147Z" }, + { url = "https://files.pythonhosted.org/packages/cc/20/d9d1fa1abffc8dab4381116ea0d91e8f8e4315acc038b76679608c097649/cellpose-4.2.1.1-py3-none-any.whl", hash = "sha256:6c5c5159fd459115bee390ffb01d94d5e1006b97c211bf3c1a7b37d5395a1a13", size = 203334, upload-time = "2026-06-14T14:25:13.797Z" }, ] [[package]] @@ -642,6 +614,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] [[package]] @@ -682,6 +676,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] @@ -790,6 +816,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, ] [[package]] @@ -858,25 +906,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cf/c2/cd91ead503045161092d3845f7bb95ea2f25131ce96d3e314dd835d91b9c/coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1", size = 223259, upload-time = "2026-05-26T20:40:20.381Z" }, { url = "https://files.pythonhosted.org/packages/71/9f/1e28d97e6bd2c76b07f38b7c02870f1371255ff6717f54eca578fcbbdd0e/coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce", size = 224320, upload-time = "2026-05-26T20:40:22.316Z" }, { url = "https://files.pythonhosted.org/packages/a9/e0/d936e908f0e1efa55e52b91e01b52f1055cef5e1ab2718493390ed8e2fb8/coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1", size = 222577, upload-time = "2026-05-26T20:40:24.894Z" }, + { url = "https://files.pythonhosted.org/packages/d6/34/fc2f101b151af3799a101f0550b0454aa008afdc0add677394ec4aa8ea10/coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee", size = 220091, upload-time = "2026-05-26T20:40:27.249Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a7/1ebae2ab5b961b5c79bb09fe7b3ac99edb190d8be4a8c510b2cf66f46468/coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500", size = 220421, upload-time = "2026-05-26T20:40:30.084Z" }, + { url = "https://files.pythonhosted.org/packages/5e/90/92aca9cf0acc95123c96cd1eb1f08917897a7f5dee01e15738922971ec31/coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906", size = 251466, upload-time = "2026-05-26T20:40:32.542Z" }, + { url = "https://files.pythonhosted.org/packages/26/2b/78048cbe3b999f6cbf9cc0d90abba6a88a3e0863a8c1c6cbc762f3f8802f/coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42", size = 253973, upload-time = "2026-05-26T20:40:34.473Z" }, + { url = "https://files.pythonhosted.org/packages/8e/21/c2e33b29d1cfde484a19d437afc343c6cd30b08d78cbbf9f5aff14e57b2b/coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8", size = 255318, upload-time = "2026-05-26T20:40:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ee/aad2f108d63b769121005302f16bf66db8625c88ceaba466942e09a2607e/coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851", size = 257633, upload-time = "2026-05-26T20:40:40.164Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f8/11a2c29b4fd76d9849f81d0bb812ec0017a9396df3217214e38934a8c837/coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034", size = 251488, upload-time = "2026-05-26T20:40:42.631Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b8/9a5820de4b8ac2b71d85e3b5fb49108d7469c665f0e2ad0dd7569023e305/coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c", size = 253329, upload-time = "2026-05-26T20:40:45.208Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ff/f33e4823667e27548e8fd8df44217515303f9808d0ff29817db56f87d990/coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36", size = 251291, upload-time = "2026-05-26T20:40:47.502Z" }, + { url = "https://files.pythonhosted.org/packages/68/9b/489db0ebb209054766b90a9014a45f6d26eb724c02ec21311c3733b5a644/coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5", size = 255564, upload-time = "2026-05-26T20:40:49.372Z" }, + { url = "https://files.pythonhosted.org/packages/27/b5/16bc2d4c2409b23c7737edb68c83bc89e345f378050549fe1d75ac7d34d5/coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4", size = 251107, upload-time = "2026-05-26T20:40:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/7d/0c/2629997469a00cd069d588a41c9dc887610f2775ae89d250c4791e65272a/coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d", size = 252764, upload-time = "2026-05-26T20:40:54.267Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ee/f78d63c8f079e0d7211c7e2401fa17e311514534ba61bae03e4b287ce4ab/coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee", size = 222837, upload-time = "2026-05-26T20:40:56.496Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b9/be539854f93a70dfbeec69117f33ec70dc42ff0b65b5b07ab8d40d04228e/coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7", size = 223650, upload-time = "2026-05-26T20:40:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9e/24e2842fef40f35ac82ba3a7719c8023d011bf3bf652d0675316a9d088a1/coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343", size = 222218, upload-time = "2026-05-26T20:41:00.321Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1d/ac0a9df5fe31c1e8bdd658074905fc12844a05c1a7e3fdb8417e97c31e23/coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1", size = 220822, upload-time = "2026-05-26T20:41:02.281Z" }, + { url = "https://files.pythonhosted.org/packages/32/cf/f964fd9aff20323f9f1a726c97135f8a76bcd87b92dad141a456a43f3c64/coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b", size = 221084, upload-time = "2026-05-26T20:41:04.593Z" }, + { url = "https://files.pythonhosted.org/packages/d8/5e/7e5ef2aba844de2b80d678619fcf0841b42e3f37f16411226f3fe4c1016f/coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474", size = 262454, upload-time = "2026-05-26T20:41:06.641Z" }, + { url = "https://files.pythonhosted.org/packages/64/62/75809bded87015cc4935524218a2a8ed8dd1a8498bfed30a2f4f7a4b4d34/coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86", size = 264578, upload-time = "2026-05-26T20:41:08.556Z" }, + { url = "https://files.pythonhosted.org/packages/f3/42/d33392dc14633525012d2d504fa1a33b05538bf535f5c1d64675e5754b78/coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e", size = 266981, upload-time = "2026-05-26T20:41:10.824Z" }, + { url = "https://files.pythonhosted.org/packages/2a/49/0157c4428c2aca7f1e09d5565930586fd5ae36f1655f08b0daa7cf1fcae1/coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65", size = 268112, upload-time = "2026-05-26T20:41:12.966Z" }, + { url = "https://files.pythonhosted.org/packages/96/26/86b9ce71f4092b1ed325ce1421698081df1286b833400b6836912834d6e0/coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e", size = 261558, upload-time = "2026-05-26T20:41:15Z" }, + { url = "https://files.pythonhosted.org/packages/20/4c/c311210c5472cf5401d8422b0d7812cdd520f24417673afabda6c323faca/coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8", size = 264447, upload-time = "2026-05-26T20:41:17.369Z" }, + { url = "https://files.pythonhosted.org/packages/fb/71/59513f8710ed3e6b0ac0a050a5b7e977bb9c9e880354863b5d00d8809256/coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07", size = 262048, upload-time = "2026-05-26T20:41:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/bceed32dc494f5bbf50f775cd2e78ca814953942b5ea28d3c1c3ac316f14/coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de", size = 265781, upload-time = "2026-05-26T20:41:21.559Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c5/9348fe40dbfd4991aaf78df2c6c3098bfb2cc834d1fd362a64b4efef855a/coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890", size = 260896, upload-time = "2026-05-26T20:41:23.428Z" }, + { url = "https://files.pythonhosted.org/packages/ca/92/1ea0f03929da7cf87206b1fa24f4c8e9c158be0455481af29ec0a1f3503f/coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd", size = 263214, upload-time = "2026-05-26T20:41:25.419Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a9/b2493c054c0e01a643266742ab45e15744e60743f9260cd930c7142b1124/coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e", size = 223624, upload-time = "2026-05-26T20:41:27.795Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/3e1e6a57fccd2d7c83fcdf338e93ba98eb85c6e877dd34731ac585375490/coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c", size = 224728, upload-time = "2026-05-26T20:41:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/31066cf1d2f0c6c797fce911bcfa01dd35642dc6da992a950256097c5860/coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af", size = 222752, upload-time = "2026-05-26T20:41:32.123Z" }, { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" }, ] -[[package]] -name = "ct3" -version = "3.4.0.post5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/c2/2a1605af41829cd2a247271b5b6f6b2dbc2e9407df2dd9bcafdceafcd4b3/ct3-3.4.0.post5.tar.gz", hash = "sha256:1c5f2000d52d591703c74f6f5f7ef427ed1b6501be28e3f1634f62c3a5d792e1", size = 303503, upload-time = "2025-11-29T12:38:18.761Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/ae/55e9a8ce30c37c43cd0677310bf4d58ed65b8f08880fa3ffccdd86f62125/ct3-3.4.0.post5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:443bff9699ccb0fac142027bea3321a57fb33fe43ec7cccdbac5dd0be337dcbd", size = 863020, upload-time = "2025-11-29T12:39:51.975Z" }, - { url = "https://files.pythonhosted.org/packages/0c/fe/33cab98b7ba9408b837300899bf1bded951bb0e82772c2f4f786951cb5e8/ct3-3.4.0.post5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61ec5dc8261ff8bb3b0d579ba95f8be9a938251e9a3547cc09ca65de702dbc69", size = 866396, upload-time = "2025-11-29T12:44:12.326Z" }, - { url = "https://files.pythonhosted.org/packages/9f/27/3c3e5398fa49227b2dd67211b4402c84ed9d5b48a4257bf5b1196f9b6a8e/ct3-3.4.0.post5-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3be2cd9d24b6c6741e90c3f854b20dd019b0c47f7887962972a515b92c35204e", size = 866146, upload-time = "2025-11-29T12:38:20.844Z" }, - { url = "https://files.pythonhosted.org/packages/8b/0e/a065a472cadd8821962aa1f3eb988bcaa93a6cf04eae89b4f72251f5df70/ct3-3.4.0.post5-cp312-cp312-win_amd64.whl", hash = "sha256:9f8ed00963e774c41031727876df99694695b62c791e7de2e4352892ac1e1e06", size = 187276, upload-time = "2025-11-29T12:43:22.548Z" }, - { url = "https://files.pythonhosted.org/packages/a3/85/f0536596eda1884f2c982f675eeaea63de4fd7aeab50785c1006bce1ccdd/ct3-3.4.0.post5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b9a98284dad8fd32e49c05f5223226610cadef69a6a816a4e979fa8af6d870c5", size = 873390, upload-time = "2025-11-29T12:44:00.222Z" }, - { url = "https://files.pythonhosted.org/packages/57/fa/e8aa47b6e4a39ee57618198ec778e7a2aa093f267c3b868bad6230ecfff9/ct3-3.4.0.post5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:371baa49402fdbf50924fa159000c54503071a8354922513c81bdd91234c17d1", size = 876586, upload-time = "2025-11-29T12:43:54.764Z" }, - { url = "https://files.pythonhosted.org/packages/04/1e/eeac3163475c7620621ab582375403a90c5c9d0b0704eab328ca62724383/ct3-3.4.0.post5-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a85e6d708344ebe93e846d2855d73e0db73b6a8c7aabdc2f4daf27b56abf44b9", size = 876462, upload-time = "2025-11-29T12:38:21.45Z" }, - { url = "https://files.pythonhosted.org/packages/77/a2/67e560a207ef077cff1937c4c983a78935544ea3b02262eed12feba664f7/ct3-3.4.0.post5-cp313-cp313-win_amd64.whl", hash = "sha256:216201ee1978bc653d40ccf7a1cafc5076e81670d64971a5be87e83ade0781fc", size = 187357, upload-time = "2025-11-29T12:42:36.992Z" }, -] - [[package]] name = "cubic" version = "0.7.0" @@ -908,6 +970,8 @@ wheels = [ { url = "https://pypi.nvidia.com/cucim-cu13/cucim_cu13-26.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:655292496907f5c3728ed5343806c940f226c2978ba0e6e327d2687284717cbb" }, { url = "https://pypi.nvidia.com/cucim-cu13/cucim_cu13-26.4.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdd2c7ce1029d80a960d3471e42a8f75038c85e02d2c66a803d1c9f8e46b05db" }, { url = "https://pypi.nvidia.com/cucim-cu13/cucim_cu13-26.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:692f4c640ee1c5d1d6dd65db6bebf9210f5bd3ac39354be7580a10322ba504a3" }, + { url = "https://pypi.nvidia.com/cucim-cu13/cucim_cu13-26.4.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49013b329d4887fc00688c8f5edb61c09541e7dfa2f2bff651981fd23a7f0711" }, + { url = "https://pypi.nvidia.com/cucim-cu13/cucim_cu13-26.4.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8db4b750de7898050e564804e88b363a927c25d9280dbfa4071ca9bde6a5f74" }, ] [[package]] @@ -915,13 +979,17 @@ name = "cuda-bindings" version = "13.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "cuda-pathfinder" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" }, { url = "https://files.pythonhosted.org/packages/39/2a/6d2e9047d1fb243dbaa364b01e0297534b9ed7fd27dba1c9f361519cf69b/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e32d08f71ebcdf00f0f41eab2eb37e8da94c8ed411cc9f7f7a019ce6b34abe3a", size = 6657965, upload-time = "2026-05-29T23:11:52.227Z" }, { url = "https://files.pythonhosted.org/packages/cc/6e/2394f8163360f8391f8f1b7e72d300a82724edb81a7b7084c799fbd4c91f/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9efb21c1ee64981e184b9e0ba5eb3179e5ba3d4b51665a6cb52b8ef3d01a7cbf", size = 5920504, upload-time = "2026-05-29T23:11:56.883Z" }, { url = "https://files.pythonhosted.org/packages/34/c2/ef9b6a63f7dc432712a462c816662e662e00d38caa9b861c8c2588195d03/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2732904099e0a4d4db774a5fc6d91ee95fae065b4d2ecabb4968c5fe2406c9d7", size = 6476660, upload-time = "2026-05-29T23:11:59.188Z" }, + { url = "https://files.pythonhosted.org/packages/b1/81/bff68ce829999c1e4209c761bbf903b1c06ec570416ddb25020864ad5907/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ab2f74ed65bfef4163ba07a8db16f1085e0729291db12a2423aff84ee8278b8", size = 6013639, upload-time = "2026-05-29T23:12:03.509Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e0/c8a1f0c8f9ffdea4f5fe6dbab89b326cef4d85caf489dad39e209da89416/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd4c814d311ec08c981f6dded1dbe7d4b371067ee4f6c14cccec4bde9590f80", size = 6534419, upload-time = "2026-05-29T23:12:05.633Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/83b1f563925b290f2d11a01a77a84013ba56052fe3653a5bef3ccfbb43d6/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3c772dfff49681541d59630c90f858e173ac926b9c593a2b7123f2a1043cc76", size = 5809771, upload-time = "2026-05-29T23:12:10.422Z" }, + { url = "https://files.pythonhosted.org/packages/12/20/e79b4bfe98f075195afb6343d41c498f9dbd2d161d7021d4d28bceb83581/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36febb7c1079d68a981dbbd8d5a67235b399802b82075c9388624719607e52b9", size = 6358584, upload-time = "2026-05-29T23:12:12.767Z" }, ] [[package]] @@ -942,34 +1010,34 @@ wheels = [ [package.optional-dependencies] cudart = [ - { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime" }, ] cufft = [ - { name = "nvidia-cufft", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cufft" }, ] cufile = [ - { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cufile" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti" }, ] curand = [ - { name = "nvidia-curand", marker = "sys_platform == 'linux'" }, + { name = "nvidia-curand" }, ] cusolver = [ - { name = "nvidia-cusolver", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusolver" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparse" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvtx" }, ] [[package]] @@ -987,6 +1055,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/0f/149a9e6c561f44e6522be6c3fdacdaed17d8fd5a1651004a0782ce045502/cupy_cuda13x-14.1.1-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:cc671cb766f1c42cb6eed3bf27e9da6092721bcb3d77e09aef28e2f5b089ea38", size = 72811428, upload-time = "2026-06-01T04:54:22.432Z" }, { url = "https://files.pythonhosted.org/packages/80/f6/f4cd043ad8c8e18410e9cea73a8de8778a0a05c705b8505729a794758bf1/cupy_cuda13x-14.1.1-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:ff4526725c1398ea19d578044737aa24859b08e1f469aba0ca45dfd6f9aa505d", size = 69094726, upload-time = "2026-06-01T04:54:26.424Z" }, { url = "https://files.pythonhosted.org/packages/54/64/f2b60a0f4d73820d774906bf8684df0c8494820d78b3494121c7ec57ee25/cupy_cuda13x-14.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:54949b4e4f90e3588eeef25b55037f156578364e4835eb9ae1b93e236f2e58d5", size = 35171412, upload-time = "2026-06-01T04:54:31.313Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/9bc8d6d40003640653161e2dba78740f2055df8bb48e216eed5584cecc3c/cupy_cuda13x-14.1.1-cp314-cp314-manylinux2014_aarch64.whl", hash = "sha256:8b068f6958811bc50d81bd0a83267ec4cffc3d7e842120b9646ec547f5a69298", size = 72680680, upload-time = "2026-06-01T04:54:35.077Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2d/8c79c577e9b35c60e5600217d1a849ab76c18a289f99a4145aa85388a531/cupy_cuda13x-14.1.1-cp314-cp314-manylinux2014_x86_64.whl", hash = "sha256:39f8aeeb5bad16603956f8ec00094e058a9dd5db92ab514975686750363c3692", size = 68432306, upload-time = "2026-06-01T04:54:38.882Z" }, + { url = "https://files.pythonhosted.org/packages/c4/03/8d4d77138eb5f60a2a1fb5484a89dad2bc6bc6283682179994d722cfb4b5/cupy_cuda13x-14.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:dae6f77edfe96ab8e014d3e589dca33906d212fdf8f72e66850d4612bc031783", size = 35323057, upload-time = "2026-06-01T04:54:42.541Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c4/eb46fec30fffd7a57a80cc7f027e50a1d530b332e81362a8b874905da2d1/cupy_cuda13x-14.1.1-cp314-cp314t-manylinux2014_aarch64.whl", hash = "sha256:509f378c865c0b6bc9ed1dfe9e94b08b4511e9865363fdac55f9d66fe341e91d", size = 72984246, upload-time = "2026-06-01T04:54:46.464Z" }, + { url = "https://files.pythonhosted.org/packages/24/59/e0098ca2e1ba99a0086ad4b515b2d7dbb8ae0f13b9721c02eed0db4adec5/cupy_cuda13x-14.1.1-cp314-cp314t-manylinux2014_x86_64.whl", hash = "sha256:6c0d19789e9f9515988d51dffaff70af8b3dcdddcdf2fbafa9eff08df7be0a51", size = 68662887, upload-time = "2026-06-01T04:54:50.932Z" }, ] [[package]] @@ -998,35 +1071,6 @@ 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 = "cython" -version = "3.1.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b4/a4/ec55945b52d0b888e2a090450a5524001bfa17c8b68348379a154badb850/cython-3.1.8.tar.gz", hash = "sha256:518475fbe633ba30c1577669f417440f41e1a23ad6b6e2dbb20703cd457f04ef", size = 3193040, upload-time = "2026-01-03T18:30:25.181Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/05/6b/0a64844db2ba8a15f4108700afde4b8f73352fdbf73516ac26d5a025ea12/cython-3.1.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:850503d88092af519662cf66aef26f95db677bdbba55a482ae8658c5b3ac6ad4", size = 3059306, upload-time = "2026-01-03T18:31:20.734Z" }, - { url = "https://files.pythonhosted.org/packages/72/fb/7448e6ba0baf50df463ee0e4f8c332750778960f21c7e73b574e90dd85fb/cython-3.1.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0aa5443cc8bfc937369871fbba59990b36cd9af73ed70db9052ac980d44492e4", size = 2967016, upload-time = "2026-01-03T18:31:22.918Z" }, - { url = "https://files.pythonhosted.org/packages/0f/a6/a5ae92b551ea07dcb106c39835d27e8133d3c80f7f752161795e2d5b8deb/cython-3.1.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9029c0eb895a56746ca0d4b3a6a0a57e4c90f2599204e99f7033818ea1c3022", size = 3385245, upload-time = "2026-01-03T18:31:24.544Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e0/97031ea58d8e68d9465039a9cb7ef0f94dae301907fc352d69f973587008/cython-3.1.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9cd8d1194ab8f52898be10d252ff17360e0af0e2a6662a8d9688fdd8e4cdd7e", size = 3163833, upload-time = "2026-01-03T18:31:28.108Z" }, - { url = "https://files.pythonhosted.org/packages/a9/5e/b590aff6c332fb6bf5c12d2fae0383f6e866b47d0b1efd8bdc9443198ce0/cython-3.1.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bcbed7578f1986a61471476ea5deb0806591aa477230954dd9898a25d0f4e2", size = 3314265, upload-time = "2026-01-03T18:31:29.758Z" }, - { url = "https://files.pythonhosted.org/packages/1d/82/54aa9d4cf913a68bc9d2f96c3d24024bb032b1d864bbe2b238571f85743d/cython-3.1.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d973d25a767abcf977737221dc11befda98daf2573e8bc3c2e6995479505f92a", size = 3176271, upload-time = "2026-01-03T18:31:31.817Z" }, - { url = "https://files.pythonhosted.org/packages/03/c0/306dbd4b795d61f1452fb955b6a3872a02de060cbfe84c27e33b7c8ef987/cython-3.1.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a9288fa02135743719942deaeb44976399a5785a59f076221ef7e9b22cd0d571", size = 3397778, upload-time = "2026-01-03T18:31:33.48Z" }, - { url = "https://files.pythonhosted.org/packages/e1/4c/9f4955fd1f14c5b05b72bb060b6568ba858b90084d7ee8f0744465458c0d/cython-3.1.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7eb8448eb1c3632c4684ee98db935cf7d3b54fb528a7a455841789316667cd88", size = 3324441, upload-time = "2026-01-03T18:31:35.774Z" }, - { url = "https://files.pythonhosted.org/packages/df/0f/036800c421ddf465312fa26872835f2388e8f4247aac1b9ac31527738d83/cython-3.1.8-cp312-cp312-win32.whl", hash = "sha256:c6b74c42bf6b2381d9fdb65f7eda44bcc5c29e4e96c677167495d2cd882d7a3c", size = 2483909, upload-time = "2026-01-03T18:31:37.72Z" }, - { url = "https://files.pythonhosted.org/packages/aa/77/59db4b7e09374a718564a4a9758c88285131d74cecde275991a2b513b05e/cython-3.1.8-cp312-cp312-win_amd64.whl", hash = "sha256:cae61e8e730bfc7de917204c8b7c1ca8e3e0d134dcaa55a2e2ef98de741590fb", size = 2701034, upload-time = "2026-01-03T18:31:39.58Z" }, - { url = "https://files.pythonhosted.org/packages/40/eb/363c88e2038c7f8acdf0d8a35a79aee9c738946f613ba070ab4ab331ca2f/cython-3.1.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e10d15d2e7f245e61dec89c27da377c2136c08efb6cc2c50baea25b39aab3f9", size = 3047679, upload-time = "2026-01-03T18:31:41.607Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ea/31560e57d1cbe5e7b18a949e2103b8e08617a6796356f4771200b7b5dff5/cython-3.1.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e04bb68f932853a38bab7f7c024088a9f6510ccb45ad9a12cca6ebb09ec7a25f", size = 2955139, upload-time = "2026-01-03T18:31:45.551Z" }, - { url = "https://files.pythonhosted.org/packages/b6/77/523f572b65ebe160493a834652fa829d955c97dccef36f8f0d2bb82efbc9/cython-3.1.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:85c39a8005f314ee90ab271d2b68b6725f94fc08c4e6728417b64704a7ebce04", size = 3359630, upload-time = "2026-01-03T18:31:48.025Z" }, - { url = "https://files.pythonhosted.org/packages/53/58/a3d7f9a0318c6334cb84b8029858dcbc64de0968fa7dde54ea54cdc8585b/cython-3.1.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1be05353f84e6a32cbae5869da1af72d356cc73a6dfa521423543688f6b59d67", size = 3153394, upload-time = "2026-01-03T18:31:49.661Z" }, - { url = "https://files.pythonhosted.org/packages/97/59/aaa43285de4970857787c4f43ae5a4f6bb40001c161552e1fe1b6bb5274b/cython-3.1.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03959ed8996e16550267469187442f3926ea3ba4bb220a06714e6203e4cb6017", size = 3304486, upload-time = "2026-01-03T18:31:51.763Z" }, - { url = "https://files.pythonhosted.org/packages/fa/56/45029eaa65b91579f557cd2c42b1dcabb0a4c2bff1be3b6b7e19743ff0f8/cython-3.1.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:354f43c6d0e312f650ed39c683e550f669ed18f49ca0bd5772f207ef31f8980c", size = 3169675, upload-time = "2026-01-03T18:31:53.448Z" }, - { url = "https://files.pythonhosted.org/packages/2a/bd/d6ad2772c38fba22820ee9ba4a0b647232ebdcb16138751e70c02eafe286/cython-3.1.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b690edfa6b333f6139e8ecaa5af8fe87ea591f04d2f2b6a492ed4f2896d818c4", size = 3373228, upload-time = "2026-01-03T18:31:55.571Z" }, - { url = "https://files.pythonhosted.org/packages/62/83/03f06cf647a0f579cfcfd7d336491426c95838cf9f3e4d6cb9e2c3c1e811/cython-3.1.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:65d0cf78a14616cb05ff21f63b47bf369e23f353acd51420526c5eb3029f682a", size = 3316815, upload-time = "2026-01-03T18:31:57.929Z" }, - { url = "https://files.pythonhosted.org/packages/3a/9d/2c5088d4c2c9d39692c5aca52fad35ade256d871ebecde61171acb3925fd/cython-3.1.8-cp313-cp313-win32.whl", hash = "sha256:34eb6dfaa0786a7477aaac86696f26efc052cda586d4f97a9d1d2c6b2ff6d26d", size = 2477612, upload-time = "2026-01-03T18:31:59.725Z" }, - { url = "https://files.pythonhosted.org/packages/00/b0/4cb436aab34f082454cf38cd21ed64021450fe51d1006aecaba62b6ac2c1/cython-3.1.8-cp313-cp313-win_amd64.whl", hash = "sha256:fc3cbaee08d424d2852c7b134ca432501792f67021b73a029cf2f13fb6172423", size = 2698217, upload-time = "2026-01-03T18:32:01.635Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c7/b81516c9bb5da55d327b3cf2f33f39217719e73b524ab49a4028e18b6a66/cython-3.1.8-py3-none-any.whl", hash = "sha256:f36368c1d7ea4003c52e9e93f3556074a631a6482f8a32e781cb073e211e4df9", size = 1223843, upload-time = "2026-01-03T18:30:22.229Z" }, -] - [[package]] name = "cytoland" source = { editable = "applications/cytoland" } @@ -1121,6 +1165,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/57/412421516afc3055fa577516f00beec3d663f9b0ab330639547ae6c57720/debugpy-1.8.21-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:ecbd158386c31ffe71d46f72d44d56e66331ab9b16cad649156d514368f23ab2", size = 3962096, upload-time = "2026-06-01T19:30:59.235Z" }, { url = "https://files.pythonhosted.org/packages/c1/62/2c616337cf6ba7b07ebbc97f02c6c945a8e2f76b365e33ee809c32ee36d1/debugpy-1.8.21-cp313-cp313-win32.whl", hash = "sha256:2c2ae706dec41d99a9ca1f7ebc987a83e65578363be6f6b3ac9067504917fae1", size = 5336288, upload-time = "2026-06-01T19:31:00.79Z" }, { url = "https://files.pythonhosted.org/packages/f8/99/9175103392f84c4b1bf7622888cdc68da07f0ff7d9e581266428f6776033/debugpy-1.8.21-cp313-cp313-win_amd64.whl", hash = "sha256:aa648733047443eb1d07682c4ef287d36a54507b643ffdf38b09a3ef002c72a0", size = 5376567, upload-time = "2026-06-01T19:31:02.56Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3d/f4bbb323a548bfab2af3d6b4ffd9bf22636e55956a1285d317a1de643aad/debugpy-1.8.21-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9bb2a685287a2ac9b181cde89edcec64845cb51de7faaa75badb9a698bc24782", size = 2477209, upload-time = "2026-06-01T19:31:04.157Z" }, + { url = "https://files.pythonhosted.org/packages/8c/2d/6e7ec524984a1702777868de49a4c53202bddac2a432a76a093469587750/debugpy-1.8.21-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:3d6922439bf33fd38a3e2c447869ebc7b97da5cd3d329ff1ef9bc06c4903437e", size = 3927115, upload-time = "2026-06-01T19:31:05.863Z" }, + { url = "https://files.pythonhosted.org/packages/97/47/d1aa6d64005a98a9144647d99306b419396f9ad7bf1d73c119e17a81fb4d/debugpy-1.8.21-cp314-cp314-win32.whl", hash = "sha256:15d4963bd5ffa48f0da0947fd06757fa7621945048a14ad7705431566d3c0e7c", size = 5336724, upload-time = "2026-06-01T19:31:07.711Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/b905b90d163af11878c1af8abafa4a25206335e112e284e413454543a6da/debugpy-1.8.21-cp314-cp314-win_amd64.whl", hash = "sha256:fe0744a12353406de0ae8ccff0d0a4a666f00801a3db8fd04e7a5f761cd520e8", size = 5373803, upload-time = "2026-06-01T19:31:09.469Z" }, { url = "https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl", hash = "sha256:b1e37d333663c8851516a47364ef473da127f9caebe4417e6df6f5825a7e9a92", size = 5352888, upload-time = "2026-06-01T19:31:25.186Z" }, ] @@ -1192,15 +1240,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, ] -[[package]] -name = "docutils" -version = "0.22.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, -] - [[package]] name = "donfig" version = "0.8.1.post1" @@ -1230,6 +1269,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ed/7f/06ce3d5ce51a959be0534584ad2556e6c8be966ef1218a866c6c3d62e3c5/dtaidistance-2.4.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:94b841d6575e3ad715b4e213f0f04de25e23c2da3ac21ee9c6775b38f5bdfecf", size = 1738478, upload-time = "2026-02-13T08:14:51.715Z" }, { url = "https://files.pythonhosted.org/packages/db/8e/6c8a5c7710f9f5e3805281974ce8fea4ad0334c00a1e0f977977c045a594/dtaidistance-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0f2a65628aea82175e7f8c5e96faf5372c933ed40e2e39a84957d8fe305158d", size = 4341606, upload-time = "2026-02-12T22:23:46.977Z" }, { url = "https://files.pythonhosted.org/packages/9f/b6/7f77c6773380742660d09f379d43814b448296fc24c3fb1de15a3d813311/dtaidistance-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8c9ef4c7270d1a192e8f1b481c2e10e63c33c6e7edfc507acac7f3fdc19949f", size = 1441578, upload-time = "2026-02-12T22:23:48.897Z" }, + { url = "https://files.pythonhosted.org/packages/05/72/ac72a2e196c66c627c1f51684ffa2fd782e9cb042baa29898206f79b0d86/dtaidistance-2.4.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:bda9849cd22800cd8b3c6abf29574db241458823814cd37302d01df99474175a", size = 2136045, upload-time = "2026-02-12T22:23:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/30/30/60f941b3fed3d8b94e7315a71b8f87294f2e053c1f3c19e53f7b6cc33689/dtaidistance-2.4.0-cp314-cp314-macosx_26_0_arm64.whl", hash = "sha256:b5aa878c57f779cb9e141c0b4b1bc6b5be9c1721b349d153758112854a1b24cf", size = 1676822, upload-time = "2026-02-13T08:14:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/b9/82/b805e66d3b05e2cfc4e209b7d7f62ac31fb7e72615c3532c7edb0bf1943a/dtaidistance-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2d7d26b9023788f62e5f245db5b78b68aacde67e5aeaa75906ce7ddb251da7f", size = 4328390, upload-time = "2026-02-12T22:23:52.115Z" }, + { url = "https://files.pythonhosted.org/packages/2c/89/da956340797c0ea022a35b6b0df9a118cc4427401d9a8119dc104d4b48de/dtaidistance-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:3a35f2957bbf50b068b0b90f5cc9b442bf9b2a90b855e713811b8badf89a668d", size = 1462169, upload-time = "2026-02-12T22:23:53.598Z" }, + { url = "https://files.pythonhosted.org/packages/a7/02/16088a7bd17340a4e600f49bf4da16a9741ddbb737202a91363407e993b2/dtaidistance-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e083a5163c780a5b711d970c190d3eca83ebc0ec86e453e6f56d63b1d6d78139", size = 4332943, upload-time = "2026-02-12T22:23:55.009Z" }, ] [[package]] @@ -1252,6 +1296,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/4c/0907c3f76adb9dd90e67610b31e0304a35814e65c4c41a354a262c09b885/duckdb-1.5.3-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787df63824f07bf18022dbc3b8ca4b2bfab0ebe616464f55c6e8cd0f59ea762e", size = 21453266, upload-time = "2026-05-20T11:55:04.5Z" }, { url = "https://files.pythonhosted.org/packages/6d/9c/d2f23a7803ddbbd9413f7572ecf66a15120ed5ced7ce5c73e698c1406b76/duckdb-1.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:bb5bb5dcdd09d62ee60f0ddbbef918e71cce304ffe28428b1131949d39ffaabf", size = 13118640, upload-time = "2026-05-20T11:55:07.389Z" }, { url = "https://files.pythonhosted.org/packages/27/d5/7ba2316415bcdab6edd765bbbe35c2ca8a3800f2fe695cd70e3cdb997f09/duckdb-1.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:2fa17ecdd5d3db122836cb71bb93601c2106a3be883c17dffddc02fbf3fa7888", size = 13926409, upload-time = "2026-05-20T11:55:10.166Z" }, + { url = "https://files.pythonhosted.org/packages/a5/c2/d4b6f8a5e4d3bc25773be6da76a99d9661ebbf3552c007c460d2dd59dbf8/duckdb-1.5.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:4bfa9a4dadf71e83e2c4eaca2f9421c82a54defecc1b0b4c0be95e2389dec4fe", size = 32636685, upload-time = "2026-05-20T11:55:13.158Z" }, + { url = "https://files.pythonhosted.org/packages/42/58/e835c8298979d29db7a62cb5acc29e9b57aeaca7cdde2fcd3ac980f5cb18/duckdb-1.5.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aea7baf67ad7e1829ac76f67d7dcbd7fb1f57c3eb179d55ac30952df4709ae30", size = 17308134, upload-time = "2026-05-20T11:55:16.194Z" }, + { url = "https://files.pythonhosted.org/packages/c9/46/617b51363f5613418c8b224b3cce16b58e6dde80904566bec232579c1d4e/duckdb-1.5.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b0b4f088a65d77e1217ce5d7eff889e63fedc44281200d899ff47c84d8ff836", size = 15449891, upload-time = "2026-05-20T11:55:18.687Z" }, + { url = "https://files.pythonhosted.org/packages/b3/72/354146656e8d9ba3853d3a5ee80a481b8c5f70edfc3d5ae80a8c4479c967/duckdb-1.5.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe8d0c1f6a120aa03fa6e0d03897c71a1842e6cf7afd31d181348391f7108fe1", size = 19338499, upload-time = "2026-05-20T11:55:21.34Z" }, + { url = "https://files.pythonhosted.org/packages/56/8f/65fc623b51448f2bfba1a9ec6ab3debb4664c0876c0113a5e782600b53ac/duckdb-1.5.3-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0405eae18ec6e8210a471c97dbfe87a7e4d605274b7fe572a1f276e92158f13", size = 21455828, upload-time = "2026-05-20T11:55:23.847Z" }, + { url = "https://files.pythonhosted.org/packages/2b/db/d0274cbe9f5fe219f77c0bdf900ac77103569e83c102a4225ce04cbc607d/duckdb-1.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:33ae08b3e818d7613d8936744b67718c2062c2f530376895bfd89efb51b81538", size = 13640011, upload-time = "2026-05-20T11:55:26.276Z" }, + { url = "https://files.pythonhosted.org/packages/07/5d/8f1899b8bef291caf953992fcd6c24df9f29387a35645e58c2504a5ca473/duckdb-1.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:746433e49bbc667b4df283153415fbe37e9083e0eff6c3cd6e54de7536869cd4", size = 14411554, upload-time = "2026-05-20T11:55:29.037Z" }, ] [[package]] @@ -1483,156 +1534,12 @@ test = [ ] [[package]] -name = "edt" -version = "3.1.1" +name = "execnet" +version = "2.1.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a5/ca/432c11df2a261fcae71dea11eeabfc5affd8ada22aaafa378212c1e9c9e2/edt-3.1.1.tar.gz", hash = "sha256:4b0ab5d898077eda76d1ebdec6e3a4085fd33d4c85195d71abf464ff225915c5", size = 3234159, upload-time = "2026-03-01T05:20:30.695Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/e2/f54b48f0385898b1e5ee406cf99394e7764c9d77fff1741f0fb979f784d0/edt-3.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7234587b35ce80dcf15ad7c81b45d79d75822c8b6b723e100573a5101bde90d9", size = 271864, upload-time = "2026-03-01T05:19:37.877Z" }, - { url = "https://files.pythonhosted.org/packages/2f/fe/3d0bb0dcb15382f290fc747d0709fe3d72adf5dad3f9f86d7743845d3a42/edt-3.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9850d32ed0e39fae0677a3ec33202a2f4a1be8bf19d0a9371b555adbca649f56", size = 253569, upload-time = "2026-03-01T05:19:39.059Z" }, - { url = "https://files.pythonhosted.org/packages/05/b1/5d88ed781e1b72d24c9ff0a08fae46b7d704d1d057f1888fb922345c79f2/edt-3.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec432bdf8441bdf7d98bf3295280662dbba65c9dcecc54d8713b5238ffeb16e7", size = 2876532, upload-time = "2026-03-01T05:19:40.457Z" }, - { url = "https://files.pythonhosted.org/packages/f1/79/dad79e48da52bed034fcf3589d4f2d97a77d010b7776ffd0d29f6d63516b/edt-3.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f5e28666d723e577327d4fc2ce5c4fd572e20299a2b14e5bb829a675a95340e", size = 2969706, upload-time = "2026-03-01T05:19:42.217Z" }, - { url = "https://files.pythonhosted.org/packages/21/4d/1509a586dddd92288410d5de605c38f83fb1f1e7572193ca941bd2a88c86/edt-3.1.1-cp312-cp312-win32.whl", hash = "sha256:d713a4b0a8e097cb5bf2c7fdebd9dc76f6668de7c4e62e0ca846b68ddbd6f64d", size = 170753, upload-time = "2026-03-01T05:19:44.335Z" }, - { url = "https://files.pythonhosted.org/packages/24/88/623a76e7451d9b147bf8f025921c42063e85eac3729e88807af3336bab46/edt-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:da204a3b1e2b98a414a0d45a5e3806b91e0e16daf43581d98e7adef74f960769", size = 206157, upload-time = "2026-03-01T05:19:43.502Z" }, - { url = "https://files.pythonhosted.org/packages/2a/3a/46f02072334c42146ffd241251ac5ca795e84f6d6ffa87b95df37d047e64/edt-3.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05959ab8c0f9ce1a1b2736d27c83780f1a2ab77232428f5a606d5e447b7f9a3a", size = 270847, upload-time = "2026-03-01T05:19:46.505Z" }, - { url = "https://files.pythonhosted.org/packages/fa/15/92f5dceb6b953f1c31b4b627466977fc5a3d18514eeed27f7be1d287b105/edt-3.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b6922e81f1b47832a7a71302eb79372f3aea2a0bbca6bb6b6f6275df18bdaab", size = 252991, upload-time = "2026-03-01T05:19:47.489Z" }, - { url = "https://files.pythonhosted.org/packages/0a/eb/37e1a719432e747aa6b8d2046eae910f76640bd9be4d71f667cc3dda2356/edt-3.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:467ce55f466ce21c5fb90462cabf82c73796920a8a9d5c0f655b01c6f427c7c3", size = 2867610, upload-time = "2026-03-01T05:19:49.352Z" }, - { url = "https://files.pythonhosted.org/packages/c0/16/35778deac2353ed385e81bda4dfd2eefcc1c0c1e1c083f4cb39ef0025d18/edt-3.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b464bd8bb6e14edfc07f8c1382700446bc20a2b521ec494f3de1fd5d20471cf3", size = 2954115, upload-time = "2026-03-01T05:19:52.558Z" }, - { url = "https://files.pythonhosted.org/packages/22/87/33fa8d7787489dd21957de56fbc6ad655dd156382b78e9ce2aeef125a044/edt-3.1.1-cp313-cp313-win32.whl", hash = "sha256:06c0ff97bc705803880a153c34b12b8245da51951198f4689942020a9bfcf6ff", size = 170606, upload-time = "2026-03-01T05:19:54.967Z" }, - { url = "https://files.pythonhosted.org/packages/fb/cf/ce2e98dc0b060c705c321090d9d9fcd9ab39b041309ccc0ca0ae9009cc94/edt-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:d6804fe5fcbd7d03813eb48fc33cdfd25cb166d0241768969ee24f209fca07de", size = 206561, upload-time = "2026-03-01T05:19:53.741Z" }, -] - -[[package]] -name = "eet-features" -version = "0.1.0" -source = { editable = "../../../../../home/eduardo.hirata/repos/eet_features" } -dependencies = [ - { name = "dask", extra = ["array"] }, - { name = "h5py" }, - { name = "numpy" }, - { name = "polars" }, - { name = "scikit-image" }, - { name = "scipy" }, - { name = "toolz" }, - { name = "tracksdata" }, -] - -[package.metadata] -requires-dist = [ - { name = "dask", extras = ["array"], specifier = ">=2022.3.0" }, - { name = "h5py", specifier = ">=3.9.0" }, - { name = "numpy", specifier = ">=1.24.0" }, - { name = "polars", specifier = ">=0.19.0" }, - { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.5.0" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" }, - { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, - { name = "scikit-image", specifier = ">=0.21.0" }, - { name = "scipy", specifier = ">=1.11.0" }, - { name = "toolz", specifier = ">=0.11.0" }, - { name = "tracksdata", git = "https://github.com/royerlab/tracksdata?rev=b605d2d6aa4bd12a31c07b499f5acf8411bac518" }, -] -provides-extras = ["dev"] - -[[package]] -name = "eet-inference" -version = "0.1.0" -source = { editable = "../../../../../home/eduardo.hirata/repos/eet_inference" } -dependencies = [ - { name = "dask" }, - { name = "eet-features" }, - { name = "gurobipy" }, - { name = "numpy" }, - { name = "polars" }, - { name = "pydantic" }, - { name = "pyyaml" }, - { name = "rich" }, - { name = "spatial-graph" }, - { name = "torch" }, - { name = "tracksdata" }, - { name = "typer" }, -] - -[package.metadata] -requires-dist = [ - { name = "dask" }, - { name = "eet-features", specifier = ">=0.1.0" }, - { name = "gurobipy", specifier = ">12.0.1,<13.0.0" }, - { name = "numpy", specifier = ">=1.24.0" }, - { name = "polars", specifier = ">=1.0.0" }, - { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.5.0" }, - { name = "pydantic", specifier = ">=2.12.5" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" }, - { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" }, - { name = "pyyaml", specifier = ">=6.0.3" }, - { name = "rich", specifier = ">=14.3.1" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, - { name = "spatial-graph", specifier = ">=0.0.5" }, - { name = "torch", specifier = ">=2.0.0" }, - { name = "tracksdata" }, - { name = "typer", specifier = ">=0.21.1" }, -] -provides-extras = ["dev"] - -[[package]] -name = "eet-tracking" -source = { editable = "applications/eet" } -dependencies = [ - { name = "dask", extra = ["array"] }, - { name = "eet-features" }, - { name = "eet-inference" }, - { name = "iohub" }, - { name = "napari", extra = ["pyqt5"] }, - { name = "napari-geff" }, - { name = "pyyaml" }, - { name = "rich" }, - { name = "torch" }, - { name = "ultrack" }, - { name = "ultrack-td" }, -] - -[package.dev-dependencies] -dev = [ - { name = "ipykernel" }, - { name = "pytest" }, - { name = "pytest-cov" }, -] -jupyter = [ - { name = "ipykernel" }, -] -test = [ - { name = "pytest" }, - { name = "pytest-cov" }, -] - -[package.metadata] -requires-dist = [ - { name = "dask", extras = ["array"] }, - { name = "eet-features", editable = "../../../../../home/eduardo.hirata/repos/eet_features" }, - { name = "eet-inference", editable = "../../../../../home/eduardo.hirata/repos/eet_inference" }, - { name = "iohub", specifier = ">=0.3.3" }, - { name = "napari", extras = ["pyqt5"] }, - { name = "napari-geff", editable = "../../../../../home/eduardo.hirata/repos/napari-geff" }, - { name = "pyyaml" }, - { name = "rich" }, - { name = "torch", specifier = ">=2.0.0" }, - { name = "ultrack", editable = "../../../../../home/eduardo.hirata/repos/ultrack" }, - { name = "ultrack-td", editable = "../../../../../home/eduardo.hirata/repos/ultrack-td" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "ipykernel" }, - { name = "pytest", specifier = ">=9.0.2" }, - { name = "pytest-cov", specifier = ">=7" }, -] -jupyter = [{ name = "ipykernel" }] -test = [ - { name = "pytest", specifier = ">=9.0.2" }, - { name = "pytest-cov", specifier = ">=7" }, + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] [[package]] @@ -1644,31 +1551,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] -[[package]] -name = "fastapi" -version = "0.138.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-doc" }, - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8a/c9/5e8defe249899c0dc900643695fc07829a67fc88b4ff2cdb03fcbdbf5a4b/fastapi-0.138.1.tar.gz", hash = "sha256:96e3702dce09ee0dce48856135620d3d865ca684a79fe7513fd7b13a12f82862", size = 419646, upload-time = "2026-06-25T15:40:42.115Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/a9/69a6924f645eb4dd8cd625bf255b3625990eb3e14e073438a53c405dcd3e/fastapi-0.138.1-py3-none-any.whl", hash = "sha256:b994cae7ba8b82c976a728b544244de31333fa5f7d261f9a1dffe526444cae23", size = 129182, upload-time = "2026-06-25T15:40:40.771Z" }, -] - -[[package]] -name = "fasteners" -version = "0.20" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/18/7881a99ba5244bfc82f06017316ffe93217dbbbcfa52b887caa1d4f2a6d3/fasteners-0.20.tar.gz", hash = "sha256:55dce8792a41b56f727ba6e123fcaee77fd87e638a6863cec00007bfea84c8d8", size = 25087, upload-time = "2025-08-11T10:19:37.785Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/ac/e5d886f892666d2d1e5cb8c1a41146e1d79ae8896477b1153a21711d3b44/fasteners-0.20-py3-none-any.whl", hash = "sha256:9422c40d1e350e4259f509fb2e608d6bc43c0136f79a00db1b49046029d0b3b7", size = 18702, upload-time = "2025-08-11T10:19:35.716Z" }, -] - [[package]] name = "fastjsonschema" version = "2.21.2" @@ -1699,6 +1581,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/41/e70db40a9d1a2cb8d4bbd5416f1c3607c52beb482fdc28ca7c4ef05630c9/fastremap-1.19.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be07b2f2f240947563e3959e561eadd3c6574254d41adbf7146cb15b4bce8f92", size = 6976692, upload-time = "2026-04-29T00:39:19.867Z" }, { url = "https://files.pythonhosted.org/packages/c2/66/9bd9e8a3485876a9cb520fdb0616999db98500206180bed88ad8d3f5be71/fastremap-1.19.0-cp313-cp313-win32.whl", hash = "sha256:6619a877a16e732a4cc909268b10ec4dccffed70b3eb05eb6d259f5282094883", size = 432989, upload-time = "2026-04-29T00:39:24.756Z" }, { url = "https://files.pythonhosted.org/packages/bd/17/5f82236f7b742753204d1388ecac5b9b2cc8d3043fbfb7176f0df071bdad/fastremap-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:bba7d43fe3770960086cc69fcaca63768dd149159df42e02ef05fa1b8b2d79c0", size = 602795, upload-time = "2026-04-29T00:39:23.397Z" }, + { url = "https://files.pythonhosted.org/packages/77/ef/65cad42fe60d4b4611a1e95ba2d406d202e825de663d86961be82ee54b3b/fastremap-1.19.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:00197b4b2609ec8a14953d3af4780ed4d3b2dedb6fa612c8854217d4673e07a8", size = 700693, upload-time = "2026-04-29T00:39:26.335Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ab/b9d5a26e3b3ab4bd624e610e0a71e47f73917edfbd4fbb70a26ef5fd4a98/fastremap-1.19.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8b8ba5e9df854c7fef5a77a48be5f892520d8222c6f4602c2de41f6966c9cefb", size = 591630, upload-time = "2026-04-29T00:39:28.18Z" }, + { url = "https://files.pythonhosted.org/packages/63/7d/dd7b1938b142978ee2dc1e656d195aedb7527affb108f31124a884d66164/fastremap-1.19.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da8b90e6e8c6e32b9e18e490509ce098097bcd9cd0a30280b381460b507e98f9", size = 6796504, upload-time = "2026-04-29T00:39:32.508Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ea/101b1aea304127b532642f7985a816a65af29953de36e83877a2cc2a7a3e/fastremap-1.19.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03627be1bac4c66578de3793a7a3b9c6fda7ec2c16af0a1c52ee5ac025b132b1", size = 6882715, upload-time = "2026-04-29T00:39:36.398Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d1/1ae713ca32044abaa326df048f9196f68ac897fe1f1d4349ec14ad4f8f96/fastremap-1.19.0-cp314-cp314-win32.whl", hash = "sha256:228f2fbf1df40da2a3268283fcaf3da700fe477f4c8a99e042e9235eab4013d2", size = 438590, upload-time = "2026-04-29T00:39:41.741Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4c/45aaed0599d2e2eaea839c1604eb8a1d7850d122697890169360509e2618/fastremap-1.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:d63c5980fbe91301d4ae37caf4e054b908b37878b68c7c8cc8cd523689576be6", size = 616956, upload-time = "2026-04-29T00:39:39.789Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d7/a9251e6e279410fe3702e7bfec87b80e1559b686ff1c5a31c575483441cd/fastremap-1.19.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2cab361d27df0c3256729ef1a2047e2a0ee3d644f8bf6acf5f6d7aa01332f59a", size = 756620, upload-time = "2026-04-29T00:39:43.009Z" }, + { url = "https://files.pythonhosted.org/packages/18/8c/26dd49757f78498619be417b1a9b062acb073f62873581f766debf9e9408/fastremap-1.19.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0305002a45e57eef021c9003638134434a1fc7fabe68e1c909132b66eef6a920", size = 648660, upload-time = "2026-04-29T00:39:44.689Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0a/40fb911b25b9c7bbc5c6a251d4109a686167791b4b6b794e2b4be62e144a/fastremap-1.19.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5704e0e632567a32adbee5f6ec85428983e1cac02531ae1ba283555ab7ab92ab", size = 6937623, upload-time = "2026-04-29T00:39:48.651Z" }, + { url = "https://files.pythonhosted.org/packages/39/3a/e0e07c196fab28a023888c27cdcfe7d421d44b12f24f1d782f3a40889bff/fastremap-1.19.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c495f7e5687a65766803b1ca8fb5468a0bcfd9b98b1585817ca91dfeb90672fc", size = 6764452, upload-time = "2026-04-29T00:39:52.621Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/311c7629d93e05b180742cb5319540fdbc506f4c8221d80cb09ff0ee9436/fastremap-1.19.0-cp314-cp314t-win32.whl", hash = "sha256:f9eddcee20abecbd96bed4c3547aaeb2a7d0692f9507e287c7001012a22ef568", size = 551561, upload-time = "2026-04-29T00:39:55.936Z" }, + { url = "https://files.pythonhosted.org/packages/09/09/8d81617b388d1be461e6e1dd318c287c3aba2c5bf3978c7075570443b2c3/fastremap-1.19.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c48b6400090721f92d3ea5ff2f5c289b9d034e12722d97c5f3239c189b00f5f4", size = 770527, upload-time = "2026-04-29T00:39:54.467Z" }, ] [[package]] @@ -1734,6 +1628,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/a0/aa0383524502292aa926e2cf88ebfda47f2ea5ca71e0e599c8536274dfab/fill_voids-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15d9d007f9b473115ae3df49feb5ed3f632b28ed3e39be82a34937b8c786bda1", size = 2301405, upload-time = "2026-04-30T03:16:00.191Z" }, { url = "https://files.pythonhosted.org/packages/e0/6b/3d23e65c4a967e62ca097decaa7caf182a7814102ed34f42e67c2edcee1d/fill_voids-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a4d9def2de9cafcec367341ce81078a30465c4de27ff55c973ee29edad07701c", size = 2360277, upload-time = "2026-04-30T03:16:01.546Z" }, { url = "https://files.pythonhosted.org/packages/ac/36/5d5d6a86d850a829dd3ea2e8036576909cc48d5ac7b26b1771625856a481/fill_voids-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:dc52abad93670d910d221330d11132ba9f14e9cea6df3d80772a1bbd007a9445", size = 173660, upload-time = "2026-04-30T03:16:02.957Z" }, + { url = "https://files.pythonhosted.org/packages/df/c9/1d2341811c284f396d4b2a8abf09a772a40cf456345f02bcfbaefac0701f/fill_voids-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:821f3e586df0d8202d4decc1b4751e49a980546d3ba600de9570557d1e6f3528", size = 208118, upload-time = "2026-04-30T03:16:03.91Z" }, + { url = "https://files.pythonhosted.org/packages/41/8a/564dba5148d7fea6c9241ee46cb306ef8c7fc3b73e802962d8f9456b76dd/fill_voids-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:72608da4556ddc09829702ab755ebfcd23ae03702303d7b757cfab102e680dc8", size = 190032, upload-time = "2026-04-30T03:16:05.392Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e4/5bd175448978d2b0742b3c2ab95e8e133088429933b25eda8a0bc5074fa4/fill_voids-2.1.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d505758017fe24deb32aca0a246ef95da3838aa623f858aeeae49aff4496d38", size = 1441672, upload-time = "2026-04-30T03:16:06.842Z" }, + { url = "https://files.pythonhosted.org/packages/41/4e/152d8a8dee6858ef378bdc6b0d7fdf380dd4d71e5ebc0d0522bc40a2efad/fill_voids-2.1.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c5666a561eab6cd24e6bcf1fe8d9e2f31b4e0d8b7f84e26eb882d53aff033f7", size = 1450956, upload-time = "2026-04-30T03:16:08.426Z" }, + { url = "https://files.pythonhosted.org/packages/42/e0/68b56983099a5ff02f3f7994397bfe4efff849cf58f4e8886759e7dfe66d/fill_voids-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:78a2f22b9b221cdc0f19030a9355a73192a1e4087d2fb5acae49c8f84653d6fa", size = 2276253, upload-time = "2026-04-30T03:16:09.942Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ed/818f29b56087ceda015cbd1c5b93c323b98b9f8afe1a918e46a776930887/fill_voids-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:14023b0803da5fc77aebf16f404596965d1699ea58a3d658da3d1564f374f4c4", size = 2344877, upload-time = "2026-04-30T03:16:11.549Z" }, + { url = "https://files.pythonhosted.org/packages/35/76/500e0803afa9c9224b5e3f2cf534fe731f36eb68090c762ef9cd004e0d16/fill_voids-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:56dbd5aaff3b5e07824d974525e14a116526b3c07ae4dafc5a805eab99ae016f", size = 175811, upload-time = "2026-04-30T03:16:13.16Z" }, + { url = "https://files.pythonhosted.org/packages/de/49/b6420ee8e85608ce57c56d3ee52f8ae4a9d9dc8d96fdc5b2019f0de4e8cc/fill_voids-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a813b53253144a82548902066a251f2ae52598fb3ef44722293aba82a8367c09", size = 218370, upload-time = "2026-04-30T03:16:14.119Z" }, + { url = "https://files.pythonhosted.org/packages/c7/91/b1e107315073fd9fd72a0be4f2ae0d750f3f6789a6ae92ced8b7d44ef4b2/fill_voids-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:afc0f086ee0d72cf124ea2e05cd91de43e6a2dcfea2214ebe2eae25532118141", size = 202100, upload-time = "2026-04-30T03:16:15.225Z" }, + { url = "https://files.pythonhosted.org/packages/2e/15/a89d04352035896d8bee737cf3af6768a8aca5b354310b25339da336248e/fill_voids-2.1.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a4f86e807afce22fa0668c97ad9959d1b1ce7b255411832741e1a9410432f63", size = 1441286, upload-time = "2026-04-30T03:16:16.479Z" }, + { url = "https://files.pythonhosted.org/packages/92/02/fdcf6a3fa3e9ee070828d397b2caf80a02183d45b205f6f12febd3dc5716/fill_voids-2.1.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b1095cbb7def09ad9d32d52adfebc16eb6b410ec087d341ccfd4013aa5a6ef8", size = 1413039, upload-time = "2026-04-30T03:16:18.01Z" }, + { url = "https://files.pythonhosted.org/packages/d8/e1/47747ee139e99d239b2df25dd2e1ae2bf979592d3a03670244c3f174474a/fill_voids-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:620455dca04628b71f02ff86dfb7056f07f93fb6022e5b90006ad80a8b1633df", size = 2257624, upload-time = "2026-04-30T03:16:19.51Z" }, + { url = "https://files.pythonhosted.org/packages/51/b8/5f62ef7ecc2531ca0e8c99b1b8a6df70e4d1960ed86520223feb9f0dc238/fill_voids-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:02a044ec78f630155fb4eab8bbaa6f935c915472e5d4b6ed0cbe643dd026dc73", size = 2307237, upload-time = "2026-04-30T03:16:21.094Z" }, + { url = "https://files.pythonhosted.org/packages/74/f6/1797633bcdeb9121de82d73c35ac4e1fa8ae24e3778e2324d7ed923e27ba/fill_voids-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:883695c30048660ef043ee8a48e5a15b0e70e5db792aa2207c9b3812472dfaf5", size = 203359, upload-time = "2026-04-30T03:16:22.489Z" }, ] [[package]] @@ -1744,30 +1652,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, ] -[[package]] -name = "flexcache" -version = "0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/b0/8a21e330561c65653d010ef112bf38f60890051d244ede197ddaa08e50c1/flexcache-0.3.tar.gz", hash = "sha256:18743bd5a0621bfe2cf8d519e4c3bfdf57a269c15d1ced3fb4b64e0ff4600656", size = 15816, upload-time = "2024-03-09T03:21:07.555Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/cd/c883e1a7c447479d6e13985565080e3fea88ab5a107c21684c813dba1875/flexcache-0.3-py3-none-any.whl", hash = "sha256:d43c9fea82336af6e0115e308d9d33a185390b8346a017564611f1466dcd2e32", size = 13263, upload-time = "2024-03-09T03:21:05.635Z" }, -] - -[[package]] -name = "flexparser" -version = "0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/82/99/b4de7e39e8eaf8207ba1a8fa2241dd98b2ba72ae6e16960d8351736d8702/flexparser-0.4.tar.gz", hash = "sha256:266d98905595be2ccc5da964fe0a2c3526fbbffdc45b65b3146d75db992ef6b2", size = 31799, upload-time = "2024-11-07T02:00:56.249Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl", hash = "sha256:3738b456192dcb3e15620f324c447721023c0293f6af9955b481e91d00179846", size = 27625, upload-time = "2024-11-07T02:00:54.523Z" }, -] - [[package]] name = "fonttools" version = "4.63.0" @@ -1790,6 +1674,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/48/cc4b66d9058c0d0982c833fad10127c4b0e9324606aafa41382295ca4102/fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27", size = 5105892, upload-time = "2026-05-14T12:03:43.525Z" }, { url = "https://files.pythonhosted.org/packages/d8/1f/a98a30a814b9ddef3a2e706025f90b9e0bc94890e6cb15254bc86547d11a/fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380", size = 2291313, upload-time = "2026-05-14T12:03:45.594Z" }, { url = "https://files.pythonhosted.org/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b", size = 2342299, upload-time = "2026-05-14T12:03:47.414Z" }, + { url = "https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745", size = 2875338, upload-time = "2026-05-14T12:03:50.052Z" }, + { url = "https://files.pythonhosted.org/packages/cd/58/7dfa0c761cb3b2964e2a84c4dc986c926a87de0cb9fb60d5b28ded3f2914/fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03", size = 2422661, upload-time = "2026-05-14T12:03:52.154Z" }, + { url = "https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49", size = 5010526, upload-time = "2026-05-14T12:03:54.647Z" }, + { url = "https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b", size = 4923946, upload-time = "2026-05-14T12:03:56.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/60/872e6e233b8c5e8b41413796ff18b7fe479661bd40147e071b450dfad7a1/fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6", size = 4962489, upload-time = "2026-05-14T12:03:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/30/c4/83c24f2ec38b90cfda84bf4b1a1f49df80e84a1db4e7ac6e0d41bf23bc39/fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4", size = 5071870, upload-time = "2026-05-14T12:04:02.122Z" }, + { url = "https://files.pythonhosted.org/packages/de/40/3ae22b60ff1d41ce0bd044b31238cdc72cef99f28b976f1e128ebd618c9b/fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616", size = 2295026, upload-time = "2026-05-14T12:04:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5", size = 2347454, upload-time = "2026-05-14T12:04:06.752Z" }, + { url = "https://files.pythonhosted.org/packages/49/4e/652d1580c5f4e39f7d103b0c793e4773129ad633dce4addd0cf4dfebde02/fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001", size = 2958152, upload-time = "2026-05-14T12:04:08.706Z" }, + { url = "https://files.pythonhosted.org/packages/0e/55/ad864c9a9b219f552eb46b32cd7906c466e5a578ba0c3abfcc0fe7413eb6/fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e", size = 2460809, upload-time = "2026-05-14T12:04:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/0aa8db70f18cf52e49b4ed5ecec68547f981160bf5ded3b5aed6faa0a6f9/fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096", size = 5148649, upload-time = "2026-05-14T12:04:12.747Z" }, + { url = "https://files.pythonhosted.org/packages/7f/63/18e4369c25043096f1048e0c9915951adc4f842bd81c6b18155824d6fa99/fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f", size = 4932147, upload-time = "2026-05-14T12:04:14.806Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3f/67f3eac2ffd8a98446c5022f8ed3864eac878a5ff7af8df4c8286dba16cc/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40", size = 5027237, upload-time = "2026-05-14T12:04:17.675Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ba/4e6214cb38a7b04779e97bb7636de9a5c7f20af7018d03dee0b64c08510a/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196", size = 5053933, upload-time = "2026-05-14T12:04:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/214dcc19ee31d3d38fb5ad2755c11ef0514e5dc300bbaf41c0b69f393799/fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8", size = 2359326, upload-time = "2026-05-14T12:04:24.22Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1e/3ff1a9b523058c2eeb6a9d50f5574e2a738200d0d94107d5bc4105e8da3f/fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419", size = 2425829, upload-time = "2026-05-14T12:04:26.829Z" }, { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, ] @@ -1802,20 +1702,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, ] -[[package]] -name = "freetype-py" -version = "2.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/9c/61ba17f846b922c2d6d101cc886b0e8fb597c109cedfcb39b8c5d2304b54/freetype-py-2.5.1.zip", hash = "sha256:cfe2686a174d0dd3d71a9d8ee9bf6a2c23f5872385cf8ce9f24af83d076e2fbd", size = 851738, upload-time = "2024-08-29T18:32:26.37Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/a8/258dd138ebe60c79cd8cfaa6d021599208a33f0175a5e29b01f60c9ab2c7/freetype_py-2.5.1-py3-none-macosx_10_9_universal2.whl", hash = "sha256:d01ded2557694f06aa0413f3400c0c0b2b5ebcaabeef7aaf3d756be44f51e90b", size = 1747885, upload-time = "2024-08-29T18:32:17.604Z" }, - { url = "https://files.pythonhosted.org/packages/a2/93/280ad06dc944e40789b0a641492321a2792db82edda485369cbc59d14366/freetype_py-2.5.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d2f6b3d68496797da23204b3b9c4e77e67559c80390fc0dc8b3f454ae1cd819", size = 1051055, upload-time = "2024-08-29T18:32:19.153Z" }, - { url = "https://files.pythonhosted.org/packages/b6/36/853cad240ec63e21a37a512ee19c896b655ce1772d803a3dd80fccfe63fe/freetype_py-2.5.1-py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:289b443547e03a4f85302e3ac91376838e0d11636050166662a4f75e3087ed0b", size = 1043856, upload-time = "2024-08-29T18:32:20.565Z" }, - { url = "https://files.pythonhosted.org/packages/93/6f/fcc1789e42b8c6617c3112196d68e87bfe7d957d80812d3c24d639782dcb/freetype_py-2.5.1-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:cd3bfdbb7e1a84818cfbc8025fca3096f4f2afcd5d4641184bf0a3a2e6f97bbf", size = 1108180, upload-time = "2024-08-29T18:32:21.871Z" }, - { url = "https://files.pythonhosted.org/packages/2a/1b/161d3a6244b8a820aef188e4397a750d4a8196316809576d015f26594296/freetype_py-2.5.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:3c1aefc4f0d5b7425f014daccc5fdc7c6f914fb7d6a695cc684f1c09cd8c1660", size = 1106792, upload-time = "2024-08-29T18:32:23.134Z" }, - { url = "https://files.pythonhosted.org/packages/93/6e/bd7fbfacca077bc6f34f1a1109800a2c41ab50f4704d3a0507ba41009915/freetype_py-2.5.1-py3-none-win_amd64.whl", hash = "sha256:0b7f8e0342779f65ca13ef8bc103938366fecade23e6bb37cb671c2b8ad7f124", size = 814608, upload-time = "2024-08-29T18:32:24.648Z" }, -] - [[package]] name = "frozenlist" version = "1.8.0" @@ -1870,6 +1756,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] @@ -1886,9 +1804,6 @@ wheels = [ http = [ { name = "aiohttp" }, ] -s3 = [ - { name = "s3fs" }, -] [[package]] name = "future" @@ -1984,6 +1899,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344, upload-time = "2025-12-16T00:40:24.742Z" }, { url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694, upload-time = "2025-12-16T00:40:25.505Z" }, { url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435, upload-time = "2025-12-16T00:35:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/8ecf3c2b864a490b9e7010c84fd203ec8cf3b280651106a3a74dd1b0ca72/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697", size = 31301, upload-time = "2025-12-16T00:24:48.527Z" }, + { url = "https://files.pythonhosted.org/packages/36/c6/f7ff6c11f5ca215d9f43d3629163727a272eabc356e5c9b2853df2bfe965/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651", size = 30868, upload-time = "2025-12-16T00:48:12.163Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/c25671c7aad70f8179d858c55a6ae8404902abe0cdcf32a29d581792b491/google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2", size = 33381, upload-time = "2025-12-16T00:40:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/42/fa/f50f51260d7b0ef5d4898af122d8a7ec5a84e2984f676f746445f783705f/google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21", size = 33734, upload-time = "2025-12-16T00:40:27.028Z" }, + { url = "https://files.pythonhosted.org/packages/08/a5/7b059810934a09fb3ccb657e0843813c1fee1183d3bc2c8041800374aa2c/google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2", size = 34878, upload-time = "2025-12-16T00:35:23.142Z" }, ] [[package]] @@ -2013,9 +1933,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/37/4549f149c9797c21b32c2683c33522af22522099de128b2406672526d005/greenlet-3.5.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:fa4f98af3a528f0c3fd592a26df7f376f93329c8f4d987f6bb979057af8bf5e2", size = 286220, upload-time = "2026-05-20T13:07:28.463Z" }, { url = "https://files.pythonhosted.org/packages/38/ff/a4f436709716965eaab9f36ea7b906c8a927fbe32fb1372a2071d964f6b1/greenlet-3.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffea73584b216150eab159b6d12348fb253e68757974de1e2c40d8a318ac89ed", size = 601585, upload-time = "2026-05-20T14:00:06.141Z" }, { url = "https://files.pythonhosted.org/packages/65/ad/54bc3fcee3ad368a61b19b67d88117f7a8c29727bf71fffdeda81fbd946e/greenlet-3.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1072b4f9edcc1e192d9283a66a3e68d6b84c561de33a83d7858beb9ba1effe10", size = 614215, upload-time = "2026-05-20T14:05:42.675Z" }, - { url = "https://files.pythonhosted.org/packages/7c/6c/de5b1b388cd2d9fbdfeab324863daba37d54e6e233ddbefd70b385a8c591/greenlet-3.5.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89101bfd5011e069be974903cb3a4e4523845e4ece2d62dcd8d358933c0ef249", size = 620094, upload-time = "2026-05-20T14:09:09.18Z" }, { url = "https://files.pythonhosted.org/packages/40/69/b91cda0647df839483201545913514c2827ebea5e5ccdf931842763bc127/greenlet-3.5.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:add5217d68b31130f0beca584d7fef4878327d2e31642b66618a14eef312b63b", size = 611358, upload-time = "2026-05-20T13:14:26.37Z" }, - { url = "https://files.pythonhosted.org/packages/4a/43/1204baffab8a6476464795a7ccf394a3248d4f22c9f87173a15b36b6d971/greenlet-3.5.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:e6cd99ea59dd5d89f0c956606571d79bfe6f68c9eb7f4a4083a41a7f1587edee", size = 422782, upload-time = "2026-05-20T14:01:39.597Z" }, { url = "https://files.pythonhosted.org/packages/59/90/3cf77e080350cd02fa307bb2abf05df48f4482c240275bbd2c203ba8bb1c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a5ea42a752d47a145eae922b605cd1634665ac3d5ec1e72402d5048e8d60d207", size = 1570475, upload-time = "2026-05-20T14:02:25.29Z" }, { url = "https://files.pythonhosted.org/packages/65/2c/18cece62045e74598c3c393f70dce4a63f56222015ba29a5d4eeb04f764c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5551170cf4f5ff5623e9af81323751979fee2c731e2287b61f73cd27257b823", size = 1635625, upload-time = "2026-05-20T13:14:34.027Z" }, { url = "https://files.pythonhosted.org/packages/30/f5/310d104ddf41eb5a70f4c268d22508dfb0c3c8e86fec152be34d0d2ed819/greenlet-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c8bb982ad117d29478ef8f5533e97df21f1e2befd17a299257b0c96d1371c0b", size = 238791, upload-time = "2026-05-20T13:10:39.018Z" }, @@ -2023,13 +1941,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/69/7f7e5372d998b81001899b1c0823c957aa413ba0f2662e65821611cc31e4/greenlet-3.5.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:51518ff74664078fc51bffcc6fc529b0df5ae58da192691cee765d45ce944a2b", size = 285060, upload-time = "2026-05-20T13:08:51.899Z" }, { url = "https://files.pythonhosted.org/packages/b1/bf/387f9b6b865fd2ae0d0be09e0004827295a01b71be76ed350dd1e28a91a4/greenlet-3.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ffdb3c0bb002c99cd8f298957e046c3dbf6006b5b7cdf11a4e19194624a0a0a", size = 604370, upload-time = "2026-05-20T14:00:07.492Z" }, { url = "https://files.pythonhosted.org/packages/32/f5/169ce3d4e4c67291bd18f8cbe0299c9f3e45102c7f1fb3c14780c93e4532/greenlet-3.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7715a5a2c3378ba602c3a440558261e13a820bb53a82693aacd7b7f6d964e283", size = 616987, upload-time = "2026-05-20T14:05:44.237Z" }, - { url = "https://files.pythonhosted.org/packages/19/ba/c24110c55dffa55aa6e1d98b45310da33801aeba7686ff0190fe5d46fd32/greenlet-3.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d40a890035c0058cadbdc4af7569800fd28a0e527a0fdbb7b5f9418f176846ce", size = 622911, upload-time = "2026-05-20T14:09:10.598Z" }, { url = "https://files.pythonhosted.org/packages/ee/e5/7f2e41d5273be07e77560d61ea4e56485b4d6c316d2a84518c62d1364061/greenlet-3.5.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc71ff466927a201b08305acac451ebe1aedfcea002f62f1f2f2ac2ac1e6a135", size = 613911, upload-time = "2026-05-20T13:14:27.539Z" }, - { url = "https://files.pythonhosted.org/packages/ec/7b/d20db2e8a5ad6c038702f3179b136f93f0a3d1a21a0c0777f3e470cdf4b2/greenlet-3.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:67821bb03e4e98664490edb787ff6af501194c29bbee0f5c1dfdcf1dc3d9d436", size = 425228, upload-time = "2026-05-20T14:01:40.837Z" }, { url = "https://files.pythonhosted.org/packages/c5/a4/fbdc67579b73615a1f91615e814303cc71e06128f7baaba87be79b8fb90c/greenlet-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cd443683db272ebaaca03af98c0b063ab30db70ea8a31a1559f35e3f7b744ccd", size = 1570689, upload-time = "2026-05-20T14:02:27.225Z" }, { url = "https://files.pythonhosted.org/packages/e6/b4/77abbe35078be39718a46cd49caf16bceb35662f97a34101dca28aa98e47/greenlet-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:089fff7a6ce8d9316d1f65ebc00273a56be258c1725b32b94de90a3a979557e1", size = 1635602, upload-time = "2026-05-20T13:14:36.344Z" }, { url = "https://files.pythonhosted.org/packages/37/f7/129f27ca700845b8ee8ca88ce7f43435a1239c2eddb7677fc938822762cf/greenlet-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:110a1ca7b49b014b097f6078272c3f4ed31af45b254de5228b79adba879f6af9", size = 238683, upload-time = "2026-05-20T13:11:50.57Z" }, { url = "https://files.pythonhosted.org/packages/6d/5c/a485a36e87df8d8fd0632ee01511244f5156a20ed3746cc6599340326395/greenlet-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f16ba1efc0715b680a18b8123d90dad887c6112ae3555b4b5c32c149540c6b4e", size = 235499, upload-time = "2026-05-20T13:12:42.028Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/c62454606daf5640369c94d8a9dd540599b1bfc090e2d2180cb77f4038d2/greenlet-3.5.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8ab31c9de8651a2facdd5c5bb0011f2380dd1a7af78ce2adf4b56095294fc07", size = 285579, upload-time = "2026-05-20T13:08:56.396Z" }, + { url = "https://files.pythonhosted.org/packages/ec/71/c4270398c2eba968a6071af1dfbdcaeee6ec1c24bc8b435b8cc452700da6/greenlet-3.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e300185139abc337ade480c327183adf42a875ac7181bfe66d7d4efea31fbea", size = 651106, upload-time = "2026-05-20T14:00:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ab/71e34b78a44ec271fb5f550c17bc46d301ddc5953890d935f270b0dcdb5a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7ffdb990dcaa0234cf9845aead5df2e3c3a8b6507d409274dd87e0d5ab05ffc2", size = 663478, upload-time = "2026-05-20T14:05:45.88Z" }, + { url = "https://files.pythonhosted.org/packages/77/96/4efd6fa5c62c85426a0c19077a586258ebc3a2a146ff2493e4312a697a22/greenlet-3.5.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f82b3597e9d83b63408affed0b48fd0f54935edac4302237b9a837be0dae33c", size = 660800, upload-time = "2026-05-20T13:14:29.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e0/6c71401a25cac7000261304e866a2f2cc04dc74810d40e2f118aa4799495/greenlet-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c0141e37414c10164e702b8fb1473304221ad98f71600850c6ef7ff4880feba0", size = 1617518, upload-time = "2026-05-20T14:02:28.662Z" }, + { url = "https://files.pythonhosted.org/packages/41/26/c5c06643e8c0af9e7bf18e16cb51d0ab7625155f0392e1c9015d66d556cd/greenlet-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:50ae25a67bea74ea41fb14b960bc532df73eb713417b2d61892dced82fe8d3bc", size = 1681593, upload-time = "2026-05-20T13:14:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/8a/bd/e11a108317485075e68af9d23039619b86b28130c3b50d227d42edece64b/greenlet-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:8a17c42330e261299766b75ac1ea32caa437a9453c8f65d16a13140db378ecd3", size = 239800, upload-time = "2026-05-20T13:09:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/47/f8/8e8e8417b7bf28639a5a56356ef934d0375e1d0c70a57e04d7701e870ffe/greenlet-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:7b5f5fae05b8ac6d176a61b60c394a8cbdc2b5b91b81793066e68745cf165e54", size = 236862, upload-time = "2026-05-20T13:09:10.498Z" }, + { url = "https://files.pythonhosted.org/packages/90/12/41bf27fde4d3605d3773ae57751eda182b8be2f5398011c041173b1d9534/greenlet-3.5.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:ea8da1e900d758d078810d4255d8c6aa572181896a31ec79d779eb79c3adc9ad", size = 293637, upload-time = "2026-05-20T13:12:35.529Z" }, + { url = "https://files.pythonhosted.org/packages/44/44/ba14b23e9757707050c2f397d305bbcae62e5d7cad122f8b6baec5ae4a1f/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a19570c52a21420dcbc94e661994bc325c0b5b11304540fed514586da5dc8f2e", size = 650840, upload-time = "2026-05-20T14:00:11.079Z" }, + { url = "https://files.pythonhosted.org/packages/a8/37/5ddc2b686a6844f91abecef43411842426da2e1573f60b49ecf2547f4ae1/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d955c89b75eeca4723d7cc14135f393cd47c32e2a6cb4a8e4c6e760a26b0986", size = 656416, upload-time = "2026-05-20T14:05:47.118Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f0/d17510297c35a2992712f0bf84de3779749999f7d3d63aa1f09db7c62dbe/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2daaaebd1a5aa88c49045b6baf9310b3263796bd88db713edf37cf53e7bb4e", size = 654397, upload-time = "2026-05-20T13:14:30.696Z" }, + { url = "https://files.pythonhosted.org/packages/37/eb/147387705bb89092645b012586e7273cb5ed3c90ef7eaf3a69173eaf0209/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bfbd69cc349e43bf3a8ae1c85548ff0718efc887615c2db16c3833d7b0b072d", size = 1614469, upload-time = "2026-05-20T14:02:30.192Z" }, + { url = "https://files.pythonhosted.org/packages/a6/4e/37ee0da7732b7aa9896f17e15579a9df34b9fcb9dd494f0adfa749af6623/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4378720dd888136c27215a0214d32a4d37c3852765d45bc37aad0623423cfd78", size = 1675115, upload-time = "2026-05-20T13:14:40.972Z" }, + { url = "https://files.pythonhosted.org/packages/57/f3/97dfcf4a6eb5077f8a672234216fb5923eb89f2cab7081cb10b2cf75b605/greenlet-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:45718441607f9325d948db98cbc691276059316d0358c188c246da4e1d4d23d2", size = 245246, upload-time = "2026-05-20T13:12:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/5d/73/d7f72e34b582f694f4a9b248162db7b09cc458a259ba8f0c0bfa1a34ea7d/greenlet-3.5.1-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:2baee5ca02031757ffe8cc3d69f0cc0aec7065ce362622da74f32d3bcab1c541", size = 285575, upload-time = "2026-05-20T13:12:07.043Z" }, + { url = "https://files.pythonhosted.org/packages/df/59/fa9c6e87dc8ad27a95dabe2f29f372b733d05a8a67470f6c901ed9975655/greenlet-3.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b1ec3274918a81d3ea778b9e75b56b72b33f300edb6cf7f3a7fe1dae56683de", size = 656428, upload-time = "2026-05-20T14:00:12.556Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/e753408871eaa61dfe35e619cfc67512b036fde99893685d50eea9e07146/greenlet-3.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:111e2390ffffc47d5840b01711dd7fac07d4c09283d0283e7f3264b14e284c64", size = 667064, upload-time = "2026-05-20T14:05:48.662Z" }, + { url = "https://files.pythonhosted.org/packages/96/27/5565b5b40389f1c7753003a07e21892fda8660926787036d5bc0308b8113/greenlet-3.5.1-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e630136e905fe5ff43e86945ae41220b6d1470956a39220e708110ac48d01ea5", size = 665697, upload-time = "2026-05-20T13:14:32.943Z" }, + { url = "https://files.pythonhosted.org/packages/cf/82/e7de4178c0c2d1c9a5a3be3cc0b33e46a85b3ee4a77c071bf7ad8600e079/greenlet-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:975eac34b44a7077ca4d421348455b94f0f518246a7f14bc6d2fdcfe5b584368", size = 1621256, upload-time = "2026-05-20T14:02:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/00/10/f2dddcf7dacac17dfc68691809589adad06135eb28930429cf58a6467a2f/greenlet-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9ab3c3a0b2ae6198e67c898dad5215a49f9ae0d0081b3c3ec59f333e39eeca26", size = 1685956, upload-time = "2026-05-20T13:14:42.55Z" }, + { url = "https://files.pythonhosted.org/packages/22/17/4a232b32133230ada52f70e9d7f5b65b0caef8772f01849bd8d149e7e4ca/greenlet-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:cbfc69be86e10dcfef5b1e6269d1d6926552aa89ee39e1de3353360c1b6989ab", size = 239802, upload-time = "2026-05-20T13:13:15.481Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ae/4e623a7e6d4d2a5f4cb8e4c82de4169fc637942caae68d6e676b8a128ac5/greenlet-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:92fd6d44ac5e5a887c8a5dc4a8ba0ba908527c31c12f78c6bc7dcfe8aab279f6", size = 236853, upload-time = "2026-05-20T13:15:37.301Z" }, + { url = "https://files.pythonhosted.org/packages/7a/57/816d9cff29119da3505b3d6a5e14a8af89006ac36f47f891ff293ee05af1/greenlet-3.5.1-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:a6fdf2433a5441ef9a95464f7c3e674775da1c8c1177fff311cee1acad4626ed", size = 293877, upload-time = "2026-05-20T13:10:19.078Z" }, + { url = "https://files.pythonhosted.org/packages/23/a1/59b0a7c7d140ff1a75626680b9a9899b79a9176cab298b394968fb023295/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7546556f0d649f99f6a361098a55f761181bb2ea12ff150bb16d26092ad88244", size = 655333, upload-time = "2026-05-20T14:00:14.758Z" }, + { url = "https://files.pythonhosted.org/packages/72/1b/5efe127597625042218939d01855109f352779050768b670b52edcc16a6c/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5ee3ea898009fa898f85f9982255d35278c477bebe185beca249cab42d4526c", size = 659443, upload-time = "2026-05-20T14:05:50.159Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6d/c404246ea4d22d097a7426d0efb5b781bd7eb67715f09e79001bd552ab18/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5c81f74d204d3edd136ebfd50dce53acbb776995d721a0fe801626cfc93b8cd", size = 658356, upload-time = "2026-05-20T13:14:35.091Z" }, + { url = "https://files.pythonhosted.org/packages/51/02/f8ee37fb6d2219329f350af241c27fcf12df57e723d11f6fc6d3bacdadaa/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:2c18ef16bf6d4dd410e4dd52996888ea1497be26892fe5bbc73580aba4287b8e", size = 1619216, upload-time = "2026-05-20T14:02:33.403Z" }, + { url = "https://files.pythonhosted.org/packages/93/c5/3dc9475ace2c7a3680da12372cddd7f1ac874eb410a1ac48d3e9dab83782/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:17d86354f0ae6b61bf9be5148d0dd34e06c3cb7c602c671f79f29ac3b150e659", size = 1678427, upload-time = "2026-05-20T13:14:43.71Z" }, + { url = "https://files.pythonhosted.org/packages/df/4e/750c15c317a41ffb36f0bf40b933e3d744a7dede61889f74443ea69690cf/greenlet-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e7516cf6ae6b8a582c2770a0caed47b8a48373ed732c33d69a72913ae6ac923e", size = 245225, upload-time = "2026-05-20T13:13:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/4f/fd/d3baea2eeb7b617efd47e87ca06e2ec2c6118d303aa9e918e0ce16eadc10/greenlet-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:5028648bf2253ec4745add746129d3904121fa7fe871a76bed23c5720573ce0a", size = 239590, upload-time = "2026-05-20T13:13:37.382Z" }, ] [[package]] @@ -2070,6 +2017,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/34/0f8202c6809a46c2b4d69125ef3667c40b1c211f8e19930e5fa1f1197039/grpcio-1.81.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0fba53cb96004b2b7fb758b46b2288cb49d0b658316a4e73f3ef67230616ee65", size = 7844481, upload-time = "2026-06-01T05:55:44.849Z" }, { url = "https://files.pythonhosted.org/packages/c0/95/c3366b5b5edf4c4adc90f2e29ca16e57965a8e56dc8d2ee89565ba1905bb/grpcio-1.81.0-cp313-cp313-win32.whl", hash = "sha256:c197e2ef75a442528072b29e9755da299110e8610e8bcbb59a6b4cf55384f005", size = 4182777, upload-time = "2026-06-01T05:55:47.459Z" }, { url = "https://files.pythonhosted.org/packages/a9/a7/932f2f748511a32e641a2aba0d30dded3ed6e8bc330e0924e4d5d86853e6/grpcio-1.81.0-cp313-cp313-win_amd64.whl", hash = "sha256:194eddfacc84d80f50512e9fd4ee851d5f2499f18f299c95aa8fb4748f0537e0", size = 4928085, upload-time = "2026-06-01T05:55:50.158Z" }, + { url = "https://files.pythonhosted.org/packages/c5/1d/28b231333857deb840bc3d182ae087510170ea6d68f21393aeb0fe499530/grpcio-1.81.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:a9351055f52660b58f3d4890ea66188b5134399f82b11aa0c55bd4b99eff5390", size = 6055712, upload-time = "2026-06-01T05:55:52.889Z" }, + { url = "https://files.pythonhosted.org/packages/e8/b8/999c14f9dff0fc47549d2e827cba1343ddc18e1d1bf0d06d2cf628eecbd9/grpcio-1.81.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:300f3337b6425fd16ead9a4f9b2ac25801acb64aa5bc0b99eb69901645b2b1d2", size = 12057189, upload-time = "2026-06-01T05:55:55.952Z" }, + { url = "https://files.pythonhosted.org/packages/1e/3d/1fbde079572562af65351151d840525a13879eb7b481d35b55cd64c6127a/grpcio-1.81.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:97bbd623f7ded558fd4f7cb5a4f600c4d4de65c5dd364c83a5b14b2a10a2d3b5", size = 6608136, upload-time = "2026-06-01T05:55:59.069Z" }, + { url = "https://files.pythonhosted.org/packages/32/89/1f17cb6882abfd8e5a303a25d5d1665abef5a8c499a96198c65a651d1b85/grpcio-1.81.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ff83d889e3ebf6341c8c7864ad8031591ad5ca61599072fc511644d1eb962d2b", size = 7307045, upload-time = "2026-06-01T05:56:02.376Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/f98e91b2e755652e637ea2144318b0229b290062199f761b445fe1fa6015/grpcio-1.81.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c4fe218c5a35e1d87a5a26544237f1fa41dfd9cbd3c856b0810a30061f8b0aaf", size = 6812794, upload-time = "2026-06-01T05:56:05.777Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0c/77892d715ac41e7ec0ace2a50080ffb64e189188056f607a66fe0014d1ee/grpcio-1.81.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b8b025b6af43ee0ad4a70307025d77bcab5adde7c4597786010d802c203e9fc5", size = 7422767, upload-time = "2026-06-01T05:56:08.524Z" }, + { url = "https://files.pythonhosted.org/packages/3f/b8/aa04590c6564714d94954515f15a236e59d4b9b3ad01e615f1b706d7792d/grpcio-1.81.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:3d4e0ce5a40a998cf608c8ba60ecfe18fdf364a9aa193ae4ac3faeecd0e86757", size = 8408551, upload-time = "2026-06-01T05:56:11.283Z" }, + { url = "https://files.pythonhosted.org/packages/43/3d/4f4a3450a1973568910c6909cb74abbf2126f68aefae5976962f9f7ad50d/grpcio-1.81.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:aa948712c8e5fa40ec250870bda14bc7578e1bb832a8912d9d2a0f720518edbe", size = 7846468, upload-time = "2026-06-01T05:56:14.536Z" }, + { url = "https://files.pythonhosted.org/packages/88/f4/5827fd248221ad3b44161c23ce9b5f4ee405b04fc6da5fd402a9aa87a84a/grpcio-1.81.0-cp314-cp314-win32.whl", hash = "sha256:fbbe81314a9d92156abce8b62c09364eb8bafc0ca2a19919a45ec64b5c6cb664", size = 4264427, upload-time = "2026-06-01T05:56:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/127dc2b246096ad50ef7c8d9b7b31d757787aeb796368bcdd4454e4204c4/grpcio-1.81.0-cp314-cp314-win_amd64.whl", hash = "sha256:b93cee313cae4e113fbb3a0ce1ea5633db6f63cfde2b2dc1d817429026b2a50b", size = 5070848, upload-time = "2026-06-01T05:56:19.735Z" }, ] [[package]] @@ -2121,15 +2078,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/c4/532123bcd9080e250696779c927f2cb906c8bf3447df98f5ceb8dcded539/h5py-3.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7e420b539fb6023a259a1b14d4c9f6df8cf50d7268f48e161169987a57b737ff", size = 5414598, upload-time = "2026-03-06T13:48:29.49Z" }, { url = "https://files.pythonhosted.org/packages/c3/d9/a27997f84341fc0dfcdd1fe4179b6ba6c32a7aa880fdb8c514d4dad6fba3/h5py-3.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:18f2bbcd545e6991412253b98727374c356d67caa920e68dc79eab36bf5fedad", size = 3175509, upload-time = "2026-03-06T13:48:31.131Z" }, { url = "https://files.pythonhosted.org/packages/a5/23/bb8647521d4fd770c30a76cfc6cb6a2f5495868904054e92f2394c5a78ff/h5py-3.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:656f00e4d903199a1d58df06b711cf3ca632b874b4207b7dbec86185b5c8c7d4", size = 2647362, upload-time = "2026-03-06T13:48:33.411Z" }, -] - -[[package]] -name = "heapdict" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/9b/d8963ae7e388270b695f3b556b6dc9adb70ae9618fba09aa1e7b1886652d/HeapDict-1.0.1.tar.gz", hash = "sha256:8495f57b3e03d8e46d5f1b2cc62ca881aca392fd5cc048dc0aa2e1a6d23ecdb6", size = 4274, upload-time = "2019-09-09T18:57:02.154Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/9d/cd4777dbcf3bef9d9627e0fe4bc43d2e294b1baeb01d0422399d5e9de319/HeapDict-1.0.1-py3-none-any.whl", hash = "sha256:6065f90933ab1bb7e50db403b90cab653c853690c5992e69294c2de2b253fc92", size = 3917, upload-time = "2019-09-09T18:57:00.821Z" }, + { url = "https://files.pythonhosted.org/packages/48/3c/7fcd9b4c9eed82e91fb15568992561019ae7a829d1f696b2c844355d95dd/h5py-3.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9c9d307c0ef862d1cd5714f72ecfafe0a5d7529c44845afa8de9f46e5ba8bd65", size = 3678608, upload-time = "2026-03-06T13:48:35.183Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b7/9366ed44ced9b7ef357ab48c94205280276db9d7f064aa3012a97227e966/h5py-3.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8c1eff849cdd53cbc73c214c30ebdb6f1bb8b64790b4b4fc36acdb5e43570210", size = 3054773, upload-time = "2026-03-06T13:48:37.139Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/4964bc0e91e86340c2bbda83420225b2f770dcf1eb8a39464871ad769436/h5py-3.16.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e2c04d129f180019e216ee5f9c40b78a418634091c8782e1f723a6ca3658b965", size = 5198886, upload-time = "2026-03-06T13:48:38.879Z" }, + { url = "https://files.pythonhosted.org/packages/f1/16/d905e7f53e661ce2c24686c38048d8e2b750ffc4350009d41c4e6c6c9826/h5py-3.16.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e4360f15875a532bc7b98196c7592ed4fc92672a57c0a621355961cafb17a6dd", size = 5404883, upload-time = "2026-03-06T13:48:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f2/58f34cb74af46d39f4cd18ea20909a8514960c5a3e5b92fd06a28161e0a8/h5py-3.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3fae9197390c325e62e0a1aa977f2f62d994aa87aab182abbea85479b791197c", size = 5192039, upload-time = "2026-03-06T13:48:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ca/934a39c24ce2e2db017268c08da0537c20fa0be7e1549be3e977313fc8f5/h5py-3.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:43259303989ac8adacc9986695b31e35dba6fd1e297ff9c6a04b7da5542139cc", size = 5421526, upload-time = "2026-03-06T13:48:44.838Z" }, + { url = "https://files.pythonhosted.org/packages/3e/14/615a450205e1b56d16c6783f5ccd116cde05550faad70ae077c955654a75/h5py-3.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:fa48993a0b799737ba7fd21e2350fa0a60701e58180fae9f2de834bc39a147ab", size = 3183263, upload-time = "2026-03-06T13:48:47.117Z" }, + { url = "https://files.pythonhosted.org/packages/7b/48/a6faef5ed632cae0c65ac6b214a6614a0b510c3183532c521bdb0055e117/h5py-3.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:1897a771a7f40d05c262fc8f37376ec37873218544b70216872876c627640f63", size = 2663450, upload-time = "2026-03-06T13:48:48.707Z" }, + { url = "https://files.pythonhosted.org/packages/5d/32/0c8bb8aedb62c772cf7c1d427c7d1951477e8c2835f872bc0a13d1f85f86/h5py-3.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:15922e485844f77c0b9d275396d435db3baa58292a9c2176a386e072e0cf2491", size = 3760693, upload-time = "2026-03-06T13:48:50.453Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1f/fcc5977d32d6387c5c9a694afee716a5e20658ac08b3ff24fdec79fb05f2/h5py-3.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:df02dd29bd247f98674634dfe41f89fd7c16ba3d7de8695ec958f58404a4e618", size = 3181305, upload-time = "2026-03-06T13:48:52.221Z" }, + { url = "https://files.pythonhosted.org/packages/f5/a1/af87f64b9f986889884243643621ebbd4ac72472ba8ec8cec891ac8e2ca1/h5py-3.16.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:0f456f556e4e2cebeebd9d66adf8dc321770a42593494a0b6f0af54a7567b242", size = 5074061, upload-time = "2026-03-06T13:48:54.089Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d0/146f5eaff3dc246a9c7f6e5e4f42bd45cc613bce16693bcd4d1f7c958bf5/h5py-3.16.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:3e6cb3387c756de6a9492d601553dffea3fe11b5f22b443aac708c69f3f55e16", size = 5279216, upload-time = "2026-03-06T13:48:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/12a13424f1e604fc7df9497b73c0356fb78c2fb206abd7465ce47226e8fd/h5py-3.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8389e13a1fd745ad2856873e8187fd10268b2d9677877bb667b41aebd771d8b7", size = 5070068, upload-time = "2026-03-06T13:48:59.169Z" }, + { url = "https://files.pythonhosted.org/packages/41/8c/bbe98f813722b4873818a8db3e15aa3e625b59278566905ac439725e8070/h5py-3.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:346df559a0f7dcb31cf8e44805319e2ab24b8957c45e7708ce503b2ec79ba725", size = 5300253, upload-time = "2026-03-06T13:49:02.033Z" }, + { url = "https://files.pythonhosted.org/packages/32/9e/87e6705b4d6890e7cecdf876e2a7d3e40654a2ae37482d79a6f1b87f7b92/h5py-3.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4c6ab014ab704b4feaa719ae783b86522ed0bf1f82184704ed3c9e4e3228796e", size = 3381671, upload-time = "2026-03-06T13:49:04.351Z" }, + { url = "https://files.pythonhosted.org/packages/96/91/9fad90cfc5f9b2489c7c26ad897157bce82f0e9534a986a221b99760b23b/h5py-3.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:faca8fb4e4319c09d83337adc80b2ca7d5c5a343c2d6f1b6388f32cfecca13c1", size = 2740706, upload-time = "2026-03-06T13:49:06.347Z" }, ] [[package]] @@ -2146,6 +2110,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" }, { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" }, { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" }, + { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" }, + { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" }, + { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" }, + { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" }, + { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" }, { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" }, { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" }, { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" }, @@ -2156,33 +2128,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" }, ] -[[package]] -name = "higra" -version = "0.6.13" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/11/ac41629d93a10a8d79faf0cec961a785536f75393396f809347d9b36bc46/higra-0.6.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aa1b6318672ebb87415ba0389059243fde0453a2bfb7d38dfe5489f429a76a9c", size = 7701486, upload-time = "2026-01-16T13:02:18.194Z" }, - { url = "https://files.pythonhosted.org/packages/3e/90/fb4e440d37c4a03d3bce3522d715f87c436c9ffd6c3c861b357163cd46e7/higra-0.6.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fecd196c551ac736bed53cf69bb19b9052d22122a36e297872fafca86b45a26f", size = 6134378, upload-time = "2026-01-16T12:55:22.208Z" }, - { url = "https://files.pythonhosted.org/packages/82/6a/27e323fe9186c74e3e9949629d04bceba76c3b99dd67ea9b3aaef66b5f7e/higra-0.6.13-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc560952fe9a6cec4482da3163e94592f95c93045a124176541d145c98f28a2d", size = 11591985, upload-time = "2026-01-16T12:58:54.959Z" }, - { url = "https://files.pythonhosted.org/packages/61/0e/82904a866a5e430622990cb9d344e6944fd65ab79cb310292a09e0ef7d23/higra-0.6.13-cp312-cp312-win_amd64.whl", hash = "sha256:71ecbf39621acc07b42c9468da4105946cee4016425b5f1edc82fc34ac0a2957", size = 5795626, upload-time = "2026-01-16T13:59:17.29Z" }, - { url = "https://files.pythonhosted.org/packages/c9/0f/09515a1b6fdef5235b0ab830efa0d7dac78a4b4fccd2088cd7d4eea96f24/higra-0.6.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:df0b605729012fb2edebb2fd7eed3810c9657d607bac2bd3cf1d6b5723962942", size = 7701704, upload-time = "2026-01-16T13:04:13.576Z" }, - { url = "https://files.pythonhosted.org/packages/ea/24/d7e691ad846106c1730924f49597c824360a58ed510440a283d0552e3a2d/higra-0.6.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ced8e0c40ef5d89e2a8abea60409a2e7cf67477302795a769121e67f599c9b75", size = 6134369, upload-time = "2026-01-16T13:06:04.963Z" }, - { url = "https://files.pythonhosted.org/packages/42/39/4af36b2460a080b4301fd9ca6c7fa8b97c96030a14bf1c928a2c07cd603f/higra-0.6.13-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b38257cd3f922aaaced79563543df64e732239942161231867ea9398f2e9161a", size = 11593680, upload-time = "2026-01-16T12:59:02.201Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b2/d1f2e3ed0870c5bc6b127a2ab917af0dd1233700d2503c67f4173f778725/higra-0.6.13-cp313-cp313-win_amd64.whl", hash = "sha256:493235a159c82c917abc17281cf4275dcb57fb1b213b3dbfdc1f81873c64e1e2", size = 5795756, upload-time = "2026-01-16T14:24:35.841Z" }, -] - -[[package]] -name = "hsluv" -version = "5.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ac/81/af16607fa045724e515579d312577261b436f36f419e7c677e7e88fcc943/hsluv-5.0.4.tar.gz", hash = "sha256:2281f946427a882010042844a38c7bbe9e0d0aaf9d46babe46366ed6f169b72e", size = 543090, upload-time = "2023-09-11T21:46:52.022Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/36/5bddefea3d7adf22a64f9aa9701492f8a9fe6948223f5cf2602c22ec9be7/hsluv-5.0.4-py2.py3-none-any.whl", hash = "sha256:0138bd10038e2ee1b13eecae9a7d49d4ec8c320b1d7eb4f860832c792e3e4567", size = 5252, upload-time = "2023-09-11T21:46:50.407Z" }, -] - [[package]] name = "httpcore" version = "1.0.9" @@ -2312,15 +2257,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/c6/fa760e12a2483469e2bf5058c5faff664acf66cadb4df2ad6205b016a73d/imageio_ffmpeg-0.6.0-py3-none-win_amd64.whl", hash = "sha256:02fa47c83703c37df6bfe4896aab339013f62bf02c5ebf2dce6da56af04ffc0a", size = 31246824, upload-time = "2025-01-16T21:34:28.6Z" }, ] -[[package]] -name = "imagesize" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, -] - [[package]] name = "importlib-metadata" version = "9.0.0" @@ -2342,15 +2278,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl", hash = "sha256:1bd7b48b4088eddb2cd16382150bb515af0bd2c70128194392725f82ad2c96a1", size = 37232, upload-time = "2026-04-12T16:36:08.219Z" }, ] -[[package]] -name = "in-n-out" -version = "0.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/08/07edfac98a38ab0208557524cbdd94a296f565b0558417ccb2c03d14a6ea/in_n_out-0.2.1.tar.gz", hash = "sha256:43cde2b7de981d41a6d70618a2b7bd989481095922a53ead4dc75f2bbd5dffea", size = 26026, upload-time = "2024-04-22T18:56:50.418Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/06/711a4d105ad3d01d3ef351a1039bb5cc517a57dbf377d7da9a0808e34c77/in_n_out-0.2.1-py3-none-any.whl", hash = "sha256:343e81edb27cf41ec946134a92964f408465abdf6a065c6c55fe96f53bc3c8b7", size = 19951, upload-time = "2024-04-22T18:56:48.572Z" }, -] - [[package]] name = "inflection" version = "0.5.1" @@ -2933,6 +2860,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, @@ -2974,6 +2931,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/fb/62ab6d4b582eaf12ac991c1bdb94f2211bd7bbeb59c04108af9e9456904e/kornia_rs-0.1.14-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:603f56ffa0ffe2de50e5c3c4c606e5a37c98c0277a2ad752feac0e25920880f4", size = 3377444, upload-time = "2026-05-19T07:45:14.587Z" }, { url = "https://files.pythonhosted.org/packages/d7/a8/9f6dec9c5b78ff76dca0fd63c6f3c96b8d00e2a475fcfadd7eeee0c65729/kornia_rs-0.1.14-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496301c800afea6867220d0f02344f44a90b50c1da22d5511c25df0c0c2b4d75", size = 3686869, upload-time = "2026-05-19T07:45:33.734Z" }, { url = "https://files.pythonhosted.org/packages/15/e7/ff8d97f922679c0f8f3aec6b27bab98c03c02feb2200543aed2e98b4b09d/kornia_rs-0.1.14-cp313-cp313t-win_amd64.whl", hash = "sha256:29cfb7b179ba0b98772bd459f6e74da67f93b290491a5c03deb9197955dfa684", size = 3359930, upload-time = "2026-05-19T07:46:20.207Z" }, + { url = "https://files.pythonhosted.org/packages/d5/49/093a3d42d52c6d5b1850f0564b82f9801575b818515a4dfbc9f82d2a7459/kornia_rs-0.1.14-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:816dd1d1713b13f3b39831d20097cb2aa69c2863c9a98555b1b32df0e5b9e309", size = 3555445, upload-time = "2026-05-19T07:46:06.481Z" }, + { url = "https://files.pythonhosted.org/packages/04/df/344d1c7e36c2cca38227b29693554c8dd1112e5b03155c5fae5fd9122df6/kornia_rs-0.1.14-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:27b23edda1f847ee4532ae2f008b16da535b947e2cb261be1865f7faff6c9fe7", size = 3349363, upload-time = "2026-05-19T07:45:52.348Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/a90c7ebe73715ec5d326cf5f215f430f32fb5f208a0f7db7bc04f523d7e6/kornia_rs-0.1.14-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1f5798b209c5e0cd6ec2629aac5b70c2b7c6c628a432a1b6a7414aca5805f9d", size = 3381572, upload-time = "2026-05-19T07:45:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ed/05/ffd6ae5b5cdddcbf9f7b7940d408c38911b8ab3911148b5b114522410ff1/kornia_rs-0.1.14-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a70df2ce65269de1f1e9c1fbe14e1fb2cdda6c3a39a31621b68a09cdba01d", size = 3695025, upload-time = "2026-05-19T07:45:35.916Z" }, + { url = "https://files.pythonhosted.org/packages/b0/96/56c15bd73c5ea7f242e6e03001b46db31c99fb7b287455dc36d1273f1b39/kornia_rs-0.1.14-cp314-cp314-win_amd64.whl", hash = "sha256:ff5ab2ede8eee7c05c6b55318ca96118785c40e9320e30c3fbb7f2b68b6fbe2b", size = 3366738, upload-time = "2026-05-19T07:46:21.693Z" }, + { url = "https://files.pythonhosted.org/packages/28/51/db8b3b72289a72c71a29a8d0ae683bd2f307c0b6f5d534744bdad0fd0183/kornia_rs-0.1.14-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7726f27690cf471e8df967d71ee6c937adce764a0de0fea02aeac216b71770fc", size = 3543838, upload-time = "2026-05-19T07:46:07.958Z" }, + { url = "https://files.pythonhosted.org/packages/78/45/adc8d6642c4cda15525b50d40fae79bf80db91a12237be824cbd67059726/kornia_rs-0.1.14-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7835328d5bf1565c42ce405db125811653a207c6e5dc16e937cf4527a04d8710", size = 3341040, upload-time = "2026-05-19T07:45:53.867Z" }, + { url = "https://files.pythonhosted.org/packages/b1/62/f3855e36213a8815ae933238b68f67c7435d7f76d36a1f08da3b8a6517a3/kornia_rs-0.1.14-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cb1a72ea7ce13a2971af16f28409c080560aa332431b3552c633197316e0869", size = 3377938, upload-time = "2026-05-19T07:45:19.273Z" }, + { url = "https://files.pythonhosted.org/packages/63/56/408f1fa7308c5ea8b3d7f5338d121580ef692f7e32858ee35f6969c158b6/kornia_rs-0.1.14-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dc1942acd2e6cbf28f1e056518db751264550f9aaa61760ce01ede266e42b61", size = 3687771, upload-time = "2026-05-19T07:45:37.952Z" }, + { url = "https://files.pythonhosted.org/packages/22/0b/bb6d1904f92a4f1d05edb4ac39513fd7a3babc48c889bd137f09d4fd5668/kornia_rs-0.1.14-cp314-cp314t-win_amd64.whl", hash = "sha256:26b13fbf0a22c133a1957defca8460faceeb22c7ce1ab37a6f4a658944682c58", size = 3360061, upload-time = "2026-05-19T07:46:23.27Z" }, ] [[package]] @@ -3053,6 +3020,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/b8/69f5565f1a280d032525878a86511eebed0645818492feeb169dfb20ae8e/llvmlite-0.47.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2699a74321189e812d476a43d6d7f652f51811e7b5aad9d9bba842a1c7927acb", size = 56275178, upload-time = "2026-03-31T18:29:05.748Z" }, { url = "https://files.pythonhosted.org/packages/d6/da/b32cafcb926fb0ce2aa25553bf32cb8764af31438f40e2481df08884c947/llvmlite-0.47.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c6951e2b29930227963e53ee152441f0e14be92e9d4231852102d986c761e40", size = 55128632, upload-time = "2026-03-31T18:29:11.235Z" }, { url = "https://files.pythonhosted.org/packages/46/9f/4898b44e4042c60fafcb1162dfb7014f6f15b1ec19bf29cfea6bf26df90d/llvmlite-0.47.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2e9adf8698d813a9a5efb2d4370caf344dbc1e145019851fee6a6f319ba760e", size = 38138695, upload-time = "2026-03-31T18:29:15.43Z" }, + { url = "https://files.pythonhosted.org/packages/1c/d4/33c8af00f0bf6f552d74f3a054f648af2c5bc6bece97972f3bfadce4f5ec/llvmlite-0.47.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:de966c626c35c9dff5ae7bf12db25637738d0df83fc370cf793bc94d43d92d14", size = 37232773, upload-time = "2026-03-31T18:29:19.453Z" }, + { url = "https://files.pythonhosted.org/packages/64/1d/a760e993e0c0ba6db38d46b9f48f6c7dceb8ac838824997fb9e25f97bc04/llvmlite-0.47.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ddbccff2aeaff8670368340a158abefc032fe9b3ccf7d9c496639263d00151aa", size = 56275176, upload-time = "2026-03-31T18:29:24.149Z" }, + { url = "https://files.pythonhosted.org/packages/84/3b/e679bc3b29127182a7f4aa2d2e9e5bea42adb93fb840484147d59c236299/llvmlite-0.47.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4a7b778a2e144fc64468fb9bf509ac1226c9813a00b4d7afea5d988c4e22fca", size = 55128631, upload-time = "2026-03-31T18:29:29.536Z" }, + { url = "https://files.pythonhosted.org/packages/be/f7/19e2a09c62809c9e63bbd14ce71fb92c6ff7b7b3045741bb00c781efc3c9/llvmlite-0.47.0-cp314-cp314-win_amd64.whl", hash = "sha256:694e3c2cdc472ed2bd8bd4555ca002eec4310961dd58ef791d508f57b5cc4c94", size = 39153826, upload-time = "2026-03-31T18:29:33.681Z" }, + { url = "https://files.pythonhosted.org/packages/40/a1/581a8c707b5e80efdbbe1dd94527404d33fe50bceb71f39d5a7e11bd57b7/llvmlite-0.47.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:92ec8a169a20b473c1c54d4695e371bde36489fc1efa3688e11e99beba0abf9c", size = 37232772, upload-time = "2026-03-31T18:29:37.952Z" }, + { url = "https://files.pythonhosted.org/packages/11/03/16090dd6f74ba2b8b922276047f15962fbeea0a75d5601607edb301ba945/llvmlite-0.47.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa1cbd800edd3b20bc141521f7fd45a6185a5b84109aa6855134e81397ffe72b", size = 56275178, upload-time = "2026-03-31T18:29:42.58Z" }, + { url = "https://files.pythonhosted.org/packages/f5/cb/0abf1dd4c5286a95ffe0c1d8c67aec06b515894a0dd2ac97f5e27b82ab0b/llvmlite-0.47.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6725179b89f03b17dabe236ff3422cb8291b4c1bf40af152826dfd34e350ae8", size = 55128632, upload-time = "2026-03-31T18:29:46.939Z" }, + { url = "https://files.pythonhosted.org/packages/4f/79/d3bbab197e86e0ff4f9c07122895b66a3e0d024247fcff7f12c473cb36d9/llvmlite-0.47.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6842cf6f707ec4be3d985a385ad03f72b2d724439e118fcbe99b2929964f0453", size = 39153839, upload-time = "2026-03-31T18:29:51.004Z" }, ] [[package]] @@ -3064,22 +3039,6 @@ wheels = [ { 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]] -name = "magicgui" -version = "0.10.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docstring-parser" }, - { name = "psygnal" }, - { name = "qtpy" }, - { name = "superqt", extra = ["iconify"] }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/9c/0d918ee0a9b31f16e9b76c1b86b81b5d4b7bc491354c76030b87790f0cab/magicgui-0.10.2.tar.gz", hash = "sha256:ae7a4cbb7ef2028b827b1877cf0b06743d756074fe6ef849391d62448ab7b65d", size = 20946219, upload-time = "2026-04-10T14:45:28.927Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/bb/5ba264d3ccc13294e74c48c3ef0c2e96b8b13765562628515654012cc47e/magicgui-0.10.2-py3-none-any.whl", hash = "sha256:0f304d4da1a4309ad15d38cb809ef73269cea73a3195ac944f38d7c56d8e0fd5", size = 128254, upload-time = "2026-04-10T14:45:27.173Z" }, -] - [[package]] name = "markdown" version = "3.10.2" @@ -3140,6 +3099,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] @@ -3180,6 +3161,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4c/3d/ed428c971139112ef730f62770654d609467346d09d4b62617e1afd68a5a/matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", size = 9680790, upload-time = "2026-04-24T00:13:10.009Z" }, { url = "https://files.pythonhosted.org/packages/e7/09/052e884aaf2b985c63cb79f715f1d5b6a3eaa7de78f6a52b9dbc077d5b53/matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", size = 8287571, upload-time = "2026-04-24T00:13:13.087Z" }, { url = "https://files.pythonhosted.org/packages/f4/38/ae27288e788c35a4250491422f3db7750366fc8c97d6f36fbdecfc1f5518/matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", size = 8188292, upload-time = "2026-04-24T00:13:15.546Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e6/3bd8afd04949f02eabc1c17115ea5255e19cacd4d06fc5abdde4eeb0052c/matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", size = 8321276, upload-time = "2026-04-24T00:13:18.318Z" }, + { url = "https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", size = 8218218, upload-time = "2026-04-24T00:13:20.974Z" }, + { url = "https://files.pythonhosted.org/packages/85/8f/becc9722cafc64f5d2eb0b7c1bf5f585271c618a45dbd8fabeb021f898b6/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", size = 9608145, upload-time = "2026-04-24T00:13:23.228Z" }, + { url = "https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", size = 9885085, upload-time = "2026-04-24T00:13:25.849Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fd/fa69f2221534e80cc5772ac2b7d222011a2acafc2ec7216d5dd174c864ae/matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", size = 9672358, upload-time = "2026-04-24T00:13:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", size = 8349970, upload-time = "2026-04-24T00:13:31.904Z" }, + { url = "https://files.pythonhosted.org/packages/64/dc/95d60ecaefe30680a154b52ea96ab4b0dab547f1fd6aa12f5fb655e89cae/matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", size = 8272785, upload-time = "2026-04-24T00:13:34.511Z" }, + { url = "https://files.pythonhosted.org/packages/70/a0/005d68bc8b8418300ce6591f18586910a8526806e2ab663933d9f20a41e9/matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", size = 8367999, upload-time = "2026-04-24T00:13:36.962Z" }, + { url = "https://files.pythonhosted.org/packages/22/05/1236cc9290be70b2498af20ca348add76e3fffe7f67b477db5133a84f3ea/matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", size = 8264543, upload-time = "2026-04-24T00:13:39.851Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c2/071f5a5ff6c5bd63aaaf2f45c811d9bf2ced94bde188d9e1a519e21d0cba/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", size = 9622800, upload-time = "2026-04-24T00:13:42.296Z" }, + { url = "https://files.pythonhosted.org/packages/95/57/da7d1f10a85624b9e7db68e069dd94e58dc41dbf9463c5921632ecbe3661/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", size = 9888561, upload-time = "2026-04-24T00:13:45.026Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/ef8d6bb59b0edb6c16c968b70f548aa13b54348972def5aa6ac85df67145/matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", size = 9680884, upload-time = "2026-04-24T00:13:48.066Z" }, + { url = "https://files.pythonhosted.org/packages/61/1c/d21bfeb9931881ebe96bcfcff27c7ae4b160ae0ec291a714c42641a56d75/matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", size = 8432333, upload-time = "2026-04-24T00:13:51.008Z" }, + { url = "https://files.pythonhosted.org/packages/78/23/92493c3e6e1b635ccfff146f7b99e674808787915420373ac399283764c2/matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", size = 8324785, upload-time = "2026-04-24T00:13:53.633Z" }, ] [[package]] @@ -3223,19 +3218,6 @@ dependencies = [ { name = "zensical" }, ] -[[package]] -name = "mip" -version = "1.17.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cbcbox" }, - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/15/7496ce00eafc5b99e98bb696588527754d7629f11ac6208fdc9355f6eec6/mip-1.17.6.tar.gz", hash = "sha256:9da8074b80bd3ef788513d5a214ef832916d82aa66487da11a49c7da9f89d270", size = 9443716, upload-time = "2026-03-23T16:20:09.009Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/65/1d/0eb2531e779be687a3249e7f2ba5e465dd34611072390252e5fcf89e7fef/mip-1.17.6-py3-none-any.whl", hash = "sha256:4fb7ff5d7beacbe7d007de172306a76aff8fb36760fa63a5d88a8cb3aa6f4a57", size = 88214, upload-time = "2026-03-23T16:20:07.289Z" }, -] - [[package]] name = "mistune" version = "3.2.1" @@ -3352,6 +3334,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/40/49/0f8c498a28c0efa5f5c95a9e374c83ec1385ca41d0e85e7cf40e5d519a21/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298", size = 5366453, upload-time = "2025-11-17T22:32:07.115Z" }, { url = "https://files.pythonhosted.org/packages/8c/27/12607423d0a9c6bbbcc780ad19f1f6baa2b68b18ce4bddcdc122c4c68dc9/ml_dtypes-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6", size = 225612, upload-time = "2025-11-17T22:32:08.615Z" }, { url = "https://files.pythonhosted.org/packages/e5/80/5a5929e92c72936d5b19872c5fb8fc09327c1da67b3b68c6a13139e77e20/ml_dtypes-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1", size = 164145, upload-time = "2025-11-17T22:32:09.782Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/1339dc6e2557a344f5ba5590872e80346f76f6cb2ac3dd16e4666e88818c/ml_dtypes-0.5.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22", size = 673781, upload-time = "2025-11-17T22:32:11.364Z" }, + { url = "https://files.pythonhosted.org/packages/04/f9/067b84365c7e83bda15bba2b06c6ca250ce27b20630b1128c435fb7a09aa/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465", size = 5036145, upload-time = "2025-11-17T22:32:12.783Z" }, + { url = "https://files.pythonhosted.org/packages/c6/bb/82c7dcf38070b46172a517e2334e665c5bf374a262f99a283ea454bece7c/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f", size = 5010230, upload-time = "2025-11-17T22:32:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e9/93/2bfed22d2498c468f6bcd0d9f56b033eaa19f33320389314c19ef6766413/ml_dtypes-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56", size = 221032, upload-time = "2025-11-17T22:32:15.763Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/9c912fe6ea747bb10fe2f8f54d027eb265db05dfb0c6335e3e063e74e6e8/ml_dtypes-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049", size = 163353, upload-time = "2025-11-17T22:32:16.932Z" }, + { url = "https://files.pythonhosted.org/packages/cd/02/48aa7d84cc30ab4ee37624a2fd98c56c02326785750cd212bc0826c2f15b/ml_dtypes-0.5.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9", size = 702085, upload-time = "2025-11-17T22:32:18.175Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e7/85cb99fe80a7a5513253ec7faa88a65306be071163485e9a626fce1b6e84/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7", size = 5355358, upload-time = "2025-11-17T22:32:19.7Z" }, + { url = "https://files.pythonhosted.org/packages/79/2b/a826ba18d2179a56e144aef69e57fb2ab7c464ef0b2111940ee8a3a223a2/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf", size = 5366332, upload-time = "2025-11-17T22:32:21.193Z" }, + { url = "https://files.pythonhosted.org/packages/84/44/f4d18446eacb20ea11e82f133ea8f86e2bf2891785b67d9da8d0ab0ef525/ml_dtypes-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1", size = 236612, upload-time = "2025-11-17T22:32:22.579Z" }, + { url = "https://files.pythonhosted.org/packages/ad/3f/3d42e9a78fe5edf792a83c074b13b9b770092a4fbf3462872f4303135f09/ml_dtypes-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d", size = 168825, upload-time = "2025-11-17T22:32:23.766Z" }, ] [[package]] @@ -3400,6 +3392,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, + { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" }, + { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" }, + { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" }, + { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" }, + { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" }, + { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, ] [[package]] @@ -3462,144 +3472,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] [[package]] -name = "nanobind" -version = "2.13.0" +name = "narwhals" +version = "2.22.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/07/e5/a6fc2f024eaef17c68b94b5f8f56098d31859a7840a11a3dd45da2d3908a/nanobind-2.13.0.tar.gz", hash = "sha256:c7b04d6a6a4cd57985571e605539399b51331ae455d7fce576a5e2fcb89b1dcf", size = 1052101, upload-time = "2026-06-18T06:03:49.491Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/1c/c80cb7719721a44846c6301ef118434bae30a423924bfad3a47f16bdc064/narwhals-2.22.0.tar.gz", hash = "sha256:6486282bb7e4b4ab55963efbd8be1451b764cc4874b74d1fd625eba9dc60b86f", size = 417565, upload-time = "2026-06-01T13:34:36.249Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/8d/cec69a0a804711fd89f47fad92cd2a438a1ceaafd9dabaf3b8b403b5cbba/nanobind-2.13.0-py3-none-any.whl", hash = "sha256:9268d7201eaf5519ae61b7a83175f2af77088cb4b99dd3ce5b00550f697da7b7", size = 269387, upload-time = "2026-06-18T06:03:47.508Z" }, -] - -[[package]] -name = "napari" -version = "0.6.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "app-model" }, - { name = "appdirs" }, - { name = "cachey" }, - { name = "certifi" }, - { name = "dask", extra = ["array"] }, - { name = "imageio" }, - { name = "jsonschema" }, - { name = "lazy-loader" }, - { name = "magicgui" }, - { name = "napari-console" }, - { name = "napari-plugin-engine" }, - { name = "napari-svg" }, - { name = "npe2" }, - { name = "numpy" }, - { name = "numpydoc" }, - { name = "pandas" }, - { name = "pillow" }, - { name = "pint" }, - { name = "psutil" }, - { name = "psygnal" }, - { name = "pydantic" }, - { name = "pygments" }, - { name = "pyopengl" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "pyyaml" }, - { name = "qtpy" }, - { name = "scikit-image", extra = ["data"] }, - { name = "scipy" }, - { name = "superqt" }, - { name = "tifffile" }, - { name = "toolz" }, - { name = "tqdm" }, - { name = "typing-extensions" }, - { name = "vispy" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/2c/cff26b706e1982317b19d1bd57bc7809fbbc586da794c968d1b3997e1b85/napari-0.6.5.tar.gz", hash = "sha256:00663221583467f41fa956a672b0b4cd7a761e2642db41fc3c336d1c82339533", size = 3249897, upload-time = "2025-10-02T02:18:48.616Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/69/1e8c5c7d487ad109206ce7f028ad02d41171641e1e95b4e45a913ee4d6b1/napari-0.6.5-py3-none-any.whl", hash = "sha256:51bc0b0fb6b6271555ad4167ce510b9453735aaad0ef947cfbc38bdd03d6a7be", size = 3536276, upload-time = "2025-10-02T02:18:46.062Z" }, -] - -[package.optional-dependencies] -pyqt5 = [ - { name = "pyqt5" }, - { name = "pyqt5-qt5", version = "5.15.2", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'Windows'" }, -] - -[[package]] -name = "napari-console" -version = "0.1.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ipykernel" }, - { name = "ipython" }, - { name = "qtconsole" }, - { name = "qtpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/41/4e/9b65e4c4e9520888fe69c6307818f4f75b0113b0fab1db67424b29bed9fe/napari_console-0.1.3.tar.gz", hash = "sha256:ba4f7e1cdca65a7924631372a5e58884e2e35a2b9092c79b98acb9c2dfe1254f", size = 20010, upload-time = "2024-12-20T13:48:58.451Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/ff/1d1aad9292aeccf01f94ca150ce621737cbe7b9fabbb0bfef32b8da70e58/napari_console-0.1.3-py3-none-any.whl", hash = "sha256:29d1700b85561b8c91331821360c9cd0e989350f4a06b86239ec65bc06c7aecb", size = 9990, upload-time = "2024-12-20T13:48:54.093Z" }, -] - -[[package]] -name = "napari-geff" -source = { editable = "../../../../../home/eduardo.hirata/repos/napari-geff" } -dependencies = [ - { name = "geff" }, - { name = "magicgui" }, - { name = "numpy" }, - { name = "pandas" }, - { name = "qtpy" }, - { name = "scikit-image" }, -] - -[package.metadata] -requires-dist = [ - { name = "geff", specifier = ">=1.1.1" }, - { name = "magicgui" }, - { name = "napari", extras = ["all"], marker = "extra == 'all'" }, - { name = "napari", extras = ["qt"], marker = "extra == 'testing'" }, - { name = "numpy" }, - { name = "pandas" }, - { name = "pytest", marker = "extra == 'testing'" }, - { name = "pytest-cov", marker = "extra == 'testing'" }, - { name = "pytest-qt", marker = "extra == 'testing'" }, - { name = "qtpy" }, - { name = "scikit-image" }, - { name = "tox", marker = "extra == 'testing'" }, -] -provides-extras = ["all", "testing"] - -[[package]] -name = "napari-plugin-engine" -version = "0.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/72/c653308edaf7f7c84d82e388a1c46bc8f26c385027af58b5bf728f600b47/napari_plugin_engine-0.2.1.tar.gz", hash = "sha256:46829cf02f368c8f2f1aa8b998ec73bcf14a2c1f5c15abd94b82154d7aef510d", size = 55809, upload-time = "2026-02-10T08:31:20.385Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/bc/2509813cddd0e02736121e21bef54deeec7f0f89af2c6096d753ee1feb09/napari_plugin_engine-0.2.1-py3-none-any.whl", hash = "sha256:de30babe6fd9477816f037207938a2da7faeddc0e9e8663cb29f3d74235b6dc5", size = 33849, upload-time = "2026-02-10T08:31:19.037Z" }, -] - -[[package]] -name = "napari-svg" -version = "0.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "imageio" }, - { name = "numpy" }, - { name = "vispy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a7/90/802f8288d16c1513b908d644779e733461a53b6c1a2c7561f1464c9f1516/napari_svg-0.2.1.tar.gz", hash = "sha256:031f13b34b0948afbdcb11eb00728fe32ef7e4e3aa3905f923001d6871a08ad9", size = 17533, upload-time = "2025-01-14T07:26:30.657Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/ae/0eeb22806c4157a9199f65a93374e5ff5c4d2cc1411b5d25053bcd9e6b91/napari_svg-0.2.1-py3-none-any.whl", hash = "sha256:9eaa54fbbf9bfd5078b67b7d1edc9eccfd872dab89fd586374909fef4ed89a49", size = 16458, upload-time = "2025-01-14T07:26:29.328Z" }, -] - -[[package]] -name = "narwhals" -version = "2.22.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/1c/c80cb7719721a44846c6301ef118434bae30a423924bfad3a47f16bdc064/narwhals-2.22.0.tar.gz", hash = "sha256:6486282bb7e4b4ab55963efbd8be1451b764cc4874b74d1fd625eba9dc60b86f", size = 417565, upload-time = "2026-06-01T13:34:36.249Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/b6/e7cdde7b8e90d5dff25b622f95833ef26567ad184c977278b93a1cbd5717/narwhals-2.22.0-py3-none-any.whl", hash = "sha256:1421797ede01789cc1537619dbc3f36f840737240f748fdb24a60a0225fc80be", size = 453815, upload-time = "2026-06-01T13:34:34.127Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b6/e7cdde7b8e90d5dff25b622f95833ef26567ad184c977278b93a1cbd5717/narwhals-2.22.0-py3-none-any.whl", hash = "sha256:1421797ede01789cc1537619dbc3f36f840737240f748fdb24a60a0225fc80be", size = 453815, upload-time = "2026-06-01T13:34:34.127Z" }, ] [[package]] @@ -3696,6 +3614,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/19/002fc1e6a4abeef8d92e9aa2e43aea4d462f6b170090f7752ea8887f4897/ndindex-1.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a91e1a0398120233d5c3b23ccb2d4b78e970d66136f1a7221fa9a53873c3d5c5", size = 1636436, upload-time = "2025-11-19T20:39:18.266Z" }, { url = "https://files.pythonhosted.org/packages/5f/8f/28b1ad78c787ac8fafd6e26419a80366617784b1779e3857fa687492f6bc/ndindex-1.10.1-cp313-cp313t-win32.whl", hash = "sha256:78bfe25941d2dac406391ddd9baf0b0fce163807b98ecc2c47a3030ee8466319", size = 158780, upload-time = "2025-11-19T20:39:20.454Z" }, { url = "https://files.pythonhosted.org/packages/d0/56/b81060607a19865bb8be8d705b1b3e8aefb8747c0fbd383e38b4cae4bd71/ndindex-1.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:08bfdc1f7a0b408d15b3ce61d141ebbebdb47a25341967e425e104c5bd512a5c", size = 167485, upload-time = "2025-11-19T20:39:21.733Z" }, + { url = "https://files.pythonhosted.org/packages/da/9b/aac1131e9f3a5635ba7b0312c3bfa610511ab4108f85c0d914a32887aa00/ndindex-1.10.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9b5297f207ebc068c7cdf9e3cd7b95aa5c9ec04295d0a7e56b529f66787d4685", size = 176478, upload-time = "2025-11-19T20:39:23.747Z" }, + { url = "https://files.pythonhosted.org/packages/1a/05/a0d8ca0432c84550bc17af6d6479a803936895b8b8403a1216c5a55475fb/ndindex-1.10.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c5e9762452b163e33cfb6e821f86e45ba0b53bdfcd23ab5d57b48a8f566898cb", size = 175480, upload-time = "2025-11-19T20:39:25.365Z" }, + { url = "https://files.pythonhosted.org/packages/09/4a/028ab78a9f29fd2a7e86a90337cde4658eaa77b425c63045d83a1d2e4f26/ndindex-1.10.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf80241b40adffdc3276b2c9fb63a96c6c98b4a9d941892738de8add65083962", size = 528125, upload-time = "2025-11-19T20:39:26.798Z" }, + { url = "https://files.pythonhosted.org/packages/00/a9/bd823b345fb06c83ade6ef1c1933521d4357cd04490e684d4fa30126926c/ndindex-1.10.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf5855881884b8467dfcf45764ccf2e4279075be14b155b89c96994bb08d2e6f", size = 527328, upload-time = "2025-11-19T20:39:28.292Z" }, + { url = "https://files.pythonhosted.org/packages/91/4f/40b9c15588cbf9dde43c4fb88a31dd1f636a913fa29649f18f8e3ebca36a/ndindex-1.10.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e81a9bd36fe054b6c9fcc53d26bc9a28cf15d1ab52a0f5b854f894116f3a54e1", size = 1497508, upload-time = "2025-11-19T20:39:30.735Z" }, + { url = "https://files.pythonhosted.org/packages/24/8f/b8048f7837d2e9dff0af507b398307fa84a2aa9ea3db71b4aa800b21da4a/ndindex-1.10.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:588e8875d836a93b3cd9af482c8074bb02288ae1aff92cf277e1f02d9ae0f992", size = 1552625, upload-time = "2025-11-19T20:39:32.404Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/0ecb53c7e690a44769f2f92a843723ccb1d0ce080d93ba1ea811304cca12/ndindex-1.10.1-cp314-cp314-win32.whl", hash = "sha256:28741daca5926adff402247cd406f453ed5bb6042e82d6855938f805190e5ce9", size = 151237, upload-time = "2025-11-19T20:39:34.847Z" }, + { url = "https://files.pythonhosted.org/packages/8c/4e/197982fa8b4e6e6b9d15c38505c41076d1c552921f09f4d35acbbbbc0b70/ndindex-1.10.1-cp314-cp314-win_amd64.whl", hash = "sha256:59a3222befc0f7cdc85fb9b90a567ae890f70a864bdeb660517e9ebcb36bf1bc", size = 158925, upload-time = "2025-11-19T20:39:37.149Z" }, + { url = "https://files.pythonhosted.org/packages/24/ad/116b6154046a69fc04e2d4490905801d3839a3f21290c0b4d49b1044e251/ndindex-1.10.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:967b87b88dadb62555ec1039695c347254eccb8ca3d124c0e5dbe084c525fa93", size = 181724, upload-time = "2025-11-19T20:39:38.635Z" }, + { url = "https://files.pythonhosted.org/packages/c4/00/3ce4351366c890bcc87a5e9f1f90102547962eef356ac7c799bfdd0dddce/ndindex-1.10.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c67dde588c0fb89d872931a4ed5f9b4d21c1c70a3d92fdf0812a1de154239816", size = 181653, upload-time = "2025-11-19T20:39:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/4d/05/a6fda696a2f02a3f8dd2ee9d816cb2edff6423bf0110a4876cc3b1259732/ndindex-1.10.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c65ca639a7abf72d79f22424f4abd18dece1f289a2b7b028a0ca455edd2168d4", size = 630898, upload-time = "2025-11-19T20:39:41.495Z" }, + { url = "https://files.pythonhosted.org/packages/73/78/eb2e5d067d4c054451e33eaece74cbdcb58236dc60516e73d783dae34c7e/ndindex-1.10.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c3634a8df43e7928122225a3d64d850c8957bd1edf2e403907deacb478af27b", size = 614419, upload-time = "2025-11-19T20:39:43.254Z" }, + { url = "https://files.pythonhosted.org/packages/78/51/261bfb49eb7920c2a7314cacba5821930a529911dce48c7c6cd786096a5a/ndindex-1.10.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9d581f931e61f182478f18bdf5edd3955899df5da4892ed0d5de547a4cfd5b6f", size = 1587517, upload-time = "2025-11-19T20:39:44.809Z" }, + { url = "https://files.pythonhosted.org/packages/ec/37/084a332ecdf8b0049151bd78001a7baf2daf7f500d043beb8a1f95d0f4e3/ndindex-1.10.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:78ce45106ebf67aeba99714818c721d8fd5fb9534daebd2565665a2d64b50fc9", size = 1635372, upload-time = "2025-11-19T20:39:47.231Z" }, + { url = "https://files.pythonhosted.org/packages/28/f4/716580fbb03018ab1daa86ed12c1925c67e79689db5fee82393e840758a2/ndindex-1.10.1-cp314-cp314t-win32.whl", hash = "sha256:fe5341e24dc992b09c258456ac90a09a6d25efdc2cb86dcc91d32c8891e1df9a", size = 162186, upload-time = "2025-11-19T20:39:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/4d/20/28f669c09a470e7f523b0cc10b94336664d9648594015e3f2a1ec29047b1/ndindex-1.10.1-cp314-cp314t-win_amd64.whl", hash = "sha256:37f87f0e7690ae0324334740e0661d6297f2e62c9bf925127d249fb7eddd0ad8", size = 171077, upload-time = "2025-11-19T20:39:50.108Z" }, ] [[package]] @@ -3742,26 +3676,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, ] -[[package]] -name = "npe2" -version = "0.8.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "build" }, - { name = "platformdirs" }, - { name = "psygnal" }, - { name = "pydantic" }, - { name = "pydantic-extra-types" }, - { name = "pyyaml" }, - { name = "rich" }, - { name = "tomli-w" }, - { name = "typer" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/29/0a/1a6ed44b038484a064a0ac8e5f96aae6ddb8768789145cf7e65e699b324b/npe2-0.8.2.tar.gz", hash = "sha256:6e41cc1f2c873257d864980dd281b5bb649a84cef02feeb0cdda1a9d23fd8f8b", size = 122768, upload-time = "2026-04-04T20:55:11.9Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/a6/f318fbc7bbb62361d36cb694f399f765f57fb282fdadc3aa2fef4dafd62d/npe2-0.8.2-py3-none-any.whl", hash = "sha256:80eff5fef50352cf0f3407c4c1122a7ae186aede40a21fd595cf91988cd55a9d", size = 93921, upload-time = "2026-04-04T20:55:10.552Z" }, -] - [[package]] name = "numba" version = "0.65.1" @@ -3780,6 +3694,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/22/b8d873f6466b20aa563fc9b33acd48dec89a07803ddaa2f1c8ca1cd33126/numba-0.65.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c09f49117ef255e1f1c6dad0c7a1ed39868243862a73be5706793241a3755f1b", size = 3810619, upload-time = "2026-04-24T02:02:36.041Z" }, { url = "https://files.pythonhosted.org/packages/62/08/e16a8b5d9a018962ebb5c66be662317cde32b9f5dab08441f90bed5522fb/numba-0.65.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:594a8680b3fadac99e97e489b1fd89007177e5336713745c3b769528c635a464", size = 3509783, upload-time = "2026-04-24T02:02:38.245Z" }, { url = "https://files.pythonhosted.org/packages/fd/a5/03c970d57f4c1741354837353ce39fb5206952ae1dba8922d29c86f64805/numba-0.65.1-cp313-cp313-win_amd64.whl", hash = "sha256:85be74c0d036842699a30058f82fb88fc5ffdc59f7615cab5792ea92914c9b62", size = 2750534, upload-time = "2026-04-24T02:02:39.903Z" }, + { url = "https://files.pythonhosted.org/packages/4f/2e/8aed9b726d9ba5f11ad287645fd479e88278db3060a25cb1225d730eb2b7/numba-0.65.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:33f5eb68eb1c843511615d14663ce60258525d6a4c65ab040e2c2b0c4cf17450", size = 2681554, upload-time = "2026-04-24T02:02:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/87/96/f3eb235fafa82a34e2ab5dd7dc9ffff998ebf5f0bbc23fa56a96aeb44da6/numba-0.65.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71e73029bf53a62cc6afcf96be4bd942290d8b4c55f0a454fb536158115790f7", size = 3779602, upload-time = "2026-04-24T02:02:43.726Z" }, + { url = "https://files.pythonhosted.org/packages/09/90/b0f09b48752d23640b8284f22aa597737e8adaddc7fbfacc4708b7f73a4c/numba-0.65.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a07635e0be926b9bdbffb09137c230fb13f6ec0e564914ba937cee12ce3eb35", size = 3479532, upload-time = "2026-04-24T02:02:45.427Z" }, + { url = "https://files.pythonhosted.org/packages/56/46/3f7fc04fb853559e74b210e0b62c19974ec844cefec611f9e535f4da3761/numba-0.65.1-cp314-cp314-win_amd64.whl", hash = "sha256:2a20fcdabdefbdacf88d85caf70c3b18c4bcb7ebb8f82e6a19486383dd26ab63", size = 2752637, upload-time = "2026-04-24T02:02:47.664Z" }, + { url = "https://files.pythonhosted.org/packages/81/7b/c1a341a9067367778f4152a5f01061cf281fb09582c92c510ec4918cabf6/numba-0.65.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:548dd4b3a4508d5062768d1514b2cd7b015f9a25ec7af651c50dee243965e652", size = 2684600, upload-time = "2026-04-24T02:02:49.653Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/98ddbcf3e4f04a6dd07e1c67249955920579ba4af6bb6868e3088f4ed282/numba-0.65.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:78abc28feff2c2ff8307fff3975b6438352759c9acb797ecd6b1fb6e7e39e31d", size = 3817198, upload-time = "2026-04-24T02:02:51.266Z" }, + { url = "https://files.pythonhosted.org/packages/a3/83/0dad21057ece5a835599f5d24099b091703995e23dbbf894f259e91c010b/numba-0.65.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee7676cb389555805f9b9a1840cbcd1ea6c8bd5376ab6918e3a29c5ea1dbda20", size = 3533862, upload-time = "2026-04-24T02:02:52.987Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/8be7118ffd4c8440881046eac3d0982cc5ab42909508cf5d67024d62a2e4/numba-0.65.1-cp314-cp314t-win_amd64.whl", hash = "sha256:20609346e3bd75204950dcbbfe383a8d7dbf4902f442aedbf00f97fef4aa8f38", size = 2758237, upload-time = "2026-04-24T02:02:54.612Z" }, ] [[package]] @@ -3835,6 +3757,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/ce/0d4fcd31ab49319740d934fba1734d7dad13aa485532ca754e555ca16c8b/numexpr-2.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:67ea4771029ce818573b1998f5ca416bd255156feea017841b86176a938f7d19", size = 1474214, upload-time = "2025-10-13T16:15:38.893Z" }, { url = "https://files.pythonhosted.org/packages/b7/47/b2a93cbdb3ba4e009728ad1b9ef1550e2655ea2c86958ebaf03b9615f275/numexpr-2.14.1-cp313-cp313t-win32.whl", hash = "sha256:15015d47d3d1487072d58c0e7682ef2eb608321e14099c39d52e2dd689483611", size = 167676, upload-time = "2025-10-13T16:17:17.351Z" }, { url = "https://files.pythonhosted.org/packages/86/99/ee3accc589ed032eea68e12172515ed96a5568534c213ad109e1f4411df1/numexpr-2.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:94c711f6d8f17dfb4606842b403699603aa591ab9f6bf23038b488ea9cfb0f09", size = 161096, upload-time = "2025-10-13T16:17:19.174Z" }, + { url = "https://files.pythonhosted.org/packages/ac/36/9db78dfbfdfa1f8bf0872993f1a334cdd8fca5a5b6567e47dcb128bcb7c2/numexpr-2.14.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ede79f7ff06629f599081de644546ce7324f1581c09b0ac174da88a470d39c21", size = 162848, upload-time = "2025-10-13T16:16:46.216Z" }, + { url = "https://files.pythonhosted.org/packages/13/c1/a5c78ae637402c5550e2e0ba175275d2515d432ec28af0cdc23c9b476e65/numexpr-2.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2eac7a5a2f70b3768c67056445d1ceb4ecd9b853c8eda9563823b551aeaa5082", size = 152270, upload-time = "2025-10-13T16:16:47.92Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ed/aabd8678077848dd9a751c5558c2057839f5a09e2a176d8dfcd0850ee00e/numexpr-2.14.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aedf38d4c0c19d3cecfe0334c3f4099fb496f54c146223d30fa930084bc8574", size = 455918, upload-time = "2025-10-13T16:13:50.338Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/3db65117f02cdefb0e5e4c440daf1c30beb45051b7f47aded25b7f4f2f34/numexpr-2.14.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439ec4d57b853792ebe5456e3160312281c3a7071ecac5532ded3278ede614de", size = 446512, upload-time = "2025-10-13T16:15:42.313Z" }, + { url = "https://files.pythonhosted.org/packages/9a/fb/7ceb9ee55b5f67e4a3e4d73d5af4c7e37e3c9f37f54bee90361b64b17e3f/numexpr-2.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e23b87f744e04e302d82ac5e2189ae20a533566aec76a46885376e20b0645bf8", size = 1417845, upload-time = "2025-10-13T16:13:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9b5764d0eafbbb2889288f80de773791358acf6fad1a55767538d8b79599/numexpr-2.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:44f84e0e5af219dbb62a081606156420815890e041b87252fbcea5df55214c4c", size = 1466211, upload-time = "2025-10-13T16:15:48.985Z" }, + { url = "https://files.pythonhosted.org/packages/5d/21/204db708eccd71aa8bc55bcad55bc0fc6c5a4e01ad78e14ee5714a749386/numexpr-2.14.1-cp314-cp314-win32.whl", hash = "sha256:1f1a5e817c534539351aa75d26088e9e1e0ef1b3a6ab484047618a652ccc4fc3", size = 168835, upload-time = "2025-10-13T16:17:20.82Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3e/d83e9401a1c3449a124f7d4b3fb44084798e0d30f7c11e60712d9b94cf11/numexpr-2.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:587c41509bc373dfb1fe6086ba55a73147297247bedb6d588cda69169fc412f2", size = 162608, upload-time = "2025-10-13T16:17:22.228Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d6/ec947806bb57836d6379a8c8a253c2aeaa602b12fef2336bfd2462bb4ed5/numexpr-2.14.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec368819502b64f190c3f71be14a304780b5935c42aae5bf22c27cc2cbba70b5", size = 163525, upload-time = "2025-10-13T16:16:50.133Z" }, + { url = "https://files.pythonhosted.org/packages/0d/77/048f30dcf661a3d52963a88c29b52b6d5ce996d38e9313a56a922451c1e0/numexpr-2.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7e87f6d203ac57239de32261c941e9748f9309cbc0da6295eabd0c438b920d3a", size = 152917, upload-time = "2025-10-13T16:16:52.055Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/956a13e628d722d649fbf2fded615134a308c082e122a48bad0e90a99ce9/numexpr-2.14.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd72d8c2a165fe45ea7650b16eb8cc1792a94a722022006bb97c86fe51fd2091", size = 466242, upload-time = "2025-10-13T16:13:55.795Z" }, + { url = "https://files.pythonhosted.org/packages/d6/dd/abe848678d82486940892f2cacf39e82eec790e8930d4d713d3f9191063b/numexpr-2.14.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70d80fcb418a54ca208e9a38e58ddc425c07f66485176b261d9a67c7f2864f73", size = 457149, upload-time = "2025-10-13T16:15:52.036Z" }, + { url = "https://files.pythonhosted.org/packages/fd/bb/797b583b5fb9da5700a5708ca6eb4f889c94d81abb28de4d642c0f4b3258/numexpr-2.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:edea2f20c2040df8b54ee8ca8ebda63de9545b2112872466118e9df4d0ae99f3", size = 1426493, upload-time = "2025-10-13T16:13:59.244Z" }, + { url = "https://files.pythonhosted.org/packages/77/c4/0519ab028fdc35e3e7ee700def7f2b4631b175cd9e1202bd7966c1695c33/numexpr-2.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:790447be6879a6c51b9545f79612d24c9ea0a41d537a84e15e6a8ddef0b6268e", size = 1474413, upload-time = "2025-10-13T16:15:59.211Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4a/33044878c8f4a75213cfe9c11d4c02058bb710a7a063fe14f362e8de1077/numexpr-2.14.1-cp314-cp314t-win32.whl", hash = "sha256:538961096c2300ea44240209181e31fae82759d26b51713b589332b9f2a4117e", size = 169502, upload-time = "2025-10-13T16:17:23.829Z" }, + { url = "https://files.pythonhosted.org/packages/41/a2/5a1a2c72528b429337f49911b18c302ecd36eeab00f409147e1aa4ae4519/numexpr-2.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a40b350cd45b4446076fa11843fa32bbe07024747aeddf6d467290bf9011b392", size = 163589, upload-time = "2025-10-13T16:17:25.696Z" }, ] [[package]] @@ -3875,18 +3813,27 @@ wheels = [ { 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" }, -] - -[[package]] -name = "numpydoc" -version = "1.10.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/3c/dfccc9e7dee357fb2aa13c3890d952a370dd0ed071e0f7ed62ed0df567c1/numpydoc-1.10.0.tar.gz", hash = "sha256:3f7970f6eee30912260a6b31ac72bba2432830cd6722569ec17ee8d3ef5ffa01", size = 94027, upload-time = "2025-12-02T16:39:12.937Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/5e/3a6a3e90f35cea3853c45e5d5fb9b7192ce4384616f932cf7591298ab6e1/numpydoc-1.10.0-py3-none-any.whl", hash = "sha256:3149da9874af890bcc2a82ef7aae5484e5aa81cb2778f08e3c307ba6d963721b", size = 69255, upload-time = "2025-12-02T16:39:11.561Z" }, + { 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" }, ] [[package]] @@ -3894,7 +3841,7 @@ name = "nvidia-cublas" version = "13.1.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cuda-nvrtc", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cuda-nvrtc" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, @@ -3933,7 +3880,7 @@ name = "nvidia-cudnn-cu13" version = "9.20.0.48" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cublas" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, @@ -3945,7 +3892,7 @@ name = "nvidia-cufft" version = "12.0.0.61" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, @@ -3975,9 +3922,9 @@ name = "nvidia-cusolver" version = "12.0.4.66" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "nvidia-cusparse", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cublas" }, + { name = "nvidia-cusparse" }, + { name = "nvidia-nvjitlink" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, @@ -3989,7 +3936,7 @@ name = "nvidia-cusparse" version = "12.6.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, @@ -4051,42 +3998,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, ] -[[package]] -name = "ome-zarr" -version = "0.18.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "dask" }, - { name = "deprecated" }, - { name = "fsspec", extra = ["s3"] }, - { name = "numpy" }, - { name = "ome-zarr-models" }, - { name = "rangehttpserver" }, - { name = "requests" }, - { name = "scikit-image" }, - { name = "toolz" }, - { name = "zarr" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/59/98/f39cf0ec00ad64996ed2d2d6059d15a30c7b8f559cd2a216589885001a6a/ome_zarr-0.18.0.tar.gz", hash = "sha256:5cc0fc682b0c177fa32e54f4479b2436f79850a171e83ad4c6b08fec32e66cd0", size = 106039, upload-time = "2026-06-17T16:22:07.76Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/be/2cf9389c9a7b80ceefae88bd713afa6a4b488cb47e524e7dadae44260efd/ome_zarr-0.18.0-py3-none-any.whl", hash = "sha256:aaf6ea6ac5790623290210efc566ad49f92da170b50b07e9113b6b06a0870cc5", size = 57947, upload-time = "2026-06-17T16:22:06.732Z" }, -] - -[[package]] -name = "ome-zarr-models" -version = "1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "pydantic-zarr" }, - { name = "zarr" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/e7/c37b8896a24eabc76d6a5e7855f722792de5bdf3f5ccc562001738be4f7e/ome_zarr_models-1.6.tar.gz", hash = "sha256:00735ad939d2b5f6a4f583cc19af1e8a5247fa1e93f1f08191143cd913b9633a", size = 106913, upload-time = "2026-03-10T10:14:28.369Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/fc/7650df09dce0edfbaa1f2898c5eb0e0523ae5964dadb83855beb52166505/ome_zarr_models-1.6-py3-none-any.whl", hash = "sha256:d855a6fe885aa2613f64cd124ff8453d75854531bb3282eeaa916e976c7bcf66", size = 64251, upload-time = "2026-03-10T10:14:27.177Z" }, -] - [[package]] name = "omegaconf" version = "2.3.0" @@ -4123,6 +4034,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/2e/27affcac63eaf2ef183a44fd1a1354b11da64a6c72fe6f3fdcf5571bcee5/onnx-1.21.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b58a4cfec8d9311b73dc083e4c1fa362069267881144c05139b3eba5dc3a840", size = 17617687, upload-time = "2026-03-27T21:33:12.619Z" }, { url = "https://files.pythonhosted.org/packages/1c/5c/ac8ed15e941593a3672ce424280b764979026317811f2e8508432bfc3429/onnx-1.21.0-cp313-cp313t-win_amd64.whl", hash = "sha256:1a9baf882562c4cebf79589bebb7cd71a20e30b51158cac3e3bbaf27da6163bd", size = 16449402, upload-time = "2026-03-27T21:33:15.555Z" }, { url = "https://files.pythonhosted.org/packages/0e/aa/d2231e0dcaad838217afc64c306c8152a080134d2034e247cc973d577674/onnx-1.21.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bba12181566acf49b35875838eba49536a327b2944664b17125577d230c637ad", size = 16408273, upload-time = "2026-03-27T21:33:18.599Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0a/8905b14694def6ad23edf1011fdd581500384062f8c4c567e114be7aa272/onnx-1.21.0-cp314-cp314t-macosx_12_0_universal2.whl", hash = "sha256:7ee9d8fd6a4874a5fa8b44bbcabea104ce752b20469b88bc50c7dcf9030779ad", size = 17975331, upload-time = "2026-03-27T21:33:21.69Z" }, + { url = "https://files.pythonhosted.org/packages/61/28/f4e401e5199d1b9c8b76c7e7ae1169e050515258e877b58fa8bb49d3bdcc/onnx-1.21.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5489f25fe461e7f32128218251a466cabbeeaf1eaa791c79daebf1a80d5a2cc9", size = 17537430, upload-time = "2026-03-27T21:33:24.547Z" }, + { url = "https://files.pythonhosted.org/packages/cf/cf/5d13320eb3660d5af360ea3b43aa9c63a70c92a9b4d1ea0d34501a32fcb8/onnx-1.21.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:db17fc0fec46180b6acbd1d5d8650a04e5527c02b09381da0b5b888d02a204c8", size = 17617662, upload-time = "2026-03-27T21:33:27.418Z" }, + { url = "https://files.pythonhosted.org/packages/4d/50/3eaa1878338247be021e6423696813d61e77e534dccbd15a703a144e703d/onnx-1.21.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19d9971a3e52a12968ae6c70fd0f86c349536de0b0c33922ecdbe52d1972fe60", size = 16463688, upload-time = "2026-03-27T21:33:30.229Z" }, + { url = "https://files.pythonhosted.org/packages/a7/48/38d46b43bbb525e0b6a4c2c4204cc6795d67e45687a2f7403e06d8e7053d/onnx-1.21.0-cp314-cp314t-win_arm64.whl", hash = "sha256:efba467efb316baf2a9452d892c2f982b9b758c778d23e38c7f44fa211b30bb9", size = 16423387, upload-time = "2026-03-27T21:33:33.446Z" }, ] [[package]] @@ -4157,6 +4073,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dd/97/fe8979f44b9275654b42f7bb556e30789b71a1b22998c83b540df2b1b774/onnxruntime_gpu-1.26.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfda2fad535595bfc3e570eb588092717711dcb2957656d814695e0c9ceb1508", size = 276974871, upload-time = "2026-05-08T19:15:58.052Z" }, { url = "https://files.pythonhosted.org/packages/67/3f/59f1777a394625ecc9a85636de57dc47c25dbb5f888da050f1463955a0ce/onnxruntime_gpu-1.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:6ab9f9c741d2e239b2e321ab0d389c04329d4ab7f11e3b92dd3aa7db1c59dee4", size = 226548083, upload-time = "2026-05-08T19:09:44.408Z" }, { url = "https://files.pythonhosted.org/packages/89/96/360328e3c463f7ea08e853c4239c397e83363dd0204de71a710dc1a544bd/onnxruntime_gpu-1.26.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bcf6f347cad9f88a9a625c2b352cf9de927528aedb627ebbc089a201f1990b94", size = 276992052, upload-time = "2026-05-08T19:16:09.892Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c8/aa2dc0e79bba577f37d5448bcb32fea79977e07506684d8138c19a0f1077/onnxruntime_gpu-1.26.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e6e4fb1ec9ae1cf456534d9115f106ab2a1ae96fa513b4ed0f4795302b4a2c6", size = 276978254, upload-time = "2026-05-08T19:16:22.096Z" }, + { url = "https://files.pythonhosted.org/packages/41/e7/923298431e669567d7ccc2a4c898b6534a47641a051569fd97165fe6d9b8/onnxruntime_gpu-1.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e592439b0183d303c2374517b5b392599a3d50b2dc9de949b9b15731ac921c9", size = 229142768, upload-time = "2026-05-08T19:09:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/97/91/93ffe5431d154989f5e04864a25a97eea480997d771232bcbbc538188241/onnxruntime_gpu-1.26.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56dc7b73954ff4bdc71f5b8ab306b6f61be5d007881b6ef423a609e2b9cd088b", size = 276991545, upload-time = "2026-05-08T19:16:33.347Z" }, ] [[package]] @@ -4230,6 +4149,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" }, { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" }, { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" }, + { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" }, + { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" }, + { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" }, + { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" }, + { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" }, ] [[package]] @@ -4275,6 +4209,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" }, { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" }, { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" }, + { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" }, + { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" }, + { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" }, + { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" }, + { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" }, + { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" }, + { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" }, + { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" }, + { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" }, + { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" }, ] [[package]] @@ -4334,7 +4284,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "ptyprocess" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -4402,21 +4352,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, -] - -[[package]] -name = "pint" -version = "0.25.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flexcache" }, - { name = "flexparser" }, - { name = "platformdirs" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/52/9d/b1379cdbd33a49d17d627bc24e2b63cca06a1c5343b38072d2889499e82e/pint-0.25.3.tar.gz", hash = "sha256:f8f5df6cf65314d74da1ade1bf96f8e3e4d0c41b51577ac53c49e7d44ca5acee", size = 255106, upload-time = "2026-03-19T21:57:08.72Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/dd/a9fe6a0a09512da23951c68bf36466aeecd89def3183dc095edbc807ddc5/pint-0.25.3-py3-none-any.whl", hash = "sha256:27eb25143bd5de9fcc4d5a4b484f16faf6b4615aa93ece6b3373a8c1a3c1b97d", size = 307488, upload-time = "2026-03-19T21:57:07.022Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, ] [[package]] @@ -4557,6 +4517,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] @@ -4587,6 +4581,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, @@ -4597,36 +4597,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] -[[package]] -name = "psycopg2-binary" -version = "2.9.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/9f/ef4ef3c8e15083df90ca35265cfd1a081a2f0cc07bb229c6314c6af817f4/psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", size = 3712459, upload-time = "2026-04-20T23:34:30.549Z" }, - { url = "https://files.pythonhosted.org/packages/b5/01/3dd14e46ba48c1e1a6ec58ee599fa1b5efa00c246d5046cd903d0eeb1af1/psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", size = 3822936, upload-time = "2026-04-20T23:34:32.77Z" }, - { url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676, upload-time = "2026-04-20T23:34:35.18Z" }, - { url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917, upload-time = "2026-04-20T23:34:37.982Z" }, - { url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843, upload-time = "2026-04-20T23:34:40.856Z" }, - { url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556, upload-time = "2026-04-20T23:34:44.016Z" }, - { url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714, upload-time = "2026-04-20T23:34:46.233Z" }, - { url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154, upload-time = "2026-04-20T23:34:49.528Z" }, - { url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882, upload-time = "2026-04-20T23:34:51.86Z" }, - { url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298, upload-time = "2026-04-20T23:34:54.124Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a8/75f4e3e11203b590150abed2cf7794b9c9c9f7eceddae955191138b44dde/psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", size = 2757230, upload-time = "2026-04-20T23:34:56.242Z" }, - { url = "https://files.pythonhosted.org/packages/91/bb/4608c96f970f6e0c56572e87027ef4404f709382a3503e9934526d7ba051/psycopg2_binary-2.9.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", size = 3712419, upload-time = "2026-04-20T23:34:58.754Z" }, - { url = "https://files.pythonhosted.org/packages/5e/af/48f76af9d50d61cf390f8cd657b503168b089e2e9298e48465d029fcc713/psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", size = 3822990, upload-time = "2026-04-20T23:35:00.821Z" }, - { url = "https://files.pythonhosted.org/packages/7a/df/aba0f99397cd811d32e06fc0cc781f1f3ce98bc0e729cb423925085d781a/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", size = 4578696, upload-time = "2026-04-20T23:35:03.409Z" }, - { url = "https://files.pythonhosted.org/packages/95/9c/eaa74021ac4e4d5c2f83d82fc6615a63f4fe6c94dc4e94c3990427053f67/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", size = 4274982, upload-time = "2026-04-20T23:35:05.583Z" }, - { url = "https://files.pythonhosted.org/packages/35/ed/c25deff98bd26187ba48b3b250a3ffc3037c46c5b89362534a15d200e0db/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", size = 5894867, upload-time = "2026-04-20T23:35:07.902Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/8d0e21ca77373c6c9589e5c4528f6e8f0c08c62cafc76fb0bddb7a2cee22/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", size = 4110578, upload-time = "2026-04-20T23:35:10.149Z" }, - { url = "https://files.pythonhosted.org/packages/00/fc/f481e2435bd8f742d0123309174aae4165160ad3ef17c1b99c3622c241d2/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", size = 3655816, upload-time = "2026-04-20T23:35:12.56Z" }, - { url = "https://files.pythonhosted.org/packages/53/79/b9f46466bdbe9f239c96cde8be33c1aace4842f06013b47b730dc9759187/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", size = 3301307, upload-time = "2026-04-20T23:35:15.029Z" }, - { url = "https://files.pythonhosted.org/packages/3f/19/7dc003b32fe35024df89b658104f7c8538a8b2dcbde7a4e746ce929742e7/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", size = 3048968, upload-time = "2026-04-20T23:35:16.757Z" }, - { url = "https://files.pythonhosted.org/packages/91/58/2dbd7db5c604d45f4950d988506aae672a14126ec22998ced5021cbb76bb/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", size = 3351369, upload-time = "2026-04-20T23:35:18.933Z" }, - { url = "https://files.pythonhosted.org/packages/42/ee/dee8dcaad07f735824de3d6563bc67119fa6c28257b17977a8d624f02fab/psycopg2_binary-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", size = 2757347, upload-time = "2026-04-20T23:35:21.283Z" }, -] - [[package]] name = "psygnal" version = "0.15.1" @@ -4643,6 +4613,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2d/4f/3593e5adb88a188c798604aed95fbc1479f30230e7f51e8f2c770e6a3832/psygnal-0.15.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e9fca977f5335deea39aed22e31d9795983e4f243e59a7d3c4105793adb7693d", size = 885616, upload-time = "2026-01-04T16:38:30.081Z" }, { url = "https://files.pythonhosted.org/packages/58/4c/14779ed4c3a1d71fa1a9a87ecfb184ad3335dd64681067f77c1c47b14ae9/psygnal-0.15.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c85b7d05b92ccbec47c75ab8a5545eda462e81a492c82424aba5ab81a3ad89d", size = 876516, upload-time = "2026-01-04T16:38:31.422Z" }, { url = "https://files.pythonhosted.org/packages/3e/bc/4f771e3cdcde4db4023dbf36d6f0aab44e02b9de719353c22954b655e2ff/psygnal-0.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:ac0e693b29e0a429e97315a52313321855bef6140e9975b7ae78b4d93c8fbb42", size = 419172, upload-time = "2026-01-04T16:38:32.82Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2e/975bd61727578d88df62797f78390965ca7905780cf01eb59cb095a13638/psygnal-0.15.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:803fc33c4280c822c6f4b22e6c3ea7c4483e190f3cc69e69350098b3799476f3", size = 595706, upload-time = "2026-01-04T16:38:34.139Z" }, + { url = "https://files.pythonhosted.org/packages/b8/55/e487f1d91497eb75e86c3fdfef69a21b1cab24d023383dd7648b08797d6a/psygnal-0.15.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4f53b4b83355b0a785b745987fd04e59bbf169a9028ed81a68ca7e05fb76d458", size = 575133, upload-time = "2026-01-04T16:38:35.448Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2f/f286355accd0e68d3eef52e63c8b9ab6ba33ec3107177719a036b3319657/psygnal-0.15.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bcbca12190f5aa65c1f8fb04a81fa6f4463c5f5dde25cd74c3a56ceff6f37b02", size = 889565, upload-time = "2026-01-04T16:38:37.003Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dc/40c6026c88d7f9220ecc913afe0501045a512c9b82f9b7e036bb089dc287/psygnal-0.15.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1ac399566852fe4354ce26a1acbe12319232e8c2b615fe5ad1e114c547095cf6", size = 880863, upload-time = "2026-01-04T16:38:38.381Z" }, + { url = "https://files.pythonhosted.org/packages/b7/85/b4f45ec3057c473b5622fc002b3a636a698c34d3a0917a064ff5247f1984/psygnal-0.15.1-cp314-cp314-win_amd64.whl", hash = "sha256:d3a03055f331ce91d44581c71edb79938ccc133a94af2ce7ad3a18fa57ac7be5", size = 423654, upload-time = "2026-01-04T16:38:39.7Z" }, { url = "https://files.pythonhosted.org/packages/46/49/7742544684bee728ec123515d2694cee859aa2a705951a461230b00f18cc/psygnal-0.15.1-py3-none-any.whl", hash = "sha256:4221140e633e45b076953c64bcb9b41a744833527f9a037c1ca98bc270798cbf", size = 90638, upload-time = "2026-01-04T16:38:40.841Z" }, ] @@ -4699,30 +4674,45 @@ wheels = [ [[package]] name = "pyarrow" -version = "19.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7f/09/a9046344212690f0632b9c709f9bf18506522feb333c894d0de81d62341a/pyarrow-19.0.1.tar.gz", hash = "sha256:3bf266b485df66a400f282ac0b6d1b500b9d2ae73314a153dbe97d6d5cc8a99e", size = 1129437, upload-time = "2025-02-18T18:55:57.027Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b4/94e828704b050e723f67d67c3535cf7076c7432cd4cf046e4bb3b96a9c9d/pyarrow-19.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:80b2ad2b193e7d19e81008a96e313fbd53157945c7be9ac65f44f8937a55427b", size = 30670749, upload-time = "2025-02-18T18:53:00.062Z" }, - { url = "https://files.pythonhosted.org/packages/7e/3b/4692965e04bb1df55e2c314c4296f1eb12b4f3052d4cf43d29e076aedf66/pyarrow-19.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:ee8dec072569f43835932a3b10c55973593abc00936c202707a4ad06af7cb294", size = 32128007, upload-time = "2025-02-18T18:53:06.581Z" }, - { url = "https://files.pythonhosted.org/packages/22/f7/2239af706252c6582a5635c35caa17cb4d401cd74a87821ef702e3888957/pyarrow-19.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d5d1ec7ec5324b98887bdc006f4d2ce534e10e60f7ad995e7875ffa0ff9cb14", size = 41144566, upload-time = "2025-02-18T18:53:11.958Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e3/c9661b2b2849cfefddd9fd65b64e093594b231b472de08ff658f76c732b2/pyarrow-19.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ad4c0eb4e2a9aeb990af6c09e6fa0b195c8c0e7b272ecc8d4d2b6574809d34", size = 42202991, upload-time = "2025-02-18T18:53:17.678Z" }, - { url = "https://files.pythonhosted.org/packages/fe/4f/a2c0ed309167ef436674782dfee4a124570ba64299c551e38d3fdaf0a17b/pyarrow-19.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d383591f3dcbe545f6cc62daaef9c7cdfe0dff0fb9e1c8121101cabe9098cfa6", size = 40507986, upload-time = "2025-02-18T18:53:26.263Z" }, - { url = "https://files.pythonhosted.org/packages/27/2e/29bb28a7102a6f71026a9d70d1d61df926887e36ec797f2e6acfd2dd3867/pyarrow-19.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b4c4156a625f1e35d6c0b2132635a237708944eb41df5fbe7d50f20d20c17832", size = 42087026, upload-time = "2025-02-18T18:53:33.063Z" }, - { url = "https://files.pythonhosted.org/packages/16/33/2a67c0f783251106aeeee516f4806161e7b481f7d744d0d643d2f30230a5/pyarrow-19.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:5bd1618ae5e5476b7654c7b55a6364ae87686d4724538c24185bbb2952679960", size = 25250108, upload-time = "2025-02-18T18:53:38.462Z" }, - { url = "https://files.pythonhosted.org/packages/2b/8d/275c58d4b00781bd36579501a259eacc5c6dfb369be4ddeb672ceb551d2d/pyarrow-19.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e45274b20e524ae5c39d7fc1ca2aa923aab494776d2d4b316b49ec7572ca324c", size = 30653552, upload-time = "2025-02-18T18:53:44.357Z" }, - { url = "https://files.pythonhosted.org/packages/a0/9e/e6aca5cc4ef0c7aec5f8db93feb0bde08dbad8c56b9014216205d271101b/pyarrow-19.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:d9dedeaf19097a143ed6da37f04f4051aba353c95ef507764d344229b2b740ae", size = 32103413, upload-time = "2025-02-18T18:53:52.971Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fa/a7033f66e5d4f1308c7eb0dfcd2ccd70f881724eb6fd1776657fdf65458f/pyarrow-19.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ebfb5171bb5f4a52319344ebbbecc731af3f021e49318c74f33d520d31ae0c4", size = 41134869, upload-time = "2025-02-18T18:53:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/2d/92/34d2569be8e7abdc9d145c98dc410db0071ac579b92ebc30da35f500d630/pyarrow-19.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a21d39fbdb948857f67eacb5bbaaf36802de044ec36fbef7a1c8f0dd3a4ab2", size = 42192626, upload-time = "2025-02-18T18:54:06.062Z" }, - { url = "https://files.pythonhosted.org/packages/0a/1f/80c617b1084fc833804dc3309aa9d8daacd46f9ec8d736df733f15aebe2c/pyarrow-19.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:99bc1bec6d234359743b01e70d4310d0ab240c3d6b0da7e2a93663b0158616f6", size = 40496708, upload-time = "2025-02-18T18:54:12.347Z" }, - { url = "https://files.pythonhosted.org/packages/e6/90/83698fcecf939a611c8d9a78e38e7fed7792dcc4317e29e72cf8135526fb/pyarrow-19.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1b93ef2c93e77c442c979b0d596af45e4665d8b96da598db145b0fec014b9136", size = 42075728, upload-time = "2025-02-18T18:54:19.364Z" }, - { url = "https://files.pythonhosted.org/packages/40/49/2325f5c9e7a1c125c01ba0c509d400b152c972a47958768e4e35e04d13d8/pyarrow-19.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9d46e06846a41ba906ab25302cf0fd522f81aa2a85a71021826f34639ad31ef", size = 25242568, upload-time = "2025-02-18T18:54:25.846Z" }, - { url = "https://files.pythonhosted.org/packages/3f/72/135088d995a759d4d916ec4824cb19e066585b4909ebad4ab196177aa825/pyarrow-19.0.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:c0fe3dbbf054a00d1f162fda94ce236a899ca01123a798c561ba307ca38af5f0", size = 30702371, upload-time = "2025-02-18T18:54:30.665Z" }, - { url = "https://files.pythonhosted.org/packages/2e/01/00beeebd33d6bac701f20816a29d2018eba463616bbc07397fdf99ac4ce3/pyarrow-19.0.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:96606c3ba57944d128e8a8399da4812f56c7f61de8c647e3470b417f795d0ef9", size = 32116046, upload-time = "2025-02-18T18:54:35.995Z" }, - { url = "https://files.pythonhosted.org/packages/1f/c9/23b1ea718dfe967cbd986d16cf2a31fe59d015874258baae16d7ea0ccabc/pyarrow-19.0.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f04d49a6b64cf24719c080b3c2029a3a5b16417fd5fd7c4041f94233af732f3", size = 41091183, upload-time = "2025-02-18T18:54:42.662Z" }, - { url = "https://files.pythonhosted.org/packages/3a/d4/b4a3aa781a2c715520aa8ab4fe2e7fa49d33a1d4e71c8fc6ab7b5de7a3f8/pyarrow-19.0.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a9137cf7e1640dce4c190551ee69d478f7121b5c6f323553b319cac936395f6", size = 42171896, upload-time = "2025-02-18T18:54:49.808Z" }, - { url = "https://files.pythonhosted.org/packages/23/1b/716d4cd5a3cbc387c6e6745d2704c4b46654ba2668260d25c402626c5ddb/pyarrow-19.0.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7c1bca1897c28013db5e4c83944a2ab53231f541b9e0c3f4791206d0c0de389a", size = 40464851, upload-time = "2025-02-18T18:54:57.073Z" }, - { url = "https://files.pythonhosted.org/packages/ed/bd/54907846383dcc7ee28772d7e646f6c34276a17da740002a5cefe90f04f7/pyarrow-19.0.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:58d9397b2e273ef76264b45531e9d552d8ec8a6688b7390b5be44c02a37aade8", size = 42085744, upload-time = "2025-02-18T18:55:08.562Z" }, +version = "24.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/a9/9686d9f07837f91f775e8932659192e02c74f9d8920524b480b85212cc68/pyarrow-24.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", size = 34981559, upload-time = "2026-04-21T10:47:22.17Z" }, + { url = "https://files.pythonhosted.org/packages/80/b6/0ddf0e9b6ead3474ab087ae598c76b031fc45532bf6a63f3a553440fb258/pyarrow-24.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", size = 36663654, upload-time = "2026-04-21T10:47:28.315Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3b/926382efe8ce27ba729071d3566ade6dfb86bdf112f366000196b2f5780a/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", size = 45679394, upload-time = "2026-04-21T10:47:34.821Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/829f7d9dfd37c207206081d6dad474d81dde29952401f07f2ba507814818/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", size = 48863122, upload-time = "2026-04-21T10:47:42.056Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e8/f88ce625fe8babaae64e8db2d417c7653adb3019b08aae85c5ed787dc816/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", size = 49376032, upload-time = "2026-04-21T10:47:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/36/7a/82c363caa145fff88fb475da50d3bf52bb024f61917be5424c3392eaf878/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", size = 51929490, upload-time = "2026-04-21T10:47:55.981Z" }, + { url = "https://files.pythonhosted.org/packages/66/1c/e3e72c8014ad2743ca64a701652c733cc5cbcee15c0463a32a8c55518d9e/pyarrow-24.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", size = 27355660, upload-time = "2026-04-21T10:48:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a1abf004482026ddc17f4503db227787fa3cfe41ec5091ff20e4fea55e57/pyarrow-24.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", size = 34976759, upload-time = "2026-04-21T10:48:07.258Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4a/34f0a36d28a2dd32225301b79daad44e243dc1a2bb77d43b60749be255c4/pyarrow-24.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", size = 36658471, upload-time = "2026-04-21T10:48:13.347Z" }, + { url = "https://files.pythonhosted.org/packages/1f/78/543b94712ae8bb1a6023bcc1acf1a740fbff8286747c289cd9468fced2a5/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", size = 45675981, upload-time = "2026-04-21T10:48:20.201Z" }, + { url = "https://files.pythonhosted.org/packages/84/9f/8fb7c222b100d314137fa40ec050de56cd8c6d957d1cfff685ce72f15b17/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", size = 48859172, upload-time = "2026-04-21T10:48:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d3/1ea72538e6c8b3b475ed78d1049a2c518e655761ea50fe1171fc855fcab7/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", size = 49385733, upload-time = "2026-04-21T10:48:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/c3/be/c3d8b06a1ba35f2260f8e1f771abbee7d5e345c0937aab90675706b1690a/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", size = 51934335, upload-time = "2026-04-21T10:48:42.099Z" }, + { url = "https://files.pythonhosted.org/packages/9c/62/89e07a1e7329d2cde3e3c6994ba0839a24977a2beda8be6005ea3d860b99/pyarrow-24.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", size = 27271748, upload-time = "2026-04-21T10:49:42.532Z" }, + { url = "https://files.pythonhosted.org/packages/17/1a/cff3a59f80b5b1658549d46611b67163f65e0664431c076ad728bf9d5af4/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", size = 35238554, upload-time = "2026-04-21T10:48:48.526Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/cce0f42a327bfef2c420fb6078a3eb834826e5d6697bf3009fe11d2ad051/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", size = 36782301, upload-time = "2026-04-21T10:48:55.181Z" }, + { url = "https://files.pythonhosted.org/packages/2a/66/8e560d5ff6793ca29aca213c53eec0dd482dd46cb93b2819e5aab52e4252/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", size = 45721929, upload-time = "2026-04-21T10:49:03.676Z" }, + { url = "https://files.pythonhosted.org/packages/27/0c/a26e25505d030716e078d9f16eb74973cbf0b33b672884e9f9da1c83b871/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", size = 48825365, upload-time = "2026-04-21T10:49:11.714Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/771f9ecb0c65e73fe9dccdd1717901b9594f08c4515d000c7c62df573811/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", size = 49451819, upload-time = "2026-04-21T10:49:21.474Z" }, + { url = "https://files.pythonhosted.org/packages/48/da/61ae89a88732f5a785646f3ec6125dbb640fa98a540eb2b9889caa561403/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", size = 51909252, upload-time = "2026-04-21T10:49:31.164Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1a/8dd5cafab7b66573fa91c03d06d213356ad4edd71813aa75e08ce2b3a844/pyarrow-24.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", size = 27388127, upload-time = "2026-04-21T10:49:37.334Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/d022a34ff05d2cbedd8ccf841fc1f532ecfa9eb5ed1711b56d0e0ea71fc9/pyarrow-24.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838", size = 35007997, upload-time = "2026-04-21T10:49:48.796Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ff/f01485fda6f4e5d441afb8dd5e7681e4db18826c1e271852f5d3957d6a80/pyarrow-24.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b", size = 36678720, upload-time = "2026-04-21T10:49:55.858Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c2/2d2d5fea814237923f71b36495211f20b43a1576f9a4d6da7e751a64ec6f/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795", size = 45741852, upload-time = "2026-04-21T10:50:04.624Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3a/28ba9c1c1ebdbb5f1b94dfebb46f207e52e6a554b7fe4132540fde29a3a0/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26", size = 48889852, upload-time = "2026-04-21T10:50:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/df/51/4a389acfd31dca009f8fb82d7f510bb4130f2b3a8e18cf00194d0687d8ac/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde", size = 49445207, upload-time = "2026-04-21T10:50:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/0bab2b23d2ae901b1b9a03c0efd4b2d070256f8ce3fc43f6e58c167b2081/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76", size = 51954117, upload-time = "2026-04-21T10:50:29.14Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/f4e9145da0417b3d2c12035a8492b35ff4a3dbc653e614fcfb51d9dedb38/pyarrow-24.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e", size = 28001155, upload-time = "2026-04-21T10:51:22.337Z" }, + { url = "https://files.pythonhosted.org/packages/79/4f/46a49a63f43526da895b1a45bbb51d5baf8e4d77159f8528fc3e5490007f/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05", size = 35250387, upload-time = "2026-04-21T10:50:35.552Z" }, + { url = "https://files.pythonhosted.org/packages/a0/da/d5e0cd5ef00796922404806d5f00325cdadc3441ce2c13fe7115f2df9a64/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a", size = 36797102, upload-time = "2026-04-21T10:50:42.417Z" }, + { url = "https://files.pythonhosted.org/packages/34/c7/5904145b0a593a05236c882933d439b5720f0a145381179063722fbfc123/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072", size = 45745118, upload-time = "2026-04-21T10:50:49.324Z" }, + { url = "https://files.pythonhosted.org/packages/13/d3/cca42fe166d1c6e4d5b80e530b7949104d10e17508a90ae202dac205ce2a/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931", size = 48844765, upload-time = "2026-04-21T10:50:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/942c3b79878ba928324d1e17c274ed84581db8c0a749b24bcf4cbdf15bd3/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699", size = 49471890, upload-time = "2026-04-21T10:51:02.439Z" }, + { url = "https://files.pythonhosted.org/packages/76/97/ff71431000a75d84135a1ace5ca4ba11726a231a8007bbb320a4c54075d5/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136", size = 51932250, upload-time = "2026-04-21T10:51:10.576Z" }, + { url = "https://files.pythonhosted.org/packages/51/be/6f79d55816d5c22557cf27533543d5d70dfe692adfbee4b99f2760674f38/pyarrow-24.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", size = 28131282, upload-time = "2026-04-21T10:51:16.815Z" }, ] [[package]] @@ -4748,18 +4738,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/ff/30cfe8142470da3e45abe43a9842449ca0180d993320559890e2be19e4a5/pycocotools-2.0.11-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:368244f30eb8d6cae7003aa2c0831fbdf0153664a32859ec7fbceea52bfb6878", size = 474658, upload-time = "2025-12-15T22:31:24.883Z" }, { url = "https://files.pythonhosted.org/packages/bc/62/254ca92604106c7a5af3258e589e465e681fe0166f9b10f97d8ca70934d6/pycocotools-2.0.11-cp313-cp313t-win_amd64.whl", hash = "sha256:ac8aa17263e6489aa521f9fa91e959dfe0ea3a5519fde2cbf547312cdce7559e", size = 89681, upload-time = "2025-12-15T22:31:26.025Z" }, { url = "https://files.pythonhosted.org/packages/6e/f0/c019314dc122ad5e6281de420adc105abe9b59d00008f72ef3ad32b1e328/pycocotools-2.0.11-cp313-cp313t-win_arm64.whl", hash = "sha256:04480330df5013f6edd94891a0ee8294274185f1b5093d1b0f23d51778f0c0e9", size = 70520, upload-time = "2025-12-15T22:31:26.999Z" }, -] - -[[package]] -name = "pyconify" -version = "0.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fd/7f/94d424dc756a6287271cf40cf1b2a44c10e3f137bf3246a2b4a7416ca3d3/pyconify-0.2.1.tar.gz", hash = "sha256:8dd53757d9fbed41711434460932b2b5dbc25da720cd9f9a44af0187b2dfc07d", size = 22478, upload-time = "2025-02-06T13:20:53.592Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/40/50dd2e8bfec81676e4619903bd452c10dc0d8efac1533e79e67cc76759b5/pyconify-0.2.1-py3-none-any.whl", hash = "sha256:d3b53eee1f8a2d60c1d135610f42e789774dbe71c6d8af68af0a21d3b3ec9eb7", size = 19459, upload-time = "2025-02-06T13:20:51.613Z" }, + { url = "https://files.pythonhosted.org/packages/66/2b/58b35c88f2086c043ff1c87bd8e7bf36f94e84f7b01a5e00b6f5fabb92a7/pycocotools-2.0.11-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a6b13baf6bfcf881b6d6ac6e23c776f87a68304cd86e53d1d6b9afa31e363c4e", size = 169883, upload-time = "2025-12-15T22:31:28.233Z" }, + { url = "https://files.pythonhosted.org/packages/24/c0/b970eefb78746c8b4f8b3fa1b49d9f3ec4c5429ef3c5d4bbcc55abebe478/pycocotools-2.0.11-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78bae4a9de9d34c4759754a848dfb3306f9ef1c2fcb12164ffbd3d013d008321", size = 486894, upload-time = "2025-12-15T22:31:29.283Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f7/db7436820a1948d96fa9764b6026103e808840979be01246049f2c1e7f94/pycocotools-2.0.11-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d896f4310379849dfcfa7893afb0ff21f4f3cdb04ab3f61b05dd98953dd0ad", size = 483249, upload-time = "2025-12-15T22:31:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a6/a14a12c9f50c41998fdc0d31fd3755bcbce124bac9abb1d6b99d1853cafd/pycocotools-2.0.11-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:eebd723503a2eb2c8b285f56ea3be1d9f3875cd7c40d945358a428db94f14015", size = 469070, upload-time = "2025-12-15T22:31:32.821Z" }, + { url = "https://files.pythonhosted.org/packages/46/de/aa4f65ece3da8e89310a1be00cad0700170fd13f41a3aaae2712291269d5/pycocotools-2.0.11-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bd7a1e19ef56a828a94bace673372071d334a9232cd32ae3cd48845a04d45c4f", size = 475589, upload-time = "2025-12-15T22:31:34.188Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/04a30df03ae6236b369b361df0c50531d173d03678978806aa2182e02d1e/pycocotools-2.0.11-cp314-cp314t-win_amd64.whl", hash = "sha256:63026e11a56211058d0e84e8263f74cbccd5e786fac18d83fd221ecb9819fcc7", size = 93863, upload-time = "2025-12-15T22:31:35.38Z" }, + { url = "https://files.pythonhosted.org/packages/da/05/8942b640d6307a21c3ede188e8c56f07bedf246fac0e501437dbda72a350/pycocotools-2.0.11-cp314-cp314t-win_arm64.whl", hash = "sha256:8cedb8ccb97ffe9ed2c8c259234fa69f4f1e8665afe3a02caf93f6ef2952c07f", size = 72038, upload-time = "2025-12-15T22:31:36.768Z" }, ] [[package]] @@ -4786,18 +4771,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] -[[package]] -name = "pydantic-compat" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9c/7e/43400b6e0800065a982efcfa3e87c8f8d247d60ea75ca1a9d01702e050f8/pydantic_compat-0.1.2.tar.gz", hash = "sha256:c5c5bca39ca2d22cad00c02898e400e1920e5127649a8e860637f15566739373", size = 12838, upload-time = "2023-10-24T23:25:09.544Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/65/2edf586ff7b3dfc520977c6529c9b718c86ef8459ece088f1ef1f74bf1d4/pydantic_compat-0.1.2-py3-none-any.whl", hash = "sha256:37a4df48565a35aedc947f0fde5edbdff270a30836d995923287292bb59d5677", size = 13092, upload-time = "2023-10-24T23:25:08.155Z" }, -] - [[package]] name = "pydantic-core" version = "2.46.4" @@ -4837,6 +4810,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, @@ -4856,46 +4859,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/17/c1/3226e6d7f5a4f736f38ac11a6fbb262d701889802595cdb0f53a885ac2e0/pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1", size = 79526, upload-time = "2026-03-16T08:08:02.533Z" }, ] -[[package]] -name = "pydantic-settings" -version = "2.14.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, -] - -[[package]] -name = "pydantic-zarr" -version = "0.10.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "packaging" }, - { name = "pydantic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2f/57/aac9e500abe15719b7406d55871fc8787e7d0ebf83de5f98d44471139f70/pydantic_zarr-0.10.0.tar.gz", hash = "sha256:996b5b4225fb05757e7fff831ee7a75c61f54514a4c2493c7b79635bade7b415", size = 80374, upload-time = "2026-04-23T14:23:12.575Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/05/04af0ca9c5747c95591a549509ebbb0a986155015de59f1e4c984e3af9ab/pydantic_zarr-0.10.0-py3-none-any.whl", hash = "sha256:d15769dab43346b070051fdf6e83509d7f55259dd00c6d12be2400935117e230", size = 52809, upload-time = "2026-04-23T14:23:11.011Z" }, -] - -[[package]] -name = "pydot" -version = "4.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyparsing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/35/b17cb89ff865484c6a20ef46bf9d95a5f07328292578de0b295f4a6beec2/pydot-4.0.1.tar.gz", hash = "sha256:c2148f681c4a33e08bf0e26a9e5f8e4099a82e0e2a068098f32ce86577364ad5", size = 162594, upload-time = "2025-06-17T20:09:56.454Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl", hash = "sha256:869c0efadd2708c0be1f916eb669f3d664ca684bc57ffb7ecc08e70d5e93fee6", size = 37087, upload-time = "2025-06-17T20:09:55.25Z" }, -] - [[package]] name = "pygments" version = "2.20.0" @@ -4947,15 +4910,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/e6/94145d714402fd5ade00b5661f2d0ab981219e07f7db9bfa16786cdb9c04/pynndescent-0.6.0-py3-none-any.whl", hash = "sha256:dc8c74844e4c7f5cbd1e0cd6909da86fdc789e6ff4997336e344779c3d5538ef", size = 73511, upload-time = "2026-01-08T21:29:57.306Z" }, ] -[[package]] -name = "pyopengl" -version = "3.1.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/16/912b7225d56284859cd9a672827f18be43f8012f8b7b932bc4bd959a298e/pyopengl-3.1.10.tar.gz", hash = "sha256:c4a02d6866b54eb119c8e9b3fb04fa835a95ab802dd96607ab4cdb0012df8335", size = 1915580, upload-time = "2025-08-18T02:33:01.76Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/e4/1ba6f44e491c4eece978685230dde56b14d51a0365bc1b774ddaa94d14cd/pyopengl-3.1.10-py3-none-any.whl", hash = "sha256:794a943daced39300879e4e47bd94525280685f42dbb5a998d336cfff151d74f", size = 3194996, upload-time = "2025-08-18T02:32:59.902Z" }, -] - [[package]] name = "pyparsing" version = "3.3.2" @@ -4965,81 +4919,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] -[[package]] -name = "pyproject-hooks" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, -] - -[[package]] -name = "pyqt5" -version = "5.15.11" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyqt5-qt5", version = "5.15.2", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "pyqt5-qt5", version = "5.15.19", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, - { name = "pyqt5-sip" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0e/07/c9ed0bd428df6f87183fca565a79fee19fa7c88c7f00a7f011ab4379e77a/PyQt5-5.15.11.tar.gz", hash = "sha256:fda45743ebb4a27b4b1a51c6d8ef455c4c1b5d610c90d2934c7802b5c1557c52", size = 3216775, upload-time = "2024-07-19T08:39:57.756Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/64/42ec1b0bd72d87f87bde6ceb6869f444d91a2d601f2e67cd05febc0346a1/PyQt5-5.15.11-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c8b03dd9380bb13c804f0bdb0f4956067f281785b5e12303d529f0462f9afdc2", size = 6579776, upload-time = "2024-07-19T08:39:19.775Z" }, - { url = "https://files.pythonhosted.org/packages/49/f5/3fb696f4683ea45d68b7e77302eff173493ac81e43d63adb60fa760b9f91/PyQt5-5.15.11-cp38-abi3-macosx_11_0_x86_64.whl", hash = "sha256:6cd75628f6e732b1ffcfe709ab833a0716c0445d7aec8046a48d5843352becb6", size = 7016415, upload-time = "2024-07-19T08:39:32.977Z" }, - { url = "https://files.pythonhosted.org/packages/b4/8c/4065950f9d013c4b2e588fe33cf04e564c2322842d84dbcbce5ba1dc28b0/PyQt5-5.15.11-cp38-abi3-manylinux_2_17_x86_64.whl", hash = "sha256:cd672a6738d1ae33ef7d9efa8e6cb0a1525ecf53ec86da80a9e1b6ec38c8d0f1", size = 8188103, upload-time = "2024-07-19T08:39:40.561Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f0/ae5a5b4f9b826b29ea4be841b2f2d951bcf5ae1d802f3732b145b57c5355/PyQt5-5.15.11-cp38-abi3-win32.whl", hash = "sha256:76be0322ceda5deecd1708a8d628e698089a1cea80d1a49d242a6d579a40babd", size = 5433308, upload-time = "2024-07-19T08:39:46.932Z" }, - { url = "https://files.pythonhosted.org/packages/56/d5/68eb9f3d19ce65df01b6c7b7a577ad3bbc9ab3a5dd3491a4756e71838ec9/PyQt5-5.15.11-cp38-abi3-win_amd64.whl", hash = "sha256:bdde598a3bb95022131a5c9ea62e0a96bd6fb28932cc1619fd7ba211531b7517", size = 6865864, upload-time = "2024-07-19T08:39:53.572Z" }, -] - -[[package]] -name = "pyqt5-qt5" -version = "5.15.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version < '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/09/99a222b0360616250fb2e6003a54e43a2a06b0774f0f8d5daafb86a2c375/PyQt5_Qt5-5.15.2-py3-none-macosx_10_13_intel.whl", hash = "sha256:76980cd3d7ae87e3c7a33bfebfaee84448fd650bad6840471d6cae199b56e154", size = 40546019, upload-time = "2021-03-10T13:52:47.763Z" }, - { url = "https://files.pythonhosted.org/packages/83/d4/241a6a518d0bcf0a9fcdcbad5edfed18d43e884317eab8d5230a2b27e206/PyQt5_Qt5-5.15.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a", size = 59921716, upload-time = "2021-03-10T13:57:39.485Z" }, -] - -[[package]] -name = "pyqt5-qt5" -version = "5.15.19" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'win32'", - "python_full_version < '3.13' and platform_machine != 's390x' and sys_platform == 'win32'", - "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'win32'", - "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'win32'", - "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'", - "python_full_version < '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'", - "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'", - "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'", -] - -[[package]] -name = "pyqt5-sip" -version = "12.18.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/31/5ef342de9faee0f3801088946ae103db9b9eaeba3d6a64fefd5ce74df244/pyqt5_sip-12.18.0.tar.gz", hash = "sha256:71c37db75a0664325de149f43e2a712ec5fa1f90429a21dafbca005cb6767f94", size = 104143, upload-time = "2026-01-13T15:53:19.576Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/61/6d78d702016ac23d2b97634a3b6a831c3f7735f0552a1c8b058db96005d1/pyqt5_sip-12.18.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b29e4cda24748e59e5bd1bdad4812091a86b4b5b08c38b7f781eb55a5166f2b7", size = 124614, upload-time = "2026-01-13T15:52:57.59Z" }, - { url = "https://files.pythonhosted.org/packages/19/bf/8f3efa10ddd3e76c1253865340ab7c2960ef96681d732b1f666c77430612/pyqt5_sip-12.18.0-cp312-cp312-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:163c2bba5e637c2222ec17d82a2c5aa158184a191923eb7d137cf4cfa0399529", size = 339412, upload-time = "2026-01-13T15:53:00.563Z" }, - { url = "https://files.pythonhosted.org/packages/72/48/f1bcf6729d01bae6729cd790b22fd579dbe34014e8be031e6f10c5b9b2aa/pyqt5_sip-12.18.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ead5e0a64ad852ac60797989d8444a6a5bd834768536b04a07b40b2937d922f6", size = 282376, upload-time = "2026-01-13T15:52:59.172Z" }, - { url = "https://files.pythonhosted.org/packages/dd/b7/d84c764ac9f1366be561255ec9bd88ee224fefdbdb349aee250f3003f0ca/pyqt5_sip-12.18.0-cp312-cp312-win32.whl", hash = "sha256:993fe3ed9a62a92e770f32d5344e3df56c2cacf1471f01b7feaf04818a2df1c4", size = 49523, upload-time = "2026-01-13T15:53:03.068Z" }, - { url = "https://files.pythonhosted.org/packages/ab/e7/ef87178d5afa5f63be38556dc0df8af89f9bf74f2555f4dab6824c0fd150/pyqt5_sip-12.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:9b689e02e400abd1ce0a30cd6eae8eceabcf1bbba0395cb5c86e64ba74351d68", size = 58001, upload-time = "2026-01-13T15:53:02.15Z" }, - { url = "https://files.pythonhosted.org/packages/79/67/8d43d0fea10ff48ddecc8534aead8b855dc80df80653b8b1bf9e1f993063/pyqt5_sip-12.18.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9254e5dd7676b76503ba20edcc919e7ac4a97b6c70a6fb2f9dba9e13b4c60509", size = 124605, upload-time = "2026-01-13T15:53:04.991Z" }, - { url = "https://files.pythonhosted.org/packages/48/2a/b08bc8efeb49c50c6cdac11417dc2c8eaefcac2f0a6382eae7b26dc0f232/pyqt5_sip-12.18.0-cp313-cp313-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c969631ada7293a81e1012b2264a62d69a91995b517586489dfe24421b87b9af", size = 339918, upload-time = "2026-01-13T15:53:08.502Z" }, - { url = "https://files.pythonhosted.org/packages/b6/99/24f82437b2f073cf39296b7c731b6a8bc0f5207911fdd93841a0ea9abe42/pyqt5_sip-12.18.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d84ac384a63285132e67762c87681191c25e28a1df7560287ec3889d9eb223b5", size = 282088, upload-time = "2026-01-13T15:53:06.632Z" }, - { url = "https://files.pythonhosted.org/packages/3e/27/20d3924943df34361fae9c6a0489ae89d0b07571693245c61678d185e4a4/pyqt5_sip-12.18.0-cp313-cp313-win32.whl", hash = "sha256:95bba4670ecf5cba73958b85aa2087c17838a402ed251c38e68060c7665c998b", size = 49501, upload-time = "2026-01-13T15:53:11.159Z" }, - { url = "https://files.pythonhosted.org/packages/3f/36/e251623c12968730730512a9e5150430e36246afbe64894610190b896f61/pyqt5_sip-12.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:aac4adc37df2f2ac1dc259409be1900f07332d140a12c9db7c84112cef64ff59", size = 58076, upload-time = "2026-01-13T15:53:09.928Z" }, -] - [[package]] name = "pyqtgraph" version = "0.14.0" @@ -5071,6 +4950,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/6f/5173067773b5e650566f90c3356bd3a71c02349e6644222a8971c0196687/pyscipopt-6.2.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8860afb9d43c93b653c7295ef674ad1ba156c731591fb7e33b5991c1136b0dc6", size = 16207762, upload-time = "2026-05-16T15:05:26.883Z" }, { url = "https://files.pythonhosted.org/packages/57/76/59288c92b7ee914c351c6003b63249e0702d990d1fdd9ff97e65ffc1c57b/pyscipopt-6.2.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:725503ea90fd0962f1111a5b46ee3e271b7375e1b0dfda708ce223dbebaeff5e", size = 17560629, upload-time = "2026-05-16T15:05:30.298Z" }, { url = "https://files.pythonhosted.org/packages/0d/42/2b1d48d11311c2fda215c8da83f8f8cabec3b5a82936a7229919ad9141e5/pyscipopt-6.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a5621ddcb59f7816e87c3deeb1bf52f8693e016a1a29148201b712adb84956e3", size = 48215647, upload-time = "2026-05-16T15:05:36.5Z" }, + { url = "https://files.pythonhosted.org/packages/fc/af/4ddfd3802ff69ad58f44e6d96878265c4ea55cac2313aae1994611b0d6aa/pyscipopt-6.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:299dbff65d60853d545788457d02720e3693df19ade4aaba1bbe8b575239ff4b", size = 8418209, upload-time = "2026-05-16T15:05:40.166Z" }, + { url = "https://files.pythonhosted.org/packages/9a/53/61ab5575a6e29547b6d13c37d7005b3f348897b48dd6cbcd4261196f2e25/pyscipopt-6.2.1-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:8bd6fb0f2a6c8bf4ef49e524d4c022b7b539098193ce312e94d29c6254658ff6", size = 12267358, upload-time = "2026-05-16T15:05:43.17Z" }, + { url = "https://files.pythonhosted.org/packages/c9/fd/851dedcabdb6d6168eae0e2c31008d02f20cf018b05d091a5bda6f5da6a1/pyscipopt-6.2.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb7858ceececa09658c3bfa58476862255353fea52d4f512ff97160f138f92cd", size = 16171696, upload-time = "2026-05-16T15:05:46.851Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/ac663246709e6ab94b225c40e9dac863a91b1b86e12c2ea65c951f944bea/pyscipopt-6.2.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cdf0e460b834f06ea68a13e59f581f7ecbec5a466a76bbf1267ac5b4573bb52", size = 17430536, upload-time = "2026-05-16T15:05:50.267Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ca/05d80040767d1d2443ea6cd6cc42985cdd532feeaecfcc19f103c8b45a1b/pyscipopt-6.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:6aed03b621decb09b38f399773bf8cf2c707e965990b778a24d28c8cc90a0756", size = 49375214, upload-time = "2026-05-16T15:05:56.493Z" }, + { url = "https://files.pythonhosted.org/packages/05/d4/caa18165a2faf876d8d482b0b92bd75abf284af2b51b64ebee62c1b47065/pyscipopt-6.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:18c6283b8fc47929787a043204e7bb26f41e414d1e8597d394057bcc60fc3ee0", size = 8528074, upload-time = "2026-05-16T15:05:59.776Z" }, + { url = "https://files.pythonhosted.org/packages/0f/3c/f6593bf4459e788f2302878369c5eb65ec5b04fd6e18524296fe05365c6b/pyscipopt-6.2.1-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:622e9640cdfc22632079ed4f73d88653d6170ee89ca6bdb6459d0db334fa7a87", size = 12341833, upload-time = "2026-05-16T15:06:02.402Z" }, + { url = "https://files.pythonhosted.org/packages/49/db/bc80dfa03ef13701b818aca26428fe05c389a71a8ec5654e1e4ea60ef3a4/pyscipopt-6.2.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a85fb7f73fd06fad9a0358b2eb47d94dbe94ee52baf85e045beb5e216022b6c7", size = 16735540, upload-time = "2026-05-16T15:06:05.522Z" }, + { url = "https://files.pythonhosted.org/packages/58/ee/97cd080513efdedea2fcd1f9b71d02e775f5e445ab39f8f012c14e4b4c5e/pyscipopt-6.2.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d323fa83b855308587794cd0ab04ae6dd341c227202d0ddaec2761917034e972", size = 17628571, upload-time = "2026-05-16T15:06:09.608Z" }, + { url = "https://files.pythonhosted.org/packages/7b/75/e280c162ae55a5db7f9031c4b92774964a8eebef94d2d463a389c9f6294b/pyscipopt-6.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:138e312cd692b7c853e7eab572986bc6d2dd28acd6e16347d73c96c225613056", size = 49551043, upload-time = "2026-05-16T15:06:15.368Z" }, ] [[package]] @@ -5104,24 +4993,28 @@ wheels = [ ] [[package]] -name = "python-dateutil" -version = "2.9.0.post0" +name = "pytest-xdist" +version = "3.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "six" }, + { name = "execnet" }, + { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] [[package]] -name = "python-dotenv" -version = "1.2.2" +name = "python-dateutil" +version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] @@ -5209,19 +5102,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/fe/2b70276ede7878c5fe8356ca07574db5da63e222ce39a463e84bfad135e8/pywavelets-1.9.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3830e6657236b53a3aae20c735cccead942bb97c54bbca9e7d07bae01645fe9c", size = 4477528, upload-time = "2025-08-04T16:19:35.932Z" }, { url = "https://files.pythonhosted.org/packages/e7/ed/d58b540c15e36508cfeded7b0d39493e811b0dce18d9d4e6787fb2e89685/pywavelets-1.9.0-cp313-cp313t-win32.whl", hash = "sha256:81bb65facfbd7b50dec50450516e72cdc51376ecfdd46f2e945bb89d39bfb783", size = 4186493, upload-time = "2025-08-04T16:19:37.198Z" }, { url = "https://files.pythonhosted.org/packages/84/b2/12a849650d618a86bbe4d8876c7e20a7afe59a8cad6f49c57eca9af26dfa/pywavelets-1.9.0-cp313-cp313t-win_amd64.whl", hash = "sha256:47d52cf35e2afded8cfe1133663f6f67106a3220b77645476ae660ad34922cb4", size = 4274821, upload-time = "2025-08-04T16:19:38.436Z" }, -] - -[[package]] -name = "pywin32" -version = "312" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, - { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, - { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, - { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, - { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, - { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1f/18c82122547c9eec2232d800b02ada1fbd30ce2136137b5738acca9d653e/pywavelets-1.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:53043d2f3f4e55a576f51ac594fe33181e1d096d958e01524db5070eb3825306", size = 4314440, upload-time = "2025-08-04T16:19:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e1/1c92ac6b538ef5388caf1a74af61cf6af16ea6d14115bb53357469cb38d6/pywavelets-1.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bc36b42b1b125fd9cb56e7956b22f8d0f83c1093f49c77fc042135e588c799", size = 4290162, upload-time = "2025-08-04T16:19:41.322Z" }, + { url = "https://files.pythonhosted.org/packages/96/d3/d856a2cac8069c20144598fa30a43ca40b5df2e633230848a9a942faf04a/pywavelets-1.9.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08076eb9a182ddc6054ac86868fb71df6267c341635036dc63d20bdbacd9ad7e", size = 4437162, upload-time = "2025-08-04T16:19:42.556Z" }, + { url = "https://files.pythonhosted.org/packages/c9/54/777e0495acd4fb008791e84889be33d6e7fc8af095b441d939390b7d2491/pywavelets-1.9.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ee1ee7d80f88c64b8ec3b5021dd1e94545cc97f0cd479fb51aa7b10f6def08e", size = 4498169, upload-time = "2025-08-04T16:19:43.791Z" }, + { url = "https://files.pythonhosted.org/packages/76/68/81b97f4d18491a18fbe17e06e2eee80a591ce445942f7b6f522de07813c5/pywavelets-1.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3226b6f62838a6ccd7782cb7449ee5d8b9d61999506c1d9b03b2baf41b01b6fd", size = 4443318, upload-time = "2025-08-04T16:19:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/92/74/5147f2f0436f7aa131cb1bc13dba32ef5f3862748ae1c7366b4cde380362/pywavelets-1.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fb7f4b11d18e2db6dd8deee7b3ce8343d45f195f3f278c2af6e3724b1b93a24", size = 4503294, upload-time = "2025-08-04T16:19:46.632Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d4/af998cc71e869919e0ab45471bd43e91d055ac7bc3ce6f56cc792c9b6bc8/pywavelets-1.9.0-cp314-cp314-win32.whl", hash = "sha256:9902d9fc9812588ab2dce359a1307d8e7f002b53a835640e2c9388fe62a82fd4", size = 4144478, upload-time = "2025-08-04T16:19:47.974Z" }, + { url = "https://files.pythonhosted.org/packages/7d/66/1d071eae5cc3e3ad0e45334462f8ce526a79767ccb759eb851aa5b78a73a/pywavelets-1.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:7e57792bde40e331d6cc65458e5970fd814dba18cfc4e9add9d051e901a7b7c7", size = 4227186, upload-time = "2025-08-04T16:19:49.57Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1f/da0c03ac99bd9d20409c0acf6417806d4cf333d70621da9f535dd0cf27fa/pywavelets-1.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b47c72fb4b76d665c4c598a5b621b505944e5b761bf03df9d169029aafcb652f", size = 4354391, upload-time = "2025-08-04T16:19:51.221Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/de9e225d8cc307fbb4fda88aefa79442775d5e27c58ee4d3c8a8580ceba6/pywavelets-1.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:969e369899e7eab546ea5d77074e4125082e6f9dad71966499bf5dee3758be55", size = 4335810, upload-time = "2025-08-04T16:19:52.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/3b/336761359d07cd44a4233ca854704ff2a9e78d285879ccc82d254b9daa57/pywavelets-1.9.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8aeffd4f35036c1fade972a61454de5709a7a8fc9a7d177eefe3ac34d76962e5", size = 4422220, upload-time = "2025-08-04T16:19:54.068Z" }, + { url = "https://files.pythonhosted.org/packages/98/61/76ccc7ada127f14f65eda40e37407b344fd3713acfca7a94d7f0f67fe57d/pywavelets-1.9.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f63f400fcd4e7007529bd06a5886009760da35cd7e76bb6adb5a5fbee4ffeb8c", size = 4470156, upload-time = "2025-08-04T16:19:55.379Z" }, + { url = "https://files.pythonhosted.org/packages/e0/de/142ca27ee729cf64113c2560748fcf2bd45b899ff282d6f6f3c0e7f177bb/pywavelets-1.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a63bcb6b5759a7eb187aeb5e8cd316b7adab7de1f4b5a0446c9a6bcebdfc22fb", size = 4430167, upload-time = "2025-08-04T16:19:56.566Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5e/90b39adff710d698c00ba9c3125e2bec99dad7c5f1a3ba37c73a78a6689f/pywavelets-1.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9950eb7c8b942e9bfa53d87c7e45a420dcddbd835c4c5f1aca045a3f775c6113", size = 4477378, upload-time = "2025-08-04T16:19:58.162Z" }, + { url = "https://files.pythonhosted.org/packages/f1/1a/89f5f4ebcb9d34d9b7b2ac0a868c8b6d8c78d699a36f54407a060cea0566/pywavelets-1.9.0-cp314-cp314t-win32.whl", hash = "sha256:097f157e07858a1eb370e0d9c1bd11185acdece5cca10756d6c3c7b35b52771a", size = 4209132, upload-time = "2025-08-04T16:20:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/68/d2/a8065103f5e2e613b916489e6c85af6402a1ec64f346d1429e2d32cb8d03/pywavelets-1.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3b6ff6ba4f625d8c955f68c2c39b0a913776d406ab31ee4057f34ad4019fb33b", size = 4306793, upload-time = "2025-08-04T16:20:02.934Z" }, ] [[package]] @@ -5236,6 +5132,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/50/724ed5c38c504d4e58a88a072776a1e880d970789deaeb2b9f7bd9a5141a/pywinpty-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:fe1f7911805127c94cf51f89ab14096c6f91ffdcacf993d2da6082b2142a2523", size = 234591, upload-time = "2026-02-04T21:52:29.821Z" }, { url = "https://files.pythonhosted.org/packages/f7/ad/90a110538696b12b39fd8758a06d70ded899308198ad2305ac68e361126e/pywinpty-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:3f07a6cf1c1d470d284e614733c3d0f726d2c85e78508ea10a403140c3c0c18a", size = 2112360, upload-time = "2026-02-04T21:55:33.397Z" }, { url = "https://files.pythonhosted.org/packages/44/0f/7ffa221757a220402bc79fda44044c3f2cc57338d878ab7d622add6f4581/pywinpty-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:15c7c0b6f8e9d87aabbaff76468dabf6e6121332c40fc1d83548d02a9d6a3759", size = 233107, upload-time = "2026-02-04T21:51:45.455Z" }, + { url = "https://files.pythonhosted.org/packages/28/88/2ff917caff61e55f38bcdb27de06ee30597881b2cae44fbba7627be015c4/pywinpty-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:d4b6b7b0fe0cdcd02e956bd57cfe9f4e5a06514eecf3b5ae174da4f951b58be9", size = 2113282, upload-time = "2026-02-04T21:52:08.188Z" }, + { url = "https://files.pythonhosted.org/packages/63/32/40a775343ace542cc43ece3f1d1fce454021521ecac41c4c4573081c2336/pywinpty-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:34789d685fc0d547ce0c8a65e5a70e56f77d732fa6e03c8f74fefb8cbb252019", size = 234207, upload-time = "2026-02-04T21:51:58.687Z" }, + { url = "https://files.pythonhosted.org/packages/8d/54/5d5e52f4cb75028104ca6faf36c10f9692389b1986d34471663b4ebebd6d/pywinpty-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0c37e224a47a971d1a6e08649a1714dac4f63c11920780977829ed5c8cadead1", size = 2112910, upload-time = "2026-02-04T21:52:30.976Z" }, + { url = "https://files.pythonhosted.org/packages/0a/44/dcd184824e21d4620b06c7db9fbb15c3ad0a0f1fa2e6de79969fb82647ec/pywinpty-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c4e9c3dff7d86ba81937438d5819f19f385a39d8f592d4e8af67148ceb4f6ab5", size = 233425, upload-time = "2026-02-04T21:51:56.754Z" }, ] [[package]] @@ -5264,6 +5164,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] @@ -5309,37 +5227,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, -] - -[[package]] -name = "qtawesome" -version = "1.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "qtpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cf/bc/475c8df94de4f358bbf7703747c13b16ae3c0c4cda50d5243b99cb45e2a8/qtawesome-1.4.2.tar.gz", hash = "sha256:b2bf9351beb335095006892796f072ffd9755a2d7e5113dc71918dcd9ba4ef4a", size = 2614207, upload-time = "2026-04-10T18:49:46.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/25/b1/da1d826ccc9258674b26dd0abcbbb1c55cab06e7fbf2697518b67edc79fc/qtawesome-1.4.2-py3-none-any.whl", hash = "sha256:dbf08524428fa2df73918ce362153254cd44f089380576d84bfaad8f40eece45", size = 2593557, upload-time = "2026-04-10T18:49:44.219Z" }, -] - -[[package]] -name = "qtconsole" -version = "5.7.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ipykernel" }, - { name = "ipython-pygments-lexers" }, - { name = "jupyter-client" }, - { name = "jupyter-core" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "qtpy" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/28/4070eb0bacb99bc00bf60173fda25fb6a559d6600040035ba96c196d3647/qtconsole-5.7.2.tar.gz", hash = "sha256:27b485b9161925924c1d8e78e66bb342e6e3bc49bf675d0a67b49bad9c291521", size = 436661, upload-time = "2026-03-25T02:24:38.895Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/60/aba9f3c3f1f48c7fbdf03bed45b576a916cd09c08242939b5294b85e37b4/qtconsole-5.7.2-py3-none-any.whl", hash = "sha256:e1d1f6a792123363626e643a7a4ee561217773571043992693fba7eccfa89f95", size = 125883, upload-time = "2026-03-25T02:24:36.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, ] [[package]] @@ -5376,15 +5273,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/25/4511e114e3f8420a0edd9febd12de1a4a56b3259a779d7ce5d92c5dfd6ac/quilt3-7.3.0-py3-none-any.whl", hash = "sha256:585245b73ad40586af6fc5be689e1113d3e44bcb75e198b3ee9a03a036b79d07", size = 136740, upload-time = "2026-04-07T21:15:31.782Z" }, ] -[[package]] -name = "rangehttpserver" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/3b/1ec139d6028c6e5eb10301f040d6eee5c5427a4b1b4d614a2f78d3bba1bd/rangehttpserver-1.4.0.tar.gz", hash = "sha256:d5ddccee219b359598e41da0c5fbf30a2579297094f5a682755e2586388a5306", size = 6993, upload-time = "2024-08-27T18:08:43.418Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/43/d7e2b9ad768c07b5473bea3ac7db9ca4d995c09399cbea3d4df1c0bd4955/rangehttpserver-1.4.0-py2.py3-none-any.whl", hash = "sha256:2a0c6926e4341de4cc19ec861292b005e4194ff497b1eefdeccb2992a5045452", size = 7773, upload-time = "2024-08-27T18:08:41.861Z" }, -] - [[package]] name = "referencing" version = "0.37.0" @@ -5453,6 +5341,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" }, { url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" }, { url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a", size = 490376, upload-time = "2026-05-09T23:14:09.64Z" }, + { url = "https://files.pythonhosted.org/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4", size = 291964, upload-time = "2026-05-09T23:14:11.424Z" }, + { url = "https://files.pythonhosted.org/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c", size = 289682, upload-time = "2026-05-09T23:14:13.123Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9", size = 796996, upload-time = "2026-05-09T23:14:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af", size = 866089, upload-time = "2026-05-09T23:14:17.757Z" }, + { url = "https://files.pythonhosted.org/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0", size = 911530, upload-time = "2026-05-09T23:14:20.353Z" }, + { url = "https://files.pythonhosted.org/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4", size = 800643, upload-time = "2026-05-09T23:14:22.265Z" }, + { url = "https://files.pythonhosted.org/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf", size = 777223, upload-time = "2026-05-09T23:14:24.179Z" }, + { url = "https://files.pythonhosted.org/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346", size = 785760, upload-time = "2026-05-09T23:14:26.065Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676", size = 860891, upload-time = "2026-05-09T23:14:28.17Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14", size = 765891, upload-time = "2026-05-09T23:14:30.06Z" }, + { url = "https://files.pythonhosted.org/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd", size = 851380, upload-time = "2026-05-09T23:14:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e", size = 789350, upload-time = "2026-05-09T23:14:34.799Z" }, + { url = "https://files.pythonhosted.org/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad", size = 272130, upload-time = "2026-05-09T23:14:36.729Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763", size = 280999, upload-time = "2026-05-09T23:14:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372", size = 273500, upload-time = "2026-05-09T23:14:41.059Z" }, + { url = "https://files.pythonhosted.org/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499", size = 494269, upload-time = "2026-05-09T23:14:42.869Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1", size = 293954, upload-time = "2026-05-09T23:14:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d", size = 292405, upload-time = "2026-05-09T23:14:47.211Z" }, + { url = "https://files.pythonhosted.org/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c", size = 811855, upload-time = "2026-05-09T23:14:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5", size = 871189, upload-time = "2026-05-09T23:14:51.555Z" }, + { url = "https://files.pythonhosted.org/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20", size = 917485, upload-time = "2026-05-09T23:14:53.633Z" }, + { url = "https://files.pythonhosted.org/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0", size = 816369, upload-time = "2026-05-09T23:14:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d", size = 785869, upload-time = "2026-05-09T23:14:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b", size = 801427, upload-time = "2026-05-09T23:15:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a", size = 866482, upload-time = "2026-05-09T23:15:03.384Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415", size = 773022, upload-time = "2026-05-09T23:15:05.625Z" }, + { url = "https://files.pythonhosted.org/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2", size = 856642, upload-time = "2026-05-09T23:15:07.966Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41", size = 803552, upload-time = "2026-05-09T23:15:10.215Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58", size = 275746, upload-time = "2026-05-09T23:15:12.609Z" }, + { url = "https://files.pythonhosted.org/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77", size = 285685, upload-time = "2026-05-09T23:15:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" }, ] [[package]] @@ -5540,15 +5460,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/34/78/3cc84d58e13234861adf91d2897254cd84d3d4a90534d91c960b672e67d1/roifile-2026.2.10-py3-none-any.whl", hash = "sha256:5928a616b58a8ff1c9f89a0590db3616ccbf069c6692f8ecec2a7349f8bbb6ec", size = 21201, upload-time = "2026-02-11T19:54:34.137Z" }, ] -[[package]] -name = "roman-numerals" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, -] - [[package]] name = "rpds-py" version = "2026.5.1" @@ -5599,6 +5510,64 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/22/9bf80a56069c0c443fcfefac639a86a744550a2898817a6dfd3e26654924/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3", size = 585633, upload-time = "2026-05-28T12:00:15.66Z" }, { url = "https://files.pythonhosted.org/packages/da/68/3b2c0a75c9e04125696f84ebdbbf304acf5a40b58ba4481cdb98a922c3ba/rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc", size = 210074, upload-time = "2026-05-28T12:00:17.291Z" }, { url = "https://files.pythonhosted.org/packages/e7/8b/609157d5a25d37d4f29f92840ba531f416907c34ae5c5739dd21fc2bef98/rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55", size = 228635, upload-time = "2026-05-28T12:00:18.73Z" }, + { url = "https://files.pythonhosted.org/packages/d4/6f/19c1918a4b590d8de87e712e4abe4b3875771eff60216fb6153cf6665c68/rpds_py-2026.5.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1f2c391c3059798093b65df23aca2cac150460ae9c630d99dec83d703d9485b9", size = 349756, upload-time = "2026-05-28T12:00:20.217Z" }, + { url = "https://files.pythonhosted.org/packages/e5/60/a06fe7da34eca79dacbf958a2ba0c6eea85bc2b29de20080bf40f72f66fa/rpds_py-2026.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:413b424f7c4ee65ab5e5be91f5731be0f8b41a1ee2b12dfe810d716312e95a78", size = 343831, upload-time = "2026-05-28T12:00:21.711Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ec/b2333b97b90e2a6ef6ca8ad386ee284968e74bcfe113b3f1a8d9036429a9/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c595a1d9255dce0599e13130d1440ab2506654f2b50294226ee06402f8fef63", size = 375127, upload-time = "2026-05-28T12:00:23.326Z" }, + { url = "https://files.pythonhosted.org/packages/14/7f/e00aae54067f2b488c4637961d5f58204d470795fc791085fa3f15060d2e/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c27c5f6102eac8c03e7595a00827a53b271ba40a53b59ff8709170e0855ea4a", size = 379034, upload-time = "2026-05-28T12:00:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/be/cc/423999bbb8ae8dc93c77fc1d5e984ade5eb89d237d3bb884ccfa72ae2890/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c7fcf61d44cacecaf3aea542b0e053db77972a4573e7ceda16fb2b399161195", size = 490823, upload-time = "2026-05-28T12:00:26.676Z" }, + { url = "https://files.pythonhosted.org/packages/0f/aa/c671bf660f12e68d3c52ff86c7066ed1372df5a0f4f2ff584e419b8207e7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c817a189d4ee14290420e5ff051e4dd6baa13f3edf84685071dee07a6d538ee", size = 388144, upload-time = "2026-05-28T12:00:28.577Z" }, + { url = "https://files.pythonhosted.org/packages/19/c8/d63bb75b68afe77b229e3021c6031bcaf01da5db5b0e69d0d10f9ba679a7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21846aac0ed2e0589f38c12dc44e77bb64e494b771eadbcf169cba00566ba7ba", size = 371959, upload-time = "2026-05-28T12:00:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/82/35/c51122014d8274ff37dc606d60049c3db7d83da02b5b282511e5a906a9a6/rpds_py-2026.5.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b317c87a13f769a4e787819bd508aaa5d69aa09b0880de9af6d3a8a54571cdec", size = 383558, upload-time = "2026-05-28T12:00:31.764Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f9/2790cb99c136a5363acdeacf5c27c56f3de0d4118a1f48fca83404c99c89/rpds_py-2026.5.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce87129d9f2c14fa6c4a8601fb80eb4488c80d38a20cd13758ef11123e14995d", size = 402789, upload-time = "2026-05-28T12:00:33.247Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1b/e4fb584f8c75d35c38150ff6a332cda949e6f97acba1f4fd123b14ab56fe/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9cdddb6c1207d284d94fd1530adf57fbd797fe7c4b8704ba85f49414f2557e7d", size = 551405, upload-time = "2026-05-28T12:00:34.819Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f7/a6731b4216cb3793ea1af5391da240f5683dacc0d13e034fe5fc3503f240/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4e237e139f94d3c036fd28eb9f564c99055476ff4ff05cd42be55ce349b5aa02", size = 616975, upload-time = "2026-05-28T12:00:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/2e051a81d95d8e63f4b35a1c463a87e8766bc3d083c067c5dfb6bf220747/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ed0954b524873214369184a9c82b0eaa45a3fbb9a798cd95b17e0d98499e7ea0", size = 578701, upload-time = "2026-05-28T12:00:37.82Z" }, + { url = "https://files.pythonhosted.org/packages/65/56/b5f6fdb2083e32bca8a8993d89e70db114b4756c9e2c38421328126689d2/rpds_py-2026.5.1-cp314-cp314-win32.whl", hash = "sha256:2d88621d6a7d4dfa633d21abe90f280bb205274e16b1d1e61c6ad4640b2453b7", size = 209806, upload-time = "2026-05-28T12:00:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/fb/80/65a5aa96c155e611d1ed844e4e1f57f3e36b021f396d9f8585d756e6b90d/rpds_py-2026.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:cef8ac28d26f4dda3533060c20fbf80a325458fa9fd23ea72a73cdfa8e978838", size = 225985, upload-time = "2026-05-28T12:00:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/27/7c/ad185212e87b05f196daef92bc5f3caf07298eb47c295b5585c3dd3093ac/rpds_py-2026.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:eaaea962c68cdc68d4a533ba985ab8e9484277910bbfaa2ab3ef7732667bfed8", size = 221219, upload-time = "2026-05-28T12:00:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/23/58/e14ae18759020334646b031e708ab4158d653a938822bfb7b95ef2e93aa3/rpds_py-2026.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:21942f52dbbd5f8758bf021213d28bd45c39e873e65e2407faf5f1846f5761ad", size = 352148, upload-time = "2026-05-28T12:00:44.638Z" }, + { url = "https://files.pythonhosted.org/packages/31/9b/5f4a1e2f960bca3ac5d052b139dd31eed97b259f9d909173821760d542e8/rpds_py-2026.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f414556f6e3958300ff941e40c9f97e3dc9774ddd1b3434c475d73dd354bbed3", size = 345196, upload-time = "2026-05-28T12:00:46.14Z" }, + { url = "https://files.pythonhosted.org/packages/1a/71/1d9574d6a2fa20ab60eaa55c7467f5aa20cbc770f341a05f09c0876f59e2/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1013a8625c74043210190b246f5b1551e09757c1f356c6e4160ef96c5bc081", size = 374981, upload-time = "2026-05-28T12:00:47.531Z" }, + { url = "https://files.pythonhosted.org/packages/0c/9a/37e99f4915a80aa71670263c1267f7ae0af95f53a3f61e6c3bdc016d4515/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc68e231a77a5f0d774ae278a1f8e55c0456501820847c1e4efb3829f3441df6", size = 379961, upload-time = "2026-05-28T12:00:49.216Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ff/6e73f74b89d2e0715e0fc86b7dde893f9a61ae2f9b256ff3bdfe41ac4e94/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9baffb505aff33acc69b422a19f77806680f3c8632227d79f48de8a810d1c2c5", size = 495965, upload-time = "2026-05-28T12:00:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/ea/e0/425faba25f59d74d4638b267f7c7a80e8649d2ef4db10a19b0c4a71e6e6f/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8d2f912928d426e8cfa396f7f3f8d29a59e6689c86dcca3c420730c1096322b", size = 389526, upload-time = "2026-05-28T12:00:52.77Z" }, + { url = "https://files.pythonhosted.org/packages/c6/76/7a41960e3fddae47fab43a28684d5da981401dffd88253de0944148654cb/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90f628283be835db980c941767d41c9a27b5239e54ba0a9c1335247e82406964", size = 376190, upload-time = "2026-05-28T12:00:54.215Z" }, + { url = "https://files.pythonhosted.org/packages/27/60/5f38dc70824fc6951b51d35377e577a3a3a4c81a6769cc5a2de25ebe0ad1/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:1ebb2f0ab7e16132995a72de805170e0203df0c3dd22e1ef1cd1fdd90bd7a131", size = 383921, upload-time = "2026-05-28T12:00:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/60/1a/d60a38caa1505f4b9483c3fbbde12c94e1079154f4f401a6da96f7e77621/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3df3d16ded76f1f8c9cdebd0e1ea55fdf4c23b812de189814da7cf229c22a81", size = 404766, upload-time = "2026-05-28T12:00:57.518Z" }, + { url = "https://files.pythonhosted.org/packages/87/ff/602fd3f174d6425f0bce05ad0dfbec0e96b38d0f7d08a79af5aa20083885/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9af8905b8f854990e40d5206aa5ac58d9b0fe0b7f351ff2bb086c20f6c8c6a47", size = 551343, upload-time = "2026-05-28T12:00:58.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c1/1be13327acdbead3eca1fde03b6a34dbb011f1e864e217f0d32cc1779a7f/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:036a36a87fb1cd3b214d11c4b3c4f7d2ddad933625dca1c900b56a057c07740a", size = 618502, upload-time = "2026-05-28T12:01:00.656Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d7/afb49b49d7f2be8b7ba1a9f0977fa5168003437b93086726f066544e8351/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca", size = 581916, upload-time = "2026-05-28T12:01:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/dbef8c1f8a10f07beb62b5f054e20099fd9924b3ec001b8f0b6ac7813a85/rpds_py-2026.5.1-cp314-cp314t-win32.whl", hash = "sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a", size = 207855, upload-time = "2026-05-28T12:01:03.821Z" }, + { url = "https://files.pythonhosted.org/packages/2a/72/bfa4e61ab8e7dc1c8adf397e05e6cbdd4239357bd72b248d3de662f23915/rpds_py-2026.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6", size = 225422, upload-time = "2026-05-28T12:01:05.194Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/7b5da92b640f67b6717ccafc83cdd06bfa7ff2395c3685c68922bb54d703/rpds_py-2026.5.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb", size = 349576, upload-time = "2026-05-28T12:01:06.722Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8a/2aafd7ad355a1bd48ca76e2262b74b15e6432b5a1efe150efd4d779cd55d/rpds_py-2026.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291", size = 343640, upload-time = "2026-05-28T12:01:08.441Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7d/6c9523c1abbe840a1b7fba3c516d48e1d3487cc80fea4366c4071cf56784/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1", size = 375322, upload-time = "2026-05-28T12:01:09.934Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5d/0b7b03fb1dc509321f01de3149784ab773e34c8573022029af8076afcb9c/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8", size = 379066, upload-time = "2026-05-28T12:01:11.48Z" }, + { url = "https://files.pythonhosted.org/packages/d7/e2/8ef6012999ebf1cb1c22f876d9ce5e63d960fd4631d2af3202d3f480aa25/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2", size = 494586, upload-time = "2026-05-28T12:01:13.051Z" }, + { url = "https://files.pythonhosted.org/packages/80/af/1eeb029bec67582c226b7809172207cd005073af4ebd906e65ff494f4983/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038", size = 388415, upload-time = "2026-05-28T12:01:14.631Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/ffbe10711c4d766c1cab0557d6906c074f795814863c67b351355d29354a/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26", size = 372427, upload-time = "2026-05-28T12:01:16.153Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3a/30ba4a6ad457e5b070c18d742a33fb77d8d922b565cc881f8a5313d63bfe/rpds_py-2026.5.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd", size = 383615, upload-time = "2026-05-28T12:01:17.809Z" }, + { url = "https://files.pythonhosted.org/packages/d3/69/62e242b53ce39c0814bd24e1a6e6eba6c92be716277745f317f9540a2e7b/rpds_py-2026.5.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9", size = 402786, upload-time = "2026-05-28T12:01:19.419Z" }, + { url = "https://files.pythonhosted.org/packages/38/c1/a770b9c186928a1ed0f7e6d7ae50e7f3950ed23e3f9e366dbc8e38cb55de/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14", size = 551583, upload-time = "2026-05-28T12:01:21.013Z" }, + { url = "https://files.pythonhosted.org/packages/21/7c/68e8579b95375b70d2a963103c42e705856cdb98569258bd807f4423891c/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01", size = 616941, upload-time = "2026-05-28T12:01:22.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/a1/a6135aed5730ff03ab957182259987ac11e55fb392a28dc6f0592048a280/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d", size = 578349, upload-time = "2026-05-28T12:01:24.118Z" }, + { url = "https://files.pythonhosted.org/packages/09/6e/f24201a76a84e6c49d0bdfdfcb735210e21701e9b21c5bfc0ba497dd62f6/rpds_py-2026.5.1-cp315-cp315-win32.whl", hash = "sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa", size = 209922, upload-time = "2026-05-28T12:01:25.522Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e4/966bc240bb0485fc265278f6de44d05834bf0b3618886e0b22e33d54c49a/rpds_py-2026.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325", size = 226003, upload-time = "2026-05-28T12:01:27.062Z" }, + { url = "https://files.pythonhosted.org/packages/5c/5c/a15a59269cd5e74472734516c73795c15eccfc841b3d4b0228c3f53f19d0/rpds_py-2026.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16", size = 221245, upload-time = "2026-05-28T12:01:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/e0/22/135ce03804e179a71ceb13be095deda4a279bc88f7a6b8fa161c5ad44e12/rpds_py-2026.5.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723", size = 352015, upload-time = "2026-05-28T12:01:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5f/f1f6d2652eb9d848f6eb369d8db83a2da6249bb49ad2c2a48f45d54538d3/rpds_py-2026.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41", size = 345016, upload-time = "2026-05-28T12:01:31.656Z" }, + { url = "https://files.pythonhosted.org/packages/88/66/b74182775691ea2290c99e52ac8d5db844e56fbec90ce421f107658c8314/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a", size = 374775, upload-time = "2026-05-28T12:01:33.136Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8f/15e5a61d9f0a43902d36561d4f07cae6ae9f4716be825159fd72717f33af/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358", size = 380270, upload-time = "2026-05-28T12:01:34.574Z" }, + { url = "https://files.pythonhosted.org/packages/02/c3/f859b12763a80540cdf2af0f15b19904cf756a71d7bdd3f82ff3e5b1bbf9/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb", size = 495285, upload-time = "2026-05-28T12:01:36.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/ff27c2ac8411d30b03b1829fd88cae8dad1a4d0da48dd25e57c4038042e6/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b", size = 389581, upload-time = "2026-05-28T12:01:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/6e/67/fe92ee32a6cc05c77228a2f8b1762e7124f386ec20ff83d0757b762d58d0/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc", size = 376041, upload-time = "2026-05-28T12:01:39.307Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/b4d6685c27aba55bd82f25b278be8237038117d05f9659a6213ad3408130/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015", size = 383946, upload-time = "2026-05-28T12:01:41.043Z" }, + { url = "https://files.pythonhosted.org/packages/bd/79/2c1d832a53c8e0f8e98fc970ec257b950fecd4f62be2ab7182b500a0cbc8/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa", size = 405526, upload-time = "2026-05-28T12:01:43.032Z" }, + { url = "https://files.pythonhosted.org/packages/78/c4/c98117b03c6a8581ab2c2dfccfe9a5ad82bd8128a3c28b46a6ad2d97c393/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972", size = 551165, upload-time = "2026-05-28T12:01:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c1/bc479ca069200af730881b1bd525e3114b2b391a351509fcb1b772f28086/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66", size = 618778, upload-time = "2026-05-28T12:01:46.337Z" }, + { url = "https://files.pythonhosted.org/packages/77/65/38ab2f90df44c2febfb63cc10ced40763d9b4bc94d173e734528663fe7f5/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb", size = 581839, upload-time = "2026-05-28T12:01:48.109Z" }, + { url = "https://files.pythonhosted.org/packages/15/2d/ce1f605fe036aadd460e5822e578c6c7ec3a860936cca37d6e0f299daa77/rpds_py-2026.5.1-cp315-cp315t-win32.whl", hash = "sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df", size = 207866, upload-time = "2026-05-28T12:01:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/79/cb/966040123eb102371559746908ef2c9471f4d43e17ec9a645a2258dab64b/rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3", size = 225441, upload-time = "2026-05-28T12:01:51.408Z" }, ] [[package]] @@ -5623,30 +5592,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/ec/cee878c1879b91ab8dc7d564535d011307839a2fea79d2a650413edf53be/rustworkx-0.17.1-cp39-abi3-win_amd64.whl", hash = "sha256:d0a48fb62adabd549f9f02927c3a159b51bf654c7388a12fc16d45452d5703ea", size = 2055049, upload-time = "2025-08-13T01:43:44.926Z" }, ] -[[package]] -name = "s3fs" -version = "2026.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiobotocore" }, - { name = "aiohttp" }, - { name = "fsspec" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cb/d8/76f3dc1558bdf4494b117a9f7a9cc0a5d9d34edadc9e5d7ceabc5a6a7c37/s3fs-2026.4.0.tar.gz", hash = "sha256:5bdce0abb00b0435ee150807a45fea727451dbc22de4cbc116464f8504ab9d37", size = 85986, upload-time = "2026-04-29T20:52:51.748Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/a4/9d1ea10ebc9e028a289a72fec84da170689549a8102c8aacfcad26bc5035/s3fs-2026.4.0-py3-none-any.whl", hash = "sha256:de0d2a1f33cdf03831fd2382d278c6e4e31fe57c3bf2f703c61f8aec6b703e2a", size = 32392, upload-time = "2026-04-29T20:52:50.295Z" }, -] - [[package]] name = "s3transfer" -version = "0.17.1" +version = "0.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/b3/bcdc2f58fa92592db511beda154c2c08d28f21f6c4637f06a42a24b10c21/s3transfer-0.17.1.tar.gz", hash = "sha256:042dd5e3b1b512355e35a23f0223e426b7042e80b97830ea2680ddce327fc45e", size = 159439, upload-time = "2026-05-26T19:45:01.714Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/1f/12417f7f493fc45e1f9fd5d4a9b6c125cf8d2cf3f8ddbdfab3e76406e9d6/s3transfer-0.18.0.tar.gz", hash = "sha256:3760b8b7ec1315da54048b2d626276732bee4300d054d492d4e1d43e20d4ecbd", size = 160560, upload-time = "2026-05-28T19:39:09.124Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/dd/904873250a6554fbae40cddbf9198e3cc37a2f1319d5e1a5ce82fe269c17/s3transfer-0.17.1-py3-none-any.whl", hash = "sha256:5b9827d1044159bbb01b86ef8902760ea39281927f5de31de75e1d657177bf4c", size = 88264, upload-time = "2026-05-26T19:45:00.452Z" }, + { url = "https://files.pythonhosted.org/packages/2b/58/a58fc997655386daa2e25784e30c288aa3e3819e401f77029ee4899fb55a/s3transfer-0.18.0-py3-none-any.whl", hash = "sha256:239c13b09e65ad0346e1be7348b8a202dcad44ac7ea7c6eb858fc881dce739b6", size = 88572, upload-time = "2026-05-28T19:39:07.999Z" }, ] [[package]] @@ -5711,11 +5666,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/94/9f46966fa042b5d57c8cd641045372b4e0df0047dd400e77ea9952674110/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:20ef4a155e2e78b8ab973998e04d8a361d49d719e65412405f4dadd9155a61d9", size = 14359526, upload-time = "2025-12-20T17:11:41.087Z" }, { url = "https://files.pythonhosted.org/packages/5d/b4/2840fe38f10057f40b1c9f8fb98a187a370936bf144a4ac23452c5ef1baf/scikit_image-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c9087cf7d0e7f33ab5c46d2068d86d785e70b05400a891f73a13400f1e1faf6a", size = 12287629, upload-time = "2025-12-20T17:11:43.11Z" }, { url = "https://files.pythonhosted.org/packages/22/ba/73b6ca70796e71f83ab222690e35a79612f0117e5aaf167151b7d46f5f2c/scikit_image-0.26.0-cp313-cp313t-win_arm64.whl", hash = "sha256:27d58bc8b2acd351f972c6508c1b557cfed80299826080a4d803dd29c51b707e", size = 11647755, upload-time = "2025-12-20T17:11:45.279Z" }, -] - -[package.optional-dependencies] -data = [ - { name = "pooch" }, + { url = "https://files.pythonhosted.org/packages/51/44/6b744f92b37ae2833fd423cce8f806d2368859ec325a699dc30389e090b9/scikit_image-0.26.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:63af3d3a26125f796f01052052f86806da5b5e54c6abef152edb752683075a9c", size = 12365810, upload-time = "2025-12-20T17:11:47.357Z" }, + { url = "https://files.pythonhosted.org/packages/40/f5/83590d9355191f86ac663420fec741b82cc547a4afe7c4c1d986bf46e4db/scikit_image-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ce00600cd70d4562ed59f80523e18cdcc1fae0e10676498a01f73c255774aefd", size = 12075717, upload-time = "2025-12-20T17:11:49.483Z" }, + { url = "https://files.pythonhosted.org/packages/72/48/253e7cf5aee6190459fe136c614e2cbccc562deceb4af96e0863f1b8ee29/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6381edf972b32e4f54085449afde64365a57316637496c1325a736987083e2ab", size = 13161520, upload-time = "2025-12-20T17:11:51.58Z" }, + { url = "https://files.pythonhosted.org/packages/73/c3/cec6a3cbaadfdcc02bd6ff02f3abfe09eaa7f4d4e0a525a1e3a3f4bce49c/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6624a76c6085218248154cc7e1500e6b488edcd9499004dd0d35040607d7505", size = 13684340, upload-time = "2025-12-20T17:11:53.708Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0d/39a776f675d24164b3a267aa0db9f677a4cb20127660d8bf4fd7fef66817/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f775f0e420faac9c2aa6757135f4eb468fb7b70e0b67fa77a5e79be3c30ee331", size = 14203839, upload-time = "2025-12-20T17:11:55.89Z" }, + { url = "https://files.pythonhosted.org/packages/ee/25/2514df226bbcedfe9b2caafa1ba7bc87231a0c339066981b182b08340e06/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede4d6d255cc5da9faeb2f9ba7fedbc990abbc652db429f40a16b22e770bb578", size = 14770021, upload-time = "2025-12-20T17:11:58.014Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5b/0671dc91c0c79340c3fe202f0549c7d3681eb7640fe34ab68a5f090a7c7f/scikit_image-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:0660b83968c15293fd9135e8d860053ee19500d52bf55ca4fb09de595a1af650", size = 12023490, upload-time = "2025-12-20T17:12:00.013Z" }, + { url = "https://files.pythonhosted.org/packages/65/08/7c4cb59f91721f3de07719085212a0b3962e3e3f2d1818cbac4eeb1ea53e/scikit_image-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:b8d14d3181c21c11170477a42542c1addc7072a90b986675a71266ad17abc37f", size = 11473782, upload-time = "2025-12-20T17:12:01.983Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/65c4258137acef3d73cb561ac55512eacd7b30bb4f4a11474cad526bc5db/scikit_image-0.26.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cde0bbd57e6795eba83cb10f71a677f7239271121dc950bc060482834a668ad1", size = 12686060, upload-time = "2025-12-20T17:12:03.886Z" }, + { url = "https://files.pythonhosted.org/packages/e7/32/76971f8727b87f1420a962406388a50e26667c31756126444baf6668f559/scikit_image-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:163e9afb5b879562b9aeda0dd45208a35316f26cc7a3aed54fd601604e5cf46f", size = 12422628, upload-time = "2025-12-20T17:12:05.921Z" }, + { url = "https://files.pythonhosted.org/packages/37/0d/996febd39f757c40ee7b01cdb861867327e5c8e5f595a634e8201462d958/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724f79fd9b6cb6f4a37864fe09f81f9f5d5b9646b6868109e1b100d1a7019e59", size = 12962369, upload-time = "2025-12-20T17:12:07.912Z" }, + { url = "https://files.pythonhosted.org/packages/48/b4/612d354f946c9600e7dea012723c11d47e8d455384e530f6daaaeb9bf62c/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3268f13310e6857508bd87202620df996199a016a1d281b309441d227c822394", size = 13272431, upload-time = "2025-12-20T17:12:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/26c00b466e06055a086de2c6e2145fe189ccdc9a1d11ccc7de020f2591ad/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fac96a1f9b06cd771cbbb3cd96c5332f36d4efd839b1d8b053f79e5887acde62", size = 14016362, upload-time = "2025-12-20T17:12:12.793Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/00a90402e1775634043c2a0af8a3c76ad450866d9fa444efcc43b553ba2d/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c1e7bd342f43e7a97e571b3f03ba4c1293ea1a35c3f13f41efdc8a81c1dc8f2", size = 14364151, upload-time = "2025-12-20T17:12:14.909Z" }, + { url = "https://files.pythonhosted.org/packages/da/ca/918d8d306bd43beacff3b835c6d96fac0ae64c0857092f068b88db531a7c/scikit_image-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b702c3bb115e1dcf4abf5297429b5c90f2189655888cbed14921f3d26f81d3a4", size = 12413484, upload-time = "2025-12-20T17:12:17.046Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cd/4da01329b5a8d47ff7ec3c99a2b02465a8017b186027590dc7425cee0b56/scikit_image-0.26.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0608aa4a9ec39e0843de10d60edb2785a30c1c47819b67866dd223ebd149acaf", size = 11769501, upload-time = "2025-12-20T17:12:19.339Z" }, ] [[package]] @@ -5743,6 +5709,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/88/8dab5de10c638c083772a6be83a3d8106ced492f74a928c8693638e5bb50/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da76d09304a4706db7cc1e3ebaa3b6b98a67365cc11d2996c4f1e58ba47df714", size = 9087723, upload-time = "2026-06-02T11:53:50.702Z" }, { url = "https://files.pythonhosted.org/packages/20/3f/7917ca72464038f6240ec70c29f94862d08a34a74291ae4d4ec5eb8186a0/scikit_learn-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5808d98f15c6bf6d9d96d2348c1997392a5888ce7097e664105f930c4bca1277", size = 8184330, upload-time = "2026-06-02T11:53:53.396Z" }, { url = "https://files.pythonhosted.org/packages/78/c7/15739eb2f61fda3c54639e9942414e5a19ad8a8d1f5a3266afad7cb7df80/scikit_learn-1.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:d77f54c017633791bc0225a43e2f8d03745fdcfe4880268fcc4df15f505dec2e", size = 7840653, upload-time = "2026-06-02T11:53:56.035Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/c9a35cf59b20a86fec24d306f1547b78dec194b08d367ce2a3e4854169d9/scikit_learn-1.9.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9656acd4e93f74e0b66c8a36c88830a99252dfa900044d36bc2212ae89a47162", size = 8713289, upload-time = "2026-06-02T11:53:58.788Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a7/552a7821597c632b907f7bfe8f36f9f572777af8ef8a48353041cf8e091a/scikit_learn-1.9.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:24360002ae845e7866522b0a5bbf690802e7bc388cac8663502e78aa98598aa2", size = 8245141, upload-time = "2026-06-02T11:54:01.694Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/f4a0c4fe9711154cddabf913471153af79056382ddc612cfe5ee0ff4b72e/scikit_learn-1.9.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5162ad10a418c8a282dde04c9aa06965de3e9a65f33c1440c0ae69bb1a09d913", size = 8847671, upload-time = "2026-06-02T11:54:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/f0/af/4d72d9e475ac83719160c662619e4bf7b95c19507cd582e7d0167a3c3dae/scikit_learn-1.9.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fea2cc5677ab49d6f5bade978c866da44957b712d92e9635e8b4f723013c3cb", size = 9118104, upload-time = "2026-06-02T11:54:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/a2/d5/6a58eea2cb9abbb9b3f2bb8b2cfb3243d1152d69f442d256c7af71304769/scikit_learn-1.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:64fa347efc1c839c487433e40c5144d38c336e8a2b59c81aa8660373945c2673", size = 8290674, upload-time = "2026-06-02T11:54:10.087Z" }, + { url = "https://files.pythonhosted.org/packages/65/5b/d4c879cf358f1187141cf90ced473f087183489090244f50c124a2ee478b/scikit_learn-1.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:1b944b6db288f6b926e3650026ddafb988929de95d11fc2cc5fa117773c9ba42", size = 7978807, upload-time = "2026-06-02T11:54:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/8a/43/bfae3121ec67ae09150d453c442c7c1cc166e9aefe056e6ab3b7728a5cfc/scikit_learn-1.9.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4ccacf04ca5f4b492158a5f28afe0ace43f81b2571e4b9a66d34848b46128949", size = 9031941, upload-time = "2026-06-02T11:54:15.436Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/20a4546eb17f3b25d3c66df15810411c14ed5065bcfab50b53c96fb627b2/scikit_learn-1.9.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ee1a8db2c18c08e34c7412d4b10be1cac214cd4ea7dc9715a6a327eb49a37c96", size = 8613528, upload-time = "2026-06-02T11:54:18.842Z" }, + { url = "https://files.pythonhosted.org/packages/18/3c/e440e039bb82cd19004edaaad00acbde0fb9b461083c3ecf37941c557312/scikit_learn-1.9.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:147e9329ef0e39f75d4cffa02b2aa48d827832684926cd5210d9a2cb5c57246b", size = 8855050, upload-time = "2026-06-02T11:54:21.699Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/b341b8dab5998da6270a3a42c2152c578501354d36f944b5856757035ef8/scikit_learn-1.9.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bad8f8b9950321b54c965fdcbac6c6c55e79e16646b49977bcf3668d3870a1a", size = 9097190, upload-time = "2026-06-02T11:54:24.454Z" }, + { url = "https://files.pythonhosted.org/packages/fb/de/b650b4d69b84468cfa2e28a3ff7b8103743029e6446ce1a97fe060ef688c/scikit_learn-1.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:78fc56eafd4edb9575d2d8950d1dd152061abb573341a1cb7e099fc40f6c6666", size = 8963204, upload-time = "2026-06-02T11:54:27.428Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/ff83d76d7418112e5a61326443cdda87be3545dd8d6599c95b2481a4419e/scikit_learn-1.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:051075bda8b7aab87b1906ab3d4740a1e1224a19d7b3781a576736edc94e76aa", size = 8222661, upload-time = "2026-06-02T11:54:30.192Z" }, ] [[package]] @@ -5784,6 +5762,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, ] [[package]] @@ -5883,15 +5881,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, ] -[[package]] -name = "snowballstemmer" -version = "3.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/f8/0a71edf031f03c40db17503cb8ca78a69a171254e568e7db241b0ab57ea1/snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260", size = 123314, upload-time = "2026-06-03T00:56:40.194Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752", size = 104164, upload-time = "2026-06-03T00:56:38.614Z" }, -] - [[package]] name = "sortedcontainers" version = "2.4.0" @@ -5910,104 +5899,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, ] -[[package]] -name = "spatial-graph" -version = "0.0.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ct3" }, - { name = "numpy" }, - { name = "setuptools" }, - { name = "typing-extensions" }, - { name = "witty" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/d9/0b1014a2d8c280801e86a1b61716f5987edd78c5bee1c914e35ae51dd0dc/spatial_graph-0.0.6.tar.gz", hash = "sha256:5e605ebc6940040db6405f259c729d2bf76f9bec9c3d0a081ef111e9c33cc812", size = 51898, upload-time = "2026-06-09T16:58:47.514Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/f5/f765c87da4a63ac6fc61364cf32cf07bbf4b5b27501ee4e50b5e54d0fcd9/spatial_graph-0.0.6-py3-none-any.whl", hash = "sha256:dda770061facb7c900fd6fc77c9eb00c8ffa93aed70ca32c98f925029b688ffa", size = 48034, upload-time = "2026-06-09T16:58:46.471Z" }, -] - -[[package]] -name = "sphinx" -version = "9.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils" }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "roman-numerals" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, -] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, -] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, -] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, -] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, -] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, -] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, -] - [[package]] name = "sqlalchemy" version = "2.0.50" @@ -6032,6 +5923,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/f6/6809349130a2de0e109e7f00fd7d431da9565b9b2868b32ee684754f672b/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2b9dcc43afef8ac157cd92fce96985d6b8b0cfbd3df4d666f66b4d55a75d202f", size = 3269375, upload-time = "2026-05-24T20:17:20.34Z" }, { url = "https://files.pythonhosted.org/packages/48/84/278a811ef4e07be9c89dc5cdd7be833268509a66a68c4897cf585e67428f/sqlalchemy-2.0.50-cp313-cp313-win32.whl", hash = "sha256:60922d6599065ddca2c6f376b9aa2f41a6b85a271725e0909490bbc50b1998a5", size = 2117229, upload-time = "2026-05-24T19:50:08.215Z" }, { url = "https://files.pythonhosted.org/packages/f6/1c/067cc6187ed32d2ec222fe6d2643acc1659a6d0659f8a7cbc5ad3ae83280/sqlalchemy-2.0.50-cp313-cp313-win_amd64.whl", hash = "sha256:287086e67275a212c4582d166a6fb03a65ccc5551d80866270ce0dd9f34eccd3", size = 2143126, upload-time = "2026-05-24T19:50:09.691Z" }, + { url = "https://files.pythonhosted.org/packages/df/32/10ac51b4be7cdecd7e93d069251c86dfbf70b7adbd7c67b48ccea6c49e1c/sqlalchemy-2.0.50-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c966932507a4d7d0a37314927dbfcd89720e3f37d2a1e3352e7ae7939fa8e8a0", size = 2158519, upload-time = "2026-05-24T19:27:56.472Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/e703d2f7681d7d66c4c891af3f07c7ccf4c76ad7f18351de035b5eda007a/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:faffef4bcc20a1892e65e155293d99d60855bbbc79250ab712819cfd56a8e6bb", size = 3282063, upload-time = "2026-05-24T20:09:38.57Z" }, + { url = "https://files.pythonhosted.org/packages/31/26/ef168b184a25701f9995e8fb7e503fafd7a99c1c77cda1bc1a26ea2ed486/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c206aec519a2e7bd08abbfb33436e325fd22c632d9c21a9047e376ce241646e", size = 3287069, upload-time = "2026-05-24T20:17:21.942Z" }, + { url = "https://files.pythonhosted.org/packages/c2/15/765acc2bc693bccc43ca4a95d5b69750da8aaf6db1b5c616536e087f8920/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bef4ac756363227ef6402a75fee025a4bc690f92328e825868939b3b3a446a6d", size = 3230453, upload-time = "2026-05-24T20:09:40.398Z" }, + { url = "https://files.pythonhosted.org/packages/63/61/08e03c3adbf5db0087a0b6816746fec8f3032fb2f7fc899a9bb9b2a48ce4/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96fbee6b19c19cd1556c8bf9419447cf2ec149ffcab7ab64348c23e54ef8547f", size = 3252413, upload-time = "2026-05-24T20:17:24.067Z" }, + { url = "https://files.pythonhosted.org/packages/03/0c/370a1f2db38436c615e10134c8a37de3688e74084792380695f3f5083860/sqlalchemy-2.0.50-cp314-cp314-win32.whl", hash = "sha256:8f00e3eb43ba30eb1b238ee03a8a62309486d1321eda3328bb611e0340033ad8", size = 2120063, upload-time = "2026-05-24T19:50:11.08Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a0/fe92bb9817863bc13ba093bda931979a26cc2ca69f8e8f26d07add3d7c6f/sqlalchemy-2.0.50-cp314-cp314-win_amd64.whl", hash = "sha256:15708c613cd5005b7dffe1f66ee6a63ee8f5e46799f71c70ebad74178c676a39", size = 2145830, upload-time = "2026-05-24T19:50:12.452Z" }, + { url = "https://files.pythonhosted.org/packages/cc/ff/e5640a98a0b2f491eb8fde10fb6c773621a2e44340de231fafcc9370f4a9/sqlalchemy-2.0.50-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3699dac4be410e97049a1658e9480da9cde956594aa0f3aebc60b88f21c5ba70", size = 2178435, upload-time = "2026-05-24T19:42:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/b7/85/337116e186f1236375b5fb70c21cfac98e8e8ab0d3a47be838dc47a59e08/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f96233858e3df43932ac11589e22520da6e8aeb624b03fedfeebb0e8ea213086", size = 3566059, upload-time = "2026-05-24T20:01:20.848Z" }, + { url = "https://files.pythonhosted.org/packages/96/34/bb0e190e161c3c2c24314a65add57218be14a4a9486886b7f5047c1ff7c8/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4e70c46fad30c3bcc6a4708bc0130a3173e11a5b25f0ea4a9d8911b450f1f52", size = 3535366, upload-time = "2026-05-24T20:03:56.768Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/a7f759f97e4fd499c5d4e4488c760d5a7fbecf3028b465a04274fcd52384/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1918a3cf564d16d95bca7301005f41ab2ad50b07cd3b9da50d3ed986db148d6a", size = 3474879, upload-time = "2026-05-24T20:01:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d9/2907ea38eb60687d297bf9c39e5ee58053c87b57fe8a9cae97090cecbf10/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b00098cdbdbd38c7be3d568b0c9c3122b8c0ec62b911b57cd5e6e0254d60a76d", size = 3486117, upload-time = "2026-05-24T20:03:59.052Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e3/5aa06f167559f8c0bdae487e297d23ba548150ab016a3418265d617a4985/sqlalchemy-2.0.50-cp314-cp314t-win32.whl", hash = "sha256:1fbd55a969d7ac44a98e3dec75016074f809fa08f871585ace58dde110d1bf3e", size = 2150823, upload-time = "2026-05-24T20:08:58.644Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/112fb8f977582d7489d036e409e3723948bcf5320b3ac465f3c481bbe8f9/sqlalchemy-2.0.50-cp314-cp314t-win_amd64.whl", hash = "sha256:c5c3cdb753a9004183e1ccb634b41611654c989e61bc68617ce878e46d6f1e51", size = 2185794, upload-time = "2026-05-24T20:09:00.319Z" }, { url = "https://files.pythonhosted.org/packages/d0/10/f7220e9b784d295d241c86ed99aeb537f92afcd469a64861f2717e9bb077/sqlalchemy-2.0.50-py3-none-any.whl", hash = "sha256:92064363517a3ff8212b5a93b8c62876579d8dfd1ca5b561335f30152d884fa9", size = 1943861, upload-time = "2026-05-24T19:59:01.119Z" }, ] @@ -6049,19 +5954,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] -[[package]] -name = "starlette" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, -] - [[package]] name = "statsmodels" version = "0.14.6" @@ -6087,25 +5979,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/b9/fd41f1f6af13a1a1212a06bb377b17762feaa6d656947bf666f76300fc05/statsmodels-0.14.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:730f3297b26749b216a06e4327fe0be59b8d05f7d594fb6caff4287b69654589", size = 10324155, upload-time = "2025-12-05T23:12:01.805Z" }, { url = "https://files.pythonhosted.org/packages/ee/0f/a6900e220abd2c69cd0a07e3ad26c71984be6061415a60e0f17b152ecf08/statsmodels-0.14.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f1c08befa85e93acc992b72a390ddb7bd876190f1360e61d10cf43833463bc9c", size = 10349765, upload-time = "2025-12-05T23:12:18.018Z" }, { url = "https://files.pythonhosted.org/packages/98/08/b79f0c614f38e566eebbdcff90c0bcacf3c6ba7a5bbb12183c09c29ca400/statsmodels-0.14.6-cp313-cp313-win_amd64.whl", hash = "sha256:8021271a79f35b842c02a1794465a651a9d06ec2080f76ebc3b7adce77d08233", size = 9540043, upload-time = "2025-12-05T23:12:33.887Z" }, -] - -[[package]] -name = "superqt" -version = "0.8.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pygments" }, - { name = "qtpy" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/84/fe/919245507b15b4633cd40292d69c3b6afa8a650bf3ec33f3329d26314a3c/superqt-0.8.2.tar.gz", hash = "sha256:faa3bd00ef1c9b209b1f31b154dd41c596fec3824f61a70c251fb5569acd7ba6", size = 110190, upload-time = "2026-05-18T14:33:47.027Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/01/b9/748c3cbcdba20ef01c055a35905cad5c771cc1df29be11b9a82da6fd8e0d/superqt-0.8.2-py3-none-any.whl", hash = "sha256:ce8400ae7b577d090368aae5bdbabb06cd6bc893f6ca73485bb686835bf20943", size = 101595, upload-time = "2026-05-18T14:33:45.462Z" }, -] - -[package.optional-dependencies] -iconify = [ - { name = "pyconify" }, + { url = "https://files.pythonhosted.org/packages/71/de/09540e870318e0c7b58316561d417be45eff731263b4234fdd2eee3511a8/statsmodels-0.14.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:00781869991f8f02ad3610da6627fd26ebe262210287beb59761982a8fa88cae", size = 10069403, upload-time = "2025-12-05T23:12:48.424Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f0/63c1bfda75dc53cee858006e1f46bd6d6f883853bea1b97949d0087766ca/statsmodels-0.14.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:73f305fbf31607b35ce919fae636ab8b80d175328ed38fdc6f354e813b86ee37", size = 9989253, upload-time = "2025-12-05T23:13:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/c1/98/b0dfb4f542b2033a3341aa5f1bdd97024230a4ad3670c5b0839d54e3dcab/statsmodels-0.14.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e443e7077a6e2d3faeea72f5a92c9f12c63722686eb80bb40a0f04e4a7e267ad", size = 10090802, upload-time = "2025-12-05T23:13:20.653Z" }, + { url = "https://files.pythonhosted.org/packages/34/0e/2408735aca9e764643196212f9069912100151414dd617d39ffc72d77eee/statsmodels-0.14.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3414e40c073d725007a6603a18247ab7af3467e1af4a5e5a24e4c27bc26673b4", size = 10337587, upload-time = "2025-12-05T23:13:37.597Z" }, + { url = "https://files.pythonhosted.org/packages/0f/36/4d44f7035ab3c0b2b6a4c4ebb98dedf36246ccbc1b3e2f51ebcd7ac83abb/statsmodels-0.14.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a518d3f9889ef920116f9fa56d0338069e110f823926356946dae83bc9e33e19", size = 10363350, upload-time = "2025-12-05T23:13:53.08Z" }, + { url = "https://files.pythonhosted.org/packages/26/33/f1652d0c59fa51de18492ee2345b65372550501ad061daa38f950be390b6/statsmodels-0.14.6-cp314-cp314-win_amd64.whl", hash = "sha256:151b73e29f01fe619dbce7f66d61a356e9d1fe5e906529b78807df9189c37721", size = 9588010, upload-time = "2025-12-05T23:14:07.28Z" }, ] [[package]] @@ -6206,6 +6085,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/b6/d574e2b758631563861d51cba4cc595d27a3965db3473a05ab268eead05b/tensordict-0.12.4-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:6e60888bc24990ead02d52f16fa607af8c01c92089ad767540eca88ade5fb49f", size = 535213, upload-time = "2026-05-22T00:09:39.561Z" }, { url = "https://files.pythonhosted.org/packages/13/a4/25c29e653878e58ed3cb111146e4dd8cdb4cfd4b6f66dd2080f94f8e78f4/tensordict-0.12.4-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:031c70d2101376e0fb8036b017c8271a27892c1b9ba6aea021c039c7535aac53", size = 539088, upload-time = "2026-05-22T00:09:41.377Z" }, { url = "https://files.pythonhosted.org/packages/35/6f/c8107ea679a60e7584bc6d36b854879a33f5a990819e174a7ed653edb781/tensordict-0.12.4-cp313-cp313t-win_amd64.whl", hash = "sha256:a1320ea2ed9e0289209b0efc51b8bf2bca02cf5273fade3aec4f60a4ddfed61b", size = 597644, upload-time = "2026-05-22T00:09:42.922Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f7/ceeacc76ed53e3bc7ba428ee35fb42dad6fdcda4974d5685b89c89906ff5/tensordict-0.12.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d52e2425ef8d509966819a8da240ed2681f685aa0ba61c155641d994587b398c", size = 890261, upload-time = "2026-05-22T00:09:44.823Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/eec984a88be0a19c538556186dd6fa937d8d39d581348e40894246272fc8/tensordict-0.12.4-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:4b3bb6b1120c2c3759abc814d1cd5f38353a1488790d8e4fc1c11e7f0e36b4a9", size = 534472, upload-time = "2026-05-22T00:09:46.336Z" }, + { url = "https://files.pythonhosted.org/packages/a6/94/610a0fc9f62a573b24e21b2a2cf230cf896e2a7e54ddffbaddfd95e2d2d1/tensordict-0.12.4-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ca594b8d67ffa899716c91922dcfcc9d7fa56ca332d43804076a6a7b54cce078", size = 537785, upload-time = "2026-05-22T00:09:47.932Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4a/b49f6d1ef76d5b51ba45d7855b3845939006a823761fddf8ca6d9436e9a9/tensordict-0.12.4-cp314-cp314-win_amd64.whl", hash = "sha256:413a7d8f05e9c38fef564d73a754e878d3a771de9b46d5b62a12af7a9cde15e9", size = 587017, upload-time = "2026-05-22T00:09:49.452Z" }, + { url = "https://files.pythonhosted.org/packages/0f/58/e2bcdedf16e7693109d74ebdd642c45698e7aa79612f49e6d60e67c013c6/tensordict-0.12.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:9f0fa20d70b54afbdf8fba6da573e27c12637aad40c44262f9b864eee3c49188", size = 894689, upload-time = "2026-05-22T00:09:51.914Z" }, + { url = "https://files.pythonhosted.org/packages/b8/62/f04b168b747fd0b7feae2553b91c10cf85a833643a3d44973e775b098878/tensordict-0.12.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:e8a61b6396d57cb60cc6dca5e3e3879a3afa993adbcce438a0af75e22783bb84", size = 534805, upload-time = "2026-05-22T00:09:53.571Z" }, + { url = "https://files.pythonhosted.org/packages/92/73/f60905b7847fe915d667cde140afae0a853be4a928510ac7d702daaa4cb1/tensordict-0.12.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:fbe824052ba1d3fafe1ea53e27a3ac097a398bb1842aacdd27bf7e4715cc8df1", size = 538514, upload-time = "2026-05-22T00:09:55.313Z" }, + { url = "https://files.pythonhosted.org/packages/af/47/b814dc48e74a2c2335985652800809ef84579f7c827fc73583c308fdca56/tensordict-0.12.4-cp314-cp314t-win_amd64.whl", hash = "sha256:113c22d41b8d791cb1e3ec875c2a7e516cc271917de90d00b77d3102b029fc89", size = 597638, upload-time = "2026-05-22T00:09:56.843Z" }, ] [[package]] @@ -6228,6 +6115,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/28/03e46405ba7c616e7a1ec5425a8f4a1b3f4d6ec2be359cb2f248199849e4/tensorstore-0.1.84-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50afb06c57a509091015af6a85da6f483a7f5ad0372284dd95d5513d877336e4", size = 19344890, upload-time = "2026-05-16T06:17:28.956Z" }, { url = "https://files.pythonhosted.org/packages/5e/c6/82669e70cef67c803852285ba6f59d7e3d102983c0ab4be8269c14756677/tensorstore-0.1.84-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ea53a851ea86aad3d99c14a790c85468d6324be14c7ac211f1f0265e8fab707", size = 20968230, upload-time = "2026-05-16T06:17:31.374Z" }, { url = "https://files.pythonhosted.org/packages/a5/12/97d8ad183e3130e168f2feb860edd68f1b72e57f29268d980f3b70e34cd0/tensorstore-0.1.84-cp313-cp313-win_amd64.whl", hash = "sha256:fe9bf1c7fef69884a91222179550f9b5ba6c1454f9534429221824d9b15c00ec", size = 13398858, upload-time = "2026-05-16T06:17:33.658Z" }, + { url = "https://files.pythonhosted.org/packages/39/bd/fda828e7915ddb61704a6f4568b5b7ce9fe607c33b7535cf51b4fd900b38/tensorstore-0.1.84-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97e767a64415297f019dd1e4b9af6a23d72e126e2a2d05cf41083253abe81428", size = 16576451, upload-time = "2026-05-16T06:17:35.993Z" }, + { url = "https://files.pythonhosted.org/packages/8a/99/2e72fcb19404de43f9412880c542a8ef8651bd30183c85454d6ca14ebe56/tensorstore-0.1.84-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7d01775985fcaa2b0f100349766a953c5086e92e746bf395e936151d4d8f9ac", size = 14907896, upload-time = "2026-05-16T06:17:38.458Z" }, + { url = "https://files.pythonhosted.org/packages/25/d4/9a3964cdfdc5a15df6d0485694de9684c13990a5b0f5d88bfa365e0a2936/tensorstore-0.1.84-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b2ff5d5536a8a9b1596c51b9075cc9d40b4c4ea4e6cc03c0480111dbe5d956d", size = 19347021, upload-time = "2026-05-16T06:17:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/43/19/70532cb2bf2f6fc3bf252f850bfb528b26eeb9c30c3cafffb075cbb7c77a/tensorstore-0.1.84-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c9108ae6c29adc90b72ca267ba2b577386c5e410ea2f8e87eabce5ebdad327e", size = 20970345, upload-time = "2026-05-16T06:17:43.874Z" }, + { url = "https://files.pythonhosted.org/packages/4e/22/a523e7576c83a6c35bd1415e5f4530b0f1e448c099d7e22684f55792755c/tensorstore-0.1.84-cp314-cp314-win_amd64.whl", hash = "sha256:4096220f4b9a2411c3751597dd8ced2f671d7a217575613c915191e19d5ea150", size = 13787429, upload-time = "2026-05-16T06:17:46.135Z" }, + { url = "https://files.pythonhosted.org/packages/2b/23/40b0a0a91973431770ac09a8b69039bd7a051169f6707b63a1c51ad36782/tensorstore-0.1.84-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:70284a7225d94adad5989de7f9238638ec9fec2bdd8c0fdb86d567f16d59e615", size = 16652804, upload-time = "2026-05-16T06:17:49.226Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/2ea3b37864adb2e9f6e724c6959ab2b7f56aa4dad01964d4b32adf211e68/tensorstore-0.1.84-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98e10cae5b3fc0828967b8ddf36242b3b26ac8bc79880bc3e36346063259212a", size = 14990904, upload-time = "2026-05-16T06:17:51.464Z" }, + { url = "https://files.pythonhosted.org/packages/ff/26/fe72887d7b91e832bef3033d244c4c548993d1b6fb19177dff3895659c12/tensorstore-0.1.84-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98edbc57d453ba8ebcf0d19bc28e4de6de95922bee2eca0805955b0833b8ce26", size = 19365698, upload-time = "2026-05-16T06:17:53.748Z" }, + { url = "https://files.pythonhosted.org/packages/37/74/35a1d41343f86f6e2ef135e81f6b8107b9f16c777a3e8be9e3fbce541d18/tensorstore-0.1.84-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bd20de85c1b83dd3ca94db24e7bd449bdb055590a1b162b05691f6b81fff00f", size = 20986107, upload-time = "2026-05-16T06:17:56.571Z" }, ] [[package]] @@ -6319,15 +6215,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, ] -[[package]] -name = "toml" -version = "0.10.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, -] - [[package]] name = "tomli" version = "2.4.1" @@ -6352,18 +6239,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] -[[package]] -name = "tomli-w" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, -] - [[package]] name = "toolz" version = "1.1.0" @@ -6407,6 +6303,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/07/055d06d985b445d67422d25b033c11cf55bbb81785d4c4e68e28bca5820e/torch-2.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af68dbf403439cae9ceaeaaf92f8352b460787dcd27b92aa05c40dd4a19c0f1e", size = 426397656, upload-time = "2026-05-13T14:52:38.84Z" }, { url = "https://files.pythonhosted.org/packages/43/94/b0b4fdc3014122e0a7302fb90086d352aa48f2576f0b252561ebb38c01a8/torch-2.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a6a2eebb237d3b1d9ad3b378e86d9b9e0782afdea8b1e0eba6a13646b9b49c07", size = 532183124, upload-time = "2026-05-13T14:53:16.178Z" }, { url = "https://files.pythonhosted.org/packages/d8/c8/052405e6ad05d3237bfe5a4df78f917773956f8e17813a2d44c059068b74/torch-2.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2140e373e9a51a3e22ef62e8d14366d0b470d18f0adf19fdc757368077133a34", size = 123232462, upload-time = "2026-05-13T14:52:27.26Z" }, + { url = "https://files.pythonhosted.org/packages/67/dc/ac069f8d6e8be701535921141055293b0d4819d3d7f224a4612cf157c7f9/torch-2.12.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f7dfae4a519197dfa050e98d8e36378a0fb5899625a875c2b54445005a2e404e", size = 88027282, upload-time = "2026-05-13T14:53:05.258Z" }, + { url = "https://files.pythonhosted.org/packages/33/c3/1c1eb00e34555b536dddf792676026a988d710ed36981aa00499b36b0620/torch-2.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:891c769072637c74e9a5a77a3bc782894696d8ffec83b938df8536dee7f0ba78", size = 426386961, upload-time = "2026-05-13T14:51:28.406Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d4/7e730dba0c7032a4154dc9056b76cf9625515e030e269cfbf8098fcfee7d/torch-2.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e2ad3eb85d39c3cab62dfa93ed5a73516e6a53c6713cb97d004004fe089f0f1f", size = 532272265, upload-time = "2026-05-13T14:51:59.308Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b4/92c80d1bbfee1c0036c06d1d2155a3065bd2423134c83bf8a47e65cd6b9b/torch-2.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:c66696857e987efb8bc1777a37357ec4f60ab5e8af6250b83d6034437fa2d8f3", size = 122987138, upload-time = "2026-05-13T14:51:45.942Z" }, + { url = "https://files.pythonhosted.org/packages/7b/78/2e12b37ce50a19a037d7bc62d652a5a8f27385a7b05859d6bc9204f20cfe/torch-2.12.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:b4556715c8572758625d62b6e0ae3b1f76c440221913a6fb5e100f321fb4fb02", size = 88320100, upload-time = "2026-05-13T14:51:39.955Z" }, + { url = "https://files.pythonhosted.org/packages/56/5e/83c450ec7b0bb40a7b74611c1b5440f9260e33c54c90d556fd4a1f0fd955/torch-2.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a43ac605a5e13116c72b64c359644cce0229f213dde48d2ae0ae5eb5becf7feb", size = 426391871, upload-time = "2026-05-13T14:52:14.989Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e9/1a0b575d98d0afedd8f157d23fa3d2759421483660448e60d0a4b10b6daa/torch-2.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6a7512adfdd7f6732e40de1c620831e3c75b39b98cef60b11d0c5f0a76473ec5", size = 532192241, upload-time = "2026-05-13T14:51:07.795Z" }, + { url = "https://files.pythonhosted.org/packages/88/21/afadd25ecd81b3cea1e11c73cf1ab41a983a50271548c3ec7ec3b9efc3e9/torch-2.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f96b63f8287f66a005dd1b5a6abba2920f11156c5e5c4d815f3e2050fd1aa16", size = 123231092, upload-time = "2026-05-13T14:51:18.854Z" }, ] [[package]] @@ -6471,6 +6375,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/b9/9dbdf76b2b49a75ba8088df6f7c755bdb520afb6c6dbac0102b46cde5e99/torchvision-0.27.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:1c01f0d1091ae22b9dfc082b0a0fe5faaf053686a29b4fb082ba7691375c73cf", size = 7791430, upload-time = "2026-05-13T14:56:56.206Z" }, { url = "https://files.pythonhosted.org/packages/5c/6a/e4a16cf2f3310c2ea7760dc5d9054496844391e0f4c1fae87fefac2f3d9e/torchvision-0.27.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dadea3c5ecfd05bbb2a3312ab0374f213c58bf6459cb059122e2f4dfe13d10ed", size = 7668441, upload-time = "2026-05-13T14:57:02.127Z" }, { url = "https://files.pythonhosted.org/packages/00/70/01b6461117a6a94b5af3f8ee166bb0f045056f3cf187750c110dabfdfffa/torchvision-0.27.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a49e55055a39a8506fe7e59850522cab004efb2c3839f6057658889c1d69c815", size = 4141602, upload-time = "2026-05-13T14:56:53.449Z" }, + { url = "https://files.pythonhosted.org/packages/92/22/c0633677b3b3f3e69554a21ac087bf705f829c40cd5e3783507b8c006681/torchvision-0.27.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:c1fac0fc2a7adf29481fc1938a0e7845c57ba1147a986784109c4d98f434ea8c", size = 1758818, upload-time = "2026-05-13T14:56:54.988Z" }, + { url = "https://files.pythonhosted.org/packages/48/e8/55f9d9667b56dae470e69e31beac9b00d458ea393feec1aae95cc4f3f1c9/torchvision-0.27.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:cbf89764fc76f3f17fbf80c12d5a89c691e91cb9d82c38412aaf0568655ffb19", size = 7789667, upload-time = "2026-05-13T14:56:48.858Z" }, + { url = "https://files.pythonhosted.org/packages/00/bc/6f8681daf3bbc4c315bb0005110f99d28e3ecd675bf9c8f2c0d393fbac7a/torchvision-0.27.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:91f61b9865423037c327eb56afa207cc72de874e458c361840db9dcf5ce0c0eb", size = 7579848, upload-time = "2026-05-13T14:56:38.209Z" }, + { url = "https://files.pythonhosted.org/packages/19/6c/8d8020e6bd1e46c53e487c9c4e9457a07f2ee28931028fb5d71e2da40adc/torchvision-0.27.0-cp314-cp314-win_amd64.whl", hash = "sha256:5bb82fc3c55daf1788621e504310b0a286f1069627a8742f692aebb075ef25a7", size = 4119284, upload-time = "2026-05-13T14:56:46.625Z" }, + { url = "https://files.pythonhosted.org/packages/8d/7e/e78c48662a8d551606efdbe11c6b9c1d6d2391b92cd0e4591b9e6a2412b8/torchvision-0.27.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:2c4099a15150143b9b034730b404a56d572efe0b79489b4c765d929cb4eac7f3", size = 1758828, upload-time = "2026-05-13T14:56:52.293Z" }, + { url = "https://files.pythonhosted.org/packages/21/dd/d03ee9f9ee7bf11a8c7c776fb8e7fd6102f59c013791a2a4e5175bd6cba7/torchvision-0.27.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b4c6bb0a670dcba017b3643e21902c9b8a1cc1c127d602f1488fa29ec3c6e865", size = 7790618, upload-time = "2026-05-13T14:56:44.721Z" }, + { url = "https://files.pythonhosted.org/packages/39/08/4002336a74742be70728603ec1769feb2b55e0d19c532c9ec9f92008de76/torchvision-0.27.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1c2db4bde82bc48ebff73436a6adf34d4f809448268a70d9a1285f5c8f92313d", size = 7580217, upload-time = "2026-05-13T14:56:43.274Z" }, + { url = "https://files.pythonhosted.org/packages/ed/cb/4dd4783eb3565f526ba6e64b6f6ca26c00eacc924cdfe60455db9d91b84b/torchvision-0.27.0-cp314-cp314t-win_amd64.whl", hash = "sha256:72bf547e58ddb948689734eed6f4b6a2031f979dba4fb08e3690688b392e929f", size = 4226392, upload-time = "2026-05-13T14:56:40.235Z" }, ] [[package]] @@ -6504,8 +6416,8 @@ wheels = [ [[package]] name = "tracksdata" -version = "0.1.0rc3.dev3+gb605d2d6a" -source = { git = "https://github.com/royerlab/tracksdata?rev=b605d2d6aa4bd12a31c07b499f5acf8411bac518#b605d2d6aa4bd12a31c07b499f5acf8411bac518" } +version = "0.1.0rc4" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bidict" }, { name = "blosc2" }, @@ -6524,6 +6436,11 @@ dependencies = [ { name = "sqlalchemy" }, { name = "tqdm" }, { name = "typing-extensions" }, + { name = "zarr" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/54/87f53a7cb255d1d76e6340b4799a84cfcedcf83aaa76926836236a7af3f8/tracksdata-0.1.0rc4.tar.gz", hash = "sha256:7f1f66293369ab1cf9de1884ba302682e494babca138d21d04e90cc7ae4538bc", size = 223977, upload-time = "2026-05-29T00:36:47.365Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/19/c2f4e1f8f347b4d8fb1266d65461d012a8b2b0baddec23814749ee01df8a/tracksdata-0.1.0rc4-py3-none-any.whl", hash = "sha256:0db3477549b8324647aa86d0a76983ae4aee9e6a8943fbb4187169c66e08e6dc", size = 253578, upload-time = "2026-05-29T00:36:45.876Z" }, ] [[package]] @@ -6566,6 +6483,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/b1/b7507bb9815d403927c8dd51d4158ed2e11751a92dbc118a044f247b6848/triton-3.7.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a35d7afe3f3f058e7ec49fcce09794049e0ffc5c59019ac25ec3413741b8c4e7", size = 201453566, upload-time = "2026-05-07T18:46:20.427Z" }, { url = "https://files.pythonhosted.org/packages/a6/8f/0bea7a6a0c989315c9135a1d7fb37e41905cfb3a17cbc1f10044ebd4cc3a/triton-3.7.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc1d61c172d257db80ddf42595131fb196ad2e9bdd751e90fe2ef13531734e8b", size = 188612899, upload-time = "2026-05-07T19:05:24.955Z" }, { url = "https://files.pythonhosted.org/packages/e1/02/d96f57828d0912aec733b9bc7e0e7dbfd2c6f079a8fa433ac25cb93d1a30/triton-3.7.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70fb9bbdc9f400afc54bbf6eb2670af28829a6ae3996863317964783141daf56", size = 201553816, upload-time = "2026-05-07T18:46:27.49Z" }, + { url = "https://files.pythonhosted.org/packages/40/fb/82a802dac4689f2a2fb2e69302e6a138eecc3e175bbe976ba3cfc717683a/triton-3.7.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a44a8476d0d3571eac4e4d1048e1ff75aad81a09ff4602ccfc56c6dea1672e", size = 188507879, upload-time = "2026-05-07T19:05:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/8f/af/9904ec6d3c93d9b24e5ec360445bbdf758b7f00bfbeedb89cb0eb64eb8bb/triton-3.7.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9b85e72968a9d8bba5ddb24e9b64aaabaf48affb042f2755cb7cfa92b7531ce", size = 201460637, upload-time = "2026-05-07T18:46:34.749Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f9/4835a8ea746b88727d8899f4e3ccce4f9cacb38abfc3bb0a638266c53111/triton-3.7.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18a160de426fd99f92b0baf509045360afbd3bfaa0b4a5171dde800ec9f09684", size = 188608706, upload-time = "2026-05-07T19:05:39.218Z" }, + { url = "https://files.pythonhosted.org/packages/c1/68/fa86e5a39608000f645535b2c124920126327ab731f8c4fafd5b07ff8d4b/triton-3.7.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce061073102714b725f3660ec6939d94a1da7984b3aa99c921417cae273672f5", size = 201546766, upload-time = "2026-05-07T18:46:42.088Z" }, ] [[package]] @@ -6626,131 +6547,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, ] -[[package]] -name = "ultrack" -source = { editable = "../../../../../home/eduardo.hirata/repos/ultrack" } -dependencies = [ - { name = "blosc2" }, - { name = "click" }, - { name = "cloudpickle" }, - { name = "edt" }, - { name = "fastapi" }, - { name = "fasteners" }, - { name = "geff" }, - { name = "gurobipy" }, - { name = "higra" }, - { name = "httpx" }, - { name = "imagecodecs" }, - { name = "imageio" }, - { name = "magicgui" }, - { name = "mip" }, - { name = "napari" }, - { name = "numba" }, - { name = "numcodecs" }, - { name = "ome-zarr" }, - { name = "pandas" }, - { name = "pillow" }, - { name = "psycopg2-binary" }, - { name = "psygnal" }, - { name = "pyarrow" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pydot" }, - { name = "qtawesome" }, - { name = "qtpy" }, - { name = "rich" }, - { name = "scikit-image" }, - { name = "seaborn" }, - { name = "sqlalchemy" }, - { name = "toml" }, - { name = "torch" }, - { name = "urllib3" }, - { name = "uvicorn" }, - { name = "websockets" }, - { name = "zarr" }, -] - -[package.metadata] -requires-dist = [ - { name = "asv", marker = "extra == 'test'", specifier = ">=0.5.1" }, - { name = "autodoc-pydantic", marker = "extra == 'docs'" }, - { name = "blosc2", specifier = ">=2.2.0" }, - { name = "catboost", marker = "extra == 'ml'", specifier = ">=1.2.7" }, - { name = "click", specifier = ">=8.1.3" }, - { name = "cloudpickle", specifier = ">=3.0.0" }, - { name = "edt", specifier = ">=2.3.2" }, - { name = "fastapi", specifier = ">=0.109.2" }, - { name = "fasteners", specifier = ">=0.20" }, - { name = "furo", marker = "extra == 'docs'" }, - { name = "geff", specifier = ">=1.0.0" }, - { name = "gurobipy", specifier = ">=9.0.0,!=12.0.0" }, - { name = "higra", specifier = ">=0.6.10" }, - { name = "httpx", specifier = ">=0.26.0" }, - { name = "imagecodecs", specifier = ">=2023.3.16" }, - { name = "imageio", specifier = ">=2.28.0" }, - { name = "magicgui", specifier = ">=0.7.2" }, - { name = "mip", specifier = ">=1.16rc0" }, - { name = "myst-parser", marker = "extra == 'docs'", specifier = ">=2.0.0" }, - { name = "napari", specifier = ">=0.4.18" }, - { name = "napari", extras = ["testing"], marker = "extra == 'test'", specifier = ">0.4.18" }, - { name = "nbsphinx", marker = "extra == 'docs'", specifier = ">=0.9.3" }, - { name = "numba", specifier = ">=0.57.0" }, - { name = "numcodecs" }, - { name = "ome-zarr", specifier = ">=0.9.0" }, - { name = "pandas", specifier = ">=2.0.1" }, - { name = "pillow", specifier = ">=10.0.0" }, - { name = "pre-commit", marker = "extra == 'test'", specifier = ">=3.2.2" }, - { name = "psycopg2-binary", specifier = ">=2.9.6" }, - { name = "psygnal", specifier = ">=0.9.0" }, - { name = "pyarrow", specifier = ">=16.1.0,<20" }, - { name = "pydantic", specifier = ">=2.11.0" }, - { name = "pydantic-settings" }, - { name = "pydot", specifier = ">=2.0.0" }, - { name = "pyqt5", marker = "extra == 'test'", specifier = ">=5.15.4" }, - { name = "pytest", marker = "extra == 'test'", specifier = ">=7.3.1" }, - { name = "pytest-qt", marker = "extra == 'test'", specifier = ">=4.2.0" }, - { name = "pytrackmate", marker = "extra == 'test'", specifier = ">=1.3.4" }, - { name = "qtawesome", specifier = ">=1.3.1" }, - { name = "qtpy" }, - { name = "rich", specifier = ">=13.3.5" }, - { name = "scikit-image", specifier = ">=0.23.0" }, - { name = "scikit-learn", marker = "extra == 'ml'", specifier = ">=1.6.0" }, - { name = "seaborn", specifier = ">=0.13.0" }, - { name = "sphinx", marker = "extra == 'docs'", specifier = "==6.2.1" }, - { name = "sphinx-click", marker = "extra == 'docs'", specifier = ">=5.0.1,<6.0.0" }, - { name = "sphinx-copybutton", marker = "extra == 'docs'" }, - { name = "sphinx-gallery", marker = "extra == 'docs'", specifier = "==0.15.0" }, - { name = "sphinxcontrib-applehelp", marker = "extra == 'docs'", specifier = "==1.0.8" }, - { name = "sqlalchemy", specifier = ">=2.0.0" }, - { name = "toml", specifier = ">=0.10.2" }, - { name = "torch", specifier = ">=2.0.1" }, - { name = "urllib3" }, - { name = "uvicorn", specifier = ">=0.27.0.post1" }, - { name = "websockets", specifier = ">=12.0" }, - { name = "zarr", specifier = ">=3.0.0" }, -] -provides-extras = ["docs", "ml", "test"] - -[[package]] -name = "ultrack-td" -version = "0.0.1" -source = { editable = "../../../../../home/eduardo.hirata/repos/ultrack-td" } -dependencies = [ - { name = "numpy" }, - { name = "tracksdata" }, -] - -[package.metadata] -requires-dist = [ - { name = "edt", marker = "extra == 'test'" }, - { name = "numpy", specifier = ">=1.24.4" }, - { name = "pytest", marker = "extra == 'test'" }, - { name = "rustworkx", marker = "extra == 'test'" }, - { name = "tracksdata", specifier = ">=0.1.0rc0" }, - { name = "ultrack", marker = "extra == 'compatibility'" }, -] -provides-extras = ["compatibility", "test"] - [[package]] name = "umap-learn" version = "0.5.12" @@ -6786,19 +6582,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] -[[package]] -name = "uvicorn" -version = "0.49.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376, upload-time = "2026-06-03T22:01:29.037Z" }, -] - [[package]] name = "verspec" version = "0.1.0" @@ -6824,6 +6607,7 @@ dev = [ { name = "jupyterlab" }, { name = "pytest" }, { name = "pytest-cov" }, + { name = "pytest-xdist" }, ] doc = [ { name = "mike" }, @@ -6837,6 +6621,7 @@ jupyter = [ test = [ { name = "pytest" }, { name = "pytest-cov" }, + { name = "pytest-xdist" }, ] [package.metadata] @@ -6853,6 +6638,7 @@ dev = [ { name = "jupyterlab", specifier = ">=4.5.3" }, { name = "pytest", specifier = ">=9.0.2" }, { name = "pytest-cov", specifier = ">=7" }, + { name = "pytest-xdist", specifier = ">=3.8" }, ] doc = [ { name = "mike", git = "https://github.com/squidfunk/mike.git" }, @@ -6866,6 +6652,7 @@ jupyter = [ test = [ { name = "pytest", specifier = ">=9.0.2" }, { name = "pytest-cov", specifier = ">=7" }, + { name = "pytest-xdist", specifier = ">=3.8" }, ] [[package]] @@ -6974,6 +6761,7 @@ dependencies = [ [package.optional-dependencies] celldiff = [ { name = "diffusers" }, + { name = "scipy" }, { name = "torchdiffeq" }, ] @@ -6993,6 +6781,7 @@ requires-dist = [ { name = "monai", specifier = ">=1.5.2" }, { name = "numpy", specifier = ">=2.4.1" }, { name = "pytorch-metric-learning" }, + { name = "scipy", marker = "extra == 'celldiff'" }, { name = "timm", specifier = ">=1.0.15" }, { name = "torch", specifier = ">=2.10" }, { name = "torchdiffeq", marker = "extra == 'celldiff'" }, @@ -7132,6 +6921,7 @@ dependencies = [ { name = "tensorboard" }, { name = "tensorstore" }, { name = "torch" }, + { name = "torchvision" }, { name = "viscy-data" }, { name = "wandb" }, { name = "xarray" }, @@ -7198,10 +6988,11 @@ requires-dist = [ { name = "tensorboard" }, { name = "tensorstore" }, { name = "torch", specifier = ">=2.10" }, + { name = "torchvision" }, { name = "umap-learn", marker = "extra == 'all'" }, { name = "umap-learn", marker = "extra == 'eval'" }, { name = "viscy-data", editable = "packages/viscy-data" }, - { name = "wandb" }, + { name = "wandb", specifier = ">=0.12.10" }, { name = "xarray" }, ] provides-extras = ["all", "anndata", "eval"] @@ -7222,31 +7013,6 @@ test = [ { name = "scikit-learn" }, ] -[[package]] -name = "vispy" -version = "0.15.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "freetype-py" }, - { name = "hsluv" }, - { name = "kiwisolver" }, - { name = "numpy" }, - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1b/2e/ef2697f963111cf1bc83568bbe55f262b7c7c8a72948a6e802a7c236f2c1/vispy-0.15.2.tar.gz", hash = "sha256:d52d10c0697f48990555cea2a2bad3f9f5a772391856fda364ea4bbc69fd075c", size = 2513383, upload-time = "2025-05-19T13:26:41.015Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/7e/31102425ea26bafab27bb5a675b499c57d29e72b1cc25fe3ae8facdd374e/vispy-0.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c8cbc22bc789f238423abadfcc3ba7589b85729be6b179b321720b511012fbee", size = 1478919, upload-time = "2025-05-19T13:26:16.962Z" }, - { url = "https://files.pythonhosted.org/packages/8d/63/a601b8f1e8e418d3821d7b4a465c2eb6695ab8eccadfce886b99ffdfe92a/vispy-0.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bf995b86dcc1eab265fa2bf9a8a5ca5a225b86c3ed4ac63900f0c5e90d5e8100", size = 1471915, upload-time = "2025-05-19T13:26:18.441Z" }, - { url = "https://files.pythonhosted.org/packages/79/1f/ca0deb4a876148c0fa515bf03d03e1dfad4553be8e6f2ab51433c074310b/vispy-0.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0aa64fcab1cd1730a10a1c9188e9cef8aefe6d099a46f2f87e2458bcef719918", size = 1866848, upload-time = "2025-05-19T13:26:19.6Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a4/dc6f335e54877f5d26ad4e4aac8f49b2d6e7719dee3e08f363d882c8aba4/vispy-0.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9d38228cedd23876cb2359ff568d523a97284d225259d450d5e5e2129e98eb1", size = 1870433, upload-time = "2025-05-19T13:26:20.794Z" }, - { url = "https://files.pythonhosted.org/packages/a8/73/5ffa34d7300c35e8423d51789d594d35eaa7256d210f9909afa9fdd0aa37/vispy-0.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:a84531c1a89f8b9d3992eb0d0ab74f84884f49d1f46a8be3755c809d7507cb01", size = 1468068, upload-time = "2025-05-19T13:26:22.073Z" }, - { url = "https://files.pythonhosted.org/packages/85/9a/6664b7f0d28e0008a28111bae847a1fa3eedd409458bf01ca45a6e99da6f/vispy-0.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ac46aab46e208b919dcf0bb26869bcd083a0a1a4927bcaf41631ba38c247639", size = 1477880, upload-time = "2025-05-19T13:26:23.321Z" }, - { url = "https://files.pythonhosted.org/packages/cf/6f/b9b36f841c5ff7320764f64822e79df3fea8a2c92270cda7f3a634d9a031/vispy-0.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7e197a012503850a77d47177964d572edab964b2a8ea0f9d998c35b81325256c", size = 1471035, upload-time = "2025-05-19T13:26:24.554Z" }, - { url = "https://files.pythonhosted.org/packages/4c/af/b892a3c9b4be29755ce4d1fc17ecb8249446bdd0e17bb5b2a9cb39bcf0f4/vispy-0.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d9eff397ecdbaf0052baae0563caa9e276272d6dc78fbaab3f5e51dbf5f7f92", size = 1860412, upload-time = "2025-05-19T13:26:26.212Z" }, - { url = "https://files.pythonhosted.org/packages/e8/13/8997d96bdc0a81f331dfd41b368935d79e8ea2917840266567e6dc40d684/vispy-0.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f13ee0cb95bbff6d7b235e1a8f96e620eee15ccb6d5ad74902427ab7e56dc8ab", size = 1865860, upload-time = "2025-05-19T13:26:27.928Z" }, - { url = "https://files.pythonhosted.org/packages/6e/37/abb30db1853b69aed4c32813cf312f301ec3641b8846193be9a6d892d607/vispy-0.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:f5955821e9b452980490706648a702da8cb2b82e762b6d6589e5872118301b84", size = 1467882, upload-time = "2025-05-19T13:26:29.608Z" }, -] - [[package]] name = "wandb" version = "0.27.0" @@ -7360,33 +7126,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, ] -[[package]] -name = "websockets" -version = "16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, - { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, - { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, - { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, - { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, - { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, - { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, - { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, - { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, - { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, - { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, - { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, - { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, - { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, -] - [[package]] name = "werkzeug" version = "3.1.8" @@ -7414,20 +7153,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl", hash = "sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366", size = 2196503, upload-time = "2025-11-01T21:15:53.565Z" }, ] -[[package]] -name = "witty" -version = "0.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cython" }, - { name = "nanobind" }, - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/79/55/8235436e9664dc824c07e9d50b4ff68088a7c3a53d4c971558616cb0fe63/witty-0.3.1.tar.gz", hash = "sha256:8ee71ba3321c77b64fd9d3a33c2f9ed2424cd7798ee22a4519687c2a88e0f108", size = 12198, upload-time = "2025-07-25T14:59:09.615Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/5e/82387eb68d7c9de328ae6ad0dbd12e3a6b71425f831f87e05738de509971/witty-0.3.1-py3-none-any.whl", hash = "sha256:e9bb379e06888a6857472f39e865e66ebb9b1523eb4725772f0b71f338e7dea6", size = 8845, upload-time = "2025-07-25T14:59:08.38Z" }, -] - [[package]] name = "wrapt" version = "2.2.1" @@ -7467,6 +7192,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/dc/b927ee9c7fc67adc3a5658f246a0d275425eb840ba36e7b702e70f18bde8/wrapt-2.2.1-cp313-cp313t-win32.whl", hash = "sha256:9907a4402ab6db12b7077a0ea5d7a4d028ecb22c8eee2b53527080d347cd1562", size = 79448, upload-time = "2026-05-22T14:48:47.901Z" }, { url = "https://files.pythonhosted.org/packages/ec/b3/fd30b473fe498c70e6b9a5f328b8d3fbaf1b8c3c481465f59724bba8eb70/wrapt-2.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:5590d63f5243251641cf543009b4c9314a79d0598fdb8a8e4cfc918494536c53", size = 83021, upload-time = "2026-05-22T14:48:49.201Z" }, { url = "https://files.pythonhosted.org/packages/ee/f3/96c39153a8737a6e9aa85adef254ac4195bea3f2d24efc60472ccc3c9e2e/wrapt-2.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:c318a64b53d97b841d7b5e637517e50a27be64bc695128422953d4b21710954e", size = 80295, upload-time = "2026-05-22T14:48:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a3/11d7f34ebbf3231bc907a3e6d5ee051b14d034c1bc7b65a97d5cc00516df/wrapt-2.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f56a647e4eaf5f0ca40330fb070f566bdf9f7b0db89a1af20d71c28dcd7a0ab", size = 80879, upload-time = "2026-05-22T14:48:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/13/3c/b74cfd984cef560b900fb1a727af20352d89e1f06bf2e1114dd3f00f5f5a/wrapt-2.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:64b7deeda4b70408e382328d8bbe52a256fe9bc63ae3db86d804608367e5422c", size = 81462, upload-time = "2026-05-22T14:48:53.18Z" }, + { url = "https://files.pythonhosted.org/packages/15/a3/7c8f704b8dc07dfe0a5d01c2edbfd88317aa8e5e3fa7c743eb7a085ae767/wrapt-2.2.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9cf53ba90717db2e292401de290776c498d4bbfb0d4a559ca2895db8b9dcb5c", size = 167251, upload-time = "2026-05-22T14:48:54.562Z" }, + { url = "https://files.pythonhosted.org/packages/80/85/a34d1888d97247da6c2ff6118c3a721c73ed8cc4dd198c00208bb73b6f80/wrapt-2.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf3638274ab9d9b724c9baa0b4c04e132cd6faefb78b4dd3dd1a02a4bdaad41e", size = 166316, upload-time = "2026-05-22T14:48:56.065Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d7/72ffaeb01eebc704afe3fb99e840480f4bda45f0fa66e3381b6a39251c8f/wrapt-2.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aed9658797d0b45d6c49adcfc6b41f66e6f2d0c6de3ec79e16cf4b1855df240f", size = 157952, upload-time = "2026-05-22T14:48:57.924Z" }, + { url = "https://files.pythonhosted.org/packages/24/5b/36f5d6b024e4edfdd90b140742d11ebcf7836daf5c9daf326c55c24db412/wrapt-2.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1d676ee388bc42a04d56dd7deb5605244dac2e35cc2fadbb43c9fa25bbd93508", size = 166130, upload-time = "2026-05-22T14:48:59.384Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/9296d9e97bfdef5483dfcc859d57b095b257144b2bc5300ab521e06f4bc7/wrapt-2.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e395f7bc31851ef9b612050368cb446e9bc14cd7454b025018980349caf25ae5", size = 156604, upload-time = "2026-05-22T14:49:00.921Z" }, + { url = "https://files.pythonhosted.org/packages/53/37/16953929ed6776175720e58fc966e779926d8d71e2c7b2273230590ca71f/wrapt-2.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f1845c2a8cc1180ccccfa45785dd06f562730d19ef75be180334254012b6283", size = 166007, upload-time = "2026-05-22T14:49:02.332Z" }, + { url = "https://files.pythonhosted.org/packages/b9/73/20ee58c0612dae7c31131a7095345812ed2c7b389019e175f68cde34e5b4/wrapt-2.2.1-cp314-cp314-win32.whl", hash = "sha256:436addbc4bb4fc0a88c702577f51195d7d73683a7f3e0e5b253d8404d7847243", size = 78327, upload-time = "2026-05-22T14:49:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/22/b3/ef7c3295d02e0448a71c639a36a057f46d524d057c9486291a7a3039e65c/wrapt-2.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:50972a1d974ea07725a7f6b1cec5f8759008afd030a0024843ebe7d52de47f2b", size = 81144, upload-time = "2026-05-22T14:49:05.093Z" }, + { url = "https://files.pythonhosted.org/packages/ac/dc/7bdf336953f99f4ceb0a584bb8870e42c8f26f93ea10c87834dad62f1668/wrapt-2.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:1c9934ea5d92957e3cd0adbc0845539dccfd62710ebe16195a8c66c53954db36", size = 79569, upload-time = "2026-05-22T14:49:06.413Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6d/6dfae80150ff1919c356d1dd528f049bcdfaae29b4d284bc957e022caef4/wrapt-2.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17de18fc12cea55b8a9587314cb830573e37fb33b247a7515696350863714188", size = 82892, upload-time = "2026-05-22T14:49:07.925Z" }, + { url = "https://files.pythonhosted.org/packages/82/7b/4e34766a7d7804ffce9e71befe47e9b3225dc350c49c94493c4ab39fd3a5/wrapt-2.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9dec1aca52dddde7df94818310fa2fe79739c8f385b2014c4cb1035f5508199", size = 83333, upload-time = "2026-05-22T14:49:09.257Z" }, + { url = "https://files.pythonhosted.org/packages/9d/57/0b34db3e8de44ccfece62d7b337abd1631dd810f5adc5f3db571727836b5/wrapt-2.2.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:69f2e9244542cb34dd59c7f073445b9e54ad9f3fce8d93606c368a1b499fc413", size = 202899, upload-time = "2026-05-22T14:49:10.572Z" }, + { url = "https://files.pythonhosted.org/packages/e5/45/ac0c459f154b99d92789a6cba7ca727185b83513b986f8ec7fe2aacddcbf/wrapt-2.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d83966dc7f4f45e8b97b5933685ac2e6e67fc0e19246ea314bceb9a8970c956", size = 209986, upload-time = "2026-05-22T14:49:12.229Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/77e37ff33ad018fa81ade52c25fa327b80b56f81d734279a63614fcb4cbc/wrapt-2.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78b0aa6bfb7be8deed0ab23e7aa028cc5210c29bc2d32a04d52b50e517a7307e", size = 194893, upload-time = "2026-05-22T14:49:14.139Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9d/7ea651d1ab032fc5fa222fbec91d0f8a1397f6ae04ebb93fa7219aa921d7/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:05d5cb74d1b232ec8cfa130a8f900708699ff2491d97b8f85a4cdc5996294b85", size = 205636, upload-time = "2026-05-22T14:49:15.714Z" }, + { url = "https://files.pythonhosted.org/packages/09/af/8e88031a701275b9085c54e64bc88c0b1cd55c77eadd400691c371cd76c4/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f6518b94edb9150452e9aba08027d4cc293433753ec1fbefb4629a21cbc74181", size = 192267, upload-time = "2026-05-22T14:49:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a8/e657ca876b06710194f243d81c4b0896ade646e244bdbec2d87c8c56a8bd/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ed55af48b3eb28f43228ca2306788892bcb629eb2b5c4876e2a3659872c2f17a", size = 198378, upload-time = "2026-05-22T14:49:18.785Z" }, + { url = "https://files.pythonhosted.org/packages/c8/59/822efe4ea722a3961331bfa35b7d90937790d2c20f0616de1997ccc3aebd/wrapt-2.2.1-cp314-cp314t-win32.whl", hash = "sha256:2e08688ab16525897da6589d56d0aebaf417bbe91c2d8e3b96203b1efa596e85", size = 80226, upload-time = "2026-05-22T14:49:20.264Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/2a7dc5f6abb2fca0b6e1610e120419f603650aceb4f1d3ac4cae0354e162/wrapt-2.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:fd0135d34387f5fd087d9be368ea77ea89cf2451dc1cd1c622d35021bcb3ab50", size = 83835, upload-time = "2026-05-22T14:49:21.634Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c0/782b86e28d1ceebeb74cccea12d2cd3d2ba0bd68e3dec20b1bc5873f6127/wrapt-2.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:f70db64e8266d7c45d3b735f2e08eeb434b5e03da9a479ae42b2e2e486a21a00", size = 80722, upload-time = "2026-05-22T14:49:23.59Z" }, { url = "https://files.pythonhosted.org/packages/53/46/29ac9daf11a86c22a8c38cd9236c62928ccae83f7ceb06bd3b0467cf9d05/wrapt-2.2.1-py3-none-any.whl", hash = "sha256:3aafea2975caef8ca49400640dde02cc7426e798f24870ed01f490bc3cffd32f", size = 61000, upload-time = "2026-05-22T14:49:41.593Z" }, ] @@ -7529,6 +7276,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362, upload-time = "2026-05-19T21:29:51Z" }, { url = "https://files.pythonhosted.org/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656", size = 92667, upload-time = "2026-05-19T21:29:52.743Z" }, { url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069, upload-time = "2026-05-19T21:29:54.442Z" }, + { url = "https://files.pythonhosted.org/packages/40/0e/e08087695fc12789263821c5dc0f8dc52b5b17efd0887cacf419f8a43ba3/yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2", size = 129670, upload-time = "2026-05-19T21:29:56.631Z" }, + { url = "https://files.pythonhosted.org/packages/3a/98/ab4b5ed1b1b5cd973c8a3eb994c3a6aefb6ce6d399e21bb5f0316c33815c/yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630", size = 91916, upload-time = "2026-05-19T21:29:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/5297bb6a7df4782f7605bffc43b31f5044070935fbbcaa6c705a07e6ac65/yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8", size = 91625, upload-time = "2026-05-19T21:30:00.412Z" }, + { url = "https://files.pythonhosted.org/packages/02/a7/45baabfff76829264e623b185cff0c340d7e11bf3e1cd9ea37e7d17934bd/yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14", size = 104574, upload-time = "2026-05-19T21:30:02.544Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/3a5ab144d3d650ca37d4f4b57e56169be8af3ca34c448793e064b30baaed/yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535", size = 97534, upload-time = "2026-05-19T21:30:04.319Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b5/5658fef3681fb5776b4513b052bec750009f47b3a592251c705d75375798/yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14", size = 111481, upload-time = "2026-05-19T21:30:05.988Z" }, + { url = "https://files.pythonhosted.org/packages/4c/06/fdcd7dde037f00866dce123ed4ba23dba94beb56fc4cf561668d27be37f2/yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3", size = 111529, upload-time = "2026-05-19T21:30:07.738Z" }, + { url = "https://files.pythonhosted.org/packages/c2/53/d81269aaafccea0d33396c03035de997b743f11e648e6e27a0df99c72980/yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208", size = 107338, upload-time = "2026-05-19T21:30:09.713Z" }, + { url = "https://files.pythonhosted.org/packages/ae/04/23049463f729bd899df203a7960505a75333edd499cda8aa1d5a82b64df5/yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50", size = 106147, upload-time = "2026-05-19T21:30:11.365Z" }, + { url = "https://files.pythonhosted.org/packages/14/18/04a4b5830b43ed5e4c5015b40e9f6241ad91487d71611061b4e111d6ac80/yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd", size = 104272, upload-time = "2026-05-19T21:30:12.978Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f7/8cffdf319aee7a7c1dbd07b61d91c3e3fda460c7a93b5f93e445f3806c4c/yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67", size = 99962, upload-time = "2026-05-19T21:30:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/d7/39/b3cce3b7dbef64ac700ad4cea156a207d01bede0f507587616c364b5468e/yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1", size = 111063, upload-time = "2026-05-19T21:30:16.683Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ea/100818505e7ebf165c7242ff17fdf7d9fee79e27234aeca871c1082920d7/yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1", size = 105438, upload-time = "2026-05-19T21:30:18.769Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d2/e075a0b32aa6625087de9e653087df0759fed5de4a435fef594181102a77/yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b", size = 111458, upload-time = "2026-05-19T21:30:21.024Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5c/ceea7ba98b65c8eb8d947fdc52f9bedfcd43c6a57c9e3c90c17be8f324a3/yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8", size = 107589, upload-time = "2026-05-19T21:30:23.412Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d9/5582d57e2b2db9b85eb6663a22efdd78e08805f3f5389566e9fcad254d1b/yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0", size = 94424, upload-time = "2026-05-19T21:30:25.425Z" }, + { url = "https://files.pythonhosted.org/packages/92/10/7dc07a0e22806a9280f42a57361395506e800c64e22737cd7b0886feab42/yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57", size = 88690, upload-time = "2026-05-19T21:30:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/13/d5b8e2c8667db955bcb3de233f18798fefe7edf1d7429c2c9d4f9c401114/yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b", size = 136248, upload-time = "2026-05-19T21:30:29.297Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/a4a97c05c9c9b8fd266bb2a0df12992c7fbd02391eb9640583411b6dab32/yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761", size = 95084, upload-time = "2026-05-19T21:30:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/95/b2/845cf2074a015e6fe0d0808cf1a2d9e868386c4220d657ebd8302b199043/yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8", size = 95272, upload-time = "2026-05-19T21:30:33.062Z" }, + { url = "https://files.pythonhosted.org/packages/fe/16/e69d4aa244aef45235ddfebc0e04036a6829842bc5a6a795aedc6c998d23/yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed", size = 101497, upload-time = "2026-05-19T21:30:34.842Z" }, + { url = "https://files.pythonhosted.org/packages/15/94/c07107715d621076863ee88b3ddf183fa5e9d4aba5769623c9979828410a/yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543", size = 94002, upload-time = "2026-05-19T21:30:37.724Z" }, + { url = "https://files.pythonhosted.org/packages/a9/35/fc1bbdd895b5e4010b8fdd037f7ed3aa289d3863e08231b30231ca9a0815/yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0", size = 106524, upload-time = "2026-05-19T21:30:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/32b66d0a4ba47c296cf86d03e2c67bff58399fe6d6d84d5205c04c66cc6d/yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024", size = 106165, upload-time = "2026-05-19T21:30:41.888Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/37cb5ff50c5e825d4d38e81bb04d1b7e96bf960f7ab89f9850b162f3f114/yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf", size = 103010, upload-time = "2026-05-19T21:30:43.985Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/4597912315096f7bb359e46e13bf8b60994fcbb2db29b804c0902ef4eff5/yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc", size = 101128, upload-time = "2026-05-19T21:30:46.291Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/c8e86e120521e646013d02a8e3b8884392e28494be8f392366e50d208efc/yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb", size = 101382, upload-time = "2026-05-19T21:30:48.085Z" }, + { url = "https://files.pythonhosted.org/packages/fa/98/70b229236118f89dbeb739b76f10225bbf53b5497725502594c9a01d699a/yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420", size = 95964, upload-time = "2026-05-19T21:30:49.785Z" }, + { url = "https://files.pythonhosted.org/packages/87/f8/56c386981e3c8648d279fdef2397ffec577e8320fd5649745e34d54faeb7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f", size = 106204, upload-time = "2026-05-19T21:30:51.862Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1e/765afe97811ca35933e2a7de70ac57b1997ea2e4ee895719ee7a231fb7e5/yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa", size = 101510, upload-time = "2026-05-19T21:30:53.62Z" }, + { url = "https://files.pythonhosted.org/packages/ee/78/393913f4b9039e1edd09ae8a9bbb9d539be909a8abf6d8a2084585bed4b7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe", size = 105584, upload-time = "2026-05-19T21:30:55.962Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/deb17b7049bbe74ea11a713b86f8f27800cc1c8648b0b797243ebb4830ba/yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd", size = 103410, upload-time = "2026-05-19T21:30:57.962Z" }, + { url = "https://files.pythonhosted.org/packages/8f/be/f9f7594e23b5b93affff0318e4593c1920331bcaefda326cabcad94296a1/yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215", size = 102980, upload-time = "2026-05-19T21:30:59.735Z" }, + { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219, upload-time = "2026-05-19T21:31:01.934Z" }, { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, ]