diff --git a/.github/workflows/lint_and_test.yaml b/.github/workflows/lint_and_test.yaml new file mode 100644 index 0000000..328d255 --- /dev/null +++ b/.github/workflows/lint_and_test.yaml @@ -0,0 +1,31 @@ +name: Lint and Test +on: + push: + branches: + - main + pull_request: + branches: + - main + +permissions: + contents: read +jobs: + lint_and_test: + name: lint and test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install poetry + run: pipx install poetry + - name: Setup python and poetry + uses: actions/setup-python@v4 + with: + python-version: 3.8.16 + cache: poetry + cache-dependency-path: poetry.lock + - name: Install dependencies + run: poetry install + - name: Lint code + run: poetry run pre-commit run --all-files --show-diff-on-failure + - name: Run tests + run: poetry run pytest -v -m "not slow" -m "not dataset" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..762960a --- /dev/null +++ b/.gitignore @@ -0,0 +1,166 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +.vscode/ +.ruff_cache +logs/ +lightning_logs/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..e4af451 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +repos: +- repo: local + hooks: + - id: isort + name: isort + entry: poetry run isort + language: system + types: [python] + - id: black + name: black + entry: poetry run black + language: system + types: [python] + - id: ruff + name: ruff + entry: poetry run ruff check --fix --show-source --show-fixes --exit-non-zero-on-fix . + language: system + types: [python] +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ff42927 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Maximilian Seitzer and Andrii Zadaianchuk + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index a15b552..e8e21d4 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,80 @@ -# [Object-Centric Learning for Real-World Videos by Predicting Temporal Feature Similarities (NeurIPS 2023)](https://martius-lab.github.io/videosaur/) +# VideoSAUR + +This is the code release for the paper **Object-Centric Learning for Real-World Videos by Predicting Temporal Feature Similarities (NeurIPS 2023)**, by Andrii Zadaianchuk, Maximilian Seitzer and Georg Martius. + +- Webpage: https://martius-lab.github.io/videosaur +- Arxiv: https://arxiv.org/abs/2306.04829 +- OpenReview: https://openreview.net/forum?id=t1jLRFvBqm + ![Temporal Feature Similarities](https://zadaianchuk.github.io/videosaur/static/images/sim_loss-1.png) -## Citation +## Summary + +Unsupervised video-based object-centric learning is a promising avenue to learn structured representations from large, unlabeled video collections, but previous approaches have only managed to scale to real-world datasets in restricted domains. Recently, it was shown that the reconstruction of pre-trained self-supervised features leads to object-centric representations on unconstrained real-world image datasets. Building on this approach, we propose a novel way to use such pre-trained features in the form of a temporal feature similarity loss. This loss encodes semantic and temporal correlations between image patches and is a natural way to introduce a motion bias for object discovery. We demonstrate that this loss leads to state-of-the-art performance on the challenging synthetic MOVi datasets. When used in combination with the feature reconstruction loss, our model is the first object-centric video model that scales to unconstrained video datasets such as YouTube-VIS. + +## Usage + +### Setup + +First, setup the python environment setup. We use [Poetry](https://python-poetry.org/) for this: + +``` +poetry install +``` + +Then you could run a test configuration to see if everything works: + +``` +poetry run python -m videosaur.train tests/configs/test_dummy_image.yml +``` + +Second, to download the datasets used in this work, follow the instructions in [data/README.md](data/README.md). +By default, datasets are expected to be contained in the folder `./data`. +You can change this to the actual folder your data is in by setting the environment variable `VIDEOSAUR_DATA_PATH`, or by running `train.py` with the `--data-dir` option. + +### Training + +Run one of the configurations in `configs/videosaur`, for example: + +``` +poetry run python -m videosaur.train configs/videosaur/movi_c.yml +``` + +The results are stored in a folder created under the log root folder (by defaults `./logs`, changeable by the argument `--log-dir`). +If you want to continue training from a previous run, you can use the `--continue` argument, like in the following command: + +``` +poetry run python -m videosaur.train --continue configs/videosaur/movi_c.yml +``` + +## Results + +### VideoSAUR + +We list the results you should roughly be able to obtain with the configs included in this repository: + +| Dataset | Model Variant | Video ARI | Video mBO | Config | +|--------------|------------------|------------|-----------|-----------------------------| +| MOVi-C | ViT-B/8, DINO | 64.8 | 38.9 | videosaur/movi_c.yml | +| MOVi-E | ViT-B/8, DINO | 73.9 | 35.6 | videosaur/movi_e.yml | +| YT-VIS 2021 | ViT-B/16, DINO | 39.5 | 29.1 | videosaur/ytvis.yml | +| YT-VIS 2021 | ViT-B/14, DINOv2 | 39.7 | 35.6 | videosaur/ytvis_dinov2.yml | + +### DINOSAUR + +We also include a configuration for the DINOSAUR model from our previous paper [Bridging the gap to real-world object-centric learning](https://arxiv.org/abs/2209.14860). +This configuration yields improved results compared to the DINOSAUR model in the original paper (mainly +due to using DINOv2 pre-trained features). +Note that there might be minor differences in the metrics, as the numbers here are computed for 224x224 masks, compared to 320x320 masks in the DINOSAUR paper. + +| Dataset | Model Variant | Image ARI | Image mBO | Config | +|-------------|--------------------|-----------|-----------|------------------------------------| +| COCO | ViT-B/14, DINOv2 | 45.6 | 29.6 | dinosaur/coco_base14_dinov2.yml | + +## Citation + +If you make use of this repository, please use the following bibtex entry to cite us: -Please use the following bibtex entry to cite us: ``` @inproceedings{zadaianchuk2023objectcentric, title={Object-Centric Learning for Real-World Videos by Predicting Temporal Feature Similarities}, @@ -12,3 +83,10 @@ Please use the following bibtex entry to cite us: year={2023}, } ``` + +## License + +This codebase is released under the MIT license. +Some parts of the codebase were adapted from other codebases. +A comment was added to the code where this is the case. +Those parts are governed by their respective licenses. diff --git a/configs/dinosaur/coco_base14_dinov2.yml b/configs/dinosaur/coco_base14_dinov2.yml new file mode 100644 index 0000000..33811f6 --- /dev/null +++ b/configs/dinosaur/coco_base14_dinov2.yml @@ -0,0 +1,111 @@ +experiment_group: dinosaur +experiment_name: coco_base14_dinov2 + +globals: + INPUT_TYPE: image + NUM_SLOTS: 7 + SLOT_DIM: 256 + VIT_MODEL: vit_base_patch14_dinov2 + FEAT_DIM: "${config_prop: VIT_PARAMS, ${.VIT_MODEL}, FEAT_DIM}" + NUM_PATCHES: "${config_prop: VIT_PARAMS, ${.VIT_MODEL}, NUM_PATCHES}" + NUM_GPUS: 1 + BATCH_SIZE_PER_GPU: 64 + TOTAL_BATCH_SIZE: "${mul: ${.NUM_GPUS}, ${.BATCH_SIZE_PER_GPU}}" + BASE_LR: 0.0002 + +trainer: + max_steps: 500000 + log_every_n_steps: 200 + val_check_interval: 2500 + gradient_clip_val: 1.0 + +optimizer: + name: Adam + # Scale learning rate by batch size: 1e-4 for every 32 samples + lr: "${eval: 'a / 32 * b', ${globals.TOTAL_BATCH_SIZE}, ${globals.BASE_LR}}" + lr_scheduler: + name: exp_decay_with_warmup + warmup_steps: 10000 + decay_steps: 100000 + +model: + input_type: ${globals.INPUT_TYPE} + visualize: true + visualize_every_n_steps: 10000 + + initializer: + name: RandomInit + n_slots: ${globals.NUM_SLOTS} + dim: ${globals.SLOT_DIM} + + encoder: + backbone: + name: TimmExtractor + model: ${globals.VIT_MODEL} + features: vit_block12 + frozen: true + pretrained: true + model_kwargs: + dynamic_img_size: True + output_transform: + name: networks.two_layer_mlp + inp_dim: ${globals.FEAT_DIM} + outp_dim: ${globals.SLOT_DIM} + hidden_dim: "${mul: ${globals.FEAT_DIM}, 2}" + layer_norm: true + + grouper: + name: SlotAttention + inp_dim: ${globals.SLOT_DIM} + slot_dim: ${globals.SLOT_DIM} + n_iters: 3 + use_mlp: true + + decoder: + name: MLPDecoder + inp_dim: ${globals.SLOT_DIM} + outp_dim: ${globals.FEAT_DIM} + hidden_dims: [2048, 2048, 2048] + n_patches: ${globals.NUM_PATCHES} + +val_metrics: + image_ari: + name: ImageARI + ignore_overlaps: true + pred_key: decoder_masks_hard + true_key: segmentations + image_mbo: + name: ImageIoU + ignore_overlaps: true + matching: overlap + pred_key: decoder_masks_hard + true_key: segmentations + +dataset: + train_shards: "coco/coco-train-{000000..000924}.tar" + val_shards: "coco/coco-validation-{000000..000039}.tar" + batch_size: ${globals.BATCH_SIZE_PER_GPU} + val_size: 5000 + num_workers: 4 + num_val_workers: 1 + train_pipeline: + name: ${globals.INPUT_TYPE} + keys: [image] + is_video_dataset: false + shuffle_size: 2048 + transforms: + name: coco_train + crop_type: short_side_resize_random + type: ${globals.INPUT_TYPE} + input_size: 224 + h_flip_prob: 0.5 + val_pipeline: + name: ${globals.INPUT_TYPE} + keys: [image, segmentations] + is_video_dataset: false + transforms: + name: coco_val + crop_type: central + type: ${globals.INPUT_TYPE} + input_size: 224 + num_classes: 63 diff --git a/configs/videosaur/movi_c.yml b/configs/videosaur/movi_c.yml new file mode 100644 index 0000000..dcfbd46 --- /dev/null +++ b/configs/videosaur/movi_c.yml @@ -0,0 +1,164 @@ +experiment_group: videosaur +experiment_name: movi_c + +globals: + NUM_SLOTS: 11 + SLOT_DIM: 128 + VIT_MODEL: vit_base_patch8_224_dino + FEAT_DIM: "${config_prop: VIT_PARAMS, ${.VIT_MODEL}, FEAT_DIM}" + NUM_PATCHES: "${config_prop: VIT_PARAMS, ${.VIT_MODEL}, NUM_PATCHES}" + NUM_GPUS: 1 + BATCH_SIZE_PER_GPU: 128 + TOTAL_BATCH_SIZE: "${mul: ${.NUM_GPUS}, ${.BATCH_SIZE_PER_GPU}}" + BASE_LR: 0.0001 + SIM_TEMP: 0.075 + SIM_WEIGHT: 0.1 + +trainer: + max_steps: 100000 + log_every_n_steps: 500 + val_check_interval: 1000 + gradient_clip_val: 0.05 + +optimizer: + name: Adam + # Scale learning rate by batch size: take base lr once for every 32 samples + lr: "${eval: 'a / 32 * b', ${globals.TOTAL_BATCH_SIZE}, ${globals.BASE_LR}}" + lr_scheduler: + name: exp_decay_with_warmup + warmup_steps: 2500 + decay_steps: ${trainer.max_steps} + +model: + input_type: video + visualize: true + visualize_every_n_steps: 10000 + losses: + loss_timesim: + name: CrossEntropyLoss + target_key: encoder.vit_block_keys12 + remove_last_n_frames: 1 + pred_dims: + - 0 + - ${globals.NUM_PATCHES} + target_transform: + name: utils.FeatureTimeSimilarity + softmax: true + temperature: ${globals.SIM_TEMP} + threshold: 0.0 + loss_weights: + loss_timesim: ${globals.SIM_WEIGHT} + + initializer: + name: RandomInit + n_slots: ${globals.NUM_SLOTS} + dim: ${globals.SLOT_DIM} + + encoder: + backbone: + name: TimmExtractor + model: ${globals.VIT_MODEL} + features: + - vit_block12 + - vit_block_keys12 + frozen: true + pretrained: true + output_transform: + name: networks.two_layer_mlp + inp_dim: ${globals.FEAT_DIM} + outp_dim: ${globals.SLOT_DIM} + hidden_dim: "${mul: ${globals.FEAT_DIM}, 2}" + layer_norm: true + + grouper: + name: SlotAttention + inp_dim: ${globals.SLOT_DIM} + slot_dim: ${globals.SLOT_DIM} + n_iters: 2 + use_mlp: false + + latent_processor: + first_step_corrector_args: + n_iters: 3 + + decoder: + name: SlotMixerDecoder + inp_dim: ${globals.SLOT_DIM} + embed_dim: ${globals.SLOT_DIM} + outp_dim: ${globals.NUM_PATCHES} + n_patches: ${globals.NUM_PATCHES} + allocator: + name: TransformerEncoder + dim: ${globals.SLOT_DIM} + memory_dim: ${globals.SLOT_DIM} + n_blocks: 2 + n_heads: 4 + renderer: + name: MLP + inp_dim: ${globals.SLOT_DIM} + outp_dim: 1024 + hidden_dims: [1024, 1024, 1024] + final_activation: true + renderer_dim: 1024 + use_layer_norms: true + pos_embed_mode: add + + predictor: + name: networks.TransformerEncoder + dim: ${globals.SLOT_DIM} + n_blocks: 1 + n_heads: 4 + +val_metrics: + ari: + name: VideoARI + ignore_background: true + pred_key: decoder_masks_hard + true_key: segmentations + image_ari: + name: ImageARI + ignore_background: true + video_input: true + pred_key: decoder_masks_hard + true_key: segmentations + mbo: + name: VideoIoU + matching: overlap + ignore_background: true + pred_key: decoder_masks_hard + true_key: segmentations + image_mbo: + name: ImageIoU + matching: overlap + ignore_background: true + video_input: true + pred_key: decoder_masks_hard + true_key: segmentations + +dataset: + train_shards: "movi_c/movi_c-train-{000000..000304}.tar" + val_shards: "movi_c/movi_c-validation-{000000..000007}.tar" + batch_size: ${globals.BATCH_SIZE_PER_GPU} + val_batch_size: 16 + val_size: 250 + num_workers: 8 + num_val_workers: 8 + train_pipeline: + video_size: 24 + chunk_size: 4 + sample_one_chunk_per_video: true + keys: [video] + shuffle_size: 512 + transforms: + name: movi_train + type: video + input_size: 224 + h_flip_prob: 0.5 + val_pipeline: + use_chunks: false + keys: [video, segmentations] + transforms: + name: movi_val + type: video + input_size: 224 + num_classes: 11 diff --git a/configs/videosaur/movi_e.yml b/configs/videosaur/movi_e.yml new file mode 100644 index 0000000..689ffcf --- /dev/null +++ b/configs/videosaur/movi_e.yml @@ -0,0 +1,165 @@ +experiment_group: videosaur +experiment_name: movi_e + +globals: + NUM_SLOTS: 15 + SLOT_DIM: 128 + VIT_MODEL: vit_base_patch8_224_dino + FEAT_DIM: "${config_prop: VIT_PARAMS, ${.VIT_MODEL}, FEAT_DIM}" + NUM_PATCHES: "${config_prop: VIT_PARAMS, ${.VIT_MODEL}, NUM_PATCHES}" + NUM_GPUS: 1 + BATCH_SIZE_PER_GPU: 128 + TOTAL_BATCH_SIZE: "${mul: ${.NUM_GPUS}, ${.BATCH_SIZE_PER_GPU}}" + BASE_LR: 0.0001 + SIM_TEMP: 0.075 + SIM_WEIGHT: 0.1 + +trainer: + max_steps: 100000 + log_every_n_steps: 500 + val_check_interval: 1000 + gradient_clip_val: 0.05 + +optimizer: + name: Adam + # Scale learning rate by batch size: take base lr once for every 32 samples + lr: "${eval: 'a / 32 * b', ${globals.TOTAL_BATCH_SIZE}, ${globals.BASE_LR}}" + lr_scheduler: + name: exp_decay_with_warmup + warmup_steps: 2500 + decay_steps: ${trainer.max_steps} + +model: + input_type: video + visualize: true + visualize_every_n_steps: 10000 + + losses: + loss_timesim: + name: CrossEntropyLoss + target_key: encoder.vit_block_keys12 + remove_last_n_frames: 1 + pred_dims: + - 0 + - ${globals.NUM_PATCHES} + target_transform: + name: utils.FeatureTimeSimilarity + softmax: true + temperature: ${globals.SIM_TEMP} + threshold: 0.0 + loss_weights: + loss_timesim: ${globals.SIM_WEIGHT} + + initializer: + name: RandomInit + n_slots: ${globals.NUM_SLOTS} + dim: ${globals.SLOT_DIM} + + encoder: + backbone: + name: TimmExtractor + model: ${globals.VIT_MODEL} + features: + - vit_block12 + - vit_block_keys12 + frozen: true + pretrained: true + output_transform: + name: networks.two_layer_mlp + inp_dim: ${globals.FEAT_DIM} + outp_dim: ${globals.SLOT_DIM} + hidden_dim: "${mul: ${globals.FEAT_DIM}, 2}" + layer_norm: true + + grouper: + name: SlotAttention + inp_dim: ${globals.SLOT_DIM} + slot_dim: ${globals.SLOT_DIM} + n_iters: 2 + use_mlp: false + + latent_processor: + first_step_corrector_args: + n_iters: 3 + + decoder: + name: SlotMixerDecoder + inp_dim: ${globals.SLOT_DIM} + embed_dim: ${globals.SLOT_DIM} + outp_dim: ${globals.NUM_PATCHES} + n_patches: ${globals.NUM_PATCHES} + allocator: + name: TransformerEncoder + dim: ${globals.SLOT_DIM} + memory_dim: ${globals.SLOT_DIM} + n_blocks: 2 + n_heads: 4 + renderer: + name: MLP + inp_dim: ${globals.SLOT_DIM} + outp_dim: 1024 + hidden_dims: [1024, 1024, 1024] + final_activation: true + renderer_dim: 1024 + use_layer_norms: true + pos_embed_mode: add + + predictor: + name: networks.TransformerEncoder + dim: ${globals.SLOT_DIM} + n_blocks: 1 + n_heads: 4 + +val_metrics: + ari: + name: VideoARI + ignore_background: true + pred_key: decoder_masks_hard + true_key: segmentations + image_ari: + name: ImageARI + ignore_background: true + video_input: true + pred_key: decoder_masks_hard + true_key: segmentations + mbo: + name: VideoIoU + matching: overlap + ignore_background: true + pred_key: decoder_masks_hard + true_key: segmentations + image_mbo: + name: ImageIoU + matching: overlap + ignore_background: true + video_input: true + pred_key: decoder_masks_hard + true_key: segmentations + +dataset: + train_shards: "movi_e/movi_e-train-{000000..000304}.tar" + val_shards: "movi_e/movi_e-validation-{000000..000007}.tar" + batch_size: ${globals.BATCH_SIZE_PER_GPU} + val_batch_size: 16 + val_size: 250 + num_workers: 8 + num_val_workers: 8 + train_pipeline: + video_size: 24 + chunk_size: 4 + sample_one_chunk_per_video: true + keys: [video] + shuffle_size: 512 + transforms: + name: movi_train + type: video + input_size: 224 + h_flip_prob: 0.5 + val_pipeline: + use_chunks: false + keys: [video, segmentations] + transforms: + name: movi_val + type: video + input_size: 224 + num_classes: 24 diff --git a/configs/videosaur/ytvis.yml b/configs/videosaur/ytvis.yml new file mode 100644 index 0000000..70731ed --- /dev/null +++ b/configs/videosaur/ytvis.yml @@ -0,0 +1,174 @@ +experiment_group: videosaur +experiment_name: ytvis2021 + +globals: + NUM_SLOTS: 7 + SLOT_DIM: 64 + VIT_MODEL: vit_base_patch16_224_dino + FEAT_DIM: "${config_prop: VIT_PARAMS, ${.VIT_MODEL}, FEAT_DIM}" + NUM_PATCHES: "${config_prop: VIT_PARAMS, ${.VIT_MODEL}, NUM_PATCHES}" + NUM_GPUS: 1 + BATCH_SIZE_PER_GPU: 128 + TOTAL_BATCH_SIZE: "${mul: ${.NUM_GPUS}, ${.BATCH_SIZE_PER_GPU}}" + BASE_LR: 0.0001 + SIM_WEIGHT: 0.1 + SIM_TEMP: 0.25 + SIM_MASK: false + +trainer: + max_steps: 100000 + log_every_n_steps: 500 + val_check_interval: 1000 + gradient_clip_val: 0.05 + +optimizer: + name: Adam + # Scale learning rate by batch size: take base lr once for every 32 samples + lr: "${eval: 'a / 32 * b', ${globals.TOTAL_BATCH_SIZE}, ${globals.BASE_LR}}" + lr_scheduler: + name: exp_decay_with_warmup + warmup_steps: 2500 + decay_steps: ${trainer.max_steps} + +model: + input_type: video + visualize: true + visualize_every_n_steps: 10000 + + losses: + loss_featrec: + name: MSELoss + pred_dims: + - 0 + - ${globals.FEAT_DIM} + loss_timesim: + name: CrossEntropyLoss + target_key: encoder.vit_block_keys12 + remove_last_n_frames: 1 + pred_dims: + - ${globals.FEAT_DIM} + - "${add: ${globals.FEAT_DIM}, ${globals.NUM_PATCHES}}" + target_transform: + name: utils.FeatureTimeSimilarity + softmax: true + temperature: ${globals.SIM_TEMP} + threshold: 0.0 + loss_weights: + loss_timesim: ${globals.SIM_WEIGHT} + loss_featrec: 1.0 + + initializer: + name: RandomInit + n_slots: ${globals.NUM_SLOTS} + dim: ${globals.SLOT_DIM} + + encoder: + backbone: + name: TimmExtractor + model: ${globals.VIT_MODEL} + features: + - vit_block12 + - vit_block_keys12 + frozen: true + pretrained: true + output_transform: + name: networks.two_layer_mlp + inp_dim: ${globals.FEAT_DIM} + outp_dim: ${globals.SLOT_DIM} + hidden_dim: "${mul: ${globals.FEAT_DIM}, 2}" + layer_norm: true + + grouper: + name: SlotAttention + inp_dim: ${globals.SLOT_DIM} + slot_dim: ${globals.SLOT_DIM} + n_iters: 2 + use_mlp: false + + latent_processor: + first_step_corrector_args: + n_iters: 3 + + decoder: + name: SlotMixerDecoder + inp_dim: ${globals.SLOT_DIM} + embed_dim: ${globals.SLOT_DIM} + outp_dim: "${add: ${globals.FEAT_DIM}, ${globals.NUM_PATCHES}}" + n_patches: ${globals.NUM_PATCHES} + allocator: + name: TransformerEncoder + dim: ${globals.SLOT_DIM} + memory_dim: ${globals.SLOT_DIM} + n_blocks: 3 + n_heads: 4 + renderer: + name: MLP + inp_dim: ${globals.SLOT_DIM} + outp_dim: 1024 + hidden_dims: [1024, 1024] + final_activation: true + renderer_dim: 1024 + use_layer_norms: true + pos_embed_mode: add + + predictor: + name: networks.TransformerEncoder + dim: ${globals.SLOT_DIM} + n_blocks: 1 + n_heads: 4 + +val_metrics: + ari: + name: VideoARI + ignore_background: false + pred_key: decoder_masks_hard + true_key: segmentations + image_ari: + name: ImageARI + video_input: true + ignore_background: false + pred_key: decoder_masks_hard + true_key: segmentations + mbo: + name: VideoIoU + matching: overlap + ignore_background: false + pred_key: decoder_masks_hard + true_key: segmentations + image_mbo: + name: ImageIoU + matching: overlap + video_input: true + ignore_background: false + pred_key: decoder_masks_hard + true_key: segmentations + +dataset: + train_shards: "ytvis2021_resized/ytvis-train-{000000..000083}.tar" + val_shards: "ytvis2021_resized/ytvis-validation-{000000..000029}.tar" + batch_size: ${globals.BATCH_SIZE_PER_GPU} + val_batch_size: 16 + val_size: 200 + num_workers: 8 + num_val_workers: 8 + train_pipeline: + chunk_size: 4 + keys: [video] + sample_one_chunk_per_video: true + transforms: + name: ytvis_train + type: video + crop_type: short_side_resize_random + input_size: 224 + num_classes: 10 + h_flip_prob: 0.5 + val_pipeline: + use_chunks: true + chunk_size: 6 + keys: [video, segmentations] + transforms: + name: ytvis_val + type: video + crop_type: central + input_size: 224 + num_classes: 25 diff --git a/configs/videosaur/ytvis_dinov2.yml b/configs/videosaur/ytvis_dinov2.yml new file mode 100644 index 0000000..da528dd --- /dev/null +++ b/configs/videosaur/ytvis_dinov2.yml @@ -0,0 +1,177 @@ +# This configuration needs YTVIS-2021 videos at 518x518 resolution. +# See `data/README.md` for the instructions to generate the dataset. +experiment_group: videosaur +experiment_name: ytvis2021_dinov2 + +globals: + NUM_SLOTS: 7 + SLOT_DIM: 64 + DINO_MODEL: vit_base_patch14_dinov2.lvd142m + FEAT_DIM: 768 + NUM_PATCHES: 1369 + NUM_GPUS: 1 + BATCH_SIZE_PER_GPU: 64 + TOTAL_BATCH_SIZE: "${mul: ${.NUM_GPUS}, ${.BATCH_SIZE_PER_GPU}}" + BASE_LR: 0.0001 + SIM_WEIGHT: 0.1 + SIM_TEMP: 0.15 + + +trainer: + max_steps: 100000 + log_every_n_steps: 500 + val_check_interval: 1000 + gradient_clip_val: 0.05 + +optimizer: + name: Adam + # Scale learning rate by batch size: take base lr once for every 32 samples + lr: "${eval: 'a / 32 * b', ${globals.TOTAL_BATCH_SIZE}, ${globals.BASE_LR}}" + lr_scheduler: + name: exp_decay_with_warmup + warmup_steps: 2500 + decay_steps: ${trainer.max_steps} + +model: + input_type: video + visualize: true + visualize_every_n_steps: 10000 + + losses: + loss_featrec: + name: MSELoss + pred_dims: + - 0 + - ${globals.FEAT_DIM} + loss_timesim: + name: CrossEntropyLoss + target_key: encoder.vit_block_keys12 + remove_last_n_frames: 1 + pred_dims: + - ${globals.FEAT_DIM} + - "${add: ${globals.FEAT_DIM}, ${globals.NUM_PATCHES}}" + target_transform: + name: utils.FeatureTimeSimilarity + softmax: true + temperature: ${globals.SIM_TEMP} + threshold: 0.0 + loss_weights: + loss_timesim: ${globals.SIM_WEIGHT} + loss_featrec: 1.0 + + initializer: + name: RandomInit + n_slots: ${globals.NUM_SLOTS} + dim: ${globals.SLOT_DIM} + + encoder: + backbone: + name: TimmExtractor + model: ${globals.DINO_MODEL} + features: + - vit_block12 + - vit_block_keys12 + frozen: true + pretrained: true + output_transform: + name: networks.two_layer_mlp + inp_dim: ${globals.FEAT_DIM} + outp_dim: ${globals.SLOT_DIM} + hidden_dim: "${mul: ${globals.FEAT_DIM}, 2}" + layer_norm: true + + grouper: + name: SlotAttention + inp_dim: ${globals.SLOT_DIM} + slot_dim: ${globals.SLOT_DIM} + n_iters: 2 + use_mlp: false + + latent_processor: + first_step_corrector_args: + n_iters: 3 + + decoder: + name: SlotMixerDecoder + inp_dim: ${globals.SLOT_DIM} + embed_dim: ${globals.SLOT_DIM} + outp_dim: "${add: ${globals.FEAT_DIM}, ${globals.NUM_PATCHES}}" + n_patches: ${globals.NUM_PATCHES} + allocator: + name: TransformerEncoder + dim: ${globals.SLOT_DIM} + memory_dim: ${globals.SLOT_DIM} + n_blocks: 3 + n_heads: 4 + renderer: + name: MLP + inp_dim: ${globals.SLOT_DIM} + outp_dim: 1024 + hidden_dims: [1024, 1024] + final_activation: true + renderer_dim: 1024 + use_layer_norms: true + pos_embed_mode: add + + predictor: + name: networks.TransformerEncoder + dim: ${globals.SLOT_DIM} + n_blocks: 1 + n_heads: 4 + +val_metrics: + ari: + name: VideoARI + ignore_background: false + pred_key: decoder_masks_hard + true_key: segmentations + image_ari: + name: ImageARI + video_input: true + ignore_background: false + pred_key: decoder_masks_hard + true_key: segmentations + mbo: + name: VideoIoU + matching: overlap + ignore_background: false + pred_key: decoder_masks_hard + true_key: segmentations + image_mbo: + name: ImageIoU + matching: overlap + video_input: true + ignore_background: false + pred_key: decoder_masks_hard + true_key: segmentations + +dataset: + train_shards: "ytvis2021_resized_518/ytvis-train-{000000..000083}.tar" + val_shards: "ytvis2021_resized_518/ytvis-validation-{000000..000029}.tar" + batch_size: ${globals.BATCH_SIZE_PER_GPU} + val_batch_size: 16 + val_size: 200 + num_workers: 8 + num_val_workers: 8 + train_pipeline: + chunk_size: 4 + keys: [video] + sample_one_chunk_per_video: true + transforms: + name: ytvis_train + type: video + crop_type: short_side_resize_random + input_size: 518 + num_classes: 10 + h_flip_prob: 0.5 + val_pipeline: + use_chunks: true + chunk_size: 6 + keys: [video, segmentations] + transforms: + name: ytvis_val + type: video + crop_type: central + input_size: 518 + mask_size: 224 + num_classes: 25 diff --git a/data/README.md b/data/README.md new file mode 100644 index 0000000..ba6b1d7 --- /dev/null +++ b/data/README.md @@ -0,0 +1,66 @@ +# Dataset Creation Scripts + +### MOVi + +Preliminaries: + +``` +poetry install -E tensorflow +``` + +**MOVi-C** + +``` +python save_movi.py --level c --split train --maxcount 32 --only-video /movi_c +python save_movi.py --level c --split validation --maxcount 32 /movi_c +``` + +**MOVi-E** + +``` +python save_movi.py --level e --split train --maxcount 32 --only-video /movi_e +python save_movi.py --level e --split validation --maxcount 32 /movi_e +``` + +### COCO + +Preliminaries: + +``` +poetry install -E coco +``` + +``` +python save_coco.py --split train --maxcount 128 --only-images --out-path /coco +python save_coco.py --split validation --maxcount 128 --out-path /coco +python save_coco.py --split unlabeled --maxcount 256 --original-image-format --out-path /coco +``` + +Note that the script downloads and extracts the raw COCO dataset to `--download-dir`. + +### YouTube-VIS 2021 + +Preliminaries: + +``` +poetry install -E coco +``` + +``` +python save_ytvis2021.py --split train --maxcount 32 --only-videos --resize --out-path /ytvis2021_resized +python save_ytvis2021.py --split validation --maxcount 10 --resize --out-path /ytvis2021_resized +``` + +### YouTube-VIS 2019 + +Preliminaries: +``` +poetry install -E tensorflow +``` + +``` +python save_ytvis2019.py --split train --maxcount 32 --only-videos --resize --out-path /ytvis2019_resized +python save_ytvis2019.py --split validation --maxcount 10 --resize --out-path /ytvis2019_resized +``` + +Note that we expect that raw files are downloaded in corresponding `ytvis2019_raw` and `ytvis2021_raw` folders. diff --git a/data/gdrive_download.sh b/data/gdrive_download.sh new file mode 100644 index 0000000..062b287 --- /dev/null +++ b/data/gdrive_download.sh @@ -0,0 +1,7 @@ +#!/bin/sh +# Script to download file from google drive using wget. +# First argument is file ID, +#second argument is path to file name for saving. + +wget --load-cookies /tmp/cookies.txt "https://docs.google.com/uc?export=download&confirm=$(wget --quiet --save-cookies /tmp/cookies.txt --keep-session-cookies --no-check-certificate 'https://docs.google.com/uc?export=download&id=FILEID' -O- | sed -rn 's/.*confirm=([0-9A-Za-z_]+).*/\1\n/p')&id=$1" -O $2 +rm -rf /tmp/cookies.txt diff --git a/data/save_coco.py b/data/save_coco.py new file mode 100644 index 0000000..8cd42fb --- /dev/null +++ b/data/save_coco.py @@ -0,0 +1,175 @@ +"""Script to download and shard COCO 2017.""" +import argparse +import os +import zipfile + +import numpy as np +import tqdm +import webdataset as wds +from PIL import Image +from pycocotools.coco import COCO +from utils import download_file, get_default_dir + +IMAGE_URL = "http://images.cocodataset.org/zips" +ANNOTATIONS_URL = "http://images.cocodataset.org/annotations" +SPLITS_TO_SUFFIX = { + "train": "train2017", + "validation": "val2017", + "test": "test2017", + "unlabeled": "unlabeled2017", +} +SPLITS_TO_ANNOTATIONS_ZIP = { + "train": "annotations_trainval2017.zip", + "validation": "annotations_trainval2017.zip", + "test": "image_info_test2017.zip", + "unlabeled": "image_info_unlabeled2017.zip", +} +SPLITS_TO_ANNOTATIONS_FILENAME = { + "train": "instances_train2017.json", + "validation": "instances_val2017.json", + "test": "image_info_test2017.json", + "unlabeled": "image_info_unlabeled2017.json", +} + + +parser = argparse.ArgumentParser("Generate sharded dataset from original COCO data.") +parser.add_argument( + "--split", default="train", choices=list(SPLITS_TO_SUFFIX), help="Which splits to write" +) +parser.add_argument( + "--download-dir", + default=get_default_dir("coco_raw"), + help="Directory where COCO images and annotations are downloaded to", +) +parser.add_argument("--maxcount", type=int, default=256, help="Max number of samples per shard") +parser.add_argument("--only-images", action="store_true", help="Whether to store only images") +parser.add_argument( + "--original-image-format", + action="store_true", + help="Whether to keep the orginal image encoding (e.g. jpeg), or convert to numpy", +) +parser.add_argument( + "--out-path", + default=get_default_dir("coco"), + help="Directory where shards are written", +) + + +def download_zip_and_extract(url, dest_dir): + print(f"Downloading {url} to {dest_dir}") + file = download_file(url, dest_dir) + print(f"\nExtracting {file} to {dest_dir}") + with zipfile.ZipFile(file, "r") as zip_ref: + zip_ref.extractall(dest_dir) + os.remove(file) + + +def get_coco_images(data_dir, split): + assert split in SPLITS_TO_SUFFIX + image_dir = os.path.join(data_dir, "images") + os.makedirs(image_dir, exist_ok=True) + download_zip_and_extract(f"{IMAGE_URL}/{SPLITS_TO_SUFFIX[split]}.zip", image_dir) + return image_dir + + +def get_coco_annotations(data_dir, annotation_zip): + os.makedirs(data_dir, exist_ok=True) + download_zip_and_extract(f"{ANNOTATIONS_URL}/{annotation_zip}", data_dir) + return data_dir + + +def write_dataset( + image_dir, + out_dir, + split, + add_annotations=True, + annotations_file=None, + convert_images_to_numpy=True, +): + if add_annotations and annotations_file is None: + raise ValueError("Requested to add annotations but annotations file not given") + + if not os.path.isdir(os.path.join(out_dir, ".")): + os.makedirs(os.path.join(out_dir, "."), exist_ok=True) + + # This is the output pattern under which we write shards. + pattern = os.path.join(out_dir, f"coco-{split}-%06d.tar") + + coco = COCO(annotations_file) + with wds.ShardWriter(pattern, maxcount=args.maxcount) as sink: + max_num_instances = 0 + for img_id, img in tqdm.tqdm(coco.imgs.items()): + img_filename = coco.loadImgs(img["id"])[0]["file_name"] + img_path = os.path.join(image_dir, img_filename) + + if convert_images_to_numpy: + img_key = "image.npy" + with Image.open(img_path).convert("RGB") as img: + img.load() + img = np.asarray(img, dtype="uint8") + assert len(img.shape) == 3 + else: + img_key = "image" + os.path.splitext(img_filename)[-1] + with open(img_path, "rb") as f: + img = f.read() + + if not add_annotations: + sample = { + "__key__": str(img_id), + img_key: img, + } + else: + ann_ids = coco.getAnnIds(img_id, iscrowd=False) # We filter out crowd labels + anns = coco.loadAnns(ann_ids) + if anns: + segmentations = np.array( + [coco.annToMask(ann) * ann["category_id"] for ann in anns], + dtype=np.uint8, + ) + assert len(segmentations.shape) == 3 + max_num_instances = max(max_num_instances, len(anns)) + else: + segmentations = np.zeros(shape=(1, img.shape[0], img.shape[1]), dtype=np.uint8) + + sample = { + "__key__": str(img_id), + "image.npy": img, + "segmentations.npy": segmentations, + } + sink.write(sample) + + if add_annotations: + print(f"Maximal number of instances per image in {split} split is: {max_num_instances}.") + + +if __name__ == "__main__": + args = parser.parse_args() + + if args.split in ("test", "unlabeled") and not args.only_images: + raise NotImplementedError( + f"Split {args.split} does not have any annotations. Run with --only-images" + ) + + if args.split in SPLITS_TO_ANNOTATIONS_FILENAME: + annotations_dir = os.path.join(args.download_dir, "annotations") + annotations_file = os.path.join(annotations_dir, SPLITS_TO_ANNOTATIONS_FILENAME[args.split]) + if not os.path.exists(annotations_file): + zip_file = SPLITS_TO_ANNOTATIONS_ZIP[args.split] + get_coco_annotations(args.download_dir, zip_file) + assert os.path.exists(annotations_file) + else: + annotations_file = None + + image_dir = os.path.join(args.download_dir, "images", SPLITS_TO_SUFFIX[args.split]) + if not os.path.exists(image_dir): + get_coco_images(args.download_dir, args.split) + assert os.path.exists(image_dir) + + write_dataset( + image_dir=image_dir, + out_dir=args.out_path, + split=args.split, + add_annotations=annotations_file is not None and not args.only_images, + annotations_file=annotations_file, + convert_images_to_numpy=not args.original_image_format, + ) diff --git a/data/save_davis.py b/data/save_davis.py new file mode 100644 index 0000000..abc0f26 --- /dev/null +++ b/data/save_davis.py @@ -0,0 +1,147 @@ +import argparse +import os +import os.path +import zipfile +from pathlib import Path + +import numpy as np +import tqdm +import webdataset as wds +from PIL import Image +from utils import download_file, get_default_dir + +parser = argparse.ArgumentParser("""Generate sharded dataset from original DAVIS data.""") +parser.add_argument( + "--split", default="train", choices=["train", "validation"], help="Which splits to write" +) +parser.add_argument("--root", default=get_default_dir("davis_raw")) +parser.add_argument("--maxcount", type=int, default=2, help="Max number of samples per shard") +parser.add_argument("--only-video", action="store_true", help="Whether to store only videos") +parser.add_argument( + "--out_path", + default=get_default_dir("davis"), + help="Directory where shards are written", +) + + +def listdirs(rootdir): + dirs = [] + for path in Path(rootdir).iterdir(): + if path.is_dir(): + dir_name = str(path) + folder_name = dir_name.split("/")[-1] + dirs.append((dir_name, folder_name)) + return dirs + + +def get_split_dirs(split_file): + with open(split_file) as f: + split = [line.rstrip() for line in f] + return split + + +def get_davis(dir): + # url = "https://data.vision.ee.ethz.ch/jpont/davis" + # file_trainval = "DAVIS-2017-trainval-480p.zip" + # should be more consisent for unsupervised object segmentation + url = "https://data.vision.ee.ethz.ch/csergi/share/davis" + file_trainval = "DAVIS-2017-Unsupervised-trainval-480p.zip" + data_dir = os.path.join(dir, "train_val") + os.makedirs(data_dir, exist_ok=True) + + file_local = os.path.join(data_dir, file_trainval) + if not os.path.isfile(file_local): + print("Downloading DAVIS 2017 (train-val)...") + download_file(f"{url}/{file_trainval}", dest_dir=data_dir) + with zipfile.ZipFile(file_local, "r") as zip_ref: + zip_ref.extractall(data_dir) + else: + print("Found DAVIS 2017 (train-val) file. Assume that dataset is already extracted.") + + +def make_dataset(root): + root = os.path.join(root, "train_val/DAVIS/") + video_root = os.path.join(root, "JPEGImages/480p/") + segmentation_root = os.path.join(root, "Annotations_unsupervised/480p/") + train_split = get_split_dirs(os.path.join(root, "ImageSets/2017/train.txt")) + val_split = get_split_dirs(os.path.join(root, "ImageSets/2017/val.txt")) + + train_data, val_data = [], [] + train_pathes, val_pathes = [], [] + for video_folder, target in listdirs(video_root): + segmentation_folder = os.path.join(segmentation_root, target) + if not os.path.isdir(video_folder): + continue + for root, _, fnames in sorted(os.walk(video_folder)): + video_frame_pathes = [] + video_frames = [] + segmentation_frames = [] + for fname in sorted(fnames): + video_frame_path = os.path.join(root, fname) + segmentation_frame_path = os.path.join(segmentation_folder, f"{fname[:-4]}.png") + with Image.open(video_frame_path) as img: + video_frame = np.asarray(img) + with Image.open(segmentation_frame_path) as img: + segmentation_frame = np.asarray(img) + video_frames.append(video_frame[None]) + segmentation_frames.append(segmentation_frame[None]) + item = (video_frame_path, segmentation_frame_path) + video_frame_pathes.append(item) + # video shape should be [B, F, H, W, 3] + # segmentation shape should be [B, F, H, W, 1] + video_frames = np.concatenate(video_frames) + segmentation_frames = np.concatenate(segmentation_frames)[..., None] + if target in train_split: + train_data.append({"video": video_frames, "segmentations": segmentation_frames}) + train_pathes.append(video_frame_pathes) + elif target in val_split: + val_data.append({"video": video_frames, "segmentations": segmentation_frames}) + val_pathes.append(video_frame_pathes) + else: + raise ValueError(f"{target} in not in splits") + return train_data, train_pathes, val_data, val_pathes + + +def write_dataset(data, split, out_path, only_video=False): + + if not os.path.isdir(os.path.join(out_path, ".")): + os.makedirs(os.path.join(out_path, "."), exist_ok=True) + # This is the output pattern under which we write shards. + pattern = os.path.join(out_path, f"davis-{split}-%06d.tar") + + if only_video: + keys_to_save = ["video"] + else: + keys_to_save = ["video", "segmentations"] + + print("Storing properties:", *keys_to_save) + + # with wds.TarWriter(pattern) as sink: + with wds.ShardWriter(pattern, maxcount=args.maxcount) as sink: + for ind, record in enumerate(tqdm.tqdm(data)): + # Construct a sample. + sample = {"__key__": str(ind)} + for k in keys_to_save: + sample[f"{k}.npy"] = record[k] + # Write the sample to the sharded tar archives. + sink.write(sample) + + +if __name__ == "__main__": + args = parser.parse_args() + get_davis(args.root) + train_data, train_pathes, val_data, val_pathes = make_dataset(args.root) + + print("DAVIS dataset loaded") + write_dataset( + train_data, + "train", + out_path=args.out_path, + only_video=args.only_video, + ) + write_dataset( + val_data, + "validation", + out_path=args.out_path, + only_video=args.only_video, + ) diff --git a/data/save_movi.py b/data/save_movi.py new file mode 100644 index 0000000..73df57d --- /dev/null +++ b/data/save_movi.py @@ -0,0 +1,63 @@ +import argparse +import os +import os.path + +import numpy as np +import tensorflow_datasets as tfds +import tqdm +import webdataset as wds + +parser = argparse.ArgumentParser("""Generate sharded dataset from original MOVI data.""") +parser.add_argument( + "--split", default="train", choices=["train", "validation"], help="Which splits to write" +) +parser.add_argument("--level", default="e") +parser.add_argument("--version", default="1.0.0") +parser.add_argument("--maxcount", type=int, default=32, help="Max number of samples per shard") +parser.add_argument("--image_size", type=int, default=128) +parser.add_argument("--only-video", action="store_true", help="Whether to store only videos") +parser.add_argument( + "out_path", + help="Directory where shards are written", +) + + +def write_dataset(args): + split = args.split + ds, ds_info = tfds.load( + f"movi_{args.level}/{args.image_size}x{args.image_size}:{args.version}", + data_dir="gs://kubric-public/tfds", + with_info=True, + ) + print(ds_info) + train_iter = iter(tfds.as_numpy(ds[args.split])) + + # This is the output pattern under which we write shards. + pattern = os.path.join(args.out_path, f"movi_{args.level}-{split}-%06d.tar") + + if args.only_video: + keys_to_save = ["video"] + else: + keys_to_save = ["video", "segmentations", "forward_flow"] + + print("Storing properties:", *keys_to_save) + + with wds.ShardWriter(pattern, maxcount=args.maxcount) as sink: + for ind, record in enumerate(tqdm.tqdm(train_iter)): + # Construct a sample. + sample = {"__key__": str(ind)} + for k in keys_to_save: + if k == "forward_flow": + out = record[k].astype(np.float32) + else: + out = record[k] + sample[f"{k}.npy"] = out + # Write the sample to the sharded tar archives. + sink.write(sample) + + +if __name__ == "__main__": + args = parser.parse_args() + if not os.path.isdir(os.path.join(args.out_path, ".")): + os.makedirs(os.path.join(args.out_path, "."), exist_ok=True) + write_dataset(args) diff --git a/data/save_ytvis2019.py b/data/save_ytvis2019.py new file mode 100644 index 0000000..47d54ad --- /dev/null +++ b/data/save_ytvis2019.py @@ -0,0 +1,136 @@ +import argparse +import os +import os.path + +# import subprocess +import einops +import numpy as np +import tensorflow_datasets as tfds +import torch +import tqdm +import webdataset as wds +from utils import get_default_dir + +from videosaur.data.transforms import Resize + +parser = argparse.ArgumentParser( + """Generate sharded dataset from tensorflow datasets YouTube VIS data.""" +) +parser.add_argument( + "--split", default="train", choices=["train", "validation"], help="Which splits to write" +) +parser.add_argument("--maxcount", type=int, default=32, help="Max number of samples per shard") +parser.add_argument("--only-videos", action="store_true", help="Whether to store only videos") +parser.add_argument("--resize", action="store_true", help="Whether to resize videos") +parser.add_argument( + "--resize_size", type=int, default=320, help="The size of min(H,W) after resizing" +) +parser.add_argument("--resize_mode", type=str, default="nearest-exact", help="Resize mode") + +parser.add_argument( + "--out-path", + default=get_default_dir("ytvis2019_resized"), + help="Directory where shards are written", +) +parser.add_argument( + "--download-dir", + default=get_default_dir("ytvis2019_raw"), + help="Directory where YTVIS 2019 videos and annotations are downloaded to", +) + +# Download is currently not supported +# because of the networks error during gdrive downloads +# Folow intstuctions here +# https://www.tensorflow.org/datasets/catalog/youtube_vis +# for manual download + +# GDRIVE_IDS = { +# # "valid.json": "106ozt8JJyQWI1RO8Uh18aOBqngznUhir", +# # "test.json": "1GwkyVcc6wVvpPmnMWHa5yLcJ2jJ67vNW", +# # "train.json": "1OgdUC29rPN3jAcM6DjpORv6AlNEpVtGc", +# # "train_all_frames.zip": "1eRO0rQjO6gEh0T9Ua5REfbreFCNY7cfq", +# # "valid_all_frames.zip": "1rWQzZcMskgpEQOZdJPJ7eTmLCBEIIpEN", +# # "test_all_frames.zip": "1WAc-AyR2vQ6UpOv8iw1p1MoBry-D8Rav", +# } + +# def download_dataset(download_dir): +# for file_name, gdrive_id in GDRIVE_IDS.items(): +# print(f"Done for {file_name}") +# subprocess.run( +# [ +# "bash", +# "/is/sg2/azadaianchuk/projects/videosaur/data/gdrive_download.sh", +# gdrive_id, +# f"{download_dir}/{file_name}", +# ] +# ) + + +def write_dataset(args): + split = args.split + ds, _ = tfds.load( + "youtube_vis/only_frames_with_labels_train_split", + data_dir=args.download_dir, + with_info=True, + ) + dataset_iter = iter(tfds.as_numpy(ds[args.split])) + + # This is the output pattern under which we write shards. + pattern = os.path.join(args.out_path, f"ytvis-{split}-%06d.tar") + if args.resize: + assert args.resize_mode in [ + "nearest-exact" + ], "Not sure how downscaling works with other modes." + resize = Resize( + args.resize_size, args.resize_mode, clamp_zero_one=False, short_side_scale=True + ) + with wds.ShardWriter(pattern, maxcount=args.maxcount) as sink: + for ind, record in enumerate(tqdm.tqdm(dataset_iter)): + # Construct a sample. + sample = {"__key__": str(ind)} + video = record["video"] + if args.resize: + video = torch.from_numpy(record["video"]) + video = einops.rearrange(video, "f h w c -> f c h w") + video = resize(video).cpu().numpy() + video = einops.rearrange(video, "f c h w -> f h w c") + sample["video.npy"] = video + if not args.only_videos: + length, h, w, _ = record["video"].shape + + n_obj = len(record["tracks"]["segmentations"].numpy()) + if n_obj == 0: + out = np.zeros(shape=(length, h, w, 1), dtype=np.uint8) + else: + out = np.zeros(shape=(length, h, w, n_obj), dtype=np.uint8) + for i, (frame_id, cat_id, seg) in enumerate( + zip( + record["tracks"]["frames"], + record["tracks"]["category"], + record["tracks"]["segmentations"], + ) + ): + out[frame_id.numpy(), :, :, i : i + 1] = seg.numpy() * cat_id + assert ( + np.unique(out) + == np.unique(np.asarray(list(record["tracks"]["category"]) + [0])) + ).all() + if args.resize: + out = torch.from_numpy(out) + out = einops.rearrange(out, "f h w c -> f c h w") + out = resize(out).cpu().numpy() + out = einops.rearrange(out, "f c h w -> f h w c") + sample["segmentations.npy"] = out + assert out.shape[:-1] == video.shape[:-1] + # Write the sample to the sharded tar archives. + sink.write(sample) + + +if __name__ == "__main__": + args = parser.parse_args() + # if not os.path.isdir(args.download_dir): + # os.makedirs(args.download_dir, exist_ok=True) + # download_dataset(args.download_dir) + if not os.path.isdir(os.path.join(args.out_path, ".")): + os.makedirs(os.path.join(args.out_path, "."), exist_ok=True) + write_dataset(args) diff --git a/data/save_ytvis2021.py b/data/save_ytvis2021.py new file mode 100644 index 0000000..fa85a8e --- /dev/null +++ b/data/save_ytvis2021.py @@ -0,0 +1,170 @@ +"""Script to download and shard YouTube VIS dataset.""" +import argparse +import os + +import einops +import numpy as np +import torch +import tqdm +import webdataset as wds +from einops import rearrange +from PIL import Image +from utils import get_default_dir +from ytvis import YTVOS as YTVIS + +from videosaur.data.transforms import Resize +from videosaur.data.transforms_video import FromTensorVideo, ToTensorVideo + +# TODO: add download from google drive +# for now use gdrive_download script with IDs above. +# TRAIN_GDRIVE_ID = "1fLO_d8ys_03CPPpCm-lL5SgCJsbNN5L9" +# VAL_GDRIVE_ID = "1Z0VEOQp4d0-aiZNDt8Hd5KdcCU4s8JaY" + +SPLITS_TO_SUFFIX = { + "train": "train", + "validation": "valid", +} +SPLITS_TO_INSTANCES_FILENAME = { + "train": "instances.json", + "validation": "instances.json", +} + + +parser = argparse.ArgumentParser("Generate sharded dataset from original YouTube-VIS data.") +parser.add_argument( + "--split", default="train", choices=list(SPLITS_TO_SUFFIX), help="Which splits to write" +) +parser.add_argument( + "--download-dir", + default=get_default_dir("ytvis2021_raw"), + help="Directory where YTVIS 2021 videos and annotations are downloaded to", +) +parser.add_argument("--maxcount", type=int, default=32, help="Max number of samples per shard") +parser.add_argument("--only-videos", action="store_true", help="Whether to store only videos") +parser.add_argument("--resize", action="store_true", help="Whether to resize videos") +parser.add_argument( + "--resize_size", type=int, default=320, help="The size of min(H,W) after resizing" +) +parser.add_argument("--resize_mode", type=str, default="nearest-exact", help="Resize mode") +parser.add_argument( + "--out-path", + default=get_default_dir("ytvis2021_resized2"), + help="Directory where shards are written", +) + + +def write_dataset(video_dir, out_dir, split, add_annotations=True, annotations_file=None): + if add_annotations and annotations_file is None: + raise ValueError("Requested to add annotations but annotations file not given") + + if not os.path.isdir(os.path.join(out_dir, ".")): + os.makedirs(os.path.join(out_dir, "."), exist_ok=True) + + # This is the output pattern under which we write shards. + pattern = os.path.join(out_dir, f"ytvis-{split}-%06d.tar") + if args.resize: + clamp_zero_one = args.resize_mode == "bicubic" + resize = Resize( + args.resize_size, args.resize_mode, clamp_zero_one=clamp_zero_one, short_side_scale=True + ) + segmentation_resize = Resize( + args.resize_size, "nearest-exact", clamp_zero_one=False, short_side_scale=True + ) + to_video = ToTensorVideo() + from_video = FromTensorVideo() + ytvis = YTVIS(annotations_file) + all_ids = ytvis.getVidIds() + val_ids = all_ids[-300:] + train_ids = all_ids[:-300] + ids = train_ids if split == "train" else val_ids + print(f"Saving {len(ids)} from {len(all_ids)} as a {split} part of the datasets.") + with wds.ShardWriter(pattern, maxcount=args.maxcount) as sink: + max_num_instances = 0 + for vid_id in tqdm.tqdm(ids): + vid = ytvis.vids[vid_id] + length = int(vid["length"]) + height = int(vid["height"]) + width = int(vid["width"]) + video = [] + + for _, file_name in enumerate(vid["file_names"]): + frame_path = os.path.join(video_dir, file_name) + with Image.open(frame_path) as img: + img = np.asarray(img, dtype="uint8") + video.append(img[None]) + video = np.concatenate(video) + # video shape should be [B, F, H, W, 3] + assert video.shape == (length, height, width, 3) + if args.resize: + video = to_video(torch.from_numpy(video)) + video = from_video(resize(video)).cpu().numpy() + + if not add_annotations: + sample = { + "__key__": str(vid_id), + "video.npy": video, + } + else: + + segmentations = [] + ann_ids = ytvis.getAnnIds(vidIds=[vid_id], iscrowd=False) + anns = ytvis.loadAnns(ann_ids) + if anns: + for f_id, _ in enumerate(vid["file_names"]): + seg = [] + num_obj = len(anns) + max_num_instances = max(max_num_instances, num_obj) + for ann in anns: + try: + mask = ytvis.annToMask(ann, f_id) * ann["category_id"] + except TypeError: + # TODO: check if this is the right way to do this. + # e.g. that error actually means that instance + # is not visible in the image + mask = np.zeros( + (int(vid["height"]), int(vid["width"])), dtype=np.uint8 + ) + seg.append(mask) + segmentations.append(seg) + segmentations = np.asarray(segmentations, dtype=np.uint8) + segmentations = rearrange(segmentations, "f i h w -> f h w i") + + # segmentation shape should be [B, F, H, W, 1] + assert segmentations.shape == ( + length, + height, + width, + num_obj, + ) + else: + segmentations = np.zeros( + (length, int(vid["height"]), int(vid["width"]), 1), dtype=np.uint8 + ) + if args.resize: + segmentations = torch.from_numpy(segmentations) + segmentations = einops.rearrange(segmentations, "f h w c -> f c h w") + segmentations = segmentation_resize(segmentations).cpu().numpy() + segmentations = einops.rearrange(segmentations, "f c h w -> f h w c") + sample = { + "__key__": str(vid_id), + "video.npy": video, + "segmentations.npy": segmentations, + } + sink.write(sample) + + if add_annotations: + print(f"Maximal number of instances per video in {split} split is: {max_num_instances}.") + + +if __name__ == "__main__": + args = parser.parse_args() + root = os.path.join(args.download_dir, "train") + annotations_file = os.path.join(root, "instances.json") + video_dir = os.path.join(root, "JPEGImages") + write_dataset( + video_dir=video_dir, + out_dir=args.out_path, + split=args.split, + add_annotations=annotations_file is not None and not args.only_videos, + annotations_file=annotations_file, + ) diff --git a/data/utils.py b/data/utils.py new file mode 100644 index 0000000..30701e8 --- /dev/null +++ b/data/utils.py @@ -0,0 +1,47 @@ +import math +import os +import pathlib +import re +import urllib.parse + +import requests +import tqdm + + +def get_default_dir(subdir): + try: + import videosaur.data + + default_dir = videosaur.data.get_data_root_dir() + except ImportError: + default_dir = "./data" + + return os.path.join(default_dir, subdir) + + +def download_file(url: str, dest_dir: str) -> pathlib.Path: + """Download file to location and return path to location. + + Adapted from https://stackoverflow.com/a/10744565 and https://stackoverflow.com/a/53299682. + """ + response = requests.get(url, stream=True) + + if "Content-Disposition" in response.headers: + file_name = re.findall("filename=(.+)", response.headers["Content-Disposition"])[0] + else: + url_path = urllib.parse.unquote(urllib.parse.urlparse(url).path) + file_name = pathlib.Path(url_path).name + + if "Content-Length" in response.headers: + file_size_kb = math.ceil(int(response.headers["Content-Length"]) / 1024) + else: + file_size_kb = None + + dest_file = pathlib.Path(dest_dir) / (file_name + ".tmp") + with open(dest_file, "wb") as handle: + for data in tqdm.tqdm(response.iter_content(chunk_size=1024), unit="kB", total=file_size_kb): + handle.write(data) + + dest_file = dest_file.rename(dest_file.with_name(file_name)) + + return dest_file diff --git a/data/ytvis.py b/data/ytvis.py new file mode 100644 index 0000000..f50c163 --- /dev/null +++ b/data/ytvis.py @@ -0,0 +1,292 @@ +__author__ = "ychfan" +# Interface for accessing the YouTubeVIS dataset. + +# The following API functions are defined: +# YTVOS - YTVOS api class that loads YouTubeVIS annotation file and prepare data structures. +# decodeMask - Decode binary mask M encoded via run-length encoding. +# encodeMask - Encode binary mask M using run-length encoding. +# getAnnIds - Get ann ids that satisfy given filter conditions. +# getCatIds - Get cat ids that satisfy given filter conditions. +# getImgIds - Get img ids that satisfy given filter conditions. +# loadAnns - Load anns with the specified ids. +# loadCats - Load cats with the specified ids. +# loadImgs - Load imgs with the specified ids. +# annToMask - Convert segmentation in an annotation to binary mask. +# loadRes - Load algorithm results and create API for accessing them. + +# Microsoft COCO Toolbox. version 2.0 +# Data, paper, and tutorials available at: http://mscoco.org/ +# Code written by Piotr Dollar and Tsung-Yi Lin, 2014. +# Licensed under the Simplified BSD License [see bsd.txt] + +import copy +import itertools +import json +import time +from collections import defaultdict + +import numpy as np +from pycocotools import mask as maskUtils + + +def _isArrayLike(obj): + return hasattr(obj, "__iter__") and hasattr(obj, "__len__") + + +class YTVOS: + def __init__(self, annotation_file=None): + """ + Constructor of Microsoft COCO helper class for reading and visualizing annotations. + :param annotation_file (str): location of annotation file + :param image_folder (str): location to the folder that hosts images. + :return: + """ + # load dataset + self.dataset, self.anns, self.cats, self.vids = dict(), dict(), dict(), dict() + self.vidToAnns, self.catToVids = defaultdict(list), defaultdict(list) + if annotation_file is not None: + print("loading annotations into memory...") + tic = time.time() + dataset = json.load(open(annotation_file, "r")) + assert isinstance(dataset, dict), "annotation file format {} not supported".format( + type(dataset) + ) + print("Done (t={:0.2f}s)".format(time.time() - tic)) + self.dataset = dataset + self.createIndex() + + def createIndex(self): + # create index + print("creating index...") + anns, cats, vids = {}, {}, {} + vidToAnns, catToVids = defaultdict(list), defaultdict(list) + if "annotations" in self.dataset: + for ann in self.dataset["annotations"]: + vidToAnns[ann["video_id"]].append(ann) + anns[ann["id"]] = ann + + if "videos" in self.dataset: + for vid in self.dataset["videos"]: + vids[vid["id"]] = vid + + if "categories" in self.dataset: + for cat in self.dataset["categories"]: + cats[cat["id"]] = cat + + if "annotations" in self.dataset and "categories" in self.dataset: + for ann in self.dataset["annotations"]: + catToVids[ann["category_id"]].append(ann["video_id"]) + + print("index created!") + + # create class members + self.anns = anns + self.vidToAnns = vidToAnns + self.catToVids = catToVids + self.vids = vids + self.cats = cats + + def info(self): + """ + Print information about the annotation file. + :return: + """ + for key, value in self.dataset["info"].items(): + print("{}: {}".format(key, value)) + + def getAnnIds(self, vidIds=(), catIds=(), areaRng=(), iscrowd=None): + """ + Get ann ids that satisfy given filter conditions. default skips that filter + :param vidIds (int array) : get anns for given vids + catIds (int array) : get anns for given cats + areaRng (float array) : get anns for given area range (e.g. [0 inf]) + iscrowd (boolean) : get anns for given crowd label (False or True) + :return: ids (int array) : integer array of ann ids + """ + vidIds = vidIds if _isArrayLike(vidIds) else [vidIds] + catIds = catIds if _isArrayLike(catIds) else [catIds] + + if len(vidIds) == len(catIds) == len(areaRng) == 0: + anns = self.dataset["annotations"] + else: + if not len(vidIds) == 0: + lists = [self.vidToAnns[vidId] for vidId in vidIds if vidId in self.vidToAnns] + anns = list(itertools.chain.from_iterable(lists)) + else: + anns = self.dataset["annotations"] + anns = ( + anns if len(catIds) == 0 else [ann for ann in anns if ann["category_id"] in catIds] + ) + anns = ( + anns + if len(areaRng) == 0 + else [ + ann + for ann in anns + if ann["avg_area"] > areaRng[0] and ann["avg_area"] < areaRng[1] + ] + ) + if iscrowd is not None: + ids = [ann["id"] for ann in anns if ann["iscrowd"] == iscrowd] + else: + ids = [ann["id"] for ann in anns] + return ids + + def getCatIds(self, catNms=(), supNms=(), catIds=()): + """ + filtering parameters. default skips that filter. + :param catNms (str array) : get cats for given cat names + :param supNms (str array) : get cats for given supercategory names + :param catIds (int array) : get cats for given cat ids + :return: ids (int array) : integer array of cat ids + """ + catNms = catNms if _isArrayLike(catNms) else [catNms] + supNms = supNms if _isArrayLike(supNms) else [supNms] + catIds = catIds if _isArrayLike(catIds) else [catIds] + + if len(catNms) == len(supNms) == len(catIds) == 0: + cats = self.dataset["categories"] + else: + cats = self.dataset["categories"] + cats = cats if len(catNms) == 0 else [cat for cat in cats if cat["name"] in catNms] + cats = ( + cats if len(supNms) == 0 else [cat for cat in cats if cat["supercategory"] in supNms] + ) + cats = cats if len(catIds) == 0 else [cat for cat in cats if cat["id"] in catIds] + ids = [cat["id"] for cat in cats] + return ids + + def getVidIds(self, vidIds=(), catIds=()): + """ + Get vid ids that satisfy given filter conditions. + :param vidIds (int array) : get vids for given ids + :param catIds (int array) : get vids with all given cats + :return: ids (int array) : integer array of vid ids + """ + vidIds = vidIds if _isArrayLike(vidIds) else [vidIds] + catIds = catIds if _isArrayLike(catIds) else [catIds] + + if len(vidIds) == len(catIds) == 0: + ids = self.vids.keys() + else: + ids = set(vidIds) + for i, catId in enumerate(catIds): + if i == 0 and len(ids) == 0: + ids = set(self.catToVids[catId]) + else: + ids &= set(self.catToVids[catId]) + return list(ids) + + def loadAnns(self, ids=()): + """ + Load anns with the specified ids. + :param ids (int array) : integer ids specifying anns + :return: anns (object array) : loaded ann objects + """ + if _isArrayLike(ids): + return [self.anns[id] for id in ids] + elif isinstance(ids, int): + return [self.anns[ids]] + + def loadCats(self, ids=()): + """ + Load cats with the specified ids. + :param ids (int array) : integer ids specifying cats + :return: cats (object array) : loaded cat objects + """ + if _isArrayLike(ids): + return [self.cats[id] for id in ids] + elif isinstance(ids, int): + return [self.cats[ids]] + + def loadVids(self, ids=()): + """ + Load anns with the specified ids. + :param ids (int array) : integer ids specifying vid + :return: vids (object array) : loaded vid objects + """ + if _isArrayLike(ids): + return [self.vids[id] for id in ids] + elif isinstance(ids, int): + return [self.vids[ids]] + + def loadRes(self, resFile): + """ + Load result file and return a result api object. + :param resFile (str) : file name of result file + :return: res (obj) : result api object + """ + res = YTVOS() + res.dataset["videos"] = [img for img in self.dataset["videos"]] + + print("Loading and preparing results...") + tic = time.time() + if isinstance(resFile, str): + anns = json.load(open(resFile)) + elif isinstance(resFile, np.ndarray): + anns = self.loadNumpyAnnotations(resFile) + else: + anns = resFile + assert isinstance(anns, list), "results in not an array of objects" + annsVidIds = [ann["video_id"] for ann in anns] + assert set(annsVidIds) == ( + set(annsVidIds) & set(self.getVidIds()) + ), "Results do not correspond to current coco set" + if "segmentations" in anns[0]: + res.dataset["categories"] = copy.deepcopy(self.dataset["categories"]) + for id, ann in enumerate(anns): + ann["areas"] = [] + if "bboxes" not in ann: + ann["bboxes"] = [] + for seg in ann["segmentations"]: + # now only support compressed RLE format as segmentation results + if seg: + ann["areas"].append(maskUtils.area(seg)) + if len(ann["bboxes"]) < len(ann["areas"]): + ann["bboxes"].append(maskUtils.toBbox(seg)) + else: + ann["areas"].append(None) + if len(ann["bboxes"]) < len(ann["areas"]): + ann["bboxes"].append(None) + ann["id"] = id + 1 + list_ = [a for a in ann["areas"] if a] + if len(list_) == 0: + ann["avg_area"] = 0 + else: + ann["avg_area"] = np.array(list_).mean() + ann["iscrowd"] = 0 + print("DONE (t={:0.2f}s)".format(time.time() - tic)) + + res.dataset["annotations"] = anns + res.createIndex() + return res + + def annToRLE(self, ann, frameId): + """ + Convert annotation which can be polygons, uncompressed RLE to RLE. + :return: binary mask (numpy 2D array) + """ + t = self.vids[ann["video_id"]] + h, w = t["height"], t["width"] + segm = ann["segmentations"][frameId] + if isinstance(segm, list): + # polygon -- a single object might consist of multiple parts + # we merge all parts into one mask rle code + rles = maskUtils.frPyObjects(segm, h, w) + rle = maskUtils.merge(rles) + elif isinstance(segm["counts"], list): + # uncompressed RLE + rle = maskUtils.frPyObjects(segm, h, w) + else: + # rle + rle = segm + return rle + + def annToMask(self, ann, frameId): + """ + Convert annotation which can be polygons, uncompressed RLE, or RLE to binary mask. + :return: binary mask (numpy 2D array) + """ + rle = self.annToRLE(ann, frameId) + m = maskUtils.decode(rle) + return m diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..7a09661 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,4621 @@ +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. + +[[package]] +name = "absl-py" +version = "1.3.0" +description = "Abseil Python Common Libraries, see https://github.com/abseil/abseil-py." +optional = true +python-versions = ">=3.6" +files = [ + {file = "absl-py-1.3.0.tar.gz", hash = "sha256:463c38a08d2e4cef6c498b76ba5bd4858e4c6ef51da1a5a1f27139a022e20248"}, + {file = "absl_py-1.3.0-py3-none-any.whl", hash = "sha256:34995df9bd7a09b3b8749e230408f5a2a2dd7a68a0d33c12a3d0cb15a041a507"}, +] + +[[package]] +name = "aiohttp" +version = "3.8.3" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ba71c9b4dcbb16212f334126cc3d8beb6af377f6703d9dc2d9fb3874fd667ee9"}, + {file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d24b8bb40d5c61ef2d9b6a8f4528c2f17f1c5d2d31fed62ec860f6006142e83e"}, + {file = "aiohttp-3.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f88df3a83cf9df566f171adba39d5bd52814ac0b94778d2448652fc77f9eb491"}, + {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97decbb3372d4b69e4d4c8117f44632551c692bb1361b356a02b97b69e18a62"}, + {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:309aa21c1d54b8ef0723181d430347d7452daaff93e8e2363db8e75c72c2fb2d"}, + {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad5383a67514e8e76906a06741febd9126fc7c7ff0f599d6fcce3e82b80d026f"}, + {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20acae4f268317bb975671e375493dbdbc67cddb5f6c71eebdb85b34444ac46b"}, + {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05a3c31c6d7cd08c149e50dc7aa2568317f5844acd745621983380597f027a18"}, + {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d6f76310355e9fae637c3162936e9504b4767d5c52ca268331e2756e54fd4ca5"}, + {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:256deb4b29fe5e47893fa32e1de2d73c3afe7407738bd3c63829874661d4822d"}, + {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5c59fcd80b9049b49acd29bd3598cada4afc8d8d69bd4160cd613246912535d7"}, + {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:059a91e88f2c00fe40aed9031b3606c3f311414f86a90d696dd982e7aec48142"}, + {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2feebbb6074cdbd1ac276dbd737b40e890a1361b3cc30b74ac2f5e24aab41f7b"}, + {file = "aiohttp-3.8.3-cp310-cp310-win32.whl", hash = "sha256:5bf651afd22d5f0c4be16cf39d0482ea494f5c88f03e75e5fef3a85177fecdeb"}, + {file = "aiohttp-3.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:653acc3880459f82a65e27bd6526e47ddf19e643457d36a2250b85b41a564715"}, + {file = "aiohttp-3.8.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:86fc24e58ecb32aee09f864cb11bb91bc4c1086615001647dbfc4dc8c32f4008"}, + {file = "aiohttp-3.8.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75e14eac916f024305db517e00a9252714fce0abcb10ad327fb6dcdc0d060f1d"}, + {file = "aiohttp-3.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d1fde0f44029e02d02d3993ad55ce93ead9bb9b15c6b7ccd580f90bd7e3de476"}, + {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ab94426ddb1ecc6a0b601d832d5d9d421820989b8caa929114811369673235c"}, + {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89d2e02167fa95172c017732ed7725bc8523c598757f08d13c5acca308e1a061"}, + {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02f9a2c72fc95d59b881cf38a4b2be9381b9527f9d328771e90f72ac76f31ad8"}, + {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7149272fb5834fc186328e2c1fa01dda3e1fa940ce18fded6d412e8f2cf76d"}, + {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:512bd5ab136b8dc0ffe3fdf2dfb0c4b4f49c8577f6cae55dca862cd37a4564e2"}, + {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7018ecc5fe97027214556afbc7c502fbd718d0740e87eb1217b17efd05b3d276"}, + {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:88c70ed9da9963d5496d38320160e8eb7e5f1886f9290475a881db12f351ab5d"}, + {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:da22885266bbfb3f78218dc40205fed2671909fbd0720aedba39b4515c038091"}, + {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:e65bc19919c910127c06759a63747ebe14f386cda573d95bcc62b427ca1afc73"}, + {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:08c78317e950e0762c2983f4dd58dc5e6c9ff75c8a0efeae299d363d439c8e34"}, + {file = "aiohttp-3.8.3-cp311-cp311-win32.whl", hash = "sha256:45d88b016c849d74ebc6f2b6e8bc17cabf26e7e40c0661ddd8fae4c00f015697"}, + {file = "aiohttp-3.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:96372fc29471646b9b106ee918c8eeb4cca423fcbf9a34daa1b93767a88a2290"}, + {file = "aiohttp-3.8.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c971bf3786b5fad82ce5ad570dc6ee420f5b12527157929e830f51c55dc8af77"}, + {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff25f48fc8e623d95eca0670b8cc1469a83783c924a602e0fbd47363bb54aaca"}, + {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e381581b37db1db7597b62a2e6b8b57c3deec95d93b6d6407c5b61ddc98aca6d"}, + {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db19d60d846283ee275d0416e2a23493f4e6b6028825b51290ac05afc87a6f97"}, + {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25892c92bee6d9449ffac82c2fe257f3a6f297792cdb18ad784737d61e7a9a85"}, + {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398701865e7a9565d49189f6c90868efaca21be65c725fc87fc305906be915da"}, + {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4a4fbc769ea9b6bd97f4ad0b430a6807f92f0e5eb020f1e42ece59f3ecfc4585"}, + {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:b29bfd650ed8e148f9c515474a6ef0ba1090b7a8faeee26b74a8ff3b33617502"}, + {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:1e56b9cafcd6531bab5d9b2e890bb4937f4165109fe98e2b98ef0dcfcb06ee9d"}, + {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ec40170327d4a404b0d91855d41bfe1fe4b699222b2b93e3d833a27330a87a6d"}, + {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2df5f139233060578d8c2c975128fb231a89ca0a462b35d4b5fcf7c501ebdbe1"}, + {file = "aiohttp-3.8.3-cp36-cp36m-win32.whl", hash = "sha256:f973157ffeab5459eefe7b97a804987876dd0a55570b8fa56b4e1954bf11329b"}, + {file = "aiohttp-3.8.3-cp36-cp36m-win_amd64.whl", hash = "sha256:437399385f2abcd634865705bdc180c8314124b98299d54fe1d4c8990f2f9494"}, + {file = "aiohttp-3.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:09e28f572b21642128ef31f4e8372adb6888846f32fecb288c8b0457597ba61a"}, + {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f3553510abdbec67c043ca85727396ceed1272eef029b050677046d3387be8d"}, + {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e168a7560b7c61342ae0412997b069753f27ac4862ec7867eff74f0fe4ea2ad9"}, + {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db4c979b0b3e0fa7e9e69ecd11b2b3174c6963cebadeecfb7ad24532ffcdd11a"}, + {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e164e0a98e92d06da343d17d4e9c4da4654f4a4588a20d6c73548a29f176abe2"}, + {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8a78079d9a39ca9ca99a8b0ac2fdc0c4d25fc80c8a8a82e5c8211509c523363"}, + {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:21b30885a63c3f4ff5b77a5d6caf008b037cb521a5f33eab445dc566f6d092cc"}, + {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4b0f30372cef3fdc262f33d06e7b411cd59058ce9174ef159ad938c4a34a89da"}, + {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:8135fa153a20d82ffb64f70a1b5c2738684afa197839b34cc3e3c72fa88d302c"}, + {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:ad61a9639792fd790523ba072c0555cd6be5a0baf03a49a5dd8cfcf20d56df48"}, + {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:978b046ca728073070e9abc074b6299ebf3501e8dee5e26efacb13cec2b2dea0"}, + {file = "aiohttp-3.8.3-cp37-cp37m-win32.whl", hash = "sha256:0d2c6d8c6872df4a6ec37d2ede71eff62395b9e337b4e18efd2177de883a5033"}, + {file = "aiohttp-3.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:21d69797eb951f155026651f7e9362877334508d39c2fc37bd04ff55b2007091"}, + {file = "aiohttp-3.8.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ca9af5f8f5812d475c5259393f52d712f6d5f0d7fdad9acdb1107dd9e3cb7eb"}, + {file = "aiohttp-3.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d90043c1882067f1bd26196d5d2db9aa6d268def3293ed5fb317e13c9413ea4"}, + {file = "aiohttp-3.8.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d737fc67b9a970f3234754974531dc9afeea11c70791dcb7db53b0cf81b79784"}, + {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebf909ea0a3fc9596e40d55d8000702a85e27fd578ff41a5500f68f20fd32e6c"}, + {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5835f258ca9f7c455493a57ee707b76d2d9634d84d5d7f62e77be984ea80b849"}, + {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da37dcfbf4b7f45d80ee386a5f81122501ec75672f475da34784196690762f4b"}, + {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87f44875f2804bc0511a69ce44a9595d5944837a62caecc8490bbdb0e18b1342"}, + {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:527b3b87b24844ea7865284aabfab08eb0faf599b385b03c2aa91fc6edd6e4b6"}, + {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d5ba88df9aa5e2f806650fcbeedbe4f6e8736e92fc0e73b0400538fd25a4dd96"}, + {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e7b8813be97cab8cb52b1375f41f8e6804f6507fe4660152e8ca5c48f0436017"}, + {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:2dea10edfa1a54098703cb7acaa665c07b4e7568472a47f4e64e6319d3821ccf"}, + {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:713d22cd9643ba9025d33c4af43943c7a1eb8547729228de18d3e02e278472b6"}, + {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2d252771fc85e0cf8da0b823157962d70639e63cb9b578b1dec9868dd1f4f937"}, + {file = "aiohttp-3.8.3-cp38-cp38-win32.whl", hash = "sha256:66bd5f950344fb2b3dbdd421aaa4e84f4411a1a13fca3aeb2bcbe667f80c9f76"}, + {file = "aiohttp-3.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:84b14f36e85295fe69c6b9789b51a0903b774046d5f7df538176516c3e422446"}, + {file = "aiohttp-3.8.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16c121ba0b1ec2b44b73e3a8a171c4f999b33929cd2397124a8c7fcfc8cd9e06"}, + {file = "aiohttp-3.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8d6aaa4e7155afaf994d7924eb290abbe81a6905b303d8cb61310a2aba1c68ba"}, + {file = "aiohttp-3.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43046a319664a04b146f81b40e1545d4c8ac7b7dd04c47e40bf09f65f2437346"}, + {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599418aaaf88a6d02a8c515e656f6faf3d10618d3dd95866eb4436520096c84b"}, + {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a2964319d359f494f16011e23434f6f8ef0434acd3cf154a6b7bec511e2fb7"}, + {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73a4131962e6d91109bca6536416aa067cf6c4efb871975df734f8d2fd821b37"}, + {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598adde339d2cf7d67beaccda3f2ce7c57b3b412702f29c946708f69cf8222aa"}, + {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75880ed07be39beff1881d81e4a907cafb802f306efd6d2d15f2b3c69935f6fb"}, + {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a0239da9fbafd9ff82fd67c16704a7d1bccf0d107a300e790587ad05547681c8"}, + {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4e3a23ec214e95c9fe85a58470b660efe6534b83e6cbe38b3ed52b053d7cb6ad"}, + {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:47841407cc89a4b80b0c52276f3cc8138bbbfba4b179ee3acbd7d77ae33f7ac4"}, + {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:54d107c89a3ebcd13228278d68f1436d3f33f2dd2af5415e3feaeb1156e1a62c"}, + {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c37c5cce780349d4d51739ae682dec63573847a2a8dcb44381b174c3d9c8d403"}, + {file = "aiohttp-3.8.3-cp39-cp39-win32.whl", hash = "sha256:f178d2aadf0166be4df834c4953da2d7eef24719e8aec9a65289483eeea9d618"}, + {file = "aiohttp-3.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:88e5be56c231981428f4f506c68b6a46fa25c4123a2e86d156c58a8369d31ab7"}, + {file = "aiohttp-3.8.3.tar.gz", hash = "sha256:3828fb41b7203176b82fe5d699e0d845435f2374750a44b480ea6b930f6be269"}, +] + +[package.dependencies] +aiosignal = ">=1.1.2" +async-timeout = ">=4.0.0a3,<5.0" +attrs = ">=17.3.0" +charset-normalizer = ">=2.0,<3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns", "cchardet"] + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "antlr4-python3-runtime" +version = "4.9.3" +description = "ANTLR 4.9.3 runtime for Python 3.7" +optional = false +python-versions = "*" +files = [ + {file = "antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b"}, +] + +[[package]] +name = "anyio" +version = "3.6.2" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = true +python-versions = ">=3.6.2" +files = [ + {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, + {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] +trio = ["trio (>=0.16,<0.22)"] + +[[package]] +name = "appnope" +version = "0.1.3" +description = "Disable App Nap on macOS >= 10.9" +optional = true +python-versions = "*" +files = [ + {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, + {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, +] + +[[package]] +name = "argon2-cffi" +version = "21.3.0" +description = "The secure Argon2 password hashing algorithm." +optional = true +python-versions = ">=3.6" +files = [ + {file = "argon2-cffi-21.3.0.tar.gz", hash = "sha256:d384164d944190a7dd7ef22c6aa3ff197da12962bd04b17f64d4e93d934dba5b"}, + {file = "argon2_cffi-21.3.0-py3-none-any.whl", hash = "sha256:8c976986f2c5c0e5000919e6de187906cfd81fb1c72bf9d88c01177e77da7f80"}, +] + +[package.dependencies] +argon2-cffi-bindings = "*" + +[package.extras] +dev = ["cogapp", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "pre-commit", "pytest", "sphinx", "sphinx-notfound-page", "tomli"] +docs = ["furo", "sphinx", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pytest"] + +[[package]] +name = "argon2-cffi-bindings" +version = "21.2.0" +description = "Low-level CFFI bindings for Argon2" +optional = true +python-versions = ">=3.6" +files = [ + {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"}, + {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"}, +] + +[package.dependencies] +cffi = ">=1.0.1" + +[package.extras] +dev = ["cogapp", "pre-commit", "pytest", "wheel"] +tests = ["pytest"] + +[[package]] +name = "arrow" +version = "1.2.3" +description = "Better dates & times for Python" +optional = true +python-versions = ">=3.6" +files = [ + {file = "arrow-1.2.3-py3-none-any.whl", hash = "sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2"}, + {file = "arrow-1.2.3.tar.gz", hash = "sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1"}, +] + +[package.dependencies] +python-dateutil = ">=2.7.0" + +[[package]] +name = "asttokens" +version = "2.2.1" +description = "Annotate AST trees with source code positions" +optional = true +python-versions = "*" +files = [ + {file = "asttokens-2.2.1-py2.py3-none-any.whl", hash = "sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c"}, + {file = "asttokens-2.2.1.tar.gz", hash = "sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3"}, +] + +[package.dependencies] +six = "*" + +[package.extras] +test = ["astroid", "pytest"] + +[[package]] +name = "astunparse" +version = "1.6.3" +description = "An AST unparser for Python" +optional = true +python-versions = "*" +files = [ + {file = "astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"}, + {file = "astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872"}, +] + +[package.dependencies] +six = ">=1.6.1,<2.0" +wheel = ">=0.23.0,<1.0" + +[[package]] +name = "async-timeout" +version = "4.0.2" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.6" +files = [ + {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, + {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, +] + +[[package]] +name = "attrs" +version = "22.1.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.5" +files = [ + {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, + {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, +] + +[package.extras] +dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] +tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] +tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] + +[[package]] +name = "backcall" +version = "0.2.0" +description = "Specifications for callback functions passed in to an API" +optional = true +python-versions = "*" +files = [ + {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, + {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, +] + +[[package]] +name = "bayesian-optimization" +version = "1.4.3" +description = "Bayesian Optimization package" +optional = true +python-versions = ">= 3.7" +files = [ + {file = "bayesian-optimization-1.4.3.tar.gz", hash = "sha256:f9a448e1b52d961301cbc953ce4199709f6c26b1c88994c9b4dadb7752a64550"}, + {file = "bayesian_optimization-1.4.3-py3-none-any.whl", hash = "sha256:2719272d5825f1ba7d7609f3b1c1fdca13eba1b7ad52a5ca2e62f34154ecae28"}, +] + +[package.dependencies] +colorama = ">=0.4.6" +numpy = ">=1.9.0" +scikit-learn = ">=0.18.0" +scipy = ">=1.0.0" + +[[package]] +name = "beautifulsoup4" +version = "4.11.1" +description = "Screen-scraping library" +optional = true +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"}, + {file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "black" +version = "22.12.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.7" +files = [ + {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, + {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, + {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, + {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, + {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, + {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, + {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, + {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, + {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, + {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, + {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, + {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "bleach" +version = "5.0.1" +description = "An easy safelist-based HTML-sanitizing tool." +optional = true +python-versions = ">=3.7" +files = [ + {file = "bleach-5.0.1-py3-none-any.whl", hash = "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a"}, + {file = "bleach-5.0.1.tar.gz", hash = "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c"}, +] + +[package.dependencies] +six = ">=1.9.0" +webencodings = "*" + +[package.extras] +css = ["tinycss2 (>=1.1.0,<1.2)"] +dev = ["Sphinx (==4.3.2)", "black (==22.3.0)", "build (==0.8.0)", "flake8 (==4.0.1)", "hashin (==0.17.0)", "mypy (==0.961)", "pip-tools (==6.6.2)", "pytest (==7.1.2)", "tox (==3.25.0)", "twine (==4.0.1)", "wheel (==0.37.1)"] + +[[package]] +name = "braceexpand" +version = "0.1.7" +description = "Bash-style brace expansion for Python" +optional = false +python-versions = "*" +files = [ + {file = "braceexpand-0.1.7-py2.py3-none-any.whl", hash = "sha256:91332d53de7828103dcae5773fb43bc34950b0c8160e35e0f44c4427a3b85014"}, + {file = "braceexpand-0.1.7.tar.gz", hash = "sha256:e6e539bd20eaea53547472ff94f4fb5c3d3bf9d0a89388c4b56663aba765f705"}, +] + +[[package]] +name = "cachetools" +version = "5.2.0" +description = "Extensible memoizing collections and decorators" +optional = true +python-versions = "~=3.7" +files = [ + {file = "cachetools-5.2.0-py3-none-any.whl", hash = "sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db"}, + {file = "cachetools-5.2.0.tar.gz", hash = "sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757"}, +] + +[[package]] +name = "certifi" +version = "2022.12.7" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] + +[[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +optional = true +python-versions = "*" +files = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] + +[[package]] +name = "charset-normalizer" +version = "2.1.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, +] + +[package.extras] +unicode-backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "cluster" +version = "2.5" +description = "" +optional = true +python-versions = ">=3.8" +files = [] +develop = false + +[package.dependencies] +colorama = "*" +gitpython = ">=3.0.5" +jinja2 = "*" +matplotlib = "*" +nevergrad = "*" +numpy = "*" +pandas = {version = "*", extras = ["output-formatting"]} +pyuv = {git = "https://github.com/saghul/pyuv.git", rev = "2a3d42d44c6315ebd73899a35118380d2d5979b5"} +scikit-learn = "*" +seaborn = ">=0.11.0" +smart_settings = {git = "https://github.com/martius-lab/smart-settings.git", rev = "eb7331fdcad58d314a842087bbf136735e890013"} +tqdm = "*" + +[package.extras] +dev = ["absolufy-imports", "black", "nox", "pre-commit", "pytest", "ruff"] +docs = ["myst-parser", "sphinx"] +mypy = ["mypy", "pandas-stubs", "types-colorama", "types-tqdm"] + +[package.source] +type = "git" +url = "git@gitlab.tuebingen.mpg.de:mrolinek/cluster_utils.git" +reference = "HEAD" +resolved_reference = "8086b820a9cc1cee9bb919f5f99486790aad4466" + +[[package]] +name = "cma" +version = "3.3.0" +description = "CMA-ES, Covariance Matrix Adaptation Evolution Strategy for non-linear numerical optimization in Python" +optional = true +python-versions = "*" +files = [ + {file = "cma-3.3.0-py3-none-any.whl", hash = "sha256:5cc571b1e2068fcf1c538be36f8f3a870107456fed22ce81c1345a96329e61db"}, + {file = "cma-3.3.0.tar.gz", hash = "sha256:b748b8e03f4e7ae816157d7b9bb2fc6b1fb2fee1d5fd3399329b646bb75861ec"}, +] + +[package.dependencies] +numpy = "*" + +[package.extras] +constrained-solution-tracking = ["moarchiving"] +plotting = ["matplotlib"] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "comm" +version = "0.1.2" +description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." +optional = true +python-versions = ">=3.6" +files = [ + {file = "comm-0.1.2-py3-none-any.whl", hash = "sha256:9f3abf3515112fa7c55a42a6a5ab358735c9dccc8b5910a9d8e3ef5998130666"}, + {file = "comm-0.1.2.tar.gz", hash = "sha256:3e2f5826578e683999b93716285b3b1f344f157bf75fa9ce0a797564e742f062"}, +] + +[package.dependencies] +traitlets = ">=5.3" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "contourpy" +version = "1.0.6" +description = "Python library for calculating contours of 2D quadrilateral grids" +optional = true +python-versions = ">=3.7" +files = [ + {file = "contourpy-1.0.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:613c665529899b5d9fade7e5d1760111a0b011231277a0d36c49f0d3d6914bd6"}, + {file = "contourpy-1.0.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:78ced51807ccb2f45d4ea73aca339756d75d021069604c2fccd05390dc3c28eb"}, + {file = "contourpy-1.0.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3b1bd7577c530eaf9d2bc52d1a93fef50ac516a8b1062c3d1b9bcec9ebe329b"}, + {file = "contourpy-1.0.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8834c14b8c3dd849005e06703469db9bf96ba2d66a3f88ecc539c9a8982e0ee"}, + {file = "contourpy-1.0.6-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4052a8a4926d4468416fc7d4b2a7b2a3e35f25b39f4061a7e2a3a2748c4fc48"}, + {file = "contourpy-1.0.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c0e1308307a75e07d1f1b5f0f56b5af84538a5e9027109a7bcf6cb47c434e72"}, + {file = "contourpy-1.0.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fc4e7973ed0e1fe689435842a6e6b330eb7ccc696080dda9a97b1a1b78e41db"}, + {file = "contourpy-1.0.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:08e8d09d96219ace6cb596506fb9b64ea5f270b2fb9121158b976d88871fcfd1"}, + {file = "contourpy-1.0.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f33da6b5d19ad1bb5e7ad38bb8ba5c426d2178928bc2b2c44e8823ea0ecb6ff3"}, + {file = "contourpy-1.0.6-cp310-cp310-win32.whl", hash = "sha256:12a7dc8439544ed05c6553bf026d5e8fa7fad48d63958a95d61698df0e00092b"}, + {file = "contourpy-1.0.6-cp310-cp310-win_amd64.whl", hash = "sha256:eadad75bf91897f922e0fb3dca1b322a58b1726a953f98c2e5f0606bd8408621"}, + {file = "contourpy-1.0.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:913bac9d064cff033cf3719e855d4f1db9f1c179e0ecf3ba9fdef21c21c6a16a"}, + {file = "contourpy-1.0.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46deb310a276cc5c1fd27958e358cce68b1e8a515fa5a574c670a504c3a3fe30"}, + {file = "contourpy-1.0.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b64f747e92af7da3b85631a55d68c45a2d728b4036b03cdaba4bd94bcc85bd6f"}, + {file = "contourpy-1.0.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50627bf76abb6ba291ad08db583161939c2c5fab38c38181b7833423ab9c7de3"}, + {file = "contourpy-1.0.6-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:358f6364e4873f4d73360b35da30066f40387dd3c427a3e5432c6b28dd24a8fa"}, + {file = "contourpy-1.0.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c78bfbc1a7bff053baf7e508449d2765964d67735c909b583204e3240a2aca45"}, + {file = "contourpy-1.0.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e43255a83835a129ef98f75d13d643844d8c646b258bebd11e4a0975203e018f"}, + {file = "contourpy-1.0.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:375d81366afd547b8558c4720337218345148bc2fcffa3a9870cab82b29667f2"}, + {file = "contourpy-1.0.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b98c820608e2dca6442e786817f646d11057c09a23b68d2b3737e6dcb6e4a49b"}, + {file = "contourpy-1.0.6-cp311-cp311-win32.whl", hash = "sha256:0e4854cc02006ad6684ce092bdadab6f0912d131f91c2450ce6dbdea78ee3c0b"}, + {file = "contourpy-1.0.6-cp311-cp311-win_amd64.whl", hash = "sha256:d2eff2af97ea0b61381828b1ad6cd249bbd41d280e53aea5cccd7b2b31b8225c"}, + {file = "contourpy-1.0.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5b117d29433fc8393b18a696d794961464e37afb34a6eeb8b2c37b5f4128a83e"}, + {file = "contourpy-1.0.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:341330ed19074f956cb20877ad8d2ae50e458884bfa6a6df3ae28487cc76c768"}, + {file = "contourpy-1.0.6-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:371f6570a81dfdddbb837ba432293a63b4babb942a9eb7aaa699997adfb53278"}, + {file = "contourpy-1.0.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9447c45df407d3ecb717d837af3b70cfef432138530712263730783b3d016512"}, + {file = "contourpy-1.0.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:730c27978a0003b47b359935478b7d63fd8386dbb2dcd36c1e8de88cbfc1e9de"}, + {file = "contourpy-1.0.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:da1ef35fd79be2926ba80fbb36327463e3656c02526e9b5b4c2b366588b74d9a"}, + {file = "contourpy-1.0.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:cd2bc0c8f2e8de7dd89a7f1c10b8844e291bca17d359373203ef2e6100819edd"}, + {file = "contourpy-1.0.6-cp37-cp37m-win32.whl", hash = "sha256:3a1917d3941dd58732c449c810fa7ce46cc305ce9325a11261d740118b85e6f3"}, + {file = "contourpy-1.0.6-cp37-cp37m-win_amd64.whl", hash = "sha256:06ca79e1efbbe2df795822df2fa173d1a2b38b6e0f047a0ec7903fbca1d1847e"}, + {file = "contourpy-1.0.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e626cefff8491bce356221c22af5a3ea528b0b41fbabc719c00ae233819ea0bf"}, + {file = "contourpy-1.0.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dbe6fe7a1166b1ddd7b6d887ea6fa8389d3f28b5ed3f73a8f40ece1fc5a3d340"}, + {file = "contourpy-1.0.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e13b31d1b4b68db60b3b29f8e337908f328c7f05b9add4b1b5c74e0691180109"}, + {file = "contourpy-1.0.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a79d239fc22c3b8d9d3de492aa0c245533f4f4c7608e5749af866949c0f1b1b9"}, + {file = "contourpy-1.0.6-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e8e686a6db92a46111a1ee0ee6f7fbfae4048f0019de207149f43ac1812cf95"}, + {file = "contourpy-1.0.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2bd02f1a7adff3a1f33e431eb96ab6d7987b039d2946a9b39fe6fb16a1036"}, + {file = "contourpy-1.0.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:03d1b9c6b44a9e30d554654c72be89af94fab7510b4b9f62356c64c81cec8b7d"}, + {file = "contourpy-1.0.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b48d94386f1994db7c70c76b5808c12e23ed7a4ee13693c2fc5ab109d60243c0"}, + {file = "contourpy-1.0.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:208bc904889c910d95aafcf7be9e677726df9ef71e216780170dbb7e37d118fa"}, + {file = "contourpy-1.0.6-cp38-cp38-win32.whl", hash = "sha256:444fb776f58f4906d8d354eb6f6ce59d0a60f7b6a720da6c1ccb839db7c80eb9"}, + {file = "contourpy-1.0.6-cp38-cp38-win_amd64.whl", hash = "sha256:9bc407a6af672da20da74823443707e38ece8b93a04009dca25856c2d9adadb1"}, + {file = "contourpy-1.0.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:aa4674cf3fa2bd9c322982644967f01eed0c91bb890f624e0e0daf7a5c3383e9"}, + {file = "contourpy-1.0.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f56515e7c6fae4529b731f6c117752247bef9cdad2b12fc5ddf8ca6a50965a5"}, + {file = "contourpy-1.0.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:344cb3badf6fc7316ad51835f56ac387bdf86c8e1b670904f18f437d70da4183"}, + {file = "contourpy-1.0.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b1e66346acfb17694d46175a0cea7d9036f12ed0c31dfe86f0f405eedde2bdd"}, + {file = "contourpy-1.0.6-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8468b40528fa1e15181cccec4198623b55dcd58306f8815a793803f51f6c474a"}, + {file = "contourpy-1.0.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dedf4c64185a216c35eb488e6f433297c660321275734401760dafaeb0ad5c2"}, + {file = "contourpy-1.0.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:494efed2c761f0f37262815f9e3c4bb9917c5c69806abdee1d1cb6611a7174a0"}, + {file = "contourpy-1.0.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:75a2e638042118118ab39d337da4c7908c1af74a8464cad59f19fbc5bbafec9b"}, + {file = "contourpy-1.0.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a628bba09ba72e472bf7b31018b6281fd4cc903f0888049a3724afba13b6e0b8"}, + {file = "contourpy-1.0.6-cp39-cp39-win32.whl", hash = "sha256:e1739496c2f0108013629aa095cc32a8c6363444361960c07493818d0dea2da4"}, + {file = "contourpy-1.0.6-cp39-cp39-win_amd64.whl", hash = "sha256:a457ee72d9032e86730f62c5eeddf402e732fdf5ca8b13b41772aa8ae13a4563"}, + {file = "contourpy-1.0.6-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d912f0154a20a80ea449daada904a7eb6941c83281a9fab95de50529bfc3a1da"}, + {file = "contourpy-1.0.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4081918147fc4c29fad328d5066cfc751da100a1098398742f9f364be63803fc"}, + {file = "contourpy-1.0.6-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0537cc1195245bbe24f2913d1f9211b8f04eb203de9044630abd3664c6cc339c"}, + {file = "contourpy-1.0.6-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcd556c8fc37a342dd636d7eef150b1399f823a4462f8c968e11e1ebeabee769"}, + {file = "contourpy-1.0.6-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f6ca38dd8d988eca8f07305125dec6f54ac1c518f1aaddcc14d08c01aebb6efc"}, + {file = "contourpy-1.0.6-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c1baa49ab9fedbf19d40d93163b7d3e735d9cd8d5efe4cce9907902a6dad391f"}, + {file = "contourpy-1.0.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:211dfe2bd43bf5791d23afbe23a7952e8ac8b67591d24be3638cabb648b3a6eb"}, + {file = "contourpy-1.0.6-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c38c6536c2d71ca2f7e418acaf5bca30a3af7f2a2fa106083c7d738337848dbe"}, + {file = "contourpy-1.0.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b1ee48a130da4dd0eb8055bbab34abf3f6262957832fd575e0cab4979a15a41"}, + {file = "contourpy-1.0.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5641927cc5ae66155d0c80195dc35726eae060e7defc18b7ab27600f39dd1fe7"}, + {file = "contourpy-1.0.6-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ee394502026d68652c2824348a40bf50f31351a668977b51437131a90d777ea"}, + {file = "contourpy-1.0.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b97454ed5b1368b66ed414c754cba15b9750ce69938fc6153679787402e4cdf"}, + {file = "contourpy-1.0.6-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0236875c5a0784215b49d00ebbe80c5b6b5d5244b3655a36dda88105334dea17"}, + {file = "contourpy-1.0.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84c593aeff7a0171f639da92cb86d24954bbb61f8a1b530f74eb750a14685832"}, + {file = "contourpy-1.0.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9b0e7fe7f949fb719b206548e5cde2518ffb29936afa4303d8a1c4db43dcb675"}, + {file = "contourpy-1.0.6.tar.gz", hash = "sha256:6e459ebb8bb5ee4c22c19cc000174f8059981971a33ce11e17dddf6aca97a142"}, +] + +[package.dependencies] +numpy = ">=1.16" + +[package.extras] +bokeh = ["bokeh", "selenium"] +docs = ["docutils (<0.18)", "sphinx (<=5.2.0)", "sphinx-rtd-theme"] +test = ["Pillow", "flake8", "isort", "matplotlib", "pytest"] +test-minimal = ["pytest"] +test-no-codebase = ["Pillow", "matplotlib", "pytest"] + +[[package]] +name = "cycler" +version = "0.11.0" +description = "Composable style cycles" +optional = true +python-versions = ">=3.6" +files = [ + {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"}, + {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, +] + +[[package]] +name = "debugpy" +version = "1.6.4" +description = "An implementation of the Debug Adapter Protocol for Python" +optional = true +python-versions = ">=3.7" +files = [ + {file = "debugpy-1.6.4-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:6ae238943482c78867ac707c09122688efb700372b617ffd364261e5e41f7a2f"}, + {file = "debugpy-1.6.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a39e7da178e1f22f4bc04b57f085e785ed1bcf424aaf318835a1a7129eefe35"}, + {file = "debugpy-1.6.4-cp310-cp310-win32.whl", hash = "sha256:143f79d0798a9acea21cd1d111badb789f19d414aec95fa6389cfea9485ddfb1"}, + {file = "debugpy-1.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:563f148f94434365ec0ce94739c749aabf60bf67339e68a9446499f3582d62f3"}, + {file = "debugpy-1.6.4-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:1caee68f7e254267df908576c0d0938f8f88af16383f172cb9f0602e24c30c01"}, + {file = "debugpy-1.6.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e2a83d31a16b83666f19fa06d97b2cc311af88e6266590579737949971a17e"}, + {file = "debugpy-1.6.4-cp37-cp37m-win32.whl", hash = "sha256:82229790442856962aec4767b98ba2559fe0998f897e9f21fb10b4fd24b6c436"}, + {file = "debugpy-1.6.4-cp37-cp37m-win_amd64.whl", hash = "sha256:67edf033f9e512958f7b472975ff9d9b7ff64bf4440f6f6ae44afdc66b89e6b6"}, + {file = "debugpy-1.6.4-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:4ab5e938925e5d973f567d6ef32751b17d10f3be3a8c4d73c52f53e727f69bf1"}, + {file = "debugpy-1.6.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8df268e9f72fc06efc2e75e8dc8e2b881d6a397356faec26efb2ee70b6863b7"}, + {file = "debugpy-1.6.4-cp38-cp38-win32.whl", hash = "sha256:86bd25f38f8b6c5d430a5e2931eebbd5f580c640f4819fcd236d0498790c7204"}, + {file = "debugpy-1.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:62ba4179b372a62abf9c89b56997d70a4100c6dea6c2a4e0e4be5f45920b3253"}, + {file = "debugpy-1.6.4-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d2968e589bda4e485a9c61f113754a28e48d88c5152ed8e0b2564a1fadbe50a5"}, + {file = "debugpy-1.6.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e62b8034ede98932b92268669318848a0d42133d857087a3b9cec03bb844c615"}, + {file = "debugpy-1.6.4-cp39-cp39-win32.whl", hash = "sha256:3d9c31baf64bf959a593996c108e911c5a9aa1693a296840e5469473f064bcec"}, + {file = "debugpy-1.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:ea4bf208054e6d41749f17612066da861dff10102729d32c85b47f155223cf2b"}, + {file = "debugpy-1.6.4-py2.py3-none-any.whl", hash = "sha256:e886a1296cd20a10172e94788009ce74b759e54229ebd64a43fa5c2b4e62cd76"}, + {file = "debugpy-1.6.4.zip", hash = "sha256:d5ab9bd3f4e7faf3765fd52c7c43c074104ab1e109621dc73219099ed1a5399d"}, +] + +[[package]] +name = "decorator" +version = "4.4.2" +description = "Decorators for Humans" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +files = [ + {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"}, + {file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"}, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + +[[package]] +name = "dill" +version = "0.3.6" +description = "serialize all of python" +optional = true +python-versions = ">=3.7" +files = [ + {file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"}, + {file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] + +[[package]] +name = "distlib" +version = "0.3.6" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, + {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, +] + +[[package]] +name = "einops" +version = "0.6.0" +description = "A new flavour of deep learning operations" +optional = false +python-versions = ">=3.7" +files = [ + {file = "einops-0.6.0-py3-none-any.whl", hash = "sha256:c7b187a5dc725f079860ec2d330c1820448948622d826273345a8dd8d5f695bd"}, + {file = "einops-0.6.0.tar.gz", hash = "sha256:6f6c78739316a2e3ccbce8052310497e69da092935e4173f2e76ec4e3a336a35"}, +] + +[[package]] +name = "entrypoints" +version = "0.4" +description = "Discover and load entry points from installed packages." +optional = true +python-versions = ">=3.6" +files = [ + {file = "entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f"}, + {file = "entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4"}, +] + +[[package]] +name = "etils" +version = "0.9.0" +description = "Collection of common python utils" +optional = true +python-versions = ">=3.7" +files = [ + {file = "etils-0.9.0-py3-none-any.whl", hash = "sha256:635d6f7d1c519eb194304228543a4c5c7df0e6b58243302473e34c18cf720588"}, + {file = "etils-0.9.0.tar.gz", hash = "sha256:489103e9e499a566765c60458ee15d185cf0065f2060a4d16a68f8f46962ed0d"}, +] + +[package.dependencies] +importlib_resources = {version = "*", optional = true, markers = "extra == \"epath\""} +typing_extensions = {version = "*", optional = true, markers = "extra == \"epy\""} +zipp = {version = "*", optional = true, markers = "extra == \"epath\""} + +[package.extras] +all = ["etils[array-types]", "etils[eapp]", "etils[ecolab]", "etils[edc]", "etils[enp]", "etils[epath]", "etils[epy]", "etils[etqdm]", "etils[etree-dm]", "etils[etree-jax]", "etils[etree-tf]", "etils[etree]"] +array-types = ["etils[enp]"] +dev = ["chex", "pylint (>=2.6.0)", "pytest", "pytest-subtests", "pytest-xdist", "yapf"] +eapp = ["absl-py", "simple_parsing"] +ecolab = ["etils[enp]", "etils[epy]", "jupyter", "mediapy", "numpy"] +edc = ["etils[epy]", "typing_extensions"] +enp = ["etils[epy]", "numpy"] +epath = ["etils[epy]", "importlib_resources", "typing_extensions", "zipp"] +epy = ["typing_extensions"] +etqdm = ["absl-py", "etils[epy]", "tqdm"] +etree = ["etils[array-types]", "etils[enp]", "etils[epy]", "etils[etqdm]"] +etree-dm = ["dm-tree", "etils[etree]"] +etree-jax = ["etils[etree]", "jax[cpu]"] +etree-tf = ["etils[etree]", "tf-nightly"] +lazy-imports = ["etils[ecolab]"] + +[[package]] +name = "exceptiongroup" +version = "1.0.4" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.0.4-py3-none-any.whl", hash = "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828"}, + {file = "exceptiongroup-1.0.4.tar.gz", hash = "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "executing" +version = "1.2.0" +description = "Get the currently executing AST node of a frame, and other information" +optional = true +python-versions = "*" +files = [ + {file = "executing-1.2.0-py2.py3-none-any.whl", hash = "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc"}, + {file = "executing-1.2.0.tar.gz", hash = "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107"}, +] + +[package.extras] +tests = ["asttokens", "littleutils", "pytest", "rich"] + +[[package]] +name = "fastjsonschema" +version = "2.16.2" +description = "Fastest Python implementation of JSON schema" +optional = true +python-versions = "*" +files = [ + {file = "fastjsonschema-2.16.2-py3-none-any.whl", hash = "sha256:21f918e8d9a1a4ba9c22e09574ba72267a6762d47822db9add95f6454e51cc1c"}, + {file = "fastjsonschema-2.16.2.tar.gz", hash = "sha256:01e366f25d9047816fe3d288cbfc3e10541daf0af2044763f3d0ade42476da18"}, +] + +[package.extras] +devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] + +[[package]] +name = "filelock" +version = "3.8.2" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.7" +files = [ + {file = "filelock-3.8.2-py3-none-any.whl", hash = "sha256:8df285554452285f79c035efb0c861eb33a4bcfa5b7a137016e32e6a90f9792c"}, + {file = "filelock-3.8.2.tar.gz", hash = "sha256:7565f628ea56bfcd8e54e42bdc55da899c85c1abfe1b5bcfd147e9188cebb3b2"}, +] + +[package.extras] +docs = ["furo (>=2022.9.29)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +testing = ["covdefaults (>=2.2.2)", "coverage (>=6.5)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "flatbuffers" +version = "22.12.6" +description = "The FlatBuffers serialization format for Python" +optional = true +python-versions = "*" +files = [ + {file = "flatbuffers-22.12.6-py2.py3-none-any.whl", hash = "sha256:b12a3214e73f325ecd392503e821ac43a791181356f020cc3657ba829b4947c8"}, + {file = "flatbuffers-22.12.6.tar.gz", hash = "sha256:27f67c6fb102d41c911c26867bda71f1f8622176ac072fa30f668f4d023b5826"}, +] + +[[package]] +name = "fonttools" +version = "4.38.0" +description = "Tools to manipulate font files" +optional = true +python-versions = ">=3.7" +files = [ + {file = "fonttools-4.38.0-py3-none-any.whl", hash = "sha256:820466f43c8be8c3009aef8b87e785014133508f0de64ec469e4efb643ae54fb"}, + {file = "fonttools-4.38.0.zip", hash = "sha256:2bb244009f9bf3fa100fc3ead6aeb99febe5985fa20afbfbaa2f8946c2fbdaf1"}, +] + +[package.extras] +all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=14.0.0)", "xattr", "zopfli (>=0.1.4)"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["munkres", "scipy"] +lxml = ["lxml (>=4.0,<5)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +repacker = ["uharfbuzz (>=0.23.0)"] +symfont = ["sympy"] +type1 = ["xattr"] +ufo = ["fs (>=2.2.0,<3)"] +unicode = ["unicodedata2 (>=14.0.0)"] +woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] + +[[package]] +name = "fqdn" +version = "1.5.1" +description = "Validates fully-qualified domain names against RFC 1123, so that they are acceptable to modern bowsers" +optional = true +python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4" +files = [ + {file = "fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014"}, + {file = "fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f"}, +] + +[[package]] +name = "frozenlist" +version = "1.3.3" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.7" +files = [ + {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4"}, + {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0"}, + {file = "frozenlist-1.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fdfc24dcfce5b48109867c13b4cb15e4660e7bd7661741a391f821f23dfdca7"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c926450857408e42f0bbc295e84395722ce74bae69a3b2aa2a65fe22cb14b99"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1841e200fdafc3d51f974d9d377c079a0694a8f06de2e67b48150328d66d5483"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f470c92737afa7d4c3aacc001e335062d582053d4dbe73cda126f2d7031068dd"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:783263a4eaad7c49983fe4b2e7b53fa9770c136c270d2d4bbb6d2192bf4d9caf"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:924620eef691990dfb56dc4709f280f40baee568c794b5c1885800c3ecc69816"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae4dc05c465a08a866b7a1baf360747078b362e6a6dbeb0c57f234db0ef88ae0"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:bed331fe18f58d844d39ceb398b77d6ac0b010d571cba8267c2e7165806b00ce"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:02c9ac843e3390826a265e331105efeab489ffaf4dd86384595ee8ce6d35ae7f"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9545a33965d0d377b0bc823dcabf26980e77f1b6a7caa368a365a9497fb09420"}, + {file = "frozenlist-1.3.3-cp310-cp310-win32.whl", hash = "sha256:d5cd3ab21acbdb414bb6c31958d7b06b85eeb40f66463c264a9b343a4e238642"}, + {file = "frozenlist-1.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:b756072364347cb6aa5b60f9bc18e94b2f79632de3b0190253ad770c5df17db1"}, + {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4395e2f8d83fbe0c627b2b696acce67868793d7d9750e90e39592b3626691b7"}, + {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14143ae966a6229350021384870458e4777d1eae4c28d1a7aa47f24d030e6678"}, + {file = "frozenlist-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d8860749e813a6f65bad8285a0520607c9500caa23fea6ee407e63debcdbef6"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23d16d9f477bb55b6154654e0e74557040575d9d19fe78a161bd33d7d76808e8"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb82dbba47a8318e75f679690190c10a5e1f447fbf9df41cbc4c3afd726d88cb"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9309869032abb23d196cb4e4db574232abe8b8be1339026f489eeb34a4acfd91"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a97b4fe50b5890d36300820abd305694cb865ddb7885049587a5678215782a6b"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c188512b43542b1e91cadc3c6c915a82a5eb95929134faf7fd109f14f9892ce4"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:303e04d422e9b911a09ad499b0368dc551e8c3cd15293c99160c7f1f07b59a48"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0771aed7f596c7d73444c847a1c16288937ef988dc04fb9f7be4b2aa91db609d"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:66080ec69883597e4d026f2f71a231a1ee9887835902dbe6b6467d5a89216cf6"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:41fe21dc74ad3a779c3d73a2786bdf622ea81234bdd4faf90b8b03cad0c2c0b4"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f20380df709d91525e4bee04746ba612a4df0972c1b8f8e1e8af997e678c7b81"}, + {file = "frozenlist-1.3.3-cp311-cp311-win32.whl", hash = "sha256:f30f1928162e189091cf4d9da2eac617bfe78ef907a761614ff577ef4edfb3c8"}, + {file = "frozenlist-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:a6394d7dadd3cfe3f4b3b186e54d5d8504d44f2d58dcc89d693698e8b7132b32"}, + {file = "frozenlist-1.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8df3de3a9ab8325f94f646609a66cbeeede263910c5c0de0101079ad541af332"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0693c609e9742c66ba4870bcee1ad5ff35462d5ffec18710b4ac89337ff16e27"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd4210baef299717db0a600d7a3cac81d46ef0e007f88c9335db79f8979c0d3d"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:394c9c242113bfb4b9aa36e2b80a05ffa163a30691c7b5a29eba82e937895d5e"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6327eb8e419f7d9c38f333cde41b9ae348bec26d840927332f17e887a8dcb70d"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e24900aa13212e75e5b366cb9065e78bbf3893d4baab6052d1aca10d46d944c"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3843f84a6c465a36559161e6c59dce2f2ac10943040c2fd021cfb70d58c4ad56"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:84610c1502b2461255b4c9b7d5e9c48052601a8957cd0aea6ec7a7a1e1fb9420"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:c21b9aa40e08e4f63a2f92ff3748e6b6c84d717d033c7b3438dd3123ee18f70e"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:efce6ae830831ab6a22b9b4091d411698145cb9b8fc869e1397ccf4b4b6455cb"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:40de71985e9042ca00b7953c4f41eabc3dc514a2d1ff534027f091bc74416401"}, + {file = "frozenlist-1.3.3-cp37-cp37m-win32.whl", hash = "sha256:180c00c66bde6146a860cbb81b54ee0df350d2daf13ca85b275123bbf85de18a"}, + {file = "frozenlist-1.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9bbbcedd75acdfecf2159663b87f1bb5cfc80e7cd99f7ddd9d66eb98b14a8411"}, + {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:034a5c08d36649591be1cbb10e09da9f531034acfe29275fc5454a3b101ce41a"}, + {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba64dc2b3b7b158c6660d49cdb1d872d1d0bf4e42043ad8d5006099479a194e5"}, + {file = "frozenlist-1.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:47df36a9fe24054b950bbc2db630d508cca3aa27ed0566c0baf661225e52c18e"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:008a054b75d77c995ea26629ab3a0c0d7281341f2fa7e1e85fa6153ae29ae99c"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:841ea19b43d438a80b4de62ac6ab21cfe6827bb8a9dc62b896acc88eaf9cecba"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e235688f42b36be2b6b06fc37ac2126a73b75fb8d6bc66dd632aa35286238703"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca713d4af15bae6e5d79b15c10c8522859a9a89d3b361a50b817c98c2fb402a2"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ac5995f2b408017b0be26d4a1d7c61bce106ff3d9e3324374d66b5964325448"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4ae8135b11652b08a8baf07631d3ebfe65a4c87909dbef5fa0cdde440444ee4"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4ea42116ceb6bb16dbb7d526e242cb6747b08b7710d9782aa3d6732bd8d27649"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:810860bb4bdce7557bc0febb84bbd88198b9dbc2022d8eebe5b3590b2ad6c842"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ee78feb9d293c323b59a6f2dd441b63339a30edf35abcb51187d2fc26e696d13"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0af2e7c87d35b38732e810befb9d797a99279cbb85374d42ea61c1e9d23094b3"}, + {file = "frozenlist-1.3.3-cp38-cp38-win32.whl", hash = "sha256:899c5e1928eec13fd6f6d8dc51be23f0d09c5281e40d9cf4273d188d9feeaf9b"}, + {file = "frozenlist-1.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:7f44e24fa70f6fbc74aeec3e971f60a14dde85da364aa87f15d1be94ae75aeef"}, + {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2b07ae0c1edaa0a36339ec6cce700f51b14a3fc6545fdd32930d2c83917332cf"}, + {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ebb86518203e12e96af765ee89034a1dbb0c3c65052d1b0c19bbbd6af8a145e1"}, + {file = "frozenlist-1.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5cf820485f1b4c91e0417ea0afd41ce5cf5965011b3c22c400f6d144296ccbc0"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c11e43016b9024240212d2a65043b70ed8dfd3b52678a1271972702d990ac6d"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8fa3c6e3305aa1146b59a09b32b2e04074945ffcfb2f0931836d103a2c38f936"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:352bd4c8c72d508778cf05ab491f6ef36149f4d0cb3c56b1b4302852255d05d5"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65a5e4d3aa679610ac6e3569e865425b23b372277f89b5ef06cf2cdaf1ebf22b"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e2c1185858d7e10ff045c496bbf90ae752c28b365fef2c09cf0fa309291669"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f163d2fd041c630fed01bc48d28c3ed4a3b003c00acd396900e11ee5316b56bb"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:05cdb16d09a0832eedf770cb7bd1fe57d8cf4eaf5aced29c4e41e3f20b30a784"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8bae29d60768bfa8fb92244b74502b18fae55a80eac13c88eb0b496d4268fd2d"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eedab4c310c0299961ac285591acd53dc6723a1ebd90a57207c71f6e0c2153ab"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3bbdf44855ed8f0fbcd102ef05ec3012d6a4fd7c7562403f76ce6a52aeffb2b1"}, + {file = "frozenlist-1.3.3-cp39-cp39-win32.whl", hash = "sha256:efa568b885bca461f7c7b9e032655c0c143d305bf01c30caf6db2854a4532b38"}, + {file = "frozenlist-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9"}, + {file = "frozenlist-1.3.3.tar.gz", hash = "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a"}, +] + +[[package]] +name = "fsspec" +version = "2022.11.0" +description = "File-system specification" +optional = false +python-versions = ">=3.7" +files = [ + {file = "fsspec-2022.11.0-py3-none-any.whl", hash = "sha256:d6e462003e3dcdcb8c7aa84c73a228f8227e72453cd22570e2363e8844edfe7b"}, + {file = "fsspec-2022.11.0.tar.gz", hash = "sha256:259d5fd5c8e756ff2ea72f42e7613c32667dc2049a4ac3d84364a7ca034acb8b"}, +] + +[package.dependencies] +aiohttp = {version = "<4.0.0a0 || >4.0.0a0,<4.0.0a1 || >4.0.0a1", optional = true, markers = "extra == \"http\""} +requests = {version = "*", optional = true, markers = "extra == \"http\""} + +[package.extras] +abfs = ["adlfs"] +adl = ["adlfs"] +arrow = ["pyarrow (>=1)"] +dask = ["dask", "distributed"] +dropbox = ["dropbox", "dropboxdrivefs", "requests"] +entrypoints = ["importlib-metadata"] +fuse = ["fusepy"] +gcs = ["gcsfs"] +git = ["pygit2"] +github = ["requests"] +gs = ["gcsfs"] +gui = ["panel"] +hdfs = ["pyarrow (>=1)"] +http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "requests"] +libarchive = ["libarchive-c"] +oci = ["ocifs"] +s3 = ["s3fs"] +sftp = ["paramiko"] +smb = ["smbprotocol"] +ssh = ["paramiko"] +tqdm = ["tqdm"] + +[[package]] +name = "gast" +version = "0.4.0" +description = "Python AST that abstracts the underlying Python version" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "gast-0.4.0-py3-none-any.whl", hash = "sha256:b7adcdd5adbebf1adf17378da5ba3f543684dbec47b1cda1f3997e573cd542c4"}, + {file = "gast-0.4.0.tar.gz", hash = "sha256:40feb7b8b8434785585ab224d1568b857edb18297e5a3047f1ba012bc83b42c1"}, +] + +[[package]] +name = "gitdb" +version = "4.0.11" +description = "Git Object Database" +optional = true +python-versions = ">=3.7" +files = [ + {file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"}, + {file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"}, +] + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "gitpython" +version = "3.1.40" +description = "GitPython is a Python library used to interact with Git repositories" +optional = true +python-versions = ">=3.7" +files = [ + {file = "GitPython-3.1.40-py3-none-any.whl", hash = "sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a"}, + {file = "GitPython-3.1.40.tar.gz", hash = "sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4"}, +] + +[package.dependencies] +gitdb = ">=4.0.1,<5" + +[package.extras] +test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-sugar"] + +[[package]] +name = "google-auth" +version = "2.15.0" +description = "Google Authentication Library" +optional = true +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" +files = [ + {file = "google-auth-2.15.0.tar.gz", hash = "sha256:72f12a6cfc968d754d7bdab369c5c5c16032106e52d32c6dfd8484e4c01a6d1f"}, + {file = "google_auth-2.15.0-py2.py3-none-any.whl", hash = "sha256:6897b93556d8d807ad70701bb89f000183aea366ca7ed94680828b37437a4994"}, +] + +[package.dependencies] +cachetools = ">=2.0.0,<6.0" +pyasn1-modules = ">=0.2.1" +rsa = {version = ">=3.1.4,<5", markers = "python_version >= \"3.6\""} +six = ">=1.9.0" + +[package.extras] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)", "requests (>=2.20.0,<3.0.0dev)"] +enterprise-cert = ["cryptography (==36.0.2)", "pyopenssl (==22.0.0)"] +pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +reauth = ["pyu2f (>=0.1.5)"] + +[[package]] +name = "google-auth-oauthlib" +version = "0.4.6" +description = "Google Authentication Library" +optional = true +python-versions = ">=3.6" +files = [ + {file = "google-auth-oauthlib-0.4.6.tar.gz", hash = "sha256:a90a072f6993f2c327067bf65270046384cda5a8ecb20b94ea9a687f1f233a7a"}, + {file = "google_auth_oauthlib-0.4.6-py2.py3-none-any.whl", hash = "sha256:3f2a6e802eebbb6fb736a370fbf3b055edcb6b52878bf2f26330b5e041316c73"}, +] + +[package.dependencies] +google-auth = ">=1.0.0" +requests-oauthlib = ">=0.7.0" + +[package.extras] +tool = ["click (>=6.0.0)"] + +[[package]] +name = "google-pasta" +version = "0.2.0" +description = "pasta is an AST-based Python refactoring library" +optional = true +python-versions = "*" +files = [ + {file = "google-pasta-0.2.0.tar.gz", hash = "sha256:c9f2c8dfc8f96d0d5808299920721be30c9eec37f2389f28904f454565c8a16e"}, + {file = "google_pasta-0.2.0-py2-none-any.whl", hash = "sha256:4612951da876b1a10fe3960d7226f0c7682cf901e16ac06e473b267a5afa8954"}, + {file = "google_pasta-0.2.0-py3-none-any.whl", hash = "sha256:b32482794a366b5366a32c92a9a9201b107821889935a02b3e51f6b432ea84ed"}, +] + +[package.dependencies] +six = "*" + +[[package]] +name = "googleapis-common-protos" +version = "1.57.0" +description = "Common protobufs used in Google APIs" +optional = true +python-versions = ">=3.7" +files = [ + {file = "googleapis-common-protos-1.57.0.tar.gz", hash = "sha256:27a849d6205838fb6cc3c1c21cb9800707a661bb21c6ce7fb13e99eb1f8a0c46"}, + {file = "googleapis_common_protos-1.57.0-py2.py3-none-any.whl", hash = "sha256:a9f4a1d7f6d9809657b7f1316a1aa527f6664891531bcfcc13b6696e685f443c"}, +] + +[package.dependencies] +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0dev" + +[package.extras] +grpc = ["grpcio (>=1.44.0,<2.0.0dev)"] + +[[package]] +name = "grpcio" +version = "1.51.1" +description = "HTTP/2-based RPC framework" +optional = true +python-versions = ">=3.7" +files = [ + {file = "grpcio-1.51.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:cc2bece1737b44d878cc1510ea04469a8073dbbcdd762175168937ae4742dfb3"}, + {file = "grpcio-1.51.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:e223a9793522680beae44671b9ed8f6d25bbe5ddf8887e66aebad5e0686049ef"}, + {file = "grpcio-1.51.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:24ac1154c4b2ab4a0c5326a76161547e70664cd2c39ba75f00fc8a2170964ea2"}, + {file = "grpcio-1.51.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4ef09f8997c4be5f3504cefa6b5c6cc3cf648274ce3cede84d4342a35d76db6"}, + {file = "grpcio-1.51.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0b77e992c64880e6efbe0086fe54dfc0bbd56f72a92d9e48264dcd2a3db98"}, + {file = "grpcio-1.51.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:eacad297ea60c72dd280d3353d93fb1dcca952ec11de6bb3c49d12a572ba31dd"}, + {file = "grpcio-1.51.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:16c71740640ba3a882f50b01bf58154681d44b51f09a5728180a8fdc66c67bd5"}, + {file = "grpcio-1.51.1-cp310-cp310-win32.whl", hash = "sha256:29cb97d41a4ead83b7bcad23bdb25bdd170b1e2cba16db6d3acbb090bc2de43c"}, + {file = "grpcio-1.51.1-cp310-cp310-win_amd64.whl", hash = "sha256:9ff42c5620b4e4530609e11afefa4a62ca91fa0abb045a8957e509ef84e54d30"}, + {file = "grpcio-1.51.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:bc59f7ba87972ab236f8669d8ca7400f02a0eadf273ca00e02af64d588046f02"}, + {file = "grpcio-1.51.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:3c2b3842dcf870912da31a503454a33a697392f60c5e2697c91d133130c2c85d"}, + {file = "grpcio-1.51.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22b011674090594f1f3245960ced7386f6af35485a38901f8afee8ad01541dbd"}, + {file = "grpcio-1.51.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49d680356a975d9c66a678eb2dde192d5dc427a7994fb977363634e781614f7c"}, + {file = "grpcio-1.51.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:094e64236253590d9d4075665c77b329d707b6fca864dd62b144255e199b4f87"}, + {file = "grpcio-1.51.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:257478300735ce3c98d65a930bbda3db172bd4e00968ba743e6a1154ea6edf10"}, + {file = "grpcio-1.51.1-cp311-cp311-win32.whl", hash = "sha256:5a6ebcdef0ef12005d56d38be30f5156d1cb3373b52e96f147f4a24b0ddb3a9d"}, + {file = "grpcio-1.51.1-cp311-cp311-win_amd64.whl", hash = "sha256:3f9b0023c2c92bebd1be72cdfca23004ea748be1813a66d684d49d67d836adde"}, + {file = "grpcio-1.51.1-cp37-cp37m-linux_armv7l.whl", hash = "sha256:cd3baccea2bc5c38aeb14e5b00167bd4e2373a373a5e4d8d850bd193edad150c"}, + {file = "grpcio-1.51.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:17ec9b13cec4a286b9e606b48191e560ca2f3bbdf3986f91e480a95d1582e1a7"}, + {file = "grpcio-1.51.1-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:fbdbe9a849854fe484c00823f45b7baab159bdd4a46075302281998cb8719df5"}, + {file = "grpcio-1.51.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31bb6bc7ff145e2771c9baf612f4b9ebbc9605ccdc5f3ff3d5553de7fc0e0d79"}, + {file = "grpcio-1.51.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e473525c28251558337b5c1ad3fa969511e42304524a4e404065e165b084c9e4"}, + {file = "grpcio-1.51.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6f0b89967ee11f2b654c23b27086d88ad7bf08c0b3c2a280362f28c3698b2896"}, + {file = "grpcio-1.51.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7942b32a291421460d6a07883033e392167d30724aa84987e6956cd15f1a21b9"}, + {file = "grpcio-1.51.1-cp37-cp37m-win32.whl", hash = "sha256:f96ace1540223f26fbe7c4ebbf8a98e3929a6aa0290c8033d12526847b291c0f"}, + {file = "grpcio-1.51.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f1fec3abaf274cdb85bf3878167cfde5ad4a4d97c68421afda95174de85ba813"}, + {file = "grpcio-1.51.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:0e1a9e1b4a23808f1132aa35f968cd8e659f60af3ffd6fb00bcf9a65e7db279f"}, + {file = "grpcio-1.51.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:6df3b63538c362312bc5fa95fb965069c65c3ea91d7ce78ad9c47cab57226f54"}, + {file = "grpcio-1.51.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:172405ca6bdfedd6054c74c62085946e45ad4d9cec9f3c42b4c9a02546c4c7e9"}, + {file = "grpcio-1.51.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:506b9b7a4cede87d7219bfb31014d7b471cfc77157da9e820a737ec1ea4b0663"}, + {file = "grpcio-1.51.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fb93051331acbb75b49a2a0fd9239c6ba9528f6bdc1dd400ad1cb66cf864292"}, + {file = "grpcio-1.51.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5dca372268c6ab6372d37d6b9f9343e7e5b4bc09779f819f9470cd88b2ece3c3"}, + {file = "grpcio-1.51.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:471d39d3370ca923a316d49c8aac66356cea708a11e647e3bdc3d0b5de4f0a40"}, + {file = "grpcio-1.51.1-cp38-cp38-win32.whl", hash = "sha256:75e29a90dc319f0ad4d87ba6d20083615a00d8276b51512e04ad7452b5c23b04"}, + {file = "grpcio-1.51.1-cp38-cp38-win_amd64.whl", hash = "sha256:f1158bccbb919da42544a4d3af5d9296a3358539ffa01018307337365a9a0c64"}, + {file = "grpcio-1.51.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:59dffade859f157bcc55243714d57b286da6ae16469bf1ac0614d281b5f49b67"}, + {file = "grpcio-1.51.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:dad6533411d033b77f5369eafe87af8583178efd4039c41d7515d3336c53b4f1"}, + {file = "grpcio-1.51.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:4c4423ea38a7825b8fed8934d6d9aeebdf646c97e3c608c3b0bcf23616f33877"}, + {file = "grpcio-1.51.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0dc5354e38e5adf2498312f7241b14c7ce3484eefa0082db4297189dcbe272e6"}, + {file = "grpcio-1.51.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97d67983189e2e45550eac194d6234fc38b8c3b5396c153821f2d906ed46e0ce"}, + {file = "grpcio-1.51.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:538d981818e49b6ed1e9c8d5e5adf29f71c4e334e7d459bf47e9b7abb3c30e09"}, + {file = "grpcio-1.51.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9235dcd5144a83f9ca6f431bd0eccc46b90e2c22fe27b7f7d77cabb2fb515595"}, + {file = "grpcio-1.51.1-cp39-cp39-win32.whl", hash = "sha256:aacb54f7789ede5cbf1d007637f792d3e87f1c9841f57dd51abf89337d1b8472"}, + {file = "grpcio-1.51.1-cp39-cp39-win_amd64.whl", hash = "sha256:2b170eaf51518275c9b6b22ccb59450537c5a8555326fd96ff7391b5dd75303c"}, + {file = "grpcio-1.51.1.tar.gz", hash = "sha256:e6dfc2b6567b1c261739b43d9c59d201c1b89e017afd9e684d85aa7a186c9f7a"}, +] + +[package.extras] +protobuf = ["grpcio-tools (>=1.51.1)"] + +[[package]] +name = "h5py" +version = "3.7.0" +description = "Read and write HDF5 files from Python" +optional = true +python-versions = ">=3.7" +files = [ + {file = "h5py-3.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d77af42cb751ad6cc44f11bae73075a07429a5cf2094dfde2b1e716e059b3911"}, + {file = "h5py-3.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63beb8b7b47d0896c50de6efb9a1eaa81dbe211f3767e7dd7db159cea51ba37a"}, + {file = "h5py-3.7.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04e2e1e2fc51b8873e972a08d2f89625ef999b1f2d276199011af57bb9fc7851"}, + {file = "h5py-3.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f73307c876af49aa869ec5df1818e9bb0bdcfcf8a5ba773cc45a4fba5a286a5c"}, + {file = "h5py-3.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:f514b24cacdd983e61f8d371edac8c1b780c279d0acb8485639e97339c866073"}, + {file = "h5py-3.7.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:43fed4d13743cf02798a9a03a360a88e589d81285e72b83f47d37bb64ed44881"}, + {file = "h5py-3.7.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c038399ce09a58ff8d89ec3e62f00aa7cb82d14f34e24735b920e2a811a3a426"}, + {file = "h5py-3.7.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03d64fb86bb86b978928bad923b64419a23e836499ec6363e305ad28afd9d287"}, + {file = "h5py-3.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e5b7820b75f9519499d76cc708e27242ccfdd9dfb511d6deb98701961d0445aa"}, + {file = "h5py-3.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a9351d729ea754db36d175098361b920573fdad334125f86ac1dd3a083355e20"}, + {file = "h5py-3.7.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6776d896fb90c5938de8acb925e057e2f9f28755f67ec3edcbc8344832616c38"}, + {file = "h5py-3.7.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0a047fddbe6951bce40e9cde63373c838a978c5e05a011a682db9ba6334b8e85"}, + {file = "h5py-3.7.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0798a9c0ff45f17d0192e4d7114d734cac9f8b2b2c76dd1d923c4d0923f27bb6"}, + {file = "h5py-3.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:0d8de8cb619fc597da7cf8cdcbf3b7ff8c5f6db836568afc7dc16d21f59b2b49"}, + {file = "h5py-3.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f084bbe816907dfe59006756f8f2d16d352faff2d107f4ffeb1d8de126fc5dc7"}, + {file = "h5py-3.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1fcb11a2dc8eb7ddcae08afd8fae02ba10467753a857fa07a404d700a93f3d53"}, + {file = "h5py-3.7.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ed43e2cc4f511756fd664fb45d6b66c3cbed4e3bd0f70e29c37809b2ae013c44"}, + {file = "h5py-3.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e7535df5ee3dc3e5d1f408fdfc0b33b46bc9b34db82743c82cd674d8239b9ad"}, + {file = "h5py-3.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:9e2ad2aa000f5b1e73b5dfe22f358ca46bf1a2b6ca394d9659874d7fc251731a"}, + {file = "h5py-3.7.0.tar.gz", hash = "sha256:3fcf37884383c5da64846ab510190720027dca0768def34dd8dcb659dbe5cbf3"}, +] + +[package.dependencies] +numpy = ">=1.14.5" + +[[package]] +name = "huggingface-hub" +version = "0.11.1" +description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "huggingface_hub-0.11.1-py3-none-any.whl", hash = "sha256:11eed7aab4fa4d1fb532f2aea3379ef4998d9f6bc24a330834dfedd3dac7f441"}, + {file = "huggingface_hub-0.11.1.tar.gz", hash = "sha256:8b9ebf9bbb1782f6f0419ec490973a6487c6c4ed84293a8a325d34c4f898f53f"}, +] + +[package.dependencies] +filelock = "*" +packaging = ">=20.9" +pyyaml = ">=5.1" +requests = "*" +tqdm = "*" +typing-extensions = ">=3.7.4.3" + +[package.extras] +all = ["InquirerPy (==0.3.4)", "Jinja2", "black (==22.3)", "flake8 (>=3.8.3)", "flake8-bugbear", "isort (>=5.5.4)", "jedi", "mypy (==0.982)", "pytest", "pytest-cov", "pytest-env", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3"] +cli = ["InquirerPy (==0.3.4)"] +dev = ["InquirerPy (==0.3.4)", "Jinja2", "black (==22.3)", "flake8 (>=3.8.3)", "flake8-bugbear", "isort (>=5.5.4)", "jedi", "mypy (==0.982)", "pytest", "pytest-cov", "pytest-env", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3"] +fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] +quality = ["black (==22.3)", "flake8 (>=3.8.3)", "flake8-bugbear", "isort (>=5.5.4)", "mypy (==0.982)"] +tensorflow = ["graphviz", "pydot", "tensorflow"] +testing = ["InquirerPy (==0.3.4)", "Jinja2", "isort (>=5.5.4)", "jedi", "pytest", "pytest-cov", "pytest-env", "soundfile"] +torch = ["torch"] +typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3"] + +[[package]] +name = "identify" +version = "2.5.9" +description = "File identification library for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "identify-2.5.9-py2.py3-none-any.whl", hash = "sha256:a390fb696e164dbddb047a0db26e57972ae52fbd037ae68797e5ae2f4492485d"}, + {file = "identify-2.5.9.tar.gz", hash = "sha256:906036344ca769539610436e40a684e170c3648b552194980bb7b617a8daeb9f"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "imageio" +version = "2.23.0" +description = "Library for reading and writing a wide range of image, video, scientific, and volumetric data formats." +optional = false +python-versions = ">=3.7" +files = [ + {file = "imageio-2.23.0-py3-none-any.whl", hash = "sha256:3f937396c2f341a7d7600a99cd14727cc90002f850fd136cf65fad79c2ff44a0"}, + {file = "imageio-2.23.0.tar.gz", hash = "sha256:cb635709765e527c94890b4fbb6870e59213fe182a1c8086d167eb3626073cbd"}, +] + +[package.dependencies] +numpy = "*" +pillow = ">=8.3.2" + +[package.extras] +all-plugins = ["astropy", "av", "imageio-ffmpeg", "opencv-python", "psutil", "tifffile"] +all-plugins-pypy = ["av", "imageio-ffmpeg", "psutil", "tifffile"] +build = ["wheel"] +dev = ["black", "flake8", "fsspec[github]", "invoke", "pytest", "pytest-cov"] +docs = ["numpydoc", "pydata-sphinx-theme", "sphinx"] +ffmpeg = ["imageio-ffmpeg", "psutil"] +fits = ["astropy"] +full = ["astropy", "av", "black", "flake8", "fsspec[github]", "gdal", "imageio-ffmpeg", "invoke", "itk", "numpydoc", "opencv-python", "psutil", "pydata-sphinx-theme", "pytest", "pytest-cov", "sphinx", "tifffile", "wheel"] +gdal = ["gdal"] +itk = ["itk"] +linting = ["black", "flake8"] +opencv = ["opencv-python"] +pyav = ["av"] +test = ["fsspec[github]", "invoke", "pytest", "pytest-cov"] +tifffile = ["tifffile"] + +[[package]] +name = "imageio-ffmpeg" +version = "0.4.7" +description = "FFMPEG wrapper for Python" +optional = false +python-versions = ">=3.5" +files = [ + {file = "imageio-ffmpeg-0.4.7.tar.gz", hash = "sha256:7a08838f97f363e37ca41821b864fd3fdc99ab1fe2421040c78eb5f56a9e723e"}, + {file = "imageio_ffmpeg-0.4.7-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:6514f1380daf42815bc8c83aad63f33e0b8b47133421ddafe7b410cd8dfbbea5"}, + {file = "imageio_ffmpeg-0.4.7-py3-none-manylinux2010_x86_64.whl", hash = "sha256:27b48c32becae1658aa81c3a6b922538e4099edf5fbcbdb4ff5dbc84b8ffd3d3"}, + {file = "imageio_ffmpeg-0.4.7-py3-none-manylinux2014_aarch64.whl", hash = "sha256:fc60686ef03c2d0f842901b206223c30051a6a120384458761390104470846fd"}, + {file = "imageio_ffmpeg-0.4.7-py3-none-win32.whl", hash = "sha256:6aba52ddf0a64442ffcb8d30ac6afb668186acec99ecbc7ae5bd171c4f500bbc"}, + {file = "imageio_ffmpeg-0.4.7-py3-none-win_amd64.whl", hash = "sha256:8e724d12dfe83e2a6eb39619e820243ca96c81c47c2648e66e05f7ee24e14312"}, +] + +[[package]] +name = "importlib-metadata" +version = "5.1.0" +description = "Read metadata from Python packages" +optional = true +python-versions = ">=3.7" +files = [ + {file = "importlib_metadata-5.1.0-py3-none-any.whl", hash = "sha256:d84d17e21670ec07990e1044a99efe8d615d860fd176fc29ef5c306068fda313"}, + {file = "importlib_metadata-5.1.0.tar.gz", hash = "sha256:d5059f9f1e8e41f80e9c56c2ee58811450c31984dfa625329ffd7c0dad88a73b"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +perf = ["ipython"] +testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] + +[[package]] +name = "importlib-resources" +version = "5.10.1" +description = "Read resources from Python packages" +optional = true +python-versions = ">=3.7" +files = [ + {file = "importlib_resources-5.10.1-py3-none-any.whl", hash = "sha256:c09b067d82e72c66f4f8eb12332f5efbebc9b007c0b6c40818108c9870adc363"}, + {file = "importlib_resources-5.10.1.tar.gz", hash = "sha256:32bb095bda29741f6ef0e5278c42df98d135391bee5f932841efc0041f748dc3"}, +] + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +optional = false +python-versions = "*" +files = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] + +[[package]] +name = "ipykernel" +version = "6.19.2" +description = "IPython Kernel for Jupyter" +optional = true +python-versions = ">=3.8" +files = [ + {file = "ipykernel-6.19.2-py3-none-any.whl", hash = "sha256:1374a55c57ca7a7286c3d8b15799cd76e1a2381b6b1fea99c494b955988926b6"}, + {file = "ipykernel-6.19.2.tar.gz", hash = "sha256:1ab68d3d3654196266baa93990055413e167263ffbe4cfe834f871bcd3d3506d"}, +] + +[package.dependencies] +appnope = {version = "*", markers = "platform_system == \"Darwin\""} +comm = ">=0.1.1" +debugpy = ">=1.0" +ipython = ">=7.23.1" +jupyter-client = ">=6.1.12" +matplotlib-inline = ">=0.1" +nest-asyncio = "*" +packaging = "*" +psutil = "*" +pyzmq = ">=17" +tornado = ">=6.1" +traitlets = ">=5.4.0" + +[package.extras] +cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt"] +lint = ["black (>=22.6.0)", "mdformat (>0.7)", "ruff (>=0.0.156)"] +test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio", "pytest-cov", "pytest-timeout"] +typing = ["mypy (>=0.990)"] + +[[package]] +name = "ipython" +version = "8.7.0" +description = "IPython: Productive Interactive Computing" +optional = true +python-versions = ">=3.8" +files = [ + {file = "ipython-8.7.0-py3-none-any.whl", hash = "sha256:352042ddcb019f7c04e48171b4dd78e4c4bb67bf97030d170e154aac42b656d9"}, + {file = "ipython-8.7.0.tar.gz", hash = "sha256:882899fe78d5417a0aa07f995db298fa28b58faeba2112d2e3a4c95fe14bb738"}, +] + +[package.dependencies] +appnope = {version = "*", markers = "sys_platform == \"darwin\""} +backcall = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +pickleshare = "*" +prompt-toolkit = ">=3.0.11,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5" + +[package.extras] +all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.20)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +black = ["black"] +doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.20)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] + +[[package]] +name = "ipython_genutils" +version = "0.2.0" +description = "Vestigial utilities from IPython" +optional = true +python-versions = "*" +files = [ + {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, + {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, +] + +[[package]] +name = "isoduration" +version = "20.11.0" +description = "Operations with ISO 8601 durations" +optional = true +python-versions = ">=3.7" +files = [ + {file = "isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042"}, + {file = "isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9"}, +] + +[package.dependencies] +arrow = ">=0.15.0" + +[[package]] +name = "isort" +version = "5.12.0" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, +] + +[package.extras] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + +[[package]] +name = "jedi" +version = "0.18.2" +description = "An autocompletion tool for Python that can be used for text editors." +optional = true +python-versions = ">=3.6" +files = [ + {file = "jedi-0.18.2-py2.py3-none-any.whl", hash = "sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e"}, + {file = "jedi-0.18.2.tar.gz", hash = "sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612"}, +] + +[package.dependencies] +parso = ">=0.8.0,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] + +[[package]] +name = "Jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +optional = true +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "joblib" +version = "1.3.2" +description = "Lightweight pipelining with Python functions" +optional = true +python-versions = ">=3.7" +files = [ + {file = "joblib-1.3.2-py3-none-any.whl", hash = "sha256:ef4331c65f239985f3f2220ecc87db222f08fd22097a3dd5698f693875f8cbb9"}, + {file = "joblib-1.3.2.tar.gz", hash = "sha256:92f865e621e17784e7955080b6d042489e3b8e294949cc44c6eac304f59772b1"}, +] + +[[package]] +name = "jsonpointer" +version = "2.3" +description = "Identify specific nodes in a JSON document (RFC 6901)" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "jsonpointer-2.3-py2.py3-none-any.whl", hash = "sha256:51801e558539b4e9cd268638c078c6c5746c9ac96bc38152d443400e4f3793e9"}, + {file = "jsonpointer-2.3.tar.gz", hash = "sha256:97cba51526c829282218feb99dab1b1e6bdf8efd1c43dc9d57be093c0d69c99a"}, +] + +[[package]] +name = "jsonschema" +version = "4.17.3" +description = "An implementation of JSON Schema validation for Python" +optional = true +python-versions = ">=3.7" +files = [ + {file = "jsonschema-4.17.3-py3-none-any.whl", hash = "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6"}, + {file = "jsonschema-4.17.3.tar.gz", hash = "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d"}, +] + +[package.dependencies] +attrs = ">=17.4.0" +fqdn = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +idna = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} +isoduration = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format-nongpl\""} +pkgutil-resolve-name = {version = ">=1.3.10", markers = "python_version < \"3.9\""} +pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2" +rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +rfc3986-validator = {version = ">0.1.0", optional = true, markers = "extra == \"format-nongpl\""} +uri-template = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +webcolors = {version = ">=1.11", optional = true, markers = "extra == \"format-nongpl\""} + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] + +[[package]] +name = "jupyter-client" +version = "7.4.8" +description = "Jupyter protocol implementation and client libraries" +optional = true +python-versions = ">=3.7" +files = [ + {file = "jupyter_client-7.4.8-py3-none-any.whl", hash = "sha256:d4a67ae86ee014bcb96bd8190714f6af921f2b0f52f4208b086aa5acfd9f8d65"}, + {file = "jupyter_client-7.4.8.tar.gz", hash = "sha256:109a3c33b62a9cf65aa8325850a0999a795fac155d9de4f7555aef5f310ee35a"}, +] + +[package.dependencies] +entrypoints = "*" +jupyter-core = ">=4.9.2" +nest-asyncio = ">=1.5.4" +python-dateutil = ">=2.8.2" +pyzmq = ">=23.0" +tornado = ">=6.2" +traitlets = "*" + +[package.extras] +doc = ["ipykernel", "myst-parser", "sphinx (>=1.3.6)", "sphinx-rtd-theme", "sphinxcontrib-github-alt"] +test = ["codecov", "coverage", "ipykernel (>=6.12)", "ipython", "mypy", "pre-commit", "pytest", "pytest-asyncio (>=0.18)", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "jupyter-core" +version = "5.1.0" +description = "Jupyter core package. A base package on which Jupyter projects rely." +optional = true +python-versions = ">=3.8" +files = [ + {file = "jupyter_core-5.1.0-py3-none-any.whl", hash = "sha256:f5740d99606958544396914b08e67b668f45e7eff99ab47a7f4bcead419c02f4"}, + {file = "jupyter_core-5.1.0.tar.gz", hash = "sha256:a5ae7c09c55c0b26f692ec69323ba2b62e8d7295354d20f6cd57b749de4a05bf"}, +] + +[package.dependencies] +platformdirs = ">=2.5" +pywin32 = {version = ">=1.0", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} +traitlets = ">=5.3" + +[package.extras] +docs = ["myst-parser", "sphinxcontrib-github-alt", "traitlets"] +test = ["ipykernel", "pre-commit", "pytest", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "jupyter-events" +version = "0.5.0" +description = "Jupyter Event System library" +optional = true +python-versions = ">=3.7" +files = [ + {file = "jupyter_events-0.5.0-py3-none-any.whl", hash = "sha256:6f7b67bf42b8a370c992187194ed02847dfa02307a7aebe9913e2d3979b9b6b8"}, + {file = "jupyter_events-0.5.0.tar.gz", hash = "sha256:e27ffdd6138699d47d42cb65ae6d79334ff7c0d923694381c991ce56a140f2cd"}, +] + +[package.dependencies] +jsonschema = {version = ">=4.3.0", extras = ["format-nongpl"]} +python-json-logger = "*" +pyyaml = "*" +traitlets = "*" + +[package.extras] +cli = ["click", "rich"] +test = ["click", "coverage", "pre-commit", "pytest (>=6.1.0)", "pytest-asyncio (>=0.19.0)", "pytest-console-scripts", "pytest-cov", "rich"] + +[[package]] +name = "jupyter-server" +version = "2.0.1" +description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications." +optional = true +python-versions = ">=3.8" +files = [ + {file = "jupyter_server-2.0.1-py3-none-any.whl", hash = "sha256:3bc09974a5290249de6924a614933e6f4f3d6d11f3061423a9f4e0271064a8b3"}, + {file = "jupyter_server-2.0.1.tar.gz", hash = "sha256:6e71268380ad7e4f2d9dda2f3e51a4fd4d1997b5390d5acdb74c7a195cfe4c00"}, +] + +[package.dependencies] +anyio = ">=3.1.0,<4" +argon2-cffi = "*" +jinja2 = "*" +jupyter-client = ">=7.4.4" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-events = ">=0.4.0" +jupyter-server-terminals = "*" +nbconvert = ">=6.4.4" +nbformat = ">=5.3.0" +packaging = "*" +prometheus-client = "*" +pywinpty = {version = "*", markers = "os_name == \"nt\""} +pyzmq = ">=24" +send2trash = "*" +terminado = ">=0.8.3" +tornado = ">=6.2.0" +traitlets = ">=5.6.0" +websocket-client = "*" + +[package.extras] +docs = ["docutils (<0.20)", "ipykernel", "jinja2", "jupyter-client", "jupyter-server", "mistune (<1.0.0)", "myst-parser", "nbformat", "prometheus-client", "pydata-sphinx-theme", "send2trash", "sphinxcontrib-github-alt", "sphinxcontrib-openapi", "sphinxemoji", "tornado"] +lint = ["black (>=22.6.0)", "mdformat (>0.7)", "ruff (>=0.0.156)"] +test = ["ipykernel", "pre-commit", "pytest (>=7.0)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.4)", "pytest-timeout", "requests"] +typing = ["mypy (>=0.990)"] + +[[package]] +name = "jupyter-server-terminals" +version = "0.4.2" +description = "A Jupyter Server Extension Providing Terminals." +optional = true +python-versions = ">=3.8" +files = [ + {file = "jupyter_server_terminals-0.4.2-py3-none-any.whl", hash = "sha256:c0eaacee6cac21b597c23c38dd523dc4e9b947f97af5101e0396c08f28db3e37"}, + {file = "jupyter_server_terminals-0.4.2.tar.gz", hash = "sha256:0e68cba38eb0f9f2d93f1160e0a7f84b943d0d0c4d2f77eeaabbb4a2919c47c6"}, +] + +[package.dependencies] +pywinpty = {version = ">=2.0.3", markers = "os_name == \"nt\""} +terminado = ">=0.8.3" + +[package.extras] +docs = ["jinja2", "jupyter-server", "mistune (<2.0)", "myst-parser", "nbformat", "packaging", "pydata-sphinx-theme", "sphinxcontrib-github-alt", "sphinxcontrib-openapi", "sphinxemoji", "tornado"] +test = ["coverage", "jupyter-server (>=2.0.0rc8)", "pytest (>=7.0)", "pytest-cov", "pytest-jupyter[server] (>=0.5.3)", "pytest-timeout"] + +[[package]] +name = "jupyterlab-pygments" +version = "0.2.2" +description = "Pygments theme using JupyterLab CSS variables" +optional = true +python-versions = ">=3.7" +files = [ + {file = "jupyterlab_pygments-0.2.2-py2.py3-none-any.whl", hash = "sha256:2405800db07c9f770863bcf8049a529c3dd4d3e28536638bd7c1c01d2748309f"}, + {file = "jupyterlab_pygments-0.2.2.tar.gz", hash = "sha256:7405d7fde60819d905a9fa8ce89e4cd830e318cdad22a0030f7a901da705585d"}, +] + +[[package]] +name = "keras" +version = "2.11.0" +description = "Deep learning for humans." +optional = true +python-versions = ">=3.7" +files = [ + {file = "keras-2.11.0-py2.py3-none-any.whl", hash = "sha256:38c6fff0ea9a8b06a2717736565c92a73c8cd9b1c239e7125ccb188b7848f65e"}, +] + +[[package]] +name = "kiwisolver" +version = "1.4.4" +description = "A fast implementation of the Cassowary constraint solver" +optional = true +python-versions = ">=3.7" +files = [ + {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6"}, + {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c"}, + {file = "kiwisolver-1.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3"}, + {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938"}, + {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d"}, + {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09"}, + {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de"}, + {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32"}, + {file = "kiwisolver-1.4.4-cp310-cp310-win32.whl", hash = "sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408"}, + {file = "kiwisolver-1.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004"}, + {file = "kiwisolver-1.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ea21f66820452a3f5d1655f8704a60d66ba1191359b96541eaf457710a5fc6"}, + {file = "kiwisolver-1.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bc9db8a3efb3e403e4ecc6cd9489ea2bac94244f80c78e27c31dcc00d2790ac2"}, + {file = "kiwisolver-1.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5b61785a9ce44e5a4b880272baa7cf6c8f48a5180c3e81c59553ba0cb0821ca"}, + {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2dbb44c3f7e6c4d3487b31037b1bdbf424d97687c1747ce4ff2895795c9bf69"}, + {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6295ecd49304dcf3bfbfa45d9a081c96509e95f4b9d0eb7ee4ec0530c4a96514"}, + {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bd472dbe5e136f96a4b18f295d159d7f26fd399136f5b17b08c4e5f498cd494"}, + {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf7d9fce9bcc4752ca4a1b80aabd38f6d19009ea5cbda0e0856983cf6d0023f5"}, + {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d6601aed50c74e0ef02f4204da1816147a6d3fbdc8b3872d263338a9052c51"}, + {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:877272cf6b4b7e94c9614f9b10140e198d2186363728ed0f701c6eee1baec1da"}, + {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:db608a6757adabb32f1cfe6066e39b3706d8c3aa69bbc353a5b61edad36a5cb4"}, + {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5853eb494c71e267912275e5586fe281444eb5e722de4e131cddf9d442615626"}, + {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f0a1dbdb5ecbef0d34eb77e56fcb3e95bbd7e50835d9782a45df81cc46949750"}, + {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:283dffbf061a4ec60391d51e6155e372a1f7a4f5b15d59c8505339454f8989e4"}, + {file = "kiwisolver-1.4.4-cp311-cp311-win32.whl", hash = "sha256:d06adcfa62a4431d404c31216f0f8ac97397d799cd53800e9d3efc2fbb3cf14e"}, + {file = "kiwisolver-1.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e7da3fec7408813a7cebc9e4ec55afed2d0fd65c4754bc376bf03498d4e92686"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:62ac9cc684da4cf1778d07a89bf5f81b35834cb96ca523d3a7fb32509380cbf6"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41dae968a94b1ef1897cb322b39360a0812661dba7c682aa45098eb8e193dbdf"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02f79693ec433cb4b5f51694e8477ae83b3205768a6fb48ffba60549080e295b"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0611a0a2a518464c05ddd5a3a1a0e856ccc10e67079bb17f265ad19ab3c7597"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:db5283d90da4174865d520e7366801a93777201e91e79bacbac6e6927cbceede"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1041feb4cda8708ce73bb4dcb9ce1ccf49d553bf87c3954bdfa46f0c3f77252c"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-win32.whl", hash = "sha256:a553dadda40fef6bfa1456dc4be49b113aa92c2a9a9e8711e955618cd69622e3"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:03baab2d6b4a54ddbb43bba1a3a2d1627e82d205c5cf8f4c924dc49284b87166"}, + {file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454"}, + {file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0"}, + {file = "kiwisolver-1.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c"}, + {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae"}, + {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0"}, + {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1"}, + {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d"}, + {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c"}, + {file = "kiwisolver-1.4.4-cp38-cp38-win32.whl", hash = "sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191"}, + {file = "kiwisolver-1.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766"}, + {file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8"}, + {file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897"}, + {file = "kiwisolver-1.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824"}, + {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29"}, + {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f"}, + {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd"}, + {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac"}, + {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9"}, + {file = "kiwisolver-1.4.4-cp39-cp39-win32.whl", hash = "sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea"}, + {file = "kiwisolver-1.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b"}, + {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a"}, + {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d"}, + {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a"}, + {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871"}, + {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:28bc5b299f48150b5f822ce68624e445040595a4ac3d59251703779836eceff9"}, + {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:81e38381b782cc7e1e46c4e14cd997ee6040768101aefc8fa3c24a4cc58e98f8"}, + {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2a66fdfb34e05b705620dd567f5a03f239a088d5a3f321e7b6ac3239d22aa286"}, + {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872b8ca05c40d309ed13eb2e582cab0c5a05e81e987ab9c521bf05ad1d5cf5cb"}, + {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:70e7c2e7b750585569564e2e5ca9845acfaa5da56ac46df68414f29fea97be9f"}, + {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9f85003f5dfa867e86d53fac6f7e6f30c045673fa27b603c397753bebadc3008"}, + {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e307eb9bd99801f82789b44bb45e9f541961831c7311521b13a6c85afc09767"}, + {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1792d939ec70abe76f5054d3f36ed5656021dcad1322d1cc996d4e54165cef9"}, + {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cb459eea32a4e2cf18ba5fcece2dbdf496384413bc1bae15583f19e567f3b2"}, + {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36dafec3d6d6088d34e2de6b85f9d8e2324eb734162fba59d2ba9ed7a2043d5b"}, + {file = "kiwisolver-1.4.4.tar.gz", hash = "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955"}, +] + +[[package]] +name = "libclang" +version = "14.0.6" +description = "Clang Python Bindings, mirrored from the official LLVM repo: https://github.com/llvm/llvm-project/tree/main/clang/bindings/python, to make the installation process easier." +optional = true +python-versions = "*" +files = [ + {file = "libclang-14.0.6-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:8791cf3c3b087c373a6d61e9199da7a541da922c9ddcfed1122090586b996d6e"}, + {file = "libclang-14.0.6-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b06fc76bd1e67c8b04b5719bf2ac5d6a323b289b245dfa9e468561d99538188"}, + {file = "libclang-14.0.6-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:e429853939423f276a25140b0b702442d7da9a09e001c05e48df888336947614"}, + {file = "libclang-14.0.6-py2.py3-none-manylinux2010_x86_64.whl", hash = "sha256:206d2789e4450a37d054e63b70451a6fc1873466397443fa13de2b3d4adb2796"}, + {file = "libclang-14.0.6-py2.py3-none-manylinux2014_aarch64.whl", hash = "sha256:e2add1703129b2abe066fb1890afa880870a89fd6ab4ec5d2a7a8dc8d271677e"}, + {file = "libclang-14.0.6-py2.py3-none-manylinux2014_armv7l.whl", hash = "sha256:5dd3c6fca1b007d308a4114afa8e4e9d32f32b2572520701d45fcc626ac5cd6c"}, + {file = "libclang-14.0.6-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cfb0e892ebb5dff6bd498ab5778adb8581f26a00fd8347b3c76c989fe2fd04f7"}, + {file = "libclang-14.0.6-py2.py3-none-win_amd64.whl", hash = "sha256:ea03c12675151837660cdd5dce65bd89320896ac3421efef43a36678f113ce95"}, + {file = "libclang-14.0.6-py2.py3-none-win_arm64.whl", hash = "sha256:2e4303e04517fcd11173cb2e51a7070eed71e16ef45d4e26a82c5e881cac3d27"}, + {file = "libclang-14.0.6.tar.gz", hash = "sha256:9052a8284d8846984f6fa826b1d7460a66d3b23a486d782633b42b6e3b418789"}, +] + +[[package]] +name = "lightning-utilities" +version = "0.9.0" +description = "PyTorch Lightning Sample project." +optional = false +python-versions = ">=3.7" +files = [ + {file = "lightning-utilities-0.9.0.tar.gz", hash = "sha256:efbf2c488c257f942abdfd06cf646fb84ca215a9663b60081811e22a15ee033b"}, + {file = "lightning_utilities-0.9.0-py3-none-any.whl", hash = "sha256:918dd90c775719e3855631db6282ad75c14da4c5727c4cebdd1589d865fad03d"}, +] + +[package.dependencies] +packaging = ">=17.1" +typing-extensions = "*" + +[package.extras] +cli = ["fire"] +docs = ["requests (>=2.0.0)"] +typing = ["mypy (>=1.0.0)"] + +[[package]] +name = "Markdown" +version = "3.4.1" +description = "Python implementation of Markdown." +optional = true +python-versions = ">=3.7" +files = [ + {file = "Markdown-3.4.1-py3-none-any.whl", hash = "sha256:08fb8465cffd03d10b9dd34a5c3fea908e20391a2a90b88d66362cb05beed186"}, + {file = "Markdown-3.4.1.tar.gz", hash = "sha256:3b809086bb6efad416156e00a0da66fe47618a5d6918dd688f53f40c8e4cfeff"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} + +[package.extras] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "MarkupSafe" +version = "2.1.1" +description = "Safely add untrusted strings to HTML/XML markup." +optional = true +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, + {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, +] + +[[package]] +name = "matplotlib" +version = "3.6.2" +description = "Python plotting package" +optional = true +python-versions = ">=3.8" +files = [ + {file = "matplotlib-3.6.2-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:8d0068e40837c1d0df6e3abf1cdc9a34a6d2611d90e29610fa1d2455aeb4e2e5"}, + {file = "matplotlib-3.6.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:252957e208c23db72ca9918cb33e160c7833faebf295aaedb43f5b083832a267"}, + {file = "matplotlib-3.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d50e8c1e571ee39b5dfbc295c11ad65988879f68009dd281a6e1edbc2ff6c18c"}, + {file = "matplotlib-3.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d840adcad7354be6f2ec28d0706528b0026e4c3934cc6566b84eac18633eab1b"}, + {file = "matplotlib-3.6.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78ec3c3412cf277e6252764ee4acbdbec6920cc87ad65862272aaa0e24381eee"}, + {file = "matplotlib-3.6.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9347cc6822f38db2b1d1ce992f375289670e595a2d1c15961aacbe0977407dfc"}, + {file = "matplotlib-3.6.2-cp310-cp310-win32.whl", hash = "sha256:e0bbee6c2a5bf2a0017a9b5e397babb88f230e6f07c3cdff4a4c4bc75ed7c617"}, + {file = "matplotlib-3.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:8a0ae37576ed444fe853709bdceb2be4c7df6f7acae17b8378765bd28e61b3ae"}, + {file = "matplotlib-3.6.2-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:5ecfc6559132116dedfc482d0ad9df8a89dc5909eebffd22f3deb684132d002f"}, + {file = "matplotlib-3.6.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9f335e5625feb90e323d7e3868ec337f7b9ad88b5d633f876e3b778813021dab"}, + {file = "matplotlib-3.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b2604c6450f9dd2c42e223b1f5dca9643a23cfecc9fde4a94bb38e0d2693b136"}, + {file = "matplotlib-3.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5afe0a7ea0e3a7a257907060bee6724a6002b7eec55d0db16fd32409795f3e1"}, + {file = "matplotlib-3.6.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca0e7a658fbafcddcaefaa07ba8dae9384be2343468a8e011061791588d839fa"}, + {file = "matplotlib-3.6.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32d29c8c26362169c80c5718ce367e8c64f4dd068a424e7110df1dd2ed7bd428"}, + {file = "matplotlib-3.6.2-cp311-cp311-win32.whl", hash = "sha256:5024b8ed83d7f8809982d095d8ab0b179bebc07616a9713f86d30cf4944acb73"}, + {file = "matplotlib-3.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:52c2bdd7cd0bf9d5ccdf9c1816568fd4ccd51a4d82419cc5480f548981b47dd0"}, + {file = "matplotlib-3.6.2-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:8a8dbe2cb7f33ff54b16bb5c500673502a35f18ac1ed48625e997d40c922f9cc"}, + {file = "matplotlib-3.6.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:380d48c15ec41102a2b70858ab1dedfa33eb77b2c0982cb65a200ae67a48e9cb"}, + {file = "matplotlib-3.6.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0844523dfaaff566e39dbfa74e6f6dc42e92f7a365ce80929c5030b84caa563a"}, + {file = "matplotlib-3.6.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7f716b6af94dc1b6b97c46401774472f0867e44595990fe80a8ba390f7a0a028"}, + {file = "matplotlib-3.6.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74153008bd24366cf099d1f1e83808d179d618c4e32edb0d489d526523a94d9f"}, + {file = "matplotlib-3.6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f41e57ad63d336fe50d3a67bb8eaa26c09f6dda6a59f76777a99b8ccd8e26aec"}, + {file = "matplotlib-3.6.2-cp38-cp38-win32.whl", hash = "sha256:d0e9ac04065a814d4cf2c6791a2ad563f739ae3ae830d716d54245c2b96fead6"}, + {file = "matplotlib-3.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:8a9d899953c722b9afd7e88dbefd8fb276c686c3116a43c577cfabf636180558"}, + {file = "matplotlib-3.6.2-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:f04f97797df35e442ed09f529ad1235d1f1c0f30878e2fe09a2676b71a8801e0"}, + {file = "matplotlib-3.6.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3964934731fd7a289a91d315919cf757f293969a4244941ab10513d2351b4e83"}, + {file = "matplotlib-3.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:168093410b99f647ba61361b208f7b0d64dde1172b5b1796d765cd243cadb501"}, + {file = "matplotlib-3.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e16dcaecffd55b955aa5e2b8a804379789c15987e8ebd2f32f01398a81e975b"}, + {file = "matplotlib-3.6.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83dc89c5fd728fdb03b76f122f43b4dcee8c61f1489e232d9ad0f58020523e1c"}, + {file = "matplotlib-3.6.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:795ad83940732b45d39b82571f87af0081c120feff2b12e748d96bb191169e33"}, + {file = "matplotlib-3.6.2-cp39-cp39-win32.whl", hash = "sha256:19d61ee6414c44a04addbe33005ab1f87539d9f395e25afcbe9a3c50ce77c65c"}, + {file = "matplotlib-3.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:5ba73aa3aca35d2981e0b31230d58abb7b5d7ca104e543ae49709208d8ce706a"}, + {file = "matplotlib-3.6.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1836f366272b1557a613f8265db220eb8dd883202bbbabe01bad5a4eadfd0c95"}, + {file = "matplotlib-3.6.2-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0eda9d1b43f265da91fb9ae10d6922b5a986e2234470a524e6b18f14095b20d2"}, + {file = "matplotlib-3.6.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec9be0f4826cdb3a3a517509dcc5f87f370251b76362051ab59e42b6b765f8c4"}, + {file = "matplotlib-3.6.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3cef89888a466228fc4e4b2954e740ce8e9afde7c4315fdd18caa1b8de58ca17"}, + {file = "matplotlib-3.6.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:54fa9fe27f5466b86126ff38123261188bed568c1019e4716af01f97a12fe812"}, + {file = "matplotlib-3.6.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e68be81cd8c22b029924b6d0ee814c337c0e706b8d88495a617319e5dd5441c3"}, + {file = "matplotlib-3.6.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0ca2c60d3966dfd6608f5f8c49b8a0fcf76de6654f2eda55fc6ef038d5a6f27"}, + {file = "matplotlib-3.6.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4426c74761790bff46e3d906c14c7aab727543293eed5a924300a952e1a3a3c1"}, + {file = "matplotlib-3.6.2.tar.gz", hash = "sha256:b03fd10a1709d0101c054883b550f7c4c5e974f751e2680318759af005964990"}, +] + +[package.dependencies] +contourpy = ">=1.0.1" +cycler = ">=0.10" +fonttools = ">=4.22.0" +kiwisolver = ">=1.0.1" +numpy = ">=1.19" +packaging = ">=20.0" +pillow = ">=6.2.0" +pyparsing = ">=2.2.1" +python-dateutil = ">=2.7" + +[[package]] +name = "matplotlib-inline" +version = "0.1.6" +description = "Inline Matplotlib backend for Jupyter" +optional = true +python-versions = ">=3.5" +files = [ + {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, + {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, +] + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "mistune" +version = "2.0.4" +description = "A sane Markdown parser with useful plugins and renderers" +optional = true +python-versions = "*" +files = [ + {file = "mistune-2.0.4-py2.py3-none-any.whl", hash = "sha256:182cc5ee6f8ed1b807de6b7bb50155df7b66495412836b9a74c8fbdfc75fe36d"}, + {file = "mistune-2.0.4.tar.gz", hash = "sha256:9ee0a66053e2267aba772c71e06891fa8f1af6d4b01d5e84e267b4570d4d9808"}, +] + +[[package]] +name = "moviepy" +version = "1.0.3" +description = "Video editing with Python" +optional = false +python-versions = "*" +files = [ + {file = "moviepy-1.0.3.tar.gz", hash = "sha256:2884e35d1788077db3ff89e763c5ba7bfddbd7ae9108c9bc809e7ba58fa433f5"}, +] + +[package.dependencies] +decorator = ">=4.0.2,<5.0" +imageio = {version = ">=2.5,<3.0", markers = "python_version >= \"3.4\""} +imageio_ffmpeg = {version = ">=0.2.0", markers = "python_version >= \"3.4\""} +numpy = {version = ">=1.17.3", markers = "python_version > \"2.7\""} +proglog = "<=1.0.0" +requests = ">=2.8.1,<3.0" +tqdm = ">=4.11.2,<5.0" + +[package.extras] +doc = ["Sphinx (>=1.5.2,<2.0)", "numpydoc (>=0.6.0,<1.0)", "pygame (>=1.9.3,<2.0)", "sphinx_rtd_theme (>=0.1.10b0,<1.0)"] +optional = ["matplotlib (>=2.0.0,<3.0)", "opencv-python (>=3.0,<4.0)", "scikit-image (>=0.13.0,<1.0)", "scikit-learn", "scipy (>=0.19.0,<1.5)", "youtube_dl"] +test = ["coverage (<5.0)", "coveralls (>=1.1,<2.0)", "pytest (>=3.0.0,<4.0)", "pytest-cov (>=2.5.1,<3.0)", "requests (>=2.8.1,<3.0)"] + +[[package]] +name = "multidict" +version = "6.0.3" +description = "multidict implementation" +optional = false +python-versions = ">=3.7" +files = [ + {file = "multidict-6.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:73009ea04205966d47e16d98686ac5c438af23a1bb30b48a2c5da3423ec9ce37"}, + {file = "multidict-6.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8b92a9f3ab904397a33b193000dc4de7318ea175c4c460a1e154c415f9008e3d"}, + {file = "multidict-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:578bfcb16f4b8675ef71b960c00f174b0426e0eeb796bab6737389d8288eb827"}, + {file = "multidict-6.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1650ea41c408755da5eed52ac6ccbc8938ccc3e698d81e6f6a1be02ff2a0945"}, + {file = "multidict-6.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d52442e7c951e4c9ee591d6047706e66923d248d83958bbf99b8b19515fffaef"}, + {file = "multidict-6.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad7d66422b9cc51125509229693d27e18c08f2dea3ac9de408d821932b1b3759"}, + {file = "multidict-6.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cd14e61f0da2a2cfb9fe05bfced2a1ed7063ce46a7a8cd473be4973de9a7f91"}, + {file = "multidict-6.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:190626ced82d4cc567a09e7346340d380154a493bac6905e0095d8158cdf1e38"}, + {file = "multidict-6.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:791458a1f7d1b4ab3bd9e93e0dcd1d59ef7ee9aa051dcd1ea030e62e49b923fd"}, + {file = "multidict-6.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b46e79a9f4db53897d17bc64a39d1c7c2be3e3d4f8dba6d6730a2b13ddf0f986"}, + {file = "multidict-6.0.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e4a095e18847c12ec20e55326ab8782d9c2d599400a3a2f174fab4796875d0e2"}, + {file = "multidict-6.0.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fb6c3dc3d65014d2c782f5acf0b3ba14e639c6c33d3ed8932ead76b9080b3544"}, + {file = "multidict-6.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3541882266247c7cd3dba78d6ef28dbe704774df60c9e4231edaa4493522e614"}, + {file = "multidict-6.0.3-cp310-cp310-win32.whl", hash = "sha256:67090b17a0a5be5704fd109f231ee73cefb1b3802d41288d6378b5df46ae89ba"}, + {file = "multidict-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:36df958b15639e40472adaa4f0c2c7828fe680f894a6b48c4ce229f59a6a798b"}, + {file = "multidict-6.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b51969503709415a35754954c2763f536a70b8bf7360322b2edb0c0a44391f6"}, + {file = "multidict-6.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:24e8d513bfcaadc1f8b0ebece3ff50961951c54b07d5a775008a882966102418"}, + {file = "multidict-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d325d61cac602976a5d47b19eaa7d04e3daf4efce2164c630219885087234102"}, + {file = "multidict-6.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbbe17f8a7211b623502d2bf41022a51da3025142401417c765bf9a56fed4c"}, + {file = "multidict-6.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4fb3fe591956d8841882c463f934c9f7485cfd5f763a08c0d467b513dc18ef89"}, + {file = "multidict-6.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1925f78a543b94c3d46274c66a366fee8a263747060220ed0188e5f3eeea1c0"}, + {file = "multidict-6.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e1ce0b187c4e93112304dcde2aa18922fdbe8fb4f13d8aa72a5657bce0563a"}, + {file = "multidict-6.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e07c24018986fb00d6e7eafca8fcd6e05095649e17fcf0e33a592caaa62a78b9"}, + {file = "multidict-6.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:114a4ab3e5cfbc56c4b6697686ecb92376c7e8c56893ef20547921552f8bdf57"}, + {file = "multidict-6.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4ccf55f28066b4f08666764a957c2b7c241c7547b0921d69c7ceab5f74fe1a45"}, + {file = "multidict-6.0.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:9d359b0a962e052b713647ac1f13eabf2263167b149ed1e27d5c579f5c8c7d2c"}, + {file = "multidict-6.0.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:df7b4cee3ff31b3335aba602f8d70dbc641e5b7164b1e9565570c9d3c536a438"}, + {file = "multidict-6.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ee9b1cae9a6c5d023e5a150f6f6b9dbb3c3bbc7887d6ee07d4c0ecb49a473734"}, + {file = "multidict-6.0.3-cp311-cp311-win32.whl", hash = "sha256:960ce1b790952916e682093788696ef7e33ac6a97482f9b983abdc293091b531"}, + {file = "multidict-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:2b66d61966b12e6bba500e5cbb2c721a35e119c30ee02495c5629bd0e91eea30"}, + {file = "multidict-6.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:526f8397fc124674b8f39748680a0ff673bd6a715fecb4866716d36e380f015f"}, + {file = "multidict-6.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f5d5129a937af4e3c4a1d6c139f4051b7d17d43276cefdd8d442a7031f7eef2"}, + {file = "multidict-6.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38d394814b39be1c36ac709006d39d50d72a884f9551acd9c8cc1ffae3fc8c4e"}, + {file = "multidict-6.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99341ca1f1db9e7f47914cb2461305665a662383765ced6f843712564766956d"}, + {file = "multidict-6.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5790cc603456b6dcf8a9a4765f666895a6afddc88b3d3ba7b53dea2b6e23116"}, + {file = "multidict-6.0.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce8e51774eb03844588d3c279adb94efcd0edeccd2f97516623292445bcc01f9"}, + {file = "multidict-6.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:baa96a3418e27d723064854143b2f414a422c84cc87285a71558722049bebc5a"}, + {file = "multidict-6.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:cb4a08f0aaaa869f189ffea0e17b86ad0237b51116d494da15ef7991ee6ad2d7"}, + {file = "multidict-6.0.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:62db44727d0befea68e8ad2881bb87a9cfb6b87d45dd78609009627167f37b69"}, + {file = "multidict-6.0.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:4cc5c8cd205a9810d16a5cd428cd81bac554ad1477cb87f4ad722b10992e794d"}, + {file = "multidict-6.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f76109387e1ec8d8e2137c94c437b89fe002f29e0881aae8ae45529bdff92000"}, + {file = "multidict-6.0.3-cp37-cp37m-win32.whl", hash = "sha256:f8a728511c977df6f3d8af388fcb157e49f11db4a6637dd60131b8b6e40b0253"}, + {file = "multidict-6.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c2a1168e5aa7c72499fb03c850e0f03f624fa4a5c8d2e215c518d0a73872eb64"}, + {file = "multidict-6.0.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:eddf604a3de2ace3d9a4e4d491be7562a1ac095a0a1c95a9ec5781ef0273ef11"}, + {file = "multidict-6.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d09daf5c6ce7fc6ed444c9339bbde5ea84e2534d1ca1cd37b60f365c77f00dea"}, + {file = "multidict-6.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:12e0d396faa6dc55ff5379eee54d1df3b508243ff15bfc8295a6ec7a4483a335"}, + {file = "multidict-6.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70740c2bc9ab1c99f7cdcb104f27d16c63860c56d51c5bf0ef82fc1d892a2131"}, + {file = "multidict-6.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e322c94596054352f5a02771eec71563c018b15699b961aba14d6dd943367022"}, + {file = "multidict-6.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4159fc1ec9ede8ab93382e0d6ba9b1b3d23c72da39a834db7a116986605c7ab4"}, + {file = "multidict-6.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47defc0218682281a52fb1f6346ebb8b68b17538163a89ea24dfe4da37a8a9a3"}, + {file = "multidict-6.0.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f9511e48bde6b995825e8d35e434fc96296cf07a25f4aae24ff9162be7eaa46"}, + {file = "multidict-6.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e0bce9f7c30e7e3a9e683f670314c0144e8d34be6b7019e40604763bd278d84f"}, + {file = "multidict-6.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:01b456046a05ff7cceefb0e1d2a9d32f05efcb1c7e0d152446304e11557639ce"}, + {file = "multidict-6.0.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:8230a39bae6c2e8a09e4da6bace5064693b00590a4a213e38f9a9366da10e7dd"}, + {file = "multidict-6.0.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:445c0851a1cbc1f2ec3b40bc22f9c4a235edb3c9a0906122a9df6ea8d51f886c"}, + {file = "multidict-6.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9aac6881454a750554ed4b280a839dcf9e2133a9d12ab4d417d673fb102289b7"}, + {file = "multidict-6.0.3-cp38-cp38-win32.whl", hash = "sha256:81c3d597591b0940e04949e4e4f79359b2d2e542a686ba0da5e25de33fec13e0"}, + {file = "multidict-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:dc4cfef5d899f5f1a15f3d2ac49f71107a01a5a2745b4dd53fa0cede1419385a"}, + {file = "multidict-6.0.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d408172519049e36fb6d29672f060dc8461fc7174eba9883c7026041ef9bfb38"}, + {file = "multidict-6.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e068dfeadbce63072b2d8096486713d04db4946aad0a0f849bd4fc300799d0d3"}, + {file = "multidict-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8b817d4ed68fd568ec5e45dd75ddf30cc72a47a6b41b74d5bb211374c296f5e"}, + {file = "multidict-6.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cf5d19e12eff855aa198259c0b02fd3f5d07e1291fbd20279c37b3b0e6c9852"}, + {file = "multidict-6.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5a811aab1b4aea0b4be669363c19847a8c547510f0e18fb632956369fdbdf67"}, + {file = "multidict-6.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cfda34b7cb99eacada2072e0f69c0ad3285cb6f8e480b11f2b6d6c1c6f92718"}, + {file = "multidict-6.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beeca903e4270b4afcd114f371a9602240dc143f9e944edfea00f8d4ad56c40d"}, + {file = "multidict-6.0.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd5771e8ea325f85cbb361ddbdeb9ae424a68e5dfb6eea786afdcd22e68a7d5d"}, + {file = "multidict-6.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9dbab2a7e9c073bc9538824a01f5ed689194db7f55f2b8102766873e906a6c1a"}, + {file = "multidict-6.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f2c0957b3e8c66c10d27272709a5299ab3670a0f187c9428f3b90d267119aedb"}, + {file = "multidict-6.0.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:94cbe5535ef150546b8321aebea22862a3284da51e7b55f6f95b7d73e96d90ee"}, + {file = "multidict-6.0.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d0e798b072cf2aab9daceb43d97c9c527a0c7593e67a7846ad4cc6051de1e303"}, + {file = "multidict-6.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a27b029caa3b555a4f3da54bc1e718eb55fcf1a11fda8bf0132147b476cf4c08"}, + {file = "multidict-6.0.3-cp39-cp39-win32.whl", hash = "sha256:018c8e3be7f161a12b3e41741b6721f9baeb2210f4ab25a6359b7d76c1017dce"}, + {file = "multidict-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:5e58ec0375803526d395f6f7e730ecc45d06e15f68f7b9cdbf644a2918324e51"}, + {file = "multidict-6.0.3.tar.gz", hash = "sha256:2523a29006c034687eccd3ee70093a697129a3ffe8732535d3b2df6a4ecc279d"}, +] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +optional = false +python-versions = "*" +files = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] + +[[package]] +name = "nbclassic" +version = "0.4.8" +description = "A web-based notebook environment for interactive computing" +optional = true +python-versions = ">=3.7" +files = [ + {file = "nbclassic-0.4.8-py3-none-any.whl", hash = "sha256:cbf05df5842b420d5cece0143462380ea9d308ff57c2dc0eb4d6e035b18fbfb3"}, + {file = "nbclassic-0.4.8.tar.gz", hash = "sha256:c74d8a500f8e058d46b576a41e5bc640711e1032cf7541dde5f73ea49497e283"}, +] + +[package.dependencies] +argon2-cffi = "*" +ipykernel = "*" +ipython-genutils = "*" +jinja2 = "*" +jupyter-client = ">=6.1.1" +jupyter-core = ">=4.6.1" +jupyter-server = ">=1.8" +nbconvert = ">=5" +nbformat = "*" +nest-asyncio = ">=1.5" +notebook-shim = ">=0.1.0" +prometheus-client = "*" +pyzmq = ">=17" +Send2Trash = ">=1.8.0" +terminado = ">=0.8.3" +tornado = ">=6.1" +traitlets = ">=4.2.1" + +[package.extras] +docs = ["myst-parser", "nbsphinx", "sphinx", "sphinx-rtd-theme", "sphinxcontrib-github-alt"] +json-logging = ["json-logging"] +test = ["coverage", "nbval", "pytest", "pytest-cov", "pytest-playwright", "pytest-tornasync", "requests", "requests-unixsocket", "testpath"] + +[[package]] +name = "nbclient" +version = "0.7.2" +description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." +optional = true +python-versions = ">=3.7.0" +files = [ + {file = "nbclient-0.7.2-py3-none-any.whl", hash = "sha256:d97ac6257de2794f5397609df754fcbca1a603e94e924eb9b99787c031ae2e7c"}, + {file = "nbclient-0.7.2.tar.gz", hash = "sha256:884a3f4a8c4fc24bb9302f263e0af47d97f0d01fe11ba714171b320c8ac09547"}, +] + +[package.dependencies] +jupyter-client = ">=6.1.12" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +nbformat = ">=5.1" +traitlets = ">=5.3" + +[package.extras] +dev = ["pre-commit"] +docs = ["autodoc-traits", "mock", "moto", "myst-parser", "nbclient[test]", "sphinx (>=1.7)", "sphinx-book-theme"] +test = ["ipykernel", "ipython", "ipywidgets", "nbconvert (>=7.0.0)", "pytest (>=7.0)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"] + +[[package]] +name = "nbconvert" +version = "7.2.6" +description = "Converting Jupyter Notebooks" +optional = true +python-versions = ">=3.7" +files = [ + {file = "nbconvert-7.2.6-py3-none-any.whl", hash = "sha256:f933e82fe48b9a421e4252249f6c0a9a9940dc555642b4729f3f1f526bb16779"}, + {file = "nbconvert-7.2.6.tar.gz", hash = "sha256:c9c0e4b26326f7658ebf4cda0acc591b9727c4e3ee3ede962f70c11833b71b40"}, +] + +[package.dependencies] +beautifulsoup4 = "*" +bleach = "*" +defusedxml = "*" +importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} +jinja2 = ">=3.0" +jupyter-core = ">=4.7" +jupyterlab-pygments = "*" +markupsafe = ">=2.0" +mistune = ">=2.0.3,<3" +nbclient = ">=0.5.0" +nbformat = ">=5.1" +packaging = "*" +pandocfilters = ">=1.4.1" +pygments = ">=2.4.1" +tinycss2 = "*" +traitlets = ">=5.0" + +[package.extras] +all = ["nbconvert[docs,qtpdf,serve,test,webpdf]"] +docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)"] +qtpdf = ["nbconvert[qtpng]"] +qtpng = ["pyqtwebengine (>=5.15)"] +serve = ["tornado (>=6.1)"] +test = ["ipykernel", "ipywidgets (>=7)", "pre-commit", "pyppeteer (>=1,<1.1)", "pytest", "pytest-dependency"] +webpdf = ["pyppeteer (>=1,<1.1)"] + +[[package]] +name = "nbformat" +version = "5.7.0" +description = "The Jupyter Notebook format" +optional = true +python-versions = ">=3.7" +files = [ + {file = "nbformat-5.7.0-py3-none-any.whl", hash = "sha256:1b05ec2c552c2f1adc745f4eddce1eac8ca9ffd59bb9fd859e827eaa031319f9"}, + {file = "nbformat-5.7.0.tar.gz", hash = "sha256:1d4760c15c1a04269ef5caf375be8b98dd2f696e5eb9e603ec2bf091f9b0d3f3"}, +] + +[package.dependencies] +fastjsonschema = "*" +jsonschema = ">=2.6" +jupyter-core = "*" +traitlets = ">=5.1" + +[package.extras] +test = ["check-manifest", "pep440", "pre-commit", "pytest", "testpath"] + +[[package]] +name = "nest-asyncio" +version = "1.5.6" +description = "Patch asyncio to allow nested event loops" +optional = true +python-versions = ">=3.5" +files = [ + {file = "nest_asyncio-1.5.6-py3-none-any.whl", hash = "sha256:b9a953fb40dceaa587d109609098db21900182b16440652454a146cffb06e8b8"}, + {file = "nest_asyncio-1.5.6.tar.gz", hash = "sha256:d267cc1ff794403f7df692964d1d2a3fa9418ffea2a3f6859a439ff482fef290"}, +] + +[[package]] +name = "nevergrad" +version = "0.8.0" +description = "A Python toolbox for performing gradient-free optimization" +optional = true +python-versions = ">=3.6" +files = [ + {file = "nevergrad-0.8.0-py3-none-any.whl", hash = "sha256:6bfbbd1616599c739a04a356219dc4a1ee1393a1a6715278956724d228b69aee"}, + {file = "nevergrad-0.8.0.tar.gz", hash = "sha256:8bcd69a5a1a5f29831172f52edd8e49e38ff3837e0a4fa3dccc925b64c1f50b7"}, +] + +[package.dependencies] +bayesian-optimization = ">=1.2.0" +cma = ">=2.6.0" +numpy = ">=1.21.1" +pandas = "*" +typing-extensions = ">=3.6.6" + +[package.extras] +all = ["IOHexperimenter (>=0.2.8.7)", "Keras-Preprocessing", "Pillow (>=8.4.0)", "autodocsumm (>=0.1.11)", "bayes-optim (==0.3.0)", "black (==22.3.0)", "box2d-py (>=2.3.5)", "cdt (>=0.5.23)", "docutils (==0.17.1)", "fcmaes (>=1.2.7)", "glfw", "gym (==0.24.1)", "gym-anm (>=1.0.1)", "hiplot", "hyperopt (>=0.2.5)", "image-quality (>=1.2.7)", "keras (>=2.4.3)", "matplotlib (>=2.2.3)", "mixsimulator (>=0.3.3)", "mujoco", "mypy (>=0.800)", "nlopt", "olymp (==0.0.1b0)", "opencv-python (>=4.1.2.30)", "openpyxl (>=3.0.0)", "pandas (>=0.23.4)", "pybullet (>=3.2.2)", "pygame (>=2.1.2)", "pylint (>=2.4.4)", "pymoo (==0.5.0)", "pyomo (==5.7.1)", "pyparsing (==2.2.1)", "pyproj (>=2.6.1)", "pytest (>=4.3.0)", "pytest-cov (>=2.6.1)", "recommonmark (>=0.5.0)", "requests (>=2.21.0)", "scikit-image (==0.18.3)", "scikit-learn (>=1.0.1)", "setuptools (>=41.2.0)", "silence-tensorflow", "sphinx (>=3.3.0)", "sphinx-rtd-theme (>=0.4.3)", "tensorflow (>=2.7.0)", "tensorflow-estimator (>=2.7.0)", "tensorflow-probability", "torch (>=1.7.0)", "torchvision (>=0.11.1)", "tqdm", "twine (>=3.1.1)", "wheel (>=0.33.6)", "xlrd (>=1.2.0)", "xlwt (>=1.3.0)"] +benchmark = ["IOHexperimenter (>=0.2.8.7)", "Keras-Preprocessing", "Pillow (>=8.4.0)", "bayes-optim (==0.3.0)", "box2d-py (>=2.3.5)", "cdt (>=0.5.23)", "fcmaes (>=1.2.7)", "glfw", "gym (==0.24.1)", "gym-anm (>=1.0.1)", "hiplot", "hyperopt (>=0.2.5)", "image-quality (>=1.2.7)", "keras (>=2.4.3)", "matplotlib (>=2.2.3)", "mixsimulator (>=0.3.3)", "mujoco", "nlopt", "olymp (==0.0.1b0)", "opencv-python (>=4.1.2.30)", "openpyxl (>=3.0.0)", "pandas (>=0.23.4)", "pybullet (>=3.2.2)", "pygame (>=2.1.2)", "pymoo (==0.5.0)", "pyomo (==5.7.1)", "pyproj (>=2.6.1)", "requests (>=2.21.0)", "scikit-image (==0.18.3)", "scikit-learn (>=1.0.1)", "silence-tensorflow", "tensorflow (>=2.7.0)", "tensorflow-estimator (>=2.7.0)", "tensorflow-probability", "torch (>=1.7.0)", "torchvision (>=0.11.1)", "tqdm", "xlrd (>=1.2.0)", "xlwt (>=1.3.0)"] +dev = ["autodocsumm (>=0.1.11)", "black (==22.3.0)", "docutils (==0.17.1)", "mypy (>=0.800)", "pandas (>=0.23.4)", "pylint (>=2.4.4)", "pyparsing (==2.2.1)", "pytest (>=4.3.0)", "pytest-cov (>=2.6.1)", "recommonmark (>=0.5.0)", "setuptools (>=41.2.0)", "sphinx (>=3.3.0)", "sphinx-rtd-theme (>=0.4.3)", "twine (>=3.1.1)", "wheel (>=0.33.6)"] + +[[package]] +name = "nodeenv" +version = "1.7.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, + {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "notebook" +version = "6.5.2" +description = "A web-based notebook environment for interactive computing" +optional = true +python-versions = ">=3.7" +files = [ + {file = "notebook-6.5.2-py3-none-any.whl", hash = "sha256:e04f9018ceb86e4fa841e92ea8fb214f8d23c1cedfde530cc96f92446924f0e4"}, + {file = "notebook-6.5.2.tar.gz", hash = "sha256:c1897e5317e225fc78b45549a6ab4b668e4c996fd03a04e938fe5e7af2bfffd0"}, +] + +[package.dependencies] +argon2-cffi = "*" +ipykernel = "*" +ipython-genutils = "*" +jinja2 = "*" +jupyter-client = ">=5.3.4" +jupyter-core = ">=4.6.1" +nbclassic = ">=0.4.7" +nbconvert = ">=5" +nbformat = "*" +nest-asyncio = ">=1.5" +prometheus-client = "*" +pyzmq = ">=17" +Send2Trash = ">=1.8.0" +terminado = ">=0.8.3" +tornado = ">=6.1" +traitlets = ">=4.2.1" + +[package.extras] +docs = ["myst-parser", "nbsphinx", "sphinx", "sphinx-rtd-theme", "sphinxcontrib-github-alt"] +json-logging = ["json-logging"] +test = ["coverage", "nbval", "pytest", "pytest-cov", "requests", "requests-unixsocket", "selenium (==4.1.5)", "testpath"] + +[[package]] +name = "notebook-shim" +version = "0.2.2" +description = "A shim layer for notebook traits and config" +optional = true +python-versions = ">=3.7" +files = [ + {file = "notebook_shim-0.2.2-py3-none-any.whl", hash = "sha256:9c6c30f74c4fbea6fce55c1be58e7fd0409b1c681b075dcedceb005db5026949"}, + {file = "notebook_shim-0.2.2.tar.gz", hash = "sha256:090e0baf9a5582ff59b607af523ca2db68ff216da0c69956b62cab2ef4fc9c3f"}, +] + +[package.dependencies] +jupyter-server = ">=1.8,<3" + +[package.extras] +test = ["pytest", "pytest-console-scripts", "pytest-tornasync"] + +[[package]] +name = "numpy" +version = "1.23.5" +description = "NumPy is the fundamental package for array computing with Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "numpy-1.23.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c88793f78fca17da0145455f0d7826bcb9f37da4764af27ac945488116efe63"}, + {file = "numpy-1.23.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e9f4c4e51567b616be64e05d517c79a8a22f3606499941d97bb76f2ca59f982d"}, + {file = "numpy-1.23.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7903ba8ab592b82014713c491f6c5d3a1cde5b4a3bf116404e08f5b52f6daf43"}, + {file = "numpy-1.23.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e05b1c973a9f858c74367553e236f287e749465f773328c8ef31abe18f691e1"}, + {file = "numpy-1.23.5-cp310-cp310-win32.whl", hash = "sha256:522e26bbf6377e4d76403826ed689c295b0b238f46c28a7251ab94716da0b280"}, + {file = "numpy-1.23.5-cp310-cp310-win_amd64.whl", hash = "sha256:dbee87b469018961d1ad79b1a5d50c0ae850000b639bcb1b694e9981083243b6"}, + {file = "numpy-1.23.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ce571367b6dfe60af04e04a1834ca2dc5f46004ac1cc756fb95319f64c095a96"}, + {file = "numpy-1.23.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56e454c7833e94ec9769fa0f86e6ff8e42ee38ce0ce1fa4cbb747ea7e06d56aa"}, + {file = "numpy-1.23.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5039f55555e1eab31124a5768898c9e22c25a65c1e0037f4d7c495a45778c9f2"}, + {file = "numpy-1.23.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58f545efd1108e647604a1b5aa809591ccd2540f468a880bedb97247e72db387"}, + {file = "numpy-1.23.5-cp311-cp311-win32.whl", hash = "sha256:b2a9ab7c279c91974f756c84c365a669a887efa287365a8e2c418f8b3ba73fb0"}, + {file = "numpy-1.23.5-cp311-cp311-win_amd64.whl", hash = "sha256:0cbe9848fad08baf71de1a39e12d1b6310f1d5b2d0ea4de051058e6e1076852d"}, + {file = "numpy-1.23.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f063b69b090c9d918f9df0a12116029e274daf0181df392839661c4c7ec9018a"}, + {file = "numpy-1.23.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0aaee12d8883552fadfc41e96b4c82ee7d794949e2a7c3b3a7201e968c7ecab9"}, + {file = "numpy-1.23.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92c8c1e89a1f5028a4c6d9e3ccbe311b6ba53694811269b992c0b224269e2398"}, + {file = "numpy-1.23.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d208a0f8729f3fb790ed18a003f3a57895b989b40ea4dce4717e9cf4af62c6bb"}, + {file = "numpy-1.23.5-cp38-cp38-win32.whl", hash = "sha256:06005a2ef6014e9956c09ba07654f9837d9e26696a0470e42beedadb78c11b07"}, + {file = "numpy-1.23.5-cp38-cp38-win_amd64.whl", hash = "sha256:ca51fcfcc5f9354c45f400059e88bc09215fb71a48d3768fb80e357f3b457e1e"}, + {file = "numpy-1.23.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8969bfd28e85c81f3f94eb4a66bc2cf1dbdc5c18efc320af34bffc54d6b1e38f"}, + {file = "numpy-1.23.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7ac231a08bb37f852849bbb387a20a57574a97cfc7b6cabb488a4fc8be176de"}, + {file = "numpy-1.23.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf837dc63ba5c06dc8797c398db1e223a466c7ece27a1f7b5232ba3466aafe3d"}, + {file = "numpy-1.23.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33161613d2269025873025b33e879825ec7b1d831317e68f4f2f0f84ed14c719"}, + {file = "numpy-1.23.5-cp39-cp39-win32.whl", hash = "sha256:af1da88f6bc3d2338ebbf0e22fe487821ea4d8e89053e25fa59d1d79786e7481"}, + {file = "numpy-1.23.5-cp39-cp39-win_amd64.whl", hash = "sha256:09b7847f7e83ca37c6e627682f145856de331049013853f344f37b0c9690e3df"}, + {file = "numpy-1.23.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:abdde9f795cf292fb9651ed48185503a2ff29be87770c3b8e2a14b0cd7aa16f8"}, + {file = "numpy-1.23.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9a909a8bae284d46bbfdefbdd4a262ba19d3bc9921b1e76126b1d21c3c34135"}, + {file = "numpy-1.23.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:01dd17cbb340bf0fc23981e52e1d18a9d4050792e8fb8363cecbf066a84b827d"}, + {file = "numpy-1.23.5.tar.gz", hash = "sha256:1b1766d6f397c18153d40015ddfc79ddb715cabadc04d2d228d4e5a8bc4ded1a"}, +] + +[[package]] +name = "nvidia-cublas-cu11" +version = "11.10.3.66" +description = "CUBLAS native runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cublas_cu11-11.10.3.66-py3-none-manylinux1_x86_64.whl", hash = "sha256:d32e4d75f94ddfb93ea0a5dda08389bcc65d8916a25cb9f37ac89edaeed3bded"}, + {file = "nvidia_cublas_cu11-11.10.3.66-py3-none-win_amd64.whl", hash = "sha256:8ac17ba6ade3ed56ab898a036f9ae0756f1e81052a317bf98f8c6d18dc3ae49e"}, +] + +[package.dependencies] +setuptools = "*" +wheel = "*" + +[[package]] +name = "nvidia-cuda-nvrtc-cu11" +version = "11.7.99" +description = "NVRTC native runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cuda_nvrtc_cu11-11.7.99-2-py3-none-manylinux1_x86_64.whl", hash = "sha256:9f1562822ea264b7e34ed5930567e89242d266448e936b85bc97a3370feabb03"}, + {file = "nvidia_cuda_nvrtc_cu11-11.7.99-py3-none-manylinux1_x86_64.whl", hash = "sha256:f7d9610d9b7c331fa0da2d1b2858a4a8315e6d49765091d28711c8946e7425e7"}, + {file = "nvidia_cuda_nvrtc_cu11-11.7.99-py3-none-win_amd64.whl", hash = "sha256:f2effeb1309bdd1b3854fc9b17eaf997808f8b25968ce0c7070945c4265d64a3"}, +] + +[package.dependencies] +setuptools = "*" +wheel = "*" + +[[package]] +name = "nvidia-cuda-runtime-cu11" +version = "11.7.99" +description = "CUDA Runtime native Libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cuda_runtime_cu11-11.7.99-py3-none-manylinux1_x86_64.whl", hash = "sha256:cc768314ae58d2641f07eac350f40f99dcb35719c4faff4bc458a7cd2b119e31"}, + {file = "nvidia_cuda_runtime_cu11-11.7.99-py3-none-win_amd64.whl", hash = "sha256:bc77fa59a7679310df9d5c70ab13c4e34c64ae2124dd1efd7e5474b71be125c7"}, +] + +[package.dependencies] +setuptools = "*" +wheel = "*" + +[[package]] +name = "nvidia-cudnn-cu11" +version = "8.5.0.96" +description = "cuDNN runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cudnn_cu11-8.5.0.96-2-py3-none-manylinux1_x86_64.whl", hash = "sha256:402f40adfc6f418f9dae9ab402e773cfed9beae52333f6d86ae3107a1b9527e7"}, + {file = "nvidia_cudnn_cu11-8.5.0.96-py3-none-manylinux1_x86_64.whl", hash = "sha256:71f8111eb830879ff2836db3cccf03bbd735df9b0d17cd93761732ac50a8a108"}, +] + +[package.dependencies] +setuptools = "*" +wheel = "*" + +[[package]] +name = "oauthlib" +version = "3.2.2" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +optional = true +python-versions = ">=3.6" +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + +[[package]] +name = "omegaconf" +version = "2.3.0" +description = "A flexible configuration library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b"}, + {file = "omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7"}, +] + +[package.dependencies] +antlr4-python3-runtime = "==4.9.*" +PyYAML = ">=5.1.0" + +[[package]] +name = "opt-einsum" +version = "3.3.0" +description = "Optimizing numpys einsum function" +optional = true +python-versions = ">=3.5" +files = [ + {file = "opt_einsum-3.3.0-py3-none-any.whl", hash = "sha256:2455e59e3947d3c275477df7f5205b30635e266fe6dc300e3d9f9646bfcea147"}, + {file = "opt_einsum-3.3.0.tar.gz", hash = "sha256:59f6475f77bbc37dcf7cd748519c0ec60722e91e63ca114e68821c0c54a46549"}, +] + +[package.dependencies] +numpy = ">=1.7" + +[package.extras] +docs = ["numpydoc", "sphinx (==1.2.3)", "sphinx-rtd-theme", "sphinxcontrib-napoleon"] +tests = ["pytest", "pytest-cov", "pytest-pep8"] + +[[package]] +name = "packaging" +version = "22.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"}, + {file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"}, +] + +[[package]] +name = "pandas" +version = "2.0.3" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = true +python-versions = ">=3.8" +files = [ + {file = "pandas-2.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c7c9f27a4185304c7caf96dc7d91bc60bc162221152de697c98eb0b2648dd8"}, + {file = "pandas-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f167beed68918d62bffb6ec64f2e1d8a7d297a038f86d4aed056b9493fca407f"}, + {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce0c6f76a0f1ba361551f3e6dceaff06bde7514a374aa43e33b588ec10420183"}, + {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba619e410a21d8c387a1ea6e8a0e49bb42216474436245718d7f2e88a2f8d7c0"}, + {file = "pandas-2.0.3-cp310-cp310-win32.whl", hash = "sha256:3ef285093b4fe5058eefd756100a367f27029913760773c8bf1d2d8bebe5d210"}, + {file = "pandas-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:9ee1a69328d5c36c98d8e74db06f4ad518a1840e8ccb94a4ba86920986bb617e"}, + {file = "pandas-2.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b084b91d8d66ab19f5bb3256cbd5ea661848338301940e17f4492b2ce0801fe8"}, + {file = "pandas-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37673e3bdf1551b95bf5d4ce372b37770f9529743d2498032439371fc7b7eb26"}, + {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9cb1e14fdb546396b7e1b923ffaeeac24e4cedd14266c3497216dd4448e4f2d"}, + {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9cd88488cceb7635aebb84809d087468eb33551097d600c6dad13602029c2df"}, + {file = "pandas-2.0.3-cp311-cp311-win32.whl", hash = "sha256:694888a81198786f0e164ee3a581df7d505024fbb1f15202fc7db88a71d84ebd"}, + {file = "pandas-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6a21ab5c89dcbd57f78d0ae16630b090eec626360085a4148693def5452d8a6b"}, + {file = "pandas-2.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4da0d45e7f34c069fe4d522359df7d23badf83abc1d1cef398895822d11061"}, + {file = "pandas-2.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:32fca2ee1b0d93dd71d979726b12b61faa06aeb93cf77468776287f41ff8fdc5"}, + {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:258d3624b3ae734490e4d63c430256e716f488c4fcb7c8e9bde2d3aa46c29089"}, + {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eae3dc34fa1aa7772dd3fc60270d13ced7346fcbcfee017d3132ec625e23bb0"}, + {file = "pandas-2.0.3-cp38-cp38-win32.whl", hash = "sha256:f3421a7afb1a43f7e38e82e844e2bca9a6d793d66c1a7f9f0ff39a795bbc5e02"}, + {file = "pandas-2.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:69d7f3884c95da3a31ef82b7618af5710dba95bb885ffab339aad925c3e8ce78"}, + {file = "pandas-2.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5247fb1ba347c1261cbbf0fcfba4a3121fbb4029d95d9ef4dc45406620b25c8b"}, + {file = "pandas-2.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81af086f4543c9d8bb128328b5d32e9986e0c84d3ee673a2ac6fb57fd14f755e"}, + {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1994c789bf12a7c5098277fb43836ce090f1073858c10f9220998ac74f37c69b"}, + {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec591c48e29226bcbb316e0c1e9423622bc7a4eaf1ef7c3c9fa1a3981f89641"}, + {file = "pandas-2.0.3-cp39-cp39-win32.whl", hash = "sha256:04dbdbaf2e4d46ca8da896e1805bc04eb85caa9a82e259e8eed00254d5e0c682"}, + {file = "pandas-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:1168574b036cd8b93abc746171c9b4f1b83467438a5e45909fed645cf8692dbc"}, + {file = "pandas-2.0.3.tar.gz", hash = "sha256:c02f372a88e0d17f36d3093a644c73cfc1788e876a7c4bcb4020a77512e2043c"}, +] + +[package.dependencies] +jinja2 = {version = ">=3.0.0", optional = true, markers = "extra == \"output_formatting\""} +numpy = {version = ">=1.20.3", markers = "python_version < \"3.10\""} +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tabulate = {version = ">=0.8.9", optional = true, markers = "extra == \"output_formatting\""} +tzdata = ">=2022.1" + +[package.extras] +all = ["PyQt5 (>=5.15.1)", "SQLAlchemy (>=1.4.16)", "beautifulsoup4 (>=4.9.3)", "bottleneck (>=1.3.2)", "brotlipy (>=0.7.0)", "fastparquet (>=0.6.3)", "fsspec (>=2021.07.0)", "gcsfs (>=2021.07.0)", "html5lib (>=1.1)", "hypothesis (>=6.34.2)", "jinja2 (>=3.0.0)", "lxml (>=4.6.3)", "matplotlib (>=3.6.1)", "numba (>=0.53.1)", "numexpr (>=2.7.3)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pandas-gbq (>=0.15.0)", "psycopg2 (>=2.8.6)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "python-snappy (>=0.6.0)", "pyxlsb (>=1.0.8)", "qtpy (>=2.2.0)", "s3fs (>=2021.08.0)", "scipy (>=1.7.1)", "tables (>=3.6.1)", "tabulate (>=0.8.9)", "xarray (>=0.21.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)", "zstandard (>=0.15.2)"] +aws = ["s3fs (>=2021.08.0)"] +clipboard = ["PyQt5 (>=5.15.1)", "qtpy (>=2.2.0)"] +compression = ["brotlipy (>=0.7.0)", "python-snappy (>=0.6.0)", "zstandard (>=0.15.2)"] +computation = ["scipy (>=1.7.1)", "xarray (>=0.21.0)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pyxlsb (>=1.0.8)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)"] +feather = ["pyarrow (>=7.0.0)"] +fss = ["fsspec (>=2021.07.0)"] +gcp = ["gcsfs (>=2021.07.0)", "pandas-gbq (>=0.15.0)"] +hdf5 = ["tables (>=3.6.1)"] +html = ["beautifulsoup4 (>=4.9.3)", "html5lib (>=1.1)", "lxml (>=4.6.3)"] +mysql = ["SQLAlchemy (>=1.4.16)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.0.0)", "tabulate (>=0.8.9)"] +parquet = ["pyarrow (>=7.0.0)"] +performance = ["bottleneck (>=1.3.2)", "numba (>=0.53.1)", "numexpr (>=2.7.1)"] +plot = ["matplotlib (>=3.6.1)"] +postgresql = ["SQLAlchemy (>=1.4.16)", "psycopg2 (>=2.8.6)"] +spss = ["pyreadstat (>=1.1.2)"] +sql-other = ["SQLAlchemy (>=1.4.16)"] +test = ["hypothesis (>=6.34.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.6.3)"] + +[[package]] +name = "pandocfilters" +version = "1.5.0" +description = "Utilities for writing pandoc filters in python" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pandocfilters-1.5.0-py2.py3-none-any.whl", hash = "sha256:33aae3f25fd1a026079f5d27bdd52496f0e0803b3469282162bafdcbdf6ef14f"}, + {file = "pandocfilters-1.5.0.tar.gz", hash = "sha256:0b679503337d233b4339a817bfc8c50064e2eff681314376a47cb582305a7a38"}, +] + +[[package]] +name = "parso" +version = "0.8.3" +description = "A Python Parser" +optional = true +python-versions = ">=3.6" +files = [ + {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, + {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, +] + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + +[[package]] +name = "pathspec" +version = "0.10.3" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"}, + {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"}, +] + +[[package]] +name = "pexpect" +version = "4.8.0" +description = "Pexpect allows easy control of interactive console applications." +optional = true +python-versions = "*" +files = [ + {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, + {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pickleshare" +version = "0.7.5" +description = "Tiny 'shelve'-like database with concurrency support" +optional = true +python-versions = "*" +files = [ + {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, + {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, +] + +[[package]] +name = "Pillow" +version = "9.3.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Pillow-9.3.0-1-cp37-cp37m-win32.whl", hash = "sha256:e6ea6b856a74d560d9326c0f5895ef8050126acfdc7ca08ad703eb0081e82b74"}, + {file = "Pillow-9.3.0-1-cp37-cp37m-win_amd64.whl", hash = "sha256:32a44128c4bdca7f31de5be641187367fe2a450ad83b833ef78910397db491aa"}, + {file = "Pillow-9.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:0b7257127d646ff8676ec8a15520013a698d1fdc48bc2a79ba4e53df792526f2"}, + {file = "Pillow-9.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b90f7616ea170e92820775ed47e136208e04c967271c9ef615b6fbd08d9af0e3"}, + {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68943d632f1f9e3dce98908e873b3a090f6cba1cbb1b892a9e8d97c938871fbe"}, + {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be55f8457cd1eac957af0c3f5ece7bc3f033f89b114ef30f710882717670b2a8"}, + {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d77adcd56a42d00cc1be30843d3426aa4e660cab4a61021dc84467123f7a00c"}, + {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:829f97c8e258593b9daa80638aee3789b7df9da5cf1336035016d76f03b8860c"}, + {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:801ec82e4188e935c7f5e22e006d01611d6b41661bba9fe45b60e7ac1a8f84de"}, + {file = "Pillow-9.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:871b72c3643e516db4ecf20efe735deb27fe30ca17800e661d769faab45a18d7"}, + {file = "Pillow-9.3.0-cp310-cp310-win32.whl", hash = "sha256:655a83b0058ba47c7c52e4e2df5ecf484c1b0b0349805896dd350cbc416bdd91"}, + {file = "Pillow-9.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:9f47eabcd2ded7698106b05c2c338672d16a6f2a485e74481f524e2a23c2794b"}, + {file = "Pillow-9.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:57751894f6618fd4308ed8e0c36c333e2f5469744c34729a27532b3db106ee20"}, + {file = "Pillow-9.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7db8b751ad307d7cf238f02101e8e36a128a6cb199326e867d1398067381bff4"}, + {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3033fbe1feb1b59394615a1cafaee85e49d01b51d54de0cbf6aa8e64182518a1"}, + {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22b012ea2d065fd163ca096f4e37e47cd8b59cf4b0fd47bfca6abb93df70b34c"}, + {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a65733d103311331875c1dca05cb4606997fd33d6acfed695b1232ba1df193"}, + {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:502526a2cbfa431d9fc2a079bdd9061a2397b842bb6bc4239bb176da00993812"}, + {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:90fb88843d3902fe7c9586d439d1e8c05258f41da473952aa8b328d8b907498c"}, + {file = "Pillow-9.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:89dca0ce00a2b49024df6325925555d406b14aa3efc2f752dbb5940c52c56b11"}, + {file = "Pillow-9.3.0-cp311-cp311-win32.whl", hash = "sha256:3168434d303babf495d4ba58fc22d6604f6e2afb97adc6a423e917dab828939c"}, + {file = "Pillow-9.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:18498994b29e1cf86d505edcb7edbe814d133d2232d256db8c7a8ceb34d18cef"}, + {file = "Pillow-9.3.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:772a91fc0e03eaf922c63badeca75e91baa80fe2f5f87bdaed4280662aad25c9"}, + {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa4107d1b306cdf8953edde0534562607fe8811b6c4d9a486298ad31de733b2"}, + {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4012d06c846dc2b80651b120e2cdd787b013deb39c09f407727ba90015c684f"}, + {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77ec3e7be99629898c9a6d24a09de089fa5356ee408cdffffe62d67bb75fdd72"}, + {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:6c738585d7a9961d8c2821a1eb3dcb978d14e238be3d70f0a706f7fa9316946b"}, + {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:828989c45c245518065a110434246c44a56a8b2b2f6347d1409c787e6e4651ee"}, + {file = "Pillow-9.3.0-cp37-cp37m-win32.whl", hash = "sha256:82409ffe29d70fd733ff3c1025a602abb3e67405d41b9403b00b01debc4c9a29"}, + {file = "Pillow-9.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:41e0051336807468be450d52b8edd12ac60bebaa97fe10c8b660f116e50b30e4"}, + {file = "Pillow-9.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:b03ae6f1a1878233ac620c98f3459f79fd77c7e3c2b20d460284e1fb370557d4"}, + {file = "Pillow-9.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4390e9ce199fc1951fcfa65795f239a8a4944117b5935a9317fb320e7767b40f"}, + {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40e1ce476a7804b0fb74bcfa80b0a2206ea6a882938eaba917f7a0f004b42502"}, + {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0a06a052c5f37b4ed81c613a455a81f9a3a69429b4fd7bb913c3fa98abefc20"}, + {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03150abd92771742d4a8cd6f2fa6246d847dcd2e332a18d0c15cc75bf6703040"}, + {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:15c42fb9dea42465dfd902fb0ecf584b8848ceb28b41ee2b58f866411be33f07"}, + {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:51e0e543a33ed92db9f5ef69a0356e0b1a7a6b6a71b80df99f1d181ae5875636"}, + {file = "Pillow-9.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3dd6caf940756101205dffc5367babf288a30043d35f80936f9bfb37f8355b32"}, + {file = "Pillow-9.3.0-cp38-cp38-win32.whl", hash = "sha256:f1ff2ee69f10f13a9596480335f406dd1f70c3650349e2be67ca3139280cade0"}, + {file = "Pillow-9.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:276a5ca930c913f714e372b2591a22c4bd3b81a418c0f6635ba832daec1cbcfc"}, + {file = "Pillow-9.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:73bd195e43f3fadecfc50c682f5055ec32ee2c933243cafbfdec69ab1aa87cad"}, + {file = "Pillow-9.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c7c8ae3864846fc95f4611c78129301e203aaa2af813b703c55d10cc1628535"}, + {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e0918e03aa0c72ea56edbb00d4d664294815aa11291a11504a377ea018330d3"}, + {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0915e734b33a474d76c28e07292f196cdf2a590a0d25bcc06e64e545f2d146c"}, + {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af0372acb5d3598f36ec0914deed2a63f6bcdb7b606da04dc19a88d31bf0c05b"}, + {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ad58d27a5b0262c0c19b47d54c5802db9b34d38bbf886665b626aff83c74bacd"}, + {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:97aabc5c50312afa5e0a2b07c17d4ac5e865b250986f8afe2b02d772567a380c"}, + {file = "Pillow-9.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9aaa107275d8527e9d6e7670b64aabaaa36e5b6bd71a1015ddd21da0d4e06448"}, + {file = "Pillow-9.3.0-cp39-cp39-win32.whl", hash = "sha256:bac18ab8d2d1e6b4ce25e3424f709aceef668347db8637c2296bcf41acb7cf48"}, + {file = "Pillow-9.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:b472b5ea442148d1c3e2209f20f1e0bb0eb556538690fa70b5e1f79fa0ba8dc2"}, + {file = "Pillow-9.3.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ab388aaa3f6ce52ac1cb8e122c4bd46657c15905904b3120a6248b5b8b0bc228"}, + {file = "Pillow-9.3.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbb8e7f2abee51cef77673be97760abff1674ed32847ce04b4af90f610144c7b"}, + {file = "Pillow-9.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca31dd6014cb8b0b2db1e46081b0ca7d936f856da3b39744aef499db5d84d02"}, + {file = "Pillow-9.3.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c7025dce65566eb6e89f56c9509d4f628fddcedb131d9465cacd3d8bac337e7e"}, + {file = "Pillow-9.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ebf2029c1f464c59b8bdbe5143c79fa2045a581ac53679733d3a91d400ff9efb"}, + {file = "Pillow-9.3.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b59430236b8e58840a0dfb4099a0e8717ffb779c952426a69ae435ca1f57210c"}, + {file = "Pillow-9.3.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12ce4932caf2ddf3e41d17fc9c02d67126935a44b86df6a206cf0d7161548627"}, + {file = "Pillow-9.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae5331c23ce118c53b172fa64a4c037eb83c9165aba3a7ba9ddd3ec9fa64a699"}, + {file = "Pillow-9.3.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0b07fffc13f474264c336298d1b4ce01d9c5a011415b79d4ee5527bb69ae6f65"}, + {file = "Pillow-9.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:073adb2ae23431d3b9bcbcff3fe698b62ed47211d0716b067385538a1b0f28b8"}, + {file = "Pillow-9.3.0.tar.gz", hash = "sha256:c935a22a557a560108d780f9a0fc426dd7459940dc54faa49d83249c8d3e760f"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "pkgutil_resolve_name" +version = "1.3.10" +description = "Resolve a name to an object." +optional = true +python-versions = ">=3.6" +files = [ + {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, + {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, +] + +[[package]] +name = "platformdirs" +version = "2.6.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-2.6.0-py3-none-any.whl", hash = "sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca"}, + {file = "platformdirs-2.6.0.tar.gz", hash = "sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e"}, +] + +[package.extras] +docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"] +test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "2.20.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"}, + {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +toml = "*" +virtualenv = ">=20.0.8" + +[[package]] +name = "proglog" +version = "0.1.10" +description = "Log and progress bar manager for console, notebooks, web..." +optional = false +python-versions = "*" +files = [ + {file = "proglog-0.1.10-py3-none-any.whl", hash = "sha256:19d5da037e8c813da480b741e3fa71fb1ac0a5b02bf21c41577c7f327485ec50"}, + {file = "proglog-0.1.10.tar.gz", hash = "sha256:658c28c9c82e4caeb2f25f488fff9ceace22f8d69b15d0c1c86d64275e4ddab4"}, +] + +[package.dependencies] +tqdm = "*" + +[[package]] +name = "prometheus-client" +version = "0.15.0" +description = "Python client for the Prometheus monitoring system." +optional = true +python-versions = ">=3.6" +files = [ + {file = "prometheus_client-0.15.0-py3-none-any.whl", hash = "sha256:db7c05cbd13a0f79975592d112320f2605a325969b270a94b71dcabc47b931d2"}, + {file = "prometheus_client-0.15.0.tar.gz", hash = "sha256:be26aa452490cfcf6da953f9436e95a9f2b4d578ca80094b4458930e5f584ab1"}, +] + +[package.extras] +twisted = ["twisted"] + +[[package]] +name = "promise" +version = "2.3" +description = "Promises/A+ implementation for Python" +optional = true +python-versions = "*" +files = [ + {file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"}, +] + +[package.dependencies] +six = "*" + +[package.extras] +test = ["coveralls", "futures", "mock", "pytest (>=2.7.3)", "pytest-benchmark", "pytest-cov"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.36" +description = "Library for building powerful interactive command lines in Python" +optional = true +python-versions = ">=3.6.2" +files = [ + {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, + {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "protobuf" +version = "3.19.6" +description = "Protocol Buffers" +optional = false +python-versions = ">=3.5" +files = [ + {file = "protobuf-3.19.6-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:010be24d5a44be7b0613750ab40bc8b8cedc796db468eae6c779b395f50d1fa1"}, + {file = "protobuf-3.19.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11478547958c2dfea921920617eb457bc26867b0d1aa065ab05f35080c5d9eb6"}, + {file = "protobuf-3.19.6-cp310-cp310-win32.whl", hash = "sha256:559670e006e3173308c9254d63facb2c03865818f22204037ab76f7a0ff70b5f"}, + {file = "protobuf-3.19.6-cp310-cp310-win_amd64.whl", hash = "sha256:347b393d4dd06fb93a77620781e11c058b3b0a5289262f094379ada2920a3730"}, + {file = "protobuf-3.19.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a8ce5ae0de28b51dff886fb922012dad885e66176663950cb2344c0439ecb473"}, + {file = "protobuf-3.19.6-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90b0d02163c4e67279ddb6dc25e063db0130fc299aefabb5d481053509fae5c8"}, + {file = "protobuf-3.19.6-cp36-cp36m-win32.whl", hash = "sha256:30f5370d50295b246eaa0296533403961f7e64b03ea12265d6dfce3a391d8992"}, + {file = "protobuf-3.19.6-cp36-cp36m-win_amd64.whl", hash = "sha256:0c0714b025ec057b5a7600cb66ce7c693815f897cfda6d6efb58201c472e3437"}, + {file = "protobuf-3.19.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5057c64052a1f1dd7d4450e9aac25af6bf36cfbfb3a1cd89d16393a036c49157"}, + {file = "protobuf-3.19.6-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:bb6776bd18f01ffe9920e78e03a8676530a5d6c5911934c6a1ac6eb78973ecb6"}, + {file = "protobuf-3.19.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84a04134866861b11556a82dd91ea6daf1f4925746b992f277b84013a7cc1229"}, + {file = "protobuf-3.19.6-cp37-cp37m-win32.whl", hash = "sha256:4bc98de3cdccfb5cd769620d5785b92c662b6bfad03a202b83799b6ed3fa1fa7"}, + {file = "protobuf-3.19.6-cp37-cp37m-win_amd64.whl", hash = "sha256:aa3b82ca1f24ab5326dcf4ea00fcbda703e986b22f3d27541654f749564d778b"}, + {file = "protobuf-3.19.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2b2d2913bcda0e0ec9a784d194bc490f5dc3d9d71d322d070b11a0ade32ff6ba"}, + {file = "protobuf-3.19.6-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:d0b635cefebd7a8a0f92020562dead912f81f401af7e71f16bf9506ff3bdbb38"}, + {file = "protobuf-3.19.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a552af4dc34793803f4e735aabe97ffc45962dfd3a237bdde242bff5a3de684"}, + {file = "protobuf-3.19.6-cp38-cp38-win32.whl", hash = "sha256:0469bc66160180165e4e29de7f445e57a34ab68f49357392c5b2f54c656ab25e"}, + {file = "protobuf-3.19.6-cp38-cp38-win_amd64.whl", hash = "sha256:91d5f1e139ff92c37e0ff07f391101df77e55ebb97f46bbc1535298d72019462"}, + {file = "protobuf-3.19.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c0ccd3f940fe7f3b35a261b1dd1b4fc850c8fde9f74207015431f174be5976b3"}, + {file = "protobuf-3.19.6-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:30a15015d86b9c3b8d6bf78d5b8c7749f2512c29f168ca259c9d7727604d0e39"}, + {file = "protobuf-3.19.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:878b4cd080a21ddda6ac6d1e163403ec6eea2e206cf225982ae04567d39be7b0"}, + {file = "protobuf-3.19.6-cp39-cp39-win32.whl", hash = "sha256:5a0d7539a1b1fb7e76bf5faa0b44b30f812758e989e59c40f77a7dab320e79b9"}, + {file = "protobuf-3.19.6-cp39-cp39-win_amd64.whl", hash = "sha256:bbf5cea5048272e1c60d235c7bd12ce1b14b8a16e76917f371c718bd3005f045"}, + {file = "protobuf-3.19.6-py2.py3-none-any.whl", hash = "sha256:14082457dc02be946f60b15aad35e9f5c69e738f80ebbc0900a19bc83734a5a4"}, + {file = "protobuf-3.19.6.tar.gz", hash = "sha256:5f5540d57a43042389e87661c6eaa50f47c19c6176e8cf1c4f287aeefeccb5c4"}, +] + +[[package]] +name = "psutil" +version = "5.9.4" +description = "Cross-platform lib for process and system monitoring in Python." +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "psutil-5.9.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8"}, + {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe"}, + {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549"}, + {file = "psutil-5.9.4-cp27-cp27m-win32.whl", hash = "sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad"}, + {file = "psutil-5.9.4-cp27-cp27m-win_amd64.whl", hash = "sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94"}, + {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24"}, + {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7"}, + {file = "psutil-5.9.4-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7"}, + {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1"}, + {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08"}, + {file = "psutil-5.9.4-cp36-abi3-win32.whl", hash = "sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff"}, + {file = "psutil-5.9.4-cp36-abi3-win_amd64.whl", hash = "sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4"}, + {file = "psutil-5.9.4-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e"}, + {file = "psutil-5.9.4.tar.gz", hash = "sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = true +python-versions = "*" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.2" +description = "Safely evaluate AST nodes without side effects" +optional = true +python-versions = "*" +files = [ + {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, + {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, +] + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] + +[[package]] +name = "pyasn1" +version = "0.4.8" +description = "ASN.1 types and codecs" +optional = true +python-versions = "*" +files = [ + {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, + {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.2.8" +description = "A collection of ASN.1-based protocols modules." +optional = true +python-versions = "*" +files = [ + {file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"}, + {file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"}, +] + +[package.dependencies] +pyasn1 = ">=0.4.6,<0.5.0" + +[[package]] +name = "pycocotools" +version = "2.0.6" +description = "Official APIs for the MS-COCO dataset" +optional = true +python-versions = ">=3.5" +files = [ + {file = "pycocotools-2.0.6.tar.gz", hash = "sha256:7fe089b05cc18e806dcf3bd764708d86dab922a100f3734eb77fb77a70a1d18c"}, +] + +[package.dependencies] +matplotlib = ">=2.1.0" +numpy = "*" + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + +[[package]] +name = "Pygments" +version = "2.13.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = true +python-versions = ">=3.6" +files = [ + {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, + {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, +] + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = true +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pyrsistent" +version = "0.19.2" +description = "Persistent/Functional/Immutable data structures" +optional = true +python-versions = ">=3.7" +files = [ + {file = "pyrsistent-0.19.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d6982b5a0237e1b7d876b60265564648a69b14017f3b5f908c5be2de3f9abb7a"}, + {file = "pyrsistent-0.19.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:187d5730b0507d9285a96fca9716310d572e5464cadd19f22b63a6976254d77a"}, + {file = "pyrsistent-0.19.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:055ab45d5911d7cae397dc418808d8802fb95262751872c841c170b0dbf51eed"}, + {file = "pyrsistent-0.19.2-cp310-cp310-win32.whl", hash = "sha256:456cb30ca8bff00596519f2c53e42c245c09e1a4543945703acd4312949bfd41"}, + {file = "pyrsistent-0.19.2-cp310-cp310-win_amd64.whl", hash = "sha256:b39725209e06759217d1ac5fcdb510e98670af9e37223985f330b611f62e7425"}, + {file = "pyrsistent-0.19.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2aede922a488861de0ad00c7630a6e2d57e8023e4be72d9d7147a9fcd2d30712"}, + {file = "pyrsistent-0.19.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879b4c2f4d41585c42df4d7654ddffff1239dc4065bc88b745f0341828b83e78"}, + {file = "pyrsistent-0.19.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c43bec251bbd10e3cb58ced80609c5c1eb238da9ca78b964aea410fb820d00d6"}, + {file = "pyrsistent-0.19.2-cp37-cp37m-win32.whl", hash = "sha256:d690b18ac4b3e3cab73b0b7aa7dbe65978a172ff94970ff98d82f2031f8971c2"}, + {file = "pyrsistent-0.19.2-cp37-cp37m-win_amd64.whl", hash = "sha256:3ba4134a3ff0fc7ad225b6b457d1309f4698108fb6b35532d015dca8f5abed73"}, + {file = "pyrsistent-0.19.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a178209e2df710e3f142cbd05313ba0c5ebed0a55d78d9945ac7a4e09d923308"}, + {file = "pyrsistent-0.19.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e371b844cec09d8dc424d940e54bba8f67a03ebea20ff7b7b0d56f526c71d584"}, + {file = "pyrsistent-0.19.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111156137b2e71f3a9936baf27cb322e8024dac3dc54ec7fb9f0bcf3249e68bb"}, + {file = "pyrsistent-0.19.2-cp38-cp38-win32.whl", hash = "sha256:e5d8f84d81e3729c3b506657dddfe46e8ba9c330bf1858ee33108f8bb2adb38a"}, + {file = "pyrsistent-0.19.2-cp38-cp38-win_amd64.whl", hash = "sha256:9cd3e9978d12b5d99cbdc727a3022da0430ad007dacf33d0bf554b96427f33ab"}, + {file = "pyrsistent-0.19.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f1258f4e6c42ad0b20f9cfcc3ada5bd6b83374516cd01c0960e3cb75fdca6770"}, + {file = "pyrsistent-0.19.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21455e2b16000440e896ab99e8304617151981ed40c29e9507ef1c2e4314ee95"}, + {file = "pyrsistent-0.19.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd880614c6237243ff53a0539f1cb26987a6dc8ac6e66e0c5a40617296a045e"}, + {file = "pyrsistent-0.19.2-cp39-cp39-win32.whl", hash = "sha256:71d332b0320642b3261e9fee47ab9e65872c2bd90260e5d225dabeed93cbd42b"}, + {file = "pyrsistent-0.19.2-cp39-cp39-win_amd64.whl", hash = "sha256:dec3eac7549869365fe263831f576c8457f6c833937c68542d08fde73457d291"}, + {file = "pyrsistent-0.19.2-py3-none-any.whl", hash = "sha256:ea6b79a02a28550c98b6ca9c35b9f492beaa54d7c5c9e9949555893c8a9234d0"}, + {file = "pyrsistent-0.19.2.tar.gz", hash = "sha256:bfa0351be89c9fcbcb8c9879b826f4353be10f58f8a677efab0c017bf7137ec2"}, +] + +[[package]] +name = "pytest" +version = "7.2.0" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, + {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, +] + +[package.dependencies] +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = true +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-json-logger" +version = "2.0.4" +description = "A python library adding a json log formatter" +optional = true +python-versions = ">=3.5" +files = [ + {file = "python-json-logger-2.0.4.tar.gz", hash = "sha256:764d762175f99fcc4630bd4853b09632acb60a6224acb27ce08cd70f0b1b81bd"}, + {file = "python_json_logger-2.0.4-py3-none-any.whl", hash = "sha256:3b03487b14eb9e4f77e4fc2a023358b5394b82fd89cecf5586259baed57d8c6f"}, +] + +[[package]] +name = "pytorch-lightning" +version = "1.8.4.post0" +description = "PyTorch Lightning is the lightweight PyTorch wrapper for ML researchers. Scale your models. Write less boilerplate." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytorch-lightning-1.8.4.post0.tar.gz", hash = "sha256:c567af53705807c734546002b1a3535993a15b7e8ed1bc92e9d4701d1787428f"}, + {file = "pytorch_lightning-1.8.4.post0-py3-none-any.whl", hash = "sha256:2807570f81609ab59f8f26865f03b06b94fa8b7ba91960c50188d7afd76a784d"}, +] + +[package.dependencies] +fsspec = {version = ">2021.06.0", extras = ["http"]} +lightning-utilities = ">=0.3.0,<0.4.0 || >0.4.0" +numpy = ">=1.17.2" +packaging = ">=17.0" +PyYAML = ">=5.4" +tensorboardX = ">=2.2" +torch = ">=1.9.0" +torchmetrics = ">=0.7.0" +tqdm = ">=4.57.0" +typing-extensions = ">=4.0.0" + +[package.extras] +all = ["cloudpickle (>=1.3)", "codecov (==2.1.12)", "colossalai (>=0.1.10)", "coverage (==6.5.0)", "deepspeed (>=0.6.0)", "fairscale (>=0.4.5)", "fastapi", "gym[classic-control] (>=0.17.0)", "hivemind (==1.1.2)", "horovod (>=0.21.2,!=0.24.0)", "hydra-core (>=1.0.5)", "ipython[all]", "jsonargparse[signatures] (>=4.15.2)", "matplotlib (>3.1)", "omegaconf (>=2.0.5)", "onnxruntime", "pandas (>1.0)", "pre-commit (==2.20.0)", "protobuf (<=3.20.1)", "psutil", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-forked (==1.4.0)", "pytest-rerunfailures (==10.2)", "rich (>=10.14.0,!=10.15.0.a)", "scikit-learn (>0.22.1)", "tensorboard (>=2.9.1)", "torchvision (>=0.10.0)", "uvicorn"] +colossalai = ["colossalai (>=0.1.10)"] +deepspeed = ["deepspeed (>=0.6.0)"] +dev = ["cloudpickle (>=1.3)", "codecov (==2.1.12)", "coverage (==6.5.0)", "fastapi", "hydra-core (>=1.0.5)", "jsonargparse[signatures] (>=4.15.2)", "matplotlib (>3.1)", "omegaconf (>=2.0.5)", "onnxruntime", "pandas (>1.0)", "pre-commit (==2.20.0)", "protobuf (<=3.20.1)", "psutil", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-forked (==1.4.0)", "pytest-rerunfailures (==10.2)", "rich (>=10.14.0,!=10.15.0.a)", "scikit-learn (>0.22.1)", "tensorboard (>=2.9.1)", "uvicorn"] +examples = ["gym[classic-control] (>=0.17.0)", "ipython[all]", "torchvision (>=0.10.0)"] +extra = ["hydra-core (>=1.0.5)", "jsonargparse[signatures] (>=4.15.2)", "matplotlib (>3.1)", "omegaconf (>=2.0.5)", "rich (>=10.14.0,!=10.15.0.a)"] +fairscale = ["fairscale (>=0.4.5)"] +hivemind = ["hivemind (==1.1.2)"] +horovod = ["horovod (>=0.21.2,!=0.24.0)"] +strategies = ["colossalai (>=0.1.10)", "deepspeed (>=0.6.0)", "fairscale (>=0.4.5)", "hivemind (==1.1.2)", "horovod (>=0.21.2,!=0.24.0)"] +test = ["cloudpickle (>=1.3)", "codecov (==2.1.12)", "coverage (==6.5.0)", "fastapi", "onnxruntime", "pandas (>1.0)", "pre-commit (==2.20.0)", "protobuf (<=3.20.1)", "psutil", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-forked (==1.4.0)", "pytest-rerunfailures (==10.2)", "scikit-learn (>0.22.1)", "tensorboard (>=2.9.1)", "uvicorn"] + +[[package]] +name = "pytz" +version = "2023.3.post1" +description = "World timezone definitions, modern and historical" +optional = true +python-versions = "*" +files = [ + {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, + {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, +] + +[[package]] +name = "pyuv" +version = "1.4.0" +description = "Python interface for libuv" +optional = true +python-versions = "*" +files = [] +develop = false + +[package.source] +type = "git" +url = "https://github.com/saghul/pyuv.git" +reference = "2a3d42d44c6315ebd73899a35118380d2d5979b5" +resolved_reference = "2a3d42d44c6315ebd73899a35118380d2d5979b5" + +[[package]] +name = "pywin32" +version = "305" +description = "Python for Window Extensions" +optional = true +python-versions = "*" +files = [ + {file = "pywin32-305-cp310-cp310-win32.whl", hash = "sha256:421f6cd86e84bbb696d54563c48014b12a23ef95a14e0bdba526be756d89f116"}, + {file = "pywin32-305-cp310-cp310-win_amd64.whl", hash = "sha256:73e819c6bed89f44ff1d690498c0a811948f73777e5f97c494c152b850fad478"}, + {file = "pywin32-305-cp310-cp310-win_arm64.whl", hash = "sha256:742eb905ce2187133a29365b428e6c3b9001d79accdc30aa8969afba1d8470f4"}, + {file = "pywin32-305-cp311-cp311-win32.whl", hash = "sha256:19ca459cd2e66c0e2cc9a09d589f71d827f26d47fe4a9d09175f6aa0256b51c2"}, + {file = "pywin32-305-cp311-cp311-win_amd64.whl", hash = "sha256:326f42ab4cfff56e77e3e595aeaf6c216712bbdd91e464d167c6434b28d65990"}, + {file = "pywin32-305-cp311-cp311-win_arm64.whl", hash = "sha256:4ecd404b2c6eceaca52f8b2e3e91b2187850a1ad3f8b746d0796a98b4cea04db"}, + {file = "pywin32-305-cp36-cp36m-win32.whl", hash = "sha256:48d8b1659284f3c17b68587af047d110d8c44837736b8932c034091683e05863"}, + {file = "pywin32-305-cp36-cp36m-win_amd64.whl", hash = "sha256:13362cc5aa93c2beaf489c9c9017c793722aeb56d3e5166dadd5ef82da021fe1"}, + {file = "pywin32-305-cp37-cp37m-win32.whl", hash = "sha256:a55db448124d1c1484df22fa8bbcbc45c64da5e6eae74ab095b9ea62e6d00496"}, + {file = "pywin32-305-cp37-cp37m-win_amd64.whl", hash = "sha256:109f98980bfb27e78f4df8a51a8198e10b0f347257d1e265bb1a32993d0c973d"}, + {file = "pywin32-305-cp38-cp38-win32.whl", hash = "sha256:9dd98384da775afa009bc04863426cb30596fd78c6f8e4e2e5bbf4edf8029504"}, + {file = "pywin32-305-cp38-cp38-win_amd64.whl", hash = "sha256:56d7a9c6e1a6835f521788f53b5af7912090674bb84ef5611663ee1595860fc7"}, + {file = "pywin32-305-cp39-cp39-win32.whl", hash = "sha256:9d968c677ac4d5cbdaa62fd3014ab241718e619d8e36ef8e11fb930515a1e918"}, + {file = "pywin32-305-cp39-cp39-win_amd64.whl", hash = "sha256:50768c6b7c3f0b38b7fb14dd4104da93ebced5f1a50dc0e834594bff6fbe1271"}, +] + +[[package]] +name = "pywinpty" +version = "2.0.9" +description = "Pseudo terminal support for Windows from Python." +optional = true +python-versions = ">=3.7" +files = [ + {file = "pywinpty-2.0.9-cp310-none-win_amd64.whl", hash = "sha256:30a7b371446a694a6ce5ef906d70ac04e569de5308c42a2bdc9c3bc9275ec51f"}, + {file = "pywinpty-2.0.9-cp311-none-win_amd64.whl", hash = "sha256:d78ef6f4bd7a6c6f94dc1a39ba8fb028540cc39f5cb593e756506db17843125f"}, + {file = "pywinpty-2.0.9-cp37-none-win_amd64.whl", hash = "sha256:5ed36aa087e35a3a183f833631b3e4c1ae92fe2faabfce0fa91b77ed3f0f1382"}, + {file = "pywinpty-2.0.9-cp38-none-win_amd64.whl", hash = "sha256:2352f44ee913faaec0a02d3c112595e56b8af7feeb8100efc6dc1a8685044199"}, + {file = "pywinpty-2.0.9-cp39-none-win_amd64.whl", hash = "sha256:ba75ec55f46c9e17db961d26485b033deb20758b1731e8e208e1e8a387fcf70c"}, + {file = "pywinpty-2.0.9.tar.gz", hash = "sha256:01b6400dd79212f50a2f01af1c65b781290ff39610853db99bf03962eb9a615f"}, +] + +[[package]] +name = "PyYAML" +version = "6.0" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] + +[[package]] +name = "pyzmq" +version = "24.0.1" +description = "Python bindings for 0MQ" +optional = true +python-versions = ">=3.6" +files = [ + {file = "pyzmq-24.0.1-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:28b119ba97129d3001673a697b7cce47fe6de1f7255d104c2f01108a5179a066"}, + {file = "pyzmq-24.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bcbebd369493d68162cddb74a9c1fcebd139dfbb7ddb23d8f8e43e6c87bac3a6"}, + {file = "pyzmq-24.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae61446166983c663cee42c852ed63899e43e484abf080089f771df4b9d272ef"}, + {file = "pyzmq-24.0.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87f7ac99b15270db8d53f28c3c7b968612993a90a5cf359da354efe96f5372b4"}, + {file = "pyzmq-24.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dca7c3956b03b7663fac4d150f5e6d4f6f38b2462c1e9afd83bcf7019f17913"}, + {file = "pyzmq-24.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8c78bfe20d4c890cb5580a3b9290f700c570e167d4cdcc55feec07030297a5e3"}, + {file = "pyzmq-24.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:48f721f070726cd2a6e44f3c33f8ee4b24188e4b816e6dd8ba542c8c3bb5b246"}, + {file = "pyzmq-24.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:afe1f3bc486d0ce40abb0a0c9adb39aed3bbac36ebdc596487b0cceba55c21c1"}, + {file = "pyzmq-24.0.1-cp310-cp310-win32.whl", hash = "sha256:3e6192dbcefaaa52ed81be88525a54a445f4b4fe2fffcae7fe40ebb58bd06bfd"}, + {file = "pyzmq-24.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:86de64468cad9c6d269f32a6390e210ca5ada568c7a55de8e681ca3b897bb340"}, + {file = "pyzmq-24.0.1-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:838812c65ed5f7c2bd11f7b098d2e5d01685a3f6d1f82849423b570bae698c00"}, + {file = "pyzmq-24.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dfb992dbcd88d8254471760879d48fb20836d91baa90f181c957122f9592b3dc"}, + {file = "pyzmq-24.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7abddb2bd5489d30ffeb4b93a428130886c171b4d355ccd226e83254fcb6b9ef"}, + {file = "pyzmq-24.0.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94010bd61bc168c103a5b3b0f56ed3b616688192db7cd5b1d626e49f28ff51b3"}, + {file = "pyzmq-24.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8242543c522d84d033fe79be04cb559b80d7eb98ad81b137ff7e0a9020f00ace"}, + {file = "pyzmq-24.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ccb94342d13e3bf3ffa6e62f95b5e3f0bc6bfa94558cb37f4b3d09d6feb536ff"}, + {file = "pyzmq-24.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6640f83df0ae4ae1104d4c62b77e9ef39be85ebe53f636388707d532bee2b7b8"}, + {file = "pyzmq-24.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a180dbd5ea5d47c2d3b716d5c19cc3fb162d1c8db93b21a1295d69585bfddac1"}, + {file = "pyzmq-24.0.1-cp311-cp311-win32.whl", hash = "sha256:624321120f7e60336be8ec74a172ae7fba5c3ed5bf787cc85f7e9986c9e0ebc2"}, + {file = "pyzmq-24.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:1724117bae69e091309ffb8255412c4651d3f6355560d9af312d547f6c5bc8b8"}, + {file = "pyzmq-24.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:15975747462ec49fdc863af906bab87c43b2491403ab37a6d88410635786b0f4"}, + {file = "pyzmq-24.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b947e264f0e77d30dcbccbb00f49f900b204b922eb0c3a9f0afd61aaa1cedc3d"}, + {file = "pyzmq-24.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ec91f1bad66f3ee8c6deb65fa1fe418e8ad803efedd69c35f3b5502f43bd1dc"}, + {file = "pyzmq-24.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:db03704b3506455d86ec72c3358a779e9b1d07b61220dfb43702b7b668edcd0d"}, + {file = "pyzmq-24.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e7e66b4e403c2836ac74f26c4b65d8ac0ca1eef41dfcac2d013b7482befaad83"}, + {file = "pyzmq-24.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:7a23ccc1083c260fa9685c93e3b170baba45aeed4b524deb3f426b0c40c11639"}, + {file = "pyzmq-24.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:fa0ae3275ef706c0309556061185dd0e4c4cd3b7d6f67ae617e4e677c7a41e2e"}, + {file = "pyzmq-24.0.1-cp36-cp36m-win32.whl", hash = "sha256:f01de4ec083daebf210531e2cca3bdb1608dbbbe00a9723e261d92087a1f6ebc"}, + {file = "pyzmq-24.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:de4217b9eb8b541cf2b7fde4401ce9d9a411cc0af85d410f9d6f4333f43640be"}, + {file = "pyzmq-24.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:78068e8678ca023594e4a0ab558905c1033b2d3e806a0ad9e3094e231e115a33"}, + {file = "pyzmq-24.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77c2713faf25a953c69cf0f723d1b7dd83827b0834e6c41e3fb3bbc6765914a1"}, + {file = "pyzmq-24.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bb4af15f305056e95ca1bd086239b9ebc6ad55e9f49076d27d80027f72752f6"}, + {file = "pyzmq-24.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0f14cffd32e9c4c73da66db97853a6aeceaac34acdc0fae9e5bbc9370281864c"}, + {file = "pyzmq-24.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0108358dab8c6b27ff6b985c2af4b12665c1bc659648284153ee501000f5c107"}, + {file = "pyzmq-24.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d66689e840e75221b0b290b0befa86f059fb35e1ee6443bce51516d4d61b6b99"}, + {file = "pyzmq-24.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae08ac90aa8fa14caafc7a6251bd218bf6dac518b7bff09caaa5e781119ba3f2"}, + {file = "pyzmq-24.0.1-cp37-cp37m-win32.whl", hash = "sha256:8421aa8c9b45ea608c205db9e1c0c855c7e54d0e9c2c2f337ce024f6843cab3b"}, + {file = "pyzmq-24.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54d8b9c5e288362ec8595c1d98666d36f2070fd0c2f76e2b3c60fbad9bd76227"}, + {file = "pyzmq-24.0.1-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:acbd0a6d61cc954b9f535daaa9ec26b0a60a0d4353c5f7c1438ebc88a359a47e"}, + {file = "pyzmq-24.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:47b11a729d61a47df56346283a4a800fa379ae6a85870d5a2e1e4956c828eedc"}, + {file = "pyzmq-24.0.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:abe6eb10122f0d746a0d510c2039ae8edb27bc9af29f6d1b05a66cc2401353ff"}, + {file = "pyzmq-24.0.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:07bec1a1b22dacf718f2c0e71b49600bb6a31a88f06527dfd0b5aababe3fa3f7"}, + {file = "pyzmq-24.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d945a85b70da97ae86113faf9f1b9294efe66bd4a5d6f82f2676d567338b66"}, + {file = "pyzmq-24.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1b7928bb7580736ffac5baf814097be342ba08d3cfdfb48e52773ec959572287"}, + {file = "pyzmq-24.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b946da90dc2799bcafa682692c1d2139b2a96ec3c24fa9fc6f5b0da782675330"}, + {file = "pyzmq-24.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c8840f064b1fb377cffd3efeaad2b190c14d4c8da02316dae07571252d20b31f"}, + {file = "pyzmq-24.0.1-cp38-cp38-win32.whl", hash = "sha256:4854f9edc5208f63f0841c0c667260ae8d6846cfa233c479e29fdc85d42ebd58"}, + {file = "pyzmq-24.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:42d4f97b9795a7aafa152a36fe2ad44549b83a743fd3e77011136def512e6c2a"}, + {file = "pyzmq-24.0.1-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:52afb0ac962963fff30cf1be775bc51ae083ef4c1e354266ab20e5382057dd62"}, + {file = "pyzmq-24.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bad8210ad4df68c44ff3685cca3cda448ee46e20d13edcff8909eba6ec01ca4"}, + {file = "pyzmq-24.0.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dabf1a05318d95b1537fd61d9330ef4313ea1216eea128a17615038859da3b3b"}, + {file = "pyzmq-24.0.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5bd3d7dfd9cd058eb68d9a905dec854f86649f64d4ddf21f3ec289341386c44b"}, + {file = "pyzmq-24.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8012bce6836d3f20a6c9599f81dfa945f433dab4dbd0c4917a6fb1f998ab33d"}, + {file = "pyzmq-24.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c31805d2c8ade9b11feca4674eee2b9cce1fec3e8ddb7bbdd961a09dc76a80ea"}, + {file = "pyzmq-24.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:3104f4b084ad5d9c0cb87445cc8cfd96bba710bef4a66c2674910127044df209"}, + {file = "pyzmq-24.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:df0841f94928f8af9c7a1f0aaaffba1fb74607af023a152f59379c01c53aee58"}, + {file = "pyzmq-24.0.1-cp39-cp39-win32.whl", hash = "sha256:a435ef8a3bd95c8a2d316d6e0ff70d0db524f6037411652803e118871d703333"}, + {file = "pyzmq-24.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:2032d9cb994ce3b4cba2b8dfae08c7e25bc14ba484c770d4d3be33c27de8c45b"}, + {file = "pyzmq-24.0.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bb5635c851eef3a7a54becde6da99485eecf7d068bd885ac8e6d173c4ecd68b0"}, + {file = "pyzmq-24.0.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:83ea1a398f192957cb986d9206ce229efe0ee75e3c6635baff53ddf39bd718d5"}, + {file = "pyzmq-24.0.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:941fab0073f0a54dc33d1a0460cb04e0d85893cb0c5e1476c785000f8b359409"}, + {file = "pyzmq-24.0.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e8f482c44ccb5884bf3f638f29bea0f8dc68c97e38b2061769c4cb697f6140d"}, + {file = "pyzmq-24.0.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:613010b5d17906c4367609e6f52e9a2595e35d5cc27d36ff3f1b6fa6e954d944"}, + {file = "pyzmq-24.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:65c94410b5a8355cfcf12fd600a313efee46ce96a09e911ea92cf2acf6708804"}, + {file = "pyzmq-24.0.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:20e7eeb1166087db636c06cae04a1ef59298627f56fb17da10528ab52a14c87f"}, + {file = "pyzmq-24.0.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2712aee7b3834ace51738c15d9ee152cc5a98dc7d57dd93300461b792ab7b43"}, + {file = "pyzmq-24.0.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a7c280185c4da99e0cc06c63bdf91f5b0b71deb70d8717f0ab870a43e376db8"}, + {file = "pyzmq-24.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:858375573c9225cc8e5b49bfac846a77b696b8d5e815711b8d4ba3141e6e8879"}, + {file = "pyzmq-24.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:80093b595921eed1a2cead546a683b9e2ae7f4a4592bb2ab22f70d30174f003a"}, + {file = "pyzmq-24.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f3f3154fde2b1ff3aa7b4f9326347ebc89c8ef425ca1db8f665175e6d3bd42f"}, + {file = "pyzmq-24.0.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abb756147314430bee5d10919b8493c0ccb109ddb7f5dfd2fcd7441266a25b75"}, + {file = "pyzmq-24.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e706bac34e9f50779cb8c39f10b53a4d15aebb97235643d3112ac20bd577b4"}, + {file = "pyzmq-24.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:687700f8371643916a1d2c61f3fdaa630407dd205c38afff936545d7b7466066"}, + {file = "pyzmq-24.0.1.tar.gz", hash = "sha256:216f5d7dbb67166759e59b0479bca82b8acf9bed6015b526b8eb10143fb08e77"}, +] + +[package.dependencies] +cffi = {version = "*", markers = "implementation_name == \"pypy\""} +py = {version = "*", markers = "implementation_name == \"pypy\""} + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "requests-oauthlib" +version = "1.3.1" +description = "OAuthlib authentication support for Requests." +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"}, + {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +description = "A pure python RFC3339 validator" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, + {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, +] + +[package.dependencies] +six = "*" + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +description = "Pure python rfc3986 validator" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9"}, + {file = "rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055"}, +] + +[[package]] +name = "rsa" +version = "4.9" +description = "Pure-Python RSA implementation" +optional = true +python-versions = ">=3.6,<4" +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] + +[package.dependencies] +pyasn1 = ">=0.1.3" + +[[package]] +name = "ruff" +version = "0.0.292" +description = "An extremely fast Python linter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.0.292-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:02f29db018c9d474270c704e6c6b13b18ed0ecac82761e4fcf0faa3728430c96"}, + {file = "ruff-0.0.292-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:69654e564342f507edfa09ee6897883ca76e331d4bbc3676d8a8403838e9fade"}, + {file = "ruff-0.0.292-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c3c91859a9b845c33778f11902e7b26440d64b9d5110edd4e4fa1726c41e0a4"}, + {file = "ruff-0.0.292-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4476f1243af2d8c29da5f235c13dca52177117935e1f9393f9d90f9833f69e4"}, + {file = "ruff-0.0.292-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be8eb50eaf8648070b8e58ece8e69c9322d34afe367eec4210fdee9a555e4ca7"}, + {file = "ruff-0.0.292-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9889bac18a0c07018aac75ef6c1e6511d8411724d67cb879103b01758e110a81"}, + {file = "ruff-0.0.292-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bdfabd4334684a4418b99b3118793f2c13bb67bf1540a769d7816410402a205"}, + {file = "ruff-0.0.292-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa7c77c53bfcd75dbcd4d1f42d6cabf2485d2e1ee0678da850f08e1ab13081a8"}, + {file = "ruff-0.0.292-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e087b24d0d849c5c81516ec740bf4fd48bf363cfb104545464e0fca749b6af9"}, + {file = "ruff-0.0.292-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f160b5ec26be32362d0774964e218f3fcf0a7da299f7e220ef45ae9e3e67101a"}, + {file = "ruff-0.0.292-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ac153eee6dd4444501c4bb92bff866491d4bfb01ce26dd2fff7ca472c8df9ad0"}, + {file = "ruff-0.0.292-py3-none-musllinux_1_2_i686.whl", hash = "sha256:87616771e72820800b8faea82edd858324b29bb99a920d6aa3d3949dd3f88fb0"}, + {file = "ruff-0.0.292-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b76deb3bdbea2ef97db286cf953488745dd6424c122d275f05836c53f62d4016"}, + {file = "ruff-0.0.292-py3-none-win32.whl", hash = "sha256:e854b05408f7a8033a027e4b1c7f9889563dd2aca545d13d06711e5c39c3d003"}, + {file = "ruff-0.0.292-py3-none-win_amd64.whl", hash = "sha256:f27282bedfd04d4c3492e5c3398360c9d86a295be00eccc63914438b4ac8a83c"}, + {file = "ruff-0.0.292-py3-none-win_arm64.whl", hash = "sha256:7f67a69c8f12fbc8daf6ae6d36705037bde315abf8b82b6e1f4c9e74eb750f68"}, + {file = "ruff-0.0.292.tar.gz", hash = "sha256:1093449e37dd1e9b813798f6ad70932b57cf614e5c2b5c51005bf67d55db33ac"}, +] + +[[package]] +name = "safetensors" +version = "0.3.3" +description = "Fast and Safe Tensor serialization" +optional = false +python-versions = "*" +files = [ + {file = "safetensors-0.3.3-cp310-cp310-macosx_10_11_x86_64.whl", hash = "sha256:92e4d0c8b2836120fddd134474c5bda8963f322333941f8b9f643e5b24f041eb"}, + {file = "safetensors-0.3.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:3dcadb6153c42addc9c625a622ebde9293fabe1973f9ef31ba10fb42c16e8536"}, + {file = "safetensors-0.3.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:08f26b61e1b0a14dc959aa9d568776bd038805f611caef1de04a80c468d4a7a4"}, + {file = "safetensors-0.3.3-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:17f41344d9a075f2f21b289a49a62e98baff54b5754240ba896063bce31626bf"}, + {file = "safetensors-0.3.3-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:f1045f798e1a16a6ced98d6a42ec72936d367a2eec81dc5fade6ed54638cd7d2"}, + {file = "safetensors-0.3.3-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:eaf0e4bc91da13f21ac846a39429eb3f3b7ed06295a32321fa3eb1a59b5c70f3"}, + {file = "safetensors-0.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25149180d4dc8ca48bac2ac3852a9424b466e36336a39659b35b21b2116f96fc"}, + {file = "safetensors-0.3.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9e943bf78c39de8865398a71818315e7d5d1af93c7b30d4da3fc852e62ad9bc"}, + {file = "safetensors-0.3.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cccfcac04a010354e87c7a2fe16a1ff004fc4f6e7ef8efc966ed30122ce00bc7"}, + {file = "safetensors-0.3.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a07121f427e646a50d18c1be0fa1a2cbf6398624c31149cd7e6b35486d72189e"}, + {file = "safetensors-0.3.3-cp310-cp310-win32.whl", hash = "sha256:a85e29cbfddfea86453cc0f4889b4bcc6b9c155be9a60e27be479a34e199e7ef"}, + {file = "safetensors-0.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:e13adad4a3e591378f71068d14e92343e626cf698ff805f61cdb946e684a218e"}, + {file = "safetensors-0.3.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:cbc3312f134baf07334dd517341a4b470b2931f090bd9284888acb7dfaf4606f"}, + {file = "safetensors-0.3.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d15030af39d5d30c22bcbc6d180c65405b7ea4c05b7bab14a570eac7d7d43722"}, + {file = "safetensors-0.3.3-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:f84a74cbe9859b28e3d6d7715ac1dd3097bebf8d772694098f6d42435245860c"}, + {file = "safetensors-0.3.3-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:10d637423d98ab2e6a4ad96abf4534eb26fcaf8ca3115623e64c00759374e90d"}, + {file = "safetensors-0.3.3-cp311-cp311-macosx_13_0_universal2.whl", hash = "sha256:3b46f5de8b44084aff2e480874c550c399c730c84b2e8ad1bddb062c94aa14e9"}, + {file = "safetensors-0.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e76da691a82dfaf752854fa6d17c8eba0c8466370c5ad8cf1bfdf832d3c7ee17"}, + {file = "safetensors-0.3.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4e342fd54e66aa9512dd13e410f791e47aa4feeb5f4c9a20882c72f3d272f29"}, + {file = "safetensors-0.3.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:178fd30b5dc73bce14a39187d948cedd0e5698e2f055b7ea16b5a96c9b17438e"}, + {file = "safetensors-0.3.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e8fdf7407dba44587ed5e79d5de3533d242648e1f2041760b21474bd5ea5c8c"}, + {file = "safetensors-0.3.3-cp311-cp311-win32.whl", hash = "sha256:7d3b744cee8d7a46ffa68db1a2ff1a1a432488e3f7a5a97856fe69e22139d50c"}, + {file = "safetensors-0.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f579877d30feec9b6ba409d05fa174633a4fc095675a4a82971d831a8bb60b97"}, + {file = "safetensors-0.3.3-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:2fff5b19a1b462c17322998b2f4b8bce43c16fe208968174d2f3a1446284ceed"}, + {file = "safetensors-0.3.3-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:41adb1d39e8aad04b16879e3e0cbcb849315999fad73bc992091a01e379cb058"}, + {file = "safetensors-0.3.3-cp37-cp37m-macosx_12_0_x86_64.whl", hash = "sha256:0f2b404250b3b877b11d34afcc30d80e7035714a1116a3df56acaca6b6c00096"}, + {file = "safetensors-0.3.3-cp37-cp37m-macosx_13_0_x86_64.whl", hash = "sha256:b43956ef20e9f4f2e648818a9e7b3499edd6b753a0f5526d4f6a6826fbee8446"}, + {file = "safetensors-0.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d61a99b34169981f088ccfbb2c91170843efc869a0a0532f422db7211bf4f474"}, + {file = "safetensors-0.3.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c0008aab36cd20e9a051a68563c6f80d40f238c2611811d7faa5a18bf3fd3984"}, + {file = "safetensors-0.3.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:93d54166072b143084fdcd214a080a088050c1bb1651016b55942701b31334e4"}, + {file = "safetensors-0.3.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c32ee08f61cea56a5d62bbf94af95df6040c8ab574afffaeb7b44ae5da1e9e3"}, + {file = "safetensors-0.3.3-cp37-cp37m-win32.whl", hash = "sha256:351600f367badd59f7bfe86d317bb768dd8c59c1561c6fac43cafbd9c1af7827"}, + {file = "safetensors-0.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:034717e297849dae1af0a7027a14b8647bd2e272c24106dced64d83e10d468d1"}, + {file = "safetensors-0.3.3-cp38-cp38-macosx_10_11_x86_64.whl", hash = "sha256:8530399666748634bc0b301a6a5523756931b0c2680d188e743d16304afe917a"}, + {file = "safetensors-0.3.3-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:9d741c1f1621e489ba10aa3d135b54202684f6e205df52e219d5eecd673a80c9"}, + {file = "safetensors-0.3.3-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:0c345fd85b4d2093a5109596ff4cd9dfc2e84992e881b4857fbc4a93a3b89ddb"}, + {file = "safetensors-0.3.3-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:69ccee8d05f55cdf76f7e6c87d2bdfb648c16778ef8acfd2ecc495e273e9233e"}, + {file = "safetensors-0.3.3-cp38-cp38-macosx_13_0_arm64.whl", hash = "sha256:c08a9a4b7a4ca389232fa8d097aebc20bbd4f61e477abc7065b5c18b8202dede"}, + {file = "safetensors-0.3.3-cp38-cp38-macosx_13_0_x86_64.whl", hash = "sha256:a002868d2e3f49bbe81bee2655a411c24fa1f8e68b703dec6629cb989d6ae42e"}, + {file = "safetensors-0.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3bd2704cb41faa44d3ec23e8b97330346da0395aec87f8eaf9c9e2c086cdbf13"}, + {file = "safetensors-0.3.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b2951bf3f0ad63df5e6a95263652bd6c194a6eb36fd4f2d29421cd63424c883"}, + {file = "safetensors-0.3.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07114cec116253ca2e7230fdea30acf76828f21614afd596d7b5438a2f719bd8"}, + {file = "safetensors-0.3.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ab43aeeb9eadbb6b460df3568a662e6f1911ecc39387f8752afcb6a7d96c087"}, + {file = "safetensors-0.3.3-cp38-cp38-win32.whl", hash = "sha256:f2f59fce31dd3429daca7269a6b06f65e6547a0c248f5116976c3f1e9b73f251"}, + {file = "safetensors-0.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:c31ca0d8610f57799925bf08616856b39518ab772c65093ef1516762e796fde4"}, + {file = "safetensors-0.3.3-cp39-cp39-macosx_10_11_x86_64.whl", hash = "sha256:59a596b3225c96d59af412385981f17dd95314e3fffdf359c7e3f5bb97730a19"}, + {file = "safetensors-0.3.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:82a16e92210a6221edd75ab17acdd468dd958ef5023d9c6c1289606cc30d1479"}, + {file = "safetensors-0.3.3-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:98a929e763a581f516373ef31983ed1257d2d0da912a8e05d5cd12e9e441c93a"}, + {file = "safetensors-0.3.3-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:12b83f1986cd16ea0454c636c37b11e819d60dd952c26978310a0835133480b7"}, + {file = "safetensors-0.3.3-cp39-cp39-macosx_13_0_arm64.whl", hash = "sha256:f439175c827c2f1bbd54df42789c5204a10983a30bc4242bc7deaf854a24f3f0"}, + {file = "safetensors-0.3.3-cp39-cp39-macosx_13_0_x86_64.whl", hash = "sha256:0085be33b8cbcb13079b3a8e131656e05b0bc5e6970530d4c24150f7afd76d70"}, + {file = "safetensors-0.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3ec70c87b1e910769034206ad5efc051069b105aac1687f6edcd02526767f4"}, + {file = "safetensors-0.3.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f490132383e5e490e710608f4acffcb98ed37f91b885c7217d3f9f10aaff9048"}, + {file = "safetensors-0.3.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:79d1b6c7ed5596baf79c80fbce5198c3cdcc521ae6a157699f427aba1a90082d"}, + {file = "safetensors-0.3.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad3cc8006e7a86ee7c88bd2813ec59cd7cc75b03e6fa4af89b9c7b235b438d68"}, + {file = "safetensors-0.3.3-cp39-cp39-win32.whl", hash = "sha256:ab29f54c6b8c301ca05fa014728996bd83aac6e21528f893aaf8945c71f42b6d"}, + {file = "safetensors-0.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:0fa82004eae1a71e2aa29843ef99de9350e459a0fc2f65fc6ee0da9690933d2d"}, + {file = "safetensors-0.3.3.tar.gz", hash = "sha256:edb7072d788c4f929d0f5735d3a2fb51e5a27f833587828583b7f5747af1a2b8"}, +] + +[package.extras] +all = ["black (==22.3)", "click (==8.0.4)", "flake8 (>=3.8.3)", "flax (>=0.6.3)", "h5py (>=3.7.0)", "huggingface-hub (>=0.12.1)", "isort (>=5.5.4)", "jax (>=0.3.25)", "jaxlib (>=0.3.25)", "numpy (>=1.21.6)", "paddlepaddle (>=2.4.1)", "pytest (>=7.2.0)", "pytest-benchmark (>=4.0.0)", "setuptools-rust (>=1.5.2)", "tensorflow (==2.11.0)", "torch (>=1.10)"] +dev = ["black (==22.3)", "click (==8.0.4)", "flake8 (>=3.8.3)", "flax (>=0.6.3)", "h5py (>=3.7.0)", "huggingface-hub (>=0.12.1)", "isort (>=5.5.4)", "jax (>=0.3.25)", "jaxlib (>=0.3.25)", "numpy (>=1.21.6)", "paddlepaddle (>=2.4.1)", "pytest (>=7.2.0)", "pytest-benchmark (>=4.0.0)", "setuptools-rust (>=1.5.2)", "tensorflow (==2.11.0)", "torch (>=1.10)"] +jax = ["flax (>=0.6.3)", "jax (>=0.3.25)", "jaxlib (>=0.3.25)", "numpy (>=1.21.6)"] +numpy = ["numpy (>=1.21.6)"] +paddlepaddle = ["numpy (>=1.21.6)", "paddlepaddle (>=2.4.1)"] +pinned-tf = ["tensorflow (==2.11.0)"] +quality = ["black (==22.3)", "click (==8.0.4)", "flake8 (>=3.8.3)", "isort (>=5.5.4)"] +tensorflow = ["numpy (>=1.21.6)", "tensorflow (>=2.11.0)"] +testing = ["h5py (>=3.7.0)", "huggingface-hub (>=0.12.1)", "numpy (>=1.21.6)", "pytest (>=7.2.0)", "pytest-benchmark (>=4.0.0)", "setuptools-rust (>=1.5.2)"] +torch = ["numpy (>=1.21.6)", "torch (>=1.10)"] + +[[package]] +name = "scikit-learn" +version = "1.3.2" +description = "A set of python modules for machine learning and data mining" +optional = true +python-versions = ">=3.8" +files = [ + {file = "scikit-learn-1.3.2.tar.gz", hash = "sha256:a2f54c76accc15a34bfb9066e6c7a56c1e7235dda5762b990792330b52ccfb05"}, + {file = "scikit_learn-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e326c0eb5cf4d6ba40f93776a20e9a7a69524c4db0757e7ce24ba222471ee8a1"}, + {file = "scikit_learn-1.3.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:535805c2a01ccb40ca4ab7d081d771aea67e535153e35a1fd99418fcedd1648a"}, + {file = "scikit_learn-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1215e5e58e9880b554b01187b8c9390bf4dc4692eedeaf542d3273f4785e342c"}, + {file = "scikit_learn-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ee107923a623b9f517754ea2f69ea3b62fc898a3641766cb7deb2f2ce450161"}, + {file = "scikit_learn-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:35a22e8015048c628ad099da9df5ab3004cdbf81edc75b396fd0cff8699ac58c"}, + {file = "scikit_learn-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6fb6bc98f234fda43163ddbe36df8bcde1d13ee176c6dc9b92bb7d3fc842eb66"}, + {file = "scikit_learn-1.3.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:18424efee518a1cde7b0b53a422cde2f6625197de6af36da0b57ec502f126157"}, + {file = "scikit_learn-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3271552a5eb16f208a6f7f617b8cc6d1f137b52c8a1ef8edf547db0259b2c9fb"}, + {file = "scikit_learn-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4144a5004a676d5022b798d9e573b05139e77f271253a4703eed295bde0433"}, + {file = "scikit_learn-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:67f37d708f042a9b8d59551cf94d30431e01374e00dc2645fa186059c6c5d78b"}, + {file = "scikit_learn-1.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8db94cd8a2e038b37a80a04df8783e09caac77cbe052146432e67800e430c028"}, + {file = "scikit_learn-1.3.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:61a6efd384258789aa89415a410dcdb39a50e19d3d8410bd29be365bcdd512d5"}, + {file = "scikit_learn-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb06f8dce3f5ddc5dee1715a9b9f19f20d295bed8e3cd4fa51e1d050347de525"}, + {file = "scikit_learn-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b2de18d86f630d68fe1f87af690d451388bb186480afc719e5f770590c2ef6c"}, + {file = "scikit_learn-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:0402638c9a7c219ee52c94cbebc8fcb5eb9fe9c773717965c1f4185588ad3107"}, + {file = "scikit_learn-1.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a19f90f95ba93c1a7f7924906d0576a84da7f3b2282ac3bfb7a08a32801add93"}, + {file = "scikit_learn-1.3.2-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:b8692e395a03a60cd927125eef3a8e3424d86dde9b2370d544f0ea35f78a8073"}, + {file = "scikit_learn-1.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15e1e94cc23d04d39da797ee34236ce2375ddea158b10bee3c343647d615581d"}, + {file = "scikit_learn-1.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:785a2213086b7b1abf037aeadbbd6d67159feb3e30263434139c98425e3dcfcf"}, + {file = "scikit_learn-1.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:64381066f8aa63c2710e6b56edc9f0894cc7bf59bd71b8ce5613a4559b6145e0"}, + {file = "scikit_learn-1.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6c43290337f7a4b969d207e620658372ba3c1ffb611f8bc2b6f031dc5c6d1d03"}, + {file = "scikit_learn-1.3.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:dc9002fc200bed597d5d34e90c752b74df516d592db162f756cc52836b38fe0e"}, + {file = "scikit_learn-1.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d08ada33e955c54355d909b9c06a4789a729977f165b8bae6f225ff0a60ec4a"}, + {file = "scikit_learn-1.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:763f0ae4b79b0ff9cca0bf3716bcc9915bdacff3cebea15ec79652d1cc4fa5c9"}, + {file = "scikit_learn-1.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:ed932ea780517b00dae7431e031faae6b49b20eb6950918eb83bd043237950e0"}, +] + +[package.dependencies] +joblib = ">=1.1.1" +numpy = ">=1.17.3,<2.0" +scipy = ">=1.5.0" +threadpoolctl = ">=2.0.0" + +[package.extras] +benchmark = ["matplotlib (>=3.1.3)", "memory-profiler (>=0.57.0)", "pandas (>=1.0.5)"] +docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.1.3)", "memory-profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.0.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.16.2)", "seaborn (>=0.9.0)", "sphinx (>=6.0.0)", "sphinx-copybutton (>=0.5.2)", "sphinx-gallery (>=0.10.1)", "sphinx-prompt (>=1.3.0)", "sphinxext-opengraph (>=0.4.2)"] +examples = ["matplotlib (>=3.1.3)", "pandas (>=1.0.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.16.2)", "seaborn (>=0.9.0)"] +tests = ["black (>=23.3.0)", "matplotlib (>=3.1.3)", "mypy (>=1.3)", "numpydoc (>=1.2.0)", "pandas (>=1.0.5)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.0.272)", "scikit-image (>=0.16.2)"] + +[[package]] +name = "scipy" +version = "1.10.1" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = "<3.12,>=3.8" +files = [ + {file = "scipy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7354fd7527a4b0377ce55f286805b34e8c54b91be865bac273f527e1b839019"}, + {file = "scipy-1.10.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:4b3f429188c66603a1a5c549fb414e4d3bdc2a24792e061ffbd607d3d75fd84e"}, + {file = "scipy-1.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1553b5dcddd64ba9a0d95355e63fe6c3fc303a8fd77c7bc91e77d61363f7433f"}, + {file = "scipy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c0ff64b06b10e35215abce517252b375e580a6125fd5fdf6421b98efbefb2d2"}, + {file = "scipy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:fae8a7b898c42dffe3f7361c40d5952b6bf32d10c4569098d276b4c547905ee1"}, + {file = "scipy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f1564ea217e82c1bbe75ddf7285ba0709ecd503f048cb1236ae9995f64217bd"}, + {file = "scipy-1.10.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d925fa1c81b772882aa55bcc10bf88324dadb66ff85d548c71515f6689c6dac5"}, + {file = "scipy-1.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaea0a6be54462ec027de54fca511540980d1e9eea68b2d5c1dbfe084797be35"}, + {file = "scipy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15a35c4242ec5f292c3dd364a7c71a61be87a3d4ddcc693372813c0b73c9af1d"}, + {file = "scipy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:43b8e0bcb877faf0abfb613d51026cd5cc78918e9530e375727bf0625c82788f"}, + {file = "scipy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5678f88c68ea866ed9ebe3a989091088553ba12c6090244fdae3e467b1139c35"}, + {file = "scipy-1.10.1-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:39becb03541f9e58243f4197584286e339029e8908c46f7221abeea4b749fa88"}, + {file = "scipy-1.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bce5869c8d68cf383ce240e44c1d9ae7c06078a9396df68ce88a1230f93a30c1"}, + {file = "scipy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07c3457ce0b3ad5124f98a86533106b643dd811dd61b548e78cf4c8786652f6f"}, + {file = "scipy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:049a8bbf0ad95277ffba9b3b7d23e5369cc39e66406d60422c8cfef40ccc8415"}, + {file = "scipy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cd9f1027ff30d90618914a64ca9b1a77a431159df0e2a195d8a9e8a04c78abf9"}, + {file = "scipy-1.10.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:79c8e5a6c6ffaf3a2262ef1be1e108a035cf4f05c14df56057b64acc5bebffb6"}, + {file = "scipy-1.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51af417a000d2dbe1ec6c372dfe688e041a7084da4fdd350aeb139bd3fb55353"}, + {file = "scipy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b4735d6c28aad3cdcf52117e0e91d6b39acd4272f3f5cd9907c24ee931ad601"}, + {file = "scipy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ff7f37b1bf4417baca958d254e8e2875d0cc23aaadbe65b3d5b3077b0eb23ea"}, + {file = "scipy-1.10.1.tar.gz", hash = "sha256:2cf9dfb80a7b4589ba4c40ce7588986d6d5cebc5457cad2c2880f6bc2d42f3a5"}, +] + +[package.dependencies] +numpy = ">=1.19.5,<1.27.0" + +[package.extras] +dev = ["click", "doit (>=0.36.0)", "flake8", "mypy", "pycodestyle", "pydevtool", "rich-click", "typing_extensions"] +doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"] +test = ["asv", "gmpy2", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + +[[package]] +name = "seaborn" +version = "0.13.0" +description = "Statistical data visualization" +optional = true +python-versions = ">=3.8" +files = [ + {file = "seaborn-0.13.0-py3-none-any.whl", hash = "sha256:70d740828c48de0f402bb17234e475eda687e3c65f4383ea25d0cc4728f7772e"}, + {file = "seaborn-0.13.0.tar.gz", hash = "sha256:0e76abd2ec291c655b516703c6a022f0fd5afed26c8e714e8baef48150f73598"}, +] + +[package.dependencies] +matplotlib = ">=3.3,<3.6.1 || >3.6.1" +numpy = ">=1.20,<1.24.0 || >1.24.0" +pandas = ">=1.2" + +[package.extras] +dev = ["flake8", "flit", "mypy", "pandas-stubs", "pre-commit", "pytest", "pytest-cov", "pytest-xdist"] +docs = ["ipykernel", "nbconvert", "numpydoc", "pydata_sphinx_theme (==0.10.0rc2)", "pyyaml", "sphinx (<6.0.0)", "sphinx-copybutton", "sphinx-design", "sphinx-issues"] +stats = ["scipy (>=1.7)", "statsmodels (>=0.12)"] + +[[package]] +name = "Send2Trash" +version = "1.8.0" +description = "Send file to trash natively under Mac OS X, Windows and Linux." +optional = true +python-versions = "*" +files = [ + {file = "Send2Trash-1.8.0-py3-none-any.whl", hash = "sha256:f20eaadfdb517eaca5ce077640cb261c7d2698385a6a0f072a4a5447fd49fa08"}, + {file = "Send2Trash-1.8.0.tar.gz", hash = "sha256:d2c24762fd3759860a0aff155e45871447ea58d2be6bdd39b5c8f966a0c99c2d"}, +] + +[package.extras] +nativelib = ["pyobjc-framework-Cocoa", "pywin32"] +objc = ["pyobjc-framework-Cocoa"] +win32 = ["pywin32"] + +[[package]] +name = "setuptools" +version = "65.6.3" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"}, + {file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "smart_settings" +version = "1.1" +description = "" +optional = true +python-versions = "*" +files = [] +develop = false + +[package.dependencies] +pyyaml = "*" +tomli = {version = "*", markers = "python_version < \"3.11\""} + +[package.source] +type = "git" +url = "https://github.com/martius-lab/smart-settings.git" +reference = "eb7331fdcad58d314a842087bbf136735e890013" +resolved_reference = "eb7331fdcad58d314a842087bbf136735e890013" + +[[package]] +name = "smmap" +version = "5.0.1" +description = "A pure Python implementation of a sliding window memory map manager" +optional = true +python-versions = ">=3.7" +files = [ + {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, + {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, +] + +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +optional = true +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] + +[[package]] +name = "soupsieve" +version = "2.3.2.post1" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = true +python-versions = ">=3.6" +files = [ + {file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"}, + {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"}, +] + +[[package]] +name = "stack-data" +version = "0.6.2" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = true +python-versions = "*" +files = [ + {file = "stack_data-0.6.2-py3-none-any.whl", hash = "sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8"}, + {file = "stack_data-0.6.2.tar.gz", hash = "sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + +[[package]] +name = "tabulate" +version = "0.9.0" +description = "Pretty-print tabular data" +optional = true +python-versions = ">=3.7" +files = [ + {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, + {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, +] + +[package.extras] +widechars = ["wcwidth"] + +[[package]] +name = "tensorboard" +version = "2.11.0" +description = "TensorBoard lets you watch Tensors Flow" +optional = true +python-versions = ">=3.7" +files = [ + {file = "tensorboard-2.11.0-py3-none-any.whl", hash = "sha256:a0e592ee87962e17af3f0dce7faae3fbbd239030159e9e625cce810b7e35c53d"}, +] + +[package.dependencies] +absl-py = ">=0.4" +google-auth = ">=1.6.3,<3" +google-auth-oauthlib = ">=0.4.1,<0.5" +grpcio = ">=1.24.3" +markdown = ">=2.6.8" +numpy = ">=1.12.0" +protobuf = ">=3.9.2,<4" +requests = ">=2.21.0,<3" +setuptools = ">=41.0.0" +tensorboard-data-server = ">=0.6.0,<0.7.0" +tensorboard-plugin-wit = ">=1.6.0" +werkzeug = ">=1.0.1" +wheel = ">=0.26" + +[[package]] +name = "tensorboard-data-server" +version = "0.6.1" +description = "Fast data loading for TensorBoard" +optional = true +python-versions = ">=3.6" +files = [ + {file = "tensorboard_data_server-0.6.1-py3-none-any.whl", hash = "sha256:809fe9887682d35c1f7d1f54f0f40f98bb1f771b14265b453ca051e2ce58fca7"}, + {file = "tensorboard_data_server-0.6.1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:fa8cef9be4fcae2f2363c88176638baf2da19c5ec90addb49b1cde05c95c88ee"}, + {file = "tensorboard_data_server-0.6.1-py3-none-manylinux2010_x86_64.whl", hash = "sha256:d8237580755e58eff68d1f3abefb5b1e39ae5c8b127cc40920f9c4fb33f4b98a"}, +] + +[[package]] +name = "tensorboard-plugin-wit" +version = "1.8.1" +description = "What-If Tool TensorBoard plugin." +optional = true +python-versions = "*" +files = [ + {file = "tensorboard_plugin_wit-1.8.1-py3-none-any.whl", hash = "sha256:ff26bdd583d155aa951ee3b152b3d0cffae8005dc697f72b44a8e8c2a77a8cbe"}, +] + +[[package]] +name = "tensorboardX" +version = "2.5.1" +description = "TensorBoardX lets you watch Tensors Flow without Tensorflow" +optional = false +python-versions = "*" +files = [ + {file = "tensorboardX-2.5.1-py2.py3-none-any.whl", hash = "sha256:8808133ccca673cd04076f6f2a85cf2d39bb2d0393a0f20d0f9cbb06d472b57e"}, + {file = "tensorboardX-2.5.1.tar.gz", hash = "sha256:ea85a3446f22ce8a917fe4fa4d8a7a96222ef84ac835267d038c34bb99f6d61b"}, +] + +[package.dependencies] +numpy = "*" +protobuf = ">=3.8.0,<=3.20.1" + +[[package]] +name = "tensorflow-cpu" +version = "2.11.0" +description = "TensorFlow is an open source machine learning framework for everyone." +optional = true +python-versions = ">=3.7" +files = [ + {file = "tensorflow_cpu-2.11.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:91bac68200ddbdff757c9d3aec8a03ad12b5fef21b937ff287721076e43b58b4"}, + {file = "tensorflow_cpu-2.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b318429219392b2e73f72099db5b92cfd516171c1e10e4ef37b0f53166f627da"}, + {file = "tensorflow_cpu-2.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c9bbd54abc00858bd4722ddaa6ba6469f9730d626786b7bd19a544defb61f11"}, + {file = "tensorflow_cpu-2.11.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:6bb3f3a8b6a96025fdffde2526ca2c58bb36410a74163a498ca9b2d68d3ccfcf"}, + {file = "tensorflow_cpu-2.11.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdcc9f733285bb1c917cde6731edcbf2ecc5ca4bd8c6a4c168a7f478e4056654"}, + {file = "tensorflow_cpu-2.11.0-cp37-cp37m-win_amd64.whl", hash = "sha256:57aee7f2f3eed2f6e26bc3695c967fa889c98cefb4b8bfb2f47e171d96c13a0a"}, + {file = "tensorflow_cpu-2.11.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:c302c1b9728b4ce32eca8041e1375d51896832d84c84ce8eeb2577b73ffb0392"}, + {file = "tensorflow_cpu-2.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a125157fdb2b1191ca6321e78127f032ce06ae17349e9affd75595782cca4cf"}, + {file = "tensorflow_cpu-2.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:08cc63ea4728ac0246063cef4f79911367c194515a45cc247ac05eb6684cd4aa"}, + {file = "tensorflow_cpu-2.11.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:d47df7bf4e684639d3d83cc27d150c6d29b8bd5f0586ca0a9a040af6840a92b0"}, + {file = "tensorflow_cpu-2.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1954bccbd78681c3df0d4ac9f020a0ee44b17bd6b5962ebb8848479879f45bc7"}, + {file = "tensorflow_cpu-2.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:d5e3c0666abdc0d9c63790238a1b91a41f2e622b488df7276750f61351b12ccc"}, +] + +[package.dependencies] +absl-py = ">=1.0.0" +astunparse = ">=1.6.0" +flatbuffers = ">=2.0" +gast = ">=0.2.1,<=0.4.0" +google-pasta = ">=0.1.1" +grpcio = ">=1.24.3,<2.0" +h5py = ">=2.9.0" +keras = ">=2.11.0,<2.12" +libclang = ">=13.0.0" +numpy = ">=1.20" +opt-einsum = ">=2.3.2" +packaging = "*" +protobuf = ">=3.9.2,<3.20" +setuptools = "*" +six = ">=1.12.0" +tensorboard = ">=2.11,<2.12" +tensorflow-estimator = ">=2.11.0,<2.12" +tensorflow-io-gcs-filesystem = {version = ">=0.23.1", markers = "platform_machine != \"arm64\" or platform_system != \"Darwin\""} +termcolor = ">=1.1.0" +typing-extensions = ">=3.6.6" +wrapt = ">=1.11.0" + +[[package]] +name = "tensorflow-datasets" +version = "4.7.0" +description = "tensorflow/datasets is a library of datasets ready to use with TensorFlow." +optional = true +python-versions = ">=3.7" +files = [ + {file = "tensorflow-datasets-4.7.0.tar.gz", hash = "sha256:590faf3763bc14757906b36c718389eeded533ee8ae4d030ede140db86aca4cd"}, + {file = "tensorflow_datasets-4.7.0-py3-none-any.whl", hash = "sha256:5761da58b35a73746f1e522c7184a41ab0acacb8928a9dde6b2f54f77ad45819"}, +] + +[package.dependencies] +absl-py = "*" +dill = "*" +etils = {version = "*", extras = ["epath"]} +importlib-resources = {version = "*", markers = "python_version < \"3.9\""} +numpy = "*" +promise = "*" +protobuf = ">=3.12.2" +requests = ">=2.19.0" +six = "*" +tensorflow-metadata = "*" +termcolor = "*" +toml = "*" +tqdm = "*" + +[package.extras] +aflw2k3d = ["scipy"] +beir = ["apache-beam"] +ble-wind-field = ["gcsfs", "zarr"] +c4 = ["apache-beam", "gcld3", "langdetect", "nltk", "tldextract"] +cats-vs-dogs = ["matplotlib"] +colorectal-histology = ["Pillow"] +common-voice = ["pydub"] +dev = ["apache-beam", "conllu", "jax[cpu]", "jupyter", "pandas", "pydub", "pylint (>=2.6.0)", "pytest", "pytest-shard", "pytest-xdist", "pyyaml", "yapf"] +duke-ultrasound = ["scipy"] +eurosat = ["imagecodecs", "scikit-image", "tifffile"] +groove = ["pretty-midi", "pydub"] +gtzan = ["pydub"] +imagenet2012-corrupted = ["opencv-python", "scikit-image", "scipy"] +librispeech = ["pydub"] +lsun = ["tensorflow-io"] +matplotlib = ["matplotlib"] +nsynth = ["crepe (>=0.0.11)", "librosa", "scikit-learn (==0.20.3)"] +ogbg-molpcba = ["networkx", "pandas"] +pet-finder = ["pandas"] +robonet = ["h5py"] +robosuite-panda-pick-place-can = ["envlogger"] +smartwatch-gestures = ["pandas"] +svhn = ["scipy"] +tensorflow = ["tensorflow (>=2.1)"] +tensorflow-data-validation = ["tensorflow-data-validation"] +tests-all = ["Pillow", "apache-beam", "bs4", "conllu", "envlogger", "gcld3", "gcsfs", "h5py", "imagecodecs", "jax[cpu]", "jupyter", "langdetect", "lxml", "matplotlib", "mwparserfromhell", "networkx", "nltk", "opencv-python", "pandas", "pretty-midi", "pycocotools", "pydub", "pytest", "pytest-shard", "pytest-xdist", "pyyaml", "scikit-image", "scipy", "tifffile", "tldextract", "zarr"] +the300w-lp = ["scipy"] +wider-face = ["Pillow"] +wiki-dialog = ["apache-beam"] +wikipedia = ["apache-beam", "mwparserfromhell"] +wsc273 = ["bs4", "lxml"] +youtube-vis = ["pycocotools"] + +[[package]] +name = "tensorflow-estimator" +version = "2.11.0" +description = "TensorFlow Estimator." +optional = true +python-versions = ">=3.7" +files = [ + {file = "tensorflow_estimator-2.11.0-py2.py3-none-any.whl", hash = "sha256:ea3b64acfff3d9a244f06178c9bdedcbdd3f125b67d0888dba8229498d06468b"}, +] + +[[package]] +name = "tensorflow-io-gcs-filesystem" +version = "0.28.0" +description = "TensorFlow IO" +optional = true +python-versions = ">=3.7, <3.11" +files = [ + {file = "tensorflow_io_gcs_filesystem-0.28.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:22753dc28c949bfaf29b573ee376370762c88d80330fe95cfb291261eb5e927a"}, + {file = "tensorflow_io_gcs_filesystem-0.28.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:52988659f405166df79905e9859bc84ae2a71e3ff61522ba32a95e4dce8e66d2"}, + {file = "tensorflow_io_gcs_filesystem-0.28.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cf11d36d5aac6365f9f6c4bc71eb23346a2f5dc139c91af1e2ea06cfd7f69a7"}, + {file = "tensorflow_io_gcs_filesystem-0.28.0-cp310-cp310-win_amd64.whl", hash = "sha256:698d7f89e09812b9afeb47c3860797343a22f997c64ab9dab98132c61daa8a7d"}, + {file = "tensorflow_io_gcs_filesystem-0.28.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:bbf245883aa52ec687b66d0fcbe0f5f0a92d98c0b1c53e6a736039a3548d29a1"}, + {file = "tensorflow_io_gcs_filesystem-0.28.0-cp311-cp311-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6d95f306ff225c5053fd06deeab3e3a2716357923cb40c44d566c11be779caa3"}, + {file = "tensorflow_io_gcs_filesystem-0.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:5fbef5836e70026245d8d9e692c44dae2c6dbc208c743d01f5b7a2978d6b6bc6"}, + {file = "tensorflow_io_gcs_filesystem-0.28.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:00cf6a92f1f9f90b2ba2d728870bcd2a70b116316d0817ab0b91dd390c25b3fd"}, + {file = "tensorflow_io_gcs_filesystem-0.28.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f76cbe1a784841c223f6861e5f6c7e53aa6232cb626d57e76881a0638c365de6"}, + {file = "tensorflow_io_gcs_filesystem-0.28.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8f4b1b83ee28906881221c0a40822cbf30ebfcc3e96430f91f396309e6b452c"}, + {file = "tensorflow_io_gcs_filesystem-0.28.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c5d99f56c12a349905ff684142e4d2df06ae68ecf50c4aad5449a5f81731d858"}, + {file = "tensorflow_io_gcs_filesystem-0.28.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:b6e2d275020fb4d1a952cd3fa546483f4e46ad91d64e90d3458e5ca3d12f6477"}, + {file = "tensorflow_io_gcs_filesystem-0.28.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a6670e0da16c884267e896ea5c3334d6fd319bd6ff7cf917043a9f3b2babb1b3"}, + {file = "tensorflow_io_gcs_filesystem-0.28.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ebb4724594bb897f74c637c256cb9509b25fc6b83860a2a6e2a6d42fd1a1a2a"}, + {file = "tensorflow_io_gcs_filesystem-0.28.0-cp38-cp38-win_amd64.whl", hash = "sha256:bfed720fc691d3f45802a7bed420716805aef0939c11cebf25798906201f626e"}, + {file = "tensorflow_io_gcs_filesystem-0.28.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:cc062ce13ec95fb64b1fd426818a6d2b0e5be9692bc0e43a19cce115b6da4336"}, + {file = "tensorflow_io_gcs_filesystem-0.28.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:366e1eff8dbd6b64333d7061e2a8efd081ae4742614f717ced08d8cc9379eb50"}, + {file = "tensorflow_io_gcs_filesystem-0.28.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47944fba38f46c9137b7bc27fb293953249d437f225bf46b96bac6a177bdfe04"}, + {file = "tensorflow_io_gcs_filesystem-0.28.0-cp39-cp39-win_amd64.whl", hash = "sha256:9484893779324b2d34874b0aacf3b824eb4f22d782e75df029cbccab2e607974"}, +] + +[package.extras] +tensorflow = ["tensorflow (>=2.11.0,<2.12.0)"] +tensorflow-aarch64 = ["tensorflow-aarch64 (>=2.11.0,<2.12.0)"] +tensorflow-cpu = ["tensorflow-cpu (>=2.11.0,<2.12.0)"] +tensorflow-gpu = ["tensorflow-gpu (>=2.11.0,<2.12.0)"] +tensorflow-rocm = ["tensorflow-rocm (>=2.11.0,<2.12.0)"] + +[[package]] +name = "tensorflow-metadata" +version = "1.12.0" +description = "Library and standards for schema and statistics." +optional = true +python-versions = ">=3.7,<4" +files = [ + {file = "tensorflow_metadata-1.12.0-py3-none-any.whl", hash = "sha256:4b86df305e5c8252df4c99a25074958f3772db57276fa6bd6a4d14fe482d8bc9"}, +] + +[package.dependencies] +absl-py = ">=0.9,<2.0.0" +googleapis-common-protos = ">=1.52.0,<2" +protobuf = ">=3.13,<4" + +[[package]] +name = "termcolor" +version = "2.1.1" +description = "ANSI color formatting for output in terminal" +optional = true +python-versions = ">=3.7" +files = [ + {file = "termcolor-2.1.1-py3-none-any.whl", hash = "sha256:fa852e957f97252205e105dd55bbc23b419a70fec0085708fc0515e399f304fd"}, + {file = "termcolor-2.1.1.tar.gz", hash = "sha256:67cee2009adc6449c650f6bcf3bdeed00c8ba53a8cda5362733c53e0a39fb70b"}, +] + +[package.extras] +tests = ["pytest", "pytest-cov"] + +[[package]] +name = "terminado" +version = "0.17.1" +description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." +optional = true +python-versions = ">=3.7" +files = [ + {file = "terminado-0.17.1-py3-none-any.whl", hash = "sha256:8650d44334eba354dd591129ca3124a6ba42c3d5b70df5051b6921d506fdaeae"}, + {file = "terminado-0.17.1.tar.gz", hash = "sha256:6ccbbcd3a4f8a25a5ec04991f39a0b8db52dfcd487ea0e578d977e6752380333"}, +] + +[package.dependencies] +ptyprocess = {version = "*", markers = "os_name != \"nt\""} +pywinpty = {version = ">=1.1.0", markers = "os_name == \"nt\""} +tornado = ">=6.1.0" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["pre-commit", "pytest (>=7.0)", "pytest-timeout"] + +[[package]] +name = "threadpoolctl" +version = "3.2.0" +description = "threadpoolctl" +optional = true +python-versions = ">=3.8" +files = [ + {file = "threadpoolctl-3.2.0-py3-none-any.whl", hash = "sha256:2b7818516e423bdaebb97c723f86a7c6b0a83d3f3b0970328d66f4d9104dc032"}, + {file = "threadpoolctl-3.2.0.tar.gz", hash = "sha256:c96a0ba3bdddeaca37dc4cc7344aafad41cdb8c313f74fdfe387a867bba93355"}, +] + +[[package]] +name = "timm" +version = "0.9.7" +description = "PyTorch Image Models" +optional = false +python-versions = ">=3.7" +files = [ + {file = "timm-0.9.7-py3-none-any.whl", hash = "sha256:1cf1082007aa1353550921804abe292292d51acc8631a140273f81f29b48059f"}, + {file = "timm-0.9.7.tar.gz", hash = "sha256:2bfb1029e90b72e65eb9c75556169815f2e82257eaa1f6ebd623a4b4a52867a2"}, +] + +[package.dependencies] +huggingface-hub = "*" +pyyaml = "*" +safetensors = "*" +torch = ">=1.7" +torchvision = "*" + +[[package]] +name = "tinycss2" +version = "1.2.1" +description = "A tiny CSS parser" +optional = true +python-versions = ">=3.7" +files = [ + {file = "tinycss2-1.2.1-py3-none-any.whl", hash = "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847"}, + {file = "tinycss2-1.2.1.tar.gz", hash = "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627"}, +] + +[package.dependencies] +webencodings = ">=0.4" + +[package.extras] +doc = ["sphinx", "sphinx_rtd_theme"] +test = ["flake8", "isort", "pytest"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "torch" +version = "1.13.0" +description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "torch-1.13.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:f68edfea71ade3862039ba66bcedf954190a2db03b0c41a9b79afd72210abd97"}, + {file = "torch-1.13.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:d2d2753519415d154de4d3e64d2eaaeefdba6b6fd7d69d5ffaef595988117700"}, + {file = "torch-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c227c16626e4ce766cca5351cc62a2358a11e8e466410a298487b9dff159eb1"}, + {file = "torch-1.13.0-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:49a949b8136b32b2ec0724cbf4c6678b54e974b7d68f19f1231eea21cde5c23b"}, + {file = "torch-1.13.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:0fdd38c96230947b1ed870fed4a560252f8d23c3a2bf4dab9d2d42b18f2e67c8"}, + {file = "torch-1.13.0-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:43db0723fc66ad6486f86dc4890c497937f7cd27429f28f73fb7e4d74b7482e2"}, + {file = "torch-1.13.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e643ac8d086706e82f77b5d4dfcf145a9dd37b69e03e64177fc23821754d2ed7"}, + {file = "torch-1.13.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:bb33a911460475d1594a8c8cb73f58c08293211760796d99cae8c2509b86d7f1"}, + {file = "torch-1.13.0-cp37-cp37m-win_amd64.whl", hash = "sha256:220325d0f4e69ee9edf00c04208244ef7cf22ebce083815ce272c7491f0603f5"}, + {file = "torch-1.13.0-cp37-none-macosx_10_9_x86_64.whl", hash = "sha256:cd1e67db6575e1b173a626077a54e4911133178557aac50683db03a34e2b636a"}, + {file = "torch-1.13.0-cp37-none-macosx_11_0_arm64.whl", hash = "sha256:9197ec216833b836b67e4d68e513d31fb38d9789d7cd998a08fba5b499c38454"}, + {file = "torch-1.13.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fa768432ce4b8ffa29184c79a3376ab3de4a57b302cdf3c026a6be4c5a8ab75b"}, + {file = "torch-1.13.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:635dbb99d981a6483ca533b3dc7be18ef08dd9e1e96fb0bb0e6a99d79e85a130"}, + {file = "torch-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:857c7d5b1624c5fd979f66d2b074765733dba3f5e1cc97b7d6909155a2aae3ce"}, + {file = "torch-1.13.0-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:ef934a21da6f6a516d0a9c712a80d09c56128abdc6af8dc151bee5199b4c3b4e"}, + {file = "torch-1.13.0-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:f01a9ae0d4b69d2fc4145e8beab45b7877342dddbd4838a7d3c11ca7f6680745"}, + {file = "torch-1.13.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:9ac382cedaf2f70afea41380ad8e7c06acef6b5b7e2aef3971cdad666ca6e185"}, + {file = "torch-1.13.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:e20df14d874b024851c58e8bb3846249cb120e677f7463f60c986e3661f88680"}, + {file = "torch-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:4a378f5091307381abfb30eb821174e12986f39b1cf7c4522bf99155256819eb"}, + {file = "torch-1.13.0-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:922a4910613b310fbeb87707f00cb76fec328eb60cc1349ed2173e7c9b6edcd8"}, + {file = "torch-1.13.0-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:47fe6228386bff6d74319a2ffe9d4ed943e6e85473d78e80502518c607d644d2"}, +] + +[package.dependencies] +nvidia-cublas-cu11 = "11.10.3.66" +nvidia-cuda-nvrtc-cu11 = "11.7.99" +nvidia-cuda-runtime-cu11 = "11.7.99" +nvidia-cudnn-cu11 = "8.5.0.96" +typing-extensions = "*" + +[package.extras] +opt-einsum = ["opt-einsum (>=3.3)"] + +[[package]] +name = "torchmetrics" +version = "1.2.0" +description = "PyTorch native Metrics" +optional = false +python-versions = ">=3.8" +files = [ + {file = "torchmetrics-1.2.0-py3-none-any.whl", hash = "sha256:da2cb18822b285786d082c40efb9e1d861aac425f58230234fe6ce233cf002f8"}, + {file = "torchmetrics-1.2.0.tar.gz", hash = "sha256:7eb28340bde45e13187a9ad54a4a7010a50417815d8181a5df6131f116ffe1b7"}, +] + +[package.dependencies] +lightning-utilities = ">=0.8.0" +numpy = ">1.20.0" +torch = ">=1.8.1" +typing-extensions = {version = "*", markers = "python_version < \"3.9\""} + +[package.extras] +all = ["SciencePlots (>=2.0.0)", "lpips (<=0.1.4)", "matplotlib (>=3.2.0)", "mypy (==1.5.1)", "nltk (>=3.6)", "piq (<=0.8.0)", "pycocotools (>2.0.0)", "pystoi (>=0.3.0)", "regex (>=2021.9.24)", "scipy (>1.0.0)", "torch-fidelity (<=0.4.0)", "torchaudio (>=0.10.0)", "torchvision (>=0.8)", "tqdm (>=4.41.0)", "transformers (>4.4.0)", "transformers (>=4.10.0)", "types-PyYAML", "types-emoji", "types-protobuf", "types-requests", "types-setuptools", "types-six", "types-tabulate"] +audio = ["pystoi (>=0.3.0)", "torchaudio (>=0.10.0)"] +detection = ["pycocotools (>2.0.0)", "torchvision (>=0.8)"] +dev = ["SciencePlots (>=2.0.0)", "bert-score (==0.3.13)", "cloudpickle (>1.3)", "coverage (==7.3.1)", "dython (<=0.7.4)", "fairlearn", "fast-bss-eval (>=0.1.0)", "faster-coco-eval (>=1.3.3)", "fire (<=0.5.0)", "huggingface-hub (<0.18)", "jiwer (>=2.3.0)", "kornia (>=0.6.7)", "lpips (<=0.1.4)", "matplotlib (>=3.2.0)", "mir-eval (>=0.6)", "mypy (==1.5.1)", "netcal (>1.0.0)", "nltk (>=3.6)", "numpy (<1.25.0)", "pandas (>1.0.0)", "pandas (>=1.4.0)", "phmdoctest (==1.4.0)", "piq (<=0.8.0)", "psutil (<=5.9.5)", "pycocotools (>2.0.0)", "pystoi (>=0.3.0)", "pytest (==7.4.2)", "pytest-cov (==4.1.0)", "pytest-doctestplus (==1.0.0)", "pytest-rerunfailures (==12.0)", "pytest-timeout (==2.1.0)", "pytorch-msssim (==1.0.0)", "regex (>=2021.9.24)", "requests (<=2.31.0)", "rouge-score (>0.1.0)", "sacrebleu (>=2.0.0)", "scikit-image (>=0.19.0)", "scikit-learn (>=1.1.1)", "scipy (>1.0.0)", "sewar (>=0.4.4)", "statsmodels (>0.13.5)", "torch-complex (<=0.4.3)", "torch-fidelity (<=0.4.0)", "torchaudio (>=0.10.0)", "torchvision (>=0.8)", "tqdm (>=4.41.0)", "transformers (>4.4.0)", "transformers (>=4.10.0)", "types-PyYAML", "types-emoji", "types-protobuf", "types-requests", "types-setuptools", "types-six", "types-tabulate"] +image = ["lpips (<=0.1.4)", "scipy (>1.0.0)", "torch-fidelity (<=0.4.0)", "torchvision (>=0.8)"] +multimodal = ["piq (<=0.8.0)", "transformers (>=4.10.0)"] +test = ["bert-score (==0.3.13)", "cloudpickle (>1.3)", "coverage (==7.3.1)", "dython (<=0.7.4)", "fairlearn", "fast-bss-eval (>=0.1.0)", "faster-coco-eval (>=1.3.3)", "fire (<=0.5.0)", "huggingface-hub (<0.18)", "jiwer (>=2.3.0)", "kornia (>=0.6.7)", "mir-eval (>=0.6)", "netcal (>1.0.0)", "numpy (<1.25.0)", "pandas (>1.0.0)", "pandas (>=1.4.0)", "phmdoctest (==1.4.0)", "psutil (<=5.9.5)", "pytest (==7.4.2)", "pytest-cov (==4.1.0)", "pytest-doctestplus (==1.0.0)", "pytest-rerunfailures (==12.0)", "pytest-timeout (==2.1.0)", "pytorch-msssim (==1.0.0)", "requests (<=2.31.0)", "rouge-score (>0.1.0)", "sacrebleu (>=2.0.0)", "scikit-image (>=0.19.0)", "scikit-learn (>=1.1.1)", "scipy (>1.0.0)", "sewar (>=0.4.4)", "statsmodels (>0.13.5)", "torch-complex (<=0.4.3)"] +text = ["nltk (>=3.6)", "regex (>=2021.9.24)", "tqdm (>=4.41.0)", "transformers (>4.4.0)"] +typing = ["mypy (==1.5.1)", "types-PyYAML", "types-emoji", "types-protobuf", "types-requests", "types-setuptools", "types-six", "types-tabulate"] +visual = ["SciencePlots (>=2.0.0)", "matplotlib (>=3.2.0)"] + +[[package]] +name = "torchvision" +version = "0.14.0" +description = "image and video datasets and models for torch deep learning" +optional = false +python-versions = ">=3.7" +files = [ + {file = "torchvision-0.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7b6e1706760eace0257ebb0677404cdd64f4cf88804bc6379f694cf3ed470591"}, + {file = "torchvision-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1db57014a6946e8633e1f2863aaea47d5b3e7424f94136ab5d50861a6db35698"}, + {file = "torchvision-0.14.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:7dbb54b47edfc08f51df3fb8c66a917b07897d9e3f2fa739aa9ccb36a38fe1bc"}, + {file = "torchvision-0.14.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:4a4516afcf8d2fc6575f8358638b3972e2b655eec59988abcfbbd1d8410b74d5"}, + {file = "torchvision-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:e8c5c26d92e7a5527c5ed31827cf6fff75aa3011cf9f818a5b0f7283fa68162c"}, + {file = "torchvision-0.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f6b41df5e4daf6ee21b61ae5a77abcce7bf7d0f7596c920ba4919fe7b7727f20"}, + {file = "torchvision-0.14.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3b283a3811c4d777295583426ceb29320c2bd2d6a38e68c5f7ff3220c0560c7c"}, + {file = "torchvision-0.14.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:9572ae1680664850df0146fe3975b17902ac429d817651f030d2bacf3f467bdd"}, + {file = "torchvision-0.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:cbf76d89624efa7d96bd6425d24dcce605976326dc2837c2f0529b49bf206bf2"}, + {file = "torchvision-0.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0123d0280c547aa9766954928b1ab9af2125a8861f53af23c07e56aeef0f2520"}, + {file = "torchvision-0.14.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:68110418c833a10153e382b0394598dd169ab823a2168139c7b4f62ea48a4446"}, + {file = "torchvision-0.14.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:897832c3be99d74afeb2705a2eb8a8147c4b0f4ab6a19ed304743b85bd5e7008"}, + {file = "torchvision-0.14.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8857eb11aee31f106b0fa35cfbe0a82a5af0c267fdd74b9d1a46bc1ff71ccdaf"}, + {file = "torchvision-0.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:281071e140bcfe0b564935ed26198001dab86b72b3eaea32939a1b63af181be1"}, + {file = "torchvision-0.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a6aa72804cff9550cbb890f98c7e9ff26acdfc48064d1129faf1bbfeaf60a0a"}, + {file = "torchvision-0.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c477dbb8ff6d0e1c7847aa94b55347076c664863aed69e8b79335cb12674c1b"}, + {file = "torchvision-0.14.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:0758632e15bd24c88daaf5fee66cf56d08318e7f561e416e26f5b90a1f61aa3a"}, + {file = "torchvision-0.14.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:e6b9f137c2d4954b1abce706ac1e56f98b73516e8fe35b62a8819bfc1226079b"}, + {file = "torchvision-0.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:6c0b8aa6f9a1c8372ee59fd31bf366745096b777cd75352a4c587781b08d72e5"}, +] + +[package.dependencies] +numpy = "*" +pillow = ">=5.3.0,<8.3.dev0 || >=8.4.dev0" +requests = "*" +torch = "1.13.0" +typing-extensions = "*" + +[package.extras] +scipy = ["scipy"] + +[[package]] +name = "tornado" +version = "6.2" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = true +python-versions = ">= 3.7" +files = [ + {file = "tornado-6.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72"}, + {file = "tornado-6.2-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9"}, + {file = "tornado-6.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac"}, + {file = "tornado-6.2-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75"}, + {file = "tornado-6.2-cp37-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e"}, + {file = "tornado-6.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8"}, + {file = "tornado-6.2-cp37-abi3-musllinux_1_1_i686.whl", hash = "sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b"}, + {file = "tornado-6.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca"}, + {file = "tornado-6.2-cp37-abi3-win32.whl", hash = "sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23"}, + {file = "tornado-6.2-cp37-abi3-win_amd64.whl", hash = "sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b"}, + {file = "tornado-6.2.tar.gz", hash = "sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13"}, +] + +[[package]] +name = "tqdm" +version = "4.64.1" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +files = [ + {file = "tqdm-4.64.1-py2.py3-none-any.whl", hash = "sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1"}, + {file = "tqdm-4.64.1.tar.gz", hash = "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["py-make (>=0.1.0)", "twine", "wheel"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "traitlets" +version = "5.7.1" +description = "Traitlets Python configuration system" +optional = true +python-versions = ">=3.7" +files = [ + {file = "traitlets-5.7.1-py3-none-any.whl", hash = "sha256:57ba2ba951632eeab9388fa45f342a5402060a5cc9f0bb942f760fafb6641581"}, + {file = "traitlets-5.7.1.tar.gz", hash = "sha256:fde8f62c05204ead43c2c1b9389cfc85befa7f54acb5da28529d671175bb4108"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +lint = ["black (>=22.6.0)", "mdformat (>0.7)", "ruff (>=0.0.156)"] +test = ["pre-commit", "pytest"] +typing = ["mypy (>=0.990)"] + +[[package]] +name = "typing-extensions" +version = "4.4.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, +] + +[[package]] +name = "tzdata" +version = "2023.3" +description = "Provider of IANA time zone data" +optional = true +python-versions = ">=2" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + +[[package]] +name = "uri-template" +version = "1.2.0" +description = "RFC 6570 URI Template Processor" +optional = true +python-versions = ">=3.6" +files = [ + {file = "uri_template-1.2.0-py3-none-any.whl", hash = "sha256:f1699c77b73b925cf4937eae31ab282a86dc885c333f2e942513f08f691fc7db"}, + {file = "uri_template-1.2.0.tar.gz", hash = "sha256:934e4d09d108b70eb8a24410af8615294d09d279ce0e7cbcdaef1bd21f932b06"}, +] + +[package.extras] +dev = ["flake8 (<4.0.0)", "flake8-annotations", "flake8-bugbear", "flake8-commas", "flake8-comprehensions", "flake8-continuation", "flake8-datetimez", "flake8-docstrings", "flake8-import-order", "flake8-literal", "flake8-noqa", "flake8-requirements", "flake8-type-annotations", "flake8-use-fstring", "mypy", "pep8-naming"] + +[[package]] +name = "urllib3" +version = "1.26.13" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, + {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "virtualenv" +version = "20.17.1" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.6" +files = [ + {file = "virtualenv-20.17.1-py3-none-any.whl", hash = "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4"}, + {file = "virtualenv-20.17.1.tar.gz", hash = "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"}, +] + +[package.dependencies] +distlib = ">=0.3.6,<1" +filelock = ">=3.4.1,<4" +platformdirs = ">=2.4,<3" + +[package.extras] +docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] +testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +optional = true +python-versions = "*" +files = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] + +[[package]] +name = "webcolors" +version = "1.12" +description = "A library for working with color names and color values formats defined by HTML and CSS." +optional = true +python-versions = ">=3.7" +files = [ + {file = "webcolors-1.12-py3-none-any.whl", hash = "sha256:d98743d81d498a2d3eaf165196e65481f0d2ea85281463d856b1e51b09f62dce"}, + {file = "webcolors-1.12.tar.gz", hash = "sha256:16d043d3a08fd6a1b1b7e3e9e62640d09790dce80d2bdd4792a175b35fe794a9"}, +] + +[[package]] +name = "webdataset" +version = "0.2.35" +description = "Record sequential storage for deep learning." +optional = false +python-versions = ">=3.6" +files = [ + {file = "webdataset-0.2.35-py3-none-any.whl", hash = "sha256:38a362f8c7a926b42c0321cdfcf1b602b56ab7af39a950759b39f2a82e62d9ff"}, + {file = "webdataset-0.2.35.tar.gz", hash = "sha256:c3753319b3cc78d757942b5cfd02b19d3077704cf80e48b08aef52ceed92c10c"}, +] + +[package.dependencies] +braceexpand = "*" +numpy = "*" +pyyaml = "*" + +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +optional = true +python-versions = "*" +files = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] + +[[package]] +name = "websocket-client" +version = "1.4.2" +description = "WebSocket client for Python with low level API options" +optional = true +python-versions = ">=3.7" +files = [ + {file = "websocket-client-1.4.2.tar.gz", hash = "sha256:d6e8f90ca8e2dd4e8027c4561adeb9456b54044312dba655e7cae652ceb9ae59"}, + {file = "websocket_client-1.4.2-py3-none-any.whl", hash = "sha256:d6b06432f184438d99ac1f456eaf22fe1ade524c3dd16e661142dc54e9cba574"}, +] + +[package.extras] +docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + +[[package]] +name = "Werkzeug" +version = "2.2.2" +description = "The comprehensive WSGI web application library." +optional = true +python-versions = ">=3.7" +files = [ + {file = "Werkzeug-2.2.2-py3-none-any.whl", hash = "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"}, + {file = "Werkzeug-2.2.2.tar.gz", hash = "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog"] + +[[package]] +name = "wheel" +version = "0.38.4" +description = "A built-package format for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "wheel-0.38.4-py3-none-any.whl", hash = "sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8"}, + {file = "wheel-0.38.4.tar.gz", hash = "sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac"}, +] + +[package.extras] +test = ["pytest (>=3.0.0)"] + +[[package]] +name = "wrapt" +version = "1.14.1" +description = "Module for decorators, wrappers and monkey patching." +optional = true +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ + {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, + {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, + {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, + {file = "wrapt-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ecee4132c6cd2ce5308e21672015ddfed1ff975ad0ac8d27168ea82e71413f55"}, + {file = "wrapt-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2020f391008ef874c6d9e208b24f28e31bcb85ccff4f335f15a3251d222b92d9"}, + {file = "wrapt-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2feecf86e1f7a86517cab34ae6c2f081fd2d0dac860cb0c0ded96d799d20b335"}, + {file = "wrapt-1.14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:240b1686f38ae665d1b15475966fe0472f78e71b1b4903c143a842659c8e4cb9"}, + {file = "wrapt-1.14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9008dad07d71f68487c91e96579c8567c98ca4c3881b9b113bc7b33e9fd78b8"}, + {file = "wrapt-1.14.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6447e9f3ba72f8e2b985a1da758767698efa72723d5b59accefd716e9e8272bf"}, + {file = "wrapt-1.14.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:acae32e13a4153809db37405f5eba5bac5fbe2e2ba61ab227926a22901051c0a"}, + {file = "wrapt-1.14.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49ef582b7a1152ae2766557f0550a9fcbf7bbd76f43fbdc94dd3bf07cc7168be"}, + {file = "wrapt-1.14.1-cp311-cp311-win32.whl", hash = "sha256:358fe87cc899c6bb0ddc185bf3dbfa4ba646f05b1b0b9b5a27c2cb92c2cea204"}, + {file = "wrapt-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:26046cd03936ae745a502abf44dac702a5e6880b2b01c29aea8ddf3353b68224"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, + {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, + {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, + {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, + {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, + {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, + {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, + {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, + {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, + {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, + {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, + {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, + {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, + {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, +] + +[[package]] +name = "yarl" +version = "1.8.2" +description = "Yet another URL library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "yarl-1.8.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bb81f753c815f6b8e2ddd2eef3c855cf7da193b82396ac013c661aaa6cc6b0a5"}, + {file = "yarl-1.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:47d49ac96156f0928f002e2424299b2c91d9db73e08c4cd6742923a086f1c863"}, + {file = "yarl-1.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3fc056e35fa6fba63248d93ff6e672c096f95f7836938241ebc8260e062832fe"}, + {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58a3c13d1c3005dbbac5c9f0d3210b60220a65a999b1833aa46bd6677c69b08e"}, + {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10b08293cda921157f1e7c2790999d903b3fd28cd5c208cf8826b3b508026996"}, + {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de986979bbd87272fe557e0a8fcb66fd40ae2ddfe28a8b1ce4eae22681728fef"}, + {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c4fcfa71e2c6a3cb568cf81aadc12768b9995323186a10827beccf5fa23d4f8"}, + {file = "yarl-1.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae4d7ff1049f36accde9e1ef7301912a751e5bae0a9d142459646114c70ecba6"}, + {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bf071f797aec5b96abfc735ab97da9fd8f8768b43ce2abd85356a3127909d146"}, + {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:74dece2bfc60f0f70907c34b857ee98f2c6dd0f75185db133770cd67300d505f"}, + {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:df60a94d332158b444301c7f569659c926168e4d4aad2cfbf4bce0e8fb8be826"}, + {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:63243b21c6e28ec2375f932a10ce7eda65139b5b854c0f6b82ed945ba526bff3"}, + {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cfa2bbca929aa742b5084fd4663dd4b87c191c844326fcb21c3afd2d11497f80"}, + {file = "yarl-1.8.2-cp310-cp310-win32.whl", hash = "sha256:b05df9ea7496df11b710081bd90ecc3a3db6adb4fee36f6a411e7bc91a18aa42"}, + {file = "yarl-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:24ad1d10c9db1953291f56b5fe76203977f1ed05f82d09ec97acb623a7976574"}, + {file = "yarl-1.8.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2a1fca9588f360036242f379bfea2b8b44cae2721859b1c56d033adfd5893634"}, + {file = "yarl-1.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f37db05c6051eff17bc832914fe46869f8849de5b92dc4a3466cd63095d23dfd"}, + {file = "yarl-1.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77e913b846a6b9c5f767b14dc1e759e5aff05502fe73079f6f4176359d832581"}, + {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0978f29222e649c351b173da2b9b4665ad1feb8d1daa9d971eb90df08702668a"}, + {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:388a45dc77198b2460eac0aca1efd6a7c09e976ee768b0d5109173e521a19daf"}, + {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2305517e332a862ef75be8fad3606ea10108662bc6fe08509d5ca99503ac2aee"}, + {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42430ff511571940d51e75cf42f1e4dbdded477e71c1b7a17f4da76c1da8ea76"}, + {file = "yarl-1.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3150078118f62371375e1e69b13b48288e44f6691c1069340081c3fd12c94d5b"}, + {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c15163b6125db87c8f53c98baa5e785782078fbd2dbeaa04c6141935eb6dab7a"}, + {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4d04acba75c72e6eb90745447d69f84e6c9056390f7a9724605ca9c56b4afcc6"}, + {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e7fd20d6576c10306dea2d6a5765f46f0ac5d6f53436217913e952d19237efc4"}, + {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:75c16b2a900b3536dfc7014905a128a2bea8fb01f9ee26d2d7d8db0a08e7cb2c"}, + {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6d88056a04860a98341a0cf53e950e3ac9f4e51d1b6f61a53b0609df342cc8b2"}, + {file = "yarl-1.8.2-cp311-cp311-win32.whl", hash = "sha256:fb742dcdd5eec9f26b61224c23baea46c9055cf16f62475e11b9b15dfd5c117b"}, + {file = "yarl-1.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8c46d3d89902c393a1d1e243ac847e0442d0196bbd81aecc94fcebbc2fd5857c"}, + {file = "yarl-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ceff9722e0df2e0a9e8a79c610842004fa54e5b309fe6d218e47cd52f791d7ef"}, + {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f6b4aca43b602ba0f1459de647af954769919c4714706be36af670a5f44c9c1"}, + {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1684a9bd9077e922300ecd48003ddae7a7474e0412bea38d4631443a91d61077"}, + {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebb78745273e51b9832ef90c0898501006670d6e059f2cdb0e999494eb1450c2"}, + {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3adeef150d528ded2a8e734ebf9ae2e658f4c49bf413f5f157a470e17a4a2e89"}, + {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57a7c87927a468e5a1dc60c17caf9597161d66457a34273ab1760219953f7f4c"}, + {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:efff27bd8cbe1f9bd127e7894942ccc20c857aa8b5a0327874f30201e5ce83d0"}, + {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a783cd344113cb88c5ff7ca32f1f16532a6f2142185147822187913eb989f739"}, + {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:705227dccbe96ab02c7cb2c43e1228e2826e7ead880bb19ec94ef279e9555b5b"}, + {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:34c09b43bd538bf6c4b891ecce94b6fa4f1f10663a8d4ca589a079a5018f6ed7"}, + {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a48f4f7fea9a51098b02209d90297ac324241bf37ff6be6d2b0149ab2bd51b37"}, + {file = "yarl-1.8.2-cp37-cp37m-win32.whl", hash = "sha256:0414fd91ce0b763d4eadb4456795b307a71524dbacd015c657bb2a39db2eab89"}, + {file = "yarl-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:d881d152ae0007809c2c02e22aa534e702f12071e6b285e90945aa3c376463c5"}, + {file = "yarl-1.8.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5df5e3d04101c1e5c3b1d69710b0574171cc02fddc4b23d1b2813e75f35a30b1"}, + {file = "yarl-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7a66c506ec67eb3159eea5096acd05f5e788ceec7b96087d30c7d2865a243918"}, + {file = "yarl-1.8.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2b4fa2606adf392051d990c3b3877d768771adc3faf2e117b9de7eb977741229"}, + {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e21fb44e1eff06dd6ef971d4bdc611807d6bd3691223d9c01a18cec3677939e"}, + {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93202666046d9edadfe9f2e7bf5e0782ea0d497b6d63da322e541665d65a044e"}, + {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc77086ce244453e074e445104f0ecb27530d6fd3a46698e33f6c38951d5a0f1"}, + {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dd68a92cab699a233641f5929a40f02a4ede8c009068ca8aa1fe87b8c20ae3"}, + {file = "yarl-1.8.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b372aad2b5f81db66ee7ec085cbad72c4da660d994e8e590c997e9b01e44901"}, + {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e6f3515aafe0209dd17fb9bdd3b4e892963370b3de781f53e1746a521fb39fc0"}, + {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dfef7350ee369197106805e193d420b75467b6cceac646ea5ed3049fcc950a05"}, + {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:728be34f70a190566d20aa13dc1f01dc44b6aa74580e10a3fb159691bc76909d"}, + {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ff205b58dc2929191f68162633d5e10e8044398d7a45265f90a0f1d51f85f72c"}, + {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf211dcad448a87a0d9047dc8282d7de59473ade7d7fdf22150b1d23859f946"}, + {file = "yarl-1.8.2-cp38-cp38-win32.whl", hash = "sha256:272b4f1599f1b621bf2aabe4e5b54f39a933971f4e7c9aa311d6d7dc06965165"}, + {file = "yarl-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:326dd1d3caf910cd26a26ccbfb84c03b608ba32499b5d6eeb09252c920bcbe4f"}, + {file = "yarl-1.8.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f8ca8ad414c85bbc50f49c0a106f951613dfa5f948ab69c10ce9b128d368baf8"}, + {file = "yarl-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:418857f837347e8aaef682679f41e36c24250097f9e2f315d39bae3a99a34cbf"}, + {file = "yarl-1.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ae0eec05ab49e91a78700761777f284c2df119376e391db42c38ab46fd662b77"}, + {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:009a028127e0a1755c38b03244c0bea9d5565630db9c4cf9572496e947137a87"}, + {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3edac5d74bb3209c418805bda77f973117836e1de7c000e9755e572c1f7850d0"}, + {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da65c3f263729e47351261351b8679c6429151ef9649bba08ef2528ff2c423b2"}, + {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ef8fb25e52663a1c85d608f6dd72e19bd390e2ecaf29c17fb08f730226e3a08"}, + {file = "yarl-1.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcd7bb1e5c45274af9a1dd7494d3c52b2be5e6bd8d7e49c612705fd45420b12d"}, + {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44ceac0450e648de86da8e42674f9b7077d763ea80c8ceb9d1c3e41f0f0a9951"}, + {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:97209cc91189b48e7cfe777237c04af8e7cc51eb369004e061809bcdf4e55220"}, + {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:48dd18adcf98ea9cd721a25313aef49d70d413a999d7d89df44f469edfb38a06"}, + {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e59399dda559688461762800d7fb34d9e8a6a7444fd76ec33220a926c8be1516"}, + {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d617c241c8c3ad5c4e78a08429fa49e4b04bedfc507b34b4d8dceb83b4af3588"}, + {file = "yarl-1.8.2-cp39-cp39-win32.whl", hash = "sha256:cb6d48d80a41f68de41212f3dfd1a9d9898d7841c8f7ce6696cf2fd9cb57ef83"}, + {file = "yarl-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:6604711362f2dbf7160df21c416f81fac0de6dbcf0b5445a2ef25478ecc4c778"}, + {file = "yarl-1.8.2.tar.gz", hash = "sha256:49d43402c6e3013ad0978602bf6bf5328535c48d192304b91b97a3c6790b1562"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + +[[package]] +name = "zipp" +version = "3.11.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = true +python-versions = ">=3.7" +files = [ + {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, + {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + +[extras] +cluster = ["cluster"] +coco = ["pycocotools"] +notebook = ["matplotlib", "notebook"] +tensorflow = ["tensorflow-cpu", "tensorflow-datasets"] + +[metadata] +lock-version = "2.0" +python-versions = "~3.8" +content-hash = "59f2919cfccd774ca4a469ce6463da422ec09635e0b5980980754216ad617702" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..36d2292 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,73 @@ +[tool.poetry] +name = "videosaur" +version = "0.1.0" +description = "VideoSAUR: Object-Centric Learning for Real-World Videos by Predicting Temporal Feature Similarities" +authors = [ + "Maximilian Seitzer ", + "Andrii Zadaianchuk " +] +readme = "README.md" + +[tool.poetry.dependencies] +python = "~3.8" +numpy = "^1.23.3" +torch = "^1.13.0" +torchvision = "^0.14.0" +torchmetrics = "^1.2.0" +pytorch-lightning = "^1.8.3" +timm = "^0.9.7" +omegaconf = "^2.2.3" +webdataset = "^0.2.35" +einops = "^0.6.0" +scipy = "^1.10.1" +tensorflow-datasets = {version = "^4.7.0", optional=true} +tensorflow-cpu = {version = "^2.11.0", optional=true} +notebook = {version = "^6.5.2", optional=true} +matplotlib = {version = "^3.6.2", optional=true} +moviepy = "^1.0.3" +pycocotools = {version = "^2.0.6", optional=true} +requests = "^2.31.0" + +[tool.poetry.extras] +tensorflow = ["tensorflow-cpu", "tensorflow_datasets"] +coco = ["pycocotools"] +notebook = ["notebook", "matplotlib"] + +[tool.poetry.group.dev.dependencies] +black = "^22.10.0" +isort = "^5.12.0" +pytest = "^7.1.3" +pre-commit = "^2.20.0" +ruff = "^0.0.292" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.black] +line-length = 101 +target-version = ["py38"] + +[tool.isort] +profile = "black" +line_length = 101 +skip_gitignore = true + +[tool.ruff] +ignore = ["E731"] +line-length = 101 +select = [ + "B", + "E", + "F", + "TID", + "W", +] + +[tool.pytest.ini_options] +filterwarnings = "ignore::DeprecationWarning" +markers = [ + "slow: Marks slow tests", + "dataset: Marks tests with dataset dependencies" +] +addopts = " -m 'not slow'" # Do not run those tests by default diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/configs/test_coco.yml b/tests/configs/test_coco.yml new file mode 100644 index 0000000..9d772c3 --- /dev/null +++ b/tests/configs/test_coco.yml @@ -0,0 +1,101 @@ +experiment_group: test +experiment_name: test_coco + +globals: + INPUT_TYPE: image + NUM_SLOTS: 5 + SLOT_DIM: 64 + FEAT_DIM: 384 + NUM_PATCHES: 196 + +trainer: + max_steps: 100 + gradient_clip_val: 1.0 + +optimizer: + name: Adam + lr: 3e-4 + lr_scheduler: + name: exp_decay_with_warmup + warmup_steps: 50 + +model: + input_type: ${globals.INPUT_TYPE} + visualize: true + visualize_every_n_steps: 2 + + initializer: + name: RandomInit + n_slots: ${globals.NUM_SLOTS} + dim: ${globals.SLOT_DIM} + + encoder: + backbone: + name: TimmExtractor + model: vit_small_patch16_224 + features: vit_block12 + frozen: true + output_transform: + name: networks.two_layer_mlp + inp_dim: ${globals.FEAT_DIM} + outp_dim: ${globals.SLOT_DIM} + hidden_dim: "${mul: ${globals.FEAT_DIM}, 2}" + layer_norm: true + grouper: + name: SlotAttention + inp_dim: ${globals.SLOT_DIM} + slot_dim: ${globals.SLOT_DIM} + n_iters: 3 + use_mlp: true + + decoder: + name: MLPDecoder + inp_dim: ${globals.SLOT_DIM} + outp_dim: ${globals.FEAT_DIM} + hidden_dims: [512, 512, 512] + n_patches: ${globals.NUM_PATCHES} + +val_metrics: + image_ari: + name: ImageARI + ignore_overlaps: true + pred_key: decoder_masks_hard + true_key: segmentations + image_mbo: + name: ImageIoU + matching: overlap + ignore_overlaps: true + pred_key: decoder_masks_hard + true_key: segmentations + image_j_and_f: + name: ImageJandF + ignore_overlaps: true + pred_key: decoder_masks_hard + true_key: segmentations + +dataset: + train_shards: "coco/coco-train-{000000..000000}.tar" + val_shards: "coco/coco-validation-{000000..000000}.tar" + batch_size: 2 + val_size: 5000 + num_workers: 1 + train_pipeline: + name: ${globals.INPUT_TYPE} + keys: [image] + is_video_dataset: false + shuffle_size: 100 + transforms: + name: coco_train + type: ${globals.INPUT_TYPE} + crop_type: random + input_size: 224 + val_pipeline: + name: ${globals.INPUT_TYPE} + keys: [image, segmentations] + is_video_dataset: false + transforms: + name: coco_val + type: ${globals.INPUT_TYPE} + crop_type: central + input_size: 224 + num_classes: 62 diff --git a/tests/configs/test_davis_video.yml b/tests/configs/test_davis_video.yml new file mode 100644 index 0000000..0d90c54 --- /dev/null +++ b/tests/configs/test_davis_video.yml @@ -0,0 +1,108 @@ +experiment_group: test +experiment_name: test_davis_video + +globals: + NUM_SLOTS: 7 + SLOT_DIM: 64 + FEAT_DIM: 384 + NUM_PATCHES: 196 + +dataset: + train_shards: "davis/davis-train-{000000..000000}.tar" + val_shards: "davis/davis-validation-{000000..000000}.tar" + batch_size: 2 + val_batch_size: 1 # the length of videos is different, so batch_size is 1. + val_size: 10 + num_workers: 1 + train_pipeline: + chunk_size: 6 + keys: [video] + transforms: + name: davis_train + type: video + crop_type: random + input_size: 224 + num_classes: 10 + h_flip_prob: 0.5 + val_pipeline: + use_chunks: false + keys: [video, segmentations] + transforms: + name: davis_val + crop_type: central + type: video + input_size: 224 + num_classes: 10 + +trainer: + max_steps: 100 + +optimizer: + name: Adam + lr: 3e-4 + lr_scheduler: + name: exp_decay_with_warmup + warmup_steps: 50 + +model: + input_type: video + visualize: true + visualize_every_n_steps: 2 + + initializer: + name: RandomInit + n_slots: ${globals.NUM_SLOTS} + dim: ${globals.SLOT_DIM} + + encoder: + backbone: + name: TimmExtractor + model: vit_small_patch16_224 + features: vit_block12 + frozen: true + + grouper: + name: SlotAttention + inp_dim: ${globals.FEAT_DIM} + slot_dim: ${globals.SLOT_DIM} + n_iters: 1 + use_mlp: false + + decoder: + name: MLPDecoder + inp_dim: ${globals.SLOT_DIM} + outp_dim: ${globals.FEAT_DIM} + hidden_dims: [128, 128] + n_patches: ${globals.NUM_PATCHES} + + predictor: + name: networks.MLP + inp_dim: ${globals.SLOT_DIM} + outp_dim: ${globals.SLOT_DIM} + hidden_dims: [128, 128] + +val_metrics: + ari: + name: VideoARI + ignore_background: false + pred_key: decoder_masks_hard + true_key: segmentations + image_ari: + name: ImageARI + video_input: true + ignore_background: false + pred_key: decoder_masks_hard + true_key: segmentations + mbo: + name: VideoIoU + matching: overlap + ignore_background: false + pred_key: decoder_masks_hard + true_key: segmentations + image_mbo: + name: ImageIoU + matching: overlap + ignore_background: false + video_input: true + pred_key: decoder_masks_hard + true_key: segmentations diff --git a/tests/configs/test_dummy_image.yml b/tests/configs/test_dummy_image.yml new file mode 100644 index 0000000..896ff03 --- /dev/null +++ b/tests/configs/test_dummy_image.yml @@ -0,0 +1,69 @@ +experiment_group: test +experiment_name: test_dummy_image + +globals: + NUM_SLOTS: 7 + SLOT_DIM: 16 + FEAT_DIM: 384 + NUM_PATCHES: 196 + +dataset: + name: DummyDataModule + train_size: 20 + val_size: 5 + batch_size: 5 + train_transforms: + name: dummyimage_train + type: image + input_size: 224 + num_classes: 4 + val_transforms: + name: dummyimage_val + type: image + input_size: 224 + num_classes: 4 + shapes: + image: [224, 224, 3] + masks: [224, 224, 1] + +trainer: + max_steps: 100 + +optimizer: + name: Adam + lr: 3e-4 + +model: + input_type: image + visualize: false + + initializer: + name: RandomInit + n_slots: ${globals.NUM_SLOTS} + dim: ${globals.SLOT_DIM} + + encoder: + backbone: + name: TimmExtractor + model: vit_small_patch16_224 + features: vit_block12 + frozen: true + + grouper: + name: SlotAttention + inp_dim: ${globals.FEAT_DIM} + slot_dim: ${globals.SLOT_DIM} + + decoder: + name: MLPDecoder + inp_dim: ${globals.SLOT_DIM} + outp_dim: ${globals.FEAT_DIM} + hidden_dims: [128, 128] + n_patches: ${globals.NUM_PATCHES} + +val_metrics: + ari: + name: ImageARI + ignore_background: true + pred_key: decoder_masks_hard + true_key: masks diff --git a/tests/configs/test_dummy_video.yml b/tests/configs/test_dummy_video.yml new file mode 100644 index 0000000..f81f879 --- /dev/null +++ b/tests/configs/test_dummy_video.yml @@ -0,0 +1,75 @@ +experiment_group: test +experiment_name: test_dummy_video + +globals: + NUM_SLOTS: 7 + SLOT_DIM: 16 + FEAT_DIM: 384 + NUM_PATCHES: 196 + +dataset: + name: DummyDataModule + train_size: 20 + val_size: 5 + batch_size: 5 + train_transforms: + name: dummy_train + type: video + input_size: 224 + num_classes: 4 + val_transforms: + name: dummy_val + type: video + input_size: 224 + num_classes: 4 + shapes: + video: [3, 224, 224, 3] + masks: [3, 224, 224, 1] + +trainer: + max_steps: 100 + +optimizer: + name: Adam + lr: 3e-4 + +model: + input_type: video + + initializer: + name: RandomInit + n_slots: ${globals.NUM_SLOTS} + dim: ${globals.SLOT_DIM} + + encoder: + backbone: + name: TimmExtractor + model: vit_small_patch16_224 + features: vit_block12 + frozen: true + + grouper: + name: SlotAttention + inp_dim: ${globals.FEAT_DIM} + slot_dim: ${globals.SLOT_DIM} + n_iters: 1 + + decoder: + name: MLPDecoder + inp_dim: ${globals.SLOT_DIM} + outp_dim: ${globals.FEAT_DIM} + hidden_dims: [128, 128] + n_patches: ${globals.NUM_PATCHES} + + predictor: + name: networks.MLP + inp_dim: ${globals.SLOT_DIM} + outp_dim: ${globals.SLOT_DIM} + hidden_dims: [128, 128] + +val_metrics: + ari: + name: VideoARI + ignore_background: true + pred_key: decoder_masks_hard + true_key: masks diff --git a/tests/configs/test_movi_c_video.yml b/tests/configs/test_movi_c_video.yml new file mode 100644 index 0000000..94be573 --- /dev/null +++ b/tests/configs/test_movi_c_video.yml @@ -0,0 +1,128 @@ +experiment_group: test +experiment_name: test_movi_c_video + +globals: + NUM_SLOTS: 7 + SLOT_DIM: 16 + FEAT_DIM: 64 + FEAT_SIZE: 64 + TARGET_FEAT_DIM: 384 + NUM_PATCHES: 196 + +dataset: + train_shards: "movi_c/movi_c-train-{000000..000000}.tar" + val_shards: "movi_c/movi_c-validation-{000000..000000}.tar" + batch_size: 2 + val_size: 250 + num_workers: 1 + train_pipeline: + video_size: 24 + chunk_size: 6 + keys: [video] + duplicate: + video: target_video + transforms: + name: movi_train + input_size: 128 + target_size: 224 + num_classes: 11 + val_pipeline: + use_chunks: false + keys: [video, segmentations] + duplicate: + video: target_video + transforms: + name: movi_val + input_size: 128 + target_size: 224 + num_classes: 11 + +trainer: + max_steps: 100 + +optimizer: + name: Adam + lr: 3e-4 + lr_scheduler: + name: exp_decay_with_warmup + warmup_steps: 50 + +model: + input_type: video + target_encoder_input: target_video + visualize: true + visualize_every_n_steps: 2 + masks_to_visualize: [decoder, grouping] + + initializer: + name: RandomInit + n_slots: ${globals.NUM_SLOTS} + dim: ${globals.SLOT_DIM} + + encoder: + backbone: + name: networks.savi_cnn_encoder + pos_embed: + name: utils.CoordinatePositionEmbed + dim: ${globals.FEAT_DIM} + size: ${globals.FEAT_SIZE} + output_transform: + name: networks.two_layer_mlp + inp_dim: ${globals.FEAT_DIM} + outp_dim: ${globals.SLOT_DIM} + hidden_dim: ${globals.SLOT_DIM} + layer_norm: true + spatial_flatten: true + + target_encoder: + backbone: + name: TimmExtractor + model: vit_small_patch16_224 + features: vit_block12 + frozen: true + + grouper: + name: SlotAttention + inp_dim: ${globals.SLOT_DIM} + slot_dim: ${globals.SLOT_DIM} + n_iters: 1 + use_mlp: false + + decoder: + name: MLPDecoder + inp_dim: ${globals.SLOT_DIM} + outp_dim: ${globals.TARGET_FEAT_DIM} + hidden_dims: [128, 128] + n_patches: ${globals.NUM_PATCHES} + + predictor: + name: networks.MLP + inp_dim: ${globals.SLOT_DIM} + outp_dim: ${globals.SLOT_DIM} + hidden_dims: [128, 128] + +val_metrics: + ari: + name: VideoARI + ignore_background: true + pred_key: decoder_masks_hard + true_key: segmentations + image_ari: + name: ImageARI + ignore_background: true + video_input: true + pred_key: decoder_masks_hard + true_key: segmentations + mbo: + name: VideoIoU + matching: overlap + ignore_background: true + pred_key: decoder_masks_hard + true_key: segmentations + image_mbo: + name: ImageIoU + matching: overlap + ignore_background: true + video_input: true + pred_key: decoder_masks_hard + true_key: segmentations diff --git a/tests/configs/test_movi_e_image.yml b/tests/configs/test_movi_e_image.yml new file mode 100644 index 0000000..287f13a --- /dev/null +++ b/tests/configs/test_movi_e_image.yml @@ -0,0 +1,91 @@ +experiment_group: test +experiment_name: test_movi_e_image + +globals: + NUM_SLOTS: 7 + SLOT_DIM: 64 + FEAT_DIM: 384 + NUM_PATCHES: 196 + +dataset: + train_shards: "movi_e/movi_e-train-{000000..000000}.tar" + val_shards: "movi_e/movi_e-validation-{000000..000000}.tar" + batch_size: 2 + val_size: 250 + num_workers: 1 + train_pipeline: + name: image + video_size: 24 + keys: [video] + shuffle_size: 100 + transforms: + name: movi_train + type: image + input_size: 224 + num_classes: 24 + val_pipeline: + name: image + keys: [video, segmentations] + transforms: + name: movi_val + type: image + input_size: 224 + num_classes: 24 + +trainer: + max_steps: 100 + +optimizer: + name: Adam + lr: 3e-4 + lr_scheduler: + name: exp_decay_with_warmup + warmup_steps: 50 + +model: + input_type: image + visualize: true + visualize_every_n_steps: 2 + + initializer: + name: RandomInit + n_slots: ${globals.NUM_SLOTS} + dim: ${globals.SLOT_DIM} + + encoder: + backbone: + name: TimmExtractor + model: vit_small_patch16_224 + features: vit_block12 + frozen: true + + grouper: + name: SlotAttention + inp_dim: ${globals.FEAT_DIM} + slot_dim: ${globals.SLOT_DIM} + + decoder: + name: MLPDecoder + inp_dim: ${globals.SLOT_DIM} + outp_dim: ${globals.FEAT_DIM} + hidden_dims: [128, 128] + n_patches: ${globals.NUM_PATCHES} + + predictor: + name: networks.MLP + inp_dim: ${globals.SLOT_DIM} + outp_dim: ${globals.SLOT_DIM} + hidden_dims: [128, 128] + +val_metrics: + image_ari: + name: ImageARI + ignore_background: true + pred_key: decoder_masks_hard + true_key: segmentations + image_mbo: + name: ImageIoU + matching: overlap + ignore_background: true + pred_key: decoder_masks_hard + true_key: segmentations diff --git a/tests/configs/test_movi_e_video.yml b/tests/configs/test_movi_e_video.yml new file mode 100644 index 0000000..7623309 --- /dev/null +++ b/tests/configs/test_movi_e_video.yml @@ -0,0 +1,136 @@ +experiment_group: test +experiment_name: test_movi_e_video + +globals: + NUM_SLOTS: 7 + SLOT_DIM: 16 + FEAT_DIM: 384 + NUM_PATCHES: 196 + PATCH_SIZE: 16 + RGB_PATCH_DIM: "${mul: ${globals.PATCH_SIZE}, ${globals.PATCH_SIZE}, 3}" + TARGET_DIMS: "${add: ${globals.FEAT_DIM}, ${globals.RGB_PATCH_DIM}}" + +dataset: + train_shards: "movi_e/movi_e-train-{000000..000000}.tar" + val_shards: "movi_e/movi_e-validation-{000000..000000}.tar" + batch_size: 2 + val_size: 250 + num_workers: 1 + train_pipeline: + video_size: 24 + chunk_size: 6 + keys: [video] + transforms: + name: movi_train + type: video + input_size: 224 + num_classes: 24 + val_pipeline: + use_chunks: false + keys: [video, segmentations] + transforms: + name: movi_val + type: video + input_size: 224 + num_classes: 24 + +trainer: + max_steps: 100 + +optimizer: + name: Adam + lr: 3e-4 + lr_scheduler: + name: exp_decay_with_warmup + warmup_steps: 50 + +model: + input_type: video + visualize: true + visualize_every_n_steps: 2 + masks_to_visualize: [decoder, grouping] + + losses: + mse_image: + name: MSELoss + target_key: video + target_transform: + name: utils.Patchify + patch_size: ${globals.PATCH_SIZE} + video_inputs: true + pred_dims: + - 0 + - ${globals.RGB_PATCH_DIM} + mse_features: + name: MSELoss + pred_dims: + - ${globals.RGB_PATCH_DIM} + - "${add: ${globals.RGB_PATCH_DIM}, ${globals.FEAT_DIM}}" + + loss_weights: + mse_image: 1.0 + mse_features: 2.0 + + initializer: + name: RandomInit + n_slots: ${globals.NUM_SLOTS} + dim: ${globals.SLOT_DIM} + + encoder: + backbone: + name: TimmExtractor + model: vit_small_patch16_224 + features: vit_block12 + frozen: true + output_transform: + name: networks.two_layer_mlp + inp_dim: ${globals.FEAT_DIM} + outp_dim: ${globals.SLOT_DIM} + hidden_dim: ${globals.SLOT_DIM} + layer_norm: true + + grouper: + name: SlotAttention + inp_dim: ${globals.SLOT_DIM} + slot_dim: ${globals.SLOT_DIM} + n_iters: 1 + use_mlp: false + + decoder: + name: MLPDecoder + inp_dim: ${globals.SLOT_DIM} + outp_dim: ${globals.TARGET_DIMS} + hidden_dims: [128, 128] + n_patches: ${globals.NUM_PATCHES} + + predictor: + name: networks.MLP + inp_dim: ${globals.SLOT_DIM} + outp_dim: ${globals.SLOT_DIM} + hidden_dims: [128, 128] + +val_metrics: + ari: + name: VideoARI + ignore_background: true + pred_key: decoder_masks_hard + true_key: segmentations + image_ari: + name: ImageARI + ignore_background: true + video_input: true + pred_key: decoder_masks_hard + true_key: segmentations + mbo: + name: VideoIoU + matching: overlap + ignore_background: true + pred_key: decoder_masks_hard + true_key: segmentations + image_mbo: + name: ImageIoU + matching: overlap + ignore_background: true + video_input: true + pred_key: decoder_masks_hard + true_key: segmentations diff --git a/tests/configs/test_param_groups.yml b/tests/configs/test_param_groups.yml new file mode 100644 index 0000000..beed675 --- /dev/null +++ b/tests/configs/test_param_groups.yml @@ -0,0 +1,86 @@ +experiment_group: test +experiment_name: test_param_groups + +globals: + NUM_SLOTS: 7 + SLOT_DIM: 16 + FEAT_DIM: 384 + NUM_PATCHES: 196 + +dataset: + name: DummyDataModule + train_size: 20 + val_size: 5 + batch_size: 5 + train_transforms: + name: dummyimage_train + type: image + input_size: 224 + num_classes: 4 + val_transforms: + name: dummyimage_val + type: image + input_size: 224 + num_classes: 4 + shapes: + image: [224, 224, 3] + masks: [224, 224, 1] + +trainer: + max_steps: 100 + +optimizer: + name: Adam + lr: 3e-4 + betas: [0.9, 0.9] + param_groups: + - modules: encoder + lr: 1e-4 # Override global LR + betas: [0.5, 0.9] # Override global betas + lr_scheduler: + name: exp_decay_with_warmup + warmup_steps: 50 + - modules: initializer + lr: 1e-3 + lr_scheduler: + name: constant + - modules: [processor, decoder] + # Inherits global LR + lr_scheduler: + name: exp_decay_with_warmup + warmup_steps: 1000 + +model: + input_type: image + visualize: false + + initializer: + name: RandomInit + n_slots: ${globals.NUM_SLOTS} + dim: ${globals.SLOT_DIM} + + encoder: + backbone: + name: TimmExtractor + model: vit_small_patch16_224 + features: vit_block12 + frozen: true + + grouper: + name: SlotAttention + inp_dim: ${globals.FEAT_DIM} + slot_dim: ${globals.SLOT_DIM} + + decoder: + name: MLPDecoder + inp_dim: ${globals.SLOT_DIM} + outp_dim: ${globals.FEAT_DIM} + hidden_dims: [128, 128] + n_patches: ${globals.NUM_PATCHES} + +val_metrics: + ari: + name: ImageARI + ignore_background: true + pred_key: decoder_masks_hard + true_key: masks diff --git a/tests/configs/test_savi++_movi_e.yml b/tests/configs/test_savi++_movi_e.yml new file mode 100644 index 0000000..b12893c --- /dev/null +++ b/tests/configs/test_savi++_movi_e.yml @@ -0,0 +1,112 @@ +# This is a test configuration, it is not replicating the exact settings of SAVi++ +experiment_group: test +experiment_name: test_savi_movi_e + +globals: + NUM_SLOTS: 3 + SLOT_DIM: 16 + FEAT_DIM: 512 + FEAT_SIZE: 16 + +dataset: + train_shards: "movi_e/movi_e-train-{000000..000000}.tar" + val_shards: "movi_e/movi_e-validation-{000000..000000}.tar" + batch_size: 2 + val_size: 250 + num_workers: 1 + train_pipeline: + video_size: 24 + chunk_size: 6 + keys: [video] + transforms: + name: movi_train + type: video + input_size: 128 + num_classes: 24 + val_pipeline: + use_chunks: false + keys: [video, segmentations] + transforms: + name: movi_val + type: video + input_size: 128 + num_classes: 24 + +trainer: + max_steps: 100 + +optimizer: + name: Adam + lr: 3e-4 + lr_scheduler: + name: exp_decay_with_warmup + warmup_steps: 50 + +model: + input_type: video + target_type: input + visualize: true + visualize_every_n_steps: 2 + + initializer: + name: RandomInit + n_slots: ${globals.NUM_SLOTS} + dim: ${globals.SLOT_DIM} + + encoder: + backbone: + name: TimmExtractor + model: resnet18_savi + features: resnet_block4 + pos_embed: + name: utils.CoordinatePositionEmbed + dim: ${globals.FEAT_DIM} + size: ${globals.FEAT_SIZE} + output_transform: + name: utils.Chain + models: + - name: networks.two_layer_mlp + inp_dim: ${globals.FEAT_DIM} + outp_dim: ${globals.SLOT_DIM} + hidden_dim: ${globals.FEAT_DIM} + initial_layer_norm: true + - name: networks.TransformerEncoder + dim: ${globals.SLOT_DIM} + n_blocks: 1 + n_heads: 4 + spatial_flatten: true + + grouper: + name: SlotAttention + inp_dim: ${globals.SLOT_DIM} + slot_dim: ${globals.SLOT_DIM} + n_iters: 1 + use_mlp: false + + decoder: + name: SpatialBroadcastDecoder + inp_dim: ${globals.SLOT_DIM} + outp_dim: 3 + backbone: + name: networks.savi_decoder + inp_dim: ${..inp_dim} + backbone_dim: 64 + + predictor: + name: networks.TransformerEncoder + dim: ${globals.SLOT_DIM} + n_blocks: 1 + n_heads: 4 + +val_metrics: + ari: + name: VideoARI + ignore_background: true + pred_key: decoder_masks_hard + true_key: segmentations + image_ari: + name: ImageARI + ignore_background: true + video_input: true + pred_key: decoder_masks_hard + true_key: segmentations diff --git a/tests/configs/test_savi_movi_e.yml b/tests/configs/test_savi_movi_e.yml new file mode 100644 index 0000000..380b732 --- /dev/null +++ b/tests/configs/test_savi_movi_e.yml @@ -0,0 +1,103 @@ +# This is a test configuration, it is not replicating the exact settings of SAVi +experiment_group: test +experiment_name: test_savi_movi_e + +globals: + NUM_SLOTS: 3 + SLOT_DIM: 16 + FEAT_DIM: 64 + FEAT_SIZE: 64 + +dataset: + train_shards: "movi_e/movi_e-train-{000000..000000}.tar" + val_shards: "movi_e/movi_e-validation-{000000..000000}.tar" + batch_size: 2 + val_size: 250 + num_workers: 1 + train_pipeline: + video_size: 24 + chunk_size: 6 + keys: [video] + transforms: + name: movi_train + type: video + input_size: 128 + num_classes: 24 + val_pipeline: + use_chunks: false + keys: [video, segmentations] + transforms: + name: movi_val + type: video + input_size: 128 + num_classes: 24 + +trainer: + max_steps: 100 + +optimizer: + name: Adam + lr: 3e-4 + lr_scheduler: + name: exp_decay_with_warmup + warmup_steps: 50 + +model: + input_type: video + target_type: input + visualize: true + visualize_every_n_steps: 2 + + initializer: + name: RandomInit + n_slots: ${globals.NUM_SLOTS} + dim: ${globals.SLOT_DIM} + + encoder: + backbone: + name: networks.savi_cnn_encoder + pos_embed: + name: utils.CoordinatePositionEmbed + dim: ${globals.FEAT_DIM} + size: ${globals.FEAT_SIZE} + output_transform: + name: networks.two_layer_mlp + dim: ${globals.FEAT_DIM} + hidden_dim: ${globals.FEAT_DIM} + initial_layer_norm: true + spatial_flatten: true + + grouper: + name: SlotAttention + inp_dim: ${globals.FEAT_DIM} + slot_dim: ${globals.SLOT_DIM} + n_iters: 1 + use_mlp: false + + decoder: + name: SpatialBroadcastDecoder + inp_dim: ${globals.SLOT_DIM} + outp_dim: 3 + backbone: + name: networks.savi_decoder + inp_dim: ${..inp_dim} + backbone_dim: 64 + + predictor: + name: networks.TransformerEncoder + dim: ${globals.SLOT_DIM} + n_blocks: 1 + n_heads: 4 + +val_metrics: + ari: + name: VideoARI + ignore_background: true + pred_key: decoder_masks_hard + true_key: segmentations + image_ari: + name: ImageARI + ignore_background: true + video_input: true + pred_key: decoder_masks_hard + true_key: segmentations diff --git a/tests/configs/test_ytvis_video.yml b/tests/configs/test_ytvis_video.yml new file mode 100644 index 0000000..752cc7a --- /dev/null +++ b/tests/configs/test_ytvis_video.yml @@ -0,0 +1,114 @@ +experiment_group: test +experiment_name: test_davis_video + +globals: + NUM_SLOTS: 7 + SLOT_DIM: 64 + FEAT_DIM: 384 + NUM_PATCHES: 196 + +dataset: + train_shards: "ytvis2021_resized/ytvis-train-{000000..000000}.tar" + val_shards: "ytvis2021_resized/ytvis-validation-{000000..000000}.tar" + batch_size: 2 + val_batch_size: 1 # the length of videos is different, so batch_size is 1. + val_size: 10 + num_workers: 1 + # here we should be careful if YTVIS 2021 + # and YTVIS 2019 have the same transformations + # for now they are proccessed with the same ytvis transform + train_pipeline: + chunk_size: 6 + keys: [video] + transforms: + name: ytvis_train + type: video + crop_type: random + input_size: 224 + num_classes: 10 + h_flip_prob: 0.5 + val_pipeline: + use_chunks: false + keys: [video, segmentations] + transforms: + name: ytvis_val + crop_type: central + type: video + input_size: 224 + num_classes: 10 + +trainer: + max_steps: 100 + +optimizer: + name: Adam + lr: 3e-4 + lr_scheduler: + name: exp_decay_with_warmup + warmup_steps: 50 + +model: + input_type: video + visualize: true + visualize_every_n_steps: 2 + + initializer: + name: RandomInit + n_slots: ${globals.NUM_SLOTS} + dim: ${globals.SLOT_DIM} + + encoder: + backbone: + name: TimmExtractor + model: vit_small_patch16_224 + features: vit_block12 + frozen: true + + grouper: + name: SlotAttention + inp_dim: ${globals.FEAT_DIM} + slot_dim: ${globals.SLOT_DIM} + n_iters: 1 + use_mlp: false + + decoder: + name: MLPDecoder + inp_dim: ${globals.SLOT_DIM} + outp_dim: ${globals.FEAT_DIM} + hidden_dims: [128, 128] + n_patches: ${globals.NUM_PATCHES} + + predictor: + name: networks.MLP + inp_dim: ${globals.SLOT_DIM} + outp_dim: ${globals.SLOT_DIM} + hidden_dims: [128, 128] + +val_metrics: + ari: + name: VideoARI + pred_key: decoder_masks_hard + true_key: segmentations + image_ari: + name: ImageARI + video_input: true + pred_key: decoder_masks_hard + true_key: segmentations + mbo: + name: VideoIoU + matching: overlap + ignore_background: false + pred_key: decoder_masks_hard + true_key: segmentations + image_mbo: + name: ImageIoU + matching: overlap + ignore_background: false + video_input: true + pred_key: decoder_masks_hard + true_key: segmentations + j_and_f: + name: VideoJandF + ignore_background: false + pred_key: decoder_masks_hard + true_key: segmentations diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..dc287fe --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,8 @@ +import pytest + + +@pytest.fixture +def data_root_dir() -> str: + from videosaur import data + + return data.get_data_root_dir(error_on_missing_path=True) diff --git a/tests/data/test_datamodules.py b/tests/data/test_datamodules.py new file mode 100644 index 0000000..d5d8779 --- /dev/null +++ b/tests/data/test_datamodules.py @@ -0,0 +1,94 @@ +import itertools + +import pytest +import torch + +from videosaur.data import datamodules + + +@pytest.fixture(scope="session") +def dummy_shards(tmp_path_factory): + import webdataset as wds + + data_dir = tmp_path_factory.mktemp("data") + + def write_split(split, n_samples, n_samples_per_shard): + pattern = str(data_dir / f"{split}-%06d.tar") + with wds.ShardWriter(pattern, maxcount=n_samples_per_shard) as sink: + sink.verbose = 0 + for idx in range(n_samples): + sink.write({"__key__": str(idx), "tensor.pth": torch.randn(2, 2)}) + + return sorted([str(p) for p in data_dir.glob(f"{split}-*.tar")]) + + train_size, val_size = 10, 7 + train_shards = write_split("train", train_size, 4) + val_shards = write_split("validation", val_size, 4) + return { + "train_shards": train_shards, + "val_shards": val_shards, + "train_size": train_size, + "val_size": val_size, + "tensor_key": "tensor", + } + + +@pytest.mark.parametrize("num_workers", [0, 1, 2]) +def test_webdataset_datamodule(dummy_shards, num_workers): + batch_size, val_batch_size = 3, 2 + datamodule = datamodules.WebdatasetDataModule( + train_shards=dummy_shards["train_shards"], + val_shards=dummy_shards["val_shards"], + val_size=dummy_shards["val_size"], + batch_size=batch_size, + val_batch_size=val_batch_size, + num_workers=num_workers, + num_val_workers=num_workers, + ) + + # Check that training iterator contains an infinite stream of data, i.e. that dataloader does + # not stop prematurely + expected_n_batches = (dummy_shards["train_size"] // batch_size) + 3 + n_batches = 0 + for batch in itertools.islice(datamodule.train_dataloader(), expected_n_batches): + assert batch[dummy_shards["tensor_key"]].shape[0] == batch_size + n_batches += 1 + + assert n_batches == expected_n_batches + + # Check that validation iterator contains exactly the validation data, modulo padding + max_batches = dummy_shards["val_size"] # Set upper bound to avoid infinite iteration + keys = [] + for batch in itertools.islice(datamodule.val_dataloader(), max_batches): + assert batch[dummy_shards["tensor_key"]].shape[0] == val_batch_size + keys.extend([key for key in batch["__key__"] if key != "PADDING"]) + + assert len(keys) == dummy_shards["val_size"] + if num_workers <= 1: + # For one worker, we expect the samples to be iterated in order + assert sorted(list(set(keys))) == keys + else: + # For more than one worker, the samples are interleaved, so we need to sort to compare + assert sorted(list(set(keys))) == sorted(keys) + + +@pytest.mark.parametrize("num_workers", [0, 1, 2]) +def test_webdataset_datamodule_fixed_epoch_len(dummy_shards, num_workers): + batch_size = 3 + samples_per_epoch = dummy_shards["train_size"] + datamodule = datamodules.WebdatasetDataModule( + train_shards=dummy_shards["train_shards"], + samples_per_epoch=samples_per_epoch, + batch_size=batch_size, + num_workers=num_workers, + ) + + # Check that training iterator stops after having sampled roughly `samples_per_epoch` samples + max_batches = dummy_shards["train_size"] # Set upper bound to avoid infinite iteration + n_batches = 0 + for batch in itertools.islice(datamodule.train_dataloader(), max_batches): + assert batch[dummy_shards["tensor_key"]].shape[0] == batch_size + n_batches += 1 + + expected_n_batches = samples_per_epoch // batch_size + assert n_batches == expected_n_batches diff --git a/tests/data/test_datasets.py b/tests/data/test_datasets.py new file mode 100644 index 0000000..2adc3b7 --- /dev/null +++ b/tests/data/test_datasets.py @@ -0,0 +1,110 @@ +import os + +import pytest +import torch + +from videosaur.data import datamodules + + +@pytest.mark.dataset +def test_movi_c(data_root_dir): + val_shards = os.path.join(data_root_dir, "movi_c/movi_c-validation-{000000..000000}.tar") + batch_size = 2 + data_module = datamodules.WebdatasetDataModule( + val_shards=val_shards, + val_size=250, + batch_size=batch_size, + num_workers=0, + ) + for batch in data_module.val_dataloader(): + assert batch["video"].shape == (batch_size, 24, 128, 128, 3) + assert batch["video"].dtype == torch.uint8 + assert batch["segmentations"].shape == (batch_size, 24, 128, 128, 1) + assert batch["segmentations"].dtype == torch.uint8 + assert batch["segmentations"].max() <= 10 + break + + +@pytest.mark.dataset +def test_movi_e(data_root_dir): + val_shards = os.path.join(data_root_dir, "movi_e/movi_e-validation-{000000..000000}.tar") + batch_size = 2 + data_module = datamodules.WebdatasetDataModule( + val_shards=val_shards, + val_size=250, + batch_size=batch_size, + num_workers=0, + ) + for batch in data_module.val_dataloader(): + assert batch["video"].shape == (batch_size, 24, 128, 128, 3) + assert batch["video"].dtype == torch.uint8 + assert batch["segmentations"].shape == (batch_size, 24, 128, 128, 1) + assert batch["segmentations"].dtype == torch.uint8 + assert batch["segmentations"].max() <= 23 + break + + +@pytest.mark.dataset +@pytest.mark.parametrize("year", [2019, 2021]) +def test_ytvis(data_root_dir, year): + val_shards = os.path.join( + data_root_dir, f"ytvis{year}_resized/" + "ytvis-validation-{000000..000000}.tar" + ) + batch_size = 1 + data_module = datamodules.WebdatasetDataModule( + val_shards=val_shards, + val_size=30, + batch_size=batch_size, + num_workers=0, + ) + for batch in data_module.val_dataloader(): + assert batch["video"].shape[0] == batch_size + assert batch["video"].shape[-1] == 3 + assert batch["video"].dtype == torch.uint8 + assert batch["segmentations"].shape[0] == batch_size + assert batch["segmentations"].dtype == torch.uint8 + break + + +@pytest.mark.dataset +def test_davis(data_root_dir): + val_shards = os.path.join(data_root_dir, "davis/davis-validation-{000000..000000}.tar") + batch_size = 1 + data_module = datamodules.WebdatasetDataModule( + val_shards=val_shards, + val_size=30, + batch_size=batch_size, + num_workers=0, + ) + for batch in data_module.val_dataloader(): + assert batch["video"].shape[0] == batch_size + assert batch["video"].shape[-1] == 3 + assert batch["video"].dtype == torch.uint8 + assert batch["segmentations"].shape[0] == batch_size + assert batch["segmentations"].shape[-1] == 1 + assert batch["segmentations"].dtype == torch.uint8 + assert batch["segmentations"].max() <= 10 + break + + +@pytest.mark.dataset +@pytest.mark.parametrize("split", ["train", "validation"]) +def test_coco(data_root_dir, split): + shards = os.path.join(data_root_dir, f"coco/coco-{split}" + "-{000000..000000}.tar") + + batch_size = 1 + data_module = datamodules.WebdatasetDataModule( + val_shards=shards, + val_size=30, + batch_size=batch_size, + num_workers=0, + ) + for batch in data_module.val_dataloader(): + assert batch["image"].shape[0] == batch_size + assert batch["image"].shape[-1] == 3 + assert batch["image"].dtype == torch.uint8 + if split == "validation": + assert len(batch["segmentations"].shape) == 4 + assert batch["segmentations"].shape[0] == batch_size + assert batch["segmentations"].dtype == torch.uint8 + assert batch["segmentations"].max() <= 91 diff --git a/tests/data/test_pipelines.py b/tests/data/test_pipelines.py new file mode 100644 index 0000000..b73d4bb --- /dev/null +++ b/tests/data/test_pipelines.py @@ -0,0 +1,502 @@ +import os + +import omegaconf +import pytest +import torch + +from videosaur.data import datamodules, pipelines + + +@pytest.mark.dataset +def test_video_transforms_movi_e(data_root_dir): + val_shards = os.path.join(data_root_dir, "movi_e/movi_e-validation-{000000..000000}.tar") + batch_size = 2 + episode_length = 6 + input_size = 224 + num_classes = 24 + + pipeline_config = omegaconf.DictConfig( + { + "name": "video", + "keys": ("video", "segmentations"), + "chunk_size": episode_length, + "transforms": { + "name": "movi_val", + "type": "video", + "input_size": input_size, + "num_classes": num_classes, + }, + } + ) + val_pipeline = pipelines.build(pipeline_config) + + data_module = datamodules.WebdatasetDataModule( + val_shards=val_shards, + val_size=250, + batch_size=batch_size, + num_workers=0, + val_pipeline=val_pipeline, + ) + dataloader = data_module.val_dataloader() + + for batch in dataloader: + assert batch["video"].shape == (batch_size, episode_length, 3, input_size, input_size) + assert batch["segmentations"].shape == ( + batch_size, + episode_length, + num_classes, + input_size, + input_size, + ) + assert batch["video"].dtype == torch.float32 + assert batch["segmentations"].dtype == torch.bool + break + + +@pytest.mark.dataset +@pytest.mark.parametrize( + "train_crop_type", ["central", "random", "short_side_resize_random", "short_side_resize_central"] +) +@pytest.mark.parametrize("val_crop_type", ["central", "short_side_resize_central"]) +@pytest.mark.parametrize("train_h_flip_prob", [0.5, None]) +@pytest.mark.parametrize("year", [2019, 2021]) +def test_video_transforms_ytvis( + data_root_dir, year, train_crop_type, val_crop_type, train_h_flip_prob +): + val_shards = os.path.join( + data_root_dir, f"ytvis{year}_resized/" + "ytvis-validation-{000000..000000}.tar" + ) + train_shards = os.path.join( + data_root_dir, f"ytvis{year}_resized/" + "ytvis-train-{000000..000000}.tar" + ) + batch_size = 2 + episode_length = 6 + input_size = 224 + num_classes = 10 + + pipeline_config = omegaconf.DictConfig( + { + "name": "video", + "keys": ("video", "segmentations"), + "chunk_size": episode_length, + "transforms": { + "name": "ytvis_val", + "type": "video", + "crop_type": val_crop_type, + "input_size": input_size, + "num_classes": num_classes, + }, + } + ) + val_pipeline = pipelines.build(pipeline_config) + + data_module = datamodules.WebdatasetDataModule( + val_shards=val_shards, + val_size=30, + batch_size=batch_size, + num_workers=0, + val_pipeline=val_pipeline, + ) + dataloader = data_module.val_dataloader() + + for batch in dataloader: + assert batch["video"].shape == (batch_size, episode_length, 3, input_size, input_size) + assert batch["segmentations"].shape == ( + batch_size, + episode_length, + num_classes, + input_size, + input_size, + ) + # check that values are binary (0.0 or 1.0) + assert torch.equal( + sum(batch["segmentations"] == i for i in [0.0, 1.0]).bool(), + torch.ones_like(batch["segmentations"]).bool(), + ) + assert batch["video"].dtype == torch.float32 + assert batch["segmentations"].dtype == torch.bool + break + + pipeline_config = omegaconf.DictConfig( + { + "name": "video", + "keys": ("video",), + "chunk_size": episode_length, + "transforms": { + "name": "ytvis_train", + "type": "video", + "crop_type": train_crop_type, + "h_flip_prob": train_h_flip_prob, + "input_size": input_size, + "num_classes": num_classes, + }, + } + ) + train_pipeline = pipelines.build(pipeline_config) + + data_module = datamodules.WebdatasetDataModule( + train_shards=train_shards, + batch_size=batch_size, + num_workers=0, + train_pipeline=train_pipeline, + ) + dataloader = data_module.train_dataloader() + + for batch in dataloader: + assert batch["video"].shape == (batch_size, episode_length, 3, input_size, input_size) + assert batch["video"].dtype == torch.float32 + break + + +@pytest.mark.dataset +@pytest.mark.parametrize( + "train_crop_type", ["central", "random", "short_side_resize_random", "short_side_resize_central"] +) +@pytest.mark.parametrize("val_crop_type", ["central", "short_side_resize_central"]) +@pytest.mark.parametrize("train_h_flip_prob", [0.5, None]) +def test_video_transforms_davis(data_root_dir, train_crop_type, val_crop_type, train_h_flip_prob): + val_shards = os.path.join(data_root_dir, "davis/davis-validation-{000000..000000}.tar") + batch_size = 2 + episode_length = 6 + input_size = 224 + num_classes = 10 + + pipeline_config = omegaconf.DictConfig( + { + "name": "video", + "keys": ("video", "segmentations"), + "chunk_size": episode_length, + "transforms": { + "name": "davis_val", + "type": "video", + "crop_type": val_crop_type, + "input_size": input_size, + "num_classes": num_classes, + }, + } + ) + val_pipeline = pipelines.build(pipeline_config) + + data_module = datamodules.WebdatasetDataModule( + val_shards=val_shards, + val_size=30, + batch_size=batch_size, + num_workers=0, + val_pipeline=val_pipeline, + ) + dataloader = data_module.val_dataloader() + + for batch in dataloader: + assert batch["video"].shape == (batch_size, episode_length, 3, input_size, input_size) + assert batch["segmentations"].shape == ( + batch_size, + episode_length, + num_classes, + input_size, + input_size, + ) + # check that values are binary (0.0 or 1.0) + assert torch.equal( + sum(batch["segmentations"] == i for i in [0.0, 1.0]).bool(), + torch.ones_like(batch["segmentations"]).bool(), + ) + assert batch["video"].dtype == torch.float32 + assert batch["segmentations"].dtype == torch.bool + break + + train_shards = os.path.join(data_root_dir, "davis/davis-train-{000000..000000}.tar") + pipeline_config = omegaconf.DictConfig( + { + "name": "video", + "keys": ("video", "segmentations"), + "chunk_size": episode_length, + "transforms": { + "name": "davis_train", + "type": "video", + "crop_type": train_crop_type, + "h_flip_prob": train_h_flip_prob, + "input_size": input_size, + "num_classes": num_classes, + }, + } + ) + train_pipeline = pipelines.build(pipeline_config) + + data_module = datamodules.WebdatasetDataModule( + train_shards=train_shards, + batch_size=batch_size, + num_workers=0, + train_pipeline=train_pipeline, + ) + dataloader = data_module.train_dataloader() + + for batch in dataloader: + assert batch["video"].shape == (batch_size, episode_length, 3, input_size, input_size) + assert batch["video"].dtype == torch.float32 + break + + +@pytest.mark.dataset +def test_image_transforms_movi_e(data_root_dir): + val_shards = os.path.join(data_root_dir, "movi_e/movi_e-validation-{000000..000000}.tar") + batch_size = 2 + input_size = 224 + num_classes = 24 + + pipeline_config = omegaconf.DictConfig( + { + "name": "image", + "keys": ("video", "segmentations"), + "transforms": { + "name": "movi_val", + "type": "image", + "input_size": input_size, + "num_classes": num_classes, + }, + } + ) + val_pipeline = pipelines.build(pipeline_config) + + data_module = datamodules.WebdatasetDataModule( + val_shards=val_shards, + val_size=250, + batch_size=batch_size, + num_workers=0, + val_pipeline=val_pipeline, + ) + dataloader = data_module.val_dataloader() + + for batch in dataloader: + assert batch["image"].shape == (batch_size, 3, input_size, input_size) + assert batch["segmentations"].shape == ( + batch_size, + num_classes, + input_size, + input_size, + ) + assert batch["image"].dtype == torch.float32 + assert batch["segmentations"].dtype == torch.bool + break + + +@pytest.mark.dataset +@pytest.mark.parametrize( + "train_crop_type", ["central", "random", "short_side_resize_random", "short_side_resize_central"] +) +@pytest.mark.parametrize("val_crop_type", ["central", "short_side_resize_central"]) +@pytest.mark.parametrize("train_h_flip_prob", [0.5, None]) +def test_image_transforms_davis(data_root_dir, val_crop_type, train_crop_type, train_h_flip_prob): + val_shards = os.path.join(data_root_dir, "davis/davis-validation-{000000..000000}.tar") + batch_size = 2 + input_size = 224 + num_classes = 10 + + pipeline_config = omegaconf.DictConfig( + { + "name": "image", + "keys": ("video", "segmentations"), + "transforms": { + "name": "davis_val", + "type": "image", + "crop_type": val_crop_type, + "input_size": input_size, + "num_classes": num_classes, + }, + } + ) + val_pipeline = pipelines.build(pipeline_config) + + data_module = datamodules.WebdatasetDataModule( + val_shards=val_shards, + val_size=30, + batch_size=batch_size, + num_workers=0, + val_pipeline=val_pipeline, + ) + dataloader = data_module.val_dataloader() + + for batch in dataloader: + assert batch["image"].shape == (batch_size, 3, input_size, input_size) + assert batch["segmentations"].shape == ( + batch_size, + num_classes, + input_size, + input_size, + ) + # check that values are binary (0.0 or 1.0) + assert torch.equal( + sum(batch["segmentations"] == i for i in [0.0, 1.0]).bool(), + torch.ones_like(batch["segmentations"]).bool(), + ) + assert batch["image"].dtype == torch.float32 + assert batch["segmentations"].dtype == torch.bool + break + + train_shards = os.path.join(data_root_dir, "davis/davis-train-{000000..000000}.tar") + pipeline_config = omegaconf.DictConfig( + { + "name": "image", + "keys": ("video", "segmentations"), + "transforms": { + "name": "davis_train", + "type": "image", + "crop_type": train_crop_type, + "input_size": input_size, + "num_classes": num_classes, + "h_flip_prob": train_h_flip_prob, + }, + } + ) + train_pipeline = pipelines.build(pipeline_config) + + data_module = datamodules.WebdatasetDataModule( + train_shards=train_shards, + batch_size=batch_size, + num_workers=0, + train_pipeline=train_pipeline, + ) + dataloader = data_module.train_dataloader() + + for batch in dataloader: + assert batch["image"].shape == (batch_size, 3, input_size, input_size) + assert batch["image"].dtype == torch.float32 + break + + +@pytest.mark.dataset +@pytest.mark.parametrize("mask_size", [384, None]) +def test_image_transforms_coco(data_root_dir, mask_size): + batch_size = 2 + input_size = 224 + num_classes = 30 + val_shards = os.path.join(data_root_dir, "coco/coco-validation-{000000..000000}.tar") + train_shards = os.path.join(data_root_dir, "coco/coco-train-{000000..000000}.tar") + mask_size = mask_size if mask_size else input_size + + pipeline_config = omegaconf.DictConfig( + { + "name": "image", + "keys": ("image", "segmentations"), + "is_video_dataset": False, + "transforms": { + "name": "coco_val", + "type": "image", + "crop_type": "central", + "input_size": input_size, + "num_classes": num_classes, + "mask_size": mask_size, + }, + } + ) + val_pipeline = pipelines.build(pipeline_config) + + data_module = datamodules.WebdatasetDataModule( + val_shards=val_shards, + val_size=30, + batch_size=batch_size, + num_workers=0, + val_pipeline=val_pipeline, + ) + dataloader = data_module.val_dataloader() + + for batch in dataloader: + assert batch["image"].shape == (batch_size, 3, input_size, input_size) + assert batch["segmentations"].shape == ( + batch_size, + num_classes, + mask_size, + mask_size, + ) + # check that values are binary (0.0 or 1.0) + assert torch.equal( + sum(batch["segmentations"] == i for i in [0.0, 1.0]).bool(), + torch.ones_like(batch["segmentations"]).bool(), + ) + assert batch["image"].dtype == torch.float32 + assert batch["segmentations"].dtype == torch.bool + break + + pipeline_config = omegaconf.DictConfig( + { + "name": "image", + "keys": ("image",), + "is_video_dataset": False, + "transforms": { + "name": "coco_train", + "type": "image", + "crop_type": "random", + "input_size": input_size, + "num_classes": num_classes, + }, + } + ) + train_pipeline = pipelines.build(pipeline_config) + + data_module = datamodules.WebdatasetDataModule( + train_shards=train_shards, + batch_size=batch_size, + num_workers=0, + train_pipeline=train_pipeline, + ) + dataloader = data_module.train_dataloader() + + for batch in dataloader: + assert batch["image"].shape == (batch_size, 3, input_size, input_size) + assert batch["image"].dtype == torch.float32 + break + + +@pytest.mark.dataset +def test_video_transforms_movi_c(data_root_dir): + val_shards = os.path.join(data_root_dir, "movi_c/movi_c-validation-{000000..000000}.tar") + batch_size = 2 + episode_length = 6 + input_size = 128 + target_size = 224 + num_classes = 24 + + pipeline_config = omegaconf.DictConfig( + { + "name": "video", + "keys": ("video", "segmentations"), + "duplicate": {"video": "target_video"}, + "chunk_size": episode_length, + "transforms": { + "name": "movi_val", + "type": "video", + "target_size": target_size, + "input_size": input_size, + "num_classes": num_classes, + }, + } + ) + val_pipeline = pipelines.build(pipeline_config) + + data_module = datamodules.WebdatasetDataModule( + val_shards=val_shards, + val_size=250, + batch_size=batch_size, + num_workers=0, + val_pipeline=val_pipeline, + ) + dataloader = data_module.val_dataloader() + + for batch in dataloader: + assert batch["target_video"].shape == ( + batch_size, + episode_length, + 3, + target_size, + target_size, + ) + assert batch["video"].shape == (batch_size, episode_length, 3, input_size, input_size) + assert batch["segmentations"].shape == ( + batch_size, + episode_length, + num_classes, + input_size, + input_size, + ) + assert batch["video"].dtype == torch.float32 + assert batch["segmentations"].dtype == torch.bool + break diff --git a/tests/data/test_transforms.py b/tests/data/test_transforms.py new file mode 100644 index 0000000..5b7dd12 --- /dev/null +++ b/tests/data/test_transforms.py @@ -0,0 +1,10 @@ +import torch + +from videosaur.data import transforms_video + + +def test_from_tensor(): + size = (10, 100, 100, 3) + video = torch.randint(0, 255, size, dtype=torch.uint8) + id_video = transforms_video.from_tensor(transforms_video.to_tensor(video)) + torch.equal(id_video, video) diff --git a/tests/modules/test_decoders.py b/tests/modules/test_decoders.py new file mode 100644 index 0000000..c787653 --- /dev/null +++ b/tests/modules/test_decoders.py @@ -0,0 +1,92 @@ +import pytest +import torch + +from videosaur.modules import decoders, networks + + +@pytest.mark.parametrize("eval_upscale", [False, True]) +def test_mlp_decoder(eval_upscale): + inp_dim, outp_dim, n_patches, n_slots = 5, 8, 4, 3 + + if eval_upscale: + hw = int(4 * n_patches**0.5) + output_size = hw, hw + n_output_patches = hw * hw + else: + output_size = None + n_output_patches = n_patches + + decoder = decoders.MLPDecoder( + inp_dim=inp_dim, + outp_dim=outp_dim, + hidden_dims=[10], + n_patches=n_patches, + eval_output_size=output_size, + ) + if eval_upscale: + decoder.eval() + + inp = torch.randn(1, n_slots, inp_dim) + with torch.no_grad(): + outp = decoder(inp) + + assert outp["reconstruction"].shape == (1, n_output_patches, outp_dim) + assert outp["masks"].shape == (1, n_slots, n_output_patches) + assert outp["masks"].min() >= 0.0 and outp["masks"].max() <= 1.0 + + +def test_spatial_broadcast_decoder(): + bs, inp_dim, outp_dim, feat_dim, height, width, n_slots = 2, 4, 6, 3, 5, 4, 3 + backbone = networks.CNNDecoder(inp_dim, [feat_dim, feat_dim], kernel_sizes=3, strides=2) + decoder = decoders.SpatialBroadcastDecoder( + inp_dim=inp_dim, + outp_dim=outp_dim, + backbone=backbone, + initial_size=(height, width), + backbone_dim=feat_dim, + ) + + inp = torch.randn(bs, n_slots, inp_dim) + with torch.no_grad(): + outp = decoder(inp) + + target_height, target_width = height * 4, width * 4 + assert outp["reconstruction"].shape == (bs, outp_dim, target_height, target_width) + assert outp["masks"].shape == (bs, n_slots, target_height, target_width) + assert outp["masks"].min() >= 0.0 and outp["masks"].max() <= 1.0 + + +@pytest.mark.parametrize("eval_upscale", [False, True]) +def test_slot_mixer_decoder(eval_upscale): + bs, inp_dim, outp_dim, embed_dim, feat_dim, n_patches, n_slots = 2, 4, 5, 6, 3, 9, 3 + transformer = networks.TransformerEncoder(embed_dim, n_blocks=1, n_heads=1, memory_dim=inp_dim) + mlp = networks.MLP(inp_dim, feat_dim, [5], final_activation=True) + + if eval_upscale: + hw = int(2 * n_patches**0.5) + output_size = hw, hw + n_output_patches = hw * hw + else: + output_size = None + n_output_patches = n_patches + + decoder = decoders.SlotMixerDecoder( + inp_dim=inp_dim, + outp_dim=outp_dim, + embed_dim=embed_dim, + n_patches=n_patches, + allocator=transformer, + renderer=mlp, + renderer_dim=feat_dim, + eval_output_size=output_size, + ) + if eval_upscale: + decoder.eval() + + inp = torch.randn(bs, n_slots, inp_dim) + with torch.no_grad(): + outp = decoder(inp) + + assert outp["reconstruction"].shape == (bs, n_output_patches, outp_dim) + assert outp["masks"].shape == (bs, n_slots, n_output_patches) + assert outp["masks"].min() >= 0.0 and outp["masks"].max() <= 1.0 diff --git a/tests/modules/test_encoders.py b/tests/modules/test_encoders.py new file mode 100644 index 0000000..5e17f1f --- /dev/null +++ b/tests/modules/test_encoders.py @@ -0,0 +1,77 @@ +import pytest +import torch + +from videosaur.modules import encoders + + +@pytest.mark.parametrize("use_spatial_flatten", [False, True]) +@pytest.mark.parametrize("use_pos_embed", [False, True]) +@pytest.mark.parametrize("use_output_transform", [False, True]) +def test_frame_encoder(use_spatial_flatten, use_pos_embed, use_output_transform): + def spatial_flatten(x): + return x.flatten(-2, -1).transpose(-2, -1) + + if use_spatial_flatten: + backbone = lambda x: x + 1 + else: + backbone = lambda x: spatial_flatten(x + 1) + + pos_embed = (lambda x: x + 2) if use_pos_embed else None + output_transform = (lambda x: x + 4) if use_output_transform else None + + encoder = encoders.FrameEncoder( + backbone, + pos_embed=pos_embed, + output_transform=output_transform, + spatial_flatten=use_spatial_flatten, + ) + + inp = torch.zeros(1, 3, 4, 4) + outputs = encoder(inp) + + expected_offset = 1 + 2 * int(use_pos_embed) + 4 * int(use_output_transform) + assert torch.allclose(outputs["features"], spatial_flatten(inp + expected_offset)) + assert torch.allclose(outputs["backbone_features"], spatial_flatten(inp + 1)) + + +@pytest.mark.parametrize( + "model,features,expected_shape", + [ + ("vit_tiny_patch16_224", ["vit_block1", "vit_block12", "vit_output"], (14 * 14, 192)), + ("resnet34_savi", ["resnet_block4"], (512, 28, 28)), + ], +) +@pytest.mark.parametrize("frozen", [False, True]) +def test_timm_encoder(model, features, expected_shape, frozen): + encoder = encoders.TimmExtractor(model, frozen=frozen, features=features) + if frozen: + for param in encoder.parameters(): + assert not param.requires_grad + + inp = torch.randn(1, 3, 224, 224) + + with torch.no_grad(): + outputs = encoder(inp) + + if len(features) > 1: + for output in outputs.values(): + assert output.shape[1:] == expected_shape + else: + # Single features are automatically unpacked + assert outputs.shape[1:] == expected_shape + + +def test_timm_encoder_dynamic_img_size(): + encoder = encoders.TimmExtractor( + model="vit_tiny_patch16_224", + features="vit_block1", + model_kwargs={"dynamic_img_size": True, "dynamic_img_pad": True}, + ) + + # After padding, should result in 128x128 image size -> 8 * 8 tokens + inp = torch.randn(1, 3, 127, 127) + + with torch.no_grad(): + outputs = encoder(inp) + + assert outputs.shape == (1, 8 * 8, 192) diff --git a/tests/modules/test_groupers.py b/tests/modules/test_groupers.py new file mode 100644 index 0000000..18dfc40 --- /dev/null +++ b/tests/modules/test_groupers.py @@ -0,0 +1,19 @@ +import pytest +import torch + +from videosaur.modules import groupers + + +@pytest.mark.parametrize("use_mlp", [False, True]) +def test_slot_attention(use_mlp): + inp_dim, slot_dim, n_patches, n_slots = 5, 8, 6, 3 + slot_attention = groupers.SlotAttention(inp_dim, slot_dim, use_mlp=use_mlp) + + features = torch.randn(1, n_patches, inp_dim) + slots = torch.randn(1, n_slots, slot_dim) + + with torch.no_grad(): + outp = slot_attention(slots, features) + + assert outp["slots"].shape == (1, n_slots, slot_dim) + assert outp["masks"].shape == (1, n_slots, n_patches) diff --git a/tests/modules/test_networks.py b/tests/modules/test_networks.py new file mode 100644 index 0000000..fbbb125 --- /dev/null +++ b/tests/modules/test_networks.py @@ -0,0 +1,138 @@ +import pytest +import torch + +from videosaur.modules import networks + + +@pytest.mark.parametrize( + "kernel_sizes,strides,expected_size", + [ + ([5, 3], 1, 32), + ([5, 3], [2, 1], 16), + ([5, 5], 2, 8), + ([5, 3], 2, 8), + (5, [2, 2], 8), + ], +) +def test_cnn_encoder(kernel_sizes, strides, expected_size): + bs, inp_dim, outp_dim = 2, 3, 5 + encoder = networks.CNNEncoder( + inp_dim, features=[12, outp_dim], kernel_sizes=kernel_sizes, strides=strides + ) + with torch.no_grad(): + outp = encoder(torch.randn(bs, inp_dim, 32, 32)) + assert outp.shape == (bs, outp_dim, expected_size, expected_size) + + +@pytest.mark.parametrize( + "kernel_sizes,strides,expected_size", + [ + ([5, 3], 1, 8), + ([5, 3], [2, 1], 16), + ([5, 5], 2, 32), + ([5, 3], 2, 32), + ([5, 5, 5, 5, 5, 3], [2, 2, 2, 2, 1, 1], 128), + ], +) +def test_cnn_decoder(kernel_sizes, strides, expected_size): + bs, inp_dim, outp_dim = 2, 3, 5 + features = [12] * (len(kernel_sizes) - 1) + [outp_dim] + decoder = networks.CNNDecoder( + inp_dim, features=features, kernel_sizes=kernel_sizes, strides=strides + ) + with torch.no_grad(): + outp = decoder(torch.randn(bs, inp_dim, 8, 8)) + assert outp.shape == (bs, outp_dim, expected_size, expected_size) + + +@pytest.mark.parametrize( + "qdim,kdim,vdim,inner_dim,same_qkv,same_kv", + [ + (4, 5, 6, 8, False, False), + (4, 5, 5, 8, False, False), + (4, 5, 5, 8, False, True), + (4, None, None, 8, False, False), + (4, None, None, 8, False, True), + (4, None, None, 8, True, True), + ], +) +@pytest.mark.parametrize("qkv_bias", [False, True]) +def test_attention(qdim, kdim, vdim, inner_dim, qkv_bias, same_qkv, same_kv): + bs, src_len, tgt_len = 2, 3, 4 + attention = networks.Attention( + dim=qdim, num_heads=2, kdim=kdim, vdim=vdim, inner_dim=inner_dim, qkv_bias=qkv_bias + ) + kdim = qdim if kdim is None else kdim + vdim = qdim if vdim is None else vdim + + q = torch.randn(bs, tgt_len, qdim) + if same_qkv: + k = v = q + elif same_kv: + k = torch.randn(bs, src_len, kdim) + v = k + else: + k = torch.randn(bs, src_len, kdim) + v = torch.randn(bs, src_len, vdim) + + attn_mask = torch.randn(q.shape[1], k.shape[1]) + attn_mask[0, 0] = -torch.inf + key_padding_mask = torch.zeros(bs, k.shape[1], dtype=torch.bool) + key_padding_mask[0, 0] = 1 + + with torch.no_grad(): + outp, attn = attention( + q, k, v, attn_mask=attn_mask, key_padding_mask=key_padding_mask, return_weights=True + ) + assert outp.shape == (bs, q.shape[1], qdim) + assert attn.shape == (bs, q.shape[1], k.shape[1]) + assert attn.min() >= 0.0 and attn.max() <= 1.0 + assert torch.allclose(attn[:, 0, 0], torch.zeros_like(attn[:, 0, 0])) + assert torch.allclose(attn[0, :, 0], torch.zeros_like(attn[0, :, 0])) + + +@pytest.mark.parametrize("memory", [False, True]) +@pytest.mark.parametrize("initial_residual_scale", [None, 0.0]) +def test_transformer_encoder(memory, initial_residual_scale): + bs, seq_len, dim = 2, 4, 8 + memory_dim = 5 if memory else None + + encoder = networks.TransformerEncoder( + dim, + n_blocks=2, + n_heads=4, + memory_dim=memory_dim, + initial_residual_scale=initial_residual_scale, + ) + + inp = torch.randn(bs, seq_len, dim) + memory = torch.randn(bs, seq_len, memory_dim) if memory else None + with torch.no_grad(): + outp = encoder(inp, memory=memory) + assert outp.shape == (bs, seq_len, dim) + + if initial_residual_scale == 0.0: + assert torch.allclose(inp, outp) + + +@pytest.mark.parametrize("initial_residual_scale", [None, 0.0]) +def test_transformer_decoder(initial_residual_scale): + bs, seq_len, mem_len, dim = 2, 3, 4, 8 + memory_dim = 5 + + decoder = networks.TransformerDecoder( + dim, + n_blocks=2, + n_heads=4, + memory_dim=memory_dim, + initial_residual_scale=initial_residual_scale, + ) + + inp = torch.randn(bs, seq_len, dim) + memory = torch.randn(bs, mem_len, memory_dim) + with torch.no_grad(): + outp = decoder(inp, memory=memory) + assert outp.shape == (bs, seq_len, dim) + + if initial_residual_scale == 0.0: + assert torch.allclose(inp, outp) diff --git a/tests/modules/test_timm.py b/tests/modules/test_timm.py new file mode 100644 index 0000000..78fb8e7 --- /dev/null +++ b/tests/modules/test_timm.py @@ -0,0 +1,164 @@ +import numpy as np +import pytest +import timm +import torch + +import videosaur.modules # noqa: F401 + + +@pytest.mark.parametrize( + "name,inp_shape,outp_shape", + [ + ("resnet18_savi", (1, 3, 128, 128), (1, 512, 16, 16)), + ("resnet18_savi", (1, 3, 224, 224), (1, 512, 28, 28)), + ("resnet34_savi", (1, 3, 128, 128), (1, 512, 16, 16)), + ("resnet50_savi", (1, 3, 128, 128), (1, 2048, 16, 16)), + ], +) +def test_savi_resnet(name, inp_shape, outp_shape): + model = timm.create_model(name, pretrained=False) + + found_bn = any(isinstance(m, torch.nn.BatchNorm2d) for m in model.modules()) + assert not found_bn + + with torch.no_grad(): + inp = torch.randn(*inp_shape) + outp = model.forward_features(inp) + + assert outp.shape == outp_shape + + +@pytest.mark.slow +@pytest.mark.parametrize( + "name,inp_shape,outp_shape", + [ + ("resnet50_dino", (1, 3, 224, 224), (1, 2048, 7, 7)), + ("resnet50_mocov3", (1, 3, 224, 224), (1, 2048, 7, 7)), + ("vit_base_patch4_224_msn", (1, 3, 224, 224), (1, 3137, 768)), + ], +) +def test_custom_pretrained_models(name, inp_shape, outp_shape): + """Check that custom models with pre-trained weights load properly.""" + model = timm.create_model(name, pretrained=True) + + with torch.no_grad(): + inp = torch.randn(*inp_shape) + outp = model.forward_features(inp) + + assert outp.shape == outp_shape + + +@pytest.mark.slow +@pytest.mark.parametrize( + "name,inp_shape,outp_shape", + [ + ("vit_base_patch16_224_dino_timetuning", (1, 3, 224, 224), (1, 197, 768)), + ("vit_base_patch8_224_dino_timetuning", (1, 3, 224, 224), (1, 785, 768)), + ], +) +def test_vit_dino_timetuning(name, inp_shape, outp_shape): + model = timm.create_model(name, pretrained=True) + + found_bn = any(isinstance(m, torch.nn.BatchNorm2d) for m in model.modules()) + assert not found_bn + + with torch.no_grad(): + inp = torch.randn(*inp_shape) + outp = model.forward_features(inp) + + assert outp.shape == outp_shape + + +# Expected outputs of custom ViT weights. We store them verbatim here to avoid putting binary +# files into git. We store them as strings instead of numbers to avoid black formatting them. +CUSTOM_VIT_WEIGHTS_RESULTS = { + "vit_base_patch16_224_mae/zeros/0_0-20_0": """ +[0.28719592, 0.24583483, 0.26907375, 0.30731523, 0.2868713 , 0.32485905, 0.30731025, +0.33331153, 0.34344897, 0.33817446, 0.3270159 , 0.29424447, 0.27153555, 0.3556132 , +0.19864245, 0.1820898 , 0.2051502 , 0.2437797 , 0.20441592, 0.24913353]""", + "vit_base_patch16_224_mae/zeros/0_0_0-20": """ +[0.28719592, -0.0576665 , -0.72432476, -0.18103008, 0.08990244, -0.9472992 , 0.6524778 , +0.17858694, -0.08992696, 0.06529036, 0.05850611, -0.30322337, -0.09932699, 2.2201734 , +-0.02982868, -0.09614885, -0.15114741, -0.61356527, 0.58381927, 0.08191889]""", + "vit_base_patch16_224_mae/ones/0_0-20_0": """ +[0.2549035 , 0.28759107, 0.31177142, 0.32582435, 0.30852506, 0.34220454, 0.32828912, +0.35779768, 0.3720604 , 0.34713408, 0.32903695, 0.29814035, 0.28354186, 0.33921587, +0.15334876, 0.21293344, 0.23456964, 0.2595923 , 0.22746845, 0.2685317]""", + "vit_base_patch16_224_mae/ones/0_0_0-20": """ +[0.2549035 , -0.09336171, -0.6880767 , -0.28376287, 0.02024214, -0.76264304, 0.5904238 , +0.15652883, -0.12118589, 0.21486482, 0.15963039, -0.27727827, 0.03670263, 1.8414731 , +0.07228839, -0.20896748, -0.27817705, -0.485394 , 0.6534163 , -0.18775147]""", + "vit_base_patch16_224_mocov3/zeros/0_0-20_0": """ +[-0.05149402, -0.03026696, -0.02507262, -0.02068919, -0.01894361, -0.02121132, -0.02440684, +-0.0279355 , -0.02931937, -0.02867937, -0.02717797, -0.02422431, -0.02747217, -0.05334421, +-0.02532654, -0.0125381 , -0.00969828, -0.00759022, -0.00619568, -0.00793152]""", + "vit_base_patch16_224_mocov3/zeros/0_0_0-20": """ +[-0.05149402, 0.01578675, -0.09754238, 0.00310562, 0.04205038, 0.00287579, 0.02752389, +-0.08285441, -0.02746764, 0.00113256, -0.02433307, 0.03415485, -0.0008044 , 0.03292493, +0.06440924, 0.04894995, -0.20814528, -0.08244848, -0.0776324 , -0.0591822]""", + "vit_base_patch16_224_mocov3/ones/0_0-20_0": """ +[-0.05119515, -0.03057244, -0.02808539, -0.02491217, -0.02365276, -0.024435 , -0.02549231, +-0.02861489, -0.03086424, -0.03028261, -0.02863837, -0.02775326, -0.03039749, -0.05470654, +-0.02382995, -0.00736903, -0.00620988, -0.00577518, -0.00560756, -0.00666199]""", + "vit_base_patch16_224_mocov3/ones/0_0_0-20": """ +[-5.1195148e-02, 2.4619440e-02, -8.7654129e-02, -8.9437347e-03, 4.8605293e-02, +6.5571134e-05, 1.6089279e-02, -8.1912845e-02, -2.9642873e-02, 1.2495698e-02, +-2.6932344e-02, 4.8234582e-02, 1.3394909e-03, 2.9346963e-02, 5.3340483e-02, +5.6126781e-02, -2.1160576e-01, -7.4558653e-02, -8.7223224e-02, -5.3826150e-02]""", + "vit_base_patch16_224_msn/zeros/0_0-20_0": """ +[13.323833, 13.172627, 13.217373, 13.198288, 13.362397, 13.165453, 13.22209 , 13.255227, +13.191107, 13.246759, 13.293469, 13.505752, 13.324869, 13.397531, 13.242817, 13.246699, +13.402661, 13.280356, 13.380144, 13.23673 ]""", + "vit_base_patch16_224_msn/zeros/0_0_0-20": """ +[ 13.323833 , -19.434715 , 10.907945 , -5.5864744, -23.869556 , -8.801638 , -14.60782 , +-9.762392 , -17.333504 , 8.255657 , -8.370134 , -7.5889435, 2.4847035, -4.690354 , +4.596179 , 23.206505 , -18.972391 , -10.831766 , 10.448822 , -8.638037]""", + "vit_base_patch16_224_msn/ones/0_0-20_0": """ +[13.377316 , 13.208665 , 13.225736 , 13.205421 , 13.411082 , 13.20577 , 13.237612 , +13.2523365, 13.221363 , 13.261968 , 13.332789 , 13.483296 , 13.371156 , 13.483837 , +13.264603 , 13.260244 , 13.37155 , 13.286394 , 13.377354 , 13.243222]""", + "vit_base_patch16_224_msn/ones/0_0_0-20": """ +[ 13.377316 , -19.428873 , 10.922306 , -5.6203094, -23.830498 , -8.848695 , -14.560103 , +-9.800345 , -17.539894 , 8.266435 , -8.417096 , -7.53442 , 2.5665472, -4.787496 , +4.625372 , 23.25421 , -18.970985 , -10.652556 , 10.562778 , -8.680251 ]""", +} + + +@pytest.mark.slow +@pytest.mark.parametrize( + "name, inp_type, dims, expected_result", + [(*key.split("/"), val) for key, val in CUSTOM_VIT_WEIGHTS_RESULTS.items()], +) +def test_custom_pretrained_vits_output(name, inp_type, dims, expected_result): + """Check that output of custom ViTs still aligns with results from their original repository.""" + model = videosaur.modules.build_encoder( + { + "name": "TimmExtractor", + "model": name, + "pretrained": True, + "frozen": True, + "features": "vit_output", + } + ) + + if inp_type == "zeros": + inp = torch.zeros(1, 3, 224, 224) + elif inp_type == "ones": + inp = torch.ones(1, 3, 224, 224) + + with torch.no_grad(): + output = model(inp) + + expected = np.array(eval(expected_result), dtype=np.float32) + + slices = [] + for dim in dims.split("_"): + if dim == "all": + slices.append(slice(None)) + elif "-" in dim: + dim = dim.split("-") + slices.append(slice(int(dim[0]), int(dim[1]))) + else: + slices.append(slice(int(dim), int(dim) + 1)) + + assert np.allclose(output[slices].squeeze().numpy(), expected, atol=1e-5) diff --git a/tests/modules/test_utils.py b/tests/modules/test_utils.py new file mode 100644 index 0000000..aecfb3a --- /dev/null +++ b/tests/modules/test_utils.py @@ -0,0 +1,136 @@ +import pytest +import torch + +from videosaur.modules import utils + + +@pytest.mark.parametrize("last_output", [False, True]) +def test_chain(last_output): + class SingleModule(torch.nn.Module): + def __init__(self): + super().__init__() + + def forward(self, inp): + return inp + + class DictModule(torch.nn.Module): + def __init__(self): + super().__init__() + + def forward(self, inp1, inp2=None): + outputs = {"out1": inp1} + if inp2 is not None: + outputs["out2"] = inp2 + + return outputs + + inp = 123 + + chain = utils.Chain({"mod1": SingleModule()}, last_output=last_output) + if last_output: + assert chain(inp) == inp + else: + assert chain(inp) == {"mod1": inp} + + chain = utils.Chain( + {"mod1": SingleModule(), "mod2": DictModule()}, + {"mod2": ["mod1", "mod1"]}, + last_output=last_output, + ) + if last_output: + assert chain(inp) == {"out1": inp, "out2": inp} + else: + assert chain(inp) == {"mod1": inp, "out1": inp, "out2": inp} + + chain = utils.Chain( + {"mod1": DictModule(), "mod2": SingleModule()}, {"mod2": ["out1"]}, last_output=last_output + ) + if last_output: + assert chain(inp) == inp + else: + assert chain(inp) == {"out1": inp, "mod2": inp} + + +def test_patchify(): + bs, channels, h, w, patch_size = 2, 3, 4, 4, 2 + n_patches = (h / patch_size) * (w / patch_size) + patchify = utils.Patchify(patch_size) + + inp = torch.stack([torch.stack([torch.eye(h)] * 3)] * bs) + patches = patchify(inp) + + assert patches.shape == (bs, n_patches, channels * patch_size * patch_size) + assert torch.allclose( + patches[0, 0], torch.tensor([1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1], dtype=torch.float) + ) + + +@pytest.mark.parametrize("input_size", [(5, 5), (4, 3)]) +@pytest.mark.parametrize("size_tensor", [False, True]) +@pytest.mark.parametrize("patch_inputs", [False, True]) +@pytest.mark.parametrize("patch_outputs", [False, True]) +@pytest.mark.parametrize("video_inputs", [False, True]) +@pytest.mark.parametrize("channels_last", [False, True]) +def test_mask_resizer( + input_size, size_tensor, patch_inputs, patch_outputs, video_inputs, channels_last +): + target_size = (8, 6) + if size_tensor: + size = None + size_tensor = torch.ones(target_size) + else: + size = target_size + size_tensor = None + + resizer = utils.Resizer( + size=size, + patch_inputs=patch_inputs, + patch_outputs=patch_outputs, + video_inputs=video_inputs, + channels_last=channels_last, + ) + + batch_size, n_frames, n_channels = 2, 3, 4 + height, width = input_size + + def build_shape(height, width, flatten_patches=False): + shape = [batch_size] + if video_inputs: + shape.append(n_frames) + + if not channels_last: + shape.append(n_channels) + + if flatten_patches: + shape.append(height * width) + else: + shape.extend([height, width]) + + if channels_last: + shape.append(n_channels) + return shape + + inputs = torch.rand(*build_shape(height, width, flatten_patches=patch_inputs)) + + if patch_inputs and height / width != target_size[0] / target_size[1]: + # Can not resize from patches if aspect ratio does not match target + with pytest.raises(ValueError): + resized_inputs = resizer(inputs, size_tensor) + else: + resized_inputs = resizer(inputs, size_tensor) + assert list(resized_inputs.shape) == build_shape(*target_size, flatten_patches=patch_outputs) + + +def test_coordinate_position_embed(): + dims, size = 5, (3, 3) + embed = utils.CoordinatePositionEmbed(dim=dims, size=size) + + grid = embed.build_grid(size, bounds=(0.0, 1.0)) + assert torch.all((0.0 <= grid) & (grid <= 1.0)) + assert list(grid.shape) == [2, *size] + + grid = embed.build_grid(size, bounds=(0.0, 1.0), add_inverse=True) + assert torch.all((0.0 <= grid) & (grid <= 1.0)) + assert list(grid.shape) == [4, *size] + + assert list(embed(torch.randn(2, dims, *size)).shape) == [2, dims, *size] diff --git a/tests/modules/test_video.py b/tests/modules/test_video.py new file mode 100644 index 0000000..48f22c9 --- /dev/null +++ b/tests/modules/test_video.py @@ -0,0 +1,39 @@ +import torch + +from videosaur.modules import video + + +def test_map_over_time(): + class SingleModule(torch.nn.Module): + def forward(self, inp): + assert inp.ndim == 2 + return inp + + batch_size, seq_len, dims = 3, 4, 5 + + time_mapper = video.MapOverTime(SingleModule()) + + inp = torch.ones(batch_size, seq_len, dims) + assert torch.allclose(time_mapper(inp), inp) + + +def test_scan_over_time(): + class RecurrentCell(torch.nn.Module): + def forward(self, state, inputs): + assert state.ndim == 2 + assert inputs.ndim == 2 + state_next = state + inputs + return {"state": state, "state_next": state_next, "aux": {"state_next": state_next}} + + batch_size, seq_len, dims = 3, 4, 1 + scanner = video.ScanOverTime(RecurrentCell(), next_state_key="state_next", pass_step=False) + + initial_state = torch.zeros(batch_size, dims) + inputs = torch.ones(batch_size, seq_len, dims) + + outputs = scanner(initial_state, inputs) + + assert torch.allclose(outputs["state"][:, 0], initial_state) + assert torch.allclose(outputs["state"][:, 1:], torch.cumsum(inputs, dim=1)[:, :-1]) + assert torch.allclose(outputs["state_next"], torch.cumsum(inputs, dim=1)) + assert torch.allclose(outputs["aux"]["state_next"], torch.cumsum(inputs, dim=1)) diff --git a/tests/test_configuration.py b/tests/test_configuration.py new file mode 100644 index 0000000..0f6a0e0 --- /dev/null +++ b/tests/test_configuration.py @@ -0,0 +1,15 @@ +from videosaur import configuration + + +def test_resolver_eval(): + # Version with full lambda function specification + fn = "lambda x, y: x + y" + assert configuration.resolver_eval(fn, 1, 2) == 3 + + # Version without lambda prefix + fn = "x, y, z: x + y + z" + assert configuration.resolver_eval(fn, 1, 2, 3) == 6 + + # Version with auto arguments + fn = "a + b + c + d" + assert configuration.resolver_eval(fn, 1, 2, 3, 4) == 10 diff --git a/tests/test_metrics.py b/tests/test_metrics.py new file mode 100644 index 0000000..a5af2c2 --- /dev/null +++ b/tests/test_metrics.py @@ -0,0 +1,384 @@ +import numpy as np +import pytest +import torch +from torch.nn import functional as F + +from videosaur import metrics + + +@pytest.mark.parametrize("video_input", [False, True]) +def test_image_ari(video_input): + metric = metrics.ImageARI(video_input=video_input) + + a = _one_hot([[[0, 0], [1, 1]]], axis=1) + b = _one_hot([[[0, 1], [2, 3]]], axis=1) + if video_input: + a = torch.stack((a, a), axis=1) + b = torch.stack((b, b), axis=1) + + metric.update(a, a) + assert np.allclose(metric.compute().numpy(), 1.0) + + metric.update(a, b) + assert np.allclose(metric.compute().numpy(), 0.5) + + # Mask not binary should raise error + a[:, :, :, 0] = 2 + with pytest.raises(ValueError): + metric.update(a, b) + a[:, :, :, 0] = 1 + b[:, :, :, 1] = 2 + with pytest.raises(ValueError): + metric.update(a, b) + + +@pytest.mark.parametrize("ignore_overlaps", [False, True]) +@pytest.mark.parametrize("video_input", [False, True]) +def test_image_ari_ignore_overlaps(ignore_overlaps, video_input): + metric = metrics.ImageARI(ignore_overlaps=ignore_overlaps, video_input=video_input) + + a = _one_hot([[[0, 0], [1, 1], [2, 2]]], axis=1) + a_pred = a.clone() + a[0, :, 2, :] = 1 # Set some overlaps, ignoring last row + b = _one_hot([[[0, 0], [1, 1], [2, 3]]], axis=1) + c = _one_hot([[[0, 1], [2, 3], [4, 4]]], axis=1) + if video_input: + a = torch.stack((a, a), axis=1) + a_pred = torch.stack((a_pred, a_pred), axis=1) + b = torch.stack((b, b), axis=1) + c = torch.stack((c, c), axis=1) + + if ignore_overlaps: + metric.update(a, a_pred) + assert np.allclose(metric.compute().numpy(), 1.0) + metric.update(a, b) + assert np.allclose(metric.compute().numpy(), 1.0) + metric.reset() + metric.update(a, c) + assert np.allclose(metric.compute().numpy(), 0.0) + + # Fully empty ground truth masks should result in no update + metric.reset() + metric.update(torch.zeros_like(a), b) + assert np.isnan(metric.compute().numpy()) + else: + with pytest.raises(ValueError): + metric.update(a, a_pred) # Not one-hot should raise error + + +def test_video_ari(): + metric = metrics.VideoARI() + + a = _one_hot([[[0, 0], [1, 1]]], axis=1, n_classes=8) + b = _one_hot([[[0, 1], [2, 3]]], axis=1, n_classes=8) + c = _one_hot([[[4, 5], [6, 7]]], axis=1, n_classes=8) + a = torch.stack((a, a), axis=1) + b = torch.stack((b, c), axis=1) + + metric.update(a, a) + assert np.allclose(metric.compute().numpy(), 1.0) + + metric.update(a, b) + assert np.allclose(metric.compute().numpy(), 0.5) + + # Mask not binary should raise error + a[:, :, :, 0] = 2 + with pytest.raises(ValueError): + metric.update(a, b) + a[:, :, :, 0] = 1 + b[:, :, :, 1] = 2 + with pytest.raises(ValueError): + metric.update(a, b) + + +@pytest.mark.parametrize("ignore_overlaps", [False, True]) +def test_video_ari_ignore_overlaps(ignore_overlaps): + metric = metrics.VideoARI(ignore_overlaps=ignore_overlaps) + + a = _one_hot([[[0, 0], [1, 1], [2, 2]]], axis=1) + a_pred = a.clone() + a[0, :, 2, :] = 1 # Set some overlaps, ignoring last row + b = _one_hot([[[0, 0], [1, 1], [2, 3]]], axis=1) + c1 = _one_hot([[[0, 1], [2, 3], [4, 4]]], axis=1, n_classes=8) + c2 = _one_hot([[[4, 5], [6, 7], [4, 4]]], axis=1, n_classes=8) + + a = torch.stack((a, a), axis=1) + a_pred = torch.stack((a_pred, a_pred), axis=1) + b = torch.stack((b, b), axis=1) + c = torch.stack((c1, c2), axis=1) + + if ignore_overlaps: + metric.update(a, a_pred) + assert np.allclose(metric.compute().numpy(), 1.0) + metric.update(a, b) + assert np.allclose(metric.compute().numpy(), 1.0) + metric.reset() + metric.update(a, c) + assert np.allclose(metric.compute().numpy(), 0.0) + + # Fully empty ground truth masks should result in no update + metric.reset() + metric.update(torch.zeros_like(a), b) + assert np.isnan(metric.compute().numpy()) + else: + with pytest.raises(ValueError): + metric.update(a, a_pred) # Not one-hot should raise error + + +def test_adjusted_rand_index(): + # All zeros ground truth mask + assert np.allclose( + metrics.adjusted_rand_index(torch.zeros(1, 1, 4), _one_hot([[0, 0, 1, 1]])).numpy(), 1.0 + ) + + # Examples from scikit-learn + # https://scikit-learn.org/stable/modules/generated/sklearn.metrics.adjusted_rand_score.html + assert np.allclose( + metrics.adjusted_rand_index(_one_hot([[0, 0, 1, 1]]), _one_hot([[0, 0, 1, 1]])).numpy(), 1.0 + ) + assert np.allclose( + metrics.adjusted_rand_index(_one_hot([[0, 0, 1, 1]]), _one_hot([[1, 1, 0, 0]])).numpy(), 1.0 + ) + assert np.allclose( + metrics.adjusted_rand_index(_one_hot([[0, 0, 1, 1]]), _one_hot([[0, 0, 1, 2]])).numpy(), + 0.571428571, + ) + assert np.allclose( + metrics.adjusted_rand_index(_one_hot([[0, 0, 1, 1]]), _one_hot([[0, 1, 2, 3]])).numpy(), 0.0 + ) + assert np.allclose( + metrics.adjusted_rand_index(_one_hot([[0, 0, 1, 1]]), _one_hot([[0, 1, 0, 1]])).numpy(), -0.5 + ) + + +@pytest.mark.parametrize("video_input", [False, True]) +def test_image_iou(video_input): + metric = metrics.ImageIoU(video_input=video_input, matching="overlap") + + a = _one_hot([[[0, 0], [1, 1]]], axis=1) + b = _one_hot([[[0, 1], [2, 3]]], axis=1) + if video_input: + a = torch.stack((a, a), axis=1) + b = torch.stack((b, b), axis=1) + + metric.update(a, a) + assert np.allclose(metric.compute().numpy(), 1.0) + + metric.update(a, b) + assert np.allclose(metric.compute().numpy(), 0.75) + + # Mask not binary should raise error + a[:, :, :, 0] = 2 + with pytest.raises(ValueError): + metric.update(a, b) + a[:, :, :, 0] = 1 + b[:, :, :, 1] = 2 + with pytest.raises(ValueError): + metric.update(a, b) + + +@pytest.mark.parametrize("ignore_overlaps", [False, True]) +@pytest.mark.parametrize("video_input", [False, True]) +def test_image_iou_ignore_overlaps(ignore_overlaps, video_input): + metric = metrics.ImageIoU( + ignore_overlaps=ignore_overlaps, video_input=video_input, matching="overlap" + ) + + a = _one_hot([[[0, 0], [1, 1], [2, 2]]], axis=1) + a_pred = a.clone() + a[0, :, 2, :] = 1 # Set some overlaps, ignoring last row + b = _one_hot([[[0, 0], [1, 1], [2, 3]]], axis=1) + c = _one_hot([[[0, 1], [2, 3], [4, 4]]], axis=1) + if video_input: + a = torch.stack((a, a), axis=1) + a_pred = torch.stack((a_pred, a_pred), axis=1) + b = torch.stack((b, b), axis=1) + c = torch.stack((c, c), axis=1) + + if ignore_overlaps: + metric.update(a, a_pred) + assert np.allclose(metric.compute().numpy(), 1.0) + metric.update(a, b) + assert np.allclose(metric.compute().numpy(), 1.0) + metric.reset() + metric.update(a, c) + assert np.allclose(metric.compute().numpy(), 0.5) + + # Fully empty ground truth masks should result in no update + metric.reset() + metric.update(torch.zeros_like(a), b) + assert np.isnan(metric.compute().numpy()) + else: + with pytest.raises(ValueError): + metric.update(a, a_pred) # Not one-hot should raise error + + +def test_video_iou(): + metric = metrics.VideoIoU(matching="overlap") + + a = _one_hot([[[0, 0], [3, 3]]], axis=1, n_classes=8) + b = _one_hot([[[0, 0], [2, 2]]], axis=1, n_classes=8) + c = _one_hot([[[4, 5], [6, 7]]], axis=1, n_classes=8) + a = torch.stack((a, a), axis=1) + b = torch.stack((b, c), axis=1) + + metric.update(a, a) + assert np.allclose(metric.compute().numpy(), 1.0) + + metric.update(a, b) + assert np.allclose(metric.compute().numpy(), 0.75) + + # Mask not binary should raise error + a[:, :, :, 0] = 2 + with pytest.raises(ValueError): + metric.update(a, b) + a[:, :, :, 0] = 1 + b[:, :, :, 1] = 2 + with pytest.raises(ValueError): + metric.update(a, b) + + +@pytest.mark.parametrize("ignore_overlaps", [False, True]) +def test_video_iou_ignore_overlaps(ignore_overlaps): + metric = metrics.VideoIoU(ignore_overlaps=ignore_overlaps, matching="overlap") + + a = _one_hot([[[0, 0], [1, 1], [2, 2]]], axis=1) + a_pred = a.clone() + a[0, :, 2, :] = 1 # Set some overlaps, ignoring last row + b = _one_hot([[[0, 0], [1, 1], [2, 3]]], axis=1) + c1 = _one_hot([[[0, 0], [2, 2], [4, 4]]], axis=1, n_classes=8) + c2 = _one_hot([[[4, 5], [6, 7], [4, 4]]], axis=1, n_classes=8) + + a = torch.stack((a, a), axis=1) + a_pred = torch.stack((a_pred, a_pred), axis=1) + b = torch.stack((b, b), axis=1) + c = torch.stack((c1, c2), axis=1) + + if ignore_overlaps: + metric.update(a, a_pred) + assert np.allclose(metric.compute().numpy(), 1.0) + metric.update(a, b) + assert np.allclose(metric.compute().numpy(), 1.0) + metric.reset() + metric.update(a, c) + assert np.allclose(metric.compute().numpy(), 0.5) + + # Fully empty ground truth masks should result in no update + metric.reset() + metric.update(torch.zeros_like(a), b) + assert np.isnan(metric.compute().numpy()) + else: + with pytest.raises(ValueError): + metric.update(a, a_pred) # Not one-hot should raise error + + +def test_intersection_over_union_with_matching(): + def call_metric(true, pred, matching, **kwargs): + if not isinstance(true, torch.Tensor): + true = _one_hot(true) + if not isinstance(pred, torch.Tensor): + pred = _one_hot(pred) + values = metrics.intersection_over_union_with_matching( + true, pred, matching=matching, **kwargs + ) + return values.numpy() + + # All zeros ground truth and pred mask + values = call_metric(torch.zeros(1, 1, 2), torch.zeros(1, 1, 2), "none", empty_value=1.0) + assert np.allclose(values, np.array([1.0, 1.0])) + + # All zeros ground truth mask + values = call_metric(torch.zeros(1, 1, 2), [[0, 0, 1, 1]], "none") + assert np.allclose(values, np.array([0.0, 0.0])) + values = call_metric(torch.zeros(1, 1, 4), [[0, 0, 1, 1]], "overlap") + assert np.allclose(values, np.array([0.0, 0.0, 0.0, 0.0])) + values = call_metric(torch.zeros(1, 1, 4), [[0, 0, 1, 1]], "hungarian") + assert np.allclose(values, np.array([0.0, 0.0, 0.0, 0.0])) + + assert np.allclose(call_metric([[0, 0, 1, 1]], [[0, 0, 1, 1]], "none")[0], np.array([1.0, 1.0])) + assert np.allclose( + call_metric([[0, 0, 1, 1]], [[0, 0, 1, 1]], "overlap")[0], np.array([1.0, 1.0]) + ) + assert np.allclose( + call_metric([[0, 0, 1, 1]], [[0, 0, 1, 1]], "hungarian")[0], np.array([1.0, 1.0]) + ) + + assert np.allclose(call_metric([[1, 1, 0, 0]], [[0, 0, 1, 1]], "none")[0], np.array([0.0, 0.0])) + assert np.allclose( + call_metric([[1, 1, 0, 0]], [[0, 0, 1, 1]], "overlap")[0], np.array([1.0, 1.0]) + ) + assert np.allclose( + call_metric([[1, 1, 0, 0]], [[0, 0, 1, 1]], "hungarian")[0], np.array([1.0, 1.0]) + ) + + assert np.allclose( + call_metric([[0, 0, 1, 1]], [[0, 0, 1, 2]], "overlap")[0], np.array([1.0, 0.5]) + ) + assert np.allclose( + call_metric([[0, 0, 1, 1]], [[0, 0, 1, 2]], "hungarian")[0], np.array([1.0, 0.5]) + ) + assert np.allclose( + call_metric([[0, 0, 1, 2]], [[0, 0, 1, 1]], "overlap")[0], np.array([1.0, 0.5, 0.5]) + ) + assert np.allclose( + call_metric([[0, 0, 1, 2]], [[0, 0, 1, 1]], "hungarian")[0], np.array([1.0, 0.5, 0.0]) + ) + assert np.allclose( + call_metric([[0, 0, 1, 1, 1]], [[1, 1, 1, 3, 3]], "overlap")[0], np.array([2 / 3, 2 / 3]) + ) + assert np.allclose( + call_metric([[0, 0, 1, 1, 1, 1]], [[1, 1, 1, 1, 1, 3]], "overlap")[0], np.array([0.4, 0.5]) + ) + assert np.allclose( + call_metric([[0, 0, 1, 1, 1, 1]], [[1, 1, 1, 1, 1, 3]], "hungarian")[0], + np.array([0.4, 0.25]), + ) + + +def test_j_and_f_metric(): + def unpack_result(result): + return {k: v.numpy() for k, v in result.items()} + + metric = metrics.ImageJandF() + + a = _one_hot([[[0, 0], [1, 1]]], axis=1, n_classes=3) + b = _one_hot([[[0, 1], [2, 3]]], axis=1) + + metric.update(a, a) + result = unpack_result(metric.compute()) + assert result == {"j_and_f": 1.0, "jaccard": 1.0, "boundary_f_measure": 1.0} + + metric.update(a, b) + result = unpack_result(metric.compute()) + assert np.allclose(result["jaccard"], (1.0 + 0.5) / 2) + assert np.allclose(result["boundary_f_measure"], (1.0 + 2 / 3) / 2) + assert np.allclose(result["j_and_f"], (1.0 + (0.5 + 2 / 3) / 2) / 2) + + +def test_masks_to_boundaries(): + masks = torch.zeros(2, 32, 32, dtype=torch.bool) + masks[0, 0:9, 0:9] = True + masks[1, 12:20, 12:20] = True + + expected_boundaries = torch.zeros(2, 32, 32, dtype=torch.bool) + expected_boundaries[0, 0, :9] = True + expected_boundaries[0, :9, 0] = True + expected_boundaries[0, 8, :9] = True + expected_boundaries[0, :9, 8] = True + expected_boundaries[1, 12, 12:20] = True + expected_boundaries[1, 12:20, 12] = True + expected_boundaries[1, 19, 12:20] = True + expected_boundaries[1, 12:20, 19] = True + + boundaries = metrics.masks_to_boundaries(masks) + assert np.allclose(boundaries, expected_boundaries) + + +def _one_hot(ids, axis=-1, n_classes=None): + n_classes = n_classes if n_classes else np.max(ids) + 1 + one_hot = F.one_hot(torch.tensor(ids), n_classes) + axis = axis if axis >= 0 else one_hot.ndim + 1 - axis + dims = list(range(one_hot.ndim - 1)) + dims.insert(axis, one_hot.ndim - 1) + one_hot = one_hot.permute(*dims) + return one_hot diff --git a/tests/test_schedulers.py b/tests/test_schedulers.py new file mode 100644 index 0000000..4ac8bdb --- /dev/null +++ b/tests/test_schedulers.py @@ -0,0 +1,50 @@ +import numpy as np +import pytest + +from videosaur import schedulers + + +@pytest.mark.parametrize( + "step, warmup_steps, expected", + [ + (0, 10, 0), + (5, 10, 0.5), + (10, 10, 1.0), + (11, 10, 1.0), + ], +) +def test_linear_warmup(step, warmup_steps, expected): + factor = schedulers.linear_warmup(step, warmup_steps) + assert np.allclose(factor, expected) + + +@pytest.mark.parametrize( + "step, warmup_steps, decay_steps, expected", + [ + (0, 10, 30, 0), + (5, 10, 30, 0.5), + (10, 10, 30, 1.0), + (20, 10, 30, 0.5**0.5), + (30, 10, 30, 0.5), + (40, 10, 30, 0.5**1.5), + ], +) +def test_exp_decay_with_warmup(step, warmup_steps, decay_steps, expected): + factor = schedulers.exp_decay_with_warmup(step, warmup_steps, decay_steps, decay_rate=0.5) + assert np.allclose(factor, expected) + + +@pytest.mark.parametrize( + "step, warmup_steps, decay_steps, expected", + [ + (0, 10, 30, 0), + (5, 10, 30, 0.5), + (10, 10, 30, 1.0), + (20, 10, 30, 0.5), + (30, 10, 30, 0.0), + (31, 10, 30, 0.0), + ], +) +def test_cosine_decay_with_warmup(step, warmup_steps, decay_steps, expected): + factor = schedulers.cosine_decay_with_warmup(step, warmup_steps, decay_steps) + assert np.allclose(factor, expected) diff --git a/tests/test_train.py b/tests/test_train.py new file mode 100644 index 0000000..5cc2c33 --- /dev/null +++ b/tests/test_train.py @@ -0,0 +1,170 @@ +import pathlib + +import pytest + +from videosaur import configuration, models, train + + +def _should_filter_config(config: pathlib.Path) -> bool: + if config.name.startswith("_"): + return True + + if "configs/eval/" in str(config): + return True + + return False + + +TEST_ROOT = pathlib.Path(__file__).parent +PROJ_ROOT = TEST_ROOT.parent +CONFIGS = sorted(p for p in PROJ_ROOT.glob("configs/**/*.yml") if not _should_filter_config(p)) +TEST_CONFIGS = sorted(TEST_ROOT.glob("configs/test_*.yml")) + +DATASETS_WITHOUT_BACKGROUND_MASK = ("coco",) +DATASETS_WITH_OVERLAPPING_MASKS = ("coco",) +DATASETS_WITHOUT_OVERLAPPING_MASKS = ("movi_c", "movi_e", "davis") + + +@pytest.mark.parametrize("config", CONFIGS) +def test_configs_loadable(config): + """Test checking that configs load and dataset and model can be constructed from them.""" + args = train.parser.parse_args( + [ + "--dry", + "--quiet", + str(config), + "trainer.max_steps=0", + "trainer.num_sanity_val_steps=0", + "trainer.accelerator=cpu", + # Do not load pre-trained weights because we can not access them in the CI + "model.load_weights=null", + ] + ) + train.main(args) + + +@pytest.mark.dataset +@pytest.mark.parametrize("config", TEST_CONFIGS) +def test_test_configs(config, tmp_path): + """Integration test checking that training and validation works for test configs.""" + args = train.parser.parse_args( + [ + "--quiet", + f"--log-dir={tmp_path}", + str(config), + "trainer.max_steps=2", + "trainer.val_check_interval=2", + "trainer.num_sanity_val_steps=0", + "trainer.limit_train_batches=2", + "trainer.limit_val_batches=2", + "trainer.limit_test_batches=2", + "trainer.accelerator=cpu", + ] + ) + train.main(args) + + checkpoints = list(tmp_path.glob(f"**/{train.CHECKPOINT_SUBDIR}/*.ckpt")) + assert len(checkpoints) > 0, "Expected a final checkpoint to be saved" + + +@pytest.mark.slow +@pytest.mark.dataset +@pytest.mark.parametrize("config", CONFIGS) +def test_configs(config, tmp_path): + """Integration test checking that training and validation works for configs.""" + args = train.parser.parse_args( + [ + "--quiet", + f"--log-dir={tmp_path}", + str(config), + "trainer.max_steps=2", + "trainer.val_check_interval=2", + "trainer.num_sanity_val_steps=0", + "trainer.limit_train_batches=2", + "trainer.limit_val_batches=2", + "trainer.limit_test_batches=2", + "trainer.accelerator=cpu", + ] + ) + train.main(args) + + checkpoints = list(tmp_path.glob(f"**/{train.CHECKPOINT_SUBDIR}/*.ckpt")) + assert len(checkpoints) > 0, "Expected a final checkpoint to be saved" + + +def test_load_image_model_checkpoint(tmp_path): + # First generate an image-model checkpoint + args = train.parser.parse_args( + [ + "--quiet", + f"--log-dir={tmp_path}", + str(TEST_ROOT / "configs/test_dummy_image.yml"), + "trainer.max_steps=1", + "trainer.val_check_interval=0", + "trainer.num_sanity_val_steps=0", + "trainer.limit_train_batches=1", + "trainer.limit_val_batches=0", + "trainer.limit_test_batches=0", + "trainer.accelerator=cpu", + ] + ) + train.main(args) + + checkpoints = list(tmp_path.glob(f"**/{train.CHECKPOINT_SUBDIR}/*.ckpt")) + assert len(checkpoints) > 0, "Expected a final checkpoint to be saved" + + # Then construct a video model and try to load the checkpoint + config = configuration.load_config(TEST_ROOT / "configs/test_dummy_video.yml") + + model = models.build(config.model, config.optimizer) + model.load_weights_from_checkpoint( + checkpoints[0], + { + "initializer": "initializer", + "processor.module.corrector": "processor.corrector", + "decoder.module": "decoder", + }, + ) + + +@pytest.mark.parametrize("config_path", CONFIGS + TEST_CONFIGS) +def test_config_metrics_settings(config_path): + config = configuration.load_config(config_path) + + def test_properties(split, dataset_name, must_exist=False, **kwargs): + if split == "train": + metric_configs = config.train_metrics if config.train_metrics else {} + else: + metric_configs = config.val_metrics if config.val_metrics else {} + + if f"{split}_shards" in config.dataset and dataset_name in config.dataset[f"{split}_shards"]: + for metric_name, metric_config in metric_configs.items(): + for name, value in kwargs.items(): + if must_exist: + msg = ( + f"Dataset {dataset_name} must have property {name} for " + f"{split} metric {metric_name}" + ) + assert name in metric_config, msg + if name in metric_config: + msg = ( + f"Dataset {dataset_name} must have {name}={value} for " + f"{split} metric {metric_name}" + ) + assert metric_config[name] == value, msg + + # On datasets which do not contain a background mask, we should not let the metric ignore the + # background mask (because there is none to remove). + for dataset in DATASETS_WITHOUT_BACKGROUND_MASK: + test_properties("train", dataset, ignore_background=False) + test_properties("val", dataset, ignore_background=False) + + # On datasets which have mask overlaps, the metric should ignore those overlaps + for dataset in DATASETS_WITH_OVERLAPPING_MASKS: + test_properties("train", dataset, must_exist=True, ignore_overlaps=True) + test_properties("val", dataset, must_exist=True, ignore_overlaps=True) + + # On datasets which do not have mask overlaps, we do not need to set ignore_overlap to true + for dataset in DATASETS_WITHOUT_OVERLAPPING_MASKS: + test_properties("train", dataset, ignore_overlaps=False) + test_properties("val", dataset, ignore_overlaps=False) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..84f95be --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,16 @@ +import pytest + +from videosaur import utils + + +@pytest.mark.parametrize( + "inp,path,value,expected", + [ + ({"a": 1}, "a", 2, {"a": 2}), + ({"a": {"b": 1}}, "a.b", 2, {"a": {"b": 2}}), + ({"a": [1, 2, 3]}, "a.0", 4, {"a": [4, 2, 3]}), + ], +) +def test_write_path(inp, path, value, expected): + utils.write_path(inp, path, value) + assert inp == expected diff --git a/videosaur/configuration.py b/videosaur/configuration.py new file mode 100644 index 0000000..c2a281d --- /dev/null +++ b/videosaur/configuration.py @@ -0,0 +1,151 @@ +import pathlib +from dataclasses import MISSING, dataclass, field +from functools import reduce +from typing import Any, Dict, List, Optional + +from omegaconf import OmegaConf + +ModuleConfig = Dict[str, Any] + + +@dataclass +class ModelConfig: + initializer: ModuleConfig + encoder: ModuleConfig + grouper: ModuleConfig + decoder: ModuleConfig + predictor: Optional[ModuleConfig] = None + target_encoder: Optional[ModuleConfig] = None + latent_processor: Optional[ModuleConfig] = None + mask_resizers: Optional[Dict[str, ModuleConfig]] = None + losses: Optional[Dict[str, ModuleConfig]] = None + loss_weights: Optional[Dict[str, float]] = None + input_type: str = "image" + target_type: str = "features" + target_encoder_input: Optional[str] = None + visualize: bool = False + eval_mode_config: Optional[Dict[str, Any]] = None + visualize_every_n_steps: Optional[int] = 1000 + masks_to_visualize: Optional[List[str]] = None + load_weights: Optional[str] = None + modules_to_load: Optional[Dict[str, str]] = None + + +@dataclass +class Config: + optimizer: ModuleConfig = MISSING + model: ModelConfig = MISSING + dataset: ModuleConfig = MISSING + trainer: Optional[ModuleConfig] = field(default_factory=lambda: {}) + train_metrics: Optional[Dict[str, ModuleConfig]] = None + val_metrics: Optional[Dict[str, ModuleConfig]] = None + + globals: Optional[Dict[str, Any]] = None + experiment_name: Optional[str] = None + experiment_group: Optional[str] = None + seed: Optional[int] = None + checkpoint_every_n_steps: int = 1000 + + +def load_config(path: pathlib.Path, overrides: Optional[List[str]] = None) -> OmegaConf: + schema = OmegaConf.structured(Config) + config = OmegaConf.load(path) + + if overrides is not None: + if isinstance(overrides, list): + + overrides = OmegaConf.from_dotlist(overrides) + elif isinstance(overrides, dict): + overrides = OmegaConf.create(overrides) + else: + ValueError("overrides should be dotlist or dict") + config = OmegaConf.merge(schema, config, overrides) + else: + config = OmegaConf.merge(schema, config) + + return config + + +def override_config( + config: Optional[pathlib.Path] = None, + override_config_path: Optional[pathlib.Path] = None, + additional_overrides: Optional[List[str]] = None, +) -> OmegaConf: + schema = OmegaConf.structured(Config) + config_objects = [schema, config] + if override_config_path is not None: + override_config = OmegaConf.load(override_config_path) + config_objects.append(override_config) + + if additional_overrides is not None: + if isinstance(additional_overrides, list): + + additional_overrides = OmegaConf.from_dotlist(additional_overrides) + elif isinstance(additional_overrides, dict): + additional_overrides = OmegaConf.create(additional_overrides) + else: + ValueError("overrides should be dotlist or dict") + config_objects.append(additional_overrides) + + config = OmegaConf.merge(*config_objects) + return config + + +def save_config(path: pathlib.Path, config: OmegaConf): + OmegaConf.save(config, path, resolve=True) + + +def resolver_eval(fn: str, *args): + params, _, body = fn.partition(":") + if body == "": + body = params + params = "" + + if len(params) == 0: + arg_names = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"] + assert len(args) <= len(arg_names), f"Only up to {len(arg_names)} arguments are supported" + params = ",".join(arg_names[: len(args)]) + + if not params.startswith("lambda "): + params = "lambda " + params + + return eval(f"{params}: {body}")(*args) + + +OmegaConf.register_new_resolver("eval", resolver_eval) +OmegaConf.register_new_resolver("add", lambda *args: sum(args)) +OmegaConf.register_new_resolver("sub", lambda a, b: a - b) +OmegaConf.register_new_resolver("mul", lambda *args: reduce(lambda prod, cur: prod * cur, args, 1)) +OmegaConf.register_new_resolver("div", lambda a, b: a / b) +OmegaConf.register_new_resolver("min", lambda *args: min(*args)) +OmegaConf.register_new_resolver("max", lambda *args: max(*args)) +# should be useful in case of dependencies between config params +OmegaConf.register_new_resolver( + "config_prop", lambda prop, *keys: get_predefined_property(prop, keys) +) + +VIT_PARAMS = { + "vit_small_patch16_224_dino": {"FEAT_DIM": 384, "NUM_PATCHES": 196}, + "vit_small_patch8_224_dino": {"FEAT_DIM": 384, "NUM_PATCHES": 784}, + "vit_base_patch16_224_dino": {"FEAT_DIM": 768, "NUM_PATCHES": 196}, + "vit_base_patch16_448_dino": {"FEAT_DIM": 768, "NUM_PATCHES": 784}, + "vit_base_patch8_224_dino": {"FEAT_DIM": 768, "NUM_PATCHES": 784}, + "vit_base_patch16_224_mae": {"FEAT_DIM": 768, "NUM_PATCHES": 196}, + "vit_base_patch16_224_mocov3": {"FEAT_DIM": 768, "NUM_PATCHES": 196}, + "vit_base_patch16_224_msn": {"FEAT_DIM": 768, "NUM_PATCHES": 196}, + "vit_base_patch14_dinov2": {"FEAT_DIM": 768, "NUM_PATCHES": 256}, + "vit_small_patch14_dinov2": {"FEAT_DIM": 384, "NUM_PATCHES": 256}, + "vit_large_patch14_dinov2": {"FEAT_DIM": 1024, "NUM_PATCHES": 256}, +} + + +def get_predefined_property(prop, keys): + value = globals()[prop] + for key in keys: + if callable(value): + value = value(key) + elif isinstance(value, dict): + value = value[key] + else: + raise ValueError(f"Can not handle type {type(value)}") + return value diff --git a/videosaur/data/__init__.py b/videosaur/data/__init__.py new file mode 100644 index 0000000..73d4b7a --- /dev/null +++ b/videosaur/data/__init__.py @@ -0,0 +1,4 @@ +from videosaur.data.datamodules import build +from videosaur.data.utils import get_data_root_dir + +__all__ = ["build", "get_data_root_dir"] diff --git a/videosaur/data/datamodules.py b/videosaur/data/datamodules.py new file mode 100644 index 0000000..dd5b8a3 --- /dev/null +++ b/videosaur/data/datamodules.py @@ -0,0 +1,611 @@ +import math +import os +import tempfile +from functools import partial +from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union + +import numpy as np +import pytorch_lightning as pl +import torch +import webdataset as wds +from omegaconf import ListConfig +from torch.utils.data._utils import collate as torch_collate + +from videosaur.data import pipelines, transforms +from videosaur.data.utils import get_data_root_dir, worker_init_function +from videosaur.utils import config_as_kwargs + + +def build(config, name: Optional[str] = "WebdatasetDataModule", data_dir: Optional[str] = None): + name = config.get("name") or name + if name == "WebdatasetDataModule": + train_pipeline = None + if config.train_pipeline: + train_pipeline = pipelines.build(config.train_pipeline, shuffle=True) + + val_pipeline = None + if config.val_pipeline: + val_pipeline = pipelines.build(config.val_pipeline, shuffle=False) + + return WebdatasetDataModule( + data_dir=data_dir, + train_pipeline=train_pipeline, + val_pipeline=val_pipeline, + **config_as_kwargs(config, to_filter=("train_pipeline", "val_pipeline")), + ) + elif name == "DummyDataModule": + return DummyDataModule( + train_transforms=transforms.build(config.train_transforms), + val_transforms=transforms.build(config.val_transforms), + **config_as_kwargs( + config, + to_filter=( + "train_transforms", + "val_transforms", + ), + ), + ) + else: + raise ValueError(f"Unknown dataset module `{name}`") + + +class WebdatasetDataModule(pl.LightningDataModule): + """DatasetModule for webdataset datasets. + + We primarily rely on iteration-based instead of epoch-based training. Epoch-based training is + difficult to realize with distributed training (i.e. multi-GPU), because it is hard to ensure + that each sample is sampled exactly once per epoch. Instead, iteration-based training instead + just samples a random stream of data. That said, this module does also support epoch-based + training by setting the `samples_per_epoch` argument. In this case, the dataloader stops after + `samples_per_epoch // (batch_size * num_nodes)` samples. + + For validation/testing, we need to make sure that each sample is seen exactly once. To do so, + this module adds padding entries that should be ignored using the "batch_padding_mask" key. + With distributed training, it is required to specify the number of samples the dataset contains + using the `val_size`, `test_size` arguments. + + The arguments `val_size` and `test_size` refer to the total number of input samples contained + in all shards of the split. As the number of samples can be changed by the data pipeline, it is + the responsibility of the data pipeline to correctly specify how many samples it will output + using the `get_num_samples` method. + """ + + BATCH_PADDING_MASK_KEY = "batch_padding_mask" + + def __init__( + self, + data_dir: Optional[str] = None, + train_shards: Optional[Union[str, List[str]]] = None, + val_shards: Optional[Union[str, List[str]]] = None, + test_shards: Optional[Union[str, List[str]]] = None, + val_size: Optional[int] = None, + test_size: Optional[int] = None, + samples_per_epoch: Optional[int] = None, + train_pipeline: Optional[pipelines.DataPipeline] = None, + val_pipeline: Optional[pipelines.DataPipeline] = None, + test_pipeline: Optional[pipelines.DataPipeline] = None, + batch_size: int = 32, + val_batch_size: Optional[int] = None, + num_workers: int = 0, + num_val_workers: Optional[int] = None, + cache_train: bool = False, + cache_val: bool = False, + cache_dir: Optional[str] = None, + ): + super().__init__() + data_dir = data_dir if data_dir else get_data_root_dir() + + def get_shards_and_num_shards(shards): + if shards is None: + return None, 0 + if isinstance(shards, ListConfig): + new_shards = [] + for s in shards: + new_shards.extend(get_shards_and_num_shards(s)[0]) + shards = new_shards + else: + shards = _to_abs_shard_path(shards, data_dir) + shards = wds.shardlists.expand_urls(shards) + return shards, len(shards) + + self.train_shards, self.num_train_shards = get_shards_and_num_shards(train_shards) + self.val_shards, self.num_val_shards = get_shards_and_num_shards(val_shards) + self.test_shards, self.num_test_shards = get_shards_and_num_shards(test_shards) + self.val_size = val_size + self.test_size = test_size + self.samples_per_epoch = samples_per_epoch + self.train_pipeline = train_pipeline + self.val_pipeline = val_pipeline + self.test_pipeline = test_pipeline if test_pipeline else val_pipeline + self.batch_size = batch_size + self.val_batch_size = val_batch_size if val_batch_size is not None else batch_size + self.num_train_workers = num_workers + self.num_val_workers = num_val_workers if num_val_workers is not None else num_workers + + if cache_dir is None and (cache_train or cache_val): + cache_dir = tempfile.mkdtemp(prefix="wds_shardcache_", dir="/tmp") + self.cache_dir = cache_dir + self.cache_train = cache_train + self.cache_val = cache_val + + self.num_nodes = None # Set lazily + + def __str__(self) -> str: + val_size = "?" if self.val_size is None else self.val_size + test_size = "?" if self.test_size is None else self.test_size + samples_per_epoch = ( + "unspecified" if self.samples_per_epoch is None else self.samples_per_epoch + ) + res = [ + "WebdatasetDataModule", + f" - Number of train shards: {self.num_train_shards}", + f" - Number of val shards: {self.num_val_shards}", + f" - Number of test shards: {self.num_test_shards}", + f" - Assumed number of val samples: {val_size}", + f" - Assumed number of test samples: {test_size}", + f" - Specified length of training epoch: {samples_per_epoch}", + f" - Train batch size: {self.batch_size}", + f" - Eval batch size: {self.val_batch_size}", + f" - Number of train workers: {self.num_train_workers}", + f" - Number of eval workers: {self.num_val_workers}", + ] + return "\n".join(res) + + def _verify_settings_lazy(self): + """Check that we have appropriate settings for the distributed setting. + + We can only do this lazily (once a dataloader is requested) because on __init__ the + number of nodes is not known. + """ + if self.num_nodes is not None: + return + + self.num_nodes = wds.utils.pytorch_worker_info()[1] + + if self.num_nodes > 1: + if self.val_shards and self.val_size is None: + raise ValueError("Need to specify `val_size` in distributed setting") + if self.test_shards and self.test_size is None: + raise ValueError("Need to specify `test_size` in distributed setting") + + def _check_workers_and_shards(split: str, num_workers: int, num_shards: int) -> int: + min_shards_per_node = num_shards // self.num_nodes + + if min_shards_per_node == 0: + raise ValueError( + f"The number of compute nodes is {self.num_nodes}, but the " + f"number of {split} shards is only {num_shards}. Increase the number of shards." + ) + + if num_workers > min_shards_per_node: + raise ValueError( + f"The number of {split} workers is {num_workers}, but the minimum number of " + f"shards per compute node is only {min_shards_per_node}. Reduce the number of " + f"workers to <={min_shards_per_node}." + ) + + if self.train_shards: + _check_workers_and_shards("train", self.num_train_workers, self.num_train_shards) + if self.val_shards: + _check_workers_and_shards("val", self.num_val_workers, self.num_val_shards) + if self.test_shards: + _check_workers_and_shards("test", self.num_val_workers, self.num_test_shards) + + @staticmethod + def _filter_properties( + input_dict: Dict[str, Any], prefixes_to_keep: Tuple[str] + ) -> Dict[str, Any]: + prefixes_to_keep = ("_",) + prefixes_to_keep # Keep underscore properties like "__key__" + out_dict = {} + for key, value in input_dict.items(): + if any(key.startswith(prefix) for prefix in prefixes_to_keep): + out_dict[key] = value + + return out_dict + + @staticmethod + def _remove_extensions(input_dict: Dict[str, Any]) -> Dict[str, Any]: + return {name.split(".")[0]: value for name, value in input_dict.items()} + + @staticmethod + def _pad(dataset: Iterable[Dict[str, Any]], n_samples: int) -> Iterable[Dict[str, Any]]: + """Iterates dataset, then adds dummy samples until reaching the specified number of samples. + + Dummy samples are constructed by copying structure of the first encountered sample. + Also adds a special property "batch_padding_mask" to indicate which entries are padding. + """ + example = None + count = 0 + for sample in dataset: + if example is None: + example = sample + count += 1 + yield {**sample, WebdatasetDataModule.BATCH_PADDING_MASK_KEY: np.array(False)} + + while count < n_samples: + if example is None: + sample = {} # Dataset was empty, should not really happen + else: + sample = { + key: WebdatasetDataModule._get_padding(key, value) + for key, value in example.items() + } + sample[WebdatasetDataModule.BATCH_PADDING_MASK_KEY] = np.array(True) + count += 1 + yield sample + + @staticmethod + def _get_padding(key: str, value: Any): + """Construct padding for property.""" + if isinstance(value, str): + return "PADDING" + elif isinstance(value, torch.Tensor): + return torch.zeros_like(value) + else: + return np.zeros_like(value) + + def _get_max_samples_per_worker( + self, dataset_size: int, num_shards: int, num_workers: int + ) -> int: + """Estimate upper bound on the number of samples per data worker. + + It is only approximate because we don't know the exact composition of the shards. If the + number of samples per shard is very different, this estimate may be too low. + """ + num_workers = max(num_workers, 1) + max_samples_per_shard = int(math.ceil(dataset_size / num_shards)) + max_shards_per_node = int(math.ceil(num_shards / self.num_nodes)) + max_shards_per_worker = int(math.ceil(max_shards_per_node / num_workers)) + return max_shards_per_worker * max_samples_per_shard + + @staticmethod + def _get_webdataset( + urls, resampled=False, splitter=None, cache_size=-1, cache_dir=None + ) -> wds.FluidWrapper: + """Create pipeline object serving same function as wds.WebDataset. + + We do this instead of directly using wds.WebDataset in order to have control over + the `always` argument for caching. + + This method either creates a shuffling, resampling dataset for `resampled=True`, or a + deterministic, non-shuffling for `resample=False`. We do not need other modes for now. + """ + if resampled: + shardlist = wds.shardlists.ResampledShards(urls, deterministic=True) + else: + shardlist = wds.shardlists.SimpleShardList(urls) + + dataset = wds.FluidWrapper(shardlist) + + if not resampled: + if splitter is None: + splitter = wds.shardlists.single_node_only + dataset.append(splitter) + dataset.append(wds.shardlists.split_by_worker) + + handler = wds.filters.reraise_exception + if cache_dir is None or cache_size == 0: + dataset.append(wds.tariterators.tarfile_to_samples(handler=handler)) + else: + assert cache_size == -1 or cache_size > 0 + dataset.append( + wds.cache.cached_tarfile_to_samples( + handler=handler, + verbose=False, + cache_size=cache_size, + cache_dir=cache_dir, + always=True, + ) + ) + + return dataset + + def _get_dataset( + self, + shards: Union[str, List[str]], + shuffle: bool = False, + pipeline: Optional[pipelines.DataPipeline] = None, + padded_size_per_worker: Optional[int] = None, + cache_dir: Optional[str] = None, + cache_size: int = -1, + ): + if shuffle: + # For shuffling samples, we sample shards with replacement. This means that each node + # and worker uses all shards from the dataset (instead of splitting shards). + dataset = self._get_webdataset( + shards, resampled=True, cache_dir=cache_dir, cache_size=cache_size + ) + else: + splitter = ( + wds.shardlists.split_by_node + if self.num_nodes > 1 + else wds.shardlists.single_node_only + ) + dataset = self._get_webdataset( + shards, splitter=splitter, cache_dir=cache_dir, cache_size=cache_size + ) + + # Filter unneeded properties first to avoid decoding them. If pipeline defines no keys, + # keep everything. + if pipeline and pipeline.keys is not None: + dataset = dataset.map( + partial(WebdatasetDataModule._filter_properties, prefixes_to_keep=pipeline.keys) + ) + + dataset = dataset.decode("rgb").map(WebdatasetDataModule._remove_extensions) + + if padded_size_per_worker is not None: + # Pad dataset stream to contain a certain number of samples. This is needed to balance + # data between nodes and workers during validation. Note that `padded_size` here refers + # to the number of samples PER WORKER, not to the total number of samples in the dataset. + dataset = dataset.compose( + partial(WebdatasetDataModule._pad, n_samples=padded_size_per_worker) + ) + # Only add length if we can be sure about the exact number of samples. If we do not add + # padding and only know the global dataset size, this is not the case, because samples + # may be unevenly distributed over nodes and workers. + dataset = dataset.with_length(padded_size_per_worker) + + if pipeline: + dataset = pipeline.apply(dataset) + + if padded_size_per_worker and pipeline.get_num_samples(padded_size_per_worker): + dataset = dataset.with_length(pipeline.get_num_samples(padded_size_per_worker)) + + return dataset + + def _get_dataloader( + self, + dataset, + batch_size: int, + num_workers: int, + num_samples_per_epoch: Optional[int], + partial_batches: bool = False, + ): + assert num_samples_per_epoch is None or num_samples_per_epoch > 0 + + # Do batching within each worker + dataset_batched = dataset.batched( + batch_size, partial=partial_batches, collation_fn=torch_collate.default_collate + ) + + dataloader = wds.WebLoader( + dataset_batched, + num_workers=num_workers, + batch_size=None, + worker_init_fn=worker_init_function, + persistent_workers=num_workers > 0, + # Heuristic to check whether GPUs are used. Misses the case where num_gpus = 1 + pin_memory=(self.num_nodes > 1 and torch.cuda.is_available()), + prefetch_factor=2, + ) + + num_batches_per_epoch = None + if num_samples_per_epoch is not None: + if partial_batches: + num_batches_per_epoch = int( + math.ceil(num_samples_per_epoch / (batch_size * self.num_nodes)) + ) + dataloader = dataloader.with_epoch(num_batches_per_epoch) + else: + num_batches_per_epoch = num_samples_per_epoch // (batch_size * self.num_nodes) + # Equalize batches across nodes and workers for DDP. This may lead to dropping some + # samples and repeating other samples across an epoch in case the shards are + # unevenly distributed across nodes. This seems to be unavoidable with DDP. + # See https://github.com/webdataset/webdataset/issues/225#issuecomment-1344642570 + dataloader = dataloader.repeat(2).with_epoch(num_batches_per_epoch) + else: + # Set dataset to loop indefinitely + dataloader = dataloader.repeat() + + if num_batches_per_epoch: + dataloader = dataloader.with_length(num_batches_per_epoch) + + return dataloader + + def train_dataset(self): + self._verify_settings_lazy() + if self.train_shards is None: + raise ValueError("No training split.") + + return self._get_dataset( + self.train_shards, + shuffle=True, + pipeline=self.train_pipeline, + cache_dir=self.cache_dir if self.cache_train else None, + ) + + def val_dataset(self): + self._verify_settings_lazy() + if self.val_shards is None: + raise ValueError("No validation split.") + + padded_size = self._get_max_samples_per_worker( + self.val_size, self.num_val_shards, self.num_val_workers + ) + return self._get_dataset( + self.val_shards, + shuffle=False, + pipeline=self.val_pipeline, + padded_size_per_worker=padded_size, + cache_dir=self.cache_dir if self.cache_val else None, + ) + + def test_dataset(self): + self._verify_settings_lazy() + if self.test_shards is None: + raise ValueError("No test split.") + + padded_size = self._get_max_samples_per_worker( + self.test_size, self.num_test_shards, self.num_val_workers + ) + return self._get_dataset( + self.test_shards, + shuffle=False, + pipeline=self.test_pipeline, + padded_size_per_worker=padded_size, + cache_dir=self.cache_dir if self.cache_val else None, + ) + + def train_dataloader(self): + return self._get_dataloader( + self.train_dataset(), + batch_size=self.batch_size, + num_workers=self.num_train_workers, + num_samples_per_epoch=self.samples_per_epoch, + partial_batches=False, + ) + + def val_dataloader(self): + dataset = self.val_dataset() + + try: + num_samples_per_worker = len(dataset) + num_samples_per_epoch = ( + num_samples_per_worker * self.num_nodes * max(self.num_val_workers, 1) + ) + except TypeError: + num_samples_per_epoch = None + + return self._get_dataloader( + dataset, + batch_size=self.val_batch_size, + num_workers=self.num_val_workers, + num_samples_per_epoch=num_samples_per_epoch, + partial_batches=True, + ) + + def test_dataloader(self): + dataset = self.val_dataset() + + try: + num_samples_per_worker = len(dataset) + num_samples_per_epoch = ( + num_samples_per_worker * self.num_nodes * max(self.num_val_workers, 1) + ) + except TypeError: + num_samples_per_epoch = None + + return self._get_dataloader( + self.test_dataset(), + batch_size=self.val_batch_size, + num_workers=self.num_val_workers, + num_samples_per_epoch=num_samples_per_epoch, + partial_batches=True, + ) + + +def _to_abs_shard_path( + shards: Union[str, List[str]], data_root_dir: Optional[str] +) -> Union[str, List[str]]: + """Turn relative shard path to absolute by preprending the path to the data root directory.""" + if isinstance(shards, str): + if os.path.isabs(shards): + return shards # Directly use absolute paths + elif "://" in shards: + return shards # Directly use URI's like `s3://abc/xyz` + else: + if data_root_dir is not None: + return os.path.join(data_root_dir, shards) + else: + raise ValueError( + f"Passed relative shard path {shards}, but data root path is missing." + ) + else: + assert isinstance(shards, Iterable), f"Expected iterable but found {type(shards)}" + return [_to_abs_shard_path(shard, data_root_dir) for shard in shards] + + +class DummyDataModule(pl.LightningDataModule): + def __init__( + self, + train_size: int, + val_size: int, + batch_size: int, + shapes: Dict[str, Tuple[int]], + train_transforms: Optional[Callable] = None, + val_transforms: Optional[Callable] = None, + ): + super().__init__() + self.train_size = train_size + self.val_size = val_size + self.batch_size = batch_size + self.shapes = shapes + self.train_transforms = train_transforms + self.val_transforms = val_transforms + + def __str__(self) -> str: + res = ["DummyDataModule"] + res.append(f" - Number of train samples: {self.train_size}") + res.append(f" - Number of val samples: {self.val_size}") + res.append(f" - Number of test samples: {self.test_size}") + res.append(f" - Batch size: {self.batch_size}") + return "\n".join(res) + + @staticmethod + def _make_random_dataset(shapes, size: int, seed: int): + rng = np.random.RandomState(seed) + dataset = [] + for idx in range(size): + data = {"__key__": str(idx)} + for name, shape in shapes.items(): + data[name] = rng.randint(0, 255, size=shape, dtype=np.uint8) + dataset.append(data) + + return dataset + + @staticmethod + def _make_squares_dataset(shapes, size: int, seed: int, n_objects: int = 3): + rng = np.random.RandomState(seed) + dataset = [] + for idx in range(size): + data = {"__key__": str(idx)} + for name, shape in shapes.items(): + height, width = shape[-3:-1] + # Place random black squares on white background + if "mask" in name: + array = np.zeros(shape, dtype=np.uint8) + else: + array = np.ones(shape, dtype=np.uint8) * 255 + for idx in range(n_objects): + x = rng.randint(0, width) + y = rng.randint(0, height) + size = rng.randint(0, int(0.3 * (height + width) / 2)) + if "mask" in name: + array[..., y : y + size, x : x + size, :] = idx + 1 + else: + array[..., y : y + size, x : x + size, :] = 0 + + data[name] = array + dataset.append(data) + + return dataset + + def setup(self, stage): + class Dataset(torch.utils.data.Dataset): + def __init__(self, data, transforms): + super().__init__() + self.data = data + self.transforms = transforms + + def __len__(self): + return len(self.data) + + def __getitem__(self, idx): + data = {**self.data[idx]} # Copy dict + if self.transforms: + for name, transform in self.transforms.items(): + data[name] = transform(data[name]) + + return data + + train_data = self._make_squares_dataset(self.shapes, self.train_size, 42) + self.train_set = Dataset(train_data, self.train_transforms) + val_data = self._make_squares_dataset(self.shapes, self.val_size, 42) + self.val_set = Dataset(val_data, self.val_transforms) + + def train_dataloader(self): + return torch.utils.data.DataLoader(self.train_set, batch_size=self.batch_size, shuffle=True) + + def val_dataloader(self): + return torch.utils.data.DataLoader(self.val_set, batch_size=self.batch_size, shuffle=False) diff --git a/videosaur/data/pipelines.py b/videosaur/data/pipelines.py new file mode 100644 index 0000000..20ff40b --- /dev/null +++ b/videosaur/data/pipelines.py @@ -0,0 +1,230 @@ +import abc +from functools import partial +from typing import Any, Callable, Dict, Optional, Tuple + +import numpy as np +import webdataset as wds + +from videosaur.data import transforms +from videosaur.data.utils import remap_dict +from videosaur.utils import config_as_kwargs + + +def build(config, name: Optional[str] = "VideoPipeline", **kwargs): + name = config.get("name") or name + if name in ("video", "VideoPipeline"): + tfs = transforms.build(config.transforms) if config.transforms else None + pipeline = VideoPipeline( + transforms=tfs, + **config_as_kwargs(config, to_filter=("transforms",), defaults=kwargs), + ) + elif name in ("image", "ImagePipeline"): + tfs = transforms.build(config.transforms) if config.transforms else None + pipeline = ImagePipeline( + transforms=tfs, + **config_as_kwargs(config, to_filter=("transforms",), defaults=kwargs), + ) + else: + raise ValueError(f"Unknown pipeline `{name}`") + + return pipeline + + +class DataPipeline(abc.ABC): + """Abstract base class for data pipelines. + + A data pipeline defines how a stream of raw data samples is transformed to the stream of + output samples. + """ + + def __init__(self, keys: Optional[Tuple[str]]): + self._keys = keys + + @property + def keys(self) -> Optional[Tuple[str]]: + """Keys of properties to keep in dataset after filtering.""" + return self._keys + + @abc.abstractmethod + def get_num_samples(self, num_orig_samples: int) -> Optional[int]: + """Number of samples after pipeline is applied, given original number of samples in dataset. + + If number can not be computed, may return `None`. + """ + ... + + @abc.abstractmethod + def apply(self, dataset: wds.WebDataset) -> wds.WebDataset: + """Apply pipeline to dataset. + + Input dataset contains dicts of samples after decoding. + """ + ... + + +class ImagePipeline(DataPipeline): + def __init__( + self, + transforms: Dict[str, Optional[Callable]] = None, + keys: Optional[Tuple[str]] = None, + is_video_dataset: bool = True, + video_size: Optional[int] = None, + one_frame_per_video: bool = False, + shuffle: bool = False, + shuffle_size: int = 1000, + duplicate: Optional[Dict[str, str]] = None, + ): + super().__init__(keys) + self.transforms = transforms + self.video_size = video_size + self.shuffle = shuffle + self.shuffle_size = shuffle_size + self.is_video_dataset = is_video_dataset + self.one_frame_per_video = one_frame_per_video + self.duplicate = duplicate + + def get_num_samples(self, num_orig_samples: int) -> Optional[int]: + if self.video_size is not None: + return num_orig_samples * self.video_size + else: + return None # Can not provide the number of samples + + def apply(self, dataset: wds.WebDataset) -> wds.WebDataset: + if self.is_video_dataset: + split_fn = partial( + split_to_chunks, + keys_to_split=self.keys, + chunk_size=1, + shuffle=self.shuffle, + one_chunk_per_video=self.one_frame_per_video, + ) + dataset = dataset.compose(split_fn) + rename_video = partial(remap_dict, rename_dict={"video": "image"}) + dataset.map(rename_video) + + if self.shuffle: + # First chunking, then shuffling + dataset = dataset.shuffle(self.shuffle_size) + + if self.duplicate: + dataset.map(partial(copy_dict_entries, copy_from_to=self.duplicate)) + + if self.transforms is not None: + dataset = dataset.map_dict(**self.transforms) + + return dataset + + +class VideoPipeline(DataPipeline): + def __init__( + self, + transforms: Dict[str, Optional[Callable]] = None, + keys: Optional[Tuple[str]] = None, + video_size: Optional[int] = None, + chunk_size: int = 6, + use_chunks: bool = True, + sample_one_chunk_per_video: bool = False, + shuffle: bool = False, + shuffle_size: int = 100, + duplicate: Optional[Dict[str, str]] = None, + ): + super().__init__(keys) + self.transforms = transforms + self.video_size = video_size + self.chunk_size = chunk_size + self.use_chunks = use_chunks + if sample_one_chunk_per_video and not use_chunks: + raise ValueError("For sampling one chunk per video, use_chunks needs to be True") + self.sample_one_chunk_per_video = sample_one_chunk_per_video + self.shuffle = shuffle + self.shuffle_size = shuffle_size + self.duplicate = duplicate + + def get_num_samples(self, num_orig_samples: int) -> Optional[int]: + if self.use_chunks: + if self.sample_one_chunk_per_video: + return num_orig_samples + else: + if self.video_size is not None: + return num_orig_samples * self.video_size // self.chunk_size + else: + return None # Can not provide the number of samples + else: + return num_orig_samples + + def apply(self, dataset: wds.WebDataset) -> wds.WebDataset: + if self.use_chunks: + # If sampling chunks, need to shuffle as well to pick a random chunk + shuffle = self.shuffle or self.sample_one_chunk_per_video + split_fn = partial( + split_to_chunks, + keys_to_split=self.keys, + shuffle=shuffle, + one_chunk_per_video=self.sample_one_chunk_per_video, + chunk_size=self.chunk_size, + ) + dataset = dataset.compose(split_fn) + + if self.shuffle: + # First chunking, then shuffling + dataset = dataset.shuffle(self.shuffle_size) + + if self.duplicate: + dataset.map(partial(copy_dict_entries, copy_from_to=self.duplicate)) + + if self.transforms is not None: + dataset = dataset.map_dict(**self.transforms) + + return dataset + + +def split_to_chunks( + data, + keys_to_split: Tuple[str], + chunk_size: int, + shuffle: bool, + one_chunk_per_video: bool, + axis: int = 0, +): + """Split video to chunks with chunk_size size.""" + for sample in data: + key = sample["__key__"] + video_size = sample[keys_to_split[0]].shape[0] + + num_chunks = video_size // chunk_size + + data_chunks = [ + np.array_split( + sample[key], + range(chunk_size, video_size, chunk_size), + axis=axis, + )[:num_chunks] + for key in keys_to_split + ] + + if shuffle: + chunks_ids = np.random.permutation(range(num_chunks)) + else: + chunks_ids = list(range(num_chunks)) + + if one_chunk_per_video: + chunks_ids = chunks_ids[:1] + + for chunk_id in chunks_ids: + chunked_data = { + key: data_key[chunk_id] for key, data_key in zip(keys_to_split, data_chunks) + } + for key, value in sample.items(): + if key in keys_to_split: + continue + # Data that is not splitted is just repeated across all chunks + chunked_data[key] = value + chunked_data["__key__"] = f"{key}_{chunk_id}" + yield chunked_data + + +def copy_dict_entries(dictionary: Dict[str, Any], copy_from_to: Dict[str, str]) -> Dict[str, Any]: + for from_key, to_key in copy_from_to.items(): + dictionary[to_key] = dictionary[from_key] + + return dictionary diff --git a/videosaur/data/transforms.py b/videosaur/data/transforms.py new file mode 100644 index 0000000..78038a9 --- /dev/null +++ b/videosaur/data/transforms.py @@ -0,0 +1,542 @@ +import math +from collections.abc import Sequence +from functools import partial +from typing import Dict, Optional + +import einops +import numpy as np +import torch +from einops.layers.torch import Rearrange +from torchvision import transforms as tvt +from torchvision.transforms import functional as tvt_functional + +from videosaur.data import transforms_video + +IMAGENET_DEFAULT_MEAN = [0.485, 0.456, 0.406] +IMAGENET_DEFAULT_STD = [0.229, 0.224, 0.225] + +MOVI_DEFAULT_MEAN = [0.5, 0.5, 0.5] +MOVI_DEFAULT_STD = [0.5, 0.5, 0.5] + +DATASET_TYPES = { + "coco": "image", + "davis": "video", + "ytvis": "video", + "movi": "video", + "dummy": "video", + "dummyimage": "image", +} + + +def build(config): + dataset_split = config.name.split("_") + assert len(dataset_split) == 2, "name should be in 'dataset_split' format" + dataset, split = dataset_split + assert dataset in DATASET_TYPES.keys(), f"{dataset} is not supported" + assert split in ["train", "val", "test"] + dataset_type = DATASET_TYPES[dataset] + transform_type = config.get("type", "video") + crop_type = config.get("crop_type", None) + h_flip_prob = config.get("h_flip_prob", None) + size = _to_2tuple(config.input_size) + mask_size = _to_2tuple(config.mask_size) if config.get("mask_size") else size + use_movi_normalization = config.get("use_movi_normalization", False) + + if dataset_type not in ("image", "video"): + raise ValueError(f"Unsupported dataset type {transform_type}") + if transform_type not in ("image", "video"): + raise ValueError(f"Unsupported transform type {transform_type}") + if transform_type == "video": + assert dataset_type == "video" + if dataset_type == "image": + assert transform_type == "image" + + if crop_type is not None: + if split == "train": + assert crop_type in [ + "central", + "random", + "short_side_resize_random", + "short_side_resize_central", + ] + + resize_input = CropResize( + dataset_type=dataset_type, + crop_type=crop_type, + size=size, + resize_mode="bicubic", + clamp_zero_one=True, + ) + if split in ["val", "test"]: + assert crop_type in [ + "central", + "short_side_resize_central", + ], f"Only central crops are supported for {split}." + assert h_flip_prob is None, f"Horizontal flips are not supported for {split}." + resize_segmentation = CropResize( + dataset_type=dataset_type, + crop_type=crop_type, + size=mask_size, + resize_mode="nearest-exact", + ) + else: + resize_input = Resize(size=size, mode="bicubic", clamp_zero_one=True) + resize_segmentation = Resize(size=mask_size, mode="nearest-exact") + + if use_movi_normalization: + normalize = Normalize( + dataset_type=dataset_type, mean=MOVI_DEFAULT_MEAN, std=MOVI_DEFAULT_STD + ) + else: + normalize = Normalize( + dataset_type=dataset_type, mean=IMAGENET_DEFAULT_MEAN, std=IMAGENET_DEFAULT_STD + ) + input_transform = tvt.Compose( + [ + ToTensorInput(dataset_type=dataset_type), + resize_input, + normalize, + ] + ) + if h_flip_prob is not None: + input_transform.transforms.append( + RandomHorizontalFlip(dataset_type=dataset_type, p=h_flip_prob) + ) + if split == "val": + segmentation_transformation = tvt.Compose( + [ + ToTensorMask(), + DenseToOneHotMask(num_classes=config.num_classes), + resize_segmentation, + ] + ) + transforms = {transform_type: input_transform} + + if dataset == "movi": + if "target_size" in config: + target_size = _to_2tuple(config.target_size) + transforms[f"target_{transform_type}"] = tvt.Compose( + [ + ToTensorInput(dataset_type=dataset_type), + Resize(size=target_size, mode="bicubic", clamp_zero_one=True), + normalize, + ] + ) + if split == "val": + transforms["segmentations"] = segmentation_transformation + elif dataset == "davis": + if "target_size" in config: + raise NotImplementedError("Separate targets not implemented for transform `davis`") + if split == "val": + transforms["segmentations"] = tvt.Compose( + [ + ToTensorMask(), + DenseToOneHotMask(num_classes=config.num_classes, remove_zero_masks=True), + resize_segmentation, + ] + ) + elif dataset == "coco": + if "target_size" in config: + raise NotImplementedError("Separate targets not implemented for transform `coco`") + if split == "val": + transforms["segmentations"] = tvt.Compose( + [COCOToBinary(num_classes=config.num_classes), resize_segmentation] + ) + elif dataset == "ytvis": + if "target_size" in config: + raise NotImplementedError("Separate targets not implemented for transform `ytvis`") + + if split == "val": + transforms["segmentations"] = tvt.Compose( + [YTVISToBinary(num_classes=config.num_classes), resize_segmentation] + ) + + elif dataset == "dummy" or dataset == "dummyimage": + if transform_type == "image": + transforms["image"] = tvt.Compose( + [ + tvt.ToTensor(), + normalize, + ] + ) + transforms["masks"] = tvt.Compose( + [ + ToTensorMask(), + DenseToOneHotMask(num_classes=config.num_classes), + ] + ) + elif transform_type == "video": + transforms["video"] = tvt.Compose( + [ + ToTensorInput(dataset_type=dataset_type), + normalize, + ] + ) + transforms["masks"] = tvt.Compose( + [ + ToTensorMask(), + DenseToOneHotMask(num_classes=config.num_classes), + ] + ) + else: + raise ValueError(f"Unknown dataset transforms module `{dataset}`") + if dataset != "dummy": + # At this point in the transforms, videos are in CFHW format. + # Now reorder to FCHW format. + if dataset_type == "video": + transforms[transform_type].transforms.append(CFHWToFCHWFormat()) + if f"target_{transform_type}" in transforms: + transforms[f"target_{transform_type}"].transforms.append(CFHWToFCHWFormat()) + + # We transform image-video datasets as one frame video + # So need to remove first dimension in the end + if transform_type == "image" and dataset_type == "video": + squeeze_video_dim = partial(torch.squeeze, dim=0) + for tf in transforms.values(): + tf.transforms.append(squeeze_video_dim) + + return transforms + + +def _to_2tuple(val): + if val is None: + return None + elif isinstance(val, int): + return (val, val) + else: + return val + + +class CFHWToFCHWFormat: + def __call__(self, tensor: torch.Tensor) -> torch.Tensor: + return torch.permute(tensor, dims=(1, 0, 2, 3)) + + +class ToTensorInput: + def __init__(self, dataset_type) -> None: + self.dataset_type = dataset_type + if dataset_type == "video": + self.to_cfhw_tensor = transforms_video.ToTensorVideo() + + def __call__(self, array: np.ndarray) -> torch.Tensor: + tensor = torch.from_numpy(array) + if self.dataset_type == "video": + tensor = self.to_cfhw_tensor(tensor) + elif self.dataset_type == "image": + tensor = tvt_functional.convert_image_dtype(tensor, dtype=torch.float) + tensor = einops.rearrange(tensor, "h w c -> c h w") + return tensor + + +class Normalize: + def __init__(self, dataset_type: str, mean, std): + if dataset_type == "image": + self.norm = tvt.Normalize(mean=mean, std=std) + elif dataset_type == "video": + self.norm = transforms_video.NormalizeVideo(mean=mean, std=std) + else: + ValueError(f"Not valid dataset type: {dataset_type}") + + def __call__(self, tensor) -> torch.Tensor: + return self.norm(tensor) + + +class RandomHorizontalFlip: + """ + Flip the video or image clip along the horizonal direction with a given probability + Args: + p (float): probability of the clip being flipped. Default value is 0.5 + """ + + def __init__(self, dataset_type: str, p: float = 0.5): + if dataset_type == "image": + self.flip = tvt.RandomHorizontalFlip(p) + elif dataset_type == "video": + self.flip = transforms_video.RandomHorizontalFlipVideo(p) + else: + ValueError(f"Not valid dataset type: {dataset_type}") + + def __call__(self, tensor: torch.Tensor) -> torch.Tensor: + return self.flip(tensor) + + +class CropResize: + def __init__( + self, + dataset_type: str, + crop_type: str, + size: int, + resize_mode: str, + clamp_zero_one: bool = False, + crop_params: Optional[Dict] = None, + ): + assert crop_type in [ + "random", + "central", + "short_side_resize_central", + "short_side_resize_random", + ] + if crop_type == "random" and crop_params is None: + crop_params = {} + + if dataset_type == "video": + self.crop_resize = get_video_crop_resize( + crop_type, crop_params, size, resize_mode, clamp_zero_one + ) + + elif dataset_type == "image": + self.crop_resize = get_image_crop_resize( + crop_type, crop_params, size, resize_mode, clamp_zero_one + ) + else: + raise ValueError(f"Unknown dataset_type dataset_type={dataset_type}") + if clamp_zero_one and crop_type == "random" and resize_mode == "bicubic": + # we clamp here only random crops + # because central are already croped in Resize + self.crop_resize = tvt.Compose([self.crop_resize, partial(torch.clamp, min=0, max=1)]) + + def __call__(self, tensor: torch.Tensor) -> torch.Tensor: + return self.crop_resize(tensor) + + +def get_image_crop_resize(crop_type, crop_params, size, resize_mode, clamp_zero_one): + if crop_type == "short_side_resize_random": + short_side_resize = Resize( + size, + resize_mode, + clamp_zero_one=clamp_zero_one, + short_side_scale=True, + ) + crop = tvt.RandomCrop(size=size) + return tvt.Compose([short_side_resize, crop]) + elif crop_type == "central": + central_crop = CenterFullCrop() + resize = Resize(size, resize_mode, clamp_zero_one=clamp_zero_one) + return tvt.Compose([central_crop, resize]) + elif crop_type == "short_side_resize_central": + short_side_resize = Resize( + size, resize_mode, clamp_zero_one=clamp_zero_one, short_side_scale=True + ) + central_crop = CenterFullCrop() + return tvt.Compose([short_side_resize, central_crop]) + elif crop_type == "random": + return tvt.RandomResizedCrop( + size=size, + interpolation=tvt_functional.InterpolationMode[resize_mode.upper()], + **crop_params, + ) + else: + ValueError(f"Not valid crop_type {crop_type}") + + +def get_video_crop_resize(crop_type, crop_params, size, resize_mode, clamp_zero_one): + if crop_type == "central": + central_crop = transforms_video.CenterFullCropVideo() + resize = Resize(size, resize_mode, clamp_zero_one=clamp_zero_one) + return tvt.Compose([central_crop, resize]) + elif crop_type == "short_side_resize_central": + short_side_resize = Resize( + size, + resize_mode, + clamp_zero_one=clamp_zero_one, + short_side_scale=True, + ) + central_crop = transforms_video.CenterFullCropVideo() + return tvt.Compose([short_side_resize, central_crop]) + elif crop_type == "random": + return transforms_video.RandomResizedCropVideo( + size=size, + interpolation_mode=resize_mode, + **crop_params, + ) + elif crop_type == "short_side_resize_random": + short_side_resize = Resize( + size, + resize_mode, + clamp_zero_one=clamp_zero_one, + short_side_scale=True, + ) + crop = transforms_video.RandomCropVideo(size) + return tvt.Compose( + [ + short_side_resize, + crop, + ] + ) + else: + ValueError(f"Not valid crop_type {crop_type}") + + +class Resize: + def __init__( + self, + size: int, + mode: str, + clamp_zero_one: bool = False, + short_side_scale: bool = False, + ): + + self.mode = mode + self.clamp_zero_one = clamp_zero_one + self.short_side_scale = short_side_scale + if short_side_scale: + if isinstance(size, Sequence): + assert size[0] == size[1] + self.size = size[0] + elif isinstance(size, int): + self.size = size + else: + raise ValueError(f"size should be int or tuple but got {size}") + else: + self.size = size + + def __call__(self, tensor: torch.Tensor) -> torch.Tensor: + is_image = len(tensor.shape) == 3 + if is_image: + tensor = tensor[None] + is_bool = False + if tensor.dtype == torch.bool: + is_bool = True + tensor = tensor.to(torch.uint8) + if self.short_side_scale: + tensor = self.scale_short_side(tensor) + else: + tensor = torch.nn.functional.interpolate(tensor, size=self.size, mode=self.mode) + if self.clamp_zero_one and self.mode == "bicubic": + tensor = torch.clamp(tensor, min=0, max=1) + if is_bool: + tensor = tensor.to(torch.bool) + if is_image: + tensor = tensor[0] + return tensor + + def scale_short_side(self, tensor: torch.Tensor) -> torch.Tensor: + """Scales the shorter spatial dim to the given size. + + To maintain aspect ratio, the longer side is then scaled accordingly. + + Args: + tensor: A 4D tensor of shape (F, C, H, W) or (B, C, H, W) + + Returns: + tensor: Tensor with scaled spatial dims. + """ + assert len(tensor.shape) == 4 + _, _, h, w = tensor.shape + if w < h: + new_h = int(math.floor((float(h) / w) * self.size)) + new_w = self.size + else: + new_h = self.size + new_w = int(math.floor((float(w) / h) * self.size)) + + return torch.nn.functional.interpolate(tensor, size=(new_h, new_w), mode=self.mode) + + +class CenterFullCrop: + def __call__(self, img): + """ + Args: + image (torch.tensor): Image to be cropped. Size is (C, H, W) + Returns: + torch.tensor: central cropping of image. Size is + (C, crop_size, crop_size) + """ + min_size = int(min(img.shape[1:])) + crop_size = (min_size, min_size) + return tvt.functional.center_crop(img, crop_size) + + +class ToTensorMask: + """Transform masks from numpy array uint8 array to float tensor.""" + + def __call__(self, mask: np.ndarray): + assert mask.shape[-1] == 1 + return torch.from_numpy(mask).squeeze(-1).to(torch.float32) + + +class YTVISToBinary: + """Transform YTVIS masks to stardart binary form with shape (I, H, W).""" + + def __init__(self, num_classes: int): + self.num_classes = num_classes + + def __call__(self, mask: torch.Tensor): + f, h, w, num_obj = mask.shape + mask_binary = torch.zeros(f, h, w, self.num_classes, dtype=torch.bool) + mask = torch.from_numpy(mask != 0).to(torch.bool) + mask_binary[..., :num_obj] = mask + mask_binary = einops.rearrange(mask_binary, "f h w i -> f i h w") + return mask_binary + + +class COCOToBinary: + """Transform COCO masks to stardart binary form with shape (I, H, W).""" + + def __init__(self, num_classes: int): + self.num_classes = num_classes + + def __call__(self, mask: torch.Tensor): + num_obj, h, w = mask.shape + mask_binary = torch.zeros(self.num_classes, h, w, dtype=torch.bool) + mask = torch.from_numpy(mask != 0).to(torch.bool) + mask_binary[:num_obj] = mask + return mask_binary + + +class DenseToOneHotMask: + """Transform dense mask of shape (..., H, W) to (..., I, H, W). + + Potentially removes one-hot dim that corresponds to zeros. + It is useful if all the instances are non-zero encoded, + whereas zeros correspond to unlabeled pixels (e.g. in DAVIS dataset). + """ + + def __init__(self, num_classes: int, remove_zero_masks: bool = False): + self.num_classes = num_classes + self.remove_zero_masks = remove_zero_masks + + def __call__(self, mask: torch.Tensor): + if self.remove_zero_masks: + mask_oh = torch.nn.functional.one_hot(mask.to(torch.long), self.num_classes + 1) + mask_oh = mask_oh[..., 1:] + else: + mask_oh = torch.nn.functional.one_hot(mask.to(torch.long), self.num_classes) + if mask_oh.dim() == 3: + mask_oh = einops.rearrange(mask_oh, "h w i -> i h w") + elif mask_oh.dim() == 4: + mask_oh = einops.rearrange(mask_oh, "f h w i ->f i h w") + return mask_oh.to(torch.bool) + + +class Denormalize: + """ + Denormalization transform for both image and video inputs. + + In case of videos, expected format is FCHW + as we apply Denormalize after switch from CFHW to FCHW. + """ + + def __init__(self, input_type, mean=IMAGENET_DEFAULT_MEAN, std=IMAGENET_DEFAULT_STD) -> None: + if input_type == "video": + denormalize = transforms_video.DenormalizeVideo(mean=mean, std=std) + self.denormalize = tvt.Compose( + [ + Rearrange("F C H W -> C F H W"), + denormalize, + Rearrange("C F H W -> F C H W"), + ] + ) + + elif input_type == "image": + self.denormalize = DenormalizeImage(mean=mean, std=std) + + def __call__(self, tensor: torch.Tensor) -> torch.Tensor: + return self.denormalize(tensor) + + +class DenormalizeImage(tvt.Normalize): + def __init__(self, mean, std): + new_mean = [-m / s for m, s in zip(mean, std)] + new_std = [1 / s for s in std] + super().__init__(new_mean, new_std) diff --git a/videosaur/data/transforms_video.py b/videosaur/data/transforms_video.py new file mode 100644 index 0000000..e4c42bd --- /dev/null +++ b/videosaur/data/transforms_video.py @@ -0,0 +1,339 @@ +# code from torchvision: +# https://github.com/pytorch/vision/blob/main/torchvision/transforms/_transforms_video.py + +import numbers +import random + +import einops +import torch +from torchvision.transforms import RandomCrop, RandomResizedCrop + +__all__ = [ + "RandomCropVideo", + "RandomResizedCropVideo", + "CenterCropVideo", + "NormalizeVideo", + "ToTensorVideo", + "RandomHorizontalFlipVideo", +] + + +class RandomCropVideo(RandomCrop): + def __init__(self, size): + if isinstance(size, numbers.Number): + self.size = (int(size), int(size)) + else: + self.size = size + + def __call__(self, clip): + """ + Args: + clip (torch.tensor): Video clip to be cropped. Size is (C, F, H, W) + Returns: + torch.tensor: randomly cropped/resized video clip. + size is (C, F, OH, OW) + """ + i, j, h, w = self.get_params(clip, self.size) + return crop(clip, i, j, h, w) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(size={self.size})" + + +class RandomResizedCropVideo(RandomResizedCrop): + def __init__( + self, + size, + scale=(0.08, 1.0), + ratio=(3.0 / 4.0, 4.0 / 3.0), + interpolation_mode="bilinear", + ): + if isinstance(size, tuple): + if len(size) != 2: + raise ValueError(f"size should be tuple (height, width), instead got {size}") + self.size = size + else: + self.size = (size, size) + + self.interpolation_mode = interpolation_mode + self.scale = scale + self.ratio = ratio + + def __call__(self, clip): + """ + Args: + clip (torch.tensor): Video clip to be cropped. Size is (C, F, H, W) + Returns: + torch.tensor: randomly cropped/resized video clip. + size is (C, F, H, W) + """ + i, j, h, w = self.get_params(clip, self.scale, self.ratio) + return resized_crop(clip, i, j, h, w, self.size, self.interpolation_mode) + + def __repr__(self) -> str: + repr_ = ( + f"{self.__class__.__name__}(size={self.size}," + + f"interpolation_mode={self.interpolation_mode}," + + f"scale={self.scale}, ratio={self.ratio})" + ) + return repr_ + + +class CenterCropVideo: + def __init__(self, crop_size): + if isinstance(crop_size, numbers.Number): + self.crop_size = (int(crop_size), int(crop_size)) + else: + self.crop_size = crop_size + + def __call__(self, clip): + """ + Args: + clip (torch.tensor): Video clip to be cropped. Size is (C, F, H, W) + Returns: + torch.tensor: central cropping of video clip. Size is + (C, F, crop_size, crop_size) + """ + return center_crop(clip, self.crop_size) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(crop_size={self.crop_size})" + + +class CenterFullCropVideo: + def __call__(self, clip): + """ + Args: + clip (torch.tensor): Video to be cropped. Size is (C, F, H, W) + Returns: + torch.tensor: central cropping of video clip. Size is + (C, F, crop_size, crop_size) + """ + min_size = int(min(clip.shape[2:])) + crop_size = (min_size, min_size) + return center_crop(clip, crop_size) + + +class NormalizeVideo: + """ + Normalize the video clip by mean subtraction and division by standard deviation + Args: + mean (3-tuple): pixel RGB mean + std (3-tuple): pixel RGB standard deviation + inplace (boolean): whether do in-place normalization + """ + + def __init__(self, mean, std, inplace=False): + self.mean = mean + self.std = std + self.inplace = inplace + + def __call__(self, clip): + """ + Args: + clip (torch.tensor): video clip to be normalized. Size is (C, F, H, W) + """ + return normalize(clip, self.mean, self.std, self.inplace) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(mean={self.mean}, std={self.std}, inplace={self.inplace})" + + +class DenormalizeVideo(NormalizeVideo): + def __init__(self, mean, std): + new_mean = [-m / s for m, s in zip(mean, std)] + new_std = [1 / s for s in std] + super().__init__(new_mean, new_std) + + +class ToTensorVideo: + """ + Convert tensor data type from uint8 to float, divide value by 255.0 and + permute the dimensions of clip tensor + """ + + def __init__(self): + pass + + def __call__(self, clip): + """ + Args: + clip (torch.tensor, dtype=torch.uint8): Size is (F, H, W, C) + Return: + clip (torch.tensor, dtype=torch.float): Size is (C, F, H, W) + """ + return to_tensor(clip) + + def __repr__(self) -> str: + return self.__class__.__name__ + + +class FromTensorVideo: + """ + Permute the dimensions of clip tensor, multiply value by 255.0 and + convert tensor data type from float to unit8. + """ + + def __init__(self): + pass + + def __call__(self, clip): + """ + Args: + clip (torch.tensor, dtype=torch.float): Size is (C, F, H, W) + Return: + clip (torch.tensor, dtype=torch.uint8): Size is (F, H, W, C) + """ + return from_tensor(clip) + + def __repr__(self) -> str: + return self.__class__.__name__ + + +class RandomHorizontalFlipVideo: + """ + Flip the video clip along the horizonal direction with a given probability + Args: + p (float): probability of the clip being flipped. Default value is 0.5 + """ + + def __init__(self, p=0.5): + self.p = p + + def __call__(self, clip): + """ + Args: + clip (torch.tensor): Size is (C, F, H, W) + Return: + clip (torch.tensor): Size is (C, F, H, W) + """ + if random.random() < self.p: + clip = hflip(clip) + return clip + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(p={self.p})" + + +def _is_tensor_video_clip(clip): + if not torch.is_tensor(clip): + raise TypeError("clip should be Tensor. Got %s" % type(clip)) + + if not clip.ndimension() == 4: + raise ValueError("clip should be 4D. Got %dD" % clip.dim()) + + return True + + +def crop(clip, i, j, h, w): + """ + Args: + clip (torch.tensor): Video clip to be cropped. Size is (C, F, H, W) + """ + if len(clip.size()) != 4: + raise ValueError("clip should be a 4D tensor") + return clip[..., i : i + h, j : j + w] + + +def resize(clip, target_size, interpolation_mode): + if len(target_size) != 2: + raise ValueError(f"target size should be tuple (height, width), instead got {target_size}") + return torch.nn.functional.interpolate( + clip, size=target_size, mode=interpolation_mode, align_corners=False + ) + + +def resized_crop(clip, i, j, h, w, size, interpolation_mode="bilinear"): + """ + Do spatial cropping and resizing to the video clip + Args: + clip (torch.tensor): Video clip to be cropped. Size is (C, F, H, W) + i (int): i in (i,j) i.e coordinates of the upper left corner. + j (int): j in (i,j) i.e coordinates of the upper left corner. + h (int): Height of the cropped region. + w (int): Width of the cropped region. + size (tuple(int, int)): height and width of resized clip + Returns: + clip (torch.tensor): Resized and cropped clip. Size is (C, F, H, W) + """ + if not _is_tensor_video_clip(clip): + raise ValueError("clip should be a 4D torch.tensor") + clip = crop(clip, i, j, h, w) + clip = resize(clip, size, interpolation_mode) + return clip + + +def center_crop(clip, crop_size): + if not _is_tensor_video_clip(clip): + raise ValueError("clip should be a 4D torch.tensor") + h, w = clip.size(-2), clip.size(-1) + th, tw = crop_size + if h < th or w < tw: + raise ValueError("height and width must be no smaller than crop_size") + + i = int(round((h - th) / 2.0)) + j = int(round((w - tw) / 2.0)) + return crop(clip, i, j, th, tw) + + +def to_tensor(clip): + """ + Convert tensor data type from uint8 to float, divide value by 255.0 and + permute the dimensions of clip tensor + Args: + clip (torch.tensor, dtype=torch.uint8): Size is (F, H, W, C) + Return: + clip (torch.tensor, dtype=torch.float): Size is (C, F, H, W) + """ + _is_tensor_video_clip(clip) + if not clip.dtype == torch.uint8: + raise TypeError("clip tensor should have data type uint8. Got %s" % str(clip.dtype)) + return clip.float().permute(3, 0, 1, 2) / 255.0 + + +def from_tensor(clip): + """ + Permute the dimensions of clip tensor multiply value by 255.0 and + convert tensor data type from float to uint8. + + Args: + clip (torch.tensor, dtype=torch.float): Size is (C, F, H, W) + Return: + clip (torch.tensor, dtype=torch.uint8): Size is (F, H, W, C) + """ + _is_tensor_video_clip(clip) + if not clip.dtype == torch.float: + raise TypeError("clip tensor should have data type float. Got %s" % str(clip.dtype)) + clip = einops.rearrange(clip, "C F H W -> F H W C") + return (clip * 255.0).byte() + + +def normalize(clip, mean, std, inplace=False): + """ + Args: + clip (torch.tensor): Video clip to be normalized. Size is (C, F, H, W) + mean (tuple): pixel RGB mean. Size is (3) + std (tuple): pixel standard deviation. Size is (3) + Returns: + normalized clip (torch.tensor): Size is (C, F, H, W) + """ + if not _is_tensor_video_clip(clip): + raise ValueError("clip should be a 4D torch.tensor") + if not inplace: + clip = clip.clone() + mean = torch.as_tensor(mean, dtype=clip.dtype, device=clip.device) + std = torch.as_tensor(std, dtype=clip.dtype, device=clip.device) + clip.sub_(mean[:, None, None, None]).div_(std[:, None, None, None]) + return clip + + +def hflip(clip): + """ + Args: + clip (torch.tensor): Video clip to be normalized. Size is (C, F, H, W) + Returns: + flipped clip (torch.tensor): Size is (C, F, H, W) + """ + if not _is_tensor_video_clip(clip): + raise ValueError("clip should be a 4D torch.tensor") + return clip.flip(-1) diff --git a/videosaur/data/utils.py b/videosaur/data/utils.py new file mode 100644 index 0000000..26f5867 --- /dev/null +++ b/videosaur/data/utils.py @@ -0,0 +1,63 @@ +import logging +import os +import random +from typing import Any, Dict, Optional + +import numpy as np +import torch + +log = logging.getLogger(__name__) + +DEFAULT_DATA_DIR = "./data" + + +def get_data_root_dir(error_on_missing_path: bool = False) -> Optional[str]: + data_dir = os.environ.get("VIDEOSAUR_DATA_DIR") + if data_dir and not os.path.isdir(data_dir) and error_on_missing_path: + raise ValueError(f"Path {data_dir} specified by VIDEOSAUR_DATA_DIR does not exists") + + if data_dir is None and os.path.isdir(DEFAULT_DATA_DIR): + data_dir = DEFAULT_DATA_DIR + + if data_dir is None and error_on_missing_path: + raise ValueError( + "No data root dir found. Check that you are on " + f"a machine with access to {DEFAULT_DATA_DIR}" + ) + + return data_dir + + +def worker_init_function(worker_id: int, rank: Optional[int] = None) -> None: + """Dataloader worker init function settings unique random seeds per worker. + + Copied from Pytorch Lightning. + """ + from pytorch_lightning.utilities import rank_zero_only + + # implementation notes: https://github.com/pytorch/pytorch/issues/5059#issuecomment-817392562 + global_rank = rank if rank is not None else rank_zero_only.rank + process_seed = torch.initial_seed() + # back out the base seed so we can use all the bits + base_seed = process_seed - worker_id + log.debug( + f"Initializing random number generators of process {global_rank} worker {worker_id} " + f"with base seed {base_seed}" + ) + ss = np.random.SeedSequence([base_seed, worker_id, global_rank]) + # use 128 bits (4 x 32-bit words) + np.random.seed(ss.generate_state(4)) + # Spawn distinct SeedSequences for the PyTorch PRNG and the stdlib random module + torch_ss, stdlib_ss = ss.spawn(2) + torch.manual_seed(torch_ss.generate_state(1, dtype=np.uint64)[0]) + # use 128 bits expressed as an integer + stdlib_seed = (stdlib_ss.generate_state(2, dtype=np.uint64).astype(object) * [1 << 64, 1]).sum() + random.seed(stdlib_seed) + + +def remap_dict(sample: Dict[str, Any], rename_dict: Dict[str, str]): + "Rename the keys of the dict." + for k, v in rename_dict.items(): + sample[v] = sample[k] + del sample[k] + return sample diff --git a/videosaur/eval.py b/videosaur/eval.py new file mode 100644 index 0000000..7722383 --- /dev/null +++ b/videosaur/eval.py @@ -0,0 +1,160 @@ +import argparse +import logging +import os +import pathlib +import random +import warnings +from typing import Any, Dict, Optional + +import pytorch_lightning as pl +import torch +from omegaconf import OmegaConf +from pytorch_lightning.utilities import rank_zero_info as log_info + +from videosaur import configuration, data, metrics, models, utils + +TENSORBOARD_SUBDIR = "tb" +METRICS_SUBDIR = "metrics" + +parser = argparse.ArgumentParser() +group = parser.add_mutually_exclusive_group() +group.add_argument("-v", "--verbose", action="store_true", help="Be verbose") +group.add_argument("-q", "--quiet", action="store_true", help="Suppress outputs") +parser.add_argument("-n", "--dry", action="store_true", help="Dry run (no logfiles)") +parser.add_argument( + "--no-interactive", action="store_true", help="If running in non-interactive environment" +) +parser.add_argument("--data-dir", help="Path to data directory") +parser.add_argument("--config", help="Configuration to run") +parser.add_argument("--log-dir", help="Path to experiment log directory") +parser.add_argument( + "--config-file", default="settings.yaml", help="Path to experiment log directory" +) +parser.add_argument("config_overrides", nargs="*", help="Additional arguments") + + +def _setup_trainer_config(trainer_config: Dict[str, Any]) -> Dict[str, Any]: + # Let Pytorch Lightning select the device if not specified otherwise. + if "accelerator" not in trainer_config: + trainer_config["accelerator"] = "auto" + + # Automatically select DDP as strategy if possible and not specified otherwise. + if ( + "strategy" not in trainer_config + and trainer_config.get("accelerator") != "cpu" + and trainer_config.get("devices") != 1 + and torch.cuda.is_available() + and torch.cuda.device_count() > 1 + ): + strategy = "ddp_find_unused_parameters_false" + if "find_unused_parameters" in trainer_config: + if trainer_config["find_unused_parameters"]: + strategy = "ddp" + del trainer_config["find_unused_parameters"] + log_info(f"Setting distributed strategy to {strategy}.") + trainer_config["strategy"] = strategy + + return trainer_config + + +def _setup_loggers(args, log_path: pathlib.Path) -> Dict[str, pl.loggers.logger.Logger]: + if args.dry: + return {} + + # Tensorboard logs go to // + logger_tensorboard = pl.loggers.TensorBoardLogger( + save_dir=log_path, name=TENSORBOARD_SUBDIR, version="" + ) + # CSV logs go to //version_N/metrics.csv, where N is the number of + # restarts of the job + logger_csv = pl.loggers.CSVLogger(save_dir=log_path, name=METRICS_SUBDIR) + + return {"tensorboard": logger_tensorboard, "csv": logger_csv} + + +def main(args, config_overrides=None): + if config_overrides is None: + config_overrides = args.config_overrides + config_file = os.path.join(args.log_dir, args.config_file) + config = configuration.load_config(config_file) + if args.config: + config = configuration.override_config(config, args.config, config_overrides) + else: + config = configuration.override_config(config, additional_overrides=config_overrides) + + if not args.verbose or args.quiet: + logging.getLogger("pytorch_lightning").setLevel(logging.ERROR) + from pytorch_lightning.utilities.warnings import PossibleUserWarning + + warnings.filterwarnings("ignore", category=PossibleUserWarning) + if args.quiet: + warnings.filterwarnings("ignore", category=UserWarning) + + # Setup log path + log_path: Optional[pathlib.Path] = None + if not args.dry: + evaluation_dir = config.get("experiment_name", "eval") + log_path = pathlib.Path(args.log_dir, evaluation_dir) + log_path.mkdir(parents=True, exist_ok=True) + + log_info(f"Using {log_path} as evaluation output directory") + + # Setup random seed + if "PL_GLOBAL_SEED" in os.environ: + # Retrieve random seed from the main process in the DDP setting + seed = int(os.environ["PL_GLOBAL_SEED"]) + elif config.seed is not None: + seed = config.seed + else: + seed = random.randint(0, 2**32 - 1) + log_info(f"Using random seed {seed}.") + config.seed = pl.seed_everything(seed, workers=True) + + dataset = data.build(config.dataset, data_dir=args.data_dir) + if args.verbose: + log_info(str(dataset)) + + train_metrics = None + + if config.val_metrics is not None: + val_metrics = {name: metrics.build(config) for name, config in config.val_metrics.items()} + else: + raise ValueError("Eval metric should be validation metrics") + + model = models.build(config.model, config.optimizer, train_metrics, val_metrics) + if config.model.load_weights is not None: + weights_path = config.model.load_weights + else: + weights_path = utils.find_last_checkpoint(pathlib.Path(args.log_dir)) + assert weights_path is not None, "No checkpoint found" + if os.path.exists(weights_path): + model.load_weights_from_checkpoint(weights_path, module_mapping=config.model.modules_to_load) + else: + raise ValueError(f"Checkpoint file {weights_path} doesn't exist.") + loggers = _setup_loggers(args, log_path) + trainer_config = _setup_trainer_config(config.setdefault("trainer", {})) + + # Save the final configuration + + log_info(f"Configuration:\n{OmegaConf.to_yaml(config, resolve=True)}") + + # When running DDP, expose log path to other processes through environment variable + if "strategy" in trainer_config and trainer_config["strategy"].startswith("ddp"): + os.environ["VIDEOSAUR_LOG_PATH"] = str(log_path) + + trainer = pl.Trainer( + max_epochs=-1, # We control training duration using `max_steps` + check_val_every_n_epoch=None, # We do not use epochs for training + default_root_dir=log_path, + callbacks=[], + logger=[logger for logger in loggers.values()] if loggers else False, + enable_progress_bar=(not args.quiet and not args.no_interactive), + enable_model_summary=not args.quiet, + enable_checkpointing=False, + **trainer_config, + ) + trainer.validate(model=model, datamodule=dataset) + + +if __name__ == "__main__": + main(parser.parse_args()) diff --git a/videosaur/losses.py b/videosaur/losses.py new file mode 100644 index 0000000..10291a9 --- /dev/null +++ b/videosaur/losses.py @@ -0,0 +1,167 @@ +from typing import Any, Dict, Optional, Tuple + +import einops +import torch +from torch import nn + +from videosaur import modules, utils + + +@utils.make_build_fn(__name__, "loss") +def build(config, name: str): + target_transform = None + if config.get("target_transform"): + target_transform = modules.build_module(config.get("target_transform")) + + cls = utils.get_class_by_name(__name__, name) + if cls is not None: + return cls( + target_transform=target_transform, + **utils.config_as_kwargs(config, ("target_transform",)), + ) + else: + raise ValueError(f"Unknown loss `{name}`") + + +class Loss(nn.Module): + """Base class for loss functions. + + Args: + video_inputs: If true, assume inputs contain a time dimension. + patch_inputs: If true, assume inputs have a one-dimensional patch dimension. If false, + assume inputs have height, width dimensions. + pred_dims: Dimensions [from, to) of prediction tensor to slice. Useful if only a + subset of the predictions should be used in the loss, i.e. because the other dimensions + are used in other losses. + remove_last_n_frames: Number of frames to remove from the prediction before computing the + loss. Only valid with video inputs. Useful if the last frame does not have a + correspoding target. + target_transform: Transform that can optionally be applied to the target. + """ + + def __init__( + self, + pred_key: str, + target_key: str, + video_inputs: bool = False, + patch_inputs: bool = True, + pred_dims: Optional[Tuple[int, int]] = None, + remove_last_n_frames: int = 0, + target_transform: Optional[nn.Module] = None, + input_key: Optional[str] = None, + ): + super().__init__() + self.pred_path = pred_key.split(".") + self.target_path = target_key.split(".") + self.video_inputs = video_inputs + self.patch_inputs = patch_inputs + self.input_key = input_key + self.n_expected_dims = 2 + (1 if patch_inputs else 2) + (1 if video_inputs else 0) + + if pred_dims is not None: + assert len(pred_dims) == 2 + self.pred_dims = slice(pred_dims[0], pred_dims[1]) + else: + self.pred_dims = None + + self.remove_last_n_frames = remove_last_n_frames + if remove_last_n_frames > 0 and not video_inputs: + raise ValueError("`remove_last_n_frames > 0` only valid with `video_inputs==True`") + + self.target_transform = target_transform + self.to_canonical_dims = self.get_dimension_canonicalizer() + + def get_dimension_canonicalizer(self) -> torch.nn.Module: + """Return a module which reshapes tensor dimensions to (batch, n_positions, n_dims).""" + if self.video_inputs: + if self.patch_inputs: + pattern = "B F P D -> B (F P) D" + else: + pattern = "B F D H W -> B (F H W) D" + else: + if self.patch_inputs: + return torch.nn.Identity() + else: + pattern = "B D H W -> B (H W) D" + + return einops.layers.torch.Rearrange(pattern) + + def get_target(self, inputs: Dict[str, Any], outputs: Dict[str, Any]) -> torch.Tensor: + target = utils.read_path(outputs, elements=self.target_path, error=False) + if target is None: + target = utils.read_path(inputs, elements=self.target_path) + + target = target.detach() + + if self.target_transform: + with torch.no_grad(): + if self.input_key is not None: + target = self.target_transform(target, inputs[self.input_key]) + else: + target = self.target_transform(target) + + # Convert to dimension order (batch, positions, dims) + target = self.to_canonical_dims(target) + + return target + + def get_prediction(self, outputs: Dict[str, Any]) -> torch.Tensor: + prediction = utils.read_path(outputs, elements=self.pred_path) + if prediction.ndim != self.n_expected_dims: + raise ValueError( + f"Prediction has {prediction.ndim} dimensions (and shape {prediction.shape}), but " + f"expected it to have {self.n_expected_dims} dimensions." + ) + + if self.video_inputs and self.remove_last_n_frames > 0: + prediction = prediction[:, : -self.remove_last_n_frames] + + # Convert to dimension order (batch, positions, dims) + prediction = self.to_canonical_dims(prediction) + + if self.pred_dims: + prediction = prediction[..., self.pred_dims] + + return prediction + + def forward(self, prediction: torch.Tensor, target: torch.Tensor) -> torch.Tensor: + raise NotImplementedError("Implement in subclasses") + + +class TorchLoss(Loss): + """Wrapper around PyTorch loss functions.""" + + def __init__( + self, + pred_key: str, + target_key: str, + loss: str, + loss_kwargs: Optional[Dict[str, Any]] = None, + **kwargs, + ): + super().__init__(pred_key, target_key, **kwargs) + loss_kwargs = loss_kwargs if loss_kwargs is not None else {} + if hasattr(torch.nn, loss): + self.loss_fn = getattr(torch.nn, loss)(reduction="mean", **loss_kwargs) + else: + raise ValueError(f"Loss function torch.nn.{loss} not found") + + # Cross entropy loss wants dimension order (batch, classes, positions) + self.positions_last = loss == "CrossEntropyLoss" + + def forward(self, prediction: torch.Tensor, target: torch.Tensor) -> torch.Tensor: + if self.positions_last: + prediction = prediction.transpose(-2, -1) + target = target.transpose(-2, -1) + + return self.loss_fn(prediction, target) + + +class MSELoss(TorchLoss): + def __init__(self, pred_key: str, target_key: str, **kwargs): + super().__init__(pred_key, target_key, loss="MSELoss", **kwargs) + + +class CrossEntropyLoss(TorchLoss): + def __init__(self, pred_key: str, target_key: str, **kwargs): + super().__init__(pred_key, target_key, loss="CrossEntropyLoss", **kwargs) diff --git a/videosaur/metrics.py b/videosaur/metrics.py new file mode 100644 index 0000000..99ac117 --- /dev/null +++ b/videosaur/metrics.py @@ -0,0 +1,981 @@ +import math +from typing import Dict, Optional, Sequence, Tuple + +import einops +import numpy as np +import torch +import torchmetrics + +from videosaur.utils import make_build_fn + + +@make_build_fn(__name__, "metric") +def build(config, name: str): + pass # No special module building needed + + +class Metric(torchmetrics.Metric): + def __init__(self, input_mapping: Dict[str, str], **kwargs) -> None: + super().__init__(**kwargs) + # Mapping from parameter in _update to name in inputs dict + self.input_mapping = input_mapping + + def update(self, *args, **kwargs): + inputs = {} + for mapped_key, input_key in self.input_mapping.items(): + if input_key is None: + continue + if input_key not in kwargs: + raise ValueError( + f"Key {input_key} not found in inputs to metric. " + f"Available inputs are: {list(kwargs)}" + ) + inputs[mapped_key] = kwargs[input_key] + + return self._update(*args, **inputs) + + def _update(self, *args, **kwargs): + raise NotImplementedError("Implement in subclasses") + + +class ImageMaskMetricMixin: + """Mixin class for mask-based per-image metrics. + + Handles shape checking and rearranging of inputs. + + Args: + video_input: If true, assumes additional frame dimension as input. + Each frame is treated as an independent image for ARI computation. + flatten_spatially: If true, flatten spatial dimensions into a single dimension. + move_classes_last: If true, move classes to the last dimension. + """ + + def __init__( + self, + video_input: bool, + *args, + flatten_spatially: bool = True, + move_classes_last: bool = True, + **kwargs, + ): + super().__init__(*args, **kwargs) + self.video_input = video_input + self.rearrange_pattern = self._get_rearrange_pattern( + video_input, flatten_spatially, move_classes_last + ) + + @staticmethod + def _get_rearrange_pattern(video_input, flatten_spatially, move_classes_last) -> str: + if video_input: + # For video input, the temporal dimension is folded into the batch dimension, i.e. + # all frames are treated independently from the downstream metrics. + if flatten_spatially: + if move_classes_last: + pattern = "b t c h w -> (b t) (h w) c" + else: + pattern = "b t c h w -> (b t) c (h w)" + else: + if move_classes_last: + pattern = "b t c h w -> (b t) h w c" + else: + pattern = "b t c h w -> (b t) c h w" + else: + if flatten_spatially: + if move_classes_last: + pattern = "b c h w -> b (h w) c" + else: + pattern = "b c h w -> b c (h w)" + else: + if move_classes_last: + pattern = "b c h w -> b h w c" + else: + pattern = "b c h w -> b c h w" + + return pattern + + def _update(self, true_mask: torch.Tensor, pred_mask: torch.Tensor): + """Update metric. + + Args: + true_mask: Binary true masks of shape (batch [, n_frames], n_true_classes, height, + width). + pred_mask: One-hot predicted masks of shape (batch [, n_frames], n_pred_classes, height, + width). + """ + if self.video_input: + _check_shape( + true_mask, + (None, None, None, None, None), + "true_mask [bs, n_frames, n_true_classes, h, w]", + ) + b, t, _, h, w = true_mask.shape + _check_shape( + pred_mask, + (b, t, None, h, w), + "pred_mask [bs, n_frames, n_pred_classes, h, w]", + ) + else: + _check_shape(true_mask, (None, None, None, None), "true_mask [bs, n_true_classes, h, w]") + b, _, h, w = true_mask.shape + _check_shape(pred_mask, (b, None, h, w), "pred_mask [bs, n_pred_classes, h, w]") + + true_mask = einops.rearrange(true_mask, self.rearrange_pattern) + pred_mask = einops.rearrange(pred_mask, self.rearrange_pattern) + + return super()._update(true_mask, pred_mask) + + +class VideoMaskMetricMixin: + """Mixin class for mask-based per-video metrics. + + Handles shape checking and rearranging of inputs. + + Args: + flatten_temporally: If true, flatten temporal dimensions into the spatial dimensions. + In this case, frames are vertically concatenated. + flatten_spatially: If true, flatten spatial dimensions into a single dimension. + move_classes_last: If true, move classes to the last dimension. + """ + + def __init__( + self, + *args, + flatten_temporally: bool = True, + flatten_spatially: bool = True, + move_classes_last: bool = True, + **kwargs, + ): + super().__init__(*args, **kwargs) + self.rearrange_pattern = self._get_rearrange_pattern( + flatten_temporally, flatten_spatially, move_classes_last + ) + + @staticmethod + def _get_rearrange_pattern( + flatten_temporally: bool, flatten_spatially: bool, move_classes_last: bool + ) -> str: + if flatten_temporally: + if flatten_spatially: + if move_classes_last: + pattern = "b t c h w -> b (t h w) c" + else: + pattern = "b t c h w -> b c (t h w)" + else: + # Temporal dimension is folded into the height dimension, i.e. all frames are + # vertically concatenated. + if move_classes_last: + pattern = "b t c h w -> b (t h) w c" + else: + pattern = "b t c h w -> b c (t h) w" + else: + if flatten_spatially: + if move_classes_last: + pattern = "b t c h w -> b t (h w) c" + else: + pattern = "b t c h w -> b t c (h w)" + else: + if move_classes_last: + pattern = "b t c h w -> b t h w c" + else: + pattern = "b t c h w -> b t c h w" + + return pattern + + def _update(self, true_mask: torch.Tensor, pred_mask: torch.Tensor): + """Update metric. + + Args: + true_mask: Binary true masks of shape (batch, n_frames, n_true_classes, height, width). + pred_mask: One-hot predicted masks of shape (batch, n_frames, n_pred_classes, height, + width). + """ + _check_shape( + true_mask, + (None, None, None, None, None), + "true_mask [bs, n_frames, n_true_classes, h, w]", + ) + b, t, _, h, w = true_mask.shape + _check_shape( + pred_mask, + (b, t, None, h, w), + "pred_mask [bs, n_frames, n_pred_classes, h, w]", + ) + true_mask = einops.rearrange(true_mask, self.rearrange_pattern) + pred_mask = einops.rearrange(pred_mask, self.rearrange_pattern) + + return super()._update(true_mask, pred_mask) + + +class AdjustedRandIndex(Metric): + """Abstract ARI metric.""" + + higher_is_better = True + full_state_update = False + + def __init__( + self, + ignore_background: bool = False, + ignore_overlaps: bool = False, + pred_key: Optional[str] = None, + true_key: Optional[str] = None, + ): + super().__init__(input_mapping={"pred_mask": pred_key, "true_mask": true_key}) + self.ignore_background = ignore_background + self.ignore_overlaps = ignore_overlaps + self.add_state( + "values", default=torch.tensor(0.0, dtype=torch.float64), dist_reduce_fx="sum" + ) + self.add_state("total", default=torch.tensor(0), dist_reduce_fx="sum") + + def _update(self, true_mask: torch.Tensor, pred_mask: torch.Tensor): + """Update metric. + + Args: + true_mask: Binary true masks of shape (batch, n_points, n_true_classes) + pred_mask: One-hot predicted masks of shape (batch, n_points, n_pred_classes) + """ + assert true_mask.ndim == 3 + assert pred_mask.ndim == 3 + if torch.any((true_mask != 0.0) & (true_mask != 1.0)): + raise ValueError("`true_mask` is not binary") + if torch.any((pred_mask != 0.0) & (pred_mask != 1.0)): + raise ValueError("`pred_mask` is not binary") + if torch.any(pred_mask.sum(dim=-1) != 1.0): + raise ValueError("`pred_mask` is not one-hot") + + n_true_classes_per_point = true_mask.sum(dim=-1) + if not self.ignore_overlaps and torch.any(n_true_classes_per_point > 1.0): + raise ValueError("There are overlaps in `true_mask`.") + if self.ignore_background and torch.any(n_true_classes_per_point != 1.0): + raise ValueError("`true_mask` is not one-hot") + + if self.ignore_overlaps: + overlaps = n_true_classes_per_point > 1.0 + true_mask = true_mask.clone() + true_mask[overlaps] = 0.0 # ARI ignores pixels where all ground truth clusters are zero + + if self.ignore_background: + true_mask = true_mask[..., 1:] # Remove the background mask + + values = adjusted_rand_index(true_mask, pred_mask) + + # Special case: skip samples without any ground truth mask + non_empty = n_true_classes_per_point.sum(dim=-1) > 0 + values = values[non_empty] + + self.values += values.sum() + self.total += len(values) + + def compute(self): + return self.values / self.total + + +class ImageARI(ImageMaskMetricMixin, AdjustedRandIndex): + """ARI metric for images. + + Inputs to metric: + true_mask: Binary true masks of shape (batch [, n_frames], n_true_classes, height, + width). + pred_mask: One-hot predicted masks of shape (batch [, n_frames], n_pred_classes, height, + width). + + Args: + video_input: If true, assumes additional frame dimension as input. + Each frame is treated as an independent image for metric computation. + ignore_background: If true, assume first dimension of true masks is background to ignore. + ignore_overlaps: If true, ignore pixels from overlapping instances. + """ + + def __init__( + self, + video_input: bool = False, + ignore_background: bool = False, + ignore_overlaps: bool = False, + pred_key: Optional[str] = None, + true_key: Optional[str] = None, + ): + super().__init__(video_input, ignore_background, ignore_overlaps, pred_key, true_key) + + +class VideoARI(VideoMaskMetricMixin, AdjustedRandIndex): + """ARI metric for videos. + + Inputs to metric: + true_mask: Binary true masks of shape (batch, n_frames, n_true_classes, height, width). + pred_mask: One-hot predicted masks of shape (batch, n_frames, n_pred_classes, height, + width). + + Args: + ignore_background: If true, assume first dimension of true masks is background to ignore. + ignore_overlaps: If true, ignore pixels from overlapping instances. + """ + + def __init__( + self, + ignore_background: bool = False, + ignore_overlaps: bool = False, + pred_key: Optional[str] = None, + true_key: Optional[str] = None, + ): + super().__init__(ignore_background, ignore_overlaps, pred_key, true_key) + + +def adjusted_rand_index( + true_mask: torch.Tensor, + pred_mask: torch.Tensor, +) -> torch.Tensor: + """Computes the adjusted Rand index (ARI), a clustering similarity score. + + Adapted to Pytorch from SAVi Jax implementation: + https://github.com/google-research/slot-attention-video/blob/main/savi/lib/metrics.py + + Args: + true_mask: A binary tensor of shape (batch_size, n_points, n_true_clusters). The true cluster + assignment encoded as one-hot with missing values allowed. + pred_mask: A binary tensor of shape (batch_size, n_points, n_pred_clusters). The predicted + cluster assignment encoded as one-hot. + + Returns: + ARI scores as a tensor of shape (batch_size,). + """ + N = torch.einsum("bpc, bpk -> bck", true_mask.to(torch.float64), pred_mask.to(torch.float64)) + A = torch.sum(N, axis=-1) # row-sum (batch_size, c) + B = torch.sum(N, axis=-2) # col-sum (batch_size, k) + num_points = torch.sum(A, axis=1) + + rindex = torch.sum(N * (N - 1), axis=[1, 2]) + aindex = torch.sum(A * (A - 1), axis=1) + bindex = torch.sum(B * (B - 1), axis=1) + expected_rindex = aindex * bindex / torch.clip(num_points * (num_points - 1), min=1) + max_rindex = (aindex + bindex) / 2 + denominator = max_rindex - expected_rindex + ari = (rindex - expected_rindex) / denominator + + # There are two cases for which the denominator can be zero: + # 1. If both label_pred and label_true assign all pixels to a single cluster. + # (max_rindex == expected_rindex == rindex == num_points * (num_points-1)) + # 2. If both label_pred and label_true assign max 1 point to each cluster. + # (max_rindex == expected_rindex == rindex == 0) + # In both cases, we want the ARI score to be 1.0: + return torch.where(denominator != 0.0, ari, 1.0) + + +class IntersectionOverUnion(Metric): + """Abstract IoU metric.""" + + higher_is_better = True + full_state_update = False + + def __init__( + self, + ignore_background: bool = False, + ignore_overlaps: bool = False, + matching: str = "none", + pred_key: Optional[str] = None, + true_key: Optional[str] = None, + ): + super().__init__(input_mapping={"pred_mask": pred_key, "true_mask": true_key}) + self.ignore_background = ignore_background + self.ignore_overlaps = ignore_overlaps + self.matching = matching + if matching not in ("none", "overlap", "hungarian"): + raise ValueError("`matching` needs to be 'none' or 'overlap' or 'hungarian'") + self.add_state( + "values", default=torch.tensor(0.0, dtype=torch.float64), dist_reduce_fx="sum" + ) + self.add_state("total", default=torch.tensor(0), dist_reduce_fx="sum") + + def _update(self, true_mask: torch.Tensor, pred_mask: torch.Tensor): + """Update metric. + + Args: + true_mask: Binary true masks of shape (batch, n_points, n_true_classes) + pred_mask: One-hot predicted masks of shape (batch, n_points, n_pred_classes) + """ + assert true_mask.ndim == 3 + assert pred_mask.ndim == 3 + if torch.any((true_mask != 0.0) & (true_mask != 1.0)): + raise ValueError("`true_mask` is not binary") + if torch.any((pred_mask != 0.0) & (pred_mask != 1.0)): + raise ValueError("`pred_mask` is not binary") + if torch.any(pred_mask.sum(dim=-1) != 1.0): + raise ValueError("`pred_mask` is not one-hot") + + n_true_classes_per_point = true_mask.sum(dim=-1) + if not self.ignore_overlaps and torch.any(n_true_classes_per_point > 1.0): + raise ValueError("There are overlaps in `true_mask`.") + if self.ignore_background and torch.any(n_true_classes_per_point != 1.0): + raise ValueError("`true_mask` is not one-hot") + if self.ignore_overlaps: + overlaps = n_true_classes_per_point > 1.0 + true_mask = true_mask.clone() + true_mask[overlaps] = 0.0 + pred_mask = pred_mask.clone() + pred_mask[overlaps] = 0.0 + + if self.ignore_background: + true_mask = true_mask[..., 1:] # Remove the background mask + + values = intersection_over_union_with_matching( + true_mask, pred_mask, self.matching, empty_value=0.0 + ) + active_true_classes = true_mask.sum(dim=1) > 0 + + # Compute mean IoU, ignoring empty true classes. This assumes that true-pred class pairs + # with union==0 have been assigned zero IoU. + n_true_classes = active_true_classes.sum(dim=-1) + mean_iou = values.sum(dim=-1) / n_true_classes + + # Special case: skip samples without any ground truth mask + non_empty = n_true_classes > 0 + mean_iou = mean_iou[non_empty] + + self.values += mean_iou.sum() + self.total += len(mean_iou) + + def compute(self): + return self.values / self.total + + +class ImageIoU(ImageMaskMetricMixin, IntersectionOverUnion): + """IoU metric for images. + + Inputs to metric: + true_mask: Binary true masks of shape (batch [, n_frames], n_true_classes, height, + width). + pred_mask: One-hot predicted masks of shape (batch [, n_frames], n_pred_classes, height, + width). + + Args: + video_input: If true, assumes additional frame dimension as input. + Each frame is treated as an independent image for metric computation. + ignore_background: If true, assume first dimension of true masks is background to ignore. + ignore_overlaps: If true, ignore pixels from overlapping instances. + matching: How to match true classes to predicted classes. For "none", assume classes are + ordered, i.e. the true class at index i corresponds to the predicted class at index i. + For "overlap", match the predicted class with the highest IoU to each true class. + """ + + def __init__( + self, + video_input: bool = False, + ignore_background: bool = False, + ignore_overlaps: bool = False, + matching: str = "none", + pred_key: Optional[str] = None, + true_key: Optional[str] = None, + ): + super().__init__( + video_input, ignore_background, ignore_overlaps, matching, pred_key, true_key + ) + + +class VideoIoU(VideoMaskMetricMixin, IntersectionOverUnion): + """IoU metric for videos. + + Inputs to metric: + true_mask: Binary true masks of shape (batch, n_frames, n_true_classes, height, width). + pred_mask: One-hot predicted masks of shape (batch, n_frames, n_pred_classes, height, + width). + + Args: + ignore_background: If true, assume first dimension of true masks is background to ignore. + ignore_overlaps: If true, ignore pixels from overlapping instances. + matching: How to match true classes to predicted classes. For "none", assume classes are + ordered, i.e. the true class at index i corresponds to the predicted class at index i. + For "overlap", match the predicted class with the highest IoU to each true class. + """ + + def __init__( + self, + ignore_background: bool = False, + ignore_overlaps: bool = False, + matching: str = "none", + pred_key: Optional[str] = None, + true_key: Optional[str] = None, + ): + super().__init__(ignore_background, ignore_overlaps, matching, pred_key, true_key) + + +def intersection_over_union_with_matching( + true_mask: torch.Tensor, + pred_mask: torch.Tensor, + matching: str = "none", + empty_value: float = 0.0, +) -> torch.Tensor: + """Computes mask intersection-over-union, matching predicted masks to ground truth masks. + + Args: + true_mask: A binary tensor of shape (batch_size, n_points, n_true_classes). The true class + mask encoded as one-hot with missing values allowed. + pred_mask: A binary tensor of shape (batch_size, n_points, n_pred_classes). The predicted + class mask encoded as one-hot with missing values allowed. + matching: How to match true classes to predicted classes. For "none", assume classes are + ordered, i.e. the true class at index i corresponds to the predicted class at index i. + For "overlap", match the predicted class with the highest IoU to each true class. + empty_value: Value to assume for the case when a class does not occur in the ground truth, + and was also not predicted. + + Returns: + IoU scores as a tensor of shape (batch_size, n_true_classes). + """ + assert matching in ("none", "overlap", "hungarian") + + pairwise_ious = intersection_over_union(true_mask, pred_mask, empty_value) + + if matching == "none": + if pairwise_ious.shape[1] != pairwise_ious.shape[2]: + raise ValueError( + "For matching 'none', n_true_classes needs to equal n_pred_classes, but is " + f"{pairwise_ious.shape[1]} vs {pairwise_ious.shape[2]}" + ) + ious = torch.diagonal(pairwise_ious, dim1=-2, dim2=-1) + elif matching == "overlap": + ious = torch.max(pairwise_ious, dim=2).values + elif matching == "hungarian": + all_true_idxs, all_pred_idxs = hungarian_matching(pairwise_ious, maximize=True) + ious = torch.zeros( + true_mask.shape[0], true_mask.shape[2], dtype=torch.float64, device=pairwise_ious.device + ) + for idx, (true_idxs, pred_idxs) in enumerate(zip(all_true_idxs, all_pred_idxs)): + ious[idx, true_idxs] = pairwise_ious[idx, true_idxs, pred_idxs] + else: + raise ValueError(f"Unknown matching for IoU `{matching}`") + + assert ious.shape == (true_mask.shape[0], true_mask.shape[2]) + return ious + + +def intersection_over_union( + true_mask: torch.Tensor, + pred_mask: torch.Tensor, + empty_value: float = 0.0, +): + """Compute pairwise intersection-over-union between predicted and ground truth masks. + + Args: + true_mask: A binary tensor of shape (batch_size, n_points, n_true_classes). The true class + mask encoded as one-hot with missing values allowed. + pred_mask: A binary tensor of shape (batch_size, n_points, n_pred_classes). The predicted + class mask encoded as one-hot with missing values allowed. + empty_value: Value to assume for the case when a class does not occur in the ground truth, + and was also not predicted. + + Returns: + Pairwise IoU scores as a tensor of shape (batch_size, n_true_classes, n_pred_classes).""" + intersection, fp, fn = confusion_matrix(true_mask, pred_mask) # all B x C x K + union = intersection + fp + fn + ious = intersection / union + + # Deal with NaN from divide-by-zero (class does not occur and was not predicted) + ious[union == 0] = empty_value + + return ious + + +def confusion_matrix(true_mask: torch.Tensor, pred_mask: torch.Tensor): + """Computes confusion matrix between two sets of masks. + + Args: + true_mask: A binary tensor of shape (batch_size, n_points, n_true_classes). The true class + mask encoded as one-hot with missing values allowed. + pred_mask: A binary tensor of shape (batch_size, n_points, n_pred_classes). The predicted + class mask encoded as one-hot with missing values allowed. + + Returns: + Tuple containing the pairwise true positives (intersection), false positives and false + negatives, all of shape (batch_size, n_true_classes, n_pred_classes). + """ + true_mask = true_mask.to(torch.float64) + pred_mask = pred_mask.to(torch.float64) + + true_positives = torch.einsum("bpc, bpk -> bck", true_mask, pred_mask) # B x C x K + n_true_points = true_mask.sum(1) # B x C + n_pred_points = pred_mask.sum(1) # B x K + + false_positives = n_pred_points.unsqueeze(1) - true_positives # B x C x K + false_negatives = n_true_points.unsqueeze(2) - true_positives # B x C x K + + return true_positives, false_positives, false_negatives + + +class JandFMetric(Metric): + """Abstract Jaccard and F-score metric used in video object discovery. + + See also official implementation from DAVIS challenge: + https://github.com/davisvideochallenge/davis2017-evaluation/blob/master/davis2017/metrics.py + """ + + higher_is_better = True + full_state_update = False + + def __init__( + self, + ignore_background: bool = False, + ignore_overlaps: bool = False, + pred_key: Optional[str] = None, + true_key: Optional[str] = None, + metric_for_matching: str = "j_and_f", + ): + super().__init__(input_mapping={"pred_mask": pred_key, "true_mask": true_key}) + self.ignore_background = ignore_background + self.ignore_overlaps = ignore_overlaps + if metric_for_matching not in ("j_and_f", "jaccard", "f_measure"): + raise ValueError( + "Matching should be one of 'j_and_f', 'jaccard', 'f_measure', but is " + f"{metric_for_matching}" + ) + self.metric_for_matching = metric_for_matching + self.add_state( + "j_and_f", default=torch.tensor(0.0, dtype=torch.float64), dist_reduce_fx="sum" + ) + self.add_state( + "jaccard", default=torch.tensor(0.0, dtype=torch.float64), dist_reduce_fx="sum" + ) + self.add_state( + "f_measure", default=torch.tensor(0.0, dtype=torch.float64), dist_reduce_fx="sum" + ) + self.add_state("total", default=torch.tensor(0), dist_reduce_fx="sum") + + @staticmethod + def _aggregate_from_pairwise_values(pairwise_values, true_idxs, pred_idxs, n_true_classes): + """ + + Args: + values: tensor of shape (batch, n_true_classes, n_pred_classes) + true_idxs: tensor of shape (batch, n_true_classes) + pred_idxs: tensor of shape (batch, n_pred_classes) + n_true_classes: tensor of shape (batch,) + + Returns: + Tensor of shape (batch_filtered,) where batch_filtered is batch size minus the number + of true masks without any active mask, containing the mean metric value per sample. + """ + values = torch.zeros( + pairwise_values.shape[:2], dtype=torch.float64, device=pairwise_values.device + ) + for idx, (t, p) in enumerate(zip(true_idxs, pred_idxs)): + values[idx, t] = pairwise_values[idx, t, p] + + # Compute mean values, ignoring empty true masks. This assumes that empty masks have been + # assigned a value of zero. + values = values.sum(dim=-1) / n_true_classes + + # Special case: skip samples without any ground truth mask + non_empty = n_true_classes > 0 + values = values[non_empty] + + return values + + def _update(self, true_mask: torch.Tensor, pred_mask: torch.Tensor): + """Update metric. + + Args: + true_mask: Binary true masks of shape (batch, [n_frames,] n_true_classes, height, width) + pred_mask: One-hot predicted masks of shape (batch, [n_frames,] n_pred_classes, height, + width) + """ + assert true_mask.ndim == pred_mask.ndim == 4 or true_mask.ndim == pred_mask.ndim == 5 + batch_size = len(true_mask) + if true_mask.ndim == 4: + # Insert dummy video dimension if we are passed images + true_mask = true_mask.unsqueeze(1) + pred_mask = pred_mask.unsqueeze(1) + + if torch.any((true_mask != 0.0) & (true_mask != 1.0)): + raise ValueError("`true_mask` is not binary") + if torch.any((pred_mask != 0.0) & (pred_mask != 1.0)): + raise ValueError("`pred_mask` is not binary") + if torch.any(pred_mask.sum(dim=2) != 1.0): + raise ValueError("`pred_mask` is not one-hot") + + n_true_classes_per_point = true_mask.sum(dim=2) + if not self.ignore_overlaps and torch.any(n_true_classes_per_point > 1.0): + raise ValueError("There are overlaps in `true_mask`.") + if self.ignore_background and torch.any(n_true_classes_per_point != 1.0): + raise ValueError("`true_mask` is not one-hot") + if self.ignore_overlaps: + overlaps = n_true_classes_per_point > 1.0 + true_mask = true_mask.clone() + m = einops.repeat(overlaps, "b t h w -> b t c h w", c=true_mask.shape[2]) + true_mask[m] = 0.0 + pred_mask = pred_mask.clone() + m = einops.repeat(overlaps, "b t h w -> b t k h w", k=pred_mask.shape[2]) + pred_mask[m] = 0.0 + + if self.ignore_background: + true_mask = true_mask[:, :, 1:] # Remove the background mask + + # Jaccard is computed frame-by-frame in the original J & F implementation. + all_jaccard = intersection_over_union( + einops.rearrange(true_mask, "b t c h w -> (b t) (h w) c"), + einops.rearrange(pred_mask, "b t k h w -> (b t) (h w) k"), + empty_value=0.0, + ) # B x true_classes x pred_classes + all_jaccard = einops.rearrange(all_jaccard, "(b t) c k -> b t c k", b=batch_size) + all_jaccard = all_jaccard.mean(1) + + # Boundary f-measure is computed frame-by-frame + all_f_measure, _, _ = boundary_f_measure( + einops.rearrange(true_mask, "b t c h w -> (b t) c h w"), + einops.rearrange(pred_mask, "b t k h w -> (b t) k h w"), + ) # (B * frames) x true_classes x pred_classes + all_f_measure = einops.rearrange(all_f_measure, "(b t) c k -> b t c k", b=batch_size) + all_f_measure = all_f_measure.mean(1) + + all_j_and_f = (all_jaccard + all_f_measure) / 2 + + if self.metric_for_matching == "j_and_f": + true_idxs, pred_idxs = hungarian_matching(all_j_and_f, maximize=True) + elif self.metric_for_matching == "jaccard": + true_idxs, pred_idxs = hungarian_matching(all_jaccard, maximize=True) + elif self.metric_for_matching == "f_measure": + true_idxs, pred_idxs = hungarian_matching(all_f_measure, maximize=True) + else: + raise ValueError(f"Unknown matching {self.metric_for_matching}") + + active_true_classes = true_mask.sum(dim=(1, -2, -1)) > 0 # B x true_classes + n_true_classes = active_true_classes.sum(dim=-1) + + j_and_f = self._aggregate_from_pairwise_values( + all_j_and_f, true_idxs, pred_idxs, n_true_classes + ) + jaccard = self._aggregate_from_pairwise_values( + all_jaccard, true_idxs, pred_idxs, n_true_classes + ) + f_measure = self._aggregate_from_pairwise_values( + all_f_measure, true_idxs, pred_idxs, n_true_classes + ) + + self.j_and_f += j_and_f.sum() + self.jaccard += jaccard.sum() + self.f_measure += f_measure.sum() + self.total += len(j_and_f) + + def compute(self): + return { + "j_and_f": self.j_and_f / self.total, + "jaccard": self.jaccard / self.total, + "boundary_f_measure": self.f_measure / self.total, + } + + +class ImageJandF(ImageMaskMetricMixin, JandFMetric): + """J&F metric for images. + + Inputs to metric: + true_mask: Binary true masks of shape (batch [, n_frames], n_true_classes, height, + width). + pred_mask: One-hot predicted masks of shape (batch [, n_frames], n_pred_classes, height, + width). + + Args: + video_input: If true, assumes additional frame dimension as input. + Each frame is treated as an independent image for metric computation. + ignore_background: If true, assume first dimension of true masks is background to ignore. + ignore_overlaps: If true, ignore pixels from overlapping instances. + """ + + def __init__( + self, + video_input: bool = False, + ignore_background: bool = False, + ignore_overlaps: bool = False, + pred_key: Optional[str] = None, + true_key: Optional[str] = None, + metric_for_matching: str = "j_and_f", + ): + super().__init__( + video_input, + ignore_background, + ignore_overlaps, + pred_key, + true_key, + flatten_spatially=False, + move_classes_last=False, + metric_for_matching=metric_for_matching, + ) + + +class VideoJandF(VideoMaskMetricMixin, JandFMetric): + """J&F metric for videos. + + Inputs to metric: + true_mask: Binary true masks of shape (batch, n_frames, n_true_classes, height, width). + pred_mask: One-hot predicted masks of shape (batch, n_frames, n_pred_classes, height, + width). + + Args: + ignore_background: If true, assume first dimension of true masks is background to ignore. + ignore_overlaps: If true, ignore pixels from overlapping instances. + """ + + def __init__( + self, + ignore_background: bool = False, + ignore_overlaps: bool = False, + pred_key: Optional[str] = None, + true_key: Optional[str] = None, + metric_for_matching: str = "j_and_f", + ): + super().__init__( + ignore_background, + ignore_overlaps, + pred_key, + true_key, + flatten_temporally=False, + flatten_spatially=False, + move_classes_last=False, + metric_for_matching=metric_for_matching, + ) + + +def boundary_f_measure(true_mask: torch.Tensor, pred_mask: torch.Tensor): + """Computes pairwise F-measure on boundaries of masks. + + Args: + true_mask: A binary tensor of shape (batch_size, n_true_classes, height, width). The true + masks encoded as one-hot with missing values allowed. + pred_mask: A binary tensor of shape (batch_size, n_pred_classes, height, width). The + predicted masks encoded as one-hot with missing values allowed. + + Returns: + Tuple containing the pairwise f-measure, precision and recall, all of shape (batch_size, + n_true_classes, n_pred_classes). + """ + true_boundaries = einops.rearrange(masks_to_boundaries(true_mask), "b c h w -> b (h w) c") + pred_boundaries = einops.rearrange(masks_to_boundaries(pred_mask), "b c h w -> b (h w) c") + return f_measure(true_boundaries, pred_boundaries) + + +def f_measure(true_mask: torch.Tensor, pred_mask: torch.Tensor, empty_value: float = 0.0): + """Compute pairwise F-measure between predicted and ground truth masks. + + Args: + true_mask: A binary tensor of shape (batch_size, n_points, n_true_classes). The true class + mask encoded as one-hot with missing values allowed. + pred_mask: A binary tensor of shape (batch_size, n_points, n_pred_classes). The predicted + class mask encoded as one-hot with missing values allowed. + empty_value: Value to assume for the case when a class does not occur in the ground truth, + or in the predicted classes. + + Returns: + Tuple containing the pairwise f-measure, precision and recall, all of shape (batch_size, + n_true_classes, n_pred_classes). + """ + tp, fp, fn = confusion_matrix(true_mask, pred_mask) # all B x C x K + n_pred_points = tp + fp + n_true_points = tp + fn + + precision = tp / n_pred_points + recall = tp / n_true_points + + # Guard against NaNs + precision[n_pred_points == 0] = empty_value + recall[n_true_points == 0] = empty_value + + f_measure = 2 * precision * recall / (precision + recall) + f_measure[precision + recall == 0] = 0 + + return f_measure, precision, recall + + +def masks_to_boundaries(masks: torch.Tensor, dilation_ratio: float = 0.02) -> torch.Tensor: + """Compute the boundaries around the provided masks using morphological operations. + + Returns a tensor of the same shape as the input masks containing the boundaries of each mask. + + This implementations is adapted from a non-merged PR to torchvision, see + https://github.com/pytorch/vision/pull/7704/. + + Args: + masks: masks to transform of shape ([batch_size,], n_masks, height, width). + dilation_ratio: ratio used for the dilation operation. + + Returns: + Tensor of shape ([batch_size,], n_masks, height, width) with boundaries. + """ + # If no masks are provided, return an empty tensor + if masks.numel() == 0: + return torch.zeros_like(masks) + + orig_dtype = masks.dtype + if masks.ndim == 4: + # Flatten batch dim into mask dim + batch_size, n_masks = masks.shape[:2] + masks = masks.flatten(0, 1) + else: + batch_size = None + _, h, w = masks.shape + img_diag = math.sqrt(h**2 + w**2) + dilation = max(1, int(round(dilation_ratio * img_diag))) + selem_size = dilation * 2 + 1 + selem = torch.ones((1, 1, selem_size, selem_size), device=masks.device) + + # Compute the boundaries for each mask + masks = masks.float().unsqueeze(1) + # The original erosion operation is emulated in two steps: + # 1) Convolution counting the number of black pixels around each pixel + # 2) Zero out pixel with less than the full number of black pixels + # Step 2) effectively performs the minimum operation in erosion. + eroded_masks = torch.nn.functional.conv2d(masks, selem, padding=dilation) + eroded_masks = (eroded_masks == selem.sum()).byte() + + contours = masks.byte() - eroded_masks + contours = contours.squeeze(1) + + if batch_size is not None: + contours = contours.unflatten(0, (batch_size, n_masks)) + + return contours.to(orig_dtype) + + +def hungarian_matching( + cost_matrix: torch.Tensor, maximize: bool = True +) -> Tuple[torch.Tensor, torch.Tensor]: + """Bipartite graph matching using Hungarian method. + + Naively computed over batches with a loop. + + Args: + cost_matrix: Tensor of shape (batch_size, n_rows, n_cols) containing costs. + + Returns: + Tuple of tensors of shape (batch_size, min_rows_cols, min_rows_cols) where min_rows_cols = + min(n_rows, n_cols), with the first entry containing the matched row indices, and the + second the matched column indices. + """ + import scipy + + cost_matrix_cpu = cost_matrix.detach().cpu() + + rows, cols = [], [] + for elem in cost_matrix_cpu: + row_idxs, col_idxs = scipy.optimize.linear_sum_assignment(elem, maximize=maximize) + rows.append(row_idxs) + cols.append(col_idxs) + + row_idxs = torch.as_tensor(np.array(rows), dtype=torch.int64, device=cost_matrix.device) + col_idxs = torch.as_tensor(np.array(cols), dtype=torch.int64, device=cost_matrix.device) + + return row_idxs, col_idxs + + +def _check_shape(x: torch.Tensor, expected_shape: Sequence[Optional[int]], name: str): + """Verify shape of x is as expected. + + Adapted from SAVi Jax implementation: + https://github.com/google-research/slot-attention-video/blob/main/savi/lib/metrics.py + """ + if not isinstance(expected_shape, (list, tuple)): + raise ValueError( + "`expected_shape` should be a list or tuple of ints but got " f"{expected_shape}." + ) + + # Scalars have shape () by definition. + shape = getattr(x, "shape", ()) + + if len(shape) != len(expected_shape) or any( + j is not None and i != j for i, j in zip(shape, expected_shape) + ): + raise ValueError(f"Input {name} has shape {shape}, but expected {expected_shape}.") diff --git a/videosaur/models.py b/videosaur/models.py new file mode 100644 index 0000000..be062bf --- /dev/null +++ b/videosaur/models.py @@ -0,0 +1,624 @@ +from typing import Any, Callable, Dict, List, Optional, Tuple, Union + +import numpy as np +import pytorch_lightning as pl +import torch +import torchmetrics +from torch import nn +from torchvision.utils import make_grid + +from videosaur import configuration, losses, modules, optimizers, utils, visualizations +from videosaur.data.transforms import Denormalize + + +def build( + model_config: configuration.ModelConfig, + optimizer_config, + train_metrics: Optional[Dict[str, torchmetrics.Metric]] = None, + val_metrics: Optional[Dict[str, torchmetrics.Metric]] = None, +): + optimizer_builder = optimizers.OptimizerBuilder(**optimizer_config) + + initializer = modules.build_initializer(model_config.initializer) + encoder = modules.build_encoder(model_config.encoder, "FrameEncoder") + grouper = modules.build_grouper(model_config.grouper) + decoder = modules.build_decoder(model_config.decoder) + + target_encoder = None + if model_config.target_encoder: + target_encoder = modules.build_encoder(model_config.target_encoder, "FrameEncoder") + assert ( + model_config.target_encoder_input is not None + ), "Please specify `target_encoder_input`." + + input_type = model_config.get("input_type", "image") + if input_type == "image": + processor = modules.LatentProcessor(grouper, predictor=None) + elif input_type == "video": + encoder = modules.MapOverTime(encoder) + decoder = modules.MapOverTime(decoder) + if target_encoder: + target_encoder = modules.MapOverTime(target_encoder) + if model_config.predictor is not None: + predictor = modules.build_module(model_config.predictor) + else: + predictor = None + if model_config.latent_processor: + processor = modules.build_video( + model_config.latent_processor, + "LatentProcessor", + corrector=grouper, + predictor=predictor, + ) + else: + processor = modules.LatentProcessor(grouper, predictor) + processor = modules.ScanOverTime(processor) + else: + raise ValueError(f"Unknown input type {input_type}") + + target_type = model_config.get("target_type", "features") + if target_type == "input": + default_target_key = input_type + elif target_type == "features": + if model_config.target_encoder_input is not None: + default_target_key = "target_encoder.backbone_features" + else: + default_target_key = "encoder.backbone_features" + else: + raise ValueError(f"Unknown target type {target_type}. Should be `input` or `features`.") + + loss_defaults = { + "pred_key": "decoder.reconstruction", + "target_key": default_target_key, + "video_inputs": input_type == "video", + "patch_inputs": target_type == "features", + } + if model_config.losses is None: + loss_fns = {"mse": losses.build(dict(**loss_defaults, name="MSELoss"))} + else: + loss_fns = { + name: losses.build({**loss_defaults, **loss_config}) + for name, loss_config in model_config.losses.items() + } + + if model_config.mask_resizers: + mask_resizers = { + name: modules.build_utils(resizer_config, "Resizer") + for name, resizer_config in model_config.mask_resizers.items() + } + else: + mask_resizers = { + "decoder": modules.build_utils( + { + "name": "Resizer", + # When using features as targets, assume patch-shaped outputs. With other + # targets, assume spatial outputs. + "patch_inputs": target_type == "features", + "video_inputs": input_type == "video", + "resize_mode": "bilinear", + } + ), + "grouping": modules.build_utils( + { + "name": "Resizer", + "patch_inputs": True, + "video_inputs": input_type == "video", + "resize_mode": "bilinear", + } + ), + } + + if model_config.masks_to_visualize: + masks_to_visualize = model_config.masks_to_visualize + else: + masks_to_visualize = "decoder" + + model = ObjectCentricModel( + optimizer_builder, + initializer, + encoder, + processor, + decoder, + loss_fns, + loss_weights=model_config.get("loss_weights", None), + target_encoder=target_encoder, + train_metrics=train_metrics, + val_metrics=val_metrics, + mask_resizers=mask_resizers, + input_type=input_type, + target_encoder_input=model_config.get("target_encoder_input", None), + visualize=model_config.get("visualize", False), + visualize_every_n_steps=model_config.get("visualize_every_n_steps", 1000), + masks_to_visualize=masks_to_visualize, + ) + + if model_config.load_weights: + model.load_weights_from_checkpoint(model_config.load_weights, model_config.modules_to_load) + + return model + + +class ObjectCentricModel(pl.LightningModule): + def __init__( + self, + optimizer_builder: Callable, + initializer: nn.Module, + encoder: nn.Module, + processor: nn.Module, + decoder: nn.Module, + loss_fns: Dict[str, losses.Loss], + *, + loss_weights: Optional[Dict[str, float]] = None, + target_encoder: Optional[nn.Module] = None, + train_metrics: Optional[Dict[str, torchmetrics.Metric]] = None, + val_metrics: Optional[Dict[str, torchmetrics.Metric]] = None, + mask_resizers: Optional[Dict[str, modules.Resizer]] = None, + input_type: str = "image", + target_encoder_input: Optional[str] = None, + visualize: bool = False, + visualize_every_n_steps: Optional[int] = None, + masks_to_visualize: Union[str, List[str]] = "decoder", + ): + super().__init__() + self.optimizer_builder = optimizer_builder + self.initializer = initializer + self.encoder = encoder + self.processor = processor + self.decoder = decoder + self.target_encoder = target_encoder + + if loss_weights is not None: + # Filter out losses that are not used + assert ( + loss_weights.keys() == loss_fns.keys() + ), f"Loss weight keys {loss_weights.keys()} != {loss_fns.keys()}" + loss_fns_filtered = {k: loss for k, loss in loss_fns.items() if loss_weights[k] != 0.0} + loss_weights_filtered = { + k: loss for k, loss in loss_weights.items() if loss_weights[k] != 0.0 + } + self.loss_fns = nn.ModuleDict(loss_fns_filtered) + self.loss_weights = loss_weights_filtered + else: + self.loss_fns = nn.ModuleDict(loss_fns) + self.loss_weights = {} + + self.mask_resizers = mask_resizers if mask_resizers else {} + self.mask_resizers["segmentation"] = modules.Resizer( + video_inputs=input_type == "video", resize_mode="nearest-exact" + ) + self.mask_soft_to_hard = modules.SoftToHardMask() + self.train_metrics = torch.nn.ModuleDict(train_metrics) + self.val_metrics = torch.nn.ModuleDict(val_metrics) + + self.visualize = visualize + if visualize: + assert visualize_every_n_steps is not None + self.visualize_every_n_steps = visualize_every_n_steps + if isinstance(masks_to_visualize, str): + masks_to_visualize = [masks_to_visualize] + for key in masks_to_visualize: + if key not in ("decoder", "grouping"): + raise ValueError(f"Unknown mask type {key}. Should be `decoder` or `grouping`.") + self.mask_keys_to_visualize = [f"{key}_masks" for key in masks_to_visualize] + + if input_type == "image": + self.input_key = "image" + self.expected_input_dims = 4 + elif input_type == "video": + self.input_key = "video" + self.expected_input_dims = 5 + else: + raise ValueError(f"Unknown input type {input_type}. Should be `image` or `video`.") + + self.target_encoder_input_key = ( + target_encoder_input if target_encoder_input else self.input_key + ) + + def configure_optimizers(self): + modules = { + "initializer": self.initializer, + "encoder": self.encoder, + "processor": self.processor, + "decoder": self.decoder, + } + return self.optimizer_builder(modules) + + def forward(self, inputs: Dict[str, Any]) -> Dict[str, Any]: + encoder_input = inputs[self.input_key] # batch [x n_frames] x n_channels x height x width + assert encoder_input.ndim == self.expected_input_dims + batch_size = len(encoder_input) + + encoder_output = self.encoder(encoder_input) + features = encoder_output["features"] + + slots_initial = self.initializer(batch_size=batch_size) + processor_output = self.processor(slots_initial, features) + slots = processor_output["state"] + decoder_output = self.decoder(slots) + + outputs = { + "batch_size": batch_size, + "encoder": encoder_output, + "processor": processor_output, + "decoder": decoder_output, + } + outputs["targets"] = self.get_targets(inputs, outputs) + + return outputs + + def process_masks( + self, + masks: torch.Tensor, + inputs: Dict[str, Any], + resizer: Optional[Callable[[torch.Tensor, torch.Tensor], torch.Tensor]], + ) -> Tuple[Optional[torch.Tensor], Optional[torch.Tensor], Optional[torch.Tensor]]: + if masks is None: + return None, None, None + + if resizer is None: + masks_for_vis = masks + masks_for_vis_hard = self.mask_soft_to_hard(masks) + masks_for_metrics_hard = masks_for_vis_hard + else: + masks_for_vis = resizer(masks, inputs[self.input_key]) + masks_for_vis_hard = self.mask_soft_to_hard(masks_for_vis) + target_masks = inputs.get("segmentations") + if target_masks is not None and masks_for_vis.shape[-2:] != target_masks.shape[-2:]: + masks_for_metrics = resizer(masks, target_masks) + masks_for_metrics_hard = self.mask_soft_to_hard(masks_for_metrics) + else: + masks_for_metrics_hard = masks_for_vis_hard + + return masks_for_vis, masks_for_vis_hard, masks_for_metrics_hard + + @torch.no_grad() + def aux_forward(self, inputs: Dict[str, Any], outputs: Dict[str, Any]) -> Dict[str, Any]: + """Compute auxilliary outputs only needed for metrics and visualisations.""" + decoder_masks = outputs["decoder"].get("masks") + decoder_masks, decoder_masks_hard, decoder_masks_metrics_hard = self.process_masks( + decoder_masks, inputs, self.mask_resizers.get("decoder") + ) + + grouping_masks = outputs["processor"]["corrector"].get("masks") + grouping_masks, grouping_masks_hard, grouping_masks_metrics_hard = self.process_masks( + grouping_masks, inputs, self.mask_resizers.get("grouping") + ) + + aux_outputs = {} + if decoder_masks is not None: + aux_outputs["decoder_masks"] = decoder_masks + if decoder_masks_hard is not None: + aux_outputs["decoder_masks_vis_hard"] = decoder_masks_hard + if decoder_masks_metrics_hard is not None: + aux_outputs["decoder_masks_hard"] = decoder_masks_metrics_hard + if grouping_masks is not None: + aux_outputs["grouping_masks"] = grouping_masks + if grouping_masks_hard is not None: + aux_outputs["grouping_masks_vis_hard"] = grouping_masks_hard + if grouping_masks_metrics_hard is not None: + aux_outputs["grouping_masks_hard"] = grouping_masks_metrics_hard + + return aux_outputs + + def get_targets( + self, inputs: Dict[str, Any], outputs: Dict[str, Any] + ) -> Dict[str, torch.Tensor]: + if self.target_encoder: + target_encoder_input = inputs[self.target_encoder_input_key] + assert target_encoder_input.ndim == self.expected_input_dims + + with torch.no_grad(): + encoder_output = self.target_encoder(target_encoder_input) + + outputs["target_encoder"] = encoder_output + + targets = {} + for name, loss_fn in self.loss_fns.items(): + targets[name] = loss_fn.get_target(inputs, outputs) + + return targets + + def compute_loss(self, outputs: Dict[str, Any]) -> Tuple[torch.Tensor, Dict[str, torch.Tensor]]: + losses = {} + for name, loss_fn in self.loss_fns.items(): + prediction = loss_fn.get_prediction(outputs) + target = outputs["targets"][name] + losses[name] = loss_fn(prediction, target) + + losses_weighted = [loss * self.loss_weights.get(name, 1.0) for name, loss in losses.items()] + total_loss = torch.stack(losses_weighted).sum() + + return total_loss, losses + + def training_step(self, batch: Dict[str, Any], batch_idx: int): + outputs = self.forward(batch) + if self.train_metrics or ( + self.visualize and self.trainer.global_step % self.visualize_every_n_steps == 0 + ): + aux_outputs = self.aux_forward(batch, outputs) + + total_loss, losses = self.compute_loss(outputs) + if len(losses) == 1: + to_log = {"train/loss": total_loss} # Log only total loss if only one loss configured + else: + to_log = {f"train/{name}": loss for name, loss in losses.items()} + to_log["train/loss"] = total_loss + + if self.train_metrics: + for key, metric in self.train_metrics.items(): + values = metric(**batch, **outputs, **aux_outputs) + self._add_metric_to_log(to_log, f"train/{key}", values) + metric.reset() + self.log_dict(to_log, on_step=True, on_epoch=False, batch_size=outputs["batch_size"]) + + del outputs # Explicitly delete to save memory + + if ( + self.visualize + and self.trainer.global_step % self.visualize_every_n_steps == 0 + and self.global_rank == 0 + ): + self._log_inputs( + batch[self.input_key], + {key: aux_outputs[f"{key}_hard"] for key in self.mask_keys_to_visualize}, + mode="train", + ) + self._log_masks(aux_outputs, self.mask_keys_to_visualize, mode="train") + + return total_loss + + def validation_step(self, batch: Dict[str, Any], batch_idx: int): + if "batch_padding_mask" in batch: + batch = self._remove_padding(batch, batch["batch_padding_mask"]) + if batch is None: + return + + outputs = self.forward(batch) + aux_outputs = self.aux_forward(batch, outputs) + + total_loss, losses = self.compute_loss(outputs) + if len(losses) == 1: + to_log = {"val/loss": total_loss} # Log only total loss if only one loss configured + else: + to_log = {f"val/{name}": loss for name, loss in losses.items()} + to_log["val/loss"] = total_loss + + if self.val_metrics: + for metric in self.val_metrics.values(): + metric.update(**batch, **outputs, **aux_outputs) + + self.log_dict( + to_log, on_step=False, on_epoch=True, batch_size=outputs["batch_size"], prog_bar=True + ) + + if self.visualize and batch_idx == 0 and self.global_rank == 0: + masks_to_vis = { + key: aux_outputs[f"{key}_vis_hard"] for key in self.mask_keys_to_visualize + } + if batch["segmentations"].shape[-2:] != batch[self.input_key].shape[-2:]: + masks_to_vis["segmentations"] = self.mask_resizers["segmentation"]( + batch["segmentations"], batch[self.input_key] + ) + else: + masks_to_vis["segmentations"] = batch["segmentations"] + self._log_inputs( + batch[self.input_key], + masks_to_vis, + mode="val", + ) + self._log_masks(aux_outputs, self.mask_keys_to_visualize, mode="val") + + def validation_epoch_end(self, outputs): + if self.val_metrics: + to_log = {} + for key, metric in self.val_metrics.items(): + self._add_metric_to_log(to_log, f"val/{key}", metric.compute()) + metric.reset() + self.log_dict(to_log, prog_bar=True) + + @staticmethod + def _add_metric_to_log( + log_dict: Dict[str, Any], name: str, values: Union[torch.Tensor, Dict[str, torch.Tensor]] + ): + if isinstance(values, dict): + for k, v in values.items(): + log_dict[f"{name}/{k}"] = v + else: + log_dict[name] = values + + def _log_inputs( + self, + inputs: torch.Tensor, + masks_by_name: Dict[str, torch.Tensor], + mode: str, + step: Optional[int] = None, + ): + denorm = Denormalize(input_type=self.input_key) + if step is None: + step = self.trainer.global_step + + if self.input_key == "video": + video = torch.stack([denorm(video) for video in inputs]) + self._log_video(f"{mode}/{self.input_key}", video, global_step=step) + for mask_name, masks in masks_by_name.items(): + video_with_masks = visualizations.mix_videos_with_masks(video, masks) + self._log_video( + f"{mode}/video_with_{mask_name}", + video_with_masks, + global_step=step, + ) + elif self.input_key == "image": + image = denorm(inputs) + self._log_images(f"{mode}/{self.input_key}", image, global_step=step) + for mask_name, masks in masks_by_name.items(): + image_with_masks = visualizations.mix_images_with_masks(image, masks) + self._log_images( + f"{mode}/image_with_{mask_name}", + image_with_masks, + global_step=step, + ) + else: + raise ValueError(f"input_type should be 'image' or 'video', but got '{self.input_key}'") + + def _log_masks( + self, + aux_outputs, + mask_keys=("decoder_masks",), + mode="val", + types: tuple = ("frames",), + step: Optional[int] = None, + ): + if step is None: + step = self.trainer.global_step + for mask_key in mask_keys: + if mask_key in aux_outputs: + masks = aux_outputs[mask_key] + if self.input_key == "video": + _, f, n_obj, H, W = masks.shape + first_masks = masks[0].permute(1, 0, 2, 3) + first_masks_inverted = 1 - first_masks.reshape(n_obj, f, 1, H, W) + self._log_video( + f"{mode}/{mask_key}", + first_masks_inverted, + global_step=step, + n_examples=n_obj, + types=types, + ) + elif self.input_key == "image": + _, n_obj, H, W = masks.shape + first_masks_inverted = 1 - masks[0].reshape(n_obj, 1, H, W) + self._log_images( + f"{mode}/{mask_key}", + first_masks_inverted, + global_step=step, + n_examples=n_obj, + ) + else: + raise ValueError( + f"input_type should be 'image' or 'video', but got '{self.input_key}'" + ) + + def _log_video( + self, + name: str, + data: torch.Tensor, + global_step: int, + n_examples: int = 8, + max_frames: int = 8, + types: tuple = ("frames",), + ): + data = data[:n_examples] + logger = self._get_tensorboard_logger() + + if logger is not None: + if "video" in types: + logger.experiment.add_video(f"{name}/video", data, global_step=global_step) + if "frames" in types: + _, num_frames, _, _, _ = data.shape + num_frames = min(max_frames, num_frames) + data = data[:, :num_frames] + data = data.flatten(0, 1) + logger.experiment.add_image( + f"{name}/frames", make_grid(data, nrow=num_frames), global_step=global_step + ) + + def _save_video(self, name: str, data: torch.Tensor, global_step: int): + assert ( + data.shape[0] == 1 + ), f"Only single videos saving are supported, but shape is: {data.shape}" + data = data.cpu().numpy()[0].transpose(0, 2, 3, 1) + data_dir = self.save_data_dir / name + data_dir.mkdir(parents=True, exist_ok=True) + np.save(data_dir / f"{global_step}.npy", data) + + def _log_images( + self, + name: str, + data: torch.Tensor, + global_step: int, + n_examples: int = 8, + ): + n_examples = min(n_examples, data.shape[0]) + data = data[:n_examples] + logger = self._get_tensorboard_logger() + + if logger is not None: + logger.experiment.add_image( + f"{name}/images", make_grid(data, nrow=n_examples), global_step=global_step + ) + + @staticmethod + def _remove_padding( + batch: Dict[str, Any], padding_mask: torch.Tensor + ) -> Optional[Dict[str, Any]]: + if torch.all(padding_mask): + # Batch consists only of padding + return None + + mask = ~padding_mask + mask_as_idxs = torch.arange(len(mask))[mask.cpu()] + + output = {} + for key, value in batch.items(): + if isinstance(value, torch.Tensor): + output[key] = value[mask] + elif isinstance(value, list): + output[key] = [value[idx] for idx in mask_as_idxs] + + return output + + def _get_tensorboard_logger(self): + if self.loggers is not None: + for logger in self.loggers: + if isinstance(logger, pl.loggers.tensorboard.TensorBoardLogger): + return logger + else: + if isinstance(self.logger, pl.loggers.tensorboard.TensorBoardLogger): + return self.logger + + def on_load_checkpoint(self, checkpoint): + # Reset timer during loading of the checkpoint + # as timer is used to track time from the start + # of the current run. + if "callbacks" in checkpoint and "Timer" in checkpoint["callbacks"]: + checkpoint["callbacks"]["Timer"]["time_elapsed"] = { + "train": 0.0, + "sanity_check": 0.0, + "validate": 0.0, + "test": 0.0, + "predict": 0.0, + } + + def load_weights_from_checkpoint( + self, checkpoint_path: str, module_mapping: Optional[Dict[str, str]] = None + ): + """Load weights from a checkpoint into the specified modules.""" + checkpoint = torch.load(checkpoint_path) + if "state_dict" in checkpoint: + checkpoint = checkpoint["state_dict"] + + if module_mapping is None: + module_mapping = { + key.split(".")[0]: key.split(".")[0] + for key in checkpoint + if hasattr(self, key.split(".")[0]) + } + + for dest_module, source_module in module_mapping.items(): + try: + module = utils.read_path(self, dest_module) + except ValueError: + raise ValueError(f"Module {dest_module} could not be retrieved from model") from None + + state_dict = {} + for key, weights in checkpoint.items(): + if key.startswith(source_module): + if key != source_module: + key = key[len(source_module + ".") :] # Remove prefix + state_dict[key] = weights + if len(state_dict) == 0: + raise ValueError( + f"No weights for module {source_module} found in checkpoint {checkpoint_path}." + ) + + module.load_state_dict(state_dict) diff --git a/videosaur/modules/__init__.py b/videosaur/modules/__init__.py new file mode 100644 index 0000000..512593d --- /dev/null +++ b/videosaur/modules/__init__.py @@ -0,0 +1,43 @@ +from videosaur.modules import timm +from videosaur.modules.decoders import build as build_decoder +from videosaur.modules.encoders import build as build_encoder +from videosaur.modules.groupers import build as build_grouper +from videosaur.modules.initializers import build as build_initializer +from videosaur.modules.networks import build as build_network +from videosaur.modules.utils import Resizer, SoftToHardMask +from videosaur.modules.utils import build as build_utils +from videosaur.modules.utils import build_module, build_torch_function, build_torch_module +from videosaur.modules.video import LatentProcessor, MapOverTime, ScanOverTime +from videosaur.modules.video import build as build_video + +__all__ = [ + "build_decoder", + "build_encoder", + "build_grouper", + "build_initializer", + "build_network", + "build_utils", + "build_module", + "build_torch_module", + "build_torch_function", + "timm", + "MapOverTime", + "ScanOverTime", + "LatentProcessor", + "Resizer", + "SoftToHardMask", +] + + +BUILD_FNS_BY_MODULE_GROUP = { + "decoders": build_decoder, + "encoders": build_encoder, + "groupers": build_grouper, + "initializers": build_initializer, + "networks": build_network, + "utils": build_utils, + "video": build_video, + "torch": build_torch_function, + "torch.nn": build_torch_module, + "nn": build_torch_module, +} diff --git a/videosaur/modules/decoders.py b/videosaur/modules/decoders.py new file mode 100644 index 0000000..98a9864 --- /dev/null +++ b/videosaur/modules/decoders.py @@ -0,0 +1,239 @@ +from typing import Dict, List, Optional, Tuple, Union + +import einops +import timm.layers.pos_embed +import torch +from torch import nn + +from videosaur.modules import networks, utils +from videosaur.utils import config_as_kwargs, make_build_fn + + +@make_build_fn(__name__, "decoder") +def build(config, name: str): + if name == "SpatialBroadcastDecoder": + output_transform = None + if config.get("output_transform"): + output_transform = utils.build_module(config.output_transform) + + return SpatialBroadcastDecoder( + backbone=utils.build_module(config.backbone, default_group="networks"), + output_transform=output_transform, + **config_as_kwargs(config, ("backbone", "output_transform")), + ) + elif name == "SlotMixerDecoder": + output_transform = None + if config.get("output_transform"): + output_transform = utils.build_module(config.output_transform) + + return SlotMixerDecoder( + allocator=utils.build_module(config.allocator, default_group="networks"), + renderer=utils.build_module(config.renderer, default_group="networks"), + output_transform=output_transform, + pos_embed_mode=config.get("pos_embed_mode", "add"), + **config_as_kwargs( + config, ("allocator", "renderer", "output_transform", "pos_embed_mode") + ), + ) + else: + return None + + +class MLPDecoder(nn.Module): + """Decoder that reconstructs independently for every position and slot.""" + + def __init__( + self, + inp_dim: int, + outp_dim: int, + hidden_dims: List[int], + n_patches: int, + activation: str = "relu", + eval_output_size: Optional[Tuple[int]] = None, + ): + super().__init__() + self.outp_dim = outp_dim + self.n_patches = n_patches + self.eval_output_size = list(eval_output_size) if eval_output_size else None + + self.mlp = networks.MLP(inp_dim, outp_dim + 1, hidden_dims, activation=activation) + self.pos_emb = nn.Parameter(torch.randn(1, 1, n_patches, inp_dim) * inp_dim**-0.5) + + def forward(self, slots: torch.Tensor) -> Dict[str, torch.Tensor]: + bs, n_slots, dims = slots.shape + + if not self.training and self.eval_output_size is not None: + pos_emb = timm.layers.pos_embed.resample_abs_pos_embed( + self.pos_emb.squeeze(1), + new_size=self.eval_output_size, + num_prefix_tokens=0, + ).unsqueeze(1) + else: + pos_emb = self.pos_emb + + slots = slots.view(bs, n_slots, 1, dims).expand(bs, n_slots, pos_emb.shape[2], dims) + slots = slots + pos_emb + + recons, alpha = self.mlp(slots).split((self.outp_dim, 1), dim=-1) + + masks = torch.softmax(alpha, dim=1) + recon = torch.sum(recons * masks, dim=1) + + return {"reconstruction": recon, "masks": masks.squeeze(-1)} + + +class SpatialBroadcastDecoder(nn.Module): + """Decoder that reconstructs a spatial map independently per slot.""" + + def __init__( + self, + inp_dim: int, + outp_dim: int, + backbone: nn.Module, + initial_size: Union[int, Tuple[int, int]] = 8, + backbone_dim: Optional[int] = None, + pos_embed: Optional[nn.Module] = None, + output_transform: Optional[nn.Module] = None, + ): + super().__init__() + self.outp_dim = outp_dim + if isinstance(initial_size, int): + self.initial_size = (initial_size, initial_size) + else: + self.initial_size = initial_size + + if pos_embed is None: + self.pos_embed = utils.CoordinatePositionEmbed(inp_dim, initial_size) + else: + self.pos_embed = pos_embed + + self.backbone = backbone + + if output_transform is None: + if backbone_dim is None: + raise ValueError("Need to provide backbone dim if output_transform is unspecified") + self.output_transform = nn.Conv2d(backbone_dim, outp_dim + 1, 1, 1) + else: + self.output_transform = output_transform + + self.init_parameters() + + def init_parameters(self): + if isinstance(self.output_transform, nn.Conv2d): + utils.init_parameters(self.output_transform) + + def forward(self, slots: torch.Tensor) -> Dict[str, torch.Tensor]: + bs, n_slots, _ = slots.shape + + slots = einops.repeat( + slots, "b s d -> (b s) d h w", h=self.initial_size[0], w=self.initial_size[1] + ) + + slots = self.pos_embed(slots) + features = self.backbone(slots) + outputs = self.output_transform(features) + + outputs = einops.rearrange(outputs, "(b s) ... -> b s ...", b=bs, s=n_slots) + recons, alpha = einops.unpack(outputs, [[self.outp_dim], [1]], "b s * h w") + + masks = torch.softmax(alpha, dim=1) + recon = torch.sum(recons * masks, dim=1) + + return {"reconstruction": recon, "masks": masks.squeeze(2)} + + +class SlotMixerDecoder(nn.Module): + """Slot mixer decoder reconstructing jointly over all slots, but independent per position. + + Introduced in Sajjadi et al., 2022: Object Scene Representation Transformer, + http://arxiv.org/abs/2206.06922 + """ + + def __init__( + self, + inp_dim: int, + outp_dim: int, + embed_dim: int, + n_patches: int, + allocator: nn.Module, + renderer: nn.Module, + renderer_dim: Optional[int] = None, + output_transform: Optional[nn.Module] = None, + pos_embed_mode: Optional[str] = None, + use_layer_norms: bool = False, + norm_memory: bool = True, + temperature: Optional[float] = None, + eval_output_size: Optional[Tuple[int]] = None, + ): + super().__init__() + self.allocator = allocator + self.renderer = renderer + self.eval_output_size = list(eval_output_size) if eval_output_size else None + + att_dim = max(embed_dim, inp_dim) + self.scale = att_dim**-0.5 if temperature is None else temperature**-1 + self.to_q = nn.Linear(embed_dim, att_dim, bias=False) + self.to_k = nn.Linear(inp_dim, att_dim, bias=False) + + if use_layer_norms: + self.norm_k = nn.LayerNorm(inp_dim, eps=1e-5) + self.norm_q = nn.LayerNorm(embed_dim, eps=1e-5) + self.norm_memory = norm_memory + if norm_memory: + self.norm_memory = nn.LayerNorm(inp_dim, eps=1e-5) + else: + self.norm_memory = nn.Identity() + else: + self.norm_k = nn.Identity() + self.norm_q = nn.Identity() + self.norm_memory = nn.Identity() + + if output_transform is None: + if renderer_dim is None: + raise ValueError("Need to provide render_mlp_dim if output_transform is unspecified") + self.output_transform = nn.Linear(renderer_dim, outp_dim) + else: + self.output_transform = output_transform + + if pos_embed_mode is not None and pos_embed_mode not in ("none", "add", "concat"): + raise ValueError("If set, `pos_embed_mode` should be 'none', 'add' or 'concat'") + self.pos_embed_mode = pos_embed_mode + self.pos_emb = nn.Parameter(torch.randn(1, n_patches, embed_dim) * embed_dim**-0.5) + self.init_parameters() + + def init_parameters(self): + layers = [self.to_q, self.to_k] + if isinstance(self.output_transform, nn.Linear): + layers.append(self.output_transform) + utils.init_parameters(layers, "xavier_uniform") + + def forward(self, slots: torch.Tensor) -> Dict[str, torch.Tensor]: + if not self.training and self.eval_output_size is not None: + pos_emb = timm.layers.pos_embed.resample_abs_pos_embed( + self.pos_emb, + new_size=self.eval_output_size, + num_prefix_tokens=0, + ) + else: + pos_emb = self.pos_emb + + pos_emb = pos_emb.expand(len(slots), -1, -1) + memory = self.norm_memory(slots) + query_features = self.allocator(pos_emb, memory=memory) + q = self.to_q(self.norm_q(query_features)) # B x P x D + k = self.to_k(self.norm_k(slots)) # B x S x D + + dots = torch.einsum("bpd, bsd -> bps", q, k) * self.scale + attn = dots.softmax(dim=-1) + + mixed_slots = torch.einsum("bps, bsd -> bpd", attn, slots) # B x P x D + + if self.pos_embed_mode == "add": + mixed_slots = mixed_slots + pos_emb + elif self.pos_embed_mode == "concat": + mixed_slots = torch.cat((mixed_slots, pos_emb), dim=-1) + + features = self.renderer(mixed_slots) + recons = self.output_transform(features) + + return {"reconstruction": recons, "masks": attn.transpose(-2, -1)} diff --git a/videosaur/modules/encoders.py b/videosaur/modules/encoders.py new file mode 100644 index 0000000..f135db4 --- /dev/null +++ b/videosaur/modules/encoders.py @@ -0,0 +1,224 @@ +from typing import Any, Dict, List, Optional, Union + +import einops +import timm +import torch +import torchvision +from torch import nn + +from videosaur.modules import utils +from videosaur.utils import config_as_kwargs, make_build_fn + + +@make_build_fn(__name__, "encoder") +def build(config, name: str): + if name == "FrameEncoder": + pos_embed = None + if config.get("pos_embed"): + pos_embed = utils.build_module(config.pos_embed) + + output_transform = None + if config.get("output_transform"): + output_transform = utils.build_module(config.output_transform) + + return FrameEncoder( + backbone=utils.build_module(config.backbone, default_group="encoders"), + pos_embed=pos_embed, + output_transform=output_transform, + **config_as_kwargs(config, ("backbone", "pos_embed", "output_transform")), + ) + else: + return None + + +class FrameEncoder(nn.Module): + """Module reducing image to set of features.""" + + def __init__( + self, + backbone: nn.Module, + pos_embed: Optional[nn.Module] = None, + output_transform: Optional[nn.Module] = None, + spatial_flatten: bool = False, + main_features_key: str = "vit_block12", + ): + super().__init__() + self.backbone = backbone + self.pos_embed = pos_embed + self.output_transform = output_transform + self.spatial_flatten = spatial_flatten + self.main_features_key = main_features_key + + def forward(self, images: torch.Tensor) -> Dict[str, torch.Tensor]: + # images: batch x n_channels x height x width + backbone_features = self.backbone(images) + if isinstance(backbone_features, dict): + features = backbone_features[self.main_features_key].clone() + else: + features = backbone_features.clone() + + if self.pos_embed: + features = self.pos_embed(features) + + if self.spatial_flatten: + features = einops.rearrange(features, "b c h w -> b (h w) c") + if self.output_transform: + features = self.output_transform(features) + + assert ( + features.ndim == 3 + ), f"Expect output shape (batch, tokens, dims), but got {features.shape}" + if isinstance(backbone_features, dict): + for k, backbone_feature in backbone_features.items(): + if self.spatial_flatten: + backbone_features[k] = einops.rearrange(backbone_feature, "b c h w -> b (h w) c") + assert ( + backbone_feature.ndim == 3 + ), f"Expect output shape (batch, tokens, dims), but got {backbone_feature.shape}" + main_backbone_features = backbone_features[self.main_features_key] + + return { + "features": features, + "backbone_features": main_backbone_features, + **backbone_features, + } + else: + if self.spatial_flatten: + backbone_features = einops.rearrange(backbone_features, "b c h w -> b (h w) c") + assert ( + backbone_features.ndim == 3 + ), f"Expect output shape (batch, tokens, dims), but got {backbone_features.shape}" + + return { + "features": features, + "backbone_features": backbone_features, + } + + +class TimmExtractor(nn.Module): + """Feature extractor utilizing models from timm library.""" + + # Convenience aliases for feature keys + FEATURE_ALIASES = { + **{f"resnet_block{i}": f"layer{i}" for i in range(1, 5)}, + **{f"vit_block{i + 1}": f"blocks.{i}" for i in range(12)}, + **{f"vit_block_values{i + 1}": f"blocks.{i}.attn.qkv" for i in range(12)}, + **{f"vit_block_queries{i + 1}": f"blocks.{i}.attn.qkv" for i in range(12)}, + **{f"vit_block_keys{i + 1}": f"blocks.{i}.attn.qkv" for i in range(12)}, + "vit_output": "norm", + } + FEATURE_MAPPING = { + **{f"layer{i}": f"resnet_block{i}" for i in range(1, 5)}, + **{f"blocks.{i}": f"vit_block{i + 1}" for i in range(12)}, + **{f"blocks.{i}.attn.qkv": f"vit_block_keys{i + 1}" for i in range(12)}, + "norm": "vit_output", + } + + def __init__( + self, + model: str, + pretrained: bool = False, + frozen: bool = False, + features: Optional[Union[str, List[str]]] = None, + checkpoint_path: Optional[str] = None, + model_kwargs: Optional[Dict[str, Any]] = None, + ): + super().__init__() + model_name = model + self.frozen = frozen + self.features = [features] if isinstance(features, str) else features + self.is_vit = model_name.startswith("vit") + + model = TimmExtractor._create_model(model_name, pretrained, checkpoint_path, model_kwargs) + + if self.features is not None: + nodes = torchvision.models.feature_extraction.get_graph_node_names(model)[0] + + features = [] + for name in self.features: + if name in TimmExtractor.FEATURE_ALIASES: + name = TimmExtractor.FEATURE_ALIASES[name] + + if not any(node.startswith(name) for node in nodes): + raise ValueError( + f"Requested features under node {name}, but this node does " + f"not exist in model {model_name}. Available nodes: {nodes}" + ) + + features.append(name) + + model = torchvision.models.feature_extraction.create_feature_extractor(model, features) + + self.model = model + + if self.frozen: + self.requires_grad_(False) + + @staticmethod + def _create_model( + model_name: str, + pretrained: bool, + checkpoint_path: Optional[str], + model_kwargs: Optional[Dict[str, Any]], + trials: int = 0, + ) -> nn.Module: + if model_kwargs is None: + model_kwargs = {} + + try: + model = timm.create_model( + model_name, pretrained=pretrained, checkpoint_path=checkpoint_path, **model_kwargs + ) + except (FileExistsError, FileNotFoundError): + # Timm uses Hugginface hub for loading the files, which does some symlinking in the + # background when loading the checkpoint. When multiple concurrent jobs attempt to + # load the checkpoint, this can create conflicts, because the symlink is first removed, + # then created again by each job. We attempt to catch the resulting errors here, and + # retry creating the model, up to 3 times. + if trials == 2: + raise + else: + model = None + + if model is None: + model = TimmExtractor._create_model( + model_name, pretrained, checkpoint_path, model_kwargs, trials=trials + 1 + ) + + return model + + def forward(self, inp): + if self.frozen: + with torch.no_grad(): + outputs = self.model(inp) + else: + outputs = self.model(inp) + + if self.features is not None: + if self.is_vit: + outputs = {k: v[:, 1:] for k, v in outputs.items()} # Remove CLS token + outputs = {self.FEATURE_MAPPING[key]: value for key, value in outputs.items()} + for name in self.features: + if ("keys" in name) or ("queries" in name) or ("values" in name): + feature_name = name.replace("queries", "keys").replace("values", "keys") + B, N, C = outputs[feature_name].shape + qkv = outputs[feature_name].reshape( + B, N, 3, C // 3 + ) # outp has shape B, N, 3 * H * (C // H) + q, k, v = qkv.unbind(2) + if "keys" in name: + outputs[name] = k + elif "queries" in name: + outputs[name] = q + elif "values" in name: + outputs[name] = v + else: + raise ValueError(f"Unknown feature name {name}.") + + if len(outputs) == 1: + # Unpack single output for now + return next(iter(outputs.values())) + else: + return outputs + else: + return outputs diff --git a/videosaur/modules/groupers.py b/videosaur/modules/groupers.py new file mode 100644 index 0000000..6d940e7 --- /dev/null +++ b/videosaur/modules/groupers.py @@ -0,0 +1,95 @@ +from typing import Optional, Tuple + +import torch +from torch import nn + +from videosaur.modules import networks +from videosaur.utils import make_build_fn + + +@make_build_fn(__name__, "grouper") +def build(config, name: str): + pass # No special module building needed + + +class SlotAttention(nn.Module): + def __init__( + self, + inp_dim: int, + slot_dim: int, + kvq_dim: Optional[int] = None, + hidden_dim: Optional[int] = None, + n_iters: int = 3, + eps: float = 1e-8, + use_gru: bool = True, + use_mlp: bool = True, + ): + super().__init__() + assert n_iters >= 1 + + if kvq_dim is None: + kvq_dim = slot_dim + self.to_k = nn.Linear(inp_dim, kvq_dim, bias=False) + self.to_v = nn.Linear(inp_dim, kvq_dim, bias=False) + self.to_q = nn.Linear(slot_dim, kvq_dim, bias=False) + + if use_gru: + self.gru = nn.GRUCell(input_size=kvq_dim, hidden_size=slot_dim) + else: + assert kvq_dim == slot_dim + self.gru = None + + if hidden_dim is None: + hidden_dim = 4 * slot_dim + + if use_mlp: + self.mlp = networks.MLP( + slot_dim, slot_dim, [hidden_dim], initial_layer_norm=True, residual=True + ) + else: + self.mlp = None + + self.norm_features = nn.LayerNorm(inp_dim) + self.norm_slots = nn.LayerNorm(slot_dim) + + self.n_iters = n_iters + self.eps = eps + self.scale = kvq_dim**-0.5 + + def step( + self, slots: torch.Tensor, keys: torch.Tensor, values: torch.Tensor + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Perform one iteration of slot attention.""" + slots = self.norm_slots(slots) + queries = self.to_q(slots) + + dots = torch.einsum("bsd, bfd -> bsf", queries, keys) * self.scale + pre_norm_attn = torch.softmax(dots, dim=1) + attn = pre_norm_attn + self.eps + attn = attn / attn.sum(-1, keepdim=True) + + updates = torch.einsum("bsf, bfd -> bsd", attn, values) + + if self.gru: + updated_slots = self.gru(updates.flatten(0, 1), slots.flatten(0, 1)) + slots = updated_slots.unflatten(0, slots.shape[:2]) + else: + slots = slots + updates + + if self.mlp is not None: + slots = self.mlp(slots) + + return slots, pre_norm_attn + + def forward(self, slots: torch.Tensor, features: torch.Tensor, n_iters: Optional[int] = None): + features = self.norm_features(features) + keys = self.to_k(features) + values = self.to_v(features) + + if n_iters is None: + n_iters = self.n_iters + + for _ in range(n_iters): + slots, pre_norm_attn = self.step(slots, keys, values) + + return {"slots": slots, "masks": pre_norm_attn} diff --git a/videosaur/modules/initializers.py b/videosaur/modules/initializers.py new file mode 100644 index 0000000..fb63a38 --- /dev/null +++ b/videosaur/modules/initializers.py @@ -0,0 +1,43 @@ +from typing import Optional + +import torch +from torch import nn + +from videosaur.utils import make_build_fn + + +@make_build_fn(__name__, "initializer") +def build(config, name: str): + pass # No special module building needed + + +class RandomInit(nn.Module): + """Sampled random initialization for all slots.""" + + def __init__(self, n_slots: int, dim: int, initial_std: Optional[float] = None): + super().__init__() + self.n_slots = n_slots + self.dim = dim + self.mean = nn.Parameter(torch.zeros(1, 1, dim)) + if initial_std is None: + initial_std = dim**-0.5 + self.log_std = nn.Parameter(torch.log(torch.ones(1, 1, dim) * initial_std)) + + def forward(self, batch_size: int): + noise = torch.randn(batch_size, self.n_slots, self.dim, device=self.mean.device) + return self.mean + noise * self.log_std.exp() + + +class FixedLearnedInit(nn.Module): + """Learned initialization with a fixed number of slots.""" + + def __init__(self, n_slots: int, dim: int, initial_std: Optional[float] = None): + super().__init__() + self.n_slots = n_slots + self.dim = dim + if initial_std is None: + initial_std = dim**-0.5 + self.slots = nn.Parameter(torch.randn(1, n_slots, dim) * initial_std) + + def forward(self, batch_size: int): + return self.slots.expand(batch_size, -1, -1) diff --git a/videosaur/modules/networks.py b/videosaur/modules/networks.py new file mode 100644 index 0000000..2d98dc6 --- /dev/null +++ b/videosaur/modules/networks.py @@ -0,0 +1,902 @@ +import math +from typing import Callable, List, Optional, Tuple, Union + +import einops +import torch +from torch import nn +from torch.nn import functional as F + +from videosaur.modules import utils +from videosaur.utils import make_build_fn + +# Default weight init for MLP, CNNEncoder, CNNDecoder +DEFAULT_WEIGHT_INIT = "default" + + +@make_build_fn(__name__, "network module") +def build(config, name: str): + if name == "two_layer_mlp": + inp_dim = None + outp_dim = None + if "dim" in config: + inp_dim = config["dim"] + outp_dim = config["dim"] + if "inp_dim" in config: + inp_dim = config["inp_dim"] + if "outp_dim" in config: + outp_dim = config["outp_dim"] + + if inp_dim is None: + raise ValueError("Specify input dimensions with `inp_dim` or `dim`") + if outp_dim is None: + raise ValueError("Specify output dimension with `outp_dim` or `dim`") + + hidden_dims = [config.get("hidden_dim", 4 * inp_dim)] + layer_norm = config.get("layer_norm") or config.get("initial_layer_norm", False) + residual = config.get("residual", False) + activation = config.get("activation", "relu") + final_activation = config.get("final_activation", False) + weight_init = config.get("weight_init", DEFAULT_WEIGHT_INIT) + + return MLP( + inp_dim, + outp_dim, + hidden_dims, + layer_norm, + activation, + final_activation, + residual, + weight_init, + ) + elif name == "slot_attention_encoder" or name.startswith("savi_cnn_encoder"): + inp_dim = config.get("inp_dim", 3) + + if name == "slot_attention_encoder": + feature_multiplier = 1 + downsamplings = 0 + elif name == "savi_cnn_encoder": + feature_multiplier = 1 + downsamplings = 1 + elif name == "savi_cnn_encoder_64": + feature_multiplier = 0.5 + downsamplings = 0 + + feature_multiplier = config.get("feature_multiplier", feature_multiplier) + downsamplings = config.get("downsamplings", downsamplings) + weight_init = config.get("weight_init", DEFAULT_WEIGHT_INIT) + + return make_slot_attention_encoder(inp_dim, feature_multiplier, downsamplings, weight_init) + elif name.startswith("savi_decoder"): + inp_dim = config.get("inp_dim") + if inp_dim is None: + raise ValueError("Need to specify input dimensions with `inp_dim`") + + if name == "savi_decoder": + upsamplings = 4 + elif name == "savi_decoder_64": + upsamplings = 3 + + upsamplings = config.get("upsamplings", upsamplings) + weight_init = config.get("weight_init", DEFAULT_WEIGHT_INIT) + + return make_savi_decoder( + inp_dim, config.get("feature_multiplier", 1), upsamplings, weight_init + ) + else: + return None + + +class MLP(nn.Module): + def __init__( + self, + inp_dim: int, + outp_dim: int, + hidden_dims: List[int], + initial_layer_norm: bool = False, + activation: Union[str, nn.Module] = "relu", + final_activation: Union[bool, str] = False, + residual: bool = False, + weight_init: str = DEFAULT_WEIGHT_INIT, + ): + super().__init__() + self.residual = residual + if residual: + assert inp_dim == outp_dim + + layers = [] + if initial_layer_norm: + layers.append(nn.LayerNorm(inp_dim)) + + cur_dim = inp_dim + for dim in hidden_dims: + layers.append(nn.Linear(cur_dim, dim)) + layers.append(utils.get_activation_fn(activation)) + cur_dim = dim + + layers.append(nn.Linear(cur_dim, outp_dim)) + if final_activation: + if isinstance(final_activation, bool): + final_activation = "relu" + layers.append(utils.get_activation_fn(final_activation)) + + self.layers = nn.Sequential(*layers) + utils.init_parameters(self.layers, weight_init) + + def forward(self, inp: torch.Tensor) -> torch.Tensor: + outp = self.layers(inp) + + if self.residual: + return inp + outp + else: + return outp + + +def _infer_common_length(fail_on_missing_length=True, **kwargs) -> int: + """Given kwargs of scalars and lists, checks that all lists have the same length and returns it. + + Optionally fails if no length was provided. + """ + length = None + name = None + for cur_name, arg in kwargs.items(): + if isinstance(arg, (tuple, list)): + cur_length = len(arg) + if length is None: + length = cur_length + name = cur_name + elif cur_length != length: + raise ValueError( + f"Inconsistent lengths: {cur_name} has length {cur_length}, " + f"but {name} has length {length}" + ) + + if fail_on_missing_length and length is None: + names = ", ".join(f"`{key}`" for key in kwargs.keys()) + raise ValueError(f"Need to specify a list for at least one of {names}.") + + return length + + +def _maybe_expand_list(arg: Union[int, List[int]], length: int) -> list: + if not isinstance(arg, (tuple, list)): + return [arg] * length + + return list(arg) + + +class CNNEncoder(nn.Sequential): + """Simple convolutional encoder. + + For `features`, `kernel_sizes`, `strides`, scalars can be used to avoid repeating arguments, + but at least one list needs to be provided to specify the number of layers. + """ + + def __init__( + self, + inp_dim: int, + features: Union[int, List[int]], + kernel_sizes: Union[int, List[int]], + strides: Union[int, List[int]] = 1, + outp_dim: Optional[int] = None, + weight_init: str = "default", + ): + length = _infer_common_length(features=features, kernel_sizes=kernel_sizes, strides=strides) + features = _maybe_expand_list(features, length) + kernel_sizes = _maybe_expand_list(kernel_sizes, length) + strides = _maybe_expand_list(strides, length) + + layers = [] + cur_dim = inp_dim + for dim, kernel_size, stride in zip(features, kernel_sizes, strides): + layers.append( + nn.Conv2d( + cur_dim, + dim, + kernel_size=kernel_size, + stride=stride, + padding=self.get_same_padding(kernel_size, stride), + ) + ) + layers.append(nn.ReLU(inplace=True)) + cur_dim = dim + + if outp_dim is not None: + layers.append(nn.Conv1d(cur_dim, outp_dim, kernel_size=1, stride=1)) + + super().__init__(*layers) + utils.init_parameters(self, weight_init) + + @staticmethod + def get_same_padding(kernel_size: int, stride: int) -> Union[str, int]: + """Try to infer same padding for convolutions.""" + # This method is very lazily implemented, but oh well.. + if stride == 1: + return "same" + if kernel_size == 3: + if stride == 2: + return 1 + elif kernel_size == 5: + if stride == 2: + return 2 + + raise ValueError(f"Don't know 'same' padding for kernel {kernel_size}, stride {stride}") + + +def make_slot_attention_encoder( + inp_dim: int, + feature_multiplier: float = 1, + downsamplings: int = 0, + weight_init: str = DEFAULT_WEIGHT_INIT, +) -> CNNEncoder: + """CNN encoder as used in Slot Attention paper. + + By default, 4 layers with 64 channels each, keeping the spatial input resolution the same. + + This encoder is also used by SAVi, in the following configurations: + + - for image resolution 64: feature_multiplier=0.5, downsamplings=0 + - for image resolution 128: feature_multiplier=1, downsamplings=1 + + and STEVE, in the following configurations: + + - for image resolution 64: feature_multiplier=1, downsamplings=0 + - for image resolution 128: feature_multiplier=1, downsamplings=1 + """ + assert 0 <= downsamplings <= 4 + channels = int(64 * feature_multiplier) + strides = [2] * downsamplings + [1] * (4 - downsamplings) + return CNNEncoder( + inp_dim, + features=[channels, channels, channels, channels], + kernel_sizes=[5, 5, 5, 5], + strides=strides, + weight_init=weight_init, + ) + + +class CNNDecoder(nn.Sequential): + """Simple convolutional decoder. + + For `features`, `kernel_sizes`, `strides`, scalars can be used to avoid repeating arguments, + but at least one list needs to be provided to specify the number of layers. + """ + + def __init__( + self, + inp_dim: int, + features: Union[int, List[int]], + kernel_sizes: Union[int, List[int]], + strides: Union[int, List[int]] = 1, + outp_dim: Optional[int] = None, + weight_init: str = DEFAULT_WEIGHT_INIT, + ): + length = _infer_common_length(features=features, kernel_sizes=kernel_sizes, strides=strides) + features = _maybe_expand_list(features, length) + kernel_sizes = _maybe_expand_list(kernel_sizes, length) + strides = _maybe_expand_list(strides, length) + + layers = [] + cur_dim = inp_dim + for dim, kernel_size, stride in zip(features, kernel_sizes, strides): + padding, output_padding = self.get_same_padding(kernel_size, stride) + layers.append( + nn.ConvTranspose2d( + cur_dim, + dim, + kernel_size=kernel_size, + stride=stride, + padding=padding, + output_padding=output_padding, + ) + ) + layers.append(nn.ReLU(inplace=True)) + cur_dim = dim + + if outp_dim is not None: + layers.append(nn.Conv1d(cur_dim, outp_dim, kernel_size=1, stride=1)) + + super().__init__(*layers) + utils.init_parameters(self, weight_init) + + @staticmethod + def get_same_padding(kernel_size: int, stride: int) -> Tuple[int, int]: + """Try to infer same padding for transposed convolutions.""" + # This method is very lazily implemented, but oh well.. + if kernel_size == 3: + if stride == 1: + return 1, 0 + if stride == 2: + return 1, 1 + elif kernel_size == 5: + if stride == 1: + return 2, 0 + if stride == 2: + return 2, 1 + + raise ValueError(f"Don't know 'same' padding for kernel {kernel_size}, stride {stride}") + + +def make_savi_decoder( + inp_dim: int, + feature_multiplier: float = 1, + upsamplings: int = 4, + weight_init: str = DEFAULT_WEIGHT_INIT, +) -> CNNDecoder: + """CNN encoder as used in SAVi paper. + + By default, 4 layers with 64 channels each, upscaling from a 8x8 feature map to 128x128. + """ + assert 0 <= upsamplings <= 4 + channels = int(64 * feature_multiplier) + strides = [2] * upsamplings + [1] * (4 - upsamplings) + return CNNDecoder( + inp_dim, + features=[channels, channels, channels, channels], + kernel_sizes=[5, 5, 5, 5], + strides=strides, + weight_init=weight_init, + ) + + +class Attention(nn.Module): + """Multihead attention. + + Adapted from timm's ViT implementation. + """ + + def __init__( + self, + dim: int, + num_heads: int, + kdim: Optional[int] = None, + vdim: Optional[int] = None, + inner_dim: Optional[int] = None, + qkv_bias: bool = False, + attn_drop: float = 0.0, + proj_drop: float = 0.0, + ): + super().__init__() + kdim = dim if kdim is None else kdim + vdim = dim if vdim is None else vdim + inner_dim = dim if inner_dim is None else inner_dim + if inner_dim % num_heads != 0: + raise ValueError("`inner_dim` must be divisible by `num_heads`") + + self.num_heads = num_heads + self.inner_dim = inner_dim + self.head_dim = inner_dim // num_heads + self.scale = self.head_dim**-0.5 + + self._same_qkv_dim = dim == kdim and dim == vdim + self._same_kv_dim = kdim == vdim + + if self._same_qkv_dim: + self.qkv = nn.Linear(dim, inner_dim * 3, bias=qkv_bias) + elif self._same_kv_dim: + self.q = nn.Linear(dim, inner_dim, bias=qkv_bias) + self.kv = nn.Linear(kdim, inner_dim * 2, bias=qkv_bias) + else: + self.q = nn.Linear(dim, inner_dim, bias=qkv_bias) + self.k = nn.Linear(kdim, inner_dim, bias=qkv_bias) + self.v = nn.Linear(vdim, inner_dim, bias=qkv_bias) + + self.attn_drop = nn.Dropout(attn_drop) + self.out_proj = nn.Linear(inner_dim, dim) + self.out_proj_drop = nn.Dropout(proj_drop) + + self.init_parameters() + + def init_parameters(self): + if self._same_qkv_dim: + bound = math.sqrt(6.0 / (self.qkv.weight.shape[0] // 3 + self.qkv.weight.shape[1])) + nn.init.uniform_(self.qkv.weight, -bound, bound) # Xavier init for separate Q, K, V + if self.qkv.bias is not None: + nn.init.constant_(self.qkv.bias, 0.0) + elif self._same_kv_dim: + utils.init_parameters(self.q, "xavier_uniform") + bound = math.sqrt(6.0 / (self.kv.weight.shape[0] // 2 + self.kv.weight.shape[1])) + nn.init.uniform_(self.kv.weight, -bound, bound) # Xavier init for separate K, V + if self.kv.bias is not None: + nn.init.constant_(self.kv.bias, 0.0) + else: + utils.init_parameters((self.q, self.k, self.v), "xavier_uniform") + + utils.init_parameters(self.out_proj, "xavier_uniform") + + def _in_proj(self, q, k, v): + """Efficiently compute in-projection. + + Adapted from torch.nn.functional.multi_head_attention. + """ + if self._same_qkv_dim: + w_kv = b_kv = b_q = b_k = b_v = None + w = self.qkv.weight + b = self.qkv.bias if hasattr(self.qkv, "bias") else None + elif self._same_kv_dim: + w = b = b_k = b_v = None + w_q = self.q.weight + w_kv = self.kv.weight + b_q = self.q.bias if hasattr(self.q, "bias") else None + b_kv = self.kv.bias if hasattr(self.kv, "bias") else None + else: + w = w_kv = b = b_kv = None + w_q = self.q.weight + w_k = self.k.weight + w_v = self.v.weight + b_q = self.q.bias if hasattr(self.q, "bias") else None + b_k = self.k.bias if hasattr(self.k, "bias") else None + b_v = self.v.bias if hasattr(self.v, "bias") else None + + if k is v: + if q is k: + # Self-attention + return F.linear(q, w, b).chunk(3, dim=-1) + else: + # Encoder-decoder attention + if w is not None: + dim = w.shape[0] // 3 + w_q, w_kv = w.split([dim, dim * 2]) + if b is not None: + b_q, b_kv = b.split([dim, dim * 2]) + return (F.linear(q, w_q, b_q),) + F.linear(k, w_kv, b_kv).chunk(2, dim=-1) + else: + if w is not None: + w_q, w_k, w_v = w.chunk(3) + if b is not None: + b_q, b_k, b_v = b.chunk(3) + elif w_kv is not None: + w_k, w_v = w_kv.chunk(2) + if b_kv is not None: + b_k, b_v = b_kv.chunk(2) + + return F.linear(q, w_q, b_q), F.linear(k, w_k, b_k), F.linear(v, w_v, b_v) + + def forward( + self, + query: torch.Tensor, + key: Optional[torch.Tensor] = None, + value: Optional[torch.Tensor] = None, + attn_mask: Optional[torch.Tensor] = None, + key_padding_mask: Optional[torch.Tensor] = None, + return_weights: bool = False, + ) -> Tuple[torch.Tensor, torch.Tensor]: + key = key if key is not None else query + value = value if value is not None else query + + bs, n_queries, _ = query.shape + n_keys = key.shape[1] + + if attn_mask is not None: + if attn_mask.ndim == 2: + expected = (n_queries, n_keys) + if attn_mask.shape != expected: + raise ValueError( + f"2D `attn_mask` should have shape {expected}, but has " + f"shape {attn_mask.shape}" + ) + attn_mask = attn_mask.unsqueeze(0) + elif attn_mask.ndim == 3: + expected = (bs * self.num_heads, n_queries, n_keys) + if attn_mask.shape != expected: + raise ValueError( + f"3D `attn_mask` should have shape {expected}, but has " + f"shape {attn_mask.shape}" + ) + if key_padding_mask is not None: + assert key_padding_mask.dtype == torch.bool + expected = (bs, n_keys) + if key_padding_mask.shape != expected: + raise ValueError( + f"`key_padding_mask` should have shape {expected}, but has shape " + f"{key_padding_mask.shape}" + ) + key_padding_mask = einops.repeat( + key_padding_mask, "b n -> (b h) 1 n", b=bs, h=self.num_heads, n=n_keys + ) + if attn_mask is None: + attn_mask = key_padding_mask + else: + attn_mask = attn_mask.masked_fill(key_padding_mask, float("-inf")) + + q, k, v = self._in_proj(query, key, value) + + q = einops.rearrange(q, "b n (h d) -> (b h) n d", h=self.num_heads, d=self.head_dim) + k = einops.rearrange(k, "b n (h d) -> (b h) n d", h=self.num_heads, d=self.head_dim) + v = einops.rearrange(v, "b n (h d) -> (b h) n d", h=self.num_heads, d=self.head_dim) + + q_scaled = q / self.scale + if attn_mask is not None: + attn = torch.baddbmm(attn_mask, q_scaled, k.transpose(-2, -1)) + else: + attn = torch.bmm(q_scaled, k.transpose(-2, -1)) + + attn = attn.softmax(dim=-1) # (B x H) x N x M + pre_dropout_attn = attn + attn = self.attn_drop(attn) + + weighted_v = attn @ v + x = einops.rearrange(weighted_v, "(b h) n d -> b n (h d)", h=self.num_heads, d=self.head_dim) + x = self.out_proj(x) + x = self.out_proj_drop(x) + + if return_weights: + weights = einops.rearrange(pre_dropout_attn, "(b h) n m -> b h n m", h=self.num_heads) + return x, weights.mean(dim=1) + else: + return x, None + + +class TransformerEncoderLayer(nn.TransformerEncoderLayer): + """Like torch.nn.TransformerEncoderLayer, but with customizations.""" + + def __init__( + self, + d_model: int, + nhead: int, + dim_feedforward: int = 2048, + dim_attn: Optional[int] = None, + dim_kv: Optional[int] = None, + qkv_bias: bool = True, + dropout: float = 0.1, + activation: Union[str, Callable[[torch.Tensor], torch.Tensor]] = torch.nn.functional.relu, + layer_norm_eps: float = 1e-5, + batch_first: bool = False, + norm_first: bool = False, + initial_residual_scale: Optional[float] = None, + device=None, + dtype=None, + ): + super().__init__( + d_model, + nhead, + dim_feedforward, + dropout, + activation, + layer_norm_eps, + batch_first, + norm_first, + device=device, + dtype=dtype, + ) + self.self_attn = Attention( + dim=d_model, + num_heads=nhead, + kdim=dim_kv, + vdim=dim_kv, + inner_dim=dim_attn, + qkv_bias=qkv_bias, + attn_drop=dropout, + proj_drop=dropout, + ) + + if initial_residual_scale is not None: + self.scale1 = utils.LayerScale(d_model, init_values=initial_residual_scale) + self.scale2 = utils.LayerScale(d_model, init_values=initial_residual_scale) + else: + self.scale1 = nn.Identity() + self.scale2 = nn.Identity() + + def _sa_block( + self, + x: torch.Tensor, + attn_mask: Optional[torch.Tensor] = None, + key_padding_mask: Optional[torch.Tensor] = None, + keys: Optional[torch.Tensor] = None, + values: Optional[torch.Tensor] = None, + return_weights: bool = False, + ) -> torch.Tensor: + keys = keys if keys is not None else x + values = values if values is not None else x + x, attn = self.self_attn( + x, + keys, + values, + attn_mask=attn_mask, + key_padding_mask=key_padding_mask, + return_weights=return_weights, + ) + x = self.dropout1(x) + + if return_weights: + return x, attn + else: + return x + + def forward( + self, + src: torch.Tensor, + src_mask: Optional[torch.Tensor] = None, + src_key_padding_mask: Optional[torch.Tensor] = None, + memory: Optional[torch.Tensor] = None, + ) -> torch.Tensor: + x = src + if self.norm_first: + x = x + self.scale1( + self._sa_block( + self.norm1(x), src_mask, src_key_padding_mask, keys=memory, values=memory + ) + ) + x = x + self.scale2(self._ff_block(self.norm2(x))) + else: + x = self.norm1( + x + + self.scale1( + self._sa_block(x, src_mask, src_key_padding_mask, keys=memory, values=memory) + ) + ) + x = self.norm2(x + self.scale2(self._ff_block(x))) + + return x + + +class TransformerEncoder(nn.Module): + def __init__( + self, + dim: int, + n_blocks: int, + n_heads: int, + qkv_dim: Optional[int] = None, + memory_dim: Optional[int] = None, + qkv_bias: bool = True, + dropout: float = 0.0, + activation: Union[str, Callable[[torch.Tensor], torch.Tensor]] = "relu", + hidden_dim: Optional[int] = None, + initial_residual_scale: Optional[float] = None, + ): + super().__init__() + + if hidden_dim is None: + hidden_dim = 4 * dim + + self.blocks = nn.ModuleList( + [ + TransformerEncoderLayer( + dim, + n_heads, + dim_feedforward=hidden_dim, + dim_attn=qkv_dim, + dim_kv=memory_dim, + qkv_bias=qkv_bias, + dropout=dropout, + activation=activation, + layer_norm_eps=1e-05, + batch_first=True, + norm_first=True, + initial_residual_scale=initial_residual_scale, + ) + for _ in range(n_blocks) + ] + ) + + def forward( + self, + inp: torch.Tensor, + mask: Optional[torch.Tensor] = None, + key_padding_mask: Optional[torch.Tensor] = None, + memory: Optional[torch.Tensor] = None, + ) -> torch.Tensor: + x = inp + + for block in self.blocks: + x = block(x, mask, key_padding_mask, memory) + + return x + + +class TransformerDecoderLayer(nn.TransformerDecoderLayer): + """Like torch.nn.TransformerDecoderLayer, but with customizations.""" + + def __init__( + self, + d_model: int, + nhead: int, + dim_feedforward: int = 2048, + dim_attn: Optional[int] = None, + dim_kv: Optional[int] = None, + qkv_bias: bool = True, + dropout: float = 0.1, + activation: Union[str, Callable[[torch.Tensor], torch.Tensor]] = torch.nn.functional.relu, + layer_norm_eps: float = 1e-5, + batch_first: bool = False, + norm_first: bool = False, + initial_residual_scale: Optional[float] = None, + device=None, + dtype=None, + ): + super().__init__( + d_model, + nhead, + dim_feedforward, + dropout, + activation, + layer_norm_eps, + batch_first, + norm_first, + device=device, + dtype=dtype, + ) + self.self_attn = Attention( + dim=d_model, + num_heads=nhead, + inner_dim=dim_attn, + qkv_bias=qkv_bias, + attn_drop=dropout, + proj_drop=dropout, + ) + self.multihead_attn = Attention( + dim=d_model, + num_heads=nhead, + kdim=dim_kv, + vdim=dim_kv, + inner_dim=dim_attn, + qkv_bias=qkv_bias, + attn_drop=dropout, + proj_drop=dropout, + ) + + if initial_residual_scale is not None: + self.scale1 = utils.LayerScale(d_model, init_values=initial_residual_scale) + self.scale2 = utils.LayerScale(d_model, init_values=initial_residual_scale) + self.scale3 = utils.LayerScale(d_model, init_values=initial_residual_scale) + else: + self.scale1 = nn.Identity() + self.scale2 = nn.Identity() + self.scale3 = nn.Identity() + + def _sa_block( + self, + x: torch.Tensor, + attn_mask: Optional[torch.Tensor] = None, + key_padding_mask: Optional[torch.Tensor] = None, + return_weights: bool = False, + ) -> torch.Tensor: + x, attn = self.self_attn( + x, + x, + x, + attn_mask=attn_mask, + key_padding_mask=key_padding_mask, + return_weights=return_weights, + ) + x = self.dropout1(x) + + if return_weights: + return x, attn + else: + return x, None + + def _mha_block( + self, + x: torch.Tensor, + mem: torch.Tensor, + attn_mask: Optional[torch.Tensor], + key_padding_mask: Optional[torch.Tensor], + return_weights: bool = False, + ) -> torch.Tensor: + x, attn = self.multihead_attn( + x, + mem, + mem, + attn_mask=attn_mask, + key_padding_mask=key_padding_mask, + return_weights=return_weights, + ) + x = self.dropout2(x) + + if return_weights: + return x, attn + else: + return x, None + + def forward( + self, + tgt: torch.Tensor, + memory: torch.Tensor, + tgt_mask: Optional[torch.Tensor] = None, + memory_mask: Optional[torch.Tensor] = None, + tgt_key_padding_mask: Optional[torch.Tensor] = None, + memory_key_padding_mask: Optional[torch.Tensor] = None, + return_weights: bool = False, + ) -> torch.Tensor: + """Pass the inputs (and mask) through the decoder layer. + + Args: + tgt: the sequence to the decoder layer (required). + memory: the sequence from the last layer of the encoder (required). + tgt_mask: the mask for the tgt sequence (optional). + memory_mask: the mask for the memory sequence (optional). + tgt_key_padding_mask: the mask for the tgt keys per batch (optional). + memory_key_padding_mask: the mask for the memory keys per batch (optional). + """ + x = tgt + if self.norm_first: + residual, attn1 = self._sa_block(self.norm1(x), tgt_mask, tgt_key_padding_mask) + x = x + self.scale1(residual) + residual, attn2 = self._mha_block( + self.norm2(x), memory, memory_mask, memory_key_padding_mask, return_weights + ) + x = x + self.scale2(residual) + residual = self._ff_block(self.norm3(x)) + x = x + self.scale3(residual) + else: + residual, attn1 = self._sa_block(x, tgt_mask, tgt_key_padding_mask) + x = self.norm1(x + self.scale1(residual)) + residual, attn2 = self._mha_block( + x, memory, memory_mask, memory_key_padding_mask, return_weights + ) + x = self.norm2(x + self.scale2(residual)) + residual = self._ff_block(x) + x = self.norm3(x + self.scale3(residual)) + + if return_weights: + return x, attn1, attn2 + else: + return x, None, None + + +class TransformerDecoder(nn.Module): + def __init__( + self, + dim: int, + n_blocks: int, + n_heads: int, + qkv_dim: Optional[int] = None, + memory_dim: Optional[int] = None, + qkv_bias: bool = True, + dropout: float = 0.0, + activation: Union[str, Callable[[torch.Tensor], torch.Tensor]] = "relu", + hidden_dim: Optional[int] = None, + initial_residual_scale: Optional[float] = None, + ): + super().__init__() + + if hidden_dim is None: + hidden_dim = 4 * dim + + self.blocks = nn.ModuleList( + [ + TransformerDecoderLayer( + dim, + n_heads, + dim_feedforward=hidden_dim, + dim_attn=qkv_dim, + dim_kv=memory_dim, + qkv_bias=qkv_bias, + dropout=dropout, + activation=activation, + layer_norm_eps=1e-05, + batch_first=True, + norm_first=True, + initial_residual_scale=initial_residual_scale, + ) + for _ in range(n_blocks) + ] + ) + + def forward( + self, + tgt: torch.Tensor, + memory: torch.Tensor, + tgt_mask: Optional[torch.Tensor] = None, + memory_mask: Optional[torch.Tensor] = None, + tgt_key_padding_mask: Optional[torch.Tensor] = None, + memory_key_padding_mask: Optional[torch.Tensor] = None, + return_weights: bool = False, + ) -> torch.Tensor: + output = tgt + + for idx, block in enumerate(self.blocks): + output, _, attn = block( + output, + memory, + tgt_mask=tgt_mask, + memory_mask=memory_mask, + tgt_key_padding_mask=tgt_key_padding_mask, + memory_key_padding_mask=memory_key_padding_mask, + return_weights=return_weights and idx == len(self.blocks) - 1, + ) + + if return_weights: + return output, attn + else: + return output diff --git a/videosaur/modules/timm.py b/videosaur/modules/timm.py new file mode 100644 index 0000000..4a3b969 --- /dev/null +++ b/videosaur/modules/timm.py @@ -0,0 +1,527 @@ +"""Implementations of custom timm models. + +Models are registered with timm and can be instantiated with `timm.create_model`. + +This module provides the following models: + +- resnet18_savi (no pre-trained weights) +- resnet34_savi (no pre-trained weights) +- resnet50_savi (no pre-trained weights) +- resnet50_dino +- vit_base_patch16_224_mae +- vit_large_patch16_224_mae +- vit_huge_patch16_224_mae +- resnet50_mocov3 +- vit_small_patch16_224_mocov3 +- vit_base_patch16_224_mocov3 +- vit_small_patch16_224_msn +- vit_base_patch16_224_msn +- vit_base_patch4_224_msn +- vit_large_patch16_224_msn +- vit_large_patch7_224_msn +""" +import math +from functools import partial +from typing import List, Optional + +import timm +import torch +from timm.models import layers, resnet, vision_transformer +from timm.models.helpers import build_model_with_cfg, resolve_pretrained_cfg +from torch import nn + + +def _create_savi_resnet(block, stages, **kwargs) -> resnet.ResNet: + """ResNet as used by SAVi and SAVi++, adapted from SAVi codebase. + + The differences to the normal timm ResNet implementation are to use group norm instead of batch + norm, and to use 3x3 filters, 1x1 strides and no max pooling in the stem. + + Returns 16x16 feature maps for input size of 128x128, and 28x28 features maps for inputs of + size 224x224. + """ + model_args = dict(block=block, layers=stages, norm_layer=layers.GroupNorm, **kwargs) + model = resnet._create_resnet("resnet34", pretrained=False, **model_args) + model.conv1 = nn.Conv2d( + model.conv1.in_channels, + model.conv1.out_channels, + kernel_size=3, + stride=1, + padding=1, + bias=False, + ) + model.maxpool = nn.Identity() + model.init_weights(zero_init_last=True) # Re-init weights because we added a new conv layer + return model + + +@timm.models.register_model +def resnet18_savi(pretrained=False, **kwargs): + """ResNet18 as implemented in SAVi codebase. + + Final features have 512 channels. + """ + if pretrained: + raise ValueError("No pretrained weights available for `resnet18_savi`.") + return _create_savi_resnet(resnet.BasicBlock, stages=[2, 2, 2, 2], **kwargs) + + +@timm.models.register_model +def resnet34_savi(pretrained=False, **kwargs): + """ResNet34 as used in SAVi and SAVi++ papers. + + Final features have 512 channels. + """ + if pretrained: + raise ValueError("No pretrained weights available for `resnet34_savi`.") + return _create_savi_resnet(resnet.BasicBlock, stages=[3, 4, 6, 3], **kwargs) + + +@timm.models.register_model +def resnet50_savi(pretrained=False, **kwargs): + """ResNet50 as implemented in SAVi codebase. + + Final features have 2048 channels. + """ + if pretrained: + raise ValueError("No pretrained weights available for `resnet50_savi`.") + return _create_savi_resnet(resnet.Bottleneck, stages=[3, 4, 6, 3], **kwargs) + + +def _resnet50_dino_pretrained_filter(state_dict, model): + del model.fc + return state_dict + + +@timm.models.register_model +def resnet50_dino(pretrained=False, **kwargs): + """ResNet50 pre-trained with DINO, without classification head. + + Weights from https://github.com/facebookresearch/dino + """ + kwargs["pretrained_cfg"] = resnet._cfg( + url=( + "https://dl.fbaipublicfiles.com/dino/dino_resnet50_pretrain/" + "dino_resnet50_pretrain.pth" + ) + ) + model_args = dict(block=resnet.Bottleneck, layers=[3, 4, 6, 3], **kwargs) + model = build_model_with_cfg( + resnet.ResNet, + "resnet50_dino", + pretrained=pretrained, + pretrained_filter_fn=_resnet50_dino_pretrained_filter, + **model_args, + ) + return model + + +@timm.models.register_model +def vit_base_patch16_224_mae(pretrained=False, checkpoint_path=None, **kwargs): + """ViT-B/16 pre-trained with MAE. + + Weights from https://github.com/facebookresearch/mae + """ + if checkpoint_path is None: + kwargs["pretrained_cfg"] = vision_transformer._cfg( + url="https://dl.fbaipublicfiles.com/mae/pretrain/mae_pretrain_vit_base.pth" + ) + else: + kwargs["pretrained_cfg"] = vision_transformer._cfg(file=checkpoint_path) + model_kwargs = dict( + patch_size=16, + embed_dim=768, + depth=12, + num_heads=12, + mlp_ratio=4, + qkv_bias=True, + norm_layer=partial(nn.LayerNorm, eps=1e-6), + num_classes=0, + **kwargs, + ) + return vision_transformer._create_vision_transformer( + "vit_base_patch16_224_mae", pretrained=pretrained, **model_kwargs + ) + + +@timm.models.register_model +def vit_large_patch16_224_mae(pretrained=False, **kwargs): + """ViT-L/16 pre-trained with MAE. + + Weights from https://github.com/facebookresearch/mae + """ + kwargs["pretrained_cfg"] = vision_transformer._cfg( + url="https://dl.fbaipublicfiles.com/mae/pretrain/mae_pretrain_vit_large.pth" + ) + model_kwargs = dict( + patch_size=16, + embed_dim=1024, + depth=24, + num_heads=16, + mlp_ratio=4, + qkv_bias=True, + norm_layer=partial(nn.LayerNorm, eps=1e-6), + num_classes=0, + **kwargs, + ) + return vision_transformer._create_vision_transformer( + "vit_large_patch16_224_mae", pretrained=pretrained, **model_kwargs + ) + + +@timm.models.register_model +def vit_huge_patch14_224_mae(pretrained=False, **kwargs): + """ViT-H/14 pre-trained with MAE. + + Weights from https://github.com/facebookresearch/mae + """ + kwargs["pretrained_cfg"] = vision_transformer._cfg( + url="https://dl.fbaipublicfiles.com/mae/pretrain/mae_pretrain_vit_huge.pth" + ) + model_kwargs = dict( + patch_size=14, + embed_dim=1280, + depth=32, + num_heads=16, + mlp_ratio=4, + qkv_bias=True, + norm_layer=partial(nn.LayerNorm, eps=1e-6), + num_classes=0, + **kwargs, + ) + return vision_transformer._create_vision_transformer( + "vit_huge_patch14_224_mae", pretrained=pretrained, **model_kwargs + ) + + +def _add_moco_v3_positional_embedding(model, temperature=10000.0): + """MoCo-v3 ViT uses 2D sincos embedding instead of learned positional encoding. + + Adapted from https://github.com/facebookresearch/moco-v3/blob/main/vits.py + """ + h, w = model.patch_embed.grid_size + grid_w = torch.arange(w, dtype=torch.float32) + grid_h = torch.arange(h, dtype=torch.float32) + grid_w, grid_h = torch.meshgrid(grid_w, grid_h, indexing="ij") + assert ( + model.embed_dim % 4 == 0 + ), "Embed dimension must be divisible by 4 for 2D sin-cos position embedding" + + pos_dim = model.embed_dim // 4 + omega = torch.arange(pos_dim, dtype=torch.float32) / pos_dim + omega = 1.0 / (temperature**omega) + out_w = torch.einsum("m,d->md", [grid_w.flatten(), omega]) + out_h = torch.einsum("m,d->md", [grid_h.flatten(), omega]) + pos_emb = torch.cat( + [torch.sin(out_w), torch.cos(out_w), torch.sin(out_h), torch.cos(out_h)], dim=1 + )[None, :, :] + if hasattr(model, "num_tokens"): # Old timm versions + assert model.num_tokens == 1, "Assuming one and only one token, [cls]" + else: + assert model.num_prefix_tokens == 1, "Assuming one and only one token, [cls]" + + pe_token = torch.zeros([1, 1, model.embed_dim], dtype=torch.float32) + model.pos_embed = nn.Parameter(torch.cat([pe_token, pos_emb], dim=1)) + model.pos_embed.requires_grad = False + + +def _moco_v3_pretrained_filter(state_dict, model, linear_name): + state_dict = state_dict["state_dict"] + + for k in list(state_dict.keys()): + # Retain only base_encoder up to before the embedding layer + if k.startswith("module.base_encoder") and not k.startswith( + f"module.base_encoder.{linear_name}" + ): + # Remove prefix + state_dict[k[len("module.base_encoder.") :]] = state_dict[k] + # Delete renamed or unused k + del state_dict[k] + + if hasattr(model, "fc"): + del model.fc + + return state_dict + + +def _create_moco_v3_vit(variant, pretrained=False, **kwargs): + pretrained_cfg = resolve_pretrained_cfg( + variant, pretrained_cfg=kwargs.pop("pretrained_cfg", None) + ) + model = build_model_with_cfg( + vision_transformer.VisionTransformer, + variant, + pretrained=pretrained, + pretrained_cfg=pretrained_cfg, + pretrained_filter_fn=partial(_moco_v3_pretrained_filter, linear_name="head"), + **kwargs, + ) + _add_moco_v3_positional_embedding(model) + return model + + +@timm.models.register_model +def vit_small_patch16_224_mocov3(pretrained=False, **kwargs): + """ViT-S/16 pre-trained with MoCo-v3. + + Weights from https://github.com/facebookresearch/moco-v3 + """ + kwargs["pretrained_cfg"] = vision_transformer._cfg( + url="https://dl.fbaipublicfiles.com/moco-v3/vit-s-300ep/vit-s-300ep.pth.tar" + ) + model_kwargs = dict( + patch_size=16, + embed_dim=384, + depth=12, + num_heads=12, + mlp_ratio=4, + qkv_bias=True, + norm_layer=partial(nn.LayerNorm, eps=1e-6), + num_classes=0, + **kwargs, + ) + return _create_moco_v3_vit("vit_small_patch16_224_mocov3", pretrained=pretrained, **model_kwargs) + + +@timm.models.register_model +def vit_base_patch16_224_mocov3(pretrained=False, **kwargs): + """ViT-B/16 pre-trained with MoCo-v3. + + Weights from https://github.com/facebookresearch/moco-v3 + """ + kwargs["pretrained_cfg"] = vision_transformer._cfg( + url="https://dl.fbaipublicfiles.com/moco-v3/vit-b-300ep/vit-b-300ep.pth.tar" + ) + model_kwargs = dict( + patch_size=16, + embed_dim=768, + depth=12, + num_heads=12, + mlp_ratio=4, + qkv_bias=True, + norm_layer=partial(nn.LayerNorm, eps=1e-6), + num_classes=0, + **kwargs, + ) + return _create_moco_v3_vit("vit_base_patch16_224_mocov3", pretrained=pretrained, **model_kwargs) + + +@timm.models.register_model +def resnet50_mocov3(pretrained=False, **kwargs): + """ResNet-50 pre-trained with MoCo-v3. + + Weights from https://github.com/facebookresearch/moco-v3 + """ + kwargs["pretrained_cfg"] = resnet._cfg( + url="https://dl.fbaipublicfiles.com/moco-v3/r-50-1000ep/r-50-1000ep.pth.tar" + ) + model_args = dict(block=resnet.Bottleneck, layers=[3, 4, 6, 3], **kwargs) + return build_model_with_cfg( + resnet.ResNet, + "resnet50_mocov3", + pretrained=pretrained, + pretrained_filter_fn=partial(_moco_v3_pretrained_filter, linear_name="fc"), + **model_args, + ) + + +def _msn_vit_pretrained_filter(state_dict, model): + state_dict = state_dict["target_encoder"] + + for k in list(state_dict.keys()): + if not k.startswith("module.fc."): + # remove prefix + state_dict[k[len("module.") :]] = state_dict[k] + # delete renamed or unused k + del state_dict[k] + + return state_dict + + +def _create_msn_vit(variant, pretrained=False, **kwargs): + pretrained_cfg = resolve_pretrained_cfg( + variant, pretrained_cfg=kwargs.pop("pretrained_cfg", None) + ) + model = build_model_with_cfg( + vision_transformer.VisionTransformer, + variant, + pretrained=pretrained, + pretrained_cfg=pretrained_cfg, + pretrained_filter_fn=_msn_vit_pretrained_filter, + **kwargs, + ) + return model + + +@timm.models.register_model +def vit_small_patch16_224_msn(pretrained=False, **kwargs): + """ViT-S/16 pre-trained with MSN. + + Weights from https://github.com/facebookresearch/msn + """ + kwargs["pretrained_cfg"] = vision_transformer._cfg( + url="https://dl.fbaipublicfiles.com/msn/vits16_800ep.pth.tar" + ) + model_kwargs = dict( + patch_size=16, + embed_dim=384, + depth=12, + num_heads=6, + mlp_ratio=4, + qkv_bias=True, + norm_layer=partial(nn.LayerNorm, eps=1e-6), + num_classes=0, + **kwargs, + ) + return _create_msn_vit("vit_small_patch16_224_msn", pretrained=pretrained, **model_kwargs) + + +@timm.models.register_model +def vit_base_patch16_224_msn(pretrained=False, **kwargs): + """ViT-B/16 pre-trained with MSN. + + Weights from https://github.com/facebookresearch/msn + """ + kwargs["pretrained_cfg"] = vision_transformer._cfg( + url="https://dl.fbaipublicfiles.com/msn/vitb16_600ep.pth.tar" + ) + model_kwargs = dict( + patch_size=16, + embed_dim=768, + depth=12, + num_heads=12, + mlp_ratio=4, + qkv_bias=True, + norm_layer=partial(nn.LayerNorm, eps=1e-6), + num_classes=0, + **kwargs, + ) + return _create_msn_vit("vit_base_patch16_224_msn", pretrained=pretrained, **model_kwargs) + + +@timm.models.register_model +def vit_base_patch4_224_msn(pretrained=False, **kwargs): + """ViT-B/4 pre-trained with MSN. + + Weights from https://github.com/facebookresearch/msn + """ + kwargs["pretrained_cfg"] = vision_transformer._cfg( + url="https://dl.fbaipublicfiles.com/msn/vitb4_300ep.pth.tar" + ) + model_kwargs = dict( + patch_size=4, + embed_dim=768, + depth=12, + num_heads=12, + mlp_ratio=4, + qkv_bias=True, + norm_layer=partial(nn.LayerNorm, eps=1e-6), + num_classes=0, + **kwargs, + ) + return _create_msn_vit("vit_base_patch4_224_msn", pretrained=pretrained, **model_kwargs) + + +@timm.models.register_model +def vit_large_patch16_224_msn(pretrained=False, **kwargs): + """ViT-L/16 pre-trained with MSN. + + Weights from https://github.com/facebookresearch/msn + """ + kwargs["pretrained_cfg"] = vision_transformer._cfg( + url="https://dl.fbaipublicfiles.com/msn/vitl16_600ep.pth.tar" + ) + model_kwargs = dict( + patch_size=16, + embed_dim=1024, + depth=24, + num_heads=16, + mlp_ratio=4, + qkv_bias=True, + norm_layer=partial(nn.LayerNorm, eps=1e-6), + num_classes=0, + **kwargs, + ) + return _create_msn_vit("vit_large_patch16_224_msn", pretrained=pretrained, **model_kwargs) + + +@timm.models.register_model +def vit_large_patch7_224_msn(pretrained=False, **kwargs): + """ViT-L/7 pre-trained with MSN. + + Weights from https://github.com/facebookresearch/msn + """ + kwargs["pretrained_cfg"] = vision_transformer._cfg( + url="https://dl.fbaipublicfiles.com/msn/vitl7_200ep.pth.tar" + ) + model_kwargs = dict( + patch_size=7, + embed_dim=1024, + depth=24, + num_heads=16, + mlp_ratio=4, + qkv_bias=True, + norm_layer=partial(nn.LayerNorm, eps=1e-6), + num_classes=0, + **kwargs, + ) + return _create_msn_vit("vit_large_patch7_224_msn", pretrained=pretrained, **model_kwargs) + + +def patch_timm_for_fx_tracing(): + """Patch timm to allow torch.fx tracing.""" + + def resample_abs_pos_embed( + posemb, + new_size: List[int], + old_size: Optional[List[int]] = None, + num_prefix_tokens: int = 1, + interpolation: str = "bicubic", + antialias: bool = True, + verbose: bool = False, + ): + """From timm.layers.pos_embed.resample_abs_pose_embed. + + To avoid control flow using dynamic variables, the check returning early for same size + is not executed. + """ + # sort out sizes, assume square if old size not provided + num_pos_tokens = posemb.shape[1] + + # REMOVED because this relies on dynamic variables: + # num_new_tokens = new_size[0] * new_size[1] + num_prefix_tokens + # if num_new_tokens == num_pos_tokens and new_size[0] == new_size[1]: + # return posemb + + if old_size is None: + hw = int(math.sqrt(num_pos_tokens - num_prefix_tokens)) + old_size = hw, hw + + if num_prefix_tokens: + posemb_prefix, posemb = posemb[:, :num_prefix_tokens], posemb[:, num_prefix_tokens:] + else: + posemb_prefix, posemb = None, posemb + + # do the interpolation + embed_dim = posemb.shape[-1] + orig_dtype = posemb.dtype + posemb = posemb.float() # interpolate needs float32 + posemb = posemb.reshape(1, old_size[0], old_size[1], -1).permute(0, 3, 1, 2) + posemb = nn.functional.interpolate( + posemb, size=new_size, mode=interpolation, antialias=antialias + ) + posemb = posemb.permute(0, 2, 3, 1).reshape(1, -1, embed_dim) + posemb = posemb.to(orig_dtype) + + # add back extra (class, etc) prefix tokens + if posemb_prefix is not None: + posemb = torch.cat([posemb_prefix, posemb], dim=1) + + return posemb + + # Monkey patch method in vision transformer + timm.models.vision_transformer.resample_abs_pos_embed = resample_abs_pos_embed + + +torch.fx.wrap("int") # Needed to allow tracing with int() +patch_timm_for_fx_tracing() diff --git a/videosaur/modules/utils.py b/videosaur/modules/utils.py new file mode 100644 index 0000000..e1e1930 --- /dev/null +++ b/videosaur/modules/utils.py @@ -0,0 +1,595 @@ +import functools +import math +from typing import Dict, Iterable, List, Mapping, Optional, Tuple, Union + +import einops +import torch +from einops.layers.torch import Rearrange +from torch import nn + +from videosaur.utils import config_as_kwargs, get_class_by_name, make_build_fn + + +@make_build_fn(__name__, "utils module") +def build(config, name: str): + if name == "Chain": + if isinstance(config.models, Mapping): + models = {name: build_module(conf) for name, conf in config.models.items()} + elif isinstance(config.models, Iterable): + models = {f"model{i}": build_module(conf) for i, conf in enumerate(config.models)} + else: + raise ValueError(f"Inappropriate type for utils.Chain.models: {type(config.models)}") + return Chain( + models, + **config_as_kwargs(config, ("models",)), + ) + else: + return None + + +def build_module( + config, default_name: Optional[str] = None, default_group: Optional[str] = None, **kwargs +): + """Generic module build function.""" + if config is None: + raise ValueError("No config specified while building module") + name = config.get("name") or default_name + if name is None: + raise ValueError("No name specified while building module") + + group = None + if "." in name: + group, _, name = name.rpartition(".") + + if group is None or group == "": + group = default_group + + from videosaur.modules import BUILD_FNS_BY_MODULE_GROUP + + if group in BUILD_FNS_BY_MODULE_GROUP: + build_fn = BUILD_FNS_BY_MODULE_GROUP[group] + return build_fn(config, name, **kwargs) + elif group is None: + raise ValueError( + f"No module group specified. Available groups: {list(BUILD_FNS_BY_MODULE_GROUP)}" + ) + else: + raise ValueError(f"Unknown module group {group}") + + +def build_torch_module(config, name: str): + """Build function for torch.nn modules.""" + # import torch.nn # noqa + + cls = get_class_by_name("torch.nn", name) + if cls is not None: + return cls( + **config_as_kwargs(config), + ) + else: + raise ValueError(f"Unknown torch.nn module `{name}`") + + +def build_torch_function(config, name: str): + """Build function for torch functions.""" + # import torch # noqa + + fn = get_class_by_name("torch", name) + if fn is not None: + return functools.partial( + fn, + **config_as_kwargs(config), + ) + else: + raise ValueError(f"Unknown torch function `{name}`") + + +def init_parameters(layers: Union[nn.Module, Iterable[nn.Module]], weight_init: str = "default"): + assert weight_init in ("default", "he_uniform", "he_normal", "xavier_uniform", "xavier_normal") + if isinstance(layers, nn.Module): + layers = [layers] + + for idx, layer in enumerate(layers): + if hasattr(layer, "bias") and layer.bias is not None: + nn.init.zeros_(layer.bias) + + if hasattr(layer, "weight") and layer.weight is not None: + gain = 1.0 + if isinstance(layers, nn.Sequential): + if idx < len(layers) - 1: + next = layers[idx + 1] + if isinstance(next, nn.ReLU): + gain = 2**0.5 + + if weight_init == "he_uniform": + torch.nn.init.kaiming_uniform_(layer.weight, gain) + elif weight_init == "he_normal": + torch.nn.init.kaiming_normal_(layer.weight, gain) + elif weight_init == "xavier_uniform": + torch.nn.init.xavier_uniform_(layer.weight, gain) + elif weight_init == "xavier_normal": + torch.nn.init.xavier_normal_(layer.weight, gain) + + +class Chain(nn.Module): + """Chain several modules.""" + + def __init__( + self, + models: Dict[str, nn.Module], + mapping: Dict[str, List[str]] = None, + last_output: bool = True, + ): + super().__init__() + self.models = nn.ModuleDict(models) + self.mapping = mapping if mapping else {} + self.last_output = last_output + + def forward(self, *args): + outputs = {} + + last_output = args + for name, model in self.models.items(): + input_keys = self.mapping.get(name) + if input_keys is None: + inputs = last_output + else: + inputs = [outputs[key] for key in input_keys] + + if isinstance(inputs, Mapping): + last_output = model(**inputs) + elif isinstance(inputs, (list, tuple)): + last_output = model(*inputs) + else: + last_output = model(inputs) + + if isinstance(last_output, Mapping): + outputs.update(last_output) + else: + outputs[name] = last_output + + if self.last_output: + return last_output + else: + return outputs + + +class Patchify: + """Module that reshapes spatial inputs to patches.""" + + def __init__(self, patch_size: int, video_inputs: bool = False): + if video_inputs: + self.to_patches = Rearrange( + "b f c (h s1) (w s2) -> b f (h w) (s1 s2 c)", s1=patch_size, s2=patch_size + ) + else: + self.to_patches = Rearrange( + "b c (h s1) (w s2) -> b (h w) (s1 s2 c)", s1=patch_size, s2=patch_size + ) + + def __call__(self, input: torch.Tensor) -> torch.Tensor: + """Patchify input. + + Args: + input: tensor with shape (batch, [frames], channels, height, width). + + Returns: + Patchified tensor of shape (batch, n_patches, n_dims). + """ + return self.to_patches(input) + + +class Resizer: + """Module that takes image-based tensor and resizes it to an appropriate size. + + Args: + size: Tuple of (height, width) to resize to. If unspecified, assume an additional + input used to infer the size. The last two dimensions of this input are taken + as height and width. + patch_inputs: If true, assumes tensor to resize has format `(batch, [frames], + channels, n_points)` instead of separate height, width dimensions. + patch_outputs: If true, flatten spatial dimensions after resizing. + video_inputs: If true, assume inputs have an additional video dimension + channels_last: If true, assume channel dimension comes after spatial dimensions. Output will + be in same format as input. + resize_mode: Mode to use for resizing. For nearest neighbor resizing, specify + nearest-exact instead of nearest. + """ + + def __init__( + self, + size: Optional[Tuple[int, int]] = None, + patch_inputs: bool = False, + patch_outputs: bool = False, + video_inputs: bool = False, + channels_last: bool = False, + resize_mode: str = "bilinear", + ): + if resize_mode not in ("linear", "bilinear", "bicubic", "nearest-exact"): + if resize_mode == "nearest": + raise ValueError("Use resize mode `nearest-exact` instead of `nearest`") + else: + raise ValueError(f"Unsupported resize mode {resize_mode}") + + self.size = size + self.patch_inputs = patch_inputs + self.patch_outputs = patch_outputs + self.video_inputs = video_inputs + self.channels_last = channels_last + self.n_expected_dims = 4 + (1 if video_inputs else 0) - (1 if patch_inputs else 0) + self.resize_mode = resize_mode + + def __call__( + self, inputs: torch.Tensor, size_tensor: Optional[torch.Tensor] = None + ) -> torch.Tensor: + if inputs.ndim != self.n_expected_dims: + raise ValueError( + f"Mask has {inputs.ndim} dimensions, but expected it to " + f"have {self.n_expected_dims} dimensions." + ) + + if self.size is None: + if size_tensor is None: + raise ValueError("If size is unspecified, need to pass a tensor to take size from") + size = size_tensor.shape[-2:] + else: + size = list(self.size) + + if self.video_inputs: + batch, n_frames = inputs.shape[:2] + inputs = inputs.flatten(0, 1) + + if self.channels_last: + if self.patch_inputs: + inputs = inputs.transpose(-1, -2) + else: + inputs = inputs.transpose(-1, -2).transpose(-2, -3) + + if self.patch_inputs: + n_patches = inputs.shape[-1] + ratio = size[1] / size[0] + height = int(math.sqrt(n_patches / ratio)) + width = int(math.sqrt(n_patches * ratio)) + if height * width != n_patches: + if height == width: + raise ValueError( + f"Can not reshape {n_patches} patches to square aspect ratio as it's not a " + "perfect square." + ) + raise ValueError(f"Can not reshape {n_patches} patches to aspect ratio {ratio}.") + + inputs = inputs.unflatten(-1, (height, width)) + + dtype = inputs.dtype + if inputs.dtype == torch.bool: + inputs = inputs.to(torch.uint8) + + outputs = torch.nn.functional.interpolate(inputs, size=size, mode=self.resize_mode) + + if inputs.dtype != dtype: + inputs = inputs.to(dtype) + + if self.resize_mode == "bicubic": + outputs.clamp_(0.0, 1.0) # Bicubic interpolation can get out of range + + if self.patch_outputs: + outputs = outputs.flatten(-2, -1) + + if self.channels_last: + if self.patch_outputs: + outputs = outputs.transpose(-2, -1) + else: + outputs = outputs.transpose(-3, -2).transpose(-2, -1) + + if self.video_inputs: + outputs = outputs.unflatten(0, (batch, n_frames)) + + return outputs + + +class SoftToHardMask: + """Module that converts masks from soft to hard.""" + + def __init__( + self, convert_one_hot: bool = True, use_threshold: bool = False, threshold: float = 0.5 + ): + self.convert_one_hot = convert_one_hot + self.use_threshold = use_threshold + self.threshold = threshold + + def __call__(self, masks: torch.Tensor) -> torch.Tensor: + return soft_to_hard_mask(masks, self.convert_one_hot, self.use_threshold, self.threshold) + + +def soft_to_hard_mask( + masks: torch.Tensor, + convert_one_hot: bool = True, + use_threshold: bool = False, + threshold: float = 0.5, +): + """Convert soft to hard masks.""" + # masks: batch [x n_frames] x n_channels x height x width + assert masks.ndim == 4 or masks.ndim == 5 + min = torch.min(masks) + max = torch.max(masks) + if min < 0: + raise ValueError(f"Minimum mask value should be >=0, but found {min.cpu().numpy()}") + if max > 1: + raise ValueError(f"Maximum mask value should be <=1, but found {max.cpu().numpy()}") + + if use_threshold: + masks = masks > threshold + + if convert_one_hot: + mask_argmax = torch.argmax(masks, dim=-3) + masks = nn.functional.one_hot(mask_argmax, masks.shape[-3]).to(torch.float32) + masks = masks.transpose(-1, -2).transpose(-2, -3) # B, [F,] H, W, C -> B, [F], C, H, W + + return masks + + +class LayerScale(nn.Module): + """Module scaling input by learned scalar. + + Adapted from timm library. + """ + + def __init__(self, dim: int, init_values: float = 1e-5, inplace: bool = False): + super().__init__() + self.inplace = inplace + self.gamma = nn.Parameter(init_values * torch.ones(dim)) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return x.mul_(self.gamma) if self.inplace else x * self.gamma + + +def get_activation_fn(name_or_instance: Union[str, nn.Module]) -> nn.Module: + if isinstance(name_or_instance, nn.Module): + return name_or_instance + elif isinstance(name_or_instance, str): + if name_or_instance.lower() == "relu": + return nn.ReLU(inplace=True) + elif name_or_instance.lower() == "gelu": + return nn.GELU() + else: + raise ValueError(f"Unknown activation function {name_or_instance}") + else: + raise ValueError( + f"Unsupported type for activation function: {type(name_or_instance)}. " + "Can be `str` or `torch.nn.Module`." + ) + + +class CoordinatePositionEmbed(nn.Module): + """Coordinate positional embedding as in Slot Attention.""" + + def __init__(self, dim: int, size: Tuple[int, int]): + super().__init__() + if isinstance(size, int): + size = (size, size) + self.register_buffer("grid", self.build_grid(size)) + self.proj = nn.Conv2d(self.grid.shape[0], dim, kernel_size=1, bias=True) + init_parameters(self.proj, "xavier_uniform") + + @staticmethod + def build_grid( + size: Tuple[int, int], + bounds: Tuple[float, float] = (-1.0, 1.0), + add_inverse: bool = False, + ) -> torch.Tensor: + ranges = [torch.linspace(*bounds, steps=res) for res in size] + grid = torch.meshgrid(*ranges, indexing="ij") + + if add_inverse: + grid = torch.stack((grid[0], grid[1], 1.0 - grid[0], 1.0 - grid[1]), axis=0) + else: + grid = torch.stack((grid[0], grid[1]), axis=0) + + return grid + + def forward(self, x: torch.Tensor) -> torch.Tensor: + assert ( + x.ndim == 4 + ), f"Expected input shape (batch, channels, height, width), but got {x.shape}" + return x + self.proj(self.grid) + + +class LearnedPositionEmbed(nn.Module): + """Learned positional embedding as in Vision Transformers.""" + + def __init__( + self, + dim: int, + *, + n_patches: Optional[int] = None, + size: Optional[Tuple[int, int]] = None, + initial_scale: Optional[float] = None, + dropout: float = 0.0, + ): + super().__init__() + if n_patches is None and size is None: + raise ValueError("Need to specify either `n_patches` (for 1D) or `size` (for 2D)") + elif n_patches is not None and size is not None: + raise ValueError("Can not to specify both `n_patches` (for 1D) or `size` (for 2D)") + + if initial_scale is None: + initial_scale = dim**-0.5 + + if n_patches is not None: + self.expected_dims = 3 + self.pos_emb = nn.Parameter(torch.zeros(1, n_patches, dim)) + else: + self.expected_dims = 4 + if isinstance(size, int): + size = (size, size) + self.pos_emb = nn.Parameter(torch.zeros(1, dim, *size)) + + nn.init.trunc_normal_( + self.pos_emb, std=initial_scale, a=-2 * initial_scale, b=2 * initial_scale + ) + + if dropout > 0.0: + self.dropout = nn.Dropout(dropout) + else: + self.dropout = None + + def forward(self, x: torch.Tensor) -> torch.Tensor: + if self.expected_dims == 3: + assert ( + x.ndim == 3 + ), f"Expected input shape (batch, patches, dimensions), but got {x.shape}" + elif self.expected_dims == 4: + assert ( + x.ndim == 4 + ), f"Expected input shape (batch, channels, height, width), but got {x.shape}" + + x = x + self.pos_emb + + if self.dropout is not None: + x = self.dropout(x) + + return x + + +class FeatureSimilarity: + """Compute dot-product based similarity between two sets of features. + + Args: + normalize: Apply L2 normalization to features before computing dot-product, i.e. compute + cosine similarity. + temperature: Divide similarities by this value after computing dot-product. + threshold: Set values below this threshold to maximum dissimilarity before temperature + scaling. + mask_diagonal: Whether to set the diagonal of the similarity matrix to maximum + dissimilarity after applying temperature scaling. + softmax: Apply softmax over last dimension after computing similarity. + sigmoid: Apply sigmoid after computing similarity. + relative: Whether to transform similarities such that resulting similarity matrix only + contains similarities spatially around position. + relative_window_size: Size of relative window. + """ + + def __init__( + self, + normalize: bool = True, + temperature: float = 1.0, + threshold: Optional[float] = None, + mask_diagonal: bool = False, + softmax: bool = False, + ): + self.normalize = normalize + self.temperature = temperature + self.threshold = threshold + self.mask_diagonal = mask_diagonal + self.softmax = softmax + + # Choose padding value such that it indicates maximum dissimilarity + self.padding_value = -torch.inf if self.softmax else -1.0 / self.temperature + + def compute_similarity(self, features1: torch.Tensor, features2: torch.Tensor) -> torch.Tensor: + similarity = torch.einsum("bpd, bkd -> bpk", features1, features2) + + if self.threshold is not None: + similarity[similarity < self.threshold] = self.padding_value + + similarity /= self.temperature + + if self.mask_diagonal: + diag = torch.diagonal(similarity, dim1=-2, dim2=-1) + diag[:, :] = self.padding_value + + if self.softmax: + # if all the values in a row are padding, softmax will return nan. + # To avoid this, we set the padding values to 0. + similarity[ + (similarity == self.padding_value) + .all(dim=-1, keepdim=True) + .expand(-1, -1, similarity.shape[-1]) + ] = 0.0 + similarity = torch.softmax(similarity, dim=-1) + + return similarity + + def __call__(self, features1: torch.Tensor, features2: torch.Tensor) -> torch.Tensor: + if self.normalize: + features1 = nn.functional.normalize(features1, p=2.0, dim=-1) + features2 = nn.functional.normalize(features2, p=2.0, dim=-1) + + return self.compute_similarity(features1, features2) + + +class FeatureSelfSimilarity(FeatureSimilarity): + """Compute self-similarity between features.""" + + def __init__( + self, + video_inputs: bool = False, + normalize: bool = True, + temperature: float = 1.0, + threshold: Optional[float] = None, + mask_diagonal: bool = False, + softmax: bool = False, + ): + super().__init__( + normalize, + temperature, + threshold, + mask_diagonal, + softmax, + ) + self.video_inputs = video_inputs + + def __call__(self, features: torch.Tensor) -> torch.Tensor: + if self.video_inputs: + bs = len(features) + features = einops.rearrange(features, "b t p d -> (b t) p d") + + if self.normalize: + features = nn.functional.normalize(features, p=2.0, dim=-1) + + similarity = self.compute_similarity(features, features) + + if self.video_inputs: + similarity = einops.rearrange(similarity, "(b t) p k -> b t p k", b=bs) + + return similarity + + +class FeatureTimeSimilarity(FeatureSimilarity): + """Compute similaritiy between features over time.""" + + def __init__( + self, + time_shift: int = 1, + normalize: bool = True, + temperature: float = 1.0, + threshold: Optional[float] = None, + mask_diagonal: bool = False, + softmax: bool = False, + ): + super().__init__( + normalize, + temperature, + threshold, + mask_diagonal, + softmax, + ) + self.time_shift = time_shift + + def __call__(self, features: torch.Tensor) -> torch.Tensor: + assert features.ndim == 4, "`features` should have shape (batch, frames, positions, dims)" + + if self.normalize: + features = nn.functional.normalize(features, p=2.0, dim=-1) + + source_features = features[:, : -self.time_shift] + dest_features = features[:, self.time_shift :] + + source_features = einops.rearrange(source_features, "b t p d -> (b t) p d") + dest_features = einops.rearrange(dest_features, "b t p d -> (b t) p d") + + similarity = self.compute_similarity(source_features, dest_features) + + similarity = einops.rearrange(similarity, "(b t) p k -> b t p k", b=len(features)) + + return similarity diff --git a/videosaur/modules/video.py b/videosaur/modules/video.py new file mode 100644 index 0000000..0e05eea --- /dev/null +++ b/videosaur/modules/video.py @@ -0,0 +1,170 @@ +from typing import Any, Dict, List, Mapping, Optional + +import torch +from torch import nn + +from videosaur.utils import make_build_fn + + +@make_build_fn(__name__, "video module") +def build(config, name: str, **kwargs): + pass # No special module building needed + + +class LatentProcessor(nn.Module): + """Updates latent state based on inputs and state and predicts next state.""" + + def __init__( + self, + corrector: nn.Module, + predictor: Optional[nn.Module] = None, + state_key: str = "slots", + first_step_corrector_args: Optional[Dict[str, Any]] = None, + ): + super().__init__() + self.corrector = corrector + self.predictor = predictor + self.state_key = state_key + if first_step_corrector_args is not None: + self.first_step_corrector_args = first_step_corrector_args + else: + self.first_step_corrector_args = None + + def forward( + self, state: torch.Tensor, inputs: Optional[torch.Tensor], time_step: Optional[int] = None + ) -> Dict[str, torch.Tensor]: + # state: batch x n_slots x slot_dim + assert state.ndim == 3 + # inputs: batch x n_inputs x input_dim + assert inputs.ndim == 3 + + if inputs is not None: + if time_step == 0 and self.first_step_corrector_args: + corrector_output = self.corrector(state, inputs, **self.first_step_corrector_args) + else: + corrector_output = self.corrector(state, inputs) + updated_state = corrector_output[self.state_key] + else: + # Run predictor without updating on current inputs + corrector_output = None + updated_state = state + + if self.predictor: + predicted_state = self.predictor(updated_state) + else: + # Just pass updated_state along as prediction + predicted_state = updated_state + + return { + "state": updated_state, + "state_predicted": predicted_state, + "corrector": corrector_output, + } + + +class MapOverTime(nn.Module): + """Wrapper applying wrapped module independently to each time step. + + Assumes batch is first dimension, time is second dimension. + """ + + def __init__(self, module: nn.Module) -> None: + super().__init__() + self.module = module + + def forward(self, *args): + batch_size = None + seq_len = None + flattened_args = [] + for idx, arg in enumerate(args): + B, T = arg.shape[:2] + if not batch_size: + batch_size = B + elif batch_size != B: + raise ValueError( + f"Inconsistent batch size of {B} of argument {idx}, was {batch_size} before." + ) + + if not seq_len: + seq_len = T + elif seq_len != T: + raise ValueError( + f"Inconsistent sequence length of {T} of argument {idx}, was {seq_len} before." + ) + + flattened_args.append(arg.flatten(0, 1)) + + outputs = self.module(*flattened_args) + + if isinstance(outputs, Mapping): + unflattened_outputs = { + k: v.unflatten(0, (batch_size, seq_len)) for k, v in outputs.items() + } + else: + unflattened_outputs = outputs.unflatten(0, (batch_size, seq_len)) + + return unflattened_outputs + + +class ScanOverTime(nn.Module): + """Wrapper applying wrapped module recurrently over time steps""" + + def __init__( + self, module: nn.Module, next_state_key: str = "state_predicted", pass_step: bool = True + ) -> None: + super().__init__() + self.module = module + self.next_state_key = next_state_key + self.pass_step = pass_step + + def forward(self, initial_state: torch.Tensor, inputs: torch.Tensor): + # initial_state: batch x ... + # inputs: batch x n_frames x ... + seq_len = inputs.shape[1] + + state = initial_state + outputs = [] + for t in range(seq_len): + if self.pass_step: + output = self.module(state, inputs[:, t], t) + else: + output = self.module(state, inputs[:, t]) + outputs.append(output) + state = output[self.next_state_key] + + return merge_dict_trees(outputs, axis=1) + + +def merge_dict_trees(trees: List[Mapping], axis: int = 0): + """Stack all leafs given a list of dictionaries trees. + + Example: + x = merge_dict_trees([ + { + "a": torch.ones(2, 1), + "b": {"x": torch.ones(2, 2)} + }, + { + "a": torch.ones(3, 1), + "b": {"x": torch.ones(1, 2)} + } + ]) + + x == { + "a": torch.ones(5, 1), + "b": {"x": torch.ones(3, 2)} + } + """ + out = {} + if len(trees) > 0: + ref_tree = trees[0] + for key, value in ref_tree.items(): + values = [tree[key] for tree in trees] + if isinstance(value, torch.Tensor): + out[key] = torch.stack(values, axis) + elif isinstance(value, Mapping): + out[key] = merge_dict_trees(values, axis) + else: + out[key] = values + + return out diff --git a/videosaur/optimizers.py b/videosaur/optimizers.py new file mode 100644 index 0000000..a7f139a --- /dev/null +++ b/videosaur/optimizers.py @@ -0,0 +1,86 @@ +import itertools +from typing import Any, Dict, List, Mapping, Optional + +import torch +from torch import nn + +from videosaur import schedulers + + +class OptimizerBuilder: + def __init__( + self, + name: str, + lr: float, + lr_scheduler: Optional[Dict[str, Any]] = None, + param_groups: Optional[List[Dict[str, Any]]] = None, + **kwargs, + ): + if name.lower() not in ("adam"): + raise ValueError(f"Unknown optimizer {name}") + + self.name = name.lower() + self.lr = lr + self.param_groups = param_groups + self.optim_kwargs = kwargs + + groups_have_lr_sched = self.param_groups is not None and any( + key == "lr_scheduler" for key in self.param_groups + ) + if groups_have_lr_sched and lr_scheduler is not None: + raise ValueError( + "Can either define global `lr_scheduler` or schedulers in " + "`param_groups`, but not both" + ) + + if groups_have_lr_sched: + self.schedule_fn = [] + for idx, param_group in enumerate(self.param_groups): + if "lr_scheduler" not in param_group: + raise ValueError(f"Missing `lr_scheduler` in param_group {idx}") + self.schedule_fn.append(schedulers.build(param_group["lr_scheduler"])) + del param_group["lr_scheduler"] + elif lr_scheduler is not None: + self.schedule_fn = schedulers.build(lr_scheduler) + else: + self.schedule_fn = None + + def __call__(self, modules: Dict[str, nn.Module]): + if self.param_groups is None: + parameters = itertools.chain.from_iterable(m.parameters() for m in modules.values()) + else: + parameters = [] + for idx, param_group in enumerate(self.param_groups): + assert isinstance(param_group, Mapping) + group_modules = param_group["modules"] + if isinstance(group_modules, str): + group_modules = [group_modules] + for name in group_modules: + if name not in modules: + raise ValueError( + f"Requested module {name} in param group {idx}, but " + f"this module is not available. Available modules: {list(modules)}." + ) + + params = itertools.chain.from_iterable( + modules[name].parameters() for name in group_modules + ) + parameters.append( + {"params": params, **{k: v for k, v in param_group.items() if k != "modules"}} + ) + + if self.name == "adam": + optimizer = torch.optim.Adam(parameters, lr=self.lr, **self.optim_kwargs) + elif self.name == "adamw": + optimizer = torch.optim.AdamW(parameters, lr=self.lr, **self.optim_kwargs) + else: + raise ValueError(f"Optimizer {self.name} is not known") + + if self.schedule_fn is not None: + scheduler = schedulers.apply_schedule_fn_to_optimizer(optimizer, self.schedule_fn) + return { + "optimizer": optimizer, + "lr_scheduler": {"scheduler": scheduler, "interval": "step"}, + } + else: + return optimizer diff --git a/videosaur/schedulers.py b/videosaur/schedulers.py new file mode 100644 index 0000000..2e89a94 --- /dev/null +++ b/videosaur/schedulers.py @@ -0,0 +1,101 @@ +import functools +import math +from typing import Callable, List, Union + +import torch +from torch.optim.lr_scheduler import LambdaLR + +from videosaur import utils + + +@utils.make_build_fn(__name__, "scheduler") +def build( + config, + name: str, +): + if name == "constant": + scheduler_fn = constant + elif name == "linear_warmup": + scheduler_fn = functools.partial( + linear_warmup, warmup_steps=config.get("warmup_steps", 1000) + ) + if name == "exp_decay_with_warmup": + scheduler_fn = functools.partial( + exp_decay_with_warmup, + warmup_steps=config.get("warmup_steps", 1000), + decay_steps=config.get("decay_steps", 100000), + decay_rate=config.get("decay_rate", 0.5), + ) + elif name == "cosine_decay_with_warmup": + scheduler_fn = functools.partial( + cosine_decay_with_warmup, + warmup_steps=config.get("warmup_steps", 1000), + decay_steps=config.get("decay_steps", 100000), + ) + else: + raise ValueError(f"Unknown scheduler {name}") + + return scheduler_fn + + +def apply_schedule_fn_to_optimizer( + optimizer: torch.optim.Optimizer, + decay_fn: Union[Callable[[int], float], List[Callable[[int], float]]], +) -> LambdaLR: + return LambdaLR(optimizer, decay_fn) + + +def constant(step: int) -> float: + """Constant schedule. + + Function maps current step or epoch to factor of learning rate schedules. + """ + return 1.0 + + +def linear_warmup(step: int, warmup_steps: int) -> float: + """Linear warmup. + + Function maps current step or epoch to factor of learning rate schedules. + """ + if warmup_steps > 0: + return min(1.0, step / warmup_steps) + else: + return 1.0 + + +def exp_decay_with_warmup( + step: int, + warmup_steps: int, + decay_steps: int, + decay_rate: float, +) -> float: + """Exponential decay with linear learning rate warmup. + + Function maps current step or epoch to factor of learning rate schedules. After `decay_steps`, + factor equals `decay_rate`. + """ + if step < warmup_steps: + return linear_warmup(step, warmup_steps) + else: + step = step - warmup_steps + decay_steps = decay_steps - warmup_steps + return decay_rate ** (step / decay_steps) + + +def cosine_decay_with_warmup( + step: int, + warmup_steps: int, + decay_steps: int, +) -> float: + """Cosine decay to zero with linear learning rate warmup. + + Function maps current step or epoch to factor of learning rate schedules. + """ + if step < warmup_steps: + return linear_warmup(step, warmup_steps) + else: + step = step - warmup_steps + decay_steps = decay_steps - warmup_steps + step = min(step, decay_steps) + return 0.5 * (1 + math.cos(math.pi * (step / decay_steps))) diff --git a/videosaur/train.py b/videosaur/train.py new file mode 100644 index 0000000..38ce2ff --- /dev/null +++ b/videosaur/train.py @@ -0,0 +1,296 @@ +import argparse +import logging +import os +import pathlib +import random +import warnings +from typing import Any, Dict, Optional + +import pytorch_lightning as pl +import torch +from omegaconf import OmegaConf +from pytorch_lightning.utilities import rank_zero_info as log_info + +from videosaur import configuration, data, metrics, models, utils + +RESULT_FINISHED = 0 +RESULT_TIMEOUT = 1 + +CHECKPOINT_SUBDIR = "checkpoints" +TENSORBOARD_SUBDIR = "tb" +METRICS_SUBDIR = "metrics" + +parser = argparse.ArgumentParser() +group = parser.add_mutually_exclusive_group() +group.add_argument("-v", "--verbose", action="store_true", help="Be verbose") +group.add_argument("-q", "--quiet", action="store_true", help="Suppress outputs") +parser.add_argument("-n", "--dry", action="store_true", help="Dry run (no logfiles)") +parser.add_argument( + "--no-interactive", action="store_true", help="If running in non-interactive environment" +) +parser.add_argument("--no-tensorboard", action="store_true", help="Do not write tensorboard logs") +parser.add_argument( + "--check-validation", action="store_true", help="Run correctness checks on data used during eval" +) +parser.add_argument( + "--run-eval-after-training", action="store_true", help="Evaluate after training has stopped" +) +parser.add_argument( + "--use-optimizations", action="store_true", help="Enable Pytorch performance optimizations" +) +parser.add_argument("--timeout", help="Stop training after this time (format: DD:HH:MM:SS)") +parser.add_argument("--data-dir", help="Path to data directory") +parser.add_argument("--log-dir", default="./logs", help="Path to log directory") +parser.add_argument( + "--no-sub-logdirs", action="store_true", help="Directly use log dir to store logs" +) +parser.add_argument( + "--continue", + dest="continue_from", + type=pathlib.Path, + help="Continue training from this log folder or checkpoint path", +) +parser.add_argument("--config_overrides_file", help="Configuration to override") +parser.add_argument("config", help="Configuration to run") +parser.add_argument("config_overrides", nargs="*", help="Additional arguments") + + +def _setup_callbacks(args, config, log_path: pathlib.Path, dataset=None) -> Dict[str, pl.Callback]: + callbacks = {} + + if not args.dry: + # Explicitly construct model checkpoint to have control over checkpoints directory + checkpointer = pl.callbacks.ModelCheckpoint( + log_path / CHECKPOINT_SUBDIR, + filename="{step}", + every_n_train_steps=config.checkpoint_every_n_steps, + verbose=args.verbose, + ) + callbacks["checkpointer"] = checkpointer + + # Monitor learning rates + lr_monitor = pl.callbacks.LearningRateMonitor(logging_interval="step") + callbacks["lr_monitor"] = lr_monitor + + if args.check_validation: + assert dataset is not None + val_size = dataset.val_size if hasattr(dataset, "val_size") else None + callbacks["check_validation"] = utils.CheckValidationCallback(val_size) + + if args.timeout: + timer = pl.callbacks.Timer(duration=args.timeout, interval="step", verbose=args.verbose) + callbacks["timer"] = timer + + return callbacks + + +def _setup_loggers(args, log_path: pathlib.Path) -> Dict[str, pl.loggers.logger.Logger]: + if args.dry: + return {} + + loggers = {} + if not args.no_tensorboard: + # Tensorboard logs go to // + loggers["tensorboard"] = pl.loggers.TensorBoardLogger( + save_dir=log_path, name=TENSORBOARD_SUBDIR, version="" + ) + + # CSV logs go to //version_N/metrics.csv, where N is the number of + # restarts of the job + loggers["csv"] = pl.loggers.CSVLogger(save_dir=log_path, name=METRICS_SUBDIR) + + return loggers + + +def _setup_trainer_config(trainer_config: Dict[str, Any]) -> Dict[str, Any]: + # Configure number of training steps + if "max_steps" not in trainer_config: + trainer_config["max_steps"] = 100000 + log_info(f"Setting number of training steps to {trainer_config['max_steps']}") + + if "max_epochs" in trainer_config: + del trainer_config["max_epochs"] + log_info("Removing `max_epochs` from config because we do not use it") + + # Configure validation frequency + if "val_check_interval" not in trainer_config: + trainer_config["val_check_interval"] = 5000 + log_info(f"Setting `val_check_interval` to {trainer_config['val_check_interval']}") + + if "check_val_every_n_epoch" in trainer_config: + del trainer_config["check_val_every_n_epoch"] + log_info("Removing `check_val_every_n_epoch` from config because we do not use it") + + # Configure logging frequency + if "log_every_n_steps" not in trainer_config: + trainer_config["log_every_n_steps"] = 100 + log_info(f"Setting logging frequency to every {trainer_config['log_every_n_steps']} steps") + + # Let Pytorch Lightning select the device if not specified otherwise. + if "accelerator" not in trainer_config: + trainer_config["accelerator"] = "auto" + + # Automatically select DDP as strategy if possible and not specified otherwise. + if ( + "strategy" not in trainer_config + and trainer_config.get("accelerator") != "cpu" + and trainer_config.get("devices") != 1 + and torch.cuda.is_available() + and torch.cuda.device_count() > 1 + ): + strategy = "ddp_find_unused_parameters_false" + if "find_unused_parameters" in trainer_config: + if trainer_config["find_unused_parameters"]: + strategy = "ddp" + del trainer_config["find_unused_parameters"] + log_info(f"Setting distributed strategy to {strategy}.") + trainer_config["strategy"] = strategy + + return trainer_config + + +def main(args, config_overrides=None): + rank_zero = utils.get_rank() == 0 + if config_overrides is None: + config_overrides = args.config_overrides + config = configuration.load_config(args.config, config_overrides) + if args.config_overrides_file is not None: + config = configuration.override_config( + config, + override_config_path=args.config_overrides_file, + additional_overrides=config_overrides, + ) + + if not args.verbose or args.quiet: + logging.getLogger("pytorch_lightning").setLevel(logging.ERROR) + from pytorch_lightning.utilities.warnings import PossibleUserWarning + + warnings.filterwarnings("ignore", category=PossibleUserWarning) + if args.quiet: + warnings.filterwarnings("ignore", category=UserWarning) + + # Setup log path + log_path: Optional[pathlib.Path] = None + if not args.dry: + if "VDINOSAUR_LOG_PATH" in os.environ: + # Retrieve log path from the main process in the DDP setting + log_path = pathlib.Path(os.environ["VDINOSAUR_LOG_PATH"]) + elif args.continue_from and args.continue_from.is_dir(): + # If `continue` points to a directory, use it as the output logging directory + log_path = args.continue_from + else: + if args.no_sub_logdirs: + log_path = pathlib.Path(args.log_dir) + log_path.mkdir(parents=True, exist_ok=True) + else: + log_path = utils.make_log_dir( + args.log_dir, config.experiment_name, config.experiment_group + ) + log_info(f"Using {log_path} as output directory") + + # Find checkpoint for automatic resuming of training + ckpt_path: Optional[str] = None + if args.continue_from: + if args.continue_from.is_file() and args.continue_from.suffix == ".ckpt": + ckpt_path = args.continue_from + elif args.continue_from.is_dir(): + ckpt_path = utils.find_last_checkpoint(args.continue_from) + else: + raise ValueError("Unclear --continue argument: should be .ckpt file or directory") + elif not args.dry: + ckpt_path = utils.find_last_checkpoint(log_path) + if ckpt_path is not None: + log_info(f"Auto-resuming training from checkpoint {ckpt_path}") + + # Setup random seed + if "PL_GLOBAL_SEED" in os.environ: + # Retrieve random seed from the main process in the DDP setting + seed = int(os.environ["PL_GLOBAL_SEED"]) + elif config.seed is not None: + seed = config.seed + else: + seed = random.randint(0, 2**32 - 1) + log_info(f"Using random seed {seed}.") + config.seed = pl.seed_everything(seed, workers=True) + + if args.use_optimizations: + torch.backends.cudnn.benchmark = True + # Allow use of TensorFloat-32 on Ampere devices + torch.backends.cuda.matmul.allow_tf32 = True + torch.backends.cudnn.allow_tf32 = True + + dataset = data.build(config.dataset, data_dir=args.data_dir) + if args.verbose: + log_info(str(dataset)) + + if config.train_metrics is not None: + train_metrics = { + name: metrics.build(config) for name, config in config.train_metrics.items() + } + else: + train_metrics = None + + if config.val_metrics is not None: + val_metrics = {name: metrics.build(config) for name, config in config.val_metrics.items()} + else: + val_metrics = None + + model = models.build(config.model, config.optimizer, train_metrics, val_metrics) + + callbacks = _setup_callbacks(args, config, log_path, dataset) + loggers = _setup_loggers(args, log_path) + trainer_config = _setup_trainer_config(config.setdefault("trainer", {})) + + # Save the final configuration + if rank_zero and log_path and not (log_path / "settings.yaml").exists(): + configuration.save_config(log_path / "settings.yaml", config) + + if "tensorboard" in loggers: + loggers["tensorboard"].log_hyperparams(config) + + log_info(f"Configuration:\n{OmegaConf.to_yaml(config, resolve=True)}") + + # When running DDP, expose log path to other processes through environment variable + if "strategy" in trainer_config and trainer_config["strategy"].startswith("ddp"): + os.environ["VIDEOSAUR_LOG_PATH"] = str(log_path) + + trainer = pl.Trainer( + max_epochs=-1, # We control training duration using `max_steps` + check_val_every_n_epoch=None, # We do not use epochs for training + default_root_dir=log_path, + callbacks=[callback for callback in callbacks.values()], + logger=[logger for logger in loggers.values()] if loggers else False, + enable_progress_bar=(not args.quiet and not args.no_interactive), + enable_model_summary=not args.quiet, + enable_checkpointing="checkpointer" in callbacks, + **trainer_config, + ) + + if ckpt_path is not None: + log_info(f"Resuming training from checkpoint {ckpt_path}") + else: + log_info("Starting training from scratch") + + trainer.fit(model=model, datamodule=dataset, ckpt_path=ckpt_path) + + if "checkpointer" in callbacks: + # Explicitly save a final checkpoint after training. This is needed because the end of + # training might not align with the checkpointing frequency. Unfortunately, we need to rely + # on private methods of ModelCheckpoint here, because we need to bypass the frequency checks + # and ModelCheckpoint does not expose any method for explictly saving checkpoints. + monitor_candidates = callbacks["checkpointer"]._monitor_candidates(trainer) + callbacks["checkpointer"]._save_topk_checkpoint(trainer, monitor_candidates) + + if args.run_eval_after_training: + # Run one more evaluation. This is useful because some more training might have happened + # after the last epoch, but before training was stopped. + trainer.validate(model=model, datamodule=dataset) + + if callbacks.get("timer") and callbacks["timer"].time_remaining() <= 0: + return RESULT_TIMEOUT # Signal that training was interrupted because of timeout + + return RESULT_FINISHED + + +if __name__ == "__main__": + main(parser.parse_args()) diff --git a/videosaur/utils.py b/videosaur/utils.py new file mode 100644 index 0000000..260cbae --- /dev/null +++ b/videosaur/utils.py @@ -0,0 +1,251 @@ +import datetime +import functools +import itertools +import os +import pathlib +import sys +from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Sequence + +import pytorch_lightning as pl +import torch +import torchmetrics + + +def make_build_fn(pymodule_name: str, group_name: str): + """Decorator for build functions. + + Automatically calls classes/functions in the decorated build function's module. + """ + + def decorator(wrapped_fn: Callable): + @functools.wraps(wrapped_fn) + def build_fn( + config, name: Optional[str] = None, default_name: Optional[str] = None, **kwargs + ): + if config is None: + raise ValueError(f"No config specified while building {group_name}") + name = name or config.get("name") or default_name + if name is None: + raise ValueError(f"No name specified while building {group_name}") + + # Build modules with special handling + module = wrapped_fn(config, name, **kwargs) + + # Build modules which do not need special handling + if module is None: + cls = get_class_by_name(pymodule_name, name) + if cls is not None: + module = cls(**config_as_kwargs(config), **kwargs) + else: + raise ValueError(f"Unknown {group_name} `{name}`") + + return module + + return build_fn + + return decorator + + +def get_class_by_name(module_name: str, name: str): + pymodule = sys.modules[module_name] + if name in pymodule.__dict__: + return pymodule.__dict__[name] + else: + return None + + +def make_log_dir( + log_dir: str, name: Optional[str] = None, group: Optional[str] = None +) -> pathlib.Path: + time = datetime.datetime.now() + dir_name = f"{time:%Y-%m-%d-%H-%M-%S}" + if name is not None: + dir_name += f"_{name}" + + if group is None: + group = "" + + log_path = pathlib.Path(log_dir) / group / dir_name + + count = 2 + while log_path.is_dir(): + log_path = log_path.with_name(f"{dir_name}_{count}") + count += 1 + + log_path.mkdir(parents=True, exist_ok=True) + return log_path + + +def config_as_kwargs( + config, to_filter: Optional[Iterable[str]] = None, defaults: Dict[str, Any] = None +) -> Dict[str, Any]: + """Build kwargs for constructor from config dictionary.""" + always_filter = ("name",) + if to_filter: + to_filter = tuple(itertools.chain(always_filter, to_filter)) + else: + to_filter = always_filter + if defaults: + # Defaults come first such that they can be overwritten by config + to_iter = itertools.chain(defaults.items(), config.items()) + else: + to_iter = config.items() + return {k: v for k, v in to_iter if k not in to_filter} + + +def write_path(root: Any, path: str, value: Any): + elems = path.split(".") + parent = read_path(root, path, elems[:-1]) + elem = elems[-1] + if isinstance(parent, Mapping): + parent[elem] = value + elif isinstance(parent, Sequence): + try: + index = int(elem) + except ValueError: + raise ValueError( + f"Element {elem} of path `{path}` can not be converted into index " + f"to index into sequence." + ) from None + parent[index] = value + else: + raise ValueError( + f"Can not handle datatype {type(parent)} at element {elem} of path `{path}`" + ) + + +def read_path( + root: Any, path: Optional[str] = None, elements: Optional[List[str]] = None, error: bool = True +): + if path is not None and elements is None: + elements = path.split(".") + elif path is None and elements is None: + raise ValueError("`elements` and `path` can not both be `None`") + + current = root + for elem in elements: + if isinstance(current, Mapping): + next = current.get(elem) + if next is None: + if not error: + return None + if path is None: + path = ".".join(elements) + raise ValueError( + f"Can not use element {elem} of path `{path}` to access into " + f"dictionary. Available options are {', '.join(list(current))}" + ) + elif isinstance(current, Sequence): + try: + index = int(elem) + except ValueError: + if not error: + return None + if path is None: + path = ".".join(elements) + raise ValueError( + f"Element {elem} of path `{path}` can not be converted to index into sequence." + ) from None + + try: + next = current[index] + except IndexError: + if not error: + return None + if path is None: + path = ".".join(elements) + raise ValueError( + f"Can not use element {elem} of path `{path}` to access into " + f"sequence of length {len(current)}" + ) from None + elif hasattr(current, elem): + next = getattr(current, elem) + else: + if not error: + return None + if path is None: + path = ".".join(elements) + raise ValueError( + f"Can not handle datatype {type(current)} at element " f"{elem} of path `{path}`" + ) + + current = next + + return current + + +def to_dict_recursive(dict_) -> dict: + dict_ = {**dict_} + for key, value in dict_.items(): + if isinstance(value, dict): + dict_[key] = to_dict_recursive(value) + return dict_ + + +def get_rank() -> int: + """Get rank of process. + + From pytorch lightning. + """ + # SLURM_PROCID can be set even if SLURM is not managing the multiprocessing, + # therefore LOCAL_RANK needs to be checked first + rank_keys = ("RANK", "LOCAL_RANK", "SLURM_PROCID", "JSM_NAMESPACE_RANK") + for key in rank_keys: + rank = os.environ.get(key) + if rank is not None: + return int(rank) + return 0 + + +class CheckValidationCallback(pl.callbacks.Callback): + """Callback that checks that each sample of the validation set is sampled exactly once. + + This is useful because the distributed logic makes it difficult to guarantee that each sample + is seen exactly once across a validation epoch. + """ + + def __init__(self, num_expected_val_samples: Optional[int]): + super().__init__() + self.num_expected_val_samples = num_expected_val_samples + self.keys: List[str] = [] + self.num_unique_keys = torchmetrics.SumMetric() + + def on_validation_start(self, trainer, pl_module): + self.keys = [] + self.num_unique_keys.to(pl_module.device) + self.num_unique_keys.reset() + + def on_validation_batch_start(self, trainer, pl_module, batch, batch_idx, dataloader_idx): + self.keys.extend([key for key in batch["__key__"] if key != "PADDING"]) + + def on_validation_end(self, trainer, pl_module): + if trainer.sanity_checking: + return # During Lightning's sanity check, there are fewer batches + + num_keys = len(self.keys) + num_unique_keys = len(set(self.keys)) + if num_keys != num_unique_keys: + # We only check whether the keys are unique per node, and rely on that sharding correctly + # splits the samples across nodes. + raise ValueError( + f"Detected repeated validation sample: gathered {num_keys} keys, but " + "only {num_unique_keys} unique keys." + ) + + self.num_unique_keys.update( + torch.tensor(num_unique_keys, dtype=torch.int64, device=pl_module.device) + ) + num_unique_keys_sum = int(self.num_unique_keys.compute().cpu().item()) + if ( + self.num_expected_val_samples is not None + and self.num_expected_val_samples != num_unique_keys_sum + ): + raise ValueError( + f"Expected to see {self.num_expected_val_samples} keys, but " + f"gathered {num_unique_keys_sum} keys" + ) + + +def find_last_checkpoint(log_path: pathlib.Path) -> pathlib.Path: + checkpoints = log_path.glob("**/*.ckpt") + return max(checkpoints, default=None, key=os.path.getctime) diff --git a/videosaur/visualizations.py b/videosaur/visualizations.py new file mode 100644 index 0000000..4e1a00a --- /dev/null +++ b/videosaur/visualizations.py @@ -0,0 +1,254 @@ +import functools +import os +import warnings +from typing import List, Optional, Tuple, Union + +import numpy as np +import torch +from PIL import ImageColor + +CMAP_STYLE = "tab" + + +def get_cmap_style() -> str: + cmap = os.environ.get("VIDEOSAUR_CMAP") + if cmap is None: + cmap = CMAP_STYLE + + if cmap not in ("tab", "generated"): + raise ValueError(f"Invalid color map {cmap}") + + return cmap + + +def mix_videos_with_masks( + video: torch.Tensor, masks: torch.Tensor, alpha: float = 0.4 +) -> torch.Tensor: + input_shape = video.shape + cmap = color_map(masks.shape[2]) + video = (255 * video).flatten(0, 1).to(torch.uint8) + masks = masks.flatten(0, 1).to(bool) + input_with_masks = torch.stack( + [ + draw_segmentation_masks_on_image(frame, mask, colors=cmap, alpha=alpha) + for frame, mask in zip(video, masks) + ] + ) + return input_with_masks.reshape(*input_shape) + + +def mix_images_with_masks( + image: torch.Tensor, masks: torch.Tensor, alpha: float = 0.4 +) -> torch.Tensor: + input_shape = image.shape + cmap = color_map(masks.shape[1]) + image = (255 * image).to(torch.uint8) + masks = masks.to(bool) + input_with_masks = torch.stack( + [ + draw_segmentation_masks_on_image(frame, mask, colors=cmap, alpha=alpha) + for frame, mask in zip(image, masks) + ] + ) + return input_with_masks.reshape(*input_shape) + + +def draw_segmentation_masks_on_image( + image: torch.Tensor, + masks: torch.Tensor, + alpha: float = 0.8, + colors: Optional[ + Union[List[Union[str, Tuple[int, int, int]]], str, Tuple[int, int, int]] + ] = None, +) -> torch.Tensor: + """ + Draws segmentation masks on given RGB image. + + The values of the input image should be uint8 between 0 and 255. + + Adapted from torchvision.utils.draw_segmentation_masks to run on GPUs if needed. + + Args: + image (Tensor): Tensor of shape (3, H, W) and dtype uint8. + masks (Tensor): Tensor of shape (num_masks, H, W) or (H, W) and dtype bool. + alpha (float): Float number between 0 and 1 denoting the transparency of the masks. + 0 means full transparency, 1 means no transparency. + colors (color or list of colors, optional): List containing the colors + of the masks or single color for all masks. The color can be represented as + PIL strings e.g. "red" or "#FF00FF", or as RGB tuples e.g. ``(240, 10, 157)``. + By default, random colors are generated for each mask. + + Returns: + img (Tensor[C, H, W]): Image Tensor, with segmentation masks drawn on top. + """ + if not isinstance(image, torch.Tensor): + raise TypeError(f"The image must be a tensor, got {type(image)}") + elif image.dtype != torch.uint8: + raise ValueError(f"The image dtype must be uint8, got {image.dtype}") + elif image.dim() != 3: + raise ValueError("Pass individual images, not batches") + elif image.size()[0] != 3: + raise ValueError("Pass an RGB image. Other Image formats are not supported") + if masks.ndim == 2: + masks = masks[None, :, :] + if masks.ndim != 3: + raise ValueError("masks must be of shape (H, W) or (num_masks, H, W)") + if masks.dtype != torch.bool: + raise ValueError(f"The masks must be of dtype bool. Got {masks.dtype}") + if masks.shape[-2:] != image.shape[-2:]: + raise ValueError( + ( + "The image and the masks must have the same height and width," + + f"but got {masks.shape[-2:]} and {image.shape[-2:]}" + ) + ) + + num_masks = masks.size()[0] + if colors is not None and num_masks > len(colors): + raise ValueError(f"There are more masks ({num_masks}) than colors ({len(colors)})") + + if num_masks == 0: + warnings.warn("masks doesn't contain any mask. No mask was drawn", stacklevel=0) + return image + + if colors is None: + + def generate_color_palette(num_objects: int): + palette = torch.tensor([2**25 - 1, 2**15 - 1, 2**21 - 1]) + return [tuple((i * palette) % 255) for i in range(num_objects)] + + colors = generate_color_palette(num_masks) + + if not isinstance(colors, list): + colors = [colors] + if not isinstance(colors[0], (tuple, str)): + raise ValueError("colors must be a tuple or a string, or a list thereof") + if isinstance(colors[0], tuple) and len(colors[0]) != 3: + raise ValueError("It seems that you passed a tuple of colors instead of a list of colors") + + out_dtype = torch.uint8 + + colors_ = [] + for color in colors: + if isinstance(color, str): + color = ImageColor.getrgb(color) + colors_.append(torch.tensor(color, dtype=out_dtype, device=image.device)) + + img_to_draw = image.detach().clone() + for mask, color in zip(masks, colors_): + img_to_draw[:, mask] = color[:, None] + + out = image * (1 - alpha) + img_to_draw * alpha + return out.to(out_dtype) + + +_TAB10_DATA = ( + (0.12156862745098039, 0.4666666666666667, 0.7058823529411765), # 1f77b4 + (1.0, 0.4980392156862745, 0.054901960784313725), # ff7f0e + (0.17254901960784313, 0.6274509803921569, 0.17254901960784313), # 2ca02c + (0.8392156862745098, 0.15294117647058825, 0.1568627450980392), # d62728 + (0.5803921568627451, 0.403921568627451, 0.7411764705882353), # 9467bd + (0.5490196078431373, 0.33725490196078434, 0.29411764705882354), # 8c564b + (0.8901960784313725, 0.4666666666666667, 0.7607843137254902), # e377c2 + (0.4980392156862745, 0.4980392156862745, 0.4980392156862745), # 7f7f7f + (0.7372549019607844, 0.7411764705882353, 0.13333333333333333), # bcbd22 + (0.09019607843137255, 0.7450980392156863, 0.8117647058823529), # 17becf +) + +_TAB20_DATA = ( + (0.12156862745098039, 0.4666666666666667, 0.7058823529411765), # 1f77b4 + (0.6823529411764706, 0.7803921568627451, 0.9098039215686274), # aec7e8 + (1.0, 0.4980392156862745, 0.054901960784313725), # ff7f0e + (1.0, 0.7333333333333333, 0.47058823529411764), # ffbb78 + (0.17254901960784313, 0.6274509803921569, 0.17254901960784313), # 2ca02c + (0.596078431372549, 0.8745098039215686, 0.5411764705882353), # 98df8a + (0.8392156862745098, 0.15294117647058825, 0.1568627450980392), # d62728 + (1.0, 0.596078431372549, 0.5882352941176471), # ff9896 + (0.5803921568627451, 0.403921568627451, 0.7411764705882353), # 9467bd + (0.7725490196078432, 0.6901960784313725, 0.8352941176470589), # c5b0d5 + (0.5490196078431373, 0.33725490196078434, 0.29411764705882354), # 8c564b + (0.7686274509803922, 0.611764705882353, 0.5803921568627451), # c49c94 + (0.8901960784313725, 0.4666666666666667, 0.7607843137254902), # e377c2 + (0.9686274509803922, 0.7137254901960784, 0.8235294117647058), # f7b6d2 + (0.4980392156862745, 0.4980392156862745, 0.4980392156862745), # 7f7f7f + (0.7803921568627451, 0.7803921568627451, 0.7803921568627451), # c7c7c7 + (0.7372549019607844, 0.7411764705882353, 0.13333333333333333), # bcbd22 + (0.8588235294117647, 0.8588235294117647, 0.5529411764705883), # dbdb8d + (0.09019607843137255, 0.7450980392156863, 0.8117647058823529), # 17becf + (0.6196078431372549, 0.8549019607843137, 0.8980392156862745), # 9edae5 +) + +_TAB20B_DATA = ( + (0.2235294117647059, 0.23137254901960785, 0.4745098039215686), # 393b79 + (0.3215686274509804, 0.32941176470588235, 0.6392156862745098), # 5254a3 + (0.4196078431372549, 0.43137254901960786, 0.8117647058823529), # 6b6ecf + (0.611764705882353, 0.6196078431372549, 0.8705882352941177), # 9c9ede + (0.38823529411764707, 0.4745098039215686, 0.2235294117647059), # 637939 + (0.5490196078431373, 0.6352941176470588, 0.3215686274509804), # 8ca252 + (0.7098039215686275, 0.8117647058823529, 0.4196078431372549), # b5cf6b + (0.807843137254902, 0.8588235294117647, 0.611764705882353), # cedb9c + (0.5490196078431373, 0.42745098039215684, 0.19215686274509805), # 8c6d31 + (0.7411764705882353, 0.6196078431372549, 0.2235294117647059), # bd9e39 + (0.9058823529411765, 0.7294117647058823, 0.3215686274509804), # e7ba52 + (0.9058823529411765, 0.796078431372549, 0.5803921568627451), # e7cb94 + (0.5176470588235295, 0.23529411764705882, 0.2235294117647059), # 843c39 + (0.6784313725490196, 0.28627450980392155, 0.2901960784313726), # ad494a + (0.8392156862745098, 0.3803921568627451, 0.4196078431372549), # d6616b + (0.9058823529411765, 0.5882352941176471, 0.611764705882353), # e7969c + (0.4823529411764706, 0.2549019607843137, 0.45098039215686275), # 7b4173 + (0.6470588235294118, 0.3176470588235294, 0.5803921568627451), # a55194 + (0.807843137254902, 0.42745098039215684, 0.7411764705882353), # ce6dbd + (0.8705882352941177, 0.6196078431372549, 0.8392156862745098), # de9ed6 +) + +# This colormap first contains the tab10 colors, then every second color of the tab20 colors, and +# then the colors of tab20b +_OUR_TAB_DATA = ( + _TAB10_DATA + + _TAB20_DATA[1::2] + + _TAB20B_DATA[::4] + + _TAB20B_DATA[1::4] + + _TAB20B_DATA[2::4] + + _TAB20B_DATA[3::4] +) + + +@functools.lru_cache +def color_map(N, normalized=False): + cmap_style = get_cmap_style() + if cmap_style == "tab" and N <= len(_OUR_TAB_DATA): + cmap = np.array(_OUR_TAB_DATA[:N], dtype=np.float32) + if N >= 8: + # Replace dark gray with a darkish pink, namely the 6th color of Accent + cmap[7] = (0.94117647058823528, 0.00784313725490196, 0.49803921568627452) + if N >= 18: + # Replace light gray with a red-brown, namely the 12th color of Paired + cmap[17] = (0.69411764705882351, 0.34901960784313724, 0.15686274509803921) + if not normalized: + cmap = (cmap * 255).astype(np.uint8) + else: + cmap = generate_color_map(N, normalized) + + return [tuple(c) for c in cmap] + + +def generate_color_map(N, normalized=False): + dtype = np.float32 if normalized else np.uint8 + + def bitget(byteval, idx): + return (byteval & (1 << idx)) != 0 + + cmap = np.zeros((N, 3), dtype=dtype) + for i in range(N): + r = g = b = 0 + c = i + for j in range(8): + r = r | (bitget(c, 0) << 7 - j) + g = g | (bitget(c, 1) << 7 - j) + b = b | (bitget(c, 2) << 7 - j) + c = c >> 3 + cmap[i] = (r, g, b) + + cmap = cmap / 255 if normalized else cmap + + return cmap