diff --git a/.github/workflows/pr-test.yaml b/.github/workflows/pr-test.yaml new file mode 100644 index 00000000..a75c14a5 --- /dev/null +++ b/.github/workflows/pr-test.yaml @@ -0,0 +1,86 @@ +name: MediSwarm PR Validation + +on: + schedule: + - cron: '0 5 * * 0' + pull_request: + branches: + - main + - dev + +permissions: + contents: read + +jobs: + validate-swarm: + runs-on: self-hosted + timeout-minutes: 45 + + env: + DATADIR: /mnt/swarm_alpha/Odelia_challange/ODELIA_Challenge_unilateral/ + SCRATCHDIR: /mnt/scratch + SITE_NAME: UKA + PYTHONUNBUFFERED: 1 + + + steps: + - name: Checkout repository (with submodules) + uses: actions/checkout@v3 + with: + submodules: true + fetch-depth: 0 + + - name: Get Docker image version + id: get_version + run: | + VERSION=$(./getVersionNumber.sh) + echo "VERSION=$VERSION" + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Build Docker image for real project (MEVIS) + run: | + chmod +x buildDockerImageAndStartupKits.sh + ./buildDockerImageAndStartupKits.sh -p application/provision/project_MEVIS_test.yml + + - name: Show workspace path for MEVIS project + run: | + echo "WORKSPACE_PATH: ${{ env.WORKSPACE_PATH }}" + find workspace -maxdepth 1 -type d -name "odelia_*_MEVIS_test" || echo "No workspace found" + + - name: Build Docker image and dummy startup kits + run: ./buildDockerImageAndStartupKits.sh -p tests/provision/dummy_project_for_testing.yml --use-docker-cache + + - name: Prepare dummy trainings + continue-on-error: true + run: | + ./runTestsInDocker.sh prepare_dummy_trainings + echo "Dummy training project prepared" + + - name: Run dummy training + continue-on-error: false + run: | + ./runTestsInDocker.sh run_dummy_training + echo "Dummy training finished" + echo "=== Checking log output ===" + ls -lh workspace/*/prod_00/client_A/logs || echo "No logs found for dummy training" + + - name: Run 3D CNN tests + continue-on-error: false + run: | + ./runTestsInDocker.sh run_3dcnn_tests + echo "3D CNN tests check finished" + echo "=== Checking synthetic log output ===" + ls -lh workspace/*/prod_00/client_A/logs || echo "No logs found for 3D CNN tests" + + - name: Run Unit Tests inside Docker + continue-on-error: true + run: | + ./runTestsInDocker.sh run_tests + echo "=== [LOG CHECK] ===" + docker logs $(docker ps -a -q --latest) | grep -i "error" && echo "Error found in logs" || echo "No error found" + + - name: Cleanup training artifacts + continue-on-error: true + run: | + ./runTestsInDocker.sh cleanup + echo "Cleanup finished" diff --git a/.github/workflows/update-apt-versions.yml b/.github/workflows/update-apt-versions.yml index baeeea8f..8e4eedc8 100644 --- a/.github/workflows/update-apt-versions.yml +++ b/.github/workflows/update-apt-versions.yml @@ -1,31 +1,24 @@ -name: Auto Update APT Versions +name: Auto Update APT Versions (Self-hosted) on: schedule: - # Every day at 05:00 UTC - - cron: '0 5 * * *' + # run eveyday at 04:00 UTC + - cron: '0 4 * * *' workflow_dispatch: jobs: update-apt: - name: Update APT Package Versions in Dockerfile - runs-on: ubuntu-latest + runs-on: self-hosted + timeout-minutes: 60 steps: - name: Checkout repository (with submodules) uses: actions/checkout@v3 with: submodules: true + fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - - name: Install dependencies - run: sudo apt-get update && sudo apt-get install -y git apt-utils - - - name: Configure Git for CI + - name: Set up Git run: | git config --global user.email "ci@github.com" git config --global user.name "GitHub CI" @@ -33,17 +26,15 @@ jobs: - name: Create and switch to apt-update branch run: | git checkout -b ci/apt-update || git switch ci/apt-update - - - name: Make update script executable - run: chmod +x scripts/ci/update_apt_versions.sh - - name: Run APT update script - run: scripts/ci/update_apt_versions.sh + run: | + chmod +x scripts/ci/update_apt_versions.sh + scripts/ci/update_apt_versions.sh - name: Show git diff for debugging - run: git diff + run: git diff || true - - name: Push ci/apt-update to origin + - name: Push apt-update branch if: env.NO_CHANGES == 'false' run: git push origin ci/apt-update --force @@ -53,8 +44,10 @@ jobs: with: commit-message: "chore: update apt versions in Dockerfile_ODELIA" branch: ci/apt-update + branch-suffix: timestamp title: "chore: Update APT versions in Dockerfile" body: | This PR automatically updates APT package version numbers in `Dockerfile_ODELIA` based on a rebuild and inspection of installation logs. base: main + delete-branch: false \ No newline at end of file diff --git a/.gitignore b/.gitignore index 57af2b10..86efbffa 100644 --- a/.gitignore +++ b/.gitignore @@ -180,3 +180,6 @@ provision # Ignore provisioned files /workspace/ + +# Ignore directory for caching pre-trained models +docker_config/torch_home_cache diff --git a/README.md b/README.md index cd8837fb..2eb29561 100644 --- a/README.md +++ b/README.md @@ -1,244 +1,43 @@ -# Introduction -MediSwarm is an open-source project dedicated to advancing medical deep learning through swarm intelligence, leveraging the NVFlare platform. Developed in collaboration with the Odelia consortium, this repository aims to create a decentralized and collaborative framework for medical research and applications. - -## Key Features -- **Swarm Learning:** Utilizes swarm intelligence principles to improve model performance and adaptability. -- **NVFlare Integration:** Built on NVFlare, providing robust and scalable federated learning capabilities. -- **Data Privacy:** Ensures data security and compliance with privacy regulations by keeping data local to each institution. -- **Collaborative Research:** Facilitates collaboration among medical researchers and institutions for enhanced outcomes. -- **Extensible Framework:** Designed to support various medical applications and easily integrate with existing workflows. - -## Prerequisites -### Hardware recommendations -* 64 GB of RAM (32 GB is the absolute minimum) -* 16 CPU cores (8 is the absolute minimum) -* an NVIDIA GPU with 48 GB of RAM (24 GB is the minimum) -* 8 TB of Storage (4 TB is the absolute minimum) - -We demonstrate that the system can run on lightweight hardware like this. For less than 10k EUR, you can configure systems from suppliers like Lambda, Dell Precision, and Dell Alienware. - -### Operating System -* Ubuntu 20.04 LTS - -### Software -* Docker -* openvpn -* git - -### Cloning the repository - ```bash - git clone https://github.com/KatherLab/MediSwarm.git --recurse-submodules - ``` -* The last argument is necessary because we are using a git submodule for the (ODELIA fork of NVFlare)[https://github.com/KatherLab/NVFlare_MediSwarm] -* If you have cloned it without this argument, use `git submodule update --init --recursive` - -### VPN -A VPN is necessary so that the swarm nodes can communicate with each other securely across firewalls. For that purpose, -1. Install OpenVPN - ```bash - sudo apt-get install openvpn - ``` -2. If you have a graphical user interface(GUI), follow this guide to connect to the VPN: [VPN setup guide(GUI).pdf](assets/VPN%20setup%20guide%28GUI%29.pdf) -3. If you have a command line interface(CLI), follow this guide to connect to the VPN: [VPN setup guide(CLI).md](assets/VPN%20setup%20guide%28CLI%29.md) - -# Usage for Swarm Participants -## Setup -1. Make sure your compute node satisfies the specification and has the necessary software installed. -2. Clone the repository and connect the client node to the VPN as described above. -3. TODO anything else? - -## Prepare Dataset -1. TODO which data is expected in which folder structure + table structure - -## Prepare Training Participation -1. Extract startup kit provided by swarm operator - -## Run Pre-Flight Check -1. Directories - ```bash - export SITE_NAME= # TODO should be defined above, also needed for dataset location - export DATADIR= - export SCRATCHDIR= - ``` -2. From the directory where you unpacked the startup kit, - ```bash - cd $SITE_NAME/startup - ``` -3. Verify that your Docker/GPU setup is working - ```bash - ./docker.sh --scratch_dir $SCRATCHDIR --GPU device=0 --dummy_training - ``` - * This will pull the Docker image, which might take a while. - * If you have multiple GPUs and 0 is busy, use a different one. - * The “training” itself should take less than minute and does not yield a meaningful classification performance. -4. Verify that your local data can be accessed and the model can be trained locally - ```bash - ./docker.sh --data_dir $DATADIR --scratch_dir $SCRATCHDIR --GPU device=0 --preflight_check - ``` - * Training time depends on the size of the local dataset - -## Start Swarm Node -1. From the directory where you unpacked the startup kit - ```bash - cd $SITE_NAME/startup # skip this if you just ran the pre-flight check - ``` -2. Start the client - ```bash - ./docker.sh --data_dir $DATADIR --scratch_dir $SCRATCHDIR --GPU device=0 --start_client - ``` -3. Console output is captured in `nohup.out`, which may have been created by the root user in the container, so make it readable: - ```bash - sudo chmod a+r nohup.out - ``` -4. Output files - * TODO describe - -## Run Local Training -1. From the directory where you unpacked the startup kit - ```bash - cd $SITE_NAME/startup - ``` -2. Start local training - ```bash - /docker.sh --data_dir $DATADIR --scratch_dir $SCRATCHDIR --GPU all --local_training - ``` - * TODO update when handling of the number of epochs has been implemented -3. Output files - * TODO describe - -# Usage for MediSwarm and Application Code Developers -## Versioning of ODELIA Docker Images -If needed, update the version number in file (odelia_image.version)[odelia_image.version]. It will be used automatically for the Docker image and startup kits. - -## Build the Docker Image and Startup Kits -The Docker image contains all dependencies for administrative purposes (dashboard, command-line provisioning, admin console, server) as well as for running the 3DCNN pipeline under the pytorch-lightning framework. -The project description specifies the swarm nodes etc. to be used for a swarm training. - ```bash - cd MediSwarm - ./buildDockerImageAndStartupKits.sh -p application/provision/ - ``` - -1. Make sure you have no uncommitted changes. -2. If package versions are still not available, you may have to check what the current version is and update the `Dockerfile` accordingly. Version numbers are hard-coded to avoid issues due to silently different versions being installed. -3. After successful build (and after verifying that everything works as expected, i.e., local tests, building startup kits, running local trainings in the startup kit), you can manually push the image to DockerHub, provided you have the necessary rights. Make sure you are not re-using a version number for this purpose. - -## Running Local Tests - ```bash - ./runTestsInDocker.sh - ``` - -You should see -1. several expected errors and warnings printed from unit tests that should succeed overall, and a coverage report -2. output of a successful simulation run with two nodes -3. output of a successful proof-of-concept run run with two nodes -4. output of a set of startup kits being generated -5. output of a dummy training run using one of the startup kits - -Optionally, uncomment running NVFlare unit tests in `_runTestsInsideDocker.sh`. - -## Distributing Startup Kits -Distribute the startup kits to the clients. - -## Running the Application -1. **CIFAR-10 example:** - See [cifar10/README.md](application/jobs/cifar10/README.md) -2. **Minimal PyTorch CNN example:** - See [application/jobs/minimal_training_pytorch_cnn/README.md](application/jobs/minimal_training_pytorch_cnn/README.md) -3. **3D CNN for classifying breast tumors:** - See [3dcnn_ptl/README.md](application/jobs/3dcnn_ptl/README.md) - -## Contributing Application Code -1. Take a look at application/jobs/minimal_training_pytorch_cnn for a minimal example how pytorch code can be adapted to work with NVFlare -2. Take a look at application/jobs/3dcnn_ptl for a more relastic example of pytorch code that can run in the swarm -3. Use the local tests to check if the code is swarm-ready -4. TODO more detailed instructions - -# Usage for Swarm Operators -## Setting up a Swarm -Production mode is designed for secure, real-world deployments. It supports both local and remote setups, whether on-premise or in the cloud. For more details, refer to the [NVFLARE Production Mode](https://nvflare.readthedocs.io/en/2.4.1/real_world_fl.html). - -To set up production mode, follow these steps: - -## Edit `/etc/hosts` -Ensure that your `/etc/hosts` file includes the correct host mappings. All hosts need to be able to communicate to the server node. - -For example, add the following line (replace `` with the server's actual IP address): - -```plaintext - dl3.tud.de dl3 -``` - -## Create Startup Kits -### Via Script (recommended) -1. Use, e.g., the file `application/provision/project_MEVIS_test.yml`, adapt as needed (network protocol etc.) -2. Call `buildStartupKits.sh /path/to/project_configuration.yml` to build the startup kits -3. Startup kits are generated to `workspace//prod_00/` -4. Deploy startup kits to the respective server/clients - -### Via the Dashboard (not recommended) -```bash -docker run -d --rm \ - --ipc=host -p 8443:8443 \ - --name=odelia_swarm_admin \ - -v /var/run/docker.sock:/var/run/docker.sock \ - \ - /bin/bash -c "nvflare dashboard --start --local --cred :" -``` -using some credentials chosen for the swarm admin account. - -Access the dashboard in a web browser at `https://localhost:8443` log in with these credentials, and configure the project: -1. enter project short name, name, description -2. enter docker download link: jefftud/nvflare-pt-dev:3dcnn -3. if needed, enter dates -4. click save -5. Server Configuration > Server (DNS name): -6. click make project public - -#### Register client per site -Access the dashboard at `https://:8443`. - -1. register a user -2. enter organziation (corresponding to the site) -3. enter role (e.g., org admin) -4. add a site (note: must not contain spaces, best use alphanumerical name) -5. specify number of GPUs and their memory - -#### Approve clients and finish configuration -Access the dashboard at `https://localhost:8443` log in with the admin credentials. -1. Users Dashboard > approve client user -2. Client Sites > approve client sites -3. Project Home > freeze project - -## Download startup kits -After setting up the project admin configuration, server and clients can download their startup kits. Store the passwords somewhere, they are only displayed once (or you can download them again). - -## Starting a Swarm Training -1. Connect the *server* host to the VPN as described above. -2. Start the *server* startup kit using the respective `startup/docker.sh` script with the option to start the server -3. Provide the *client* startup kits to the swarm participants (be aware that email providers or other channels may prevent encrypted archives) -4. Make sure the participants have started their clients via the respective startup kits, see below -5. Start the *admin* startup kit using the respective `startup/docker.sh` script to start the admin console -6. Deploy a job by `submit_job ` - - -# License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. - -# Maintainers -[Jeff](https://github.com/Ultimate-Storm) -[Ole Schwen](mailto:ole.schwen@mevis.fraunhofer.de) -[Steffen Renisch](mailto:steffen.renisch@mevis.fraunhofer.de) - -# Contributing -Feel free to dive in! [Open an issue](https://github.com/KatherLab/MediSwarm/issues) or submit pull requests. - -# Credits -This project utilizes platforms and resources from the following repositories: - -- **[NVFLARE](https://github.com/NVIDIA/NVFlare)**: NVFLARE (NVIDIA Federated Learning Application Runtime Environment) is an open-source framework that provides a robust and scalable platform for federated learning applications. We have integrated NVFLARE to efficiently handle the federated learning aspects of our project. - -Special thanks to the contributors and maintainers of these repositories for their valuable work and support. - ---- - -For more details about NVFLARE and its features, please visit the [NVFLARE GitHub repository](https://github.com/NVIDIA/NVFlare). +# MediSwarm + +An open-source platform advancing medical AI via privacy-preserving swarm learning, based on NVFlare and developed with +the ODELIA consortium. + +[![PR Tests](https://github.com/KatherLab/MediSwarm/actions/workflows/pr-test.yaml/badge.svg)](https://github.com/KatherLab/MediSwarm/actions/workflows/pr-test.yaml) +[![Build](https://github.com/KatherLab/MediSwarm/actions/workflows/update-apt-versions.yml/badge.svg)](https://github.com/KatherLab/MediSwarm/actions/workflows/update-apt-versions.yml) + +## Quick Start for Your Role + +Choose your role and follow the instructions: + +- [Swarm Participant (Medical Site / Data Scientist)](assets/readme/README.participant.md) +- [Developer (Docker, Code, Pipeline)](assets/readme/README.developer.md) +- [Swarm Operator (Provisioning, VPN, Server)](assets/readme/README.operator.md) + +## Overview + +MediSwarm enables: + +- **Privacy-preserving training** of deep learning models on distributed medical datasets +- **Decentralized collaboration** between institutions +- **Dockerized, reproducible** experiments built on NVFlare + +## License + +MIT — see [LICENSE](LICENSE). + +## Maintainers + +- [Jeff](https://github.com/Ultimate-Storm) +- [Ole Schwen](mailto:ole.schwen@mevis.fraunhofer.de) +- [Steffen Renisch](mailto:steffen.renisch@mevis.fraunhofer.de) + +## Contributing + +Contributions welcome! [Open an issue](https://github.com/KatherLab/MediSwarm/issues) or submit a PR. + +## Credits + +Built on: + +- [NVFLARE](https://github.com/NVIDIA/NVFlare) diff --git a/_buildStartupKits.sh b/_buildStartupKits.sh index bf3fec04..29755d27 100755 --- a/_buildStartupKits.sh +++ b/_buildStartupKits.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +set -euo pipefail + if [ "$#" -ne 2 ]; then echo "Usage: _buildStartupKits.sh SWARM_PROJECT.yml VERSION_STRING" exit 1 @@ -9,7 +11,16 @@ PROJECT_YML=$1 VERSION=$2 sed -i 's#__REPLACED_BY_CURRENT_VERSION_NUMBER_WHEN_BUILDING_STARTUP_KITS__#'$VERSION'#' $PROJECT_YML - -docker run --rm -it -u $(id -u):$(id -g) -v /etc/passwd:/etc/passwd -v /etc/group:/etc/group -v ./:/workspace/ -w /workspace/ jefftud/odelia:$VERSION /bin/bash -c "nvflare provision -p $PROJECT_YML && ./_generateStartupKitArchives.sh $PROJECT_YML $VERSION" +echo "Building startup kits for project $PROJECT_YML with version $VERSION" +docker run --rm \ + -u $(id -u):$(id -g) \ + -v /etc/passwd:/etc/passwd \ + -v /etc/group:/etc/group \ + -v ./:/workspace/ \ + -w /workspace/ \ + -e PROJECT_YML=$PROJECT_YML \ + -e VERSION=$VERSION \ + jefftud/odelia:$VERSION \ + /bin/bash -c "nvflare provision -p \$PROJECT_YML && ./_generateStartupKitArchives.sh \$PROJECT_YML \$VERSION"|| { echo "Docker run failed"; exit 1; } sed -i 's#'$VERSION'#__REPLACED_BY_CURRENT_VERSION_NUMBER_WHEN_BUILDING_STARTUP_KITS__#' $PROJECT_YML diff --git a/_generateStartupKitArchives.sh b/_generateStartupKitArchives.sh index c1055153..ea842b41 100755 --- a/_generateStartupKitArchives.sh +++ b/_generateStartupKitArchives.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +set -e + OUTPUT_FOLDER=workspace/`grep "^name: " $1 | sed 's/name: //'` TARGET_FOLDER=`ls -d $OUTPUT_FOLDER/prod_* | tail -n 1` LONG_VERSION=$2 diff --git a/_run3DdcnnptlTestsInDocker.sh b/_run3DdcnnptlTestsInDocker.sh new file mode 100755 index 00000000..7fb7a877 --- /dev/null +++ b/_run3DdcnnptlTestsInDocker.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -e + +run_3dcnn_simulation_mode () { + # both clients use the same data according to SITE_NAME, there are no separate env variables from which the code could read which client it is + # change training configuration to run 2 rounds + cd /MediSwarm + export TMPDIR=$(mktemp -d) + cp -R application/jobs/ODELIA_ternary_classification ${TMPDIR}/ODELIA_ternary_classification + sed -i 's/num_rounds = .*/num_rounds = 2/' ${TMPDIR}/ODELIA_ternary_classification/app/config/config_fed_server.conf + export TRAINING_MODE="swarm" + export SITE_NAME="client_A" + nvflare simulator -w /tmp/ODELIA_ternary_classification -n 2 -t 2 ${TMPDIR}/ODELIA_ternary_classification -c client_A,client_B + unset TRAINING_MODE + unset SITE_NAME + rm -rf ${TMPDIR} + unset TMPDIR +} + +run_3dcnn_simulation_mode diff --git a/_runTestsInsideDocker.sh b/_runTestsInsideDocker.sh index 794d7320..d3d07c18 100755 --- a/_runTestsInsideDocker.sh +++ b/_runTestsInsideDocker.sh @@ -1,38 +1,54 @@ #!/usr/bin/env bash -# run unit tests of ODELIA swarm learning and report coverage -export MPLCONFIGDIR=/tmp -cd /MediSwarm/tests/unit_tests/controller -PYTHONPATH=/MediSwarm/controller/controller python3 -m coverage run --source=/MediSwarm/controller/controller -m unittest discover -coverage report -m -rm .coverage +run_controller_unit_tests_with_coverage () { + # run unit tests of ODELIA swarm learning and report coverage + export MPLCONFIGDIR=/tmp + cd /MediSwarm/tests/unit_tests/controller + PYTHONPATH=/MediSwarm/controller/controller python3 -m coverage run --source=/MediSwarm/controller/controller -m unittest discover + coverage report -m + rm .coverage +} -# uncomment to run NVFlare's unit tests (takes about 2 minutes and will install python packages in the container) -# cd /MediSwarm/docker_config/NVFlare -# ./runtest.sh -c -r -# coverage report -m -# cd .. +run_nvflare_unit_tests () { + cd /MediSwarm/docker_config/NVFlare + ./runtest.sh -c -r + coverage report -m + cd .. +} -# run standalone version of minimal example -cd /MediSwarm/application/jobs/minimal_training_pytorch_cnn/app/custom/ -export TRAINING_MODE="local_training" -./main.py +run_minimal_example_standalone () { + # run standalone version of minimal example + cd /MediSwarm/application/jobs/minimal_training_pytorch_cnn/app/custom/ + export TRAINING_MODE="local_training" + ./main.py +} -# run simulation mode for minimal example -cd /MediSwarm -export TRAINING_MODE="swarm" -nvflare simulator -w /tmp/minimal_training_pytorch_cnn -n 2 -t 2 application/jobs/minimal_training_pytorch_cnn -c simulated_node_0,simulated_node_1 +run_minimal_example_simulation_mode () { + # run simulation mode for minimal example + cd /MediSwarm + export TRAINING_MODE="swarm" + nvflare simulator -w /tmp/minimal_training_pytorch_cnn -n 2 -t 2 application/jobs/minimal_training_pytorch_cnn -c simulated_node_0,simulated_node_1 +} -# run proof-of-concept mode for minimal example -cd /MediSwarm -export TRAINING_MODE="swarm" -nvflare poc prepare -c poc_client_0 poc_client_1 -nvflare poc prepare-jobs-dir -j application/jobs/ -nvflare poc start -ex admin@nvidia.com -sleep 15 -echo "Will submit job now after sleeping 15 seconds to allow the background process to complete" -nvflare job submit -j application/jobs/minimal_training_pytorch_cnn -sleep 60 -echo "Will shut down now after sleeping 60 seconds to allow the background process to complete" -sleep 2 -nvflare poc stop +run_minimal_example_proof_of_concept_mode () { + # run proof-of-concept mode for minimal example + cd /MediSwarm + export TRAINING_MODE="swarm" + nvflare poc prepare -c poc_client_0 poc_client_1 + nvflare poc prepare-jobs-dir -j application/jobs/ + nvflare poc start -ex admin@nvidia.com + sleep 15 + echo "Will submit job now after sleeping 15 seconds to allow the background process to complete" + nvflare job submit -j application/jobs/minimal_training_pytorch_cnn + sleep 60 + echo "Will shut down now after sleeping 60 seconds to allow the background process to complete" + sleep 2 + nvflare poc stop +} + +run_controller_unit_tests_with_coverage +# uncomment the following line to run NVFlare's unit tests (takes about 2 minutes and will install python packages in the container) +# run_nvflare_unit_tests +run_minimal_example_standalone +run_minimal_example_simulation_mode +run_minimal_example_proof_of_concept_mode diff --git a/application/jobs/3dcnn_ptl/app/custom/data/augmentation/augmentations_3d.py b/application/jobs/3dcnn_ptl/app/custom/data/augmentation/augmentations_3d.py deleted file mode 100644 index cc206cf0..00000000 --- a/application/jobs/3dcnn_ptl/app/custom/data/augmentation/augmentations_3d.py +++ /dev/null @@ -1,244 +0,0 @@ -import torchio as tio -from typing import Tuple, Union, Optional, Dict -from numbers import Number -import nibabel as nib -import numpy as np -import torch -from torchio.typing import TypeRangeFloat -from torchio.transforms.transform import TypeMaskingMethod -from torchio import Subject, Image - - -class SubjectToTensor: - """Transforms TorchIO Subjects into a Python dict and changes axes order from TorchIO to Torch.""" - - def __call__(self, subject: Subject) -> Dict[str, torch.Tensor]: - """Transforms the given subject. - - Args: - subject (Subject): The subject to be transformed. - - Returns: - Dict[str, torch.Tensor]: A dictionary with transformed subject data. - """ - return {key: val.data.swapaxes(1, -1) if isinstance(val, Image) else val for key, val in subject.items()} - - -class ImageToTensor: - """Transforms TorchIO Image into a Numpy/Torch Tensor and changes axes order from TorchIO [B, C, W, H, D] to Torch [B, C, D, H, W].""" - - def __call__(self, image: Image) -> torch.Tensor: - """Transforms the given image. - - Args: - image (Image): The image to be transformed. - - Returns: - torch.Tensor: The transformed image tensor. - """ - return image.data.swapaxes(1, -1) - - -def parse_per_channel(per_channel: Union[bool, list], channels: int) -> list: - """Parses the per_channel argument. - - Args: - per_channel (Union[bool, list]): Whether to apply per channel. - channels (int): The number of channels. - - Returns: - list: A list of channel tuples. - """ - if isinstance(per_channel, bool): - if per_channel: - return [(ch,) for ch in range(channels)] - else: - return [tuple(ch for ch in range(channels))] - else: - return per_channel - - -class ZNormalization(tio.ZNormalization): - """Add option 'per_channel' to apply znorm for each channel independently and percentiles to clip values first.""" - - def __init__( - self, - percentiles: TypeRangeFloat = (0, 100), - per_channel: Union[bool, list] = True, - masking_method: TypeMaskingMethod = None, - **kwargs - ): - super().__init__(masking_method=masking_method, **kwargs) - self.percentiles = percentiles - self.per_channel = per_channel - - def apply_normalization( - self, - subject: Subject, - image_name: str, - mask: torch.Tensor, - ) -> None: - """Applies normalization to the given subject. - - Args: - subject (Subject): The subject to normalize. - image_name (str): The name of the image to normalize. - mask (torch.Tensor): The mask tensor. - """ - image = subject[image_name] - per_channel = parse_per_channel(self.per_channel, image.shape[0]) - - image.set_data(torch.cat([ - self._znorm(image.data[chs,], mask[chs,], image_name, image.path) - for chs in per_channel]) - ) - - def _znorm(self, image_data: torch.Tensor, mask: torch.Tensor, image_name: str, image_path: str) -> torch.Tensor: - """Applies z-normalization to the given image data. - - Args: - image_data (torch.Tensor): The image data to normalize. - mask (torch.Tensor): The mask tensor. - image_name (str): The name of the image. - image_path (str): The path of the image. - - Returns: - torch.Tensor: The normalized image data. - - Raises: - RuntimeError: If standard deviation is 0 for masked values. - """ - cutoff = torch.quantile(image_data.masked_select(mask).float(), torch.tensor(self.percentiles) / 100.0) - torch.clamp(image_data, *cutoff.to(image_data.dtype).tolist(), out=image_data) - - standardized = self.znorm(image_data, mask) - if standardized is None: - message = ( - 'Standard deviation is 0 for masked values' - f' in image "{image_name}" ({image_path})' - ) - raise RuntimeError(message) - return standardized - - -class RescaleIntensity(tio.RescaleIntensity): - """Add option 'per_channel' to apply rescale for each channel independently.""" - - def __init__( - self, - out_min_max: TypeRangeFloat = (0, 1), - percentiles: TypeRangeFloat = (0, 100), - masking_method: TypeMaskingMethod = None, - in_min_max: Optional[Tuple[float, float]] = None, - per_channel: Union[bool, list] = True, - # Bool or List of tuples containing channel indices that should be normalized together - **kwargs - ): - super().__init__(out_min_max, percentiles, masking_method, in_min_max, **kwargs) - self.per_channel = per_channel - - def apply_normalization( - self, - subject: Subject, - image_name: str, - mask: torch.Tensor, - ) -> None: - """Applies normalization to the given subject. - - Args: - subject (Subject): The subject to normalize. - image_name (str): The name of the image to normalize. - mask (torch.Tensor): The mask tensor. - """ - image = subject[image_name] - per_channel = parse_per_channel(self.per_channel, image.shape[0]) - - image.set_data(torch.cat([ - self.rescale(image.data[chs,], mask[chs,], image_name) - for chs in per_channel]) - ) - - -class Pad(tio.Pad): - """Fixed version of TorchIO Pad. - - Pads with zeros for LabelMaps independent of padding mode (e.g., don't pad with mean). - Pads with global (not per axis) 'maximum', 'mean', 'median', 'minimum' if any of these padding modes were selected. - """ - - def apply_transform(self, subject: Subject) -> Subject: - """Applies padding to the given subject. - - Args: - subject (Subject): The subject to pad. - - Returns: - Subject: The padded subject. - """ - assert self.bounds_parameters is not None - low = self.bounds_parameters[::2] - for image in self.get_images(subject): - new_origin = nib.affines.apply_affine(image.affine, -np.array(low)) - new_affine = image.affine.copy() - new_affine[:3, 3] = new_origin - kwargs: Dict[str, Union[str, float]] - if isinstance(self.padding_mode, Number): - kwargs = { - 'mode': 'constant', - 'constant_values': self.padding_mode, - } - elif isinstance(image, tio.LabelMap): # FIX - kwargs = { - 'mode': 'constant', - 'constant_values': 0, - } - else: - if self.padding_mode in ['maximum', 'mean', 'median', 'minimum']: - if self.padding_mode == 'maximum': - constant_values = image.data.min() - elif self.padding_mode == 'mean': - constant_values = image.data.to(torch.float).mean().to(image.data.dtype) - elif self.padding_mode == 'median': - constant_values = image.data.median() - elif self.padding_mode == 'minimum': - constant_values = image.data.min() - kwargs = { - 'mode': 'constant', - 'constant_values': constant_values, - } - else: - kwargs = {'mode': self.padding_mode} - pad_params = self.bounds_parameters - paddings = (0, 0), pad_params[:2], pad_params[2:4], pad_params[4:] - padded = np.pad(image.data, paddings, **kwargs) # type: ignore[call-overload] # noqa: E501 - image.set_data(torch.as_tensor(padded)) - image.affine = new_affine - return subject - - -class CropOrPad(tio.CropOrPad): - """Fixed version of TorchIO CropOrPad. - - Pads with zeros for LabelMaps independent of padding mode (e.g., don't pad with mean). - Pads with global (not per axis) 'maximum', 'mean', 'median', 'minimum' if any of these padding modes were selected. - """ - - def apply_transform(self, subject: Subject) -> Subject: - """Applies cropping or padding to the given subject. - - Args: - subject (Subject): The subject to crop or pad. - - Returns: - Subject: The cropped or padded subject. - """ - subject.check_consistent_space() - padding_params, cropping_params = self.compute_crop_or_pad(subject) - padding_kwargs = {'padding_mode': self.padding_mode} - if padding_params is not None: - pad = Pad(padding_params, **padding_kwargs) - subject = pad(subject) # type: ignore[assignment] - if cropping_params is not None: - crop = tio.Crop(cropping_params) - subject = crop(subject) # type: ignore[assignment] - return subject diff --git a/application/jobs/3dcnn_ptl/app/custom/data/datamodules/datamodule.py b/application/jobs/3dcnn_ptl/app/custom/data/datamodules/datamodule.py deleted file mode 100644 index b8c8cb44..00000000 --- a/application/jobs/3dcnn_ptl/app/custom/data/datamodules/datamodule.py +++ /dev/null @@ -1,125 +0,0 @@ -import pytorch_lightning as pl -import torch -from torch.utils.data.dataloader import DataLoader -import torch.multiprocessing as mp -from torch.utils.data.sampler import WeightedRandomSampler, RandomSampler - - -class DataModule(pl.LightningDataModule): - """ - LightningDataModule for handling dataset loading and batching. - - Attributes: - ds_train (object): Training dataset. - ds_val (object): Validation dataset. - ds_test (object): Test dataset. - batch_size (int): Batch size for dataloaders. - num_workers (int): Number of workers for data loading. - seed (int): Random seed for reproducibility. - pin_memory (bool): If True, pin memory for faster data transfer to GPU. - weights (list): Weights for the weighted random sampler. - """ - - def __init__( - self, - ds_train: object = None, - ds_val: object = None, - ds_test: object = None, - batch_size: int = 1, - num_workers: int = mp.cpu_count(), - seed: int = 0, - pin_memory: bool = False, - weights: list = None - ): - """ - Initializes the DataModule with datasets and parameters. - - Args: - ds_train (object, optional): Training dataset. Defaults to None. - ds_val (object, optional): Validation dataset. Defaults to None. - ds_test (object, optional): Test dataset. Defaults to None. - batch_size (int, optional): Batch size. Defaults to 1. - num_workers (int, optional): Number of workers. Defaults to mp.cpu_count(). - seed (int, optional): Random seed. Defaults to 0. - pin_memory (bool, optional): Pin memory. Defaults to False. - weights (list, optional): Weights for sampling. Defaults to None. - """ - super().__init__() - self.hyperparameters = {**locals()} - self.hyperparameters.pop('__class__') - self.hyperparameters.pop('self') - - self.ds_train = ds_train - self.ds_val = ds_val - self.ds_test = ds_test - - self.batch_size = batch_size - self.num_workers = num_workers - self.seed = seed - self.pin_memory = pin_memory - self.weights = weights - - def train_dataloader(self) -> DataLoader: - """ - Returns the training dataloader. - - Returns: - DataLoader: DataLoader for the training dataset. - - Raises: - AssertionError: If the training dataset is not initialized. - """ - generator = torch.Generator() - generator.manual_seed(self.seed) - - if self.ds_train is not None: - if self.weights is not None: - sampler = WeightedRandomSampler(self.weights, len(self.weights), generator=generator) - else: - sampler = RandomSampler(self.ds_train, replacement=False, generator=generator) - return DataLoader( - self.ds_train, batch_size=self.batch_size, num_workers=self.num_workers, - sampler=sampler, generator=generator, drop_last=True, pin_memory=self.pin_memory - ) - - raise AssertionError("A training set was not initialized.") - - def val_dataloader(self) -> DataLoader: - """ - Returns the validation dataloader. - - Returns: - DataLoader: DataLoader for the validation dataset. - - Raises: - AssertionError: If the validation dataset is not initialized. - """ - generator = torch.Generator() - generator.manual_seed(self.seed) - if self.ds_val is not None: - return DataLoader( - self.ds_val, batch_size=self.batch_size, num_workers=self.num_workers, shuffle=False, - generator=generator, drop_last=False, pin_memory=self.pin_memory - ) - - raise AssertionError("A validation set was not initialized.") - - def test_dataloader(self) -> DataLoader: - """ - Returns the test dataloader. - - Returns: - DataLoader: DataLoader for the test dataset. - - Raises: - AssertionError: If the test dataset is not initialized. - """ - generator = torch.Generator() - generator.manual_seed(self.seed) - if self.ds_test is not None: - return DataLoader( - self.ds_test, batch_size=self.batch_size, num_workers=self.num_workers, shuffle=False, - generator=generator, drop_last=False, pin_memory=self.pin_memory - ) - - raise AssertionError("A test dataset was not initialized.") diff --git a/application/jobs/3dcnn_ptl/app/custom/data/datasets/__init__.py b/application/jobs/3dcnn_ptl/app/custom/data/datasets/__init__.py deleted file mode 100644 index e34b2577..00000000 --- a/application/jobs/3dcnn_ptl/app/custom/data/datasets/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -This package initializes the necessary modules and classes for the project. -""" - -from .dataset_3d import SimpleDataset3D -from .dataset_3d_collab import DUKE_Dataset3D_collab -from .dataset_3d_duke import DUKE_Dataset3D -from .dataset_3d_duke_external import DUKE_Dataset3D_external - -__all__ = [name for name in dir() if not name.startswith('_')] diff --git a/application/jobs/3dcnn_ptl/app/custom/data/datasets/dataset_3d.py b/application/jobs/3dcnn_ptl/app/custom/data/datasets/dataset_3d.py deleted file mode 100644 index 234a6d8b..00000000 --- a/application/jobs/3dcnn_ptl/app/custom/data/datasets/dataset_3d.py +++ /dev/null @@ -1,124 +0,0 @@ -from pathlib import Path -import torch.utils.data as data -import torchio as tio -from data.augmentation.augmentations_3d import ImageToTensor, RescaleIntensity, ZNormalization - -class SimpleDataset3D(data.Dataset): - """ - A simple dataset class for 3D medical images using TorchIO for preprocessing and augmentation. - - Args: - path_root (str): Root directory of the dataset. - item_pointers (list, optional): List of file paths. Defaults to []. - crawler_glob (str, optional): Glob pattern for crawling files. Defaults to '*.nii.gz'. - transform (callable, optional): Transformations to apply to the data. Defaults to None. - image_resize (tuple, optional): Desired output image size. Defaults to None. - flip (bool, optional): Whether to apply random flipping. Defaults to False. - image_crop (tuple, optional): Desired crop size. Defaults to None. - norm (str, optional): Normalization method. Defaults to 'znorm_clip'. - to_tensor (bool, optional): Whether to convert images to tensor. Defaults to True. - """ - - def __init__( - self, - path_root, - item_pointers=[], - crawler_glob='*.nii.gz', - transform=None, - image_resize=None, - flip=False, - image_crop=None, - norm='znorm_clip', - to_tensor=True, - ): - super().__init__() - self.path_root = Path(path_root) - self.crawler_glob = crawler_glob - - if transform is None: - self.transform = tio.Compose([ - tio.Resize(image_resize) if image_resize is not None else tio.Lambda(lambda x: x), - tio.RandomFlip((0, 1, 2)) if flip else tio.Lambda(lambda x: x), - tio.CropOrPad(image_crop) if image_crop is not None else tio.Lambda(lambda x: x), - self.get_norm(norm), - ImageToTensor() if to_tensor else tio.Lambda(lambda x: x) # [C, W, H, D] -> [C, D, H, W] - ]) - else: - self.transform = transform - - if item_pointers: - self.item_pointers = item_pointers - else: - self.item_pointers = self.run_item_crawler(self.path_root, self.crawler_glob) - - def __len__(self): - """Returns the number of items in the dataset.""" - return len(self.item_pointers) - - def __getitem__(self, index): - """ - Retrieves an item from the dataset. - - Args: - index (int): Index of the item to retrieve. - - Returns: - dict: A dictionary with 'uid' and 'source' keys. - """ - rel_path_item = self.item_pointers[index] - path_item = self.path_root / rel_path_item - img = self.load_item(path_item) - return {'uid': str(rel_path_item), 'source': self.transform(img)} - - def load_item(self, path_item): - """ - Loads an image from the given path. - - Args: - path_item (Path): Path to the image file. - - Returns: - tio.ScalarImage: Loaded image. - """ - return tio.ScalarImage(path_item) - - @classmethod - def run_item_crawler(cls, path_root, crawler_glob, **kwargs): - """ - Crawls the directory to find items matching the glob pattern. - - Args: - path_root (Path): Root directory to start crawling. - crawler_glob (str): Glob pattern to match files. - - Returns: - list: List of relative file paths. - """ - return [path.relative_to(path_root) for path in Path(path_root).rglob(f'{crawler_glob}')] - - @staticmethod - def get_norm(norm): - """ - Returns the normalization transform based on the provided norm string. - - Args: - norm (str): Normalization method name. - - Returns: - tio.Transform: The normalization transform. - """ - if norm is None: - return tio.Lambda(lambda x: x) - elif isinstance(norm, str): - if norm == 'min-max': - return RescaleIntensity((-1, 1), per_channel=True, masking_method=lambda x: x > 0) - elif norm == 'min-max_clip': - return RescaleIntensity((-1, 1), per_channel=True, percentiles=(0.5, 99.5), masking_method=lambda x: x > 0) - elif norm == 'znorm': - return ZNormalization(per_channel=True, masking_method=lambda x: x > 0) - elif norm == 'znorm_clip': - return ZNormalization(per_channel=True, percentiles=(0.5, 99.5), masking_method=lambda x: x > 0) - else: - raise ValueError("Unknown normalization") - else: - return norm diff --git a/application/jobs/3dcnn_ptl/app/custom/data/datasets/dataset_3d_collab.py b/application/jobs/3dcnn_ptl/app/custom/data/datasets/dataset_3d_collab.py deleted file mode 100755 index c867aec7..00000000 --- a/application/jobs/3dcnn_ptl/app/custom/data/datasets/dataset_3d_collab.py +++ /dev/null @@ -1,106 +0,0 @@ -from pathlib import Path -import pandas as pd -from data.datasets import SimpleDataset3D - - -class DUKE_Dataset3D_collab(SimpleDataset3D): - """ - DUKE Collaboration Dataset for 3D medical images, extending SimpleDataset3D. - - Args: - path_root (str): Root directory of the dataset. - item_pointers (list, optional): List of file paths. Defaults to None. - crawler_glob (str, optional): Glob pattern for crawling files. Defaults to '*.nii.gz'. - transform (callable, optional): Transformations to apply to the data. Defaults to None. - image_resize (tuple, optional): Desired output image size. Defaults to None. - flip (bool, optional): Whether to apply random flipping. Defaults to False. - image_crop (tuple, optional): Desired crop size. Defaults to None. - norm (str, optional): Normalization method. Defaults to 'znorm_clip'. - to_tensor (bool, optional): Whether to convert images to tensor. Defaults to True. - """ - - def __init__( - self, - path_root, - item_pointers=None, - crawler_glob='*.nii.gz', - transform=None, - image_resize=None, - flip=False, - image_crop=None, - norm='znorm_clip', - to_tensor=True - ): - if item_pointers is None: - item_pointers = [] - super().__init__(path_root, item_pointers, crawler_glob, transform, image_resize, flip, image_crop, norm, - to_tensor) - df = pd.read_csv(self.path_root.parent / 'datasheet.csv') - - df = df[[df.columns[0], df.columns[1]]] # Only pick relevant columns: Patient ID, Tumor Side, Bilateral - existing_folders = {folder.name for folder in Path(path_root).iterdir() if folder.is_dir()} - self.df = df[df['PATIENT'].isin(existing_folders)] - self.df = self.df.set_index('PATIENT', drop=True) - self.item_pointers = self.df.index[self.df.index.isin(self.item_pointers)].tolist() - - def __getitem__(self, index): - """ - Retrieves an item from the dataset. - - Args: - index (int): Index of the item to retrieve. - - Returns: - dict: A dictionary with 'uid', 'source', and 'target' keys. - """ - uid = self.item_pointers[index] - item_dir = self.path_root / uid - nii_gz_files = list(item_dir.glob('**/*.nii.gz')) - file_name = 'SUB_4.nii.gz' - - if len(nii_gz_files) > 1: - sub_4_path = item_dir / file_name - if sub_4_path in nii_gz_files: - path_item = sub_4_path - else: - path_item = nii_gz_files[0] - elif nii_gz_files: - path_item = nii_gz_files[0] - else: - raise FileNotFoundError(f"No .nii.gz files found in {item_dir}") - - img = self.load_item(path_item) - target = self.df.loc[uid]['Malign'] - return {'uid': uid, 'source': self.transform(img), 'target': target} - - @classmethod - def run_item_crawler(cls, path_root, crawler_ext, **kwargs): - """ - Crawls the directory to find items matching the glob pattern. - - Args: - path_root (Path): Root directory to start crawling. - crawler_ext (str): Extension to match files. - - Returns: - list: List of relative file paths. - """ - return [path.relative_to(path_root).name for path in Path(path_root).iterdir() if path.is_dir()] - - def get_labels(self): - """ - Gets the labels for the dataset items. - - Returns: - array: Array of labels. - """ - return self.df['Malign'].values - - def __len__(self): - """ - Returns the number of items in the dataset. - - Returns: - int: Number of items. - """ - return len(self.item_pointers) diff --git a/application/jobs/3dcnn_ptl/app/custom/data/datasets/dataset_3d_duke.py b/application/jobs/3dcnn_ptl/app/custom/data/datasets/dataset_3d_duke.py deleted file mode 100644 index ab8a4338..00000000 --- a/application/jobs/3dcnn_ptl/app/custom/data/datasets/dataset_3d_duke.py +++ /dev/null @@ -1,105 +0,0 @@ -from pathlib import Path -import pandas as pd -from data.datasets import SimpleDataset3D - - -class DUKE_Dataset3D(SimpleDataset3D): - """ - DUKE Dataset for 3D medical images, extending SimpleDataset3D. - - Args: - path_root (str): Root directory of the dataset. - item_pointers (list, optional): List of file paths. Defaults to []. - crawler_glob (str, optional): Glob pattern for crawling files. Defaults to '*.nii.gz'. - transform (callable, optional): Transformations to apply to the data. Defaults to None. - image_resize (tuple, optional): Desired output image size. Defaults to None. - flip (bool, optional): Whether to apply random flipping. Defaults to False. - image_crop (tuple, optional): Desired crop size. Defaults to None. - norm (str, optional): Normalization method. Defaults to 'znorm_clip'. - to_tensor (bool, optional): Whether to convert images to tensor. Defaults to True. - sequence (str, optional): Sequence type to use for loading images. Defaults to 'sub'. - """ - - def __init__( - self, - path_root, - item_pointers=[], - crawler_glob='*.nii.gz', - transform=None, - image_resize=None, - flip=False, - image_crop=None, - norm='znorm_clip', - to_tensor=True, - sequence='sub' - ): - super().__init__(path_root, item_pointers, crawler_glob, transform, image_resize, flip, image_crop, norm, - to_tensor) - df = pd.read_excel(self.path_root.parent / 'Clinical_and_Other_Features.xlsx', header=[0, 1, 2]) - df = df[df[df.columns[38]] == 0] # check if cancer is bilateral=1, unilateral=0 or NC - df = df[[df.columns[0], df.columns[36], - df.columns[38]]] # Only pick relevant columns: Patient ID, Tumor Side, Bilateral - df.columns = ['PatientID', 'Location', 'Bilateral'] # Simplify columns as: Patient ID, Tumor Side - dfs = [] - existing_folders = {folder.name for folder in Path(path_root).iterdir() if folder.is_dir()} - - for side in ["left", 'right']: - dfs.append(pd.DataFrame({ - 'PatientID': df["PatientID"].str.split('_').str[2] + f"_{side}", - 'Malign': df[["Location", "Bilateral"]].apply(lambda ds: (ds[0] == side[0].upper()) | (ds[1] == 1), - axis=1) - })) - - self.df = df[df['PatientID'].isin(existing_folders)] - self.df = self.df.set_index('PatientID', drop=True) - self.df = pd.concat(dfs, ignore_index=True).set_index('PatientID', drop=True) - self.item_pointers = self.df.index[self.df.index.isin(self.item_pointers)].tolist() - self.sequence = sequence - - def __getitem__(self, index): - """ - Retrieves an item from the dataset. - - Args: - index (int): Index of the item to retrieve. - - Returns: - dict: A dictionary with 'uid', 'source', and 'target' keys. - """ - uid = self.item_pointers[index] - path_item = [self.path_root / uid / name for name in [f'{self.sequence}.nii.gz']] - img = self.load_item(path_item) - target = self.df.loc[uid]['Malign'] - return {'uid': uid, 'source': self.transform(img), 'target': target} - - @classmethod - def run_item_crawler(cls, path_root, crawler_ext, **kwargs): - """ - Crawls the directory to find items matching the glob pattern. - - Args: - path_root (Path): Root directory to start crawling. - crawler_ext (str): Extension to match files. - - Returns: - list: List of relative file paths. - """ - return [path.relative_to(path_root).name for path in Path(path_root).iterdir() if path.is_dir()] - - def get_labels(self): - """ - Gets the labels for the dataset items. - - Returns: - list: List of labels. - """ - return self.df.loc[self.item_pointers, 'Malign'].tolist() - - def __len__(self): - """ - Returns the number of items in the dataset. - - Returns: - int: Number of items. - """ - return len(self.item_pointers) diff --git a/application/jobs/3dcnn_ptl/app/custom/data/datasets/dataset_3d_duke_external.py b/application/jobs/3dcnn_ptl/app/custom/data/datasets/dataset_3d_duke_external.py deleted file mode 100755 index 3dc2d1ad..00000000 --- a/application/jobs/3dcnn_ptl/app/custom/data/datasets/dataset_3d_duke_external.py +++ /dev/null @@ -1,69 +0,0 @@ -from pathlib import Path -import pandas as pd -from data.datasets import SimpleDataset3D - -class DUKE_Dataset3D_external(SimpleDataset3D): - """ - DUKE External Dataset for 3D medical images, extending SimpleDataset3D. - - Args: - path_root (str): Root directory of the dataset. - item_pointers (list, optional): List of file paths. Defaults to None. - crawler_glob (str, optional): Glob pattern for crawling files. Defaults to '*.nii.gz'. - transform (callable, optional): Transformations to apply to the data. Defaults to None. - image_resize (tuple, optional): Desired output image size. Defaults to None. - flip (bool, optional): Whether to apply random flipping. Defaults to False. - image_crop (tuple, optional): Desired crop size. Defaults to None. - norm (str, optional): Normalization method. Defaults to 'znorm_clip'. - to_tensor (bool, optional): Whether to convert images to tensor. Defaults to True. - """ - - def __init__( - self, - path_root, - item_pointers=None, - crawler_glob='*.nii.gz', - transform=None, - image_resize=None, - flip=False, - image_crop=None, - norm='znorm_clip', - to_tensor=True - ): - if item_pointers is None: - item_pointers = [] - super().__init__(path_root, item_pointers, crawler_glob, transform, image_resize, flip, image_crop, norm, to_tensor) - df = pd.read_csv(self.path_root.parent / 'segmentation_metadata_unilateral.csv') - df = df[[df.columns[0], df.columns[5]]] # Only pick relevant columns: Patient ID, Tumor Side, Bilateral - self.df = df.set_index('PATIENT', drop=True) - self.item_pointers = self.df.index[self.df.index.isin(self.item_pointers)].tolist() - - def __getitem__(self, index): - """ - Retrieves an item from the dataset. - - Args: - index (int): Index of the item to retrieve. - - Returns: - dict: A dictionary with 'uid', 'source', and 'target' keys. - """ - uid = self.item_pointers[index] - path_item = [self.path_root / uid / name for name in ['Sub.nii.gz']] - img = self.load_item(path_item) - target = self.df.loc[uid]['Malign'] - return {'uid': uid, 'source': self.transform(img), 'target': target} - - @classmethod - def run_item_crawler(cls, path_root, crawler_ext, **kwargs): - """ - Crawls the directory to find items matching the glob pattern. - - Args: - path_root (Path): Root directory to start crawling. - crawler_ext (str): Extension to match files. - - Returns: - list: List of relative file paths. - """ - return [path.relative_to(path_root).name for path in Path(path_root).iterdir() if path.is_dir()] diff --git a/application/jobs/3dcnn_ptl/app/custom/data/datasets/simple_dataset_3d.py b/application/jobs/3dcnn_ptl/app/custom/data/datasets/simple_dataset_3d.py deleted file mode 100644 index 99a80047..00000000 --- a/application/jobs/3dcnn_ptl/app/custom/data/datasets/simple_dataset_3d.py +++ /dev/null @@ -1,138 +0,0 @@ -from pathlib import Path -import torch.utils.data as data -import torchio as tio - -from data.augmentation.augmentations_3d import ImageToTensor, RescaleIntensity, ZNormalization - - -class SimpleDataset3D(data.Dataset): - """ - A simple 3D dataset class that handles loading, transforming, and normalizing 3D medical images. - - Attributes: - path_root (Path): The root directory of the dataset. - crawler_glob (str): Glob pattern for crawling the dataset directory. - transform (callable): Transformation to be applied to the images. - item_pointers (list): List of relative paths to the dataset items. - """ - - def __init__( - self, - path_root: str, - item_pointers: list = [], - crawler_glob: str = '*.nii.gz', - transform: tio.transforms.Transform = None, - image_resize: tuple = None, - flip: bool = False, - image_crop: tuple = None, - norm: str = 'znorm_clip', - to_tensor: bool = True, - ): - """ - Initializes the dataset with the given parameters. - - Args: - path_root (str): The root directory of the dataset. - item_pointers (list, optional): List of item pointers. Defaults to []. - crawler_glob (str, optional): Glob pattern for crawling the dataset directory. Defaults to '*.nii.gz'. - transform (callable, optional): Transformation to be applied to the images. Defaults to None. - image_resize (tuple, optional): Size to resize images to. Defaults to None. - flip (bool, optional): Whether to apply random flipping. Defaults to False. - image_crop (tuple, optional): Size to crop or pad images to. Defaults to None. - norm (str, optional): Normalization method. Defaults to 'znorm_clip'. - to_tensor (bool, optional): Whether to convert images to tensors. Defaults to True. - """ - super().__init__() - self.path_root = Path(path_root) - self.crawler_glob = crawler_glob - - if transform is None: - self.transform = tio.Compose([ - tio.Resize(image_resize) if image_resize is not None else tio.Lambda(lambda x: x), - tio.RandomFlip((0, 1, 2)) if flip else tio.Lambda(lambda x: x), - tio.CropOrPad(image_crop) if image_crop is not None else tio.Lambda(lambda x: x), - self.get_norm(norm), - ImageToTensor() if to_tensor else tio.Lambda(lambda x: x) # [C, W, H, D] -> [C, D, H, W] - ]) - else: - self.transform = transform - - if len(item_pointers): - self.item_pointers = item_pointers - else: - self.item_pointers = self.run_item_crawler(self.path_root, self.crawler_glob) - - def __len__(self) -> int: - """Returns the number of items in the dataset. - - Returns: - int: Number of items in the dataset. - """ - return len(self.item_pointers) - - def __getitem__(self, index: int) -> dict: - """Gets the item at the given index. - - Args: - index (int): Index of the item. - - Returns: - dict: A dictionary with 'uid' and 'source' keys. - """ - rel_path_item = self.item_pointers[index] - path_item = self.path_root / rel_path_item - img = self.load_item(path_item) - return {'uid': str(rel_path_item), 'source': self.transform(img)} - - def load_item(self, path_item: Path) -> tio.ScalarImage: - """Loads the item from the given path. - - Args: - path_item (Path): Path to the item. - - Returns: - tio.ScalarImage: The loaded image. - """ - return tio.ScalarImage(path_item) - - @classmethod - def run_item_crawler(cls, path_root: Path, crawler_glob: str, **kwargs) -> list: - """Crawls the dataset directory and returns a list of item pointers. - - Args: - path_root (Path): Root directory of the dataset. - crawler_glob (str): Glob pattern for crawling the dataset directory. - - Returns: - list: List of relative paths to the dataset items. - """ - return [path.relative_to(path_root) for path in Path(path_root).rglob(f'{crawler_glob}')] - - @staticmethod - def get_norm(norm: str) -> tio.transforms.Transform: - """Gets the normalization transform based on the given norm type. - - Args: - norm (str): Normalization method. - - Returns: - tio.transforms.Transform: The normalization transform. - - Raises: - ValueError: If the normalization method is unknown. - """ - if norm is None: - return tio.Lambda(lambda x: x) - elif isinstance(norm, str): - if norm == 'min-max': - return RescaleIntensity((-1, 1), per_channel=True, masking_method=lambda x: x > 0) - elif norm == 'min-max_clip': - return RescaleIntensity((-1, 1), per_channel=True, percentiles=(0.5, 99.5), masking_method=lambda x: x > 0) - elif norm == 'znorm': - return ZNormalization(per_channel=True, masking_method=lambda x: x > 0) - elif norm == 'znorm_clip': - return ZNormalization(per_channel=True, percentiles=(0.5, 99.5), masking_method=lambda x: x > 0) - else: - raise ValueError("Unknown normalization") - else: - return norm diff --git a/application/jobs/3dcnn_ptl/app/custom/env_config.py b/application/jobs/3dcnn_ptl/app/custom/env_config.py deleted file mode 100755 index c108f3c3..00000000 --- a/application/jobs/3dcnn_ptl/app/custom/env_config.py +++ /dev/null @@ -1,76 +0,0 @@ -import os -from datetime import datetime - - -def load_environment_variables(): - """Load environment variables and return them as a dictionary.""" - return { - 'task_data_name': os.getenv('DATA_FOLDER', 'DUKE'), - 'scratch_dir': os.getenv('SCRATCH_DIR', '/scratch/'), - 'data_dir': os.getenv('DATA_DIR', '/data/'), - 'max_epochs': int(os.getenv('MAX_EPOCHS', 100)), - 'min_peers': int(os.getenv('MIN_PEERS', 2)), - 'max_peers': int(os.getenv('MAX_PEERS', 7)), - 'local_compare_flag': os.getenv('LOCAL_COMPARE_FLAG', 'False').lower() == 'true', - 'use_adaptive_sync': os.getenv('USE_ADAPTIVE_SYNC', 'False').lower() == 'true', - 'sync_frequency': int(os.getenv('SYNC_FREQUENCY', 1024)), - 'model_name': os.getenv('MODEL_NAME', 'ResNet50'), - 'prediction_flag': os.getenv('PREDICT_FLAG', 'ext') - } - -def load_prediction_modules(prediction_flag): - """Dynamically load prediction modules based on the prediction flag.""" - from predict import predict - return predict, prediction_flag - -def prepare_dataset(task_data_name, data_dir, site_name): - - - """Prepare the dataset based on task data name.""" - print('task_data_name: ', task_data_name) - print("Current Directory ", os.getcwd()) - - # Check if data_dir contains only DUKE_ext - try: - available_dirs = next(os.walk(data_dir))[1] # List directories directly under data_dir - except StopIteration: - print(f"No directories found under data_dir: {data_dir}") - raise ValueError("No directories found under data_dir") - if 'DUKE_ext' in available_dirs: - print("Only DUKE_ext directory found under data_dir. Setting task_data_name to DUKE_ext.") - task_data_name = "DUKE_ext" - - dataset_class = None - if task_data_name == "multi_ext": - from data.datasets import DUKE_Dataset3D_collab as dataset_class - elif task_data_name == "DUKE_ext": - from data.datasets import DUKE_Dataset3D_external as dataset_class - elif task_data_name == "DUKE": - from data.datasets import DUKE_Dataset3D as dataset_class - else: - print(f"Invalid task data name specified: {task_data_name}") - - - if dataset_class: - return dataset_class(flip=True, path_root=os.path.join(data_dir, site_name)), task_data_name - else: - raise ValueError("Invalid task data name specified") - -def generate_run_directory(scratch_dir, task_data_name, model_name, local_compare_flag): - """Generate the directory path for the current run.""" - current_time = datetime.now().strftime("%Y_%m_%d_%H%M%S") - mode = 'local_compare' if local_compare_flag else 'swarm_learning' - # make dir if not exist - if not os.path.exists(scratch_dir): - os.makedirs(scratch_dir) - return os.path.join(scratch_dir, f"{current_time}_{task_data_name}_{model_name}_{mode}") - -def cal_weightage(train_size): - estimated_full_dataset_size = 808 # exact training size of Duke 80% dataset, which is the largest across multiple nodes - weightage = int(100 * train_size / estimated_full_dataset_size) - if weightage > 100: - weightage = 100 - return weightage - -def cal_max_epochs(preset_max_epochs, weightage): - return int(preset_max_epochs / (weightage / 100)) \ No newline at end of file diff --git a/application/jobs/3dcnn_ptl/app/custom/model_selector.py b/application/jobs/3dcnn_ptl/app/custom/model_selector.py deleted file mode 100755 index f51bf0d3..00000000 --- a/application/jobs/3dcnn_ptl/app/custom/model_selector.py +++ /dev/null @@ -1,80 +0,0 @@ -from models import ResNet, VisionTransformer, EfficientNet, DenseNet121, UNet3D - - -def select_model(model_name: str): - """ - Selects and returns a model based on the provided model name. - - Args: - model_name (str): The name of the model to select. - - Returns: - nn.Module: The selected model. - - Raises: - ValueError: If an invalid model name is provided. - """ - print('Using model:', model_name) - - # Define ResNet layer configurations - resnet_layers = { - 'ResNet18': [2, 2, 2, 2], - 'ResNet34': [3, 4, 6, 3], - 'ResNet50': [3, 4, 6, 3], - 'ResNet101': [3, 4, 23, 3], - 'ResNet152': [3, 8, 36, 3], - } - - try: - if model_name in resnet_layers: - layers = resnet_layers[model_name] - model = ResNet(in_ch=1, out_ch=1, spatial_dims=3, layers=layers) - elif model_name in ['efficientnet_l1', 'efficientnet_l2', 'efficientnet_b4', 'efficientnet_b7']: - model = EfficientNet(model_name=model_name, in_ch=1, out_ch=1, spatial_dims=3) - elif model_name.startswith('EfficientNet3D'): - # Define EfficientNet3D configurations based on model_name - blocks_args_str = { - 'EfficientNet3Db0': [ - "r1_k3_s11_e1_i32_o16_se0.25", - "r2_k3_s22_e6_i16_o24_se0.25", - "r2_k5_s22_e6_i24_o40_se0.25", - "r3_k3_s22_e6_i40_o80_se0.25", - "r3_k5_s11_e6_i80_o112_se0.25", - "r4_k5_s22_e6_i112_o192_se0.25", - "r1_k3_s11_e6_i192_o320_se0.25" - ], - 'EfficientNet3Db4': [ - "r1_k3_s11_e1_i48_o24_se0.25", - "r3_k3_s22_e6_i24_o32_se0.25", - "r3_k5_s22_e6_i32_o56_se0.25", - "r4_k3_s22_e6_i56_o112_se0.25", - "r4_k5_s11_e6_i112_o160_se0.25", - "r5_k5_s22_e6_i160_o272_se0.25", - "r2_k3_s11_e6_i272_o448_se0.25" - ], - 'EfficientNet3Db7': [ - "r1_k3_s11_e1_i32_o32_se0.25", - "r4_k3_s22_e6_i32_o48_se0.25", - "r4_k5_s22_e6_i48_o80_se0.25", - "r4_k3_s22_e6_i80_o160_se0.25", - "r6_k5_s11_e6_i160_o256_se0.25", - "r6_k5_s22_e6_i256_o384_se0.25", - "r3_k3_s11_e6_i384_o640_se0.25" - ], - }[model_name[-2:]] # Extract b0, b4, b7 from model_name - model = EfficientNet(in_ch=1, out_ch=1, spatial_dims=3) - elif model_name == 'DenseNet121': - model = DenseNet121(in_ch=1, out_ch=1, spatial_dims=3) - elif model_name == 'UNet3D': - model = UNet3D(in_ch=1, out_ch=1, spatial_dims=3) - elif model_name == 'VisionTransformer': - model = VisionTransformer(in_ch=1, out_ch=1, spatial_dims=3) - else: - raise ValueError("Invalid network model specified") - - return model - except KeyError as e: - raise ValueError(f"Model configuration for {model_name} not found: {e}") - except Exception as e: - raise RuntimeError(f"Error while creating the model {model_name}: {e}") - diff --git a/application/jobs/3dcnn_ptl/app/custom/models/__init__.py b/application/jobs/3dcnn_ptl/app/custom/models/__init__.py deleted file mode 100644 index b2d2c392..00000000 --- a/application/jobs/3dcnn_ptl/app/custom/models/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -This package initializes the necessary modules and classes for the project. - -Modules: - base_model: Contains basic models including VeryBasicModel, BasicModel, and BasicClassifier. - resnet: Contains the ResNet model implementation. -""" - -from .base_model import VeryBasicModel, BasicModel, BasicClassifier -from .resnet import ResNet -from .densenet import DenseNet121 -from .efficientNet import EfficientNet -from .uNet3D import UNet3D -from .vit import VisionTransformer - -__all__ = ['VeryBasicModel', 'BasicModel', 'BasicClassifier', 'ResNet', 'DenseNet121', 'EfficientNet', 'UNet3D', 'VisionTransformer'] diff --git a/application/jobs/3dcnn_ptl/app/custom/models/base_model.py b/application/jobs/3dcnn_ptl/app/custom/models/base_model.py deleted file mode 100644 index 2c998c83..00000000 --- a/application/jobs/3dcnn_ptl/app/custom/models/base_model.py +++ /dev/null @@ -1,269 +0,0 @@ -from typing import List, Union -from pathlib import Path -import json -import torch -import torch.nn as nn -import torch.nn.functional as F -import pytorch_lightning as pl -from pytorch_lightning.utilities.cloud_io import load as pl_load -from pytorch_lightning.utilities.migration import pl_legacy_patch -from pytorch_lightning.utilities.types import EPOCH_OUTPUT -from torchmetrics import AUROC, Accuracy - - -class VeryBasicModel(pl.LightningModule): - """ - A very basic model class extending LightningModule with basic functionality. - - Attributes: - _step_train (int): Counter for training steps. - _step_val (int): Counter for validation steps. - _step_test (int): Counter for test steps. - """ - - def __init__(self): - super().__init__() - self.save_hyperparameters() - self._step_train = -1 - self._step_val = -1 - self._step_test = -1 - - def forward(self, x_in): - """Forward pass. Must be implemented by subclasses.""" - raise NotImplementedError - - def _step(self, batch: dict, batch_idx: int, state: str, step: int, optimizer_idx: int): - """Step function for training, validation, and testing. Must be implemented by subclasses.""" - raise NotImplementedError - - def _epoch_end(self, outputs: Union[EPOCH_OUTPUT, List[EPOCH_OUTPUT]], state: str): - """Epoch end function.""" - return - - def training_step(self, batch: dict, batch_idx: int, optimizer_idx: int = 0): - self._step_train += 1 - return self._step(batch, batch_idx, "train", self._step_train, optimizer_idx) - - def validation_step(self, batch: dict, batch_idx: int, optimizer_idx: int = 0): - self._step_val += 1 - return self._step(batch, batch_idx, "val", self._step_val, optimizer_idx) - - def test_step(self, batch: dict, batch_idx: int, optimizer_idx: int = 0): - self._step_test += 1 - return self._step(batch, batch_idx, "test", self._step_test, optimizer_idx) - - def training_epoch_end(self, outputs: Union[EPOCH_OUTPUT, List[EPOCH_OUTPUT]]) -> None: - self._epoch_end(outputs, "train") - return super().training_epoch_end(outputs) - - def validation_epoch_end(self, outputs: Union[EPOCH_OUTPUT, List[EPOCH_OUTPUT]]) -> None: - self._epoch_end(outputs, "val") - return super().validation_epoch_end(outputs) - - def test_epoch_end(self, outputs: Union[EPOCH_OUTPUT, List[EPOCH_OUTPUT]]) -> None: - self._epoch_end(outputs, "test") - return super().test_epoch_end(outputs) - - @classmethod - def save_best_checkpoint(cls, path_checkpoint_dir, best_model_path): - """Saves the best model checkpoint path. - - Args: - path_checkpoint_dir (str): Directory to save the checkpoint. - best_model_path (str): Path to the best model. - """ - with open(Path(path_checkpoint_dir) / 'best_checkpoint.json', 'w') as f: - json.dump({'best_model_epoch': Path(best_model_path).name}, f) - - @classmethod - def _get_best_checkpoint_path(cls, path_checkpoint_dir, version=0, **kwargs): - """Gets the best model checkpoint path. - - Args: - path_checkpoint_dir (str): Directory containing the checkpoint. - version (int, optional): Version of the checkpoint. Defaults to 0. - - Returns: - Path: Path to the best checkpoint. - """ - path_version = 'lightning_logs/version_' + str(version) - with open(Path(path_checkpoint_dir) / path_version / 'best_checkpoint.json', 'r') as f: - path_rel_best_checkpoint = Path(json.load(f)['best_model_epoch']) - return Path(path_checkpoint_dir) / path_rel_best_checkpoint - - @classmethod - def load_best_checkpoint(cls, path_checkpoint_dir, version=0, **kwargs): - """Loads the best model checkpoint. - - Args: - path_checkpoint_dir (str): Directory containing the checkpoint. - version (int, optional): Version of the checkpoint. Defaults to 0. - - Returns: - LightningModule: The loaded model. - """ - path_best_checkpoint = cls._get_best_checkpoint_path(path_checkpoint_dir, version) - return cls.load_from_checkpoint(path_best_checkpoint, **kwargs) - - def load_pretrained(self, checkpoint_path, map_location=None, **kwargs): - """Loads pretrained weights from a checkpoint. - - Args: - checkpoint_path (str): Path to the checkpoint. - map_location (str, optional): Device to map the checkpoint. Defaults to None. - - Returns: - LightningModule: The model with loaded weights. - """ - if checkpoint_path.is_dir(): - checkpoint_path = self._get_best_checkpoint_path(checkpoint_path, **kwargs) - - with pl_legacy_patch(): - if map_location is not None: - checkpoint = pl_load(checkpoint_path, map_location=map_location) - else: - checkpoint = pl_load(checkpoint_path, map_location=lambda storage, loc: storage) - return self.load_weights(checkpoint["state_dict"], **kwargs) - - def load_weights(self, pretrained_weights, strict=True, **kwargs): - """Loads weights into the model. - - Args: - pretrained_weights (dict): Pretrained weights. - strict (bool, optional): Whether to strictly enforce that the keys in `state_dict` match the keys returned by this module’s `state_dict` function. Defaults to True. - - Returns: - LightningModule: The model with loaded weights. - """ - filter_fn = kwargs.get('filter', lambda key: key in pretrained_weights) - init_weights = self.state_dict() - pretrained_weights = {key: value for key, value in pretrained_weights.items() if filter_fn(key)} - init_weights.update(pretrained_weights) - self.load_state_dict(init_weights, strict=strict) - return self - - -class BasicModel(VeryBasicModel): - """ - A basic model class with optimizer and learning rate scheduler configurations. - - Attributes: - optimizer (Optimizer): The optimizer to use. - optimizer_kwargs (dict): Keyword arguments for the optimizer. - lr_scheduler (Scheduler): The learning rate scheduler to use. - lr_scheduler_kwargs (dict): Keyword arguments for the learning rate scheduler. - """ - - def __init__( - self, - optimizer=torch.optim.AdamW, - optimizer_kwargs={'lr': 1e-3, 'weight_decay': 1e-2}, - lr_scheduler=None, - lr_scheduler_kwargs={}, - ): - super().__init__() - self.save_hyperparameters() - self.optimizer = optimizer - self.optimizer_kwargs = optimizer_kwargs - self.lr_scheduler = lr_scheduler - self.lr_scheduler_kwargs = lr_scheduler_kwargs - - def configure_optimizers(self): - """Configures the optimizers and learning rate schedulers. - - Returns: - list: List containing the optimizer and optionally the learning rate scheduler. - """ - optimizer = self.optimizer(self.parameters(), **self.optimizer_kwargs) - if self.lr_scheduler is not None: - lr_scheduler = self.lr_scheduler(optimizer, **self.lr_scheduler_kwargs) - return [optimizer], [lr_scheduler] - else: - return [optimizer] - - -class BasicClassifier(BasicModel): - """ - A basic classifier model with loss function and metrics. - - Attributes: - in_ch (int): Number of input channels. - out_ch (int): Number of output channels. - spatial_dims (int): Number of spatial dimensions. - loss (Loss): The loss function. - loss_kwargs (dict): Keyword arguments for the loss function. - auc_roc (ModuleDict): Dictionary of AUROC metrics. - acc (ModuleDict): Dictionary of Accuracy metrics. - """ - - def __init__( - self, - in_ch: int, - out_ch: int, - spatial_dims: int, - loss=torch.nn.CrossEntropyLoss, - loss_kwargs={}, - optimizer=torch.optim.AdamW, - optimizer_kwargs={'lr': 1e-3, 'weight_decay': 1e-2}, - lr_scheduler=None, - lr_scheduler_kwargs={}, - aucroc_kwargs={"task": "binary"}, - acc_kwargs={"task": "binary"} - ): - super().__init__(optimizer, optimizer_kwargs, lr_scheduler, lr_scheduler_kwargs) - self.in_ch = in_ch - self.out_ch = out_ch - self.spatial_dims = spatial_dims - self.loss = loss(**loss_kwargs) - self.loss_kwargs = loss_kwargs - - self.auc_roc = nn.ModuleDict({state: AUROC(**aucroc_kwargs) for state in ["train_", "val_", "test_"]}) - self.acc = nn.ModuleDict({state: Accuracy(**acc_kwargs) for state in ["train_", "val_", "test_"]}) - - def _step(self, batch: dict, batch_idx: int, state: str, step: int, optimizer_idx: int): - """Step function for training, validation, and testing. - - Args: - batch (dict): Input batch. - batch_idx (int): Batch index. - state (str): State of the model ('train', 'val', 'test'). - step (int): Current step. - optimizer_idx (int): Index of the optimizer. - - Returns: - Tensor: Loss value. - """ - source, target = batch['source'], batch['target'] - target = target[:, None].float() - batch_size = source.shape[0] - - # Run Model - pred = self(source) - - # Compute Loss - logging_dict = {} - logging_dict['loss'] = self.loss(pred, target) - - # Compute Metrics - with torch.no_grad(): - self.acc[state + "_"].update(pred, target) - self.auc_roc[state + "_"].update(pred, target) - - # Log Scalars - for metric_name, metric_val in logging_dict.items(): - self.log(f"{state}/{metric_name}", metric_val.cpu() if hasattr(metric_val, 'cpu') else metric_val, - batch_size=batch_size, on_step=True, on_epoch=True) - - return logging_dict['loss'] - - def _epoch_end(self, outputs, state): - """Epoch end function. - - Args: - outputs (Union[EPOCH_OUTPUT, List[EPOCH_OUTPUT]]): Outputs of the epoch. - state (str): State of the model ('train', 'val', 'test'). - """ - batch_size = len(outputs) - for name, value in [("ACC", self.acc[state + "_"]), ("AUC_ROC", self.auc_roc[state + "_"])]: - self.log(f"{state}/{name}", value.compute().cpu(), batch_size=batch_size, on_step=False, on_epoch=True) - value.reset() diff --git a/application/jobs/3dcnn_ptl/app/custom/models/densenet.py b/application/jobs/3dcnn_ptl/app/custom/models/densenet.py deleted file mode 100755 index 6ef6fa3d..00000000 --- a/application/jobs/3dcnn_ptl/app/custom/models/densenet.py +++ /dev/null @@ -1,59 +0,0 @@ -from .base_model import BasicClassifier -import monai.networks.nets as nets -import torch -import torch.nn.functional as F - -class DenseNet121(BasicClassifier): - """ - DenseNet121 model for classification tasks. - - Attributes: - model (nn.Module): The DenseNet model from MONAI. - """ - - def __init__( - self, - in_ch: int, - out_ch: int, - spatial_dims: int = 3, - loss=torch.nn.BCEWithLogitsLoss, - loss_kwargs: dict = {}, - optimizer=torch.optim.AdamW, - optimizer_kwargs: dict = {'lr': 1e-4}, - lr_scheduler=None, - lr_scheduler_kwargs: dict = {}, - aucroc_kwargs: dict = {"task": "binary"}, - acc_kwargs: dict = {"task": "binary"} - ): - """ - Initializes the DenseNet121 model with the given parameters. - - Args: - in_ch (int): Number of input channels. - out_ch (int): Number of output channels. - spatial_dims (int, optional): Number of spatial dimensions. Defaults to 3. - loss (callable, optional): Loss function. Defaults to torch.nn.BCEWithLogitsLoss. - loss_kwargs (dict, optional): Keyword arguments for the loss function. Defaults to {}. - optimizer (Optimizer, optional): Optimizer. Defaults to torch.optim.AdamW. - optimizer_kwargs (dict, optional): Keyword arguments for the optimizer. Defaults to {'lr': 1e-4}. - lr_scheduler (Scheduler, optional): Learning rate scheduler. Defaults to None. - lr_scheduler_kwargs (dict, optional): Keyword arguments for the learning rate scheduler. Defaults to {}. - aucroc_kwargs (dict, optional): Keyword arguments for AUROC. Defaults to {"task": "binary"}. - acc_kwargs (dict, optional): Keyword arguments for Accuracy. Defaults to {"task": "binary"}. - """ - super().__init__(in_ch, out_ch, spatial_dims, loss, loss_kwargs, optimizer, optimizer_kwargs, lr_scheduler, - lr_scheduler_kwargs, aucroc_kwargs, acc_kwargs) - self.model = nets.DenseNet264(spatial_dims=spatial_dims, in_channels=in_ch, out_channels=out_ch) - - def forward(self, x_in: torch.Tensor, **kwargs) -> torch.Tensor: - """ - Forward pass of the DenseNet121 model. - - Args: - x_in (torch.Tensor): Input tensor. - - Returns: - torch.Tensor: Output tensor. - """ - pred_hor = self.model(x_in) - return pred_hor diff --git a/application/jobs/3dcnn_ptl/app/custom/models/efficientNet.py b/application/jobs/3dcnn_ptl/app/custom/models/efficientNet.py deleted file mode 100755 index 33c20c0d..00000000 --- a/application/jobs/3dcnn_ptl/app/custom/models/efficientNet.py +++ /dev/null @@ -1,243 +0,0 @@ -from .base_model import BasicClassifier -import torch -import torch.nn.functional as F -import timm -import monai.networks.nets as nets - -class EfficientNet(BasicClassifier): - """ - EfficientNet model for 2D classification tasks. - - Attributes: - model (nn.Module): The EfficientNet model from TIMM. - """ - - def __init__( - self, - in_ch: int, - out_ch: int, - spatial_dims: int = 3, - model_name: str = 'efficientnet_l2', - pretrained: bool = False, - loss=torch.nn.BCEWithLogitsLoss, - loss_kwargs: dict = {}, - optimizer=torch.optim.AdamW, - optimizer_kwargs: dict = {'lr': 1e-4}, - lr_scheduler=None, - lr_scheduler_kwargs: dict = {}, - aucroc_kwargs: dict = {"task": "binary"}, - acc_kwargs: dict = {"task": "binary"} - ): - """ - Initializes the EfficientNet model with the given parameters. - - Args: - in_ch (int): Number of input channels. - out_ch (int): Number of output channels. - spatial_dims (int, optional): Number of spatial dimensions. Defaults to 3. - model_name (str, optional): Name of the EfficientNet model. Defaults to 'efficientnet_l2'. - pretrained (bool, optional): Whether to use pretrained weights. Defaults to False. - loss (callable, optional): Loss function. Defaults to torch.nn.BCEWithLogitsLoss. - loss_kwargs (dict, optional): Keyword arguments for the loss function. Defaults to {}. - optimizer (Optimizer, optional): Optimizer. Defaults to torch.optim.AdamW. - optimizer_kwargs (dict, optional): Keyword arguments for the optimizer. Defaults to {'lr': 1e-4}. - lr_scheduler (Scheduler, optional): Learning rate scheduler. Defaults to None. - lr_scheduler_kwargs (dict, optional): Keyword arguments for the learning rate scheduler. Defaults to {}. - aucroc_kwargs (dict, optional): Keyword arguments for AUROC. Defaults to {"task": "binary"}. - acc_kwargs (dict, optional): Keyword arguments for Accuracy. Defaults to {"task": "binary"}. - """ - super().__init__(in_ch, out_ch, spatial_dims, loss, loss_kwargs, optimizer, optimizer_kwargs, lr_scheduler, - lr_scheduler_kwargs, aucroc_kwargs, acc_kwargs) - self.model = timm.create_model(model_name, pretrained=pretrained, in_chans=in_ch, num_classes=out_ch) - - def forward(self, x_in: torch.Tensor, **kwargs) -> torch.Tensor: - """ - Forward pass of the EfficientNet model. - - Args: - x_in (torch.Tensor): Input tensor. - - Returns: - torch.Tensor: Output tensor. - """ - batch_size, _, num_slices, height, width = x_in.shape - x_in = x_in.view(batch_size * num_slices, 1, height, width) # Reshape to [batch_size * num_slices, 1, height, width] - - pred_hor = self.model(x_in) # Process each slice with EfficientNet - - # Reshape the output back to [batch_size, num_slices, out_ch] - out_ch = pred_hor.shape[1] - pred_hor = pred_hor.view(batch_size, num_slices, out_ch) - - # Combine the results from each slice (e.g., by averaging or max-pooling) - combined_pred = torch.mean(pred_hor, dim=1) - - return combined_pred - - -class EfficientNet3D(BasicClassifier): - """ - EfficientNet model for 3D classification tasks. - - Attributes: - model (nn.Module): The EfficientNet model from MONAI. - """ - - def __init__( - self, - in_ch: int, - out_ch: int, - spatial_dims: int = 3, - blocks_args_str: list = None, - width_coefficient: float = 1.0, - depth_coefficient: float = 1.0, - dropout_rate: float = 0.2, - image_size: int = 224, - norm: tuple = ('batch', {'eps': 0.001, 'momentum': 0.01}), - drop_connect_rate: float = 0.2, - depth_divisor: int = 8, - loss=torch.nn.BCEWithLogitsLoss, - loss_kwargs: dict = {}, - optimizer=torch.optim.AdamW, - optimizer_kwargs: dict = {'lr': 1e-4}, - lr_scheduler=None, - lr_scheduler_kwargs: dict = {}, - aucroc_kwargs: dict = {"task": "binary"}, - acc_kwargs: dict = {"task": "binary"} - ): - """ - Initializes the EfficientNet3D model with the given parameters. - - Args: - in_ch (int): Number of input channels. - out_ch (int): Number of output channels. - spatial_dims (int, optional): Number of spatial dimensions. Defaults to 3. - blocks_args_str (list, optional): List of block arguments. Defaults to None. - width_coefficient (float, optional): Width coefficient for EfficientNet. Defaults to 1.0. - depth_coefficient (float, optional): Depth coefficient for EfficientNet. Defaults to 1.0. - dropout_rate (float, optional): Dropout rate. Defaults to 0.2. - image_size (int, optional): Image size. Defaults to 224. - norm (tuple, optional): Normalization configuration. Defaults to ('batch', {'eps': 0.001, 'momentum': 0.01}). - drop_connect_rate (float, optional): Drop connect rate. Defaults to 0.2. - depth_divisor (int, optional): Depth divisor. Defaults to 8. - loss (callable, optional): Loss function. Defaults to torch.nn.BCEWithLogitsLoss. - loss_kwargs (dict, optional): Keyword arguments for the loss function. Defaults to {}. - optimizer (Optimizer, optional): Optimizer. Defaults to torch.optim.AdamW. - optimizer_kwargs (dict, optional): Keyword arguments for the optimizer. Defaults to {'lr': 1e-4}. - lr_scheduler (Scheduler, optional): Learning rate scheduler. Defaults to None. - lr_scheduler_kwargs (dict, optional): Keyword arguments for the learning rate scheduler. Defaults to {}. - aucroc_kwargs (dict, optional): Keyword arguments for AUROC. Defaults to {"task": "binary"}. - acc_kwargs (dict, optional): Keyword arguments for Accuracy. Defaults to {"task": "binary"}. - """ - super().__init__(in_ch, out_ch, spatial_dims, loss, loss_kwargs, optimizer, optimizer_kwargs, lr_scheduler, - lr_scheduler_kwargs, aucroc_kwargs, acc_kwargs) - if blocks_args_str is None: - blocks_args_str = [ - "r1_k3_s11_e1_i32_o16_se0.25", - "r2_k3_s22_e6_i16_o24_se0.25", - "r2_k5_s22_e6_i24_o40_se0.25", - "r3_k3_s22_e6_i40_o80_se0.25", - "r3_k5_s11_e6_i80_o112_se0.25", - "r4_k5_s22_e6_i112_o192_se0.25", - "r1_k3_s11_e6_i192_o320_se0.25"] - self.model = nets.EfficientNet(blocks_args_str, spatial_dims, in_ch, out_ch, - width_coefficient, depth_coefficient, dropout_rate, - image_size, norm, drop_connect_rate, depth_divisor) - - def forward(self, x_in: torch.Tensor, **kwargs) -> torch.Tensor: - """ - Forward pass of the EfficientNet3D model. - - Args: - x_in (torch.Tensor): Input tensor. - - Returns: - torch.Tensor: Output tensor. - """ - pred_hor = self.model(x_in) - return pred_hor - - -class EfficientNet3Db7(BasicClassifier): - """ - EfficientNetB7 model for 3D classification tasks. - - Attributes: - model (nn.Module): The EfficientNetB7 model from MONAI. - """ - - def __init__( - self, - in_ch: int, - out_ch: int, - spatial_dims: int = 3, - blocks_args_str: list = None, - width_coefficient: float = 1.0, - depth_coefficient: float = 1.0, - dropout_rate: float = 0.2, - image_size: int = 224, - norm: tuple = ('batch', {'eps': 0.001, 'momentum': 0.01}), - drop_connect_rate: float = 0.2, - depth_divisor: int = 8, - loss=torch.nn.BCEWithLogitsLoss, - loss_kwargs: dict = {}, - optimizer=torch.optim.AdamW, - optimizer_kwargs: dict = {'lr': 1e-4}, - lr_scheduler=None, - lr_scheduler_kwargs: dict = {}, - aucroc_kwargs: dict = {"task": "binary"}, - acc_kwargs: dict = {"task": "binary"} - ): - """ - Initializes the EfficientNet3Db7 model with the given parameters. - - Args: - in_ch (int): Number of input channels. - out_ch (int): Number of output channels. - spatial_dims (int, optional): Number of spatial dimensions. Defaults to 3. - blocks_args_str (list, optional): List of block arguments. Defaults to None. - width_coefficient (float, optional): Width coefficient for EfficientNet. Defaults to 1.0. - depth_coefficient (float, optional): Depth coefficient for EfficientNet. Defaults to 1.0. - dropout_rate (float, optional): Dropout rate. Defaults to 0.2. - image_size (int, optional): Image size. Defaults to 224. - norm (tuple, optional): Normalization configuration. Defaults to ('batch', {'eps': 0.001, 'momentum': 0.01}). - drop_connect_rate (float, optional): Drop connect rate. Defaults to 0.2. - depth_divisor (int, optional): Depth divisor. Defaults to 8. - loss (callable, optional): Loss function. Defaults to torch.nn.BCEWithLogitsLoss. - loss_kwargs (dict, optional): Keyword arguments for the loss function. Defaults to {}. - optimizer (Optimizer, optional): Optimizer. Defaults to torch.optim.AdamW. - optimizer_kwargs (dict, optional): Keyword arguments for the optimizer. Defaults to {'lr': 1e-4}. - lr_scheduler (Scheduler, optional): Learning rate scheduler. Defaults to None. - lr_scheduler_kwargs (dict, optional): Keyword arguments for the learning rate scheduler. Defaults to {}. - aucroc_kwargs (dict, optional): Keyword arguments for AUROC. Defaults to {"task": "binary"}. - acc_kwargs (dict, optional): Keyword arguments for Accuracy. Defaults to {"task": "binary"}. - """ - super().__init__(in_ch, out_ch, spatial_dims, loss, loss_kwargs, optimizer, optimizer_kwargs, lr_scheduler, - lr_scheduler_kwargs, aucroc_kwargs, acc_kwargs) - if blocks_args_str is None: - blocks_args_str = [ - "r1_k3_s11_e1_i32_o32_se0.25", - "r4_k3_s22_e6_i32_o48_se0.25", - "r4_k5_s22_e6_i48_o80_se0.25", - "r4_k3_s22_e6_i80_o160_se0.25", - "r6_k5_s11_e6_i160_o256_se0.25", - "r6_k5_s22_e6_i256_o384_se0.25", - "r3_k3_s11_e6_i384_o640_se0.25", - ] - - self.model = nets.EfficientNet(blocks_args_str, spatial_dims, in_ch, out_ch, - width_coefficient, depth_coefficient, dropout_rate, - image_size, norm, drop_connect_rate, depth_divisor) - - def forward(self, x_in: torch.Tensor, **kwargs) -> torch.Tensor: - """ - Forward pass of the EfficientNet3Db7 model. - - Args: - x_in (torch.Tensor): Input tensor. - - Returns: - torch.Tensor: Output tensor. - """ - pred_hor = self.model(x_in) - return pred_hor diff --git a/application/jobs/3dcnn_ptl/app/custom/models/resnet.py b/application/jobs/3dcnn_ptl/app/custom/models/resnet.py deleted file mode 100644 index fd94a674..00000000 --- a/application/jobs/3dcnn_ptl/app/custom/models/resnet.py +++ /dev/null @@ -1,68 +0,0 @@ -from models.base_model import BasicClassifier -import monai.networks.nets as nets -import torch - - -class ResNet(BasicClassifier): - """ - ResNet model for classification tasks. - - Attributes: - model (nn.Module): The ResNet model from MONAI. - """ - - def __init__( - self, - in_ch: int, - out_ch: int, - spatial_dims: int = 3, - block: str = 'basic', - layers: list = [3, 4, 6, 3], - block_inplanes: list = [64, 128, 256, 512], - feed_forward: bool = True, - loss=torch.nn.BCEWithLogitsLoss, - loss_kwargs: dict = {}, - optimizer=torch.optim.AdamW, - optimizer_kwargs: dict = {'lr': 1e-4}, - lr_scheduler=None, - lr_scheduler_kwargs: dict = {}, - aucroc_kwargs: dict = {"task": "binary"}, - acc_kwargs: dict = {"task": "binary"} - ): - """ - Initializes the ResNet model with the given parameters. - - Args: - in_ch (int): Number of input channels. - out_ch (int): Number of output channels. - spatial_dims (int, optional): Number of spatial dimensions. Defaults to 3. - block (str, optional): Block type for ResNet. Defaults to 'basic'. - layers (list, optional): List of layer configurations. Defaults to [3, 4, 6, 3]. - block_inplanes (list, optional): List of block in-plane sizes. Defaults to [64, 128, 256, 512]. - feed_forward (bool, optional): Whether to use feed forward. Defaults to True. - loss (callable, optional): Loss function. Defaults to torch.nn.BCEWithLogitsLoss. - loss_kwargs (dict, optional): Keyword arguments for the loss function. Defaults to {}. - optimizer (Optimizer, optional): Optimizer. Defaults to torch.optim.AdamW. - optimizer_kwargs (dict, optional): Keyword arguments for the optimizer. Defaults to {'lr': 1e-4}. - lr_scheduler (Scheduler, optional): Learning rate scheduler. Defaults to None. - lr_scheduler_kwargs (dict, optional): Keyword arguments for the learning rate scheduler. Defaults to {}. - aucroc_kwargs (dict, optional): Keyword arguments for AUROC. Defaults to {"task": "binary"}. - acc_kwargs (dict, optional): Keyword arguments for Accuracy. Defaults to {"task": "binary"}. - """ - super().__init__(in_ch, out_ch, spatial_dims, loss, loss_kwargs, optimizer, optimizer_kwargs, lr_scheduler, - lr_scheduler_kwargs, aucroc_kwargs, acc_kwargs) - self.model = nets.ResNet( - block, layers, block_inplanes, spatial_dims, in_ch, 7, 1, False, 'B', 1.0, out_ch, feed_forward, True - ) - - def forward(self, x_in: torch.Tensor, **kwargs) -> torch.Tensor: - """ - Forward pass of the ResNet model. - - Args: - x_in (torch.Tensor): Input tensor. - - Returns: - torch.Tensor: Output tensor. - """ - return self.model(x_in) diff --git a/application/jobs/3dcnn_ptl/app/custom/models/uNet3D.py b/application/jobs/3dcnn_ptl/app/custom/models/uNet3D.py deleted file mode 100755 index aba13554..00000000 --- a/application/jobs/3dcnn_ptl/app/custom/models/uNet3D.py +++ /dev/null @@ -1,147 +0,0 @@ -from .base_model import BasicClassifier -import monai.networks.nets as nets -import torch - -class UNet3D(BasicClassifier): - """ - UNet3D model for 3D segmentation tasks. - - Attributes: - model (nn.Module): The UNet3D model from MONAI. - """ - - def __init__( - self, - in_ch: int, - out_ch: int, - spatial_dims: int = 3, - channels: tuple = (16, 32, 64, 128, 256), - strides: tuple = (2, 2, 2, 2), - num_res_units: int = 2, - loss=torch.nn.BCEWithLogitsLoss, - loss_kwargs: dict = {}, - optimizer=torch.optim.AdamW, - optimizer_kwargs: dict = {'lr': 1e-4}, - lr_scheduler=None, - lr_scheduler_kwargs: dict = {}, - aucroc_kwargs: dict = {"task": "binary"}, - acc_kwargs: dict = {"task": "binary"} - ): - """ - Initializes the UNet3D model with the given parameters. - - Args: - in_ch (int): Number of input channels. - out_ch (int): Number of output channels. - spatial_dims (int, optional): Number of spatial dimensions. Defaults to 3. - channels (tuple, optional): Tuple of channel sizes. Defaults to (16, 32, 64, 128, 256). - strides (tuple, optional): Tuple of stride sizes. Defaults to (2, 2, 2, 2). - num_res_units (int, optional): Number of residual units. Defaults to 2. - loss (callable, optional): Loss function. Defaults to torch.nn.BCEWithLogitsLoss. - loss_kwargs (dict, optional): Keyword arguments for the loss function. Defaults to {}. - optimizer (Optimizer, optional): Optimizer. Defaults to torch.optim.AdamW. - optimizer_kwargs (dict, optional): Keyword arguments for the optimizer. Defaults to {'lr': 1e-4}. - lr_scheduler (Scheduler, optional): Learning rate scheduler. Defaults to None. - lr_scheduler_kwargs (dict, optional): Keyword arguments for the learning rate scheduler. Defaults to {}. - aucroc_kwargs (dict, optional): Keyword arguments for AUROC. Defaults to {"task": "binary"}. - acc_kwargs (dict, optional): Keyword arguments for Accuracy. Defaults to {"task": "binary"}. - """ - super().__init__(in_ch, out_ch, spatial_dims, loss, loss_kwargs, optimizer, optimizer_kwargs, lr_scheduler, - lr_scheduler_kwargs, aucroc_kwargs, acc_kwargs) - self.model = nets.UNet( - dimensions=spatial_dims, - in_channels=in_ch, - out_channels=out_ch, - channels=channels, - strides=strides, - num_res_units=num_res_units - ) - - def forward(self, x_in: torch.Tensor, **kwargs) -> torch.Tensor: - """ - Forward pass of the UNet3D model. - - Args: - x_in (torch.Tensor): Input tensor. - - Returns: - torch.Tensor: Output tensor. - """ - pred_hor = self.model(x_in) - return pred_hor - - def _generate_predictions(self, source: torch.Tensor) -> torch.Tensor: - """ - Generates predictions for the given input tensor. - - Args: - source (torch.Tensor): Input tensor. - - Returns: - torch.Tensor: Predicted tensor. - """ - return self.forward(source) - - def _step(self, batch: dict, batch_idx: int, phase: str, optimizer_idx: int = 0) -> torch.Tensor: - """ - Performs a step in the training or validation phase. - - Args: - batch (dict): Input batch. - batch_idx (int): Batch index. - phase (str): Current phase ('train' or 'val'). - optimizer_idx (int, optional): Index of the optimizer. Defaults to 0. - - Returns: - torch.Tensor: Loss value. - """ - source, target = batch['source'], batch['target'] - - if phase == "train": - pred = self._generate_predictions(source) - elif phase == "val": - pred = self._generate_predictions(source) - else: - raise ValueError(f"Invalid phase: {phase}") - - target = target.unsqueeze(-1).unsqueeze(-1).unsqueeze(-1).expand_as(pred).float() # Cast target to float - loss = self.loss(pred, target) - - logging_dict = {f"{phase}_loss": loss} - - if phase == "val": - logging_dict["y_true"] = target - logging_dict["y_pred"] = pred - - logging_dict = {k: v.mean() for k, v in logging_dict.items()} # Add this line before logging - self.log_dict(logging_dict, on_step=(phase == "train"), on_epoch=True, prog_bar=True, logger=True) - - return loss - - def validation_step(self, batch: dict, batch_idx: int, optimizer_idx: int = 0) -> torch.Tensor: - """ - Performs a step in the validation phase. - - Args: - batch (dict): Input batch. - batch_idx (int): Batch index. - optimizer_idx (int, optional): Index of the optimizer. Defaults to 0. - - Returns: - torch.Tensor: Loss value. - """ - return self._step(batch, batch_idx, "val", optimizer_idx) - - def training_step(self, batch: dict, batch_idx: int, optimizer_idx: int = 0) -> torch.Tensor: - """ - Performs a step in the training phase. - - Args: - batch (dict): Input batch. - batch_idx (int): Batch index. - optimizer_idx (int, optional): Index of the optimizer. Defaults to 0. - - Returns: - torch.Tensor: Loss value. - """ - return self._step(batch, batch_idx, "train", optimizer_idx) diff --git a/application/jobs/3dcnn_ptl/app/custom/models/vit.py b/application/jobs/3dcnn_ptl/app/custom/models/vit.py deleted file mode 100755 index 92bba936..00000000 --- a/application/jobs/3dcnn_ptl/app/custom/models/vit.py +++ /dev/null @@ -1,128 +0,0 @@ -from .base_model import BasicClassifier -import torch -import torch.nn as nn -import torch.nn.functional as F -import timm -from timm.models.vision_transformer import VisionTransformer as TimmVisionTransformer - -class VisionTransformer(BasicClassifier): - """ - VisionTransformer model for 3D classification tasks. - - Attributes: - model (nn.Module): The VisionTransformer3D model. - """ - - def __init__( - self, - in_ch: int, - out_ch: int, - spatial_dims: int = 3, - model_name: str = 'vit_base_patch16_224', - pretrained: bool = False, - loss=torch.nn.BCEWithLogitsLoss, - loss_kwargs: dict = {}, - optimizer=torch.optim.AdamW, - optimizer_kwargs: dict = {'lr': 1e-4}, - lr_scheduler=None, - lr_scheduler_kwargs: dict = {}, - aucroc_kwargs: dict = {"task": "binary"}, - acc_kwargs: dict = {"task": "binary"} - ): - """ - Initializes the VisionTransformer model with the given parameters. - - Args: - in_ch (int): Number of input channels. - out_ch (int): Number of output channels. - spatial_dims (int, optional): Number of spatial dimensions. Defaults to 3. - model_name (str, optional): Name of the VisionTransformer model. Defaults to 'vit_base_patch16_224'. - pretrained (bool, optional): Whether to use pretrained weights. Defaults to False. - loss (callable, optional): Loss function. Defaults to torch.nn.BCEWithLogitsLoss. - loss_kwargs (dict, optional): Keyword arguments for the loss function. Defaults to {}. - optimizer (Optimizer, optional): Optimizer. Defaults to torch.optim.AdamW. - optimizer_kwargs (dict, optional): Keyword arguments for the optimizer. Defaults to {'lr': 1e-4}. - lr_scheduler (Scheduler, optional): Learning rate scheduler. Defaults to None. - lr_scheduler_kwargs (dict, optional): Keyword arguments for the learning rate scheduler. Defaults to {}. - aucroc_kwargs (dict, optional): Keyword arguments for AUROC. Defaults to {"task": "binary"}. - acc_kwargs (dict, optional): Keyword arguments for Accuracy. Defaults to {"task": "binary"}. - """ - super().__init__(in_ch, out_ch, spatial_dims, loss, loss_kwargs, optimizer, optimizer_kwargs, lr_scheduler, - lr_scheduler_kwargs, aucroc_kwargs, acc_kwargs) - self.model = VisionTransformer3D(model_name, pretrained=pretrained, in_chans=in_ch, num_classes=out_ch) - - def forward(self, x_in: torch.Tensor, **kwargs) -> torch.Tensor: - """ - Forward pass of the VisionTransformer model. - - Args: - x_in (torch.Tensor): Input tensor. - - Returns: - torch.Tensor: Output tensor. - """ - print(x_in.shape) - pred_hor = self.model(x_in) - print(pred_hor.shape) - pred_hor = self.model(x_in) - return pred_hor - -class PatchEmbed3D(nn.Module): - """ - 3D Patch Embedding module for Vision Transformer. - - Attributes: - proj (nn.Module): The convolutional projection layer. - """ - - def __init__(self, in_chans: int, embed_dim: int, patch_size: tuple): - """ - Initializes the PatchEmbed3D module. - - Args: - in_chans (int): Number of input channels. - embed_dim (int): Embedding dimension. - patch_size (tuple): Size of the patches. - """ - super().__init__() - self.proj = nn.Sequential( - nn.Conv3d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size), - nn.Flatten(2) - ) - - def forward(self, x: torch.Tensor) -> torch.Tensor: - """ - Forward pass of the PatchEmbed3D module. - - Args: - x (torch.Tensor): Input tensor. - - Returns: - torch.Tensor: Output tensor. - """ - B, C, D, H, W = x.shape - x = self.proj(x) - x = x.transpose(1, 2) - return x - -class VisionTransformer3D(TimmVisionTransformer): - """ - 3D Vision Transformer model extending TimmVisionTransformer. - - Attributes: - patch_embed (nn.Module): The 3D Patch Embedding module. - """ - - def __init__(self, *args, **kwargs): - """ - Initializes the VisionTransformer3D model with the given parameters. - - Args: - *args: Positional arguments for the TimmVisionTransformer. - **kwargs: Keyword arguments for the TimmVisionTransformer. - """ - super().__init__(*args, **kwargs) - in_chans = kwargs.get("in_chans", 3) - embed_dim = kwargs.get("embed_dim", 768) - patch_size = kwargs.get("patch_size", (2, 16, 16)) - self.patch_embed = PatchEmbed3D(in_chans, embed_dim, patch_size) diff --git a/application/jobs/3dcnn_ptl/app/custom/predict.py b/application/jobs/3dcnn_ptl/app/custom/predict.py deleted file mode 100755 index 3249bc69..00000000 --- a/application/jobs/3dcnn_ptl/app/custom/predict.py +++ /dev/null @@ -1,232 +0,0 @@ -#!/usr/bin/env python3 - -import torch -import numpy as np -from pathlib import Path -import logging -from tqdm import tqdm -from sklearn.metrics import confusion_matrix, f1_score, precision_recall_curve, average_precision_score -import matplotlib.pyplot as plt -import seaborn as sns -import pandas as pd -from data.datasets import DUKE_Dataset3D, DUKE_Dataset3D_external, DUKE_Dataset3D_collab -from data.datamodules import DataModule -from utils.roc_curve import plot_roc_curve, cm2acc, cm2x -from models import ResNet, VisionTransformer, EfficientNet, DenseNet121, UNet3D - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -def predict(model_dir, test_data_dir, model_name, last_flag, prediction_flag, cohort_flag='aachen'): - """ - Predicts and evaluates the model on the test dataset. - - Args: - model_dir (str): Directory containing the model. - test_data_dir (str): Directory containing the test data. - model_name (str): Name of the model to use. - last_flag (bool): Whether to use the last checkpoint or the best checkpoint. - prediction_flag (str): Flag to indicate which dataset to use ('ext', 'internal', 'collab'). - cohort_flag (str, optional): Cohort flag for the output directory name. Defaults to 'aachen'. - """ - try: - path_run = Path(model_dir) - path_out = Path(path_run, f"{prediction_flag}_{cohort_flag}") - logger.info(f"Output path: {path_out.absolute()}") - path_out.mkdir(parents=True, exist_ok=True) - device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - fontdict = {'fontsize': 10, 'fontweight': 'bold'} - - # Load Data - if prediction_flag == 'ext': - ds = DUKE_Dataset3D_external(flip=False, path_root=test_data_dir) - elif prediction_flag == 'internal': - ds = DUKE_Dataset3D(flip=False, path_root=test_data_dir) - elif prediction_flag == 'collab': - ds = DUKE_Dataset3D_collab(flip=False, path_root=test_data_dir) - else: - raise ValueError("Invalid prediction_flag specified") - - logger.info(f"Number of test samples: {len(ds)}") - dm = DataModule(ds_test=ds, batch_size=1) - - # Initialize Model - model = initialize_model(model_name, path_run, last_flag) - model.to(device) - model.eval() - - results = {'uid': [], 'GT': [], 'NN': [], 'NN_pred': []} - threshold = 0.5 - - for batch in tqdm(dm.test_dataloader()): - source, target = batch['source'], batch['target'] - - # Run Model - pred = model(source.to(device)).cpu() - pred_proba = torch.sigmoid(pred).squeeze() - pred_binary = (pred_proba > threshold).long() - - results['GT'].extend(target.tolist()) - results['NN'].extend(pred_binary.tolist() if isinstance(pred_binary.tolist(), list) else [pred_binary.tolist()]) - results['NN_pred'].extend(pred_proba.tolist() if isinstance(pred_proba.tolist(), list) else [pred_proba.tolist()]) - results['uid'].extend(batch['uid']) - - df = pd.DataFrame(results) - save_results(df, path_out, last_flag) - evaluate_results(df, path_out, last_flag, fontdict) - - del model - torch.cuda.empty_cache() - except Exception as e: - logger.error(f"Error in predict function: {e}") - raise - -def initialize_model(model_name, path_run, last_flag): - """ - Initializes the model based on the provided model name. - - Args: - model_name (str): Name of the model to initialize. - path_run (Path): Path to the model directory. - last_flag (bool): Whether to use the last checkpoint or the best checkpoint. - - Returns: - nn.Module: The initialized model. - """ - try: - layers = None - if model_name in ['ResNet18', 'ResNet34', 'ResNet50', 'ResNet101', 'ResNet152']: - layers = {'ResNet18': [2, 2, 2, 2], 'ResNet34': [3, 4, 6, 3], 'ResNet50': [3, 4, 6, 3], 'ResNet101': [3, 4, 23, 3], 'ResNet152': [3, 8, 36, 3]}[model_name] - if last_flag: - return ResNet.load_last_checkpoint(path_run, version=0, layers=layers) - return ResNet.load_best_checkpoint(path_run, version=0, layers=layers) - - if model_name in ['efficientnet_l1', 'efficientnet_l2', 'efficientnet_b4', 'efficientnet_b7']: - if last_flag: - return EfficientNet.load_last_checkpoint(path_run, version=0, model_name=model_name) - return EfficientNet.load_best_checkpoint(path_run, version=0, model_name=model_name) - - if model_name.startswith('EfficientNet3D'): - blocks_args_str = { - 'EfficientNet3Db0': ["r1_k3_s11_e1_i32_o16_se0.25", "r2_k3_s22_e6_i16_o24_se0.25", "r2_k5_s22_e6_i24_o40_se0.25", "r3_k3_s22_e6_i40_o80_se0.25", "r3_k5_s11_e6_i80_o112_se0.25", "r4_k5_s22_e6_i112_o192_se0.25", "r1_k3_s11_e6_i192_o320_se0.25"], - 'EfficientNet3Db4': ["r1_k3_s11_e1_i48_o24_se0.25", "r3_k3_s22_e6_i24_o32_se0.25", "r3_k5_s22_e6_i32_o56_se0.25", "r4_k3_s22_e6_i56_o112_se0.25", "r4_k5_s11_e6_i112_o160_se0.25", "r5_k5_s22_e6_i160_o272_se0.25", "r2_k3_s11_e6_i272_o448_se0.25"], - 'EfficientNet3Db7': ["r1_k3_s11_e1_i32_o32_se0.25", "r4_k3_s22_e6_i32_o48_se0.25", "r4_k5_s22_e6_i48_o80_se0.25", "r4_k3_s22_e6_i80_o160_se0.25", "r6_k5_s11_e6_i160_o256_se0.25", "r6_k5_s22_e6_i256_o384_se0.25", "r3_k3_s11_e6_i384_o640_se0.25"] - }[model_name] - if last_flag: - return EfficientNet3D.load_last_checkpoint(path_run, version=0, blocks_args_str=blocks_args_str) - return EfficientNet3D.load_best_checkpoint(path_run, version=0, blocks_args_str=blocks_args_str) - - if model_name == 'DenseNet121': - if last_flag: - return DenseNet121.load_last_checkpoint(path_run, version=0) - return DenseNet121.load_best_checkpoint(path_run, version=0) - - if model_name == 'UNet3D': - if last_flag: - return UNet3D.load_last_checkpoint(path_run, version=0) - return UNet3D.load_best_checkpoint(path_run, version=0) - - raise ValueError("Invalid network model specified") - except Exception as e: - logger.error(f"Error in initialize_model function: {e}") - raise - -def save_results(df, path_out, last_flag): - """ - Saves the prediction results to a CSV file. - - Args: - df (pd.DataFrame): DataFrame containing the results. - path_out (Path): Path to the output directory. - last_flag (bool): Whether to save results for the last checkpoint or the best checkpoint. - """ - try: - file_name = 'results_last.csv' if last_flag else 'results.csv' - df.to_csv(path_out / file_name, index=False) - except Exception as e: - logger.error(f"Error in save_results function: {e}") - raise - -def evaluate_results(df, path_out, last_flag, fontdict): - """ - Evaluates the prediction results and saves metrics and plots. - - Args: - df (pd.DataFrame): DataFrame containing the results. - path_out (Path): Path to the output directory. - last_flag (bool): Whether to save results for the last checkpoint or the best checkpoint. - fontdict (dict): Font dictionary for plot titles and labels. - """ - try: - f1 = f1_score(df['GT'], df['NN']) - logger.info(f"F1 Score: {f1:.2f}") - - cm = confusion_matrix(df['GT'], df['NN']) - tn, fp, fn, tp = cm.ravel() - n = len(df) - logger.info(f"Confusion Matrix: TN {tn} ({tn / n * 100:.2f}%), FP {fp} ({fp / n * 100:.2f}%), FN {fn} ({fn / n * 100:.2f}%), TP {tp} ({tp / n * 100:.2f}%)") - - fig, axis = plt.subplots(ncols=1, nrows=1, figsize=(6, 6)) - y_pred_lab = np.asarray(df['NN_pred']) - y_true_lab = np.asarray(df['GT']) - tprs, fprs, auc_val, thrs, opt_idx, cm = plot_roc_curve(y_true_lab, y_pred_lab, axis, fontdict=fontdict) - fig.tight_layout() - file_name = 'roc_last.png' if last_flag else 'roc.png' - fig.savefig(path_out / file_name, dpi=300) - - precision, recall, _ = precision_recall_curve(y_true_lab, y_pred_lab) - ap = average_precision_score(y_true_lab, y_pred_lab) - - ppv = tp / (tp + fp) - npv = tn / (tn + fn) - - acc = cm2acc(cm) - _, _, sens, spec = cm2x(cm) - df_cm = pd.DataFrame(data=cm, columns=['False', 'True'], index=['False', 'True']) - fig, axis = plt.subplots(1, 1, figsize=(4, 4)) - sns.heatmap(df_cm, ax=axis, cbar=False, fmt='d', annot=True) - axis.set_title(f'Confusion Matrix ACC={acc:.2f}', fontdict=fontdict) - axis.set_xlabel('Prediction', fontdict=fontdict) - axis.set_ylabel('True', fontdict=fontdict) - fig.tight_layout() - file_name = 'confusion_matrix_last.png' if last_flag else 'confusion_matrix.png' - fig.savefig(path_out / file_name, dpi=300) - - logger.info(f"Malign Objects: {np.sum(y_true_lab)}") - logger.info(f"Confusion Matrix {cm}") - logger.info(f"Sensitivity {sens:.2f}") - logger.info(f"Specificity {spec:.2f}") - - with open(path_out / 'metrics.txt', 'w') as f: - f.write(f"AUC: {auc_val:.2f}\n") - f.write(f"F1 Score: {f1:.2f}\n") - f.write(f"Sensitivity: {sens:.2f}\n") - f.write(f"Specificity: {spec:.2f}\n") - f.write(f"PPV: {ppv:.2f}\n") - f.write(f"NPV: {npv:.2f}\n") - f.write(f"ACC: {acc:.2f}\n") - f.write(f"AP: {ap:.2f}\n") - - print(f"AUC: {auc_val:.2f}") - print(f"F1 Score: {f1:.2f}") - print(f"Sensitivity: {sens:.2f}") - print(f"Specificity: {spec:.2f}") - print(f"PPV: {ppv:.2f}") - print(f"NPV: {npv:.2f}") - print(f"ACC: {acc:.2f}") - print(f"AP: {ap:.2f}") - except Exception as e: - logger.error(f"Error in evaluate_results function: {e}") - raise - -if __name__ == "__main__": - wouter_data_path = "/mnt/sda1/swarm-learning/wouter_data/preprocessed_re/" - athens_data_path = "/mnt/sda1/swarm-learning/athens_data/preprocessed_athens/" - predict( - model_dir=Path('/mnt/sda1/odelia_paper_trained_results/2023_07_04_180000_DUKE_ext_ResNet101_swarm_learning'), - test_data_dir=athens_data_path, - model_name='ResNet101', - last_flag=False, - prediction_flag='collab', - cohort_flag='athens' - ) diff --git a/application/jobs/3dcnn_ptl/app/custom/threedcnn_ptl.py b/application/jobs/3dcnn_ptl/app/custom/threedcnn_ptl.py deleted file mode 100644 index 4a298c76..00000000 --- a/application/jobs/3dcnn_ptl/app/custom/threedcnn_ptl.py +++ /dev/null @@ -1,162 +0,0 @@ -from sklearn.model_selection import train_test_split -from torch.utils.data import DataLoader, Subset -from collections import Counter -import torch -from pytorch_lightning import Trainer -from pytorch_lightning.callbacks import ModelCheckpoint -from pytorch_lightning.loggers import TensorBoardLogger -from data.datamodules import DataModule -from model_selector import select_model -from env_config import load_environment_variables, load_prediction_modules, prepare_dataset, generate_run_directory - -import os -import logging - - -def get_num_epochs_per_round(site_name: str) -> int: - #TODO: Set max_epochs based on the data set size - NUM_EPOCHS_FOR_SITE = { "TUD_1": 2, - "TUD_2": 4, - "TUD_3": 8, - "MEVIS_1": 2, - "MEVIS_2": 4, - "UKA": 2, - } - - if site_name in NUM_EPOCHS_FOR_SITE.keys(): - MAX_EPOCHS = NUM_EPOCHS_FOR_SITE[site_name] - else: - MAX_EPOCHS = 5 - - print(f"Site name: {site_name}") - print(f"Max epochs set to: {MAX_EPOCHS}") - - return MAX_EPOCHS - - -def set_up_logging(): - logging.basicConfig(level=logging.INFO) - logger = logging.getLogger(__name__) - return logger - - -def set_up_data_module(env_vars, logger, site_name: str): - ds, task_data_name = prepare_dataset(env_vars['task_data_name'], env_vars['data_dir'], site_name=site_name) - - labels = ds.get_labels() - - # Generate indices and perform stratified split - indices = list(range(len(ds))) - train_indices, val_indices = train_test_split(indices, test_size=0.2, stratify=labels, random_state=42) - - # Create training and validation subsets - ds_train = Subset(ds, train_indices) - ds_val = Subset(ds, val_indices) - - # Extract training labels using the train_indices - train_labels = [labels[i] for i in train_indices] - label_counts = Counter(train_labels) - - # Calculate the total number of samples in the training set - total_samples = len(train_labels) - - # Print the percentage of the training set for each label - for label, count in label_counts.items(): - percentage = (count / total_samples) * 100 - logger.info(f"Label '{label}': {percentage:.2f}% of the training set, Exact count: {count}") - - logger.info(f"Total number of different labels in the training set: {len(label_counts)}") - - ads_val_data = DataLoader(ds_val, batch_size=2, shuffle=False) - logger.info(f'ads_val_data type: {type(ads_val_data)}') - - train_size = len(ds_train) - val_size = len(ds_val) - logger.info(f'Train size: {train_size}') - logger.info(f'Val size: {val_size}') - - dm = DataModule( - ds_train=ds_train, - ds_val=ds_val, - batch_size=1, - num_workers=16, - pin_memory=True, - ) - - return dm - - -def create_run_directory(env_vars): - path_run_dir = generate_run_directory(env_vars['scratch_dir'], env_vars['task_data_name'], env_vars['model_name'], env_vars['local_compare_flag']) - return path_run_dir - - -def prepare_training(logger, max_epochs:int , site_name: str): - try: - env_vars = load_environment_variables() - path_run_dir = create_run_directory(env_vars) - if not torch.cuda.is_available(): - raise(RuntimeError("This example does not work without GPU")) - accelerator = 'gpu' - logger.info(f"Using {accelerator} for training") - - data_module = set_up_data_module(env_vars, logger, site_name) - - # max_epochs = env_vars['max_epochs'] - # cal_max_epochs = cal_max_epochs(max_epochs, cal_weightage(train_size)) - # logger.info(f"Max epochs set to: {cal_max_epochs}") - - # Initialize the model - model_name = env_vars['model_name'] - model = select_model(model_name) - logger.info(f"Using model: {model_name}") - - to_monitor = "val/AUC_ROC" - min_max = "max" - log_every_n_steps = 1 - - checkpointing = ModelCheckpoint( - dirpath=str(path_run_dir), - monitor=to_monitor, - save_last=True, - save_top_k=2, - mode=min_max, - ) - - trainer = Trainer( - accelerator=accelerator, - precision=16, - default_root_dir=str(path_run_dir), - callbacks=[checkpointing], - enable_checkpointing=True, - check_val_every_n_epoch=1, - log_every_n_steps=log_every_n_steps, - max_epochs=max_epochs, - num_sanity_val_steps=2, - logger=TensorBoardLogger(save_dir=path_run_dir) - ) - - except Exception as e: - logger.error(f"Error in set_up_training: {e}") - raise - - return data_module, model, checkpointing, trainer, path_run_dir, env_vars - - -def validate_and_train(logger, data_module, model, trainer) -> None: - logger.info("--- Validate global model ---") - trainer.validate(model, datamodule=data_module) - - logger.info("--- Train new model ---") - trainer.fit(model, datamodule=data_module) - - -def finalize_training(logger, model, checkpointing, trainer, path_run_dir, env_vars) -> None: - model.save_best_checkpoint(trainer.logger.log_dir, checkpointing.best_model_path) - predict, prediction_flag = load_prediction_modules(env_vars['prediction_flag']) - test_data_path = os.path.join(env_vars['data_dir'], env_vars['task_data_name'], 'test') - if os.path.exists(test_data_path): - predict(path_run_dir, test_data_path, env_vars['model_name'], last_flag=False, prediction_flag=prediction_flag) - else: - logger.info('No test data found, not running evaluation') - logger.info('Training completed successfully') diff --git a/application/jobs/3dcnn_ptl/app/custom/utils/roc_curve.py b/application/jobs/3dcnn_ptl/app/custom/utils/roc_curve.py deleted file mode 100644 index 2cfd4a9d..00000000 --- a/application/jobs/3dcnn_ptl/app/custom/utils/roc_curve.py +++ /dev/null @@ -1,132 +0,0 @@ -import numpy as np -from sklearn.metrics import roc_curve, auc, confusion_matrix -import matplotlib - -def plot_roc_curve(y_true, y_score, axis, bootstrapping=1000, drop_intermediate=False, fontdict={}): - """ - Plots the ROC curve with bootstrapping. - - Args: - y_true (array-like): True binary labels. - y_score (array-like): Target scores. - axis (matplotlib.axes.Axes): Matplotlib axis object. - bootstrapping (int, optional): Number of bootstrap samples. Defaults to 1000. - drop_intermediate (bool, optional): Whether to drop some intermediate thresholds. Defaults to False. - fontdict (dict, optional): Dictionary of font properties. Defaults to {}. - - Returns: - tuple: tprs, fprs, auc_val, thrs, opt_idx, conf_matrix - """ - # ----------- Bootstrapping ------------ - tprs, aucs, thrs = [], [], [] - mean_fpr = np.linspace(0, 1, 100) - rand_idxs = np.random.randint(0, len(y_true), size=(bootstrapping, len(y_true))) # Note: with replacement - for rand_idx in rand_idxs: - y_true_set = y_true[rand_idx] - y_score_set = y_score[rand_idx] - fpr, tpr, thresholds = roc_curve(y_true_set, y_score_set, drop_intermediate=drop_intermediate) - tpr_interp = np.interp(mean_fpr, fpr, tpr) # must be interpolated to gain constant/equal fpr positions - tprs.append(tpr_interp) - aucs.append(auc(fpr, tpr)) - optimal_idx = np.argmax(tpr - fpr) - thrs.append(thresholds[optimal_idx]) - - mean_tpr = np.mean(tprs, axis=0) - mean_tpr[-1] = 1.0 - std_tpr = np.std(tprs, axis=0, ddof=1) - tprs_upper = np.minimum(mean_tpr + std_tpr, 1) - tprs_lower = np.maximum(mean_tpr - std_tpr, 0) - - # ------ Averaged based on bootstrapping ------ - mean_auc = np.mean(aucs) - std_auc = np.std(aucs, ddof=1) - - # --------- Specific Case ------------- - fprs, tprs, thrs = roc_curve(y_true, y_score, drop_intermediate=drop_intermediate) - auc_val = auc(fprs, tprs) - opt_idx = np.argmax(tprs - fprs) - opt_tpr = tprs[opt_idx] - opt_fpr = fprs[opt_idx] - - y_scores_bin = y_score >= thrs[opt_idx] # WARNING: Must be >= not > - conf_matrix = confusion_matrix(y_true, y_scores_bin) # [[TN, FP], [FN, TP]] - - axis.plot(fprs, tprs, color='b', label=rf"ROC (AUC = {auc_val:.2f} $\pm$ {std_auc:.2f})", lw=2, alpha=.8) - axis.fill_between(mean_fpr, tprs_lower, tprs_upper, color='grey', alpha=.2, label=r'$\pm$ 1 std. dev.') - axis.hlines(y=opt_tpr, xmin=0.0, xmax=opt_fpr, color='g', linestyle='--') - axis.vlines(x=opt_fpr, ymin=0.0, ymax=opt_tpr, color='g', linestyle='--') - axis.plot(opt_fpr, opt_tpr, color='g', marker='o') - axis.plot([0, 1], [0, 1], linestyle='--', color='k') - axis.set_xlim([0.0, 1.0]) - axis.set_ylim([0.0, 1.0]) - - axis.legend(loc='lower right') - axis.set_xlabel('1 - Specificity', fontdict=fontdict) - axis.set_ylabel('Sensitivity', fontdict=fontdict) - - axis.grid(color='#dddddd') - axis.set_axisbelow(True) - axis.tick_params(colors='#dddddd', which='both') - for xtick in axis.get_xticklabels(): - xtick.set_color('k') - for ytick in axis.get_yticklabels(): - ytick.set_color('k') - for child in axis.get_children(): - if isinstance(child, matplotlib.spines.Spine): - child.set_color('#dddddd') - - return tprs, fprs, auc_val, thrs, opt_idx, conf_matrix - -def cm2acc(cm): - """ - Calculates accuracy from the confusion matrix. - - Args: - cm (array-like): Confusion matrix [[TN, FP], [FN, TP]]. - - Returns: - float: Accuracy. - """ - tn, fp, fn, tp = cm.ravel() - return (tn + tp) / (tn + tp + fn + fp) - -def safe_div(x, y): - """ - Safely divides two numbers, returning NaN if the denominator is zero. - - Args: - x (float): Numerator. - y (float): Denominator. - - Returns: - float: Result of division or NaN if denominator is zero. - """ - if y == 0: - return float('nan') - return x / y - -def cm2x(cm): - """ - Calculates various metrics from the confusion matrix. - - Args: - cm (array-like): Confusion matrix [[TN, FP], [FN, TP]]. - - Returns: - tuple: (ppv, npv, tpr, tnr) - ppv (float): Positive predictive value. - npv (float): Negative predictive value. - tpr (float): True positive rate (sensitivity, recall). - tnr (float): True negative rate (specificity). - """ - tn, fp, fn, tp = cm.ravel() - pp = tp + fp # predicted positive - pn = fn + tn # predicted negative - p = tp + fn # actual positive - n = fp + tn # actual negative - - ppv = safe_div(tp, pp) # positive predictive value - npv = safe_div(tn, pn) # negative predictive value - tpr = safe_div(tp, p) # true positive rate (sensitivity, recall) - tnr = safe_div(tn, n) # true negative rate (specificity) - return ppv, npv, tpr, tnr diff --git a/application/jobs/3dcnn_ptl/README.md b/application/jobs/ODELIA_ternary_classification/README.md similarity index 90% rename from application/jobs/3dcnn_ptl/README.md rename to application/jobs/ODELIA_ternary_classification/README.md index 25b686ff..d10de042 100644 --- a/application/jobs/3dcnn_ptl/README.md +++ b/application/jobs/ODELIA_ternary_classification/README.md @@ -32,7 +32,7 @@ docker run -it --rm \ Before running a swarm dummy training, first make sure the code works in non-swarm mode. ```bash -cd application/jobs/3dcnn_ptl/app/custom/ +cd application/jobs/ODELIA_ternary_classification/app/custom/ export TRAINING_MODE="local_training" export SITE_NAME= export NUM_EPOCHS=1 @@ -45,10 +45,10 @@ cd /workspace The FL Simulator is a lightweight tool that uses threads to simulate multiple clients. It is useful for quick local testing and debugging. Run the following command to start the simulator: ```bash -nvflare simulator -w /tmp/3dcnn_ptl -n 2 -t 2 application/jobs/3dcnn_ptl -c simulated_node_0,simulated_node_1 +nvflare simulator -w /tmp/ODELIA_ternary_classification -n 2 -t 2 application/jobs/ODELIA_ternary_classification -c simulated_node_0,simulated_node_1 ``` -* `-w /tmp/3dcnn_ptl`: Specifies the working directory. +* `-w /tmp/ODELIA_ternary_classification`: Specifies the working directory. * `-n 2`: Sets the number of clients. * `-t 2`: Specifies the number of threads. * `-c simulated_node_0,simulated_node_1`: Names the two simulated nodes. diff --git a/application/jobs/3dcnn_ptl/app/config/config_fed_client.conf b/application/jobs/ODELIA_ternary_classification/app/config/config_fed_client.conf similarity index 94% rename from application/jobs/3dcnn_ptl/app/config/config_fed_client.conf rename to application/jobs/ODELIA_ternary_classification/app/config/config_fed_client.conf index 1b0fa456..3b82afec 100644 --- a/application/jobs/3dcnn_ptl/app/config/config_fed_client.conf +++ b/application/jobs/ODELIA_ternary_classification/app/config/config_fed_client.conf @@ -27,7 +27,7 @@ tasks = ["swarm_*"] executor { # client-side controller for training and logic and aggregation management - path = "controller.SwarmClientController" + path = "nvflare.app_common.ccwf.SwarmClientController" args { # train task must be implemented by Executor learn_task_name = "train" @@ -82,11 +82,12 @@ path = "nvflare.app_opt.pt.file_model_persistor.PTFileModelPersistor" args { model { - path = "models.resnet.ResNet" - args { - in_ch = 1 - out_ch = 1 - } + path = "models.mst.MST" + args { + n_input_channels = 1 + num_classes = 3 + spatial_dims = 3 + } } } } diff --git a/application/jobs/3dcnn_ptl/app/config/config_fed_server.conf b/application/jobs/ODELIA_ternary_classification/app/config/config_fed_server.conf similarity index 85% rename from application/jobs/3dcnn_ptl/app/config/config_fed_server.conf rename to application/jobs/ODELIA_ternary_classification/app/config/config_fed_server.conf index d408e3f0..335b173e 100644 --- a/application/jobs/3dcnn_ptl/app/config/config_fed_server.conf +++ b/application/jobs/ODELIA_ternary_classification/app/config/config_fed_server.conf @@ -13,13 +13,12 @@ workflows = [ { # server-side controller to manage job life cycle id = "swarm_controller" - path = "controller.SwarmServerController" + path = "nvflare.app_common.ccwf.SwarmServerController" args { # can also set aggregation clients and train clients, see class for all available args - num_rounds = 30 + num_rounds = 20 start_task_timeout = 360000 progress_timeout = 360000 - end_workflow_timeout = 360000 configure_task_timeout = 360000 max_status_report_interval = 360000 } diff --git a/application/jobs/ODELIA_ternary_classification/app/custom/data/augmentation/augmentations_3d.py b/application/jobs/ODELIA_ternary_classification/app/custom/data/augmentation/augmentations_3d.py new file mode 100644 index 00000000..043f93c0 --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/custom/data/augmentation/augmentations_3d.py @@ -0,0 +1,124 @@ +from typing import Union, Optional, Sequence + +import torchio as tio +from torchio.typing import TypeRangeFloat, TypeTripletInt +from torchio.transforms.transform import TypeMaskingMethod +from torchio import Subject, Image + +import torch +import numpy as np + + +class ImageOrSubjectToTensor(object): + """Converts a torchio Image or Subject to a tensor format by swapping axes.""" + + def __call__(self, input: Union[Image, Subject]): + if isinstance(input, Subject): + return {key: val.data.swapaxes(1, -1) if isinstance(val, Image) else val for key, val in input.items()} + else: + return input.data.swapaxes(1, -1) + + +def parse_per_channel(per_channel, channels): + if isinstance(per_channel, bool): + if per_channel == True: + return [(ch,) for ch in range(channels)] + else: + return [tuple(ch for ch in range(channels))] + else: + return per_channel + + +class ZNormalization(tio.ZNormalization): + """Z-Normalization with support for per-channel and per-slice options, and percentile-based clipping.""" + + def __init__( + self, + percentiles: TypeRangeFloat = (0, 100), + per_channel=True, + per_slice=False, + masking_method: TypeMaskingMethod = None, + **kwargs + ): + super().__init__(masking_method=masking_method, **kwargs) + self.percentiles = percentiles + self.per_channel = per_channel + self.per_slice = per_slice + + def apply_normalization(self, subject: Subject, image_name: str, mask: torch.Tensor) -> None: + image = subject[image_name] + per_channel = parse_per_channel(self.per_channel, image.shape[0]) + per_slice = parse_per_channel(self.per_slice, image.shape[-1]) + + image.set_data( + torch.cat([ + torch.cat([ + self._znorm(image.data[chs,][:, :, :, sl, ], mask[chs,][:, :, :, sl, ], image_name, image.path) + for sl in per_slice], dim=-1) + for chs in per_channel]) + ) + + def _znorm(self, image_data, mask, image_name, image_path): + cutoff = torch.quantile(image_data.masked_select(mask).float(), torch.tensor(self.percentiles) / 100.0) + torch.clamp(image_data, *cutoff.to(image_data.dtype).tolist(), out=image_data) + standardized = self.znorm(image_data, mask) + if standardized is None: + raise RuntimeError( + f'Standard deviation is 0 for masked values in image "{image_name}" ({image_path})' + ) + return standardized + + +class CropOrPad(tio.CropOrPad): + """Crop or pad a subject with optional random center logic for padding.""" + + def __init__( + self, + target_shape: Union[int, TypeTripletInt, None] = None, + padding_mode: Union[str, float] = 0, + mask_name: Optional[str] = None, + labels: Optional[Sequence[int]] = None, + random_center=False, + **kwargs + ): + super().__init__( + target_shape=target_shape, + padding_mode=padding_mode, + mask_name=mask_name, + labels=labels, + **kwargs + ) + self.random_center = random_center + + def _get_six_bounds_parameters(self, parameters: np.ndarray): + result = [] + for number in parameters: + if self.random_center: + ini = np.random.randint(low=0, high=number + 1) + else: + ini = int(np.ceil(number / 2)) + fin = number - ini + result.extend([ini, fin]) + return tuple(result) + + def apply_transform(self, subject: tio.Subject) -> tio.Subject: + subject.check_consistent_space() + padding_params, cropping_params = self.compute_crop_or_pad(subject) + padding_kwargs = {'padding_mode': self.padding_mode} + + if padding_params is not None: + if self.random_center: + random_padding_params = [] + for i in range(0, len(padding_params), 2): + s = padding_params[i] + padding_params[i + 1] + r = np.random.randint(0, s + 1) + random_padding_params.extend([r, s - r]) + padding_params = random_padding_params + pad = tio.Pad(padding_params, **padding_kwargs) + subject = pad(subject) + + if cropping_params is not None: + crop = tio.Crop(cropping_params) + subject = crop(subject) + + return subject diff --git a/application/jobs/3dcnn_ptl/app/custom/data/datamodules/__init__.py b/application/jobs/ODELIA_ternary_classification/app/custom/data/datamodules/__init__.py similarity index 100% rename from application/jobs/3dcnn_ptl/app/custom/data/datamodules/__init__.py rename to application/jobs/ODELIA_ternary_classification/app/custom/data/datamodules/__init__.py diff --git a/application/jobs/ODELIA_ternary_classification/app/custom/data/datamodules/datamodule.py b/application/jobs/ODELIA_ternary_classification/app/custom/data/datamodules/datamodule.py new file mode 100644 index 00000000..6bbb42ef --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/custom/data/datamodules/datamodule.py @@ -0,0 +1,99 @@ +import pytorch_lightning as pl +import torch +from torch.utils.data.dataloader import DataLoader +import torch.multiprocessing as mp +from torch.utils.data.sampler import WeightedRandomSampler, RandomSampler + + +class DataModule(pl.LightningDataModule): + """Flexible LightningDataModule with weighted or random sampling support.""" + + def __init__( + self, + ds_train: object = None, + ds_val: object = None, + ds_test: object = None, + batch_size: int = 1, + batch_size_val: int = None, + batch_size_test: int = None, + num_train_samples: int = None, + num_workers: int = mp.cpu_count(), + seed: int = 0, + pin_memory: bool = False, + weights: list = None + ): + super().__init__() + self.hyperparameters = {**locals()} + self.hyperparameters.pop('__class__') + self.hyperparameters.pop('self') + + self.ds_train = ds_train + self.ds_val = ds_val + self.ds_test = ds_test + + self.batch_size = batch_size + self.batch_size_val = batch_size if batch_size_val is None else batch_size_val + self.batch_size_test = batch_size if batch_size_test is None else batch_size_test + self.num_train_samples = num_train_samples + self.num_workers = num_workers + self.seed = seed + self.pin_memory = pin_memory + self.weights = weights + + def train_dataloader(self): + generator = torch.Generator() + generator.manual_seed(self.seed) + + if self.ds_train is not None: + if self.weights is not None: + num_samples = len(self.weights) if self.num_train_samples is None else self.num_train_samples + sampler = WeightedRandomSampler(self.weights, num_samples=num_samples, generator=generator) + else: + num_samples = len(self.ds_train) if self.num_train_samples is None else self.num_train_samples + sampler = RandomSampler(self.ds_train, num_samples=num_samples, replacement=False, generator=generator) + + return DataLoader( + self.ds_train, + batch_size=self.batch_size, + num_workers=self.num_workers, + sampler=sampler, + generator=generator, + drop_last=True, + pin_memory=self.pin_memory + ) + + raise AssertionError("A training set was not initialized.") + + def val_dataloader(self): + generator = torch.Generator() + generator.manual_seed(self.seed) + + if self.ds_val is not None: + return DataLoader( + self.ds_val, + batch_size=self.batch_size_val, + num_workers=self.num_workers, + shuffle=False, + generator=generator, + drop_last=False, + pin_memory=self.pin_memory + ) + + raise AssertionError("A validation set was not initialized.") + + def test_dataloader(self): + generator = torch.Generator() + generator.manual_seed(self.seed) + + if self.ds_test is not None: + return DataLoader( + self.ds_test, + batch_size=self.batch_size_test, + num_workers=self.num_workers, + shuffle=False, + generator=generator, + drop_last=False, + pin_memory=self.pin_memory + ) + + raise AssertionError("A test test set was not initialized.") diff --git a/application/jobs/ODELIA_ternary_classification/app/custom/data/datasets/__init__.py b/application/jobs/ODELIA_ternary_classification/app/custom/data/datasets/__init__.py new file mode 100644 index 00000000..a2c7d909 --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/custom/data/datasets/__init__.py @@ -0,0 +1,11 @@ +""" +This package initializes the necessary modules and classes for the project. +""" + +# from .dataset_3d import SimpleDataset3D +# from .dataset_3d_collab import DUKE_Dataset3D_collab +# from .dataset_3d_duke import DUKE_Dataset3D +# from .dataset_3d_duke_external import DUKE_Dataset3D_external +from .dataset_3d_odelia import ODELIA_Dataset3D + +# __all__ = [name for name in dir() if not name.startswith('_')] diff --git a/application/jobs/ODELIA_ternary_classification/app/custom/data/datasets/dataset_3d_odelia.py b/application/jobs/ODELIA_ternary_classification/app/custom/data/datasets/dataset_3d_odelia.py new file mode 100644 index 00000000..eba4aa12 --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/custom/data/datasets/dataset_3d_odelia.py @@ -0,0 +1,151 @@ +from pathlib import Path +import pandas as pd +import torch.utils.data as data +import torchio as tio +import torch +import numpy as np +from sklearn.preprocessing import OneHotEncoder + +from data.augmentation.augmentations_3d import ImageOrSubjectToTensor, ZNormalization, CropOrPad + + +class ODELIA_Dataset3D(data.Dataset): + PATH_ROOT = Path('/data') + ALL_INSTITUTIONS = ['CAM', 'MHA', 'RSH', 'UKA', 'UMCU', 'VHIO', 'RUMC', 'USZ'] + DATA_DIR = { + "original": "data", + "unilateral": "data_unilateral" + } + META_DIR = { + "original": "metadata", + "unilateral": "metadata_unilateral" + } + CLASS_LABELS = { + 'original': { + 'Lesion_Left': ['No', 'Benign', 'Malignant'], + 'Lesion_Right': ['No', 'Benign', 'Malignant'], + }, + 'unilateral': { + 'Lesion': ['No', 'Benign', 'Malignant'], + } + } + + def __init__( + self, + path_root=None, + institutions=None, + fold=0, + labels=None, # None = all labels or list of labels + config=None, # original, unilateral + split=None, + fraction=None, + transform=None, + random_flip=False, + random_rotate=False, + random_inverse=False, + noise=False, + to_tensor=True, + + ): + self.path_root = Path(self.PATH_ROOT if path_root is None else path_root) + self.split = split + self.config = config + self.class_labels = self.CLASS_LABELS[config] + self.meta_dir = self.META_DIR[config] + self.data_dir = self.DATA_DIR[config] + self.labels = list(self.class_labels.keys()) if labels is None else labels + self.class_labels_num = [len(self.class_labels[l]) for l in self.labels] # For CORN Loss -1 + + if (institutions is None) or (institutions == "ODELIA"): + institutions = self.ALL_INSTITUTIONS + elif isinstance(institutions, str): + institutions = [institutions] + self.institutions = institutions + + flip_axes = (0, 1) if config == "original" else (0, 1, 2) # Do not flip horizontal axis 2, otherwise labels incorrect + if transform is None: + self.transform = tio.Compose([ + tio.ToCanonical() if config == "original" else tio.Lambda(lambda x: x), + tio.Resample((0.7, 0.7, 3)) if config == "original" else tio.Lambda(lambda x: x), + + tio.Flip((1, 0)), # Just for viewing, otherwise upside down + CropOrPad((448, 448, 32), random_center=random_rotate) if config == "original" else CropOrPad( + (224, 224, 32), random_center=random_rotate), + + ZNormalization(per_channel=True, per_slice=False, + masking_method=lambda x: (x > x.min()) & (x < x.max()), percentiles=(0.5, 99.5)), + + tio.OneOf([ + # tio.Lambda(lambda x: x.moveaxis(1, 2) if torch.rand((1,),)[0]<0.5 else x ) if random_rotate else tio.Lambda(lambda x: x), # WARNING: 1,2 if Subject, 2, 3 if tensor + tio.RandomAffine(scales=0, degrees=(0, 0, 0, 0, 0, 90), translation=0, isotropic=True, + default_pad_value='minimum') if random_rotate else tio.Lambda(lambda x: x), + tio.RandomFlip(flip_axes) if random_flip else tio.Lambda(lambda x: x), # WARNING: Padding mask + ]), + tio.Lambda(lambda x: -x if torch.rand((1,), )[0] < 0.5 else x, + types_to_apply=[tio.INTENSITY]) if random_inverse else tio.Lambda(lambda x: x), + tio.RandomNoise(std=(0.0, 0.25)) if noise else tio.Lambda(lambda x: x), + + ImageOrSubjectToTensor() if to_tensor else tio.Lambda(lambda x: x) + ]) + else: + self.transform = transform + + # Get split + dfs = [] + for institution in self.institutions: + path_metadata = self.path_root / institution / self.meta_dir + df = self.load_split(path_metadata / 'split.csv', fold=fold, split=split, fraction=fraction) + df['Institution'] = institution + + # Verify files exist + # uids = self.run_item_crawler(self.path_root/institution/'data_unilateral') + # df = df[df['UID'].isin(uids)] + + # Merge with annotations + df_anno = pd.read_csv(path_metadata / 'annotation.csv', dtype={'UID': str, 'PatientID': str}) + df = df.merge(df_anno, on='UID', how='inner') + + dfs.append(df) + df = pd.concat(dfs).reset_index(drop=True) + + self.item_pointers = df.index.tolist() + self.df = df + + def __len__(self): + return len(self.item_pointers) + + def load_img(self, path_img): + return tio.ScalarImage(path_img) + + def load_map(self, path_img): + return tio.LabelMap(path_img) + + def __getitem__(self, index): + idx = self.item_pointers[index] + item = self.df.loc[idx] + uid = item['UID'] + institution = item['Institution'] + + target = np.stack(item[self.labels].values) + + path_folder = self.path_root / institution / self.data_dir / uid + # img = self.load_img([path_folder/f'{name}.nii.gz' for name in [ 'Pre', 'Sub_1', 'T2']]) + img = self.load_img(path_folder / 'Sub_1.nii.gz') + img = self.transform(img) + + return {'uid': uid, 'source': img, 'target': target} + + @classmethod + def load_split(cls, filepath_or_buffer=None, fold=0, split=None, fraction=None): + # WARNING: PatientID must be read as string otherwise leading zeros are cut off + df = pd.read_csv(filepath_or_buffer, dtype={'UID': str}) + df = df[df['Fold'] == fold] + if split is not None: + df = df[df['Split'] == split] + if fraction is not None: + df = df.sample(frac=fraction, random_state=0).reset_index() + return df + + @classmethod + def run_item_crawler(cls, path_root, **kwargs): + return [path.relative_to(path_root).name for path in Path(path_root).iterdir() if path.is_dir()] diff --git a/application/jobs/ODELIA_ternary_classification/app/custom/env_config.py b/application/jobs/ODELIA_ternary_classification/app/custom/env_config.py new file mode 100755 index 00000000..93efb091 --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/custom/env_config.py @@ -0,0 +1,75 @@ +import os +from datetime import datetime +from pathlib import Path + + +def load_environment_variables(): + return { + 'site_name': os.environ['SITE_NAME'], + 'task_data_name': os.environ.get('DATA_FOLDER', 'Odelia'), + 'scratch_dir': os.environ['SCRATCH_DIR'], + 'data_dir': os.environ['DATA_DIR'], + 'max_epochs': int(os.environ.get('MAX_EPOCHS', 100)), + 'min_peers': int(os.environ.get('MIN_PEERS', 2)), + 'max_peers': int(os.environ.get('MAX_PEERS', 10)), + 'local_compare_flag': os.environ.get('LOCAL_COMPARE_FLAG', 'False').lower() == 'true', + 'use_adaptive_sync': os.environ.get('USE_ADAPTIVE_SYNC', 'False').lower() == 'true', + 'sync_frequency': int(os.environ.get('SYNC_FREQUENCY', 1024)), + 'model_name': os.environ.get('MODEL_NAME', 'ResNet101'), + 'prediction_flag': os.environ.get('PREDICT_FLAG', 'ext'), + 'mediswarm_version': os.environ.get('MEDISWARM_VERSION', 'unset'), + } + + +def load_prediction_modules(prediction_flag): + from predict import predict + return predict, prediction_flag + + +def prepare_odelia_dataset(): + # parser removed, now read from environment + institution = os.environ.get('INSTITUTION', os.environ['SITE_NAME']) # TODO think about how this should be handled + model = os.environ.get('MODEL_NAME', 'MST') + config = os.environ.get('CONFIG', 'unilateral') + + current_time = datetime.now().strftime("%Y_%m_%d_%H%M%S") + run_name = f'{model}_{config}_{current_time}' + path_run_dir = Path.cwd() / 'runs' / institution / run_name + path_run_dir.mkdir(parents=True, exist_ok=True) + + from data.datasets import ODELIA_Dataset3D + ds_train = ODELIA_Dataset3D(institutions=institution, split='train', config=config, + random_flip=True, random_rotate=True, random_inverse=False, noise=True) + ds_val = ODELIA_Dataset3D(institutions=institution, split='val', config=config) + + print(f"Total samples loaded: {len(ds_train)} (train) + {len(ds_val)} (val)") + print(f"Train set: {len(ds_train)}, Val set: {len(ds_val)}") + # print(f"Labels in val: {[sample['label'] for sample in ds_val]}") + + return ds_train, ds_val, path_run_dir, run_name + + +def generate_run_directory(scratch_dir, task_data_name, model_name, local_compare_flag): + current_time = datetime.now().strftime("%Y_%m_%d_%H%M%S") + mode = 'local_compare' if local_compare_flag else 'swarm_learning' + if not os.path.exists(scratch_dir): + os.makedirs(scratch_dir) + return os.path.join(scratch_dir, f"{current_time}_{task_data_name}_{model_name}_{mode}") + + +# TODO: Implement dynamic weightage calculation based on actual dataset size +def cal_weightage(train_size): + """ + Placeholder function for calculating training weightage. + Currently unused. + """ + pass # To be implemented + + +# TODO: Implement max epochs adjustment logic based on weightage +def cal_max_epochs(preset_max_epochs, weightage): + """ + Placeholder function for dynamically adjusting max epochs. + Currently unused. + """ + pass # To be implemented diff --git a/application/jobs/3dcnn_ptl/app/custom/main.py b/application/jobs/ODELIA_ternary_classification/app/custom/main.py similarity index 71% rename from application/jobs/3dcnn_ptl/app/custom/main.py rename to application/jobs/ODELIA_ternary_classification/app/custom/main.py index 747cc353..b86d6665 100755 --- a/application/jobs/3dcnn_ptl/app/custom/main.py +++ b/application/jobs/ODELIA_ternary_classification/app/custom/main.py @@ -1,28 +1,35 @@ #!/usr/bin/env python3 import os +import torch import nvflare.client.lightning as flare import nvflare.client as flare_util -import torch import threedcnn_ptl TRAINING_MODE = os.getenv("TRAINING_MODE") TM_PREFLIGHT_CHECK = "preflight_check" -TM_LOCAL_TRAINING="local_training" +TM_LOCAL_TRAINING = "local_training" TM_SWARM = "swarm" +if not TRAINING_MODE: + raise ValueError("TRAINING_MODE environment variable must be set") if TRAINING_MODE == TM_SWARM: flare_util.init() - SITE_NAME=flare.get_site_name() + SITE_NAME = flare.get_site_name() NUM_EPOCHS = threedcnn_ptl.get_num_epochs_per_round(SITE_NAME) elif TRAINING_MODE in [TM_PREFLIGHT_CHECK, TM_LOCAL_TRAINING]: - SITE_NAME=os.getenv("SITE_NAME") - NUM_EPOCHS = int(os.getenv("NUM_EPOCHS")) + SITE_NAME = os.getenv("SITE_NAME") + if not SITE_NAME: + raise ValueError("SITE_NAME environment variable must be set for local training") + try: + NUM_EPOCHS = int(os.getenv("NUM_EPOCHS", "1")) + except ValueError: + raise ValueError("NUM_EPOCHS must be an integer") else: - raise Exception(f"Illegal TRAINING_MODE {TRAINING_MODE}") + raise ValueError(f"Unsupported TRAINING_MODE: {TRAINING_MODE}") def main(): @@ -30,8 +37,11 @@ def main(): Main function for training and evaluating the model using NVFlare and PyTorch Lightning. """ logger = threedcnn_ptl.set_up_logging() + try: - data_module, model, checkpointing, trainer, path_run_dir, env_vars = threedcnn_ptl.prepare_training(logger, NUM_EPOCHS, SITE_NAME) + data_module, model, checkpointing, trainer, path_run_dir, env_vars = threedcnn_ptl.prepare_training( + logger, NUM_EPOCHS, SITE_NAME + ) if TRAINING_MODE == TM_SWARM: flare.patch(trainer) # Patch trainer to enable swarm learning @@ -55,5 +65,6 @@ def main(): logger.error(f"Error in main function: {e}") raise + if __name__ == "__main__": main() diff --git a/application/jobs/ODELIA_ternary_classification/app/custom/models/__init__.py b/application/jobs/ODELIA_ternary_classification/app/custom/models/__init__.py new file mode 100644 index 00000000..be8e0a1b --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/custom/models/__init__.py @@ -0,0 +1,9 @@ +""" +This package initializes the necessary modules and classes for the project. +""" + +from .base_model import VeryBasicModel, BasicModel, BasicClassifier +from .resnet import ResNet +from .mst import MST + +__all__ = ['VeryBasicModel', 'BasicModel', 'BasicClassifier', 'ResNet', 'MST'] diff --git a/application/jobs/ODELIA_ternary_classification/app/custom/models/base_model.py b/application/jobs/ODELIA_ternary_classification/app/custom/models/base_model.py new file mode 100644 index 00000000..a01b5b03 --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/custom/models/base_model.py @@ -0,0 +1,179 @@ +from pathlib import Path +import json +import torch +import torch.nn as nn +import torch.nn.functional as F +import pytorch_lightning as pl +from torchmetrics import AUROC, Accuracy + + +class VeryBasicModel(pl.LightningModule): + """Base LightningModule with training, validation, and test hooks stubbed out.""" + + def __init__(self, save_hyperparameters=True): + super().__init__() + if save_hyperparameters: + self.save_hyperparameters() + self._step_train = -1 + self._step_val = -1 + self._step_test = -1 + + def forward(self, x, cond=None): + raise NotImplementedError + + def _step(self, batch: dict, batch_idx: int, state: str, step: int): + raise NotImplementedError + + def _epoch_end(self, state: str): + return + + def training_step(self, batch: dict, batch_idx: int): + self._step_train += 1 + return self._step(batch, batch_idx, "train", self._step_train) + + def validation_step(self, batch: dict, batch_idx: int): + self._step_val += 1 + return self._step(batch, batch_idx, "val", self._step_val) + + def test_step(self, batch: dict, batch_idx: int): + self._step_test += 1 + return self._step(batch, batch_idx, "test", self._step_test) + + def on_train_epoch_end(self) -> None: + self._epoch_end("train") + + def on_validation_epoch_end(self) -> None: + self._epoch_end("val") + + def on_test_epoch_end(self) -> None: + self._epoch_end("test") + + @classmethod + def save_best_checkpoint(cls, path_checkpoint_dir, best_model_path): + with open(Path(path_checkpoint_dir) / 'best_checkpoint.json', 'w') as f: + json.dump({'best_model_epoch': Path(best_model_path).name}, f) + + @classmethod + def _get_best_checkpoint_path(cls, path_checkpoint_dir, **kwargs): + with open(Path(path_checkpoint_dir) / 'best_checkpoint.json', 'r') as f: + path_rel_best_checkpoint = Path(json.load(f)['best_model_epoch']) + return Path(path_checkpoint_dir) / path_rel_best_checkpoint + + @classmethod + def load_best_checkpoint(cls, path_checkpoint_dir, **kwargs): + path_best_checkpoint = cls._get_best_checkpoint_path(path_checkpoint_dir) + return cls.load_from_checkpoint(path_best_checkpoint, **kwargs) + + def load_pretrained(self, checkpoint_path, map_location=None, **kwargs): + if checkpoint_path.is_dir(): + checkpoint_path = self._get_best_checkpoint_path(checkpoint_path, **kwargs) + + checkpoint = torch.load(checkpoint_path, map_location=map_location) + return self.load_weights(checkpoint["state_dict"], **kwargs) + + def load_weights(self, pretrained_weights, strict=True, **kwargs): + filter = kwargs.get('filter', lambda key: key in pretrained_weights) + init_weights = self.state_dict() + pretrained_weights = {key: value for key, value in pretrained_weights.items() if filter(key)} + init_weights.update(pretrained_weights) + self.load_state_dict(init_weights, strict=strict) + return self + + +class BasicModel(VeryBasicModel): + """Extension of VeryBasicModel that includes optimizer and scheduler configuration.""" + + def __init__( + self, + optimizer=torch.optim.Adam, + optimizer_kwargs={'lr': 1e-3, 'weight_decay': 1e-2}, + lr_scheduler=None, + lr_scheduler_kwargs={}, + save_hyperparameters=True + ): + super().__init__(save_hyperparameters=save_hyperparameters) + if save_hyperparameters: + self.save_hyperparameters() + self.optimizer = optimizer + self.optimizer_kwargs = optimizer_kwargs + self.lr_scheduler = lr_scheduler + self.lr_scheduler_kwargs = lr_scheduler_kwargs + + def configure_optimizers(self): + optimizer = self.optimizer(self.parameters(), **self.optimizer_kwargs) + if self.lr_scheduler is not None: + lr_scheduler = self.lr_scheduler(optimizer, **self.lr_scheduler_kwargs) + return [optimizer], [{"scheduler": lr_scheduler, "interval": "epoch", "frequency": 1}] + return [optimizer] + + +class BasicClassifier(BasicModel): + """Generic classifier with dynamic metric and loss configuration based on task type.""" + + def __init__( + self, + in_ch, + out_ch, + spatial_dims, + loss_kwargs={}, + optimizer=torch.optim.AdamW, + optimizer_kwargs={'lr': 1e-4, 'weight_decay': 1e-2}, + lr_scheduler=None, + lr_scheduler_kwargs={}, + aucroc_kwargs={}, + acc_kwargs={}, + save_hyperparameters=True + ): + super().__init__(optimizer, optimizer_kwargs, lr_scheduler, lr_scheduler_kwargs) + self.in_ch = in_ch + self.out_ch = out_ch + self.spatial_dims = spatial_dims + + loss = torch.nn.CrossEntropyLoss + + self.loss = loss(**loss_kwargs) + self.loss_kwargs = loss_kwargs + + aucroc_kwargs.update({"task": "multiclass", 'num_classes': out_ch}) + acc_kwargs.update({"task": "multiclass", 'num_classes': out_ch}) + + self.auc_roc = nn.ModuleDict({state: AUROC(**aucroc_kwargs) for state in ["train_", "val_", "test_"]}) + self.acc = nn.ModuleDict({state: Accuracy(**acc_kwargs) for state in ["train_", "val_", "test_"]}) + + def _step(self, batch: dict, batch_idx: int, state: str, step: int): + source = batch['source'] + target = batch['target'] + batch_size = source.shape[0] + self.batch_size = batch_size + + pred = self(source) + loss_val = self.compute_loss(pred, target) + target_squeezed = torch.squeeze(target, 1) # TODO Why is this necessary and is it the right thing to do? + self.acc[state + "_"].update(pred, target_squeezed) + self.auc_roc[state + "_"].update(pred, target_squeezed) + + self.log(f"{state}/loss", loss_val, batch_size=batch_size, on_step=True, on_epoch=True) + return loss_val + + def _epoch_end(self, state): + acc_value = self.acc[state + "_"].compute() + auc_roc_value = self.auc_roc[state + "_"].compute() + self.log(f"{state}/ACC", acc_value, batch_size=self.batch_size, on_step=False, on_epoch=True) + self.log(f"{state}/AUC_ROC", auc_roc_value, batch_size=self.batch_size, on_step=False, on_epoch=True) + # For ModelCheckpoint, also log as "val/AUC_ROC" if state == "val" + if state == "val": + self.log("val/AUC_ROC", auc_roc_value, batch_size=self.batch_size, on_step=False, on_epoch=True) + # print some debug information + print(f"Epoch {self.current_epoch} - {state} ACC: {acc_value:.4f}, AUC_ROC: {auc_roc_value:.4f}") + self.acc[state + "_"].reset() + self.auc_roc[state + "_"].reset() + + def compute_loss(self, pred, target): + target_squeezed = torch.squeeze(target, 1) # TODO Why is this necessary and is it the right thing to do? + return self.loss(pred, target_squeezed) + + def logits2labels(self, logits): + return torch.argmax(logits, dim=1, keepdim=True) + + def logits2probabilities(self, logits): + return F.softmax(logits, dim=1) diff --git a/application/jobs/ODELIA_ternary_classification/app/custom/models/mst.py b/application/jobs/ODELIA_ternary_classification/app/custom/models/mst.py new file mode 100644 index 00000000..540441ae --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/custom/models/mst.py @@ -0,0 +1,101 @@ +import torch +import torch.nn as nn +from einops import rearrange +from x_transformers import Encoder + +from .base_model import BasicClassifier + + +class TransformerEncoder(Encoder): + """Override the default forward to match input formatting.""" + + def forward(self, x, mask=None, src_key_padding_mask=None): + src_key_padding_mask = ~src_key_padding_mask if src_key_padding_mask is not None else None + mask = ~mask if mask is not None else None + return super().forward(x=x, context=None, mask=src_key_padding_mask, context_mask=None, attn_mask=mask) + + +class _MST(nn.Module): + """Multi-slice transformer for 3D volume input classification or regression.""" + + def __init__( + self, + out_ch=1, + backbone_type="dinov2", + model_size=None, + slice_fusion_type="transformer" + ): + super().__init__() + self.backbone_type = backbone_type + self.slice_fusion_type = slice_fusion_type + + if backbone_type == "dinov2": + torch.hub._validate_not_a_forked_repo = lambda a, b, c: True + self.backbone = torch.hub.load('facebookresearch/dinov2', f'dinov2_vit{model_size}14') + self.backbone.mask_token = None + emb_ch = self.backbone.num_features + else: + raise ValueError("Unknown backbone_type") + + self.emb_ch = emb_ch + + if slice_fusion_type == "transformer": + self.slice_fusion = TransformerEncoder( + dim=emb_ch, + heads=12 if emb_ch % 12 == 0 else 8, + ff_mult=1, + attn_dropout=0.0, + pre_norm=True, + depth=1, + attn_flash=True, + ff_no_bias=True, + rotary_pos_emb=True, + ) + self.cls_token = nn.Parameter(torch.randn(1, 1, emb_ch)) + elif slice_fusion_type in ["average", "none"]: + self.slice_fusion = None + else: + raise ValueError("Unknown slice_fusion_type") + + self.linear = nn.Linear(emb_ch, out_ch) + + def forward(self, x): + B, *_ = x.shape + x = rearrange(x, 'b c d h w -> (b c d) h w') + x = x[:, None].repeat(1, 3, 1, 1) # Gray to RGB + + x = self.backbone(x) # (B * D, E) + x = rearrange(x, '(b d) e -> b d e', b=B) + + if self.slice_fusion_type == 'none': + return x + elif self.slice_fusion_type == 'transformer': + x = torch.cat([x, self.cls_token.repeat(B, 1, 1)], dim=1) + x = self.slice_fusion(x) + elif self.slice_fusion_type == 'average': + x = x.mean(dim=1, keepdim=True) + + x = self.linear(x[:, -1]) + return x + + +class MST(BasicClassifier): + """MST-based classifier using ViT or ResNet as backbone.""" + + def __init__( + self, + n_input_channels: int, + num_classes: int, + spatial_dims: int, + backbone_type="dinov2", + model_size="s", + slice_fusion_type="transformer", + optimizer_kwargs={'lr': 1e-6}, + **kwargs + ): + super().__init__(n_input_channels, num_classes, spatial_dims, optimizer_kwargs=optimizer_kwargs, **kwargs) + self.mst = _MST(out_ch=num_classes, backbone_type=backbone_type, model_size=model_size, + slice_fusion_type=slice_fusion_type) + + def forward(self, x): + return self.mst(x) diff --git a/application/jobs/ODELIA_ternary_classification/app/custom/models/resnet.py b/application/jobs/ODELIA_ternary_classification/app/custom/models/resnet.py new file mode 100644 index 00000000..49503b26 --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/custom/models/resnet.py @@ -0,0 +1,79 @@ +from models import BasicClassifier +import monai.networks.nets as nets +import torch.nn as nn +from einops import rearrange + + +class _ResNet(nn.Module): + """Wrapper for MONAI ResNet models supporting 3D/2D input.""" + + def __init__(self, n_input_channels: int, num_classes: int, spatial_dims: int, resnet_variant: int): + super().__init__() + Model = { + 10: nets.resnet10, + 18: nets.resnet18, + 34: nets.resnet34, + 50: nets.resnet50, + 101: nets.resnet101, + 152: nets.resnet152 + }.get(resnet_variant) + if Model is None: + raise ValueError(f"Unsupported ResNet model number: {resnet_variant}") + + shortcut_type = { + 10: 'B', + 18: 'A', + 34: 'A', + 50: 'B', + 101: 'B', + 152: 'B', + }.get(resnet_variant) + + bias_downsample = { + 10: False, + 18: True, + 34: True, + 50: False, + 101: False, + 152: False, + }.get(resnet_variant) + + num_channels = { + 10: 512, + 18: 512, + 34: 512, + 50: 2048, + 101: 2048, + 152: 2048, + }.get(resnet_variant) + + self.model = Model(n_input_channels=n_input_channels, spatial_dims=spatial_dims, num_classes=num_classes, + feed_forward=False, shortcut_type=shortcut_type, bias_downsample=bias_downsample, pretrained=True) + self.model.fc = nn.Linear(num_channels, + num_classes) + + def forward(self, x): + return self.model(x) + + +class ResNet(BasicClassifier): + """ResNet-based classifier using MONAI backbones.""" + + def __init__(self, n_input_channels: int, num_classes: int, spatial_dims: int, resnet_variant: int, **kwargs): + super().__init__(n_input_channels, num_classes, spatial_dims, **kwargs) + self.model = _ResNet(n_input_channels, num_classes, spatial_dims, resnet_variant) + + def forward(self, x): + return self.model(x) + + +''' +class ResNetRegression(BasicRegression): + """ResNet-based regression model using MONAI backbones.""" + def __init__(self, n_input_channels: int, num_classes: int , spatial_dims: int, resnet_variant: str, **kwargs): + super().__init__(n_input_channels, num_classes, spatial_dims, **kwargs) + self.model = _ResNet(n_input_channels, num_classes, resnet_variant) + + def forward(self, x): + return self.model(x) +''' diff --git a/application/jobs/ODELIA_ternary_classification/app/custom/threedcnn_ptl.py b/application/jobs/ODELIA_ternary_classification/app/custom/threedcnn_ptl.py new file mode 100644 index 00000000..ad291652 --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/custom/threedcnn_ptl.py @@ -0,0 +1,156 @@ +from sklearn.model_selection import train_test_split +import torch +from pytorch_lightning import Trainer +from pytorch_lightning.callbacks import ModelCheckpoint +from pytorch_lightning.loggers import TensorBoardLogger +from data.datamodules import DataModule +from models import ResNet, MST +from env_config import load_environment_variables, prepare_odelia_dataset, generate_run_directory +import torch.multiprocessing as mp + +import logging + + +def get_num_epochs_per_round(site_name: str) -> int: + NUM_EPOCHS_FOR_SITE = { + "TUD_1": 2, "TUD_2": 4, "TUD_3": 8, + "MEVIS_1": 2, "MEVIS_2": 4, + } + max_epochs = NUM_EPOCHS_FOR_SITE.get(site_name, 5) + print(f"Site name: {site_name}") + print(f"Max epochs set to: {max_epochs}") + return max_epochs + + +def set_up_logging(): + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + return logger + + +def set_up_data_module(logger): + torch.set_float32_matmul_precision('high') + ds_train, ds_val, path_run_dir, run_name = prepare_odelia_dataset() + num_classes = sum(ds_train.class_labels_num) + logger.info(f"Dataset path: {ds_train}") + logger.info(f"Run directory: {path_run_dir}") + logger.info(f"Run name: {run_name}") + # logger.info(f"Number of classes: {num_classes}") # number of possible classes, not number of classes present, thus misleading + logger.info(f"Length of train dataset: {len(ds_train)}") + logger.info(f"Length of val dataset: {len(ds_val)}") + + dm = DataModule( + ds_train=ds_train, + ds_val=ds_val, + ds_test=ds_val, + batch_size=1, + pin_memory=True, + weights=None, + num_workers=mp.cpu_count(), + ) + + # # Log label distribution + # distribution = dm.get_train_label_distribution(lambda sample: sample['label']) + # logger.info(f"Total samples in training set: {distribution['total']}") + # for label, pct in distribution['percentages'].items(): + # logger.info(f"Label '{label}': {pct:.2f}% of training set, Count: {distribution['counts'][label]}") + # logger.info(f"Number of unique labels: {len(distribution['counts'])}") + + loss_kwargs = {} + + return dm, path_run_dir, run_name, num_classes, loss_kwargs + + +def create_run_directory(env_vars): + return generate_run_directory( + env_vars['scratch_dir'], + env_vars['task_data_name'], + env_vars['model_name'], + env_vars['local_compare_flag'] + ) + + +def prepare_training(logger, max_epochs: int, site_name: str): + try: + env_vars = load_environment_variables() + data_module, path_run_dir, run_name, num_classes, loss_kwargs = set_up_data_module(logger) + + if not torch.cuda.is_available(): + raise RuntimeError("This example requires a GPU") + + logger.info(f"Running code version {env_vars['mediswarm_version']}") + logger.info(f"Using GPU for training") + + model_name = env_vars['model_name'] + + model = None + if model_name in ['ResNet10', 'ResNet18', 'ResNet34', 'ResNet50', 'ResNet101', 'ResNet152']: + resnet_variant = int(model_name[6:]) + model = ResNet(n_input_channels=1, + num_classes=num_classes, + spatial_dims=3, + resnet_variant=resnet_variant, + loss_kwargs=loss_kwargs) + elif model_name == 'MST': + model = MST(n_input_channels=1, + num_classes=num_classes, + spatial_dims=3, + loss_kwargs=loss_kwargs) + + logger.info(f"Using model: {model_name}") + + to_monitor = "val/ACC" + min_max = "max" + log_every_n_steps = 50 + + ''' + early_stopping = EarlyStopping( + monitor=to_monitor, + min_delta=0.0, + patience=25, + mode=min_max + ) + ''' + checkpointing = ModelCheckpoint( + dirpath=str(path_run_dir), + monitor=to_monitor, + save_last=True, + save_top_k=1, + mode=min_max, + ) + + trainer = Trainer( + accelerator='gpu', + accumulate_grad_batches=1, + precision='16-mixed', + default_root_dir=str(path_run_dir), + callbacks=[checkpointing], + enable_checkpointing=True, + check_val_every_n_epoch=1, + log_every_n_steps=log_every_n_steps, + max_epochs=max_epochs, + num_sanity_val_steps=2, + logger=TensorBoardLogger(save_dir=path_run_dir) + ) + + except Exception as e: + logger.error(f"Error in prepare_training: {e}") + raise + + return data_module, model, checkpointing, trainer, path_run_dir, env_vars + + +def validate_and_train(logger, data_module, model, trainer) -> None: + logger.info("--- Validate global model ---") + trainer.validate(model, datamodule=data_module) + + logger.info("--- Train new model ---") + trainer.fit(model, datamodule=data_module) + + +def finalize_training(logger, model, checkpointing, trainer, path_run_dir, env_vars) -> None: + model.save_best_checkpoint(trainer.logger.log_dir, checkpointing.best_model_path) + + logger.info('Prediction currently not implemented.') + + logger.info('Training completed successfully.') diff --git a/application/jobs/ODELIA_ternary_classification/app/custom/utils/roc_curve.py b/application/jobs/ODELIA_ternary_classification/app/custom/utils/roc_curve.py new file mode 100644 index 00000000..0ac2f4f2 --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/custom/utils/roc_curve.py @@ -0,0 +1,238 @@ +import numpy as np +import matplotlib +from sklearn.metrics import roc_curve, auc, confusion_matrix + + +def auc_bootstrapping(y_true, y_score, bootstrapping=1000, drop_intermediate=False): + """Perform bootstrapping to compute variability of ROC curve and AUC. + + Args: + y_true (np.ndarray): True binary labels. + y_score (np.ndarray): Predicted scores or probabilities. + bootstrapping (int): Number of bootstrap samples. + drop_intermediate (bool): Whether to drop some thresholds for faster computation. + + Returns: + Tuple[list, list, list, np.ndarray]: + - List of interpolated TPRs, + - List of AUCs, + - List of optimal thresholds, + - Mean FPR values used for interpolation. + """ + tprs, aucs, thrs = [], [], [] + mean_fpr = np.linspace(0, 1, 100) + rng = np.random.default_rng(seed) + + # Generate bootstrap samples with replacement + rand_idxs = rng.integers(0, len(y_true), size=(bootstrapping, len(y_true))) + + for rand_idx in rand_idxs: + y_true_set = y_true[rand_idx] + y_score_set = y_score[rand_idx] + + # Compute ROC for the sample + fpr, tpr, thresholds = roc_curve(y_true_set, y_score_set, drop_intermediate=drop_intermediate) + + # Interpolate TPRs to a common FPR scale + tpr_interp = np.interp(mean_fpr, fpr, tpr) + tprs.append(tpr_interp) + aucs.append(auc(fpr, tpr)) + + # Identify optimal threshold (Youden's J statistic) + optimal_idx = np.argmax(tpr - fpr) + thrs.append(thresholds[optimal_idx]) + + return tprs, aucs, thrs, mean_fpr + + +def plot_roc_curve(y_true, y_score, axis, bootstrapping=1000, drop_intermediate=False, fontdict={}, + name='ROC', color='b', show_wp=True): + """Plot ROC curve with bootstrapped AUC and shaded confidence interval. + + Args: + y_true (np.ndarray): True binary labels. + y_score (np.ndarray): Predicted probabilities or scores. + axis (matplotlib.axes.Axes): Axis to plot on. + bootstrapping (int): Number of bootstrap samples. + drop_intermediate (bool): Drop thresholds for faster computation. + fontdict (dict): Font styling dictionary. + name (str): Curve label. + color (str): Line color. + show_wp (bool): Show working point (optimal threshold marker). + + Returns: + Tuple[np.ndarray, np.ndarray, float, np.ndarray, int]: + FPR, TPR, AUC, thresholds, and index of optimal threshold. + """ + # Bootstrapping + tprs, aucs, thrs, mean_fpr = auc_bootstrapping(y_true, y_score, bootstrapping, drop_intermediate) + + mean_tpr = np.nanmean(tprs, axis=0) + mean_tpr[-1] = 1.0 # Ensure proper endpoint + std_tpr = np.nanstd(tprs, axis=0, ddof=1) + tprs_upper = np.minimum(mean_tpr + std_tpr, 1) + tprs_lower = np.maximum(mean_tpr - std_tpr, 0) + + mean_auc = np.nanmean(aucs) + std_auc = np.nanstd(aucs, ddof=1) + + # Compute actual ROC + fprs, tprs_, thrs_ = roc_curve(y_true, y_score, drop_intermediate=drop_intermediate) + auc_val = auc(fprs, tprs_) + opt_idx = np.argmax(tprs_ - fprs) + opt_tpr = tprs_[opt_idx] + opt_fpr = fprs[opt_idx] + + # Plot ROC + axis.plot(fprs, tprs_, color=color, label=rf"{name} (AUC={auc_val:.2f}$\pm${std_auc:.2f})", lw=2, alpha=.8) + axis.fill_between(mean_fpr, tprs_lower, tprs_upper, color='grey', alpha=.2, label=r'$\pm$ 1 std. dev.') + + if show_wp: + axis.hlines(y=opt_tpr, xmin=0.0, xmax=opt_fpr, color=color, linestyle='--') + axis.vlines(x=opt_fpr, ymin=0.0, ymax=opt_tpr, color=color, linestyle='--') + axis.plot(opt_fpr, opt_tpr, color=color, marker='o') + + axis.plot([0, 1], [0, 1], linestyle='--', color='k') + axis.set_xlim([0.0, 1.0]) + axis.set_ylim([0.0, 1.0]) + + axis.legend(loc='lower right') + axis.set_xlabel('1 - Specificity', fontdict=fontdict) + axis.set_ylabel('Sensitivity', fontdict=fontdict) + + # Aesthetic tweaks + axis.grid(color='#dddddd') + axis.set_axisbelow(True) + axis.tick_params(colors='#dddddd', which='both') + for xtick in axis.get_xticklabels(): + xtick.set_color('k') + for ytick in axis.get_yticklabels(): + ytick.set_color('k') + for child in axis.get_children(): + if isinstance(child, matplotlib.spines.Spine): + child.set_color('#dddddd') + + return fprs, tprs_, auc_val, thrs_, opt_idx + + +def cm2acc(cm): + """Calculate accuracy from a 2x2 confusion matrix.""" + tn, fp, fn, tp = cm.ravel() + return (tn + tp) / (tn + tp + fn + fp) + + +def safe_div(x, y): + """Safely divide x by y, return NaN if y is zero.""" + return float('nan') if y == 0 else x / y + + +def specificity_at_fixed_sensitivity(y_true, y_scores, tpr, thresholds, sensitivity_target=0.90): + """Calculate specificity at a given sensitivity level. + + Args: + y_true (np.ndarray): Ground truth labels. + y_scores (np.ndarray): Predicted scores. + tpr (np.ndarray): True positive rates from ROC. + thresholds (np.ndarray): Thresholds from ROC. + sensitivity_target (float): Desired sensitivity level. + + Returns: + float: Specificity at the closest sensitivity. + """ + idx = np.argmin(np.abs(tpr - sensitivity_target)) + chosen_threshold = thresholds[idx] + y_pred = (y_scores >= chosen_threshold).astype(int) + tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel() + return tn / (tn + fp) + + +def sensitivity_at_fixed_specificity(y_true, y_scores, fpr, thresholds, specificity_target=0.90): + """Calculate sensitivity at a given specificity level. + + Args: + y_true (np.ndarray): Ground truth labels. + y_scores (np.ndarray): Predicted scores. + fpr (np.ndarray): False positive rates from ROC. + thresholds (np.ndarray): Thresholds from ROC. + specificity_target (float): Desired specificity level. + + Returns: + float: Sensitivity at the closest specificity. + """ + specificity = 1 - fpr + idx = np.argmin(np.abs(specificity - specificity_target)) + chosen_threshold = thresholds[idx] + y_pred = (y_scores >= chosen_threshold).astype(int) + tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel() + return tp / (tp + fn) + + +def cm2x(cm, average='macro', pos_label=1): + """Compute PPV, NPV, Sensitivity (TPR), and Specificity (TNR) from confusion matrix. + + Args: + cm (np.ndarray): Confusion matrix. + average (str): 'binary', 'micro', 'macro', or 'weighted'. + pos_label (int): Class considered positive in binary mode. + + Returns: + dict: Dictionary with PPV, NPV, TPR, and TNR. + """ + num_classes = cm.shape[0] + metrics_per_class = {} + + if average == 'micro': + TP = np.sum([cm[i, i] for i in range(num_classes)]) + FP = np.sum([cm[:, i].sum() - cm[i, i] for i in range(num_classes)]) + FN = np.sum([cm[i, :].sum() - cm[i, i] for i in range(num_classes)]) + TN = np.sum([cm.sum() - (cm[i, :].sum() + cm[:, i].sum() - cm[i, i]) for i in range(num_classes)]) + + return { + "PPV": safe_div(TP, TP + FP), + "NPV": safe_div(TN, TN + FN), + "TPR": safe_div(TP, TP + FN), + "TNR": safe_div(TN, TN + FP), + } + + for i in range(num_classes): + TP = cm[i, i] + FP = cm[:, i].sum() - TP + FN = cm[i, :].sum() - TP + TN = cm.sum() - (TP + FP + FN) + + metrics_per_class[i] = { + "PPV": safe_div(TP, TP + FP), + "NPV": safe_div(TN, TN + FN), + "TPR": safe_div(TP, TP + FN), + "TNR": safe_div(TN, TN + FP), + } + + if average == 'binary': + if pos_label not in metrics_per_class: + raise ValueError(f"pos_label={pos_label} not in class labels: {list(metrics_per_class.keys())}") + return metrics_per_class[pos_label] + + ppv_vals = [metrics_per_class[i]["PPV"] for i in range(num_classes)] + npv_vals = [metrics_per_class[i]["NPV"] for i in range(num_classes)] + tpr_vals = [metrics_per_class[i]["TPR"] for i in range(num_classes)] + tnr_vals = [metrics_per_class[i]["TNR"] for i in range(num_classes)] + + if average == 'macro': + return { + "PPV": np.mean(ppv_vals), + "NPV": np.mean(npv_vals), + "TPR": np.mean(tpr_vals), + "TNR": np.mean(tnr_vals), + } + + if average == 'weighted': + support = cm.sum(axis=1) + weights = support / support.sum() + return { + "PPV": np.sum(weights * np.array(ppv_vals)), + "NPV": np.sum(weights * np.array(npv_vals)), + "TPR": np.sum(weights * np.array(tpr_vals)), + "TNR": np.sum(weights * np.array(tnr_vals)), + } + + raise ValueError("Invalid average method. Choose from {'binary', 'micro', 'macro', 'weighted'}.") diff --git a/application/jobs/ODELIA_ternary_classification/app/scripts/README.md b/application/jobs/ODELIA_ternary_classification/app/scripts/README.md new file mode 100644 index 00000000..caf02444 --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/scripts/README.md @@ -0,0 +1,90 @@ +# Preprocessing scripts for ODELIA - Breast MRI Classification + +## Step 1: Download [DUKE](https://sites.duke.edu/mazurowski/resources/breast-cancer-mri-dataset/) Dataset + +* Create a folder `DUKE` with a subfolder `data_raw` +* [Download](https://wiki.cancerimagingarchive.net/pages/viewpage.action?pageId=70226903) files form The Cancer Imaging + Archive (TCIA) into `data_raw` +* Make sure to download the dataset in the "classical" structure (PatientID - StudyInstanceUID - SeriesInstanceUID) +* Place all tables in a folder "metadata" +* The folder structure should look like: + ```bash + DUKE + ├── data_raw + │ ├── Breast_MRI_001 + │ │ ├── 1.3.6.1.4.1.14519 + | | | ├── 1.3.6.1.4.1.14519.5.2.1.10 + | | | ├── 1.3.6.1.4.1.14519.5.2.1.17 + │ ├── Breast_MRI_002 + │ | ├── ... + ├── metadata + | ├── Breast-Cancer-MRI-filepath_filename-mapping.xlsx + | ├── Clinical_and_Other_Features.xlsx + ``` + +## Step 2: Prepare Data ([DUKE](https://sites.duke.edu/mazurowski/resources/breast-cancer-mri-dataset/)) + +* Specify the path to the parent folder as `path_root=...` and `dataset=DUKE` in the following scripts +* Run [step1_dicom2nifti.py](preprocessing/duke/step1_dicom2nifti.py) - It will + store DICOM files as NIFTI files in a new folder `data` +* Run [scripts/preprocessing/step2_compute_sub.py](preprocessing/step2_compute_sub.py) - computes the + subtraction image +* Run [scripts/preprocessing/step3_unilateral.py](preprocessing/step3_unilateral.py) - splits breasts into left + and right side and resamples to uniform shape. The result is stored in a new folder `data_unilateral` +* Run [scripts/preprocessing/duke/step4_create_split.py](preprocessing/duke/step4_create_split.py) - creates a + stratified five-fold split and stores the result in `metadata/split.csv` + +
+ +## Step 3: Prepare Data ([ODELIA](https://odelia.ai/)) + +* Create a folder with the initials of your institution e.g. `ABC` +* Place your DICOM files in a subfolder `data_raw` +* Create a folder `metadata` with the following file inside: + * Challenge: `annotation.xlsx` + * Local Training: `ODELIA annotation scheme-2.0.xlsx` +* Overwrite [scripts/preprocessing/odelia/step1_dicom2nifti.py](preprocessing/odelia/step1_dicom2nifti.py). It + should create a subfolder `data` and subfolders with files named as `T2.nii.gz`, `Pre.nii.gz`, `Post_1.nii.gz`, + `Post_2.nii.gz`, etc. + The subfolder should be labeled as follows: + * Challenge: Folders must have the same name as the entries in the `ID` column of the `annotation.xlsx` file. + * Local Training: Folders must have the same name as the entries in the `StudyInstanceUID` column of the + `ODELIA annotation scheme-2.0.xlsx` file. +* Run [scripts/preprocessing/step2_compute_sub.py](preprocessing/step2_compute_sub.py) - computes the + subtraction image +* Run [scripts/preprocessing/step3_unilateral.py](preprocessing/step3_unilateral.py) - splits breasts into left + and right side and resamples to uniform shape. The result is stored in a new folder `data_unilateral` +* To create a five-fold stratified split and store the result in `metadata/split.csv`, run the following script: + * Local Training: [scripts/preprocessing/odelia/step4_create_split.py](preprocessing/odelia/step4_create_split.py) + +* The final folder structure should look like: + ```bash + ABC + ├── data_raw + ├── data + │ ├── ID_001 + │ │ ├── Pre.nii.gz + | | ├── Post_1.nii.gz + | | ├── Post_2.nii.gz + │ ├── ID_002 + │ | ├── ... + ├── data_unilateral + │ ├── ID_001_left + │ ├── ID_001_right + ├── metadata + | ├── annotation.xlsx + | ├── split.csv + ``` + +
+ +## Step 4: Run Training + +* Specify path to downloaded folder as `PATH_ROOT=` + in [dataset_3d_odelia.py](../custom/data/datasets/dataset_3d_odelia.py) +* Run Script: [main_train.py](main_train.py) + +## Step 5: Predict & Evaluate Performance + +* Run Script: [main_predict.py](main_predict.py) +* Set `path_run` to root directory of latest model diff --git a/application/jobs/ODELIA_ternary_classification/app/scripts/create_synthetic_dataset/.gitignore b/application/jobs/ODELIA_ternary_classification/app/scripts/create_synthetic_dataset/.gitignore new file mode 100644 index 00000000..47d769c1 --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/scripts/create_synthetic_dataset/.gitignore @@ -0,0 +1 @@ +synthetic_dataset \ No newline at end of file diff --git a/application/jobs/ODELIA_ternary_classification/app/scripts/create_synthetic_dataset/create_synthetic_dataset.py b/application/jobs/ODELIA_ternary_classification/app/scripts/create_synthetic_dataset/create_synthetic_dataset.py new file mode 100755 index 00000000..52e8ab4f --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/scripts/create_synthetic_dataset/create_synthetic_dataset.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 + +import csv +from itertools import product +import numpy as np +import os +import pathlib +import shutil +import sys +import SimpleITK as sitk +from tqdm import tqdm + +np.random.seed(1) + +size = (32, 256, 256) +num_images_per_site = 15 +sites = ('client_A', 'client_B') # this must match the swarm project definition +metadata_folder = 'metadata_unilateral' +data_folder = 'data_unilateral' +other_unused_folders = ('data_raw', 'data') +folders = other_unused_folders + (metadata_folder, data_folder) +some_age = 42 * 365 +num_folds = 5 + + +def create_folder_structure(output_folder) -> None: + shutil.rmtree(output_folder, ignore_errors=True) + os.makedirs(output_folder, exist_ok=True) + for i, site in enumerate(sites): + os.mkdir(output_folder / site) + for folder in folders: + os.mkdir(output_folder / site / folder) + + +def get_image(i: int, j: int, lesion_class: int): + # create three different types of images depending on the class + array = np.random.randint(-10, 10, size=size, dtype=np.int16) + if lesion_class == 0: + array[:, i, j] = -50 + elif lesion_class == 1: + array[:, i, j] = 200 + else: + array[:size[2] // 2, i, j] = 200 + array[size[2] // 2:, i, j] = 50 + image = sitk.GetImageFromArray(array) + return image + + +def save_table(output_folder, site: str, table_data: dict) -> None: + def write_split_csv(output_folder, site: str, table_data: dict) -> None: + with open(output_folder / site / metadata_folder / 'split.csv', 'w') as output_csv: + split_fields = ('UID', 'Fold', 'Split') + writer = csv.DictWriter(output_csv, fieldnames=split_fields) + writer.writeheader() + for linedata in table_data: + writer.writerow({sf: linedata[sf] for sf in split_fields}) + + def _get_annotation_data(table_data: dict, annotation_fields: tuple) -> list: + annotation_data = [{af: linedata[af] for af in annotation_fields} for linedata in table_data] + entries = list({tuple(d.items()) for d in annotation_data}) + entries.sort() + annotation_data = [dict(t) for t in entries] + return annotation_data + + def write_annotation_csv(output_folder, site: str, table_data: dict) -> None: + with open(output_folder / site / metadata_folder / 'annotation.csv', 'w') as output_csv: + annotation_fields = ('UID', 'PatientID', 'Age', 'Lesion') + writer = csv.DictWriter(output_csv, fieldnames=annotation_fields) + writer.writeheader() + + annotation_data = _get_annotation_data(table_data, annotation_fields) + for linedata in annotation_data: + writer.writerow(linedata) + + write_split_csv(output_folder, site, table_data) + write_annotation_csv(output_folder, site, table_data) + + +def get_split(fold: int, num: int) -> str: + # mimic 60/20/20 split that slightly differs between folds + index = ((fold + num) % num_images_per_site) / num_images_per_site + if index < 0.6: + return 'train' + elif index < 0.8: + return 'val' + else: + return 'test' + + +if __name__ == '__main__': + if len(sys.argv) != 2: + print('usage: create_synthetic_dataset.py ') + exit(1) + + output_folder = pathlib.Path(sys.argv[1]) + create_folder_structure(output_folder) + + for i, site in enumerate(sites): + table_data = [] + for j in tqdm(range(num_images_per_site), f'Generating synthetic images for {site}'): + lesion_class = j % 3 + image = get_image(i, j, lesion_class) + for side in ('left', 'right'): + patientid = f'ID_{j:03d}' + uid = f'{patientid}_{side}' + side_folder = output_folder / site / data_folder / uid + os.mkdir(side_folder) + # sitk.WriteImage(image, side_folder/'Pre.nii.gz') + sitk.WriteImage(image, side_folder / 'Sub_1.nii.gz') + # sitk.WriteImage(image, side_folder/'T2.nii.gz') + for f in range(num_folds): + table_data.append( + {'UID': uid, 'PatientID': patientid, 'Lesion': lesion_class, 'Age': some_age + i + j, 'Fold': f, + 'Split': get_split(j, f)}) + + save_table(output_folder, site, table_data) diff --git a/application/jobs/ODELIA_ternary_classification/app/scripts/main_predict.py b/application/jobs/ODELIA_ternary_classification/app/scripts/main_predict.py new file mode 100644 index 00000000..30d2f45e --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/scripts/main_predict.py @@ -0,0 +1,185 @@ +import argparse +from pathlib import Path +import logging +from tqdm import tqdm +import torch +import numpy as np +import matplotlib.pyplot as plt +import seaborn as sns +import pandas as pd +import ast +import torch.nn.functional as F +import torch.multiprocessing as mp +from sklearn.metrics import confusion_matrix, accuracy_score, cohen_kappa_score, roc_auc_score, roc_curve + +from odelia.data.datasets import ODELIA_Dataset3D +from odelia.data.datamodules import DataModule +from odelia.models import MST, ResNet, MSTRegression, ResNetRegression +from odelia.utils.roc_curve import cm2x, plot_roc_curve, sensitivity_at_fixed_specificity, \ + specificity_at_fixed_sensitivity + + +def one_hot(y, num_classes): + return np.eye(num_classes, dtype=int)[y] + + +def evaluate(gt, nn, nn_prob, label, label_vals, path_out): + plt.rcParams.update({'font.size': 12}) + fontdict = {'fontsize': 12, 'fontweight': 'bold'} + colors = ['b', 'g', 'r'] + y_prob = np.asarray(nn_prob) + y_pred = np.asarray(nn) + y_true = np.asarray(gt) + labels = list(range(len(label_vals))) + + fig, axes = plt.subplots(ncols=2, figsize=(12, 6)) + + # ------------------------------- ROC-AUC --------------------------------- + y_true_hot = one_hot(y_true, len(label_vals)) + y_prob = np.stack([1 - y_prob, y_prob], axis=1) if binary else y_prob # Convert to one-hot + # fig, axis = plt.subplots(ncols=1, nrows=1, figsize=(6,6)) + axis = axes[0] + results = {'AUC': [], 'Sensitivity': [], 'Specificity': []} + for i in range(len(label_vals)): + if binary and i == 0: + continue + y_true_i = y_true_hot[:, i] + y_prob_i = y_prob[:, i] + fprs, tprs, auc_val, thrs, opt_idx = plot_roc_curve(y_true_i, y_prob_i, axis, color=colors[i], + name=f"AUC {label_vals[i]} {label} ", fontdict=fontdict) + # fprs, tprs, thrs = roc_curve(y_true_hot[:,i], y_prob[:, i], drop_intermediate=False) + sensitivity = sensitivity_at_fixed_specificity(y_true_i, y_prob_i, fprs, thrs, 0.9) + specificity = specificity_at_fixed_sensitivity(y_true_i, y_prob_i, tprs, thrs, 0.9) + print( + f"{label_vals[i]} {label}: AUC {auc_val:.2f} Sensitivity {sensitivity:.2f} Specificity: {specificity:.2f}") + results['AUC'].append(auc_val) + results['Sensitivity'].append(sensitivity) + results['Specificity'].append(specificity) + print( + f"{label}: AUC {np.mean(results['AUC']):.2f} Sensitivity {np.mean(results['Sensitivity']):.2f} Specificity: {np.mean(results['Specificity']):.2f}") + # fig.tight_layout() + # fig.savefig(path_out/f'roc_{label}.png', dpi=300) + + # -------------------------- Confusion Matrix ------------------------- + cm = confusion_matrix(y_true, y_pred, labels=labels) + acc = accuracy_score(y_true, y_pred) + metrics = cm2x(cm, "macro") + + print(f"Accuracy: {acc:.2f}") + print(f"Sensitivity: {metrics['TPR']:.2f}") + print(f"Specificity {metrics['TNR']:.2f}") + + df_cm = pd.DataFrame(data=cm, columns=label_vals, index=label_vals) + # fig, axis = plt.subplots(1, 1, figsize=(4,4)) + axis = axes[1] + sns.heatmap(df_cm, ax=axis, cbar=False, cmap="Blues", fmt='d', annot=True) + axis.set_title(f'{label}', fontdict=fontdict) # CM = [[TN, FP], [FN, TP]] + axis.set_xlabel('Neural Network', fontdict=fontdict) + axis.set_ylabel('Radiologist', fontdict=fontdict) + # fig.tight_layout() + # fig.savefig(path_out/f'confusion_matrix_{label}.png', dpi=300) + + fig.tight_layout() + fig.subplots_adjust(wspace=0.4) + fig.savefig(path_out / f'roc_conf_{label}.png', dpi=300) + + # -------------------------- Agreement ------------------------- + # kappa = cohen_kappa_score(y_true, y_pred, weights="linear") + # print(label, "Kappa", kappa) + + +if __name__ == "__main__": + # ------------ Get Arguments ---------------- + parser = argparse.ArgumentParser() + parser.add_argument('--path_run', + default='runs/ODELIA/MST_binary_unilateral_2025_05_13_170027/epoch=22-step=188922.ckpt', + type=str) + parser.add_argument('--test_institution', default='ODELIA', type=str) + args = parser.parse_args() + batch_size = 4 + + # ------------ Settings/Defaults ---------------- + path_run = Path(args.path_run) + train_institution = path_run.parent.parent.name + run_name = path_run.parent.name + path_out = Path().cwd() / 'results' / train_institution / run_name / args.test_institution + path_out.mkdir(parents=True, exist_ok=True) + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + # ------------ Logging -------------------- + logger = logging.getLogger(__name__) + logging.basicConfig(level=logging.INFO) + + # ------------ Load Data ---------------- + split = None if args.test_institution == 'RUMC' else 'test' # Use all samples if testing on RUMC + binary = run_name.split('_')[1] == "binary" + config = run_name.split('_')[2] + ds_test = ODELIA_Dataset3D(split=split, institutions=args.test_institution, binary=binary, config=config) + + dm = DataModule( + ds_test=ds_test, + batch_size=batch_size, + num_workers=mp.cpu_count(), + # pin_memory=True, + ) + + # ------------ Initialize Model ------------ + model = run_name.split('_')[0] + model_map = { + 'ResNet': ResNet if binary else ResNetRegression, + 'MST': MST if binary else MSTRegression + } + MODEL = model_map.get(model, None) + model = MODEL.load_from_checkpoint(path_run) + model.to(device) + model.eval() + + # ------------ Predict ---------------- + results = [] + for batch in tqdm(dm.test_dataloader()): + uid, source, target = batch['uid'], batch['source'], batch['target'] + + with torch.no_grad(): + logits = model(source.to(device)).cpu() + + # Transfer logits to integer + pred_prob = model.logits2probabilities(logits) + pred = model.logits2labels(logits) + + for b in range(pred.size(0)): + results.append({ + 'UID': uid[b], + 'GT': target[b].tolist(), + 'NN': pred[b].tolist(), + 'NN_prob': pred_prob[b].tolist(), + }) + + # ------------ Save Results ---------------- + df = pd.DataFrame(results) + df.to_csv(path_out / 'results.csv', index=False) + + # ------------ Evaluate ---------------- + df = pd.read_csv(path_out / 'results.csv') + df['GT'] = df['GT'].apply(ast.literal_eval) + df['NN'] = df['NN'].apply(ast.literal_eval) + df['NN_prob'] = df['NN_prob'].apply(ast.literal_eval) + + gt = np.stack(df['GT'].values) + nn = np.stack(df['NN'].values) + nn_prob = np.stack(df['NN_prob'].values) + labels = ODELIA_Dataset3D.CLASS_LABELS[config] # {'Malignant Lesion': ['No', 'Yes']} if binary else + for i in range(gt.shape[1]): + label = list(labels.keys())[i] + label_vals = labels[label] + evaluate(gt[:, i], nn[:, i], nn_prob[:, i], label, label_vals, path_out) + + # If original(bilateral), evaluate for left and right together + if config == 'original': + gt = gt.reshape(-1, 1) + nn = nn.reshape(-1, 1) + nn_prob = nn_prob.reshape(-1, 1) + labels = ODELIA_Dataset3D.CLASS_LABELS['unilateral'] + for i in range(gt.shape[1]): + label = list(labels.keys())[i] + label_vals = labels[label] + evaluate(gt[:, i], nn[:, i], nn_prob[:, i], label, label_vals, path_out) diff --git a/application/jobs/ODELIA_ternary_classification/app/scripts/main_train.py b/application/jobs/ODELIA_ternary_classification/app/scripts/main_train.py new file mode 100644 index 00000000..a2580f33 --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/scripts/main_train.py @@ -0,0 +1,114 @@ +from pathlib import Path +from datetime import datetime + +import torch +from pytorch_lightning.trainer import Trainer +from pytorch_lightning.callbacks import EarlyStopping, ModelCheckpoint +from pytorch_lightning.loggers import WandbLogger +import torch.multiprocessing as mp +from odelia.data.datasets import ODELIA_Dataset3D +from odelia.data.datamodules import DataModule +from odelia.models import ResNet, MST, ResNetRegression, MSTRegression +import argparse + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('--institution', default='ODELIA', type=str) + parser.add_argument('--model', type=str, default='MST', choices=['ResNet', 'MST']) + parser.add_argument('--task', type=str, default="binary", choices=['binary', + 'ordinal']) # binary: malignant lesion yes/no, ordinal: no lesion, benign, malignant + parser.add_argument('--config', type=str, default="unilateral", choices=['original', 'unilateral']) + args = parser.parse_args() + binary = args.task == 'binary' + + # ------------ Settings/Defaults ---------------- + current_time = datetime.now().strftime("%Y_%m_%d_%H%M%S") + run_name = f'{args.model}_{args.task}_{args.config}_{current_time}' + path_run_dir = Path.cwd() / 'runs' / args.institution / run_name + path_run_dir.mkdir(parents=True, exist_ok=True) + accelerator = 'gpu' if torch.cuda.is_available() else 'cpu' + torch.set_float32_matmul_precision('high') + + # ------------ Load Data ---------------- + ds_train = ODELIA_Dataset3D(institutions=args.institution, split='train', binary=binary, config=args.config, + random_flip=True, random_rotate=True, random_inverse=False, noise=True) + ds_val = ODELIA_Dataset3D(institutions=args.institution, split='val', binary=binary, config=args.config) + + samples = len(ds_train) + len(ds_val) + batch_size = 1 + accumulate_grad_batches = 1 + steps_per_epoch = samples / batch_size / accumulate_grad_batches + + # class_counts = ds_train.df["Lesion"].value_counts() + # class_weights = 1 / class_counts / len(class_counts) + # weights = ds_train.df["Lesion"].map(lambda x: class_weights[x]).values + + dm = DataModule( + ds_train=ds_train, + ds_val=ds_val, + ds_test=ds_val, + batch_size=batch_size, + pin_memory=True, + weights=None, # weights, + num_workers=mp.cpu_count(), + ) + + # ------------ Initialize Model ------------ + loss_kwargs = {} + out_ch = len(ds_train.labels) + if not binary: + out_ch = sum(ds_train.class_labels_num) + loss_kwargs = {'class_labels_num': ds_train.class_labels_num} + + model_map = { + 'ResNet': ResNet if binary else ResNetRegression, + 'MST': MST if binary else MSTRegression + } + MODEL = model_map.get(args.model, None) + model = MODEL( + in_ch=1, + out_ch=out_ch, + loss_kwargs=loss_kwargs + ) + + # Load pretrained model + # model = ResNet.load_from_checkpoint('runs/DUKE/2024_11_14_132823/epoch=41-step=17514.ckpt') + + # -------------- Training Initialization --------------- + to_monitor = "val/AUC_ROC" if binary else "val/MAE" + min_max = "max" if binary else "min" + log_every_n_steps = 50 + logger = WandbLogger(project='ODELIA', group=args.institution, name=run_name, log_model=False) + + early_stopping = EarlyStopping( + monitor=to_monitor, + min_delta=0.0, # minimum change in the monitored quantity to qualify as an improvement + patience=25, # number of checks with no improvement + mode=min_max + ) + checkpointing = ModelCheckpoint( + dirpath=str(path_run_dir), # dirpath + monitor=to_monitor, + # every_n_train_steps=log_every_n_steps, + save_last=True, + save_top_k=1, + mode=min_max, + ) + trainer = Trainer( + accelerator=accelerator, + accumulate_grad_batches=accumulate_grad_batches, + precision='16-mixed', + default_root_dir=str(path_run_dir), + callbacks=[checkpointing, early_stopping], + enable_checkpointing=True, + check_val_every_n_epoch=1, + log_every_n_steps=log_every_n_steps, + max_epochs=1000, + num_sanity_val_steps=2, + logger=logger + ) + # ---------------- Execute Training ---------------- + trainer.fit(model, datamodule=dm) + + # ------------- Save path to best model ------------- + model.save_best_checkpoint(path_run_dir, checkpointing.best_model_path) diff --git a/application/jobs/ODELIA_ternary_classification/app/scripts/preprocessing/duke/step1_dicom2nifti.py b/application/jobs/ODELIA_ternary_classification/app/scripts/preprocessing/duke/step1_dicom2nifti.py new file mode 100644 index 00000000..5dcf8c4c --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/scripts/preprocessing/duke/step1_dicom2nifti.py @@ -0,0 +1,134 @@ +from pathlib import Path +import logging +import pandas as pd +from multiprocessing import Pool + +import pydicom +import pydicom.datadict +import pydicom.dataelem +import pydicom.sequence +import pydicom.valuerep +from tqdm import tqdm +import SimpleITK as sitk + +# Logging +# path_log_file = path_root/'preprocessing.log' +logger = logging.getLogger(__name__) + + +# s_handler = logging.StreamHandler(sys.stdout) +# f_handler = logging.FileHandler(path_log_file, 'w') +# logging.basicConfig(level=logging.DEBUG, +# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', +# handlers=[s_handler, f_handler]) + + +def maybe_convert(x): + if isinstance(x, pydicom.sequence.Sequence): + # return [maybe_convert(item) for item in x] + return None # Don't store this type of data + elif isinstance(x, pydicom.dataset.Dataset): + # return dataset2dict(x) + return None # Don't store this type of data + elif isinstance(x, pydicom.multival.MultiValue): + return list(x) + elif isinstance(x, pydicom.valuerep.PersonName): + return str(x) + else: + return x + + +def dataset2dict(ds, exclude=['PixelData', '']): + return {keyword: value for key in ds.keys() + if ((keyword := ds[key].keyword) not in exclude) and ((value := maybe_convert(ds[key].value)) is not None)} + + +def series2nifti(series_info): + seq_name, path_series = series_info + path_series = path_root_data / Path(path_series) + if not path_series.is_dir(): + logger.warning(f"Directory not found: {path_series}:") + return + + try: + # Read DICOM + dicom_names = reader.GetGDCMSeriesFileNames(str(path_series)) + reader.SetFileNames(dicom_names) + img_nii = reader.Execute() + + # Read Metadata + ds = pydicom.dcmread(next(path_series.glob('*.dcm'), None), stop_before_pixels=True) + metadata = dataset2dict(ds) + + # Create output folder + path_out_dir = path_root_out_data / path_series.parts[-3] + path_out_dir.mkdir(exist_ok=True, parents=True) + + # Write + filename = seq_name + logger.info(f"Writing file: {filename}:") + path_file = path_out_dir / f'{seq_name}.nii.gz' + sitk.WriteImage(img_nii, path_file) + + metadata['_path_file'] = str(path_file.relative_to(path_root_out_data)) + return metadata + + + except Exception as e: + logger.warning(f"Error in: {path_series}") + logger.warning(str(e)) + + +if __name__ == "__main__": + # Setting + path_root = Path('/home/gustav/Documents/datasets/') + path_root_dataset = path_root / 'DUKE' + + path_root_data = path_root_dataset / 'data_raw/' + path_root_metadata = path_root_dataset / 'metadata' + + path_root_out_data = path_root_dataset / 'data' + path_root_out_data.mkdir(parents=True, exist_ok=True) + + # Init reader + reader = sitk.ImageSeriesReader() + + # Note: Contains path to every single dicom file + # WARNING: reading this .xlsx file takes some time + df_path2name = pd.read_excel(path_root_metadata / 'Breast-Cancer-MRI-filepath_filename-mapping.xlsx') + df_path2name = df_path2name[df_path2name.columns[:4]].copy() + seq_paths = df_path2name['original_path_and_filename'].str.split('/') + df_path2name['PatientID'] = seq_paths.apply(lambda x: int(x[1].rsplit('_', 1)[1])) + df_path2name['SequenceName'] = seq_paths.apply(lambda x: x[2]) + df_path2name['classic_path'] = df_path2name['classic_path'].str.rsplit('/', n=1).str[0] # remove xx.dcm + df_path2name['classic_path'] = df_path2name['classic_path'].str.split('/', n=1).str[ + 1] # remove Duke-Breast-Cancer-MRI/ + df_path2name = df_path2name.drop_duplicates(subset=['PatientID', 'SequenceName'], keep='first') + df_path2name['SequenceName'] = df_path2name['SequenceName'].str.capitalize() # Just convention + df_path2name.to_csv(path_root_metadata / 'Breast-Cancer-MRI-filepath_filename-mapping.csv', index=False) + + df_path2name = pd.read_csv(path_root_metadata / 'Breast-Cancer-MRI-filepath_filename-mapping.csv') + series = list(zip(df_path2name['SequenceName'], + df_path2name['classic_path'])) # NOTE: Only working with TCIA download strategy 'classic_path' + + # Validate + print("Number Series: ", len(series), "of 5034 (5034+127=5161) ") + + # Option 1: Multi-CPU + metadata_list = [] + with Pool() as pool: + for meta in tqdm(pool.imap_unordered(series2nifti, series), total=len(series)): + metadata_list.append(meta) + + # Option 2: Single-CPU (if you need a coffee break) + # metadata_list = [] + # for series_info in tqdm(series): + # meta = series2nifti(series_info) + # metadata_list.append(meta) + + df = pd.DataFrame(metadata_list) + df.to_csv(path_root_metadata / 'metadata.csv', index=False) + + # Check export + num_series = len([path for path in path_root_out_data.rglob('*.nii.gz')]) + print("Number Series: ", num_series, "of 5034 (5034+127=5161) ") diff --git a/application/jobs/ODELIA_ternary_classification/app/scripts/preprocessing/duke/step4_create_split.py b/application/jobs/ODELIA_ternary_classification/app/scripts/preprocessing/duke/step4_create_split.py new file mode 100644 index 00000000..c6645455 --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/scripts/preprocessing/duke/step4_create_split.py @@ -0,0 +1,41 @@ +from pathlib import Path +import numpy as np +import pandas as pd + +from sklearn.model_selection import StratifiedGroupKFold, StratifiedKFold + +path_root = Path('/home/gustav/Documents/datasets/ODELIA/') +path_root_dataset = path_root / 'DUKE' +path_root_metadata = path_root_dataset / 'metadata' + +df = pd.read_excel(path_root_metadata / 'Clinical_and_Other_Features.xlsx', header=[0, 1, 2]) +df = df[df[df.columns[38]] != 'NC'] # check if cancer is bilateral=1, unilateral=0 or NC +df = df[ + [df.columns[0], df.columns[36], df.columns[38]]] # Only pick relevant columns: Patient ID, Tumor Side, Bilateral +df.columns = ['PatientID', 'Location', 'Bilateral'] # Simplify columns as: Patient ID, Tumor Side +dfs = [] +for side in ["left", 'right']: + dfs.append(pd.DataFrame({ + 'PatientID': df["PatientID"].str.split('_').str[2], + 'UID': df["PatientID"] + f"_{side}", + 'Class': df[["Location", "Bilateral"]].apply(lambda ds: int((ds[0] == side[0].upper()) | (ds[1] == 1)), + axis=1)})) +df = pd.concat(dfs, ignore_index=True) + +df = df.reset_index(drop=True) +splits = [] +sgkf = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=0) # StratifiedGroupKFold +sgkf2 = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=0) +for fold_i, (train_val_idx, test_idx) in enumerate(sgkf.split(df['UID'], df['Class'], groups=df['PatientID'])): + df_split = df.copy() + df_split['Fold'] = fold_i + df_trainval = df_split.loc[train_val_idx] + train_idx, val_idx = list(sgkf2.split(df_trainval['UID'], df_trainval['Class'], groups=df_trainval['PatientID']))[0] + train_idx, val_idx = df_trainval.iloc[train_idx].index, df_trainval.iloc[val_idx].index + df_split.loc[train_idx, 'Split'] = 'train' + df_split.loc[val_idx, 'Split'] = 'val' + df_split.loc[test_idx, 'Split'] = 'test' + splits.append(df_split) +df_splits = pd.concat(splits) + +df_splits.to_csv(path_root_metadata / 'split.csv', index=False) diff --git a/application/jobs/ODELIA_ternary_classification/app/scripts/preprocessing/odelia/step1_dicom2nifti.py b/application/jobs/ODELIA_ternary_classification/app/scripts/preprocessing/odelia/step1_dicom2nifti.py new file mode 100644 index 00000000..8e5831bd --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/scripts/preprocessing/odelia/step1_dicom2nifti.py @@ -0,0 +1,3 @@ +# ------------------ +# Add your code here +# ------------------- diff --git a/application/jobs/ODELIA_ternary_classification/app/scripts/preprocessing/odelia/step4_create_split.py b/application/jobs/ODELIA_ternary_classification/app/scripts/preprocessing/odelia/step4_create_split.py new file mode 100644 index 00000000..122832a9 --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/scripts/preprocessing/odelia/step4_create_split.py @@ -0,0 +1,80 @@ +from pathlib import Path +import pandas as pd +from multiprocessing import Pool +from tqdm import tqdm + +from sklearn.model_selection import StratifiedGroupKFold + + +def create_split(df, uid_col='UID', label_col='Label', group_col='PatientID'): + df = df.reset_index(drop=True) + splits = [] + sgkf = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=0) # StratifiedGroupKFold + sgkf2 = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=0) + for fold_i, (train_val_idx, test_idx) in enumerate(sgkf.split(df[uid_col], df[label_col], groups=df[group_col])): + df_split = df.copy() + df_split['Fold'] = fold_i + df_trainval = df_split.iloc[train_val_idx] + train_idx, val_idx = \ + list(sgkf2.split(df_trainval[uid_col], df_trainval[label_col], groups=df_trainval[group_col]))[0] + train_idx, val_idx = df_trainval.iloc[train_idx].index, df_trainval.iloc[val_idx].index + df_split.loc[train_idx, 'Split'] = 'train' + df_split.loc[val_idx, 'Split'] = 'val' + df_split.loc[test_idx, 'Split'] = 'test' + splits.append(df_split) + df_splits = pd.concat(splits) + return df_splits + + +if __name__ == "__main__": + for dataset in ['UKA']: # 'CAM', 'MHA', 'RSH', 'RUMC', 'UKA', 'UMCU' + print(f"----------------- {dataset} ---------------") + + path_root = Path('/home/homesOnMaster/gfranzes/Documents/datasets/ODELIA/') / dataset + path_root_metadata = path_root / 'metadata' + + df = pd.read_excel(path_root_metadata / 'ODELIA annotation scheme-2.0.xlsx', dtype={'Patient ID': str}) + df = df[11:].reset_index(drop=True) # Remove rows with annotation hints + df = df.rename(columns={'Patient ID': 'PatientID', 'Type of Lesion': 'Lesion'}) + assert not df[['PatientID', 'StudyInstanceUID', 'Lesion']].isna().any().any(), "Missing values detected" + + # Define class mapping + class_mapping = { + 'No lesion': 0, + 'Benign lesion': 1, + 'DCIS': 2, + 'Proliferative with atypia': 2, + 'Invasive Cancer (no special type)': 2, # TODO should invasive cancer be separate class? + 'Invasive Cancer (lobular carcinoma)': 2, + 'Invasive Cancer (all other)': 2, + 'not provided': pd.NA + } + + df_left = df[df['Side'] == "left"] + df_left = df_left[['PatientID', 'StudyInstanceUID', 'Side', 'Lesion']] + df_left.insert(0, 'UID', df_left['StudyInstanceUID'].astype(str) + '_' + df_left['Side']) + + df_left['Class'] = df_left['Lesion'].map(class_mapping) + df_left = df_left.dropna(subset='Class').reset_index(drop=True) # TODO: Should the entire study be removed? + df_left = df_left.loc[df_left.groupby('StudyInstanceUID')['Class'].idxmax()] + + df_right = df[df['Side'] == "right"] + df_right = df_right[['PatientID', 'StudyInstanceUID', 'Side', 'Lesion']] + df_right.insert(0, 'UID', df_right['StudyInstanceUID'].astype(str) + '_' + df_right['Side']) + + df_right['Class'] = df_right['Lesion'].map(class_mapping) + df_right = df_right.dropna(subset='Class').reset_index(drop=True) # TODO: Should the entire study be removed? + df_right = df_right.loc[df_right.groupby('StudyInstanceUID')['Class'].idxmax()] + + # ------------------- Merge left and right ---------------------- + df = pd.concat([df_left, df_right]).reset_index(drop=True) + df['Class'] = df['Class'].astype(int) + + print("Patients", df['PatientID'].nunique()) + print("Studies", df['StudyInstanceUID'].nunique()) + print("Breasts", df['UID'].nunique()) + for class_name, count in df['Class'].value_counts().sort_index().items(): + print(f"Lesion Type {class_name}: {count}") + + df_splits = create_split(df, uid_col='UID', label_col='Class', group_col='PatientID') + df_splits.to_csv(path_root_metadata / 'split.csv', index=False) diff --git a/application/jobs/ODELIA_ternary_classification/app/scripts/preprocessing/step2_compute_sub.py b/application/jobs/ODELIA_ternary_classification/app/scripts/preprocessing/step2_compute_sub.py new file mode 100644 index 00000000..ac490b41 --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/scripts/preprocessing/step2_compute_sub.py @@ -0,0 +1,37 @@ +from pathlib import Path +import SimpleITK as sitk +import numpy as np +from multiprocessing import Pool +from tqdm import tqdm + + +def process(path_patient): + # Compute subtraction image + # Note: if dtype not specified, data is read as uint16 -> subtraction wrong + dyn0_nii = sitk.ReadImage(str(path_patient / 'Pre.nii.gz'), sitk.sitkInt16) + dyn1_nii = sitk.ReadImage(str(path_patient / 'Post_1.nii.gz'), sitk.sitkInt16) + dyn0 = sitk.GetArrayFromImage(dyn0_nii) + dyn1 = sitk.GetArrayFromImage(dyn1_nii) + sub = dyn1 - dyn0 + sub = sub - sub.min() # Note: negative values causes overflow when using uint + sub = sub.astype(np.uint16) + sub_nii = sitk.GetImageFromArray(sub) + sub_nii.CopyInformation(dyn0_nii) + sitk.WriteImage(sub_nii, str(path_patient / 'Sub.nii.gz')) + + +if __name__ == "__main__": + path_root = Path('/home/gustav/Documents/datasets/ODELIA/') + for dataset in ['DUKE', ]: # 'CAM', 'MHA', 'RSH', 'RUMC', 'UKA', 'UMCU', 'DUKE' + path_data = path_root / dataset / 'data' + + files = path_data.iterdir() + + # Option 1: Multi-CPU + with Pool() as pool: + for _ in tqdm(pool.imap_unordered(process, files)): + pass + + # Option 2: Single-CPU + # for path_dir in tqdm(files): + # process(path_dir) diff --git a/application/jobs/ODELIA_ternary_classification/app/scripts/preprocessing/step3_unilateral.py b/application/jobs/ODELIA_ternary_classification/app/scripts/preprocessing/step3_unilateral.py new file mode 100644 index 00000000..926c5347 --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/scripts/preprocessing/step3_unilateral.py @@ -0,0 +1,89 @@ +from pathlib import Path +import torchio as tio +import torch +import numpy as np +from multiprocessing import Pool +from tqdm import tqdm + + +def crop_breast_height(image, margin_top=10): + "Crop height to 256 and try to cover breast based on intensity localization" + # threshold = int(image.data.float().quantile(0.9)) + threshold = int(np.quantile(image.data.float(), 0.9)) + foreground = image.data > threshold + fg_rows = foreground[0].sum(axis=(0, 2)) + top = min(max(512 - int(torch.argwhere(fg_rows).max()) - margin_top, 0), 256) + bottom = 256 - top + return tio.Crop((0, 0, bottom, top, 0, 0)) + + +def preprocess(path_dir): + # -------- Settings -------------- + ref_img = tio.ScalarImage(path_dir / 'Pre.nii.gz') + ref_img = tio.ToCanonical()(ref_img) + + # Spacing + target_spacing = (0.7, 0.7, 3) + ref_img = tio.Resample(target_spacing)(ref_img) + + # Crop + target_shape = (512, 512, 32) + + padding_constant = ref_img.data.min().item() # Ugly workaround: padding_mode='minimum' calculates the minimum per axis, not globally + transform = tio.Compose([ + tio.Resample(ref_img), # Resample to reference image to ensure that origin, direction, etc, fit + tio.CropOrPad(target_shape, padding_mode=padding_constant), + ]) + crop_height = crop_breast_height(transform(ref_img)) + split_side = { + 'right': tio.Crop((256, 0, 0, 0, 0, 0)), + 'left': tio.Crop((0, 256, 0, 0, 0, 0)), + } + + for n, path_img in enumerate(path_dir.glob('*.nii.gz')): + # Read image + img = tio.ScalarImage(path_img) + + # Preprocess (eg. Crop/Pad) + padding_constant = img.data.min().item() + transform = tio.Compose([ + tio.Resample(ref_img), + tio.CropOrPad(target_shape, padding_mode=padding_constant), + ]) + img = transform(img) + + # Crop bottom and top so that height is 256 and breast is preserved + img = crop_height(img) + + # Split left and right side + for side in ['left', 'right']: + # Create output directory + path_out_dir = path_root_out_data / f"{path_dir.relative_to(path_root_in_data)}_{side}" + path_out_dir.mkdir(exist_ok=True, parents=True) + + # Crop left/right side + img_side = split_side[side](img) + + # Save + img_side.save(path_out_dir / path_img.name) + + +if __name__ == "__main__": + for dataset in ['DUKE', ]: # 'CAM', 'MHA', 'RSH', 'RUMC', 'UKA', 'UMCU', 'DUKE' + + path_root = Path('/home/gustav/Documents/datasets/ODELIA/') / dataset + path_root_in_data = path_root / 'data' + + path_root_out_data = path_root / 'data_unilateral' + path_root_out_data.mkdir(parents=True, exist_ok=True) + + path_patients = list(path_root_in_data.iterdir()) # Convert the iterator to a list + + # Option 1: Multi-CPU + with Pool() as pool: + for _ in tqdm(pool.imap_unordered(preprocess, path_patients)): + pass + + # Option 2: Single-CPU + # for path_dir in tqdm(path_patients): + # preprocess(path_dir) diff --git a/application/jobs/ODELIA_ternary_classification/app/scripts/preprocessing/uka/step4_create_split.py b/application/jobs/ODELIA_ternary_classification/app/scripts/preprocessing/uka/step4_create_split.py new file mode 100644 index 00000000..b94b6cb4 --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/scripts/preprocessing/uka/step4_create_split.py @@ -0,0 +1,44 @@ +from pathlib import Path +import numpy as np +import pandas as pd + +from sklearn.model_selection import StratifiedGroupKFold, StratifiedKFold + +path_root = Path('/home/gustav/Documents/datasets/ODELIA/') +path_root_dataset = path_root / 'UKA_all' +path_root_metadata = path_root_dataset / 'metadata' + +df = pd.read_excel(path_root_metadata / 'annotation_regex.xlsx') +assert len(df[df.duplicated(subset='UID', keep=False)]) == 0, "Duplicates exist" + +df['DCISoderKarzinom'] = df[df.columns[-1]] | df[df.columns[-2]] +print(f"Text available for {len(df)} cases") + +# Include only examinations were MR image is available +uids = [path.name for path in (path_root_dataset / 'data_unilateral').iterdir()] +print(f"Image available for {len(uids)} cases") + +# Merge +df = df[df['UID'].isin(uids)].reset_index(drop=True) +print(f"Text and Image available for {len(df)} cases") + +# label = df.columns[6] +for label in df.columns[6:]: + print(label) + df = df.reset_index(drop=True) + splits = [] + sgkf = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=0) # StratifiedGroupKFold + sgkf2 = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=0) + for fold_i, (train_val_idx, test_idx) in enumerate(sgkf.split(df['UID'], df[label], groups=df['PNR'])): + df_split = df.copy() + df_split['Fold'] = fold_i + df_trainval = df_split.loc[train_val_idx] + train_idx, val_idx = list(sgkf2.split(df_trainval['UID'], df_trainval[label], groups=df_trainval['PNR']))[0] + train_idx, val_idx = df_trainval.iloc[train_idx].index, df_trainval.iloc[val_idx].index + df_split.loc[train_idx, 'Split'] = 'train' + df_split.loc[val_idx, 'Split'] = 'val' + df_split.loc[test_idx, 'Split'] = 'test' + splits.append(df_split) + df_splits = pd.concat(splits) + + df_splits.to_csv(path_root_metadata / f'split_regex_{label}.csv', index=False) diff --git a/application/jobs/ODELIA_ternary_classification/app/tests/data/test_dataset_odelia.py b/application/jobs/ODELIA_ternary_classification/app/tests/data/test_dataset_odelia.py new file mode 100644 index 00000000..9c906dc9 --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/tests/data/test_dataset_odelia.py @@ -0,0 +1,43 @@ +from odelia.data.datasets import ODELIA_Dataset3D +import torch +from pathlib import Path +from torchvision.utils import save_image + + +def tensor2image(tensor, batch=0): + return (tensor if tensor.ndim < 5 else torch.swapaxes(tensor[batch], 0, 1).reshape(-1, *tensor.shape[-2:])[:, None]) + + +all_institutions = ODELIA_Dataset3D.ALL_INSTITUTIONS +for institution in all_institutions: + ds = ODELIA_Dataset3D( + institutions=institution, + random_flip=True, + random_rotate=True, + # random_inverse=True, + # noise=True + binary=False, + config='unilateral', + ) + + print(f" ------------- Dataset {institution} ------------") + df = ds.df + print("Number of exams: ", len(df)) + print("Number of patients: ", df['PatientID'].nunique()) + + for label in ds.labels: + print(f"Label {label}") + print(df[label].value_counts()) + + # ----------------- Print some examples ---------------- + item = ds[20] + uid = item["uid"] + img = item['source'] + label = item['target'] + + print("UID", uid, "Image Shape", list(img.shape), "Label", label) + + path_out = Path.cwd() / 'results/test' + path_out.mkdir(parents=True, exist_ok=True) + img = tensor2image(img[None]) + save_image(img, path_out / f'test_{institution}.png', normalize=True) diff --git a/application/jobs/ODELIA_ternary_classification/app/tests/model/test_model_step.py b/application/jobs/ODELIA_ternary_classification/app/tests/model/test_model_step.py new file mode 100644 index 00000000..b0d7aa7a --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/tests/model/test_model_step.py @@ -0,0 +1,50 @@ +import torch +from tqdm import tqdm + +from odelia.models import MST, MSTRegression +from odelia.models import ResNet, ResNetRegression +from odelia.data.datasets import ODELIA_Dataset3D +from odelia.data.datamodules import DataModule + +config = "unilateral" # original or unilateral +task = "ordinal" # binary or ordinal +model = "MST" # ResNet or MST +label = None + +binary = task == "binary" +ds_train = ODELIA_Dataset3D(split='train', institutions='ODELIA', binary=binary, config=config, labels=label) + +device = torch.device(f'cuda:5') + +loss_kwargs = {} +out_ch = len(ds_train.labels) +if task == "ordinal": + out_ch = sum(ds_train.class_labels_num) + loss_kwargs = {'class_labels_num': ds_train.class_labels_num} + +if label is not None: + class_counts = ds_train.df[label].value_counts() + class_weights = 1 / class_counts / len(class_counts) + weights = ds_train.df[label].map(lambda x: class_weights[x]).values + +model_map = { + 'ResNet': ResNet if binary else ResNetRegression, + 'MST': MST if binary else MSTRegression +} +MODEL = model_map.get(model, None) +model = MODEL( + in_ch=1, + out_ch=out_ch, + loss_kwargs=loss_kwargs +) + +model.to(device) +model.eval() + +dm = DataModule(ds_train=ds_train, batch_size=3, num_workers=0) +dl = dm.train_dataloader() + +for idx, batch in tqdm(enumerate(iter(dl))): + batch = {k: v.to(device) if isinstance(v, torch.Tensor) else v for k, v in batch.items()} + loss = model._step(batch, batch_idx=idx, state="train", step=idx * dm.batch_size) + print("loss", loss) diff --git a/application/jobs/ODELIA_ternary_classification/app/tests/model/test_mst.py b/application/jobs/ODELIA_ternary_classification/app/tests/model/test_mst.py new file mode 100644 index 00000000..f83b86da --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/tests/model/test_mst.py @@ -0,0 +1,10 @@ +import torch +from odelia.models import MST, MSTRegression + +input = torch.randn((1, 1, 32, 224, 224)) +model = MST(in_ch=1, out_ch=2, spatial_dims=3) +model = MSTRegression(in_ch=1, out_ch=2 + 3, spatial_dims=3, loss_kwargs={"class_labels_num": [2, 3]}) + +pred = model(input) +print(pred.shape) +print(pred) diff --git a/application/jobs/ODELIA_ternary_classification/app/tests/model/test_resnet.py b/application/jobs/ODELIA_ternary_classification/app/tests/model/test_resnet.py new file mode 100644 index 00000000..0ad9846e --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/app/tests/model/test_resnet.py @@ -0,0 +1,10 @@ +import torch +from odelia.models import ResNet, ResNetRegression + +input = torch.randn((1, 1, 32, 224, 224)) +model = ResNet(in_ch=1, out_ch=2, spatial_dims=3, model=18) +model = ResNetRegression(in_ch=1, out_ch=2 + 3, spatial_dims=3, loss_kwargs={"class_labels_num": [2, 3]}) + +pred = model(input) +print(pred.shape) +print(pred) diff --git a/application/jobs/ODELIA_ternary_classification/meta.conf b/application/jobs/ODELIA_ternary_classification/meta.conf new file mode 100644 index 00000000..15fc1b67 --- /dev/null +++ b/application/jobs/ODELIA_ternary_classification/meta.conf @@ -0,0 +1,9 @@ +name = "ODELIA_ternary_classification" +resource_spec {} +deploy_map { + app = [ + "@ALL" + ] +} +min_clients = 2 +mandatory_clients = [] diff --git a/application/jobs/minimal_training_pytorch_cnn/app/config/config_fed_client.conf b/application/jobs/minimal_training_pytorch_cnn/app/config/config_fed_client.conf index ff18ef95..92c80ae5 100644 --- a/application/jobs/minimal_training_pytorch_cnn/app/config/config_fed_client.conf +++ b/application/jobs/minimal_training_pytorch_cnn/app/config/config_fed_client.conf @@ -27,7 +27,7 @@ tasks = ["swarm_*"] executor { # client-side controller for training and logic and aggregation management - path = "controller.SwarmClientController" + path = "nvflare.app_common.ccwf.SwarmClientController" args { # train task must be implemented by Executor learn_task_name = "train" diff --git a/application/jobs/minimal_training_pytorch_cnn/app/config/config_fed_server.conf b/application/jobs/minimal_training_pytorch_cnn/app/config/config_fed_server.conf index 0ece834f..309d239f 100644 --- a/application/jobs/minimal_training_pytorch_cnn/app/config/config_fed_server.conf +++ b/application/jobs/minimal_training_pytorch_cnn/app/config/config_fed_server.conf @@ -13,13 +13,13 @@ workflows = [ { # server-side controller to manage job life cycle id = "swarm_controller" - path = "controller.SwarmServerController" + path = "nvflare.app_common.ccwf.SwarmServerController" args { # can also set aggregation clients and train clients, see class for all available args num_rounds = 5 start_task_timeout = 36000 progress_timeout = 36000 - end_workflow_timeout = 36000 + #end_workflow_timeout = 36000 configure_task_timeout = 36000 max_status_report_interval = 36000 } diff --git a/application/jobs/minimal_training_pytorch_cnn/app/custom/main.py b/application/jobs/minimal_training_pytorch_cnn/app/custom/main.py index a8ac2752..340f3b97 100755 --- a/application/jobs/minimal_training_pytorch_cnn/app/custom/main.py +++ b/application/jobs/minimal_training_pytorch_cnn/app/custom/main.py @@ -2,14 +2,14 @@ import os -import nvflare.client.lightning as flare import nvflare.client as flare_util +import nvflare.client.lightning as flare import torch import minimal_training TRAINING_MODE = os.getenv("TRAINING_MODE") - +TRAINING_MODE = "swarm" if TRAINING_MODE == "swarm": flare_util.init() SITE_NAME=flare.get_site_name() @@ -34,7 +34,17 @@ def main(): logger.info(f"Site name: {SITE_NAME}") while flare.is_running(): + logger.info('Waiting for input model from server...') input_model = flare.receive() + + if input_model is not None: + logger.info("==== Swarm model received ====") + logger.info( + f"input_model.params.keys() = {list(input_model.params.keys())[:10]} ... total = {len(input_model.params)}") + logger.info( + f"model.state_dict().keys() = {list(model.state_dict().keys())[:10]} ... total = {len(model.state_dict())}") + + logger.info(f"Current round: {input_model.current_round}") minimal_training.validate_and_train(logger, data_module, model, trainer) diff --git a/application/jobs/minimal_training_pytorch_cnn/app/custom/models/base_model.py b/application/jobs/minimal_training_pytorch_cnn/app/custom/models/base_model.py index bf1d3519..3d933bb4 100644 --- a/application/jobs/minimal_training_pytorch_cnn/app/custom/models/base_model.py +++ b/application/jobs/minimal_training_pytorch_cnn/app/custom/models/base_model.py @@ -1,25 +1,16 @@ -from typing import List, Union +from typing import List, Union, Any from pathlib import Path import json import torch import torch.nn as nn import pytorch_lightning as pl -from pytorch_lightning.utilities.cloud_io import load as pl_load -from pytorch_lightning.utilities.migration import pl_legacy_patch -from pytorch_lightning.utilities.types import EPOCH_OUTPUT from torchmetrics import AUROC, Accuracy class VeryBasicModel(pl.LightningModule): """ A very basic model class extending LightningModule with basic functionality. - - Attributes: - _step_train (int): Counter for training steps. - _step_val (int): Counter for validation steps. - _step_test (int): Counter for test steps. """ - def __init__(self): super().__init__() self.save_hyperparameters() @@ -28,112 +19,58 @@ def __init__(self): self._step_test = -1 def forward(self, x_in): - """Forward pass. Must be implemented by subclasses.""" raise NotImplementedError def _step(self, batch: dict, batch_idx: int, state: str, step: int, optimizer_idx: int): - """Step function for training, validation, and testing. Must be implemented by subclasses.""" raise NotImplementedError - def _epoch_end(self, outputs: Union[EPOCH_OUTPUT, List[EPOCH_OUTPUT]], state: str): - """Epoch end function.""" + def _epoch_end(self, outputs: List[Any], state: str): return - def training_step(self, batch: dict, batch_idx: int, optimizer_idx: int = 0): + def training_step(self, batch, batch_idx): self._step_train += 1 - return self._step(batch, batch_idx, "train", self._step_train, optimizer_idx) + return self._step(batch, batch_idx, "train", self._step_train, 0) - def validation_step(self, batch: dict, batch_idx: int, optimizer_idx: int = 0): + def validation_step(self, batch: dict, batch_idx: int) -> Any: self._step_val += 1 - return self._step(batch, batch_idx, "val", self._step_val, optimizer_idx) + return self._step(batch, batch_idx, "val", self._step_val, 0) - def test_step(self, batch: dict, batch_idx: int, optimizer_idx: int = 0): + def test_step(self, batch: dict, batch_idx: int) -> Any: self._step_test += 1 - return self._step(batch, batch_idx, "test", self._step_test, optimizer_idx) + return self._step(batch, batch_idx, "test", self._step_test, 0) - def training_epoch_end(self, outputs: Union[EPOCH_OUTPUT, List[EPOCH_OUTPUT]]) -> None: - self._epoch_end(outputs, "train") - return super().training_epoch_end(outputs) + def on_train_epoch_end(self): + self._epoch_end([], "train") - def validation_epoch_end(self, outputs: Union[EPOCH_OUTPUT, List[EPOCH_OUTPUT]]) -> None: - self._epoch_end(outputs, "val") - return super().validation_epoch_end(outputs) + def on_validation_epoch_end(self): + self._epoch_end([], "val") - def test_epoch_end(self, outputs: Union[EPOCH_OUTPUT, List[EPOCH_OUTPUT]]) -> None: - self._epoch_end(outputs, "test") - return super().test_epoch_end(outputs) + def on_test_epoch_end(self): + self._epoch_end([], "test") @classmethod def save_best_checkpoint(cls, path_checkpoint_dir, best_model_path): - """Saves the best model checkpoint path. - - Args: - path_checkpoint_dir (str): Directory to save the checkpoint. - best_model_path (str): Path to the best model. - """ with open(Path(path_checkpoint_dir) / 'best_checkpoint.json', 'w') as f: json.dump({'best_model_epoch': Path(best_model_path).name}, f) @classmethod - def _get_best_checkpoint_path(cls, path_checkpoint_dir, version=0, **kwargs): - """Gets the best model checkpoint path. - - Args: - path_checkpoint_dir (str): Directory containing the checkpoint. - version (int, optional): Version of the checkpoint. Defaults to 0. - - Returns: - Path: Path to the best checkpoint. - """ - path_version = 'lightning_logs/version_' + str(version) - with open(Path(path_checkpoint_dir) / path_version / 'best_checkpoint.json', 'r') as f: + def _get_best_checkpoint_path(cls, path_checkpoint_dir, **kwargs): + with open(Path(path_checkpoint_dir) / 'best_checkpoint.json', 'r') as f: path_rel_best_checkpoint = Path(json.load(f)['best_model_epoch']) return Path(path_checkpoint_dir) / path_rel_best_checkpoint @classmethod - def load_best_checkpoint(cls, path_checkpoint_dir, version=0, **kwargs): - """Loads the best model checkpoint. - - Args: - path_checkpoint_dir (str): Directory containing the checkpoint. - version (int, optional): Version of the checkpoint. Defaults to 0. - - Returns: - LightningModule: The loaded model. - """ - path_best_checkpoint = cls._get_best_checkpoint_path(path_checkpoint_dir, version) + def load_best_checkpoint(cls, path_checkpoint_dir, **kwargs): + path_best_checkpoint = cls._get_best_checkpoint_path(path_checkpoint_dir) return cls.load_from_checkpoint(path_best_checkpoint, **kwargs) def load_pretrained(self, checkpoint_path, map_location=None, **kwargs): - """Loads pretrained weights from a checkpoint. - - Args: - checkpoint_path (str): Path to the checkpoint. - map_location (str, optional): Device to map the checkpoint. Defaults to None. - - Returns: - LightningModule: The model with loaded weights. - """ if checkpoint_path.is_dir(): checkpoint_path = self._get_best_checkpoint_path(checkpoint_path, **kwargs) - - with pl_legacy_patch(): - if map_location is not None: - checkpoint = pl_load(checkpoint_path, map_location=map_location) - else: - checkpoint = pl_load(checkpoint_path, map_location=lambda storage, loc: storage) + checkpoint = torch.load(checkpoint_path, map_location=map_location) return self.load_weights(checkpoint["state_dict"], **kwargs) def load_weights(self, pretrained_weights, strict=True, **kwargs): - """Loads weights into the model. - - Args: - pretrained_weights (dict): Pretrained weights. - strict (bool, optional): Whether to strictly enforce that the keys in `state_dict` match the keys returned by this module’s `state_dict` function. Defaults to True. - - Returns: - LightningModule: The model with loaded weights. - """ filter_fn = kwargs.get('filter', lambda key: key in pretrained_weights) init_weights = self.state_dict() pretrained_weights = {key: value for key, value in pretrained_weights.items() if filter_fn(key)} @@ -143,126 +80,75 @@ def load_weights(self, pretrained_weights, strict=True, **kwargs): class BasicModel(VeryBasicModel): - """ - A basic model class with optimizer and learning rate scheduler configurations. - - Attributes: - optimizer (Optimizer): The optimizer to use. - optimizer_kwargs (dict): Keyword arguments for the optimizer. - lr_scheduler (Scheduler): The learning rate scheduler to use. - lr_scheduler_kwargs (dict): Keyword arguments for the learning rate scheduler. - """ - def __init__( self, optimizer=torch.optim.AdamW, - optimizer_kwargs={'lr': 1e-3, 'weight_decay': 1e-2}, + optimizer_kwargs=None, lr_scheduler=None, - lr_scheduler_kwargs={}, + lr_scheduler_kwargs=None, ): super().__init__() - self.save_hyperparameters() self.optimizer = optimizer - self.optimizer_kwargs = optimizer_kwargs + self.optimizer_kwargs = optimizer_kwargs or {'lr': 1e-3, 'weight_decay': 1e-2} self.lr_scheduler = lr_scheduler - self.lr_scheduler_kwargs = lr_scheduler_kwargs + self.lr_scheduler_kwargs = lr_scheduler_kwargs or {} + self.save_hyperparameters() def configure_optimizers(self): - """Configures the optimizers and learning rate schedulers. - - Returns: - list: List containing the optimizer and optionally the learning rate scheduler. - """ optimizer = self.optimizer(self.parameters(), **self.optimizer_kwargs) if self.lr_scheduler is not None: lr_scheduler = self.lr_scheduler(optimizer, **self.lr_scheduler_kwargs) - return [optimizer], [lr_scheduler] - else: - return [optimizer] + return [optimizer], [{"scheduler": lr_scheduler, "interval": "epoch", "frequency": 1}] + return [optimizer] class BasicClassifier(BasicModel): - """ - A basic classifier model with loss function and metrics. - - Attributes: - in_ch (int): Number of input channels. - out_ch (int): Number of output channels. - spatial_dims (int): Number of spatial dimensions. - loss (Loss): The loss function. - loss_kwargs (dict): Keyword arguments for the loss function. - auc_roc (ModuleDict): Dictionary of AUROC metrics. - acc (ModuleDict): Dictionary of Accuracy metrics. - """ - def __init__( self, in_ch: int, out_ch: int, spatial_dims: int, loss=torch.nn.CrossEntropyLoss, - loss_kwargs={}, + loss_kwargs=None, optimizer=torch.optim.AdamW, - optimizer_kwargs={'lr': 1e-3, 'weight_decay': 1e-2}, + optimizer_kwargs=None, lr_scheduler=None, - lr_scheduler_kwargs={}, - aucroc_kwargs={"task": "binary"}, - acc_kwargs={"task": "binary"} + lr_scheduler_kwargs=None, + aucroc_kwargs=None, + acc_kwargs=None, ): super().__init__(optimizer, optimizer_kwargs, lr_scheduler, lr_scheduler_kwargs) + self.in_ch = in_ch self.out_ch = out_ch self.spatial_dims = spatial_dims - self.loss = loss(**loss_kwargs) - self.loss_kwargs = loss_kwargs + self.loss_kwargs = loss_kwargs or {} + self.loss = loss(**self.loss_kwargs) + + aucroc_kwargs = aucroc_kwargs or {"task": "binary"} + acc_kwargs = acc_kwargs or {"task": "binary"} self.auc_roc = nn.ModuleDict({state: AUROC(**aucroc_kwargs) for state in ["train_", "val_", "test_"]}) self.acc = nn.ModuleDict({state: Accuracy(**acc_kwargs) for state in ["train_", "val_", "test_"]}) def _step(self, batch: dict, batch_idx: int, state: str, step: int, optimizer_idx: int): - """Step function for training, validation, and testing. - - Args: - batch (dict): Input batch. - batch_idx (int): Batch index. - state (str): State of the model ('train', 'val', 'test'). - step (int): Current step. - optimizer_idx (int): Index of the optimizer. - - Returns: - Tensor: Loss value. - """ source, target = batch['source'], batch['target'] target = target[:, None].float() + target_int = target.int() batch_size = source.shape[0] - # Run Model pred = self(source) + loss_val = self.loss(pred, target) - # Compute Loss - logging_dict = {} - logging_dict['loss'] = self.loss(pred, target) - - # Compute Metrics with torch.no_grad(): - self.acc[state + "_"].update(pred, target) - self.auc_roc[state + "_"].update(pred, target) - - # Log Scalars - for metric_name, metric_val in logging_dict.items(): - self.log(f"{state}/{metric_name}", metric_val.cpu() if hasattr(metric_val, 'cpu') else metric_val, - batch_size=batch_size, on_step=True, on_epoch=True) + prob = torch.sigmoid(pred) # logits -> probability + self.acc[state + "_"].update(prob, target_int) + self.auc_roc[state + "_"].update(prob, target_int) - return logging_dict['loss'] + self.log(f"{state}/loss", loss_val, batch_size=batch_size, on_step=True, on_epoch=True) + return loss_val def _epoch_end(self, outputs, state): - """Epoch end function. - - Args: - outputs (Union[EPOCH_OUTPUT, List[EPOCH_OUTPUT]]): Outputs of the epoch. - state (str): State of the model ('train', 'val', 'test'). - """ - batch_size = len(outputs) - for name, value in [("ACC", self.acc[state + "_"]), ("AUC_ROC", self.auc_roc[state + "_"])]: - self.log(f"{state}/{name}", value.compute().cpu(), batch_size=batch_size, on_step=False, on_epoch=True) - value.reset() + for name, metric in [("ACC", self.acc[state + "_"]), ("AUC_ROC", self.auc_roc[state + "_"])]: + self.log(f"{state}/{name}", metric.compute(), on_epoch=True) + metric.reset() diff --git a/application/jobs/stamp/README.md b/application/jobs/stamp/README.md new file mode 100644 index 00000000..7de86729 --- /dev/null +++ b/application/jobs/stamp/README.md @@ -0,0 +1,97 @@ +# STAMP: A Protocol for Solid Tumor Associative Modeling in Pathology + + + +![CI](https://github.com/KatherLab/STAMP/actions/workflows/ci.yml/badge.svg) + +This repository contains the accompanying code for the steps described in the [Nature Protocols paper][stamp paper]: +"From Whole Slide Image to Biomarker Prediction: +A Protocol for End-to-End Deep Learning in Computational Pathology". + +> [!NOTE] +> This repo contains an updated version of the codebase. +> For a version compatible with the instructions in the paper, +> please check out [version 1 of STAMP][stamp v1]. + +[stamp paper]: https://www.nature.com/articles/s41596-024-01047-2 "From whole-slide image to biomarker prediction: end-to-end weakly supervised deep learning in computational pathology" + +[stamp v1]: https://github.com/KatherLab/STAMP/tree/v1 + +## Installing stamp + +We recommend installing STAMP with [uv](https://docs.astral.sh/uv/): + +```bash +git clone https://github.com/KatherLab/STAMP.git + +cd STAMP/ + +uv sync --all-extras + +source .venv/bin/activate +``` + +> [!IMPORTANT] +> STAMP additionally requires OpenSlide to be installed, as well as OpenCV dependencies. +> +> For Ubuntu < 23.10: +> ```bash +> apt update && apt install -y openslide-tools libgl1-mesa-glx # libgl1-mesa-glx is needed for OpenCV +> ``` +> +> For Ubuntu >= 23.10: +> ```bash +> apt update && apt install -y openslide-tools libgl1 libglx-mesa0 libglib2.0-0 # libgl1, libglx-mesa0, libglib2.0-0 are needed for OpenCV +> ``` + +If the installation was successful, running `stamp` in your terminal should yield the following output: + +``` +$ stamp +usage: stamp [-h] [--config CONFIG_FILE_PATH] {init,preprocess,encode_slides,encode_patients,train,crossval,deploy,statistics,config,heatmaps} ... + +STAMP: Solid Tumor Associative Modeling in Pathology + +positional arguments: + {init,preprocess,encode_slides,encode_patients,train,crossval,deploy,statistics,config,heatmaps} + init Create a new STAMP configuration file at the path specified by --config + preprocess Preprocess whole-slide images into feature vectors + encode_slides Encode patch-level features into slide-level embeddings + encode_patients Encode features into patient-level embeddings + train Train a Vision Transformer model + crossval Train a Vision Transformer model with cross validation for modeling.n_splits folds + deploy Deploy a trained Vision Transformer model + statistics Generate AUROCs and AUPRCs with 95%CI for a trained Vision Transformer model + config Print the loaded configuration + heatmaps Generate heatmaps for a trained model + +options: + -h, --help show this help message and exit + --config CONFIG_FILE_PATH, -c CONFIG_FILE_PATH + Path to config file. Default: config.yaml +``` + +## Running stamp + +For a quick introduction how to run stamp, +check out our [getting started guide](getting-started.md). + +## Reference + +If you find our work useful in your research +or if you use parts of this code +please consider citing our [Nature Protocols publication](https://www.nature.com/articles/s41596-024-01047-2): + +``` +@Article{ElNahhas2024, + author={El Nahhas, Omar S. M. and van Treeck, Marko and W{\"o}lflein, Georg and Unger, Michaela and Ligero, Marta and Lenz, Tim and Wagner, Sophia J. and Hewitt, Katherine J. and Khader, Firas and Foersch, Sebastian and Truhn, Daniel and Kather, Jakob Nikolas}, + title={From whole-slide image to biomarker prediction: end-to-end weakly supervised deep learning in computational pathology}, + journal={Nature Protocols}, + year={2024}, + month={Sep}, + day={16}, + issn={1750-2799}, + doi={10.1038/s41596-024-01047-2}, + url={https://doi.org/10.1038/s41596-024-01047-2} +} +``` diff --git a/application/jobs/stamp/app/config/config_fed_client.conf b/application/jobs/stamp/app/config/config_fed_client.conf new file mode 100644 index 00000000..826af222 --- /dev/null +++ b/application/jobs/stamp/app/config/config_fed_client.conf @@ -0,0 +1,147 @@ +{ + format_version = 2 + app_script = "main.py" + app_config = "" + executors = [ + { + tasks = [ + "train" + ] + executor { + path = "nvflare.app_opt.pt.client_api_launcher_executor.PTClientAPILauncherExecutor" + args { + launcher_id = "launcher" + pipe_id = "pipe" + last_result_transfer_timeout = 360000 + external_pre_init_timeout = 360000 + peer_read_timeout = 360000 + heartbeat_timeout = 360000 + params_exchange_format = "pytorch" + params_transfer_type = "DIFF" + train_with_evaluation = true + } + } + } + { + # All tasks prefixed with swarm_ are routed to SwarmClientController + tasks = ["swarm_*"] + executor { + # client-side controller for training and logic and aggregation management + path = "nvflare.app_common.ccwf.SwarmClientController" + args { + # train task must be implemented by Executor + learn_task_name = "train" + # how long to wait for current learn task before timing out the gathering + #learn_task_timeout = 360000 + #learn_task_abort_timeout = 360000 + #learn_task_ack_timeout = 360000 + #final_result_ack_timeout = 360000 + + # ids must map to corresponding components + persistor_id = "persistor" + aggregator_id = "aggregator" + shareable_generator_id = "shareable_generator" + min_responses_required = 2 + #wait_time_after_min_resps_received = 360000 + } + } + } + ] + task_data_filters = [] + task_result_filters = [] + components = [ + { + id = "launcher" + path = "nvflare.app_common.launchers.subprocess_launcher.SubprocessLauncher" + args { + script = "python3 custom/{app_script} {app_config} " + launch_once = true + } + } + { + id = "aggregator" + path = "nvflare.app_common.aggregators.intime_accumulate_model_aggregator.InTimeAccumulateWeightedAggregator" + args { + expected_data_kind = "WEIGHT_DIFF" + } + } + { + id = "pipe" + path = "nvflare.fuel.utils.pipe.cell_pipe.CellPipe" + args { + mode = "PASSIVE" + site_name = "{SITE_NAME}" + token = "{JOB_ID}" + root_url = "{ROOT_URL}" + secure_mode = "{SECURE_MODE}" + workspace_dir = "{WORKSPACE}" + } + } + { + id = "persistor" + path = "nvflare.app_opt.pt.file_model_persistor.PTFileModelPersistor" + args { + model { + path = "modeling.lightning_model.LitVisionTransformer" + args { + categories = ['WT', 'MUT'] + dim_input = 1536 + category_weights =[0.8676, 0.1324] + dim_model = 512 + dim_feedforward=512 + n_heads=8 + n_layers=2 + dropout=0.25 +use_alibi=False + ground_truth_label='isMSIH' + train_patients=['TCGA-AH-6644', 'TCGA-AA-3664', 'TCGA-AZ-4614', 'TCGA-D5-5537', 'TCGA-G4-6628', 'TCGA-EI-6882', 'TCGA-AF-2689', 'TCGA-F4-6805', 'TCGA-AZ-4681', 'TCGA-AG-3882', 'TCGA-A6-6648', 'TCGA-AA-A017', 'TCGA-G4-6303', 'TCGA-EI-6508', 'TCGA-AF-6136', 'TCGA-A6-6654', 'TCGA-AD-6895', 'TCGA-CM-5860', 'TCGA-A6-4105', 'TCGA-F5-6810', 'TCGA-QG-A5YV', 'TCGA-DC-6155', 'TCGA-F5-6812', 'TCGA-QL-A97D', 'TCGA-AZ-4615', 'TCGA-CK-6751', 'TCGA-AH-6547', 'TCGA-AA-3980', 'TCGA-CL-5918', 'TCGA-DM-A282', 'TCGA-AA-3514', 'TCGA-EI-6509', 'TCGA-AF-3911', 'TCGA-CL-5917', 'TCGA-A6-2686', 'TCGA-AA-3950', 'TCGA-AA-3549', 'TCGA-AA-3973', 'TCGA-D5-6922', 'TCGA-AG-3887', 'TCGA-CA-6717', 'TCGA-CM-6171', 'TCGA-AD-6964', 'TCGA-A6-6142', 'TCGA-AD-6890', 'TCGA-A6-6138', 'TCGA-AD-A5EK', 'TCGA-D5-6929', 'TCGA-G4-6588', 'TCGA-AZ-4616', 'TCGA-AA-3561', 'TCGA-QG-A5Z2', 'TCGA-DM-A1HA', 'TCGA-CL-4957', 'TCGA-CA-5255', 'TCGA-DC-6158', 'TCGA-G4-6295', 'TCGA-AY-A69D', 'TCGA-DM-A1D6', 'TCGA-AG-3896', 'TCGA-A6-6652', 'TCGA-CM-6680', 'TCGA-AA-3821', 'TCGA-D5-5540', 'TCGA-DC-5869', 'TCGA-AG-4008', 'TCGA-CK-4952', 'TCGA-AA-3875', 'TCGA-AA-A02W', 'TCGA-EI-6514', 'TCGA-DM-A288', 'TCGA-AG-3890', 'TCGA-AA-3979', 'TCGA-NH-A5IV', 'TCGA-G4-6297', 'TCGA-D5-6927', 'TCGA-AA-3680', 'TCGA-NH-A8F8', 'TCGA-A6-5662', 'TCGA-A6-2674', 'TCGA-AG-A01N', 'TCGA-DM-A1D8', 'TCGA-AA-3837', 'TCGA-AA-A01I', 'TCGA-DM-A0XF', 'TCGA-F4-6855', 'TCGA-CA-6715', 'TCGA-AA-3975', 'TCGA-AA-3989', 'TCGA-AG-A016', 'TCGA-A6-5666', 'TCGA-CK-6746', 'TCGA-CM-4752', 'TCGA-CM-6676', 'TCGA-D5-6541', 'TCGA-A6-5667', 'TCGA-AF-2691', 'TCGA-AD-A5EJ', 'TCGA-F5-6864', 'TCGA-AG-3726', 'TCGA-A6-6649', 'TCGA-AA-A03J', 'TCGA-AF-2687', 'TCGA-AA-3858', 'TCGA-G4-6321', 'TCGA-G5-6641', 'TCGA-G4-6309', 'TCGA-D5-5538', 'TCGA-DM-A28K', 'TCGA-AG-3878', 'TCGA-AZ-6598', 'TCGA-AA-3688', 'TCGA-AG-A01W', 'TCGA-AA-3517', 'TCGA-D5-6898', 'TCGA-CM-5341', 'TCGA-CM-6167', 'TCGA-D5-6531', 'TCGA-DC-6683', 'TCGA-AF-2690', 'TCGA-CM-6161', 'TCGA-A6-5660', 'TCGA-NH-A8F7', 'TCGA-CM-6163', 'TCGA-AA-3968', 'TCGA-CM-5868', 'TCGA-AA-3976', 'TCGA-A6-A565', 'TCGA-AM-5821', 'TCGA-AD-6965', 'TCGA-AY-A71X', 'TCGA-AZ-4682', 'TCGA-AG-3892', 'TCGA-A6-2675', 'TCGA-F4-6569', 'TCGA-AG-3893', 'TCGA-CM-5862', 'TCGA-AZ-6606', 'TCGA-CM-6678', 'TCGA-D5-6931', 'TCGA-D5-6923', 'TCGA-AG-3575', 'TCGA-AA-A00Z', 'TCGA-D5-6540', 'TCGA-A6-3808', 'TCGA-CM-4743', 'TCGA-DC-4749', 'TCGA-T9-A92H', 'TCGA-A6-3810', 'TCGA-AD-6889', 'TCGA-G4-6320', 'TCGA-AA-3971', 'TCGA-AZ-4308', 'TCGA-D5-7000', 'TCGA-DM-A28A', 'TCGA-AG-A01L', 'TCGA-EI-6512', 'TCGA-A6-5661', 'TCGA-AG-3599', 'TCGA-AG-3902', 'TCGA-AD-6548', 'TCGA-AA-A022', 'TCGA-AG-3580', 'TCGA-A6-A567', 'TCGA-AA-3679', 'TCGA-CA-5256', 'TCGA-F4-6808', 'TCGA-D5-6536', 'TCGA-DM-A1D9', 'TCGA-AG-3885', 'TCGA-DM-A1D0', 'TCGA-CM-6674', 'TCGA-EI-6507', 'TCGA-F4-6459', 'TCGA-A6-6137', 'TCGA-AD-6899', 'TCGA-EI-6885', 'TCGA-WS-AB45', 'TCGA-D5-6930', 'TCGA-AU-6004', 'TCGA-AY-6196', 'TCGA-AA-A010', 'TCGA-AG-A00C', 'TCGA-4T-AA8H', 'TCGA-G4-6302', 'TCGA-AA-3966', 'TCGA-AF-2693', 'TCGA-D5-6926', 'TCGA-DM-A0XD', 'TCGA-AA-3854', 'TCGA-D5-6932', 'TCGA-EI-7004', 'TCGA-AG-3594', 'TCGA-EI-6510', 'TCGA-AA-3715', 'TCGA-AA-A01X', 'TCGA-F4-6570', 'TCGA-EI-6511', 'TCGA-CM-5861', 'TCGA-CA-5254', 'TCGA-G4-6317', 'TCGA-DM-A28M', 'TCGA-EI-6506', 'TCGA-AG-A020', 'TCGA-AG-3583', 'TCGA-G4-6294', 'TCGA-CM-6165', 'TCGA-D5-6535', 'TCGA-D5-5541', 'TCGA-DM-A1DB', 'TCGA-AG-A01J', 'TCGA-AG-4001', 'TCGA-AG-A00Y', 'TCGA-AA-3949', 'TCGA-AA-3842', 'TCGA-CA-6716', 'TCGA-CM-5348', 'TCGA-AA-3696', 'TCGA-AA-3833', 'TCGA-NH-A6GC', 'TCGA-CK-4947', 'TCGA-AA-3846', 'TCGA-A6-2677', 'TCGA-A6-2684', 'TCGA-NH-A6GA', 'TCGA-F5-6811', 'TCGA-A6-4107', 'TCGA-F5-6571', 'TCGA-CK-6747', 'TCGA-AA-3666', 'TCGA-CK-5914', 'TCGA-G4-6627', 'TCGA-DC-6681', 'TCGA-AZ-4315', 'TCGA-DT-5265', 'TCGA-CM-4751', 'TCGA-CM-6168', 'TCGA-AY-5543', 'TCGA-AZ-6607', 'TCGA-AG-3901', 'TCGA-AA-3695', 'TCGA-AD-6963', 'TCGA-CK-5913', 'TCGA-AF-2692', 'TCGA-G4-6307', 'TCGA-AA-A01T', 'TCGA-DY-A1DG', 'TCGA-AA-A01C', 'TCGA-AZ-6603', 'TCGA-AG-3727', 'TCGA-AG-3909', 'TCGA-D5-6539', 'TCGA-AZ-5407', 'TCGA-NH-A6GB', 'TCGA-AA-3851', 'TCGA-AF-5654', 'TCGA-AA-3530', 'TCGA-F5-6813', 'TCGA-AA-3845', 'TCGA-AG-3584', 'TCGA-AM-5820', 'TCGA-AA-3982', 'TCGA-D5-6924', 'TCGA-AA-3984', 'TCGA-5M-AATE', 'TCGA-AG-3894', 'TCGA-AU-3779', 'TCGA-A6-2671', 'TCGA-DC-6682', 'TCGA-A6-6653', 'TCGA-DM-A28E', 'TCGA-AZ-5403', 'TCGA-CK-4948', 'TCGA-AF-6655', 'TCGA-AA-A01P', 'TCGA-AA-A024', 'TCGA-F4-6809', 'TCGA-AA-3548', 'TCGA-QG-A5YW', 'TCGA-AA-3818', 'TCGA-EI-6513', 'TCGA-AY-4071', 'TCGA-D5-6532', 'TCGA-AZ-6600', 'TCGA-CM-6172', 'TCGA-AG-A01Y', 'TCGA-AA-3675', 'TCGA-AD-6888', 'TCGA-AH-6897', 'TCGA-AA-3852', 'TCGA-CM-4747', 'TCGA-AG-A02N', 'TCGA-AA-A02F', 'TCGA-DM-A1D7', 'TCGA-AA-3678', 'TCGA-CM-6677', 'TCGA-AA-A004', 'TCGA-D5-5539', 'TCGA-F5-6861', 'TCGA-AA-3544', 'TCGA-F4-6463', 'TCGA-AG-4015', 'TCGA-AF-3400', 'TCGA-CM-6169', 'TCGA-F4-6461', 'TCGA-AD-6901', 'TCGA-DM-A280', 'TCGA-AA-3819', 'TCGA-DM-A0X9', 'TCGA-G4-6315', 'TCGA-AA-3850', 'TCGA-CA-5797', 'TCGA-AA-3856', 'TCGA-AY-6386', 'TCGA-AG-3598', 'TCGA-CM-5863', 'TCGA-AG-3605', 'TCGA-AZ-6599', 'TCGA-CM-5344', 'TCGA-AA-3693', 'TCGA-G4-6306', 'TCGA-F4-6854', 'TCGA-A6-5656', 'TCGA-DY-A0XA', 'TCGA-A6-2685', 'TCGA-AY-6197', 'TCGA-4N-A93T', 'TCGA-NH-A50U', 'TCGA-A6-2683', 'TCGA-AA-3952', 'TCGA-DC-6160', 'TCGA-A6-3807', 'TCGA-AA-3522', 'TCGA-G4-6304', 'TCGA-AA-3526', 'TCGA-DC-4745', 'TCGA-AA-3877', 'TCGA-CM-6162', 'TCGA-AA-3692', 'TCGA-AA-3524', 'TCGA-AG-3881', 'TCGA-AA-3681', 'TCGA-CK-4951', 'TCGA-AA-3534', 'TCGA-A6-A56B', 'TCGA-DM-A1D4', 'TCGA-5M-AAT5', 'TCGA-AG-4022', 'TCGA-AA-3812', 'TCGA-D5-6529', 'TCGA-AA-3673', 'TCGA-EI-6917', 'TCGA-EI-6884', 'TCGA-AG-A011', 'TCGA-AY-A8YK', 'TCGA-EI-6881', 'TCGA-A6-2678', 'TCGA-AA-3947', 'TCGA-A6-5659', 'TCGA-AA-3994', 'TCGA-D5-6534', 'TCGA-AG-3574', 'TCGA-F5-6465', 'TCGA-CA-6719', 'TCGA-AA-3532', 'TCGA-G4-6322', 'TCGA-AF-A56K', 'TCGA-AA-3864', 'TCGA-AA-3685', 'TCGA-AA-3986'] + valid_patients=['TCGA-AG-4021', 'TCGA-AG-A02X', 'TCGA-AA-A02H', 'TCGA-F4-6807', 'TCGA-G4-6311', 'TCGA-AF-A56L', 'TCGA-CM-4748', 'TCGA-AZ-6605', 'TCGA-G4-6299', 'TCGA-AA-3667', 'TCGA-5M-AAT4', 'TCGA-5M-AAT6', 'TCGA-F4-6704', 'TCGA-AH-6544', 'TCGA-3L-AA1B', 'TCGA-CK-4950', 'TCGA-AA-3956', 'TCGA-G4-6323', 'TCGA-AA-3831', 'TCGA-AH-6903', 'TCGA-CA-5796', 'TCGA-AA-3684', 'TCGA-EI-7002', 'TCGA-F4-6460', 'TCGA-AA-3844', 'TCGA-AA-3848', 'TCGA-D5-6928', 'TCGA-A6-5665', 'TCGA-AA-A01S', 'TCGA-DM-A28H', 'TCGA-DC-6157', 'TCGA-DY-A1DD', 'TCGA-AA-3519', 'TCGA-AA-3855', 'TCGA-F4-6806', 'TCGA-AA-3520', 'TCGA-CM-6675', 'TCGA-AG-3898', 'TCGA-CK-6748', 'TCGA-G4-6298', 'TCGA-G4-6626', 'TCGA-QG-A5Z1', 'TCGA-DM-A28G', 'TCGA-A6-2681', 'TCGA-DM-A28F', 'TCGA-G4-6586', 'TCGA-CK-5916', 'TCGA-AG-A002', 'TCGA-AG-A026', 'TCGA-AA-A02E', 'TCGA-F5-6464', 'TCGA-EI-6883', 'TCGA-F4-6856', 'TCGA-AG-A015', 'TCGA-CM-5349', 'TCGA-DM-A1DA', 'TCGA-D5-6530', 'TCGA-A6-A566', 'TCGA-NH-A50V', 'TCGA-F5-6814', 'TCGA-CM-6166', 'TCGA-CI-6622', 'TCGA-AA-3977', 'TCGA-CA-6718', 'TCGA-AA-3841', 'TCGA-AA-3521', 'TCGA-NH-A50T', 'TCGA-AA-A01R', 'TCGA-AG-A008', 'TCGA-CM-4746', 'TCGA-AA-A02R', 'TCGA-DC-6154', 'TCGA-AA-3531', 'TCGA-F5-6863', 'TCGA-F4-6703', 'TCGA-AY-A54L', 'TCGA-AA-3811', 'TCGA-AA-3814', 'TCGA-CK-5912', 'TCGA-A6-3809', 'TCGA-A6-5657', 'TCGA-EF-5830', 'TCGA-CM-6164', 'TCGA-SS-A7HO', 'TCGA-CM-5864', 'TCGA-AF-4110', 'TCGA-AF-6672', 'TCGA-AA-3866', 'TCGA-AA-A01V', 'TCGA-AA-A00N', 'TCGA-D5-6538', 'TCGA-CM-6170', 'TCGA-D5-6537', 'TCGA-AA-A02Y', 'TCGA-DY-A1DC', 'TCGA-AG-3883', 'TCGA-AD-5900', 'TCGA-AA-3955', 'TCGA-AG-3581', 'TCGA-A6-5664', 'TCGA-DY-A1DF', 'TCGA-AA-A02O', 'TCGA-AA-3527', 'TCGA-F5-6702', 'TCGA-AH-6643', 'TCGA-G4-6293', 'TCGA-DC-5337', 'TCGA-AA-A03F', 'TCGA-CI-6624', 'TCGA-AG-3602', 'TCGA-AA-3538', 'TCGA-AA-A01Z', 'TCGA-AA-3558', 'TCGA-AA-3560', 'TCGA-AA-3867', 'TCGA-A6-6650', 'TCGA-CK-5915', 'TCGA-A6-6651', 'TCGA-AY-4070', 'TCGA-AF-A56N', 'TCGA-D5-6533', 'TCGA-AA-3529', 'TCGA-AG-A032', 'TCGA-QG-A5YX'] + + } + } + } + } + { + id = "shareable_generator" + path = "nvflare.app_common.ccwf.comps.simple_model_shareable_generator.SimpleModelShareableGenerator" + args {} + } + { + id = "metrics_pipe" + path = "nvflare.fuel.utils.pipe.cell_pipe.CellPipe" + args { + mode = "PASSIVE" + site_name = "{SITE_NAME}" + token = "{JOB_ID}" + root_url = "{ROOT_URL}" + secure_mode = "{SECURE_MODE}" + workspace_dir = "{WORKSPACE}" + } + } + { + id = "metric_relay" + path = "nvflare.app_common.widgets.metric_relay.MetricRelay" + args { + pipe_id = "metrics_pipe" + event_type = "fed.analytix_log_stats" + read_interval = 0.1 + } + } + { + id = "model_selector" + path = "nvflare.app_common.widgets.intime_model_selector.IntimeModelSelector" + args { + key_metric = "accuracy" + } + } + { + id = "config_preparer" + path = "nvflare.app_common.widgets.external_configurator.ExternalConfigurator" + args { + component_ids = [ + "metric_relay" + ] + } + } + ] +} diff --git a/application/jobs/stamp/app/config/config_fed_server.conf b/application/jobs/stamp/app/config/config_fed_server.conf new file mode 100644 index 00000000..b013ea37 --- /dev/null +++ b/application/jobs/stamp/app/config/config_fed_server.conf @@ -0,0 +1,26 @@ +format_version = 2 +task_data_filters = [] +task_result_filters = [] +components = [ + { + # write validation results to json file + id = "json_generator" + path = "nvflare.app_common.widgets.validation_json_generator.ValidationJsonGenerator" + args {} + } +] +workflows = [ + { + # server-side controller to manage job life cycle + id = "swarm_controller" + path = "nvflare.app_common.ccwf.SwarmServerController" + args { + # can also set aggregation clients and train clients, see class for all available args + num_rounds = 20 + #start_task_timeout = 360000 + #progress_timeout = 360000 + #configure_task_timeout = 360000 + #max_status_report_interval = 360000 + } + } +] diff --git a/application/jobs/stamp/app/custom/__init__.py b/application/jobs/stamp/app/custom/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/application/jobs/stamp/app/custom/config.py b/application/jobs/stamp/app/custom/config.py new file mode 100644 index 00000000..c4274211 --- /dev/null +++ b/application/jobs/stamp/app/custom/config.py @@ -0,0 +1,101 @@ +import os +from collections.abc import Sequence +from pathlib import Path + +import torch +from pydantic import BaseModel, ConfigDict, Field + +from modeling.registry import ModelName +from modeling.types import Category, PandasLabel + + +class TrainConfig(BaseModel): + model_config = ConfigDict(extra="forbid") + + output_dir: Path = Field(description="The directory to save the results to") + + clini_table: Path = Field(description="Excel or CSV to read clinical data from") + slide_table: Path | None = Field( + default=None, description="Excel or CSV to read patient-slide associations from" + ) + feature_dir: Path = Field(description="Directory containing feature files") + + ground_truth_label: PandasLabel = Field( + description="Name of categorical column in clinical table to train on" + ) + categories: Sequence[Category] | None = None + + patient_label: PandasLabel = "PATIENT" + filename_label: PandasLabel = "FILENAME" + + params_path: Path | None = Field( + default=None, + description="Optional: Path to a YAML file with advanced training parameters.", + ) + + # Experimental features + use_vary_precision_transform: bool = False + use_alibi: bool = False + + +class CrossvalConfig(TrainConfig): + n_splits: int = Field(5, ge=2) + + +class DeploymentConfig(BaseModel): + model_config = ConfigDict(extra="forbid") + + output_dir: Path + + checkpoint_paths: list[Path] + clini_table: Path | None = None + slide_table: Path + feature_dir: Path + + ground_truth_label: PandasLabel | None = None + patient_label: PandasLabel = "PATIENT" + filename_label: PandasLabel = "FILENAME" + + num_workers: int = min(os.cpu_count() or 1, 16) + accelerator: str = "gpu" if torch.cuda.is_available() else "cpu" + + +class VitModelParams(BaseModel): + model_config = ConfigDict(extra="forbid") + dim_model: int = 512 + dim_feedforward: int = 512 + n_heads: int = 8 + n_layers: int = 2 + dropout: float = 0.0 + # Experimental feature: Use ALiBi positional embedding + use_alibi: bool = False + + +class MlpModelParams(BaseModel): + model_config = ConfigDict(extra="forbid") + dim_hidden: int = 512 + num_layers: int = 2 + dropout: float = 0.25 + + +class ModelParams(BaseModel): + model_config = ConfigDict(extra="forbid") + vit: VitModelParams + mlp: MlpModelParams + + +class AdvancedConfig(BaseModel): + model_config = ConfigDict(extra="forbid") + bag_size: int = 512 + num_workers: int = min(os.cpu_count() or 1, 16) + batch_size: int = 64 + max_epochs: int = 32 + patience: int = 16 + accelerator: str = "gpu" if torch.cuda.is_available() else "cpu" + max_lr: float = 1e-4 + div_factor: float = 25.0 + model_name: ModelName | None = Field( + default=None, + description='Optional: "vit" or "mlp". Defaults based on feature type.', + ) + model_params: ModelParams diff --git a/application/jobs/stamp/app/custom/data.py b/application/jobs/stamp/app/custom/data.py new file mode 100755 index 00000000..52ab10c8 --- /dev/null +++ b/application/jobs/stamp/app/custom/data.py @@ -0,0 +1,608 @@ +"""Helper classes to manage pytorch data.""" + +import logging +from collections.abc import Callable, Iterable, Mapping, Sequence +from dataclasses import KW_ONLY, dataclass +from itertools import groupby +from pathlib import Path +from typing import IO, BinaryIO, Generic, TextIO, TypeAlias, cast, Union + +import h5py +import numpy as np +import pandas as pd +import torch +from jaxtyping import Bool, Float +from packaging.version import Version +from torch import Tensor +from torch.utils.data import DataLoader, Dataset + +from modeling.types import ( + Bags, + BagSize, + BagSizes, + Category, + CoordinatesBatch, + EncodedTargets, + FeaturePath, + GroundTruth, + GroundTruthType, + Microns, + PandasLabel, + PatientId, + SlideMPP, + TilePixels, +) + +_logger = logging.getLogger("stamp") +_logged_stamp_v1_warning = False + + +__author__ = "Marko van Treeck" +__copyright__ = "Copyright (C) 2022-2025 Marko van Treeck" +__license__ = "MIT" + +_Bag: TypeAlias = Float[Tensor, "tile feature"] +_EncodedTarget: TypeAlias = Bool[Tensor, "category_is_hot"] # noqa: F821 +_BinaryIOLike: TypeAlias = Union[BinaryIO, IO[bytes]] +"""The ground truth, encoded numerically (currently: one-hot)""" +_Coordinates: TypeAlias = Float[Tensor, "tile 2"] + + +@dataclass +class PatientData(Generic[GroundTruthType]): + """All raw (i.e. non-generated) information we have on the patient.""" + + _ = KW_ONLY + ground_truth: GroundTruthType + feature_files: Iterable[FeaturePath | BinaryIO] + + +def tile_bag_dataloader( + *, + patient_data: Sequence[PatientData[GroundTruth | None]], + bag_size: int | None, + categories: Sequence[Category] | None = None, + batch_size: int, + shuffle: bool, + num_workers: int, + transform: Callable[[Tensor], Tensor] | None, +) -> tuple[ + DataLoader[tuple[Bags, CoordinatesBatch, BagSizes, EncodedTargets]], + Sequence[Category], +]: + """Creates a dataloader from patient data for tile-level (bagged) features. + + Args: + categories: + Order of classes for one-hot encoding. + If `None`, classes are inferred from patient data. + """ + + raw_ground_truths = np.array([patient.ground_truth for patient in patient_data]) + categories = ( + categories if categories is not None else list(np.unique(raw_ground_truths)) + ) + one_hot = torch.tensor(raw_ground_truths.reshape(-1, 1) == categories) + ds = BagDataset( + bags=[patient.feature_files for patient in patient_data], + bag_size=bag_size, + ground_truths=one_hot, + transform=transform, + ) + + return ( + cast( + DataLoader[tuple[Bags, CoordinatesBatch, BagSizes, EncodedTargets]], + DataLoader( + ds, + batch_size=batch_size, + shuffle=shuffle, + num_workers=num_workers, + collate_fn=_collate_to_tuple, + ), + ), + list(categories), + ) + + +def _collate_to_tuple( + items: list[tuple[_Bag, _Coordinates, BagSize, _EncodedTarget]], +) -> tuple[Bags, CoordinatesBatch, BagSizes, EncodedTargets]: + bags = torch.stack([bag for bag, _, _, _ in items]) + coords = torch.stack([coord for _, coord, _, _ in items]) + bag_sizes = torch.tensor([bagsize for _, _, bagsize, _ in items]) + encoded_targets = torch.stack([encoded_target for _, _, _, encoded_target in items]) + + return (bags, coords, bag_sizes, encoded_targets) + + +def patient_feature_dataloader( + *, + patient_data: Sequence[PatientData[GroundTruth | None]], + categories: Sequence[Category] | None = None, + batch_size: int, + shuffle: bool, + num_workers: int, + transform: Callable[[Tensor], Tensor] | None, +) -> tuple[DataLoader, Sequence[Category]]: + """ + Creates a dataloader for patient-level features (one feature vector per patient). + """ + feature_files = [next(iter(p.feature_files)) for p in patient_data] + raw_ground_truths = np.array([patient.ground_truth for patient in patient_data]) + categories = ( + categories if categories is not None else list(np.unique(raw_ground_truths)) + ) + one_hot = torch.tensor(raw_ground_truths.reshape(-1, 1) == categories) + ds = PatientFeatureDataset(feature_files, one_hot, transform=transform) + dl = DataLoader(ds, batch_size=batch_size, shuffle=shuffle, num_workers=num_workers) + return dl, categories + + +def detect_feature_type(feature_dir: Path) -> str: + """ + Detects feature type by inspecting all .h5 files in feature_dir. + + Returns: + "tile" if all files are tile-level, "patient" if all are patient-level. + If files have mixed types, raises an error. + If no .h5 files are found, raises an error. + """ + feature_types = set() + files_checked = 0 + + for file in feature_dir.glob("*.h5"): + files_checked += 1 + with h5py.File(file, "r") as h5: + feat_type = h5.attrs.get("feat_type") + encoder = h5.attrs.get("encoder") + + if feat_type is not None or encoder is not None: + feature_types.add(str(feat_type)) + else: + # If feat_type is missing, always treat as tile-level feature + feature_types.add("tile") + + if files_checked == 0: + raise RuntimeError("No .h5 feature files found in feature_dir.") + + if len(feature_types) > 1: + raise RuntimeError( + f"Multiple feature types detected in {feature_dir}: {feature_types}. " + "All feature files must have the same type." + ) + + return feature_types.pop() + + +def load_patient_level_data( + *, + clini_table: Path, + feature_dir: Path, + patient_label: PandasLabel, + ground_truth_label: PandasLabel, + feature_ext: str = ".h5", +) -> dict[PatientId, PatientData]: + """ + Loads PatientData for patient-level features, matching patients in the clinical table + to feature files in feature_dir named {patient_id}.h5. + """ + # TODO: I'm not proud at all of this. Any other alternative for mapping + # clinical data to the patient-level feature paths that avoids + # creating another slide table for encoded featuress is welcome :P. + + clini_df = read_table( + clini_table, + usecols=[patient_label, ground_truth_label], + dtype=str, + ).dropna() + + patient_to_data: dict[PatientId, PatientData] = {} + missing_features = [] + for _, row in clini_df.iterrows(): + patient_id = PatientId(str(row[patient_label])) + ground_truth = row[ground_truth_label] + feature_file = feature_dir / f"{patient_id}{feature_ext}" + if feature_file.exists(): + patient_to_data[patient_id] = PatientData( + ground_truth=ground_truth, + feature_files=[FeaturePath(feature_file)], + ) + else: + missing_features.append(patient_id) + + if missing_features: + _logger.warning( + f"Some patients have no feature file in {feature_dir}: {missing_features}" + ) + + return patient_to_data + + +@dataclass +class BagDataset(Dataset[tuple[_Bag, _Coordinates, BagSize, _EncodedTarget]]): + """A dataset of bags of instances.""" + + _: KW_ONLY + bags: Sequence[Iterable[FeaturePath | _BinaryIOLike]] + """The `.h5` files containing the bags. + + Each bag consists of the features taken from one or multiple h5 files. + Each of the h5 files needs to have a dataset called `feats` of shape N x F, + where N is the number of instances and F the number of features per instance. + """ + + bag_size: BagSize | None = None + """The number of instances in each bag. + + For bags containing more instances, + a random sample of `bag_size` instances will be drawn. + Smaller bags are padded with zeros. + If `bag_size` is None, all the samples will be used. + """ + + ground_truths: Bool[Tensor, "index category_is_hot"] + """The ground truth for each bag, one-hot encoded.""" + + transform: Callable[[Tensor], Tensor] | None + + def __post_init__(self) -> None: + if len(self.bags) != len(self.ground_truths): + raise ValueError( + "the number of ground truths has to match the number of bags" + ) + + def __len__(self) -> int: + return len(self.bags) + + def __getitem__( + self, index: int + ) -> tuple[_Bag, _Coordinates, BagSize, _EncodedTarget]: + # Collect all the features + feats = [] + coords_um = [] + for bag_file in self.bags[index]: + with h5py.File(bag_file, "r") as h5: + feats.append( + torch.from_numpy(h5["feats"][:]) # pyright: ignore[reportIndexIssue] + ) + coords_um.append(torch.from_numpy(get_coords(h5).coords_um)) + + feats = torch.concat(feats).float() + coords_um = torch.concat(coords_um).float() + + if self.transform is not None: + feats = self.transform(feats) + + # Sample a subset, if required + if self.bag_size is not None: + return ( + *_to_fixed_size_bag(feats, coords=coords_um, bag_size=self.bag_size), + self.ground_truths[index], + ) + else: + return ( + feats, + coords_um, + len(feats), + self.ground_truths[index], + ) + + +class PatientFeatureDataset(Dataset): + """ + Dataset for single feature vector per sample (e.g. slide-level or patient-level). + Each item is a (feature_vector, label_onehot) tuple. + """ + + def __init__( + self, + feature_files: Sequence[FeaturePath | BinaryIO], + ground_truths: Tensor, # shape: [num_samples, num_classes] + transform: Callable[[Tensor], Tensor] | None, + ): + if len(feature_files) != len(ground_truths): + raise ValueError("Number of feature files and ground truths must match.") + self.feature_files = feature_files + self.ground_truths = ground_truths + self.transform = transform + + def __len__(self): + return len(self.feature_files) + + def __getitem__(self, idx: int): + feature_file = self.feature_files[idx] + with h5py.File(feature_file, "r") as h5: + feats = torch.from_numpy(h5["feats"][:]) # pyright: ignore[reportIndexIssue] + # Accept [V] or [1, V] + if feats.ndim == 2 and feats.shape[0] == 1: + feats = feats[0] + elif feats.ndim == 1: + pass + else: + raise RuntimeError( + f"Expected single feature vector (shape [F] or [1, F]), got {feats.shape} in {feature_file}." + "Check that the features are patient-level." + ) + if self.transform is not None: + feats = self.transform(feats) + label = self.ground_truths[idx] + return feats, label + + +@dataclass +class CoordsInfo: + coords_um: np.ndarray + tile_size_um: Microns + tile_size_px: TilePixels | None = None + + @property + def mpp(self) -> SlideMPP: + if not self.tile_size_px: + raise RuntimeError( + "tile size in pixels is not available. Please reextract them using `stamp preprocess`." + ) + return SlideMPP(self.tile_size_um / self.tile_size_px) + + +def get_coords(feature_h5: h5py.File) -> CoordsInfo: + coords: np.ndarray = feature_h5["coords"][:] # type: ignore + coords_um: np.ndarray | None = None + tile_size_um: Microns | None = None + tile_size_px: TilePixels | None = None + if (tile_size := feature_h5.attrs.get("tile_size", None)) and feature_h5.attrs.get( + "unit", None + ) == "um": + # STAMP v2 format + tile_size_um = Microns(float(tile_size)) + coords_um = coords + elif tile_size := feature_h5.attrs.get("tile_size_um", None): + # Newer STAMP format + tile_size_um = Microns(float(tile_size)) + coords_um = coords + elif ( + round( + feature_h5.attrs.get( + "tile_size", get_stride(torch.from_numpy(coords).float()) + ) + ) + == 224 + ): + # Historic STAMP format + # TODO: find a better way to get this warning just once + global _logged_stamp_v1_warning + if not _logged_stamp_v1_warning: + _logger.info( + f"{feature_h5.filename}: tile stride is roughly 224, assuming coordinates have unit 256um/224px (historic STAMP format)" + ) + _logged_stamp_v1_warning = True + tile_size_um = Microns(256.0) + tile_size_px = TilePixels(224) + coords_um = coords / 224 * 256 + + if (version_str := feature_h5.attrs.get("stamp_version")) and ( + extraction_version := Version(version_str) + ) > Version(stamp.__version__): + raise RuntimeError( + f"features were extracted with a newer version of stamp, please update your stamp to at least version {extraction_version}." + ) + + if not tile_size_px and "tile_size_px" in feature_h5.attrs: + tile_size_px = TilePixels(int(feature_h5.attrs["tile_size_px"])) # pyright: ignore[reportArgumentType] + + if not tile_size_um or coords_um is None: + raise RuntimeError( + "unable to infer coordinates from feature file. Please reextract them using `stamp preprocess`." + ) + + return CoordsInfo(coords_um, tile_size_um, tile_size_px) + + +def _to_fixed_size_bag( + bag: _Bag, coords: _Coordinates, bag_size: BagSize +) -> tuple[_Bag, _Coordinates, BagSize]: + """Samples a fixed-size bag of tiles from an arbitrary one. + + If the original bag did not have enough tiles, + the bag is zero-padded to the right. + """ + # get up to bag_size elements + n_tiles, _dim_feats = bag.shape + bag_idxs = torch.randperm(n_tiles)[:bag_size] + bag_samples = bag[bag_idxs] + coord_samples = coords[bag_idxs] + + # zero-pad if we don't have enough samples + zero_padded_bag = torch.cat( + ( + bag_samples, + torch.zeros(bag_size - bag_samples.shape[0], bag_samples.shape[1]), + ) + ) + zero_padded_coord = torch.cat( + ( + coord_samples, + torch.zeros(bag_size - coord_samples.shape[0], coord_samples.shape[1]), + ) + ) + return zero_padded_bag, zero_padded_coord, min(bag_size, len(bag)) + + +def patient_to_ground_truth_from_clini_table_( + *, + clini_table_path: Path | TextIO, + patient_label: PandasLabel, + ground_truth_label: PandasLabel, +) -> dict[PatientId, GroundTruth]: + """Loads the patients and their ground truths from a clini table.""" + clini_df = read_table( + clini_table_path, + usecols=[patient_label, ground_truth_label], + dtype=str, + ).dropna() + try: + patient_to_ground_truth: Mapping[PatientId, GroundTruth] = clini_df.set_index( + patient_label, verify_integrity=True + )[ground_truth_label].to_dict() + except KeyError as e: + if patient_label not in clini_df: + raise ValueError( + f"{patient_label} was not found in clini table " + f"(columns in clini table: {clini_df.columns})" + ) from e + elif ground_truth_label not in clini_df: + raise ValueError( + f"{ground_truth_label} was not found in clini table " + f"(columns in clini table: {clini_df.columns})" + ) from e + else: + raise e from e + + return patient_to_ground_truth + + +def slide_to_patient_from_slide_table_( + *, + slide_table_path: Path, + feature_dir: Path, + patient_label: PandasLabel, + filename_label: PandasLabel, +) -> dict[FeaturePath, PatientId]: + """ + Creates a slide-to-patient mapping from a slide table. + Side effects: + Verifies that all files in the slide tables filename_label + column has an .h5 extension. + """ + slide_df = read_table( + slide_table_path, + usecols=[patient_label, filename_label], + dtype=str, + ) + # Verify the slide table contains a feature path with .h5 extension by + # checking the filename_label. + for x in slide_df[filename_label]: + if not str(x).endswith(".h5"): + raise ValueError( + "One or more files are missing the .h5 extension in the " + "filename_label column. The first file missing the .h5 " + "extension is: " + str(x) + "." + ) + + slide_to_patient: Mapping[FeaturePath, PatientId] = { + FeaturePath(feature_dir / cast(str, k)): PatientId(cast(str, patient)) + for k, patient in slide_df.set_index(filename_label, verify_integrity=True)[ + patient_label + ].items() + } + + return slide_to_patient + + +def read_table(path: Path | TextIO, **kwargs) -> pd.DataFrame: + if not isinstance(path, Path): + return pd.read_csv(path, **kwargs) + elif path.suffix == ".xlsx": + return pd.read_excel(path, **kwargs) + elif path.suffix == ".csv": + return pd.read_csv(path, **kwargs) + else: + raise ValueError( + "table to load has to either be an excel (`*.xlsx`) or csv (`*.csv`) file." + ) + + +def filter_complete_patient_data_( + *, + patient_to_ground_truth: Mapping[PatientId, GroundTruth | None], + slide_to_patient: Mapping[FeaturePath, PatientId], + drop_patients_with_missing_ground_truth: bool, +) -> Mapping[PatientId, PatientData]: + """Aggregate information for all patients for which we have complete data. + + This will sort out slides with missing ground truth, missing features, etc. + Patients with their ground truth set explicitly set to `None` will be _included_. + + Side effects: + Checks feature paths' existance. + """ + + _log_patient_slide_feature_inconsistencies( + patient_to_ground_truth=patient_to_ground_truth, + slide_to_patient=slide_to_patient, + ) + + patient_to_slides: dict[PatientId, set[FeaturePath]] = { + patient: set(slides) + for patient, slides in groupby( + slide_to_patient, lambda slide: slide_to_patient[slide] + ) + } + + if not drop_patients_with_missing_ground_truth: + patient_to_ground_truth = { + **{patient_id: None for patient_id in patient_to_slides}, + **patient_to_ground_truth, + } + + patients = { + patient_id: PatientData( + ground_truth=ground_truth, feature_files=existing_features_for_patient + ) + for patient_id, ground_truth in patient_to_ground_truth.items() + # Restrict to only patients which have slides and features + if (slides := patient_to_slides.get(patient_id)) is not None + and ( + existing_features_for_patient := { + feature_path for feature_path in slides if feature_path.exists() + } + ) + } + + return patients + + +def _log_patient_slide_feature_inconsistencies( + *, + patient_to_ground_truth: Mapping[PatientId, GroundTruthType], + slide_to_patient: Mapping[FeaturePath, PatientId], +) -> None: + """Checks whether the arguments are consistent and logs all irregularities. + + Has no side effects outside of logging. + """ + if ( + patients_without_slides := patient_to_ground_truth.keys() + - slide_to_patient.values() + ): + _logger.warning( + f"some patients have no associated slides: {patients_without_slides}" + ) + + if patients_without_ground_truth := ( + slide_to_patient.values() - patient_to_ground_truth.keys() + ): + _logger.warning( + f"some patients have no clinical information: {patients_without_ground_truth}" + ) + + if slides_without_features := { + slide for slide in slide_to_patient.keys() if not slide.exists() + }: + _logger.warning( + f"some feature files could not be found: {slides_without_features}" + ) + + +def get_stride(coords: Float[Tensor, "tile 2"]) -> float: + """Gets the minimum step width between any two coordintes.""" + xs: Tensor = coords[:, 0].unique(sorted=True) + ys: Tensor = coords[:, 1].unique(sorted=True) + stride = cast( + float, + min( + (xs[1:] - xs[:-1]).min().item(), + (ys[1:] - ys[:-1]).min().item(), + ), + ) + return stride diff --git a/application/jobs/stamp/app/custom/main.py b/application/jobs/stamp/app/custom/main.py new file mode 100644 index 00000000..c42220cf --- /dev/null +++ b/application/jobs/stamp/app/custom/main.py @@ -0,0 +1,440 @@ +import logging +import shutil +from collections import Counter +from collections.abc import Callable, Mapping, Sequence +from pathlib import Path +from typing import cast + +import lightning +import lightning.pytorch +import lightning.pytorch.accelerators +import lightning.pytorch.accelerators.accelerator +import nvflare.client as flare_util +import nvflare.client.lightning as flare +import torch +from lightning.pytorch.accelerators.accelerator import Accelerator +from lightning.pytorch.callbacks import EarlyStopping, ModelCheckpoint +from lightning.pytorch.loggers import CSVLogger +from sklearn.model_selection import train_test_split +from torch.utils.data.dataloader import DataLoader + +from modeling.config import AdvancedConfig, TrainConfig +from modeling.lightning_model import ( + Bags, + BagSizes, + EncodedTargets, +) +from modeling.registry import MODEL_REGISTRY, ModelName +from modeling.types import Category, CoordinatesBatch, GroundTruth, PandasLabel, PatientId +from .data import ( + BagDataset, + PatientData, + PatientFeatureDataset, + detect_feature_type, + filter_complete_patient_data_, + load_patient_level_data, + patient_feature_dataloader, + patient_to_ground_truth_from_clini_table_, + slide_to_patient_from_slide_table_, + tile_bag_dataloader, +) +from .transforms import VaryPrecisionTransform + +__author__ = "Marko van Treeck" +__copyright__ = "Copyright (C) 2024 Marko van Treeck" +__license__ = "MIT" + +_logger = logging.getLogger("stamp") + + +def train_categorical_model_( + *, + config: TrainConfig, + advanced: AdvancedConfig, +) -> None: + """Trains a model based on the feature type.""" + feature_type = detect_feature_type(config.feature_dir) + _logger.info(f"Detected feature type: {feature_type}") + + if feature_type == "tile": + if config.slide_table is None: + raise ValueError("A slide table is required for tile-level modeling") + patient_to_ground_truth = patient_to_ground_truth_from_clini_table_( + clini_table_path=config.clini_table, + ground_truth_label=config.ground_truth_label, + patient_label=config.patient_label, + ) + slide_to_patient = slide_to_patient_from_slide_table_( + slide_table_path=config.slide_table, + feature_dir=config.feature_dir, + patient_label=config.patient_label, + filename_label=config.filename_label, + ) + patient_to_data = filter_complete_patient_data_( + patient_to_ground_truth=patient_to_ground_truth, + slide_to_patient=slide_to_patient, + drop_patients_with_missing_ground_truth=True, + ) + elif feature_type == "patient": + # Patient-level: ignore slide_table + if config.slide_table is not None: + _logger.warning("slide_table is ignored for patient-level features.") + patient_to_data = load_patient_level_data( + clini_table=config.clini_table, + feature_dir=config.feature_dir, + patient_label=config.patient_label, + ground_truth_label=config.ground_truth_label, + ) + elif feature_type == "slide": + raise RuntimeError( + "Slide-level features are not supported for training." + "Please rerun the encoding step with patient-level encoding." + ) + else: + raise RuntimeError(f"Unknown feature type: {feature_type}") + + # Train the model (the rest of the logic is unchanged) + model, train_dl, valid_dl = setup_model_for_training( + patient_to_data=patient_to_data, + categories=config.categories, + advanced=advanced, + ground_truth_label=config.ground_truth_label, + clini_table=config.clini_table, + slide_table=config.slide_table, + feature_dir=config.feature_dir, + train_transform=( + VaryPrecisionTransform(min_fraction_bits=1) + if config.use_vary_precision_transform + else None + ), + feature_type=feature_type, + ) + train_model_( + output_dir=config.output_dir, + model=model, + train_dl=train_dl, + valid_dl=valid_dl, + max_epochs=advanced.max_epochs, + patience=advanced.patience, + accelerator=advanced.accelerator, + ) + + +def setup_model_for_training( + *, + patient_to_data: Mapping[PatientId, PatientData[GroundTruth]], + categories: Sequence[Category] | None, + train_transform: Callable[[torch.Tensor], torch.Tensor] | None, + feature_type: str, + advanced: AdvancedConfig, + # Metadata, has no effect on model training + ground_truth_label: PandasLabel, + clini_table: Path, + slide_table: Path | None, + feature_dir: Path, +) -> tuple[ + lightning.LightningModule, + DataLoader, + DataLoader, +]: + """Creates a model and dataloaders for training""" + + train_dl, valid_dl, train_categories, dim_feats, train_patients, valid_patients = ( + setup_dataloaders_for_training( + patient_to_data=patient_to_data, + categories=categories, + bag_size=advanced.bag_size, + batch_size=advanced.batch_size, + num_workers=advanced.num_workers, + train_transform=train_transform, + feature_type=feature_type, + ) + ) + + _logger.info( + "Training dataloaders: bag_size=%s, batch_size=%s, num_workers=%s", + advanced.bag_size, + advanced.batch_size, + advanced.num_workers, + ) + + category_weights = _compute_class_weights_and_check_categories( + train_dl=train_dl, + feature_type=feature_type, + train_categories=train_categories, + ) + + # 1. Default to a model if none is specified + if advanced.model_name is None: + advanced.model_name = ModelName.VIT if feature_type == "tile" else ModelName.MLP + _logger.info( + f"No model specified, defaulting to '{advanced.model_name.value}' for feature type '{feature_type}'" + ) + + # 2. Validate that the chosen model supports the feature type + model_info = MODEL_REGISTRY[advanced.model_name] + if feature_type not in model_info["supported_features"]: + raise ValueError( + f"Model '{advanced.model_name.value}' does not support feature type '{feature_type}'. " + f"Supported types are: {model_info['supported_features']}" + ) + + # 3. Get model-specific hyperparameters + model_specific_params = advanced.model_params.model_dump()[ + advanced.model_name.value + ] + + # 4. Calculate total steps for scheduler + steps_per_epoch = len(train_dl) + total_steps = steps_per_epoch * advanced.max_epochs + + # 5. Prepare common parameters + common_params = { + "categories": train_categories, + "category_weights": category_weights, + "dim_input": dim_feats, + "total_steps": total_steps, + "max_lr": advanced.max_lr, + "div_factor": advanced.div_factor, + # Metadata, has no effect on model training + "model_name": advanced.model_name.value, + "ground_truth_label": ground_truth_label, + "train_patients": train_patients, + "valid_patients": valid_patients, + "clini_table": clini_table, + "slide_table": slide_table, + "feature_dir": feature_dir, + } + + # 6. Instantiate the model dynamically + ModelClass = model_info["model_class"] + all_params = {**common_params, **model_specific_params} + _logger.info( + f"Instantiating model '{advanced.model_name.value}' with parameters: {model_specific_params}" + ) + _logger.info( + "Other params: max_epochs=%s, patience=%s", + advanced.max_epochs, + advanced.patience, + ) + model = ModelClass(**all_params) + + return model, train_dl, valid_dl + + +def setup_dataloaders_for_training( + *, + patient_to_data: Mapping[PatientId, PatientData[GroundTruth]], + categories: Sequence[Category] | None, + bag_size: int, + batch_size: int, + num_workers: int, + train_transform: Callable[[torch.Tensor], torch.Tensor] | None, + feature_type: str, +) -> tuple[ + DataLoader, + DataLoader, + Sequence[Category], + int, + Sequence[PatientId], + Sequence[PatientId], +]: + """ + Creates train/val dataloaders for tile-level or patient-level features. + + Returns: + train_dl, valid_dl, categories, feature_dim, train_patients, valid_patients + """ + # Sample count for training + log_total_class_summary(patient_to_data, categories) + + # Stratified split + ground_truths = [ + patient_data.ground_truth + for patient_data in patient_to_data.values() + if patient_data.ground_truth is not None + ] + if len(ground_truths) != len(patient_to_data): + raise ValueError( + "patient_to_data must have a ground truth defined for all targets!" + ) + + train_patients, valid_patients = cast( + tuple[Sequence[PatientId], Sequence[PatientId]], + train_test_split( + list(patient_to_data), stratify=ground_truths, shuffle=True, random_state=0 + ), + ) + + if feature_type == "tile": + # Use existing BagDataset logic + train_dl, train_categories = tile_bag_dataloader( + patient_data=[patient_to_data[pid] for pid in train_patients], + categories=categories, + bag_size=bag_size, + batch_size=batch_size, + shuffle=True, + num_workers=num_workers, + transform=train_transform, + ) + valid_dl, _ = tile_bag_dataloader( + patient_data=[patient_to_data[pid] for pid in valid_patients], + bag_size=None, + categories=train_categories, + batch_size=1, + shuffle=False, + num_workers=num_workers, + transform=None, + ) + bags, _, _, _ = next(iter(train_dl)) + dim_feats = bags.shape[-1] + return ( + train_dl, + valid_dl, + train_categories, + dim_feats, + train_patients, + valid_patients, + ) + + elif feature_type == "patient": + train_dl, train_categories = patient_feature_dataloader( + patient_data=[patient_to_data[pid] for pid in train_patients], + categories=categories, + batch_size=batch_size, + shuffle=True, + num_workers=num_workers, + transform=train_transform, + ) + valid_dl, _ = patient_feature_dataloader( + patient_data=[patient_to_data[pid] for pid in valid_patients], + categories=train_categories, + batch_size=1, + shuffle=False, + num_workers=num_workers, + transform=None, + ) + feats, _ = next(iter(train_dl)) + dim_feats = feats.shape[-1] + return ( + train_dl, + valid_dl, + train_categories, + dim_feats, + train_patients, + valid_patients, + ) + else: + raise RuntimeError( + f"Unsupported feature type: {feature_type}. Only 'tile' and 'patient' are supported." + ) + + +def train_model_( + *, + output_dir: Path, + model: lightning.LightningModule, + train_dl: DataLoader[tuple[Bags, CoordinatesBatch, BagSizes, EncodedTargets]], + valid_dl: DataLoader[tuple[Bags, CoordinatesBatch, BagSizes, EncodedTargets]], + max_epochs: int, + patience: int, + accelerator: str | Accelerator, +) -> lightning.LightningModule: + """Trains a model. + + Returns: + The model with the best validation loss during training. + """ + torch.set_float32_matmul_precision("high") + + model_checkpoint = ModelCheckpoint( + monitor="validation_loss", + mode="min", + filename="checkpoint-{epoch:02d}-{validation_loss:0.3f}", + ) + + trainer = lightning.Trainer( + default_root_dir=output_dir, + callbacks=[ + EarlyStopping(monitor="validation_loss", mode="min", patience=patience), + model_checkpoint, + ], + max_epochs=max_epochs, + # FIXME The number of accelerators is currently fixed to one for the + # following reasons: + # 1. `trainer.predict()` does not return any predictions if used with + # the default strategy no multiple GPUs + # 2. `barspoon.model.SafeMulticlassAUROC` breaks on multiple GPUs + accelerator=accelerator, + devices=1, + gradient_clip_val=0.5, + logger=CSVLogger(save_dir=output_dir), + log_every_n_steps=len(train_dl), + ) + flare_util.init() + SITE_NAME = flare.get_site_name() + flare.patch(trainer) # Patch trainer to enable swarm learning + while flare.is_running(): + input_model = flare.receive() + trainer.fit( + model=model, + train_dataloaders=train_dl, + val_dataloaders=valid_dl, + ) + shutil.copy(model_checkpoint.best_model_path, output_dir / "model.ckpt") + + # Reload the best model using the same class as the input model + ModelClass = type(model) + return ModelClass.load_from_checkpoint(model_checkpoint.best_model_path) + + +def _compute_class_weights_and_check_categories( + *, + train_dl: DataLoader, + feature_type: str, + train_categories: Sequence[str], +) -> torch.Tensor: + """ + Computes class weights and checks for category issues. + Logs warnings if there are too few or underpopulated categories. + Returns normalized category weights as a torch.Tensor. + """ + if feature_type == "tile": + category_counts = cast(BagDataset, train_dl.dataset).ground_truths.sum(dim=0) + else: + category_counts = cast( + PatientFeatureDataset, train_dl.dataset + ).ground_truths.sum(dim=0) + cat_ratio_reciprocal = category_counts.sum() / category_counts + category_weights = cat_ratio_reciprocal / cat_ratio_reciprocal.sum() + + if len(train_categories) <= 1: + raise ValueError(f"not enough categories to train on: {train_categories}") + elif any(category_counts < 16): + underpopulated_categories = { + category: int(count) + for category, count in zip(train_categories, category_counts, strict=True) + if count < 16 + } + _logger.warning( + f"Some categories do not have enough samples to meaningfully train a model: {underpopulated_categories}. " + "You may want to consider removing these categories; the model will likely overfit on the few samples available." + ) + return category_weights + + +def log_total_class_summary( + patient_to_data: Mapping[PatientId, PatientData], + categories: Sequence[Category] | None, +) -> None: + ground_truths = [ + patient_data.ground_truth + for patient_data in patient_to_data.values() + if patient_data.ground_truth is not None + ] + cats = categories or sorted(set(ground_truths)) + counter = Counter(ground_truths) + _logger.info( + f"Total samples: {len(ground_truths)} | " + + " | ".join([f"Class {cls}: {counter.get(cls, 0)}" for cls in cats]) + ) diff --git a/application/jobs/stamp/app/custom/modeling/alibi.py b/application/jobs/stamp/app/custom/modeling/alibi.py new file mode 100644 index 00000000..69c61aed --- /dev/null +++ b/application/jobs/stamp/app/custom/modeling/alibi.py @@ -0,0 +1,147 @@ +import torch +from jaxtyping import Bool, Float +from torch import Tensor, nn + + +class _RunningMeanScaler(nn.Module): + """Scales values by the inverse of the mean of values seen before.""" + + def __init__(self, dtype=torch.float32) -> None: + super().__init__() + self.running_mean = nn.Buffer(torch.ones(1, dtype=dtype)) + self.items_so_far = nn.Buffer(torch.ones(1, dtype=dtype)) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + if self.training: + # Welford's algorithm + self.running_mean.copy_( + (self.running_mean + (x - self.running_mean) / self.items_so_far).mean() + ) + self.items_so_far += 1 + + return x / self.running_mean + + +class _ALiBi(nn.Module): + # See MultiHeadAliBi + def __init__(self) -> None: + super().__init__() + + self.scale_distance = _RunningMeanScaler() + self.bias_scale = nn.Parameter(torch.rand(1)) + + def forward( + self, + *, + q: Float[Tensor, "batch query qk_feature"], + k: Float[Tensor, "batch key qk_feature"], + v: Float[Tensor, "batch key v_feature"], + coords_q: Float[Tensor, "batch query coord"], + coords_k: Float[Tensor, "batch key coord"], + attn_mask: Bool[Tensor, "batch query key"] | None, + alibi_mask: Bool[Tensor, "batch query key"] | None, + ) -> Float[Tensor, "batch query v_feature"]: + """ + Args: + alibi_mask: + Which query-key pairs to mask from ALiBi (i.e. don't apply ALiBi to). + """ + weight_logits = torch.einsum("bqf,bkf->bqk", q, k) * (k.size(-1) ** -0.5) + distances = torch.linalg.norm( + coords_q.unsqueeze(2) - coords_k.unsqueeze(1), dim=-1 + ) + scaled_distances = self.scale_distance(distances) * self.bias_scale + + if alibi_mask is not None: + scaled_distances = scaled_distances.where(~alibi_mask, 0.0) + + weights = torch.softmax(weight_logits, dim=-1) + + if attn_mask is not None: + weights = (weights - scaled_distances).where(~attn_mask, 0.0) + else: + weights = weights - scaled_distances + + attention = torch.einsum("bqk,bkf->bqf", weights, v) + + return attention + + +class MultiHeadALiBi(nn.Module): + """Attention with Linear Biases + + Based on + > PRESS, Ofir; SMITH, Noah A.; LEWIS, Mike. + > Train short, test long: Attention with linear biases enables input length extrapolation. + > arXiv preprint arXiv:2108.12409, 2021. + + Since the distances between in WSIs may be quite large, + we scale the distances by the mean distance seen during training. + """ + + def __init__( + self, + *, + embed_dim: int, + num_heads: int, + ) -> None: + super().__init__() + + if embed_dim % num_heads != 0: + raise ValueError(f"{embed_dim=} has to be divisible by {num_heads=}") + + self.query_encoders = nn.ModuleList( + [ + nn.Linear(in_features=embed_dim, out_features=embed_dim // num_heads) + for _ in range(num_heads) + ] + ) + self.key_encoders = nn.ModuleList( + [ + nn.Linear(in_features=embed_dim, out_features=embed_dim // num_heads) + for _ in range(num_heads) + ] + ) + self.value_encoders = nn.ModuleList( + [ + nn.Linear(in_features=embed_dim, out_features=embed_dim // num_heads) + for _ in range(num_heads) + ] + ) + + self.attentions = nn.ModuleList([_ALiBi() for _ in range(num_heads)]) + + self.fc = nn.Linear(in_features=embed_dim, out_features=embed_dim) + + def forward( + self, + *, + q: Float[Tensor, "batch query mh_qk_feature"], + k: Float[Tensor, "batch key mh_qk_feature"], + v: Float[Tensor, "batch key hm_v_feature"], + coords_q: Float[Tensor, "batch query coord"], + coords_k: Float[Tensor, "batch key coord"], + attn_mask: Bool[Tensor, "batch query key"] | None, + alibi_mask: Bool[Tensor, "batch query key"] | None, + ) -> Float[Tensor, "batch query mh_v_feature"]: + stacked_attentions = torch.stack( + [ + att( + q=q_enc(q), + k=k_enc(k), + v=v_enc(v), + coords_q=coords_q, + coords_k=coords_k, + attn_mask=attn_mask, + alibi_mask=alibi_mask, + ) + for q_enc, k_enc, v_enc, att in zip( + self.query_encoders, + self.key_encoders, + self.value_encoders, + self.attentions, + strict=True, + ) + ] + ) + return self.fc(stacked_attentions.permute(1, 2, 0, 3).flatten(-2, -1)) diff --git a/application/jobs/stamp/app/custom/modeling/config.py b/application/jobs/stamp/app/custom/modeling/config.py new file mode 100644 index 00000000..34917b27 --- /dev/null +++ b/application/jobs/stamp/app/custom/modeling/config.py @@ -0,0 +1,100 @@ +import os +from collections.abc import Sequence +from pathlib import Path + +import torch +from pydantic import BaseModel, ConfigDict, Field + +from .registry import ModelName +from .types import Category, PandasLabel + + +class TrainConfig(BaseModel): + model_config = ConfigDict(extra="forbid") + + output_dir: Path = Field(description="The directory to save the results to") + + clini_table: Path = Field(description="Excel or CSV to read clinical data from") + slide_table: Path | None = Field( + default=None, description="Excel or CSV to read patient-slide associations from" + ) + feature_dir: Path = Field(description="Directory containing feature files") + + ground_truth_label: PandasLabel = Field( + description="Name of categorical column in clinical table to train on" + ) + categories: Sequence[Category] | None = None + + patient_label: PandasLabel = "PATIENT" + filename_label: PandasLabel = "FILENAME" + + params_path: Path | None = Field( + default=None, + description="Optional: Path to a YAML file with advanced training parameters.", + ) + + # Experimental features + use_vary_precision_transform: bool = False + + +class CrossvalConfig(TrainConfig): + n_splits: int = Field(5, ge=2) + + +class DeploymentConfig(BaseModel): + model_config = ConfigDict(extra="forbid") + + output_dir: Path + + checkpoint_paths: list[Path] + clini_table: Path | None = None + slide_table: Path + feature_dir: Path + + ground_truth_label: PandasLabel | None = None + patient_label: PandasLabel = "PATIENT" + filename_label: PandasLabel = "FILENAME" + + num_workers: int = min(os.cpu_count() or 1, 16) + accelerator: str = "gpu" if torch.cuda.is_available() else "cpu" + + +class VitModelParams(BaseModel): + model_config = ConfigDict(extra="forbid") + dim_model: int = 512 + dim_feedforward: int = 512 + n_heads: int = 8 + n_layers: int = 2 + dropout: float = 0.0 + # Experimental feature: Use ALiBi positional embedding + use_alibi: bool = False + + +class MlpModelParams(BaseModel): + model_config = ConfigDict(extra="forbid") + dim_hidden: int = 512 + num_layers: int = 2 + dropout: float = 0.25 + + +class ModelParams(BaseModel): + model_config = ConfigDict(extra="forbid") + vit: VitModelParams + mlp: MlpModelParams + + +class AdvancedConfig(BaseModel): + model_config = ConfigDict(extra="forbid") + bag_size: int = 512 + num_workers: int = min(os.cpu_count() or 1, 16) + batch_size: int = 64 + max_epochs: int = 32 + patience: int = 16 + accelerator: str = "gpu" if torch.cuda.is_available() else "cpu" + max_lr: float = 1e-4 + div_factor: float = 25.0 + model_name: ModelName | None = Field( + default=None, + description='Optional: "vit" or "mlp". Defaults based on feature type.', + ) + model_params: ModelParams diff --git a/application/jobs/stamp/app/custom/modeling/lightning_model.py b/application/jobs/stamp/app/custom/modeling/lightning_model.py new file mode 100644 index 00000000..c45f9d6e --- /dev/null +++ b/application/jobs/stamp/app/custom/modeling/lightning_model.py @@ -0,0 +1,235 @@ +"""Lightning wrapper around the model""" + +from collections.abc import Iterable, Sequence +from typing import TypeAlias + +import lightning +import numpy as np +import torch +from jaxtyping import Bool, Float +from packaging.version import Version +from torch import Tensor, nn, optim +from torchmetrics.classification import MulticlassAUROC + +from .types import ( + Bags, + BagSizes, + Category, + CoordinatesBatch, + EncodedTargets, + PandasLabel, + PatientId, +) +from .vision_transformer import VisionTransformer + +Loss: TypeAlias = Float[Tensor, ""] + + +class LitVisionTransformer(lightning.LightningModule): + """ + PyTorch Lightning wrapper for the Vision Transformer (ViT) model used in weakly supervised + learning settings, such as Multiple Instance Learning (MIL) for whole-slide images or patch-based data. + + This class encapsulates training, validation, testing, and prediction logic, along with: + - Masking logic that ensures only valid tiles (patches) participate in attention during training (deactivated) + - AUROC metric tracking during validation for multiclass classification. + - Compatibility checks based on the `stamp` framework version. + - Integration of class imbalance handling through weighted cross-entropy loss. + + The attention mask is currently deactivated to reduce memory usage. + + Args: + categories: List of class labels. + category_weights: Class weights for cross-entropy loss to handle imbalance. + dim_input: Input feature dimensionality per tile. + dim_model: Latent dimensionality used inside the transformer. + dim_feedforward: Dimensionality of the transformer MLP block. + n_heads: Number of self-attention heads. + n_layers: Number of transformer layers. + dropout: Dropout rate used throughout the model. + total_steps: Number of steps done in the LR Scheduler cycle. + max_lr: max learning rate. + div_factor: Determines the initial learning rate via initial_lr = max_lr/div_factor + use_alibi: Whether to use ALiBi-style positional bias in attention (optional). + ground_truth_label: Column name for accessing ground-truth labels from metadata. + train_patients: List of patient IDs used for training. + valid_patients: List of patient IDs used for validation. + stamp_version: Version of the `stamp` framework used during training. + **metadata: Additional metadata to store with the model. + """ + + supported_features = ["tile"] + + def __init__( + self, + *, + categories: Sequence[Category], + category_weights: Float[Tensor, "category_weight"], # noqa: F821 + dim_input: int, + dim_model: int, + dim_feedforward: int, + n_heads: int, + n_layers: int, + dropout: float, + # Learning Rate Scheduler params, not used in inference + total_steps: int, + max_lr: float, + div_factor: float, + # Experimental features + use_alibi: bool, + # Metadata used by other parts of stamp, but not by the model itself + ground_truth_label: PandasLabel, + train_patients: Iterable[PatientId], + valid_patients: Iterable[PatientId], + # Other metadata + **metadata, + ) -> None: + super().__init__() + + if len(categories) != len(category_weights): + raise ValueError( + "the number of category weights has to match the number of categories!" + ) + + self.vision_transformer = VisionTransformer( + dim_output=len(categories), + dim_input=dim_input, + dim_model=dim_model, + n_layers=n_layers, + n_heads=n_heads, + dim_feedforward=dim_feedforward, + dropout=dropout, + use_alibi=use_alibi, + ) + self.class_weights = category_weights + self.valid_auroc = MulticlassAUROC(len(categories)) + self.total_steps = total_steps + self.max_lr = max_lr + self.div_factor = div_factor + + # Used during deployment + self.ground_truth_label = ground_truth_label + self.categories = np.array(categories) + self.train_patients = train_patients + self.valid_patients = valid_patients + + _ = metadata # unused, but saved in model + + self.save_hyperparameters() + + def forward( + self, + bags: Bags, + ) -> Float[Tensor, "batch logit"]: + return self.vision_transformer(bags) + + def _step( + self, + *, + batch: tuple[Bags, CoordinatesBatch, BagSizes, EncodedTargets], + step_name: str, + use_mask: bool, + ) -> Loss: + bags, coords, bag_sizes, targets = batch + + mask = _mask_from_bags(bags=bags, bag_sizes=bag_sizes) if use_mask else None + + logits = self.vision_transformer(bags, coords=coords, mask=mask) + + loss = nn.functional.cross_entropy( + logits, + targets.type_as(logits), + weight=self.class_weights.type_as(logits), + ) + + self.log( + f"{step_name}_loss", + loss, + on_step=False, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + if step_name == "validation": + # TODO this is a bit ugly, we'd like to have `_step` without special cases + self.valid_auroc.update(logits, targets.long().argmax(dim=-1)) + self.log( + f"{step_name}_auroc", + self.valid_auroc, + on_step=False, + on_epoch=True, + sync_dist=True, + ) + + return loss + + def training_step( + self, + batch: tuple[Bags, CoordinatesBatch, BagSizes, EncodedTargets], + batch_idx: int, + ) -> Loss: + return self._step(batch=batch, step_name="training", use_mask=False) + + def validation_step( + self, + batch: tuple[Bags, CoordinatesBatch, BagSizes, EncodedTargets], + batch_idx: int, + ) -> Loss: + return self._step(batch=batch, step_name="validation", use_mask=False) + + def test_step( + self, + batch: tuple[Bags, CoordinatesBatch, BagSizes, EncodedTargets], + batch_idx: int, + ) -> Loss: + return self._step(batch=batch, step_name="test", use_mask=False) + + def predict_step( + self, + batch: tuple[Bags, CoordinatesBatch, BagSizes, EncodedTargets], + batch_idx: int, + ) -> Float[Tensor, "batch logit"]: + bags, coords, bag_sizes, _ = batch + # adding a mask here will *drastically* and *unbearably* increase memory usage + return self.vision_transformer(bags, coords=coords, mask=None) + + def configure_optimizers( + self, + ) -> tuple[list[optim.Optimizer], list[optim.lr_scheduler.LRScheduler]]: + optimizer = optim.AdamW( + self.parameters(), lr=1e-3 + ) # this lr value should be ignored with the scheduler + + scheduler = optim.lr_scheduler.OneCycleLR( + optimizer=optimizer, + total_steps=self.total_steps, + max_lr=self.max_lr, + div_factor=self.div_factor, + ) + return [optimizer], [scheduler] + + def on_train_batch_end(self, outputs, batch, batch_idx): + # Log learning rate at the end of each training batch + current_lr = self.trainer.optimizers[0].param_groups[0]["lr"] + self.log( + "learning_rate", + current_lr, + on_step=False, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + +def _mask_from_bags( + *, + bags: Bags, + bag_sizes: BagSizes, +) -> Bool[Tensor, "batch tile"]: + max_possible_bag_size = bags.size(1) + mask = torch.arange(max_possible_bag_size).type_as(bag_sizes).unsqueeze(0).repeat( + len(bags), 1 + ) >= bag_sizes.unsqueeze(1) + + return mask diff --git a/application/jobs/stamp/app/custom/modeling/mlp_classifier.py b/application/jobs/stamp/app/custom/modeling/mlp_classifier.py new file mode 100644 index 00000000..0bf13517 --- /dev/null +++ b/application/jobs/stamp/app/custom/modeling/mlp_classifier.py @@ -0,0 +1,154 @@ +from collections.abc import Iterable, Sequence + +import lightning +import numpy as np +import torch +from packaging.version import Version +from torch import Tensor, nn, optim +from torchmetrics.classification import MulticlassAUROC + +from .types import Category, PandasLabel, PatientId + + +class MLPClassifier(nn.Module): + """ + Simple MLP for classification from a single feature vector. + """ + + def __init__( + self, + dim_input: int, + dim_hidden: int, + dim_output: int, + num_layers: int, + dropout: float, + ): + super().__init__() + layers = [] + in_dim = dim_input + for i in range(num_layers - 1): + layers.append(nn.Linear(in_dim, dim_hidden)) + layers.append(nn.ReLU()) + layers.append(nn.Dropout(dropout)) + in_dim = dim_hidden + layers.append(nn.Linear(in_dim, dim_output)) + self.mlp = nn.Sequential(*layers) + + def forward(self, x: Tensor) -> Tensor: + return self.mlp(x) + + +class LitMLPClassifier(lightning.LightningModule): + """ + PyTorch Lightning wrapper for MLPClassifier. + """ + + supported_features = ["patient"] + + def __init__( + self, + *, + categories: Sequence[Category], + category_weights: torch.Tensor, + dim_input: int, + dim_hidden: int, + num_layers: int, + dropout: float, + ground_truth_label: PandasLabel, + train_patients: Iterable[PatientId], + valid_patients: Iterable[PatientId], + # Learning Rate Scheduler params, used only in training + total_steps: int, + max_lr: float, + div_factor: float, + **metadata, + ): + super().__init__() + self.save_hyperparameters() + self.model = MLPClassifier( + dim_input=dim_input, + dim_hidden=dim_hidden, + dim_output=len(categories), + num_layers=num_layers, + dropout=dropout, + ) + self.class_weights = category_weights + self.valid_auroc = MulticlassAUROC(len(categories)) + self.ground_truth_label = ground_truth_label + self.categories = np.array(categories) + self.train_patients = train_patients + self.valid_patients = valid_patients + self.total_steps = total_steps + self.max_lr = max_lr + self.div_factor = div_factor + + + def forward(self, x: Tensor) -> Tensor: + return self.model(x) + + def _step(self, batch, step_name: str): + feats, targets = batch + logits = self.model(feats) + loss = nn.functional.cross_entropy( + logits, + targets.type_as(logits), + weight=self.class_weights.type_as(logits), + ) + self.log( + f"{step_name}_loss", + loss, + on_step=False, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + if step_name == "validation": + self.valid_auroc.update(logits, targets.long().argmax(dim=-1)) + self.log( + f"{step_name}_auroc", + self.valid_auroc, + on_step=False, + on_epoch=True, + sync_dist=True, + ) + return loss + + def training_step(self, batch, batch_idx): + return self._step(batch, "training") + + def validation_step(self, batch, batch_idx): + return self._step(batch, "validation") + + def test_step(self, batch, batch_idx): + return self._step(batch, "test") + + def predict_step(self, batch, batch_idx): + feats, _ = batch + return self.model(feats) + + def configure_optimizers( + self, + ) -> tuple[list[optim.Optimizer], list[optim.lr_scheduler.LRScheduler]]: + optimizer = optim.AdamW( + self.parameters(), lr=1e-3 + ) # this lr value should be ignored with the scheduler + + scheduler = optim.lr_scheduler.OneCycleLR( + optimizer=optimizer, + total_steps=self.total_steps, + max_lr=self.max_lr, + div_factor=25.0, + ) + return [optimizer], [scheduler] + + def on_train_batch_end(self, outputs, batch, batch_idx): + # Log learning rate at the end of each training batch + current_lr = self.trainer.optimizers[0].param_groups[0]["lr"] + self.log( + "learning_rate", + current_lr, + on_step=False, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) diff --git a/application/jobs/stamp/app/custom/modeling/module.py b/application/jobs/stamp/app/custom/modeling/module.py new file mode 100644 index 00000000..30fa7551 --- /dev/null +++ b/application/jobs/stamp/app/custom/modeling/module.py @@ -0,0 +1,1654 @@ +# Copyright The Lightning AI team. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The LightningModule - an nn.Module with many additional features.""" + +import logging +import numbers +import weakref +from collections.abc import Generator, Mapping, Sequence +from contextlib import contextmanager +from io import BytesIO +from pathlib import Path +from typing import ( + IO, + TYPE_CHECKING, + Any, + Callable, + Literal, + Optional, + Union, + cast, + overload, +) + +import lightning.fabric as lf +import lightning.pytorch as pl +import torch +from lightning.fabric.loggers import Logger as FabricLogger +from lightning.fabric.utilities.apply_func import convert_to_tensors +from lightning.fabric.utilities.cloud_io import get_filesystem +from lightning.fabric.utilities.device_dtype_mixin import _DeviceDtypeModuleMixin +from lightning.fabric.utilities.types import _MAP_LOCATION_TYPE, _PATH +from lightning.fabric.wrappers import _FabricOptimizer +from lightning.pytorch.callbacks.callback import Callback +from lightning.pytorch.core.hooks import CheckpointHooks, DataHooks, ModelHooks +from lightning.pytorch.core.mixins import HyperparametersMixin +from lightning.pytorch.core.optimizer import LightningOptimizer +from lightning.pytorch.core.saving import _load_from_checkpoint +from lightning.pytorch.loggers import Logger +from lightning.pytorch.trainer import call +from lightning.pytorch.trainer.connectors.logger_connector.fx_validator import _FxValidator +from lightning.pytorch.trainer.connectors.logger_connector.result import _get_default_dtype +from lightning.pytorch.utilities import GradClipAlgorithmType +from lightning.pytorch.utilities.exceptions import MisconfigurationException +from lightning.pytorch.utilities.imports import _TORCHMETRICS_GREATER_EQUAL_0_9_1 +from lightning.pytorch.utilities.model_helpers import _restricted_classmethod +from lightning.pytorch.utilities.rank_zero import WarningCache, rank_zero_warn +from lightning.pytorch.utilities.signature_utils import is_param_in_hook_signature +from lightning.pytorch.utilities.types import ( + _METRIC, + STEP_OUTPUT, + LRSchedulerPLType, + LRSchedulerTypeUnion, + OptimizerLRScheduler, +) +from lightning_utilities.core.apply_func import apply_to_collection +from lightning_utilities.core.imports import RequirementCache +from torch import ScriptModule, Tensor +from torch.nn import Module +from torch.optim.optimizer import Optimizer +from torchmetrics import Metric, MetricCollection +from typing_extensions import Self, override + +if TYPE_CHECKING: + from torch.distributed.device_mesh import DeviceMesh + +_ONNX_AVAILABLE = RequirementCache("onnx") + +warning_cache = WarningCache() +log = logging.getLogger(__name__) + +MODULE_OPTIMIZERS = Union[ + Optimizer, LightningOptimizer, _FabricOptimizer, list[Optimizer], list[LightningOptimizer], list[_FabricOptimizer] +] + + +class LightningModule( + _DeviceDtypeModuleMixin, + HyperparametersMixin, + ModelHooks, + DataHooks, + CheckpointHooks, + Module, +): + # Below is for property support of JIT + # since none of these are important when using JIT, we are going to ignore them. + __jit_unused_properties__: list[str] = ( + [ + "example_input_array", + "on_gpu", + "current_epoch", + "global_step", + "global_rank", + "local_rank", + "logger", + "loggers", + "automatic_optimization", + "trainer", + "fabric", + "strict_loading", + "device_mesh", + ] + + _DeviceDtypeModuleMixin.__jit_unused_properties__ + + HyperparametersMixin.__jit_unused_properties__ + ) + _jit_is_scripting = False + + CHECKPOINT_HYPER_PARAMS_KEY = "hyper_parameters" + CHECKPOINT_HYPER_PARAMS_NAME = "hparams_name" + CHECKPOINT_HYPER_PARAMS_TYPE = "hparams_type" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + # pointer to the trainer object + self._trainer: Optional[pl.Trainer] = None + + # attributes that can be set by user + self._example_input_array: Optional[Union[Tensor, tuple, dict]] = None + self._automatic_optimization: bool = True + self._strict_loading: Optional[bool] = None + + # attributes used internally + self._current_fx_name: Optional[str] = None + self._param_requires_grad_state: dict[str, bool] = {} + self._metric_attributes: Optional[dict[int, str]] = None + self._compiler_ctx: Optional[dict[str, Any]] = None + + # attributes only used when using fabric + self._fabric: Optional[lf.Fabric] = None + self._fabric_optimizers: list[_FabricOptimizer] = [] + + # access to device mesh in `conigure_model()` hook + self._device_mesh: Optional[DeviceMesh] = None + + @overload + def optimizers( + self, use_pl_optimizer: Literal[True] = True + ) -> Union[LightningOptimizer, list[LightningOptimizer]]: + ... + + @overload + def optimizers(self, use_pl_optimizer: Literal[False]) -> Union[Optimizer, list[Optimizer]]: + ... + + @overload + def optimizers(self, use_pl_optimizer: bool) -> MODULE_OPTIMIZERS: + ... + + def optimizers(self, use_pl_optimizer: bool = True) -> MODULE_OPTIMIZERS: + """Returns the optimizer(s) that are being used during training. Useful for manual optimization. + + Args: + use_pl_optimizer: If ``True``, will wrap the optimizer(s) in a + :class:`~lightning.pytorch.core.optimizer.LightningOptimizer` for automatic handling of precision, + profiling, and counting of step calls for proper logging and checkpointing. It specifically wraps the + ``step`` method and custom optimizers that don't have this method are not supported. + + Returns: + A single optimizer, or a list of optimizers in case multiple ones are present. + + """ + if self._fabric: + opts: MODULE_OPTIMIZERS = self._fabric_optimizers + elif use_pl_optimizer: + opts = self.trainer.strategy._lightning_optimizers + else: + opts = self.trainer.optimizers + + # single optimizer + if ( + isinstance(opts, list) + and len(opts) == 1 + and isinstance(opts[0], (Optimizer, LightningOptimizer, _FabricOptimizer)) + ): + return opts[0] + # multiple opts + return opts + + def lr_schedulers(self) -> Union[None, list[LRSchedulerPLType], LRSchedulerPLType]: + """Returns the learning rate scheduler(s) that are being used during training. Useful for manual optimization. + + Returns: + A single scheduler, or a list of schedulers in case multiple ones are present, or ``None`` if no + schedulers were returned in :meth:`~lightning.pytorch.core.LightningModule.configure_optimizers`. + + """ + if not self.trainer.lr_scheduler_configs: + return None + + # ignore other keys "interval", "frequency", etc. + lr_schedulers: list[LRSchedulerPLType] = [config.scheduler for config in self.trainer.lr_scheduler_configs] + + # single scheduler + if len(lr_schedulers) == 1: + return lr_schedulers[0] + + # multiple schedulers + return lr_schedulers + + @property + def trainer(self) -> "pl.Trainer": + if self._fabric is not None: + return _TrainerFabricShim(fabric=self._fabric) # type: ignore[return-value] + if not self._jit_is_scripting and self._trainer is None: + raise RuntimeError(f"{self.__class__.__qualname__} is not attached to a `Trainer`.") + return self._trainer # type: ignore[return-value] + + @trainer.setter + def trainer(self, trainer: Optional["pl.Trainer"]) -> None: + for v in self.children(): + if isinstance(v, LightningModule): + v.trainer = trainer + self._trainer = trainer + + @property + def fabric(self) -> Optional["lf.Fabric"]: + return self._fabric + + @fabric.setter + def fabric(self, fabric: Optional["lf.Fabric"]) -> None: + for v in self.children(): + if isinstance(v, LightningModule): + v.fabric = fabric + if fabric is not None and not isinstance(fabric, weakref.ProxyTypes): + fabric = weakref.proxy(fabric) + self._fabric = fabric + + @property + def example_input_array(self) -> Optional[Union[Tensor, tuple, dict]]: + """The example input array is a specification of what the module can consume in the :meth:`forward` method. The + return type is interpreted as follows: + + - Single tensor: It is assumed the model takes a single argument, i.e., + ``model.forward(model.example_input_array)`` + - Tuple: The input array should be interpreted as a sequence of positional arguments, i.e., + ``model.forward(*model.example_input_array)`` + - Dict: The input array represents named keyword arguments, i.e., + ``model.forward(**model.example_input_array)`` + + """ + return self._example_input_array + + @example_input_array.setter + def example_input_array(self, example: Optional[Union[Tensor, tuple, dict]]) -> None: + self._example_input_array = example + + @property + def current_epoch(self) -> int: + """The current epoch in the ``Trainer``, or 0 if not attached.""" + return self.trainer.current_epoch if self._trainer else 0 + + @property + def global_step(self) -> int: + """Total training batches seen across all epochs. + + If no Trainer is attached, this property is 0. + + """ + return self.trainer.global_step if self._trainer else 0 + + @property + def global_rank(self) -> int: + """The index of the current process across all nodes and devices.""" + return self.trainer.global_rank if self._trainer else 0 + + @property + def local_rank(self) -> int: + """The index of the current process within a single node.""" + return self.trainer.local_rank if self._trainer else 0 + + @property + def on_gpu(self) -> bool: + """Returns ``True`` if this model is currently located on a GPU. + + Useful to set flags around the LightningModule for different CPU vs GPU behavior. + + """ + return self.device.type == "cuda" + + @property + def automatic_optimization(self) -> bool: + """If set to ``False`` you are responsible for calling ``.backward()``, ``.step()``, ``.zero_grad()``.""" + return self._automatic_optimization + + @automatic_optimization.setter + def automatic_optimization(self, automatic_optimization: bool) -> None: + self._automatic_optimization = automatic_optimization + + @property + def strict_loading(self) -> bool: + """Determines how Lightning loads this model using `.load_state_dict(..., strict=model.strict_loading)`.""" + # We use None as the default internally to determine whether the user has set a value + return self._strict_loading in (None, True) + + @strict_loading.setter + def strict_loading(self, strict_loading: bool) -> None: + self._strict_loading = strict_loading + + @property + def logger(self) -> Optional[Union[Logger, FabricLogger]]: + """Reference to the logger object in the Trainer.""" + if self._fabric is not None: + return self._fabric.logger + return self._trainer.logger if self._trainer is not None else None + + @property + def loggers(self) -> Union[list[Logger], list[FabricLogger]]: + """Reference to the list of loggers in the Trainer.""" + if self._fabric is not None: + return self._fabric.loggers + if self._trainer is not None: + return self._trainer.loggers + return [] + + @property + def device_mesh(self) -> Optional["DeviceMesh"]: + """Strategies like ``ModelParallelStrategy`` will create a device mesh that can be accessed in the + :meth:`~lightning.pytorch.core.hooks.ModelHooks.configure_model` hook to parallelize the LightningModule.""" + return self._device_mesh + + def _call_batch_hook(self, hook_name: str, *args: Any) -> Any: + trainer = self._trainer + if trainer: + datahook_selector = trainer._data_connector._datahook_selector + assert datahook_selector is not None + obj = datahook_selector.get_instance(hook_name) + if isinstance(obj, self.__class__): + trainer_method = call._call_lightning_module_hook + else: + trainer_method = call._call_lightning_datamodule_hook + + return trainer_method(trainer, hook_name, *args) + hook = getattr(self, hook_name) + return hook(*args) + + def _on_before_batch_transfer(self, batch: Any, dataloader_idx: int = 0) -> Any: + return self._call_batch_hook("on_before_batch_transfer", batch, dataloader_idx) + + def _apply_batch_transfer_handler( + self, batch: Any, device: Optional[torch.device] = None, dataloader_idx: int = 0 + ) -> Any: + device = device or self.device + batch = self._call_batch_hook("transfer_batch_to_device", batch, device, dataloader_idx) + batch = self._call_batch_hook("on_after_batch_transfer", batch, dataloader_idx) + return batch + + def print(self, *args: Any, **kwargs: Any) -> None: + r"""Prints only from process 0. Use this in any distributed mode to log only once. + + Args: + *args: The thing to print. The same as for Python's built-in print function. + **kwargs: The same as for Python's built-in print function. + + Example:: + + def forward(self, x): + self.print(x, 'in forward') + + """ + if self.trainer.is_global_zero: + progress_bar = self.trainer.progress_bar_callback + if progress_bar is not None and progress_bar.is_enabled: + progress_bar.print(*args, **kwargs) + else: + print(*args, **kwargs) + + def log( + self, + name: str, + value: _METRIC, + prog_bar: bool = False, + logger: Optional[bool] = None, + on_step: Optional[bool] = None, + on_epoch: Optional[bool] = None, + reduce_fx: Union[str, Callable] = "mean", + enable_graph: bool = False, + sync_dist: bool = False, + sync_dist_group: Optional[Any] = None, + add_dataloader_idx: bool = True, + batch_size: Optional[int] = None, + metric_attribute: Optional[str] = None, + rank_zero_only: bool = False, + ) -> None: + """Log a key, value pair. + + Example:: + + self.log('train_loss', loss) + + The default behavior per hook is documented here: :ref:`extensions/logging:Automatic Logging`. + + Args: + name: key to log. Must be identical across all processes if using DDP or any other distributed strategy. + value: value to log. Can be a ``float``, ``Tensor``, or a ``Metric``. + prog_bar: if ``True`` logs to the progress bar. + logger: if ``True`` logs to the logger. + on_step: if ``True`` logs at this step. The default value is determined by the hook. + See :ref:`extensions/logging:Automatic Logging` for details. + on_epoch: if ``True`` logs epoch accumulated metrics. The default value is determined by the hook. + See :ref:`extensions/logging:Automatic Logging` for details. + reduce_fx: reduction function over step values for end of epoch. :meth:`torch.mean` by default. + enable_graph: if ``True``, will not auto detach the graph. + sync_dist: if ``True``, reduces the metric across devices. Use with care as this may lead to a significant + communication overhead. + sync_dist_group: the DDP group to sync across. + add_dataloader_idx: if ``True``, appends the index of the current dataloader to + the name (when using multiple dataloaders). If False, user needs to give unique names for + each dataloader to not mix the values. + batch_size: Current batch_size. This will be directly inferred from the loaded batch, + but for some data structures you might need to explicitly provide it. + metric_attribute: To restore the metric state, Lightning requires the reference of the + :class:`torchmetrics.Metric` in your model. This is found automatically if it is a model attribute. + rank_zero_only: Tells Lightning if you are calling ``self.log`` from every process (default) or only from + rank 0. If ``True``, you won't be able to use this metric as a monitor in callbacks + (e.g., early stopping). Warning: Improper use can lead to deadlocks! See + :ref:`Advanced Logging ` for more details. + + """ + if self._fabric is not None: + self._log_dict_through_fabric(dictionary={name: value}, logger=logger) + return + + # check for invalid values + apply_to_collection(value, dict, self.__check_not_nested, name) + apply_to_collection( + value, object, self.__check_allowed, name, value, wrong_dtype=(numbers.Number, Metric, Tensor) + ) + + trainer = self._trainer + if trainer is None: + # not an error to support testing the `*_step` methods without a `Trainer` reference + rank_zero_warn( + "You are trying to `self.log()` but the `self.trainer` reference is not registered on the model yet." + " This is most likely because the model hasn't been passed to the `Trainer`" + ) + return + if trainer.barebones: + rank_zero_warn( + "You are trying to `self.log()` but `Trainer(barebones=True)` is configured." + " Logging can impact raw speed so it is disabled under this setting." + ) + return + results = trainer._results + if results is None: + raise MisconfigurationException( + "You are trying to `self.log()` but the loop's result collection is not registered" + " yet. This is most likely because you are trying to log in a `predict` hook," + " but it doesn't support logging" + ) + if self._current_fx_name is None: + raise MisconfigurationException( + "You are trying to `self.log()` but it is not managed by the `Trainer` control flow" + ) + + on_step, on_epoch = _FxValidator.check_logging_and_get_default_levels( + self._current_fx_name, on_step=on_step, on_epoch=on_epoch + ) + + # make sure user doesn't introduce logic for multi-dataloaders + if "/dataloader_idx_" in name: + raise MisconfigurationException( + f"You called `self.log` with the key `{name}`" + " but it should not contain information about `dataloader_idx`" + ) + + value = apply_to_collection(value, (Tensor, numbers.Number), self.__to_tensor, name) + + if trainer._logger_connector.should_reset_tensors(self._current_fx_name): + # if we started a new epoch (running its first batch) the hook name has changed + # reset any tensors for the new hook name + results.reset(metrics=False, fx=self._current_fx_name) + + if metric_attribute is None and isinstance(value, Metric): + if self._metric_attributes is None: + # compute once + self._metric_attributes = { + id(module): name for name, module in self.named_modules() if isinstance(module, Metric) + } + if not self._metric_attributes: + raise MisconfigurationException( + "Could not find the `LightningModule` attribute for the `torchmetrics.Metric` logged." + " You can fix this by setting an attribute for the metric in your `LightningModule`." + ) + # try to find the passed metric in the LightningModule + metric_attribute = self._metric_attributes.get(id(value), None) + if metric_attribute is None: + raise MisconfigurationException( + "Could not find the `LightningModule` attribute for the `torchmetrics.Metric` logged." + f" You can fix this by calling `self.log({name}, ..., metric_attribute=name)` where `name` is one" + f" of {list(self._metric_attributes.values())}" + ) + + if ( + trainer.training + and is_param_in_hook_signature(self.training_step, "dataloader_iter", explicit=True) + and batch_size is None + ): + raise MisconfigurationException( + "With `def training_step(self, dataloader_iter)`, `self.log(..., batch_size=...)` should be provided." + ) + + if logger and trainer.logger is None: + rank_zero_warn( + f"You called `self.log({name!r}, ..., logger=True)` but have no logger configured. You can enable one" + " by doing `Trainer(logger=ALogger(...))`" + ) + if logger is None: + # we could set false here if there's no configured logger, however, we still need to compute the "logged" + # metrics anyway because that's what the evaluation loops use as return value + logger = True + + results.log( + self._current_fx_name, + name, + value, + prog_bar=prog_bar, + logger=logger, + on_step=on_step, + on_epoch=on_epoch, + reduce_fx=reduce_fx, + enable_graph=enable_graph, + add_dataloader_idx=add_dataloader_idx, + batch_size=batch_size, + sync_dist=sync_dist and trainer._accelerator_connector.is_distributed, + sync_dist_fn=trainer.strategy.reduce, + sync_dist_group=sync_dist_group, + metric_attribute=metric_attribute, + rank_zero_only=rank_zero_only, + ) + + trainer._logger_connector._current_fx = self._current_fx_name + + def log_dict( + self, + dictionary: Union[Mapping[str, _METRIC], MetricCollection], + prog_bar: bool = False, + logger: Optional[bool] = None, + on_step: Optional[bool] = None, + on_epoch: Optional[bool] = None, + reduce_fx: Union[str, Callable] = "mean", + enable_graph: bool = False, + sync_dist: bool = False, + sync_dist_group: Optional[Any] = None, + add_dataloader_idx: bool = True, + batch_size: Optional[int] = None, + rank_zero_only: bool = False, + ) -> None: + """Log a dictionary of values at once. + + Example:: + + values = {'loss': loss, 'acc': acc, ..., 'metric_n': metric_n} + self.log_dict(values) + + Args: + dictionary: key value pairs. + Keys must be identical across all processes if using DDP or any other distributed strategy. + The values can be a ``float``, ``Tensor``, ``Metric``, or ``MetricCollection``. + prog_bar: if ``True`` logs to the progress base. + logger: if ``True`` logs to the logger. + on_step: if ``True`` logs at this step. + ``None`` auto-logs for training_step but not validation/test_step. + The default value is determined by the hook. + See :ref:`extensions/logging:Automatic Logging` for details. + on_epoch: if ``True`` logs epoch accumulated metrics. + ``None`` auto-logs for val/test step but not ``training_step``. + The default value is determined by the hook. + See :ref:`extensions/logging:Automatic Logging` for details. + reduce_fx: reduction function over step values for end of epoch. :meth:`torch.mean` by default. + enable_graph: if ``True``, will not auto-detach the graph + sync_dist: if ``True``, reduces the metric across GPUs/TPUs. Use with care as this may lead to a significant + communication overhead. + sync_dist_group: the ddp group to sync across. + add_dataloader_idx: if ``True``, appends the index of the current dataloader to + the name (when using multiple). If ``False``, user needs to give unique names for + each dataloader to not mix values. + batch_size: Current batch size. This will be directly inferred from the loaded batch, + but some data structures might need to explicitly provide it. + rank_zero_only: Tells Lightning if you are calling ``self.log`` from every process (default) or only from + rank 0. If ``True``, you won't be able to use this metric as a monitor in callbacks + (e.g., early stopping). Warning: Improper use can lead to deadlocks! See + :ref:`Advanced Logging ` for more details. + + """ + if self._fabric is not None: + return self._log_dict_through_fabric(dictionary=dictionary, logger=logger) + + kwargs: dict[str, bool] = {} + + if isinstance(dictionary, MetricCollection): + kwargs["keep_base"] = False + if _TORCHMETRICS_GREATER_EQUAL_0_9_1 and dictionary._enable_compute_groups: + kwargs["copy_state"] = False + + for k, v in dictionary.items(**kwargs): + self.log( + name=k, + value=v, + prog_bar=prog_bar, + logger=logger, + on_step=on_step, + on_epoch=on_epoch, + reduce_fx=reduce_fx, + enable_graph=enable_graph, + sync_dist=sync_dist, + sync_dist_group=sync_dist_group, + add_dataloader_idx=add_dataloader_idx, + batch_size=batch_size, + rank_zero_only=rank_zero_only, + ) + return None + + def _log_dict_through_fabric( + self, dictionary: Union[Mapping[str, _METRIC], MetricCollection], logger: Optional[bool] = None + ) -> None: + if logger is False: + # Passing `logger=False` with Fabric does not make much sense because there is no other destination to + # log to, but we support it in case the original code was written for Trainer use + return + + if any(isinstance(v, dict) for v in dictionary.values()): + raise ValueError(f"`self.log_dict({dictionary})` was called, but nested dictionaries cannot be logged") + for name, value in dictionary.items(): + apply_to_collection(value, object, self.__check_allowed, name, value, wrong_dtype=(numbers.Number, Tensor)) + + assert self._fabric is not None + self._fabric.log_dict(metrics=dictionary) # type: ignore[arg-type] + + @staticmethod + def __check_not_nested(value: dict, name: str) -> None: + # self-imposed restriction. for simplicity + if any(isinstance(v, dict) for v in value.values()): + raise ValueError(f"`self.log({name}, {value})` was called, but nested dictionaries cannot be logged") + + @staticmethod + def __check_allowed(v: Any, name: str, value: Any) -> None: + raise ValueError(f"`self.log({name}, {value})` was called, but `{type(v).__name__}` values cannot be logged") + + def __to_tensor(self, value: Union[Tensor, numbers.Number], name: str) -> Tensor: + value = ( + value.clone().detach() + if isinstance(value, Tensor) + else torch.tensor(value, device=self.device, dtype=_get_default_dtype()) + ) + if not torch.numel(value) == 1: + raise ValueError( + f"`self.log({name}, {value})` was called, but the tensor must have a single element." + f" You can try doing `self.log({name}, {value}.mean())`" + ) + value = value.squeeze() + return value + + def all_gather( + self, data: Union[Tensor, dict, list, tuple], group: Optional[Any] = None, sync_grads: bool = False + ) -> Union[Tensor, dict, list, tuple]: + r"""Gather tensors or collections of tensors from multiple processes. + + This method needs to be called on all processes and the tensors need to have the same shape across all + processes, otherwise your program will stall forever. + + Args: + data: int, float, tensor of shape (batch, ...), or a (possibly nested) collection thereof. + group: the process group to gather results from. Defaults to all processes (world) + sync_grads: flag that allows users to synchronize gradients for the all_gather operation + + Return: + A tensor of shape (world_size, batch, ...), or if the input was a collection + the output will also be a collection with tensors of this shape. For the special case where + world_size is 1, no additional dimension is added to the tensor(s). + + """ + group = group if group is not None else torch.distributed.group.WORLD + all_gather = self.trainer.strategy.all_gather + data = convert_to_tensors(data, device=self.device) + return apply_to_collection(data, Tensor, all_gather, group=group, sync_grads=sync_grads) + + @override + def forward(self, *args: Any, **kwargs: Any) -> Any: + r"""Same as :meth:`torch.nn.Module.forward`. + + Args: + *args: Whatever you decide to pass into the forward method. + **kwargs: Keyword arguments are also possible. + + Return: + Your model's output + + """ + return super().forward(*args, **kwargs) + + def training_step(self, *args: Any, **kwargs: Any) -> STEP_OUTPUT: + r"""Here you compute and return the training loss and some additional metrics for e.g. the progress bar or + logger. + + Args: + batch: The output of your data iterable, normally a :class:`~torch.utils.data.DataLoader`. + batch_idx: The index of this batch. + dataloader_idx: The index of the dataloader that produced this batch. + (only if multiple dataloaders used) + + Return: + - :class:`~torch.Tensor` - The loss tensor + - ``dict`` - A dictionary which can include any keys, but must include the key ``'loss'`` in the case of + automatic optimization. + - ``None`` - In automatic optimization, this will skip to the next batch (but is not supported for + multi-GPU, TPU, or DeepSpeed). For manual optimization, this has no special meaning, as returning + the loss is not required. + + In this step you'd normally do the forward pass and calculate the loss for a batch. + You can also do fancier things like multiple forward passes or something model specific. + + Example:: + + def training_step(self, batch, batch_idx): + x, y, z = batch + out = self.encoder(x) + loss = self.loss(out, x) + return loss + + To use multiple optimizers, you can switch to 'manual optimization' and control their stepping: + + .. code-block:: python + + def __init__(self): + super().__init__() + self.automatic_optimization = False + + + # Multiple optimizers (e.g.: GANs) + def training_step(self, batch, batch_idx): + opt1, opt2 = self.optimizers() + + # do training_step with encoder + ... + opt1.step() + # do training_step with decoder + ... + opt2.step() + + Note: + When ``accumulate_grad_batches`` > 1, the loss returned here will be automatically + normalized by ``accumulate_grad_batches`` internally. + + """ + rank_zero_warn("`training_step` must be implemented to be used with the Lightning Trainer") + + def validation_step(self, *args: Any, **kwargs: Any) -> STEP_OUTPUT: + r"""Operates on a single batch of data from the validation set. In this step you'd might generate examples or + calculate anything of interest like accuracy. + + Args: + batch: The output of your data iterable, normally a :class:`~torch.utils.data.DataLoader`. + batch_idx: The index of this batch. + dataloader_idx: The index of the dataloader that produced this batch. + (only if multiple dataloaders used) + + Return: + - :class:`~torch.Tensor` - The loss tensor + - ``dict`` - A dictionary. Can include any keys, but must include the key ``'loss'``. + - ``None`` - Skip to the next batch. + + .. code-block:: python + + # if you have one val dataloader: + def validation_step(self, batch, batch_idx): ... + + + # if you have multiple val dataloaders: + def validation_step(self, batch, batch_idx, dataloader_idx=0): ... + + Examples:: + + # CASE 1: A single validation dataset + def validation_step(self, batch, batch_idx): + x, y = batch + + # implement your own + out = self(x) + loss = self.loss(out, y) + + # log 6 example images + # or generated text... or whatever + sample_imgs = x[:6] + grid = torchvision.utils.make_grid(sample_imgs) + self.logger.experiment.add_image('example_images', grid, 0) + + # calculate acc + labels_hat = torch.argmax(out, dim=1) + val_acc = torch.sum(y == labels_hat).item() / (len(y) * 1.0) + + # log the outputs! + self.log_dict({'val_loss': loss, 'val_acc': val_acc}) + + If you pass in multiple val dataloaders, :meth:`validation_step` will have an additional argument. We recommend + setting the default value of 0 so that you can quickly switch between single and multiple dataloaders. + + .. code-block:: python + + # CASE 2: multiple validation dataloaders + def validation_step(self, batch, batch_idx, dataloader_idx=0): + # dataloader_idx tells you which dataset this is. + ... + + Note: + If you don't need to validate you don't need to implement this method. + + Note: + When the :meth:`validation_step` is called, the model has been put in eval mode + and PyTorch gradients have been disabled. At the end of validation, + the model goes back to training mode and gradients are enabled. + + """ + + def test_step(self, *args: Any, **kwargs: Any) -> STEP_OUTPUT: + r"""Operates on a single batch of data from the test set. In this step you'd normally generate examples or + calculate anything of interest such as accuracy. + + Args: + batch: The output of your data iterable, normally a :class:`~torch.utils.data.DataLoader`. + batch_idx: The index of this batch. + dataloader_idx: The index of the dataloader that produced this batch. + (only if multiple dataloaders used) + + Return: + - :class:`~torch.Tensor` - The loss tensor + - ``dict`` - A dictionary. Can include any keys, but must include the key ``'loss'``. + - ``None`` - Skip to the next batch. + + .. code-block:: python + + # if you have one test dataloader: + def test_step(self, batch, batch_idx): ... + + + # if you have multiple test dataloaders: + def test_step(self, batch, batch_idx, dataloader_idx=0): ... + + Examples:: + + # CASE 1: A single test dataset + def test_step(self, batch, batch_idx): + x, y = batch + + # implement your own + out = self(x) + loss = self.loss(out, y) + + # log 6 example images + # or generated text... or whatever + sample_imgs = x[:6] + grid = torchvision.utils.make_grid(sample_imgs) + self.logger.experiment.add_image('example_images', grid, 0) + + # calculate acc + labels_hat = torch.argmax(out, dim=1) + test_acc = torch.sum(y == labels_hat).item() / (len(y) * 1.0) + + # log the outputs! + self.log_dict({'test_loss': loss, 'test_acc': test_acc}) + + If you pass in multiple test dataloaders, :meth:`test_step` will have an additional argument. We recommend + setting the default value of 0 so that you can quickly switch between single and multiple dataloaders. + + .. code-block:: python + + # CASE 2: multiple test dataloaders + def test_step(self, batch, batch_idx, dataloader_idx=0): + # dataloader_idx tells you which dataset this is. + ... + + Note: + If you don't need to test you don't need to implement this method. + + Note: + When the :meth:`test_step` is called, the model has been put in eval mode and + PyTorch gradients have been disabled. At the end of the test epoch, the model goes back + to training mode and gradients are enabled. + + """ + + def predict_step(self, *args: Any, **kwargs: Any) -> Any: + """Step function called during :meth:`~lightning.pytorch.trainer.trainer.Trainer.predict`. By default, it calls + :meth:`~lightning.pytorch.core.LightningModule.forward`. Override to add any processing logic. + + The :meth:`~lightning.pytorch.core.LightningModule.predict_step` is used + to scale inference on multi-devices. + + To prevent an OOM error, it is possible to use :class:`~lightning.pytorch.callbacks.BasePredictionWriter` + callback to write the predictions to disk or database after each batch or on epoch end. + + The :class:`~lightning.pytorch.callbacks.BasePredictionWriter` should be used while using a spawn + based accelerator. This happens for ``Trainer(strategy="ddp_spawn")`` + or training on 8 TPU cores with ``Trainer(accelerator="tpu", devices=8)`` as predictions won't be returned. + + Args: + batch: The output of your data iterable, normally a :class:`~torch.utils.data.DataLoader`. + batch_idx: The index of this batch. + dataloader_idx: The index of the dataloader that produced this batch. + (only if multiple dataloaders used) + + Return: + Predicted output (optional). + + Example :: + + class MyModel(LightningModule): + + def predict_step(self, batch, batch_idx, dataloader_idx=0): + return self(batch) + + dm = ... + model = MyModel() + trainer = Trainer(accelerator="gpu", devices=2) + predictions = trainer.predict(model, dm) + + """ + # For backwards compatibility + batch = kwargs.get("batch", args[0]) + return self(batch) + + def configure_callbacks(self) -> Union[Sequence[Callback], Callback]: + """Configure model-specific callbacks. When the model gets attached, e.g., when ``.fit()`` or ``.test()`` gets + called, the list or a callback returned here will be merged with the list of callbacks passed to the Trainer's + ``callbacks`` argument. If a callback returned here has the same type as one or several callbacks already + present in the Trainer's callbacks list, it will take priority and replace them. In addition, Lightning will + make sure :class:`~lightning.pytorch.callbacks.model_checkpoint.ModelCheckpoint` callbacks run last. + + Return: + A callback or a list of callbacks which will extend the list of callbacks in the Trainer. + + Example:: + + def configure_callbacks(self): + early_stop = EarlyStopping(monitor="val_acc", mode="max") + checkpoint = ModelCheckpoint(monitor="val_loss") + return [early_stop, checkpoint] + + """ + return [] + + def configure_optimizers(self) -> OptimizerLRScheduler: + r"""Choose what optimizers and learning-rate schedulers to use in your optimization. Normally you'd need one. + But in the case of GANs or similar you might have multiple. Optimization with multiple optimizers only works in + the manual optimization mode. + + Return: + Any of these 6 options. + + - **Single optimizer**. + - **List or Tuple** of optimizers. + - **Two lists** - The first list has multiple optimizers, and the second has multiple LR schedulers + (or multiple ``lr_scheduler_config``). + - **Dictionary**, with an ``"optimizer"`` key, and (optionally) a ``"lr_scheduler"`` + key whose value is a single LR scheduler or ``lr_scheduler_config``. + - **None** - Fit will run without any optimizer. + + The ``lr_scheduler_config`` is a dictionary which contains the scheduler and its associated configuration. + The default configuration is shown below. + + .. code-block:: python + + lr_scheduler_config = { + # REQUIRED: The scheduler instance + "scheduler": lr_scheduler, + # The unit of the scheduler's step size, could also be 'step'. + # 'epoch' updates the scheduler on epoch end whereas 'step' + # updates it after a optimizer update. + "interval": "epoch", + # How many epochs/steps should pass between calls to + # `scheduler.step()`. 1 corresponds to updating the learning + # rate after every epoch/step. + "frequency": 1, + # Metric to monitor for schedulers like `ReduceLROnPlateau` + "monitor": "val_loss", + # If set to `True`, will enforce that the value specified 'monitor' + # is available when the scheduler is updated, thus stopping + # training if not found. If set to `False`, it will only produce a warning + "strict": True, + # If using the `LearningRateMonitor` callback to monitor the + # learning rate progress, this keyword can be used to specify + # a custom logged name + "name": None, + } + + When there are schedulers in which the ``.step()`` method is conditioned on a value, such as the + :class:`torch.optim.lr_scheduler.ReduceLROnPlateau` scheduler, Lightning requires that the + ``lr_scheduler_config`` contains the keyword ``"monitor"`` set to the metric name that the scheduler + should be conditioned on. + + .. testcode:: + + # The ReduceLROnPlateau scheduler requires a monitor + def configure_optimizers(self): + optimizer = Adam(...) + return { + "optimizer": optimizer, + "lr_scheduler": { + "scheduler": ReduceLROnPlateau(optimizer, ...), + "monitor": "metric_to_track", + "frequency": "indicates how often the metric is updated", + # If "monitor" references validation metrics, then "frequency" should be set to a + # multiple of "trainer.check_val_every_n_epoch". + }, + } + + + # In the case of two optimizers, only one using the ReduceLROnPlateau scheduler + def configure_optimizers(self): + optimizer1 = Adam(...) + optimizer2 = SGD(...) + scheduler1 = ReduceLROnPlateau(optimizer1, ...) + scheduler2 = LambdaLR(optimizer2, ...) + return ( + { + "optimizer": optimizer1, + "lr_scheduler": { + "scheduler": scheduler1, + "monitor": "metric_to_track", + }, + }, + {"optimizer": optimizer2, "lr_scheduler": scheduler2}, + ) + + Metrics can be made available to monitor by simply logging it using + ``self.log('metric_to_track', metric_val)`` in your :class:`~lightning.pytorch.core.LightningModule`. + + Note: + Some things to know: + + - Lightning calls ``.backward()`` and ``.step()`` automatically in case of automatic optimization. + - If a learning rate scheduler is specified in ``configure_optimizers()`` with key + ``"interval"`` (default "epoch") in the scheduler configuration, Lightning will call + the scheduler's ``.step()`` method automatically in case of automatic optimization. + - If you use 16-bit precision (``precision=16``), Lightning will automatically handle the optimizer. + - If you use :class:`torch.optim.LBFGS`, Lightning handles the closure function automatically for you. + - If you use multiple optimizers, you will have to switch to 'manual optimization' mode and step them + yourself. + - If you need to control how often the optimizer steps, override the :meth:`optimizer_step` hook. + + """ + rank_zero_warn("`configure_optimizers` must be implemented to be used with the Lightning Trainer") + + def manual_backward(self, loss: Tensor, *args: Any, **kwargs: Any) -> None: + """Call this directly from your :meth:`training_step` when doing optimizations manually. By using this, + Lightning can ensure that all the proper scaling gets applied when using mixed precision. + + See :ref:`manual optimization` for more examples. + + Example:: + + def training_step(...): + opt = self.optimizers() + loss = ... + opt.zero_grad() + # automatically applies scaling, etc... + self.manual_backward(loss) + opt.step() + + Args: + loss: The tensor on which to compute gradients. Must have a graph attached. + *args: Additional positional arguments to be forwarded to :meth:`~torch.Tensor.backward` + **kwargs: Additional keyword arguments to be forwarded to :meth:`~torch.Tensor.backward` + + """ + if self._fabric: + self._fabric.backward(loss, *args, **kwargs) + else: + self._verify_is_manual_optimization("manual_backward") + self.trainer.strategy.backward(loss, None, *args, **kwargs) + + def backward(self, loss: Tensor, *args: Any, **kwargs: Any) -> None: + """Called to perform backward on the loss returned in :meth:`training_step`. Override this hook with your own + implementation if you need to. + + Args: + loss: The loss tensor returned by :meth:`training_step`. If gradient accumulation is used, the loss here + holds the normalized value (scaled by 1 / accumulation steps). + + Example:: + + def backward(self, loss): + loss.backward() + + """ + if self._fabric: + self._fabric.backward(loss, *args, **kwargs) + else: + loss.backward(*args, **kwargs) + + def toggle_optimizer(self, optimizer: Union[Optimizer, LightningOptimizer]) -> None: + """Makes sure only the gradients of the current optimizer's parameters are calculated in the training step to + prevent dangling gradients in multiple-optimizer setup. + + It works with :meth:`untoggle_optimizer` to make sure ``param_requires_grad_state`` is properly reset. + + Args: + optimizer: The optimizer to toggle. + + """ + # Iterate over all optimizer parameters to preserve their `requires_grad` information + # in case these are pre-defined during `configure_optimizers` + param_requires_grad_state = {} + for opt in self.trainer.optimizers: + for group in opt.param_groups: + for param in group["params"]: + # If a param already appear in param_requires_grad_state, continue + if param in param_requires_grad_state: + continue + param_requires_grad_state[param] = param.requires_grad + param.requires_grad = False + + # Then iterate over the current optimizer's parameters and set its `requires_grad` + # properties accordingly + for group in optimizer.param_groups: + for param in group["params"]: + param.requires_grad = param_requires_grad_state[param] + self._param_requires_grad_state = param_requires_grad_state + + def untoggle_optimizer(self, optimizer: Union[Optimizer, LightningOptimizer]) -> None: + """Resets the state of required gradients that were toggled with :meth:`toggle_optimizer`. + + Args: + optimizer: The optimizer to untoggle. + + """ + for opt in self.trainer.optimizers: + if not (opt is optimizer or (isinstance(optimizer, LightningOptimizer) and opt is optimizer.optimizer)): + for group in opt.param_groups: + for param in group["params"]: + if param in self._param_requires_grad_state: + param.requires_grad = self._param_requires_grad_state[param] + # save memory + self._param_requires_grad_state = {} + + @contextmanager + def toggled_optimizer(self, optimizer: Union[Optimizer, LightningOptimizer]) -> Generator: + """Makes sure only the gradients of the current optimizer's parameters are calculated in the training step to + prevent dangling gradients in multiple-optimizer setup. Combines :meth:`toggle_optimizer` and + :meth:`untoggle_optimizer` into context manager. + + Args: + optimizer: The optimizer to toggle. + + Example:: + + def training_step(...): + opt = self.optimizers() + with self.toggled_optimizer(opt): + loss = ... + opt.zero_grad() + self.manual_backward(loss) + opt.step() + + """ + self.toggle_optimizer(optimizer) + try: + yield + finally: + self.untoggle_optimizer(optimizer) + + def clip_gradients( + self, + optimizer: Optimizer, + gradient_clip_val: Optional[Union[int, float]] = None, + gradient_clip_algorithm: Optional[str] = None, + ) -> None: + """Handles gradient clipping internally. + + Note: + - Do not override this method. If you want to customize gradient clipping, consider using + :meth:`configure_gradient_clipping` method. + - For manual optimization (``self.automatic_optimization = False``), if you want to use + gradient clipping, consider calling + ``self.clip_gradients(opt, gradient_clip_val=0.5, gradient_clip_algorithm="norm")`` + manually in the training step. + + Args: + optimizer: Current optimizer being used. + gradient_clip_val: The value at which to clip gradients. + gradient_clip_algorithm: The gradient clipping algorithm to use. Pass ``gradient_clip_algorithm="value"`` + to clip by value, and ``gradient_clip_algorithm="norm"`` to clip by norm. + + """ + + if self.fabric is not None: + self.fabric.clip_gradients( + self, + optimizer, + clip_val=gradient_clip_val if gradient_clip_algorithm == GradClipAlgorithmType.VALUE else None, + max_norm=None if gradient_clip_algorithm == GradClipAlgorithmType.VALUE else gradient_clip_val, + ) + return + + if gradient_clip_val is None: + gradient_clip_val = self.trainer.gradient_clip_val or 0.0 + elif self.trainer.gradient_clip_val is not None and self.trainer.gradient_clip_val != gradient_clip_val: + raise MisconfigurationException( + f"You have set `Trainer(gradient_clip_val={self.trainer.gradient_clip_val!r})`" + f" and have passed `clip_gradients(gradient_clip_val={gradient_clip_val!r})`." + " Please use only one of them." + ) + + if gradient_clip_algorithm is None: + gradient_clip_algorithm = self.trainer.gradient_clip_algorithm or "norm" + else: + gradient_clip_algorithm = gradient_clip_algorithm.lower() + if ( + self.trainer.gradient_clip_algorithm is not None + and self.trainer.gradient_clip_algorithm != gradient_clip_algorithm + ): + raise MisconfigurationException( + f"You have set `Trainer(gradient_clip_algorithm={self.trainer.gradient_clip_algorithm.value!r})`" + f" and have passed `clip_gradients(gradient_clip_algorithm={gradient_clip_algorithm!r})" + " Please use only one of them." + ) + + if not isinstance(gradient_clip_val, (int, float)): + raise TypeError(f"`gradient_clip_val` should be an int or a float. Got {gradient_clip_val}.") + + if not GradClipAlgorithmType.supported_type(gradient_clip_algorithm.lower()): + raise MisconfigurationException( + f"`gradient_clip_algorithm` {gradient_clip_algorithm} is invalid." + f" Allowed algorithms: {GradClipAlgorithmType.supported_types()}." + ) + + gradient_clip_algorithm = GradClipAlgorithmType(gradient_clip_algorithm) + self.trainer.precision_plugin.clip_gradients(optimizer, gradient_clip_val, gradient_clip_algorithm) + + def configure_gradient_clipping( + self, + optimizer: Optimizer, + gradient_clip_val: Optional[Union[int, float]] = None, + gradient_clip_algorithm: Optional[str] = None, + ) -> None: + """Perform gradient clipping for the optimizer parameters. Called before :meth:`optimizer_step`. + + Args: + optimizer: Current optimizer being used. + gradient_clip_val: The value at which to clip gradients. By default, value passed in Trainer + will be available here. + gradient_clip_algorithm: The gradient clipping algorithm to use. By default, value + passed in Trainer will be available here. + + Example:: + + def configure_gradient_clipping(self, optimizer, gradient_clip_val, gradient_clip_algorithm): + # Implement your own custom logic to clip gradients + # You can call `self.clip_gradients` with your settings: + self.clip_gradients( + optimizer, + gradient_clip_val=gradient_clip_val, + gradient_clip_algorithm=gradient_clip_algorithm + ) + + """ + self.clip_gradients( + optimizer, gradient_clip_val=gradient_clip_val, gradient_clip_algorithm=gradient_clip_algorithm + ) + + def lr_scheduler_step(self, scheduler: LRSchedulerTypeUnion, metric: Optional[Any]) -> None: + r"""Override this method to adjust the default way the :class:`~lightning.pytorch.trainer.trainer.Trainer` calls + each scheduler. By default, Lightning calls ``step()`` and as shown in the example for each scheduler based on + its ``interval``. + + Args: + scheduler: Learning rate scheduler. + metric: Value of the monitor used for schedulers like ``ReduceLROnPlateau``. + + Examples:: + + # DEFAULT + def lr_scheduler_step(self, scheduler, metric): + if metric is None: + scheduler.step() + else: + scheduler.step(metric) + + # Alternative way to update schedulers if it requires an epoch value + def lr_scheduler_step(self, scheduler, metric): + scheduler.step(epoch=self.current_epoch) + + """ + if metric is None: + scheduler.step() # type: ignore[call-arg] + else: + scheduler.step(metric) + + def optimizer_step( + self, + epoch: int, + batch_idx: int, + optimizer: Union[Optimizer, LightningOptimizer], + optimizer_closure: Optional[Callable[[], Any]] = None, + ) -> None: + r"""Override this method to adjust the default way the :class:`~lightning.pytorch.trainer.trainer.Trainer` calls + the optimizer. + + By default, Lightning calls ``step()`` and ``zero_grad()`` as shown in the example. + This method (and ``zero_grad()``) won't be called during the accumulation phase when + ``Trainer(accumulate_grad_batches != 1)``. Overriding this hook has no benefit with manual optimization. + + Args: + epoch: Current epoch + batch_idx: Index of current batch + optimizer: A PyTorch optimizer + optimizer_closure: The optimizer closure. This closure must be executed as it includes the + calls to ``training_step()``, ``optimizer.zero_grad()``, and ``backward()``. + + Examples:: + + def optimizer_step(self, epoch, batch_idx, optimizer, optimizer_closure): + # Add your custom logic to run directly before `optimizer.step()` + + optimizer.step(closure=optimizer_closure) + + # Add your custom logic to run directly after `optimizer.step()` + + """ + optimizer.step(closure=optimizer_closure) + + def optimizer_zero_grad(self, epoch: int, batch_idx: int, optimizer: Optimizer) -> None: + """Override this method to change the default behaviour of ``optimizer.zero_grad()``. + + Args: + epoch: Current epoch + batch_idx: Index of current batch + optimizer: A PyTorch optimizer + + Examples:: + + # DEFAULT + def optimizer_zero_grad(self, epoch, batch_idx, optimizer): + optimizer.zero_grad() + + # Set gradients to `None` instead of zero to improve performance (not required on `torch>=2.0.0`). + def optimizer_zero_grad(self, epoch, batch_idx, optimizer): + optimizer.zero_grad(set_to_none=True) + + See :meth:`torch.optim.Optimizer.zero_grad` for the explanation of the above example. + + """ + optimizer.zero_grad() + + def freeze(self) -> None: + r"""Freeze all params for inference. + + Example:: + + model = MyLightningModule(...) + model.freeze() + + """ + for param in self.parameters(): + param.requires_grad = False + + self.eval() + + def unfreeze(self) -> None: + """Unfreeze all parameters for training. + + .. code-block:: python + + model = MyLightningModule(...) + model.unfreeze() + + """ + for param in self.parameters(): + param.requires_grad = True + + self.train() + + def _verify_is_manual_optimization(self, fn_name: str) -> None: + if self.automatic_optimization: + raise MisconfigurationException( + f"to use {fn_name}, please disable automatic optimization:" + " set model property `automatic_optimization` as False" + ) + + @torch.no_grad() + def to_onnx(self, file_path: Union[str, Path, BytesIO], input_sample: Optional[Any] = None, **kwargs: Any) -> None: + """Saves the model in ONNX format. + + Args: + file_path: The path of the file the onnx model should be saved to. + input_sample: An input for tracing. Default: None (Use self.example_input_array) + **kwargs: Will be passed to torch.onnx.export function. + + Example:: + + class SimpleModel(LightningModule): + def __init__(self): + super().__init__() + self.l1 = torch.nn.Linear(in_features=64, out_features=4) + + def forward(self, x): + return torch.relu(self.l1(x.view(x.size(0), -1) + + model = SimpleModel() + input_sample = torch.randn(1, 64) + model.to_onnx("export.onnx", input_sample, export_params=True) + + """ + if not _ONNX_AVAILABLE: + raise ModuleNotFoundError(f"`{type(self).__name__}.to_onnx()` requires `onnx` to be installed.") + + mode = self.training + + if input_sample is None: + if self.example_input_array is None: + raise ValueError( + "Could not export to ONNX since neither `input_sample` nor" + " `model.example_input_array` attribute is set." + ) + input_sample = self.example_input_array + + input_sample = self._on_before_batch_transfer(input_sample) + input_sample = self._apply_batch_transfer_handler(input_sample) + + file_path = str(file_path) if isinstance(file_path, Path) else file_path + # PyTorch (2.5) declares file_path to be str | PathLike[Any] | None, but + # BytesIO does work, too. + torch.onnx.export(self, input_sample, file_path, **kwargs) # type: ignore + self.train(mode) + + @torch.no_grad() + def to_torchscript( + self, + file_path: Optional[Union[str, Path]] = None, + method: Optional[str] = "script", + example_inputs: Optional[Any] = None, + **kwargs: Any, + ) -> Union[ScriptModule, dict[str, ScriptModule]]: + """By default compiles the whole model to a :class:`~torch.jit.ScriptModule`. If you want to use tracing, + please provided the argument ``method='trace'`` and make sure that either the `example_inputs` argument is + provided, or the model has :attr:`example_input_array` set. If you would like to customize the modules that are + scripted you should override this method. In case you want to return multiple modules, we recommend using a + dictionary. + + Args: + file_path: Path where to save the torchscript. Default: None (no file saved). + method: Whether to use TorchScript's script or trace method. Default: 'script' + example_inputs: An input to be used to do tracing when method is set to 'trace'. + Default: None (uses :attr:`example_input_array`) + **kwargs: Additional arguments that will be passed to the :func:`torch.jit.script` or + :func:`torch.jit.trace` function. + + Note: + - Requires the implementation of the + :meth:`~lightning.pytorch.core.LightningModule.forward` method. + - The exported script will be set to evaluation mode. + - It is recommended that you install the latest supported version of PyTorch + to use this feature without limitations. See also the :mod:`torch.jit` + documentation for supported features. + + Example:: + + class SimpleModel(LightningModule): + def __init__(self): + super().__init__() + self.l1 = torch.nn.Linear(in_features=64, out_features=4) + + def forward(self, x): + return torch.relu(self.l1(x.view(x.size(0), -1))) + + model = SimpleModel() + model.to_torchscript(file_path="model.pt") + + torch.jit.save(model.to_torchscript( + file_path="model_trace.pt", method='trace', example_inputs=torch.randn(1, 64)) + ) + + Return: + This LightningModule as a torchscript, regardless of whether `file_path` is + defined or not. + + """ + mode = self.training + + if method == "script": + with _jit_is_scripting(): + torchscript_module = torch.jit.script(self.eval(), **kwargs) + elif method == "trace": + # if no example inputs are provided, try to see if model has example_input_array set + if example_inputs is None: + if self.example_input_array is None: + raise ValueError( + "Choosing method=`trace` requires either `example_inputs`" + " or `model.example_input_array` to be defined." + ) + example_inputs = self.example_input_array + + if kwargs.get("check_inputs") is not None: + kwargs["check_inputs"] = self._on_before_batch_transfer(kwargs["check_inputs"]) + kwargs["check_inputs"] = self._apply_batch_transfer_handler(kwargs["check_inputs"]) + + # automatically send example inputs to the right device and use trace + example_inputs = self._on_before_batch_transfer(example_inputs) + example_inputs = self._apply_batch_transfer_handler(example_inputs) + with _jit_is_scripting(): + torchscript_module = torch.jit.trace(func=self.eval(), example_inputs=example_inputs, **kwargs) + else: + raise ValueError(f"The 'method' parameter only supports 'script' or 'trace', but value given was: {method}") + + self.train(mode) + + if file_path is not None: + fs = get_filesystem(file_path) + with fs.open(file_path, "wb") as f: + torch.jit.save(torchscript_module, f) + + return torchscript_module + + @_restricted_classmethod + def load_from_checkpoint( + cls, + checkpoint_path: Union[_PATH, IO], + map_location: _MAP_LOCATION_TYPE = None, + hparams_file: Optional[_PATH] = None, + strict: Optional[bool] = None, + **kwargs: Any, + ) -> Self: + r"""Primary way of loading a model from a checkpoint. When Lightning saves a checkpoint it stores the arguments + passed to ``__init__`` in the checkpoint under ``"hyper_parameters"``. + + Any arguments specified through \*\*kwargs will override args stored in ``"hyper_parameters"``. + + Args: + checkpoint_path: Path to checkpoint. This can also be a URL, or file-like object + map_location: + If your checkpoint saved a GPU model and you now load on CPUs + or a different number of GPUs, use this to map to the new setup. + The behaviour is the same as in :func:`torch.load`. + hparams_file: Optional path to a ``.yaml`` or ``.csv`` file with hierarchical structure + as in this example:: + + drop_prob: 0.2 + dataloader: + batch_size: 32 + + You most likely won't need this since Lightning will always save the hyperparameters + to the checkpoint. + However, if your checkpoint weights don't have the hyperparameters saved, + use this method to pass in a ``.yaml`` file with the hparams you'd like to use. + These will be converted into a :class:`~dict` and passed into your + :class:`LightningModule` for use. + + If your model's ``hparams`` argument is :class:`~argparse.Namespace` + and ``.yaml`` file has hierarchical structure, you need to refactor your model to treat + ``hparams`` as :class:`~dict`. + strict: Whether to strictly enforce that the keys in :attr:`checkpoint_path` match the keys + returned by this module's state dict. Defaults to ``True`` unless ``LightningModule.strict_loading`` is + set, in which case it defaults to the value of ``LightningModule.strict_loading``. + \**kwargs: Any extra keyword args needed to init the model. Can also be used to override saved + hyperparameter values. + + Return: + :class:`LightningModule` instance with loaded weights and hyperparameters (if available). + + Note: + ``load_from_checkpoint`` is a **class** method. You should use your :class:`LightningModule` + **class** to call it instead of the :class:`LightningModule` instance, or a + ``TypeError`` will be raised. + + Note: + To ensure all layers can be loaded from the checkpoint, this function will call + :meth:`~lightning.pytorch.core.hooks.ModelHooks.configure_model` directly after instantiating the + model if this hook is overridden in your LightningModule. However, note that ``load_from_checkpoint`` does + not support loading sharded checkpoints, and you may run out of memory if the model is too large. In this + case, consider loading through the Trainer via ``.fit(ckpt_path=...)``. + + Example:: + + # load weights without mapping ... + model = MyLightningModule.load_from_checkpoint('path/to/checkpoint.ckpt') + + # or load weights mapping all weights from GPU 1 to GPU 0 ... + map_location = {'cuda:1':'cuda:0'} + model = MyLightningModule.load_from_checkpoint( + 'path/to/checkpoint.ckpt', + map_location=map_location + ) + + # or load weights and hyperparameters from separate files. + model = MyLightningModule.load_from_checkpoint( + 'path/to/checkpoint.ckpt', + hparams_file='/path/to/hparams_file.yaml' + ) + + # override some of the params with new values + model = MyLightningModule.load_from_checkpoint( + PATH, + num_layers=128, + pretrained_ckpt_path=NEW_PATH, + ) + + # predict + pretrained_model.eval() + pretrained_model.freeze() + y_hat = pretrained_model(x) + + """ + loaded = _load_from_checkpoint( + cls, + checkpoint_path, + map_location, + hparams_file, + strict, + **kwargs, + ) + return cast(Self, loaded) + + @override + def __getstate__(self) -> dict[str, Any]: + state = dict(self.__dict__) + state["_trainer"] = None + return state + + +@contextmanager +def _jit_is_scripting() -> Generator: + """Workaround for https://github.com/pytorch/pytorch/issues/67146.""" + LightningModule._jit_is_scripting = True + try: + yield + finally: + LightningModule._jit_is_scripting = False + + +class _TrainerFabricShim: + """Intercepts attribute access on LightningModule's trainer reference and redirects it to the Fabric object.""" + + def __init__(self, fabric: lf.Fabric) -> None: + super().__init__() + self._fabric = fabric + + def __getattr__(self, item: Any) -> Any: + try: + return getattr(self._fabric, item) + except AttributeError: + raise AttributeError( + f"Your LightningModule code tried to access `self.trainer.{item}` but this attribute is not available" + f" when using Fabric with a LightningModule." + ) diff --git a/application/jobs/stamp/app/custom/modeling/registry.py b/application/jobs/stamp/app/custom/modeling/registry.py new file mode 100644 index 00000000..e826cadd --- /dev/null +++ b/application/jobs/stamp/app/custom/modeling/registry.py @@ -0,0 +1,34 @@ +from enum import StrEnum +from typing import Sequence, Type, TypedDict + +import lightning + +from .lightning_model import LitVisionTransformer +from .mlp_classifier import LitMLPClassifier + + +class ModelName(StrEnum): + """Enum for available model names.""" + + VIT = "vit" + MLP = "mlp" + + +class ModelInfo(TypedDict): + """A dictionary to map a model to supported feature types. For example, + a linear classifier is not compatible with tile-evel feats.""" + + model_class: Type[lightning.LightningModule] + supported_features: Sequence[str] + + +MODEL_REGISTRY: dict[ModelName, ModelInfo] = { + ModelName.VIT: { + "model_class": LitVisionTransformer, + "supported_features": LitVisionTransformer.supported_features, + }, + ModelName.MLP: { + "model_class": LitMLPClassifier, + "supported_features": LitMLPClassifier.supported_features, + }, +} diff --git a/application/jobs/stamp/app/custom/modeling/types.py b/application/jobs/stamp/app/custom/modeling/types.py new file mode 100644 index 00000000..4d48293a --- /dev/null +++ b/application/jobs/stamp/app/custom/modeling/types.py @@ -0,0 +1,55 @@ +from pathlib import Path +from typing import ( + Final, + Literal, + NewType, + TypeAlias, + TypeVar, +) + +import torch +from beartype.typing import Mapping +from jaxtyping import Bool, Float, Integer +from torch import Tensor + +# tiling + +ImageExtension: TypeAlias = Literal["png", "jpg"] +EXTENSION_TO_FORMAT: Final[Mapping[ImageExtension, str]] = { + "png": "png", + "jpg": "jpeg", +} + +Microns = NewType("Microns", float) +"""Micrometers, usually referring to the tissue on the slide""" + +SlidePixels = NewType("SlidePixels", int) +"""Pixels of the WSI scan at largest magnification (i.e. coordinates used by OpenSlide)""" + +TilePixels = NewType("TilePixels", int) +"""Pixels after resizing, i.e. how they appear on the final tile""" + +SlideMPP = NewType("SlideMPP", float) + +# modeling + +DeviceLikeType: TypeAlias = str | torch.device | int + +PatientId: TypeAlias = str +GroundTruth: TypeAlias = str +FeaturePath = NewType("FeaturePath", Path) + +Category: TypeAlias = str + +BagSize: TypeAlias = int + +# A batch of the above +Bags: TypeAlias = Float[Tensor, "batch tile feature"] +BagSizes: TypeAlias = Integer[Tensor, "batch"] # noqa: F821 +EncodedTargets: TypeAlias = Bool[Tensor, "batch category_is_hot"] +"""The ground truth, encoded numerically (currently: one-hot)""" +CoordinatesBatch: TypeAlias = Float[Tensor, "batch tile 2"] + +PandasLabel: TypeAlias = str + +GroundTruthType = TypeVar("GroundTruthType", covariant=True) diff --git a/application/jobs/stamp/app/custom/modeling/vision_transformer.py b/application/jobs/stamp/app/custom/modeling/vision_transformer.py new file mode 100755 index 00000000..3853c40b --- /dev/null +++ b/application/jobs/stamp/app/custom/modeling/vision_transformer.py @@ -0,0 +1,244 @@ +""" +In parts from https://github.com/lucidrains/vit-pytorch/blob/main/vit_pytorch/vit.py +""" + +from collections.abc import Iterable +from typing import assert_never, cast + +import torch +from beartype import beartype +from einops import repeat +from jaxtyping import Bool, Float, jaxtyped +from torch import Tensor, nn + +from .alibi import MultiHeadALiBi + + +def feed_forward( + dim: int, + hidden_dim: int, + dropout: float = 0.5, +) -> nn.Module: + return nn.Sequential( + nn.LayerNorm(dim), + nn.Linear(dim, hidden_dim), + nn.GELU(), + nn.Dropout(dropout), + nn.Linear(hidden_dim, dim), + nn.Dropout(dropout), + ) + + +class SelfAttention(nn.Module): + def __init__( + self, + *, + dim: int, + num_heads: int, + dropout: float, + use_alibi: bool, + ) -> None: + super().__init__() + self.heads = num_heads + self.norm = nn.LayerNorm(dim) + + if use_alibi: + self.mhsa = MultiHeadALiBi( + embed_dim=dim, + num_heads=num_heads, + ) + else: + self.mhsa = nn.MultiheadAttention(dim, num_heads, dropout, batch_first=True) + + @jaxtyped(typechecker=beartype) + def forward( + self, + x: Float[Tensor, "batch sequence proj_feature"], + *, + coords: Float[Tensor, "batch sequence xy"], + attn_mask: Bool[Tensor, "batch sequence sequence"] | None, + # Help, my abstractions are leaking! + alibi_mask: Bool[Tensor, "batch sequence sequence"] | None, + ) -> Float[Tensor, "batch sequence proj_feature"]: + """ + Args: + attn_mask: + Which of the features to ignore during self-attention. + `attn_mask[b,q,k] == False` means that + query `q` of batch `b` can attend to key `k`. + If `attn_mask` is `None`, all tokens can attend to all others. + alibi_mask: + Which query-key pairs to apply ALiBi to. + If this module was constructed using `use_alibi=False`, + this has no effect. + """ + x = self.norm(x) + match self.mhsa: + case nn.MultiheadAttention(): + attn_output, _ = self.mhsa( + x, + x, + x, + need_weights=False, + attn_mask=( + attn_mask.repeat(self.mhsa.num_heads, 1, 1) + if attn_mask is not None + else None + ), + ) + case MultiHeadALiBi(): + attn_output = self.mhsa( + q=x, + k=x, + v=x, + coords_q=coords, + coords_k=coords, + attn_mask=attn_mask, + alibi_mask=alibi_mask, + ) + case _ as unreachable: + assert_never(unreachable) + + return attn_output + + +class Transformer(nn.Module): + def __init__( + self, + *, + dim: int, + depth: int, + heads: int, + mlp_dim: int, + dropout: float, + use_alibi: bool, + ) -> None: + super().__init__() + self.depth = depth + self.layers = nn.ModuleList( + [ + nn.ModuleList( + [ + SelfAttention( + dim=dim, + num_heads=heads, + dropout=dropout, + use_alibi=use_alibi, + ), + feed_forward( + dim, + mlp_dim, + ), + ] + ) + for _ in range(depth) + ] + ) + + self.norm = nn.LayerNorm(dim) + + @jaxtyped(typechecker=beartype) + def forward( + self, + x: Float[Tensor, "batch sequence proj_feature"], + *, + coords: Float[Tensor, "batch sequence 2"], + attn_mask: Bool[Tensor, "batch sequence sequence"] | None, + alibi_mask: Bool[Tensor, "batch sequence sequence"] | None, + ) -> Float[Tensor, "batch sequence proj_feature"]: + for attn, ff in cast(Iterable[tuple[nn.Module, nn.Module]], self.layers): + x_attn = attn(x, coords=coords, attn_mask=attn_mask, alibi_mask=alibi_mask) + x = x_attn + x + x = ff(x) + x + + x = self.norm(x) + return x + + +class VisionTransformer(nn.Module): + def __init__( + self, + *, + dim_output: int, + dim_input: int, + dim_model: int, + n_layers: int, + n_heads: int, + dim_feedforward: int, + dropout: float, + use_alibi: bool, + ) -> None: + super().__init__() + self.class_token = nn.Parameter(torch.randn(dim_model)) + + self.project_features = nn.Sequential( + nn.Linear(dim_input, dim_model, bias=True), + nn.GELU(), + nn.Dropout(dropout), + ) + + self.transformer = Transformer( + dim=dim_model, + depth=n_layers, + heads=n_heads, + mlp_dim=dim_feedforward, + dropout=dropout, + use_alibi=use_alibi, + ) + + self.mlp_head = nn.Sequential(nn.Linear(dim_model, dim_output)) + + @jaxtyped(typechecker=beartype) + def forward( + self, + bags: Float[Tensor, "batch tile feature"], + *, + coords: Float[Tensor, "batch tile 2"], + mask: Bool[Tensor, "batch tile"] | None, + ) -> Float[Tensor, "batch logit"]: + batch_size, _n_tiles, _n_features = bags.shape + + # Map input sequence to latent space of TransMIL + bags = self.project_features(bags) + + # Prepend a class token to every bag, + # include it in the mask. + # TODO should the tiles be able to refer to the class token? Test! + cls_tokens = repeat(self.class_token, "d -> b 1 d", b=batch_size) + bags = torch.cat([cls_tokens, bags], dim=1) + coords = torch.cat( + [torch.zeros(batch_size, 1, 2).type_as(coords), coords], dim=1 + ) + + match mask: + case None: + bags = self.transformer( + bags, coords=coords, attn_mask=None, alibi_mask=None + ) + + case _: + mask_with_class_token = torch.cat( + [torch.zeros(mask.shape[0], 1).type_as(mask), mask], dim=1 + ) + square_attn_mask = torch.einsum( + "bq,bk->bqk", mask_with_class_token, mask_with_class_token + ) + # Don't allow other tiles to reference the class token + square_attn_mask[:, 1:, 0] = True + + # Don't apply ALiBi to the query, as the coordinates don't make sense here + alibi_mask = torch.zeros_like(square_attn_mask) + alibi_mask[:, 0, :] = True + alibi_mask[:, :, 0] = True + + bags = self.transformer( + bags, + coords=coords, + attn_mask=square_attn_mask, + alibi_mask=alibi_mask, + ) + + # Only take class token + bags = bags[:, 0] + + return self.mlp_head(bags) diff --git a/application/jobs/stamp/app/custom/transforms.py b/application/jobs/stamp/app/custom/transforms.py new file mode 100644 index 00000000..6fd9755f --- /dev/null +++ b/application/jobs/stamp/app/custom/transforms.py @@ -0,0 +1,41 @@ +import torch +from jaxtyping import Float + + +def vary_precision( + data: Float[torch.Tensor, "*dims"], *, min_fraction_bits: int +) -> Float[torch.Tensor, "*dims"]: + """Randomly reduces the precision of the tensor's values.""" + if min_fraction_bits < 1: + raise ValueError("min_fraction bits has to be at least 1") + + if data.dtype == torch.float32: + fraction_bits = 23 + mask_dtype = torch.int32 + elif data.dtype == torch.float16: + fraction_bits = 10 + mask_dtype = torch.int16 + elif data.dtype == torch.bfloat16: + fraction_bits = 7 + mask_dtype = torch.int16 + else: + raise NotImplementedError( + f"precision variation not implemented for {data.dtype}" + ) + + no_of_bits_to_mask = torch.randint(0, fraction_bits - min_fraction_bits, data.shape) + mask = (~0 << no_of_bits_to_mask).to(dtype=mask_dtype, device=data.device) + augmented = (data.view(mask_dtype) & mask).view(data.dtype) + return augmented + + +class VaryPrecisionTransform: + """A transform randomly reducing the precision of its inputs.""" + + def __init__(self, *, min_fraction_bits: int = 1) -> None: + self.min_fraction_bits = min_fraction_bits + + def __call__( + self, batch: Float[torch.Tensor, "*dims"] + ) -> Float[torch.Tensor, "*dims"]: + return vary_precision(data=batch, min_fraction_bits=self.min_fraction_bits) diff --git a/application/jobs/stamp/getting-started.md b/application/jobs/stamp/getting-started.md new file mode 100644 index 00000000..87010516 --- /dev/null +++ b/application/jobs/stamp/getting-started.md @@ -0,0 +1,398 @@ +# Getting Started with Stamp + +This guide is designed to help you with your first steps using the stamp pipeline +to predict biomarkers and other attributes from whole slide images (WSIs). +To follow along, +you will need some WSIs, +a table mapping each of these slides to a patient +as well as some ground truth we will eventually train a neural network on. + +## Whole Slide Images + +The whole slide images have to be in any of the formats [supported by OpenSlide][openslide]. +For the next steps we assume that all these WSIs are stored in the same directory. +We will call this directory the _WSI directory_. + +[openslide]: https://openslide.org/#about-openslide "About OpenSlide" + +## Creating a Configuration File + +Stamp is configured using configuration files. +We recommend creating one configuration file per experiment +and storing in the same folder as the eventual results, +as this makes it easier to reconstruct which data and parameters a model was trained with later. + +The `stamp init` command creates a new configuration file with dummy values. +By default, it is created in `$PWD/config.yaml`, +but we can use the `--config` option to specify its location: + +```sh +# Create a directory to save our experiment results to +mkdir stamp-test-experiment +# Create a new config file in said directory +stamp --config stamp-test-experiment/config.yaml init +``` + +## Feature Extraction + +To do any kind of training on our data, we first have to convert it into a form +more easily usable by neural networks. +We do this using a _feature extractor_. +A feature extractor is a neural network has been trained on a large amount of WSIs +to extract extract the information relevant for our domain from images. +This way, we can compress WSIs into a more compact representation, +which in turn allows us to efficiently train machine learning models with them. + +Stamp currently supports the following feature extractors: + +- [ctranspath][ctranspath] +- [chief_ctranspath][chief_ctranspath] +- [DinoBloom][dinobloom] +- [CONCH][conch] +- [CONCHv1.5][conch1_5] +- [UNI][uni] +- [UNI2][uni2] +- [Virchow][virchow] +- [Virchow2][virchow2] +- [Gigapath][gigapath] +- [H-optimus-0][h_optimus_0] +- [H-optimus-1][h_optimus_1] +- [mSTAR][mstar] +- [MUSK][musk] +- [PLIP][plip] + +As some of the above require you to request access to the model on huggingface, +we will stick with ctranspath for this example. + +In order to use a feature extractor, +you also have to install their respective dependencies. +You can do so by specifying the feature extractor you want to use +when installing stamp: + +```sh +# Install stamp including the dependencies for all feature extractors +pip install "git+https://github.com/KatherLab/stamp@v2[all]" +``` + +Open the `stamp-test-experiment/config.yaml` we created in the last step +and modify the `output_dir`, `wsi_dir` and `cache_dir` entries +in the `preprocessing` section +to contain the absolute paths of the directory the configuration file resides in. +`wsi_dir` Needs to point to a path containing the WSIs you want to extract features from. + +The `cache_dir` will be used to save intermediate data. +Should you decide to try another feature extractor later, +using the same cache dir again will significantly speed up the extraction process. +If you will only extract features once, it can be set to `none`. + +```yaml +# stamp-test-experiment/config.yaml + +preprocessing: + output_dir: "/absolute/path/to/stamp-test-experiment" + wsi_dir: "/absolute/path/to/wsi_dir" + + # Other possible values are "mahmood-uni" and "mahmood-conch" + extractor: "ctranspath" + + # Having a cache dir will speed up extracting features multiple times, + # e.g. with different feature extractors. + # Optional. + cache_dir: "/absolute/path/to/stamp-test-experiment/../cache" + # If you do not want to use a cache, + # change the cache dir to the following: + # cache_dir: null + + # Device to run feature extraction on. + # Set this to "cpu" if you do not have a CUDA-capable GPU. + device: "cuda" + + # How many workers to use for tile extraction. Should be less or equal to + # the number of cores of your system. + max_workers: 8 +``` + +Extracting the features is then as easy as running + +```sh +stamp --config stamp-test-experiment/config.yaml preprocess +``` + +Depending on the size of your dataset and your hardware, +this process may take anything between a few hours and days. + +You can interrupt this process at any time. +It will continue where you stopped it the next time you run `stamp preprocess`. + +As the preprocessing is running, +you can see the output directory fill up with the features, saved in `.h5` files, +as well as `.jpg`s showing from which parts of the slide features are extracted. +Most of the background should be marked in red, +meaning ignored that it was ignored during feature extraction. + +> **If you are using the UNI or CONCH models** +> and working in an environment where your home directory storage is limited, +> you may want to also specify your huggingface storage directory +> by setting the `HF_HOME` environment variable: +> ```sh +> export HF_HOME=/path/to/directory/to/store/huggingface/data/in +> huggingface-cli login # only needs to be done once per $HF_HOME +> stamp -c stamp-test-experiment/config.yaml preprocess +> ``` + +[ctranspath]: https://www.sciencedirect.com/science/article/abs/pii/S1361841522002043 "Transformer-based unsupervised contrastive learning for histopathological image classification" + +[dinobloom]: https://github.com/marrlab/DinoBloom "DinoBloom: A Foundation Model for Generalizable Cell Embeddings in Hematology" + +[uni]: https://www.nature.com/articles/s41591-024-02857-3 "Towards a general-purpose foundation model for computational pathology" + +[uni2]: https://huggingface.co/MahmoodLab/UNI2-h + +[conch]: https://www.nature.com/articles/s41591-024-02856-4 "A visual-language foundation model for computational pathology" + +[conch1_5]: https://huggingface.co/MahmoodLab/conchv1_5 + +[virchow]: https://huggingface.co/paige-ai/Virchow "A foundation model for clinical-grade computational pathology and rare cancers detection" + +[virchow2]: https://huggingface.co/paige-ai/Virchow2 + +[chief_ctranspath]: https://github.com/hms-dbmi/CHIEF + +[gigapath]: https://huggingface.co/prov-gigapath/prov-gigapath + +[h_optimus_0]: https://huggingface.co/bioptimus/H-optimus-0 + +[h_optimus_1]: https://huggingface.co/bioptimus/H-optimus-1 + +[mstar]: https://huggingface.co/Wangyh/mSTAR + +[musk]: https://huggingface.co/xiangjx/musk + +[plip]: https://github.com/PathologyFoundation/plip + +[TITAN]: https://huggingface.co/MahmoodLab/TITAN + +[COBRA2]: https://huggingface.co/KatherLab/COBRA + +[EAGLE]: https://github.com/KatherLab/EAGLE + +[MADELEINE]: https://huggingface.co/MahmoodLab/madeleine + +## Doing Cross-Validation on the Data Set + +One way to quickly ascertain if a neural network can be trained to recognize a specific pattern +without the need to source a separate testing set +is to perform a cross-validation on it. +During a cross validation, +we train multiple models on a subset of the data, +testing its effectiveness on the held-out part of the data not used during training. +To perform a cross-validation, add the following lines to your `stamp-test-experiment/config.yaml`, +with `feature_dir` adapted to match the directory the `.h5` files were output to in the last step. +`clini_table` and `slide_table` both need to point to tables, +either in excel or `.csv` format, +with contents as described below. +Finally, `ground_truth_label` needs to contain the column name +of the data we want to train our model on. +Stamp only can be used to train neural networks for categorical targets. +We recommend explicitly setting the possible classes using the `categories` field. + +```yaml +# stamp-test-experiment/config.yaml + +crossval: + output_dir: "/absolute/path/to/stamp-test-experiment" + + # An excel (.xlsx) or CSV (.csv) table containing the clinical information of + # patients. Patients not present in this file will be ignored during training. + # Has to contain at least two columns, one titled "PATIENT", containing a patient ID, + # and a second column containing the categorical ground truths for that patient. + clini_table: "metadata-CRC/TCGA-CRC-DX_CLINI.xlsx" + + # Directory the extracted features are saved in. + feature_dir: "/absolute/path/to/stamp-test-experiment/xiyuewang-ctranspath-7c998680-112fc79c" + + # A table (.xlsx or .csv) relating every patient to their feature files. + # The table must contain at least two columns, one titled "PATIENT", + # containing the patient ID (matching those in the `clini_table`), and one + # called "FILENAME", containing the feature file path relative to `feature_dir`. + # Patient IDs not present in the clini table as well as non-existent feature + # paths are ignored. + slide_table: "slide.csv" + + # Name of the column from the clini table to train on. + ground_truth_label: "isMSIH" + + # Optional settings: + + # The categories occurring in the target label column of the clini table. + # If unspecified, they will be inferred from the table itself. + categories: [ "yes", "no" ] + + # Number of folds to split the data into for cross-validation + #n_splits: 5 +``` + +After specifying all the parameters of our cross-validation, +we can run it by invoking: + +```sh +stamp --config stamp-test-experiment/config.yaml crossval +``` + +## Generating Statistics + +After training and validating your model, you may want to generate statistics to evaluate its performance. +This can be done by adding a `statistics` section to your `stamp-test-experiment/config.yaml` file. +The configuration should look like this: + +```yaml +# stamp-test-experiment/config.yaml + +statistics: + output_dir: "/absolute/path/to/stamp-test-experiment/statistics" + + # Name of the target label. + ground_truth_label: "isMSIH" + + # A lot of the statistics are computed "one-vs-all", i.e. there needs to be + # a positive class to calculate the statistics for. + true_class: "yes" + + pred_csvs: + - "/absolute/path/to/stamp-test-experiment/split-0/patient-preds.csv" + - "/absolute/path/to/stamp-test-experiment/split-1/patient-preds.csv" + - "/absolute/path/to/stamp-test-experiment/split-2/patient-preds.csv" + - "/absolute/path/to/stamp-test-experiment/split-3/patient-preds.csv" + - "/absolute/path/to/stamp-test-experiment/split-4/patient-preds.csv" +``` + +To generate the statistics, run the following command: + +```sh +stamp --config stamp-test-experiment/config.yaml statistics +``` + +Afterwards, the `output_dir` should contain the following files: + +- `isMSIH-categorical-stats-individual.csv` contains statistical scores + for each individual split. +- `isMSIH-categorical-stats-aggregated.csv` contains the mean + as well as the 95% confidence interval for the statistical scores + for the splits. +- `roc-curve_isMSIH=yes.svg` and `pr-curve_isMSIH=yes.svg` + contain the ROC and precision recall curves of the splits. + +## Slide-Level Encoding + +Tile-Level features can be enconded into a single feature per slide, this is useful +when trying to capture global patterns across whole slides. + +STAMP currently supports the following encoders: + +- [CHIEF][CHIEF_CTRANSPATH] +- [TITAN] +- [GIGAPATH] +- [COBRA2] +- [EAGLE] +- [MADELEINE] + +Slide encoders take as input the already extracted tile-level features in the +preprocessing step. Each encoder accepts only certain extractors and most +work only on CUDA devices: + +| Encoder | Required Extractor | Compatible Devices | +|-----------|-------------------------------------|--------------------| +| CHIEF | CTRANSPATH, CHIEF-CTRANSPATH | CUDA only | +| TITAN | CONCH1.5 | CUDA, cpu, mps +| GIGAPATH | GIGAPATH | CUDA only +| COBRA2 | CONCH, UNI, VIRCHOW2 or H-OPTIMUS-0 | CUDA only +| EAGLE | CTRANSPATH, CHIEF-CTRANSPATH | CUDA only +| MADELEINE | CONCH | CUDA only + +As with feature extractors, most of these models require you to request +access. The following example uses CHIEF, which is available if you installed +STAMP with `uv sync --all-extras`. The configuration should look like this: + +```yaml +# stamp-test-experiment/config.yaml + +slide_encoding: + # Encoder to use for slide encoding. Possible options are "cobra", + # "eagle", "titan", "gigapath", "chief", "prism", "madeleine". + encoder: "chief" + + # Directory to save the output files. + output_dir: "/path/to/save/files/to" + + # Directory where the extracted features are stored. + feat_dir: "/path/your/extracted/features/are/stored/in" + + # Device to run slide encoding on ("cpu", "cuda", "cuda:0", etc.) + device: "cuda" + + # Optional settings: + # Directory where the aggregated features are stored. Needed for + # some encoders such as eagle (it requires virchow2 features). + #agg_feat_dir: "/path/your/aggregated/features/are/stored/in" + + # Add a hash of the entire preprocessing codebase in the feature folder name. + #generate_hash: True + ``` + +Don't forget to put in `feat_dir` a path containing, in this case, `ctranspath` or +`chief-ctranspath` tile-level features. Once everything is set, you can simply run: + +```sh +stamp --config stamp-test-experiment/config.yaml encode_slides +``` + +The output will be one `.h5` file per slide. + +## Patient-Level Encoding + +Even though the available encoders are designed for slide-level use, this +option concatenates the slides of a patient along the x-axis, creating a single +"virtual" slide that contains two blocks of tissue. The configuration is the same +except for `slide_table` which is required to link slides with patients. + +```yaml +# stamp-test-experiment/config.yaml + +patient_encoding: + # Encoder to use for patient encoding. Possible options are "cobra", + # "eagle", "titan", "gigapath", "chief", "prism", "madeleine". + encoder: "eagle" + + # Directory to save the output files. + output_dir: "/path/to/save/files/to" + + # Directory where the extracted features are stored. + feat_dir: "/path/your/extracted/features/are/stored/in" + + # A table (.xlsx or .csv) relating every slide to their feature files. + # The table must contain at least two columns, one titled "SLIDE", + # containing the slide ID, and one called "FILENAME", containing the feature file path relative to `feat_dir`. + slide_table: "/path/of/slide.csv" + + # Device to run slide encoding on ("cpu", "cuda", "cuda:0", etc.) + device: "cuda" + + # Optional settings: + patient_label: "PATIENT" + filename_label: "FILENAME" + + # Directory where the aggregated features are stored. Needed for + # some encoders such as eagle (it requires virchow2 features). + #agg_feat_dir: "/path/your/aggregated/features/are/stored/in" + + # Add a hash of the entire preprocessing codebase in the feature folder name. + #generate_hash: True + ``` + +Then run: + + ```sh +stamp --config stamp-test-experiment/config.yaml encode_patients +``` + +The output `.h5` features will have the patient's id as name. \ No newline at end of file diff --git a/application/jobs/3dcnn_ptl/meta.conf b/application/jobs/stamp/meta.conf similarity index 83% rename from application/jobs/3dcnn_ptl/meta.conf rename to application/jobs/stamp/meta.conf index 2675ccfd..13b8071b 100644 --- a/application/jobs/3dcnn_ptl/meta.conf +++ b/application/jobs/stamp/meta.conf @@ -1,4 +1,4 @@ -name = "3dcnn_ptl" +name = "stamp" resource_spec {} deploy_map { app = [ diff --git a/application/provision/project_HA.yml b/application/provision/project_HA.yml index 99cb5150..39e1deee 100644 --- a/application/provision/project_HA.yml +++ b/application/provision/project_HA.yml @@ -1,5 +1,5 @@ api_version: 3 -name: 3dcnn_ptl_HA +name: ODELIA_ternary_classification_HA description: > NVIDIA FLARE project YAML file for configuring a federated learning environment with High Availability (HA). focused on 3D convolutional neural networks (3D CNNs) using PyTorch Lightning (PTL). diff --git a/application/provision/project_MEVIS_test.yml b/application/provision/project_MEVIS_test.yml index 787a93d6..b4e962c1 100644 --- a/application/provision/project_MEVIS_test.yml +++ b/application/provision/project_MEVIS_test.yml @@ -4,15 +4,27 @@ description: > Test setup. participants: - - name: odeliatempvm.local + - name: odeliaswarmvm.local type: server org: MEVIS_Test fed_learn_port: 8002 admin_port: 8003 - - name: temporary_vm + - name: CAM type: client org: MEVIS_Test - - name: permanent_vm + - name: MHA + type: client + org: MEVIS_Test + - name: RUMC + type: client + org: MEVIS_Test + - name: UKA + type: client + org: MEVIS_Test + - name: UMCU + type: client + org: MEVIS_Test + - name: Centralized type: client org: MEVIS_Test - name: admin@mevis.odelia @@ -32,7 +44,7 @@ builders: config_folder: config # scheme for communication driver (currently supporting the default, grpc, only). - scheme: grpc + scheme: http # app_validator is used to verify if uploaded app has proper structures # if not set, no app_validator is included in fed_server.json @@ -54,7 +66,7 @@ builders: # overseer_exists: false args: - sp_end_point: odeliatempvm.local:8002:8003 + sp_end_point: odeliaswarmvm.local:8002:8003 - path: nvflare.lighter.impl.cert.CertBuilder - path: nvflare.lighter.impl.signature.SignatureBuilder diff --git a/application/provision/project_Odelia_allsites.yml b/application/provision/project_Odelia_allsites.yml new file mode 100644 index 00000000..8f1b1531 --- /dev/null +++ b/application/provision/project_Odelia_allsites.yml @@ -0,0 +1,98 @@ +api_version: 3 +name: odelia_1.0-dev.250718.ebb3b10_allsites_test +description: Odelia TUD server all collaborators clients on Odelia challenge dataset provision http based yaml file + +participants: + # change example.com to the FQDN of the server + - name: dl3.tud.de + type: server + org: TUD + fed_learn_port: 8002 + admin_port: 8003 + - name: TUD_1 + type: client + org: TUD + - name: TUD_2 + type: client + org: TUD + # Specifying listening_host will enable the creation of one pair of + # certificate/private key for this client, allowing the client to function + # as a server for 3rd-party integration. + # The value must be a hostname that the external trainer can reach via the network. + # listening_host: site-1-lh + - name: MEVIS_1 + type: client + org: MEVIS + - name: MEVIS_2 + type: client + org: MEVIS + - name: MEVIS_3 + type: client + org: MEVIS + - name: UKA_1 + type: client + org: UKA + - name: CAM_1 + type: client + org: Cambridge + - name: VHIO_1 + type: client + org: VHIO + - name: MHA_1 + type: client + org: MHA + - name: RSH_1 + type: client + org: RSH + - name: USZ_1 + type: client + org: USZ + - name: UMCU_1 + type: client + org: UMCU + - name: RUMC_1 + type: client + org: RUMC + - name: jiefu.zhu@tu-dresden.de + type: admin + org: TUD + role: project_admin + +# The same methods in all builders are called in their order defined in builders section +builders: + - path: nvflare.lighter.impl.workspace.WorkspaceBuilder + args: + template_file: master_template.yml + - path: nvflare.lighter.impl.template.TemplateBuilder + - path: nvflare.lighter.impl.static_file.StaticFileBuilder + args: + # config_folder can be set to inform NVIDIA FLARE where to get configuration + config_folder: config + + # scheme for communication driver (currently supporting the default, grpc, only). + scheme: http + + # app_validator is used to verify if uploaded app has proper structures + # if not set, no app_validator is included in fed_server.json + # app_validator: PATH_TO_YOUR_OWN_APP_VALIDATOR + + # when docker_image is set to a docker image name, docker.sh will be generated on server/client/admin + docker_image: jefftud/odelia:1.0-dev.250718.ebb3b10 + + # download_job_url is set to http://download.server.com/ as default in fed_server.json. You can override this + # to different url. + # download_job_url: http://download.server.com/ + + overseer_agent: + path: nvflare.ha.dummy_overseer_agent.DummyOverseerAgent + # if overseer_exists is true, args here are ignored. Provisioning + # tool will fill role, name and other local parameters automatically. + # if overseer_exists is false, args in this section will be used and the sp_end_point + # must match the server defined above in the format of SERVER_NAME:FL_PORT:ADMIN_PORT + # + overseer_exists: false + args: + sp_end_point: dl3.tud.de:8002:8003 + + - path: nvflare.lighter.impl.cert.CertBuilder + - path: nvflare.lighter.impl.signature.SignatureBuilder diff --git a/application/provision/project_nonHA.yml b/application/provision/project_nonHA.yml index 811c5f3d..e00297c7 100644 --- a/application/provision/project_nonHA.yml +++ b/application/provision/project_nonHA.yml @@ -1,5 +1,5 @@ api_version: 3 -name: 3dcnn_ptl_nonHA +name: ODELIA_ternary_classification_nonHA description: > NVIDIA FLARE project YAML file for configuring a federated learning environment without High Availability (HA). focused on 3D convolutional neural networks (3D CNNs) using PyTorch Lightning (PTL). diff --git a/assets/openvpn_configs/good_access/.gitignore b/assets/openvpn_configs/good_access/.gitignore new file mode 100644 index 00000000..2e66e21c --- /dev/null +++ b/assets/openvpn_configs/good_access/.gitignore @@ -0,0 +1 @@ +*.ovpn \ No newline at end of file diff --git a/assets/readme/README.developer.md b/assets/readme/README.developer.md new file mode 100644 index 00000000..fb6aafc3 --- /dev/null +++ b/assets/readme/README.developer.md @@ -0,0 +1,95 @@ +# Usage for MediSwarm and Application Code Developers + +## Versioning of ODELIA Docker Images + +If needed, update the version number in file [odelia_image.version](../../odelia_image.version). It will be used +automatically for the Docker image and startup kits. + +## Build the Docker Image and Startup Kits + +The Docker image contains all dependencies for administrative purposes (dashboard, command-line provisioning, admin +console, server) as well as for running the 3DCNN pipeline under the pytorch-lightning framework. +The project description specifies the swarm nodes etc. to be used for a swarm training. + + ```bash + cd MediSwarm + ./buildDockerImageAndStartupKits.sh -p application/provision/ + ``` + +1. Make sure you have no uncommitted changes. +2. If package versions are still not available, you may have to check what the current version is and update the + `Dockerfile` accordingly. Version numbers are hard-coded to avoid issues due to silently different versions being + installed. +3. After successful build (and after verifying that everything works as expected, i.e., local tests, building startup + kits, running local trainings in the startup kit), you can manually push the image to DockerHub, provided you have + the necessary rights. Make sure you are not re-using a version number for this purpose. + +## Running Local Tests + + ```bash + ./runTestsInDocker.sh + ``` + +You should see + +1. several expected errors and warnings printed from unit tests that should succeed overall, and a coverage report +2. output of a successful simulation run with two nodes +3. output of a successful proof-of-concept run run with two nodes +4. output of a set of startup kits being generated +5. output of a dummy training run using one of the startup kits +6. TODO update this to what the tests output now + +Optionally, uncomment running NVFlare unit tests in `_runTestsInsideDocker.sh`. + +## Distributing Startup Kits + +Distribute the startup kits to the clients. + +## Running the Startup Kits + +See [README.participant.md](./README.participant.md). + +### Configurable Parameters for docker.sh + +* The `docker.sh` script run by the swarm participants passes the following environment variables into the container automatically. +* You can override them to customize training behavior. +* Only do this for testing and debugging purposes! The startup kits are designed to ensure that all sites run the same training code, manipulating `docker.sh` might break this. + +| Environment Variable | Default | Description | +|----------------------|-----------------|----------------------------------------------------------------------| +| `SITE_NAME` | *from flag* | Name of your local site, e.g. `TUD_1`, passed via `--start_client` | +| `DATA_DIR` | *from flag* | Path to the host folder that contains your local data | +| `SCRATCH_DIR` | *from flag* | Path for saving training outputs and temporary files | +| `GPU_DEVICE` | `device=0` | GPU identifier to use inside the container (or `all`) | +| `MODEL` | `MST` | Model architecture, choices: `MST`, `ResNet` | +| `INSTITUTION` | `ODELIA` | Institution name, used to group experiment logs | +| `CONFIG` | `unilateral` | Configuration schema for dataset (e.g. label scheme) | +| `NUM_EPOCHS` | `1` (test mode) | Number of training epochs (used in preflight/local training) | +| `TRAINING_MODE` | derived | Internal use. Automatically set based on flags like `--start_client` | + +These are injected into the container as `--env` variables. You can modify their defaults by editing `docker.sh` or exporting before run: + +```bash +export MODEL=ResNet +export CONFIG=original +./docker.sh --data_dir $DATADIR --scratch_dir $SCRATCHDIR --GPU device=1 --start_client +``` + + +## Running the Application + +1. **CIFAR-10 example:** + See [README.md](../../application/jobs/cifar10/README.md) +2. **Minimal PyTorch CNN example:** + See [README.md](../../application/jobs/minimal_training_pytorch_cnn/README.md) +3. **3D CNN for classifying breast tumors:** + See [README.md](../../application/jobs/ODELIA_ternary_classification/README.md) + +## Contributing Application Code + +1. Take a look at application/jobs/minimal_training_pytorch_cnn for a minimal example how pytorch code can be adapted to + work with NVFlare +2. Take a look at application/jobs/ODELIA_ternary_classification for a more realistic example of pytorch code that can + run in the swarm +3. Use the local tests to check if the code is swarm-ready +4. TODO more detailed instructions diff --git a/assets/readme/README.operator.md b/assets/readme/README.operator.md new file mode 100644 index 00000000..101d5266 --- /dev/null +++ b/assets/readme/README.operator.md @@ -0,0 +1,85 @@ +# Usage for Swarm Operators + +## Setting up a Swarm + +Production mode is designed for secure, real-world deployments. It supports both local and remote setups, whether +on-premise or in the cloud. For more details, refer to +the [NVFLARE Production Mode](https://nvflare.readthedocs.io/en/2.4.1/real_world_fl.html). + +To set up production mode, follow these steps: + +## Edit `/etc/hosts` + +Ensure that your `/etc/hosts` file includes the correct host mappings. All hosts need to be able to communicate to the +server node. + +For example, add the following line (replace `` with the server's actual IP address): + +```plaintext + dl3.tud.de dl3 +``` + +## Create Startup Kits + +### Via Script (recommended) + +1. Use, e.g., the file `application/provision/project_MEVIS_test.yml`, adapt as needed (network protocol etc.) +2. Call `buildStartupKits.sh /path/to/project_configuration.yml` to build the startup kits +3. Startup kits are generated to `workspace//prod_00/` +4. Deploy startup kits to the respective server/clients + +### Via the Dashboard (not recommended) + +```bash +docker run -d --rm \ + --ipc=host -p 8443:8443 \ + --name=odelia_swarm_admin \ + -v /var/run/docker.sock:/var/run/docker.sock \ + \ + /bin/bash -c "nvflare dashboard --start --local --cred :" +``` + +using some credentials chosen for the swarm admin account. + +Access the dashboard in a web browser at `https://localhost:8443` log in with these credentials, and configure the +project: + +1. enter project short name, name, description +2. enter docker download link: jefftud/odelia: +3. if needed, enter dates +4. click save +5. Server Configuration > Server (DNS name): +6. click make project public + +#### Register client per site + +Access the dashboard at `https://:8443`. + +1. register a user +2. enter organziation (corresponding to the site) +3. enter role (e.g., org admin) +4. add a site (note: must not contain spaces, best use alphanumerical name) +5. specify number of GPUs and their memory + +#### Approve clients and finish configuration + +Access the dashboard at `https://localhost:8443` log in with the admin credentials. + +1. Users Dashboard > approve client user +2. Client Sites > approve client sites +3. Project Home > freeze project + +## Download startup kits + +After setting up the project admin configuration, server and clients can download their startup kits. Store the +passwords somewhere, they are only displayed once (or you can download them again). + +## Starting a Swarm Training + +1. Connect the *server* host to the VPN as described above. +2. Start the *server* startup kit using the respective `startup/docker.sh` script with the option to start the server +3. Provide the *client* startup kits to the swarm participants (be aware that email providers or other channels may + prevent encrypted archives) +4. Make sure the participants have started their clients via the respective startup kits, see below +5. Start the *admin* startup kit using the respective `startup/docker.sh` script to start the admin console +6. Deploy a job by `submit_job ` diff --git a/assets/readme/README.participant.md b/assets/readme/README.participant.md new file mode 100644 index 00000000..0ffcc77c --- /dev/null +++ b/assets/readme/README.participant.md @@ -0,0 +1,165 @@ +# MediSwarm Participant Guide + +This guide is for data scientists and medical research sites participating in a Swarm Learning project. + +## Prerequisites + +- Hardware: Min. 32GB RAM, 8 cores, NVIDIA GPU with 24GB VRAM, 4TB storage +- OS: Ubuntu 20.04 LTS +- Software: Docker, OpenVPN, Git + +## Setup + +1. Make sure your compute node satisfies the specification and has the necessary software installed. +2. Set up the VPN. A VPN is necessary so that the swarm nodes can communicate with each other securely across firewalls. For that purpose, + 1. Install OpenVPN + ```bash + sudo apt-get install openvpn + ``` + 2. If you have a graphical user interface(GUI), follow this guide to connect to the + VPN: [VPN setup guide(GUI).pdf](../VPN%20setup%20guide%28GUI%29.pdf) + 3. If you have a command line interface(CLI), follow this guide to connect to the + VPN: [VPN setup guide(CLI).md](../VPN%20setup%20guide%28CLI%29.md) + 4. You may want to clone this repository or selectively download VPN-related scripts for this purpose. + +## Prepare Dataset + +The dataset must be in the following format. + +### Folder Structure + + ```bash + + ├── data_unilateral + │ ├── ID_001_left + │ │ └── Sub_1.nii.gz + │ ├── ID_001_right + │ │ └── Sub_1.nii.gz + │ ├── ID_002_left + │ │ └── Sub_1.nii.gz + │ ├── ID_002_right + │ │ └── Sub_1.nii.gz + │ └── ... + └── metadata_unilateral + ├── annotation.csv + └── split.csv + ``` + +* The name of your site should usually end in `_1`, e.g., `UKA_1`, unless you participate with multiple nodes. +* `ID_001`, `ID_002` need to be unique identifiers in your dataset, not specifically of this format +* You might have additional images in the folder like `Pre.nii.gz`, `Post_1.nii.gz`, `Post_2.nii.gz`, `T2.nii.gz`, and you might have additional folders like `data_raw`, `data`, `metadata` etc. These will be ignored and should not cause problems. +* If you clone the repository, you will find a script that generates a synthetic dataset as an example. + +### Table Format + +#### Annotation + +* `split.csv` defines the class labels +* The file contains the columns `UID`, `PatientID`, `Age`, `Lesion` + * `UID` is the identifier used in the folder name, e.g., `ID_001_left`. + * `PatientID` is the identifier of the patient, in this case, `ID_001`. + * `Age` is the age of the patient at the time of the scan in days. + * `Lesion` is 0 for no lesion, 1 for benign lesion, and 2 for malicious lesion. + +#### Split + +* `split.csv` defines the training/validation/test split. +* These splits are hard-coded rather than randomized during training in order to have consistent and documented splits. +* The file contains the columns `UID`, `Split`, and `Fold`. + * `UID` is the identifier used in the folder name, e.g., `ID_001_left`. + * `Split` is either `train`, `val`, or `test`. The test set is currently ignored. + * `Fold` is the 0-based index of the fold (for a potential cross-validation). + + +## Prepare Training Participation + +1. Extract startup kit provided by swarm operator + +### Local Testing on Your Data + +1. Directories + ```bash + export SITE_NAME= + export DATADIR= + export SCRATCHDIR= + ``` +2. From the directory where you unpacked the startup kit, + ```bash + cd $SITE_NAME/startup + ``` +3. Verify that your Docker/GPU setup is working + ```bash + ./docker.sh --scratch_dir $SCRATCHDIR --GPU device=0 --dummy_training 2>&1 | tee dummy_training_console_output.txt + ``` + * This will pull the Docker image, which might take a while. + * If you have multiple GPUs and 0 is busy, use a different one. + * The “training” itself should take less than minute and does not yield a meaningful classification performance. +4. Verify that your local data can be accessed and the model can be trained locally + ```bash + ./docker.sh --data_dir $DATADIR --scratch_dir $SCRATCHDIR --GPU device=0 --preflight_check 2>&1 | tee preflight_check_console_output.txt + ``` + * Training time depends on the size of the local dataset. + +### Run Local Training + +To have a baseline for swarm training, train the same model in a comparable way on the local data only. + +1. From the directory where you unpacked the startup kit (unless you just ran the pre-flight check) + ```bash + cd $SITE_NAME/startup + ``` +2. Start local training + ```bash + ./docker.sh --data_dir $DATADIR --scratch_dir $SCRATCHDIR --GPU device=0 --local_training 2>&1 | tee local_training_console_output.txt + ``` + * This currently runs 100 epochs (somewhat comparable to 20 rounds with 5 epochs each in the swarm case). +3. Output files + * Same as for the swarm training (see below). + +### Start Swarm Node + +#### VPN + +1. Connect to VPN as described in [VPN setup guide(GUI).pdf](../VPN%20setup%20guide%28GUI%29.pdf) (GUI) or [VPN setup guide(CLI).md](../VPN%20setup%20guide%28CLI%29.md) (command line). + +#### Start the Client + +1. From the directory where you unpacked the startup kit: + ```bash + cd $SITE_NAME/startup # Skip this if you just ran the pre-flight check + ``` + +2. Start the client: + ```bash + ./docker.sh --data_dir $DATADIR --scratch_dir $SCRATCHDIR --GPU device=0 --start_client + ``` + If you have multiple GPUs and 0 is busy, use a different one. + +3. Console output is captured in `nohup.out`, which may have been created with limited permissions in the container, so + make it readable if necessary: + ```bash + sudo chmod a+r nohup.out + ``` + +4. Output files: + - **Training logs and checkpoints** are saved under: + ``` + $SCRATCHDIR/runs/$SITE_NAME// + ``` + - **Best checkpoint** usually saved as `best.ckpt` or `last.ckpt` + - TODO describe prediction results once implemented + - **TensorBoard logs** are stored in their respective folders inside the run directory + +5. (Optional) You can verify that the container is running properly: + ```bash + docker ps # Check if odelia_swarm_client_$SITE_NAME is listed + nvidia-smi # Check if the GPU is busy training (it will be idling while waiting for model transfer) + tail -f nohup.out # Follow training log + ``` +For any issues, check if the commands above point to problems and contact your Swarm Operator. + +## Troubleshooting + +* Image files need to have the correct file name including capitalization +* The directories listed as identifiers in the tables `annotation.csv` and `split.csv` should all be present, only those directories should be present +* The tables should not have additional or duplicate columns, entries need to have the correct captitalization diff --git a/assets/readme/README_old.md b/assets/readme/README_old.md new file mode 100644 index 00000000..516d4d0b --- /dev/null +++ b/assets/readme/README_old.md @@ -0,0 +1,362 @@ +# Introduction + +MediSwarm is an open-source project dedicated to advancing medical deep learning through swarm intelligence, leveraging +the NVFlare platform. Developed in collaboration with the Odelia consortium, this repository aims to create a +decentralized and collaborative framework for medical research and applications. + +## Key Features + +- **Swarm Learning:** Utilizes swarm intelligence principles to improve model performance and adaptability. +- **NVFlare Integration:** Built on NVFlare, providing robust and scalable federated learning capabilities. +- **Data Privacy:** Ensures data security and compliance with privacy regulations by keeping data local to each + institution. +- **Collaborative Research:** Facilitates collaboration among medical researchers and institutions for enhanced + outcomes. +- **Extensible Framework:** Designed to support various medical applications and easily integrate with existing + workflows. + +## Prerequisites + +### Hardware recommendations + +* 64 GB of RAM (32 GB is the absolute minimum) +* 16 CPU cores (8 is the absolute minimum) +* an NVIDIA GPU with 48 GB of RAM (24 GB is the minimum) +* 8 TB of Storage (4 TB is the absolute minimum) + +We demonstrate that the system can run on lightweight hardware like this. For less than 10k EUR, you can configure +systems from suppliers like Lambda, Dell Precision, and Dell Alienware. + +### Operating System + +* Ubuntu 20.04 LTS + +### Software + +* Docker +* openvpn +* git + +### Cloning the repository + + ```bash + git clone https://github.com/KatherLab/MediSwarm.git --recurse-submodules + ``` + +* The last argument is necessary because we are using a git submodule for the (ODELIA fork of + NVFlare)[https://github.com/KatherLab/NVFlare_MediSwarm] +* If you have cloned it without this argument, use `git submodule update --init --recursive` + +### VPN + +A VPN is necessary so that the swarm nodes can communicate with each other securely across firewalls. For that purpose, + +1. Install OpenVPN + ```bash + sudo apt-get install openvpn + ``` +2. If you have a graphical user interface(GUI), follow this guide to connect to the + VPN: [VPN setup guide(GUI).pdf](assets/VPN%20setup%20guide%28GUI%29.pdf) +3. If you have a command line interface(CLI), follow this guide to connect to the + VPN: [VPN setup guide(CLI).md](assets/VPN%20setup%20guide%28CLI%29.md) + +# Usage for Swarm Participants + +## Setup + +1. Make sure your compute node satisfies the specification and has the necessary software installed. +2. Clone the repository and connect the client node to the VPN as described above. TODO is cloning the repository + necessary for swarm participants? +3. TODO anything else? + +## Prepare Dataset + +1. see Step 3: Prepare Data in (this document)[application/jobs/ODELIA_ternary_classification/app/scripts/README.md] + +## Prepare Training Participation + +1. Extract startup kit provided by swarm operator + +## Run Pre-Flight Check + +1. Directories + ```bash + export SITE_NAME= # TODO should be defined above, also needed for dataset location + export DATADIR= + export SCRATCHDIR= + ``` +2. From the directory where you unpacked the startup kit, + ```bash + cd $SITE_NAME/startup + ``` +3. Verify that your Docker/GPU setup is working + ```bash + ./docker.sh --scratch_dir $SCRATCHDIR --GPU device=0 --dummy_training + ``` + * This will pull the Docker image, which might take a while. + * If you have multiple GPUs and 0 is busy, use a different one. + * The “training” itself should take less than minute and does not yield a meaningful classification performance. +4. Verify that your local data can be accessed and the model can be trained locally + ```bash + ./docker.sh --data_dir $DATADIR --scratch_dir $SCRATCHDIR --GPU device=0 --preflight_check + ``` + * Training time depends on the size of the local dataset. + +## Configurable Parameters for docker.sh + +TODO consider what should be described and recommended as configurable here, given that the goal of the startup kits is +to ensure everyone runs the same training + +When launching the client using `./docker.sh`, the following environment variables are automatically passed into the +container. You can override them to customize training behavior: + +| Environment Variable | Default | Description | +|----------------------|-----------------|----------------------------------------------------------------------| +| `SITE_NAME` | *from flag* | Name of your local site, e.g. `TUD_1`, passed via `--start_client` | +| `DATA_DIR` | *from flag* | Path to the host folder that contains your local data | +| `SCRATCH_DIR` | *from flag* | Path for saving training outputs and temporary files | +| `GPU_DEVICE` | `device=0` | GPU identifier to use inside the container (or `all`) | +| `MODEL` | `MST` | Model architecture, choices: `MST`, `ResNet` | +| `INSTITUTION` | `ODELIA` | Institution name, used to group experiment logs | +| `CONFIG` | `unilateral` | Configuration schema for dataset (e.g. label scheme) | +| `NUM_EPOCHS` | `1` (test mode) | Number of training epochs (used in preflight/local training) | +| `TRAINING_MODE` | derived | Internal use. Automatically set based on flags like `--start_client` | + +These are injected into the container as `--env` variables. You can modify their defaults by editing `docker.sh` or +exporting before run: + +```bash +export MODEL=ResNet +export CONFIG=original +./docker.sh --data_dir $DATADIR --scratch_dir $SCRATCHDIR --GPU device=1 --start_client +``` + +## Start Swarm Node + +1. From the directory where you unpacked the startup kit: + ```bash + cd $SITE_NAME/startup # Skip this if you just ran the pre-flight check + ``` + +2. Start the client: + ```bash + ./docker.sh --data_dir $DATADIR --scratch_dir $SCRATCHDIR --GPU device=0 --start_client + ``` + If you have multiple GPUs and 0 is busy, use a different one. + +3. Console output is captured in `nohup.out`, which may have been created with limited permissions in the container, so + make it readable if necessary: + ```bash + sudo chmod a+r nohup.out + ``` + +4. Output files: + - **Training logs and checkpoints** are saved under: + ``` + $SCRATCHDIR/runs/$SITE_NAME// + ``` + - **Best checkpoint** usually saved as `best.ckpt` or `last.ckpt` + - **Prediction results**, if enabled, will appear in subfolders of the same directory + - **TensorBoard logs**, if activated, are stored in their respective folders inside the run directory + - TODO what is enabled/activated should be hard-coded, adapt accordingly + +5. (Optional) You can verify that the container is running properly: + ```bash + docker ps # Check if odelia_swarm_client_$SITE_NAME is listed + nvidia-smi # Check if the GPU is busy training (it will be idling while waiting for model transfer) + tail -f nohup.out # Follow training log + ``` + +## Run Local Training + +1. From the directory where you unpacked the startup kit + ```bash + cd $SITE_NAME/startup + ``` +2. Start local training + ```bash + /docker.sh --data_dir $DATADIR --scratch_dir $SCRATCHDIR --GPU all --local_training + ``` + * TODO update when handling of the number of epochs has been implemented +3. Output files + * TODO describe + +# Usage for MediSwarm and Application Code Developers + +## Versioning of ODELIA Docker Images + +If needed, update the version number in file (odelia_image.version)[odelia_image.version]. It will be used automatically +for the Docker image and startup kits. + +## Build the Docker Image and Startup Kits + +The Docker image contains all dependencies for administrative purposes (dashboard, command-line provisioning, admin +console, server) as well as for running the 3DCNN pipeline under the pytorch-lightning framework. +The project description specifies the swarm nodes etc. to be used for a swarm training. + +```bash +cd MediSwarm +./buildDockerImageAndStartupKits.sh -p application/provision/ +``` + +1. Make sure you have no uncommitted changes. +2. If package versions are still not available, you may have to check what the current version is and update the + `Dockerfile` accordingly. Version numbers are hard-coded to avoid issues due to silently different versions being + installed. +3. After successful build (and after verifying that everything works as expected, i.e., local tests, building startup + kits, running local trainings in the startup kit), you can manually push the image to DockerHub, provided you have + the necessary rights. Make sure you are not re-using a version number for this purpose. + +## Running Local Tests + + ```bash + ./runTestsInDocker.sh + ``` + +You should see + +1. several expected errors and warnings printed from unit tests that should succeed overall, and a coverage report +2. output of a successful simulation run with two nodes +3. output of a successful proof-of-concept run run with two nodes +4. output of a set of startup kits being generated +5. output of a dummy training run using one of the startup kits +6. TODO update this to what the tests output now + +Optionally, uncomment running NVFlare unit tests in `_runTestsInsideDocker.sh`. + +## Distributing Startup Kits + +Distribute the startup kits to the clients. + +## Running the Application + +1. **CIFAR-10 example:** + See [cifar10/README.md](application/jobs/cifar10/README.md) +2. **Minimal PyTorch CNN example:** + See [application/jobs/minimal_training_pytorch_cnn/README.md](application/jobs/minimal_training_pytorch_cnn/README.md) +3. **3D CNN for classifying breast tumors:** + See [ODELIA_ternary_classification/README.md](application/jobs/ODELIA_ternary_classification/README.md) + +## Contributing Application Code + +1. Take a look at application/jobs/minimal_training_pytorch_cnn for a minimal example how pytorch code can be adapted to + work with NVFlare +2. Take a look at application/jobs/ODELIA_ternary_classification for a more relastic example of pytorch code that can + run in the swarm +3. Use the local tests to check if the code is swarm-ready +4. TODO more detailed instructions + +# Usage for Swarm Operators + +## Setting up a Swarm + +Production mode is designed for secure, real-world deployments. It supports both local and remote setups, whether +on-premise or in the cloud. For more details, refer to +the [NVFLARE Production Mode](https://nvflare.readthedocs.io/en/2.4.1/real_world_fl.html). + +To set up production mode, follow these steps: + +## Edit `/etc/hosts` + +Ensure that your `/etc/hosts` file includes the correct host mappings. All hosts need to be able to communicate to the +server node. + +For example, add the following line (replace `` with the server's actual IP address): + +```plaintext + dl3.tud.de dl3 +``` + +## Create Startup Kits + +### Via Script (recommended) + +1. Use, e.g., the file `application/provision/project_MEVIS_test.yml`, adapt as needed (network protocol etc.) +2. Call `buildStartupKits.sh /path/to/project_configuration.yml` to build the startup kits +3. Startup kits are generated to `workspace//prod_00/` +4. Deploy startup kits to the respective server/clients + +### Via the Dashboard (not recommended) + +```bash +docker run -d --rm \ + --ipc=host -p 8443:8443 \ + --name=odelia_swarm_admin \ + -v /var/run/docker.sock:/var/run/docker.sock \ + \ + /bin/bash -c "nvflare dashboard --start --local --cred :" +``` + +using some credentials chosen for the swarm admin account. + +Access the dashboard in a web browser at `https://localhost:8443` log in with these credentials, and configure the +project: + +1. enter project short name, name, description +2. enter docker download link: jefftud/odelia: +3. if needed, enter dates +4. click save +5. Server Configuration > Server (DNS name): +6. click make project public + +#### Register client per site + +Access the dashboard at `https://:8443`. + +1. register a user +2. enter organziation (corresponding to the site) +3. enter role (e.g., org admin) +4. add a site (note: must not contain spaces, best use alphanumerical name) +5. specify number of GPUs and their memory + +#### Approve clients and finish configuration + +Access the dashboard at `https://localhost:8443` log in with the admin credentials. + +1. Users Dashboard > approve client user +2. Client Sites > approve client sites +3. Project Home > freeze project + +## Download startup kits + +After setting up the project admin configuration, server and clients can download their startup kits. Store the +passwords somewhere, they are only displayed once (or you can download them again). + +## Starting a Swarm Training + +1. Connect the *server* host to the VPN as described above. +2. Start the *server* startup kit using the respective `startup/docker.sh` script with the option to start the server +3. Provide the *client* startup kits to the swarm participants (be aware that email providers or other channels may + prevent encrypted archives) +4. Make sure the participants have started their clients via the respective startup kits, see below +5. Start the *admin* startup kit using the respective `startup/docker.sh` script to start the admin console +6. Deploy a job by `submit_job ` + +# License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +# Maintainers + +[Jeff](https://github.com/Ultimate-Storm) +[Ole Schwen](mailto:ole.schwen@mevis.fraunhofer.de) +[Steffen Renisch](mailto:steffen.renisch@mevis.fraunhofer.de) + +# Contributing + +Feel free to dive in! [Open an issue](https://github.com/KatherLab/MediSwarm/issues) or submit pull requests. + +# Credits + +This project utilizes platforms and resources from the following repositories: + +- **[NVFLARE](https://github.com/NVIDIA/NVFlare)**: NVFLARE (NVIDIA Federated Learning Application Runtime Environment) + is an open-source framework that provides a robust and scalable platform for federated learning applications. We have + integrated NVFLARE to efficiently handle the federated learning aspects of our project. + +Special thanks to the contributors and maintainers of these repositories for their valuable work and support. + +--- + +For more details about NVFLARE and its features, please visit +the [NVFLARE GitHub repository](https://github.com/NVIDIA/NVFlare). diff --git a/buildDockerImageAndStartupKits.sh b/buildDockerImageAndStartupKits.sh index 30f330c8..3e3be724 100755 --- a/buildDockerImageAndStartupKits.sh +++ b/buildDockerImageAndStartupKits.sh @@ -3,47 +3,93 @@ set -e # make sure we are building from a state without local changes -if ! git diff --quiet || ! git diff --staged --quiet ; then - echo "Local changes exist, aborting" - exit 1 -fi -DOCKER_BUILD_ARGS="--no-cache --progress=plain"; +DOCKER_BUILD_ARGS="--no-cache --progress=plain" +DOCKERFILE_SUFFIX="ODELIA" +PROJECT_FILE="" while [[ "$#" -gt 0 ]]; do case $1 in - -p) PROJECT_FILE="$2"; shift ;; - --use-docker-cache) DOCKER_BUILD_ARGS="";; + -p) PROJECT_FILE="$2"; shift ;; + --use-docker-cache) DOCKER_BUILD_ARGS="" ;; + -d) DOCKERFILE_SUFFIX="$2"; shift ;; *) echo "Unknown parameter passed: $1"; exit 1 ;; esac shift done if [ -z "$PROJECT_FILE" ]; then - echo "Usage: buildDockerImageAndStartupKits.sh -p [--use-docker-cache]" + echo "Usage: buildDockerImageAndStartupKits.sh -p [-d ] [--use-docker-cache]" exit 1 fi -VERSION=`./getVersionNumber.sh` -DOCKER_IMAGE=jefftud/odelia:$VERSION +VERSION=$(./getVersionNumber.sh) +DOCKER_IMAGE="jefftud/${DOCKERFILE_SUFFIX}:$VERSION" +DOCKERFILE_PATH="docker_config/Dockerfile_${DOCKERFILE_SUFFIX}" + +if [[ ! -f $DOCKERFILE_PATH ]]; then + echo "Error: Dockerfile $DOCKERFILE_PATH does not exist." + exit 1 +fi # prepare clean version of source code repository clone for building Docker image + CWD=`pwd` CLEAN_SOURCE_DIR=`mktemp -d` -cp -r . $CLEAN_SOURCE_DIR/ -cd $CLEAN_SOURCE_DIR +mkdir $CLEAN_SOURCE_DIR/MediSwarm +rsync -ax --exclude workspace . $CLEAN_SOURCE_DIR/MediSwarm/ +cd $CLEAN_SOURCE_DIR/MediSwarm git clean -x -q -f . cd docker_config/NVFlare git clean -x -q -f . cd ../.. -rm .git -rf +rm -rf .git chmod a+rX . -R +sed -i 's#__REPLACED_BY_CURRENT_VERSION_NUMBER_WHEN_BUILDING_DOCKER_IMAGE__#'$VERSION'#' docker_config/master_template.yml cd $CWD -docker build $DOCKER_BUILD_ARGS -t $DOCKER_IMAGE $CLEAN_SOURCE_DIR -f docker_config/Dockerfile_ODELIA -rm -rf $CLEAN_SOURCE_DIR +# prepare pre-trained model weights for being included in Docker image + +if [[ "$DOCKERFILE_SUFFIX" == "ODELIA" ]]; then + echo "Preparing DINOv2 model weights for ODELIA build..." + + MODEL_WEIGHTS_FILE='docker_config/torch_home_cache/hub/checkpoints/dinov2_vits14_pretrain.pth' + MODEL_LICENSE_FILE='docker_config/torch_home_cache/hub/facebookresearch_dinov2_main/LICENSE' -./_buildStartupKits.sh $PROJECT_FILE $VERSION + if [[ ! -f $MODEL_WEIGHTS_FILE || ! -f $MODEL_LICENSE_FILE ]]; then + echo "Pre-trained model not available. Attempting download" + HUBDIR=$(dirname $(dirname $MODEL_LICENSE_FILE)) + mkdir -p "$(dirname $MODEL_WEIGHTS_FILE)" + wget https://dl.fbaipublicfiles.com/dinov2/dinov2_vits14/dinov2_vits14_pretrain.pth -O "$MODEL_WEIGHTS_FILE" + wget https://github.com/facebookresearch/dinov2/archive/refs/heads/main.zip -O /tmp/dinov2.zip + unzip /tmp/dinov2.zip -d "$HUBDIR" + mv "$HUBDIR/dinov2-main" "$HUBDIR/$(basename $(dirname $MODEL_LICENSE_FILE))" + touch "$HUBDIR/trusted_list" + fi + + if echo 2e405cee1bad14912278296d4f42e993 $MODEL_WEIGHTS_FILE | md5sum --check - \ + && echo 153d2db1c329326a2d9f881317ea942e $MODEL_LICENSE_FILE | md5sum --check -; then + cp -r ./docker_config/torch_home_cache "$CLEAN_SOURCE_DIR/torch_home_cache" + chmod a+rX "$CLEAN_SOURCE_DIR/torch_home_cache" -R + else + echo "Model file checksum verification failed" + exit 1 + fi +else + echo "Skipping pre-trained model download; not required for Dockerfile_${DOCKERFILE_SUFFIX}" +fi + + +# build the Docker image + +docker build $DOCKER_BUILD_ARGS -t "$DOCKER_IMAGE" "$CLEAN_SOURCE_DIR" -f "$DOCKERFILE_PATH" + +echo "Docker image $DOCKER_IMAGE built successfully" +echo "./_buildStartupKits.sh $PROJECT_FILE $VERSION" +./_buildStartupKits.sh "$PROJECT_FILE" "$VERSION" +echo "Startup kits built successfully" + +rm -rf $CLEAN_SOURCE_DIR echo "If you wish, manually push $DOCKER_IMAGE now" diff --git a/docker.sh b/docker.sh deleted file mode 100755 index 50ff1bb5..00000000 --- a/docker.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# docker run script for FL client -# local data directory -: ${MY_DATA_DIR:="/mnt/swarm_alpha/odelia_dataset_divided"} -# The syntax above is to set MY_DATA_DIR to /home/flcient/data if this -# environment variable is not set previously. -# Therefore, users can set their own MY_DATA_DIR with -# export MY_DATA_DIR=$SOME_DIRECTORY -# before running docker.sh - -# for all gpus use line below -GPU2USE='--gpus=all' -# for 2 gpus use line below -#GPU2USE='--gpus=2' -# for specific gpus as gpu#0 and gpu#2 use line below -#GPU2USE='--gpus="device=0,2"' -# to use host network, use line below -NETARG="--net=host" -# FL clients do not need to open ports, so the following line is not needed. -#NETARG="-p 443:443 -p 8003:8003" -DOCKER_IMAGE=jefftud/nvflare-pt-dev:3dcnn -echo "Starting docker with $DOCKER_IMAGE" -mode="${1:--r}" -if [ $mode = "-d" ] -then - docker run -d --rm --name=mediswarm_root $GPU2USE -u $(id -u):$(id -g) \ - -v /etc/passwd:/etc/passwd -v /etc/group:/etc/group -v $DIR/..:/workspace/ \ - -v $MY_DATA_DIR:/data/:ro -w /workspace/ --ipc=host $NETARG $DOCKER_IMAGE \ - /bin/bash -c "python -u -m nvflare.private.fed.app.client.client_train -m /workspace -s fed_client.json --set uid=mediswarm_root secure_train=true config_folder=config org=tud" -else - docker run --rm -it --name=mediswarm_root $GPU2USE -u $(id -u):$(id -g) \ - -v /etc/passwd:/etc/passwd -v /etc/group:/etc/group -v $DIR/..:/workspace/ \ - -v $MY_DATA_DIR:/data/:ro -w /workspace/ --ipc=host $NETARG $DOCKER_IMAGE /bin/bash -fi diff --git a/docker_config/Dockerfile_ODELIA b/docker_config/Dockerfile_ODELIA index fd6023e1..83253588 100644 --- a/docker_config/Dockerfile_ODELIA +++ b/docker_config/Dockerfile_ODELIA @@ -1,5 +1,5 @@ # Use the specified PyTorch image as the base -ARG PYTORCH_IMAGE=pytorch/pytorch:2.2.0-cuda12.1-cudnn8-runtime +ARG PYTORCH_IMAGE=pytorch/pytorch:2.2.2-cuda12.1-cudnn8-runtime FROM ${PYTORCH_IMAGE} # Specify the NVFlare version @@ -12,13 +12,100 @@ ENV PYTHON_VERSION=3.10.14 # Install updates of installed packages RUN apt update -RUN apt install -y apt=2.4.14 apt-utils=2.4.14 libapt-pkg6.0=2.4.14 +RUN apt install -y \ + apt=2.4.14 \ + apt-utils=2.4.14 \ + libapt-pkg6.0=2.4.14 # Update versions of installed packages -RUN apt install -y base-files=12ubuntu4.7 bash=5.1-6ubuntu1.1 bsdutils=1:2.37.2-4ubuntu3.4 ca-certificates=20240203~22.04.1 coreutils=8.32-4.1ubuntu1.2 dpkg=1.21.1ubuntu2.3 e2fsprogs=1.46.5-2ubuntu1.2 gpgv=2.2.27-3ubuntu2.3 libblkid1=2.37.2-4ubuntu3.4 libc-bin=2.35-0ubuntu3.10 libc-dev-bin=2.35-0ubuntu3.10 libc6-dev=2.35-0ubuntu3.10 libc6=2.35-0ubuntu3.10 libcap2=1:2.44-1ubuntu0.22.04.2 libcom-err2=1.46.5-2ubuntu1.2 libext2fs2=1.46.5-2ubuntu1.2 libgnutls30=3.7.3-4ubuntu1.6 libgssapi-krb5-2=1.19.2-2ubuntu0.7 libk5crypto3=1.19.2-2ubuntu0.7 libkrb5-3=1.19.2-2ubuntu0.7 libkrb5support0=1.19.2-2ubuntu0.7 libmount1=2.37.2-4ubuntu3.4 libpam-modules-bin=1.4.0-11ubuntu2.5 libpam-modules=1.4.0-11ubuntu2.5 libpam-runtime=1.4.0-11ubuntu2.5 libpam0g=1.4.0-11ubuntu2.5 libseccomp2=2.5.3-2ubuntu3~22.04.1 libsmartcols1=2.37.2-4ubuntu3.4 libss2=1.46.5-2ubuntu1.2 libssl3=3.0.2-0ubuntu1.19 libsystemd0=249.11-0ubuntu3.16 libtasn1-6=4.18.0-4ubuntu0.1 libudev1=249.11-0ubuntu3.16 libuuid1=2.37.2-4ubuntu3.4 linux-libc-dev=5.15.0-141.151 logsave=1.46.5-2ubuntu1.2 mount=2.37.2-4ubuntu3.4 openssl=3.0.2-0ubuntu1.19 util-linux=2.37.2-4ubuntu3.4 +RUN apt install -y \ + base-files=12ubuntu4.7 \ + bash=5.1-6ubuntu1.1 \ + bsdutils=1:2.37.2-4ubuntu3.4 \ + ca-certificates=20240203~22.04.1 \ + coreutils=8.32-4.1ubuntu1.2 \ + dpkg=1.21.1ubuntu2.3 \ + e2fsprogs=1.46.5-2ubuntu1.2 \ + gpgv=2.2.27-3ubuntu2.4 \ + libblkid1=2.37.2-4ubuntu3.4 \ + libc-bin=2.35-0ubuntu3.10 \ + libc-dev-bin=2.35-0ubuntu3.10 \ + libc6-dev=2.35-0ubuntu3.10 \ + libc6=2.35-0ubuntu3.10 \ + libcap2=1:2.44-1ubuntu0.22.04.2 \ + libcom-err2=1.46.5-2ubuntu1.2 \ + libext2fs2=1.46.5-2ubuntu1.2 \ + libgnutls30=3.7.3-4ubuntu1.7 \ + libgssapi-krb5-2=1.19.2-2ubuntu0.7 \ + libk5crypto3=1.19.2-2ubuntu0.7 \ + libkrb5-3=1.19.2-2ubuntu0.7 \ + libkrb5support0=1.19.2-2ubuntu0.7 \ + libmount1=2.37.2-4ubuntu3.4 \ + libpam-modules-bin=1.4.0-11ubuntu2.6 \ + libpam-modules=1.4.0-11ubuntu2.6 \ + libpam-runtime=1.4.0-11ubuntu2.6 \ + libpam0g=1.4.0-11ubuntu2.6 \ + libseccomp2=2.5.3-2ubuntu3~22.04.1 \ + libsmartcols1=2.37.2-4ubuntu3.4 \ + libss2=1.46.5-2ubuntu1.2 \ + libssl3=3.0.2-0ubuntu1.19 \ + libsystemd0=249.11-0ubuntu3.16 \ + libtasn1-6=4.18.0-4ubuntu0.1 \ + libudev1=249.11-0ubuntu3.16 \ + libuuid1=2.37.2-4ubuntu3.4 \ + linux-libc-dev=5.15.0-151.161 \ + logsave=1.46.5-2ubuntu1.2 \ + mount=2.37.2-4ubuntu3.4 \ + openssl=3.0.2-0ubuntu1.19 \ + util-linux=2.37.2-4ubuntu3.4 # Install apt-transport-https curl gnupg lsb-release zip and dependencies at defined versions -RUN apt install -y apt-transport-https=2.4.14 curl=7.81.0-1ubuntu1.20 dirmngr=2.2.27-3ubuntu2.3 distro-info-data=0.52ubuntu0.9 gnupg-l10n=2.2.27-3ubuntu2.3 gnupg-utils=2.2.27-3ubuntu2.3 gnupg=2.2.27-3ubuntu2.3 gpg-agent=2.2.27-3ubuntu2.3 gpg-wks-client=2.2.27-3ubuntu2.3 gpg-wks-server=2.2.27-3ubuntu2.3 gpg=2.2.27-3ubuntu2.3 gpgconf=2.2.27-3ubuntu2.3 gpgsm=2.2.27-3ubuntu2.3 libassuan0=2.5.5-1build1 libbrotli1=1.0.9-2build6 libcurl4=7.81.0-1ubuntu1.20 libexpat1=2.4.7-1ubuntu0.6 libksba8=1.6.0-2ubuntu0.2 libldap-2.5-0=2.5.19+dfsg-0ubuntu0.22.04.1 libldap-common=2.5.19+dfsg-0ubuntu0.22.04.1 libmpdec3=2.5.1-2build2 libnghttp2-14=1.43.0-1ubuntu0.2 libnpth0=1.6-3build2 libpsl5=0.21.0-1.2build2 libpython3-stdlib=3.10.6-1~22.04.1 libpython3.10-minimal=3.10.12-1~22.04.9 libpython3.10-stdlib=3.10.12-1~22.04.9 libreadline8=8.1.2-1 librtmp1=2.4+20151223.gitfa8646d.1-2build4 libsasl2-2=2.1.27+dfsg2-3ubuntu1.2 libsasl2-modules-db=2.1.27+dfsg2-3ubuntu1.2 libsasl2-modules=2.1.27+dfsg2-3ubuntu1.2 libsqlite3-0=3.37.2-2ubuntu0.4 libssh-4=0.9.6-2ubuntu0.22.04.3 lsb-release=11.1.0ubuntu4 media-types=7.0.0 pinentry-curses=1.1.1-1build2 publicsuffix=20211207.1025-1 python3-minimal=3.10.6-1~22.04.1 python3.10-minimal=3.10.12-1~22.04.9 python3.10=3.10.12-1~22.04.9 python3=3.10.6-1~22.04.1 readline-common=8.1.2-1 unzip=6.0-26ubuntu3.2 zip=3.0-12build2 +RUN apt install -y \ + apt-transport-https=2.4.14 \ + curl=7.81.0-1ubuntu1.20 \ + dirmngr=2.2.27-3ubuntu2.4 \ + distro-info-data=0.52ubuntu0.9 \ + gnupg-l10n=2.2.27-3ubuntu2.4 \ + gnupg-utils=2.2.27-3ubuntu2.4 \ + gnupg=2.2.27-3ubuntu2.4 \ + gpg-agent=2.2.27-3ubuntu2.4 \ + gpg-wks-client=2.2.27-3ubuntu2.4 \ + gpg-wks-server=2.2.27-3ubuntu2.4 \ + gpg=2.2.27-3ubuntu2.4 \ + gpgconf=2.2.27-3ubuntu2.4 \ + gpgsm=2.2.27-3ubuntu2.4 \ + libassuan0=2.5.5-1build1 \ + libbrotli1=1.0.9-2build6 \ + libcurl4=7.81.0-1ubuntu1.20 \ + libexpat1=2.4.7-1ubuntu0.6 \ + libksba8=1.6.0-2ubuntu0.2 \ + libldap-2.5-0=2.5.19+dfsg-0ubuntu0.22.04.1 \ + libldap-common=2.5.19+dfsg-0ubuntu0.22.04.1 \ + libmpdec3=2.5.1-2build2 \ + libnghttp2-14=1.43.0-1ubuntu0.2 \ + libnpth0=1.6-3build2 \ + libpsl5=0.21.0-1.2build2 \ + libpython3-stdlib=3.10.6-1~22.04.1 \ + libpython3.10-minimal=3.10.12-1~22.04.10 \ + libpython3.10-stdlib=3.10.12-1~22.04.10 \ + libreadline8=8.1.2-1 \ + librtmp1=2.4+20151223.gitfa8646d.1-2build4 \ + libsasl2-2=2.1.27+dfsg2-3ubuntu1.2 \ + libsasl2-modules-db=2.1.27+dfsg2-3ubuntu1.2 \ + libsasl2-modules=2.1.27+dfsg2-3ubuntu1.2 \ + libsqlite3-0=3.37.2-2ubuntu0.5 \ + libssh-4=0.9.6-2ubuntu0.22.04.4 \ + lsb-release=11.1.0ubuntu4 \ + media-types=7.0.0 \ + pinentry-curses=1.1.1-1build2 \ + publicsuffix=20211207.1025-1 \ + python3-minimal=3.10.6-1~22.04.1 \ + python3.10-minimal=3.10.12-1~22.04.10 \ + python3.10=3.10.12-1~22.04.10 \ + python3=3.10.6-1~22.04.1 \ + readline-common=8.1.2-1 \ + unzip=6.0-26ubuntu3.2 \ + zip=3.0-12build2 # Prepare Docker installation RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc \ @@ -27,7 +114,80 @@ RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings && apt update # Install docker-ce docker-ce-cli containerd.io and dependencies at fixed versions -RUN apt install -y apparmor=3.0.4-2ubuntu2.4 containerd.io=1.7.27-1 dbus-user-session=1.12.20-2ubuntu4.1 dbus=1.12.20-2ubuntu4.1 dmsetup=2:1.02.175-2.1ubuntu5 docker-buildx-plugin docker-ce-cli docker-ce-rootless-extras docker-ce docker-compose-plugin gir1.2-glib-2.0=1.72.0-1 git-man=1:2.34.1-1ubuntu1.12 git=1:2.34.1-1ubuntu1.12 iptables=1.8.7-1ubuntu5.2 less=590-1ubuntu0.22.04.3 libapparmor1=3.0.4-2ubuntu2.4 libargon2-1=0~20171227-0.3 libbsd0=0.11.5-1 libcbor0.8=0.8.0-2ubuntu1 libcryptsetup12=2:2.4.3-1ubuntu1.3 libcurl3-gnutls=7.81.0-1ubuntu1.20 libdbus-1-3=1.12.20-2ubuntu4.1 libdevmapper1.02.1=2:1.02.175-2.1ubuntu5 libedit2=3.1-20210910-1build1 liberror-perl=0.17029-1 libfido2-1=1.10.0-1 libgdbm-compat4=1.23-1 libgdbm6=1.23-1 libgirepository-1.0-1=1.72.0-1 libglib2.0-0=2.72.4-0ubuntu2.5 libglib2.0-data=2.72.4-0ubuntu2.5 libicu70=70.1-2 libip4tc2=1.8.7-1ubuntu5.2 libip6tc2=1.8.7-1ubuntu5.2 libjson-c5=0.15-3~ubuntu1.22.04.2 libkmod2=29-1ubuntu1 libltdl7=2.4.6-15build2 libmd0=1.0.4-1build1 libmnl0=1.0.4-3build2 libnetfilter-conntrack3=1.0.9-1 libnfnetlink0=1.0.1-3build3 libnftnl11=1.2.1-1build1 libnss-systemd=249.11-0ubuntu3.16 libpam-systemd=249.11-0ubuntu3.16 libperl5.34=5.34.0-3ubuntu1.4 libslirp0=4.6.1-1build1 libx11-6=2:1.7.5-1ubuntu0.3 libx11-data=2:1.7.5-1ubuntu0.3 libxau6=1:1.0.9-1build5 libxcb1=1.14-3ubuntu3 libxdmcp6=1:1.1.3-0ubuntu5 libxext6=2:1.3.4-1build1 libxml2=2.9.13+dfsg-1ubuntu0.7 libxmuu1=2:1.1.3-3 libxtables12=1.8.7-1ubuntu5.2 netbase=6.3 networkd-dispatcher=2.1-2ubuntu0.22.04.2 openssh-client=1:8.9p1-3ubuntu0.13 patch=2.7.6-7build2 perl-base=5.34.0-3ubuntu1.4 perl-modules-5.34=5.34.0-3ubuntu1.4 perl=5.34.0-3ubuntu1.4 pigz=2.6-1 python3-dbus=1.2.18-3build1 python3-gi=3.42.1-0ubuntu1 shared-mime-info=2.1-2 slirp4netns=1.0.1-2 systemd-sysv=249.11-0ubuntu3.16 systemd-timesyncd=249.11-0ubuntu3.16 systemd=249.11-0ubuntu3.16 xauth=1:1.1-1build2 xdg-user-dirs=0.17-2ubuntu4 xz-utils=5.2.5-2ubuntu1 +RUN apt install -y \ + apparmor=3.0.4-2ubuntu2.4 \ + containerd.io=1.7.27-1 \ + dbus-user-session=1.12.20-2ubuntu4.1 \ + dbus=1.12.20-2ubuntu4.1 \ + dmsetup=2:1.02.175-2.1ubuntu5 \ + docker-buildx-plugin=0.26.1-1~ubuntu.22.04~jammy \ + docker-ce-cli=5:28.3.3-1~ubuntu.22.04~jammy \ + docker-ce-rootless-extras=5:28.3.3-1~ubuntu.22.04~jammy \ + docker-ce=5:28.3.3-1~ubuntu.22.04~jammy \ + docker-compose-plugin=2.39.1-1~ubuntu.22.04~jammy \ + gir1.2-glib-2.0=1.72.0-1 \ + git-man=1:2.34.1-1ubuntu1.15 \ + git=1:2.34.1-1ubuntu1.15 \ + iptables=1.8.7-1ubuntu5.2 \ + less=590-1ubuntu0.22.04.3 \ + libapparmor1=3.0.4-2ubuntu2.4 \ + libargon2-1=0~20171227-0.3 \ + libbsd0=0.11.5-1 \ + libcbor0.8=0.8.0-2ubuntu1 \ + libcryptsetup12=2:2.4.3-1ubuntu1.3 \ + libcurl3-gnutls=7.81.0-1ubuntu1.20 \ + libdbus-1-3=1.12.20-2ubuntu4.1 \ + libdevmapper1.02.1=2:1.02.175-2.1ubuntu5 \ + libedit2=3.1-20210910-1build1 \ + liberror-perl=0.17029-1 \ + libfido2-1=1.10.0-1 \ + libgdbm-compat4=1.23-1 \ + libgdbm6=1.23-1 \ + libgirepository-1.0-1=1.72.0-1 \ + libglib2.0-0=2.72.4-0ubuntu2.5 \ + libglib2.0-data=2.72.4-0ubuntu2.5 \ + libicu70=70.1-2 \ + libip4tc2=1.8.7-1ubuntu5.2 \ + libip6tc2=1.8.7-1ubuntu5.2 \ + libjson-c5=0.15-3~ubuntu1.22.04.2 \ + libkmod2=29-1ubuntu1 \ + libltdl7=2.4.6-15build2 \ + libmd0=1.0.4-1build1 \ + libmnl0=1.0.4-3build2 \ + libnetfilter-conntrack3=1.0.9-1 \ + libnfnetlink0=1.0.1-3build3 \ + libnftnl11=1.2.1-1build1 \ + libnss-systemd=249.11-0ubuntu3.16 \ + libpam-systemd=249.11-0ubuntu3.16 \ + libperl5.34=5.34.0-3ubuntu1.5 \ + libslirp0=4.6.1-1build1 \ + libx11-6=2:1.7.5-1ubuntu0.3 \ + libx11-data=2:1.7.5-1ubuntu0.3 \ + libxau6=1:1.0.9-1build5 \ + libxcb1=1.14-3ubuntu3 \ + libxdmcp6=1:1.1.3-0ubuntu5 \ + libxext6=2:1.3.4-1build1 \ + libxml2=2.9.13+dfsg-1ubuntu0.7 \ + libxmuu1=2:1.1.3-3 \ + libxtables12=1.8.7-1ubuntu5.2 \ + netbase=6.3 \ + networkd-dispatcher=2.1-2ubuntu0.22.04.2 \ + openssh-client=1:8.9p1-3ubuntu0.13 \ + patch=2.7.6-7build2 \ + perl-base=5.34.0-3ubuntu1.5 \ + perl-modules-5.34=5.34.0-3ubuntu1.5 \ + perl=5.34.0-3ubuntu1.5 \ + pigz=2.6-1 \ + python3-dbus=1.2.18-3build1 \ + python3-gi=3.42.1-0ubuntu1 \ + shared-mime-info=2.1-2 \ + slirp4netns=1.0.1-2 \ + systemd-sysv=249.11-0ubuntu3.16 \ + systemd-timesyncd=249.11-0ubuntu3.16 \ + systemd=249.11-0ubuntu3.16 \ + xauth=1:1.1-1build2 \ + xdg-user-dirs=0.17-2ubuntu4 \ + xz-utils=5.2.5-2ubuntu1 # Clean up apt cache RUN rm -rf /var/lib/apt/lists/* @@ -36,36 +196,150 @@ RUN rm -rf /var/lib/apt/lists/* RUN python3 -m pip uninstall -y conda conda-package-handling conda_index # Install specific versions of pip and setuptools -RUN python3 -m pip install -U pip==23.3.1 setuptools==75.8.2 +RUN python3 -m pip install \ + -U \ + pip==25.1.1 \ + setuptools==80.8.0 # Install dependencies of NVFlare at fixed versions -RUN python3 -m pip install --upgrade psutil==7.0.0 -RUN python3 -m pip install Flask==3.0.2 Flask-JWT-Extended==4.6.0 Flask-SQLAlchemy==3.1.1 PyJWT==2.10.1 SQLAlchemy==2.0.16 Werkzeug==3.0.1 blinker==1.9.0 docker==7.1.0 greenlet==3.1.1 grpcio==1.62.1 gunicorn==23.0.0 itsdangerous==2.2.0 msgpack==1.1.0 protobuf==4.24.4 pyhocon==0.3.61 pyparsing==3.0.9 websockets==15.0 +RUN python3 -m pip install \ + --upgrade \ + psutil==7.0.0 +RUN python3 -m pip install \ + Flask==3.0.2 \ + Flask-JWT-Extended==4.6.0 \ + Flask-SQLAlchemy==3.1.1 \ + PyJWT==2.10.1 \ + SQLAlchemy==2.0.16 \ + Werkzeug==3.0.1 \ + blinker==1.9.0 \ + docker==7.1.0 \ + greenlet==3.2.2 \ + grpcio==1.62.1 \ + gunicorn==23.0.0 \ + itsdangerous==2.2.0 \ + msgpack==1.1.0 \ + protobuf==4.24.4 \ + pyhocon==0.3.61 \ + pyparsing==3.2.3 \ + websockets==15.0.1 -# Install additional Python packages for swarm training at defined versions -RUN python3 -m pip install Deprecated==1.2.14 SimpleITK==2.2.1 absl-py==2.1.0 aiohttp==3.9.5 aiosignal==1.3.1 async-timeout==4.0.3 cachetools==5.3.3 contourpy==1.2.1 cycler==0.12.1 et-xmlfile==1.1.0 fonttools==4.53.1 frozenlist==1.4.1 google-auth-oauthlib==1.0.0 google-auth==2.31.0 huggingface_hub==0.23.4 humanize==4.9.0 joblib==1.4.2 kiwisolver==1.4.5 lightning-utilities==0.11.3.post0 markdown-it-py==3.0.0 markdown==3.6 matplotlib==3.7.2 mdurl==0.1.2 monai==1.3.0 multidict==6.0.5 nibabel==5.2.1 oauthlib==3.2.2 openpyxl==3.1.0 pandas==2.2.2 pyasn1-modules==0.4.0 pyasn1==0.6.0 pydicom==2.4.4 python-dateutil==2.9.0.post0 pytorch-lightning==1.9.0 requests-oauthlib==2.0.0 rich==13.7.1 rsa==4.9 safetensors==0.4.3 scikit-learn==1.3.0 scipy==1.14.0 seaborn==0.12.2 shellingham==1.5.4 tensorboard-data-server==0.7.2 tensorboard-plugin-wit==1.8.1 tensorboard==2.12.1 threadpoolctl==3.5.0 timm==0.9.16 torchio==0.19.6 torchmetrics==1.4.0.post0 torchvision==0.17.0 tqdm==4.65.0 typer==0.12.3 tzdata==2024.1 wrapt==1.16.0 yarl==1.9.4 +# Install additional Python packages for application code at defined versions +RUN python3 -m pip install \ + Deprecated==1.2.18 \ + SimpleITK==2.5.0 \ + absl-py==2.2.2 \ + aiohttp==3.11.18 \ + aiosignal==1.3.2 \ + async-timeout==5.0.1 \ + cachetools==5.5.2 \ + contourpy==1.3.2 \ + cycler==0.12.1 \ + et-xmlfile==2.0.0 \ + fonttools==4.58.0 \ + frozenlist==1.6.0 \ + google-auth-oauthlib==1.2.2 \ + google-auth==2.40.2 \ + huggingface_hub==0.29.3 \ + datasets==3.4.1 \ + coral_pytorch==1.4.0 \ + humanize==4.12.3 \ + joblib==1.5.1 \ + kiwisolver==1.4.8 \ + lightning-utilities==0.14.3 \ + markdown-it-py==3.0.0 \ + markdown==3.8 \ + matplotlib==3.9.2 \ + mdurl==0.1.2 \ + monai==1.4.0 \ + multidict==6.4.4 \ + nibabel==5.3.2 \ + oauthlib==3.2.2 \ + openpyxl==3.1.5 \ + pandas==2.2.3 \ + numpy==1.26.4 \ + pyasn1-modules==0.4.2 \ + pyasn1==0.6.1 \ + pydicom==3.0.1 \ + python-dateutil==2.9.0.post0 \ + x-transformers==2.3.5 \ + pytorch-lightning==2.4.0 \ + requests==2.32.3 \ + requests-oauthlib==2.0.0 \ + rich==14.0.0 \ + rsa==4.9.1 \ + safetensors==0.5.3 \ + scikit-learn==1.5.2 \ + scipy==1.15.3 \ + seaborn==0.13.2 \ + wandb==0.18.6 \ + einops==0.8.0 \ + shellingham==1.5.4 \ + tensorboard-data-server==0.7.2 \ + tensorboard-plugin-wit==1.8.1 \ + tensorboard==2.19.0 \ + threadpoolctl==3.6.0 \ + timm==1.0.15 \ + torchio==0.20.1 \ + torchmetrics==1.7.1 \ + torchvision==0.17.2 \ + torchaudio==2.2.2 \ + tqdm==4.67.0 \ + typer==0.15.4 \ + tzdata==2025.2 \ + wrapt==1.17.2 \ + yarl==1.20.0 \ + aiohappyeyeballs==2.6.1 \ + annotated-types==0.7.0 \ + dill==0.3.8 \ + docker-pycreds==0.4.0 \ + einx==0.3.0 \ + frozendict==2.4.6 \ + gitdb==4.0.12 \ + gitpython==3.1.44 \ + hf-xet==1.1.2 \ + importlib-resources==6.5.2 \ + loguru==0.7.3 \ + multiprocess==0.70.16 \ + propcache==0.3.1 \ + pyarrow==20.0.0 \ + pydantic==2.11.5 \ + pydantic-core==2.33.2 \ + sentry-sdk==2.29.1 \ + setproctitle==1.3.6 \ + smmap==5.0.2 \ + typing-extensions==4.13.2 \ + typing-inspection==0.4.1 \ + xxhash==3.5.0 # Install packages needed for testing and for listing licenses of installed packages -RUN python3 -m pip install coverage==7.5.4 mock==5.1.0 -RUN python3 -m pip install pip-licenses==5.0.0 prettytable==3.14.0 +RUN python3 -m pip install \ + coverage==7.8.2 \ + mock==5.2.0 +RUN python3 -m pip install \ + pip-licenses==5.0.0 \ + prettytable==3.16.0 # Clean up pip cache RUN python3 -m pip cache purge # install ODELIA fork of NVFlare from local source WORKDIR /workspace/ -COPY ./docker_config/NVFlare /workspace/nvflare +COPY ./MediSwarm/docker_config/NVFlare /workspace/nvflare ## use startup kit template in the dashboard -COPY ./docker_config/master_template.yml /workspace/nvflare/nvflare/lighter/impl/ +COPY ./MediSwarm/docker_config/master_template.yml /workspace/nvflare/nvflare/lighter/impl/ RUN python3 -m pip install /workspace/nvflare RUN rm -rf /workspace/nvflare # Install the ODELIA controller package from local source -COPY ./controller /workspace/controller -RUN python3 -m pip install /workspace/controller +COPY ./MediSwarm/controller /workspace/controller +RUN python3 -m pip install /workspace/controller RUN rm -rf /workspace/controller # Copy the source code for local training and deploying to the swarm -COPY . /MediSwarm +COPY ./MediSwarm /MediSwarm RUN mkdir -p /fl_admin/transfer RUN ln -s /MediSwarm /fl_admin/transfer/MediSwarm + +# Copy pre-trained model weights to image +COPY ./torch_home_cache /torch_home diff --git a/docker_config/Dockerfile_stamp b/docker_config/Dockerfile_stamp new file mode 100644 index 00000000..b1003e46 --- /dev/null +++ b/docker_config/Dockerfile_stamp @@ -0,0 +1,53 @@ +# ----------- Base Image with Python 3.11 + CUDA 12.1 ----------- +ARG PYTORCH_IMAGE=pytorch/pytorch:2.2.2-cuda12.1-cudnn8-devel +FROM ${PYTORCH_IMAGE} + +# ----------- Metadata & Environment Variables ----------- +ENV DEBIAN_FRONTEND=noninteractive \ + CUDA_HOME=/usr/local/cuda \ + HF_HOME=/workspace/.hf_cache \ + PATH="/workspace/STAMP/.venv/bin:$PATH" + +# ----------- System Dependencies ----------- +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3.11 python3.11-venv python3.11-dev python3-pip \ + git curl wget unzip ca-certificates build-essential \ + openslide-tools libgl1 libglx-mesa0 libglib2.0-0 \ + && rm -rf /var/lib/apt/lists/* + +# ----------- Install uv ----------- +RUN pip install uv + +# ----------- Clone STAMP and Install via uv.lock ----------- +WORKDIR /workspace/ +RUN git clone https://github.com/KatherLab/STAMP.git +COPY docker_config/pyproject.toml docker_config/uv.lock /workspace/STAMP/ + +WORKDIR /workspace/STAMP +RUN uv venv && uv pip install --upgrade pip && uv sync --all-extras + +# ----------- Expose STAMP CLI globally ----------- +RUN ln -s /workspace/STAMP/.venv/bin/stamp /usr/local/bin/stamp + +# ----------- HuggingFace CLI (Optional) ----------- +RUN huggingface-cli login --help || true + +# ----------- Install NVFlare ----------- +COPY ./docker_config/NVFlare /workspace/nvflare +COPY ./docker_config/master_template.yml /workspace/nvflare/nvflare/lighter/impl/ +RUN /usr/bin/python3.11 -m pip install /workspace/nvflare && rm -rf /workspace/nvflare + +# ----------- Install MediSwarm Controller ----------- +COPY ./controller /workspace/controller +RUN /usr/bin/python3.11 -m pip install /workspace/controller && rm -rf /workspace/controller + +# ----------- Copy MediSwarm Source and Link ----------- +#COPY ./MediSwarm /MediSwarm originally +COPY ./ /MediSwarm +RUN mkdir -p /fl_admin/transfer && ln -s /MediSwarm /fl_admin/transfer/MediSwarm + +# ----------- Set Default Workdir ----------- +WORKDIR /workspace/ + +#docker build -f docker_config/Dockerfile_stamp -t stamp-image . +#docker run --rm -it stamp-image:latest bash \ No newline at end of file diff --git a/docker_config/master_template.yml b/docker_config/master_template.yml index 2c4b1170..0a2306db 100644 --- a/docker_config/master_template.yml +++ b/docker_config/master_template.yml @@ -620,25 +620,34 @@ sub_start_svr_sh: | docker_cln_sh: | #!/usr/bin/env bash - # docker run script for FL client + # docker run script for FL client with proper env variable forwarding + # Auto disable TTY in non-interactive CI environments + if [ -t 1 ]; then + TTY_OPT="-it" + else + echo "[INFO] No interactive terminal detected, disabling TTY." + TTY_OPT="" + fi + # Parse command-line arguments while [[ "$#" -gt 0 ]]; do case $1 in --data_dir) MY_DATA_DIR="$2"; shift ;; --scratch_dir) MY_SCRATCH_DIR="$2"; shift ;; --GPU) GPU2USE="$2"; shift ;; - --no_pull) NOPULL="1";; - --dummy_training) DUMMY_TRAINING="1";; - --preflight_check) PREFLIGHT_CHECK="1";; - --local_training) LOCAL_TRAINING="1";; - --start_client) START_CLIENT="1";; - --interactive) INTERACTIVE="1";; + --no_pull) NOPULL="1" ;; + --dummy_training) DUMMY_TRAINING="1" ;; + --preflight_check) PREFLIGHT_CHECK="1" ;; + --local_training) LOCAL_TRAINING="1" ;; + --start_client) START_CLIENT="1" ;; + --interactive) INTERACTIVE="1" ;; + --run_script) SCRIPT_TO_RUN="$2"; shift ;; *) echo "Unknown parameter passed: $1"; exit 1 ;; esac shift done - # Ask user for required parameters not passed as command line arguments + # Prompt for parameters if missing if [[ -z "$DUMMY_TRAINING" && -z "$MY_DATA_DIR" ]]; then read -p "Enter the path to your data directory (default: /home/flclient/data): " user_data_dir : ${MY_DATA_DIR:="${user_data_dir:-/home/flclient/data}"} @@ -654,26 +663,24 @@ docker_cln_sh: | : ${GPU2USE:="${user_gpu:-device=0}"} fi - # Get the directory of the current script + # Resolve script directory DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" - sudo mkdir -p $MY_SCRATCH_DIR - sudo chown -R $(id -u):$(id -g) $MY_SCRATCH_DIR - sudo chmod -R 777 $MY_SCRATCH_DIR + mkdir -p "$MY_SCRATCH_DIR" + chmod -R 777 "$MY_SCRATCH_DIR" - # To use host network + # Networking & Cleanup NETARG="--net=host" + rm -rf ../pid.fl ../daemon_pid.fl - rm -rf ../pid.fl ../daemon_pid.fl # clean up potential leftovers from previous run - - # Docker image to use + # Docker image and container name DOCKER_IMAGE={~~docker_image~~} if [ -z "$NOPULL" ]; then echo "Updating docker image" - docker pull $DOCKER_IMAGE + docker pull "$DOCKER_IMAGE" fi - CONTAINER_NAME=odelia_swarm_client_{~~client_name~~} + CONTAINER_NAME=odelia_swarm_client_{~~client_name~~} DOCKER_OPTIONS_A="--name=$CONTAINER_NAME --gpus=$GPU2USE -u $(id -u):$(id -g)" DOCKER_MOUNTS="-v /etc/passwd:/etc/passwd -v /etc/group:/etc/group -v $DIR/..:/startupkit/ -v $MY_SCRATCH_DIR:/scratch/" if [[ ! -z "$MY_DATA_DIR" ]]; then @@ -682,44 +689,53 @@ docker_cln_sh: | DOCKER_OPTIONS_B="-w /startupkit/startup/ --ipc=host $NETARG" DOCKER_OPTIONS="${DOCKER_OPTIONS_A} ${DOCKER_MOUNTS} ${DOCKER_OPTIONS_B}" - echo "Starting docker with $DOCKER_IMAGE as $CONTAINER_NAME" - # Run docker with appropriate parameters + # Common ENV vars + ENV_VARS="--env SITE_NAME={~~client_name~~} \ + --env DATA_DIR=/data \ + --env SCRATCH_DIR=/scratch \ + --env TORCH_HOME=/torch_home \ + --env GPU_DEVICE=$GPU2USE \ + --env MODEL_NAME=MST \ + --env CONFIG=unilateral \ + --env MEDISWARM_VERSION=__REPLACED_BY_CURRENT_VERSION_NUMBER_WHEN_BUILDING_DOCKER_IMAGE__" + + # Execution modes if [[ ! -z "$DUMMY_TRAINING" ]]; then - DOCKER_ENV_VAR="--env TRAINING_MODE=local_training" - docker run --rm -it \ - $DOCKER_OPTIONS $DOCKER_ENV_VAR $DOCKER_IMAGE \ + docker run --rm $TTY_OPT $DOCKER_OPTIONS $ENV_VARS --env TRAINING_MODE=local_training $DOCKER_IMAGE \ /bin/bash -c "/MediSwarm/application/jobs/minimal_training_pytorch_cnn/app/custom/main.py" + elif [[ ! -z "$PREFLIGHT_CHECK" ]]; then - DOCKER_ENV_VAR="--env TRAINING_MODE=preflight_check --env SITE_NAME={~~client_name~~} --env NUM_EPOCHS=1" - docker run --rm -it \ - $DOCKER_OPTIONS $DOCKER_ENV_VAR $DOCKER_IMAGE \ - /bin/bash -c "/MediSwarm/application/jobs/3dcnn_ptl/app/custom/main.py" + docker run --rm $TTY_OPT $DOCKER_OPTIONS $ENV_VARS --env TRAINING_MODE=preflight_check --env NUM_EPOCHS=1 $DOCKER_IMAGE \ + /bin/bash -c "/MediSwarm/application/jobs/ODELIA_ternary_classification/app/custom/main.py" + elif [[ ! -z "$LOCAL_TRAINING" ]]; then - # TODO how to set number of epochs - DOCKER_ENV_VAR="--env TRAINING_MODE=local_training --env SITE_NAME={~~client_name~~} --env NUM_EPOCHS=1" - docker run --rm -it \ - $DOCKER_OPTIONS $DOCKER_ENV_VAR $DOCKER_IMAGE \ - /bin/bash -c "/MediSwarm/application/jobs/3dcnn_ptl/app/custom/main.py" + docker run --rm $TTY_OPT $DOCKER_OPTIONS $ENV_VARS --env TRAINING_MODE=local_training --env NUM_EPOCHS=100 $DOCKER_IMAGE \ + /bin/bash -c "/MediSwarm/application/jobs/ODELIA_ternary_classification/app/custom/main.py" + elif [[ ! -z "$START_CLIENT" ]]; then - DOCKER_ENV_VAR="--env TRAINING_MODE=swarm" - docker run -d -t --rm \ - $DOCKER_OPTIONS $DOCKER_ENV_VAR $DOCKER_IMAGE \ + docker run -d -t --rm $DOCKER_OPTIONS $ENV_VARS --env TRAINING_MODE=swarm $DOCKER_IMAGE \ /bin/bash -c "nohup ./start.sh >> nohup.out 2>&1 && /bin/bash" + elif [[ ! -z "$INTERACTIVE" ]]; then - # start interactive container - DOCKER_ENV_VAR="" - docker run --rm -it --detach-keys="ctrl-x" \ - $DOCKER_OPTIONS $DOCKER_ENV_VAR $DOCKER_IMAGE \ - /bin/bash -c "/bin/bash" + docker run --rm $TTY_OPT --detach-keys="ctrl-x" $DOCKER_OPTIONS $DOCKER_IMAGE /bin/bash + + elif [[ ! -z "$SCRIPT_TO_RUN" ]]; then + docker run --rm $TTY_OPT $DOCKER_OPTIONS $ENV_VARS $DOCKER_IMAGE \ + /bin/bash -c "$SCRIPT_TO_RUN" + else - echo "One of the following options must be passed:" - echo "--dummy_training locally train a minimum example (to check if the Docker/GPU setup is working)" - echo "--preflight_check run a single epoch of local training (to check if your data can be accessed properly and if you are ready for swarm training)" - echo "--local_training run a local training (to train a local model on your data only)" - echo "--start_client start the swarm learning client" - echo "--interactive start the container with an interactive shell (for debugging purposes)" + echo "❗ One of the following options must be passed:" + echo "--dummy_training minimal sanity check for Docker/GPU" + echo "--preflight_check verify data access & local training" + echo "--local_training train a local model" + echo "--start_client launch FL client in swarm mode" + echo "--interactive drop into interactive container (for debugging)" + echo "--run_script execute script in container (for testing)" + exit 1 fi + + docker_svr_sh: | #!/usr/bin/env bash # docker run script for FL server diff --git a/docker_config/pyproject.toml b/docker_config/pyproject.toml new file mode 100644 index 00000000..e4992f44 --- /dev/null +++ b/docker_config/pyproject.toml @@ -0,0 +1,142 @@ +[project] +name = "stamp" +version = "2.2.0" +authors = [ + { name = "Omar El Nahhas", email = "omar.el_nahhas@tu-dresden.de" }, + { name = "Marko van Treeck", email = "markovantreeck@gmail.com" }, + { name = "Georg Wölflein", email = "georgw7777@gmail.com" }, + { name = "Tim Lenz", email = "tim.lenz@tu-dresden.de" }, + { name = "Laura Žigutytė", email = "laura.zigutyte@tu-dresden.de" }, + { name = "Cornelius Kummer", email = "cornelius.kummer@tu-dresden.de" }, + { name = "Juan Pablo Ricapito", email = "juan_pablo.ricapito@tu-dresden.de" } +] +description = "A protocol for Solid Tumor Associative Modeling in Pathology" +readme = "README.md" +requires-python = ">=3.11" + +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +dependencies = [ + "beartype>=0.19.0", + "einops>=0.8.0", + "h5py>=3.12.1", + "jaxtyping>=0.2.36", + "lightning>=2.4.0", + "matplotlib>=3.9.2", + "numpy>=2.2.2", + "opencv-python>=4.10.0.84", + "openpyxl>=3.1.5", + "openslide-bin>=4.0.0.6", + "openslide-python>=1.4.1", + "packaging>=24.2", + "pandas>=2.2.3", + "pillow>=11.1.0", + "pydantic>=2.10.3", + "pyyaml>=6.0.2", + "scikit-learn>=1.5.2", + "scipy>=1.15.1", + "torch>=2.5.1", + "torchmetrics>=1.6.0", + "torchvision>=0.20.1", + "tqdm>=4.66.6", + "timm>=0.9.11", +] + +[project.optional-dependencies] +conch = [ + "huggingface-hub>=0.26.2", + "conch @ git+https://github.com/Mahmoodlab/CONCH.git@02d6ac59cc20874bff0f581de258c2b257f69a84", +] +conch1_5 = [ + "transformers>=4.45.2", + "einops-exts==0.0.4", +] +ctranspath = [ + "gdown>=5.2.0", +] +chief_ctranspath = [ + "gdown>=5.2.0", + "torch>=2.0.0" +] +gigapath = [ + "gigapath @ git+https://github.com/EzicStar/prov-gigapath.git@d4cf55321df37aaf867e24a31c61bcf490a296eb" +] +uni = [ + "huggingface-hub>=0.26.2", + "uni @ git+https://github.com/mahmoodlab/UNI.git", +] +virchow2 = [ + "huggingface-hub>=0.27.1", + "torch>=2.0.0", +] +cobra = [ + "jinja2>=3.1.4", + "cobra @ git+https://github.com/KatherLab/COBRA.git@f1a576e1133330ffc2d1df6ee110701921c7b7c9", +] +prism = [ + "sacremoses==0.1.1", + "environs==11.0.0", +] +madeleine = [ + "madeleine @ git+https://github.com/mahmoodlab/MADELEINE.git@de7c85acc2bdad352e6df8eee5694f8b6f288012" +] +musk = [ + "musk @ git+https://github.com/lilab-stanford/MUSK.git@e1699c27687f44bbf6d4adfcbb2abe89795d347f", +] +plip = [ + "transformers>=4.45.2" +] + +# Blanket target +all = ["stamp[conch,ctranspath,uni,virchow2,chief_ctranspath,conch1_5,prism,madeleine,musk,plip]"] + +[project.scripts] +"stamp" = "stamp.__main__:main" + +[project.urls] +"Homepage" = "https://github.com/KatherLab/STAMP" +"Bug Tracker" = "https://github.com/KatherLab/STAMP/issues" + +[dependency-groups] +dev = [ + "huggingface-hub>=0.27.1", + "ipykernel>=6.29.5", + "pyright>=1.1.389,!=1.1.391", + "pytest>=8.3.4", + "ruff>=0.8.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.metadata] +# To allow referencing git repos in dependencies +allow-direct-references = true + +[tool.pytest.ini_options] +markers = [ + "slow: marks tests as slow (deselect with '-m \"not slow\"')", +] + +[tool.ruff] +lint.ignore = ["F722"] # https://docs.kidger.site/jaxtyping/faq/#flake8-or-ruff-are-throwing-an-error + +[[tool.uv.dependency-metadata]] +name = "uni" +version = "v0.1.0" +requires-dist = [ + "torch>=2.0.1", + "torchvision", + "timm>=0.9.8", + "numpy", + "pandas", + "scikit-learn", + "tqdm", + "transformers", + "xformers; sys_platform != 'darwin'" # xformers is not supported on macOS +] \ No newline at end of file diff --git a/docker_config/uv.lock b/docker_config/uv.lock new file mode 100644 index 00000000..d29d37c7 --- /dev/null +++ b/docker_config/uv.lock @@ -0,0 +1,3641 @@ +version = 1 +requires-python = ">=3.11" +resolution-markers = [ + "python_version < '0'", + "python_full_version < '3.12' and sys_platform == 'darwin'", + "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux'", +] + +[manifest] + +[[manifest.dependency-metadata]] +name = "uni" +version = "0.1.0" +requires-dist = ["torch>=2.0.1", "torchvision", "timm>=0.9.8", "numpy", "pandas", "scikit-learn", "tqdm", "transformers", "xformers ; sys_platform != 'darwin'"] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265 }, +] + +[[package]] +name = "aiohttp" +version = "3.12.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/28/2d96dffe4deb40faa7f5615b4aa96c87528e65837d8cb5385da4aecf1c07/aiohttp-3.12.6.tar.gz", hash = "sha256:37b1c6034a1e14764adad1829cd710543b1699d7985e1d336f0aa52a2dd76ba9", size = 7784449 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/f0/313bd623a40638ed65eddd930fbee3a81efd9c87441ea117067beb66b7e8/aiohttp-3.12.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed4db015494a6d0acaadce035531f9fb321afab2075a4b348811e4f7795e87e6", size = 702633 }, + { url = "https://files.pythonhosted.org/packages/07/59/cd70b7798b5f6c13c65a692dbbbeacf4c085a9ce05a34363c4413384d895/aiohttp-3.12.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:59e19517abef2af49cff79b8a863497036ff401051c79d6a3b6149a48213a7be", size = 474917 }, + { url = "https://files.pythonhosted.org/packages/74/89/fe980184d1313669f731d7f32ce824a3ee1af50b4fe83fe723fcb56ca425/aiohttp-3.12.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d557918fefb29884335e1a257df6c961f35ba1caf8eddaabad762b3436cf87ff", size = 463178 }, + { url = "https://files.pythonhosted.org/packages/cb/2a/abe1c72f9b6959b5459f8b99bcdb661a7c2de7901b0c541c26996dd70006/aiohttp-3.12.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e4fb0d7f221c36ed8469c1d2d9a2bb6a27b543cf90aa46ca701f63fb83dd7ed", size = 1733367 }, + { url = "https://files.pythonhosted.org/packages/61/e2/992378c6b1e1b4beed78044ce5b70b749c269cc6e42472fc878339f90f4e/aiohttp-3.12.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:deddf6b1c83ce518a156b7597a0d7a1a7ec5c1d2c973ba3f1a23f18fa2b7d65e", size = 1682037 }, + { url = "https://files.pythonhosted.org/packages/64/11/620be3270f913c8d5ae4e9a88fa5ce80b7a7848324506da8b2916053f5f0/aiohttp-3.12.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eefd98dd043c33c45123c56a79c6c39acb628304337c90f16f33569cc3aa4ba6", size = 1780829 }, + { url = "https://files.pythonhosted.org/packages/c7/92/475e7c1781aa0907671b66355ae414f0d479f0bcf0929469ea44678b72c1/aiohttp-3.12.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:efbbde2297e4ab10d187103aba9b565277c85ac7d24d98cae201c033ce885504", size = 1819873 }, + { url = "https://files.pythonhosted.org/packages/70/04/9f4ad736af830d68dbd376db17f7294c648af393ec24a148bcd5cc2112c1/aiohttp-3.12.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a74a566872f41247774980334e5b0309dac11b402e188bde6db8a57de4506cd", size = 1722335 }, + { url = "https://files.pythonhosted.org/packages/76/da/5ec4f8deacc4107bf95590d285879f4054d62ff9300a96d8abb4b1143384/aiohttp-3.12.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24d19cbd1d21d207ee855500d2033f1852b4d2113a741246ff62eb16a3921306", size = 1659410 }, + { url = "https://files.pythonhosted.org/packages/85/38/30df9eedcfe28265f1efb1bfe9b19cc94c5a37aea5d2cd246dde8c8080e3/aiohttp-3.12.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:86fb0a5762f936606dcab1ca248f5053587a598ed44825f4744ce3c53ae9a2e9", size = 1707684 }, + { url = "https://files.pythonhosted.org/packages/e9/4c/c8d375fa9b6ea5c381747e5e56bc0249d33bb12c5d7171d4c1b4fcae02b4/aiohttp-3.12.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d7ff55a38fc9851fa5cff41b30605534dfe4d57d02f79447abfed01499fe31d3", size = 1702843 }, + { url = "https://files.pythonhosted.org/packages/1e/3c/73b6e184df80ebc5ec07c6e9d398713c40091f6cf4a5e75eb93972ee35d6/aiohttp-3.12.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:545f89c389a47bac024655b5676658f35f80b0d007e4c3c7ff865d9aa3bf343a", size = 1683031 }, + { url = "https://files.pythonhosted.org/packages/9d/5a/827ca828af26ceeda69459c2848fc58e76adbbea0ad9994429ae885d1a33/aiohttp-3.12.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:25dac87ee297e2b5826ce8e96c7615ebe7a1613856b1614a207e3376b776021b", size = 1776629 }, + { url = "https://files.pythonhosted.org/packages/d6/07/8b9081655c08807fa49134b209eddc823e7501bbcf6b044f48a01f30a504/aiohttp-3.12.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c1d8a4a5a7e28d8b9ec815ffecca8712b71130a4eee1c5b45e9f2cc4975f3f7c", size = 1797092 }, + { url = "https://files.pythonhosted.org/packages/5d/18/c761b934543512077c3de8d8f383bcafd94ada30c83273ea6ceeeda8aa2c/aiohttp-3.12.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc4be1d8d68a62859f74f9ada9e174791895366601ce66342f54478d3518c8b3", size = 1710149 }, + { url = "https://files.pythonhosted.org/packages/e0/ba/730f75a17b613f2ecc336b698259140bbd439d8f7b14eae10aea14158085/aiohttp-3.12.6-cp311-cp311-win32.whl", hash = "sha256:a057680218430231eb6ab644d166b7ef398b3ffbac0232f4f789cdce9391400e", size = 420141 }, + { url = "https://files.pythonhosted.org/packages/df/71/8aaff029d07b15f7f79c484ca9b10f34cf8466d8dc4b13ecf32cc46b8de0/aiohttp-3.12.6-cp311-cp311-win_amd64.whl", hash = "sha256:8a88046a5adddf5d99f15a1920f6b8f659f46a4cfb5bfabbd668d06df045df7a", size = 444534 }, + { url = "https://files.pythonhosted.org/packages/e2/71/d4534c305623ba4e759caa381a5902713284f1ee52163d14894e60b3d254/aiohttp-3.12.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cfbf8ed94b57e3b5a886bfe2a530c8eb067064cc4419fd94431a2cbeeddec54c", size = 693736 }, + { url = "https://files.pythonhosted.org/packages/0c/f4/0e4c010b669ef7418fcd5249edc1671bd07492be7989699b92cbc65e19c2/aiohttp-3.12.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:012ea107092d4465aeeb681d5b2fb8b51a847a72f0b71906f40876419fba1355", size = 468347 }, + { url = "https://files.pythonhosted.org/packages/b0/6e/6bc969bab1d4790548220b7bd061b711f246b708d7d8a6d88a0ecb04154c/aiohttp-3.12.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdb03da5ecf74a331511604f3cf91563bf29127eabb28f4e16d390a73cb826da", size = 461191 }, + { url = "https://files.pythonhosted.org/packages/0a/56/de7ac8b49ce179618ede56f01f0050f75a270f3d9eb5d6905793e331a7d7/aiohttp-3.12.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ca81cb1e41d251cc193164409c0bbb0175e696a9997491a10db9171a2f70603", size = 1707982 }, + { url = "https://files.pythonhosted.org/packages/43/dd/74d8f791bf7832077ce5f7592126a64c6de57849182f730bb75dc7030ee7/aiohttp-3.12.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:15817882d25e840aba85d1f5706a7128350b81050f8ca9dabfc25a5f521a792c", size = 1690630 }, + { url = "https://files.pythonhosted.org/packages/10/f2/c5e96be25dd3efd0fe4b21a0c583fffadbadfc85f039cc0dd013e08bdc07/aiohttp-3.12.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db5c402ea0aed10af2e54e5946bf32f3ebb02a7604eaaa4c41a608053889de4a", size = 1745727 }, + { url = "https://files.pythonhosted.org/packages/ad/19/6fb117cf46a99d302405012f05faf67e7ebae925e8ba5a0948f5c046a7b4/aiohttp-3.12.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ea77675818fd8cac28491d0d59582e5e2e5b14dbf5e21bef797aa5b23b5ca8b", size = 1791881 }, + { url = "https://files.pythonhosted.org/packages/78/2f/cdde703cbfd281aca8679b75289c3cde865a541efc593354e50c5c6b7c01/aiohttp-3.12.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c232720190ca4240c15abefc7b765e987ef88df44d2384612890db87b33898f3", size = 1711323 }, + { url = "https://files.pythonhosted.org/packages/e8/a1/edfdfe7ea9160f1bbf7bd00964da45ac290a5d19661761098eefa95ac400/aiohttp-3.12.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a2f3c974874bd0c76dfdcc60db5a6f96ca023a85318a5ac401603baa7e299272", size = 1627125 }, + { url = "https://files.pythonhosted.org/packages/5e/f0/de34cad1d44c6a7e4bffef9d42f982a28a4cdce8815733134aceb542be1d/aiohttp-3.12.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:25de52753386b0c16d5acd2153e7819f52c9e7fc05f5eca804adc174e99b735d", size = 1688185 }, + { url = "https://files.pythonhosted.org/packages/c7/46/e486289bc0a52d2cc4b87091de7428c4c4ddc76465c1aaa22eb953b8bcb7/aiohttp-3.12.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3cc06a99e065ed7e766d2cd574671428261c1b8f30fedfbd91ab3c738fd9c08d", size = 1709637 }, + { url = "https://files.pythonhosted.org/packages/1f/f2/fe3d3955a2c9e78c308783ef0b1b53e5a3b56beb87133a52584e8dda52c0/aiohttp-3.12.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:aac87d78f55057ab48ddcc43055620546d40bbc0888d2658d8705d183c98f901", size = 1650291 }, + { url = "https://files.pythonhosted.org/packages/1c/68/2b425bd8031666be7db81e37918a9b3a9898e02b8d7e672737b05c55e2c5/aiohttp-3.12.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:de83f567e31418fd7bc22c5a03526a2b0a82e68c7a7fec23ef91a398228f559b", size = 1729852 }, + { url = "https://files.pythonhosted.org/packages/10/40/a14b0cf78531d504391a311c3e7c190f230cbf7dba5d4ccfbf53a5d121e5/aiohttp-3.12.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fd1d6116c1364ab00ffed1654a01091dc7f897d315c5103bcc6e5ab7f70172c7", size = 1757824 }, + { url = "https://files.pythonhosted.org/packages/25/1d/250baf6961354772bf7447bb280dffa2df15c08875e535cf6a735a41373e/aiohttp-3.12.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:58f79b376a426961418df1d08656ec3a01494b7ba81824ae629e6636deddfff7", size = 1717436 }, + { url = "https://files.pythonhosted.org/packages/dc/43/9b9204284c08c244d89c69e3e556dfd7757e0393d4ef20a9238bf2643795/aiohttp-3.12.6-cp312-cp312-win32.whl", hash = "sha256:561f545dc062e6c31fc53535d8584c06516bda2fc37821a67a61b69202061e71", size = 414878 }, + { url = "https://files.pythonhosted.org/packages/fe/c1/8561f01a6386a7ecdc54aefff155aae51a349c98c04b1325619e12049fbc/aiohttp-3.12.6-cp312-cp312-win_amd64.whl", hash = "sha256:d83ab494eb583ba691af9d4d7c073987526bb9f73aa5a19907258ef3a1e39e8a", size = 440981 }, + { url = "https://files.pythonhosted.org/packages/be/5d/4db8a8972642779aa981aae2d71d88106a12f3f6a8354725ef4dbcf31a70/aiohttp-3.12.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7487f707a4b8167394f6afefa690198300d8a618505583eb536b92202bdec24d", size = 688139 }, + { url = "https://files.pythonhosted.org/packages/9b/b0/f0326159505f05a32e0dd858ca4770bdeb97900797d80ece9e8031f87c76/aiohttp-3.12.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9dd9211229fa2f474da01d42fafff196f607a63aaf12d8b34928c43a713eb6d5", size = 465812 }, + { url = "https://files.pythonhosted.org/packages/d1/3c/4abaf69965a63aac3bf3c9054c58b1eef68d6cf520ffeb593ed47e590da1/aiohttp-3.12.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3331ef09dd775302aa5f4d3170bd46659ad018843fab3656f5e72e3ff68df21f", size = 458109 }, + { url = "https://files.pythonhosted.org/packages/98/5f/8603860deada8e84ac5954bc736428ef370f8dd600b266c7d8177eea69ad/aiohttp-3.12.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c88ed8c54f7fd6102ef711d24710454707cde4bb3ffdec09982dcb3cb966a3e1", size = 1696923 }, + { url = "https://files.pythonhosted.org/packages/ec/40/209bb8dbb0b03f5758b7de70f86b7ac6acd8450a9bc4b4128cc5e89a20b2/aiohttp-3.12.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:148ffa6b2b825ff8520844ce23df9e2a5b969bb6917c4e35a832fbaa025d260d", size = 1678187 }, + { url = "https://files.pythonhosted.org/packages/26/bf/faa89212e33b6c6ba5913bd7319942f2955f0d199b7c6097896bac35ad6c/aiohttp-3.12.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8da054804352e974f4349fb871b07c8ffa1978e64cfb455e88fbe6fbe4d6dcb", size = 1730257 }, + { url = "https://files.pythonhosted.org/packages/d9/0c/02df1921239913d91a74563547d8e1c79ec6caa052d0bddfbc48e09708a4/aiohttp-3.12.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d162c4f87f9dcdc7151f6329438de96beb527820381e3159ce08544c57e9ced", size = 1779630 }, + { url = "https://files.pythonhosted.org/packages/d8/d0/c72d6b5a204291bbae5ae38fc367df9df11ce32dca6dcca6355d469c9c13/aiohttp-3.12.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da073f88270aa434ef16a78c21a4269c96c68badc2b9ad5011fa175c06143eee", size = 1701959 }, + { url = "https://files.pythonhosted.org/packages/b4/b1/2e2cc4bb3de9d0b185a5c5b6b9d04e3a37c79e52529c634a962ca7a22bfe/aiohttp-3.12.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2e026a9f9ac0df70f14ca5dcaf1f83a55b678e51aa6515d710dd879d2691fd7", size = 1615662 }, + { url = "https://files.pythonhosted.org/packages/bc/55/00c119c8ce2d65879b7b6d64b5a344df3ee8845f0d2a11d190376c9e7256/aiohttp-3.12.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b700cf48fd04b4328965d1afe01f835fe6cdecc3b85ca2d950431e5cc0647f7", size = 1668684 }, + { url = "https://files.pythonhosted.org/packages/b8/be/59bc7538ccaff6fe9cf0b3a27f976d8b2729150b700dc37beef71705f009/aiohttp-3.12.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:38af291559401d13eb90259ba79ef6ac537ae6b5bdb1251604606a88cd0fd5e0", size = 1700350 }, + { url = "https://files.pythonhosted.org/packages/a2/3c/bcfc532cf09755c5d094e320ba7e9e7a6b977d6487b211278a5d400d0649/aiohttp-3.12.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6860351cfba0196db2edc387cfeddaf1dae443e55f261ea2bcb77fecb33aae34", size = 1642945 }, + { url = "https://files.pythonhosted.org/packages/44/27/ebc660cb7624ce8d6b71486490478bb52784074cc46f5fed8fbf5f0306d2/aiohttp-3.12.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:06f20adcdc4f383aeb7ce884705faea44c0376cde5cdee4d32ef62d6cb1f97cc", size = 1719080 }, + { url = "https://files.pythonhosted.org/packages/81/6e/fd000fa2708cb3b887c0fe8a144f926ca34960a80ed1c44d3606027fd831/aiohttp-3.12.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a52aa39eb1160775a6e80e3025c990e8872c8927c5dd4b51304788bc149b9549", size = 1752550 }, + { url = "https://files.pythonhosted.org/packages/ee/2d/5a0bd6d09ea38fcb3ec683425b25946156b99ab451c69ef84ea3d03b6eaf/aiohttp-3.12.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:52ce7e90ee9dd25bcd2ed4513e650cc4f9a03bef07a39193b82fb58892004bd6", size = 1701441 }, + { url = "https://files.pythonhosted.org/packages/bd/7b/db64cfd8fd522de73b803b600d967cc2821250f82ba97892a90c4f7e204e/aiohttp-3.12.6-cp313-cp313-win32.whl", hash = "sha256:259269870d9783de87c0430760b2498b770201ead3e11ee86761d268ce5d196a", size = 413900 }, + { url = "https://files.pythonhosted.org/packages/7f/d6/4680e3601edf5ec0e1e56cca7746f0de9b9758a33b88067b1935e95f7005/aiohttp-3.12.6-cp313-cp313-win_amd64.whl", hash = "sha256:938afd243c9ee76a6d78fad10ecca14b88b48b71553e0e9c74b8098efff5ddf8", size = 439844 }, +] + +[[package]] +name = "aiosignal" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321 }, +] + +[[package]] +name = "asttokens" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918 }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, +] + +[[package]] +name = "autograd" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/1c/3c24ec03c8ba4decc742b1df5a10c52f98c84ca8797757f313e7bdcdf276/autograd-1.8.0.tar.gz", hash = "sha256:107374ded5b09fc8643ac925348c0369e7b0e73bbed9565ffd61b8fd04425683", size = 2562146 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/ea/e16f0c423f7d83cf8b79cae9452040fb7b2e020c7439a167ee7c317de448/autograd-1.8.0-py3-none-any.whl", hash = "sha256:4ab9084294f814cf56c280adbe19612546a35574d67c574b04933c7d2ecb7d78", size = 51478 }, +] + +[[package]] +name = "autograd-gamma" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "autograd" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/ae/7f2031ea76140444b2453fa139041e5afd4a09fc5300cfefeb1103291f80/autograd-gamma-0.5.0.tar.gz", hash = "sha256:f27abb7b8bb9cffc8badcbf59f3fe44a9db39e124ecacf1992b6d952934ac9c4", size = 3952 } + +[[package]] +name = "beartype" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/f9/21e5a9c731e14f08addd53c71fea2e70794e009de5b98e6a2c3d2f3015d6/beartype-0.21.0.tar.gz", hash = "sha256:f9a5078f5ce87261c2d22851d19b050b64f6a805439e8793aecf01ce660d3244", size = 1437066 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/31/87045d1c66ee10a52486c9d2047bc69f00f2689f69401bb1e998afb4b205/beartype-0.21.0-py3-none-any.whl", hash = "sha256:b6a1bd56c72f31b0a496a36cc55df6e2f475db166ad07fa4acc7e74f4c7f34c0", size = 1191340 }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285 }, +] + +[[package]] +name = "braceexpand" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/93/badd4f5ccf25209f3fef2573073da9fe4a45a3da99fca2f800f942130c0f/braceexpand-0.1.7.tar.gz", hash = "sha256:e6e539bd20eaea53547472ff94f4fb5c3d3bf9d0a89388c4b56663aba765f705", size = 7777 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/93/e8c04e80e82391a6e51f218ca49720f64236bc824e92152a2633b74cf7ab/braceexpand-0.1.7-py2.py3-none-any.whl", hash = "sha256:91332d53de7828103dcae5773fb43bc34950b0c8160e35e0f44c4427a3b85014", size = 5923 }, +] + +[[package]] +name = "causal-conv1d" +version = "1.5.0.post8" +source = { git = "https://github.com/KatherLab/causal-conv1d.git?rev=55b4626e1a2d3d6b939811725f2f3ef65b7b3ff1#55b4626e1a2d3d6b939811725f2f3ef65b7b3ff1" } +dependencies = [ + { name = "ninja" }, + { name = "packaging" }, + { name = "torch" }, +] + +[[package]] +name = "certifi" +version = "2025.4.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794 }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846 }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350 }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657 }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260 }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164 }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571 }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952 }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959 }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030 }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015 }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936 }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790 }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924 }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626 }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567 }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957 }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408 }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399 }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815 }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537 }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565 }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357 }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776 }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622 }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435 }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653 }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231 }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243 }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442 }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147 }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057 }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454 }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174 }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166 }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064 }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641 }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 }, +] + +[[package]] +name = "cobra" +version = "0.1.0" +source = { git = "https://github.com/KatherLab/COBRA.git?rev=f1a576e1133330ffc2d1df6ee110701921c7b7c9#f1a576e1133330ffc2d1df6ee110701921c7b7c9" } +dependencies = [ + { name = "causal-conv1d" }, + { name = "einops" }, + { name = "h5py" }, + { name = "huggingface-hub" }, + { name = "jinja2" }, + { name = "mamba-ssm" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "openpyxl" }, + { name = "openslide-bin" }, + { name = "openslide-python" }, + { name = "pandas" }, + { name = "pytorch-lightning" }, + { name = "pyyaml" }, + { name = "scikit-learn" }, + { name = "torch" }, + { name = "torchmetrics" }, + { name = "torchvision" }, + { name = "tqdm" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "comm" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/a8/fb783cb0abe2b5fded9f55e5703015cdf1c9c85b3669087c538dd15a6a86/comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e", size = 6210 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/75/49e5bfe642f71f272236b5b2d2691cf915a7283cc0ceda56357b61daa538/comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3", size = 7180 }, +] + +[[package]] +name = "conch" +version = "0.1.0" +source = { git = "https://github.com/Mahmoodlab/CONCH.git?rev=02d6ac59cc20874bff0f581de258c2b257f69a84#02d6ac59cc20874bff0f581de258c2b257f69a84" } +dependencies = [ + { name = "ftfy" }, + { name = "h5py" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "regex" }, + { name = "scikit-learn" }, + { name = "timm" }, + { name = "tokenizers" }, + { name = "torch" }, + { name = "torchvision" }, + { name = "transformers" }, +] + +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636 }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636 }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053 }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985 }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750 }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246 }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728 }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762 }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196 }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017 }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580 }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530 }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688 }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331 }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963 }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681 }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674 }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480 }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489 }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042 }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630 }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670 }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694 }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986 }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060 }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747 }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895 }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098 }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535 }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096 }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090 }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643 }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443 }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865 }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162 }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355 }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935 }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168 }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550 }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214 }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807 }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729 }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791 }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 }, +] + +[[package]] +name = "debugpy" +version = "1.8.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/75/087fe07d40f490a78782ff3b0a30e3968936854105487decdb33446d4b0e/debugpy-1.8.14.tar.gz", hash = "sha256:7cd287184318416850aa8b60ac90105837bb1e59531898c07569d197d2ed5322", size = 1641444 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/e8/57fe0c86915671fd6a3d2d8746e40485fd55e8d9e682388fbb3a3d42b86f/debugpy-1.8.14-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:1b2ac8c13b2645e0b1eaf30e816404990fbdb168e193322be8f545e8c01644a9", size = 2175064 }, + { url = "https://files.pythonhosted.org/packages/3b/97/2b2fd1b1c9569c6764ccdb650a6f752e4ac31be465049563c9eb127a8487/debugpy-1.8.14-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf431c343a99384ac7eab2f763980724834f933a271e90496944195318c619e2", size = 3132359 }, + { url = "https://files.pythonhosted.org/packages/c0/ee/b825c87ed06256ee2a7ed8bab8fb3bb5851293bf9465409fdffc6261c426/debugpy-1.8.14-cp311-cp311-win32.whl", hash = "sha256:c99295c76161ad8d507b413cd33422d7c542889fbb73035889420ac1fad354f2", size = 5133269 }, + { url = "https://files.pythonhosted.org/packages/d5/a6/6c70cd15afa43d37839d60f324213843174c1d1e6bb616bd89f7c1341bac/debugpy-1.8.14-cp311-cp311-win_amd64.whl", hash = "sha256:7816acea4a46d7e4e50ad8d09d963a680ecc814ae31cdef3622eb05ccacf7b01", size = 5158156 }, + { url = "https://files.pythonhosted.org/packages/d9/2a/ac2df0eda4898f29c46eb6713a5148e6f8b2b389c8ec9e425a4a1d67bf07/debugpy-1.8.14-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:8899c17920d089cfa23e6005ad9f22582fd86f144b23acb9feeda59e84405b84", size = 2501268 }, + { url = "https://files.pythonhosted.org/packages/10/53/0a0cb5d79dd9f7039169f8bf94a144ad3efa52cc519940b3b7dde23bcb89/debugpy-1.8.14-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6bb5c0dcf80ad5dbc7b7d6eac484e2af34bdacdf81df09b6a3e62792b722826", size = 4221077 }, + { url = "https://files.pythonhosted.org/packages/f8/d5/84e01821f362327bf4828728aa31e907a2eca7c78cd7c6ec062780d249f8/debugpy-1.8.14-cp312-cp312-win32.whl", hash = "sha256:281d44d248a0e1791ad0eafdbbd2912ff0de9eec48022a5bfbc332957487ed3f", size = 5255127 }, + { url = "https://files.pythonhosted.org/packages/33/16/1ed929d812c758295cac7f9cf3dab5c73439c83d9091f2d91871e648093e/debugpy-1.8.14-cp312-cp312-win_amd64.whl", hash = "sha256:5aa56ef8538893e4502a7d79047fe39b1dae08d9ae257074c6464a7b290b806f", size = 5297249 }, + { url = "https://files.pythonhosted.org/packages/4d/e4/395c792b243f2367d84202dc33689aa3d910fb9826a7491ba20fc9e261f5/debugpy-1.8.14-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:329a15d0660ee09fec6786acdb6e0443d595f64f5d096fc3e3ccf09a4259033f", size = 2485676 }, + { url = "https://files.pythonhosted.org/packages/ba/f1/6f2ee3f991327ad9e4c2f8b82611a467052a0fb0e247390192580e89f7ff/debugpy-1.8.14-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f920c7f9af409d90f5fd26e313e119d908b0dd2952c2393cd3247a462331f15", size = 4217514 }, + { url = "https://files.pythonhosted.org/packages/79/28/b9d146f8f2dc535c236ee09ad3e5ac899adb39d7a19b49f03ac95d216beb/debugpy-1.8.14-cp313-cp313-win32.whl", hash = "sha256:3784ec6e8600c66cbdd4ca2726c72d8ca781e94bce2f396cc606d458146f8f4e", size = 5254756 }, + { url = "https://files.pythonhosted.org/packages/e0/62/a7b4a57013eac4ccaef6977966e6bec5c63906dd25a86e35f155952e29a1/debugpy-1.8.14-cp313-cp313-win_amd64.whl", hash = "sha256:684eaf43c95a3ec39a96f1f5195a7ff3d4144e4a18d69bb66beeb1a6de605d6e", size = 5297119 }, + { url = "https://files.pythonhosted.org/packages/97/1a/481f33c37ee3ac8040d3d51fc4c4e4e7e61cb08b8bc8971d6032acc2279f/debugpy-1.8.14-py2.py3-none-any.whl", hash = "sha256:5cd9a579d553b6cb9759a7908a41988ee6280b961f24f63336835d9418216a20", size = 5256230 }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190 }, +] + +[[package]] +name = "docker-pycreds" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/e6/d1f6c00b7221e2d7c4b470132c931325c8b22c51ca62417e300f5ce16009/docker-pycreds-0.4.0.tar.gz", hash = "sha256:6ce3270bcaf404cc4c3e27e4b6c70d3521deae82fb508767870fdbf772d584d4", size = 8754 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/e8/f6bd1eee09314e7e6dee49cbe2c5e22314ccdb38db16c9fc72d2fa80d054/docker_pycreds-0.4.0-py2.py3-none-any.whl", hash = "sha256:7266112468627868005106ec19cd0d722702d2b7d5912a28e19b826c3d37af49", size = 8982 }, +] + +[[package]] +name = "ecos" +version = "2.0.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/5f/17716c533da95ed110815b159efa22b1064c8c41fd5c862f21aff7a7fec0/ecos-2.0.14.tar.gz", hash = "sha256:64b3201c0e0a7f0129050557c4ac50b00031e80a10534506dba1200c8dc1efe4", size = 142430 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/9b/c886a268d4b7adfaa1171244cdbfa3c944e5a599fe7a5e738ee27390ab20/ecos-2.0.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dc90b54eaae16ead128bfdd95e04bf808b73578bdf40ed652c55aa36a6d02e42", size = 92594 }, + { url = "https://files.pythonhosted.org/packages/49/e9/fae34e8ef6a9b78c3098a4428ed0e8f77cdeb334a7dc17c649abb686ed08/ecos-2.0.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8be3b4856838ae351fec40fb3589181d52b41cf75bf4d35342686a508c37a6", size = 220084 }, + { url = "https://files.pythonhosted.org/packages/2f/45/1e52519d6c29dd26bbfaf92ece5b45ca3de3b7c8b2615a818aaeadb7ad63/ecos-2.0.14-cp311-cp311-win_amd64.whl", hash = "sha256:7495b3031ccc2d4cec72cdb40aed8a2d1fdd734fe40519b7e6047aead5e811cf", size = 72199 }, + { url = "https://files.pythonhosted.org/packages/af/c3/84e392f2410f51fa557198937cc52a2e80f887c517ef4e3fb6d46e3bb008/ecos-2.0.14-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4a7e2704a3ef9acfb8146d594deff9942d3a0f0d0399de8fe2e0bd95e8b0855c", size = 92545 }, + { url = "https://files.pythonhosted.org/packages/82/12/42f4d953f9284571726b085f99e13bfa84522bf63bf2e7a81460013b09e6/ecos-2.0.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3cbb1a66ecf10955a1a4bcd6b99db55148000cb79fd176bfac26d98b21a4814", size = 222132 }, + { url = "https://files.pythonhosted.org/packages/56/9a/ca30572f3e3ff3cef6a0ea8aa6cdc12c36f9fefe559f65c7d6265713196a/ecos-2.0.14-cp312-cp312-win_amd64.whl", hash = "sha256:718eb62afb8e45426bcc365ebaf3ca9f610afcbb754de6073ef5f104da8fca1f", size = 72248 }, +] + +[[package]] +name = "einops" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/81/df4fbe24dff8ba3934af99044188e20a98ed441ad17a274539b74e82e126/einops-0.8.1.tar.gz", hash = "sha256:de5d960a7a761225532e0f1959e5315ebeafc0cd43394732f103ca44b9837e84", size = 54805 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/62/9773de14fe6c45c23649e98b83231fffd7b9892b6cf863251dc2afa73643/einops-0.8.1-py3-none-any.whl", hash = "sha256:919387eb55330f5757c6bea9165c5ff5cfe63a642682ea788a6d472576d81737", size = 64359 }, +] + +[[package]] +name = "einops-exts" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "einops" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/cc/f2b03b24894faaaa425cf46508e6b1f449d908841f61aeb7c14f0b18a3f3/einops-exts-0.0.4.tar.gz", hash = "sha256:616f145b3411f8e9e3be5da5c968bbe372e55c249de11faa909c7a4b74580a6c", size = 3548 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/01/8da8dd078b354a89602a875d310a0d725dad92b5b4d61069576e0a0e02e4/einops_exts-0.0.4-py3-none-any.whl", hash = "sha256:6d310a4c858e459ebff8288580f90255d354cfa3bde22a53b59baae64b48cb95", size = 3925 }, +] + +[[package]] +name = "environs" +version = "11.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "marshmallow" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/13/3d448cfbed9f1baff5765f49434cd849501351f14fd3f09f0f2e9bd35322/environs-11.0.0.tar.gz", hash = "sha256:069727a8f73d8ba8d033d3cd95c0da231d44f38f1da773bf076cef168d312ee8", size = 25787 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/30/ef8a3022e6cdcedfd7ba03ca88ab29e30334f8e958cdbf5ce120912397e8/environs-11.0.0-py3-none-any.whl", hash = "sha256:e0bcfd41c718c07a7db422f9109e490746450da38793fe4ee197f397b9343435", size = 12216 }, +] + +[[package]] +name = "et-xmlfile" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059 }, +] + +[[package]] +name = "executing" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702 }, +] + +[[package]] +name = "fairscale" +version = "0.4.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/08/b3334d7b543ac10dcb129cef4f84723ab696725512f18d69ab3a784b0bf5/fairscale-0.4.13.tar.gz", hash = "sha256:1b797825c427f5dba92253fd0d8daa574e8bd651a2423497775fab1b30cfb768", size = 266261 } + +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, +] + +[[package]] +name = "flash-attn" +version = "2.8.0.post2" +source = { git = "https://github.com/KatherLab/flash-attention.git?rev=7593c84a0d36b7f2ead10660209b6f8b374ade4e#7593c84a0d36b7f2ead10660209b6f8b374ade4e" } +dependencies = [ + { name = "einops" }, + { name = "torch" }, +] + +[[package]] +name = "fonttools" +version = "4.58.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/7a/30c581aeaa86d94e7a29344bccefd2408870bf5b0e7640b6f4ffede61bd0/fonttools-4.58.1.tar.gz", hash = "sha256:cbc8868e0a29c3e22628dfa1432adf7a104d86d1bc661cecc3e9173070b6ab2d", size = 3519505 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/3f/9fecd69149b0eec5ca46ec58de83b2fd34d07204fe2c12c209255082507a/fonttools-4.58.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9966e14729669bcfbb56f83b747a2397c4d97c6d4798cb2e2adc28f9388fa008", size = 2754713 }, + { url = "https://files.pythonhosted.org/packages/c8/19/d04ea5f3ab2afa7799f2b1ebe1d57ff71b479f99f29b82bddc7197d50220/fonttools-4.58.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64cc1647bbe83dea57f5496ec878ad19ccdba7185b0dd34955d3e6f03dc789e6", size = 2316637 }, + { url = "https://files.pythonhosted.org/packages/5c/3f/375f59d756b17318336c050363849011e03ac82904538f39ebe8189835bc/fonttools-4.58.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:464f790ce681d08d1583df0735776aa9cb1999594bf336ddd0bf962c17b629ac", size = 4915730 }, + { url = "https://files.pythonhosted.org/packages/2f/90/069f859d6f6480503574cda21b84ceee98bf5f5fd1764f26674e828a2600/fonttools-4.58.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c53c6a720ee70cc25746d511ba88c45c95ec510fd258026ed209b0b9e3ba92f", size = 4936194 }, + { url = "https://files.pythonhosted.org/packages/01/11/339973e588e1c27f20c578f845bdcf84376c5e42bd35fca05419fd8d1648/fonttools-4.58.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6823a633bbce29cf3033508ebb54a433c473fb9833eff7f936bfdc5204fd98d", size = 4978982 }, + { url = "https://files.pythonhosted.org/packages/a7/aa/1c627532a69715f54b8d96ab3a7bc8628f6e89989e9275dfc067dc2d6d56/fonttools-4.58.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5701fe66a1408c1974d2f78c00f964f8aad17cccbc32bc041e1b81421f31f448", size = 5090087 }, + { url = "https://files.pythonhosted.org/packages/77/ce/cf7b624db35bce589ac1f2c98329ea91b28f0283d3b7e9e6126dfaeb5abd/fonttools-4.58.1-cp311-cp311-win32.whl", hash = "sha256:4cad2c74adf9ee31ae43be6b0b376fdb386d4d50c60979790e32c3548efec051", size = 2188923 }, + { url = "https://files.pythonhosted.org/packages/b9/22/c4f1f76eeb1b9353e9cc81451d0ae08acc3d3aa31b9ab8f3791a18af1f89/fonttools-4.58.1-cp311-cp311-win_amd64.whl", hash = "sha256:7ade12485abccb0f6b6a6e2a88c50e587ff0e201e48e0153dd9b2e0ed67a2f38", size = 2236853 }, + { url = "https://files.pythonhosted.org/packages/32/97/ed1078b1e138fbc0b4ee75878000d549a70c02d83bb4e557e416efc34140/fonttools-4.58.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f56085a65769dc0100822c814069327541db9c3c4f21e599c6138f9dbda75e96", size = 2740473 }, + { url = "https://files.pythonhosted.org/packages/28/35/53d49fb7d6b30128153d11628b976fda3ce8ae44234b5a81c4edb3023798/fonttools-4.58.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:19c65a88e522c9f1be0c05d73541de20feada99d23d06e9b5354023cc3e517b0", size = 2309936 }, + { url = "https://files.pythonhosted.org/packages/0c/db/8b63c1d673b2bf0cfed77500d47769dc4aa85453b5f0ef525db2cf952895/fonttools-4.58.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b01bb37006e97703300bfde7a73d1c7038574dd1df9d8d92ca99af151becf2ca", size = 4814671 }, + { url = "https://files.pythonhosted.org/packages/a6/13/0b96eeb148b77c521b8e94628c59d15e4fb0e76191c41f5616a656d6adb9/fonttools-4.58.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d629dea240f0fc826d8bb14566e95c663214eece21b5932c9228d3e8907f55aa", size = 4881493 }, + { url = "https://files.pythonhosted.org/packages/ac/b0/9f8aa60e8e5be91aba8dfaa3fa6b33fd950511686921cf27e97bf4154e3d/fonttools-4.58.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef0b33ff35421a04a638e736823c2dee9d200cdd275cfdb43e875ca745150aae", size = 4874960 }, + { url = "https://files.pythonhosted.org/packages/b6/7e/83b409659eb4818f1283a8319f3570497718d6d3b70f4fca2ddf962e948e/fonttools-4.58.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4db9399ee633855c718fe8bea5eecbdc5bf3fdbed2648e50f67f8946b943ed1c", size = 5026677 }, + { url = "https://files.pythonhosted.org/packages/34/52/1eb69802d3b54e569158c97810195f317d350f56390b83c43e1c999551d8/fonttools-4.58.1-cp312-cp312-win32.whl", hash = "sha256:5cf04c4f73d36b30ea1cff091a7a9e65f8d5b08345b950f82679034e9f7573f4", size = 2176201 }, + { url = "https://files.pythonhosted.org/packages/6f/25/8dcfeb771de8d9cdffab2b957a05af4395d41ec9a198ec139d2326366a07/fonttools-4.58.1-cp312-cp312-win_amd64.whl", hash = "sha256:4a3841b59c67fa1f739542b05211609c453cec5d11d21f863dd2652d5a81ec9b", size = 2225519 }, + { url = "https://files.pythonhosted.org/packages/83/7a/7ed2e4e381f9b1f5122d33b7e626a40f646cacc1ef72d8806aacece9e580/fonttools-4.58.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68379d1599fc59569956a97eb7b07e0413f76142ac8513fa24c9f2c03970543a", size = 2731231 }, + { url = "https://files.pythonhosted.org/packages/e7/28/74864dc9248e917cbe07c903e0ce1517c89d42e2fab6b0ce218387ef0e24/fonttools-4.58.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8631905657de4f9a7ae1e12186c1ed20ba4d6168c2d593b9e0bd2908061d341b", size = 2305224 }, + { url = "https://files.pythonhosted.org/packages/e7/f1/ced758896188c1632c5b034a0741457f305e087eb4fa762d86aa3c1ae422/fonttools-4.58.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2ecea7289061c2c71468723409a8dd6e70d1ecfce6bc7686e5a74b9ce9154fe", size = 4793934 }, + { url = "https://files.pythonhosted.org/packages/c1/46/8b46469c6edac393de1c380c7ec61922d5440f25605dfca7849e5ffff295/fonttools-4.58.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b8860f8cd48b345bd1df1d7be650f600f69ee971ffe338c5bd5bcb6bdb3b92c", size = 4863415 }, + { url = "https://files.pythonhosted.org/packages/12/1b/82aa678bb96af6663fe163d51493ffb8622948f4908c886cba6b67fbf6c5/fonttools-4.58.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7c9a0acdefcb8d7ccd7c59202056166c400e797047009ecb299b75ab950c2a9c", size = 4865025 }, + { url = "https://files.pythonhosted.org/packages/7d/26/b66ab2f2dc34b962caecd6fa72a036395b1bc9fb849f52856b1e1144cd63/fonttools-4.58.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1fac0be6be3e4309058e156948cb73196e5fd994268b89b5e3f5a26ee2b582", size = 5002698 }, + { url = "https://files.pythonhosted.org/packages/7b/56/cdddc63333ed77e810df56e5e7fb93659022d535a670335d8792be6d59fd/fonttools-4.58.1-cp313-cp313-win32.whl", hash = "sha256:aed7f93a9a072f0ce6fb46aad9474824ac6dd9c7c38a72f8295dd14f2215950f", size = 2174515 }, + { url = "https://files.pythonhosted.org/packages/ba/81/c7f395718e44cebe1010fcd7f1b91957d65d512d5f03114d2d6d00cae1c4/fonttools-4.58.1-cp313-cp313-win_amd64.whl", hash = "sha256:b27d69c97c20c9bca807f7ae7fc7df459eb62994859ff6a2a489e420634deac3", size = 2225290 }, + { url = "https://files.pythonhosted.org/packages/21/ff/995277586691c0cc314c28b24b4ec30610440fd7bf580072aed1409f95b0/fonttools-4.58.1-py3-none-any.whl", hash = "sha256:db88365d0962cd6f5bce54b190a4669aeed9c9941aa7bd60a5af084d8d9173d6", size = 1113429 }, +] + +[[package]] +name = "formulaic" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "interface-meta" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "scipy" }, + { name = "typing-extensions" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/30/03b5e3bb62374db3f665ca3020fdfc4304e98ceeaaa9dcd7a47a6b574ebf/formulaic-1.1.1.tar.gz", hash = "sha256:ddf80e4bef976dd99698aa27512015276c7b86c314b601ae6fd360c7741b7231", size = 652602 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/c2/a34097e53efe70a538ae97574ff9e9866e60fc1c792c19da5fd6b56ce7b5/formulaic-1.1.1-py3-none-any.whl", hash = "sha256:bbb7e38f99e4bcdc62cb0a6a818ad33b370b4e98e9e4f0b276561448482c8268", size = 115718 }, +] + +[[package]] +name = "frozenlist" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/f4/d744cba2da59b5c1d88823cf9e8a6c74e4659e2b27604ed973be2a0bf5ab/frozenlist-1.6.0.tar.gz", hash = "sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68", size = 42831 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/b5/bc883b5296ec902115c00be161da93bf661199c465ec4c483feec6ea4c32/frozenlist-1.6.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae8337990e7a45683548ffb2fee1af2f1ed08169284cd829cdd9a7fa7470530d", size = 160912 }, + { url = "https://files.pythonhosted.org/packages/6f/93/51b058b563d0704b39c56baa222828043aafcac17fd3734bec5dbeb619b1/frozenlist-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8c952f69dd524558694818a461855f35d36cc7f5c0adddce37e962c85d06eac0", size = 124315 }, + { url = "https://files.pythonhosted.org/packages/c9/e0/46cd35219428d350558b874d595e132d1c17a9471a1bd0d01d518a261e7c/frozenlist-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f5fef13136c4e2dee91bfb9a44e236fff78fc2cd9f838eddfc470c3d7d90afe", size = 122230 }, + { url = "https://files.pythonhosted.org/packages/d1/0f/7ad2ce928ad06d6dd26a61812b959ded573d3e9d0ee6109d96c2be7172e9/frozenlist-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:716bbba09611b4663ecbb7cd022f640759af8259e12a6ca939c0a6acd49eedba", size = 314842 }, + { url = "https://files.pythonhosted.org/packages/34/76/98cbbd8a20a5c3359a2004ae5e5b216af84a150ccbad67c8f8f30fb2ea91/frozenlist-1.6.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7b8c4dc422c1a3ffc550b465090e53b0bf4839047f3e436a34172ac67c45d595", size = 304919 }, + { url = "https://files.pythonhosted.org/packages/9a/fa/258e771ce3a44348c05e6b01dffc2bc67603fba95761458c238cd09a2c77/frozenlist-1.6.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b11534872256e1666116f6587a1592ef395a98b54476addb5e8d352925cb5d4a", size = 324074 }, + { url = "https://files.pythonhosted.org/packages/d5/a4/047d861fd8c538210e12b208c0479912273f991356b6bdee7ea8356b07c9/frozenlist-1.6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c6eceb88aaf7221f75be6ab498dc622a151f5f88d536661af3ffc486245a626", size = 321292 }, + { url = "https://files.pythonhosted.org/packages/c0/25/cfec8af758b4525676cabd36efcaf7102c1348a776c0d1ad046b8a7cdc65/frozenlist-1.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62c828a5b195570eb4b37369fcbbd58e96c905768d53a44d13044355647838ff", size = 301569 }, + { url = "https://files.pythonhosted.org/packages/87/2f/0c819372fa9f0c07b153124bf58683b8d0ca7bb73ea5ccde9b9ef1745beb/frozenlist-1.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1c6bd2c6399920c9622362ce95a7d74e7f9af9bfec05fff91b8ce4b9647845a", size = 313625 }, + { url = "https://files.pythonhosted.org/packages/50/5f/f0cf8b0fdedffdb76b3745aa13d5dbe404d63493cc211ce8250f2025307f/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:49ba23817781e22fcbd45fd9ff2b9b8cdb7b16a42a4851ab8025cae7b22e96d0", size = 312523 }, + { url = "https://files.pythonhosted.org/packages/e1/6c/38c49108491272d3e84125bbabf2c2d0b304899b52f49f0539deb26ad18d/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:431ef6937ae0f853143e2ca67d6da76c083e8b1fe3df0e96f3802fd37626e606", size = 322657 }, + { url = "https://files.pythonhosted.org/packages/bd/4b/3bd3bad5be06a9d1b04b1c22be80b5fe65b502992d62fab4bdb25d9366ee/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9d124b38b3c299ca68433597ee26b7819209cb8a3a9ea761dfe9db3a04bba584", size = 303414 }, + { url = "https://files.pythonhosted.org/packages/5b/89/7e225a30bef6e85dbfe22622c24afe932e9444de3b40d58b1ea589a14ef8/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:118e97556306402e2b010da1ef21ea70cb6d6122e580da64c056b96f524fbd6a", size = 320321 }, + { url = "https://files.pythonhosted.org/packages/22/72/7e3acef4dd9e86366cb8f4d8f28e852c2b7e116927e9722b31a6f71ea4b0/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fb3b309f1d4086b5533cf7bbcf3f956f0ae6469664522f1bde4feed26fba60f1", size = 323975 }, + { url = "https://files.pythonhosted.org/packages/d8/85/e5da03d20507e13c66ce612c9792b76811b7a43e3320cce42d95b85ac755/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54dece0d21dce4fdb188a1ffc555926adf1d1c516e493c2914d7c370e454bc9e", size = 316553 }, + { url = "https://files.pythonhosted.org/packages/ac/8e/6c609cbd0580ae8a0661c408149f196aade7d325b1ae7adc930501b81acb/frozenlist-1.6.0-cp311-cp311-win32.whl", hash = "sha256:654e4ba1d0b2154ca2f096bed27461cf6160bc7f504a7f9a9ef447c293caf860", size = 115511 }, + { url = "https://files.pythonhosted.org/packages/f2/13/a84804cfde6de12d44ed48ecbf777ba62b12ff09e761f76cdd1ff9e14bb1/frozenlist-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e911391bffdb806001002c1f860787542f45916c3baf764264a52765d5a5603", size = 120863 }, + { url = "https://files.pythonhosted.org/packages/9c/8a/289b7d0de2fbac832ea80944d809759976f661557a38bb8e77db5d9f79b7/frozenlist-1.6.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c5b9e42ace7d95bf41e19b87cec8f262c41d3510d8ad7514ab3862ea2197bfb1", size = 160193 }, + { url = "https://files.pythonhosted.org/packages/19/80/2fd17d322aec7f430549f0669f599997174f93ee17929ea5b92781ec902c/frozenlist-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ca9973735ce9f770d24d5484dcb42f68f135351c2fc81a7a9369e48cf2998a29", size = 123831 }, + { url = "https://files.pythonhosted.org/packages/99/06/f5812da431273f78c6543e0b2f7de67dfd65eb0a433978b2c9c63d2205e4/frozenlist-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ac40ec76041c67b928ca8aaffba15c2b2ee3f5ae8d0cb0617b5e63ec119ca25", size = 121862 }, + { url = "https://files.pythonhosted.org/packages/d0/31/9e61c6b5fc493cf24d54881731204d27105234d09878be1a5983182cc4a5/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b7a8a3180dfb280eb044fdec562f9b461614c0ef21669aea6f1d3dac6ee576", size = 316361 }, + { url = "https://files.pythonhosted.org/packages/9d/55/22ca9362d4f0222324981470fd50192be200154d51509ee6eb9baa148e96/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c444d824e22da6c9291886d80c7d00c444981a72686e2b59d38b285617cb52c8", size = 307115 }, + { url = "https://files.pythonhosted.org/packages/ae/39/4fff42920a57794881e7bb3898dc7f5f539261711ea411b43bba3cde8b79/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb52c8166499a8150bfd38478248572c924c003cbb45fe3bcd348e5ac7c000f9", size = 322505 }, + { url = "https://files.pythonhosted.org/packages/55/f2/88c41f374c1e4cf0092a5459e5f3d6a1e17ed274c98087a76487783df90c/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b35298b2db9c2468106278537ee529719228950a5fdda686582f68f247d1dc6e", size = 322666 }, + { url = "https://files.pythonhosted.org/packages/75/51/034eeb75afdf3fd03997856195b500722c0b1a50716664cde64e28299c4b/frozenlist-1.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d108e2d070034f9d57210f22fefd22ea0d04609fc97c5f7f5a686b3471028590", size = 302119 }, + { url = "https://files.pythonhosted.org/packages/2b/a6/564ecde55ee633270a793999ef4fd1d2c2b32b5a7eec903b1012cb7c5143/frozenlist-1.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e1be9111cb6756868ac242b3c2bd1f09d9aea09846e4f5c23715e7afb647103", size = 316226 }, + { url = "https://files.pythonhosted.org/packages/f1/c8/6c0682c32377f402b8a6174fb16378b683cf6379ab4d2827c580892ab3c7/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:94bb451c664415f02f07eef4ece976a2c65dcbab9c2f1705b7031a3a75349d8c", size = 312788 }, + { url = "https://files.pythonhosted.org/packages/b6/b8/10fbec38f82c5d163ca1750bfff4ede69713badf236a016781cf1f10a0f0/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d1a686d0b0949182b8faddea596f3fc11f44768d1f74d4cad70213b2e139d821", size = 325914 }, + { url = "https://files.pythonhosted.org/packages/62/ca/2bf4f3a1bd40cdedd301e6ecfdbb291080d5afc5f9ce350c0739f773d6b9/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ea8e59105d802c5a38bdbe7362822c522230b3faba2aa35c0fa1765239b7dd70", size = 305283 }, + { url = "https://files.pythonhosted.org/packages/09/64/20cc13ccf94abc2a1f482f74ad210703dc78a590d0b805af1c9aa67f76f9/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:abc4e880a9b920bc5020bf6a431a6bb40589d9bca3975c980495f63632e8382f", size = 319264 }, + { url = "https://files.pythonhosted.org/packages/20/ff/86c6a2bbe98cfc231519f5e6d712a0898488ceac804a917ce014f32e68f6/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9a79713adfe28830f27a3c62f6b5406c37376c892b05ae070906f07ae4487046", size = 326482 }, + { url = "https://files.pythonhosted.org/packages/2f/da/8e381f66367d79adca245d1d71527aac774e30e291d41ef161ce2d80c38e/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a0318c2068e217a8f5e3b85e35899f5a19e97141a45bb925bb357cfe1daf770", size = 318248 }, + { url = "https://files.pythonhosted.org/packages/39/24/1a1976563fb476ab6f0fa9fefaac7616a4361dbe0461324f9fd7bf425dbe/frozenlist-1.6.0-cp312-cp312-win32.whl", hash = "sha256:853ac025092a24bb3bf09ae87f9127de9fe6e0c345614ac92536577cf956dfcc", size = 115161 }, + { url = "https://files.pythonhosted.org/packages/80/2e/fb4ed62a65f8cd66044706b1013f0010930d8cbb0729a2219561ea075434/frozenlist-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:2bdfe2d7e6c9281c6e55523acd6c2bf77963cb422fdc7d142fb0cb6621b66878", size = 120548 }, + { url = "https://files.pythonhosted.org/packages/6f/e5/04c7090c514d96ca00887932417f04343ab94904a56ab7f57861bf63652d/frozenlist-1.6.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d7fb014fe0fbfee3efd6a94fc635aeaa68e5e1720fe9e57357f2e2c6e1a647e", size = 158182 }, + { url = "https://files.pythonhosted.org/packages/e9/8f/60d0555c61eec855783a6356268314d204137f5e0c53b59ae2fc28938c99/frozenlist-1.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01bcaa305a0fdad12745502bfd16a1c75b14558dabae226852f9159364573117", size = 122838 }, + { url = "https://files.pythonhosted.org/packages/5a/a7/d0ec890e3665b4b3b7c05dc80e477ed8dc2e2e77719368e78e2cd9fec9c8/frozenlist-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b314faa3051a6d45da196a2c495e922f987dc848e967d8cfeaee8a0328b1cd4", size = 120980 }, + { url = "https://files.pythonhosted.org/packages/cc/19/9b355a5e7a8eba903a008579964192c3e427444752f20b2144b10bb336df/frozenlist-1.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da62fecac21a3ee10463d153549d8db87549a5e77eefb8c91ac84bb42bb1e4e3", size = 305463 }, + { url = "https://files.pythonhosted.org/packages/9c/8d/5b4c758c2550131d66935ef2fa700ada2461c08866aef4229ae1554b93ca/frozenlist-1.6.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1eb89bf3454e2132e046f9599fbcf0a4483ed43b40f545551a39316d0201cd1", size = 297985 }, + { url = "https://files.pythonhosted.org/packages/48/2c/537ec09e032b5865715726b2d1d9813e6589b571d34d01550c7aeaad7e53/frozenlist-1.6.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18689b40cb3936acd971f663ccb8e2589c45db5e2c5f07e0ec6207664029a9c", size = 311188 }, + { url = "https://files.pythonhosted.org/packages/31/2f/1aa74b33f74d54817055de9a4961eff798f066cdc6f67591905d4fc82a84/frozenlist-1.6.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e67ddb0749ed066b1a03fba812e2dcae791dd50e5da03be50b6a14d0c1a9ee45", size = 311874 }, + { url = "https://files.pythonhosted.org/packages/bf/f0/cfec18838f13ebf4b37cfebc8649db5ea71a1b25dacd691444a10729776c/frozenlist-1.6.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc5e64626e6682638d6e44398c9baf1d6ce6bc236d40b4b57255c9d3f9761f1f", size = 291897 }, + { url = "https://files.pythonhosted.org/packages/ea/a5/deb39325cbbea6cd0a46db8ccd76150ae2fcbe60d63243d9df4a0b8c3205/frozenlist-1.6.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:437cfd39564744ae32ad5929e55b18ebd88817f9180e4cc05e7d53b75f79ce85", size = 305799 }, + { url = "https://files.pythonhosted.org/packages/78/22/6ddec55c5243a59f605e4280f10cee8c95a449f81e40117163383829c241/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62dd7df78e74d924952e2feb7357d826af8d2f307557a779d14ddf94d7311be8", size = 302804 }, + { url = "https://files.pythonhosted.org/packages/5d/b7/d9ca9bab87f28855063c4d202936800219e39db9e46f9fb004d521152623/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a66781d7e4cddcbbcfd64de3d41a61d6bdde370fc2e38623f30b2bd539e84a9f", size = 316404 }, + { url = "https://files.pythonhosted.org/packages/a6/3a/1255305db7874d0b9eddb4fe4a27469e1fb63720f1fc6d325a5118492d18/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:482fe06e9a3fffbcd41950f9d890034b4a54395c60b5e61fae875d37a699813f", size = 295572 }, + { url = "https://files.pythonhosted.org/packages/2a/f2/8d38eeee39a0e3a91b75867cc102159ecccf441deb6ddf67be96d3410b84/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e4f9373c500dfc02feea39f7a56e4f543e670212102cc2eeb51d3a99c7ffbde6", size = 307601 }, + { url = "https://files.pythonhosted.org/packages/38/04/80ec8e6b92f61ef085422d7b196822820404f940950dde5b2e367bede8bc/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e69bb81de06827147b7bfbaeb284d85219fa92d9f097e32cc73675f279d70188", size = 314232 }, + { url = "https://files.pythonhosted.org/packages/3a/58/93b41fb23e75f38f453ae92a2f987274c64637c450285577bd81c599b715/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7613d9977d2ab4a9141dde4a149f4357e4065949674c5649f920fec86ecb393e", size = 308187 }, + { url = "https://files.pythonhosted.org/packages/6a/a2/e64df5c5aa36ab3dee5a40d254f3e471bb0603c225f81664267281c46a2d/frozenlist-1.6.0-cp313-cp313-win32.whl", hash = "sha256:4def87ef6d90429f777c9d9de3961679abf938cb6b7b63d4a7eb8a268babfce4", size = 114772 }, + { url = "https://files.pythonhosted.org/packages/a0/77/fead27441e749b2d574bb73d693530d59d520d4b9e9679b8e3cb779d37f2/frozenlist-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:37a8a52c3dfff01515e9bbbee0e6063181362f9de3db2ccf9bc96189b557cbfd", size = 119847 }, + { url = "https://files.pythonhosted.org/packages/df/bd/cc6d934991c1e5d9cafda83dfdc52f987c7b28343686aef2e58a9cf89f20/frozenlist-1.6.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:46138f5a0773d064ff663d273b309b696293d7a7c00a0994c5c13a5078134b64", size = 174937 }, + { url = "https://files.pythonhosted.org/packages/f2/a2/daf945f335abdbfdd5993e9dc348ef4507436936ab3c26d7cfe72f4843bf/frozenlist-1.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f88bc0a2b9c2a835cb888b32246c27cdab5740059fb3688852bf91e915399b91", size = 136029 }, + { url = "https://files.pythonhosted.org/packages/51/65/4c3145f237a31247c3429e1c94c384d053f69b52110a0d04bfc8afc55fb2/frozenlist-1.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:777704c1d7655b802c7850255639672e90e81ad6fa42b99ce5ed3fbf45e338dd", size = 134831 }, + { url = "https://files.pythonhosted.org/packages/77/38/03d316507d8dea84dfb99bdd515ea245628af964b2bf57759e3c9205cc5e/frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ef8d41764c7de0dcdaf64f733a27352248493a85a80661f3c678acd27e31f2", size = 392981 }, + { url = "https://files.pythonhosted.org/packages/37/02/46285ef9828f318ba400a51d5bb616ded38db8466836a9cfa39f3903260b/frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:da5cb36623f2b846fb25009d9d9215322318ff1c63403075f812b3b2876c8506", size = 371999 }, + { url = "https://files.pythonhosted.org/packages/0d/64/1212fea37a112c3c5c05bfb5f0a81af4836ce349e69be75af93f99644da9/frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cbb56587a16cf0fb8acd19e90ff9924979ac1431baea8681712716a8337577b0", size = 392200 }, + { url = "https://files.pythonhosted.org/packages/81/ce/9a6ea1763e3366e44a5208f76bf37c76c5da570772375e4d0be85180e588/frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6154c3ba59cda3f954c6333025369e42c3acd0c6e8b6ce31eb5c5b8116c07e0", size = 390134 }, + { url = "https://files.pythonhosted.org/packages/bc/36/939738b0b495b2c6d0c39ba51563e453232813042a8d908b8f9544296c29/frozenlist-1.6.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e8246877afa3f1ae5c979fe85f567d220f86a50dc6c493b9b7d8191181ae01e", size = 365208 }, + { url = "https://files.pythonhosted.org/packages/b4/8b/939e62e93c63409949c25220d1ba8e88e3960f8ef6a8d9ede8f94b459d27/frozenlist-1.6.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0f6cce16306d2e117cf9db71ab3a9e8878a28176aeaf0dbe35248d97b28d0c", size = 385548 }, + { url = "https://files.pythonhosted.org/packages/62/38/22d2873c90102e06a7c5a3a5b82ca47e393c6079413e8a75c72bff067fa8/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1b8e8cd8032ba266f91136d7105706ad57770f3522eac4a111d77ac126a25a9b", size = 391123 }, + { url = "https://files.pythonhosted.org/packages/44/78/63aaaf533ee0701549500f6d819be092c6065cb5c577edb70c09df74d5d0/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e2ada1d8515d3ea5378c018a5f6d14b4994d4036591a52ceaf1a1549dec8e1ad", size = 394199 }, + { url = "https://files.pythonhosted.org/packages/54/45/71a6b48981d429e8fbcc08454dc99c4c2639865a646d549812883e9c9dd3/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:cdb2c7f071e4026c19a3e32b93a09e59b12000751fc9b0b7758da899e657d215", size = 373854 }, + { url = "https://files.pythonhosted.org/packages/3f/f3/dbf2a5e11736ea81a66e37288bf9f881143a7822b288a992579ba1b4204d/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:03572933a1969a6d6ab509d509e5af82ef80d4a5d4e1e9f2e1cdd22c77a3f4d2", size = 395412 }, + { url = "https://files.pythonhosted.org/packages/b3/f1/c63166806b331f05104d8ea385c4acd511598568b1f3e4e8297ca54f2676/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:77effc978947548b676c54bbd6a08992759ea6f410d4987d69feea9cd0919911", size = 394936 }, + { url = "https://files.pythonhosted.org/packages/ef/ea/4f3e69e179a430473eaa1a75ff986526571215fefc6b9281cdc1f09a4eb8/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a2bda8be77660ad4089caf2223fdbd6db1858462c4b85b67fbfa22102021e497", size = 391459 }, + { url = "https://files.pythonhosted.org/packages/d3/c3/0fc2c97dea550df9afd072a37c1e95421652e3206bbeaa02378b24c2b480/frozenlist-1.6.0-cp313-cp313t-win32.whl", hash = "sha256:a4d96dc5bcdbd834ec6b0f91027817214216b5b30316494d2b1aebffb87c534f", size = 128797 }, + { url = "https://files.pythonhosted.org/packages/ae/f5/79c9320c5656b1965634fe4be9c82b12a3305bdbc58ad9cb941131107b20/frozenlist-1.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e18036cb4caa17ea151fd5f3d70be9d354c99eb8cf817a3ccde8a7873b074348", size = 134709 }, + { url = "https://files.pythonhosted.org/packages/71/3e/b04a0adda73bd52b390d730071c0d577073d3d26740ee1bad25c3ad0f37b/frozenlist-1.6.0-py3-none-any.whl", hash = "sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191", size = 12404 }, +] + +[[package]] +name = "fsspec" +version = "2025.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/f7/27f15d41f0ed38e8fcc488584b57e902b331da7f7c6dcda53721b15838fc/fsspec-2025.5.1.tar.gz", hash = "sha256:2e55e47a540b91843b755e83ded97c6e897fa0942b11490113f09e9c443c2475", size = 303033 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl", hash = "sha256:24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462", size = 199052 }, +] + +[package.optional-dependencies] +http = [ + { name = "aiohttp" }, +] + +[[package]] +name = "ftfy" +version = "6.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/d3/8650919bc3c7c6e90ee3fa7fd618bf373cbbe55dff043bd67353dbb20cd8/ftfy-6.3.1.tar.gz", hash = "sha256:9b3c3d90f84fb267fe64d375a07b7f8912d817cf86009ae134aa03e1819506ec", size = 308927 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/6e/81d47999aebc1b155f81eca4477a616a70f238a2549848c38983f3c22a82/ftfy-6.3.1-py3-none-any.whl", hash = "sha256:7c70eb532015cd2f9adb53f101fb6c7945988d023a085d127d1573dc49dd0083", size = 44821 }, +] + +[[package]] +name = "fvcore" +version = "0.1.5.post20221221" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "iopath" }, + { name = "numpy" }, + { name = "pillow" }, + { name = "pyyaml" }, + { name = "tabulate" }, + { name = "termcolor" }, + { name = "tqdm" }, + { name = "yacs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/93/d056a9c4efc6c79ba7b5159cc66bb436db93d2cc46dca18ed65c59cc8e4e/fvcore-0.1.5.post20221221.tar.gz", hash = "sha256:f2fb0bb90572ae651c11c78e20493ed19b2240550a7e4bbb2d6de87bdd037860", size = 50217 } + +[[package]] +name = "gdown" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "filelock" }, + { name = "requests", extra = ["socks"] }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/6a/37e6b70c5bda3161e40265861e63b64a86bfc6ca6a8f1c35328a675c84fd/gdown-5.2.0.tar.gz", hash = "sha256:2145165062d85520a3cd98b356c9ed522c5e7984d408535409fd46f94defc787", size = 284647 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/70/e07c381e6488a77094f04c85c9caf1c8008cdc30778f7019bc52e5285ef0/gdown-5.2.0-py3-none-any.whl", hash = "sha256:33083832d82b1101bdd0e9df3edd0fbc0e1c5f14c9d8c38d2a35bf1683b526d6", size = 18235 }, +] + +[[package]] +name = "geopandas" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pyogrio" }, + { name = "pyproj" }, + { name = "shapely" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/ca/e62641e5391285cda854c2802e706e6686f62fc9d919ecf78ff7f8d42654/geopandas-1.1.0.tar.gz", hash = "sha256:d176b084170539044ce7554a1219a4433fa1bfba94035b5a519c8986330e429e", size = 331955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/82/79e02a0e5dd4aca81894842b9d6522624a40048a913c6384efb2987a4144/geopandas-1.1.0-py3-none-any.whl", hash = "sha256:b19b18bdc736ee05b237f5e9184211c452768a4c883f7d7f8421b0cbe1da5875", size = 338014 }, +] + +[[package]] +name = "gigapath" +version = "0.1.0" +source = { git = "https://github.com/EzicStar/prov-gigapath.git?rev=d4cf55321df37aaf867e24a31c61bcf490a296eb#d4cf55321df37aaf867e24a31c61bcf490a296eb" } +dependencies = [ + { name = "fairscale" }, + { name = "flash-attn" }, + { name = "fvcore" }, + { name = "iopath" }, + { name = "lifelines" }, + { name = "monai" }, + { name = "ninja" }, + { name = "scikit-image" }, + { name = "scikit-survival" }, + { name = "wandb" }, + { name = "webdataset" }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 }, +] + +[[package]] +name = "gitpython" +version = "3.1.44" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599 }, +] + +[[package]] +name = "h5py" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/2e/a22d6a8bfa6f8be33e7febd985680fba531562795f0a9077ed1eb047bfb0/h5py-3.13.0.tar.gz", hash = "sha256:1870e46518720023da85d0895a1960ff2ce398c5671eac3b1a41ec696b7105c3", size = 414876 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/2b/50b15fdefb577d073b49699e6ea6a0a77a3a1016c2b67e2149fc50124a10/h5py-3.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8a8e38ef4ceb969f832cc230c0cf808c613cc47e31e768fd7b1106c55afa1cb8", size = 3422922 }, + { url = "https://files.pythonhosted.org/packages/94/59/36d87a559cab9c59b59088d52e86008d27a9602ce3afc9d3b51823014bf3/h5py-3.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f35640e81b03c02a88b8bf99fb6a9d3023cc52f7c627694db2f379e0028f2868", size = 2921619 }, + { url = "https://files.pythonhosted.org/packages/37/ef/6f80b19682c0b0835bbee7b253bec9c16af9004f2fd6427b1dd858100273/h5py-3.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:337af114616f3656da0c83b68fcf53ecd9ce9989a700b0883a6e7c483c3235d4", size = 4259366 }, + { url = "https://files.pythonhosted.org/packages/03/71/c99f662d4832c8835453cf3476f95daa28372023bda4aa1fca9e97c24f09/h5py-3.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:782ff0ac39f455f21fd1c8ebc007328f65f43d56718a89327eec76677ebf238a", size = 4509058 }, + { url = "https://files.pythonhosted.org/packages/56/89/e3ff23e07131ff73a72a349be9639e4de84e163af89c1c218b939459a98a/h5py-3.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:22ffe2a25770a2d67213a1b94f58006c14dce06933a42d2aaa0318c5868d1508", size = 2966428 }, + { url = "https://files.pythonhosted.org/packages/d8/20/438f6366ba4ded80eadb38f8927f5e2cd6d2e087179552f20ae3dbcd5d5b/h5py-3.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:477c58307b6b9a2509c59c57811afb9f598aedede24a67da808262dfa0ee37b4", size = 3384442 }, + { url = "https://files.pythonhosted.org/packages/10/13/cc1cb7231399617d9951233eb12fddd396ff5d4f7f057ee5d2b1ca0ee7e7/h5py-3.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:57c4c74f627c616f02b7aec608a8c706fe08cb5b0ba7c08555a4eb1dde20805a", size = 2917567 }, + { url = "https://files.pythonhosted.org/packages/9e/d9/aed99e1c858dc698489f916eeb7c07513bc864885d28ab3689d572ba0ea0/h5py-3.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:357e6dc20b101a805ccfd0024731fbaf6e8718c18c09baf3b5e4e9d198d13fca", size = 4669544 }, + { url = "https://files.pythonhosted.org/packages/a7/da/3c137006ff5f0433f0fb076b1ebe4a7bf7b5ee1e8811b5486af98b500dd5/h5py-3.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6f13f9b5ce549448c01e4dfe08ea8d1772e6078799af2c1c8d09e941230a90d", size = 4932139 }, + { url = "https://files.pythonhosted.org/packages/25/61/d897952629cae131c19d4c41b2521e7dd6382f2d7177c87615c2e6dced1a/h5py-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:21daf38171753899b5905f3d82c99b0b1ec2cbbe282a037cad431feb620e62ec", size = 2954179 }, + { url = "https://files.pythonhosted.org/packages/60/43/f276f27921919a9144074320ce4ca40882fc67b3cfee81c3f5c7df083e97/h5py-3.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e520ec76de00943dd017c8ea3f354fa1d2f542eac994811943a8faedf2a7d5cb", size = 3358040 }, + { url = "https://files.pythonhosted.org/packages/1b/86/ad4a4cf781b08d4572be8bbdd8f108bb97b266a14835c640dc43dafc0729/h5py-3.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e79d8368cd9295045956bfb436656bea3f915beaa11d342e9f79f129f5178763", size = 2892766 }, + { url = "https://files.pythonhosted.org/packages/69/84/4c6367d6b58deaf0fa84999ec819e7578eee96cea6cbd613640d0625ed5e/h5py-3.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56dd172d862e850823c4af02dc4ddbc308f042b85472ffdaca67f1598dff4a57", size = 4664255 }, + { url = "https://files.pythonhosted.org/packages/fd/41/bc2df86b72965775f6d621e0ee269a5f3ac23e8f870abf519de9c7d93b4d/h5py-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be949b46b7388074c5acae017fbbe3e5ba303fd9daaa52157fdfef30bbdacadd", size = 4927580 }, + { url = "https://files.pythonhosted.org/packages/97/34/165b87ea55184770a0c1fcdb7e017199974ad2e271451fd045cfe35f3add/h5py-3.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:4f97ecde7ac6513b21cd95efdfc38dc6d19f96f6ca6f2a30550e94e551458e0a", size = 2940890 }, +] + +[[package]] +name = "hf-xet" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/be/58f20728a5b445f8b064e74f0618897b3439f5ef90934da1916b9dfac76f/hf_xet-1.1.2.tar.gz", hash = "sha256:3712d6d4819d3976a1c18e36db9f503e296283f9363af818f50703506ed63da3", size = 467009 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/ae/f1a63f75d9886f18a80220ba31a1c7b9c4752f03aae452f358f538c6a991/hf_xet-1.1.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dfd1873fd648488c70735cb60f7728512bca0e459e61fcd107069143cd798469", size = 2642559 }, + { url = "https://files.pythonhosted.org/packages/50/ab/d2c83ae18f1015d926defd5bfbe94c62d15e93f900e6a192e318ee947105/hf_xet-1.1.2-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:29b584983b2d977c44157d9241dcf0fd50acde0b7bff8897fe4386912330090d", size = 2541360 }, + { url = "https://files.pythonhosted.org/packages/9f/a7/693dc9f34f979e30a378125e2150a0b2d8d166e6d83ce3950eeb81e560aa/hf_xet-1.1.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b29ac84298147fe9164cc55ad994ba47399f90b5d045b0b803b99cf5f06d8ec", size = 5183081 }, + { url = "https://files.pythonhosted.org/packages/3d/23/c48607883f692a36c0a7735f47f98bad32dbe459a32d1568c0f21576985d/hf_xet-1.1.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d921ba32615676e436a0d15e162331abc9ed43d440916b1d836dc27ce1546173", size = 5356100 }, + { url = "https://files.pythonhosted.org/packages/eb/5b/b2316c7f1076da0582b52ea228f68bea95e243c388440d1dc80297c9d813/hf_xet-1.1.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d9b03c34e13c44893ab6e8fea18ee8d2a6878c15328dd3aabedbdd83ee9f2ed3", size = 5647688 }, + { url = "https://files.pythonhosted.org/packages/2c/98/e6995f0fa579929da7795c961f403f4ee84af36c625963f52741d56f242c/hf_xet-1.1.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:01b18608955b3d826307d37da8bd38b28a46cd2d9908b3a3655d1363274f941a", size = 5322627 }, + { url = "https://files.pythonhosted.org/packages/59/40/8f1d5a44a64d8bf9e3c19576e789f716af54875b46daae65426714e75db1/hf_xet-1.1.2-cp37-abi3-win_amd64.whl", hash = "sha256:3562902c81299b09f3582ddfb324400c6a901a2f3bc854f83556495755f4954c", size = 2739542 }, +] + +[[package]] +name = "huggingface-hub" +version = "0.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/74/c4961b31e0f142a032ea24f477c3a7524dfabfd8126398a968b3cc6bf804/huggingface_hub-0.32.3.tar.gz", hash = "sha256:752c889ebf3a63cbd39803f6d87ccc135a463bbcb36abfa2faff0ccbf1cec087", size = 424525 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/dc/4f4d8080cbce7a38c1d0f1ba4932f9134480b9761af8ef4c65d49254b2bd/huggingface_hub-0.32.3-py3-none-any.whl", hash = "sha256:e46f7ea7fe2b5e5f67cc4e37eb201140091946a314d7c2b134a9673dadd80b6a", size = 512094 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "imageio" +version = "2.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/47/57e897fb7094afb2d26e8b2e4af9a45c7cf1a405acdeeca001fdf2c98501/imageio-2.37.0.tar.gz", hash = "sha256:71b57b3669666272c818497aebba2b4c5f20d5b37c81720e5e1a56d59c492996", size = 389963 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/bd/b394387b598ed84d8d0fa90611a90bee0adc2021820ad5729f7ced74a8e2/imageio-2.37.0-py3-none-any.whl", hash = "sha256:11efa15b87bc7871b61590326b2d635439acc321cf7f8ce996f812543ce10eed", size = 315796 }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, +] + +[[package]] +name = "interface-meta" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/75/10526292b332f3479c246750a96f6ec11a28e297839a9c25583b2aadc119/interface_meta-1.3.0.tar.gz", hash = "sha256:8a4493f8bdb73fb9655dcd5115bc897e207319e36c8835f39c516a2d7e9d79a1", size = 15007 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/3f/a6ec28c88e2d8e54d32598a1e0b5208a4baa72a8e7f6e241beab5731eb9d/interface_meta-1.3.0-py3-none-any.whl", hash = "sha256:de35dc5241431886e709e20a14d6597ed07c9f1e8b4bfcffde2190ca5b700ee8", size = 14854 }, +] + +[[package]] +name = "iopath" +version = "0.1.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "portalocker" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/73/b3d451dfc523756cf177d3ebb0af76dc7751b341c60e2a21871be400ae29/iopath-0.1.10.tar.gz", hash = "sha256:3311c16a4d9137223e20f141655759933e1eda24f8bff166af834af3c645ef01", size = 42226 } + +[[package]] +name = "ipykernel" +version = "6.29.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/5c/67594cb0c7055dc50814b21731c22a601101ea3b1b50a9a1b090e11f5d0f/ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215", size = 163367 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/5c/368ae6c01c7628438358e6d337c19b05425727fbb221d2a3c4303c372f42/ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5", size = 117173 }, +] + +[[package]] +name = "ipython" +version = "9.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/09/4c7e06b96fbd203e06567b60fb41b06db606b6a82db6db7b2c85bb72a15c/ipython-9.3.0.tar.gz", hash = "sha256:79eb896f9f23f50ad16c3bc205f686f6e030ad246cc309c6279a242b14afe9d8", size = 4426460 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/99/9ed3d52d00f1846679e3aa12e2326ac7044b5e7f90dc822b60115fa533ca/ipython-9.3.0-py3-none-any.whl", hash = "sha256:1a0b6dd9221a1f5dddf725b57ac0cb6fddc7b5f470576231ae9162b9b3455a04", size = 605320 }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074 }, +] + +[[package]] +name = "jaxtyping" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wadler-lindig" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/a8/416bd7ea110ec6b68e8868b0f99c985c735adcf7badc491d3c343937260a/jaxtyping-0.3.2.tar.gz", hash = "sha256:f30483fac4b42e915db8ad2414a85c3b63284aa7d3c100b96b59f755cf4a86ad", size = 44989 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/b9/281e10e2d967ea5e481683eaec99f55ac5a61085ee60551c36942ef32bef/jaxtyping-0.3.2-py3-none-any.whl", hash = "sha256:6a020fd276226ddb5ac4f5725323843dd65e3c7e85c64fd62431e5f738c74e04", size = 55409 }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278 }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, +] + +[[package]] +name = "joblib" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/fe/0f5a938c54105553436dbff7a61dc4fed4b1b2c98852f8833beaf4d5968f/joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444", size = 330475 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746 }, +] + +[[package]] +name = "jupyter-client" +version = "8.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105 }, +] + +[[package]] +name = "jupyter-core" +version = "5.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/1b/72906d554acfeb588332eaaa6f61577705e9ec752ddb486f302dafa292d9/jupyter_core-5.8.1.tar.gz", hash = "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941", size = 88923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/57/6bffd4b20b88da3800c5d691e0337761576ee688eb01299eae865689d2df/jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0", size = 28880 }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635 }, + { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717 }, + { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413 }, + { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994 }, + { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804 }, + { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690 }, + { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839 }, + { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109 }, + { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269 }, + { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468 }, + { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394 }, + { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901 }, + { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306 }, + { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966 }, + { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311 }, + { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152 }, + { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555 }, + { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067 }, + { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443 }, + { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728 }, + { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388 }, + { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849 }, + { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533 }, + { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898 }, + { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605 }, + { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801 }, + { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077 }, + { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410 }, + { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853 }, + { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424 }, + { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156 }, + { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555 }, + { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071 }, + { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053 }, + { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278 }, + { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139 }, + { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517 }, + { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952 }, + { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132 }, + { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997 }, + { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060 }, + { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471 }, + { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793 }, + { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855 }, + { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430 }, + { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294 }, + { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736 }, + { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194 }, + { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942 }, + { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341 }, + { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455 }, + { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138 }, + { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857 }, + { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129 }, + { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538 }, + { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661 }, + { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710 }, + { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213 }, +] + +[[package]] +name = "lazy-loader" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097 }, +] + +[[package]] +name = "lifelines" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "autograd" }, + { name = "autograd-gamma" }, + { name = "formulaic" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/4f/f0b363278d40baf7d7a03217bee839cb880946c62109f243391c8754bb09/lifelines-0.30.0.tar.gz", hash = "sha256:f7f6f6275fcb167fe0f5b1ef98f868993f9c074cb74b1dd6e92736efa854be18", size = 383221 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/f7/379e185a75ac8166ac70756d0ba68d9a2b02b555c7fde4983246752396bd/lifelines-0.30.0-py3-none-any.whl", hash = "sha256:ac7c602c8aceced9770d3977817c9d99c250ed8cd86f2567fa0d23e4e8014bf9", size = 349319 }, +] + +[[package]] +name = "lightning" +version = "2.5.1.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fsspec", extra = ["http"] }, + { name = "lightning-utilities" }, + { name = "packaging" }, + { name = "pytorch-lightning" }, + { name = "pyyaml" }, + { name = "torch" }, + { name = "torchmetrics" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/d0/fb3c5077efdd74c28ea3a277fd80bbf03738d866013a8637691138bfebca/lightning-2.5.1.post0.tar.gz", hash = "sha256:fda1ac63c283b3b08a54be8d905dd88469cf09e9845d36dd28b699e78911cbc8", size = 631113 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/1b/67201d693a575e8a086831710f33e697fab66166223f792e459ef2b84934/lightning-2.5.1.post0-py3-none-any.whl", hash = "sha256:a228a52ca52f0c5006ff327c92b8942f09e1aea3f2d9b0d7c8a209edd5b9e71d", size = 819001 }, +] + +[[package]] +name = "lightning-utilities" +version = "0.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "setuptools" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/bb/63a6a8c9e7a96b6ba92647fa5b1595c2dbee29f8178705adb4704d82ecba/lightning_utilities-0.14.3.tar.gz", hash = "sha256:37e2f83f273890052955a44054382c211a303012ee577619efbaa5df9e65e9f5", size = 30346 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/c1/31b3184cba7b257a4a3b5ca5b88b9204ccb7aa02fe3c992280899293ed54/lightning_utilities-0.14.3-py3-none-any.whl", hash = "sha256:4ab9066aa36cd7b93a05713808901909e96cc3f187ea6fd3052b2fd91313b468", size = 28894 }, +] + +[[package]] +name = "madeleine" +version = "0.0.1" +source = { git = "https://github.com/mahmoodlab/MADELEINE.git?rev=de7c85acc2bdad352e6df8eee5694f8b6f288012#de7c85acc2bdad352e6df8eee5694f8b6f288012" } +dependencies = [ + { name = "einops" }, + { name = "geopandas" }, + { name = "h5py" }, + { name = "huggingface-hub" }, + { name = "scikit-learn" }, + { name = "shapely" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "wandb" }, +] + +[[package]] +name = "mamba-ssm" +version = "2.2.4" +source = { git = "https://github.com/KatherLab/mamba.git?rev=ffef06879361ea36893697d88f43d4f76bb28877#ffef06879361ea36893697d88f43d4f76bb28877" } +dependencies = [ + { name = "einops" }, + { name = "ninja" }, + { name = "packaging" }, + { name = "setuptools" }, + { name = "torch" }, + { name = "transformers" }, + { name = "triton", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, + { name = "triton", version = "3.3.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'aarch64' or sys_platform != 'linux'" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "marshmallow" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/ff/26df5a9f5ac57ccf693a5854916ab47243039d2aa9e0fe5f5a0331e7b74b/marshmallow-4.0.0.tar.gz", hash = "sha256:3b6e80aac299a7935cfb97ed01d1854fb90b5079430969af92118ea1b12a8d55", size = 220507 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/26/6cc45d156f44dbe1d5696d9e54042e4dcaf7b946c0b86df6a97d29706f32/marshmallow-4.0.0-py3-none-any.whl", hash = "sha256:e7b0528337e9990fd64950f8a6b3a1baabed09ad17a0dfb844d701151f92d203", size = 48420 }, +] + +[[package]] +name = "matplotlib" +version = "3.10.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/91/d49359a21893183ed2a5b6c76bec40e0b1dcbf8ca148f864d134897cfc75/matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0", size = 34799811 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/bd/af9f655456f60fe1d575f54fb14704ee299b16e999704817a7645dfce6b0/matplotlib-3.10.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0ef061f74cd488586f552d0c336b2f078d43bc00dc473d2c3e7bfee2272f3fa8", size = 8178873 }, + { url = "https://files.pythonhosted.org/packages/c2/86/e1c86690610661cd716eda5f9d0b35eaf606ae6c9b6736687cfc8f2d0cd8/matplotlib-3.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96985d14dc5f4a736bbea4b9de9afaa735f8a0fc2ca75be2fa9e96b2097369d", size = 8052205 }, + { url = "https://files.pythonhosted.org/packages/54/51/a9f8e49af3883dacddb2da1af5fca1f7468677f1188936452dd9aaaeb9ed/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5f0283da91e9522bdba4d6583ed9d5521566f63729ffb68334f86d0bb98049", size = 8465823 }, + { url = "https://files.pythonhosted.org/packages/e7/e3/c82963a3b86d6e6d5874cbeaa390166458a7f1961bab9feb14d3d1a10f02/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdfa07c0ec58035242bc8b2c8aae37037c9a886370eef6850703d7583e19964b", size = 8606464 }, + { url = "https://files.pythonhosted.org/packages/0e/34/24da1027e7fcdd9e82da3194c470143c551852757a4b473a09a012f5b945/matplotlib-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c0b9849a17bce080a16ebcb80a7b714b5677d0ec32161a2cc0a8e5a6030ae220", size = 9413103 }, + { url = "https://files.pythonhosted.org/packages/a6/da/948a017c3ea13fd4a97afad5fdebe2f5bbc4d28c0654510ce6fd6b06b7bd/matplotlib-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:eef6ed6c03717083bc6d69c2d7ee8624205c29a8e6ea5a31cd3492ecdbaee1e1", size = 8065492 }, + { url = "https://files.pythonhosted.org/packages/eb/43/6b80eb47d1071f234ef0c96ca370c2ca621f91c12045f1401b5c9b28a639/matplotlib-3.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ab1affc11d1f495ab9e6362b8174a25afc19c081ba5b0775ef00533a4236eea", size = 8179689 }, + { url = "https://files.pythonhosted.org/packages/0f/70/d61a591958325c357204870b5e7b164f93f2a8cca1dc6ce940f563909a13/matplotlib-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2a818d8bdcafa7ed2eed74487fdb071c09c1ae24152d403952adad11fa3c65b4", size = 8050466 }, + { url = "https://files.pythonhosted.org/packages/e7/75/70c9d2306203148cc7902a961240c5927dd8728afedf35e6a77e105a2985/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748ebc3470c253e770b17d8b0557f0aa85cf8c63fd52f1a61af5b27ec0b7ffee", size = 8456252 }, + { url = "https://files.pythonhosted.org/packages/c4/91/ba0ae1ff4b3f30972ad01cd4a8029e70a0ec3b8ea5be04764b128b66f763/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed70453fd99733293ace1aec568255bc51c6361cb0da94fa5ebf0649fdb2150a", size = 8601321 }, + { url = "https://files.pythonhosted.org/packages/d2/88/d636041eb54a84b889e11872d91f7cbf036b3b0e194a70fa064eb8b04f7a/matplotlib-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbed9917b44070e55640bd13419de83b4c918e52d97561544814ba463811cbc7", size = 9406972 }, + { url = "https://files.pythonhosted.org/packages/b1/79/0d1c165eac44405a86478082e225fce87874f7198300bbebc55faaf6d28d/matplotlib-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:cf37d8c6ef1a48829443e8ba5227b44236d7fcaf7647caa3178a4ff9f7a5be05", size = 8067954 }, + { url = "https://files.pythonhosted.org/packages/3b/c1/23cfb566a74c696a3b338d8955c549900d18fe2b898b6e94d682ca21e7c2/matplotlib-3.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9f2efccc8dcf2b86fc4ee849eea5dcaecedd0773b30f47980dc0cbeabf26ec84", size = 8180318 }, + { url = "https://files.pythonhosted.org/packages/6c/0c/02f1c3b66b30da9ee343c343acbb6251bef5b01d34fad732446eaadcd108/matplotlib-3.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ddbba06a6c126e3301c3d272a99dcbe7f6c24c14024e80307ff03791a5f294e", size = 8051132 }, + { url = "https://files.pythonhosted.org/packages/b4/ab/8db1a5ac9b3a7352fb914133001dae889f9fcecb3146541be46bed41339c/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748302b33ae9326995b238f606e9ed840bf5886ebafcb233775d946aa8107a15", size = 8457633 }, + { url = "https://files.pythonhosted.org/packages/f5/64/41c4367bcaecbc03ef0d2a3ecee58a7065d0a36ae1aa817fe573a2da66d4/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a80fcccbef63302c0efd78042ea3c2436104c5b1a4d3ae20f864593696364ac7", size = 8601031 }, + { url = "https://files.pythonhosted.org/packages/12/6f/6cc79e9e5ab89d13ed64da28898e40fe5b105a9ab9c98f83abd24e46d7d7/matplotlib-3.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55e46cbfe1f8586adb34f7587c3e4f7dedc59d5226719faf6cb54fc24f2fd52d", size = 9406988 }, + { url = "https://files.pythonhosted.org/packages/b1/0f/eed564407bd4d935ffabf561ed31099ed609e19287409a27b6d336848653/matplotlib-3.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:151d89cb8d33cb23345cd12490c76fd5d18a56581a16d950b48c6ff19bb2ab93", size = 8068034 }, + { url = "https://files.pythonhosted.org/packages/3e/e5/2f14791ff69b12b09e9975e1d116d9578ac684460860ce542c2588cb7a1c/matplotlib-3.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c26dd9834e74d164d06433dc7be5d75a1e9890b926b3e57e74fa446e1a62c3e2", size = 8218223 }, + { url = "https://files.pythonhosted.org/packages/5c/08/30a94afd828b6e02d0a52cae4a29d6e9ccfcf4c8b56cc28b021d3588873e/matplotlib-3.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:24853dad5b8c84c8c2390fc31ce4858b6df504156893292ce8092d190ef8151d", size = 8094985 }, + { url = "https://files.pythonhosted.org/packages/89/44/f3bc6b53066c889d7a1a3ea8094c13af6a667c5ca6220ec60ecceec2dabe/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68f7878214d369d7d4215e2a9075fef743be38fa401d32e6020bab2dfabaa566", size = 8483109 }, + { url = "https://files.pythonhosted.org/packages/ba/c7/473bc559beec08ebee9f86ca77a844b65747e1a6c2691e8c92e40b9f42a8/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6929fc618cb6db9cb75086f73b3219bbb25920cb24cee2ea7a12b04971a4158", size = 8618082 }, + { url = "https://files.pythonhosted.org/packages/d8/e9/6ce8edd264c8819e37bbed8172e0ccdc7107fe86999b76ab5752276357a4/matplotlib-3.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c7818292a5cc372a2dc4c795e5c356942eb8350b98ef913f7fda51fe175ac5d", size = 9413699 }, + { url = "https://files.pythonhosted.org/packages/1b/92/9a45c91089c3cf690b5badd4be81e392ff086ccca8a1d4e3a08463d8a966/matplotlib-3.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4f23ffe95c5667ef8a2b56eea9b53db7f43910fa4a2d5472ae0f72b64deab4d5", size = 8139044 }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 }, +] + +[[package]] +name = "monai" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/61/dc84aca9934c823dbbb46d6f5b4b09f461afa744ea092b66c591e033cff9/monai-1.5.0.tar.gz", hash = "sha256:8c5e4555839812fe060e13da34d9b3342d750d8ad5934f1b79f9b08eb6b35d64", size = 1676883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/96/f8ede335fa3501c57ec67163ef4d113427effc5ccfc8907c0bb58abe5e2d/monai-1.5.0-py3-none-any.whl", hash = "sha256:93259cfa8b68fbf006dea7c78376a46ce38f369bf8b20b36f74ab1d3f484d37b", size = 2659521 }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 }, +] + +[[package]] +name = "multidict" +version = "6.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/2f/a3470242707058fe856fe59241eee5635d79087100b7042a867368863a27/multidict-6.4.4.tar.gz", hash = "sha256:69ee9e6ba214b5245031b76233dd95408a0fd57fdb019ddcc1ead4790932a8e8", size = 90183 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/1b/4c6e638195851524a63972c5773c7737bea7e47b1ba402186a37773acee2/multidict-6.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4f5f29794ac0e73d2a06ac03fd18870adc0135a9d384f4a306a951188ed02f95", size = 65515 }, + { url = "https://files.pythonhosted.org/packages/25/d5/10e6bca9a44b8af3c7f920743e5fc0c2bcf8c11bf7a295d4cfe00b08fb46/multidict-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c04157266344158ebd57b7120d9b0b35812285d26d0e78193e17ef57bfe2979a", size = 38609 }, + { url = "https://files.pythonhosted.org/packages/26/b4/91fead447ccff56247edc7f0535fbf140733ae25187a33621771ee598a18/multidict-6.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bb61ffd3ab8310d93427e460f565322c44ef12769f51f77277b4abad7b6f7223", size = 37871 }, + { url = "https://files.pythonhosted.org/packages/3b/37/cbc977cae59277e99d15bbda84cc53b5e0c4929ffd91d958347200a42ad0/multidict-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e0ba18a9afd495f17c351d08ebbc4284e9c9f7971d715f196b79636a4d0de44", size = 226661 }, + { url = "https://files.pythonhosted.org/packages/15/cd/7e0b57fbd4dc2fc105169c4ecce5be1a63970f23bb4ec8c721b67e11953d/multidict-6.4.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9faf1b1dcaadf9f900d23a0e6d6c8eadd6a95795a0e57fcca73acce0eb912065", size = 223422 }, + { url = "https://files.pythonhosted.org/packages/f1/01/1de268da121bac9f93242e30cd3286f6a819e5f0b8896511162d6ed4bf8d/multidict-6.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a4d1cb1327c6082c4fce4e2a438483390964c02213bc6b8d782cf782c9b1471f", size = 235447 }, + { url = "https://files.pythonhosted.org/packages/d2/8c/8b9a5e4aaaf4f2de14e86181a3a3d7b105077f668b6a06f043ec794f684c/multidict-6.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:941f1bec2f5dbd51feeb40aea654c2747f811ab01bdd3422a48a4e4576b7d76a", size = 231455 }, + { url = "https://files.pythonhosted.org/packages/35/db/e1817dcbaa10b319c412769cf999b1016890849245d38905b73e9c286862/multidict-6.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5f8a146184da7ea12910a4cec51ef85e44f6268467fb489c3caf0cd512f29c2", size = 223666 }, + { url = "https://files.pythonhosted.org/packages/4a/e1/66e8579290ade8a00e0126b3d9a93029033ffd84f0e697d457ed1814d0fc/multidict-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:232b7237e57ec3c09be97206bfb83a0aa1c5d7d377faa019c68a210fa35831f1", size = 217392 }, + { url = "https://files.pythonhosted.org/packages/7b/6f/f8639326069c24a48c7747c2a5485d37847e142a3f741ff3340c88060a9a/multidict-6.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:55ae0721c1513e5e3210bca4fc98456b980b0c2c016679d3d723119b6b202c42", size = 228969 }, + { url = "https://files.pythonhosted.org/packages/d2/c3/3d58182f76b960eeade51c89fcdce450f93379340457a328e132e2f8f9ed/multidict-6.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:51d662c072579f63137919d7bb8fc250655ce79f00c82ecf11cab678f335062e", size = 217433 }, + { url = "https://files.pythonhosted.org/packages/e1/4b/f31a562906f3bd375f3d0e83ce314e4a660c01b16c2923e8229b53fba5d7/multidict-6.4.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0e05c39962baa0bb19a6b210e9b1422c35c093b651d64246b6c2e1a7e242d9fd", size = 225418 }, + { url = "https://files.pythonhosted.org/packages/99/89/78bb95c89c496d64b5798434a3deee21996114d4d2c28dd65850bf3a691e/multidict-6.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5b1cc3ab8c31d9ebf0faa6e3540fb91257590da330ffe6d2393d4208e638925", size = 235042 }, + { url = "https://files.pythonhosted.org/packages/74/91/8780a6e5885a8770442a8f80db86a0887c4becca0e5a2282ba2cae702bc4/multidict-6.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:93ec84488a384cd7b8a29c2c7f467137d8a73f6fe38bb810ecf29d1ade011a7c", size = 230280 }, + { url = "https://files.pythonhosted.org/packages/68/c1/fcf69cabd542eb6f4b892469e033567ee6991d361d77abdc55e3a0f48349/multidict-6.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b308402608493638763abc95f9dc0030bbd6ac6aff784512e8ac3da73a88af08", size = 223322 }, + { url = "https://files.pythonhosted.org/packages/b8/85/5b80bf4b83d8141bd763e1d99142a9cdfd0db83f0739b4797172a4508014/multidict-6.4.4-cp311-cp311-win32.whl", hash = "sha256:343892a27d1a04d6ae455ecece12904d242d299ada01633d94c4f431d68a8c49", size = 35070 }, + { url = "https://files.pythonhosted.org/packages/09/66/0bed198ffd590ab86e001f7fa46b740d58cf8ff98c2f254e4a36bf8861ad/multidict-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:73484a94f55359780c0f458bbd3c39cb9cf9c182552177d2136e828269dee529", size = 38667 }, + { url = "https://files.pythonhosted.org/packages/d2/b5/5675377da23d60875fe7dae6be841787755878e315e2f517235f22f59e18/multidict-6.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc388f75a1c00000824bf28b7633e40854f4127ede80512b44c3cfeeea1839a2", size = 64293 }, + { url = "https://files.pythonhosted.org/packages/34/a7/be384a482754bb8c95d2bbe91717bf7ccce6dc38c18569997a11f95aa554/multidict-6.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:98af87593a666f739d9dba5d0ae86e01b0e1a9cfcd2e30d2d361fbbbd1a9162d", size = 38096 }, + { url = "https://files.pythonhosted.org/packages/66/6d/d59854bb4352306145bdfd1704d210731c1bb2c890bfee31fb7bbc1c4c7f/multidict-6.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aff4cafea2d120327d55eadd6b7f1136a8e5a0ecf6fb3b6863e8aca32cd8e50a", size = 37214 }, + { url = "https://files.pythonhosted.org/packages/99/e0/c29d9d462d7cfc5fc8f9bf24f9c6843b40e953c0b55e04eba2ad2cf54fba/multidict-6.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:169c4ba7858176b797fe551d6e99040c531c775d2d57b31bcf4de6d7a669847f", size = 224686 }, + { url = "https://files.pythonhosted.org/packages/dc/4a/da99398d7fd8210d9de068f9a1b5f96dfaf67d51e3f2521f17cba4ee1012/multidict-6.4.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9eb4c59c54421a32b3273d4239865cb14ead53a606db066d7130ac80cc8ec93", size = 231061 }, + { url = "https://files.pythonhosted.org/packages/21/f5/ac11add39a0f447ac89353e6ca46666847051103649831c08a2800a14455/multidict-6.4.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cf3bd54c56aa16fdb40028d545eaa8d051402b61533c21e84046e05513d5780", size = 232412 }, + { url = "https://files.pythonhosted.org/packages/d9/11/4b551e2110cded705a3c13a1d4b6a11f73891eb5a1c449f1b2b6259e58a6/multidict-6.4.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f682c42003c7264134bfe886376299db4cc0c6cd06a3295b41b347044bcb5482", size = 231563 }, + { url = "https://files.pythonhosted.org/packages/4c/02/751530c19e78fe73b24c3da66618eda0aa0d7f6e7aa512e46483de6be210/multidict-6.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920f9cf2abdf6e493c519492d892c362007f113c94da4c239ae88429835bad1", size = 223811 }, + { url = "https://files.pythonhosted.org/packages/c7/cb/2be8a214643056289e51ca356026c7b2ce7225373e7a1f8c8715efee8988/multidict-6.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:530d86827a2df6504526106b4c104ba19044594f8722d3e87714e847c74a0275", size = 216524 }, + { url = "https://files.pythonhosted.org/packages/19/f3/6d5011ec375c09081f5250af58de85f172bfcaafebff286d8089243c4bd4/multidict-6.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ecde56ea2439b96ed8a8d826b50c57364612ddac0438c39e473fafad7ae1c23b", size = 229012 }, + { url = "https://files.pythonhosted.org/packages/67/9c/ca510785df5cf0eaf5b2a8132d7d04c1ce058dcf2c16233e596ce37a7f8e/multidict-6.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:dc8c9736d8574b560634775ac0def6bdc1661fc63fa27ffdfc7264c565bcb4f2", size = 226765 }, + { url = "https://files.pythonhosted.org/packages/36/c8/ca86019994e92a0f11e642bda31265854e6ea7b235642f0477e8c2e25c1f/multidict-6.4.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f3d3b3c34867579ea47cbd6c1f2ce23fbfd20a273b6f9e3177e256584f1eacc", size = 222888 }, + { url = "https://files.pythonhosted.org/packages/c6/67/bc25a8e8bd522935379066950ec4e2277f9b236162a73548a2576d4b9587/multidict-6.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:87a728af265e08f96b6318ebe3c0f68b9335131f461efab2fc64cc84a44aa6ed", size = 234041 }, + { url = "https://files.pythonhosted.org/packages/f1/a0/70c4c2d12857fccbe607b334b7ee28b6b5326c322ca8f73ee54e70d76484/multidict-6.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9f193eeda1857f8e8d3079a4abd258f42ef4a4bc87388452ed1e1c4d2b0c8740", size = 231046 }, + { url = "https://files.pythonhosted.org/packages/c1/0f/52954601d02d39742aab01d6b92f53c1dd38b2392248154c50797b4df7f1/multidict-6.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be06e73c06415199200e9a2324a11252a3d62030319919cde5e6950ffeccf72e", size = 227106 }, + { url = "https://files.pythonhosted.org/packages/af/24/679d83ec4379402d28721790dce818e5d6b9f94ce1323a556fb17fa9996c/multidict-6.4.4-cp312-cp312-win32.whl", hash = "sha256:622f26ea6a7e19b7c48dd9228071f571b2fbbd57a8cd71c061e848f281550e6b", size = 35351 }, + { url = "https://files.pythonhosted.org/packages/52/ef/40d98bc5f986f61565f9b345f102409534e29da86a6454eb6b7c00225a13/multidict-6.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:5e2bcda30d5009996ff439e02a9f2b5c3d64a20151d34898c000a6281faa3781", size = 38791 }, + { url = "https://files.pythonhosted.org/packages/df/2a/e166d2ffbf4b10131b2d5b0e458f7cee7d986661caceae0de8753042d4b2/multidict-6.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:82ffabefc8d84c2742ad19c37f02cde5ec2a1ee172d19944d380f920a340e4b9", size = 64123 }, + { url = "https://files.pythonhosted.org/packages/8c/96/e200e379ae5b6f95cbae472e0199ea98913f03d8c9a709f42612a432932c/multidict-6.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6a2f58a66fe2c22615ad26156354005391e26a2f3721c3621504cd87c1ea87bf", size = 38049 }, + { url = "https://files.pythonhosted.org/packages/75/fb/47afd17b83f6a8c7fa863c6d23ac5ba6a0e6145ed8a6bcc8da20b2b2c1d2/multidict-6.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5883d6ee0fd9d8a48e9174df47540b7545909841ac82354c7ae4cbe9952603bd", size = 37078 }, + { url = "https://files.pythonhosted.org/packages/fa/70/1af3143000eddfb19fd5ca5e78393985ed988ac493bb859800fe0914041f/multidict-6.4.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9abcf56a9511653fa1d052bfc55fbe53dbee8f34e68bd6a5a038731b0ca42d15", size = 224097 }, + { url = "https://files.pythonhosted.org/packages/b1/39/d570c62b53d4fba844e0378ffbcd02ac25ca423d3235047013ba2f6f60f8/multidict-6.4.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6ed5ae5605d4ad5a049fad2a28bb7193400700ce2f4ae484ab702d1e3749c3f9", size = 230768 }, + { url = "https://files.pythonhosted.org/packages/fd/f8/ed88f2c4d06f752b015933055eb291d9bc184936903752c66f68fb3c95a7/multidict-6.4.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbfcb60396f9bcfa63e017a180c3105b8c123a63e9d1428a36544e7d37ca9e20", size = 231331 }, + { url = "https://files.pythonhosted.org/packages/9c/6f/8e07cffa32f483ab887b0d56bbd8747ac2c1acd00dc0af6fcf265f4a121e/multidict-6.4.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0f1987787f5f1e2076b59692352ab29a955b09ccc433c1f6b8e8e18666f608b", size = 230169 }, + { url = "https://files.pythonhosted.org/packages/e6/2b/5dcf173be15e42f330110875a2668ddfc208afc4229097312212dc9c1236/multidict-6.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0121ccce8c812047d8d43d691a1ad7641f72c4f730474878a5aeae1b8ead8c", size = 222947 }, + { url = "https://files.pythonhosted.org/packages/39/75/4ddcbcebe5ebcd6faa770b629260d15840a5fc07ce8ad295a32e14993726/multidict-6.4.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83ec4967114295b8afd120a8eec579920c882831a3e4c3331d591a8e5bfbbc0f", size = 215761 }, + { url = "https://files.pythonhosted.org/packages/6a/c9/55e998ae45ff15c5608e384206aa71a11e1b7f48b64d166db400b14a3433/multidict-6.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:995f985e2e268deaf17867801b859a282e0448633f1310e3704b30616d269d69", size = 227605 }, + { url = "https://files.pythonhosted.org/packages/04/49/c2404eac74497503c77071bd2e6f88c7e94092b8a07601536b8dbe99be50/multidict-6.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d832c608f94b9f92a0ec8b7e949be7792a642b6e535fcf32f3e28fab69eeb046", size = 226144 }, + { url = "https://files.pythonhosted.org/packages/62/c5/0cd0c3c6f18864c40846aa2252cd69d308699cb163e1c0d989ca301684da/multidict-6.4.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d21c1212171cf7da703c5b0b7a0e85be23b720818aef502ad187d627316d5645", size = 221100 }, + { url = "https://files.pythonhosted.org/packages/71/7b/f2f3887bea71739a046d601ef10e689528d4f911d84da873b6be9194ffea/multidict-6.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cbebaa076aaecad3d4bb4c008ecc73b09274c952cf6a1b78ccfd689e51f5a5b0", size = 232731 }, + { url = "https://files.pythonhosted.org/packages/e5/b3/d9de808349df97fa75ec1372758701b5800ebad3c46ae377ad63058fbcc6/multidict-6.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c93a6fb06cc8e5d3628b2b5fda215a5db01e8f08fc15fadd65662d9b857acbe4", size = 229637 }, + { url = "https://files.pythonhosted.org/packages/5e/57/13207c16b615eb4f1745b44806a96026ef8e1b694008a58226c2d8f5f0a5/multidict-6.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8cd8f81f1310182362fb0c7898145ea9c9b08a71081c5963b40ee3e3cac589b1", size = 225594 }, + { url = "https://files.pythonhosted.org/packages/3a/e4/d23bec2f70221604f5565000632c305fc8f25ba953e8ce2d8a18842b9841/multidict-6.4.4-cp313-cp313-win32.whl", hash = "sha256:3e9f1cd61a0ab857154205fb0b1f3d3ace88d27ebd1409ab7af5096e409614cd", size = 35359 }, + { url = "https://files.pythonhosted.org/packages/a7/7a/cfe1a47632be861b627f46f642c1d031704cc1c0f5c0efbde2ad44aa34bd/multidict-6.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:8ffb40b74400e4455785c2fa37eba434269149ec525fc8329858c862e4b35373", size = 38903 }, + { url = "https://files.pythonhosted.org/packages/68/7b/15c259b0ab49938a0a1c8f3188572802704a779ddb294edc1b2a72252e7c/multidict-6.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6a602151dbf177be2450ef38966f4be3467d41a86c6a845070d12e17c858a156", size = 68895 }, + { url = "https://files.pythonhosted.org/packages/f1/7d/168b5b822bccd88142e0a3ce985858fea612404edd228698f5af691020c9/multidict-6.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d2b9712211b860d123815a80b859075d86a4d54787e247d7fbee9db6832cf1c", size = 40183 }, + { url = "https://files.pythonhosted.org/packages/e0/b7/d4b8d98eb850ef28a4922ba508c31d90715fd9b9da3801a30cea2967130b/multidict-6.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d2fa86af59f8fc1972e121ade052145f6da22758f6996a197d69bb52f8204e7e", size = 39592 }, + { url = "https://files.pythonhosted.org/packages/18/28/a554678898a19583548e742080cf55d169733baf57efc48c2f0273a08583/multidict-6.4.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50855d03e9e4d66eab6947ba688ffb714616f985838077bc4b490e769e48da51", size = 226071 }, + { url = "https://files.pythonhosted.org/packages/ee/dc/7ba6c789d05c310e294f85329efac1bf5b450338d2542498db1491a264df/multidict-6.4.4-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5bce06b83be23225be1905dcdb6b789064fae92499fbc458f59a8c0e68718601", size = 222597 }, + { url = "https://files.pythonhosted.org/packages/24/4f/34eadbbf401b03768dba439be0fb94b0d187facae9142821a3d5599ccb3b/multidict-6.4.4-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66ed0731f8e5dfd8369a883b6e564aca085fb9289aacabd9decd70568b9a30de", size = 228253 }, + { url = "https://files.pythonhosted.org/packages/c0/e6/493225a3cdb0d8d80d43a94503fc313536a07dae54a3f030d279e629a2bc/multidict-6.4.4-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:329ae97fc2f56f44d91bc47fe0972b1f52d21c4b7a2ac97040da02577e2daca2", size = 226146 }, + { url = "https://files.pythonhosted.org/packages/2f/70/e411a7254dc3bff6f7e6e004303b1b0591358e9f0b7c08639941e0de8bd6/multidict-6.4.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c27e5dcf520923d6474d98b96749e6805f7677e93aaaf62656005b8643f907ab", size = 220585 }, + { url = "https://files.pythonhosted.org/packages/08/8f/beb3ae7406a619100d2b1fb0022c3bb55a8225ab53c5663648ba50dfcd56/multidict-6.4.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:058cc59b9e9b143cc56715e59e22941a5d868c322242278d28123a5d09cdf6b0", size = 212080 }, + { url = "https://files.pythonhosted.org/packages/9c/ec/355124e9d3d01cf8edb072fd14947220f357e1c5bc79c88dff89297e9342/multidict-6.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:69133376bc9a03f8c47343d33f91f74a99c339e8b58cea90433d8e24bb298031", size = 226558 }, + { url = "https://files.pythonhosted.org/packages/fd/22/d2b95cbebbc2ada3be3812ea9287dcc9712d7f1a012fad041770afddb2ad/multidict-6.4.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d6b15c55721b1b115c5ba178c77104123745b1417527ad9641a4c5e2047450f0", size = 212168 }, + { url = "https://files.pythonhosted.org/packages/4d/c5/62bfc0b2f9ce88326dbe7179f9824a939c6c7775b23b95de777267b9725c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a887b77f51d3d41e6e1a63cf3bc7ddf24de5939d9ff69441387dfefa58ac2e26", size = 217970 }, + { url = "https://files.pythonhosted.org/packages/79/74/977cea1aadc43ff1c75d23bd5bc4768a8fac98c14e5878d6ee8d6bab743c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:632a3bf8f1787f7ef7d3c2f68a7bde5be2f702906f8b5842ad6da9d974d0aab3", size = 226980 }, + { url = "https://files.pythonhosted.org/packages/48/fc/cc4a1a2049df2eb84006607dc428ff237af38e0fcecfdb8a29ca47b1566c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a145c550900deb7540973c5cdb183b0d24bed6b80bf7bddf33ed8f569082535e", size = 220641 }, + { url = "https://files.pythonhosted.org/packages/3b/6a/a7444d113ab918701988d4abdde373dbdfd2def7bd647207e2bf645c7eac/multidict-6.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc5d83c6619ca5c9672cb78b39ed8542f1975a803dee2cda114ff73cbb076edd", size = 221728 }, + { url = "https://files.pythonhosted.org/packages/2b/b0/fdf4c73ad1c55e0f4dbbf2aa59dd37037334091f9a4961646d2b7ac91a86/multidict-6.4.4-cp313-cp313t-win32.whl", hash = "sha256:3312f63261b9df49be9d57aaa6abf53a6ad96d93b24f9cc16cf979956355ce6e", size = 41913 }, + { url = "https://files.pythonhosted.org/packages/8e/92/27989ecca97e542c0d01d05a98a5ae12198a243a9ee12563a0313291511f/multidict-6.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:ba852168d814b2c73333073e1c7116d9395bea69575a01b0b3c89d2d5a87c8fb", size = 46112 }, + { url = "https://files.pythonhosted.org/packages/84/5d/e17845bb0fa76334477d5de38654d27946d5b5d3695443987a094a71b440/multidict-6.4.4-py3-none-any.whl", hash = "sha256:bd4557071b561a8b3b6075c3ce93cf9bfb6182cb241805c3d66ced3b75eff4ac", size = 10481 }, +] + +[[package]] +name = "musk" +version = "1.0.0" +source = { git = "https://github.com/lilab-stanford/MUSK.git?rev=e1699c27687f44bbf6d4adfcbb2abe89795d347f#e1699c27687f44bbf6d4adfcbb2abe89795d347f" } + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195 }, +] + +[[package]] +name = "networkx" +version = "3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406 }, +] + +[[package]] +name = "ninja" +version = "1.11.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/2c/d717d13a413d6f7579cdaa1e28e6e2c98de95461549b08d311c8a5bf4c51/ninja-1.11.1.1.tar.gz", hash = "sha256:9d793b08dd857e38d0b6ffe9e6b7145d7c485a42dcfea04905ca0cdb6017cc3c", size = 132392 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/6e/04ed11bb244039908f6f212cb5f3e97933e238655248e4ce307c1687ba1f/ninja-1.11.1.1-py2.py3-none-macosx_10_9_universal2.macosx_10_9_x86_64.macosx_11_0_arm64.macosx_11_0_universal2.whl", hash = "sha256:376889c76d87b95b5719fdd61dd7db193aa7fd4432e5d52d2e44e4c497bdbbee", size = 270611 }, + { url = "https://files.pythonhosted.org/packages/2c/52/0e5423311eb9939b6f9354059a6d88a6211eb4fa1c7a4ef303ecee1c1fe0/ninja-1.11.1.1-py2.py3-none-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:ecf80cf5afd09f14dcceff28cb3f11dc90fb97c999c89307aea435889cb66877", size = 324256 }, + { url = "https://files.pythonhosted.org/packages/6d/92/8d7aebd4430ab5ff65df2bfee6d5745f95c004284db2d8ca76dcbfd9de47/ninja-1.11.1.1-py2.py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:84502ec98f02a037a169c4b0d5d86075eaf6afc55e1879003d6cab51ced2ea4b", size = 307194 }, + { url = "https://files.pythonhosted.org/packages/01/c8/96424839fd127b4492229acf50763ed9940d864ca35d17d151934aef1f6f/ninja-1.11.1.1-py2.py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:73b93c14046447c7c5cc892433d4fae65d6364bec6685411cb97a8bcf815f93a", size = 155643 }, + { url = "https://files.pythonhosted.org/packages/6b/fa/5ca8e65a98cdb9a71d4f1e38cac7bd757bbb9555a5aef5a4d293aa890e5c/ninja-1.11.1.1-py2.py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:18302d96a5467ea98b68e1cae1ae4b4fb2b2a56a82b955193c637557c7273dbd", size = 179538 }, + { url = "https://files.pythonhosted.org/packages/45/ef/60086f02cbc6882da00a02c81d645cefd8d2d65b01fade41b873d8dd85a2/ninja-1.11.1.1-py2.py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aad34a70ef15b12519946c5633344bc775a7656d789d9ed5fdb0d456383716ef", size = 156217 }, + { url = "https://files.pythonhosted.org/packages/1c/00/2fd13ac6aafdb566f00d6b541101fca54e58ae58bf96c00f9780df019607/ninja-1.11.1.1-py2.py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:d491fc8d89cdcb416107c349ad1e3a735d4c4af5e1cb8f5f727baca6350fdaea", size = 372069 }, + { url = "https://files.pythonhosted.org/packages/ad/5d/6e97c8a25167d4867694c7fb0b9bdbc9b096d6479c8e56c5bd41b49613f6/ninja-1.11.1.1-py2.py3-none-musllinux_1_1_i686.whl", hash = "sha256:7563ce1d9fe6ed5af0b8dd9ab4a214bf4ff1f2f6fd6dc29f480981f0f8b8b249", size = 418859 }, + { url = "https://files.pythonhosted.org/packages/43/78/34af88d753389a9412438d16142c77e587e0d69152faf0bbf99701063dd8/ninja-1.11.1.1-py2.py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:9df724344202b83018abb45cb1efc22efd337a1496514e7e6b3b59655be85205", size = 419782 }, + { url = "https://files.pythonhosted.org/packages/3b/74/de0633f8bced3b188942fca64a950e8f2206c60c10c97af465b356ae9b25/ninja-1.11.1.1-py2.py3-none-musllinux_1_1_s390x.whl", hash = "sha256:3e0f9be5bb20d74d58c66cc1c414c3e6aeb45c35b0d0e41e8d739c2c0d57784f", size = 415476 }, + { url = "https://files.pythonhosted.org/packages/9a/f3/3e4a56ff77739d1582749b93497bdebf11e003fbc7a66363ef6c772ebd0a/ninja-1.11.1.1-py2.py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:76482ba746a2618eecf89d5253c0d1e4f1da1270d41e9f54dfbd91831b0f6885", size = 379229 }, + { url = "https://files.pythonhosted.org/packages/c5/ee/53df34fcc9c0b1db62b2f2e2c848e28d9354e1c7f0dce029ee50b16ca157/ninja-1.11.1.1-py2.py3-none-win32.whl", hash = "sha256:fa2ba9d74acfdfbfbcf06fad1b8282de8a7a8c481d9dee45c859a8c93fcc1082", size = 265049 }, + { url = "https://files.pythonhosted.org/packages/b6/2f/a3bc50fa63fc4fe9348e15b53dc8c87febfd4e0c660fcf250c4b19a3aa3b/ninja-1.11.1.1-py2.py3-none-win_amd64.whl", hash = "sha256:95da904130bfa02ea74ff9c0116b4ad266174fafb1c707aa50212bc7859aebf1", size = 312958 }, + { url = "https://files.pythonhosted.org/packages/73/2a/f5b7b3b7ecd5cf4e31375580bf5c6a01a328ed1ebdfff90fab463e3f4bc7/ninja-1.11.1.1-py2.py3-none-win_arm64.whl", hash = "sha256:185e0641bde601e53841525c4196278e9aaf4463758da6dd1e752c0a0f54136a", size = 272686 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "numexpr" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/8f/2cc977e91adbfbcdb6b49fdb9147e1d1c7566eb2c0c1e737e9a47020b5ca/numexpr-2.11.0.tar.gz", hash = "sha256:75b2c01a4eda2e7c357bc67a3f5c3dd76506c15b5fd4dc42845ef2e182181bad", size = 108960 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/d1/1cf8137990b3f3d445556ed63b9bc347aec39bde8c41146b02d3b35c1adc/numexpr-2.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:450eba3c93c3e3e8070566ad8d70590949d6e574b1c960bf68edd789811e7da8", size = 147535 }, + { url = "https://files.pythonhosted.org/packages/b6/5e/bac7649d043f47c7c14c797efe60dbd19476468a149399cd706fe2e47f8c/numexpr-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f0eb88dbac8a7e61ee433006d0ddfd6eb921f5c6c224d1b50855bc98fb304c44", size = 136710 }, + { url = "https://files.pythonhosted.org/packages/1b/9f/c88fc34d82d23c66ea0b78b00a1fb3b64048e0f7ac7791b2cd0d2a4ce14d/numexpr-2.11.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a194e3684b3553ea199c3f4837f422a521c7e2f0cce13527adc3a6b4049f9e7c", size = 411169 }, + { url = "https://files.pythonhosted.org/packages/e4/8d/4d78dad430b41d836146f9e6f545f5c4f7d1972a6aa427d8570ab232bf16/numexpr-2.11.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f677668ab2bb2452fee955af3702fbb3b71919e61e4520762b1e5f54af59c0d8", size = 401671 }, + { url = "https://files.pythonhosted.org/packages/83/1c/414670eb41a82b78bd09769a4f5fb49a934f9b3990957f02c833637a511e/numexpr-2.11.0-cp311-cp311-win32.whl", hash = "sha256:7d9e76a77c9644fbd60da3984e516ead5b84817748c2da92515cd36f1941a04d", size = 153159 }, + { url = "https://files.pythonhosted.org/packages/0c/97/8d00ca9b36f3ac68a8fd85e930ab0c9448d8c9ca7ce195ee75c188dabd45/numexpr-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7163b488bfdcd13c300a8407c309e4cee195ef95d07facf5ac2678d66c988805", size = 146224 }, + { url = "https://files.pythonhosted.org/packages/38/45/7a0e5a0b800d92e73825494ac695fa05a52c7fc7088d69a336880136b437/numexpr-2.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4229060be866813122385c608bbd3ea48fe0b33e91f2756810d28c1cdbfc98f1", size = 147494 }, + { url = "https://files.pythonhosted.org/packages/74/46/3a26b84e44f4739ec98de0ede4b95b4b8096f721e22d0e97517eeb02017e/numexpr-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:097aa8835d32d6ac52f2be543384019b4b134d1fb67998cbfc4271155edfe54a", size = 136832 }, + { url = "https://files.pythonhosted.org/packages/75/05/e3076ff25d4a108b47640c169c0a64811748c43b63d9cc052ea56de1631e/numexpr-2.11.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f082321c244ff5d0e252071fb2c4fe02063a45934144a1456a5370ca139bec2", size = 412618 }, + { url = "https://files.pythonhosted.org/packages/70/e8/15e0e077a004db0edd530da96c60c948689c888c464ee5d14b82405ebd86/numexpr-2.11.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7a19435ca3d7dd502b8d8dce643555eb1b6013989e3f7577857289f6db6be16", size = 403363 }, + { url = "https://files.pythonhosted.org/packages/10/14/f22afb3a7ae41d03ba87f62d00fbcfb76389f9cc91b7a82593c39c509318/numexpr-2.11.0-cp312-cp312-win32.whl", hash = "sha256:f326218262c8d8537887cc4bbd613c8409d62f2cac799835c0360e0d9cefaa5c", size = 153307 }, + { url = "https://files.pythonhosted.org/packages/18/70/abc585269424582b3cd6db261e33b2ec96b5d4971da3edb29fc9b62a8926/numexpr-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:0a184e5930c77ab91dd9beee4df403b825cd9dfc4e9ba4670d31c9fcb4e2c08e", size = 146337 }, + { url = "https://files.pythonhosted.org/packages/74/63/dbf4fb6c48006d413a82db138d03c3c007d0ed0684f693c4b77196448660/numexpr-2.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eb766218abad05c7c3ddad5367d0ec702d6152cb4a48d9fd56a6cef6abade70c", size = 147495 }, + { url = "https://files.pythonhosted.org/packages/3a/e4/2fbbf5b9121f54722dc4d4dfc75bc0b4e8ee2675f92ec86ee5697aecc53f/numexpr-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2036be213a6a1b5ce49acf60de99b911a0f9d174aab7679dde1fae315134f826", size = 136839 }, + { url = "https://files.pythonhosted.org/packages/a8/3f/aa36415919c90f712a11127eaa7c0c8d045768d62a484a29364e4801c383/numexpr-2.11.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:096ec768bee2ef14ac757b4178e3c5f05e5f1cb6cae83b2eea9b4ba3ec1a86dd", size = 416240 }, + { url = "https://files.pythonhosted.org/packages/b9/7d/4911f40d3610fc5557029f0d1f20ef9f571488319567ac4d8ee6d0978ee6/numexpr-2.11.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1719788a787808c15c9bb98b6ff0c97d64a0e59c1a6ebe36d4ae4d7c5c09b95", size = 406641 }, + { url = "https://files.pythonhosted.org/packages/6f/bc/d00e717e77691c410c6c461d7880b4c498896874316acc0e044d7eafacbf/numexpr-2.11.0-cp313-cp313-win32.whl", hash = "sha256:6b5fdfc86cbf5373ea67d554cc6f08863825ea8e928416bed8d5285e387420c6", size = 153313 }, + { url = "https://files.pythonhosted.org/packages/52/a2/93346789e6d73a76fdb68171904ade25c112f25df363a8f602c6b21bc220/numexpr-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:5ff337b36db141a1a0b49f01282783744f49f0d401cc83a512fc5596eb7db5c6", size = 146340 }, + { url = "https://files.pythonhosted.org/packages/0b/20/c0e3aaf3cc4497e5253df2523a55c83b9d316cb5c9d5caaa4a1156cef6e3/numexpr-2.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b9854fa70edbe93242b8bb4840e58d1128c45766d9a70710f05b4f67eb0feb6e", size = 148206 }, + { url = "https://files.pythonhosted.org/packages/de/49/22fd38ac990ba333f25b771305a5ffcd98c771f4d278868661ffb26deac1/numexpr-2.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:321736cb98f090ce864b58cc5c37661cb5548e394e0fe24d5f2c7892a89070c3", size = 137573 }, + { url = "https://files.pythonhosted.org/packages/fb/1e/50074e472e9e6bea4fe430869708d9ede333a187d8d0740e70d5a9560aad/numexpr-2.11.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5cc434eb4a4df2fe442bcc50df114e82ff7aa234657baf873b2c9cf3f851e8e", size = 426674 }, + { url = "https://files.pythonhosted.org/packages/8e/6d/7ccbc72b950653df62d29e2531c811ed80cfff93c927a5bfd86a71edb4da/numexpr-2.11.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:238d19465a272ada3967600fada55e4c6900485aefb42122a78dfcaf2efca65f", size = 416037 }, + { url = "https://files.pythonhosted.org/packages/31/7c/bbccad2734dd4b251cc6bdff8cf5ded18b5383f5a05aa8de7bf02acbb65b/numexpr-2.11.0-cp313-cp313t-win32.whl", hash = "sha256:0db4c2dcad09f9594b45fce794f4b903345195a8c216e252de2aa92884fd81a8", size = 153967 }, + { url = "https://files.pythonhosted.org/packages/75/d7/41287384e413e8d20457d35e264d9c9754e65eb13a988af51ceb7057f61b/numexpr-2.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a69b5c02014448a412012752dc46091902d28932c3be0c6e02e73cecceffb700", size = 147207 }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963 }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616 }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579 }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005 }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570 }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548 }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521 }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866 }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455 }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348 }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362 }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103 }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382 }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462 }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618 }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511 }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783 }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506 }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190 }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828 }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006 }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765 }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736 }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719 }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072 }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213 }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632 }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532 }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885 }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467 }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144 }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217 }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014 }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935 }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122 }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143 }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260 }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225 }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374 }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.4.5.8" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/7f/7fbae15a3982dc9595e49ce0f19332423b260045d0a6afe93cdbe2f1f624/nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0f8aa1706812e00b9f19dfe0cdb3999b092ccb8ca168c0db5b8ea712456fd9b3", size = 363333771 }, + { url = "https://files.pythonhosted.org/packages/ae/71/1c91302526c45ab494c23f61c7a84aa568b8c1f9d196efa5993957faf906/nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_x86_64.whl", hash = "sha256:2fc8da60df463fdefa81e323eef2e36489e1c94335b5358bcb38360adf75ac9b", size = 363438805 }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/b5/9fb3d00386d3361b03874246190dfec7b206fd74e6e287b26a8fcb359d95/nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:79279b35cf6f91da114182a5ce1864997fd52294a87a16179ce275773799458a", size = 12354556 }, + { url = "https://files.pythonhosted.org/packages/67/42/f4f60238e8194a3106d06a058d494b18e006c10bb2b915655bd9f6ea4cb1/nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:9dec60f5ac126f7bb551c055072b69d85392b13311fcc1bcda2202d172df30fb", size = 13813957 }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/aa/083b01c427e963ad0b314040565ea396f914349914c298556484f799e61b/nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0eedf14185e04b76aa05b1fea04133e59f465b6f960c0cbf4e37c3cb6b0ea198", size = 24133372 }, + { url = "https://files.pythonhosted.org/packages/2c/14/91ae57cd4db3f9ef7aa99f4019cfa8d54cb4caa7e00975df6467e9725a9f/nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a178759ebb095827bd30ef56598ec182b85547f1508941a3d560eb7ea1fbf338", size = 24640306 }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/aa/b656d755f474e2084971e9a297def515938d56b466ab39624012070cb773/nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:961fe0e2e716a2a1d967aab7caee97512f71767f852f67432d572e36cb3a11f3", size = 894177 }, + { url = "https://files.pythonhosted.org/packages/ea/27/1795d86fe88ef397885f2e580ac37628ed058a92ed2c39dc8eac3adf0619/nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:64403288fa2136ee8e467cdc9c9427e0434110899d07c779f25b5c068934faa5", size = 883737 }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.1.0.70" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741 }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.2.1.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/8a/0e728f749baca3fbeffad762738276e5df60851958be7783af121a7221e7/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:5dad8008fc7f92f5ddfa2101430917ce2ffacd86824914c82e28990ad7f00399", size = 211422548 }, + { url = "https://files.pythonhosted.org/packages/27/94/3266821f65b92b3138631e9c8e7fe1fb513804ac934485a8d05776e1dd43/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f083fc24912aa410be21fa16d157fed2055dab1cc4b6934a0e03cba69eb242b9", size = 211459117 }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.5.147" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/9c/a79180e4d70995fdf030c6946991d0171555c6edf95c265c6b2bf7011112/nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1f173f09e3e3c76ab084aba0de819c49e56614feae5c12f69883f4ae9bb5fad9", size = 56314811 }, + { url = "https://files.pythonhosted.org/packages/8a/6d/44ad094874c6f1b9c654f8ed939590bdc408349f137f9b98a3a23ccec411/nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a88f583d4e0bb643c49743469964103aa59f7f708d862c3ddb0fc07f851e3b8b", size = 56305206 }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.6.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/6b/a5c33cf16af09166845345275c34ad2190944bcc6026797a39f8e0a282e0/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_aarch64.whl", hash = "sha256:d338f155f174f90724bbde3758b7ac375a70ce8e706d70b018dd3375545fc84e", size = 127634111 }, + { url = "https://files.pythonhosted.org/packages/3a/e1/5b9089a4b2a4790dfdea8b3a006052cfecff58139d5a4e34cb1a51df8d6f/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:19e33fa442bcfd085b3086c4ebf7e8debc07cfe01e11513cc6d332fd918ac260", size = 127936057 }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.3.1.170" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/a9/c0d2f83a53d40a4a41be14cea6a0bf9e668ffcf8b004bd65633f433050c0/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_aarch64.whl", hash = "sha256:9d32f62896231ebe0480efd8a7f702e143c98cfaa0e8a76df3386c1ba2b54df3", size = 207381987 }, + { url = "https://files.pythonhosted.org/packages/db/f7/97a9ea26ed4bbbfc2d470994b8b4f338ef663be97b8f677519ac195e113d/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ea4f11a2904e2a8dc4b1833cc1b5181cde564edd0d5cd33e3c168eff2d1863f1", size = 207454763 }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/8e/675498726c605c9441cf46653bd29cb1b8666da1fb1469ffa25f67f20c58/nvidia_cusparselt_cu12-0.6.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:067a7f6d03ea0d4841c85f0c6f1991c5dda98211f6302cb83a4ab234ee95bef8", size = 149422781 }, + { url = "https://files.pythonhosted.org/packages/78/a8/bcbb63b53a4b1234feeafb65544ee55495e1bb37ec31b999b963cbccfd1d/nvidia_cusparselt_cu12-0.6.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:df2c24502fd76ebafe7457dbc4716b2fec071aabaed4fb7691a201cde03704d9", size = 150057751 }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.21.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/99/12cd266d6233f47d00daf3a72739872bdc10267d0383508b0b9c84a18bb6/nvidia_nccl_cu12-2.21.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:8579076d30a8c24988834445f8d633c697d42397e92ffc3f63fa26766d25e0a0", size = 188654414 }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/45/239d52c05074898a80a900f49b1615d81c07fceadd5ad6c4f86a987c0bc4/nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4abe7fef64914ccfa909bc2ba39739670ecc9e820c83ccc7a6ed414122599b83", size = 20552510 }, + { url = "https://files.pythonhosted.org/packages/ff/ff/847841bacfbefc97a00036e0fce5a0f086b640756dc38caea5e1bb002655/nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:06b3b9b25bf3f8af351d664978ca26a16d2c5127dbd53c0497e28d1fb9611d57", size = 21066810 }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/39/471f581edbb7804b39e8063d92fc8305bdc7a80ae5c07dbe6ea5c50d14a5/nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7959ad635db13edf4fc65c06a6e9f9e55fc2f92596db928d169c0bb031e88ef3", size = 100417 }, + { url = "https://files.pythonhosted.org/packages/87/20/199b8713428322a2f22b722c62b8cc278cc53dffa9705d744484b5035ee9/nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:781e950d9b9f60d8241ccea575b32f5105a5baf4c2351cab5256a24869f12a1a", size = 99144 }, +] + +[[package]] +name = "opencv-python" +version = "4.11.0.86" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/06/68c27a523103dad5837dc5b87e71285280c4f098c60e4fe8a8db6486ab09/opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4", size = 95171956 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/4d/53b30a2a3ac1f75f65a59eb29cf2ee7207ce64867db47036ad61743d5a23/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a", size = 37326322 }, + { url = "https://files.pythonhosted.org/packages/3b/84/0a67490741867eacdfa37bc18df96e08a9d579583b419010d7f3da8ff503/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66", size = 56723197 }, + { url = "https://files.pythonhosted.org/packages/f3/bd/29c126788da65c1fb2b5fb621b7fed0ed5f9122aa22a0868c5e2c15c6d23/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202", size = 42230439 }, + { url = "https://files.pythonhosted.org/packages/2c/8b/90eb44a40476fa0e71e05a0283947cfd74a5d36121a11d926ad6f3193cc4/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d", size = 62986597 }, + { url = "https://files.pythonhosted.org/packages/fb/d7/1d5941a9dde095468b288d989ff6539dd69cd429dbf1b9e839013d21b6f0/opencv_python-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b", size = 29384337 }, + { url = "https://files.pythonhosted.org/packages/a4/7d/f1c30a92854540bf789e9cd5dde7ef49bbe63f855b85a2e6b3db8135c591/opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec", size = 39488044 }, +] + +[[package]] +name = "openpyxl" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "et-xmlfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910 }, +] + +[[package]] +name = "openslide-bin" +version = "4.0.0.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/49/0a62d150b620bf045ddf1e70ae90e42e423aefdc07b4927d846faefcca19/openslide-bin-4.0.0.8.tar.gz", hash = "sha256:bae3b5e374ada9d6b5110d19f60a71e1d164578be98c46c0761c028dec8949e4", size = 17872279 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/4b/d527b317dddd9fe17addf16a49eed0907943590228ccf2d2f535e541e928/openslide_bin-4.0.0.8-py3-none-macosx_11_0_universal2.whl", hash = "sha256:9389cf7bb1b4ecd9e79e6d00151ae6a43a1e8b07ce7119609324d9260345f3c7", size = 5372459 }, + { url = "https://files.pythonhosted.org/packages/8c/be/f4028886d0e2bc4f4114bfa929427ec29a6be86831327f131d423fde041c/openslide_bin-4.0.0.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:79c94c39a48d497caa39ecdad5ef64a931b31c6592354a142497bc3db2e88494", size = 4221147 }, + { url = "https://files.pythonhosted.org/packages/d7/a0/7255bfd87c47b18f29fe78719b1e9fd075624c8d7cb957d65b951468cc38/openslide_bin-4.0.0.8-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:d9cb149af1a5c2eddbc0e3b75f552d932a840689f011d53724912de37071cc40", size = 4327171 }, + { url = "https://files.pythonhosted.org/packages/b1/05/c01a0c145ba88b19acd9874693774e6bedb7aa1bb1fa4c0b40e1bc42851e/openslide_bin-4.0.0.8-py3-none-win_amd64.whl", hash = "sha256:a06abf5b6f7807fa7aad2b240496a1d825cbd885267d521458eacbcfd782ed44", size = 4176324 }, +] + +[[package]] +name = "openslide-python" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/c7/1b79b6f31aa23d33ae93d7fd34fdf0d03569dbfa35a0689be8ce27471c05/openslide_python-1.4.2.tar.gz", hash = "sha256:610d7a71552afe7be33038c10d8a2b20b9824ca2836710870e81f193257411bc", size = 386361 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/12/b6e8ec85746f02aa5433cf2eaf2493ef8cdf5ef0a8675278a0263db6d88e/openslide_python-1.4.2-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0f459f0e071048a5e3cafbf814fdac92da55df9b07730a832b54b99191f8a612", size = 33187 }, + { url = "https://files.pythonhosted.org/packages/c5/2f/d3004b89dc7810db756c367cf6c95040864f79323dc25e8f9bbd0cf71c94/openslide_python-1.4.2-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b24566c2167d5909b4f54d5ee53c1caada5ec1aa14921a19de67a694ed988765", size = 37375 }, + { url = "https://files.pythonhosted.org/packages/82/75/664b94930660f124a36d15afd8e7c4b56d8c9b3e36c0cbe76311aaaa65b0/openslide_python-1.4.2-cp311-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:69cc23d608120bc528399d006c47501ac5e390770d4d5090800defd51be1b595", size = 36794 }, + { url = "https://files.pythonhosted.org/packages/7f/5d/5e8825f93cfa305fb2e601b947dd55741bad49fddf12e43bf3b8f07100aa/openslide_python-1.4.2-cp311-abi3-win_amd64.whl", hash = "sha256:e53cfb967c0987c76a9b890bb9551e238b12a99d947362434a08d4aa40ba1739", size = 35385 }, +] + +[[package]] +name = "osqp" +version = "0.6.7.post3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "qdldl" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/35/45d4d1832b31d207f83e0f9734d041be125fb4f0dff49413674bd1b08032/osqp-0.6.7.post3.tar.gz", hash = "sha256:b0c5e0a721f21c9724097a4fd50108304d296468d124e16f34ac67046f7020e1", size = 229274 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/dd/123079f0ad8409d3be9074344a3d45073ce928f701890f010ab506ffee9f/osqp-0.6.7.post3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b1a1dcd869fd6ac501e06262c21483a3691b6281e4f3f65af6951330958b89ca", size = 251844 }, + { url = "https://files.pythonhosted.org/packages/cd/6d/0d17e8fa61809c125f97685d86e6cd6f7b1e745e01b8d3f96d783c8de41b/osqp-0.6.7.post3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46b93d1110dc0ad311f6691c4df9ee41cbbde5ffc0d8c8d520d4555bf5d8765b", size = 237567 }, + { url = "https://files.pythonhosted.org/packages/4f/74/d748a9f42426fa48ab0139d0738988296a3c599a6b3c78395258d02e436b/osqp-0.6.7.post3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5209104d6fe3ace4fdbf9ace08caa2cba9de1e7ccd5f56279a346c235917138b", size = 293855 }, + { url = "https://files.pythonhosted.org/packages/55/72/8746c4bc488a31641091ccc50e71f92e0a4211e2ef882e00904940531962/osqp-0.6.7.post3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdfefa07740e9fb1c574cdc836e5afe2600b73c0c12089955d4ae6587c55f0eb", size = 298312 }, + { url = "https://files.pythonhosted.org/packages/f8/7b/ec42030f389c1b2a7e5517d4ba4a169f1d8fb6f4beb92c5b457e0cc284e4/osqp-0.6.7.post3-cp311-cp311-win_amd64.whl", hash = "sha256:c48c91dfba02ce11e8b8f5d401ec5b67a316782bfdf4f53ca753e49907f7387f", size = 293043 }, + { url = "https://files.pythonhosted.org/packages/22/26/4cf65e82cf63c4f4ff5186618c006d95a1a5bc9f4f015563ad6d87d75a42/osqp-0.6.7.post3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:023af06764f7aba9c64536ecb7204019906bb7e78237f335f82b404f16623eef", size = 252062 }, + { url = "https://files.pythonhosted.org/packages/ce/bc/ece5348baef40bf355c5ef8000103aaf77973060f4c940da9cce0999e00d/osqp-0.6.7.post3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4cec7cb5bf1615c4129277275dc08e20a037372a874cff35eb891b4b35a463de", size = 237577 }, + { url = "https://files.pythonhosted.org/packages/31/33/f09c305591606e59edc5f09aa5cba3606c0e29e7b0fff42d044585bcc1f4/osqp-0.6.7.post3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb882ab24b97b14843b7c71d2474fb8b415bafc8dd60aa94870c2ef338c20bfb", size = 295407 }, + { url = "https://files.pythonhosted.org/packages/ef/63/356f01888eb0e4cd8603eb8b7711a6865e26bc2d9a1882a1e4562333debd/osqp-0.6.7.post3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:502fde0ae710cef1e6418fb8d26efef9597d1dcba877489a1c2eb9c3eb2ff2e9", size = 300002 }, + { url = "https://files.pythonhosted.org/packages/57/b5/958d4188cb9347e420d3de2d19d8cb1113f691b7a093cdef67f86b598f30/osqp-0.6.7.post3-cp312-cp312-win_amd64.whl", hash = "sha256:468588cfb690becba4d1f460c2a53e75530584e3efcf2caed59f5219032e6888", size = 293164 }, + { url = "https://files.pythonhosted.org/packages/94/78/f01a209777678e94546b8c43c12a28db16094dfeea689e9db6d59d3b89ad/osqp-0.6.7.post3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cee478eedf9cfad11ff9c27ef0b1e032506a16888b8b874f622816cf8749db7f", size = 251946 }, + { url = "https://files.pythonhosted.org/packages/05/89/2d2dc40ebe25f92901d52d706bf8f31ea5718570e55828f1000fd380911f/osqp-0.6.7.post3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5dd739c4c6c91e40d2e3ea2bb78c635c897e07697ab24a46d3a5d197e254b0f3", size = 237548 }, + { url = "https://files.pythonhosted.org/packages/65/d3/76c076599a290f6ed5b7ad3bd2c68fa0dd198ccd63fec17f004b859843e4/osqp-0.6.7.post3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:002f280f23d15ad3c6386a868688f0b17c90dba13d0f7f8da1c833a14fc4d7f8", size = 295476 }, + { url = "https://files.pythonhosted.org/packages/63/29/b8c9cf93a6a04399960e51c92c92a195d32b80a330c6a25a51d300b86e1a/osqp-0.6.7.post3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a2922fe8cb666964cf01b643da81eadf4bb435139a5f042d5bb6dcb87496778", size = 300072 }, + { url = "https://files.pythonhosted.org/packages/4b/0a/acd48ad432ccf2538972805095108801a3b29a2433b48bd3a34e640df1e4/osqp-0.6.7.post3-cp313-cp313-win_amd64.whl", hash = "sha256:acb219e941f5248da5de3ee9b70e6a5aaddf5f3989dffd1d4c03b0f7b1dfa17b", size = 293170 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pandas" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222 }, + { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274 }, + { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836 }, + { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505 }, + { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420 }, + { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457 }, + { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166 }, + { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893 }, + { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475 }, + { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645 }, + { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445 }, + { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235 }, + { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756 }, + { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 }, + { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 }, + { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 }, + { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 }, + { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 }, + { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 }, + { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 }, + { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 }, + { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 }, + { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 }, + { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 }, + { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 }, + { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 }, + { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 }, +] + +[[package]] +name = "parso" +version = "0.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, +] + +[[package]] +name = "pillow" +version = "11.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/08/3fbf4b98924c73037a8e8b4c2c774784805e0fb4ebca6c5bb60795c40125/pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70", size = 3198450 }, + { url = "https://files.pythonhosted.org/packages/84/92/6505b1af3d2849d5e714fc75ba9e69b7255c05ee42383a35a4d58f576b16/pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf", size = 3030550 }, + { url = "https://files.pythonhosted.org/packages/3c/8c/ac2f99d2a70ff966bc7eb13dacacfaab57c0549b2ffb351b6537c7840b12/pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7", size = 4415018 }, + { url = "https://files.pythonhosted.org/packages/1f/e3/0a58b5d838687f40891fff9cbaf8669f90c96b64dc8f91f87894413856c6/pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8", size = 4498006 }, + { url = "https://files.pythonhosted.org/packages/21/f5/6ba14718135f08fbfa33308efe027dd02b781d3f1d5c471444a395933aac/pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600", size = 4517773 }, + { url = "https://files.pythonhosted.org/packages/20/f2/805ad600fc59ebe4f1ba6129cd3a75fb0da126975c8579b8f57abeb61e80/pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788", size = 4607069 }, + { url = "https://files.pythonhosted.org/packages/71/6b/4ef8a288b4bb2e0180cba13ca0a519fa27aa982875882392b65131401099/pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e", size = 4583460 }, + { url = "https://files.pythonhosted.org/packages/62/ae/f29c705a09cbc9e2a456590816e5c234382ae5d32584f451c3eb41a62062/pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e", size = 4661304 }, + { url = "https://files.pythonhosted.org/packages/6e/1a/c8217b6f2f73794a5e219fbad087701f412337ae6dbb956db37d69a9bc43/pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6", size = 2331809 }, + { url = "https://files.pythonhosted.org/packages/e2/72/25a8f40170dc262e86e90f37cb72cb3de5e307f75bf4b02535a61afcd519/pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193", size = 2676338 }, + { url = "https://files.pythonhosted.org/packages/06/9e/76825e39efee61efea258b479391ca77d64dbd9e5804e4ad0fa453b4ba55/pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7", size = 2414918 }, + { url = "https://files.pythonhosted.org/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", size = 3190185 }, + { url = "https://files.pythonhosted.org/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", size = 3030306 }, + { url = "https://files.pythonhosted.org/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", size = 4416121 }, + { url = "https://files.pythonhosted.org/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", size = 4501707 }, + { url = "https://files.pythonhosted.org/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", size = 4522921 }, + { url = "https://files.pythonhosted.org/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", size = 4612523 }, + { url = "https://files.pythonhosted.org/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", size = 4587836 }, + { url = "https://files.pythonhosted.org/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", size = 4669390 }, + { url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309 }, + { url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768 }, + { url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087 }, + { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098 }, + { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166 }, + { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674 }, + { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005 }, + { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707 }, + { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008 }, + { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420 }, + { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655 }, + { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329 }, + { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388 }, + { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950 }, + { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759 }, + { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284 }, + { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826 }, + { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329 }, + { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049 }, + { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408 }, + { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863 }, + { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938 }, + { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774 }, + { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895 }, + { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234 }, + { url = "https://files.pythonhosted.org/packages/a4/ad/2613c04633c7257d9481ab21d6b5364b59fc5d75faafd7cb8693523945a3/pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed", size = 3181734 }, + { url = "https://files.pythonhosted.org/packages/a4/fd/dcdda4471ed667de57bb5405bb42d751e6cfdd4011a12c248b455c778e03/pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c", size = 2999841 }, + { url = "https://files.pythonhosted.org/packages/ac/89/8a2536e95e77432833f0db6fd72a8d310c8e4272a04461fb833eb021bf94/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd", size = 3437470 }, + { url = "https://files.pythonhosted.org/packages/9d/8f/abd47b73c60712f88e9eda32baced7bfc3e9bd6a7619bb64b93acff28c3e/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076", size = 3460013 }, + { url = "https://files.pythonhosted.org/packages/f6/20/5c0a0aa83b213b7a07ec01e71a3d6ea2cf4ad1d2c686cc0168173b6089e7/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b", size = 3527165 }, + { url = "https://files.pythonhosted.org/packages/58/0e/2abab98a72202d91146abc839e10c14f7cf36166f12838ea0c4db3ca6ecb/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f", size = 3571586 }, + { url = "https://files.pythonhosted.org/packages/21/2c/5e05f58658cf49b6667762cca03d6e7d85cededde2caf2ab37b81f80e574/pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044", size = 2674751 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + +[[package]] +name = "portalocker" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424 }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810 }, +] + +[[package]] +name = "propcache" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/c8/fdc6686a986feae3541ea23dcaa661bd93972d3940460646c6bb96e21c40/propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf", size = 43651 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/0f/5a5319ee83bd651f75311fcb0c492c21322a7fc8f788e4eef23f44243427/propcache-0.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5", size = 80243 }, + { url = "https://files.pythonhosted.org/packages/ce/84/3db5537e0879942783e2256616ff15d870a11d7ac26541336fe1b673c818/propcache-0.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371", size = 46503 }, + { url = "https://files.pythonhosted.org/packages/e2/c8/b649ed972433c3f0d827d7f0cf9ea47162f4ef8f4fe98c5f3641a0bc63ff/propcache-0.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da", size = 45934 }, + { url = "https://files.pythonhosted.org/packages/59/f9/4c0a5cf6974c2c43b1a6810c40d889769cc8f84cea676cbe1e62766a45f8/propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744", size = 233633 }, + { url = "https://files.pythonhosted.org/packages/e7/64/66f2f4d1b4f0007c6e9078bd95b609b633d3957fe6dd23eac33ebde4b584/propcache-0.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0", size = 241124 }, + { url = "https://files.pythonhosted.org/packages/aa/bf/7b8c9fd097d511638fa9b6af3d986adbdf567598a567b46338c925144c1b/propcache-0.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5", size = 240283 }, + { url = "https://files.pythonhosted.org/packages/fa/c9/e85aeeeaae83358e2a1ef32d6ff50a483a5d5248bc38510d030a6f4e2816/propcache-0.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256", size = 232498 }, + { url = "https://files.pythonhosted.org/packages/8e/66/acb88e1f30ef5536d785c283af2e62931cb934a56a3ecf39105887aa8905/propcache-0.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073", size = 221486 }, + { url = "https://files.pythonhosted.org/packages/f5/f9/233ddb05ffdcaee4448508ee1d70aa7deff21bb41469ccdfcc339f871427/propcache-0.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d", size = 222675 }, + { url = "https://files.pythonhosted.org/packages/98/b8/eb977e28138f9e22a5a789daf608d36e05ed93093ef12a12441030da800a/propcache-0.3.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f", size = 215727 }, + { url = "https://files.pythonhosted.org/packages/89/2d/5f52d9c579f67b8ee1edd9ec073c91b23cc5b7ff7951a1e449e04ed8fdf3/propcache-0.3.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0", size = 217878 }, + { url = "https://files.pythonhosted.org/packages/7a/fd/5283e5ed8a82b00c7a989b99bb6ea173db1ad750bf0bf8dff08d3f4a4e28/propcache-0.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a", size = 230558 }, + { url = "https://files.pythonhosted.org/packages/90/38/ab17d75938ef7ac87332c588857422ae126b1c76253f0f5b1242032923ca/propcache-0.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a", size = 233754 }, + { url = "https://files.pythonhosted.org/packages/06/5d/3b921b9c60659ae464137508d3b4c2b3f52f592ceb1964aa2533b32fcf0b/propcache-0.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9", size = 226088 }, + { url = "https://files.pythonhosted.org/packages/54/6e/30a11f4417d9266b5a464ac5a8c5164ddc9dd153dfa77bf57918165eb4ae/propcache-0.3.1-cp311-cp311-win32.whl", hash = "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005", size = 40859 }, + { url = "https://files.pythonhosted.org/packages/1d/3a/8a68dd867da9ca2ee9dfd361093e9cb08cb0f37e5ddb2276f1b5177d7731/propcache-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7", size = 45153 }, + { url = "https://files.pythonhosted.org/packages/41/aa/ca78d9be314d1e15ff517b992bebbed3bdfef5b8919e85bf4940e57b6137/propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723", size = 80430 }, + { url = "https://files.pythonhosted.org/packages/1a/d8/f0c17c44d1cda0ad1979af2e593ea290defdde9eaeb89b08abbe02a5e8e1/propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976", size = 46637 }, + { url = "https://files.pythonhosted.org/packages/ae/bd/c1e37265910752e6e5e8a4c1605d0129e5b7933c3dc3cf1b9b48ed83b364/propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b", size = 46123 }, + { url = "https://files.pythonhosted.org/packages/d4/b0/911eda0865f90c0c7e9f0415d40a5bf681204da5fd7ca089361a64c16b28/propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f", size = 243031 }, + { url = "https://files.pythonhosted.org/packages/0a/06/0da53397c76a74271621807265b6eb61fb011451b1ddebf43213df763669/propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70", size = 249100 }, + { url = "https://files.pythonhosted.org/packages/f1/eb/13090e05bf6b963fc1653cdc922133ced467cb4b8dab53158db5a37aa21e/propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7", size = 250170 }, + { url = "https://files.pythonhosted.org/packages/3b/4c/f72c9e1022b3b043ec7dc475a0f405d4c3e10b9b1d378a7330fecf0652da/propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25", size = 245000 }, + { url = "https://files.pythonhosted.org/packages/e8/fd/970ca0e22acc829f1adf5de3724085e778c1ad8a75bec010049502cb3a86/propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277", size = 230262 }, + { url = "https://files.pythonhosted.org/packages/c4/42/817289120c6b9194a44f6c3e6b2c3277c5b70bbad39e7df648f177cc3634/propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8", size = 236772 }, + { url = "https://files.pythonhosted.org/packages/7c/9c/3b3942b302badd589ad6b672da3ca7b660a6c2f505cafd058133ddc73918/propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e", size = 231133 }, + { url = "https://files.pythonhosted.org/packages/98/a1/75f6355f9ad039108ff000dfc2e19962c8dea0430da9a1428e7975cf24b2/propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee", size = 230741 }, + { url = "https://files.pythonhosted.org/packages/67/0c/3e82563af77d1f8731132166da69fdfd95e71210e31f18edce08a1eb11ea/propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815", size = 244047 }, + { url = "https://files.pythonhosted.org/packages/f7/50/9fb7cca01532a08c4d5186d7bb2da6c4c587825c0ae134b89b47c7d62628/propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5", size = 246467 }, + { url = "https://files.pythonhosted.org/packages/a9/02/ccbcf3e1c604c16cc525309161d57412c23cf2351523aedbb280eb7c9094/propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7", size = 241022 }, + { url = "https://files.pythonhosted.org/packages/db/19/e777227545e09ca1e77a6e21274ae9ec45de0f589f0ce3eca2a41f366220/propcache-0.3.1-cp312-cp312-win32.whl", hash = "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b", size = 40647 }, + { url = "https://files.pythonhosted.org/packages/24/bb/3b1b01da5dd04c77a204c84e538ff11f624e31431cfde7201d9110b092b1/propcache-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3", size = 44784 }, + { url = "https://files.pythonhosted.org/packages/58/60/f645cc8b570f99be3cf46714170c2de4b4c9d6b827b912811eff1eb8a412/propcache-0.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8", size = 77865 }, + { url = "https://files.pythonhosted.org/packages/6f/d4/c1adbf3901537582e65cf90fd9c26fde1298fde5a2c593f987112c0d0798/propcache-0.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f", size = 45452 }, + { url = "https://files.pythonhosted.org/packages/d1/b5/fe752b2e63f49f727c6c1c224175d21b7d1727ce1d4873ef1c24c9216830/propcache-0.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111", size = 44800 }, + { url = "https://files.pythonhosted.org/packages/62/37/fc357e345bc1971e21f76597028b059c3d795c5ca7690d7a8d9a03c9708a/propcache-0.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5", size = 225804 }, + { url = "https://files.pythonhosted.org/packages/0d/f1/16e12c33e3dbe7f8b737809bad05719cff1dccb8df4dafbcff5575002c0e/propcache-0.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb", size = 230650 }, + { url = "https://files.pythonhosted.org/packages/3e/a2/018b9f2ed876bf5091e60153f727e8f9073d97573f790ff7cdf6bc1d1fb8/propcache-0.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7", size = 234235 }, + { url = "https://files.pythonhosted.org/packages/45/5f/3faee66fc930dfb5da509e34c6ac7128870631c0e3582987fad161fcb4b1/propcache-0.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120", size = 228249 }, + { url = "https://files.pythonhosted.org/packages/62/1e/a0d5ebda5da7ff34d2f5259a3e171a94be83c41eb1e7cd21a2105a84a02e/propcache-0.3.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654", size = 214964 }, + { url = "https://files.pythonhosted.org/packages/db/a0/d72da3f61ceab126e9be1f3bc7844b4e98c6e61c985097474668e7e52152/propcache-0.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e", size = 222501 }, + { url = "https://files.pythonhosted.org/packages/18/6d/a008e07ad7b905011253adbbd97e5b5375c33f0b961355ca0a30377504ac/propcache-0.3.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b", size = 217917 }, + { url = "https://files.pythonhosted.org/packages/98/37/02c9343ffe59e590e0e56dc5c97d0da2b8b19fa747ebacf158310f97a79a/propcache-0.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53", size = 217089 }, + { url = "https://files.pythonhosted.org/packages/53/1b/d3406629a2c8a5666d4674c50f757a77be119b113eedd47b0375afdf1b42/propcache-0.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5", size = 228102 }, + { url = "https://files.pythonhosted.org/packages/cd/a7/3664756cf50ce739e5f3abd48febc0be1a713b1f389a502ca819791a6b69/propcache-0.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7", size = 230122 }, + { url = "https://files.pythonhosted.org/packages/35/36/0bbabaacdcc26dac4f8139625e930f4311864251276033a52fd52ff2a274/propcache-0.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef", size = 226818 }, + { url = "https://files.pythonhosted.org/packages/cc/27/4e0ef21084b53bd35d4dae1634b6d0bad35e9c58ed4f032511acca9d4d26/propcache-0.3.1-cp313-cp313-win32.whl", hash = "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24", size = 40112 }, + { url = "https://files.pythonhosted.org/packages/a6/2c/a54614d61895ba6dd7ac8f107e2b2a0347259ab29cbf2ecc7b94fa38c4dc/propcache-0.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037", size = 44034 }, + { url = "https://files.pythonhosted.org/packages/5a/a8/0a4fd2f664fc6acc66438370905124ce62e84e2e860f2557015ee4a61c7e/propcache-0.3.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f", size = 82613 }, + { url = "https://files.pythonhosted.org/packages/4d/e5/5ef30eb2cd81576256d7b6caaa0ce33cd1d2c2c92c8903cccb1af1a4ff2f/propcache-0.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c", size = 47763 }, + { url = "https://files.pythonhosted.org/packages/87/9a/87091ceb048efeba4d28e903c0b15bcc84b7c0bf27dc0261e62335d9b7b8/propcache-0.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc", size = 47175 }, + { url = "https://files.pythonhosted.org/packages/3e/2f/854e653c96ad1161f96194c6678a41bbb38c7947d17768e8811a77635a08/propcache-0.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de", size = 292265 }, + { url = "https://files.pythonhosted.org/packages/40/8d/090955e13ed06bc3496ba4a9fb26c62e209ac41973cb0d6222de20c6868f/propcache-0.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6", size = 294412 }, + { url = "https://files.pythonhosted.org/packages/39/e6/d51601342e53cc7582449e6a3c14a0479fab2f0750c1f4d22302e34219c6/propcache-0.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7", size = 294290 }, + { url = "https://files.pythonhosted.org/packages/3b/4d/be5f1a90abc1881884aa5878989a1acdafd379a91d9c7e5e12cef37ec0d7/propcache-0.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458", size = 282926 }, + { url = "https://files.pythonhosted.org/packages/57/2b/8f61b998c7ea93a2b7eca79e53f3e903db1787fca9373af9e2cf8dc22f9d/propcache-0.3.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11", size = 267808 }, + { url = "https://files.pythonhosted.org/packages/11/1c/311326c3dfce59c58a6098388ba984b0e5fb0381ef2279ec458ef99bd547/propcache-0.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c", size = 290916 }, + { url = "https://files.pythonhosted.org/packages/4b/74/91939924b0385e54dc48eb2e4edd1e4903ffd053cf1916ebc5347ac227f7/propcache-0.3.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf", size = 262661 }, + { url = "https://files.pythonhosted.org/packages/c2/d7/e6079af45136ad325c5337f5dd9ef97ab5dc349e0ff362fe5c5db95e2454/propcache-0.3.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27", size = 264384 }, + { url = "https://files.pythonhosted.org/packages/b7/d5/ba91702207ac61ae6f1c2da81c5d0d6bf6ce89e08a2b4d44e411c0bbe867/propcache-0.3.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757", size = 291420 }, + { url = "https://files.pythonhosted.org/packages/58/70/2117780ed7edcd7ba6b8134cb7802aada90b894a9810ec56b7bb6018bee7/propcache-0.3.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18", size = 290880 }, + { url = "https://files.pythonhosted.org/packages/4a/1f/ecd9ce27710021ae623631c0146719280a929d895a095f6d85efb6a0be2e/propcache-0.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a", size = 287407 }, + { url = "https://files.pythonhosted.org/packages/3e/66/2e90547d6b60180fb29e23dc87bd8c116517d4255240ec6d3f7dc23d1926/propcache-0.3.1-cp313-cp313t-win32.whl", hash = "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d", size = 42573 }, + { url = "https://files.pythonhosted.org/packages/cb/8f/50ad8599399d1861b4d2b6b45271f0ef6af1b09b0a2386a46dbaf19c9535/propcache-0.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e", size = 46757 }, + { url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376 }, +] + +[[package]] +name = "protobuf" +version = "6.31.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/f3/b9655a711b32c19720253f6f06326faf90580834e2e83f840472d752bc8b/protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a", size = 441797 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/6f/6ab8e4bf962fd5570d3deaa2d5c38f0a363f57b4501047b5ebeb83ab1125/protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9", size = 423603 }, + { url = "https://files.pythonhosted.org/packages/44/3a/b15c4347dd4bf3a1b0ee882f384623e2063bb5cf9fa9d57990a4f7df2fb6/protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447", size = 435283 }, + { url = "https://files.pythonhosted.org/packages/6a/c9/b9689a2a250264a84e66c46d8862ba788ee7a641cdca39bccf64f59284b7/protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402", size = 425604 }, + { url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39", size = 322115 }, + { url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6", size = 321070 }, + { url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724 }, +] + +[[package]] +name = "psutil" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pydantic" +version = "2.11.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229 }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584 }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071 }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823 }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792 }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338 }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998 }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200 }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890 }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359 }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883 }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074 }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538 }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909 }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786 }, + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000 }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996 }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957 }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199 }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296 }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109 }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028 }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044 }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881 }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034 }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187 }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628 }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866 }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894 }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688 }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808 }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580 }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859 }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810 }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498 }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611 }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924 }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196 }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389 }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223 }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162 }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560 }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200 }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123 }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852 }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484 }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896 }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475 }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013 }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715 }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "pyogrio" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "numpy" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/c3/5e30f913ad8a975abe6f6582a2d3cf321bdf40fd696940d9283c63880c7a/pyogrio-0.11.0.tar.gz", hash = "sha256:a7e0a97bc10c0d7204f6bf52e1b928cba0554c35a907c32b23065aed1ed97b3f", size = 286915 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d1/035667f23d8e7066471c500636e9ee77b159a9d92f32b5e4944d541aad69/pyogrio-0.11.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:862b79d36d39c1f755739bde00cfd82fd1034fd287084d9202b14e3a85576f5c", size = 19492247 }, + { url = "https://files.pythonhosted.org/packages/0b/da/558be674dbbf18b9cb2f31b8c9d5691e1a42100bdbd159b4771f608f01e2/pyogrio-0.11.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:21b1924c02513185e3df1301dfc9d313f1450d7c366f8629e26757f51ba31003", size = 20678449 }, + { url = "https://files.pythonhosted.org/packages/c4/78/3761a80818a148ba9544abaf9c41bef5353054054c5ed16872e65cbf9dd6/pyogrio-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:103313202414ffa7378016791d287442541af60ac57b78536f0c67f3a82904a4", size = 27068276 }, + { url = "https://files.pythonhosted.org/packages/ad/6c/9a6faa094b33054957b4eef389106aa4f94e9dbdd384c9db5f03d6a4d379/pyogrio-0.11.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2e48956e68c41a17cbf3df32d979553de2839a082a7a9b0beef14948aa4ca5df", size = 26571289 }, + { url = "https://files.pythonhosted.org/packages/25/19/6a24c2052f2f99190482c83dcf8ecdc02bde9c8dbc2d604f088f9bbb5dbb/pyogrio-0.11.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ec5666cc8bf97aef9993c998198f85fe209b8a9ad4737696d3d2ab573b3e9a5b", size = 27769581 }, + { url = "https://files.pythonhosted.org/packages/3d/ad/afc1cdea5dac6afb95d561c9ec73c27722d494d8faab7e0452cf71fba71f/pyogrio-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:8ad3744e679de2a31b1a885dc5ea260e3482f0d5e71461a88f431cda8d536b17", size = 19178064 }, + { url = "https://files.pythonhosted.org/packages/22/39/927036db0c550d35efb4d998dfe90c56515bc14d6ed0166b6c01ca28be24/pyogrio-0.11.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:a6f114d32c5c8a157c6fbf74e3ecfe69be7efb29363102f2aad14c9813de637a", size = 19491944 }, + { url = "https://files.pythonhosted.org/packages/49/78/92db6ca3650996ca80287e59b799aa303ccecd4f1cd677f15832e466d9e2/pyogrio-0.11.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:596e3f26e792882e35f25715634c12c1d6658a3d8d178c0089a9462c56b48be5", size = 20674571 }, + { url = "https://files.pythonhosted.org/packages/c8/a4/bc37ddcee3f47c79197887d6386d31d97182a94cff6a5093cad37d873bc5/pyogrio-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11d693ca24e80bd7ede7b27ea3598593be5b41fb7cec315a57f5bb24d15faef8", size = 27033355 }, + { url = "https://files.pythonhosted.org/packages/5c/6f/984a513d5deab8ca94dde440084cab3eda5684825d70395a3bd21c2a9e5d/pyogrio-0.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:961100786ae44e2f27b4049b5262e378a3cba07872fc22051905fed8b4ce42db", size = 26528521 }, + { url = "https://files.pythonhosted.org/packages/39/d6/6026ef8903aef2a15b7ba5ad84c74ca2ce67d29fc6d99e07262a65061619/pyogrio-0.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:334563d24defc5d706bd2a1fa7d7433e33140e64b0fb9cb4afc715e4f6035c2b", size = 27734210 }, + { url = "https://files.pythonhosted.org/packages/94/81/232d4808e54e026b9059f966bc2a4a5de7e42f42e4bd4e3897e1b31ea87f/pyogrio-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:bf1f9128136abcbd1605d6fc6bf8c529c2092558246d8046ee6fbc383c550074", size = 19165401 }, + { url = "https://files.pythonhosted.org/packages/ba/2b/098692d9be9defb5d40327af50ffdc0c5486a4724c06b3d1f757cd5abd6d/pyogrio-0.11.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:0b39e34199460dcd6a606db184094e69bcba89d1babb9a76cee74a134b53b232", size = 19485661 }, + { url = "https://files.pythonhosted.org/packages/00/06/5c197d76ea33d4667f427309b108281e7a3a0224e9a32c3fdb3c54e47133/pyogrio-0.11.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:5a952ef7a68fdfaf796a91b88c706108cb50ddd0a74096418e84aab7ac8a38be", size = 20667327 }, + { url = "https://files.pythonhosted.org/packages/9d/24/08715971846562624e1f185fc6f93d0a305950cc9167ac0b761f571c3c62/pyogrio-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4527abcac23bdac5781f9be9a7dd55fccd9967c7241a8e53de8ea1a06ea0cc2b", size = 27007054 }, + { url = "https://files.pythonhosted.org/packages/0d/07/c6c6d33e5b052b6bb785904477e906ed880509bc3748862ef59ed017739a/pyogrio-0.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:373a29d56a9016978aff57b88a640b5a8c3024dba7be1c059ad5af4ba932b59e", size = 26493010 }, + { url = "https://files.pythonhosted.org/packages/9f/bb/e12bebcf2668bcb83736cc76177f36ee300ac8069880fca3a73f8753fc70/pyogrio-0.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ea2a131369ae8e62e30fa4f7e1442074d4828417d05ded660acea04a6a1d199b", size = 27710440 }, + { url = "https://files.pythonhosted.org/packages/46/8f/a9d134fbbf213db259b79f5bd5bbe7e3de1ff34fbe2a0b0be9d7d2919323/pyogrio-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:bf041d65bd1e89a4bb61845579c2963f2cca1bb33cde79f4ec2c0e0dc6f93afb", size = 19163300 }, +] + +[[package]] +name = "pyparsing" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120 }, +] + +[[package]] +name = "pyproj" +version = "3.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/10/a8480ea27ea4bbe896c168808854d00f2a9b49f95c0319ddcbba693c8a90/pyproj-3.7.1.tar.gz", hash = "sha256:60d72facd7b6b79853f19744779abcd3f804c4e0d4fa8815469db20c9f640a47", size = 226339 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/0d/63670fc527e664068b70b7cab599aa38b7420dd009bdc29ea257e7f3dfb3/pyproj-3.7.1-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:a94e26c1a4950cea40116775588a2ca7cf56f1f434ff54ee35a84718f3841a3d", size = 6264315 }, + { url = "https://files.pythonhosted.org/packages/25/9d/cbaf82cfb290d1f1fa42feb9ba9464013bb3891e40c4199f8072112e4589/pyproj-3.7.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:263b54ba5004b6b957d55757d846fc5081bc02980caa0279c4fc95fa0fff6067", size = 4666267 }, + { url = "https://files.pythonhosted.org/packages/79/53/24f9f9b8918c0550f3ff49ad5de4cf3f0688c9f91ff191476db8979146fe/pyproj-3.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6d6a2ccd5607cd15ef990c51e6f2dd27ec0a741e72069c387088bba3aab60fa", size = 9680510 }, + { url = "https://files.pythonhosted.org/packages/3c/ac/12fab74a908d40b63174dc704587febd0729414804bbfd873cabe504ff2d/pyproj-3.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c5dcf24ede53d8abab7d8a77f69ff1936c6a8843ef4fcc574646e4be66e5739", size = 9493619 }, + { url = "https://files.pythonhosted.org/packages/c4/45/26311d6437135da2153a178125db5dfb6abce831ce04d10ec207eabac70a/pyproj-3.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c2e7449840a44ce860d8bea2c6c1c4bc63fa07cba801dcce581d14dcb031a02", size = 10709755 }, + { url = "https://files.pythonhosted.org/packages/99/52/4ecd0986f27d0e6c8ee3a7bc5c63da15acd30ac23034f871325b297e61fd/pyproj-3.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0829865c1d3a3543f918b3919dc601eea572d6091c0dd175e1a054db9c109274", size = 10642970 }, + { url = "https://files.pythonhosted.org/packages/3f/a5/d3bfc018fc92195a000d1d28acc1f3f1df15ff9f09ece68f45a2636c0134/pyproj-3.7.1-cp311-cp311-win32.whl", hash = "sha256:6181960b4b812e82e588407fe5c9c68ada267c3b084db078f248db5d7f45d18a", size = 5868295 }, + { url = "https://files.pythonhosted.org/packages/92/39/ef6f06a5b223dbea308cfcbb7a0f72e7b506aef1850e061b2c73b0818715/pyproj-3.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ad0ff443a785d84e2b380869fdd82e6bfc11eba6057d25b4409a9bbfa867970", size = 6279871 }, + { url = "https://files.pythonhosted.org/packages/e6/c9/876d4345b8d17f37ac59ebd39f8fa52fc6a6a9891a420f72d050edb6b899/pyproj-3.7.1-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:2781029d90df7f8d431e29562a3f2d8eafdf233c4010d6fc0381858dc7373217", size = 6264087 }, + { url = "https://files.pythonhosted.org/packages/ff/e6/5f8691f8c90e7f402cc80a6276eb19d2ec1faa150d5ae2dd9c7b0a254da8/pyproj-3.7.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:d61bf8ab04c73c1da08eedaf21a103b72fa5b0a9b854762905f65ff8b375d394", size = 4669628 }, + { url = "https://files.pythonhosted.org/packages/42/ec/16475bbb79c1c68845c0a0d9c60c4fb31e61b8a2a20bc18b1a81e81c7f68/pyproj-3.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04abc517a8555d1b05fcee768db3280143fe42ec39fdd926a2feef31631a1f2f", size = 9721415 }, + { url = "https://files.pythonhosted.org/packages/b3/a3/448f05b15e318bd6bea9a32cfaf11e886c4ae61fa3eee6e09ed5c3b74bb2/pyproj-3.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084c0a475688f934d386c2ab3b6ce03398a473cd48adfda70d9ab8f87f2394a0", size = 9556447 }, + { url = "https://files.pythonhosted.org/packages/6a/ae/bd15fe8d8bd914ead6d60bca7f895a4e6f8ef7e3928295134ff9a7dad14c/pyproj-3.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a20727a23b1e49c7dc7fe3c3df8e56a8a7acdade80ac2f5cca29d7ca5564c145", size = 10758317 }, + { url = "https://files.pythonhosted.org/packages/9d/d9/5ccefb8bca925f44256b188a91c31238cae29ab6ee7f53661ecc04616146/pyproj-3.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bf84d766646f1ebd706d883755df4370aaf02b48187cedaa7e4239f16bc8213d", size = 10771259 }, + { url = "https://files.pythonhosted.org/packages/2a/7d/31dedff9c35fa703162f922eeb0baa6c44a3288469a5fd88d209e2892f9e/pyproj-3.7.1-cp312-cp312-win32.whl", hash = "sha256:5f0da2711364d7cb9f115b52289d4a9b61e8bca0da57f44a3a9d6fc9bdeb7274", size = 5859914 }, + { url = "https://files.pythonhosted.org/packages/3e/47/c6ab03d6564a7c937590cff81a2742b5990f096cce7c1a622d325be340ee/pyproj-3.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:aee664a9d806612af30a19dba49e55a7a78ebfec3e9d198f6a6176e1d140ec98", size = 6273196 }, + { url = "https://files.pythonhosted.org/packages/ef/01/984828464c9960036c602753fc0f21f24f0aa9043c18fa3f2f2b66a86340/pyproj-3.7.1-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:5f8d02ef4431dee414d1753d13fa82a21a2f61494737b5f642ea668d76164d6d", size = 6253062 }, + { url = "https://files.pythonhosted.org/packages/68/65/6ecdcdc829811a2c160cdfe2f068a009fc572fd4349664f758ccb0853a7c/pyproj-3.7.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:0b853ae99bda66cbe24b4ccfe26d70601d84375940a47f553413d9df570065e0", size = 4660548 }, + { url = "https://files.pythonhosted.org/packages/67/da/dda94c4490803679230ba4c17a12f151b307a0d58e8110820405ca2d98db/pyproj-3.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83db380c52087f9e9bdd8a527943b2e7324f275881125e39475c4f9277bdeec4", size = 9662464 }, + { url = "https://files.pythonhosted.org/packages/6f/57/f61b7d22c91ae1d12ee00ac4c0038714e774ebcd851b9133e5f4f930dd40/pyproj-3.7.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b35ed213892e211a3ce2bea002aa1183e1a2a9b79e51bb3c6b15549a831ae528", size = 9497461 }, + { url = "https://files.pythonhosted.org/packages/b7/f6/932128236f79d2ac7d39fe1a19667fdf7155d9a81d31fb9472a7a497790f/pyproj-3.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a8b15b0463d1303bab113d1a6af2860a0d79013c3a66fcc5475ce26ef717fd4f", size = 10708869 }, + { url = "https://files.pythonhosted.org/packages/1d/0d/07ac7712994454a254c383c0d08aff9916a2851e6512d59da8dc369b1b02/pyproj-3.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:87229e42b75e89f4dad6459200f92988c5998dfb093c7c631fb48524c86cd5dc", size = 10729260 }, + { url = "https://files.pythonhosted.org/packages/b0/d0/9c604bc72c37ba69b867b6df724d6a5af6789e8c375022c952f65b2af558/pyproj-3.7.1-cp313-cp313-win32.whl", hash = "sha256:d666c3a3faaf3b1d7fc4a544059c4eab9d06f84a604b070b7aa2f318e227798e", size = 5855462 }, + { url = "https://files.pythonhosted.org/packages/98/df/68a2b7f5fb6400c64aad82d72bcc4bc531775e62eedff993a77c780defd0/pyproj-3.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:d3caac7473be22b6d6e102dde6c46de73b96bc98334e577dfaee9886f102ea2e", size = 6266573 }, +] + +[[package]] +name = "pyright" +version = "1.1.401" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/9a/7ab2b333b921b2d6bfcffe05a0e0a0bbeff884bd6fb5ed50cd68e2898e53/pyright-1.1.401.tar.gz", hash = "sha256:788a82b6611fa5e34a326a921d86d898768cddf59edde8e93e56087d277cc6f1", size = 3894193 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/e6/1f908fce68b0401d41580e0f9acc4c3d1b248adcff00dfaad75cd21a1370/pyright-1.1.401-py3-none-any.whl", hash = "sha256:6fde30492ba5b0d7667c16ecaf6c699fab8d7a1263f6a18549e0b00bf7724c06", size = 5629193 }, +] + +[[package]] +name = "pysocks" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725 }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +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 }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, +] + +[[package]] +name = "pytorch-lightning" +version = "2.5.1.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fsspec", extra = ["http"] }, + { name = "lightning-utilities" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "torch" }, + { name = "torchmetrics" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/0c/cfa6223c525f67ea3a7a2907e36e9e9a9653300f82cfd9af88f8136514ab/pytorch_lightning-2.5.1.post0.tar.gz", hash = "sha256:abc3d5a804d41f941b14e3fd7db5572a1270cd1e9889b50e962984c87d498d94", size = 634368 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/a9/e14821cfaf08e8d78185cca0477c9d3a62bafe1b4b530100f7b66bb1f7bb/pytorch_lightning-2.5.1.post0-py3-none-any.whl", hash = "sha256:873fb21392c8b79908218f5ca8f65bd835439216e52550c36ff55d849e99c93e", size = 823084 }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 }, +] + +[[package]] +name = "pywin32" +version = "310" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/b1/68aa2986129fb1011dabbe95f0136f44509afaf072b12b8f815905a39f33/pywin32-310-cp311-cp311-win32.whl", hash = "sha256:1e765f9564e83011a63321bb9d27ec456a0ed90d3732c4b2e312b855365ed8bd", size = 8784284 }, + { url = "https://files.pythonhosted.org/packages/b3/bd/d1592635992dd8db5bb8ace0551bc3a769de1ac8850200cfa517e72739fb/pywin32-310-cp311-cp311-win_amd64.whl", hash = "sha256:126298077a9d7c95c53823934f000599f66ec9296b09167810eb24875f32689c", size = 9520748 }, + { url = "https://files.pythonhosted.org/packages/90/b1/ac8b1ffce6603849eb45a91cf126c0fa5431f186c2e768bf56889c46f51c/pywin32-310-cp311-cp311-win_arm64.whl", hash = "sha256:19ec5fc9b1d51c4350be7bb00760ffce46e6c95eaf2f0b2f1150657b1a43c582", size = 8455941 }, + { url = "https://files.pythonhosted.org/packages/6b/ec/4fdbe47932f671d6e348474ea35ed94227fb5df56a7c30cbbb42cd396ed0/pywin32-310-cp312-cp312-win32.whl", hash = "sha256:8a75a5cc3893e83a108c05d82198880704c44bbaee4d06e442e471d3c9ea4f3d", size = 8796239 }, + { url = "https://files.pythonhosted.org/packages/e3/e5/b0627f8bb84e06991bea89ad8153a9e50ace40b2e1195d68e9dff6b03d0f/pywin32-310-cp312-cp312-win_amd64.whl", hash = "sha256:bf5c397c9a9a19a6f62f3fb821fbf36cac08f03770056711f765ec1503972060", size = 9503839 }, + { url = "https://files.pythonhosted.org/packages/1f/32/9ccf53748df72301a89713936645a664ec001abd35ecc8578beda593d37d/pywin32-310-cp312-cp312-win_arm64.whl", hash = "sha256:2349cc906eae872d0663d4d6290d13b90621eaf78964bb1578632ff20e152966", size = 8459470 }, + { url = "https://files.pythonhosted.org/packages/1c/09/9c1b978ffc4ae53999e89c19c77ba882d9fce476729f23ef55211ea1c034/pywin32-310-cp313-cp313-win32.whl", hash = "sha256:5d241a659c496ada3253cd01cfaa779b048e90ce4b2b38cd44168ad555ce74ab", size = 8794384 }, + { url = "https://files.pythonhosted.org/packages/45/3c/b4640f740ffebadd5d34df35fecba0e1cfef8fde9f3e594df91c28ad9b50/pywin32-310-cp313-cp313-win_amd64.whl", hash = "sha256:667827eb3a90208ddbdcc9e860c81bde63a135710e21e4cb3348968e4bd5249e", size = 9503039 }, + { url = "https://files.pythonhosted.org/packages/b4/f4/f785020090fb050e7fb6d34b780f2231f302609dc964672f72bfaeb59a28/pywin32-310-cp313-cp313-win_arm64.whl", hash = "sha256:e308f831de771482b7cf692a1f308f8fca701b2d8f9dde6cc440c7da17e47b33", size = 8458152 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "pyzmq" +version = "26.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/11/b9213d25230ac18a71b39b3723494e57adebe36e066397b961657b3b41c1/pyzmq-26.4.0.tar.gz", hash = "sha256:4bd13f85f80962f91a651a7356fe0472791a5f7a92f227822b5acf44795c626d", size = 278293 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/6d/234e3b0aa82fd0290b1896e9992f56bdddf1f97266110be54d0177a9d2d9/pyzmq-26.4.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:bfcf82644c9b45ddd7cd2a041f3ff8dce4a0904429b74d73a439e8cab1bd9e54", size = 1339723 }, + { url = "https://files.pythonhosted.org/packages/4f/11/6d561efe29ad83f7149a7cd48e498e539ed09019c6cd7ecc73f4cc725028/pyzmq-26.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9bcae3979b2654d5289d3490742378b2f3ce804b0b5fd42036074e2bf35b030", size = 672645 }, + { url = "https://files.pythonhosted.org/packages/19/fd/81bfe3e23f418644660bad1a90f0d22f0b3eebe33dd65a79385530bceb3d/pyzmq-26.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccdff8ac4246b6fb60dcf3982dfaeeff5dd04f36051fe0632748fc0aa0679c01", size = 910133 }, + { url = "https://files.pythonhosted.org/packages/97/68/321b9c775595ea3df832a9516252b653fe32818db66fdc8fa31c9b9fce37/pyzmq-26.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4550af385b442dc2d55ab7717837812799d3674cb12f9a3aa897611839c18e9e", size = 867428 }, + { url = "https://files.pythonhosted.org/packages/4e/6e/159cbf2055ef36aa2aa297e01b24523176e5b48ead283c23a94179fb2ba2/pyzmq-26.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:2f9f7ffe9db1187a253fca95191854b3fda24696f086e8789d1d449308a34b88", size = 862409 }, + { url = "https://files.pythonhosted.org/packages/05/1c/45fb8db7be5a7d0cadea1070a9cbded5199a2d578de2208197e592f219bd/pyzmq-26.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3709c9ff7ba61589b7372923fd82b99a81932b592a5c7f1a24147c91da9a68d6", size = 1205007 }, + { url = "https://files.pythonhosted.org/packages/f8/fa/658c7f583af6498b463f2fa600f34e298e1b330886f82f1feba0dc2dd6c3/pyzmq-26.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f8f3c30fb2d26ae5ce36b59768ba60fb72507ea9efc72f8f69fa088450cff1df", size = 1514599 }, + { url = "https://files.pythonhosted.org/packages/4d/d7/44d641522353ce0a2bbd150379cb5ec32f7120944e6bfba4846586945658/pyzmq-26.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:382a4a48c8080e273427fc692037e3f7d2851959ffe40864f2db32646eeb3cef", size = 1414546 }, + { url = "https://files.pythonhosted.org/packages/72/76/c8ed7263218b3d1e9bce07b9058502024188bd52cc0b0a267a9513b431fc/pyzmq-26.4.0-cp311-cp311-win32.whl", hash = "sha256:d56aad0517d4c09e3b4f15adebba8f6372c5102c27742a5bdbfc74a7dceb8fca", size = 579247 }, + { url = "https://files.pythonhosted.org/packages/c3/d0/2d9abfa2571a0b1a67c0ada79a8aa1ba1cce57992d80f771abcdf99bb32c/pyzmq-26.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:963977ac8baed7058c1e126014f3fe58b3773f45c78cce7af5c26c09b6823896", size = 644727 }, + { url = "https://files.pythonhosted.org/packages/0d/d1/c8ad82393be6ccedfc3c9f3adb07f8f3976e3c4802640fe3f71441941e70/pyzmq-26.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:c0c8e8cadc81e44cc5088fcd53b9b3b4ce9344815f6c4a03aec653509296fae3", size = 559942 }, + { url = "https://files.pythonhosted.org/packages/10/44/a778555ebfdf6c7fc00816aad12d185d10a74d975800341b1bc36bad1187/pyzmq-26.4.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:5227cb8da4b6f68acfd48d20c588197fd67745c278827d5238c707daf579227b", size = 1341586 }, + { url = "https://files.pythonhosted.org/packages/9c/4f/f3a58dc69ac757e5103be3bd41fb78721a5e17da7cc617ddb56d973a365c/pyzmq-26.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1c07a7fa7f7ba86554a2b1bef198c9fed570c08ee062fd2fd6a4dcacd45f905", size = 665880 }, + { url = "https://files.pythonhosted.org/packages/fe/45/50230bcfb3ae5cb98bee683b6edeba1919f2565d7cc1851d3c38e2260795/pyzmq-26.4.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae775fa83f52f52de73183f7ef5395186f7105d5ed65b1ae65ba27cb1260de2b", size = 902216 }, + { url = "https://files.pythonhosted.org/packages/41/59/56bbdc5689be5e13727491ad2ba5efd7cd564365750514f9bc8f212eef82/pyzmq-26.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c760d0226ebd52f1e6b644a9e839b5db1e107a23f2fcd46ec0569a4fdd4e63", size = 859814 }, + { url = "https://files.pythonhosted.org/packages/81/b1/57db58cfc8af592ce94f40649bd1804369c05b2190e4cbc0a2dad572baeb/pyzmq-26.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ef8c6ecc1d520debc147173eaa3765d53f06cd8dbe7bd377064cdbc53ab456f5", size = 855889 }, + { url = "https://files.pythonhosted.org/packages/e8/92/47542e629cbac8f221c230a6d0f38dd3d9cff9f6f589ed45fdf572ffd726/pyzmq-26.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3150ef4084e163dec29ae667b10d96aad309b668fac6810c9e8c27cf543d6e0b", size = 1197153 }, + { url = "https://files.pythonhosted.org/packages/07/e5/b10a979d1d565d54410afc87499b16c96b4a181af46e7645ab4831b1088c/pyzmq-26.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4448c9e55bf8329fa1dcedd32f661bf611214fa70c8e02fee4347bc589d39a84", size = 1507352 }, + { url = "https://files.pythonhosted.org/packages/ab/58/5a23db84507ab9c01c04b1232a7a763be66e992aa2e66498521bbbc72a71/pyzmq-26.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e07dde3647afb084d985310d067a3efa6efad0621ee10826f2cb2f9a31b89d2f", size = 1406834 }, + { url = "https://files.pythonhosted.org/packages/22/74/aaa837b331580c13b79ac39396601fb361454ee184ca85e8861914769b99/pyzmq-26.4.0-cp312-cp312-win32.whl", hash = "sha256:ba034a32ecf9af72adfa5ee383ad0fd4f4e38cdb62b13624278ef768fe5b5b44", size = 577992 }, + { url = "https://files.pythonhosted.org/packages/30/0f/55f8c02c182856743b82dde46b2dc3e314edda7f1098c12a8227eeda0833/pyzmq-26.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:056a97aab4064f526ecb32f4343917a4022a5d9efb6b9df990ff72e1879e40be", size = 640466 }, + { url = "https://files.pythonhosted.org/packages/e4/29/073779afc3ef6f830b8de95026ef20b2d1ec22d0324d767748d806e57379/pyzmq-26.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f23c750e485ce1eb639dbd576d27d168595908aa2d60b149e2d9e34c9df40e0", size = 556342 }, + { url = "https://files.pythonhosted.org/packages/d7/20/fb2c92542488db70f833b92893769a569458311a76474bda89dc4264bd18/pyzmq-26.4.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:c43fac689880f5174d6fc864857d1247fe5cfa22b09ed058a344ca92bf5301e3", size = 1339484 }, + { url = "https://files.pythonhosted.org/packages/58/29/2f06b9cabda3a6ea2c10f43e67ded3e47fc25c54822e2506dfb8325155d4/pyzmq-26.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:902aca7eba477657c5fb81c808318460328758e8367ecdd1964b6330c73cae43", size = 666106 }, + { url = "https://files.pythonhosted.org/packages/77/e4/dcf62bd29e5e190bd21bfccaa4f3386e01bf40d948c239239c2f1e726729/pyzmq-26.4.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5e48a830bfd152fe17fbdeaf99ac5271aa4122521bf0d275b6b24e52ef35eb6", size = 902056 }, + { url = "https://files.pythonhosted.org/packages/1a/cf/b36b3d7aea236087d20189bec1a87eeb2b66009731d7055e5c65f845cdba/pyzmq-26.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31be2b6de98c824c06f5574331f805707c667dc8f60cb18580b7de078479891e", size = 860148 }, + { url = "https://files.pythonhosted.org/packages/18/a6/f048826bc87528c208e90604c3bf573801e54bd91e390cbd2dfa860e82dc/pyzmq-26.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6332452034be001bbf3206ac59c0d2a7713de5f25bb38b06519fc6967b7cf771", size = 855983 }, + { url = "https://files.pythonhosted.org/packages/0a/27/454d34ab6a1d9772a36add22f17f6b85baf7c16e14325fa29e7202ca8ee8/pyzmq-26.4.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:da8c0f5dd352136853e6a09b1b986ee5278dfddfebd30515e16eae425c872b30", size = 1197274 }, + { url = "https://files.pythonhosted.org/packages/f4/3d/7abfeab6b83ad38aa34cbd57c6fc29752c391e3954fd12848bd8d2ec0df6/pyzmq-26.4.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:f4ccc1a0a2c9806dda2a2dd118a3b7b681e448f3bb354056cad44a65169f6d86", size = 1507120 }, + { url = "https://files.pythonhosted.org/packages/13/ff/bc8d21dbb9bc8705126e875438a1969c4f77e03fc8565d6901c7933a3d01/pyzmq-26.4.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c0b5fceadbab461578daf8d1dcc918ebe7ddd2952f748cf30c7cf2de5d51101", size = 1406738 }, + { url = "https://files.pythonhosted.org/packages/f5/5d/d4cd85b24de71d84d81229e3bbb13392b2698432cf8fdcea5afda253d587/pyzmq-26.4.0-cp313-cp313-win32.whl", hash = "sha256:28e2b0ff5ba4b3dd11062d905682bad33385cfa3cc03e81abd7f0822263e6637", size = 577826 }, + { url = "https://files.pythonhosted.org/packages/c6/6c/f289c1789d7bb6e5a3b3bef7b2a55089b8561d17132be7d960d3ff33b14e/pyzmq-26.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:23ecc9d241004c10e8b4f49d12ac064cd7000e1643343944a10df98e57bc544b", size = 640406 }, + { url = "https://files.pythonhosted.org/packages/b3/99/676b8851cb955eb5236a0c1e9ec679ea5ede092bf8bf2c8a68d7e965cac3/pyzmq-26.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:1edb0385c7f025045d6e0f759d4d3afe43c17a3d898914ec6582e6f464203c08", size = 556216 }, + { url = "https://files.pythonhosted.org/packages/65/c2/1fac340de9d7df71efc59d9c50fc7a635a77b103392d1842898dd023afcb/pyzmq-26.4.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:93a29e882b2ba1db86ba5dd5e88e18e0ac6b627026c5cfbec9983422011b82d4", size = 1333769 }, + { url = "https://files.pythonhosted.org/packages/5c/c7/6c03637e8d742c3b00bec4f5e4cd9d1c01b2f3694c6f140742e93ca637ed/pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb45684f276f57110bb89e4300c00f1233ca631f08f5f42528a5c408a79efc4a", size = 658826 }, + { url = "https://files.pythonhosted.org/packages/a5/97/a8dca65913c0f78e0545af2bb5078aebfc142ca7d91cdaffa1fbc73e5dbd/pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f72073e75260cb301aad4258ad6150fa7f57c719b3f498cb91e31df16784d89b", size = 891650 }, + { url = "https://files.pythonhosted.org/packages/7d/7e/f63af1031eb060bf02d033732b910fe48548dcfdbe9c785e9f74a6cc6ae4/pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be37e24b13026cfedd233bcbbccd8c0bcd2fdd186216094d095f60076201538d", size = 849776 }, + { url = "https://files.pythonhosted.org/packages/f6/fa/1a009ce582802a895c0d5fe9413f029c940a0a8ee828657a3bb0acffd88b/pyzmq-26.4.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:237b283044934d26f1eeff4075f751b05d2f3ed42a257fc44386d00df6a270cf", size = 842516 }, + { url = "https://files.pythonhosted.org/packages/6e/bc/f88b0bad0f7a7f500547d71e99f10336f2314e525d4ebf576a1ea4a1d903/pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:b30f862f6768b17040929a68432c8a8be77780317f45a353cb17e423127d250c", size = 1189183 }, + { url = "https://files.pythonhosted.org/packages/d9/8c/db446a3dd9cf894406dec2e61eeffaa3c07c3abb783deaebb9812c4af6a5/pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:c80fcd3504232f13617c6ab501124d373e4895424e65de8b72042333316f64a8", size = 1495501 }, + { url = "https://files.pythonhosted.org/packages/05/4c/bf3cad0d64c3214ac881299c4562b815f05d503bccc513e3fd4fdc6f67e4/pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:26a2a7451606b87f67cdeca2c2789d86f605da08b4bd616b1a9981605ca3a364", size = 1395540 }, + { url = "https://files.pythonhosted.org/packages/04/52/a70fcd5592715702248306d8e1729c10742c2eac44529984413b05c68658/pyzmq-26.4.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4478b14cb54a805088299c25a79f27eaf530564a7a4f72bf432a040042b554eb", size = 834405 }, + { url = "https://files.pythonhosted.org/packages/25/f9/1a03f1accff16b3af1a6fa22cbf7ced074776abbf688b2e9cb4629700c62/pyzmq-26.4.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a28ac29c60e4ba84b5f58605ace8ad495414a724fe7aceb7cf06cd0598d04e1", size = 569578 }, + { url = "https://files.pythonhosted.org/packages/76/0c/3a633acd762aa6655fcb71fa841907eae0ab1e8582ff494b137266de341d/pyzmq-26.4.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43b03c1ceea27c6520124f4fb2ba9c647409b9abdf9a62388117148a90419494", size = 798248 }, + { url = "https://files.pythonhosted.org/packages/cd/cc/6c99c84aa60ac1cc56747bed6be8ce6305b9b861d7475772e7a25ce019d3/pyzmq-26.4.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7731abd23a782851426d4e37deb2057bf9410848a4459b5ede4fe89342e687a9", size = 756757 }, + { url = "https://files.pythonhosted.org/packages/13/9c/d8073bd898eb896e94c679abe82e47506e2b750eb261cf6010ced869797c/pyzmq-26.4.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a222ad02fbe80166b0526c038776e8042cd4e5f0dec1489a006a1df47e9040e0", size = 555371 }, +] + +[[package]] +name = "qdldl" +version = "0.1.7.post5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/88/9254835c513a381b8c6d52773060844acf76dfa739648c18f61809c8ee04/qdldl-0.1.7.post5.tar.gz", hash = "sha256:0b1399e1c49b5bed5aac8fd63ef08ab708d340c37fb426fe00128bc1f36b286e", size = 73920 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/6c/ce4cab36da9a7c0bff69067377b513ec88ff753de07f33f65959f4141308/qdldl-0.1.7.post5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa22df45e625c763d129b2893b284b7bde16a535a7e900288d588be9dc24fe9f", size = 106139 }, + { url = "https://files.pythonhosted.org/packages/86/cf/641787a0c64019e76eb8bea925930005960323f1a5539361c209613f4747/qdldl-0.1.7.post5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e196871dafe4febb86c2886713c8a2226d19455226e56e3b9480aa78eb59b5e", size = 103421 }, + { url = "https://files.pythonhosted.org/packages/be/87/91d2f0debdd515b653c701c023b939325c51157d74154336b8495f156659/qdldl-0.1.7.post5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ba5ff31a66d1f92b41d0b97d27288d28a8c849dd6db2221a579b1a5a5a6df0f", size = 1179946 }, + { url = "https://files.pythonhosted.org/packages/b8/7e/5fe5a081bd229a2b703a4b93e5ecaf44f51902e9b6a645c8ce4ea325ec0d/qdldl-0.1.7.post5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c34872867c2bcac60279034594eac8dee042b9dedd4c45948e55884b8c5c9cd0", size = 1193311 }, + { url = "https://files.pythonhosted.org/packages/53/dc/d6b760217f0fa7007e45c03dc0193c828ee5010f037acb58b79cd0010fbc/qdldl-0.1.7.post5-cp311-cp311-win_amd64.whl", hash = "sha256:b1280e886f734e3d0d67f643e3d76c55d2e23d0e7b06d89b987681dc165892c5", size = 90488 }, + { url = "https://files.pythonhosted.org/packages/14/c1/eba61a848f9dfa0b54e954aa71f18eb35576f8842ef31dc76a3569a50526/qdldl-0.1.7.post5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d67a95d0ba73147a05cf98dc9284103f64150c9e2c214cd35ee0258f06922c5e", size = 106277 }, + { url = "https://files.pythonhosted.org/packages/02/2e/5daa29b8ecf25277c36a220ef3b509d2ec4079ab81ff3adc544bc12cd675/qdldl-0.1.7.post5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e23d684427ce49f5d657e353322363555d1a31605fe72cbe4b965a4e260742c", size = 103179 }, + { url = "https://files.pythonhosted.org/packages/c6/74/5818f5027a0c252d1e8a2eba996359155d1518db90ce545f1becf0dd4a10/qdldl-0.1.7.post5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c4953d4fe61951fb515a6439009248b5a7b73627d74ee929d02b19bea41b19d", size = 1182542 }, + { url = "https://files.pythonhosted.org/packages/ae/55/90ad03c32e673a9b33cfa7cb43f55d1ab0509b60396afbb1031fa1516fd9/qdldl-0.1.7.post5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:520dbe4006a333c773ff474d2dc1e0af928c0dc7d9ca36db5637ba738ee608ba", size = 1201734 }, + { url = "https://files.pythonhosted.org/packages/c1/82/730d0d2c6093c4dc574947eea94e0cddeea836f43823a80fc8b064a82ddf/qdldl-0.1.7.post5-cp312-cp312-win_amd64.whl", hash = "sha256:13dfc0b225a5c180512488fa51f1771e8fa3c06d7fce9fd3c1d018bc03ba0eec", size = 90706 }, + { url = "https://files.pythonhosted.org/packages/d1/33/5c4348f6e2e4868ec843c05ec9679ec59b37cd9a251ee7ee9baf00b2f8b4/qdldl-0.1.7.post5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7600985d2321cb15f71f8bb3a92ef2a85284b4fd740d8bbd4960b8c2f7ee6d33", size = 106318 }, + { url = "https://files.pythonhosted.org/packages/c6/1a/aaa6835253c94a53816b4bea7c6051fd406c492bed7024fc683d0da1b939/qdldl-0.1.7.post5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:314153f574641c846a85ff9b4a5c0e0d23e32d0de11d8381866bb27577088bef", size = 103216 }, + { url = "https://files.pythonhosted.org/packages/9f/0e/fd317e269ba7c3ed11e0e999ebfe2f3611de9bdd1fe7052ea32f0a3fd22e/qdldl-0.1.7.post5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b1ee3840d7d8ef4f1e3ffce0620116a71abd72c52ba46e0c194d4b294a0ad2", size = 1182628 }, + { url = "https://files.pythonhosted.org/packages/41/ad/6cd39b1c3ac5c0201eaf4be257303d1c6f7193f0d0f5c54ab151ba8faad9/qdldl-0.1.7.post5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cba6df1eedbaea844485e1c7a6ae9013bbdc86f07c4ebb13c89249b003de4ef4", size = 1201935 }, + { url = "https://files.pythonhosted.org/packages/08/f7/abac03a09f6848cee6d5dd7a7a8bd1dfed68766ee77f9cbf3e9de596ad68/qdldl-0.1.7.post5-cp313-cp313-win_amd64.whl", hash = "sha256:cc9be378e7bec67d4c62b7fa27cafb4f77d3e5e059d753c3dce0a5ae1ef5fea0", size = 90735 }, +] + +[[package]] +name = "regex" +version = "2024.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, + { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, + { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, + { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, + { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, + { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, + { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, + { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, + { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, + { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, + { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, + { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, + { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, + { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, + { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, + { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, + { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, + { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, + { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, + { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, + { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, + { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, + { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, + { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, + { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, + { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, + { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, + { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, + { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, + { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, + { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, + { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, + { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, + { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, + { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, + { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, + { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, + { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, + { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, + { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, + { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, + { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, + { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[package.optional-dependencies] +socks = [ + { name = "pysocks" }, +] + +[[package]] +name = "ruff" +version = "0.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/0a/92416b159ec00cdf11e5882a9d80d29bf84bba3dbebc51c4898bfbca1da6/ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603", size = 4202289 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/cc/53eb79f012d15e136d40a8e8fc519ba8f55a057f60b29c2df34efd47c6e3/ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc", size = 10285597 }, + { url = "https://files.pythonhosted.org/packages/e7/d7/73386e9fb0232b015a23f62fea7503f96e29c29e6c45461d4a73bac74df9/ruff-0.11.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3", size = 11053154 }, + { url = "https://files.pythonhosted.org/packages/4e/eb/3eae144c5114e92deb65a0cb2c72326c8469e14991e9bc3ec0349da1331c/ruff-0.11.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa", size = 10403048 }, + { url = "https://files.pythonhosted.org/packages/29/64/20c54b20e58b1058db6689e94731f2a22e9f7abab74e1a758dfba058b6ca/ruff-0.11.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012", size = 10597062 }, + { url = "https://files.pythonhosted.org/packages/29/3a/79fa6a9a39422a400564ca7233a689a151f1039110f0bbbabcb38106883a/ruff-0.11.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a", size = 10155152 }, + { url = "https://files.pythonhosted.org/packages/e5/a4/22c2c97b2340aa968af3a39bc38045e78d36abd4ed3fa2bde91c31e712e3/ruff-0.11.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7", size = 11723067 }, + { url = "https://files.pythonhosted.org/packages/bc/cf/3e452fbd9597bcd8058856ecd42b22751749d07935793a1856d988154151/ruff-0.11.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a", size = 12460807 }, + { url = "https://files.pythonhosted.org/packages/2f/ec/8f170381a15e1eb7d93cb4feef8d17334d5a1eb33fee273aee5d1f8241a3/ruff-0.11.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13", size = 12063261 }, + { url = "https://files.pythonhosted.org/packages/0d/bf/57208f8c0a8153a14652a85f4116c0002148e83770d7a41f2e90b52d2b4e/ruff-0.11.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be", size = 11329601 }, + { url = "https://files.pythonhosted.org/packages/c3/56/edf942f7fdac5888094d9ffa303f12096f1a93eb46570bcf5f14c0c70880/ruff-0.11.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd", size = 11522186 }, + { url = "https://files.pythonhosted.org/packages/ed/63/79ffef65246911ed7e2290aeece48739d9603b3a35f9529fec0fc6c26400/ruff-0.11.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef", size = 10449032 }, + { url = "https://files.pythonhosted.org/packages/88/19/8c9d4d8a1c2a3f5a1ea45a64b42593d50e28b8e038f1aafd65d6b43647f3/ruff-0.11.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5", size = 10129370 }, + { url = "https://files.pythonhosted.org/packages/bc/0f/2d15533eaa18f460530a857e1778900cd867ded67f16c85723569d54e410/ruff-0.11.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02", size = 11123529 }, + { url = "https://files.pythonhosted.org/packages/4f/e2/4c2ac669534bdded835356813f48ea33cfb3a947dc47f270038364587088/ruff-0.11.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c", size = 11577642 }, + { url = "https://files.pythonhosted.org/packages/a7/9b/c9ddf7f924d5617a1c94a93ba595f4b24cb5bc50e98b94433ab3f7ad27e5/ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6", size = 10475511 }, + { url = "https://files.pythonhosted.org/packages/fd/d6/74fb6d3470c1aada019ffff33c0f9210af746cca0a4de19a1f10ce54968a/ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832", size = 11523573 }, + { url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770 }, +] + +[[package]] +name = "sacremoses" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "joblib" }, + { name = "regex" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/51/fbdc4af4f6e85d26169e28be3763fe50ddfd0d4bf8b871422b0788dcc4d2/sacremoses-0.1.1.tar.gz", hash = "sha256:b6fd5d3a766b02154ed80b962ddca91e1fd25629c0978c7efba21ebccf663934", size = 883188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/f0/89ee2bc9da434bd78464f288fdb346bc2932f2ee80a90b2a4bbbac262c74/sacremoses-0.1.1-py3-none-any.whl", hash = "sha256:31e04c98b169bfd902144824d191825cd69220cdb4ae4bcf1ec58a7db5587b1a", size = 897476 }, +] + +[[package]] +name = "safetensors" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/7e/2d5d6ee7b40c0682315367ec7475693d110f512922d582fef1bd4a63adc3/safetensors-0.5.3.tar.gz", hash = "sha256:b6b0d6ecacec39a4fdd99cc19f4576f5219ce858e6fd8dbe7609df0b8dc56965", size = 67210 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/ae/88f6c49dbd0cc4da0e08610019a3c78a7d390879a919411a410a1876d03a/safetensors-0.5.3-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd20eb133db8ed15b40110b7c00c6df51655a2998132193de2f75f72d99c7073", size = 436917 }, + { url = "https://files.pythonhosted.org/packages/b8/3b/11f1b4a2f5d2ab7da34ecc062b0bc301f2be024d110a6466726bec8c055c/safetensors-0.5.3-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:21d01c14ff6c415c485616b8b0bf961c46b3b343ca59110d38d744e577f9cce7", size = 418419 }, + { url = "https://files.pythonhosted.org/packages/5d/9a/add3e6fef267658075c5a41573c26d42d80c935cdc992384dfae435feaef/safetensors-0.5.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11bce6164887cd491ca75c2326a113ba934be596e22b28b1742ce27b1d076467", size = 459493 }, + { url = "https://files.pythonhosted.org/packages/df/5c/bf2cae92222513cc23b3ff85c4a1bb2811a2c3583ac0f8e8d502751de934/safetensors-0.5.3-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4a243be3590bc3301c821da7a18d87224ef35cbd3e5f5727e4e0728b8172411e", size = 472400 }, + { url = "https://files.pythonhosted.org/packages/58/11/7456afb740bd45782d0f4c8e8e1bb9e572f1bf82899fb6ace58af47b4282/safetensors-0.5.3-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8bd84b12b1670a6f8e50f01e28156422a2bc07fb16fc4e98bded13039d688a0d", size = 522891 }, + { url = "https://files.pythonhosted.org/packages/57/3d/fe73a9d2ace487e7285f6e157afee2383bd1ddb911b7cb44a55cf812eae3/safetensors-0.5.3-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:391ac8cab7c829452175f871fcaf414aa1e292b5448bd02620f675a7f3e7abb9", size = 537694 }, + { url = "https://files.pythonhosted.org/packages/a6/f8/dae3421624fcc87a89d42e1898a798bc7ff72c61f38973a65d60df8f124c/safetensors-0.5.3-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cead1fa41fc54b1e61089fa57452e8834f798cb1dc7a09ba3524f1eb08e0317a", size = 471642 }, + { url = "https://files.pythonhosted.org/packages/ce/20/1fbe16f9b815f6c5a672f5b760951e20e17e43f67f231428f871909a37f6/safetensors-0.5.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1077f3e94182d72618357b04b5ced540ceb71c8a813d3319f1aba448e68a770d", size = 502241 }, + { url = "https://files.pythonhosted.org/packages/5f/18/8e108846b506487aa4629fe4116b27db65c3dde922de2c8e0cc1133f3f29/safetensors-0.5.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:799021e78287bac619c7b3f3606730a22da4cda27759ddf55d37c8db7511c74b", size = 638001 }, + { url = "https://files.pythonhosted.org/packages/82/5a/c116111d8291af6c8c8a8b40628fe833b9db97d8141c2a82359d14d9e078/safetensors-0.5.3-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df26da01aaac504334644e1b7642fa000bfec820e7cef83aeac4e355e03195ff", size = 734013 }, + { url = "https://files.pythonhosted.org/packages/7d/ff/41fcc4d3b7de837963622e8610d998710705bbde9a8a17221d85e5d0baad/safetensors-0.5.3-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:32c3ef2d7af8b9f52ff685ed0bc43913cdcde135089ae322ee576de93eae5135", size = 670687 }, + { url = "https://files.pythonhosted.org/packages/40/ad/2b113098e69c985a3d8fbda4b902778eae4a35b7d5188859b4a63d30c161/safetensors-0.5.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:37f1521be045e56fc2b54c606d4455573e717b2d887c579ee1dbba5f868ece04", size = 643147 }, + { url = "https://files.pythonhosted.org/packages/0a/0c/95aeb51d4246bd9a3242d3d8349c1112b4ee7611a4b40f0c5c93b05f001d/safetensors-0.5.3-cp38-abi3-win32.whl", hash = "sha256:cfc0ec0846dcf6763b0ed3d1846ff36008c6e7290683b61616c4b040f6a54ace", size = 296677 }, + { url = "https://files.pythonhosted.org/packages/69/e2/b011c38e5394c4c18fb5500778a55ec43ad6106126e74723ffaee246f56e/safetensors-0.5.3-cp38-abi3-win_amd64.whl", hash = "sha256:836cbbc320b47e80acd40e44c8682db0e8ad7123209f69b093def21ec7cafd11", size = 308878 }, +] + +[[package]] +name = "scikit-image" +version = "0.25.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "imageio" }, + { name = "lazy-loader" }, + { name = "networkx" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "scipy" }, + { name = "tifffile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/a8/3c0f256012b93dd2cb6fda9245e9f4bff7dc0486880b248005f15ea2255e/scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde", size = 22693594 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/97/3051c68b782ee3f1fb7f8f5bb7d535cf8cb92e8aae18fa9c1cdf7e15150d/scikit_image-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f4bac9196fb80d37567316581c6060763b0f4893d3aca34a9ede3825bc035b17", size = 14003057 }, + { url = "https://files.pythonhosted.org/packages/19/23/257fc696c562639826065514d551b7b9b969520bd902c3a8e2fcff5b9e17/scikit_image-0.25.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d989d64ff92e0c6c0f2018c7495a5b20e2451839299a018e0e5108b2680f71e0", size = 13180335 }, + { url = "https://files.pythonhosted.org/packages/ef/14/0c4a02cb27ca8b1e836886b9ec7c9149de03053650e9e2ed0625f248dd92/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2cfc96b27afe9a05bc92f8c6235321d3a66499995675b27415e0d0c76625173", size = 14144783 }, + { url = "https://files.pythonhosted.org/packages/dd/9b/9fb556463a34d9842491d72a421942c8baff4281025859c84fcdb5e7e602/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cc986e1f4187a12aa319f777b36008764e856e5013666a4a83f8df083c2641", size = 14785376 }, + { url = "https://files.pythonhosted.org/packages/de/ec/b57c500ee85885df5f2188f8bb70398481393a69de44a00d6f1d055f103c/scikit_image-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:b4f6b61fc2db6340696afe3db6b26e0356911529f5f6aee8c322aa5157490c9b", size = 12791698 }, + { url = "https://files.pythonhosted.org/packages/35/8c/5df82881284459f6eec796a5ac2a0a304bb3384eec2e73f35cfdfcfbf20c/scikit_image-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8db8dd03663112783221bf01ccfc9512d1cc50ac9b5b0fe8f4023967564719fb", size = 13986000 }, + { url = "https://files.pythonhosted.org/packages/ce/e6/93bebe1abcdce9513ffec01d8af02528b4c41fb3c1e46336d70b9ed4ef0d/scikit_image-0.25.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:483bd8cc10c3d8a7a37fae36dfa5b21e239bd4ee121d91cad1f81bba10cfb0ed", size = 13235893 }, + { url = "https://files.pythonhosted.org/packages/53/4b/eda616e33f67129e5979a9eb33c710013caa3aa8a921991e6cc0b22cea33/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d1e80107bcf2bf1291acfc0bf0425dceb8890abe9f38d8e94e23497cbf7ee0d", size = 14178389 }, + { url = "https://files.pythonhosted.org/packages/6b/b5/b75527c0f9532dd8a93e8e7cd8e62e547b9f207d4c11e24f0006e8646b36/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a17e17eb8562660cc0d31bb55643a4da996a81944b82c54805c91b3fe66f4824", size = 15003435 }, + { url = "https://files.pythonhosted.org/packages/34/e3/49beb08ebccda3c21e871b607c1cb2f258c3fa0d2f609fed0a5ba741b92d/scikit_image-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:bdd2b8c1de0849964dbc54037f36b4e9420157e67e45a8709a80d727f52c7da2", size = 12899474 }, + { url = "https://files.pythonhosted.org/packages/e6/7c/9814dd1c637f7a0e44342985a76f95a55dd04be60154247679fd96c7169f/scikit_image-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da", size = 13921841 }, + { url = "https://files.pythonhosted.org/packages/84/06/66a2e7661d6f526740c309e9717d3bd07b473661d5cdddef4dd978edab25/scikit_image-0.25.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc", size = 13196862 }, + { url = "https://files.pythonhosted.org/packages/4e/63/3368902ed79305f74c2ca8c297dfeb4307269cbe6402412668e322837143/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341", size = 14117785 }, + { url = "https://files.pythonhosted.org/packages/cd/9b/c3da56a145f52cd61a68b8465d6a29d9503bc45bc993bb45e84371c97d94/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147", size = 14977119 }, + { url = "https://files.pythonhosted.org/packages/8a/97/5fcf332e1753831abb99a2525180d3fb0d70918d461ebda9873f66dcc12f/scikit_image-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f", size = 12885116 }, + { url = "https://files.pythonhosted.org/packages/10/cc/75e9f17e3670b5ed93c32456fda823333c6279b144cd93e2c03aa06aa472/scikit_image-0.25.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd", size = 13862801 }, +] + +[[package]] +name = "scikit-learn" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/a5/4ae3b3a0755f7b35a280ac90b28817d1f380318973cff14075ab41ef50d9/scikit_learn-1.6.1.tar.gz", hash = "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e", size = 7068312 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/2a/e291c29670795406a824567d1dfc91db7b699799a002fdaa452bceea8f6e/scikit_learn-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72abc587c75234935e97d09aa4913a82f7b03ee0b74111dcc2881cba3c5a7b33", size = 12102620 }, + { url = "https://files.pythonhosted.org/packages/25/92/ee1d7a00bb6b8c55755d4984fd82608603a3cc59959245068ce32e7fb808/scikit_learn-1.6.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b3b00cdc8f1317b5f33191df1386c0befd16625f49d979fe77a8d44cae82410d", size = 11116234 }, + { url = "https://files.pythonhosted.org/packages/30/cd/ed4399485ef364bb25f388ab438e3724e60dc218c547a407b6e90ccccaef/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc4765af3386811c3ca21638f63b9cf5ecf66261cc4815c1db3f1e7dc7b79db2", size = 12592155 }, + { url = "https://files.pythonhosted.org/packages/a8/f3/62fc9a5a659bb58a03cdd7e258956a5824bdc9b4bb3c5d932f55880be569/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25fc636bdaf1cc2f4a124a116312d837148b5e10872147bdaf4887926b8c03d8", size = 13497069 }, + { url = "https://files.pythonhosted.org/packages/a1/a6/c5b78606743a1f28eae8f11973de6613a5ee87366796583fb74c67d54939/scikit_learn-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:fa909b1a36e000a03c382aade0bd2063fd5680ff8b8e501660c0f59f021a6415", size = 11139809 }, + { url = "https://files.pythonhosted.org/packages/0a/18/c797c9b8c10380d05616db3bfb48e2a3358c767affd0857d56c2eb501caa/scikit_learn-1.6.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:926f207c804104677af4857b2c609940b743d04c4c35ce0ddc8ff4f053cddc1b", size = 12104516 }, + { url = "https://files.pythonhosted.org/packages/c4/b7/2e35f8e289ab70108f8cbb2e7a2208f0575dc704749721286519dcf35f6f/scikit_learn-1.6.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c2cae262064e6a9b77eee1c8e768fc46aa0b8338c6a8297b9b6759720ec0ff2", size = 11167837 }, + { url = "https://files.pythonhosted.org/packages/a4/f6/ff7beaeb644bcad72bcfd5a03ff36d32ee4e53a8b29a639f11bcb65d06cd/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1061b7c028a8663fb9a1a1baf9317b64a257fcb036dae5c8752b2abef31d136f", size = 12253728 }, + { url = "https://files.pythonhosted.org/packages/29/7a/8bce8968883e9465de20be15542f4c7e221952441727c4dad24d534c6d99/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e69fab4ebfc9c9b580a7a80111b43d214ab06250f8a7ef590a4edf72464dd86", size = 13147700 }, + { url = "https://files.pythonhosted.org/packages/62/27/585859e72e117fe861c2079bcba35591a84f801e21bc1ab85bce6ce60305/scikit_learn-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:70b1d7e85b1c96383f872a519b3375f92f14731e279a7b4c6cfd650cf5dffc52", size = 11110613 }, + { url = "https://files.pythonhosted.org/packages/2e/59/8eb1872ca87009bdcdb7f3cdc679ad557b992c12f4b61f9250659e592c63/scikit_learn-1.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ffa1e9e25b3d93990e74a4be2c2fc61ee5af85811562f1288d5d055880c4322", size = 12010001 }, + { url = "https://files.pythonhosted.org/packages/9d/05/f2fc4effc5b32e525408524c982c468c29d22f828834f0625c5ef3d601be/scikit_learn-1.6.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dc5cf3d68c5a20ad6d571584c0750ec641cc46aeef1c1507be51300e6003a7e1", size = 11096360 }, + { url = "https://files.pythonhosted.org/packages/c8/e4/4195d52cf4f113573fb8ebc44ed5a81bd511a92c0228889125fac2f4c3d1/scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c06beb2e839ecc641366000ca84f3cf6fa9faa1777e29cf0c04be6e4d096a348", size = 12209004 }, + { url = "https://files.pythonhosted.org/packages/94/be/47e16cdd1e7fcf97d95b3cb08bde1abb13e627861af427a3651fcb80b517/scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8ca8cb270fee8f1f76fa9bfd5c3507d60c6438bbee5687f81042e2bb98e5a97", size = 13171776 }, + { url = "https://files.pythonhosted.org/packages/34/b0/ca92b90859070a1487827dbc672f998da95ce83edce1270fc23f96f1f61a/scikit_learn-1.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:7a1c43c8ec9fde528d664d947dc4c0789be4077a3647f232869f41d9bf50e0fb", size = 11071865 }, + { url = "https://files.pythonhosted.org/packages/12/ae/993b0fb24a356e71e9a894e42b8a9eec528d4c70217353a1cd7a48bc25d4/scikit_learn-1.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a17c1dea1d56dcda2fac315712f3651a1fea86565b64b48fa1bc090249cbf236", size = 11955804 }, + { url = "https://files.pythonhosted.org/packages/d6/54/32fa2ee591af44507eac86406fa6bba968d1eb22831494470d0a2e4a1eb1/scikit_learn-1.6.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6a7aa5f9908f0f28f4edaa6963c0a6183f1911e63a69aa03782f0d924c830a35", size = 11100530 }, + { url = "https://files.pythonhosted.org/packages/3f/58/55856da1adec655bdce77b502e94a267bf40a8c0b89f8622837f89503b5a/scikit_learn-1.6.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0650e730afb87402baa88afbf31c07b84c98272622aaba002559b614600ca691", size = 12433852 }, + { url = "https://files.pythonhosted.org/packages/ff/4f/c83853af13901a574f8f13b645467285a48940f185b690936bb700a50863/scikit_learn-1.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:3f59fe08dc03ea158605170eb52b22a105f238a5d512c4470ddeca71feae8e5f", size = 11337256 }, +] + +[[package]] +name = "scikit-survival" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ecos" }, + { name = "joblib" }, + { name = "numexpr" }, + { name = "numpy" }, + { name = "osqp" }, + { name = "pandas" }, + { name = "scikit-learn" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/cb/0d3aa99c3997f22b0561900c06726c97a1f0799d17ed64fff98c7b1c2f58/scikit_survival-0.24.1.tar.gz", hash = "sha256:059ca5911f980e44f69951baf08efc8d7a7cf1adba7a5422580ef65330cfd88e", size = 2819926 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/42/19b84de0eb6af19cf6005d2a192d680eadc31aa15bbaf3435663fed6ebc3/scikit_survival-0.24.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:eb2902f6da4eb65751a0fe5efb539c0468e012e980c198f4bc956e7339c22996", size = 868890 }, + { url = "https://files.pythonhosted.org/packages/7e/18/4d29805f94e793799512f469a8e0cc97a3d4d73057c4052e62c0781d819a/scikit_survival-0.24.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51a624760bf88df0a469d363737394c1d45aedd9eaaeb0fedb76d09af8fa8b90", size = 842118 }, + { url = "https://files.pythonhosted.org/packages/73/f4/b474631b68a5a7fe799cc63dc30d9db5432d16e24ac760e6560aeb84aff9/scikit_survival-0.24.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d686d0c94c9ad919c32bd02ccacd89d6d7a296b9deab1f9b797bb670350856e6", size = 3924338 }, + { url = "https://files.pythonhosted.org/packages/cf/e4/8fbba4d983c619aff7235618600e9c32395b007d29db14973066a7e952ff/scikit_survival-0.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:bc77a649fe960f0f00aa09b6c13b125f5d893451775fe1cfd14f49697d7b885a", size = 826727 }, + { url = "https://files.pythonhosted.org/packages/3d/7e/a4f0f223246e52a91d2ca6f0a1283a4578160febbec2065ab128d4e3303d/scikit_survival-0.24.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4f44b6fd3147fa41771977317be990ff44c471513f0e42191db2b88248f7bd72", size = 876169 }, + { url = "https://files.pythonhosted.org/packages/c1/e4/5090bf732722b6190b79d8cf39a12bee40b1873a2d4f1d4427342ac0e377/scikit_survival-0.24.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9e0c690394ad58ff8d20f0525eb5eae93966ed9531f0956a105b66a7503e4ac4", size = 847521 }, + { url = "https://files.pythonhosted.org/packages/67/d2/8e3a115ff6adc5ace14b5cf32a2d8d77b32041ebeb2bad84fb82038dacbe/scikit_survival-0.24.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e06abac35705b471d950fa491bbb0f7473fe9005f753b2996819b231dc0380ec", size = 3885151 }, + { url = "https://files.pythonhosted.org/packages/01/34/6d6b2554c1d3c27371fa4ed8719f2d659802d11f333a26d0e7ee854e9197/scikit_survival-0.24.1-cp312-cp312-win_amd64.whl", hash = "sha256:62d8d37904d38a7140b8918557d55cc8d3bfe832fd39de63e723b0aad8b9bb21", size = 832557 }, + { url = "https://files.pythonhosted.org/packages/eb/03/a8fe9119ac7dbac03b7554ddeaf632680f7a7d005670a9f1f363ce847bf7/scikit_survival-0.24.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1a0126081f84b45804359fc0a68ad4c3a2dbee88656baa0b78fa304b52a03b23", size = 869938 }, + { url = "https://files.pythonhosted.org/packages/ab/d7/554c7fe38e52f1895f0bdfd2de96e4c17da55f23f7264cc1fab34c731a4d/scikit_survival-0.24.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1332d310d9bfed12932989c869e6e5757e4a20ab01a89f25b64eb83049d1098d", size = 841411 }, + { url = "https://files.pythonhosted.org/packages/30/3f/d6fa1a1f64cb5d09d33d476779809fed05d2971c5ffd07de4a471ad3f1a0/scikit_survival-0.24.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e978b79bc542e259cf75024b1b6381f2636122ebe440a64fd7aed7f8a9f1afc0", size = 3861630 }, + { url = "https://files.pythonhosted.org/packages/b9/76/ce24305c24ae89ceff0e8ed7305cf5542016cded3bbb57fc599a2d459028/scikit_survival-0.24.1-cp313-cp313-win_amd64.whl", hash = "sha256:4fa5fdb77c7902b90a353e04655245bd3b81dc980743b7f83ad68cb74ed63d83", size = 830658 }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255 }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035 }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499 }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602 }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415 }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622 }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796 }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684 }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504 }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735 }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284 }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958 }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454 }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199 }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455 }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140 }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549 }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184 }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256 }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540 }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115 }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884 }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018 }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716 }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342 }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869 }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851 }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011 }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407 }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030 }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709 }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045 }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062 }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132 }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503 }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097 }, +] + +[[package]] +name = "sentry-sdk" +version = "2.29.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/67/d552a5f8e5a6a56b2feea6529e2d8ccd54349084c84176d5a1f7295044bc/sentry_sdk-2.29.1.tar.gz", hash = "sha256:8d4a0206b95fa5fe85e5e7517ed662e3888374bdc342c00e435e10e6d831aa6d", size = 325518 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/e5/da07b0bd832cefd52d16f2b9bbbe31624d57552602c06631686b93ccb1bd/sentry_sdk-2.29.1-py2.py3-none-any.whl", hash = "sha256:90862fe0616ded4572da6c9dadb363121a1ae49a49e21c418f0634e9d10b4c19", size = 341553 }, +] + +[[package]] +name = "setproctitle" +version = "1.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/af/56efe21c53ac81ac87e000b15e60b3d8104224b4313b6eacac3597bd183d/setproctitle-1.3.6.tar.gz", hash = "sha256:c9f32b96c700bb384f33f7cf07954bb609d35dd82752cef57fb2ee0968409169", size = 26889 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/3b/8288d0cd969a63500dd62fc2c99ce6980f9909ccef0770ab1f86c361e0bf/setproctitle-1.3.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a1d856b0f4e4a33e31cdab5f50d0a14998f3a2d726a3fd5cb7c4d45a57b28d1b", size = 17412 }, + { url = "https://files.pythonhosted.org/packages/39/37/43a5a3e25ca1048dbbf4db0d88d346226f5f1acd131bb8e660f4bfe2799f/setproctitle-1.3.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:50706b9c0eda55f7de18695bfeead5f28b58aa42fd5219b3b1692d554ecbc9ec", size = 11963 }, + { url = "https://files.pythonhosted.org/packages/5b/47/f103c40e133154783c91a10ab08ac9fc410ed835aa85bcf7107cb882f505/setproctitle-1.3.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af188f3305f0a65c3217c30c6d4c06891e79144076a91e8b454f14256acc7279", size = 31718 }, + { url = "https://files.pythonhosted.org/packages/1f/13/7325dd1c008dd6c0ebd370ddb7505977054a87e406f142318e395031a792/setproctitle-1.3.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cce0ed8b3f64c71c140f0ec244e5fdf8ecf78ddf8d2e591d4a8b6aa1c1214235", size = 33027 }, + { url = "https://files.pythonhosted.org/packages/0c/0a/6075bfea05a71379d77af98a9ac61163e8b6e5ef1ae58cd2b05871b2079c/setproctitle-1.3.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70100e2087fe05359f249a0b5f393127b3a1819bf34dec3a3e0d4941138650c9", size = 30223 }, + { url = "https://files.pythonhosted.org/packages/cc/41/fbf57ec52f4f0776193bd94334a841f0bc9d17e745f89c7790f336420c65/setproctitle-1.3.6-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1065ed36bd03a3fd4186d6c6de5f19846650b015789f72e2dea2d77be99bdca1", size = 31204 }, + { url = "https://files.pythonhosted.org/packages/97/b5/f799fb7a00de29fb0ac1dfd015528dea425b9e31a8f1068a0b3df52d317f/setproctitle-1.3.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4adf6a0013fe4e0844e3ba7583ec203ca518b9394c6cc0d3354df2bf31d1c034", size = 31181 }, + { url = "https://files.pythonhosted.org/packages/b5/b7/81f101b612014ec61723436022c31146178813d6ca6b947f7b9c84e9daf4/setproctitle-1.3.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb7452849f6615871eabed6560ffedfe56bc8af31a823b6be4ce1e6ff0ab72c5", size = 30101 }, + { url = "https://files.pythonhosted.org/packages/67/23/681232eed7640eab96719daa8647cc99b639e3daff5c287bd270ef179a73/setproctitle-1.3.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a094b7ce455ca341b59a0f6ce6be2e11411ba6e2860b9aa3dbb37468f23338f4", size = 32438 }, + { url = "https://files.pythonhosted.org/packages/19/f8/4d075a7bdc3609ac71535b849775812455e4c40aedfbf0778a6f123b1774/setproctitle-1.3.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ad1c2c2baaba62823a7f348f469a967ece0062140ca39e7a48e4bbb1f20d54c4", size = 30625 }, + { url = "https://files.pythonhosted.org/packages/5f/73/a2a8259ebee166aee1ca53eead75de0e190b3ddca4f716e5c7470ebb7ef6/setproctitle-1.3.6-cp311-cp311-win32.whl", hash = "sha256:8050c01331135f77ec99d99307bfbc6519ea24d2f92964b06f3222a804a3ff1f", size = 11488 }, + { url = "https://files.pythonhosted.org/packages/c9/15/52cf5e1ff0727d53704cfdde2858eaf237ce523b0b04db65faa84ff83e13/setproctitle-1.3.6-cp311-cp311-win_amd64.whl", hash = "sha256:9b73cf0fe28009a04a35bb2522e4c5b5176cc148919431dcb73fdbdfaab15781", size = 12201 }, + { url = "https://files.pythonhosted.org/packages/8f/fb/99456fd94d4207c5f6c40746a048a33a52b4239cd7d9c8d4889e2210ec82/setproctitle-1.3.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:af44bb7a1af163806bbb679eb8432fa7b4fb6d83a5d403b541b675dcd3798638", size = 17399 }, + { url = "https://files.pythonhosted.org/packages/d5/48/9699191fe6062827683c43bfa9caac33a2c89f8781dd8c7253fa3dba85fd/setproctitle-1.3.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3cca16fd055316a48f0debfcbfb6af7cea715429fc31515ab3fcac05abd527d8", size = 11966 }, + { url = "https://files.pythonhosted.org/packages/33/03/b085d192b9ecb9c7ce6ad6ef30ecf4110b7f39430b58a56245569827fcf4/setproctitle-1.3.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea002088d5554fd75e619742cefc78b84a212ba21632e59931b3501f0cfc8f67", size = 32017 }, + { url = "https://files.pythonhosted.org/packages/ae/68/c53162e645816f97212002111420d1b2f75bf6d02632e37e961dc2cd6d8b/setproctitle-1.3.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb465dd5825356c1191a038a86ee1b8166e3562d6e8add95eec04ab484cfb8a2", size = 33419 }, + { url = "https://files.pythonhosted.org/packages/ac/0d/119a45d15a816a6cf5ccc61b19729f82620095b27a47e0a6838216a95fae/setproctitle-1.3.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2c8e20487b3b73c1fa72c56f5c89430617296cd380373e7af3a538a82d4cd6d", size = 30711 }, + { url = "https://files.pythonhosted.org/packages/e3/fb/5e9b5068df9e9f31a722a775a5e8322a29a638eaaa3eac5ea7f0b35e6314/setproctitle-1.3.6-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d6252098e98129a1decb59b46920d4eca17b0395f3d71b0d327d086fefe77d", size = 31742 }, + { url = "https://files.pythonhosted.org/packages/35/88/54de1e73e8fce87d587889c7eedb48fc4ee2bbe4e4ca6331690d03024f86/setproctitle-1.3.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cf355fbf0d4275d86f9f57be705d8e5eaa7f8ddb12b24ced2ea6cbd68fdb14dc", size = 31925 }, + { url = "https://files.pythonhosted.org/packages/f3/01/65948d7badd66e63e3db247b923143da142790fa293830fdecf832712c2d/setproctitle-1.3.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e288f8a162d663916060beb5e8165a8551312b08efee9cf68302687471a6545d", size = 30981 }, + { url = "https://files.pythonhosted.org/packages/22/20/c495e61786f1d38d5dc340b9d9077fee9be3dfc7e89f515afe12e1526dbc/setproctitle-1.3.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b2e54f4a2dc6edf0f5ea5b1d0a608d2af3dcb5aa8c8eeab9c8841b23e1b054fe", size = 33209 }, + { url = "https://files.pythonhosted.org/packages/98/3f/a457b8550fbd34d5b482fe20b8376b529e76bf1fbf9a474a6d9a641ab4ad/setproctitle-1.3.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b6f4abde9a2946f57e8daaf1160b2351bcf64274ef539e6675c1d945dbd75e2a", size = 31587 }, + { url = "https://files.pythonhosted.org/packages/44/fe/743517340e5a635e3f1c4310baea20c16c66202f96a6f4cead222ffd6d84/setproctitle-1.3.6-cp312-cp312-win32.whl", hash = "sha256:db608db98ccc21248370d30044a60843b3f0f3d34781ceeea67067c508cd5a28", size = 11487 }, + { url = "https://files.pythonhosted.org/packages/60/9a/d88f1c1f0f4efff1bd29d9233583ee341114dda7d9613941453984849674/setproctitle-1.3.6-cp312-cp312-win_amd64.whl", hash = "sha256:082413db8a96b1f021088e8ec23f0a61fec352e649aba20881895815388b66d3", size = 12208 }, + { url = "https://files.pythonhosted.org/packages/89/76/f1a2fdbf9b9602945a7489ba5c52e9863de37381ef1a85a2b9ed0ff8bc79/setproctitle-1.3.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e2a9e62647dc040a76d55563580bf3bb8fe1f5b6ead08447c2ed0d7786e5e794", size = 17392 }, + { url = "https://files.pythonhosted.org/packages/5c/5b/4e0db8b10b4543afcb3dbc0827793d46e43ec1de6b377e313af3703d08e0/setproctitle-1.3.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:751ba352ed922e0af60458e961167fa7b732ac31c0ddd1476a2dfd30ab5958c5", size = 11951 }, + { url = "https://files.pythonhosted.org/packages/dc/fe/d5d00aaa700fe1f6160b6e95c225b29c01f4d9292176d48fd968815163ea/setproctitle-1.3.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7890e291bf4708e3b61db9069ea39b3ab0651e42923a5e1f4d78a7b9e4b18301", size = 32087 }, + { url = "https://files.pythonhosted.org/packages/9f/b3/894b827b93ef813c082479bebf88185860f01ac243df737823dd705e7fff/setproctitle-1.3.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2b17855ed7f994f3f259cf2dfbfad78814538536fa1a91b50253d84d87fd88d", size = 33502 }, + { url = "https://files.pythonhosted.org/packages/b2/cd/5330734cca1a4cfcb721432c22cb7899ff15a4101ba868b2ef452ffafea1/setproctitle-1.3.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e51ec673513465663008ce402171192a053564865c2fc6dc840620871a9bd7c", size = 30713 }, + { url = "https://files.pythonhosted.org/packages/fa/d3/c2590c5daa2e9a008d3f2b16c0f4a351826193be55f147cb32af49c6d814/setproctitle-1.3.6-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63cc10352dc6cf35a33951656aa660d99f25f574eb78132ce41a85001a638aa7", size = 31792 }, + { url = "https://files.pythonhosted.org/packages/e6/b1/c553ed5af8cfcecd5ae7737e63af58a17a03d26f3d61868c7eb20bf7e3cf/setproctitle-1.3.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0dba8faee2e4a96e934797c9f0f2d093f8239bf210406a99060b3eabe549628e", size = 31927 }, + { url = "https://files.pythonhosted.org/packages/70/78/2d5385206540127a3dca0ff83225b1ac66873f5cc89d4a6d3806c92f5ae2/setproctitle-1.3.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e3e44d08b61de0dd6f205528498f834a51a5c06689f8fb182fe26f3a3ce7dca9", size = 30981 }, + { url = "https://files.pythonhosted.org/packages/31/62/e3e4a4e006d0e549748e53cded4ff3b667be0602860fc61b7de8b412b667/setproctitle-1.3.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:de004939fc3fd0c1200d26ea9264350bfe501ffbf46c8cf5dc7f345f2d87a7f1", size = 33244 }, + { url = "https://files.pythonhosted.org/packages/aa/05/4b223fd4ef94e105dc7aff27fa502fb7200cf52be2bb0c064bd2406b5611/setproctitle-1.3.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3f8194b4d631b003a1176a75d1acd545e04b1f54b821638e098a93e6e62830ef", size = 31630 }, + { url = "https://files.pythonhosted.org/packages/1b/ba/5f68eb969f7336f54b54a599fd3ffbd7662f9733b080bc8598705971b3dd/setproctitle-1.3.6-cp313-cp313-win32.whl", hash = "sha256:d714e002dd3638170fe7376dc1b686dbac9cb712cde3f7224440af722cc9866a", size = 11480 }, + { url = "https://files.pythonhosted.org/packages/ba/f5/7f47f0ca35c9c357f16187cee9229f3eda0237bc6fdd3061441336f361c0/setproctitle-1.3.6-cp313-cp313-win_amd64.whl", hash = "sha256:b70c07409d465f3a8b34d52f863871fb8a00755370791d2bd1d4f82b3cdaf3d5", size = 12198 }, + { url = "https://files.pythonhosted.org/packages/39/ad/c3941b8fc6b32a976c9e2d9615a90ae793b69cd010ca8c3575dbc822104f/setproctitle-1.3.6-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:23a57d3b8f1549515c2dbe4a2880ebc1f27780dc126c5e064167563e015817f5", size = 17401 }, + { url = "https://files.pythonhosted.org/packages/04/38/a184f857b988d3a9c401e470a4e38182a5c99ee77bf90432d7665e9d35a3/setproctitle-1.3.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:81c443310831e29fabbd07b75ebbfa29d0740b56f5907c6af218482d51260431", size = 11959 }, + { url = "https://files.pythonhosted.org/packages/b7/b9/4878ef9d8483adfd1edf6bf95151362aaec0d05aac306a97ff0383f491b5/setproctitle-1.3.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d88c63bd395c787b0aa81d8bbc22c1809f311032ce3e823a6517b711129818e4", size = 33463 }, + { url = "https://files.pythonhosted.org/packages/cc/60/3ef49d1931aff2a36a7324a49cca10d77ef03e0278452fd468c33a52d7e3/setproctitle-1.3.6-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73f14b86d0e2858ece6bf5807c9889670e392c001d414b4293d0d9b291942c3", size = 34959 }, + { url = "https://files.pythonhosted.org/packages/81/c6/dee0a973acecefb0db6c9c2e0ea7f18b7e4db773a72e534741ebdee8bbb8/setproctitle-1.3.6-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3393859eb8f19f5804049a685bf286cb08d447e28ba5c6d8543c7bf5500d5970", size = 32055 }, + { url = "https://files.pythonhosted.org/packages/ea/a5/5dd5c4192cf18d16349a32a07f728a9a48a2a05178e16966cabd6645903e/setproctitle-1.3.6-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:785cd210c0311d9be28a70e281a914486d62bfd44ac926fcd70cf0b4d65dff1c", size = 32986 }, + { url = "https://files.pythonhosted.org/packages/df/a6/1508d37eb8008670d33f13fcdb91cbd8ef54697276469abbfdd3d4428c59/setproctitle-1.3.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c051f46ed1e13ba8214b334cbf21902102807582fbfaf0fef341b9e52f0fafbf", size = 32736 }, + { url = "https://files.pythonhosted.org/packages/1a/73/c84ec8880d543766a12fcd6b65dbd013770974a40577889f357409b0441e/setproctitle-1.3.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:49498ebf68ca3e75321ffe634fcea5cc720502bfaa79bd6b03ded92ce0dc3c24", size = 31945 }, + { url = "https://files.pythonhosted.org/packages/95/0a/126b9ff7a406a69a62825fe5bd6d1ba8671919a7018c4f9e2c63f49bfcb6/setproctitle-1.3.6-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4431629c178193f23c538cb1de3da285a99ccc86b20ee91d81eb5f1a80e0d2ba", size = 34333 }, + { url = "https://files.pythonhosted.org/packages/9a/fd/5474b04f1c013ff460129d2bc774557dd6e186da4667865efef9a83bf378/setproctitle-1.3.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d136fbf8ad4321716e44d6d6b3d8dffb4872626010884e07a1db54b7450836cf", size = 32508 }, + { url = "https://files.pythonhosted.org/packages/32/21/2503e38520cb076a7ecaef6a35d6a6fa89cf02af3541c84c811fd7500d20/setproctitle-1.3.6-cp313-cp313t-win32.whl", hash = "sha256:d483cc23cc56ab32911ea0baa0d2d9ea7aa065987f47de847a0a93a58bf57905", size = 11482 }, + { url = "https://files.pythonhosted.org/packages/65/23/7833d75a27fba25ddc5cd3b54cd03c4bf8e18b8e2dbec622eb6326278ce8/setproctitle-1.3.6-cp313-cp313t-win_amd64.whl", hash = "sha256:74973aebea3543ad033b9103db30579ec2b950a466e09f9c2180089e8346e0ec", size = 12209 }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486 }, +] + +[[package]] +name = "shapely" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/3c/2da625233f4e605155926566c0e7ea8dda361877f48e8b1655e53456f252/shapely-2.1.1.tar.gz", hash = "sha256:500621967f2ffe9642454808009044c21e5b35db89ce69f8a2042c2ffd0e2772", size = 315422 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/2df985b1e03f90c503796ad5ecd3d9ed305123b64d4ccb54616b30295b29/shapely-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:587a1aa72bc858fab9b8c20427b5f6027b7cbc92743b8e2c73b9de55aa71c7a7", size = 1819368 }, + { url = "https://files.pythonhosted.org/packages/56/17/504518860370f0a28908b18864f43d72f03581e2b6680540ca668f07aa42/shapely-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9fa5c53b0791a4b998f9ad84aad456c988600757a96b0a05e14bba10cebaaaea", size = 1625362 }, + { url = "https://files.pythonhosted.org/packages/36/a1/9677337d729b79fce1ef3296aac6b8ef4743419086f669e8a8070eff8f40/shapely-2.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aabecd038841ab5310d23495253f01c2a82a3aedae5ab9ca489be214aa458aa7", size = 2999005 }, + { url = "https://files.pythonhosted.org/packages/a2/17/e09357274699c6e012bbb5a8ea14765a4d5860bb658df1931c9f90d53bd3/shapely-2.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586f6aee1edec04e16227517a866df3e9a2e43c1f635efc32978bb3dc9c63753", size = 3108489 }, + { url = "https://files.pythonhosted.org/packages/17/5d/93a6c37c4b4e9955ad40834f42b17260ca74ecf36df2e81bb14d12221b90/shapely-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b9878b9e37ad26c72aada8de0c9cfe418d9e2ff36992a1693b7f65a075b28647", size = 3945727 }, + { url = "https://files.pythonhosted.org/packages/a3/1a/ad696648f16fd82dd6bfcca0b3b8fbafa7aacc13431c7fc4c9b49e481681/shapely-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9a531c48f289ba355e37b134e98e28c557ff13965d4653a5228d0f42a09aed0", size = 4109311 }, + { url = "https://files.pythonhosted.org/packages/d4/38/150dd245beab179ec0d4472bf6799bf18f21b1efbef59ac87de3377dbf1c/shapely-2.1.1-cp311-cp311-win32.whl", hash = "sha256:4866de2673a971820c75c0167b1f1cd8fb76f2d641101c23d3ca021ad0449bab", size = 1522982 }, + { url = "https://files.pythonhosted.org/packages/93/5b/842022c00fbb051083c1c85430f3bb55565b7fd2d775f4f398c0ba8052ce/shapely-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:20a9d79958b3d6c70d8a886b250047ea32ff40489d7abb47d01498c704557a93", size = 1703872 }, + { url = "https://files.pythonhosted.org/packages/fb/64/9544dc07dfe80a2d489060791300827c941c451e2910f7364b19607ea352/shapely-2.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2827365b58bf98efb60affc94a8e01c56dd1995a80aabe4b701465d86dcbba43", size = 1833021 }, + { url = "https://files.pythonhosted.org/packages/07/aa/fb5f545e72e89b6a0f04a0effda144f5be956c9c312c7d4e00dfddbddbcf/shapely-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9c551f7fa7f1e917af2347fe983f21f212863f1d04f08eece01e9c275903fad", size = 1643018 }, + { url = "https://files.pythonhosted.org/packages/03/46/61e03edba81de729f09d880ce7ae5c1af873a0814206bbfb4402ab5c3388/shapely-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78dec4d4fbe7b1db8dc36de3031767e7ece5911fb7782bc9e95c5cdec58fb1e9", size = 2986417 }, + { url = "https://files.pythonhosted.org/packages/1f/1e/83ec268ab8254a446b4178b45616ab5822d7b9d2b7eb6e27cf0b82f45601/shapely-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:872d3c0a7b8b37da0e23d80496ec5973c4692920b90de9f502b5beb994bbaaef", size = 3098224 }, + { url = "https://files.pythonhosted.org/packages/f1/44/0c21e7717c243e067c9ef8fa9126de24239f8345a5bba9280f7bb9935959/shapely-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2e2b9125ebfbc28ecf5353511de62f75a8515ae9470521c9a693e4bb9fbe0cf1", size = 3925982 }, + { url = "https://files.pythonhosted.org/packages/15/50/d3b4e15fefc103a0eb13d83bad5f65cd6e07a5d8b2ae920e767932a247d1/shapely-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4b96cea171b3d7f6786976a0520f178c42792897653ecca0c5422fb1e6946e6d", size = 4089122 }, + { url = "https://files.pythonhosted.org/packages/bd/05/9a68f27fc6110baeedeeebc14fd86e73fa38738c5b741302408fb6355577/shapely-2.1.1-cp312-cp312-win32.whl", hash = "sha256:39dca52201e02996df02e447f729da97cfb6ff41a03cb50f5547f19d02905af8", size = 1522437 }, + { url = "https://files.pythonhosted.org/packages/bc/e9/a4560e12b9338842a1f82c9016d2543eaa084fce30a1ca11991143086b57/shapely-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:13d643256f81d55a50013eff6321142781cf777eb6a9e207c2c9e6315ba6044a", size = 1703479 }, + { url = "https://files.pythonhosted.org/packages/71/8e/2bc836437f4b84d62efc1faddce0d4e023a5d990bbddd3c78b2004ebc246/shapely-2.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3004a644d9e89e26c20286d5fdc10f41b1744c48ce910bd1867fdff963fe6c48", size = 1832107 }, + { url = "https://files.pythonhosted.org/packages/12/a2/12c7cae5b62d5d851c2db836eadd0986f63918a91976495861f7c492f4a9/shapely-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1415146fa12d80a47d13cfad5310b3c8b9c2aa8c14a0c845c9d3d75e77cb54f6", size = 1642355 }, + { url = "https://files.pythonhosted.org/packages/5b/7e/6d28b43d53fea56de69c744e34c2b999ed4042f7a811dc1bceb876071c95/shapely-2.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21fcab88b7520820ec16d09d6bea68652ca13993c84dffc6129dc3607c95594c", size = 2968871 }, + { url = "https://files.pythonhosted.org/packages/dd/87/1017c31e52370b2b79e4d29e07cbb590ab9e5e58cf7e2bdfe363765d6251/shapely-2.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5ce6a5cc52c974b291237a96c08c5592e50f066871704fb5b12be2639d9026a", size = 3080830 }, + { url = "https://files.pythonhosted.org/packages/1d/fe/f4a03d81abd96a6ce31c49cd8aaba970eaaa98e191bd1e4d43041e57ae5a/shapely-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:04e4c12a45a1d70aeb266618d8cf81a2de9c4df511b63e105b90bfdfb52146de", size = 3908961 }, + { url = "https://files.pythonhosted.org/packages/ef/59/7605289a95a6844056a2017ab36d9b0cb9d6a3c3b5317c1f968c193031c9/shapely-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6ca74d851ca5264aae16c2b47e96735579686cb69fa93c4078070a0ec845b8d8", size = 4079623 }, + { url = "https://files.pythonhosted.org/packages/bc/4d/9fea036eff2ef4059d30247128b2d67aaa5f0b25e9fc27e1d15cc1b84704/shapely-2.1.1-cp313-cp313-win32.whl", hash = "sha256:fd9130501bf42ffb7e0695b9ea17a27ae8ce68d50b56b6941c7f9b3d3453bc52", size = 1521916 }, + { url = "https://files.pythonhosted.org/packages/12/d9/6d13b8957a17c95794f0c4dfb65ecd0957e6c7131a56ce18d135c1107a52/shapely-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:ab8d878687b438a2f4c138ed1a80941c6ab0029e0f4c785ecfe114413b498a97", size = 1702746 }, + { url = "https://files.pythonhosted.org/packages/60/36/b1452e3e7f35f5f6454d96f3be6e2bb87082720ff6c9437ecc215fa79be0/shapely-2.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0c062384316a47f776305ed2fa22182717508ffdeb4a56d0ff4087a77b2a0f6d", size = 1833482 }, + { url = "https://files.pythonhosted.org/packages/ce/ca/8e6f59be0718893eb3e478141285796a923636dc8f086f83e5b0ec0036d0/shapely-2.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4ecf6c196b896e8f1360cc219ed4eee1c1e5f5883e505d449f263bd053fb8c05", size = 1642256 }, + { url = "https://files.pythonhosted.org/packages/ab/78/0053aea449bb1d4503999525fec6232f049abcdc8df60d290416110de943/shapely-2.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb00070b4c4860f6743c600285109c273cca5241e970ad56bb87bef0be1ea3a0", size = 3016614 }, + { url = "https://files.pythonhosted.org/packages/ee/53/36f1b1de1dfafd1b457dcbafa785b298ce1b8a3e7026b79619e708a245d5/shapely-2.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d14a9afa5fa980fbe7bf63706fdfb8ff588f638f145a1d9dbc18374b5b7de913", size = 3093542 }, + { url = "https://files.pythonhosted.org/packages/b9/bf/0619f37ceec6b924d84427c88835b61f27f43560239936ff88915c37da19/shapely-2.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b640e390dabde790e3fb947198b466e63223e0a9ccd787da5f07bcb14756c28d", size = 3945961 }, + { url = "https://files.pythonhosted.org/packages/93/c9/20ca4afeb572763b07a7997f00854cb9499df6af85929e93012b189d8917/shapely-2.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:69e08bf9697c1b73ec6aa70437db922bafcea7baca131c90c26d59491a9760f9", size = 4089514 }, + { url = "https://files.pythonhosted.org/packages/33/6a/27036a5a560b80012a544366bceafd491e8abb94a8db14047b5346b5a749/shapely-2.1.1-cp313-cp313t-win32.whl", hash = "sha256:ef2d09d5a964cc90c2c18b03566cf918a61c248596998a0301d5b632beadb9db", size = 1540607 }, + { url = "https://files.pythonhosted.org/packages/ea/f1/5e9b3ba5c7aa7ebfaf269657e728067d16a7c99401c7973ddf5f0cf121bd/shapely-2.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8cb8f17c377260452e9d7720eeaf59082c5f8ea48cf104524d953e5d36d4bdb7", size = 1723061 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, +] + +[[package]] +name = "soupsieve" +version = "2.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677 }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 }, +] + +[[package]] +name = "stamp" +version = "2.1.0" +source = { editable = "." } +dependencies = [ + { name = "beartype" }, + { name = "einops" }, + { name = "h5py" }, + { name = "jaxtyping" }, + { name = "lightning" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "opencv-python" }, + { name = "openpyxl" }, + { name = "openslide-bin" }, + { name = "openslide-python" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "timm" }, + { name = "torch" }, + { name = "torchmetrics" }, + { name = "torchvision" }, + { name = "tqdm" }, +] + +[package.optional-dependencies] +all = [ + { name = "conch" }, + { name = "einops-exts" }, + { name = "environs" }, + { name = "gdown" }, + { name = "gigapath" }, + { name = "huggingface-hub" }, + { name = "madeleine" }, + { name = "musk" }, + { name = "sacremoses" }, + { name = "torch" }, + { name = "transformers" }, + { name = "uni" }, +] +chief-ctranspath = [ + { name = "gdown" }, + { name = "torch" }, +] +cobra = [ + { name = "cobra" }, + { name = "jinja2" }, +] +conch = [ + { name = "conch" }, + { name = "huggingface-hub" }, +] +conch1-5 = [ + { name = "einops-exts" }, + { name = "transformers" }, +] +ctranspath = [ + { name = "gdown" }, +] +gigapath = [ + { name = "gigapath" }, +] +madeleine = [ + { name = "madeleine" }, +] +musk = [ + { name = "musk" }, +] +plip = [ + { name = "transformers" }, +] +prism = [ + { name = "environs" }, + { name = "sacremoses" }, +] +uni = [ + { name = "huggingface-hub" }, + { name = "uni" }, +] +virchow2 = [ + { name = "huggingface-hub" }, + { name = "torch" }, +] + +[package.dev-dependencies] +dev = [ + { name = "huggingface-hub" }, + { name = "ipykernel" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "beartype", specifier = ">=0.19.0" }, + { name = "cobra", marker = "extra == 'cobra'", git = "https://github.com/KatherLab/COBRA.git?rev=f1a576e1133330ffc2d1df6ee110701921c7b7c9#f1a576e1133330ffc2d1df6ee110701921c7b7c9" }, + { name = "conch", marker = "extra == 'conch'", git = "https://github.com/Mahmoodlab/CONCH.git?rev=02d6ac59cc20874bff0f581de258c2b257f69a84#02d6ac59cc20874bff0f581de258c2b257f69a84" }, + { name = "einops", specifier = ">=0.8.0" }, + { name = "einops-exts", marker = "extra == 'conch1-5'", specifier = "==0.0.4" }, + { name = "environs", marker = "extra == 'prism'", specifier = "==11.0.0" }, + { name = "gdown", marker = "extra == 'chief-ctranspath'", specifier = ">=5.2.0" }, + { name = "gdown", marker = "extra == 'ctranspath'", specifier = ">=5.2.0" }, + { name = "gigapath", marker = "extra == 'gigapath'", git = "https://github.com/EzicStar/prov-gigapath.git?rev=d4cf55321df37aaf867e24a31c61bcf490a296eb#d4cf55321df37aaf867e24a31c61bcf490a296eb" }, + { name = "h5py", specifier = ">=3.12.1" }, + { name = "huggingface-hub", marker = "extra == 'conch'", specifier = ">=0.26.2" }, + { name = "huggingface-hub", marker = "extra == 'uni'", specifier = ">=0.26.2" }, + { name = "huggingface-hub", marker = "extra == 'virchow2'", specifier = ">=0.27.1" }, + { name = "jaxtyping", specifier = ">=0.2.36" }, + { name = "jinja2", marker = "extra == 'cobra'", specifier = ">=3.1.4" }, + { name = "lightning", specifier = ">=2.4.0" }, + { name = "madeleine", marker = "extra == 'madeleine'", git = "https://github.com/mahmoodlab/MADELEINE.git?rev=de7c85acc2bdad352e6df8eee5694f8b6f288012#de7c85acc2bdad352e6df8eee5694f8b6f288012" }, + { name = "matplotlib", specifier = ">=3.9.2" }, + { name = "musk", marker = "extra == 'musk'", git = "https://github.com/lilab-stanford/MUSK.git?rev=e1699c27687f44bbf6d4adfcbb2abe89795d347f#e1699c27687f44bbf6d4adfcbb2abe89795d347f" }, + { name = "numpy", specifier = ">=2.2.2" }, + { name = "opencv-python", specifier = ">=4.10.0.84" }, + { name = "openpyxl", specifier = ">=3.1.5" }, + { name = "openslide-bin", specifier = ">=4.0.0.6" }, + { name = "openslide-python", specifier = ">=1.4.1" }, + { name = "packaging", specifier = ">=24.2" }, + { name = "pandas", specifier = ">=2.2.3" }, + { name = "pillow", specifier = ">=11.1.0" }, + { name = "pydantic", specifier = ">=2.10.3" }, + { name = "pyyaml", specifier = ">=6.0.2" }, + { name = "sacremoses", marker = "extra == 'prism'", specifier = "==0.1.1" }, + { name = "scikit-learn", specifier = ">=1.5.2" }, + { name = "scipy", specifier = ">=1.15.1" }, + { name = "stamp", extras = ["conch", "ctranspath", "uni", "virchow2", "chief-ctranspath", "conch1-5", "gigapath", "prism", "madeleine", "musk", "plip"], marker = "extra == 'all'" }, + { name = "timm", specifier = ">=0.9.11" }, + { name = "torch", specifier = ">=2.5.1" }, + { name = "torch", marker = "extra == 'chief-ctranspath'", specifier = ">=2.0.0" }, + { name = "torch", marker = "extra == 'virchow2'", specifier = ">=2.0.0" }, + { name = "torchmetrics", specifier = ">=1.6.0" }, + { name = "torchvision", specifier = ">=0.20.1" }, + { name = "tqdm", specifier = ">=4.66.6" }, + { name = "transformers", marker = "extra == 'conch1-5'", specifier = ">=4.45.2" }, + { name = "transformers", marker = "extra == 'plip'", specifier = ">=4.45.2" }, + { name = "uni", marker = "extra == 'uni'", git = "https://github.com/mahmoodlab/UNI.git" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "huggingface-hub", specifier = ">=0.27.1" }, + { name = "ipykernel", specifier = ">=6.29.5" }, + { name = "pyright", specifier = ">=1.1.389,!=1.1.391" }, + { name = "pytest", specifier = ">=8.3.4" }, + { name = "ruff", specifier = ">=0.8.1" }, +] + +[[package]] +name = "sympy" +version = "1.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/99/5a5b6f19ff9f083671ddf7b9632028436167cd3d33e11015754e41b249a4/sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f", size = 7533040 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/fe/81695a1aa331a842b582453b605175f419fe8540355886031328089d840a/sympy-1.13.1-py3-none-any.whl", hash = "sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8", size = 6189177 }, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252 }, +] + +[[package]] +name = "termcolor" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/6c/3d75c196ac07ac8749600b60b03f4f6094d54e132c4d94ebac6ee0e0add0/termcolor-3.1.0.tar.gz", hash = "sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970", size = 14324 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/bd/de8d508070629b6d84a30d01d57e4a65c69aa7f5abe7560b8fad3b50ea59/termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa", size = 7684 }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638 }, +] + +[[package]] +name = "tifffile" +version = "2025.6.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/9e/636e3e433c24da41dd639e0520db60750dbf5e938d023b83af8097382ea3/tifffile-2025.6.11.tar.gz", hash = "sha256:0ece4c2e7a10656957d568a093b07513c0728d30c1bd8cc12725901fffdb7143", size = 370125 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/d8/1ba8f32bfc9cb69e37edeca93738e883f478fbe84ae401f72c0d8d507841/tifffile-2025.6.11-py3-none-any.whl", hash = "sha256:32effb78b10b3a283eb92d4ebf844ae7e93e151458b0412f38518b4e6d2d7542", size = 230800 }, +] + +[[package]] +name = "timm" +version = "0.9.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, + { name = "torchvision" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/f5/c0306adbd9ffa41b80d0b564b498794356e4986f064351e1ea0d55f2b60f/timm-0.9.11.tar.gz", hash = "sha256:728b433eb4cf0bad0c22a037058388fe7c94662f9b9f7d9f5f7dbe627b41c7fc", size = 2127616 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/aa/4b54f6047c442883243f68f6f9e3a0ab77aaae4b3e6e51a98b371e73dd77/timm-0.9.11-py3-none-any.whl", hash = "sha256:02bba56786633ff46b55ee0ce3b991fa85375556844e500ad18e6b12921dc3da", size = 2231391 }, +] + +[[package]] +name = "tokenizers" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/76/5ac0c97f1117b91b7eb7323dcd61af80d72f790b4df71249a7850c195f30/tokenizers-0.21.1.tar.gz", hash = "sha256:a1bb04dc5b448985f86ecd4b05407f5a8d97cb2c0532199b2a302a604a0165ab", size = 343256 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/1f/328aee25f9115bf04262e8b4e5a2050b7b7cf44b59c74e982db7270c7f30/tokenizers-0.21.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e78e413e9e668ad790a29456e677d9d3aa50a9ad311a40905d6861ba7692cf41", size = 2780767 }, + { url = "https://files.pythonhosted.org/packages/ae/1a/4526797f3719b0287853f12c5ad563a9be09d446c44ac784cdd7c50f76ab/tokenizers-0.21.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:cd51cd0a91ecc801633829fcd1fda9cf8682ed3477c6243b9a095539de4aecf3", size = 2650555 }, + { url = "https://files.pythonhosted.org/packages/4d/7a/a209b29f971a9fdc1da86f917fe4524564924db50d13f0724feed37b2a4d/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28da6b72d4fb14ee200a1bd386ff74ade8992d7f725f2bde2c495a9a98cf4d9f", size = 2937541 }, + { url = "https://files.pythonhosted.org/packages/3c/1e/b788b50ffc6191e0b1fc2b0d49df8cff16fe415302e5ceb89f619d12c5bc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34d8cfde551c9916cb92014e040806122295a6800914bab5865deb85623931cf", size = 2819058 }, + { url = "https://files.pythonhosted.org/packages/36/aa/3626dfa09a0ecc5b57a8c58eeaeb7dd7ca9a37ad9dd681edab5acd55764c/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaa852d23e125b73d283c98f007e06d4595732104b65402f46e8ef24b588d9f8", size = 3133278 }, + { url = "https://files.pythonhosted.org/packages/a4/4d/8fbc203838b3d26269f944a89459d94c858f5b3f9a9b6ee9728cdcf69161/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a21a15d5c8e603331b8a59548bbe113564136dc0f5ad8306dd5033459a226da0", size = 3144253 }, + { url = "https://files.pythonhosted.org/packages/d8/1b/2bd062adeb7c7511b847b32e356024980c0ffcf35f28947792c2d8ad2288/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdbd4c067c60a0ac7eca14b6bd18a5bebace54eb757c706b47ea93204f7a37c", size = 3398225 }, + { url = "https://files.pythonhosted.org/packages/8a/63/38be071b0c8e06840bc6046991636bcb30c27f6bb1e670f4f4bc87cf49cc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd9a0061e403546f7377df940e866c3e678d7d4e9643d0461ea442b4f89e61a", size = 3038874 }, + { url = "https://files.pythonhosted.org/packages/ec/83/afa94193c09246417c23a3c75a8a0a96bf44ab5630a3015538d0c316dd4b/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:db9484aeb2e200c43b915a1a0150ea885e35f357a5a8fabf7373af333dcc8dbf", size = 9014448 }, + { url = "https://files.pythonhosted.org/packages/ae/b3/0e1a37d4f84c0f014d43701c11eb8072704f6efe8d8fc2dcdb79c47d76de/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed248ab5279e601a30a4d67bdb897ecbe955a50f1e7bb62bd99f07dd11c2f5b6", size = 8937877 }, + { url = "https://files.pythonhosted.org/packages/ac/33/ff08f50e6d615eb180a4a328c65907feb6ded0b8f990ec923969759dc379/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:9ac78b12e541d4ce67b4dfd970e44c060a2147b9b2a21f509566d556a509c67d", size = 9186645 }, + { url = "https://files.pythonhosted.org/packages/5f/aa/8ae85f69a9f6012c6f8011c6f4aa1c96154c816e9eea2e1b758601157833/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e5a69c1a4496b81a5ee5d2c1f3f7fbdf95e90a0196101b0ee89ed9956b8a168f", size = 9384380 }, + { url = "https://files.pythonhosted.org/packages/e8/5b/a5d98c89f747455e8b7a9504910c865d5e51da55e825a7ae641fb5ff0a58/tokenizers-0.21.1-cp39-abi3-win32.whl", hash = "sha256:1039a3a5734944e09de1d48761ade94e00d0fa760c0e0551151d4dd851ba63e3", size = 2239506 }, + { url = "https://files.pythonhosted.org/packages/e6/b6/072a8e053ae600dcc2ac0da81a23548e3b523301a442a6ca900e92ac35be/tokenizers-0.21.1-cp39-abi3-win_amd64.whl", hash = "sha256:0f0dcbcc9f6e13e675a66d7a5f2f225a736745ce484c1a4e07476a89ccdad382", size = 2435481 }, +] + +[[package]] +name = "torch" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools", marker = "python_full_version >= '3.12'" }, + { name = "sympy" }, + { name = "triton", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/a9/97cbbc97002fff0de394a2da2cdfa859481fdca36996d7bd845d50aa9d8d/torch-2.6.0-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:7979834102cd5b7a43cc64e87f2f3b14bd0e1458f06e9f88ffa386d07c7446e1", size = 766715424 }, + { url = "https://files.pythonhosted.org/packages/6d/fa/134ce8f8a7ea07f09588c9cc2cea0d69249efab977707cf67669431dcf5c/torch-2.6.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:ccbd0320411fe1a3b3fec7b4d3185aa7d0c52adac94480ab024b5c8f74a0bf1d", size = 95759416 }, + { url = "https://files.pythonhosted.org/packages/11/c5/2370d96b31eb1841c3a0883a492c15278a6718ccad61bb6a649c80d1d9eb/torch-2.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:46763dcb051180ce1ed23d1891d9b1598e07d051ce4c9d14307029809c4d64f7", size = 204164970 }, + { url = "https://files.pythonhosted.org/packages/0b/fa/f33a4148c6fb46ca2a3f8de39c24d473822d5774d652b66ed9b1214da5f7/torch-2.6.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:94fc63b3b4bedd327af588696559f68c264440e2503cc9e6954019473d74ae21", size = 66530713 }, + { url = "https://files.pythonhosted.org/packages/e5/35/0c52d708144c2deb595cd22819a609f78fdd699b95ff6f0ebcd456e3c7c1/torch-2.6.0-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:2bb8987f3bb1ef2675897034402373ddfc8f5ef0e156e2d8cfc47cacafdda4a9", size = 766624563 }, + { url = "https://files.pythonhosted.org/packages/01/d6/455ab3fbb2c61c71c8842753b566012e1ed111e7a4c82e0e1c20d0c76b62/torch-2.6.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b789069020c5588c70d5c2158ac0aa23fd24a028f34a8b4fcb8fcb4d7efcf5fb", size = 95607867 }, + { url = "https://files.pythonhosted.org/packages/18/cf/ae99bd066571656185be0d88ee70abc58467b76f2f7c8bfeb48735a71fe6/torch-2.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:7e1448426d0ba3620408218b50aa6ada88aeae34f7a239ba5431f6c8774b1239", size = 204120469 }, + { url = "https://files.pythonhosted.org/packages/81/b4/605ae4173aa37fb5aa14605d100ff31f4f5d49f617928c9f486bb3aaec08/torch-2.6.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:9a610afe216a85a8b9bc9f8365ed561535c93e804c2a317ef7fabcc5deda0989", size = 66532538 }, + { url = "https://files.pythonhosted.org/packages/24/85/ead1349fc30fe5a32cadd947c91bda4a62fbfd7f8c34ee61f6398d38fb48/torch-2.6.0-cp313-cp313-manylinux1_x86_64.whl", hash = "sha256:4874a73507a300a5d089ceaff616a569e7bb7c613c56f37f63ec3ffac65259cf", size = 766626191 }, + { url = "https://files.pythonhosted.org/packages/dd/b0/26f06f9428b250d856f6d512413e9e800b78625f63801cbba13957432036/torch-2.6.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a0d5e1b9874c1a6c25556840ab8920569a7a4137afa8a63a32cee0bc7d89bd4b", size = 95611439 }, + { url = "https://files.pythonhosted.org/packages/c2/9c/fc5224e9770c83faed3a087112d73147cd7c7bfb7557dcf9ad87e1dda163/torch-2.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:510c73251bee9ba02ae1cb6c9d4ee0907b3ce6020e62784e2d7598e0cfa4d6cc", size = 204126475 }, + { url = "https://files.pythonhosted.org/packages/88/8b/d60c0491ab63634763be1537ad488694d316ddc4a20eaadd639cedc53971/torch-2.6.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:ff96f4038f8af9f7ec4231710ed4549da1bdebad95923953a25045dcf6fd87e2", size = 66536783 }, +] + +[[package]] +name = "torchmetrics" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lightning-utilities" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/ec/f5a4f94c77a1b4c0a37e5c5c8b666a33bc074130258a6b655346bec560c2/torchmetrics-1.7.2.tar.gz", hash = "sha256:ba401cd01aeaa268e809c0e4f42ef8f95669bf9b485e1d93d54dc765e012338a", size = 566185 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/89/b5fd7eb99b27457d71d3b7d9eca0b884fa5992abca7672aab1177c5f22d8/torchmetrics-1.7.2-py3-none-any.whl", hash = "sha256:9cc3bff07a715fcb37fb04d2a1a5ae36267c36066c097578020056653a94f2a8", size = 962510 }, +] + +[[package]] +name = "torchvision" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/3d/b7241abfa3e6651c6e00796f5de2bd1ce4d500bf5159bcbfeea47e711b93/torchvision-0.21.0-1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:ff96666b94a55e802ea6796cabe788541719e6f4905fc59c380fed3517b6a64d", size = 2329320 }, + { url = "https://files.pythonhosted.org/packages/52/5b/76ca113a853b19c7b1da761f8a72cb6429b3bd0bf932537d8df4657f47c3/torchvision-0.21.0-1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ffa2a16499508fe6798323e455f312c7c55f2a88901c9a7c0fb1efa86cf7e327", size = 2329878 }, + { url = "https://files.pythonhosted.org/packages/4e/fe/5e193353706dab96fe73ae100d5a633ff635ce310e0d92f3bc2958d075b1/torchvision-0.21.0-1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:7e9e9afa150e40cd2a8f0701c43cb82a8d724f512896455c0918b987f94b84a4", size = 2280711 }, + { url = "https://files.pythonhosted.org/packages/29/88/00c69db213ee2443ada8886ec60789b227e06bb869d85ee324578221a7f7/torchvision-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110d115333524d60e9e474d53c7d20f096dbd8a080232f88dddb90566f90064c", size = 1784141 }, + { url = "https://files.pythonhosted.org/packages/be/a2/b0cedf0a411f1a5d75cfc0b87cde56dd1ddc1878be46a42c905cd8580220/torchvision-0.21.0-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:3891cd086c5071bda6b4ee9d266bb2ac39c998c045c2ebcd1e818b8316fb5d41", size = 7237719 }, + { url = "https://files.pythonhosted.org/packages/8c/a1/ee962ef9d0b2bf7a6f8b14cb95acb70e05cd2101af521032a09e43f8582f/torchvision-0.21.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:54454923a50104c66a9ab6bd8b73a11c2fc218c964b1006d5d1fe5b442c3dcb6", size = 14700617 }, + { url = "https://files.pythonhosted.org/packages/88/53/4ad334b9b1d8dd99836869fec139cb74a27781298360b91b9506c53f1d10/torchvision-0.21.0-cp311-cp311-win_amd64.whl", hash = "sha256:49bcfad8cfe2c27dee116c45d4f866d7974bcf14a5a9fbef893635deae322f2f", size = 1560523 }, + { url = "https://files.pythonhosted.org/packages/6e/1b/28f527b22d5e8800184d0bc847f801ae92c7573a8c15979d92b7091c0751/torchvision-0.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:97a5814a93c793aaf0179cfc7f916024f4b63218929aee977b645633d074a49f", size = 1784140 }, + { url = "https://files.pythonhosted.org/packages/36/63/0722e153fd27d64d5b0af45b5c8cb0e80b35a68cf0130303bc9a8bb095c7/torchvision-0.21.0-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:b578bcad8a4083b40d34f689b19ca9f7c63e511758d806510ea03c29ac568f7b", size = 7238673 }, + { url = "https://files.pythonhosted.org/packages/bb/ea/03541ed901cdc30b934f897060d09bbf7a98466a08ad1680320f9ce0cbe0/torchvision-0.21.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5083a5b1fec2351bf5ea9900a741d54086db75baec4b1d21e39451e00977f1b1", size = 14701186 }, + { url = "https://files.pythonhosted.org/packages/4c/6a/c7752603060d076dfed95135b78b047dc71792630cbcb022e3693d6f32ef/torchvision-0.21.0-cp312-cp312-win_amd64.whl", hash = "sha256:6eb75d41e3bbfc2f7642d0abba9383cc9ae6c5a4ca8d6b00628c225e1eaa63b3", size = 1560520 }, + { url = "https://files.pythonhosted.org/packages/f9/56/47d456b61c3bbce7bed4af3925c83d405bb87468e659fd3cf3d9840c3b51/torchvision-0.21.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:659b76c86757cb2ee4ca2db245e0740cfc3081fef46f0f1064d11adb4a8cee31", size = 1784141 }, + { url = "https://files.pythonhosted.org/packages/cb/4c/99880813aa50e64447fb1c4c6c804a793d2d78f7f7c53e99ddee7fa175fa/torchvision-0.21.0-cp313-cp313-manylinux1_x86_64.whl", hash = "sha256:084ac3f5a1f50c70d630a488d19bf62f323018eae1b1c1232f2b7047d3a7b76d", size = 7238714 }, + { url = "https://files.pythonhosted.org/packages/0b/2d/3c3ee10608310a395594aac7da8640372ed79c6585910ccae6919658dcdc/torchvision-0.21.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5045a3a5f21ec3eea6962fa5f2fa2d4283f854caec25ada493fcf4aab2925467", size = 2281252 }, + { url = "https://files.pythonhosted.org/packages/ed/b4/fc60e3bc003879d3de842baea258fffc3586f4b49cd435a5ba1e09c33315/torchvision-0.21.0-cp313-cp313-win_amd64.whl", hash = "sha256:9147f5e096a9270684e3befdee350f3cacafd48e0c54ab195f45790a9c146d67", size = 1560519 }, +] + +[[package]] +name = "tornado" +version = "6.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/89/c72771c81d25d53fe33e3dca61c233b665b2780f21820ba6fd2c6793c12b/tornado-6.5.1.tar.gz", hash = "sha256:84ceece391e8eb9b2b95578db65e920d2a61070260594819589609ba9bc6308c", size = 509934 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/89/f4532dee6843c9e0ebc4e28d4be04c67f54f60813e4bf73d595fe7567452/tornado-6.5.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d50065ba7fd11d3bd41bcad0825227cc9a95154bad83239357094c36708001f7", size = 441948 }, + { url = "https://files.pythonhosted.org/packages/15/9a/557406b62cffa395d18772e0cdcf03bed2fff03b374677348eef9f6a3792/tornado-6.5.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e9ca370f717997cb85606d074b0e5b247282cf5e2e1611568b8821afe0342d6", size = 440112 }, + { url = "https://files.pythonhosted.org/packages/55/82/7721b7319013a3cf881f4dffa4f60ceff07b31b394e459984e7a36dc99ec/tornado-6.5.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b77e9dfa7ed69754a54c89d82ef746398be82f749df69c4d3abe75c4d1ff4888", size = 443672 }, + { url = "https://files.pythonhosted.org/packages/7d/42/d11c4376e7d101171b94e03cef0cbce43e823ed6567ceda571f54cf6e3ce/tornado-6.5.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253b76040ee3bab8bcf7ba9feb136436a3787208717a1fb9f2c16b744fba7331", size = 443019 }, + { url = "https://files.pythonhosted.org/packages/7d/f7/0c48ba992d875521ac761e6e04b0a1750f8150ae42ea26df1852d6a98942/tornado-6.5.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:308473f4cc5a76227157cdf904de33ac268af770b2c5f05ca6c1161d82fdd95e", size = 443252 }, + { url = "https://files.pythonhosted.org/packages/89/46/d8d7413d11987e316df4ad42e16023cd62666a3c0dfa1518ffa30b8df06c/tornado-6.5.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:caec6314ce8a81cf69bd89909f4b633b9f523834dc1a352021775d45e51d9401", size = 443930 }, + { url = "https://files.pythonhosted.org/packages/78/b2/f8049221c96a06df89bed68260e8ca94beca5ea532ffc63b1175ad31f9cc/tornado-6.5.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:13ce6e3396c24e2808774741331638ee6c2f50b114b97a55c5b442df65fd9692", size = 443351 }, + { url = "https://files.pythonhosted.org/packages/76/ff/6a0079e65b326cc222a54720a748e04a4db246870c4da54ece4577bfa702/tornado-6.5.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5cae6145f4cdf5ab24744526cc0f55a17d76f02c98f4cff9daa08ae9a217448a", size = 443328 }, + { url = "https://files.pythonhosted.org/packages/49/18/e3f902a1d21f14035b5bc6246a8c0f51e0eef562ace3a2cea403c1fb7021/tornado-6.5.1-cp39-abi3-win32.whl", hash = "sha256:e0a36e1bc684dca10b1aa75a31df8bdfed656831489bc1e6a6ebed05dc1ec365", size = 444396 }, + { url = "https://files.pythonhosted.org/packages/7b/09/6526e32bf1049ee7de3bebba81572673b19a2a8541f795d887e92af1a8bc/tornado-6.5.1-cp39-abi3-win_amd64.whl", hash = "sha256:908e7d64567cecd4c2b458075589a775063453aeb1d2a1853eedb806922f568b", size = 444840 }, + { url = "https://files.pythonhosted.org/packages/55/a7/535c44c7bea4578e48281d83c615219f3ab19e6abc67625ef637c73987be/tornado-6.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:02420a0eb7bf617257b9935e2b754d1b63897525d8a289c9d65690d580b4dcf7", size = 443596 }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 }, +] + +[[package]] +name = "transformers" +version = "4.52.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/a9/275037087f9d846580b02f2d7cae0e0a6955d46f84583d0151d6227bd416/transformers-4.52.4.tar.gz", hash = "sha256:aff3764441c1adc192a08dba49740d3cbbcb72d850586075aed6bd89b98203e6", size = 8945376 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/f2/25b27b396af03d5b64e61976b14f7209e2939e9e806c10749b6d277c273e/transformers-4.52.4-py3-none-any.whl", hash = "sha256:203f5c19416d5877e36e88633943761719538a25d9775977a24fe77a1e5adfc7", size = 10460375 }, +] + +[[package]] +name = "triton" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/2e/757d2280d4fefe7d33af7615124e7e298ae7b8e3bc4446cdb8e88b0f9bab/triton-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8009a1fb093ee8546495e96731336a33fb8856a38e45bb4ab6affd6dbc3ba220", size = 253157636 }, + { url = "https://files.pythonhosted.org/packages/06/00/59500052cb1cf8cf5316be93598946bc451f14072c6ff256904428eaf03c/triton-3.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d9b215efc1c26fa7eefb9a157915c92d52e000d2bf83e5f69704047e63f125c", size = 253159365 }, + { url = "https://files.pythonhosted.org/packages/c7/30/37a3384d1e2e9320331baca41e835e90a3767303642c7a80d4510152cbcf/triton-3.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5dfa23ba84541d7c0a531dfce76d8bcd19159d50a4a8b14ad01e91734a5c1b0", size = 253154278 }, +] + +[[package]] +name = "triton" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12' and sys_platform == 'darwin'", + "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux'", +] +dependencies = [ + { name = "setuptools", marker = "platform_machine == 'aarch64' or sys_platform != 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/c5/4874a81131cc9e934d88377fbc9d24319ae1fb540f3333b4e9c696ebc607/triton-3.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3161a2bf073d6b22c4e2f33f951f3e5e3001462b2570e6df9cd57565bdec2984", size = 156528461 }, + { url = "https://files.pythonhosted.org/packages/11/53/ce18470914ab6cfbec9384ee565d23c4d1c55f0548160b1c7b33000b11fd/triton-3.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b68c778f6c4218403a6bd01be7484f6dc9e20fe2083d22dd8aef33e3b87a10a3", size = 156504509 }, + { url = "https://files.pythonhosted.org/packages/7d/74/4bf2702b65e93accaa20397b74da46fb7a0356452c1bb94dbabaf0582930/triton-3.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47bc87ad66fa4ef17968299acacecaab71ce40a238890acc6ad197c3abe2b8f1", size = 156516468 }, + { url = "https://files.pythonhosted.org/packages/0a/93/f28a696fa750b9b608baa236f8225dd3290e5aff27433b06143adc025961/triton-3.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce4700fc14032af1e049005ae94ba908e71cd6c2df682239aed08e49bc71b742", size = 156580729 }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, +] + +[[package]] +name = "uni" +version = "0.1.0" +source = { git = "https://github.com/mahmoodlab/UNI.git#42715efc11722a496e0a67f3369505a8f277206c" } +dependencies = [ + { name = "numpy" }, + { name = "pandas" }, + { name = "scikit-learn" }, + { name = "timm" }, + { name = "torch" }, + { name = "torchvision" }, + { name = "tqdm" }, + { name = "transformers" }, + { name = "xformers", marker = "sys_platform != 'darwin'" }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, +] + +[[package]] +name = "wadler-lindig" +version = "0.1.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/c8/e2112ecb627e01c9e2911f9b388167231c23a114946946d046f4e9535118/wadler_lindig-0.1.6.tar.gz", hash = "sha256:8b6adad9718291a7d82fb088a596b93659ce2346321ca76819810affbc66102b", size = 15812 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/9a/937038f3efc70871fb26b0ee6148efcfcfb96643c517c2aaddd7ed07ad76/wadler_lindig-0.1.6-py3-none-any.whl", hash = "sha256:d707f63994c7d3e1e125e7fb7e196f4adb6f80f4a11beb955c6da937754026a3", size = 20483 }, +] + +[[package]] +name = "wandb" +version = "0.19.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "docker-pycreds" }, + { name = "gitpython" }, + { name = "platformdirs" }, + { name = "protobuf" }, + { name = "psutil" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sentry-sdk" }, + { name = "setproctitle" }, + { name = "setuptools" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/98/0ff2925a21b998d4b84731429f4554ca3d9b5cad42c09c075e7306c3aca0/wandb-0.19.11.tar.gz", hash = "sha256:3f50a27dfadbb25946a513ffe856c0e8e538b5626ef207aa50b00c3b0356bff8", size = 39511477 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/2c/f8bab58c73fdde4442f1baffd9ea5d1bb3113906a97a27e8d9ab72db7a69/wandb-0.19.11-py3-none-any.whl", hash = "sha256:ff3bf050ba25ebae7aedc9a775ffab90c28068832edfe5458423f488c2558f82", size = 6481327 }, + { url = "https://files.pythonhosted.org/packages/45/4a/34b364280f690f4c6d7660f528fba9f13bdecabc4c869d266a4632cf836e/wandb-0.19.11-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:0823fd9aa6343f40c04e01959997ca8c6d6adf1bd81c8d45261fa4915f1c6b67", size = 20555751 }, + { url = "https://files.pythonhosted.org/packages/d8/e6/a27868fdb83a60df37b9d15e52c3353dd88d74442f27ae48cf765c6b9554/wandb-0.19.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c758ef5439599d9023db5b3cf1698477055d82f9fae48af2779f63f1d289167c", size = 20377587 }, + { url = "https://files.pythonhosted.org/packages/21/f7/d5cf5b58c2b3015364c7b2b6af6a440cbeda4103b67332e1e64b30f6252d/wandb-0.19.11-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:de2dfd4911e7691735e271654c735e7b90cdee9d29a3796fbf06e9e92d48f3d7", size = 20985041 }, + { url = "https://files.pythonhosted.org/packages/68/06/8b827f16a0b8f18002d2fffa7c5a7fd447946e0d0c68aeec0dd7eb18cdd3/wandb-0.19.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfff738850770d26b13f8f3fe400a6456f1e39e87f3f29d5aa241b249476df95", size = 20017696 }, + { url = "https://files.pythonhosted.org/packages/f9/31/eeb2878b26566c04c3e9b8b20b3ec3c54a2be50535088d36a37c008e07a3/wandb-0.19.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8ff673007448df11cc69379ae0df28ead866800dc1ec7bc151b402db0bbcf40", size = 21425857 }, + { url = "https://files.pythonhosted.org/packages/10/30/08988360678ae78334bb16625c28260fcaba49f500b89f8766807cb74d71/wandb-0.19.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:858bc5023fa1b3285d89d15f62be78afdb28301064daa49ea3f4ebde5dcedad2", size = 20023145 }, + { url = "https://files.pythonhosted.org/packages/c8/e9/a639c42c8ca517c4d25e8970d64d0c5a9bd35b784faed5f47d9cca3dcd12/wandb-0.19.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:90e4b57649896acb16c3dd41b3093df1a169c2f1d94ff15d76af86b8a60dcdac", size = 21504842 }, + { url = "https://files.pythonhosted.org/packages/44/74/dbe9277dd935b77dd16939cdf15357766fec0813a6e336cf5f1d07eb016e/wandb-0.19.11-py3-none-win32.whl", hash = "sha256:38dea43c7926d8800405a73b80b9adfe81eb315fc6f2ac6885c77eb966634421", size = 20767584 }, + { url = "https://files.pythonhosted.org/packages/36/d5/215cac3edec5c5ac6e7231beb9d22466d5d4e4a132fa3a1d044f7d682c15/wandb-0.19.11-py3-none-win_amd64.whl", hash = "sha256:73402003c56ddc2198878492ab2bff55bb49bce5587eae5960e737d27c0c48f7", size = 20767588 }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, +] + +[[package]] +name = "webdataset" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "braceexpand" }, + { name = "numpy" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/3a/68800d92e065cf4750ebecf973b13979c0c929b439e1293012938862038d/webdataset-1.0.2.tar.gz", hash = "sha256:7f0498be827cfa46cc5430a58768a24e2c6a410676a61be1838f53d61afdaab4", size = 80090 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/00/aca6beb3658dab4ed3dbb41a78e6e7f31342e0b41d28088f205525751601/webdataset-1.0.2-py3-none-any.whl", hash = "sha256:3dbfced32b25c0d199c6b9787937b6f85742bc3c84f652c846893075c1c082d9", size = 74956 }, +] + +[[package]] +name = "wrapt" +version = "1.17.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308 }, + { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488 }, + { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776 }, + { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776 }, + { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420 }, + { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199 }, + { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307 }, + { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025 }, + { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879 }, + { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419 }, + { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773 }, + { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799 }, + { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821 }, + { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919 }, + { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721 }, + { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899 }, + { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222 }, + { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707 }, + { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685 }, + { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567 }, + { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672 }, + { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865 }, + { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800 }, + { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824 }, + { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920 }, + { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690 }, + { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861 }, + { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174 }, + { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721 }, + { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763 }, + { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585 }, + { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676 }, + { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871 }, + { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312 }, + { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062 }, + { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155 }, + { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471 }, + { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208 }, + { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339 }, + { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232 }, + { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476 }, + { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377 }, + { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986 }, + { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750 }, + { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 }, +] + +[[package]] +name = "xformers" +version = "0.0.29.post3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", marker = "sys_platform != 'darwin'" }, + { name = "torch", marker = "sys_platform != 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/fd/e9201fbee6a1a6d7a9c67c24a256ad4c2377bc67a634f7dbeaea23bd668a/xformers-0.0.29.post3.tar.gz", hash = "sha256:0b77c67ecc3c9fdd8a0e4399e675adf12e2ff40285e00974cca2d09108157f60", size = 8461348 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/05/9c9faf1c7b3b7b986bbf7a488a185eb67670a8435d0eae94aa59f56181cd/xformers-0.0.29.post3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:bbf2f500dfdbcf4649bf568cc2c9f434399f704dc4064fd1fbdbef2b524a8139", size = 43362399 }, + { url = "https://files.pythonhosted.org/packages/e0/9f/8195d17a5ad1b601bb487f24e54331d102df7f1649e2ced6375eef272e28/xformers-0.0.29.post3-cp311-cp311-win_amd64.whl", hash = "sha256:00f2dfd94c894ff6372e21bee3f09e96bce75b55649df366649c43f049eb7a1e", size = 167742633 }, + { url = "https://files.pythonhosted.org/packages/2d/4a/20b2d9ac50efa0d40fbdb13283fd168cc2db28a2f21a159abbdd17a24213/xformers-0.0.29.post3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:08fa92f3e06372c4ce2a5306c54ae3d4a3a399fc7e24e02aac3761112ec3aeed", size = 43364118 }, + { url = "https://files.pythonhosted.org/packages/d9/ec/7846937d26b2601e40cd6e64583657f753415b94ad318e4ca350270e77d2/xformers-0.0.29.post3-cp312-cp312-win_amd64.whl", hash = "sha256:3706eca371767ff9709595185910d809fc817ec3cf4234ef44d70d2b8844d7e2", size = 167743565 }, +] + +[[package]] +name = "yacs" +version = "0.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/3e/4a45cb0738da6565f134c01d82ba291c746551b5bc82e781ec876eb20909/yacs-0.1.8.tar.gz", hash = "sha256:efc4c732942b3103bea904ee89af98bcd27d01f0ac12d8d4d369f1e7a2914384", size = 11100 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/4f/fe9a4d472aa867878ce3bb7efb16654c5d63672b86dc0e6e953a67018433/yacs-0.1.8-py3-none-any.whl", hash = "sha256:99f893e30497a4b66842821bac316386f7bd5c4f47ad35c9073ef089aa33af32", size = 14747 }, +] + +[[package]] +name = "yarl" +version = "1.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/51/c0edba5219027f6eab262e139f73e2417b0f4efffa23bf562f6e18f76ca5/yarl-1.20.0.tar.gz", hash = "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307", size = 185258 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/82/a59d8e21b20ffc836775fa7daedac51d16bb8f3010c4fcb495c4496aa922/yarl-1.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fdb5204d17cb32b2de2d1e21c7461cabfacf17f3645e4b9039f210c5d3378bf3", size = 145178 }, + { url = "https://files.pythonhosted.org/packages/ba/81/315a3f6f95947cfbf37c92d6fbce42a1a6207b6c38e8c2b452499ec7d449/yarl-1.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eaddd7804d8e77d67c28d154ae5fab203163bd0998769569861258e525039d2a", size = 96859 }, + { url = "https://files.pythonhosted.org/packages/ad/17/9b64e575583158551b72272a1023cdbd65af54fe13421d856b2850a6ddb7/yarl-1.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:634b7ba6b4a85cf67e9df7c13a7fb2e44fa37b5d34501038d174a63eaac25ee2", size = 94647 }, + { url = "https://files.pythonhosted.org/packages/2c/29/8f291e7922a58a21349683f6120a85701aeefaa02e9f7c8a2dc24fe3f431/yarl-1.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d409e321e4addf7d97ee84162538c7258e53792eb7c6defd0c33647d754172e", size = 355788 }, + { url = "https://files.pythonhosted.org/packages/26/6d/b4892c80b805c42c228c6d11e03cafabf81662d371b0853e7f0f513837d5/yarl-1.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ea52f7328a36960ba3231c6677380fa67811b414798a6e071c7085c57b6d20a9", size = 344613 }, + { url = "https://files.pythonhosted.org/packages/d7/0e/517aa28d3f848589bae9593717b063a544b86ba0a807d943c70f48fcf3bb/yarl-1.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8703517b924463994c344dcdf99a2d5ce9eca2b6882bb640aa555fb5efc706a", size = 370953 }, + { url = "https://files.pythonhosted.org/packages/5f/9b/5bd09d2f1ad6e6f7c2beae9e50db78edd2cca4d194d227b958955573e240/yarl-1.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:077989b09ffd2f48fb2d8f6a86c5fef02f63ffe6b1dd4824c76de7bb01e4f2e2", size = 369204 }, + { url = "https://files.pythonhosted.org/packages/9c/85/d793a703cf4bd0d4cd04e4b13cc3d44149470f790230430331a0c1f52df5/yarl-1.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0acfaf1da020253f3533526e8b7dd212838fdc4109959a2c53cafc6db611bff2", size = 358108 }, + { url = "https://files.pythonhosted.org/packages/6f/54/b6c71e13549c1f6048fbc14ce8d930ac5fb8bafe4f1a252e621a24f3f1f9/yarl-1.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4230ac0b97ec5eeb91d96b324d66060a43fd0d2a9b603e3327ed65f084e41f8", size = 346610 }, + { url = "https://files.pythonhosted.org/packages/a0/1a/d6087d58bdd0d8a2a37bbcdffac9d9721af6ebe50d85304d9f9b57dfd862/yarl-1.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a6a1e6ae21cdd84011c24c78d7a126425148b24d437b5702328e4ba640a8902", size = 365378 }, + { url = "https://files.pythonhosted.org/packages/02/84/e25ddff4cbc001dbc4af76f8d41a3e23818212dd1f0a52044cbc60568872/yarl-1.20.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:86de313371ec04dd2531f30bc41a5a1a96f25a02823558ee0f2af0beaa7ca791", size = 356919 }, + { url = "https://files.pythonhosted.org/packages/04/76/898ae362353bf8f64636495d222c8014c8e5267df39b1a9fe1e1572fb7d0/yarl-1.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dd59c9dd58ae16eaa0f48c3d0cbe6be8ab4dc7247c3ff7db678edecbaf59327f", size = 364248 }, + { url = "https://files.pythonhosted.org/packages/1b/b0/9d9198d83a622f1c40fdbf7bd13b224a6979f2e1fc2cf50bfb1d8773c495/yarl-1.20.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a0bc5e05f457b7c1994cc29e83b58f540b76234ba6b9648a4971ddc7f6aa52da", size = 378418 }, + { url = "https://files.pythonhosted.org/packages/c7/ce/1f50c1cc594cf5d3f5bf4a9b616fca68680deaec8ad349d928445ac52eb8/yarl-1.20.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c9471ca18e6aeb0e03276b5e9b27b14a54c052d370a9c0c04a68cefbd1455eb4", size = 383850 }, + { url = "https://files.pythonhosted.org/packages/89/1e/a59253a87b35bfec1a25bb5801fb69943330b67cfd266278eb07e0609012/yarl-1.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40ed574b4df723583a26c04b298b283ff171bcc387bc34c2683235e2487a65a5", size = 381218 }, + { url = "https://files.pythonhosted.org/packages/85/b0/26f87df2b3044b0ef1a7cf66d321102bdca091db64c5ae853fcb2171c031/yarl-1.20.0-cp311-cp311-win32.whl", hash = "sha256:db243357c6c2bf3cd7e17080034ade668d54ce304d820c2a58514a4e51d0cfd6", size = 86606 }, + { url = "https://files.pythonhosted.org/packages/33/46/ca335c2e1f90446a77640a45eeb1cd8f6934f2c6e4df7db0f0f36ef9f025/yarl-1.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:8c12cd754d9dbd14204c328915e23b0c361b88f3cffd124129955e60a4fbfcfb", size = 93374 }, + { url = "https://files.pythonhosted.org/packages/c3/e8/3efdcb83073df978bb5b1a9cc0360ce596680e6c3fac01f2a994ccbb8939/yarl-1.20.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f", size = 147089 }, + { url = "https://files.pythonhosted.org/packages/60/c3/9e776e98ea350f76f94dd80b408eaa54e5092643dbf65fd9babcffb60509/yarl-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e", size = 97706 }, + { url = "https://files.pythonhosted.org/packages/0c/5b/45cdfb64a3b855ce074ae607b9fc40bc82e7613b94e7612b030255c93a09/yarl-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e", size = 95719 }, + { url = "https://files.pythonhosted.org/packages/2d/4e/929633b249611eeed04e2f861a14ed001acca3ef9ec2a984a757b1515889/yarl-1.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33", size = 343972 }, + { url = "https://files.pythonhosted.org/packages/49/fd/047535d326c913f1a90407a3baf7ff535b10098611eaef2c527e32e81ca1/yarl-1.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58", size = 339639 }, + { url = "https://files.pythonhosted.org/packages/48/2f/11566f1176a78f4bafb0937c0072410b1b0d3640b297944a6a7a556e1d0b/yarl-1.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f", size = 353745 }, + { url = "https://files.pythonhosted.org/packages/26/17/07dfcf034d6ae8837b33988be66045dd52f878dfb1c4e8f80a7343f677be/yarl-1.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae", size = 354178 }, + { url = "https://files.pythonhosted.org/packages/15/45/212604d3142d84b4065d5f8cab6582ed3d78e4cc250568ef2a36fe1cf0a5/yarl-1.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018", size = 349219 }, + { url = "https://files.pythonhosted.org/packages/e6/e0/a10b30f294111c5f1c682461e9459935c17d467a760c21e1f7db400ff499/yarl-1.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672", size = 337266 }, + { url = "https://files.pythonhosted.org/packages/33/a6/6efa1d85a675d25a46a167f9f3e80104cde317dfdf7f53f112ae6b16a60a/yarl-1.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8", size = 360873 }, + { url = "https://files.pythonhosted.org/packages/77/67/c8ab718cb98dfa2ae9ba0f97bf3cbb7d45d37f13fe1fbad25ac92940954e/yarl-1.20.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7", size = 360524 }, + { url = "https://files.pythonhosted.org/packages/bd/e8/c3f18660cea1bc73d9f8a2b3ef423def8dadbbae6c4afabdb920b73e0ead/yarl-1.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594", size = 365370 }, + { url = "https://files.pythonhosted.org/packages/c9/99/33f3b97b065e62ff2d52817155a89cfa030a1a9b43fee7843ef560ad9603/yarl-1.20.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6", size = 373297 }, + { url = "https://files.pythonhosted.org/packages/3d/89/7519e79e264a5f08653d2446b26d4724b01198a93a74d2e259291d538ab1/yarl-1.20.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1", size = 378771 }, + { url = "https://files.pythonhosted.org/packages/3a/58/6c460bbb884abd2917c3eef6f663a4a873f8dc6f498561fc0ad92231c113/yarl-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b", size = 375000 }, + { url = "https://files.pythonhosted.org/packages/3b/2a/dd7ed1aa23fea996834278d7ff178f215b24324ee527df53d45e34d21d28/yarl-1.20.0-cp312-cp312-win32.whl", hash = "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64", size = 86355 }, + { url = "https://files.pythonhosted.org/packages/ca/c6/333fe0338305c0ac1c16d5aa7cc4841208d3252bbe62172e0051006b5445/yarl-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c", size = 92904 }, + { url = "https://files.pythonhosted.org/packages/0f/6f/514c9bff2900c22a4f10e06297714dbaf98707143b37ff0bcba65a956221/yarl-1.20.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2137810a20b933b1b1b7e5cf06a64c3ed3b4747b0e5d79c9447c00db0e2f752f", size = 145030 }, + { url = "https://files.pythonhosted.org/packages/4e/9d/f88da3fa319b8c9c813389bfb3463e8d777c62654c7168e580a13fadff05/yarl-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:447c5eadd750db8389804030d15f43d30435ed47af1313303ed82a62388176d3", size = 96894 }, + { url = "https://files.pythonhosted.org/packages/cd/57/92e83538580a6968b2451d6c89c5579938a7309d4785748e8ad42ddafdce/yarl-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42fbe577272c203528d402eec8bf4b2d14fd49ecfec92272334270b850e9cd7d", size = 94457 }, + { url = "https://files.pythonhosted.org/packages/e9/ee/7ee43bd4cf82dddd5da97fcaddb6fa541ab81f3ed564c42f146c83ae17ce/yarl-1.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18e321617de4ab170226cd15006a565d0fa0d908f11f724a2c9142d6b2812ab0", size = 343070 }, + { url = "https://files.pythonhosted.org/packages/4a/12/b5eccd1109e2097bcc494ba7dc5de156e41cf8309fab437ebb7c2b296ce3/yarl-1.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4345f58719825bba29895011e8e3b545e6e00257abb984f9f27fe923afca2501", size = 337739 }, + { url = "https://files.pythonhosted.org/packages/7d/6b/0eade8e49af9fc2585552f63c76fa59ef469c724cc05b29519b19aa3a6d5/yarl-1.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d9b980d7234614bc4674468ab173ed77d678349c860c3af83b1fffb6a837ddc", size = 351338 }, + { url = "https://files.pythonhosted.org/packages/45/cb/aaaa75d30087b5183c7b8a07b4fb16ae0682dd149a1719b3a28f54061754/yarl-1.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af4baa8a445977831cbaa91a9a84cc09debb10bc8391f128da2f7bd070fc351d", size = 353636 }, + { url = "https://files.pythonhosted.org/packages/98/9d/d9cb39ec68a91ba6e66fa86d97003f58570327d6713833edf7ad6ce9dde5/yarl-1.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123393db7420e71d6ce40d24885a9e65eb1edefc7a5228db2d62bcab3386a5c0", size = 348061 }, + { url = "https://files.pythonhosted.org/packages/72/6b/103940aae893d0cc770b4c36ce80e2ed86fcb863d48ea80a752b8bda9303/yarl-1.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab47acc9332f3de1b39e9b702d9c916af7f02656b2a86a474d9db4e53ef8fd7a", size = 334150 }, + { url = "https://files.pythonhosted.org/packages/ef/b2/986bd82aa222c3e6b211a69c9081ba46484cffa9fab2a5235e8d18ca7a27/yarl-1.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4a34c52ed158f89876cba9c600b2c964dfc1ca52ba7b3ab6deb722d1d8be6df2", size = 362207 }, + { url = "https://files.pythonhosted.org/packages/14/7c/63f5922437b873795d9422cbe7eb2509d4b540c37ae5548a4bb68fd2c546/yarl-1.20.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:04d8cfb12714158abf2618f792c77bc5c3d8c5f37353e79509608be4f18705c9", size = 361277 }, + { url = "https://files.pythonhosted.org/packages/81/83/450938cccf732466953406570bdb42c62b5ffb0ac7ac75a1f267773ab5c8/yarl-1.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7dc63ad0d541c38b6ae2255aaa794434293964677d5c1ec5d0116b0e308031f5", size = 364990 }, + { url = "https://files.pythonhosted.org/packages/b4/de/af47d3a47e4a833693b9ec8e87debb20f09d9fdc9139b207b09a3e6cbd5a/yarl-1.20.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d02b591a64e4e6ca18c5e3d925f11b559c763b950184a64cf47d74d7e41877", size = 374684 }, + { url = "https://files.pythonhosted.org/packages/62/0b/078bcc2d539f1faffdc7d32cb29a2d7caa65f1a6f7e40795d8485db21851/yarl-1.20.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:95fc9876f917cac7f757df80a5dda9de59d423568460fe75d128c813b9af558e", size = 382599 }, + { url = "https://files.pythonhosted.org/packages/74/a9/4fdb1a7899f1fb47fd1371e7ba9e94bff73439ce87099d5dd26d285fffe0/yarl-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb769ae5760cd1c6a712135ee7915f9d43f11d9ef769cb3f75a23e398a92d384", size = 378573 }, + { url = "https://files.pythonhosted.org/packages/fd/be/29f5156b7a319e4d2e5b51ce622b4dfb3aa8d8204cd2a8a339340fbfad40/yarl-1.20.0-cp313-cp313-win32.whl", hash = "sha256:70e0c580a0292c7414a1cead1e076c9786f685c1fc4757573d2967689b370e62", size = 86051 }, + { url = "https://files.pythonhosted.org/packages/52/56/05fa52c32c301da77ec0b5f63d2d9605946fe29defacb2a7ebd473c23b81/yarl-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:4c43030e4b0af775a85be1fa0433119b1565673266a70bf87ef68a9d5ba3174c", size = 92742 }, + { url = "https://files.pythonhosted.org/packages/d4/2f/422546794196519152fc2e2f475f0e1d4d094a11995c81a465faf5673ffd/yarl-1.20.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b6c4c3d0d6a0ae9b281e492b1465c72de433b782e6b5001c8e7249e085b69051", size = 163575 }, + { url = "https://files.pythonhosted.org/packages/90/fc/67c64ddab6c0b4a169d03c637fb2d2a212b536e1989dec8e7e2c92211b7f/yarl-1.20.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8681700f4e4df891eafa4f69a439a6e7d480d64e52bf460918f58e443bd3da7d", size = 106121 }, + { url = "https://files.pythonhosted.org/packages/6d/00/29366b9eba7b6f6baed7d749f12add209b987c4cfbfa418404dbadc0f97c/yarl-1.20.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:84aeb556cb06c00652dbf87c17838eb6d92cfd317799a8092cee0e570ee11229", size = 103815 }, + { url = "https://files.pythonhosted.org/packages/28/f4/a2a4c967c8323c03689383dff73396281ced3b35d0ed140580825c826af7/yarl-1.20.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f166eafa78810ddb383e930d62e623d288fb04ec566d1b4790099ae0f31485f1", size = 408231 }, + { url = "https://files.pythonhosted.org/packages/0f/a1/66f7ffc0915877d726b70cc7a896ac30b6ac5d1d2760613603b022173635/yarl-1.20.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5d3d6d14754aefc7a458261027a562f024d4f6b8a798adb472277f675857b1eb", size = 390221 }, + { url = "https://files.pythonhosted.org/packages/41/15/cc248f0504610283271615e85bf38bc014224122498c2016d13a3a1b8426/yarl-1.20.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a8f64df8ed5d04c51260dbae3cc82e5649834eebea9eadfd829837b8093eb00", size = 411400 }, + { url = "https://files.pythonhosted.org/packages/5c/af/f0823d7e092bfb97d24fce6c7269d67fcd1aefade97d0a8189c4452e4d5e/yarl-1.20.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d9949eaf05b4d30e93e4034a7790634bbb41b8be2d07edd26754f2e38e491de", size = 411714 }, + { url = "https://files.pythonhosted.org/packages/83/70/be418329eae64b9f1b20ecdaac75d53aef098797d4c2299d82ae6f8e4663/yarl-1.20.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c366b254082d21cc4f08f522ac201d0d83a8b8447ab562732931d31d80eb2a5", size = 404279 }, + { url = "https://files.pythonhosted.org/packages/19/f5/52e02f0075f65b4914eb890eea1ba97e6fd91dd821cc33a623aa707b2f67/yarl-1.20.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91bc450c80a2e9685b10e34e41aef3d44ddf99b3a498717938926d05ca493f6a", size = 384044 }, + { url = "https://files.pythonhosted.org/packages/6a/36/b0fa25226b03d3f769c68d46170b3e92b00ab3853d73127273ba22474697/yarl-1.20.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c2aa4387de4bc3a5fe158080757748d16567119bef215bec643716b4fbf53f9", size = 416236 }, + { url = "https://files.pythonhosted.org/packages/cb/3a/54c828dd35f6831dfdd5a79e6c6b4302ae2c5feca24232a83cb75132b205/yarl-1.20.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d2cbca6760a541189cf87ee54ff891e1d9ea6406079c66341008f7ef6ab61145", size = 402034 }, + { url = "https://files.pythonhosted.org/packages/10/97/c7bf5fba488f7e049f9ad69c1b8fdfe3daa2e8916b3d321aa049e361a55a/yarl-1.20.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:798a5074e656f06b9fad1a162be5a32da45237ce19d07884d0b67a0aa9d5fdda", size = 407943 }, + { url = "https://files.pythonhosted.org/packages/fd/a4/022d2555c1e8fcff08ad7f0f43e4df3aba34f135bff04dd35d5526ce54ab/yarl-1.20.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f106e75c454288472dbe615accef8248c686958c2e7dd3b8d8ee2669770d020f", size = 423058 }, + { url = "https://files.pythonhosted.org/packages/4c/f6/0873a05563e5df29ccf35345a6ae0ac9e66588b41fdb7043a65848f03139/yarl-1.20.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3b60a86551669c23dc5445010534d2c5d8a4e012163218fc9114e857c0586fdd", size = 423792 }, + { url = "https://files.pythonhosted.org/packages/9e/35/43fbbd082708fa42e923f314c24f8277a28483d219e049552e5007a9aaca/yarl-1.20.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e429857e341d5e8e15806118e0294f8073ba9c4580637e59ab7b238afca836f", size = 422242 }, + { url = "https://files.pythonhosted.org/packages/ed/f7/f0f2500cf0c469beb2050b522c7815c575811627e6d3eb9ec7550ddd0bfe/yarl-1.20.0-cp313-cp313t-win32.whl", hash = "sha256:65a4053580fe88a63e8e4056b427224cd01edfb5f951498bfefca4052f0ce0ac", size = 93816 }, + { url = "https://files.pythonhosted.org/packages/3f/93/f73b61353b2a699d489e782c3f5998b59f974ec3156a2050a52dfd7e8946/yarl-1.20.0-cp313-cp313t-win_amd64.whl", hash = "sha256:53b2da3a6ca0a541c1ae799c349788d480e5144cac47dba0266c7cb6c76151fe", size = 101093 }, + { url = "https://files.pythonhosted.org/packages/ea/1f/70c57b3d7278e94ed22d85e09685d3f0a38ebdd8c5c73b65ba4c0d0fe002/yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124", size = 46124 }, +] diff --git a/odelia_image.version b/odelia_image.version index ecc84fdd..4812aa29 100644 --- a/odelia_image.version +++ b/odelia_image.version @@ -1,2 +1,2 @@ # version of the ODELIA Docker image, read by different scripts -0.9 \ No newline at end of file +1.0 \ No newline at end of file diff --git a/runTestsInDocker.sh b/runTestsInDocker.sh index 4e5a5f5e..329ee6cf 100755 --- a/runTestsInDocker.sh +++ b/runTestsInDocker.sh @@ -1,22 +1,84 @@ #!/usr/bin/env bash -VERSION=`./getVersionNumber.sh` +set -e + +VERSION=$(./getVersionNumber.sh) DOCKER_IMAGE=jefftud/odelia:$VERSION +PROJECT_DIR="workspace/odelia_${VERSION}_dummy_project_for_testing" +CWD=$(pwd) +if [ -z "$GPU_FOR_TESTING" ]; then + export GPU_FOR_TESTING="all" +fi + + +run_tests () { + echo "[Run] Unit tests inside Docker..." + docker run --rm \ + --shm-size=16g \ + --ipc=host \ + --ulimit memlock=-1 \ + --ulimit stack=67108864 \ + -v /tmp:/scratch \ + --gpus="$GPU_FOR_TESTING" \ + --entrypoint=/MediSwarm/_runTestsInsideDocker.sh \ + "$DOCKER_IMAGE" +} + +prepare_dummy_trainings () { + echo "[Prepare] Startup kits for dummy project..." + rm -rf "$PROJECT_DIR" + ./_buildStartupKits.sh tests/provision/dummy_project_for_testing.yml "$VERSION" +} + +run_dummy_training () { + echo "[Run] Dummy training session..." + cd "$PROJECT_DIR/prod_00/client_A/startup/" + ./docker.sh --data_dir /tmp/ --scratch_dir /tmp/scratch --GPU "$GPU_FOR_TESTING" --no_pull --dummy_training + cd "$CWD" +} + +run_3dcnn_tests () { + echo "[Run] Synthetic data + 3D CNN preflight check..." + SYNTHETIC_DATA_DIR=$(mktemp -d) + + # create synthetic data + docker run --rm \ + -u $(id -u):$(id -g) \ + -v "$SYNTHETIC_DATA_DIR":/synthetic_data \ + -w /MediSwarm \ + jefftud/odelia:$VERSION \ + /bin/bash -c "python3 application/jobs/ODELIA_ternary_classification/app/scripts/create_synthetic_dataset/create_synthetic_dataset.py /synthetic_data" + + # run tests using synthetic data + cd "$PROJECT_DIR/prod_00/client_A/startup/" + # preflight check (standalone) and swarm simulation mode + ./docker.sh --data_dir "$SYNTHETIC_DATA_DIR" --scratch_dir /tmp/scratch --GPU "$GPU_FOR_TESTING" --no_pull --preflight_check + ./docker.sh --data_dir "$SYNTHETIC_DATA_DIR" --scratch_dir /tmp/scratch --GPU "$GPU_FOR_TESTING" --no_pull --run_script /MediSwarm/_run3DdcnnptlTestsInDocker.sh + + cd "$CWD" + + # clean up synthetic data + rm -rf "$SYNTHETIC_DATA_DIR" || echo "Warning: cleanup failed" +} + + +cleanup_dummy_trainings () { + echo "[Cleanup] Removing dummy workspace..." + rm -rf "$PROJECT_DIR" +} -docker run -it --rm \ - --shm-size=16g \ - --ipc=host \ - --ulimit memlock=-1 \ - --ulimit stack=67108864 \ - -v /tmp:/scratch \ - --gpus=all \ - --entrypoint=/MediSwarm/_runTestsInsideDocker.sh \ - $DOCKER_IMAGE - -./_buildStartupKits.sh tests/provision/dummy_project_for_testing.yml $VERSION - -PROJECT_DIR=workspace/odelia_${VERSION}_dummy_project_for_testing -cd $PROJECT_DIR/prod_00/client_A/startup/ -./docker.sh --data_dir /tmp/ --scratch_dir /tmp/scratch --GPU all --no_pull --dummy_training -cd ../../../../../ -rm -rf $PROJECT_DIR +case "$1" in + run_tests) run_tests ;; + prepare_dummy_trainings) prepare_dummy_trainings ;; + run_dummy_training) run_dummy_training ;; + run_3dcnn_tests) run_3dcnn_tests ;; + cleanup) cleanup_dummy_trainings ;; + all | "") + run_tests + prepare_dummy_trainings + run_dummy_training + run_3dcnn_tests + cleanup_dummy_trainings + ;; + *) echo "Unknown argument: $1"; exit 1 ;; +esac diff --git a/scripts/ci/update_apt_versions.sh b/scripts/ci/update_apt_versions.sh index c3041f83..d86bbc6d 100755 --- a/scripts/ci/update_apt_versions.sh +++ b/scripts/ci/update_apt_versions.sh @@ -1,5 +1,4 @@ #!/usr/bin/env bash - set -e DOCKERFILE_PATH="docker_config/Dockerfile_ODELIA" @@ -15,10 +14,18 @@ git config user.name "GitHub CI" git commit "$DOCKERFILE_PATH" -m "WIP: remove apt versions for rebuild" || echo "[INFO] No version pin removal change to commit." echo "[INFO] Rebuilding Docker image and capturing logs..." -if ! ./buildDockerImageAndStartupKits.sh -p "$PROJECT_YML" 2>&1 | tee "$LOG_PATH"; then - echo "[WARNING] Docker build failed. Proceeding to clean invalid versions..." +if ! ./buildDockerImageAndStartupKits.sh -p "$PROJECT_YML" > "$LOG_PATH" 2>&1; then + echo "Build failed. Output:" + cat "$LOG_PATH" + exit 1 fi +echo "[DEBUG] First 20 lines of build log:" +head -n 20 "$LOG_PATH" + +echo "[DEBUG] Checking for apt install commands:" +grep "apt install" "$LOG_PATH" || echo "[WARN] No apt install command found in log!" + echo "[INFO] Re-adding updated APT version pins to Dockerfile..." scripts/dev_utils/dockerfile_update_addAptVersionNumbers.py "$DOCKERFILE_PATH" "$LOG_PATH" rm "$LOG_PATH" @@ -38,11 +45,9 @@ while IFS= read -r match; do fi done < <(grep -oP '\b[a-z0-9\.\-]+=[a-zA-Z0-9:~.+-]+\b' "$DOCKERFILE_PATH") -if git diff --quiet; then - echo "[INFO] No changes to apt versions found. Skipping commit." +git fetch origin main +if git diff --quiet origin/main..HEAD; then echo "NO_CHANGES=true" >> "$GITHUB_ENV" else - echo "[INFO] Committing updated apt versions..." - git commit "$DOCKERFILE_PATH" -m "chore: update apt versions based on rebuild" echo "NO_CHANGES=false" >> "$GITHUB_ENV" -fi +fi \ No newline at end of file diff --git a/scripts/dev_utils/dockerfile_update_addAptVersionNumbers.py b/scripts/dev_utils/dockerfile_update_addAptVersionNumbers.py index cca37ddd..cd9c94c7 100755 --- a/scripts/dev_utils/dockerfile_update_addAptVersionNumbers.py +++ b/scripts/dev_utils/dockerfile_update_addAptVersionNumbers.py @@ -3,16 +3,12 @@ import re import sys -def load_file(filename: str) -> str: - with open(filename, 'r') as infile: - return infile.read() +from dockerfile_update_removeVersionApt import LINE_BREAK_IN_COMMAND, LINE_BREAK_REPLACEMENT, load_file, save_file -def save_file(contents: str, filename: str) -> None: - with open(filename, 'w') as outfile: - outfile.write(contents) +APT_INSTALL_COMMAND = 'RUN apt install -y' +APT_INSTALL_REPLACEMENT = 'ΡΥΝ απτ ινσταλλ -υ' - -def parse_apt_versions(installlog: str) -> str: +def parse_apt_versions(installlog: str) -> dict: versions = {} for line in installlog.splitlines(): if re.match('.*Get:[0-9]* http.*', line): @@ -27,10 +23,11 @@ def parse_apt_versions(installlog: str) -> str: def add_apt_versions(dockerfile: str, versions: dict) -> str: - dockerfile = dockerfile.replace('RUN apt install', 'RUN_apt_install') + dockerfile = dockerfile.replace(LINE_BREAK_IN_COMMAND, LINE_BREAK_REPLACEMENT) + dockerfile = dockerfile.replace(APT_INSTALL_COMMAND, APT_INSTALL_REPLACEMENT) outlines = [] for line in dockerfile.splitlines(): - if line.startswith('RUN_apt_install'): + if line.startswith(APT_INSTALL_REPLACEMENT): outline = '' + line for package, version in versions.items(): outline = outline.replace(f' {package} ', f' {package}={version} ') @@ -39,7 +36,8 @@ def add_apt_versions(dockerfile: str, versions: dict) -> str: else: outlines.append(line) dockerfile = '\n'.join(outlines) + '\n' - dockerfile = dockerfile.replace('RUN_apt_install', 'RUN apt install') + dockerfile = dockerfile.replace(APT_INSTALL_REPLACEMENT, APT_INSTALL_COMMAND) + dockerfile = dockerfile.replace(LINE_BREAK_REPLACEMENT, LINE_BREAK_IN_COMMAND) return dockerfile @@ -52,6 +50,9 @@ def report_non_fixed_versions(dockerfile: str, versions: dict) -> None: if __name__ == '__main__': dockerfile = load_file(sys.argv[1]) installlog = load_file(sys.argv[2]) + if LINE_BREAK_REPLACEMENT in dockerfile or APT_INSTALL_REPLACEMENT in dockerfile: + raise Exception('Line break replacement {LINE_BREAK_REPLACEMENT} or apt command replacement {APT_INSTALL_REPLACEMENT} in Dockerfile, cannot process it.') + versions = parse_apt_versions(installlog) report_non_fixed_versions(dockerfile, versions) dockerfile = add_apt_versions(dockerfile, versions) diff --git a/scripts/dev_utils/dockerfile_update_removeVersionApt.py b/scripts/dev_utils/dockerfile_update_removeVersionApt.py index 15055b7f..7f87aa21 100755 --- a/scripts/dev_utils/dockerfile_update_removeVersionApt.py +++ b/scripts/dev_utils/dockerfile_update_removeVersionApt.py @@ -3,6 +3,9 @@ import re import sys +LINE_BREAK_IN_COMMAND = ' \\\n ' +LINE_BREAK_REPLACEMENT = ' λινε βρεακ ρεπλαζεμεντ ' + def load_file(filename: str) -> str: with open(filename, 'r') as infile: return infile.read() @@ -12,18 +15,22 @@ def save_file(contents: str, filename: str) -> None: outfile.write(contents) -def remove_apt_versions(dockerfile: str) -> str: +def remove_apt_versions(contents: str) -> str: + contents = contents.replace(LINE_BREAK_IN_COMMAND, LINE_BREAK_REPLACEMENT) output = [] - for line in dockerfile.splitlines(): - if line.startswith('RUN apt install'): + for line in contents.splitlines(): + if line.startswith('RUN apt install -y'): out_line = re.sub('=[^ ]*', '', line) output.append(out_line) else: output.append(line) - return '\n'.join(output) - + output = '\n'.join(output) + '\n' + output = output.replace(LINE_BREAK_REPLACEMENT, LINE_BREAK_IN_COMMAND) + return output if __name__ == '__main__': - dockerfile = load_file(sys.argv[1]) - dockerfile = remove_apt_versions(dockerfile) - save_file(dockerfile, sys.argv[1]) + contents = load_file(sys.argv[1]) + if LINE_BREAK_REPLACEMENT in contents: + raise Exception('Line break replacement {LINE_BREAK_REPLACEMENT} in Dockerfile, cannot process it.') + contents = remove_apt_versions(contents) + save_file(contents, sys.argv[1]) diff --git a/scripts/dev_utils/remove_old_odelia_docker_images.sh b/scripts/dev_utils/remove_old_odelia_docker_images.sh new file mode 100755 index 00000000..7da4ee25 --- /dev/null +++ b/scripts/dev_utils/remove_old_odelia_docker_images.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +export OLD_ODELIA_DOCKER_IMAGES=$(docker image list | grep jefftud/odelia | sed 's|jefftud/odelia *[0-9a-z.-]* *||' | sed 's| *.*||' | tail -n +2) + +echo "All docker images:" + +docker image list + +echo "The following Docker images are old ODELIA docker images:" + +echo "$OLD_ODELIA_DOCKER_IMAGES" + +read -p "Delete these Docker images, unless they have additional tags? (y/n): " answer + +if [[ "$answer" == "y" ]]; then + for image in $OLD_ODELIA_DOCKER_IMAGES; do + docker rmi $image + done +fi diff --git a/scripts/pr_validation.py b/scripts/pr_validation.py new file mode 100644 index 00000000..79a56346 --- /dev/null +++ b/scripts/pr_validation.py @@ -0,0 +1,51 @@ +# scripts/pr_validation.py + +import os +import subprocess +from pathlib import Path +import logging + +logging.basicConfig(level=logging.INFO) +print("Script is running") + +import os + +print("PWD:", os.getcwd()) +print("Files in current dir:", os.listdir()) + + +def get_latest_workspace(): + root = Path.cwd() + candidates = list(root.rglob("odelia_0.9-dev.*_MEVIS_test")) + if not candidates: + raise RuntimeError("No workspace found matching pattern 'odelia_0.9-dev.*_MEVIS_test'") + return sorted(candidates, reverse=True)[0] + + +def run_command(cmd, cwd=None): + print(f"\n>>> Running: {' '.join(cmd)} in {cwd}") + subprocess.run(cmd, cwd=cwd, check=True) + + +def main(): + site = os.environ.get("SITE_NAME", "UKA") + datadir = os.environ["DATADIR"] + scratchdir = os.environ["SCRATCHDIR"] + + workspace_version = get_latest_workspace() + startup_dir = workspace_version / "prod_00" / site / "startup" + + print(f"Using workspace: {workspace_version}") + print(f"Startup directory: {startup_dir}") + + # Run dummy training + run_command(["./docker.sh", "--scratch_dir", scratchdir, "--GPU", "device=0", "--dummy_training"], cwd=startup_dir) + + # Run preflight check + run_command( + ["./docker.sh", "--data_dir", datadir, "--scratch_dir", scratchdir, "--GPU", "device=0", "--preflight_check"], + cwd=startup_dir) + + +if __name__ == "__main__": + main() diff --git a/tests/provision/dummy_project_for_testing.yml b/tests/provision/dummy_project_for_testing.yml index 1ab98c45..7e259592 100644 --- a/tests/provision/dummy_project_for_testing.yml +++ b/tests/provision/dummy_project_for_testing.yml @@ -28,7 +28,7 @@ builders: - path: nvflare.lighter.impl.static_file.StaticFileBuilder args: config_folder: config - scheme: grpc + scheme: http docker_image: jefftud/odelia:__REPLACED_BY_CURRENT_VERSION_NUMBER_WHEN_BUILDING_STARTUP_KITS__ overseer_agent: path: nvflare.ha.dummy_overseer_agent.DummyOverseerAgent