diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..82d958a
--- /dev/null
+++ b/.envrc
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+# This file is automatically loaded with `direnv` if allowed.
+# It enters you into the venv.
+
+source .localenv
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..683940d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+__pycache__
+/data/models/
+/data/archives/
+/experiments/logdir/
+/.env/
+/.direnv/
+*.zip
+*.sh
+default.yaml # pandoc preview enhanced
diff --git a/.localenv b/.localenv
new file mode 100644
index 0000000..8fd87dc
--- /dev/null
+++ b/.localenv
@@ -0,0 +1,71 @@
+#!/usr/bin/env bash
+
+# =======================
+# bootstrap a venv
+# =======================
+
+LOCAL_ENV_NAME="py310-$(basename $(pwd))"
+LOCAL_ENV_DIR="$(pwd)/.env/$LOCAL_ENV_NAME"
+mkdir -p "$LOCAL_ENV_DIR"
+
+# make configs and caches a part of venv
+export POETRY_CACHE_DIR="$LOCAL_ENV_DIR/xdg/cache/poetry"
+export PIP_CACHE_DIR="$LOCAL_ENV_DIR/xdg/cache/pip"
+mkdir -p "$POETRY_CACHE_DIR" "$PIP_CACHE_DIR"
+
+#export POETRY_VIRTUALENVS_IN_PROJECT=true # store venv in ./.venv/
+#export POETRY_VIRTUALENVS_CREATE=false # install globally
+export SETUPTOOLS_USE_DISTUTILS=stdlib # https://github.com/pre-commit/pre-commit/issues/2178#issuecomment-1002163763
+export IFIELD_PRETTY_TRACEBACK=1
+#export SHOW_LOCALS=1 # locals in tracebacks
+export PYTHON_KEYRING_BACKEND="keyring.backends.null.Keyring"
+
+# ensure we have the correct python and poetry. Bootstrap via conda if missing
+if ! command -v python310 >/dev/null || ! command -v poetry >/dev/null; then
+ source .localenv-bootstrap-conda
+
+ if command -v mamba >/dev/null; then
+ CONDA=mamba
+ elif command -v conda >/dev/null; then
+ CONDA=conda
+ else
+ >&2 echo "ERROR: 'poetry' nor 'conda'/'mamba' could be found!"
+ exit 1
+ fi
+
+ function verbose {
+ echo +"$(printf " %q" "$@")"
+ "$@"
+ }
+
+ if ! ($CONDA env list | grep -q "^$LOCAL_ENV_NAME "); then
+ verbose $CONDA create --yes --name "$LOCAL_ENV_NAME" -c conda-forge \
+ python==3.10.8 poetry==1.3.1 #python-lsp-server
+ true
+ fi
+
+ verbose conda activate "$LOCAL_ENV_NAME" || exit $?
+ #verbose $CONDA activate "$LOCAL_ENV_NAME" || exit $?
+
+ unset -f verbose
+fi
+
+
+# enter poetry venv
+# source .envrc
+poetry run true # ensure venv exists
+#source "$(poetry env info -p)/bin/activate"
+export VIRTUAL_ENV=$(poetry env info --path)
+export POETRY_ACTIVE=1
+export PATH="$VIRTUAL_ENV/bin":"$PATH"
+# NOTE: poetry currently reuses and populates the conda venv.
+# See: https://github.com/python-poetry/poetry/issues/1724
+
+
+# ensure output dirs exist
+mkdir -p experiments/logdir
+
+# first-time-setup poetry
+if ! command -v fix-my-functions >/dev/null; then
+ poetry install
+fi
diff --git a/.localenv-bootstrap-conda b/.localenv-bootstrap-conda
new file mode 100644
index 0000000..d6e52b5
--- /dev/null
+++ b/.localenv-bootstrap-conda
@@ -0,0 +1,53 @@
+#!/usr/bin/env bash
+
+# =======================
+# bootstrap a conda venv
+# =======================
+
+CONDA_ENV_DIR="${LOCAL_ENV_DIR:-$(pwd)/.conda310}"
+mkdir -p "$CONDA_ENV_DIR"
+#touch "$HOME/.Xauthority"
+
+MINICONDA_PY310_URL="https://repo.anaconda.com/miniconda/Miniconda3-py310_22.11.1-1-Linux-x86_64.sh"
+MINICONDA_PY310_HASH="00938c3534750a0e4069499baf8f4e6dc1c2e471c86a59caa0dd03f4a9269db6"
+
+# Check if conda is available
+if ! command -v conda >/dev/null; then
+ export PATH="$CONDA_ENV_DIR/conda/bin:$PATH"
+fi
+
+# Check again if conda is available, install miniconda if not
+if ! command -v conda >/dev/null; then
+ (set -e #x
+ function verbose {
+ echo +"$(printf " %q" "$@")"
+ "$@"
+ }
+
+ if command -v curl >/dev/null; then
+ verbose curl -sLo "$CONDA_ENV_DIR/miniconda_py310.sh" "$MINICONDA_PY310_URL"
+ elif command -v wget >/dev/null; then
+ verbose wget -O "$CONDA_ENV_DIR/miniconda_py310.sh" "$MINICONDA_PY310_URL"
+ else
+ echo "ERROR: unable to download miniconda!"
+ exit 1
+ fi
+
+ verbose test "$(sha256sum "$CONDA_ENV_DIR/miniconda_py310.sh")" = "$MINICONDA_PY310_HASH"
+ verbose chmod +x "$CONDA_ENV_DIR/miniconda_py310.sh"
+
+ verbose "$CONDA_ENV_DIR/miniconda_py310.sh" -b -u -p "$CONDA_ENV_DIR/conda"
+ verbose rm "$CONDA_ENV_DIR/miniconda_py310.sh"
+
+ eval "$(conda shell.bash hook)" # basically `conda init`, without modifying .bashrc
+ verbose conda install --yes --name base mamba -c conda-forge
+
+ ) || exit $?
+fi
+
+unset CONDA_ENV_DIR
+unset MINICONDA_PY310_URL
+unset MINICONDA_PY310_HASH
+
+# Enter conda environment
+eval "$(conda shell.bash hook)" # basically `conda init`, without modifying .bashrc
diff --git a/.remoteenv b/.remoteenv
new file mode 100644
index 0000000..42519a5
--- /dev/null
+++ b/.remoteenv
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+# this file is used by remote-cli
+
+# Assumes repo is put in a "remotes/name-hash" folder,
+# the default behaviour of remote-exec
+REMOTES_DIR="$(dirname $(pwd))"
+LOCAL_ENV_NAME="py310-$(basename $(pwd))"
+LOCAL_ENV_DIR="$REMOTES_DIR/envs/$REMOTE_ENV_NAME"
+
+#export XDG_CACHE_HOME="$LOCAL_ENV_DIR/xdg/cache"
+#export XDG_DATA_HOME="$LOCAL_ENV_DIR/xdg/share"
+#export XDG_STATE_HOME="$LOCAL_ENV_DIR/xdg/state"
+#mkdir -p "$XDG_CACHE_HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME"
+export XDG_CONFIG_HOME="$LOCAL_ENV_DIR/xdg/config"
+mkdir -p "$XDG_CONFIG_HOME"
+
+
+export PYOPENGL_PLATFORM=egl # makes pyrender work headless
+#export PYOPENGL_PLATFORM=osmesa # makes pyrender work headless
+export SDL_VIDEODRIVER=dummy # pygame
+
+source .localenv
+
+# SLURM logs output dir
+if command -v sbatch >/dev/null; then
+ mkdir -p slurm_logs
+ test -L experiments/logdir/slurm_logs ||
+ ln -s ../../slurm_logs experiments/logdir/
+fi
diff --git a/.remoteignore.toml b/.remoteignore.toml
new file mode 100644
index 0000000..c8bd856
--- /dev/null
+++ b/.remoteignore.toml
@@ -0,0 +1,30 @@
+[push]
+exclude = [
+ "*.egg-info",
+ "*.pyc",
+ ".ipynb_checkpoints",
+ ".mypy_cache",
+ ".remote.toml",
+ ".remoteignore.toml",
+ ".venv",
+ ".wandb",
+ "__pycache__",
+ "data/models",
+ "docs",
+ "experiments/logdir",
+ "poetry.toml",
+ "slurm_logs",
+ "tmp",
+]
+include = []
+
+[pull]
+exclude = [
+ "*",
+]
+include = []
+
+[both]
+exclude = [
+]
+include = []
diff --git a/README.md b/README.md
index 6cc3f36..e01b2b1 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,127 @@
-This is where the code for the paper _"MARF: The Medial Atom Ray Field Object Representation"_ will be published.
+# MARF: The Medial Atom Ray Field Object Representation
+
+
+
+
+
+[Publication](https://doi.org/10.1016/j.cag.2023.06.032) | [Arxiv](https://arxiv.org/abs/2307.00037) | [Training data](https://mega.nz/file/9tsz3SbA#V6SIXpCFC4hbqWaFFvKmmS8BKir7rltXuhsqpEpE9wo) | [Network weights](https://mega.nz/file/t01AyTLK#7ZNMNgbqT9x2mhq5dxLuKeKyP7G0slfQX1RaZxifayw)
+
+
+
+**TL;DR:** We achieve _fast_ surface rendering by predicting _n_ maximally inscribed spherical intersection candidates for each camera ray.
+
+---
+
+## Entering the Virtual Environment
+
+The environment is defined in `pyproject.toml` using [Poetry](https://github.com/python-poetry/poetry) and reproducibly locked in `poetry.lock`.
+We propose three ways to enter the venv:
+
+```shell
+# Requires Python 3.10 and Poetry
+poetry install
+poetry shell
+
+# Will bootstrap a Miniconda 3.10 environment into .env/ if needed, then run poetry
+source .localenv
+```
+
+
+## Evaluation
+
+### Pretrained models
+
+You can download our pre-trained models` from .
+It should be unpacked into the root directory, such that the `experiment` folder gets merged.
+
+### The interactive renderer
+
+We automatically create experiment names with a schema of `{{model}}-{{experiment-name}}-{{hparams-summary}}-{{date}}-{{random-uid}}`.
+You can load experiment weights using either the full path, or just the `random-uid` bit.
+
+From the `experiments` directory:
+
+```shell
+./marf.py model {{experiment}} viewer
+```
+
+If you have downloaded our pre-trained network weights, consider trying:
+
+```shell
+./marf.py model nqzh viewer # Stanford Bunny (single-shape)
+./marf.py model wznx viewer # Stanford Buddha (single-shape)
+./marf.py model mxwd viewer # Stanford Armadillo (single-shape)
+./marf.py model camo viewer # Stanford Dragon (single-shape)
+./marf.py model ksul viewer # Stanford Lucy (single-shape)
+./marf.py model oxrf viewer # COSEG four-legged (multi-shape)
+```
+
+## Training and Evaluation Data
+
+You can download a pre-computed archive from .
+It should be extracted into the root directory such that a `data` directory is added to the root directory.
+
+
+
+Optionally, you may compute the data yourself.
+
+
+Single-shape training data:
+
+```shell
+# takes takes about 23 minutes, mainly due to lucy
+download-stanford bunny happy_buddha dragon armadillo lucy
+preprocess-stanford bunny happy_buddha dragon armadillo lucy \
+ --precompute-mesh-sv-scan-uv \
+ --compute-miss-distances \
+ --fill-missing-uv-points
+```
+
+Multi-shape training data:
+
+```shell
+# takes takes about 29 minutes
+download-coseg four-legged --shapes
+preprocess-coseg four-legged \
+ --precompute-mesh-sv-scan-uv \
+ --compute-miss-distances \
+ --fill-missing-uv-points
+```
+
+Evaluation data:
+
+```shell
+# takes takes about 2 hour 20 minutes, mainly due to lucy
+preprocess-stanford bunny happy_buddha dragon armadillo lucy \
+ --precompute-mesh-sphere-scan \
+ --compute-miss-distances
+```
+
+```shell
+# takes takes about 4 hours
+preprocess-coseg four-legged \
+ --precompute-mesh-sphere-scan \
+ --compute-miss-distances
+```
+
+
+
+## Training
+
+Our experiments are defined using YAML config files, optionally templated using Jinja2 as a preprocessor.
+These templates accept additional input from the command line in the form of `-Okey=value` options.
+Our whole experiment matrix is defined in `marf.yaml.j12`. We select between different experiment groups using `-Omode={single,ablation,multi}`, and which experiment using `-Oselect={{integer}}`
+
+From the `experiments` directory:
+
+CPU mode:
+
+```shell
+./marf.py model marf.yaml.j2 -Oexperiment_name=cpu_test -Omode=single -Oselect=0 fit
+```
+
+GPU mode:
+
+```shell
+./marf.py model marf.yaml.j2 -Oexperiment_name=cpu_test -Omode=single -Oselect=0 fit --accelerator gpu --devices 1
+```
diff --git a/ablation.md b/ablation.md
new file mode 100644
index 0000000..34fcb3b
--- /dev/null
+++ b/ablation.md
@@ -0,0 +1,139 @@
+### MARF
+- `experiment-stanfordv12-bunny-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0010-nqzh`
+- `experiment-stanfordv12-happy_buddha-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0312-wznx`
+- `experiment-stanfordv12-armadillo-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-30-1944-mxwd`
+- `experiment-stanfordv12-dragon-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0529-camo`
+- `experiment-stanfordv12-lucy-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0743-ksul`
+
+### LFN encoding
+- `experiment-stanfordv12-dragon-plkr2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0539-xjte`
+- `experiment-stanfordv12-lucy-plkr2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0753-ayvt`
+- `experiment-stanfordv12-bunny-plkr2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0022-axft`
+- `experiment-stanfordv12-happy_buddha-plkr2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0322-xfoc`
+- `experiment-stanfordv12-armadillo-plkr2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-30-2039-vbks`
+
+### PRIF encoding
+- `experiment-stanfordv12-armadillo-prpft2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-30-2033-nkxm`
+- `experiment-stanfordv12-happy_buddha-prpft2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0313-huci`
+- `experiment-stanfordv12-dragon-prpft2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0537-dxsb`
+- `experiment-stanfordv12-bunny-prpft2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0011-tzic`
+- `experiment-stanfordv12-lucy-prpft2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0744-hzvw`
+
+### No init scheme.
+- `experiment-stanfordv12-happy_buddha-both2marf-16atom-50xinscr-10dmiss-nogeom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0444-uohy`
+- `experiment-stanfordv12-armadillo-both2marf-16atom-50xinscr-10dmiss-nogeom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-30-2307-wjcf`
+- `experiment-stanfordv12-dragon-both2marf-16atom-50xinscr-10dmiss-nogeom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0707-eanc`
+- `experiment-stanfordv12-bunny-both2marf-16atom-50xinscr-10dmiss-nogeom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0225-kcfw`
+- `experiment-stanfordv12-lucy-both2marf-16atom-50xinscr-10dmiss-nogeom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0852-lkfh`
+
+### 1 atom candidate
+- `experiment-stanfordv12-lucy-both2marf-1atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0755-qzth`
+- `experiment-stanfordv12-bunny-both2marf-1atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0027-ycnl`
+- `experiment-stanfordv12-armadillo-both2marf-1atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-30-2121-fwvo`
+- `experiment-stanfordv12-dragon-both2marf-1atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0541-nvhs`
+- `experiment-stanfordv12-happy_buddha-both2marf-1atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0324-cuyw`
+
+### 4 atom candidates
+- `experiment-stanfordv12-armadillo-both2marf-4atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-30-2122-qiwg`
+- `experiment-stanfordv12-dragon-both2marf-4atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0544-ihkx`
+- `experiment-stanfordv12-lucy-both2marf-4atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0757-jwxm`
+- `experiment-stanfordv12-happy_buddha-both2marf-4atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0328-chhs`
+- `experiment-stanfordv12-bunny-both2marf-4atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0038-zymb`
+
+### 8 atom candidates
+- `experiment-stanfordv12-bunny-both2marf-8atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0055-ogpd`
+- `experiment-stanfordv12-lucy-both2marf-8atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0757-frxb`
+- `experiment-stanfordv12-happy_buddha-both2marf-8atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0337-twys`
+- `experiment-stanfordv12-dragon-both2marf-8atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0551-bubw`
+- `experiment-stanfordv12-armadillo-both2marf-8atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-30-2137-nnlj`
+
+### 32 atom candidates
+- `experiment-stanfordv12-bunny-both2marf-32atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0056-ourc`
+- `experiment-stanfordv12-armadillo-both2marf-32atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-30-2141-byaj`
+- `experiment-stanfordv12-dragon-both2marf-32atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0554-zobg`
+- `experiment-stanfordv12-happy_buddha-both2marf-32atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0337-rmyq`
+- `experiment-stanfordv12-lucy-both2marf-32atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0800-lqen`
+
+### 64 atom candidates
+- `experiment-stanfordv12-happy_buddha-both2marf-64atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0339-whcx`
+- `experiment-stanfordv12-bunny-both2marf-64atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0058-seen`
+- `experiment-stanfordv12-lucy-both2marf-64atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0806-ycxj`
+- `experiment-stanfordv12-armadillo-both2marf-64atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-30-2153-wnfq`
+- `experiment-stanfordv12-dragon-both2marf-64atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0555-zgcb`
+
+### No intersection loss
+- `experiment-stanfordv12-happy_buddha-both2marf-16atom-50xinscr-10dmiss-geom-0chit-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-1053-ydnh`
+- `experiment-stanfordv12-lucy-both2marf-16atom-50xinscr-10dmiss-geom-0chit-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-1111-fawl`
+- `experiment-stanfordv12-bunny-both2marf-16atom-50xinscr-10dmiss-geom-0chit-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-1045-umwl`
+- `experiment-stanfordv12-dragon-both2marf-16atom-50xinscr-10dmiss-geom-0chit-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-1103-lwmb`
+- `experiment-stanfordv12-armadillo-both2marf-16atom-50xinscr-10dmiss-geom-0chit-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-1041-lhcc`
+
+### No silhouette loss
+- `experiment-stanfordv12-armadillo-both2marf-16atom-50xinscr-0dmiss-geom-20chit-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-1042-fsuw`
+- `experiment-stanfordv12-bunny-both2marf-16atom-50xinscr-0dmiss-geom-20chit-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-1046-nszw`
+- `experiment-stanfordv12-dragon-both2marf-16atom-50xinscr-0dmiss-geom-20chit-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-1111-mlal`
+- `experiment-stanfordv12-happy_buddha-both2marf-16atom-50xinscr-0dmiss-geom-20chit-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-1055-cvkg`
+- `experiment-stanfordv12-lucy-both2marf-16atom-50xinscr-0dmiss-geom-20chit-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-1114-pdyh`
+
+### More silhouette loss
+- `experiment-stanfordv12-bunny-both2marf-16atom-50xinscr-50dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0157-yekm`
+- `experiment-stanfordv12-armadillo-both2marf-16atom-50xinscr-50dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-30-2243-nlrv`
+- `experiment-stanfordv12-dragon-both2marf-16atom-50xinscr-50dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0639-yros`
+- `experiment-stanfordv12-lucy-both2marf-16atom-50xinscr-50dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0842-xktg`
+- `experiment-stanfordv12-happy_buddha-both2marf-16atom-50xinscr-50dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0423-ibxs`
+
+### No normal loss
+- `experiment-stanfordv12-dragon-both2marf-16atom-50xinscr-10dmiss-geom-nocnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0614-ttta`
+- `experiment-stanfordv12-bunny-both2marf-16atom-50xinscr-10dmiss-geom-nocnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0106-bnke`
+- `experiment-stanfordv12-armadillo-both2marf-16atom-50xinscr-10dmiss-geom-nocnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-30-2154-bxwl`
+- `experiment-stanfordv12-lucy-both2marf-16atom-50xinscr-10dmiss-geom-nocnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0811-qqgu`
+- `experiment-stanfordv12-happy_buddha-both2marf-16atom-50xinscr-10dmiss-geom-nocnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0357-gwca`
+
+### No inscription loss
+- `experiment-stanfordv12-bunny-both2marf-16atom-noxinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0227-xrqt`
+- `experiment-stanfordv12-armadillo-both2marf-16atom-noxinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-30-2312-cgzv`
+- `experiment-stanfordv12-happy_buddha-both2marf-16atom-noxinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0452-rerr`
+- `experiment-stanfordv12-dragon-both2marf-16atom-noxinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0709-tfgg`
+- `experiment-stanfordv12-lucy-both2marf-16atom-noxinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0856-ctvc`
+
+### More inscription loss
+- `experiment-stanfordv12-happy_buddha-both2marf-16atom-250xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0459-kyyh`
+- `experiment-stanfordv12-bunny-both2marf-16atom-250xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0243-qqqj`
+- `experiment-stanfordv12-armadillo-both2marf-16atom-250xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-30-2336-yclo`
+- `experiment-stanfordv12-lucy-both2marf-16atom-250xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0913-mulv`
+- `experiment-stanfordv12-dragon-both2marf-16atom-250xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0714-zugg`
+
+### No maximality reg.
+- `experiment-stanfordv12-lucy-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-0sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0842-cvln`
+- `experiment-stanfordv12-happy_buddha-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-0sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0425-vpen`
+- `experiment-stanfordv12-bunny-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-0sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0207-qpdb`
+- `experiment-stanfordv12-armadillo-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-0sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-30-2251-zqvi`
+- `experiment-stanfordv12-dragon-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-0sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0641-ucdo`
+
+### More maximality reg.
+- `experiment-stanfordv12-dragon-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-5000sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0659-bqvf`
+- `experiment-stanfordv12-armadillo-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-5000sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-30-2256-escz`
+- `experiment-stanfordv12-bunny-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-5000sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0208-wmvs`
+- `experiment-stanfordv12-happy_buddha-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-5000sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0442-gdah`
+- `experiment-stanfordv12-lucy-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-5000sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0845-halc`
+
+### No specialization reg.
+- `experiment-stanfordv12-lucy-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-nominatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0913-odyn`
+- `experiment-stanfordv12-bunny-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-nominatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0251-xzig`
+- `experiment-stanfordv12-dragon-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-nominatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0722-gxps`
+- `experiment-stanfordv12-armadillo-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-nominatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-30-2342-zybo`
+- `experiment-stanfordv12-happy_buddha-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-nominatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-10dmv-nocond-100cwu500clr70tvs-2023-05-31-0507-tvlt`
+
+### No multi-view loss
+- `experiment-stanfordv12-bunny-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-nogradreg-nocond-100cwu500clr70tvs-2023-05-31-0310-wbqj`
+- `experiment-stanfordv12-armadillo-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-nogradreg-nocond-100cwu500clr70tvs-2023-05-30-2357-qnct`
+- `experiment-stanfordv12-happy_buddha-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-nogradreg-nocond-100cwu500clr70tvs-2023-05-31-0527-psnk`
+- `experiment-stanfordv12-lucy-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-nogradreg-nocond-100cwu500clr70tvs-2023-05-31-0927-wxcq`
+- `experiment-stanfordv12-dragon-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-nogradreg-nocond-100cwu500clr70tvs-2023-05-31-0743-pdbc`
+
+### More multi-view loss
+- `experiment-stanfordv12-happy_buddha-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-20dmv-nocond-100cwu500clr70tvs-2023-05-31-0510-caah`
+- `experiment-stanfordv12-dragon-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-20dmv-nocond-100cwu500clr70tvs-2023-05-31-0726-zkyg`
+- `experiment-stanfordv12-bunny-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-20dmv-nocond-100cwu500clr70tvs-2023-05-31-0254-akbq`
+- `experiment-stanfordv12-lucy-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-20dmv-nocond-100cwu500clr70tvs-2023-05-31-0924-aahb`
+- `experiment-stanfordv12-armadillo-both2marf-16atom-50xinscr-10dmiss-geom-25cnrml-8x512fc-leaky_relu-hit-0minatomstdngxp-500sphgrow-10mdrop-layernorm-multi_view-20dmv-nocond-100cwu500clr70tvs-2023-05-30-2352-xlrn`
diff --git a/experiments/marf.py b/experiments/marf.py
new file mode 100755
index 0000000..73bf151
--- /dev/null
+++ b/experiments/marf.py
@@ -0,0 +1,624 @@
+#!/usr/bin/env python3
+from abc import ABC, abstractmethod
+from argparse import Namespace
+from collections import defaultdict
+from datetime import datetime
+from ifield import logging
+from ifield.cli import CliInterface
+from ifield.data.common.scan import SingleViewUVScan
+from ifield.data.coseg import read as coseg_read
+from ifield.data.stanford import read as stanford_read
+from ifield.datasets import stanford, coseg, common
+from ifield.models import intersection_fields
+from ifield.utils.operators import diff
+from ifield.viewer.ray_field import ModelViewer
+from munch import Munch
+from pathlib import Path
+from pytorch3d.loss.chamfer import chamfer_distance
+from pytorch_lightning.utilities import rank_zero_only
+from torch.nn import functional as F
+from torch.utils.data import DataLoader, Subset
+from tqdm import tqdm
+from trimesh import Trimesh
+from typing import Union
+import builtins
+import itertools
+import json
+import numpy as np
+import pytorch_lightning as pl
+import rich
+import rich.pretty
+import statistics
+import torch
+pl.seed_everything(31337)
+torch.set_float32_matmul_precision('medium')
+
+
+IField = intersection_fields.IntersectionFieldAutoDecoderModel # brevity
+
+
+class RayFieldAdDataModuleBase(pl.LightningDataModule, ABC):
+ @property
+ @abstractmethod
+ def observation_ids(self) -> list[str]:
+ ...
+
+ @abstractmethod
+ def mk_ad_dataset(self) -> common.AutodecoderDataset:
+ ...
+
+ @staticmethod
+ @abstractmethod
+ def get_trimesh_from_uid(uid) -> Trimesh:
+ ...
+
+ @staticmethod
+ @abstractmethod
+ def get_sphere_scan_from_uid(uid) -> SingleViewUVScan:
+ ...
+
+ def setup(self, stage=None):
+ assert stage in ["fit", None] # fit is for train/val, None is for all. "test" not supported ATM
+
+ if not self.hparams.data_dir is None:
+ coseg.config.DATA_PATH = self.hparams.data_dir
+ step = self.hparams.step # brevity
+
+ dataset = self.mk_ad_dataset()
+ n_items_pre_step_mapping = len(dataset)
+
+ if step > 1:
+ dataset = common.TransformExtendedDataset(dataset)
+
+ for sx in range(step):
+ for sy in range(step):
+ def make_slicer(sx, sy, step) -> callable: # the closure is required
+ if step > 1:
+ return lambda t: t[sx::step, sy::step]
+ else:
+ return lambda t: t
+ @dataset.map(slicer=make_slicer(sx, sy, step))
+ def unpack(sample: tuple[str, SingleViewUVScan], slicer: callable):
+ scan: SingleViewUVScan = sample[1]
+ assert not scan.hits.shape[0] % step, f"{scan.hits.shape[0]=} not divisible by {step=}"
+ assert not scan.hits.shape[1] % step, f"{scan.hits.shape[1]=} not divisible by {step=}"
+
+ return {
+ "z_uid" : sample[0],
+ "origins" : scan.cam_pos,
+ "dirs" : slicer(scan.ray_dirs),
+ "points" : slicer(scan.points),
+ "hits" : slicer(scan.hits),
+ "miss" : slicer(scan.miss),
+ "normals" : slicer(scan.normals),
+ "distances" : slicer(scan.distances),
+ }
+
+ # Split each object into train/val with SampleSplit
+ n_items = len(dataset)
+ n_val = int(n_items * self.hparams.val_fraction)
+ n_train = n_items - n_val
+ self.generator = torch.Generator().manual_seed(self.hparams.prng_seed)
+
+ # split the dataset such that all steps are in same part
+ assert n_items == n_items_pre_step_mapping * step * step, (n_items, n_items_pre_step_mapping, step)
+ indices = [
+ i*step*step + sx*step + sy
+ for i in torch.randperm(n_items_pre_step_mapping, generator=self.generator).tolist()
+ for sx in range(step)
+ for sy in range(step)
+ ]
+ self.dataset_train = Subset(dataset, sorted(indices[:n_train], key=lambda x: torch.rand(1, generator=self.generator).tolist()[0]))
+ self.dataset_val = Subset(dataset, sorted(indices[n_train:n_train+n_val], key=lambda x: torch.rand(1, generator=self.generator).tolist()[0]))
+
+ assert len(self.dataset_train) % self.hparams.batch_size == 0
+ assert len(self.dataset_val) % self.hparams.batch_size == 0
+
+ def train_dataloader(self):
+ return DataLoader(self.dataset_train,
+ batch_size = self.hparams.batch_size,
+ drop_last = self.hparams.drop_last,
+ num_workers = self.hparams.num_workers,
+ persistent_workers = self.hparams.persistent_workers,
+ pin_memory = self.hparams.pin_memory,
+ prefetch_factor = self.hparams.prefetch_factor,
+ shuffle = self.hparams.shuffle,
+ generator = self.generator,
+ )
+
+ def val_dataloader(self):
+ return DataLoader(self.dataset_val,
+ batch_size = self.hparams.batch_size,
+ drop_last = self.hparams.drop_last,
+ num_workers = self.hparams.num_workers,
+ persistent_workers = self.hparams.persistent_workers,
+ pin_memory = self.hparams.pin_memory,
+ prefetch_factor = self.hparams.prefetch_factor,
+ generator = self.generator,
+ )
+
+
+class StanfordUVDataModule(RayFieldAdDataModuleBase):
+ skyward = "+Z"
+ def __init__(self,
+ data_dir : Union[str, Path, None] = None,
+ obj_names : list[str] = ["bunny"], # empty means all
+
+ prng_seed : int = 1337,
+ step : int = 2,
+ batch_size : int = 5,
+ drop_last : bool = False,
+ num_workers : int = 8,
+ persistent_workers : bool = True,
+ pin_memory : int = True,
+ prefetch_factor : int = 2,
+ shuffle : bool = True,
+ val_fraction : float = 0.30,
+ ):
+ super().__init__()
+ if not obj_names:
+ obj_names = stanford_read.list_object_names()
+ self.save_hyperparameters()
+
+ @property
+ def observation_ids(self) -> list[str]:
+ return self.hparams.obj_names
+
+ def mk_ad_dataset(self) -> common.AutodecoderDataset:
+ return stanford.AutodecoderSingleViewUVScanDataset(
+ obj_names = self.hparams.obj_names,
+ data_path = self.hparams.data_dir,
+ )
+
+ @staticmethod
+ def get_trimesh_from_uid(obj_name) -> Trimesh:
+ import mesh_to_sdf
+ mesh = stanford_read.read_mesh(obj_name)
+ return mesh_to_sdf.scale_to_unit_sphere(mesh)
+
+ @staticmethod
+ def get_sphere_scan_from_uid(obj_name) -> SingleViewUVScan:
+ return stanford_read.read_mesh_mesh_sphere_scan(obj_name)
+
+
+class CosegUVDataModule(RayFieldAdDataModuleBase):
+ skyward = "+Y"
+ def __init__(self,
+ data_dir : Union[str, Path, None] = None,
+ object_sets : tuple[str] = ["tele-aliens"], # empty means all
+
+ prng_seed : int = 1337,
+ step : int = 2,
+ batch_size : int = 5,
+ drop_last : bool = False,
+ num_workers : int = 8,
+ persistent_workers : bool = True,
+ pin_memory : int = True,
+ prefetch_factor : int = 2,
+ shuffle : bool = True,
+ val_fraction : float = 0.30,
+ ):
+ super().__init__()
+ if not object_sets:
+ object_sets = coseg_read.list_object_sets()
+ object_sets = tuple(object_sets)
+ self.save_hyperparameters()
+
+ @property
+ def observation_ids(self) -> list[str]:
+ return coseg_read.list_model_id_strings(self.hparams.object_sets)
+
+ def mk_ad_dataset(self) -> common.AutodecoderDataset:
+ return coseg.AutodecoderSingleViewUVScanDataset(
+ object_sets = self.hparams.object_sets,
+ data_path = self.hparams.data_dir,
+ )
+
+ @staticmethod
+ def get_trimesh_from_uid(string_uid):
+ raise NotImplementedError
+
+ @staticmethod
+ def get_sphere_scan_from_uid(string_uid) -> SingleViewUVScan:
+ uid = coseg_read.model_id_string_to_uid(string_uid)
+ return coseg_read.read_mesh_mesh_sphere_scan(*uid)
+
+
+def mk_cli(args=None) -> CliInterface:
+ cli = CliInterface(
+ module_cls = IField,
+ datamodule_cls = [StanfordUVDataModule, CosegUVDataModule],
+ workdir = Path(__file__).parent.resolve(),
+ experiment_name_prefix = "ifield",
+ )
+ cli.trainer_defaults.update(dict(
+ precision = 16,
+ min_epochs = 5,
+ ))
+
+ @cli.register_pre_training_callback
+ def populate_autodecoder_z_uids(args: Namespace, config: Munch, module: IField, trainer: pl.Trainer, datamodule: RayFieldAdDataModuleBase, logger: logging.Logger):
+ module.set_observation_ids(datamodule.observation_ids)
+ rank = getattr(rank_zero_only, "rank", 0)
+ rich.print(f"[rank {rank}] {len(datamodule.observation_ids) = }")
+ rich.print(f"[rank {rank}] {len(datamodule.observation_ids) > 1 = }")
+ rich.print(f"[rank {rank}] {module.is_conditioned = }")
+
+ @cli.register_action(help="Interactive window with direct renderings from the model", args=[
+ ("--shading", dict(type=int, default=ModelViewer.vizmodes_shading .index("lambertian"), help=f"Rendering mode. {{{', '.join(f'{i}: {m!r}'for i, m in enumerate(ModelViewer.vizmodes_shading))}}}")),
+ ("--centroid", dict(type=int, default=ModelViewer.vizmodes_centroids.index("best-centroids-colored"), help=f"Rendering mode. {{{', '.join(f'{i}: {m!r}'for i, m in enumerate(ModelViewer.vizmodes_centroids))}}}")),
+ ("--spheres", dict(type=int, default=ModelViewer.vizmodes_spheres .index(None), help=f"Rendering mode. {{{', '.join(f'{i}: {m!r}'for i, m in enumerate(ModelViewer.vizmodes_spheres))}}}")),
+ ("--analytical-normals", dict(action="store_true")),
+ ("--ground-truth", dict(action="store_true")),
+ ("--solo-atom",dict(type=int, default=None, help="Rendering mode")),
+ ("--res", dict(type=int, nargs=2, default=(210, 160), help="Rendering resolution")),
+ ("--bg", dict(choices=["map", "white", "black"], default="map")),
+ ("--skyward", dict(type=str, default="+Z", help='one of: "+X", "-X", "+Y", "-Y", ["+Z"], "-Z"')),
+ ("--scale", dict(type=int, default=3, help="Rendering scale")),
+ ("--fps", dict(type=int, default=None, help="FPS upper limit")),
+ ("--cam-state",dict(type=str, default=None, help="json cam state, expored with CTRL+H")),
+ ("--write", dict(type=Path, default=None, help="Where to write a screenshot.")),
+ ])
+ @torch.no_grad()
+ def viewer(args: Namespace, config: Munch, model: IField):
+ datamodule_cls: RayFieldAdDataModuleBase = cli.get_datamodule_cls_from_config(args, config)
+
+ if torch.cuda.is_available() and torch.cuda.device_count() > 0:
+ model.to("cuda")
+ viewer = ModelViewer(model, start_uid=next(iter(model.keys())),
+ name = config.experiment_name,
+ screenshot_dir = Path(__file__).parent.parent / "images/pygame-viewer",
+ res = args.res,
+ skyward = args.skyward,
+ scale = args.scale,
+ mesh_gt_getter = datamodule_cls.get_trimesh_from_uid,
+ )
+ viewer.display_mode_shading = args.shading
+ viewer.display_mode_centroid = args.centroid
+ viewer.display_mode_spheres = args.spheres
+ if args.ground_truth: viewer.display_mode_normals = viewer.vizmodes_normals.index("ground_truth")
+ if args.analytical_normals: viewer.display_mode_normals = viewer.vizmodes_normals.index("analytical")
+ viewer.atom_index_solo = args.solo_atom
+ viewer.fps_cap = args.fps
+ viewer.display_sphere_map_bg = { "map": True, "white": 255, "black": 0 }[args.bg]
+ if args.cam_state is not None:
+ viewer.cam_state = json.loads(args.cam_state)
+ if args.write is None:
+ viewer.run()
+ else:
+ assert args.write.suffix == ".png", args.write.name
+ viewer.render_headless(args.write,
+ n_frames = 1,
+ fps = 1,
+ state_callback = None,
+ )
+
+ @cli.register_action(help="Prerender direct renderings from the model", args=[
+ ("output_path",dict(type=Path, help="Where to store the output. We recommend a .mp4 suffix.")),
+ ("uids", dict(type=str, nargs="*")),
+ ("--frames", dict(type=int, default=60, help="Number of per interpolation. Default is 60")),
+ ("--fps", dict(type=int, default=60, help="Default is 60")),
+ ("--shading", dict(type=int, default=ModelViewer.vizmodes_shading .index("lambertian"), help=f"Rendering mode. {{{', '.join(f'{i}: {m!r}'for i, m in enumerate(ModelViewer.vizmodes_shading))}}}")),
+ ("--centroid", dict(type=int, default=ModelViewer.vizmodes_centroids.index("best-centroids-colored"), help=f"Rendering mode. {{{', '.join(f'{i}: {m!r}'for i, m in enumerate(ModelViewer.vizmodes_centroids))}}}")),
+ ("--spheres", dict(type=int, default=ModelViewer.vizmodes_spheres .index(None), help=f"Rendering mode. {{{', '.join(f'{i}: {m!r}'for i, m in enumerate(ModelViewer.vizmodes_spheres))}}}")),
+ ("--analytical-normals", dict(action="store_true")),
+ ("--solo-atom",dict(type=int, default=None, help="Rendering mode")),
+ ("--res", dict(type=int, nargs=2, default=(240, 240), help="Rendering resolution. Default is 240 240")),
+ ("--bg", dict(choices=["map", "white", "black"], default="map")),
+ ("--skyward", dict(type=str, default="+Z", help='one of: "+X", "-X", "+Y", "-Y", ["+Z"], "-Z"')),
+ ("--bitrate", dict(type=str, default="1500k", help="Encoding bitrate. Default is 1500k")),
+ ("--cam-state",dict(type=str, default=None, help="json cam state, expored with CTRL+H")),
+ ])
+ @torch.no_grad()
+ def render_video_interpolation(args: Namespace, config: Munch, model: IField, **kw):
+ if torch.cuda.is_available() and torch.cuda.device_count() > 0:
+ model.to("cuda")
+ uids = args.uids or list(model.keys())
+ assert len(uids) > 1
+ if not args.uids: uids.append(uids[0])
+ viewer = ModelViewer(model, uids[0],
+ name = config.experiment_name,
+ screenshot_dir = Path(__file__).parent.parent / "images/pygame-viewer",
+ res = args.res,
+ skyward = args.skyward,
+ )
+ if args.cam_state is not None:
+ viewer.cam_state = json.loads(args.cam_state)
+ viewer.display_mode_shading = args.shading
+ viewer.display_mode_centroid = args.centroid
+ viewer.display_mode_spheres = args.spheres
+ if args.analytical_normals: viewer.display_mode_normals = viewer.vizmodes_normals.index("analytical")
+ viewer.atom_index_solo = args.solo_atom
+ viewer.display_sphere_map_bg = { "map": True, "white": 255, "black": 0 }[args.bg]
+ def state_callback(self: ModelViewer, frame: int):
+ if frame % args.frames:
+ self.lambertian_color = (0.8, 0.8, 1.0)
+ else:
+ self.lambertian_color = (1.0, 1.0, 1.0)
+ self.fps = args.frames
+ idx = frame // args.frames + 1
+ if idx != len(uids):
+ self.current_uid = uids[idx]
+ print(f"Writing video to {str(args.output_path)!r}...")
+ viewer.render_headless(args.output_path,
+ n_frames = args.frames * (len(uids)-1) + 1,
+ fps = args.fps,
+ state_callback = state_callback,
+ bitrate = args.bitrate,
+ )
+
+ @cli.register_action(help="Prerender direct renderings from the model", args=[
+ ("output_path",dict(type=Path, help="Where to store the output. We recommend a .mp4 suffix.")),
+ ("--frames", dict(type=int, default=180, help="Number of frames. Default is 180")),
+ ("--fps", dict(type=int, default=60, help="Default is 60")),
+ ("--shading", dict(type=int, default=ModelViewer.vizmodes_shading .index("lambertian"), help=f"Rendering mode. {{{', '.join(f'{i}: {m!r}'for i, m in enumerate(ModelViewer.vizmodes_shading))}}}")),
+ ("--centroid", dict(type=int, default=ModelViewer.vizmodes_centroids.index("best-centroids-colored"), help=f"Rendering mode. {{{', '.join(f'{i}: {m!r}'for i, m in enumerate(ModelViewer.vizmodes_centroids))}}}")),
+ ("--spheres", dict(type=int, default=ModelViewer.vizmodes_spheres .index(None), help=f"Rendering mode. {{{', '.join(f'{i}: {m!r}'for i, m in enumerate(ModelViewer.vizmodes_spheres))}}}")),
+ ("--analytical-normals", dict(action="store_true")),
+ ("--solo-atom",dict(type=int, default=None, help="Rendering mode")),
+ ("--res", dict(type=int, nargs=2, default=(320, 240), help="Rendering resolution. Default is 320 240")),
+ ("--bg", dict(choices=["map", "white", "black"], default="map")),
+ ("--skyward", dict(type=str, default="+Z", help='one of: "+X", "-X", "+Y", "-Y", ["+Z"], "-Z"')),
+ ("--bitrate", dict(type=str, default="1500k", help="Encoding bitrate. Default is 1500k")),
+ ("--cam-state",dict(type=str, default=None, help="json cam state, expored with CTRL+H")),
+ ])
+ @torch.no_grad()
+ def render_video_spin(args: Namespace, config: Munch, model: IField, **kw):
+ if torch.cuda.is_available() and torch.cuda.device_count() > 0:
+ model.to("cuda")
+ viewer = ModelViewer(model, start_uid=next(iter(model.keys())),
+ name = config.experiment_name,
+ screenshot_dir = Path(__file__).parent.parent / "images/pygame-viewer",
+ res = args.res,
+ skyward = args.skyward,
+ )
+ if args.cam_state is not None:
+ viewer.cam_state = json.loads(args.cam_state)
+ viewer.display_mode_shading = args.shading
+ viewer.display_mode_centroid = args.centroid
+ viewer.display_mode_spheres = args.spheres
+ if args.analytical_normals: viewer.display_mode_normals = viewer.vizmodes_normals.index("analytical")
+ viewer.atom_index_solo = args.solo_atom
+ viewer.display_sphere_map_bg = { "map": True, "white": 255, "black": 0 }[args.bg]
+ cam_rot_x_init = viewer.cam_rot_x
+ def state_callback(self: ModelViewer, frame: int):
+ self.cam_rot_x = cam_rot_x_init + 3.14 * (frame / args.frames) * 2
+ print(f"Writing video to {str(args.output_path)!r}...")
+ viewer.render_headless(args.output_path,
+ n_frames = args.frames,
+ fps = args.fps,
+ state_callback = state_callback,
+ bitrate = args.bitrate,
+ )
+
+ @cli.register_action(help="foo", args=[
+ ("fname", dict(type=Path, help="where to write json")),
+ ("-t", "--transpose", dict(action="store_true", help="transpose the output")),
+ ("--single-shape", dict(action="store_true", help="break after first shape")),
+ ("--batch-size", dict(type=int, default=40_000, help="tradeoff between vram usage and efficiency")),
+ ("--n-cd", dict(type=int, default=30_000, help="Number of points to use when computing chamfer distance")),
+ ("--filter-outliers", dict(action="store_true", help="like in PRIF")),
+ ])
+ @torch.enable_grad()
+ def compute_scores(args: Namespace, config: Munch, model: IField, **kw):
+ datamodule_cls: RayFieldAdDataModuleBase = cli.get_datamodule_cls_from_config(args, config)
+ model.eval()
+ if torch.cuda.is_available() and torch.cuda.device_count() > 0:
+ model.to("cuda")
+
+ def T(array: np.ndarray, **kw) -> torch.Tensor:
+ if isinstance(array, torch.Tensor): return array
+ return torch.tensor(array, device=model.device, dtype=model.dtype if isinstance(array, np.floating) else None, **kw)
+
+ MEDIAL = model.hparams.output_mode == "medial_sphere"
+ if not MEDIAL: assert model.hparams.output_mode == "orthogonal_plane"
+
+
+ uids = sorted(model.keys())
+ if args.single_shape: uids = [uids[0]]
+ rich.print(f"{datamodule_cls.__name__ = }")
+ rich.print(f"{len(uids) = }")
+
+ # accumulators for IoU and F-Score, CD and COS
+
+ # sum reduction:
+ n = defaultdict(int)
+ n_gt_hits = defaultdict(int)
+ n_gt_miss = defaultdict(int)
+ n_gt_missing = defaultdict(int)
+ n_outliers = defaultdict(int)
+ p_mse = defaultdict(int)
+ s_mse = defaultdict(int)
+ cossim_med = defaultdict(int) # medial normals
+ cossim_jac = defaultdict(int) # jacovian normals
+ TP,FN,FP,TN = [defaultdict(int) for _ in range(4)] # IoU and f-score
+ # mean reduction:
+ cd_dist = {} # chamfer distance
+ cd_cos_med = {} # chamfer medial normals
+ cd_cos_jac = {} # chamfer jacovian normals
+ all_metrics = dict(
+ n=n, n_gt_hits=n_gt_hits, n_gt_miss=n_gt_miss, n_gt_missing=n_gt_missing, p_mse=p_mse,
+ cossim_jac=cossim_jac,
+ TP=TP, FN=FN, FP=FP, TN=TN, cd_dist=cd_dist,
+ cd_cos_jac=cd_cos_jac,
+ )
+ if MEDIAL:
+ all_metrics["s_mse"] = s_mse
+ all_metrics["cossim_med"] = cossim_med
+ all_metrics["cd_cos_med"] = cd_cos_med
+ if args.filter_outliers:
+ all_metrics["n_outliers"] = n_outliers
+
+ t = datetime.now()
+ for uid in tqdm(uids, desc="Dataset", position=0, leave=True, disable=len(uids)<=1):
+ sphere_scan_gt = datamodule_cls.get_sphere_scan_from_uid(uid)
+
+ z = model[uid].detach()
+
+ all_intersections = []
+ all_medial_normals = []
+ all_jacobian_normals = []
+
+ step = args.batch_size
+ for i in tqdm(range(0, sphere_scan_gt.hits.shape[0], step), desc=f"Item {uid!r}", position=1, leave=False):
+ # prepare batch and gt
+ origins = T(sphere_scan_gt.cam_pos [i:i+step, :], requires_grad = True)
+ dirs = T(sphere_scan_gt.ray_dirs [i:i+step, :])
+ gt_hits = T(sphere_scan_gt.hits [i:i+step])
+ gt_miss = T(sphere_scan_gt.miss [i:i+step])
+ gt_missing = T(sphere_scan_gt.missing [i:i+step])
+ gt_points = T(sphere_scan_gt.points [i:i+step, :])
+ gt_normals = T(sphere_scan_gt.normals [i:i+step, :])
+ gt_distances = T(sphere_scan_gt.distances[i:i+step])
+
+ # forward
+ if MEDIAL:
+ (
+ depths,
+ silhouettes,
+ intersections,
+ medial_normals,
+ is_intersecting,
+ sphere_centers,
+ sphere_radii,
+ ) = model({
+ "origins" : origins,
+ "dirs" : dirs,
+ }, z, intersections_only=False, allow_nans=False)
+ else:
+ silhouettes = medial_normals = None
+ intersections, is_intersecting = model({
+ "origins" : origins,
+ "dirs" : dirs,
+ }, z, normalize_origins = True)
+ is_intersecting = is_intersecting > 0.5
+ jac = diff.jacobian(intersections, origins, detach=True)
+
+ # outlier removal (PRIF)
+ if args.filter_outliers:
+ outliers = jac.norm(dim=-2).norm(dim=-1) > 5
+ n_outliers[uid] += outliers[is_intersecting].sum().item()
+ # We count filtered points as misses
+ is_intersecting &= ~outliers
+
+ model.zero_grad()
+ jacobian_normals = model.compute_normals_from_intersection_origin_jacobian(jac, dirs)
+
+ all_intersections .append(intersections .detach()[is_intersecting.detach(), :])
+ all_medial_normals .append(medial_normals .detach()[is_intersecting.detach(), :]) if MEDIAL else None
+ all_jacobian_normals.append(jacobian_normals.detach()[is_intersecting.detach(), :])
+
+ # accumulate metrics
+ with torch.no_grad():
+ n [uid] += dirs.shape[0]
+ n_gt_hits [uid] += gt_hits.sum().item()
+ n_gt_miss [uid] += gt_miss.sum().item()
+ n_gt_missing [uid] += gt_missing.sum().item()
+ p_mse [uid] += (gt_points [gt_hits, :] - intersections[gt_hits, :]).norm(2, dim=-1).pow(2).sum().item()
+ if MEDIAL: s_mse [uid] += (gt_distances[gt_miss] - silhouettes [gt_miss] ) .pow(2).sum().item()
+ if MEDIAL: cossim_med[uid] += (1-F.cosine_similarity(gt_normals[gt_hits, :], medial_normals [gt_hits, :], dim=-1).abs()).sum().item() # to match what pytorch3d does for CD
+ cossim_jac [uid] += (1-F.cosine_similarity(gt_normals[gt_hits, :], jacobian_normals[gt_hits, :], dim=-1).abs()).sum().item() # to match what pytorch3d does for CD
+ not_intersecting = ~is_intersecting
+ TP [uid] += ((gt_hits | gt_missing) & is_intersecting).sum().item() # True Positive
+ FN [uid] += ((gt_hits | gt_missing) & not_intersecting).sum().item() # False Negative
+ FP [uid] += (gt_miss & is_intersecting).sum().item() # False Positive
+ TN [uid] += (gt_miss & not_intersecting).sum().item() # True Negative
+
+ all_intersections = torch.cat(all_intersections, dim=0)
+ all_medial_normals = torch.cat(all_medial_normals, dim=0) if MEDIAL else None
+ all_jacobian_normals = torch.cat(all_jacobian_normals, dim=0)
+
+ hits = sphere_scan_gt.hits # brevity
+ print()
+
+ assert all_intersections.shape[0] >= args.n_cd
+ idx_cd_pred = torch.randperm(all_intersections.shape[0])[:args.n_cd]
+ idx_cd_gt = torch.randperm(hits.sum()) [:args.n_cd]
+
+ print("cd... ", end="")
+ tt = datetime.now()
+ loss_cd, loss_cos_jac = chamfer_distance(
+ x = all_intersections [None, :, :][:, idx_cd_pred, :].detach(),
+ x_normals = all_jacobian_normals [None, :, :][:, idx_cd_pred, :].detach(),
+ y = T(sphere_scan_gt.points [None, hits, :][:, idx_cd_gt, :]),
+ y_normals = T(sphere_scan_gt.normals[None, hits, :][:, idx_cd_gt, :]),
+ batch_reduction = "sum", point_reduction = "sum",
+ )
+ if MEDIAL: _, loss_cos_med = chamfer_distance(
+ x = all_intersections [None, :, :][:, idx_cd_pred, :].detach(),
+ x_normals = all_medial_normals [None, :, :][:, idx_cd_pred, :].detach(),
+ y = T(sphere_scan_gt.points [None, hits, :][:, idx_cd_gt, :]),
+ y_normals = T(sphere_scan_gt.normals[None, hits, :][:, idx_cd_gt, :]),
+ batch_reduction = "sum", point_reduction = "sum",
+ )
+ print(datetime.now() - tt)
+
+ cd_dist [uid] = loss_cd.item()
+ cd_cos_med [uid] = loss_cos_med.item() if MEDIAL else None
+ cd_cos_jac [uid] = loss_cos_jac.item()
+
+ print()
+ model.zero_grad(set_to_none=True)
+ print("Total time:", datetime.now() - t)
+ print("Time per item:", (datetime.now() - t) / len(uids)) if len(uids) > 1 else None
+
+ sum = lambda *xs: builtins .sum (itertools.chain(*(x.values() for x in xs)))
+ mean = lambda *xs: statistics.mean (itertools.chain(*(x.values() for x in xs)))
+ stdev = lambda *xs: statistics.stdev(itertools.chain(*(x.values() for x in xs)))
+ n_cd = args.n_cd
+ P = sum(TP)/(sum(TP, FP))
+ R = sum(TP)/(sum(TP, FN))
+ print(f"{mean(n) = :11.1f} (rays per object)")
+ print(f"{mean(n_gt_hits) = :11.1f} (gt rays hitting per object)")
+ print(f"{mean(n_gt_miss) = :11.1f} (gt rays missing per object)")
+ print(f"{mean(n_gt_missing) = :11.1f} (gt rays unknown per object)")
+ print(f"{mean(n_outliers) = :11.1f} (gt rays unknown per object)") if args.filter_outliers else None
+ print(f"{n_cd = :11.0f} (cd rays per object)")
+ print(f"{mean(n_gt_hits) / mean(n) = :11.8f} (fraction rays hitting per object)")
+ print(f"{mean(n_gt_miss) / mean(n) = :11.8f} (fraction rays missing per object)")
+ print(f"{mean(n_gt_missing)/ mean(n) = :11.8f} (fraction rays unknown per object)")
+ print(f"{mean(n_outliers) / mean(n) = :11.8f} (fraction rays unknown per object)") if args.filter_outliers else None
+ print(f"{sum(TP)/sum(n) = :11.8f} (total ray TP)")
+ print(f"{sum(TN)/sum(n) = :11.8f} (total ray TN)")
+ print(f"{sum(FP)/sum(n) = :11.8f} (total ray FP)")
+ print(f"{sum(FN)/sum(n) = :11.8f} (total ray FN)")
+ print(f"{sum(TP, FN, FP)/sum(n) = :11.8f} (total ray union)")
+ print(f"{sum(TP)/sum(TP, FN, FP) = :11.8f} (total ray IoU)")
+ print(f"{sum(TP)/(sum(TP, FP)) = :11.8f} -> P (total ray precision)")
+ print(f"{sum(TP)/(sum(TP, FN)) = :11.8f} -> R (total ray recall)")
+ print(f"{2*(P*R)/(P+R) = :11.8f} (total ray F-score)")
+ print(f"{sum(p_mse)/sum(n_gt_hits) = :11.8f} (mean ray intersection mean squared error)")
+ print(f"{sum(s_mse)/sum(n_gt_miss) = :11.8f} (mean ray silhoutette mean squared error)")
+ print(f"{sum(cossim_med)/sum(n_gt_hits) = :11.8f} (mean ray medial reduced cosine similarity)") if MEDIAL else None
+ print(f"{sum(cossim_jac)/sum(n_gt_hits) = :11.8f} (mean ray analytical reduced cosine similarity)")
+ print(f"{mean(cd_dist) /n_cd * 1e3 = :11.8f} (mean chamfer distance)")
+ print(f"{mean(cd_cos_med)/n_cd = :11.8f} (mean chamfer reduced medial cossim distance)") if MEDIAL else None
+ print(f"{mean(cd_cos_jac)/n_cd = :11.8f} (mean chamfer reduced analytical cossim distance)")
+ print(f"{stdev(cd_dist) /n_cd * 1e3 = :11.8f} (stdev chamfer distance)") if len(cd_dist) > 1 else None
+ print(f"{stdev(cd_cos_med)/n_cd = :11.8f} (stdev chamfer reduced medial cossim distance)") if len(cd_cos_med) > 1 and MEDIAL else None
+ print(f"{stdev(cd_cos_jac)/n_cd = :11.8f} (stdev chamfer reduced analytical cossim distance)") if len(cd_cos_jac) > 1 else None
+
+ if args.transpose:
+ all_metrics, old_metrics = defaultdict(dict), all_metrics
+ for m, table in old_metrics.items():
+ for uid, vals in table.items():
+ all_metrics[uid][m] = vals
+ all_metrics["_hparams"] = dict(n_cd=args.n_cd)
+ else:
+ all_metrics["n_cd"] = args.n_cd
+
+ if str(args.fname) == "-":
+ print("{", ',\n'.join(
+ f" {json.dumps(k)}: {json.dumps(v)}"
+ for k, v in all_metrics.items()
+ ), "}", sep="\n")
+ else:
+ args.fname.parent.mkdir(parents=True, exist_ok=True)
+ with args.fname.open("w") as f:
+ json.dump(all_metrics, f, indent=2)
+
+ return cli
+
+
+if __name__ == "__main__":
+ mk_cli().run()
diff --git a/experiments/marf.yaml.j2 b/experiments/marf.yaml.j2
new file mode 100755
index 0000000..c12a7f7
--- /dev/null
+++ b/experiments/marf.yaml.j2
@@ -0,0 +1,263 @@
+#!/usr/bin/env -S python ./marf.py module
+{% do require_defined("select", select, 0, "$SLURM_ARRAY_TASK_ID") %}{# requires jinja2.ext.do #}
+{% do require_defined("mode", mode, "single", "ablation", "multi", strict=true, exchaustive=true) %}{# requires jinja2.ext.do #}
+{% set counter = itertools.count(start=0, step=1) %}
+{% set do_condition = mode == "multi" %}
+{% set do_ablation = mode == "ablation" %}
+
+{% set hp_matrix = namespace() %}{# hyper parameter matrix #}
+
+{% set hp_matrix.input_mode = [
+ "both",
+ "perp_foot",
+ "plucker",
+] if do_ablation else [ "both" ] %}
+{% set hp_matrix.output_mode = ["medial_sphere", "orthogonal_plane"] %}{##}
+{% set hp_matrix.output_mode = ["medial_sphere"] %}{##}
+{% set hp_matrix.n_atoms = [16, 1, 4, 8, 32, 64] if do_ablation else [16] %}{##}
+{% set hp_matrix.normal_coeff = [0.25, 0] if do_ablation else [0.25] %}{##}
+{% set hp_matrix.dataset_item = [objname] if objname is defined else (["armadillo", "bunny", "happy_buddha", "dragon", "lucy"] if not do_condition else ["four-legged"]) %}{##}
+{% set hp_matrix.test_val_split_frac = [0.7] %}{##}
+{% set hp_matrix.lr_coeff = [5] %}{##}
+{% set hp_matrix.warmup_epochs = [1] if not do_condition else [0.1] %}{##}
+{% set hp_matrix.improve_miss_grads = [True] %}{##}
+{% set hp_matrix.normalize_ray_dirs = [True] %}{##}
+{% set hp_matrix.intersection_coeff = [2, 0] if do_ablation else [2] %}{##}
+{% set hp_matrix.miss_distance_coeff = [1, 0, 5] if do_ablation else [1] %}{##}
+{% set hp_matrix.relative_out = [False] %}{##}
+{% set hp_matrix.hidden_features = [512] %}{# like deepsdf and prif #}
+{% set hp_matrix.hidden_layers = [8] %}{# like deepsdf, nerf, prif #}
+{% set hp_matrix.nonlinearity = ["leaky_relu"] %}{##}
+{% set hp_matrix.omega = [30] %}{##}
+{% set hp_matrix.normalization = ["layernorm"] %}{##}
+{% set hp_matrix.dropout_percent = [1] %}{##}
+{% set hp_matrix.sphere_grow_reg_coeff = [500, 0, 5000] if do_ablation else [500] %}{##}
+{% set hp_matrix.geom_init = [True, False] if do_ablation else [True] %}{##}
+{% set hp_matrix.loss_inscription = [50, 0, 250] if do_ablation else [50] %}{##}
+{% set hp_matrix.atom_centroid_norm_std_reg_negexp = [0, None] if do_ablation else [0] %}{##}
+{% set hp_matrix.curvature_reg_coeff = [0.2] %}{##}
+{% set hp_matrix.multi_view_reg_coeff = [1, 2] if do_ablation else [1] %}{##}
+{% set hp_matrix.grad_reg = [ "multi_view", "nogradreg" ] if do_ablation else [ "multi_view" ] %}
+
+{#% for hp in cartesian_hparams(hp_matrix) %}{##}
+{% for hp in ablation_hparams(hp_matrix, caartesian_keys=["output_mode", "dataset_item", "nonlinearity", "test_val_split_frac"]) %}
+
+{% if hp.output_mode == "orthogonal_plane"%}
+{% if hp.normal_coeff == 0 %}{% set hp.normal_coeff = 0.25 %}
+{% elif hp.normal_coeff == 0.25 %}{% set hp.normal_coeff = 0 %}
+{% endif %}
+{% if hp.grad_reg == "multi_view" %}{% set hp.grad_reg = "nogradreg" %}
+{% elif hp.grad_reg == "nogradreg" %}{% set hp.grad_reg = "multi_view" %}
+{% endif %}
+{% endif %}
+
+{# filter bad/uninteresting hparam combos #}
+{% if ( hp.nonlinearity != "sine" and hp.omega != 30 )
+ or ( hp.nonlinearity == "sine" and hp.normalization in ("layernorm", "layernorm_na") )
+ or ( hp.multi_view_reg_coeff != 1 and "multi_view" not in hp.grad_reg )
+ or ( "curvature" not in hp.grad_reg and hp.curvature_reg_coeff != 0.2 )
+ or ( hp.output_mode == "orthogonal_plane" and hp.input_mode != "both" )
+ or ( hp.output_mode == "orthogonal_plane" and hp.atom_centroid_norm_std_reg_negexp != 0 )
+ or ( hp.output_mode == "orthogonal_plane" and hp.n_atoms != 16 )
+ or ( hp.output_mode == "orthogonal_plane" and hp.sphere_grow_reg_coeff != 500 )
+ or ( hp.output_mode == "orthogonal_plane" and hp.loss_inscription != 50 )
+ or ( hp.output_mode == "orthogonal_plane" and hp.miss_distance_coeff != 1 )
+ or ( hp.output_mode == "orthogonal_plane" and hp.test_val_split_frac != 0.7 )
+ or ( hp.output_mode == "orthogonal_plane" and hp.lr_coeff != 5 )
+ or ( hp.output_mode == "orthogonal_plane" and not hp.geom_init )
+ or ( hp.output_mode == "orthogonal_plane" and not hp.intersection_coeff )
+%}
+ {% continue %}{# requires jinja2.ext.loopcontrols #}
+{% endif %}
+
+{% set index = next(counter) %}
+{% if select is not defined and index > 0 %}---{% endif %}
+{% if select is not defined or int(select) == index %}
+
+trainer:
+ gradient_clip_val : 1.0
+ max_epochs : 200
+ min_epochs : 200
+ log_every_n_steps : 20
+
+{% if not do_condition %}
+
+StanfordUVDataModule:
+ obj_names : ["{{ hp.dataset_item }}"]
+ step : 4
+ batch_size : 8
+ val_fraction : {{ 1-hp.test_val_split_frac }}
+
+{% else %}{# if do_condition #}
+
+CosegUVDataModule:
+ object_sets : ["{{ hp.dataset_item }}"]
+ step : 4
+ batch_size : 8
+ val_fraction : {{ 1-hp.test_val_split_frac }}
+
+{% endif %}{# if do_condition #}
+
+logging:
+ save_dir : logdir
+ type : tensorboard
+ project : ifield
+
+{% autoescape false %}
+{% do require_defined("experiment_name", experiment_name, "single-shape" if do_condition else "multi-shape", strict=true) %}
+{% set input_mode_abbr = hp.input_mode
+ .replace("plucker", "plkr")
+ .replace("perp_foot", "prpft")
+%}
+{% set output_mode_abbr = hp.output_mode
+ .replace("medial_sphere", "marf")
+ .replace("orthogonal_plane", "prif")
+%}
+experiment_name: experiment-{{ "" if experiment_name is not defined else experiment_name }}
+{#--#}-{{ hp.dataset_item }}
+{#--#}-{{ input_mode_abbr }}2{{ output_mode_abbr }}
+{#--#}
+{%- if hp.output_mode == "medial_sphere" -%}
+ {#--#}-{{ hp.n_atoms }}atom
+ {#--# }-{{ "rel" if hp.relative_out else "norel" }}
+ {#--# }-{{ "e" if hp.improve_miss_grads else "0" }}sqrt
+ {#--#}-{{ int(hp.loss_inscription) if hp.loss_inscription else "no" }}xinscr
+ {#--#}-{{ int(hp.miss_distance_coeff * 10) }}dmiss
+ {#--#}-{{ "geom" if hp.geom_init else "nogeom" }}
+ {#--#}{% if "curvature" in hp.grad_reg %}
+ {#- -#}-{{ int(hp.curvature_reg_coeff*10) }}crv
+ {#--#}{%- endif -%}
+{%- elif hp.output_mode == "orthogonal_plane" -%}
+ {#--#}
+{%- endif -%}
+{#--#}-{{ int(hp.intersection_coeff*10) }}chit
+{#--#}-{{ int(hp.normal_coeff*100) or "no" }}cnrml
+{#--# }-{{ "do" if hp.normalize_ray_dirs else "no" }}raynorm
+{#--#}-{{ hp.hidden_layers }}x{{ hp.hidden_features }}fc
+{#--#}-{{ hp.nonlinearity or "linear" }}
+{#--#}
+{%- if hp.nonlinearity == "sine" -%}
+ {#--#}-{{ hp.omega }}omega
+ {#--#}
+{%- endif -%}
+{%- if hp.output_mode == "medial_sphere" -%}
+ {#--#}-{{ str(hp.atom_centroid_norm_std_reg_negexp).replace(*"-n") if hp.atom_centroid_norm_std_reg_negexp is not none else 'no' }}minatomstdngxp
+ {#--#}-{{ hp.sphere_grow_reg_coeff }}sphgrow
+ {#--#}
+{%- endif -%}
+{#--#}-{{ int(hp.dropout_percent*10) }}mdrop
+{#--#}-{{ hp.normalization or "nonorm" }}
+{#--#}-{{ hp.grad_reg }}
+{#--#}{% if "multi_view" in hp.grad_reg %}
+{#- -#}-{{ int(hp.multi_view_reg_coeff*10) }}dmv
+{#--#}{%- endif -%}
+{#--#}-{{ "concat" if do_condition else "nocond" }}
+{#--#}-{{ int(hp.warmup_epochs*100) }}cwu{{ int(hp.lr_coeff*100) }}clr{{ int(hp.test_val_split_frac*100) }}tvs
+{#--#}-{{ gen_run_uid(4) }} # select with --Oselect={{ index }}
+{#--#}
+{##}
+
+{% endautoescape %}
+IntersectionFieldAutoDecoderModel:
+ _extra: # used for easier introspection with jq
+ dataset_item: {{ hp.dataset_item | to_json}}
+ dataset_test_val_frac: {{ hp.test_val_split_frac }}
+ select: {{ index }}
+
+ input_mode : {{ hp.input_mode }} # in {plucker, perp_foot, both}
+ output_mode : {{ hp.output_mode }} # in {medial_sphere, orthogonal_plane}
+ #latent_features : 256 # int
+ #latent_features : 128 # int
+ latent_features : 16 # int
+ hidden_features : {{ hp.hidden_features }} # int
+ hidden_layers : {{ hp.hidden_layers }} # int
+
+ improve_miss_grads : {{ bool(hp.improve_miss_grads) | to_json }}
+ normalize_ray_dirs : {{ bool(hp.normalize_ray_dirs) | to_json }}
+
+ loss_intersection : {{ hp.intersection_coeff }}
+ loss_intersection_l2 : 0
+ loss_intersection_proj : 0
+ loss_intersection_proj_l2 : 0
+
+ loss_normal_cossim : {{ hp.normal_coeff }} * EaseSin(85, 15)
+ loss_normal_euclid : 0
+ loss_normal_cossim_proj : 0
+ loss_normal_euclid_proj : 0
+
+{% if "multi_view" in hp.grad_reg %}
+ loss_multi_view_reg : 0.1 * {{ hp.multi_view_reg_coeff }} * Linear(50)
+{% else %}
+ loss_multi_view_reg : 0
+{% endif %}
+
+{% if hp.output_mode == "orthogonal_plane" %}
+
+ loss_hit_cross_entropy : 1
+
+{% elif hp.output_mode == "medial_sphere" %}
+
+ loss_hit_nodistance_l1 : 0
+ loss_hit_nodistance_l2 : 100 * {{ hp.miss_distance_coeff }}
+ loss_miss_distance_l1 : 0
+ loss_miss_distance_l2 : 10 * {{ hp.miss_distance_coeff }}
+
+ loss_inscription_hits : {{ 0.4 * hp.loss_inscription }}
+ loss_inscription_miss : 0
+ loss_inscription_hits_l2 : 0
+ loss_inscription_miss_l2 : {{ 6 * hp.loss_inscription }}
+
+ loss_sphere_grow_reg : 1e-6 * {{ hp.sphere_grow_reg_coeff }} # constant
+ loss_atom_centroid_norm_std_reg: (0.09*(1-Linear(40)) + 0.01) * {{ 10**(-hp.atom_centroid_norm_std_reg_negexp) if hp.atom_centroid_norm_std_reg_negexp is not none else 0 }}
+
+{% else %}{#endif hp.output_mode == "medial_sphere" #}
+ THIS IS INVALID YAML
+{% endif %}
+
+ loss_embedding_norm : 0.01**2 * Linear(30, 0.1)
+
+ opt_learning_rate : {{ hp.lr_coeff }} * 10**(-4-0.5*EaseSin(170, 30)) # layernorm
+ opt_warmup : {{ hp.warmup_epochs }}
+ opt_weight_decay : 5e-6 # float
+
+{% if hp.output_mode == "medial_sphere" %}
+
+ # MedialAtomNet:
+ n_atoms : {{ hp.n_atoms }} # int
+ {% if hp.geom_init %}
+ final_init_wrr: [0.05, 0.6, 0.1]
+ {% else %}
+ final_init_wrr: null
+ {% endif %}
+
+{% endif %}
+
+
+ # FCBlock:
+ normalization : {{ hp.normalization or "null" }} # in {null, layernorm, layernorm_na, weightnorm}
+ nonlinearity : {{ hp.nonlinearity or "null" }} # in {null, relu, leaky_relu, silu, softplus, elu, selu, sine, sigmoid, tanh }
+ {% set middle = 1 + hp.hidden_layers // 2 + (hp.hidden_layers % 2) %}{##}
+ concat_skipped_layers : [{{ middle }}, -1]
+{% if do_condition %}
+ concat_conditioned_layers : [0, {{ middle }}]
+{% else %}
+ concat_conditioned_layers : []
+{% endif %}
+
+ # FCLayer:
+ negative_slope : 0.01 # float
+ omega_0 : {{ hp.omega }} # float
+ residual_mode : null # in {null, identity}
+
+{% endif %}{# -Oselect #}
+
+
+{% endfor %}
+
+
+{% set index = next(counter) %}
+# number of possible -Oselect: {{ index }}, from 0 to {{ index-1 }}
+# local: for select in {0..{{ index-1 }}}; do python ... -Omode={{ mode }} -Oselect=$select ... ; done
+# local: for select in {0..{{ index-1 }}}; do python -O {{ argv[0] }} model marf.yaml.j2 -Omode={{ mode }} -Oselect=$select -Oexperiment_name='{{ experiment_name }}' fit --accelerator gpu ; done
+# slurm: sbatch --array=0-{{ index-1 }} runcommand.slurm python ... -Omode={{ mode }} -Oselect=\$SLURM_ARRAY_TASK_ID ...
+# slurm: sbatch --array=0-{{ index-1 }} runcommand.slurm python -O {{ argv[0] }} model marf.yaml.j2 -Omode={{ mode }} -Oselect=\$SLURM_ARRAY_TASK_ID -Oexperiment_name='{{ experiment_name }}' fit --accelerator gpu --devices -1 --strategy ddp
diff --git a/experiments/summary.py b/experiments/summary.py
new file mode 100755
index 0000000..7cbaae8
--- /dev/null
+++ b/experiments/summary.py
@@ -0,0 +1,849 @@
+#!/usr/bin/env python
+from concurrent.futures import ThreadPoolExecutor, Future, ProcessPoolExecutor
+from functools import partial
+from more_itertools import first, last, tail
+from munch import Munch, DefaultMunch, munchify, unmunchify
+from pathlib import Path
+from statistics import mean, StatisticsError
+from mpl_toolkits.axes_grid1 import make_axes_locatable
+from typing import Iterable, Optional, Literal
+from math import isnan
+import json
+import stat
+import matplotlib
+import matplotlib.colors as mcolors
+import matplotlib.pyplot as plt
+import os, os.path
+import re
+import shlex
+import time
+import itertools
+import shutil
+import subprocess
+import sys
+import traceback
+import typer
+import warnings
+import yaml
+import tempfile
+
+EXPERIMENTS = Path(__file__).resolve()
+LOGDIR = EXPERIMENTS / "logdir"
+TENSORBOARD = LOGDIR / "tensorboard"
+SLURM_LOGS = LOGDIR / "slurm_logs"
+CACHED_SUMMARIES = LOGDIR / "cached_summaries"
+COMPUTED_SCORES = LOGDIR / "computed_scores"
+
+MISSING = object()
+
+class SafeLoaderIgnoreUnknown(yaml.SafeLoader):
+ def ignore_unknown(self, node):
+ return None
+SafeLoaderIgnoreUnknown.add_constructor(None, SafeLoaderIgnoreUnknown.ignore_unknown)
+
+def camel_to_snake_case(text: str, sep: str = "_", join_abbreviations: bool = False) -> str:
+ parts = (
+ part.lower()
+ for part in re.split(r'(?=[A-Z])', text)
+ if part
+ )
+ if join_abbreviations: # this operation is not reversible
+ parts = list(parts)
+ if len(parts) > 1:
+ for i, (a, b) in list(enumerate(zip(parts[:-1], parts[1:])))[::-1]:
+ if len(a) == len(b) == 1:
+ parts[i] = parts[i] + parts.pop(i+1)
+ return sep.join(parts)
+
+def flatten_dict(data: dict, key_mapper: callable = lambda x: x) -> dict:
+ if not any(isinstance(val, dict) for val in data.values()):
+ return data
+ else:
+ return {
+ k: v
+ for k, v in data.items()
+ if not isinstance(v, dict)
+ } | {
+ f"{key_mapper(p)}/{k}":v
+ for p,d in data.items()
+ if isinstance(d, dict)
+ for k,v in d.items()
+ }
+
+def parse_jsonl(data: str) -> Iterable[dict]:
+ yield from map(json.loads, (line for line in data.splitlines() if line.strip()))
+
+def read_jsonl(path: Path) -> Iterable[dict]:
+ with path.open("r") as f:
+ data = f.read()
+ yield from parse_jsonl(data)
+
+def get_experiment_paths(filter: str | None, assert_dumped = False) -> Iterable[Path]:
+ for path in TENSORBOARD.iterdir():
+ if filter is not None and not re.search(filter, path.name): continue
+ if not path.is_dir(): continue
+
+ if not (path / "hparams.yaml").is_file():
+ warnings.warn(f"Missing hparams: {path}")
+ continue
+ if not any(path.glob("events.out.tfevents.*")):
+ warnings.warn(f"Missing tfevents: {path}")
+ continue
+
+ if __debug__ and assert_dumped:
+ assert (path / "scalars/epoch.json").is_file(), path
+ assert (path / "scalars/IntersectionFieldAutoDecoderModel.validation_step/loss.json").is_file(), path
+ assert (path / "scalars/IntersectionFieldAutoDecoderModel.training_step/loss.json").is_file(), path
+
+ yield path
+
+def dump_pl_tensorboard_hparams(experiment: Path):
+ with (experiment / "hparams.yaml").open() as f:
+ hparams = yaml.load(f, Loader=SafeLoaderIgnoreUnknown)
+
+ shebang = None
+ with (experiment / "config.yaml").open("w") as f:
+ raw_yaml = hparams.get('_pickled_cli_args', {}).get('_raw_yaml', "").replace("\n\r", "\n")
+ if raw_yaml.startswith("#!"): # preserve shebang
+ shebang, _, raw_yaml = raw_yaml.partition("\n")
+ f.write(f"{shebang}\n")
+ f.write(f"# {' '.join(map(shlex.quote, hparams.get('_pickled_cli_args', {}).get('sys_argv', ['None'])))}\n\n")
+ f.write(raw_yaml)
+ if shebang is not None:
+ os.chmod(experiment / "config.yaml", (experiment / "config.yaml").stat().st_mode | stat.S_IXUSR)
+ print(experiment / "config.yaml", "written!", file=sys.stderr)
+
+ with (experiment / "environ.yaml").open("w") as f:
+ yaml.safe_dump(hparams.get('_pickled_cli_args', {}).get('host', {}).get('environ'), f)
+ print(experiment / "environ.yaml", "written!", file=sys.stderr)
+
+ with (experiment / "repo.patch").open("w") as f:
+ f.write(hparams.get('_pickled_cli_args', {}).get('host', {}).get('vcs', "None"))
+ print(experiment / "repo.patch", "written!", file=sys.stderr)
+
+def dump_simple_tf_events_to_jsonl(output_dir: Path, *tf_files: Path):
+ from google.protobuf.json_format import MessageToDict
+ import tensorboard.backend.event_processing.event_accumulator
+ s, l = {}, [] # reused sentinels
+
+ #resource.setrlimit(resource.RLIMIT_NOFILE, (2**16,-1))
+ file_handles = {}
+ try:
+ for tffile in tf_files:
+ loader = tensorboard.backend.event_processing.event_file_loader.LegacyEventFileLoader(str(tffile))
+ for event in loader.Load():
+ for summary in MessageToDict(event).get("summary", s).get("value", l):
+ if "simpleValue" in summary:
+ tag = summary["tag"]
+ if tag not in file_handles:
+ fname = output_dir / f"{tag}.json"
+ print(f"Opening {str(fname)!r}...", file=sys.stderr)
+ fname.parent.mkdir(parents=True, exist_ok=True)
+ file_handles[tag] = fname.open("w") # ("a")
+ val = summary["simpleValue"]
+ data = json.dumps({
+ "step" : event.step,
+ "value" : float(val) if isinstance(val, str) else val,
+ "wall_time" : event.wall_time,
+ })
+ file_handles[tag].write(f"{data}\n")
+ finally:
+ if file_handles:
+ print("Closing json files...", file=sys.stderr)
+ for k, v in file_handles.items():
+ v.close()
+
+
+NO_FILTER = {
+ "__uid",
+ "_minutes",
+ "_epochs",
+ "_hp_nonlinearity",
+ "_val_uloss_intersection",
+ "_val_uloss_normal_cossim",
+ "_val_uloss_intersection",
+}
+def filter_jsonl_columns(data: Iterable[dict | None], no_filter=NO_FILTER) -> list[dict]:
+ def merge_siren_omega(data: dict) -> dict:
+ return {
+ key: (
+ f"{val}-{data.get('hp_omega_0', 'ERROR')}"
+ if (key.removeprefix("_"), val) == ("hp_nonlinearity", "sine") else
+ val
+ )
+ for key, val in data.items()
+ if key != "hp_omega_0"
+ }
+
+ def remove_uninteresting_cols(rows: list[dict]) -> Iterable[dict]:
+ unique_vals = {}
+ def register_val(key, val):
+ unique_vals.setdefault(key, set()).add(repr(val))
+ return val
+
+ whitelisted = {
+ key
+ for row in rows
+ for key, val in row.items()
+ if register_val(key, val) and val not in ("None", "0", "0.0")
+ }
+ for key in unique_vals:
+ for row in rows:
+ if key not in row:
+ unique_vals[key].add(MISSING)
+ for key, vals in unique_vals.items():
+ if key not in whitelisted: continue
+ if len(vals) == 1:
+ whitelisted.remove(key)
+
+ whitelisted.update(no_filter)
+
+ yield from (
+ {
+ key: val
+ for key, val in row.items()
+ if key in whitelisted
+ }
+ for row in rows
+ )
+
+ def pessemize_types(rows: list[dict]) -> Iterable[dict]:
+ types = {}
+ order = (str, float, int, bool, tuple, type(None))
+ for row in rows:
+ for key, val in row.items():
+ if isinstance(val, list): val = tuple(val)
+ assert type(val) in order, (type(val), val)
+ index = order.index(type(val))
+ types[key] = min(types.get(key, 999), index)
+
+ yield from (
+ {
+ key: order[types[key]](val) if val is not None else None
+ for key, val in row.items()
+ }
+ for row in rows
+ )
+
+ data = (row for row in data if row is not None)
+ data = map(partial(flatten_dict, key_mapper=camel_to_snake_case), data)
+ data = map(merge_siren_omega, data)
+ data = remove_uninteresting_cols(list(data))
+ data = pessemize_types(list(data))
+
+ return data
+
+PlotMode = Literal["stackplot", "lineplot"]
+
+def plot_losses(experiments: list[Path], mode: PlotMode, write: bool = False, dump: bool = False, training: bool = False, unscaled: bool = False, force=True):
+ def get_losses(experiment: Path, training: bool = True, unscaled: bool = False) -> Iterable[Path]:
+ if not training and unscaled:
+ return experiment.glob("scalars/*.validation_step/unscaled_loss_*.json")
+ elif not training and not unscaled:
+ return experiment.glob("scalars/*.validation_step/loss_*.json")
+ elif training and unscaled:
+ return experiment.glob("scalars/*.training_step/unscaled_loss_*.json")
+ elif training and not unscaled:
+ return experiment.glob("scalars/*.training_step/loss_*.json")
+
+ print("Mapping colors...")
+ configurations = [
+ dict(unscaled=unscaled, training=training),
+ ] if not write else [
+ dict(unscaled=False, training=False),
+ dict(unscaled=False, training=True),
+ dict(unscaled=True, training=False),
+ dict(unscaled=True, training=True),
+ ]
+ legends = set(
+ f"""{
+ loss.parent.name.split(".", 1)[0]
+ }.{
+ loss.name.removesuffix(loss.suffix).removeprefix("unscaled_")
+ }"""
+ for experiment in experiments
+ for kw in configurations
+ for loss in get_losses(experiment, **kw)
+ )
+ colormap = dict(zip(
+ sorted(legends),
+ itertools.cycle(mcolors.TABLEAU_COLORS),
+ ))
+
+ def mkplot(experiment: Path, training: bool = True, unscaled: bool = False) -> tuple[bool, str]:
+ label = f"{'unscaled' if unscaled else 'scaled'} {'training' if training else 'validation'}"
+ if write:
+ old_savefig_fname = experiment / f"{label.replace(' ', '-')}-{mode}.png"
+ savefig_fname = experiment / "plots" / f"{label.replace(' ', '-')}-{mode}.png"
+ savefig_fname.parent.mkdir(exist_ok=True, parents=True)
+ if old_savefig_fname.is_file():
+ old_savefig_fname.rename(savefig_fname)
+ if savefig_fname.is_file() and not force:
+ return True, "savefig_fname already exists"
+
+ # Get and sort data
+ losses = {}
+ for loss in get_losses(experiment, training=training, unscaled=unscaled):
+ model = loss.parent.name.split(".", 1)[0]
+ name = loss.name.removesuffix(loss.suffix).removeprefix("unscaled_")
+ losses[f"{model}.{name}"] = (loss, list(read_jsonl(loss)))
+ losses = dict(sorted(losses.items())) # sort keys
+ if not losses:
+ return True, "no losses"
+
+ # unwrap
+ steps = [i["step"] for i in first(losses.values())[1]]
+ values = [
+ [i["value"] if not isnan(i["value"]) else 0 for i in data]
+ for name, (scalar, data) in losses.items()
+ ]
+
+ # normalize
+ if mode == "stackplot":
+ totals = list(map(sum, zip(*values)))
+ values = [
+ [i / t for i, t in zip(data, totals)]
+ for data in values
+ ]
+
+ print(experiment.name, label)
+ fig, ax = plt.subplots(figsize=(16, 12))
+
+ if mode == "stackplot":
+ ax.stackplot(steps, values,
+ colors = list(map(colormap.__getitem__, losses.keys())),
+ labels = list(
+ label.split(".", 1)[1].removeprefix("loss_")
+ for label in losses.keys()
+ ),
+ )
+ ax.set_xlim(0, steps[-1])
+ ax.set_ylim(0, 1)
+ ax.invert_yaxis()
+
+ elif mode == "lineplot":
+ for data, color, label in zip(
+ values,
+ map(colormap.__getitem__, losses.keys()),
+ list(losses.keys()),
+ ):
+ ax.plot(steps, data,
+ color = color,
+ label = label,
+ )
+ ax.set_xlim(0, steps[-1])
+
+ else:
+ raise ValueError(f"{mode=}")
+
+ ax.legend()
+ ax.set_title(f"{label} loss\n{experiment.name}")
+ ax.set_xlabel("Step")
+ ax.set_ylabel("loss%")
+
+ if mode == "stackplot":
+ ax2 = make_axes_locatable(ax).append_axes("bottom", 0.8, pad=0.05, sharex=ax)
+ ax2.stackplot( steps, totals )
+
+ for tl in ax.get_xticklabels(): tl.set_visible(False)
+
+ fig.tight_layout()
+
+ if write:
+ fig.savefig(savefig_fname, dpi=300)
+ print(savefig_fname)
+ plt.close(fig)
+
+ return False, None
+
+ print("Plotting...")
+ if write:
+ matplotlib.use('agg') # fixes "WARNING: QApplication was not created in the main() thread."
+ any_error = False
+ if write:
+ with ThreadPoolExecutor(max_workers=None) as pool:
+ futures = [
+ (experiment, pool.submit(mkplot, experiment, **kw))
+ for experiment in experiments
+ for kw in configurations
+ ]
+ else:
+ def mkfuture(item):
+ f = Future()
+ f.set_result(item)
+ return f
+ futures = [
+ (experiment, mkfuture(mkplot(experiment, **kw)))
+ for experiment in experiments
+ for kw in configurations
+ ]
+
+ for experiment, future in futures:
+ try:
+ err, msg = future.result()
+ except Exception:
+ traceback.print_exc(file=sys.stderr)
+ any_error = True
+ continue
+ if err:
+ print(f"{msg}: {experiment.name}")
+ any_error = True
+ continue
+
+ if not any_error and not write: # show in main thread
+ plt.show()
+ elif not write:
+ print("There were errors, will not show figure...", file=sys.stderr)
+
+
+
+# =========
+
+app = typer.Typer(no_args_is_help=True, add_completion=False)
+
+@app.command(help="Dump simple tensorboard events to json and extract some pytorch lightning hparams")
+def tf_dump(tfevent_files: list[Path], j: int = typer.Option(1, "-j"), force: bool = False):
+ # expand to all tfevents files (there may be more than one)
+ tfevent_files = sorted(set([
+ tffile
+ for tffile in tfevent_files
+ if tffile.name.startswith("events.out.tfevents.")
+ ] + [
+ tffile
+ for experiment_dir in tfevent_files
+ if experiment_dir.is_dir()
+ for tffile in experiment_dir.glob("events.out.tfevents.*")
+ ] + [
+ tffile
+ for hparam_file in tfevent_files
+ if hparam_file.name in ("hparams.yaml", "config.yaml")
+ for tffile in hparam_file.parent.glob("events.out.tfevents.*")
+ ]))
+
+ # filter already dumped
+ if not force:
+ tfevent_files = [
+ tffile
+ for tffile in tfevent_files
+ if not (
+ (tffile.parent / "scalars/epoch.json").is_file()
+ and
+ tffile.stat().st_mtime < (tffile.parent / "scalars/epoch.json").stat().st_mtime
+ )
+ ]
+
+ if not tfevent_files:
+ raise typer.BadParameter("Nothing to be done, consider --force")
+
+ jobs = {}
+ for tffile in tfevent_files:
+ if not tffile.is_file():
+ print("ERROR: file not found:", tffile, file=sys.stderr)
+ continue
+ output_dir = tffile.parent / "scalars"
+ jobs.setdefault(output_dir, []).append(tffile)
+ with ProcessPoolExecutor() as p:
+ for experiment in set(tffile.parent for tffile in tfevent_files):
+ p.submit(dump_pl_tensorboard_hparams, experiment)
+ for output_dir, tffiles in jobs.items():
+ p.submit(dump_simple_tf_events_to_jsonl, output_dir, *tffiles)
+
+@app.command(help="Propose experiment regexes")
+def propose(cmd: str = typer.Argument("summary"), null: bool = False):
+ def get():
+ for i in TENSORBOARD.iterdir():
+ if not i.is_dir(): continue
+ if not (i / "hparams.yaml").is_file(): continue
+ prefix, name, *hparams, year, month, day, hhmm, uid = i.name.split("-")
+ yield f"{name}.*-{year}-{month}-{day}"
+ proposals = sorted(set(get()), key=lambda x: x.split(".*-", 1)[1])
+ print("\n".join(
+ f"{'>/dev/null ' if null else ''}{sys.argv[0]} {cmd or 'summary'} {shlex.quote(i)}"
+ for i in proposals
+ ))
+
+@app.command("list", help="List used experiment regexes")
+def list_cached_summaries(cmd: str = typer.Argument("summary")):
+ if not CACHED_SUMMARIES.is_dir():
+ cached = []
+ else:
+ cached = [
+ i.name.removesuffix(".jsonl")
+ for i in CACHED_SUMMARIES.iterdir()
+ if i.suffix == ".jsonl"
+ if i.is_file() and i.stat().st_size
+ ]
+ def order(key: str) -> list[str]:
+ return re.sub(r'[^0-9\-]', '', key.split(".*")[-1]).strip("-").split("-") + [key]
+
+ print("\n".join(
+ f"{sys.argv[0]} {cmd or 'summary'} {shlex.quote(i)}"
+ for i in sorted(cached, key=order)
+ ))
+
+@app.command(help="Precompute the summary of a experiment regex")
+def compute_summary(filter: str, force: bool = False, dump: bool = False, no_cache: bool = False):
+ cache = CACHED_SUMMARIES / f"{filter}.jsonl"
+ if cache.is_file() and cache.stat().st_size:
+ if not force:
+ raise FileExistsError(cache)
+
+ def mk_summary(path: Path) -> dict | None:
+ cache = path / "train_summary.json"
+ if cache.is_file() and cache.stat().st_size and cache.stat().st_mtime > (path/"scalars/epoch.json").stat().st_mtime:
+ with cache.open() as f:
+ return json.load(f)
+ else:
+ with (path / "hparams.yaml").open() as f:
+ hparams = munchify(yaml.load(f, Loader=SafeLoaderIgnoreUnknown), factory=partial(DefaultMunch, None))
+ config = hparams._pickled_cli_args._raw_yaml
+ config = munchify(yaml.load(config, Loader=SafeLoaderIgnoreUnknown), factory=partial(DefaultMunch, None))
+
+ try:
+ train_loss = list(read_jsonl(path / "scalars/IntersectionFieldAutoDecoderModel.training_step/loss.json"))
+ val_loss = list(read_jsonl(path / "scalars/IntersectionFieldAutoDecoderModel.validation_step/loss.json"))
+ except:
+ traceback.print_exc(file=sys.stderr)
+ return None
+
+ out = Munch()
+ out.uid = path.name.rsplit("-", 1)[-1]
+ out.name = path.name
+ out.date = "-".join(path.name.split("-")[-5:-1])
+ out.epochs = int(last(read_jsonl(path / "scalars/epoch.json"))["value"])
+ out.steps = val_loss[-1]["step"]
+ out.gpu = hparams._pickled_cli_args.host.gpus[1][1]
+
+ if val_loss[-1]["wall_time"] - val_loss[0]["wall_time"] > 0:
+ out.batches_per_second = val_loss[-1]["step"] / (val_loss[-1]["wall_time"] - val_loss[0]["wall_time"])
+ else:
+ out.batches_per_second = 0
+
+ out.minutes = (val_loss[-1]["wall_time"] - train_loss[0]["wall_time"]) / 60
+
+ if (path / "scalars/PsutilMonitor/gpu.00.memory.used.json").is_file():
+ max(i["value"] for i in read_jsonl(path / "scalars/PsutilMonitor/gpu.00.memory.used.json"))
+
+ for metric_path in (path / "scalars/IntersectionFieldAutoDecoderModel.validation_step").glob("*.json"):
+ if not metric_path.is_file() or not metric_path.stat().st_size: continue
+
+ metric_name = metric_path.name.removesuffix(".json")
+ metric_data = read_jsonl(metric_path)
+ try:
+ out[f"val_{metric_name}"] = mean(i["value"] for i in tail(5, metric_data))
+ except StatisticsError:
+ out[f"val_{metric_name}"] = float('nan')
+
+ for metric_path in (path / "scalars/IntersectionFieldAutoDecoderModel.training_step").glob("*.json"):
+ if not any(i in metric_path.name for i in ("miss_radius_grad", "sphere_center_grad", "loss_tangential_reg", "multi_view")): continue
+ if not metric_path.is_file() or not metric_path.stat().st_size: continue
+
+ metric_name = metric_path.name.removesuffix(".json")
+ metric_data = read_jsonl(metric_path)
+ try:
+ out[f"train_{metric_name}"] = mean(i["value"] for i in tail(5, metric_data))
+ except StatisticsError:
+ out[f"train_{metric_name}"] = float('nan')
+
+ out.hostname = hparams._pickled_cli_args.host.hostname
+
+ for key, val in config.IntersectionFieldAutoDecoderModel.items():
+ if isinstance(val, dict):
+ out.update({f"hp_{key}_{k}": v for k, v in val.items()})
+ elif isinstance(val, float | int | str | bool | None):
+ out[f"hp_{key}"] = val
+
+ with cache.open("w") as f:
+ json.dump(unmunchify(out), f)
+
+ return dict(out)
+
+ experiments = list(get_experiment_paths(filter, assert_dumped=not dump))
+ if not experiments:
+ raise typer.BadParameter("No matching experiment")
+ if dump:
+ try:
+ tf_dump(experiments) # force=force_dump)
+ except typer.BadParameter:
+ pass
+
+ # does literally nothing, thanks GIL
+ with ThreadPoolExecutor() as p:
+ results = list(p.map(mk_summary, experiments))
+
+ if any(result is None for result in results):
+ if all(result is None for result in results):
+ print("No summary succeeded", file=sys.stderr)
+ raise typer.Exit(exit_code=1)
+ warnings.warn("Some summaries failed:\n" + "\n".join(
+ str(experiment)
+ for result, experiment in zip(results, experiments)
+ if result is None
+ ))
+
+ summaries = "\n".join( map(json.dumps, results) )
+ if not no_cache:
+ cache.parent.mkdir(parents=True, exist_ok=True)
+ with cache.open("w") as f:
+ f.write(summaries)
+ return summaries
+
+@app.command(help="Show the summary of a experiment regex, precompute it if needed")
+def summary(filter: Optional[str] = typer.Argument(None), force: bool = False, dump: bool = False, all: bool = False):
+ if filter is None:
+ return list_cached_summaries("summary")
+
+ def key_mangler(key: str) -> str:
+ for pattern, sub in (
+ (r'^val_unscaled_loss_', r'val_uloss_'),
+ (r'^train_unscaled_loss_', r'train_uloss_'),
+ (r'^val_loss_', r'val_sloss_'),
+ (r'^train_loss_', r'train_sloss_'),
+ ):
+ key = re.sub(pattern, sub, key)
+
+ return key
+
+ cache = CACHED_SUMMARIES / f"{filter}.jsonl"
+ if force or not (cache.is_file() and cache.stat().st_size):
+ compute_summary(filter, force=force, dump=dump)
+ assert cache.is_file() and cache.stat().st_size, (cache, cache.stat())
+
+ if os.isatty(0) and os.isatty(1) and shutil.which("vd"):
+ rows = read_jsonl(cache)
+ rows = ({key_mangler(k): v for k, v in row.items()} if row is not None else None for row in rows)
+ if not all:
+ rows = filter_jsonl_columns(rows)
+ rows = ({k: v for k, v in row.items() if not k.startswith(("val_sloss_", "train_sloss_"))} for row in rows)
+ data = "\n".join(map(json.dumps, rows))
+ subprocess.run(["vd",
+ #"--play", EXPERIMENTS / "set-key-columns.vd",
+ "-f", "jsonl"
+ ], input=data, text=True, check=True)
+ else:
+ with cache.open() as f:
+ print(f.read())
+
+@app.command(help="Filter uninteresting keys from jsonl stdin")
+def filter_cols():
+ rows = map(json.loads, (line for line in sys.stdin.readlines() if line.strip()))
+ rows = filter_jsonl_columns(rows)
+ print(*map(json.dumps, rows), sep="\n")
+
+@app.command(help="Run a command for each experiment matched by experiment regex")
+def exec(filter: str, cmd: list[str], j: int = typer.Option(1, "-j"), dumped: bool = False, undumped: bool = False):
+ # inspired by fd / gnu parallel
+ def populate_cmd(experiment: Path, cmd: Iterable[str]) -> Iterable[str]:
+ any = False
+ for i in cmd:
+ if i == "{}":
+ any = True
+ yield str(experiment / "hparams.yaml")
+ elif i == "{//}":
+ any = True
+ yield str(experiment)
+ else:
+ yield i
+ if not any:
+ yield str(experiment / "hparams.yaml")
+
+ with ThreadPoolExecutor(max_workers=j or None) as p:
+ results = p.map(subprocess.run, (
+ list(populate_cmd(experiment, cmd))
+ for experiment in get_experiment_paths(filter)
+ if not dumped or (experiment / "scalars/epoch.json").is_file()
+ if not undumped or not (experiment / "scalars/epoch.json").is_file()
+ ))
+
+ if any(i.returncode for i in results):
+ return typer.Exit(1)
+
+@app.command(help="Show stackplot of experiment loss")
+def stackplot(filter: str, write: bool = False, dump: bool = False, training: bool = False, unscaled: bool = False, force: bool = False):
+ experiments = list(get_experiment_paths(filter, assert_dumped=not dump))
+ if not experiments:
+ raise typer.BadParameter("No match")
+ if dump:
+ try:
+ tf_dump(experiments)
+ except typer.BadParameter:
+ pass
+
+ plot_losses(experiments,
+ mode = "stackplot",
+ write = write,
+ dump = dump,
+ training = training,
+ unscaled = unscaled,
+ force = force,
+ )
+
+@app.command(help="Show stackplot of experiment loss")
+def lineplot(filter: str, write: bool = False, dump: bool = False, training: bool = False, unscaled: bool = False, force: bool = False):
+ experiments = list(get_experiment_paths(filter, assert_dumped=not dump))
+ if not experiments:
+ raise typer.BadParameter("No match")
+ if dump:
+ try:
+ tf_dump(experiments)
+ except typer.BadParameter:
+ pass
+
+ plot_losses(experiments,
+ mode = "lineplot",
+ write = write,
+ dump = dump,
+ training = training,
+ unscaled = unscaled,
+ force = force,
+ )
+
+@app.command(help="Open tensorboard for the experiments matching the regex")
+def tensorboard(filter: Optional[str] = typer.Argument(None), watch: bool = False):
+ if filter is None:
+ return list_cached_summaries("tensorboard")
+ experiments = list(get_experiment_paths(filter, assert_dumped=False))
+ if not experiments:
+ raise typer.BadParameter("No match")
+
+ with tempfile.TemporaryDirectory(suffix=f"ifield-{filter}") as d:
+ treefarm = Path(d)
+ with ThreadPoolExecutor(max_workers=2) as p:
+ for experiment in experiments:
+ (treefarm / experiment.name).symlink_to(experiment)
+
+ cmd = ["tensorboard", "--logdir", d]
+ print("+", *map(shlex.quote, cmd), file=sys.stderr)
+ tensorboard = p.submit(subprocess.run, cmd, check=True)
+ if not watch:
+ tensorboard.result()
+
+ else:
+ all_experiments = set(get_experiment_paths(None, assert_dumped=False))
+ while not tensorboard.done():
+ time.sleep(10)
+ new_experiments = set(get_experiment_paths(None, assert_dumped=False)) - all_experiments
+ if new_experiments:
+ for experiment in new_experiments:
+ print(f"Adding {experiment.name!r}...", file=sys.stderr)
+ (treefarm / experiment.name).symlink_to(experiment)
+ all_experiments.update(new_experiments)
+
+@app.command(help="Compute evaluation metrics")
+def metrics(filter: Optional[str] = typer.Argument(None), dump: bool = False, dry: bool = False, prefix: Optional[str] = typer.Option(None), derive: bool = False, each: bool = False, no_total: bool = False):
+ if filter is None:
+ return list_cached_summaries("metrics --derive")
+ experiments = list(get_experiment_paths(filter, assert_dumped=False))
+ if not experiments:
+ raise typer.BadParameter("No match")
+ if dump:
+ try:
+ tf_dump(experiments)
+ except typer.BadParameter:
+ pass
+
+ def run(*cmd):
+ if prefix is not None:
+ cmd = [*shlex.split(prefix), *cmd]
+ if dry:
+ print(*map(shlex.quote, map(str, cmd)))
+ else:
+ print("+", *map(shlex.quote, map(str, cmd)))
+ subprocess.run(cmd)
+
+ for experiment in experiments:
+ if no_total: continue
+ if not (experiment / "compute-scores/metrics.json").is_file():
+ run(
+ "python", "./marf.py", "module", "--best", experiment / "hparams.yaml",
+ "compute-scores", experiment / "compute-scores/metrics.json",
+ "--transpose",
+ )
+ if not (experiment / "compute-scores/metrics-last.json").is_file():
+ run(
+ "python", "./marf.py", "module", "--last", experiment / "hparams.yaml",
+ "compute-scores", experiment / "compute-scores/metrics-last.json",
+ "--transpose",
+ )
+ if "2prif-" not in experiment.name: continue
+ if not (experiment / "compute-scores/metrics-sans_outliers.json").is_file():
+ run(
+ "python", "./marf.py", "module", "--best", experiment / "hparams.yaml",
+ "compute-scores", experiment / "compute-scores/metrics-sans_outliers.json",
+ "--transpose", "--filter-outliers"
+ )
+ if not (experiment / "compute-scores/metrics-last-sans_outliers.json").is_file():
+ run(
+ "python", "./marf.py", "module", "--last", experiment / "hparams.yaml",
+ "compute-scores", experiment / "compute-scores/metrics-last-sans_outliers.json",
+ "--transpose", "--filter-outliers"
+ )
+
+ if dry: return
+ if prefix is not None:
+ print("prefix was used, assuming a job scheduler was used, will not print scores.", file=sys.stderr)
+ return
+
+ metrics = [
+ *(experiment / "compute-scores/metrics.json" for experiment in experiments),
+ *(experiment / "compute-scores/metrics-last.json" for experiment in experiments),
+ *(experiment / "compute-scores/metrics-sans_outliers.json" for experiment in experiments if "2prif-" in experiment.name),
+ *(experiment / "compute-scores/metrics-last-sans_outliers.json" for experiment in experiments if "2prif-" in experiment.name),
+ ]
+ if not no_total:
+ assert all(metric.exists() for metric in metrics)
+ else:
+ metrics = (metric for metric in metrics if metric.exists())
+
+ out = []
+ for metric in metrics:
+ experiment = metric.parent.parent.name
+ is_last = metric.name in ("metrics-last.json", "metrics-last-sans_outliers.json")
+ with metric.open() as f:
+ data = json.load(f)
+
+ if derive:
+ derived = {}
+ objs = [i for i in data.keys() if i != "_hparams"]
+ for obj in (objs if each else []) + [None]:
+ if obj is None:
+ d = DefaultMunch(0)
+ for obj in objs:
+ for k, v in data[obj].items():
+ d[k] += v
+ obj = "_all_"
+ n_cd = data["_hparams"]["n_cd"] * len(objs)
+ n_emd = data["_hparams"]["n_emd"] * len(objs)
+ else:
+ d = munchify(data[obj])
+ n_cd = data["_hparams"]["n_cd"]
+ n_emd = data["_hparams"]["n_emd"]
+
+ precision = d.TP / (d.TP + d.FP)
+ recall = d.TP / (d.TP + d.FN)
+ derived[obj] = dict(
+ filtered = d.n_outliers / d.n if "n_outliers" in d else None,
+ iou = d.TP / (d.TP + d.FN + d.FP),
+ precision = precision,
+ recall = recall,
+ f_score = 2 * (precision * recall) / (precision + recall),
+ cd = d.cd_dist / n_cd,
+ emd = d.emd / n_emd,
+ cos_med = 1 - (d.cd_cos_med / n_cd) if "cd_cos_med" in d else None,
+ cos_jac = 1 - (d.cd_cos_jac / n_cd),
+ )
+ data = derived if each else derived["_all_"]
+
+ data["uid"] = experiment.rsplit("-", 1)[-1]
+ data["experiment_name"] = experiment
+ data["is_last"] = is_last
+
+ out.append(json.dumps(data))
+
+ if derive and not each and os.isatty(0) and os.isatty(1) and shutil.which("vd"):
+ subprocess.run(["vd", "-f", "jsonl"], input="\n".join(out), text=True, check=True)
+ else:
+ print("\n".join(out))
+
+if __name__ == "__main__":
+ app()
diff --git a/figures/nn-architecture.svg b/figures/nn-architecture.svg
new file mode 100644
index 0000000..8112a36
--- /dev/null
+++ b/figures/nn-architecture.svg
@@ -0,0 +1,822 @@
+
+
+
diff --git a/ifield/__init__.py b/ifield/__init__.py
new file mode 100644
index 0000000..0dc46c4
--- /dev/null
+++ b/ifield/__init__.py
@@ -0,0 +1,57 @@
+def setup_print_hooks():
+ import os
+ if not os.environ.get("IFIELD_PRETTY_TRACEBACK", None):
+ return
+
+ from rich.traceback import install
+ from rich.console import Console
+ import warnings, sys
+
+ if not os.isatty(2):
+ # https://github.com/Textualize/rich/issues/1809
+ os.environ.setdefault("COLUMNS", "120")
+
+ install(
+ show_locals = bool(os.environ.get("SHOW_LOCALS", "")),
+ width = None,
+ )
+
+ # custom warnings
+ # https://github.com/Textualize/rich/issues/433
+
+ from rich.traceback import install
+ from rich.console import Console
+ import warnings, sys
+
+
+ def showwarning(message, category, filename, lineno, file=None, line=None):
+ msg = warnings.WarningMessage(message, category, filename, lineno, file, line)
+
+ if file is None:
+ file = sys.stderr
+ if file is None:
+ # sys.stderr is None when run with pythonw.exe:
+ # warnings get lost
+ return
+ text = warnings._formatwarnmsg(msg)
+ if file.isatty():
+ Console(file=file, stderr=True).print(text)
+ else:
+ try:
+ file.write(text)
+ except OSError:
+ # the file (probably stderr) is invalid - this warning gets lost.
+ pass
+ warnings.showwarning = showwarning
+
+ def warning_no_src_line(message, category, filename, lineno, file=None, line=None):
+ if (file or sys.stderr) is not None:
+ if (file or sys.stderr).isatty():
+ if file is None or file is sys.stderr:
+ return f"[yellow]{category.__name__}[/yellow]: {message}\n ({filename}:{lineno})"
+ return f"{category.__name__}: {message} ({filename}:{lineno})\n"
+ warnings.formatwarning = warning_no_src_line
+
+
+setup_print_hooks()
+del setup_print_hooks
diff --git a/ifield/cli.py b/ifield/cli.py
new file mode 100644
index 0000000..51999d0
--- /dev/null
+++ b/ifield/cli.py
@@ -0,0 +1,1006 @@
+from . import logging, param
+from .utils import helpers
+from .utils.helpers import camel_to_snake_case
+from argparse import ArgumentParser, _SubParsersAction, Namespace
+from contextlib import contextmanager
+from datetime import datetime
+from functools import partial
+from munch import Munch, munchify
+from pathlib import Path
+from pytorch_lightning.utilities.exceptions import MisconfigurationException
+from serve_me_once import serve_once_in_background, gen_random_port
+from torch import nn
+from tqdm import tqdm
+from typing import Optional, Callable, TypeVar, Union, Any
+import argparse, collections, copy
+import inspect, io, os, platform, psutil, pygments, pygments.lexers, pygments.formatters
+import pytorch_lightning as pl, re, rich, rich.pretty, shlex, shutil, string, subprocess, sys, textwrap
+import traceback, time, torch, torchviz, urllib.parse, warnings, webbrowser, yaml
+
+
+CONSOLE = rich.console.Console(width=None if os.isatty(1) else 140)
+torch.set_printoptions(threshold=200)
+
+# https://gist.github.com/pypt/94d747fe5180851196eb#gistcomment-3595282
+#class UniqueKeyYAMLLoader(yaml.SafeLoader):
+class UniqueKeyYAMLLoader(yaml.Loader):
+ def construct_mapping(self, node, deep=False):
+ mapping = set()
+ for key_node, value_node in node.value:
+ key = self.construct_object(key_node, deep=deep)
+ if key in mapping:
+ raise KeyError(f"Duplicate {key!r} key found in YAML.")
+ mapping.add(key)
+ return super().construct_mapping(node, deep)
+
+# load scientific notation correctly as floats and not as strings
+# basically, support for the to_json filter in jinja
+# https://stackoverflow.com/a/30462009
+# https://github.com/yaml/pyyaml/issues/173
+UniqueKeyYAMLLoader.add_implicit_resolver(
+ u'tag:yaml.org,2002:float',
+ re.compile(u'''^(?:
+ [-+]?(?:[0-9][0-9_]*)\\.[0-9_]*(?:[eE][-+]?[0-9]+)?
+ |[-+]?(?:[0-9][0-9_]*)(?:[eE][-+]?[0-9]+)
+ |\\.[0-9_]+(?:[eE][-+][0-9]+)?
+ |[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]*
+ |[-+]?\\.(?:inf|Inf|INF)
+ |\\.(?:nan|NaN|NAN))$''', re.X),
+ list(u'-+0123456789.'))
+
+class IgnorantActionsContainer(argparse._ActionsContainer):
+ """
+ Ignores conflicts with
+ Must be enabled with ArgumentParser(conflict_handler="ignore")
+ """
+ # https://stackoverflow.com/a/71782808
+ def _handle_conflict_ignore(self, action, conflicting_actions):
+ pass
+argparse.ArgumentParser.__bases__ = (argparse._AttributeHolder, IgnorantActionsContainer)
+argparse._ArgumentGroup.__bases__ = (IgnorantActionsContainer,)
+
+@contextmanager
+def ignore_action_container_conflicts(parser: Union[argparse.ArgumentParser, argparse._ArgumentGroup]):
+ old = parser.conflict_handler
+ parser.conflict_handler = "ignore"
+ yield
+ parser.conflict_handler = old
+
+def _print_with_syntax_highlighting(language, string, indent=""):
+ if os.isatty(1):
+ string = pygments.highlight(string,
+ lexer = pygments.lexers.get_lexer_by_name(language),
+ formatter = pygments.formatters.Terminal256Formatter(style="monokai"),
+ )
+ if indent:
+ string = textwrap.indent(string, indent)
+ print(string)
+
+def print_column_dict(data: dict, n_columns: int = 2, prefix: str=" "):
+ small = {k: v for k, v in data.items() if not isinstance(v, dict) and len(repr(v)) <= 40}
+ wide = {k: v for k, v in data.items() if not isinstance(v, dict) and len(repr(v)) > 40}
+ dicts = {k: v for k, v in data.items() if isinstance(v, dict)}
+ kw = dict(
+ crop = False,
+ overflow = "ignore",
+ )
+ if small:
+ CONSOLE.print(helpers.columnize_dict(small, prefix=prefix, n_columns=n_columns, sep=" "), **kw)
+ key_len = max(map(len, map(repr, wide.keys()))) if wide else 0
+ for key, val in wide.items():
+ CONSOLE.print(f"{prefix}{repr(key).ljust(key_len)} : {val!r},", **kw)
+ for key, val in dicts.items():
+ CONSOLE.print(f"{prefix}{key!r}: {{", **kw)
+ print_column_dict(val, n_columns=n_columns, prefix=prefix+" ")
+ CONSOLE.print(f"{prefix}}},", **kw)
+
+
+M = TypeVar("M", bound=nn.Module)
+DM = TypeVar("DM", bound=pl.LightningDataModule)
+FitHook = Callable[[Namespace, Munch, M, pl.Trainer, DM, logging.Logger], None]
+
+class CliInterface:
+ trainer_defaults: dict
+
+ def __init__(self, *, module_cls: type[M], workdir: Path, datamodule_cls: Union[list[type[DM]], type[DM], None] = None, experiment_name_prefix = "experiment"):
+ self.module_cls = module_cls
+ self.datamodule_cls = [datamodule_cls] if not isinstance(datamodule_cls, list) and datamodule_cls is not None else datamodule_cls
+ self.workdir = workdir
+ self.experiment_name_prefix = experiment_name_prefix
+
+ self.trainer_defaults = dict(
+ enable_model_summary = False,
+ )
+
+ self.pre_fit_handlers: list[FitHook] = []
+ self.post_fit_handlers: list[FitHook] = []
+
+ self._registered_actions : dict[str, tuple[Callable[M, None], list, dict, Optional[callable]]] = {}
+ self._included_in_config_template : dict[str, tuple[callable, dict]] = {}
+
+ self.register_action(_func=self.repr, help="Print str(module).", args=[])
+ self.register_action(_func=self.yaml, help="Print evaluated config.", args=[])
+ self.register_action(_func=self.hparams, help="Print hparams, like during training.", args=[])
+ self.register_action(_func=self.dot, help="Print graphviz graph of computation graph.", args=[
+ ("-e", "--eval", dict(action="store_true")),
+ ("-f", "--filter", dict(action="store_true")),
+ ])
+ self.register_action(_func=self.jit, help="Print a TorchScript graph of the model", args=[])
+ self.register_action(_func=self.trace, help="Dump a TorchScript trace of the model.", args=[
+ ("output_file", dict(type=Path,
+ help="Path to write the .pt file. Use \"-\" to instead open the trace in Netron.app")),
+ ])
+ self.register_action(_func=self.onnx, help="Dump a ONNX trace of the model.", args=[
+ ("output_file", dict(type=Path,
+ help="Path to write the .onnx file. Use \"-\" to instead open the onnx in Netron.app")),
+ ])
+
+ if self.datamodule_cls:
+ names = [i.__name__ for i in self.datamodule_cls]
+ names_snake = [datamodule_name_to_snake_case(i) for i in names]
+ assert len(names) == len(set(names)),\
+ f"Datamodule names are not unique: {names!r}"
+ assert len(names) == len(set(names_snake)),\
+ f"Datamodule snake-names are not unique: {names_snake!r}"
+
+ self.register_action(_func=self.test_dataloader,
+ help="Benchmark the speed of the dataloader",
+ args=[
+ ("datamodule", dict(type=str, default=None, nargs='?', choices=names_snake,
+ help="Which dataloader to test. Defaults to the first one found in config.")),
+ ("--limit-cores", dict(type=int, default=None,
+ help="Limits the cpu affinity to N cores. Perfect to simulate a SLURM environ.")),
+ ("--profile", dict(type=Path, default=None,
+ help="Profile using cProfile, marshaling the result to a .prof or .log file.")),
+ ("-n", "--n-rounds", dict(type=int, default=3,
+ help="Number of times to read the dataloader.")),
+ ],
+ conflict_handler = "ignore" if len(self.datamodule_cls) > 1 else "error",
+ add_argparse_args=[i.add_argparse_args for i in self.datamodule_cls],
+ )
+
+
+ # decorator
+ def register_pre_training_callback(self, func: FitHook):
+ self.pre_fit_handlers.append(func)
+ return func
+
+ # decorator
+ def register_post_training_callback(self, func: FitHook):
+ self.post_fit_handlers.append(func)
+ return func
+
+ # decorator
+ def register_action(self, *,
+ help : str,
+ args : list[tuple[Any, ..., dict]] = [],
+ _func : Optional[Callable[[Namespace, Munch, M], None]] = None,
+ add_argparse_args : Union[list[Callable[[ArgumentParser], ArgumentParser]], Callable[[ArgumentParser], ArgumentParser], None] = None,
+ **kw,
+ ):
+ def wrapper(action: Callable[[Namespace, Munch, M], None]):
+ cli_name = action.__name__.lower().replace("_", "-")
+ self._registered_actions[cli_name] = (
+ action,
+ args,
+ kw | {"help": help},
+ add_argparse_args,
+ )
+ return action
+ if _func is not None: # shortcut
+ return wrapper(_func)
+ else:
+ return wrapper
+
+ def make_parser(self,
+ parser : ArgumentParser = None,
+ subparsers : _SubParsersAction = None,
+ add_trainer : bool = False,
+ ) -> tuple[ArgumentParser, _SubParsersAction, _SubParsersAction]:
+ if parser is None:
+ parser = ArgumentParser()
+ if subparsers is None:
+ subparsers = parser.add_subparsers(dest="mode", required=True)
+
+ parser.add_argument("-pm", "--post-mortem", action="store_true",
+ help="Start a debugger if a uncaught exception is thrown.")
+
+ # Template generation and exploration
+ parser_template = subparsers.add_parser("template",
+ help="Generate or evaluate a config template")
+ if 1: # fold me
+ parser_mode_mutex = parser_template.add_mutually_exclusive_group()#(required=True)
+ parser_mode_mutex.add_argument("-e", "--evaluate", metavar="TEMPLATE", type=Path,
+ help="Read jinja2 yaml config template file, then evaluate and print it.")
+ parser_mode_mutex.add_argument("-p", "--parse", metavar="TEMPLATE", type=Path,
+ help="Read jinja2 yaml config template file, then evaluate, parse and print it.")
+
+ def pair(data: str) -> tuple[str, str]:
+ key, sep, value = data.partition("=")
+ if not sep:
+ if key in os.environ:
+ value = os.environ[key]
+ else:
+ raise ValueError(f"the variable {key!r} was not given any value, and none was found in the environment.")
+ elif "$" in value:
+ value = string.Template(value).substitute(os.environ)
+ return (key, value)
+ parser_template.add_argument("-O", dest="jinja2_variables", action="append", type=pair,
+ help="Variable available as string in the jinja2. (a=b). b will be expanded as an"
+ " env var if prefixed with $, or set equal to the env var a if =b is omitted.")
+
+ parser_template.add_argument("-s", "--strict", action="store_true",
+ help="Enable {% do require_defined(\"var\",var) %}".replace("%", "%%"))
+ parser_template.add_argument("-d", "--defined-only", action="store_true",
+ help="Disallow any use of undefined variables")
+
+
+ # Load a module
+ parser_module = subparsers.add_parser("module", aliases=["model"],
+ help="Load a config template, evaluate it and use the resulting module")
+ if 1: # fold me
+ parser_module.add_argument("module_file", type=Path,
+ help="Jinja2 yaml config template or pytorch-lightning .ckpt file.")
+ parser_module.add_argument("-O", dest="jinja2_variables", action="append", type=pair,
+ help="Variable available as string in the jinja2. (a=b). b will be expanded as an"
+ " env var if prefixed with $, or set equal to the env var a if =b is omitted.")
+ parser_module.add_argument("--last", action="store_true",
+ help="if multiple ckpt match, prefer the last one")
+ parser_module.add_argument("--best", action="store_true",
+ help="if multiple ckpt match, prefer the best one")
+
+ parser_module.add_argument("--add-shape-prehook", action="store_true",
+ help="Add a forward hook which prints the tensor shapes of all inputs, but not the outputs.")
+ parser_module.add_argument("--add-shape-hook", action="store_true",
+ help="Add a forward hook which prints the tensor shapes of all inputs AND outputs.")
+ parser_module.add_argument("--add-oob-hook", action="store_true",
+ help="Add a forward hook checking for INF and NaN values in inputs or outputs.")
+ parser_module.add_argument("--add-oob-hook-input", action="store_true",
+ help="Add a forward hook checking for INF and NaN values in inputs.")
+ parser_module.add_argument("--add-oob-hook-output", action="store_true",
+ help="Add a forward hook checking for INF and NaN values in outputs.")
+
+
+ module_actions_subparser = parser_module.add_subparsers(dest="action", required=True)
+
+ # add pluggables
+ for name, (action, args, kw, add_argparse_args) in self._registered_actions.items():
+ action_parser = module_actions_subparser.add_parser(name, **kw)
+ if add_argparse_args is not None and add_argparse_args:
+ for func in add_argparse_args if isinstance(add_argparse_args, list) else [add_argparse_args]:
+ action_parser = func(action_parser)
+ for *a, kw in args:
+ action_parser.add_argument(*a, **kw)
+
+ # Module: train or test
+ if self.datamodule_cls:
+ parser_trainer = module_actions_subparser.add_parser("fit", aliases=["test"],
+ help="Train/fit or evaluate the module with train/val or test data.")
+
+ # pl.Trainer
+ parser_trainer = pl.Trainer.add_argparse_args(parser_trainer)
+
+ # datamodule
+ parser_trainer.add_argument("datamodule", type=str, default=None, nargs='?',
+ choices=[datamodule_name_to_snake_case(i) for i in self.datamodule_cls],
+ help="Which dataloader to test. Defaults to the first one found in config.")
+ if len(self.datamodule_cls) > 1:
+ # check that none of the datamodules conflict with trainer or module
+ for datamodule_cls in self.datamodule_cls:
+ datamodule_cls.add_argparse_args(copy.deepcopy(parser_trainer)) # will raise on conflict
+ # Merge the datamodule options, the above sanity check makes it "okay"
+ with ignore_action_container_conflicts(parser_trainer):
+ for datamodule_cls in self.datamodule_cls:
+ parser_trainer = datamodule_cls.add_argparse_args(parser_trainer)
+
+ # defaults and jinja template
+ self._included_in_config_template.clear()
+ remove_options_from_parser(parser_trainer, "--logger")
+ parser_trainer.set_defaults(**self.trainer_defaults)
+ self.add_to_jinja_template("trainer", pl.Trainer, defaults=self.trainer_defaults, exclude_list={
+ # not yaml friendly, already covered anyway:
+ "logger",
+ "plugins",
+ "callbacks",
+ # deprecated or covered by callbacks:
+ "stochastic_weight_avg",
+ "enable_model_summary",
+ "track_grad_norm",
+ "log_gpu_memory",
+ })
+ for datamodule_cls in self.datamodule_cls:
+ self.add_to_jinja_template(datamodule_cls.__name__, datamodule_cls,
+ comment=f"select with {datamodule_name_to_snake_case(datamodule_cls)!r}")#, commented=False)
+ self.add_to_jinja_template("logging", logging, save_dir = "logdir", commented=False)
+
+ return parser, subparsers, module_actions_subparser
+
+ def add_to_jinja_template(self, name: str, func: callable, **kwargs):
+ """
+ Basically a call to `make_jinja_template`.
+ Will ensure the keys are present in the output from `from_argparse_args`.
+ """
+ self._included_in_config_template[name] = (func, dict(commented=True) | kwargs)
+
+ def make_jinja_template(self) -> str:
+ return "\n".join([
+ f'#!/usr/bin/env -S python {sys.argv[0]} module',
+ r'{% do require_defined("select", select, 0, "$SLURM_ARRAY_TASK_ID") %}{# requires jinja2.ext.do #}',
+ r"{% set counter = itertools.count(start=0, step=1) %}",
+ r"",
+ r"{% set hp_matrix = namespace() %}{# hyper parameter matrix #}",
+ r"{% set hp_matrix.my_hparam = [0] %}{##}",
+ r"",
+ r"{% for hp in cartesian_hparams(hp_matrix) %}{##}",
+ r"{#% for hp in ablation_hparams(hp_matrix, caartesian_keys=[]) %}{##}",
+ r"",
+ r"{% set index = next(counter) %}",
+ r"{% if select is not defined and index > 0 %}---{% endif %}",
+ r"{% if select is not defined or int(select) == index %}",
+ r"",
+ *[
+ func.make_jinja_template(name=name, **kwargs)
+ if hasattr(func, "make_jinja_template") else
+ param.make_jinja_template(func, name=name, **kwargs)
+ for name, (func, kwargs) in self._included_in_config_template.items()
+ ],
+ r"{% autoescape false %}",
+ r'{% do require_defined("experiment_name", experiment_name, "test", strict=true) %}',
+ f"experiment_name: { self.experiment_name_prefix }-{{{{ experiment_name }}}}",
+ r'{#--#}-{{ hp.my_hparam }}',
+ r'{#--#}-{{ gen_run_uid(4) }} # select with -Oselect={{ index }}',
+ r"{% endautoescape %}",
+ self.module_cls.make_jinja_template(),
+ r"{% endif %}{# -Oselect #}",
+ r"",
+ r"{% endfor %}",
+ r"",
+ r"{% set index = next(counter) %}",
+ r"# number of possible 'select': {{ index }}, from 0 to {{ index-1 }}",
+ r"# local: for select in {0..{{ index-1 }}}; do python ... -Oselect=$select ... ; done",
+ r"# local: for select in {0..{{ index-1 }}}; do python -O {{ argv[0] }} model marf.yaml.j2 -Oselect=$select -Oexperiment_name='{{ experiment_name }}' fit --accelerator gpu ; done",
+ r"# slurm: sbatch --array=0-{{ index-1 }} runcommand.slurm python ... -Oselect=\$SLURM_ARRAY_TASK_ID ...",
+ r"# slurm: sbatch --array=0-{{ index-1 }} runcommand.slurm python -O {{ argv[0] }} model this-file.yaml.j2 -Oselect=\$SLURM_ARRAY_TASK_ID -Oexperiment_name='{{ experiment_name }}' fit --accelerator gpu --devices -1 --strategy ddp"
+ ])
+
+ def run(self, args=None, args_hook: Optional[Callable[[ArgumentParser, _SubParsersAction, _SubParsersAction], None]] = None):
+ parser, mode_subparser, action_subparser = self.make_parser()
+ if args_hook is not None:
+ args_hook(parser, mode_subparser, action_subparser)
+ args = parser.parse_args(args) # may exit
+ if os.isatty(0) and args.post_mortem:
+ warnings.warn("post-mortem debugging is enabled without any TTY attached. Will be ignored.")
+ if args.post_mortem and os.isatty(0):
+ try:
+ self.handle_args(args)
+ except Exception:
+ # print exception
+ sys.excepthook(*sys.exc_info())
+ # debug
+ *debug_module, debug_func = os.environ.get("PYTHONBREAKPOINT", "pdb.set_trace").split(".")
+ __import__(".".join(debug_module)).post_mortem()
+ exit(1)
+ else:
+ self.handle_args(args)
+
+ def handle_args(self, args: Namespace):
+ """
+ May call exit()
+ """
+ if args.mode == "template":
+
+ if args.evaluate or args.parse:
+ template_file = args.evaluate or args.parse
+ env = param.make_jinja_env(globals=param.make_jinja_globals(enable_require_defined=args.strict), allow_undef=not args.defined_only)
+ if str(template_file) == "-":
+ template = env.from_string(sys.stdin.read(), globals=dict(args.jinja2_variables or []))
+ else:
+ template = env.get_template(str(template_file.absolute()), globals=dict(args.jinja2_variables or []))
+ config_yaml = param.squash_newlines(template.render())#.lstrip("\n").rstrip()
+ if args.evaluate:
+ _print_with_syntax_highlighting("yaml+jinja", config_yaml)
+ else:
+ config = yaml.load(config_yaml, UniqueKeyYAMLLoader)
+ CONSOLE.print(config)
+
+ else:
+ _print_with_syntax_highlighting("yaml+jinja", self.make_jinja_template())
+
+ elif args.mode in ("module", "model"):
+
+ module: nn.Module
+
+ if not args.module_file.is_file():
+ matches = [*Path("logdir/tensorboard").rglob(f"*-{args.module_file}/checkpoints/*.ckpt")]
+ if len(matches) == 1:
+ args.module_file, = matches
+ elif len(matches) > 1:
+ if (args.last or args.best) and len(set(match.parent.parent.name for match in matches)) == 1:
+ if args.last:
+ args.module_file, = (match for match in matches if match.name == "last.ckpt")
+ elif args.best:
+ args.module_file, = (match for match in matches if match.name.startswith("epoch="))
+ else:
+ assert False
+ else:
+ raise ValueError("uid matches multiple paths:\n"+"\n".join(map(str, matches)))
+ else:
+ raise ValueError("path does not exist, and is not a uid")
+
+ # load module from cli args
+ if args.module_file.suffix == ".ckpt": # from checkpoint
+ # load from checkpoint
+ rich.print(f"Loading module from {str(args.module_file)!r}...", file=sys.stderr)
+ module = self.module_cls.load_from_checkpoint(args.module_file)
+
+ if (args.module_file.parent.parent / "hparams.yaml").is_file():
+ with (args.module_file.parent.parent / "hparams.yaml").open() as f:
+ config_yaml = yaml.load(f.read(), UniqueKeyYAMLLoader)["_pickled_cli_args"]["_raw_yaml"]
+ else:
+ with (args.module_file.parent.parent / "config.yaml").open() as f:
+ config_yaml = f.read()
+
+ config = munchify(yaml.load(config_yaml, UniqueKeyYAMLLoader) | {"_raw_yaml": config_yaml})
+
+ else: # from yaml
+
+ # read, evaluate and parse config
+ if args.module_file.suffix == ".j2" or str(args.module_file) == "-":
+ env = param.make_jinja_env()
+ if str(args.module_file) == "-":
+ template = env.from_string(sys.stdin.read(), globals=dict(args.jinja2_variables or []))
+ else: # jinja+yaml file
+ template = env.get_template(str(args.module_file.absolute()), globals=dict(args.jinja2_variables or []))
+ config_yaml = param.squash_newlines(template.render()).lstrip("\n").rstrip()
+ else: # yaml file (the git diffs in _pickled_cli_args may trigger jinja's escape sequences)
+ with args.module_file.open() as f:
+ config_yaml = f.read().lstrip("\n").rstrip()
+
+ config = yaml.load(config_yaml, UniqueKeyYAMLLoader)
+
+ if "_pickled_cli_args" in config: # hparams.yaml in tensorboard logdir
+ config_yaml = config["_pickled_cli_args"]["_raw_yaml"]
+ config = yaml.load(config_yaml, UniqueKeyYAMLLoader)
+
+ from_checkpoint: Optional[Path] = None
+ if (args.module_file.parent / "checkpoints").glob("*.ckpt"):
+ checkpoints_fnames = list((args.module_file.parent / "checkpoints").glob("*.ckpt"))
+ if len(checkpoints_fnames) == 1:
+ from_checkpoint = checkpoints_fnames[0]
+ elif args.last:
+ from_checkpoint, = (i for i in checkpoints_fnames if i.name == "last.ckpt")
+ elif args.best:
+ from_checkpoint, = (i for i in checkpoints_fnames if i.name.startswith("epoch="))
+ elif len(checkpoints_fnames) > 1:
+ rich.print(f"[yellow]WARNING:[/] {str(args.module_file.parent / 'checkpoints')!r} contains more than one checkpoint, unable to automatically load one.", file=sys.stderr)
+
+ config = munchify(config | {"_raw_yaml": config_yaml})
+
+ # Ensure date and uid to experiment name, allowing for reruns and organization
+ assert config.experiment_name
+ assert re.match(r'^.*-[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}-[a-z]{4}$', config.experiment_name),\
+ config.experiment_name
+
+ # init the module
+ if from_checkpoint:
+ rich.print(f"Loading module from {str(from_checkpoint)!r}...", file=sys.stderr)
+ module = self.module_cls.load_from_checkpoint(from_checkpoint)
+ else:
+ module = self.module_cls(**{k:v for k, v in config[self.module_cls.__name__].items() if k != "_extra"})
+
+ # optional debugging forward hooks
+
+ if args.add_shape_hook or args.add_shape_prehook:
+ def shape_forward_hook(is_prehook: bool, name: str, module: nn.Module, input, output=None):
+ def tensor_to_shape(val):
+ if isinstance(val, torch.Tensor):
+ return tuple(val.shape)
+ elif isinstance(val, (str, float, int)) or val is None:
+ return 1
+ else:
+ assert 0, (val, name)
+ with torch.no_grad():
+ rich.print(
+ f"{name}.forward({helpers.map_tree(tensor_to_shape, input)})"
+ if is_prehook else
+ f"{name}.forward({helpers.map_tree(tensor_to_shape, input)})"
+ f" -> {helpers.map_tree(tensor_to_shape, output)}"
+ , file=sys.stderr)
+
+ for submodule_name, submodule in module.named_modules():
+ if submodule_name:
+ submodule_name = f"{module.__class__.__qualname__}.{submodule_name}"
+ else:
+ submodule_name = f"{module.__class__.__qualname__}"
+ if args.add_shape_prehook:
+ submodule.register_forward_pre_hook(partial(shape_forward_hook, True, submodule_name))
+ if args.add_shape_hook:
+ submodule.register_forward_hook(partial(shape_forward_hook, False, submodule_name))
+
+ if args.add_oob_hook or args.add_oob_hook_input or args.add_oob_hook_output:
+ def oob_forward_hook(name: str, module: nn.Module, input, output):
+ def raise_if_oob(key, val):
+ if isinstance(val, collections.abc.Mapping):
+ for k, subval in val.items():
+ raise_if_oob(f"{key}[{k!r}]", subval)
+ elif isinstance(val, (tuple, list)):
+ for i, subval in enumerate(val):
+ raise_if_oob(f"{key}[{i}]", subval)
+ elif isinstance(val, torch.Tensor):
+ assert not torch.isinf(val).any(), \
+ f"INFs found in {key}"
+ assert not val.isnan().any(), \
+ f"NaNs found in {key}"
+ elif isinstance(val, (str, float, int)):
+ pass
+ elif val is None:
+ warnings.warn(f"None found in {key}")
+ else:
+ assert False, val
+ with torch.no_grad():
+ if args.add_oob_hook or args.add_oob_hook_input:
+ raise_if_oob(f"{name}.forward input", input)
+ if args.add_oob_hook or args.add_oob_hook_output:
+ raise_if_oob(f"{name}.forward output", output)
+
+ for submodule_name, submodule in module.named_modules():
+ submodule.register_forward_hook(partial(oob_forward_hook,
+ f"{module.__class__.__qualname__}.{submodule_name}"
+ if submodule_name else
+ f"{module.__class__.__qualname__}"
+ ))
+
+ # Ensure all the top-level config keys are there
+ for key in self._included_in_config_template.keys():
+ if key in (i.__name__ for i in self.datamodule_cls):
+ continue
+ if key not in config or config[key] is None:
+ config[key] = {}
+
+ # Run registered action
+ if args.action in self._registered_actions:
+ action, *_ = self._registered_actions[args.action]
+ action(args, config, module)
+ elif args.action in ("fit", "test") and self.datamodule_cls is not None:
+ self.fit(args, config, module)
+ else:
+ raise ValueError(f"{args.mode=}, {args.action=}")
+
+ else:
+ raise ValueError(f"{args.mode=}")
+
+ def get_datamodule_cls_from_config(self, args: Namespace, config: Munch) -> DM:
+ assert self.datamodule_cls
+ cli = getattr(args, "datamodule", None)
+ datamodule_cls: pl.LightningDataModule
+ if cli is not None:
+ datamodule_cls, = (i for i in self.datamodule_cls if datamodule_name_to_snake_case(i) == cli)
+ else:
+ datamodules = {
+ cls.__name__: cls
+ for cls in self.datamodule_cls
+ }
+ for key in config.keys():
+ if key in datamodules:
+ datamodule_cls = datamodules[key]
+ break
+ else:
+ datamodule_cls = self.datamodule_cls[0]
+ warnings.warn(f"None of the following datamodules were found in config: {set(datamodules.keys())!r}. {datamodule_cls.__name__!r} was chosen as the default.")
+
+ return datamodule_cls
+
+ def init_datamodule_cls_from_config(self, args: Namespace, config: Munch) -> DM:
+ datamodule_cls = self.get_datamodule_cls_from_config(args, config)
+ return datamodule_cls.from_argparse_args(args, **(config.get(datamodule_cls.__name__) or {}))
+
+
+ # Module actions
+
+ def repr(self, args: Namespace, config: Munch, module: M):
+ rich.print(module)
+
+ def yaml(self, args: Namespace, config: Munch, module: M):
+ _print_with_syntax_highlighting("yaml+jinja", config["_raw_yaml"])
+
+ def dot(self, args: Namespace, config: Munch, module: M):
+ module.train(not args.eval)
+ assert not args.filter, "not implemented! pipe it through examples/scripts/filter_dot.py in the meanwhile"
+
+ example_input_array = module.example_input_array
+ assert example_input_array is not None, f"{module.__class__.__qualname__}.example_input_array=None"
+ assert isinstance(example_input_array, (tuple, dict, torch.Tensor)), type(example_input_array)
+
+ def set_requires_grad(val):
+ if isinstance(val, torch.Tensor):
+ val.requires_grad = True
+ return val
+
+ with torch.enable_grad():
+ outputs = module(*helpers.map_tree(set_requires_grad, example_input_array))
+
+ dot = torchviz.make_dot(outputs, params=dict(module.named_parameters()), show_attrs=False, show_saved=False)
+ _print_with_syntax_highlighting("dot", str(dot))
+
+ def jit(self, args: Namespace, config: Munch, module: M):
+ example_input_array = module.example_input_array
+ assert example_input_array is not None, f"{module.__class__.__qualname__}.example_input_array=None"
+ assert isinstance(example_input_array, (tuple, dict, torch.Tensor)), type(example_input_array)
+ trace = torch.jit.trace_module(module, {"forward": example_input_array})
+ _print_with_syntax_highlighting("python", str(trace.inlined_graph))
+
+ def trace(self, args: Namespace, config: Munch, module: M):
+ if isinstance(module, pl.LightningModule):
+ trace = module.to_torchscript(method="trace")
+ else:
+ example_input_array = module.example_input_array
+ assert example_input_array is not None, f"{module.__class__.__qualname__}.example_input_array is None"
+ assert isinstance(module, torch.Module)
+ trace = torch.jit.trace_module(module, {"forward": example_input_array})
+
+ use_netron = str(args.output_file) == "-"
+ trace_f = io.BytesIO() if use_netron else args.output_file
+
+ torch.jit.save(trace, trace_f)
+
+ if use_netron:
+ open_in_netron(f"{self.module_cls.__name__}.pt", trace_f.getvalue())
+
+ def onnx(self, args: Namespace, config: Munch, module: M):
+ example_input_array = module.example_input_array
+ assert example_input_array is not None, f"{module.__class__.__qualname__}.example_input_array=None"
+ assert isinstance(example_input_array, (tuple, dict, torch.Tensor)), type(example_input_array)
+
+ use_netron = str(args.output_file) == "-"
+ onnx_f = io.BytesIO() if use_netron else args.output_file
+
+ torch.onnx.export(module,
+ tuple(example_input_array),
+ onnx_f,
+ export_params = True,
+ opset_version = 17,
+ do_constant_folding = True,
+ input_names = ["input"],
+ output_names = ["output"],
+ dynamic_axes = {
+ "input" : {0 : "batch_size"},
+ "output" : {0 : "batch_size"},
+ },
+ )
+
+ if use_netron:
+ open_in_netron(f"{self.module_cls.__name__}.onnx", onnx_f.getvalue())
+
+ def hparams(self, args: Namespace, config: Munch, module: M):
+ assert isinstance(module, self.module_cls)
+ print(f"{self.module_cls.__qualname__} hparams:")
+ print_column_dict(map_type_to_repr(module.hparams, nn.Module, lambda t: f"{t.__class__.__qualname__}"), 3)
+
+
+ def fit(self, args: Namespace, config: Munch, module: M):
+ is_rank_zero = pl.utilities.rank_zero_only.rank == 0
+
+ metric_prefix = f"{module.__class__.__name__}.validation_step/"
+
+ pl_callbacks = [
+ pl.callbacks.LearningRateMonitor(log_momentum=True),
+ pl.callbacks.EarlyStopping(monitor=metric_prefix+getattr(module, "metric_early_stop", "loss"), patience=200, check_on_train_epoch_end=False, verbose=True),
+ pl.callbacks.ModelCheckpoint(monitor=metric_prefix+getattr(module, "metric_best_model", "loss"), mode="min", save_top_k=1, save_last=True),
+ logging.ModelOutputMonitor(),
+ logging.EpochTimeMonitor(),
+ (pl.callbacks.RichModelSummary if os.isatty(1) else pl.callbacks.ModelSummary)(max_depth=30),
+ logging.PsutilMonitor(),
+ ]
+ if os.isatty(1):
+ pl_callbacks.append( pl.callbacks.RichProgressBar() )
+
+ trainer: pl.Trainer
+ logger = logging.make_logger(config.experiment_name, config.trainer.get("default_root_dir", args.default_root_dir or self.workdir), **config.logging)
+ trainer = pl.Trainer.from_argparse_args(args, logger=logger, callbacks=pl_callbacks, **config.trainer)
+
+ datamodule = self.init_datamodule_cls_from_config(args, config)
+
+ for f in self.pre_fit_handlers:
+ print(f"pre-train hook {f.__name__!r}...")
+ f(args, config, module, trainer, datamodule, logger)
+
+ # print and log hparams/config
+ if 1: # fold me
+ if is_rank_zero:
+ CONSOLE.print(f"Experiment name: {config.experiment_name!r}", soft_wrap=False, crop=False, no_wrap=False, overflow="ignore")
+
+ # parser.args and sys.argv
+ pickled_cli_args = dict(
+ sys_argv = sys.argv,
+ parser_args = args.__dict__,
+ config = config.copy(),
+ _raw_yaml = config["_raw_yaml"],
+ )
+ del pickled_cli_args["config"]["_raw_yaml"]
+ for k,v in pickled_cli_args["parser_args"].items():
+ if isinstance(v, Path):
+ pickled_cli_args["parser_args"][k] = str(v)
+
+ # trainer
+ params_trainer = inspect.signature(pl.Trainer.__init__).parameters
+ trainer_hparams = vars(pl.Trainer.parse_argparser(args))
+ trainer_hparams = { name: trainer_hparams[name] for name in params_trainer if name in trainer_hparams }
+ if is_rank_zero:
+ print("pl.Trainer hparams:")
+ print_column_dict(trainer_hparams, 3)
+ pickled_cli_args.update(trainer_hparams=trainer_hparams)
+
+ # module
+ assert isinstance(module, self.module_cls)
+ if is_rank_zero:
+ print(f"{self.module_cls.__qualname__} hparams:")
+ print_column_dict(map_type_to_repr(module.hparams, nn.Module, lambda t: f"{t.__class__.__qualname__}"), 3)
+ pickled_cli_args.update(module_hparams={
+ k : v
+ for k, v in module.hparams.items()
+ if k != "_raw_yaml"
+ })
+
+ # module extra state, like autodecoder uids
+ for submodule_name, submodule in module.named_modules():
+ if not submodule_name:
+ submodule_name = module.__class__.__qualname__
+ else:
+ submodule_name = module.__class__.__qualname__ + "." + submodule_name
+ try:
+ state = submodule.get_extra_state()
+ except RuntimeError:
+ continue
+ if "extra_state" not in pickled_cli_args:
+ pickled_cli_args["extra_state"] = {}
+ pickled_cli_args["extra_state"][submodule_name] = state
+
+ # datamodule
+ if self.datamodule_cls:
+ assert datamodule is not None and any(isinstance(datamodule, i) for i in self.datamodule_cls), datamodule
+ for datamodule_cls in self.datamodule_cls:
+ params_d = inspect.signature(datamodule_cls.__init__).parameters
+ assert {"self"} == set(params_trainer).intersection(params_d), \
+ f"trainer and datamodule has overlapping params: {set(params_trainer).intersection(params_d) - {'self'}}"
+
+ if is_rank_zero:
+ print(f"{datamodule.__class__.__qualname__} hparams:")
+ print_column_dict(datamodule.hparams)
+ pickled_cli_args.update(datamodule_hparams=dict(datamodule.hparams))
+
+ # logger
+ if logger is not None:
+ print(f"{logger.__class__.__qualname__} hparams:")
+ print_column_dict(config.logging)
+ pickled_cli_args.update(logger_hparams = {"_class": logger.__class__.__name__} | config.logging)
+
+ # host info
+ def cmd(cmd: Union[str, list[str]]) -> str:
+ if isinstance(cmd, str):
+ cmd = shlex.split(cmd)
+ if shutil.which(cmd[0]):
+ try:
+ return subprocess.run(cmd,
+ capture_output=True,
+ check=True,
+ text=True,
+ ).stdout.strip()
+ except subprocess.CalledProcessError as e:
+ warnings.warn(f"{e.__class__.__name__}: {e}")
+ return f"{e.__class__.__name__}: {e}\n{e.output = }\n{e.stderr = }"
+ else:
+ warnings.warn(f"command {cmd[0]!r} not found")
+ return f"*command {cmd[0]!r} not found*"
+
+ pickled_cli_args.update(host = dict(
+ platform = textwrap.dedent(f"""
+ {platform.architecture() = }
+ {platform.java_ver() = }
+ {platform.libc_ver() = }
+ {platform.mac_ver() = }
+ {platform.machine() = }
+ {platform.node() = }
+ {platform.platform() = }
+ {platform.processor() = }
+ {platform.python_branch() = }
+ {platform.python_build() = }
+ {platform.python_compiler() = }
+ {platform.python_implementation() = }
+ {platform.python_revision() = }
+ {platform.python_version() = }
+ {platform.release() = }
+ {platform.system() = }
+ {platform.uname() = }
+ {platform.version() = }
+ """.rstrip()).lstrip(),
+ cuda = dict(
+ gpus = [
+ torch.cuda.get_device_name(i)
+ for i in range(torch.cuda.device_count())
+ ],
+ available = torch.cuda.is_available(),
+ version = torch.version.cuda,
+ ),
+ hostname = cmd("hostname --fqdn"),
+ cwd = os.getcwd(),
+ date = datetime.now().astimezone().isoformat(),
+ date_utc = datetime.utcnow().isoformat(),
+ ifconfig = cmd("ifconfig"),
+ lspci = cmd("lspci"),
+ lsusb = cmd("lsusb"),
+ lsblk = cmd("lsblk"),
+ mount = cmd("mount"),
+ environ = os.environ.copy(),
+ vcs = f"commit {cmd('git rev-parse HEAD')}\n{cmd('git status')}\n{cmd('git diff --stat --patch HEAD')}",
+ venv_pip = cmd("pip list --format=freeze"),
+ venv_conda = cmd("conda list"),
+ venv_poetry = cmd("poetry show -t"),
+ gpus = [i.split(", ") for i in cmd("nvidia-smi --query-gpu=index,name,memory.total,driver_version,uuid --format=csv").splitlines()],
+ ))
+
+ if logger is not None:
+ logging.log_config(logger, _pickled_cli_args=pickled_cli_args)
+
+ warnings.filterwarnings(action="ignore", category=torch.jit.TracerWarning)
+
+ if __debug__ and is_rank_zero:
+ warnings.warn("You're running python with assertions active. Enable optimizations with `python -O` for improved performance.")
+
+ # train
+
+ t_begin = datetime.now()
+ if args.action == "fit":
+ trainer.fit(module, datamodule)
+ elif args.action == "test":
+ trainer.test(module, datamodule)
+ else:
+ raise ValueError(f"{args.mode=}, {args.action=}")
+
+ if not is_rank_zero:
+ return
+
+ t_end = datetime.now()
+ print(f"Training time: {t_end - t_begin}")
+
+ for f in self.post_fit_handlers:
+ print(f"post-train hook {f.__name__!r}...")
+ try:
+ f(args, config, module, trainer, datamodule, logger)
+ except Exception:
+ traceback.print_exc()
+
+ rich.print(f"Experiment name: {config.experiment_name!r}")
+ rich.print(f"Best model path: {helpers.make_relative(trainer.checkpoint_callback.best_model_path).__str__()!r}")
+ rich.print(f"Last model path: {helpers.make_relative(trainer.checkpoint_callback.last_model_path).__str__()!r}")
+
+ def test_dataloader(self, args: Namespace, config: Munch, module: M):
+ # limit CPU affinity
+ if args.limit_cores is not None:
+ # https://stackoverflow.com/a/40856471
+ p = psutil.Process()
+ assert len(p.cpu_affinity()) >= args.limit_cores
+ cpus = list(range(args.limit_cores))
+ p.cpu_affinity(cpus)
+ print("Process limited to CPUs", cpus)
+
+ datamodule = self.init_datamodule_cls_from_config(args, config)
+
+ # setup
+ rich.print(f"Setup {datamodule.__class__.__qualname__}...")
+ datamodule.prepare_data()
+ datamodule.setup("fit")
+ try:
+ train = datamodule.train_dataloader()
+ except (MisconfigurationException, NotImplementedError):
+ train = None
+ try:
+ val = datamodule.val_dataloader()
+ except (MisconfigurationException, NotImplementedError):
+ val = None
+ try:
+ test = datamodule.test_dataloader()
+ except (MisconfigurationException, NotImplementedError):
+ test = None
+
+ # inspect
+ rich.print("batch[0] = ", end="")
+ rich.pretty.pprint(
+ map_type_to_repr(
+ next(iter(train)),
+ torch.Tensor,
+ lambda x: f"Tensor(..., shape={x.shape}, dtype={x.dtype}, device={x.device})",
+ ),
+ indent_guides = False,
+ )
+
+ if args.profile is not None:
+ import cProfile
+ profile = cProfile.Profile()
+ profile.enable()
+
+ # measure
+ n_train, td_train = 0, 0
+ n_val, td_val = 0, 0
+ n_test, td_test = 0, 0
+ try:
+ for i in range(args.n_rounds):
+ print(f"Round {i+1} of {args.n_rounds}")
+ if train is not None:
+ epoch = time.perf_counter_ns()
+ n_train += sum(1 for _ in tqdm(train, desc=f"train {i+1}/{args.n_rounds}"))
+ td_train += time.perf_counter_ns() - epoch
+ if val is not None:
+ epoch = time.perf_counter_ns()
+ n_val += sum(1 for _ in tqdm(val, desc=f"val {i+1}/{args.n_rounds}"))
+ td_val += time.perf_counter_ns() - epoch
+ if test is not None:
+ epoch = time.perf_counter_ns()
+ n_test += sum(1 for _ in tqdm(test, desc=f"train {i+1}/{args.n_rounds}"))
+ td_test += time.perf_counter_ns() - epoch
+ except KeyboardInterrupt:
+ rich.print("Recieved a `KeyboardInterrupt`...")
+
+ if args.profile is not None:
+ profile.disable()
+ if args.profile != "-":
+ profile.dump_stats(args.profile)
+ profile.print_stats("tottime")
+
+ # summary
+ for label, data, n, td in [
+ ("train", train, n_train, td_train),
+ ("val", val, n_val, td_val),
+ ("test", test, n_test, td_test),
+ ]:
+ if not n: continue
+ if data is not None:
+ print(f"{label}:",
+ f" - per epoch: {td / args.n_rounds * 1e-9 :11.6f} s",
+ f" - per batch: {td / n * 1e-9 :11.6f} s",
+ f" - batches/s: {n / (td * 1e-9):11.6f}",
+ sep="\n")
+
+ datamodule.teardown("fit")
+
+
+
+# helpers:
+
+def open_in_netron(filename: str, data: bytes, *, timeout: float = 10):
+ # filename is only used to determine the filetype
+ url = serve_once_in_background(
+ data,
+ mime_type = "application/octet-stream",
+ timeout = timeout,
+ port = gen_random_port(),
+ )
+ url = f"https://netron.app/?url={urllib.parse.quote(url)}{filename}"
+ print("Open in Netron:", url)
+ webbrowser.get("firefox").open_new_tab(url)
+ if timeout:
+ time.sleep(timeout)
+
+def remove_options_from_parser(parser: ArgumentParser, *options: str):
+ options = set(options)
+ # https://stackoverflow.com/questions/32807319/disable-remove-argument-in-argparse/36863647#36863647
+ for action in parser._actions:
+ if action.option_strings:
+ option = action.option_strings[0]
+ if option in options:
+ parser._handle_conflict_resolve(None, [(option, action)])
+
+def map_type_to_repr(batch, type_match: type, repr_func: callable):
+ def mapper(value):
+ if isinstance(value, type_match):
+ return helpers.CustomRepr(repr_func(value))
+ else:
+ return value
+ return helpers.map_tree(mapper, batch)
+
+def datamodule_name_to_snake_case(datamodule: Union[str, type[DM]]) -> str:
+ if not isinstance(datamodule, str):
+ datamodule = datamodule.__name__
+ datamodule = datamodule.replace("DataModule", "Datamodule")
+ if datamodule != "Datamodule":
+ datamodule = datamodule.removesuffix("Datamodule")
+ return camel_to_snake_case(datamodule, sep="-", join_abbreviations=True)
diff --git a/ifield/cli_utils.py b/ifield/cli_utils.py
new file mode 100644
index 0000000..bd50bdd
--- /dev/null
+++ b/ifield/cli_utils.py
@@ -0,0 +1,174 @@
+#!/usr/bin/env python3
+from .data.common.scan import SingleViewScan, SingleViewUVScan
+from datetime import datetime
+import re
+import click
+import gzip
+import h5py as h5
+import matplotlib.pyplot as plt
+import numpy as np
+import pyrender
+import trimesh
+import trimesh.transformations as T
+
+__doc__ = """
+Here are a bunch of helper scripts exposed as cli command by poetry
+"""
+
+
+# these entrypoints are exposed by poetry as shell commands
+
+@click.command()
+@click.argument("h5file")
+@click.argument("key", default="")
+def show_h5_items(h5file: str, key: str):
+ "Show contents of HDF5 dataset"
+ f = h5.File(h5file, "r")
+ if not key:
+ mlen = max(map(len, f.keys()))
+ for i in sorted(f.keys()):
+ print(i.ljust(mlen), ":",
+ str (f[i].dtype).ljust(10),
+ repr(f[i].shape).ljust(16),
+ "mean:", f[i][:].mean()
+ )
+ else:
+ if not f[key].shape:
+ print(f[key].value)
+ else:
+ print(f[key][:])
+
+
+@click.command()
+@click.argument("h5file")
+@click.argument("key", default="")
+def show_h5_img(h5file: str, key: str):
+ "Show a 2D HDF5 dataset as an image"
+ f = h5.File(h5file, "r")
+ if not key:
+ mlen = max(map(len, f.keys()))
+ for i in sorted(f.keys()):
+ print(i.ljust(mlen), ":", str(f[i].dtype).ljust(10), f[i].shape)
+ else:
+ plt.imshow(f[key])
+ plt.show()
+
+
+@click.command()
+@click.argument("h5file")
+@click.option("--force-distances", is_flag=True, help="Always show miss distances.")
+@click.option("--uv", is_flag=True, help="Load as UV scan cloud and convert it.")
+@click.option("--show-unit-sphere", is_flag=True, help="Show the unit sphere.")
+@click.option("--missing", is_flag=True, help="Show miss points that are not hits nor misses as purple.")
+def show_h5_scan_cloud(
+ h5file : str,
+ force_distances : bool = False,
+ uv : bool = False,
+ missing : bool = False,
+ show_unit_sphere = False,
+ ):
+ "Show a SingleViewScan HDF5 dataset"
+ print("Reading data...")
+ t = datetime.now()
+ if uv:
+ scan = SingleViewUVScan.from_h5_file(h5file)
+ if missing and scan.any_missing:
+ if not scan.has_missing:
+ scan.fill_missing_points()
+ points_missing = scan.points[scan.missing]
+ else:
+ missing = False
+ if not scan.is_single_view:
+ scan.cam_pos = None
+ scan = scan.to_scan()
+ else:
+ scan = SingleViewScan.from_h5_file(h5file)
+ if missing:
+ uvscan = scan.to_uv_scan()
+ if scan.any_missing:
+ uvscan.fill_missing_points()
+ points_missing = uvscan.points[uvscan.missing]
+ else:
+ missing = False
+ print("loadtime: ", datetime.now() - t)
+
+ if force_distances and not scan.has_miss_distances:
+ print("Computing miss distances...")
+ scan.compute_miss_distances()
+ use_miss_distances = force_distances
+ print("Constructing scene...")
+ if not scan.has_colors:
+ scan.colors_hit = np.zeros_like(scan.points_hit)
+ scan.colors_miss = np.zeros_like(scan.points_miss)
+ scan.colors_hit [:] = ( 31/255, 119/255, 180/255)
+ scan.colors_miss[:] = (243/255, 156/255, 18/255)
+ use_miss_distances = True
+ if scan.has_miss_distances and use_miss_distances:
+ sdm = scan.distances_miss / scan.distances_miss.max()
+ sdm = sdm[..., None]
+ scan.colors_miss \
+ = np.array([0.8, 0, 0])[None, :] * sdm \
+ + np.array([0, 1, 0.2])[None, :] * (1-sdm)
+
+
+ scene = pyrender.Scene()
+
+ scene.add(pyrender.Mesh.from_points(scan.points_hit, colors=scan.colors_hit, normals=scan.normals_hit))
+ scene.add(pyrender.Mesh.from_points(scan.points_miss, colors=scan.colors_miss))
+
+ if missing:
+ scene.add(pyrender.Mesh.from_points(points_missing, colors=(np.array((0xff, 0x00, 0xff))/255)[None, :].repeat(points_missing.shape[0], axis=0)))
+
+ # camera:
+ if not scan.points_cam is None:
+ camera_mesh = trimesh.creation.uv_sphere(radius=scan.points_hit_std.max()*0.2)
+ camera_mesh.visual.vertex_colors = [0.0, 0.8, 0.0]
+ tfs = np.tile(np.eye(4), (len(scan.points_cam), 1, 1))
+ tfs[:,:3,3] = scan.points_cam
+ scene.add(pyrender.Mesh.from_trimesh(camera_mesh, poses=tfs))
+
+ # UV sphere:
+ if show_unit_sphere:
+ unit_sphere_mesh = trimesh.creation.uv_sphere(radius=1)
+ unit_sphere_mesh.invert()
+ unit_sphere_mesh.visual.vertex_colors = [0.8, 0.8, 0.0]
+ scene.add(pyrender.Mesh.from_trimesh(unit_sphere_mesh, poses=np.eye(4)[None, ...]))
+
+ print("Launch!")
+ viewer = pyrender.Viewer(scene, use_raymond_lighting=True, point_size=2)
+
+
+@click.command()
+@click.argument("meshfile")
+@click.option('--aabb', is_flag=True)
+@click.option('--z-skyward', is_flag=True)
+def show_model(
+ meshfile : str,
+ aabb : bool,
+ z_skyward : bool,
+ ):
+ "Show a 3D model with pyrender, supports .gz suffix"
+ if meshfile.endswith(".gz"):
+ with gzip.open(meshfile, "r") as f:
+ mesh = trimesh.load(f, file_type=meshfile.split(".", 1)[1].removesuffix(".gz"))
+ else:
+ mesh = trimesh.load(meshfile)
+
+ if isinstance(mesh, trimesh.Scene):
+ mesh = mesh.dump(concatenate=True)
+
+ if aabb:
+ from .data.common.mesh import rotate_to_closest_axis_aligned_bounds
+ mesh.apply_transform(rotate_to_closest_axis_aligned_bounds(mesh, fail_ok=True))
+
+ if z_skyward:
+ mesh.apply_transform(T.rotation_matrix(np.pi/2, (1, 0, 0)))
+
+ print(
+ *(i.strip() for i in pyrender.Viewer.__doc__.splitlines() if re.search(r"- ``[a-z0-9]``: ", i)),
+ sep="\n"
+ )
+
+ scene = pyrender.Scene()
+ scene.add(pyrender.Mesh.from_trimesh(mesh))
+ pyrender.Viewer(scene, use_raymond_lighting=True)
diff --git a/ifield/data/__init__.py b/ifield/data/__init__.py
new file mode 100644
index 0000000..dd50c82
--- /dev/null
+++ b/ifield/data/__init__.py
@@ -0,0 +1,3 @@
+__doc__ = """
+Submodules to read and process datasets
+"""
diff --git a/ifield/data/common/__init__.py b/ifield/data/common/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/ifield/data/common/download.py b/ifield/data/common/download.py
new file mode 100644
index 0000000..3f583e8
--- /dev/null
+++ b/ifield/data/common/download.py
@@ -0,0 +1,90 @@
+from ...utils.helpers import make_relative
+from pathlib import Path
+from tqdm import tqdm
+from typing import Union, Optional
+import io
+import os
+import json
+import requests
+
+PathLike = Union[os.PathLike, str]
+
+__doc__ = """
+Here are some helper functions for processing data.
+"""
+
+def check_url(url): # HTTP HEAD
+ return requests.head(url).ok
+
+def download_stream(
+ url : str,
+ file_object,
+ block_size : int = 1024,
+ silent : bool = False,
+ label : Optional[str] = None,
+ ):
+ resp = requests.get(url, stream=True)
+ total_size = int(resp.headers.get("content-length", 0))
+ if not silent:
+ progress_bar = tqdm(total=total_size , unit="iB", unit_scale=True, desc=label)
+
+ for chunk in resp.iter_content(block_size):
+ if not silent:
+ progress_bar.update(len(chunk))
+ file_object.write(chunk)
+
+ if not silent:
+ progress_bar.close()
+ if total_size != 0 and progress_bar.n != total_size:
+ print("ERROR, something went wrong")
+
+def download_data(
+ url : str,
+ block_size : int = 1024,
+ silent : bool = False,
+ label : Optional[str] = None,
+ ) -> bytearray:
+ f = io.BytesIO()
+ download_stream(url, f, block_size=block_size, silent=silent, label=label)
+ f.seek(0)
+ return bytearray(f.read())
+
+def download_file(
+ url : str,
+ fname : Union[Path, str],
+ block_size : int = 1024,
+ silent = False,
+ ):
+ if not isinstance(fname, Path):
+ fname = Path(fname)
+ with fname.open("wb") as f:
+ download_stream(url, f, block_size=block_size, silent=silent, label=make_relative(fname, Path.cwd()).name)
+
+def is_downloaded(
+ target_dir : PathLike,
+ url : str,
+ *,
+ add : bool = False,
+ dbfiles : Union[list[PathLike], PathLike],
+ ):
+ if not isinstance(target_dir, os.PathLike):
+ target_dir = Path(target_dir)
+ if not isinstance(dbfiles, list):
+ dbfiles = [dbfiles]
+ if not dbfiles:
+ raise ValueError("'dbfiles' empty")
+ downloaded = set()
+ for dbfile_fname in dbfiles:
+ dbfile_fname = target_dir / dbfile_fname
+ if dbfile_fname.is_file():
+ with open(dbfile_fname, "r") as f:
+ downloaded.update(json.load(f)["downloaded"])
+
+ if add and url not in downloaded:
+ downloaded.add(url)
+ with open(dbfiles[0], "w") as f:
+ data = {"downloaded": sorted(downloaded)}
+ json.dump(data, f, indent=2, sort_keys=True)
+ return True
+
+ return url in downloaded
diff --git a/ifield/data/common/h5_dataclasses.py b/ifield/data/common/h5_dataclasses.py
new file mode 100644
index 0000000..c7cd46f
--- /dev/null
+++ b/ifield/data/common/h5_dataclasses.py
@@ -0,0 +1,370 @@
+#!/usr/bin/env python3
+from abc import abstractmethod, ABCMeta
+from collections import namedtuple
+from pathlib import Path
+import copy
+import dataclasses
+import functools
+import h5py as h5
+import hdf5plugin
+import numpy as np
+import operator
+import os
+import sys
+import typing
+
+__all__ = [
+ "DataclassMeta",
+ "Dataclass",
+ "H5Dataclass",
+ "H5Array",
+ "H5ArrayNoSlice",
+]
+
+T = typing.TypeVar("T")
+NoneType = type(None)
+PathLike = typing.Union[os.PathLike, str]
+H5Array = typing._alias(np.ndarray, 0, inst=False, name="H5Array")
+H5ArrayNoSlice = typing._alias(np.ndarray, 0, inst=False, name="H5ArrayNoSlice")
+
+DataclassField = namedtuple("DataclassField", [
+ "name",
+ "type",
+ "is_optional",
+ "is_array",
+ "is_sliceable",
+ "is_prefix",
+])
+
+def strip_optional(val: type) -> type:
+ if typing.get_origin(val) is typing.Union:
+ union = set(typing.get_args(val))
+ if len(union - {NoneType}) == 1:
+ val, = union - {NoneType}
+ else:
+ raise TypeError(f"Non-'typing.Optional' 'typing.Union' is not supported: {typing._type_repr(val)!r}")
+ return val
+
+def is_array(val, *, _inner=False):
+ """
+ Hacky way to check if a value or type is an array.
+ The hack omits having to depend on large frameworks such as pytorch or pandas
+ """
+ val = strip_optional(val)
+ if val is H5Array or val is H5ArrayNoSlice:
+ return True
+
+ if typing._type_repr(val) in (
+ "numpy.ndarray",
+ "torch.Tensor",
+ ):
+ return True
+ if not _inner:
+ return is_array(type(val), _inner=True)
+ return False
+
+def prod(numbers: typing.Iterable[T], initial: typing.Optional[T] = None) -> T:
+ if initial is not None:
+ return functools.reduce(operator.mul, numbers, initial)
+ else:
+ return functools.reduce(operator.mul, numbers)
+
+class DataclassMeta(type):
+ def __new__(
+ mcls,
+ name : str,
+ bases : tuple[type, ...],
+ attrs : dict[str, typing.Any],
+ **kwargs,
+ ):
+ cls = super().__new__(mcls, name, bases, attrs, **kwargs)
+ if sys.version_info[:2] >= (3, 10) and not hasattr(cls, "__slots__"):
+ cls = dataclasses.dataclass(slots=True)(cls)
+ else:
+ cls = dataclasses.dataclass(cls)
+ return cls
+
+class DataclassABCMeta(DataclassMeta, ABCMeta):
+ pass
+
+class Dataclass(metaclass=DataclassMeta):
+ def __getitem__(self, key: str) -> typing.Any:
+ if key in self.keys():
+ return getattr(self, key)
+ raise KeyError(key)
+
+ def __setitem__(self, key: str, value: typing.Any):
+ if key in self.keys():
+ return setattr(self, key, value)
+ raise KeyError(key)
+
+ def keys(self) -> typing.KeysView:
+ return self.as_dict().keys()
+
+ def values(self) -> typing.ValuesView:
+ return self.as_dict().values()
+
+ def items(self) -> typing.ItemsView:
+ return self.as_dict().items()
+
+ def as_dict(self, properties_to_include: set[str] = None, **kw) -> dict[str, typing.Any]:
+ out = dataclasses.asdict(self, **kw)
+ for name in (properties_to_include or []):
+ out[name] = getattr(self, name)
+ return out
+
+ def as_tuple(self, properties_to_include: list[str]) -> tuple:
+ out = dataclasses.astuple(self)
+ if not properties_to_include:
+ return out
+ else:
+ return (
+ *out,
+ *(getattr(self, name) for name in properties_to_include),
+ )
+
+ def copy(self: T, *, deep=True) -> T:
+ return (copy.deepcopy if deep else copy.copy)(self)
+
+class H5Dataclass(Dataclass):
+ # settable with class params:
+ _prefix : str = dataclasses.field(init=False, repr=False, default="")
+ _n_pages : int = dataclasses.field(init=False, repr=False, default=10)
+ _require_all : bool = dataclasses.field(init=False, repr=False, default=False)
+
+ def __init_subclass__(cls,
+ prefix : typing.Optional[str] = None,
+ n_pages : typing.Optional[int] = None,
+ require_all : typing.Optional[bool] = None,
+ **kw,
+ ):
+ super().__init_subclass__(**kw)
+ assert dataclasses.is_dataclass(cls)
+ if prefix is not None: cls._prefix = prefix
+ if n_pages is not None: cls._n_pages = n_pages
+ if require_all is not None: cls._require_all = require_all
+
+ @classmethod
+ def _get_fields(cls) -> typing.Iterable[DataclassField]:
+ for field in dataclasses.fields(cls):
+ if not field.init:
+ continue
+ assert field.name not in ("_prefix", "_n_pages", "_require_all"), (
+ f"{field.name!r} can not be in {cls.__qualname__}.__init__.\n"
+ "Set it with dataclasses.field(default=YOUR_VALUE, init=False, repr=False)"
+ )
+ if isinstance(field.type, str):
+ raise TypeError("Type hints are strings, perhaps avoid using `from __future__ import annotations`")
+
+ type_inner = strip_optional(field.type)
+ is_prefix = typing.get_origin(type_inner) is dict and typing.get_args(type_inner)[:1] == (str,)
+ field_type = typing.get_args(type_inner)[1] if is_prefix else field.type
+ if field.default is None or typing.get_origin(field.type) is typing.Union and NoneType in typing.get_args(field.type):
+ field_type = typing.Optional[field_type]
+
+ yield DataclassField(
+ name = field.name,
+ type = strip_optional(field_type),
+ is_optional = typing.get_origin(field_type) is typing.Union and NoneType in typing.get_args(field_type),
+ is_array = is_array(field_type),
+ is_sliceable = is_array(field_type) and strip_optional(field_type) is not H5ArrayNoSlice,
+ is_prefix = is_prefix,
+ )
+
+ @classmethod
+ def from_h5_file(cls : type[T],
+ fname : typing.Union[PathLike, str],
+ *,
+ page : typing.Optional[int] = None,
+ n_pages : typing.Optional[int] = None,
+ read_slice : slice = slice(None),
+ require_even_pages : bool = True,
+ ) -> T:
+ if not isinstance(fname, Path):
+ fname = Path(fname)
+ if n_pages is None:
+ n_pages = cls._n_pages
+ if not fname.exists():
+ raise FileNotFoundError(str(fname))
+ if not h5.is_hdf5(fname):
+ raise TypeError(f"Not a HDF5 file: {str(fname)!r}")
+
+ # if this class has no fields, print a example class:
+ if not any(field.init for field in dataclasses.fields(cls)):
+ with h5.File(fname, "r") as f:
+ klen = max(map(len, f.keys()))
+ example_cls = f"\nclass {cls.__name__}(Dataclass, require_all=True):\n" + "\n".join(
+ f" {k.ljust(klen)} : "
+ + (
+ "H5Array" if prod(v.shape, 1) > 1 else (
+ "float" if issubclass(v.dtype.type, np.floating) else (
+ "int" if issubclass(v.dtype.type, np.integer) else (
+ "bool" if issubclass(v.dtype.type, np.bool_) else (
+ "typing.Any"
+ ))))).ljust(14 + 1)
+ + f" #{repr(v).split(':', 1)[1].removesuffix('>')}"
+ for k, v in f.items()
+ )
+ raise NotImplementedError(f"{cls!r} has no fields!\nPerhaps try the following:{example_cls}")
+
+ fields_consumed = set()
+
+ def make_kwarg(
+ file : h5.File,
+ keys : typing.KeysView,
+ field : DataclassField,
+ ) -> tuple[str, typing.Any]:
+ if field.is_optional:
+ if field.name not in keys:
+ return field.name, None
+ if field.is_sliceable:
+ if page is not None:
+ n_items = int(f[cls._prefix + field.name].shape[0])
+ page_len = n_items // n_pages
+ modulus = n_items % n_pages
+ if modulus: page_len += 1 # round up
+ if require_even_pages and modulus:
+ raise ValueError(f"Field {field.name!r} {tuple(f[cls._prefix + field.name].shape)} is not cleanly divisible into {n_pages} pages")
+ this_slice = slice(
+ start = page_len * page,
+ stop = page_len * (page+1),
+ step = read_slice.step, # inherit step
+ )
+ else:
+ this_slice = read_slice
+ else:
+ this_slice = slice(None) # read all
+
+ # array or scalar?
+ def read_dataset(var):
+ # https://docs.h5py.org/en/stable/high/dataset.html#reading-writing-data
+ if field.is_array:
+ return var[this_slice]
+ if var.shape == (1,):
+ return var[0]
+ else:
+ return var[()]
+
+ if field.is_prefix:
+ fields_consumed.update(
+ key
+ for key in keys if key.startswith(f"{cls._prefix}{field.name}_")
+ )
+ return field.name, {
+ key.removeprefix(f"{cls._prefix}{field.name}_") : read_dataset(file[key])
+ for key in keys if key.startswith(f"{cls._prefix}{field.name}_")
+ }
+ else:
+ fields_consumed.add(cls._prefix + field.name)
+ return field.name, read_dataset(file[cls._prefix + field.name])
+
+ with h5.File(fname, "r") as f:
+ keys = f.keys()
+ init_dict = dict( make_kwarg(f, keys, i) for i in cls._get_fields() )
+
+ try:
+ out = cls(**init_dict)
+ except Exception as e:
+ class_attrs = set(field.name for field in dataclasses.fields(cls))
+ file_attr = set(init_dict.keys())
+ raise e.__class__(f"{e}. {class_attrs=}, {file_attr=}, diff={class_attrs.symmetric_difference(file_attr)}") from e
+
+ if cls._require_all:
+ fields_not_consumed = set(keys) - fields_consumed
+ if fields_not_consumed:
+ raise ValueError(f"Not all HDF5 fields consumed: {fields_not_consumed!r}")
+
+ return out
+
+ def to_h5_file(self,
+ fname : PathLike,
+ mkdir : bool = False,
+ ):
+ if not isinstance(fname, Path):
+ fname = Path(fname)
+ if not fname.parent.is_dir():
+ if mkdir:
+ fname.parent.mkdir(parents=True)
+ else:
+ raise NotADirectoryError(fname.parent)
+
+ with h5.File(fname, "w") as f:
+ for field in type(self)._get_fields():
+ if field.is_optional and getattr(self, field.name) is None:
+ continue
+ value = getattr(self, field.name)
+ if field.is_array:
+ if any(type(i) is not np.ndarray for i in (value.values() if field.is_prefix else [value])):
+ raise TypeError(
+ "When dumping a H5Dataclass, make sure the array fields are "
+ f"numpy arrays (the type of {field.name!r} is {typing._type_repr(type(value))}).\n"
+ "Example: h5dataclass.map_arrays(torch.Tensor.numpy)"
+ )
+ else:
+ pass
+
+ def write_value(key: str, value: typing.Any):
+ if field.is_array:
+ f.create_dataset(key, data=value, **hdf5plugin.LZ4())
+ else:
+ f.create_dataset(key, data=value)
+
+ if field.is_prefix:
+ for k, v in value.items():
+ write_value(self._prefix + field.name + "_" + k, v)
+ else:
+ write_value(self._prefix + field.name, value)
+
+ def map_arrays(self: T, func: typing.Callable[[H5Array], H5Array], do_copy: bool = False) -> T:
+ if do_copy: # shallow
+ self = self.copy(deep=False)
+ for field in type(self)._get_fields():
+ if field.is_optional and getattr(self, field.name) is None:
+ continue
+ if field.is_prefix and field.is_array:
+ setattr(self, field.name, {
+ k : func(v)
+ for k, v in getattr(self, field.name).items()
+ })
+ elif field.is_array:
+ setattr(self, field.name, func(getattr(self, field.name)))
+
+ return self
+
+ def astype(self: T, t: type, do_copy: bool = False, convert_nonfloats: bool = False) -> T:
+ return self.map_arrays(lambda x: x.astype(t) if convert_nonfloats or not np.issubdtype(x.dtype, int) else x)
+
+ def copy(self: T, *, deep=True) -> T:
+ out = super().copy(deep=deep)
+ if not deep:
+ for field in type(self)._get_fields():
+ if field.is_prefix:
+ out[field.name] = copy.copy(field.name)
+ return out
+
+ @property
+ def shape(self) -> dict[str, tuple[int, ...]]:
+ return {
+ key: value.shape
+ for key, value in self.items()
+ if hasattr(value, "shape")
+ }
+
+class TransformableDataclassMixin(metaclass=DataclassABCMeta):
+
+ @abstractmethod
+ def transform(self: T, mat4: np.ndarray, inplace=False) -> T:
+ ...
+
+ def transform_to(self: T, name: str, inverse_name: str = None, *, inplace=False) -> T:
+ mtx = self.transforms[name]
+ out = self.transform(mtx, inplace=inplace)
+ out.transforms.pop(name) # consumed
+
+ inv = np.linalg.inv(mtx)
+ for key in list(out.transforms.keys()): # maintain the other transforms
+ out.transforms[key] = out.transforms[key] @ inv
+ if inverse_name is not None: # store inverse
+ out.transforms[inverse_name] = inv
+
+ return out
diff --git a/ifield/data/common/mesh.py b/ifield/data/common/mesh.py
new file mode 100644
index 0000000..fc5a6bc
--- /dev/null
+++ b/ifield/data/common/mesh.py
@@ -0,0 +1,48 @@
+from math import pi
+from trimesh import Trimesh
+import numpy as np
+import os
+import trimesh
+import trimesh.transformations as T
+
+DEBUG = bool(os.environ.get("IFIELD_DEBUG", ""))
+
+__doc__ = """
+Here are some helper functions for processing data.
+"""
+
+def rotate_to_closest_axis_aligned_bounds(
+ mesh : Trimesh,
+ order_axes : bool = True,
+ fail_ok : bool = True,
+ ) -> np.ndarray:
+ to_origin_mat4, extents = trimesh.bounds.oriented_bounds(mesh, ordered=not order_axes)
+ to_aabb_rot_mat4 = T.euler_matrix(*T.decompose_matrix(to_origin_mat4)[3])
+
+ if not order_axes:
+ return to_aabb_rot_mat4
+
+ v = pi / 4 * 1.01 # tolerance
+ v2 = pi / 2
+
+ faces = (
+ (0, 0),
+ (1, 0),
+ (2, 0),
+ (3, 0),
+ (0, 1),
+ (0,-1),
+ )
+ orientations = [ # 6 faces x 4 rotations per face
+ (f[0] * v2, f[1] * v2, i * v2)
+ for i in range(4)
+ for f in faces]
+
+ for x, y, z in orientations:
+ mat4 = T.euler_matrix(x, y, z) @ to_aabb_rot_mat4
+ ai, aj, ak = T.euler_from_matrix(mat4)
+ if abs(ai) <= v and abs(aj) <= v and abs(ak) <= v:
+ return mat4
+
+ if fail_ok: return to_aabb_rot_mat4
+ raise Exception("Unable to orient mesh")
diff --git a/ifield/data/common/points.py b/ifield/data/common/points.py
new file mode 100644
index 0000000..58cb9da
--- /dev/null
+++ b/ifield/data/common/points.py
@@ -0,0 +1,297 @@
+from __future__ import annotations
+from ...utils.helpers import compose
+from functools import reduce, lru_cache
+from math import ceil
+from typing import Iterable
+import numpy as np
+import operator
+
+__doc__ = """
+Here are some helper functions for processing data.
+"""
+
+
+def img2col(img: np.ndarray, psize: int) -> np.ndarray:
+ # based of ycb_generate_point_cloud.py provided by YCB
+
+ n_channels = 1 if len(img.shape) == 2 else img.shape[0]
+ n_channels, rows, cols = (1,) * (3 - len(img.shape)) + img.shape
+
+ # pad the image
+ img_pad = np.zeros((
+ n_channels,
+ int(ceil(1.0 * rows / psize) * psize),
+ int(ceil(1.0 * cols / psize) * psize),
+ ))
+ img_pad[:, 0:rows, 0:cols] = img
+
+ # allocate output buffer
+ final = np.zeros((
+ img_pad.shape[1],
+ img_pad.shape[2],
+ n_channels,
+ psize,
+ psize,
+ ))
+
+ for c in range(n_channels):
+ for x in range(psize):
+ for y in range(psize):
+ img_shift = np.vstack((
+ img_pad[c, x:],
+ img_pad[c, :x]))
+ img_shift = np.column_stack((
+ img_shift[:, y:],
+ img_shift[:, :y]))
+ final[x::psize, y::psize, c] = np.swapaxes(
+ img_shift.reshape(
+ int(img_pad.shape[1] / psize), psize,
+ int(img_pad.shape[2] / psize), psize),
+ 1,
+ 2)
+
+ # crop output and unwrap axes with size==1
+ return np.squeeze(final[
+ 0:rows - psize + 1,
+ 0:cols - psize + 1])
+
+def filter_depth_discontinuities(depth_map: np.ndarray, filt_size = 7, thresh = 1000) -> np.ndarray:
+ """
+ Removes data close to discontinuities, with size filt_size.
+ """
+ # based of ycb_generate_point_cloud.py provided by YCB
+
+ # Ensure that filter sizes are okay
+ assert filt_size % 2, "Can only use odd filter sizes."
+
+ # Compute discontinuities
+ offset = int(filt_size - 1) // 2
+ patches = 1.0 * img2col(depth_map, filt_size)
+ mids = patches[:, :, offset, offset]
+ mins = np.min(patches, axis=(2, 3))
+ maxes = np.max(patches, axis=(2, 3))
+
+ discont = np.maximum(
+ np.abs(mins - mids),
+ np.abs(maxes - mids))
+ mark = discont > thresh
+
+ # Account for offsets
+ final_mark = np.zeros(depth_map.shape, dtype=np.uint16)
+ final_mark[offset:offset + mark.shape[0],
+ offset:offset + mark.shape[1]] = mark
+
+ return depth_map * (1 - final_mark)
+
+def reorient_depth_map(
+ depth_map : np.ndarray,
+ rgb_map : np.ndarray,
+ depth_mat3 : np.ndarray, # 3x3 intrinsic camera matrix
+ depth_vec5 : np.ndarray, # 5 distortion parameters (k1, k2, p1, p2, k3)
+ rgb_mat3 : np.ndarray, # 3x3 intrinsic camera matrix
+ rgb_vec5 : np.ndarray, # 5 distortion parameters (k1, k2, p1, p2, k3)
+ ir_to_rgb_mat4 : np.ndarray, # extrinsic transformation matrix from depth to rgb camera viewpoint
+ rgb_mask_map : np.ndarray = None,
+ _output_points = False, # retval (H, W) if false else (N, XYZRGB)
+ _output_hits_uvs = False, # retval[1] is dtype=bool of hits shaped like depth_map
+ ) -> np.ndarray:
+
+ """
+ Corrects depth_map to be from the same view as the rgb_map, with the same dimensions.
+ If _output_points is True, the points returned are in the rgb camera space.
+ """
+ # based of ycb_generate_point_cloud.py provided by YCB
+ # now faster AND more easy on the GIL
+
+ height_old, width_old, *_ = depth_map.shape
+ height, width, *_ = rgb_map.shape
+
+
+ d_cx, r_cx = depth_mat3[0, 2], rgb_mat3[0, 2] # optical center
+ d_cy, r_cy = depth_mat3[1, 2], rgb_mat3[1, 2]
+ d_fx, r_fx = depth_mat3[0, 0], rgb_mat3[0, 0] # focal length
+ d_fy, r_fy = depth_mat3[1, 1], rgb_mat3[1, 1]
+ d_k1, d_k2, d_p1, d_p2, d_k3 = depth_vec5
+ c_k1, c_k2, c_p1, c_p2, c_k3 = rgb_vec5
+
+ # make a UV grid over depth_map
+ u, v = np.meshgrid(
+ np.arange(width_old),
+ np.arange(height_old),
+ )
+
+ # compute xyz coordinates for all depths
+ xyz_depth = np.stack((
+ (u - d_cx) / d_fx,
+ (v - d_cy) / d_fy,
+ depth_map,
+ np.ones(depth_map.shape)
+ )).reshape((4, -1))
+ xyz_depth = xyz_depth[:, xyz_depth[2] != 0]
+
+ # undistort depth coordinates
+ d_x, d_y = xyz_depth[:2]
+ r = np.linalg.norm(xyz_depth[:2], axis=0)
+ xyz_depth[0, :] \
+ = d_x / (1 + d_k1*r**2 + d_k2*r**4 + d_k3*r**6) \
+ - (2*d_p1*d_x*d_y + d_p2*(r**2 + 2*d_x**2))
+ xyz_depth[1, :] \
+ = d_y / (1 + d_k1*r**2 + d_k2*r**4 + d_k3*r**6) \
+ - (d_p1*(r**2 + 2*d_y**2) + 2*d_p2*d_x*d_y)
+
+ # unproject x and y
+ xyz_depth[0, :] *= xyz_depth[2, :]
+ xyz_depth[1, :] *= xyz_depth[2, :]
+
+ # convert depths to RGB camera viewpoint
+ xyz_rgb = ir_to_rgb_mat4 @ xyz_depth
+
+ # project depths to RGB canvas
+ rgb_z_inv = 1 / xyz_rgb[2] # perspective correction
+ rgb_uv = np.stack((
+ xyz_rgb[0] * rgb_z_inv * r_fx + r_cx + 0.5,
+ xyz_rgb[1] * rgb_z_inv * r_fy + r_cy + 0.5,
+ )).astype(np.int)
+
+ # mask of the rgb_xyz values within view of rgb_map
+ mask = reduce(operator.and_, [
+ rgb_uv[0] >= 0,
+ rgb_uv[1] >= 0,
+ rgb_uv[0] < width,
+ rgb_uv[1] < height,
+ ])
+ if rgb_mask_map is not None:
+ mask[mask] &= rgb_mask_map[
+ rgb_uv[1, mask],
+ rgb_uv[0, mask]]
+
+ if not _output_points: # output image
+ output = np.zeros((height, width), dtype=depth_map.dtype)
+ output[
+ rgb_uv[1, mask],
+ rgb_uv[0, mask],
+ ] = xyz_rgb[2, mask]
+
+ else: # output pointcloud
+ rgbs = rgb_map[ # lookup rgb values using rgb_uv
+ rgb_uv[1, mask],
+ rgb_uv[0, mask]]
+ output = np.stack((
+ xyz_rgb[0, mask], # x
+ xyz_rgb[1, mask], # y
+ xyz_rgb[2, mask], # z
+ rgbs[:, 0], # r
+ rgbs[:, 1], # g
+ rgbs[:, 2], # b
+ )).T
+
+ # output for realsies
+ if not _output_hits_uvs: #raw
+ return output
+ else: # with hit mask
+ uv = np.zeros((height, width), dtype=bool)
+ # filter points overlapping in the depth map
+ uv_indices = (
+ rgb_uv[1, mask],
+ rgb_uv[0, mask],
+ )
+ _, chosen = np.unique( uv_indices[0] << 32 | uv_indices[1], return_index=True )
+ output = output[chosen, :]
+ uv[uv_indices] = True
+ return output, uv
+
+def join_rgb_and_depth_to_points(*a, **kw) -> np.ndarray:
+ return reorient_depth_map(*a, _output_points=True, **kw)
+
+@compose(np.array) # block lru cache mutation
+@lru_cache(maxsize=1)
+@compose(list)
+def generate_equidistant_sphere_points(
+ n : int,
+ centroid : np.ndarray = (0, 0, 0),
+ radius : float = 1,
+ compute_sphere_coordinates : bool = False,
+ compute_normals : bool = False,
+ shift_theta : bool = False,
+ ) -> Iterable[tuple[float, ...]]:
+ # Deserno M. How to generate equidistributed points on the surface of a sphere
+ # https://www.cmu.edu/biolphys/deserno/pdf/sphere_equi.pdf
+
+ if compute_sphere_coordinates and compute_normals:
+ raise ValueError(
+ "'compute_sphere_coordinates' and 'compute_normals' are mutually exclusive"
+ )
+
+ n_count = 0
+ a = 4 * np.pi / n
+ d = np.sqrt(a)
+ n_theta = round(np.pi / d)
+ d_theta = np.pi / n_theta
+ d_phi = a / d_theta
+
+ for i in range(0, n_theta):
+ theta = np.pi * (i + 0.5) / n_theta
+ n_phi = round(2 * np.pi * np.sin(theta) / d_phi)
+
+ for j in range(0, n_phi):
+ phi = 2 * np.pi * j / n_phi
+
+ if compute_sphere_coordinates: # (theta, phi)
+ yield (
+ theta if shift_theta else theta - 0.5*np.pi,
+ phi,
+ )
+ elif compute_normals: # (x, y, z, nx, ny, nz)
+ yield (
+ centroid[0] + radius * np.sin(theta) * np.cos(phi),
+ centroid[1] + radius * np.sin(theta) * np.sin(phi),
+ centroid[2] + radius * np.cos(theta),
+ np.sin(theta) * np.cos(phi),
+ np.sin(theta) * np.sin(phi),
+ np.cos(theta),
+ )
+ else: # (x, y, z)
+ yield (
+ centroid[0] + radius * np.sin(theta) * np.cos(phi),
+ centroid[1] + radius * np.sin(theta) * np.sin(phi),
+ centroid[2] + radius * np.cos(theta),
+ )
+ n_count += 1
+
+
+def generate_random_sphere_points(
+ n : int,
+ centroid : np.ndarray = (0, 0, 0),
+ radius : float = 1,
+ compute_sphere_coordinates : bool = False,
+ compute_normals : bool = False,
+ shift_theta : bool = False, # depends on convention
+ ) -> np.ndarray:
+ if compute_sphere_coordinates and compute_normals:
+ raise ValueError(
+ "'compute_sphere_coordinates' and 'compute_normals' are mutually exclusive"
+ )
+
+ theta = np.arcsin(np.random.uniform(-1, 1, n)) # inverse transform sampling
+ phi = np.random.uniform(0, 2*np.pi, n)
+
+ if compute_sphere_coordinates: # (theta, phi)
+ return np.stack((
+ theta if not shift_theta else 0.5*np.pi + theta,
+ phi,
+ ), axis=1)
+ elif compute_normals: # (x, y, z, nx, ny, nz)
+ return np.stack((
+ centroid[0] + radius * np.cos(theta) * np.cos(phi),
+ centroid[1] + radius * np.cos(theta) * np.sin(phi),
+ centroid[2] + radius * np.sin(theta),
+ np.cos(theta) * np.cos(phi),
+ np.cos(theta) * np.sin(phi),
+ np.sin(theta),
+ ), axis=1)
+ else: # (x, y, z)
+ return np.stack((
+ centroid[0] + radius * np.cos(theta) * np.cos(phi),
+ centroid[1] + radius * np.cos(theta) * np.sin(phi),
+ centroid[2] + radius * np.sin(theta),
+ ), axis=1)
diff --git a/ifield/data/common/processing.py b/ifield/data/common/processing.py
new file mode 100644
index 0000000..f884685
--- /dev/null
+++ b/ifield/data/common/processing.py
@@ -0,0 +1,85 @@
+from .h5_dataclasses import H5Dataclass
+from datetime import datetime, timedelta
+from pathlib import Path
+from typing import Hashable, Optional, Callable
+import os
+
+DEBUG = bool(os.environ.get("IFIELD_DEBUG", ""))
+
+__doc__ = """
+Here are some helper functions for processing data.
+"""
+
+# multiprocessing does not work due to my rediculous use of closures, which seemingly cannot be pickled
+# paralelize it in the shell instead
+
+def precompute_data(
+ computer : Callable[[Hashable], Optional[H5Dataclass]],
+ identifiers : list[Hashable],
+ output_paths : list[Path],
+ page : tuple[int, int] = (0, 1),
+ *,
+ force : bool = False,
+ debug : bool = False,
+ ):
+ """
+ precomputes data and stores them as HDF5 datasets using `.to_file(path: Path)`
+ """
+
+ page, n_pages = page
+ assert len(identifiers) == len(output_paths)
+
+ total = len(identifiers)
+ identifier_max_len = max(map(len, map(str, identifiers)))
+ t_epoch = None
+ def log(state: str, is_start = False):
+ nonlocal t_epoch
+ if is_start: t_epoch = datetime.now()
+ td = timedelta(0) if is_start else datetime.now() - t_epoch
+ print(" - "
+ f"{str(index+1).rjust(len(str(total)))}/{total}: "
+ f"{str(identifier).ljust(identifier_max_len)} @ {td}: {state}"
+ )
+
+ print(f"precompute_data(computer={computer.__module__}.{computer.__qualname__}, identifiers=..., force={force}, page={page})")
+ t_begin = datetime.now()
+ failed = []
+
+ # pagination
+ page_size = total // n_pages + bool(total % n_pages)
+ jobs = list(zip(identifiers, output_paths))[page_size*page : page_size*(page+1)]
+
+ for index, (identifier, output_path) in enumerate(jobs, start=page_size*page):
+ if not force and output_path.exists() and output_path.stat().st_size > 0:
+ continue
+
+ log("compute", is_start=True)
+
+ # compute
+ try:
+ res = computer(identifier)
+ except Exception as e:
+ failed.append(identifier)
+ log(f"failed compute: {e.__class__.__name__}: {e}")
+ if DEBUG or debug: raise e
+ continue
+ if res is None:
+ failed.append(identifier)
+ log("no result")
+ continue
+
+ # write to file
+ try:
+ output_path.parent.mkdir(parents=True, exist_ok=True)
+ res.to_h5_file(output_path)
+ except Exception as e:
+ failed.append(identifier)
+ log(f"failed write: {e.__class__.__name__}: {e}")
+ if output_path.is_file(): output_path.unlink() # cleanup
+ if DEBUG or debug: raise e
+ continue
+
+ log("done")
+
+ print("precompute_data finished in", datetime.now() - t_begin)
+ print("failed:", failed or None)
diff --git a/ifield/data/common/scan.py b/ifield/data/common/scan.py
new file mode 100644
index 0000000..0dc6782
--- /dev/null
+++ b/ifield/data/common/scan.py
@@ -0,0 +1,768 @@
+from ...utils.helpers import compose
+from . import points
+from .h5_dataclasses import H5Dataclass, H5Array, H5ArrayNoSlice, TransformableDataclassMixin
+from methodtools import lru_cache
+from sklearn.neighbors import BallTree
+import faiss
+from trimesh import Trimesh
+from typing import Iterable
+from typing import Optional, TypeVar
+import mesh_to_sdf
+import mesh_to_sdf.scan as sdf_scan
+import numpy as np
+import trimesh
+import trimesh.transformations as T
+import warnings
+
+__doc__ = """
+Here are some helper types for data.
+"""
+
+_T = TypeVar("T")
+
+class InvalidateLRUOnWriteMixin:
+ def __setattr__(self, key, value):
+ if not key.startswith("__wire|"):
+ for attr in dir(self):
+ if attr.startswith("__wire|"):
+ getattr(self, attr).cache_clear()
+ return super().__setattr__(key, value)
+def lru_property(func):
+ return lru_cache(maxsize=1)(property(func))
+
+class SingleViewScan(H5Dataclass, TransformableDataclassMixin, InvalidateLRUOnWriteMixin, require_all=True):
+ points_hit : H5ArrayNoSlice # (N, 3)
+ normals_hit : Optional[H5ArrayNoSlice] # (N, 3)
+ points_miss : H5ArrayNoSlice # (M, 3)
+ distances_miss : Optional[H5ArrayNoSlice] # (M)
+ colors_hit : Optional[H5ArrayNoSlice] # (N, 3)
+ colors_miss : Optional[H5ArrayNoSlice] # (M, 3)
+ uv_hits : Optional[H5ArrayNoSlice] # (H, W) dtype=bool
+ uv_miss : Optional[H5ArrayNoSlice] # (H, W) dtype=bool (the reason we store both is due to missing data depth sensor data or filtered backfaces)
+ cam_pos : H5ArrayNoSlice # (3)
+ cam_mat4 : Optional[H5ArrayNoSlice] # (4, 4)
+ proj_mat4 : Optional[H5ArrayNoSlice] # (4, 4)
+ transforms : dict[str, H5ArrayNoSlice] # a map of 4x4 transformation matrices
+
+ def transform(self: _T, mat4: np.ndarray, inplace=False) -> _T:
+ scale_xyz = mat4[:3, :3].sum(axis=0) # https://math.stackexchange.com/a/1463487
+ assert all(scale_xyz - scale_xyz[0] < 1e-8), f"differenty scaled axes: {scale_xyz}"
+
+ out = self if inplace else self.copy(deep=False)
+ out.points_hit = T.transform_points(self.points_hit, mat4)
+ out.normals_hit = T.transform_points(self.normals_hit, mat4) if self.normals_hit is not None else None
+ out.points_miss = T.transform_points(self.points_miss, mat4)
+ out.distances_miss = self.distances_miss * scale_xyz
+ out.cam_pos = T.transform_points(self.points_cam, mat4)[-1]
+ out.cam_mat4 = (mat4 @ self.cam_mat4) if self.cam_mat4 is not None else None
+ out.proj_mat4 = (mat4 @ self.proj_mat4) if self.proj_mat4 is not None else None
+ return out
+
+ def compute_miss_distances(self: _T, *, copy: bool = False, deep: bool = False) -> _T:
+ assert not self.has_miss_distances
+ if not self.is_hitting:
+ raise ValueError("No hits to compute the ray distance towards")
+
+ out = self.copy(deep=deep) if copy else self
+ out.distances_miss \
+ = distance_from_rays_to_point_cloud(
+ ray_origins = out.points_cam,
+ ray_dirs = out.ray_dirs_miss,
+ points = out.points_hit,
+ ).astype(out.points_cam.dtype)
+
+ return out
+
+ @lru_property
+ def points(self) -> np.ndarray: # (N+M+1, 3)
+ return np.concatenate((
+ self.points_hit,
+ self.points_miss,
+ self.points_cam,
+ ))
+
+ @lru_property
+ def uv_points(self) -> np.ndarray: # (N+M+1, 3)
+ if not self.has_uv: raise ValueError
+ out = np.full((*self.uv_hits.shape, 3), np.nan, dtype=self.points_hit.dtype)
+ out[self.uv_hits, :] = self.points_hit
+ out[self.uv_miss, :] = self.points_miss
+ return out
+
+ @lru_property
+ def uv_normals(self) -> np.ndarray: # (N+M+1, 3)
+ if not self.has_uv: raise ValueError
+ out = np.full((*self.uv_hits.shape, 3), np.nan, dtype=self.normals_hit.dtype)
+ out[self.uv_hits, :] = self.normals_hit
+ return out
+
+ @lru_property
+ def points_cam(self) -> Optional[np.ndarray]: # (1, 3)
+ if self.cam_pos is None: return None
+ return self.cam_pos[None, :]
+
+ @lru_property
+ def points_hit_centroid(self) -> np.ndarray:
+ return self.points_hit.mean(axis=0)
+
+ @lru_property
+ def points_hit_std(self) -> np.ndarray:
+ return self.points_hit.std(axis=0)
+
+ @lru_property
+ def is_hitting(self) -> bool:
+ return len(self.points_hit) > 0
+
+ @lru_property
+ def is_empty(self) -> bool:
+ return not (len(self.points_hit) or len(self.points_miss))
+
+ @lru_property
+ def has_colors(self) -> bool:
+ return self.colors_hit is not None or self.colors_miss is not None
+
+ @lru_property
+ def has_normals(self) -> bool:
+ return self.normals_hit is not None
+
+ @lru_property
+ def has_uv(self) -> bool:
+ return self.uv_hits is not None
+
+ @lru_property
+ def has_miss_distances(self) -> bool:
+ return self.distances_miss is not None
+
+ @lru_property
+ def xyzrgb_hit(self) -> np.ndarray: # (N, 6)
+ if self.colors_hit is None: raise ValueError
+ return np.concatenate([self.points_hit, self.colors_hit], axis=1)
+
+ @lru_property
+ def xyzrgb_miss(self) -> np.ndarray: # (M, 6)
+ if self.colors_miss is None: raise ValueError
+ return np.concatenate([self.points_miss, self.colors_miss], axis=1)
+
+ @lru_property
+ def ray_dirs_hit(self) -> np.ndarray: # (N, 3)
+ out = self.points_hit - self.points_cam
+ out /= np.linalg.norm(out, axis=-1)[:, None] # normalize
+ return out
+
+ @lru_property
+ def ray_dirs_miss(self) -> np.ndarray: # (N, 3)
+ out = self.points_miss - self.points_cam
+ out /= np.linalg.norm(out, axis=-1)[:, None] # normalize
+ return out
+
+ @classmethod
+ def from_mesh_single_view(cls, mesh: Trimesh, *, compute_miss_distances: bool = False, **kw) -> "SingleViewScan":
+ if "phi" not in kw and not "theta" in kw:
+ kw["theta"], kw["phi"] = points.generate_random_sphere_points(1, compute_sphere_coordinates=True)[0]
+ scan = sample_single_view_scan_from_mesh(mesh, **kw)
+ if compute_miss_distances and scan.is_hitting:
+ scan.compute_miss_distances()
+ return scan
+
+ def to_uv_scan(self) -> "SingleViewUVScan":
+ return SingleViewUVScan.from_scan(self)
+
+ @classmethod
+ def from_uv_scan(self, uvscan: "SingleViewUVScan") -> "SingleViewUVScan":
+ return uvscan.to_scan()
+
+# The same, but with support for pagination (should have been this way since the start...)
+class SingleViewUVScan(H5Dataclass, TransformableDataclassMixin, InvalidateLRUOnWriteMixin, require_all=True):
+ # B may be (N) or (H, W), the latter may be flattened
+ hits : H5Array # (*B) dtype=bool
+ miss : H5Array # (*B) dtype=bool (the reason we store both is due to missing data depth sensor data or filtered backface hits)
+ points : H5Array # (*B, 3) on far plane if miss, NaN if neither hit or miss
+ normals : Optional[H5Array] # (*B, 3) NaN if not hit
+ colors : Optional[H5Array] # (*B, 3)
+ distances : Optional[H5Array] # (*B) NaN if not miss
+ cam_pos : Optional[H5ArrayNoSlice] # (3) or (*B, 3)
+ cam_mat4 : Optional[H5ArrayNoSlice] # (4, 4)
+ proj_mat4 : Optional[H5ArrayNoSlice] # (4, 4)
+ transforms : dict[str, H5ArrayNoSlice] # a map of 4x4 transformation matrices
+
+ @classmethod
+ def from_scan(cls, scan: SingleViewScan):
+ if not scan.has_uv:
+ raise ValueError("Scan cloud has no UV data")
+ hits, miss = scan.uv_hits, scan.uv_miss
+ dtype = scan.points_hit.dtype
+ assert hits.ndim in (1, 2), hits.ndim
+ assert hits.shape == miss.shape, (hits.shape, miss.shape)
+
+ points = np.full((*hits.shape, 3), np.nan, dtype=dtype)
+ points[hits, :] = scan.points_hit
+ points[miss, :] = scan.points_miss
+
+ normals = None
+ if scan.has_normals:
+ normals = np.full((*hits.shape, 3), np.nan, dtype=dtype)
+ normals[hits, :] = scan.normals_hit
+
+ distances = None
+ if scan.has_miss_distances:
+ distances = np.full(hits.shape, np.nan, dtype=dtype)
+ distances[miss] = scan.distances_miss
+
+ colors = None
+ if scan.has_colors:
+ colors = np.full((*hits.shape, 3), np.nan, dtype=dtype)
+ if scan.colors_hit is not None:
+ colors[hits, :] = scan.colors_hit
+ if scan.colors_miss is not None:
+ colors[miss, :] = scan.colors_miss
+
+ return cls(
+ hits = hits,
+ miss = miss,
+ points = points,
+ normals = normals,
+ colors = colors,
+ distances = distances,
+ cam_pos = scan.cam_pos,
+ cam_mat4 = scan.cam_mat4,
+ proj_mat4 = scan.proj_mat4,
+ transforms = scan.transforms,
+ )
+
+ def to_scan(self) -> "SingleViewScan":
+ if not self.is_single_view: raise ValueError
+ return SingleViewScan(
+ points_hit = self.points [self.hits, :],
+ points_miss = self.points [self.miss, :],
+ normals_hit = self.normals [self.hits, :] if self.has_normals else None,
+ distances_miss = self.distances[self.miss] if self.has_miss_distances else None,
+ colors_hit = self.colors [self.hits, :] if self.has_colors else None,
+ colors_miss = self.colors [self.miss, :] if self.has_colors else None,
+ uv_hits = self.hits,
+ uv_miss = self.miss,
+ cam_pos = self.cam_pos,
+ cam_mat4 = self.cam_mat4,
+ proj_mat4 = self.proj_mat4,
+ transforms = self.transforms,
+ )
+
+ def to_mesh(self) -> trimesh.Trimesh:
+ faces: list[(tuple[int, int],)*3] = []
+ for x in range(self.hits.shape[0]-1):
+ for y in range(self.hits.shape[1]-1):
+ c11 = x, y
+ c12 = x, y+1
+ c22 = x+1, y+1
+ c21 = x+1, y
+
+ n = sum(map(self.hits.__getitem__, (c11, c12, c22, c21)))
+ if n == 3:
+ faces.append((*filter(self.hits.__getitem__, (c11, c12, c22, c21)),))
+ elif n == 4:
+ faces.append((c11, c12, c22))
+ faces.append((c11, c22, c21))
+ xy2idx = {c:i for i, c in enumerate(set(k for j in faces for k in j))}
+ assert self.colors is not None
+ return trimesh.Trimesh(
+ vertices = [self.points[i] for i in xy2idx.keys()],
+ vertex_colors = [self.colors[i] for i in xy2idx.keys()] if self.colors is not None else None,
+ faces = [tuple(xy2idx[i] for i in face) for face in faces],
+ )
+
+ def transform(self: _T, mat4: np.ndarray, inplace=False) -> _T:
+ scale_xyz = mat4[:3, :3].sum(axis=0) # https://math.stackexchange.com/a/1463487
+ assert all(scale_xyz - scale_xyz[0] < 1e-8), f"differenty scaled axes: {scale_xyz}"
+
+ unflat = self.hits.shape
+ flat = np.product(unflat)
+
+ out = self if inplace else self.copy(deep=False)
+ out.points = T.transform_points(self.points .reshape((*flat, 3)), mat4).reshape((*unflat, 3))
+ out.normals = T.transform_points(self.normals.reshape((*flat, 3)), mat4).reshape((*unflat, 3)) if self.normals_hit is not None else None
+ out.distances = self.distances_miss * scale_xyz
+ out.cam_pos = T.transform_points(self.cam_pos[None, ...], mat4)[0]
+ out.cam_mat4 = (mat4 @ self.cam_mat4) if self.cam_mat4 is not None else None
+ out.proj_mat4 = (mat4 @ self.proj_mat4) if self.proj_mat4 is not None else None
+ return out
+
+ def compute_miss_distances(self: _T, *, copy: bool = False, deep: bool = False, surface_points: Optional[np.ndarray] = None) -> _T:
+ assert not self.has_miss_distances
+
+ shape = self.hits.shape
+
+ out = self.copy(deep=deep) if copy else self
+ out.distances = np.zeros(shape, dtype=self.points.dtype)
+ if self.is_hitting:
+ out.distances[self.miss] \
+ = distance_from_rays_to_point_cloud(
+ ray_origins = self.cam_pos_unsqueezed_miss,
+ ray_dirs = self.ray_dirs_miss,
+ points = surface_points if surface_points is not None else self.points[self.hits],
+ )
+
+ return out
+
+ def fill_missing_points(self: _T, *, copy: bool = False, deep: bool = False) -> _T:
+ """
+ Fill in missing points as hitting the far plane.
+ """
+ if not self.is_2d:
+ raise ValueError("Cannot fill missing points for non-2d scan!")
+ if not self.is_single_view:
+ raise ValueError("Cannot fill missing points for non-single-view scans!")
+ if self.cam_mat4 is None:
+ raise ValueError("cam_mat4 is None")
+ if self.proj_mat4 is None:
+ raise ValueError("proj_mat4 is None")
+
+ uv = np.argwhere(self.missing).astype(self.points.dtype)
+ uv[:, 0] /= (self.missing.shape[1] - 1) / 2
+ uv[:, 1] /= (self.missing.shape[0] - 1) / 2
+ uv -= 1
+ uv = np.stack((
+ uv[:, 1],
+ -uv[:, 0],
+ np.ones(uv.shape[0]), # far clipping plane
+ np.ones(uv.shape[0]), # homogeneous coordinate
+ ), axis=-1)
+ uv = uv @ (self.cam_mat4 @ np.linalg.inv(self.proj_mat4)).T
+
+ out = self.copy(deep=deep) if copy else self
+ out.points[self.missing, :] = uv[:, :3] / uv[:, 3][:, None]
+ return out
+
+ @lru_property
+ def is_hitting(self) -> bool:
+ return np.any(self.hits)
+
+ @lru_property
+ def has_colors(self) -> bool:
+ return not self.colors is None
+
+ @lru_property
+ def has_normals(self) -> bool:
+ return not self.normals is None
+
+ @lru_property
+ def has_miss_distances(self) -> bool:
+ return not self.distances is None
+
+ @lru_property
+ def any_missing(self) -> bool:
+ return np.any(self.missing)
+
+ @lru_property
+ def has_missing(self) -> bool:
+ return self.any_missing and not np.any(np.isnan(self.points[self.missing]))
+
+ @lru_property
+ def cam_pos_unsqueezed(self) -> H5Array:
+ if self.cam_pos.ndim != 1:
+ return self.cam_pos
+ else:
+ cam_pos = self.cam_pos
+ for _ in range(self.hits.ndim):
+ cam_pos = cam_pos[None, ...]
+ return cam_pos
+
+ @lru_property
+ def cam_pos_unsqueezed_hit(self) -> H5Array:
+ if self.cam_pos.ndim != 1:
+ return self.cam_pos[self.hits, :]
+ else:
+ return self.cam_pos[None, :]
+
+ @lru_property
+ def cam_pos_unsqueezed_miss(self) -> H5Array:
+ if self.cam_pos.ndim != 1:
+ return self.cam_pos[self.miss, :]
+ else:
+ return self.cam_pos[None, :]
+
+ @lru_property
+ def ray_dirs(self) -> H5Array:
+ return (self.points - self.cam_pos_unsqueezed) * (1 / self.depths[..., None])
+
+ @lru_property
+ def ray_dirs_hit(self) -> H5Array:
+ out = self.points[self.hits, :] - self.cam_pos_unsqueezed_hit
+ out /= np.linalg.norm(out, axis=-1)[..., None] # normalize
+ return out
+
+ @lru_property
+ def ray_dirs_miss(self) -> H5Array:
+ out = self.points[self.miss, :] - self.cam_pos_unsqueezed_miss
+ out /= np.linalg.norm(out, axis=-1)[..., None] # normalize
+ return out
+
+ @lru_property
+ def depths(self) -> H5Array:
+ return np.linalg.norm(self.points - self.cam_pos_unsqueezed, axis=-1)
+
+ @lru_property
+ def missing(self) -> H5Array:
+ return ~(self.hits | self.miss)
+
+ @classmethod
+ def from_mesh_single_view(cls, mesh: Trimesh, *, compute_miss_distances: bool = False, **kw) -> "SingleViewUVScan":
+ if "phi" not in kw and not "theta" in kw:
+ kw["theta"], kw["phi"] = points.generate_random_sphere_points(1, compute_sphere_coordinates=True)[0]
+ scan = sample_single_view_scan_from_mesh(mesh, **kw).to_uv_scan()
+ if compute_miss_distances:
+ scan.compute_miss_distances()
+ assert scan.is_2d
+ return scan
+
+ @classmethod
+ def from_mesh_sphere_view(cls, mesh: Trimesh, *, compute_miss_distances: bool = False, **kw) -> "SingleViewUVScan":
+ scan = sample_sphere_view_scan_from_mesh(mesh, **kw)
+ if compute_miss_distances:
+ surface_points = None
+ if scan.hits.sum() > mesh.vertices.shape[0]:
+ surface_points = mesh.vertices.astype(scan.points.dtype)
+ if not kw.get("no_unit_sphere", False):
+ translation, scale = compute_unit_sphere_transform(mesh, dtype=scan.points.dtype)
+ surface_points = (surface_points + translation) * scale
+ scan.compute_miss_distances(surface_points=surface_points)
+ assert scan.is_flat
+ return scan
+
+ def flatten_and_permute_(self: _T, copy=False) -> _T: # inplace by default
+ n_items = np.product(self.hits.shape)
+ permutation = np.random.permutation(n_items)
+
+ out = self.copy(deep=False) if copy else self
+ out.hits = out.hits .reshape((n_items, ))[permutation]
+ out.miss = out.miss .reshape((n_items, ))[permutation]
+ out.points = out.points .reshape((n_items, 3))[permutation, :]
+ out.normals = out.normals .reshape((n_items, 3))[permutation, :] if out.has_normals else None
+ out.colors = out.colors .reshape((n_items, 3))[permutation, :] if out.has_colors else None
+ out.distances = out.distances.reshape((n_items, ))[permutation] if out.has_miss_distances else None
+ return out
+
+ @property
+ def is_single_view(self) -> bool:
+ return np.product(self.cam_pos.shape[:-1]) == 1 if not self.cam_pos is None else True
+
+ @property
+ def is_flat(self) -> bool:
+ return len(self.hits.shape) == 1
+
+ @property
+ def is_2d(self) -> bool:
+ return len(self.hits.shape) == 2
+
+
+# transforms can be found in pytorch3d.transforms and in open3d
+# and in trimesh.transformations
+
+def sample_single_view_scans_from_mesh(
+ mesh : Trimesh,
+ *,
+ n_batches : int,
+ scan_resolution : int = 400,
+ compute_normals : bool = False,
+ fov : float = 1.0472, # 60 degrees in radians, vertical field of view.
+ camera_distance : float = 2,
+ no_filter_backhits : bool = False,
+ ) -> Iterable[SingleViewScan]:
+
+ normalized_mesh_cache = []
+
+ for _ in range(n_batches):
+ theta, phi = points.generate_random_sphere_points(1, compute_sphere_coordinates=True)[0]
+
+ yield sample_single_view_scan_from_mesh(
+ mesh = mesh,
+ phi = phi,
+ theta = theta,
+ _mesh_is_normalized = False,
+ scan_resolution = scan_resolution,
+ compute_normals = compute_normals,
+ fov = fov,
+ camera_distance = camera_distance,
+ no_filter_backhits = no_filter_backhits,
+ _mesh_cache = normalized_mesh_cache,
+ )
+
+def sample_single_view_scan_from_mesh(
+ mesh : Trimesh,
+ *,
+ phi : float,
+ theta : float,
+ scan_resolution : int = 200,
+ compute_normals : bool = False,
+ fov : float = 1.0472, # 60 degrees in radians, vertical field of view.
+ camera_distance : float = 2,
+ no_filter_backhits : bool = False,
+ no_unit_sphere : bool = False,
+ dtype : type = np.float32,
+ _mesh_cache : Optional[list] = None, # provide a list if mesh is reused
+ ) -> SingleViewScan:
+
+ # scale and center to unit sphere
+ is_cache = isinstance(_mesh_cache, list)
+ if is_cache and _mesh_cache and _mesh_cache[0] is mesh:
+ _, mesh, translation, scale = _mesh_cache
+ else:
+ if is_cache:
+ if _mesh_cache:
+ _mesh_cache.clear()
+ _mesh_cache.append(mesh)
+ translation, scale = compute_unit_sphere_transform(mesh)
+ mesh = mesh_to_sdf.scale_to_unit_sphere(mesh)
+ if is_cache:
+ _mesh_cache.extend((mesh, translation, scale))
+
+ z_near = 1
+ z_far = 3
+ cam_mat4 = sdf_scan.get_camera_transform_looking_at_origin(phi, theta, camera_distance=camera_distance)
+ cam_pos = cam_mat4 @ np.array([0, 0, 0, 1])
+
+ scan = sdf_scan.Scan(mesh,
+ camera_transform = cam_mat4,
+ resolution = scan_resolution,
+ calculate_normals = compute_normals,
+ fov = fov,
+ z_near = z_near,
+ z_far = z_far,
+ no_flip_backfaced_normals = True
+ )
+
+ # all the scan rays that hit the far plane, based on sdf_scan.Scan.__init__
+ misses = np.argwhere(scan.depth_buffer == 0)
+ points_miss = np.ones((misses.shape[0], 4))
+ points_miss[:, [1, 0]] = misses.astype(float) / (scan_resolution -1) * 2 - 1
+ points_miss[:, 1] *= -1
+ points_miss[:, 2] = 1 # far plane in clipping space
+ points_miss = points_miss @ (cam_mat4 @ np.linalg.inv(scan.projection_matrix)).T
+ points_miss /= points_miss[:, 3][:, np.newaxis]
+ points_miss = points_miss[:, :3]
+
+ uv_hits = scan.depth_buffer != 0
+ uv_miss = ~uv_hits
+
+ if not no_filter_backhits:
+ if not compute_normals:
+ raise ValueError("not `no_filter_backhits` requires `compute_normals`")
+ # inner product
+ mask = np.einsum('ij,ij->i', scan.points - cam_pos[:3][None, :], scan.normals) < 0
+ scan.points = scan.points [mask, :]
+ scan.normals = scan.normals[mask, :]
+ uv_hits[uv_hits] = mask
+
+ transforms = {}
+
+ # undo unit-sphere transform
+ if no_unit_sphere:
+ scan.points = scan.points * (1 / scale) - translation
+ points_miss = points_miss * (1 / scale) - translation
+ cam_pos[:3] = cam_pos[:3] * (1 / scale) - translation
+ cam_mat4[:3, :] *= 1 / scale
+ cam_mat4[:3, 3] -= translation
+
+ transforms["unit_sphere"] = T.scale_and_translate(scale=scale, translate=translation)
+ transforms["model"] = np.eye(4)
+ else:
+ transforms["model"] = np.linalg.inv(T.scale_and_translate(scale=scale, translate=translation))
+ transforms["unit_sphere"] = np.eye(4)
+
+ return SingleViewScan(
+ normals_hit = scan.normals .astype(dtype),
+ points_hit = scan.points .astype(dtype),
+ points_miss = points_miss .astype(dtype),
+ distances_miss = None,
+ colors_hit = None,
+ colors_miss = None,
+ uv_hits = uv_hits .astype(bool),
+ uv_miss = uv_miss .astype(bool),
+ cam_pos = cam_pos[:3] .astype(dtype),
+ cam_mat4 = cam_mat4 .astype(dtype),
+ proj_mat4 = scan.projection_matrix .astype(dtype),
+ transforms = {k:v.astype(dtype) for k, v in transforms.items()},
+ )
+
+def sample_sphere_view_scan_from_mesh(
+ mesh : Trimesh,
+ *,
+ sphere_points : int = 4000, # resulting rays are n*(n-1)
+ compute_normals : bool = False,
+ no_filter_backhits : bool = False,
+ no_unit_sphere : bool = False,
+ no_permute : bool = False,
+ dtype : type = np.float32,
+ **kw,
+ ) -> SingleViewUVScan:
+ translation, scale = compute_unit_sphere_transform(mesh, dtype=dtype)
+
+ # get unit-sphere points, then transform to model space
+ two_sphere = generate_equidistant_sphere_rays(sphere_points, **kw).astype(dtype) # (n*(n-1), 2, 3)
+ two_sphere = two_sphere / scale - translation # we transform after cache lookup
+
+ if mesh.ray.__class__.__module__.split(".")[-1] != "ray_pyembree":
+ warnings.warn("Pyembree not found, the ray-tracing will be SLOW!")
+
+ (
+ locations,
+ index_ray,
+ index_tri,
+ ) = mesh.ray.intersects_location(
+ two_sphere[:, 0, :],
+ two_sphere[:, 1, :] - two_sphere[:, 0, :], # direction, not target coordinate
+ multiple_hits=False,
+ )
+
+
+ if compute_normals:
+ location_normals = mesh.face_normals[index_tri]
+
+ batch = two_sphere.shape[:1]
+ hits = np.zeros((*batch,), dtype=np.bool)
+ miss = np.ones((*batch,), dtype=np.bool)
+ cam_pos = two_sphere[:, 0, :]
+ intersections = two_sphere[:, 1, :] # far-plane, effectively
+ normals = np.zeros((*batch, 3), dtype=dtype)
+
+ index_ray_front = index_ray
+ if not no_filter_backhits:
+ if not compute_normals:
+ raise ValueError("not `no_filter_backhits` requires `compute_normals`")
+ mask = ((intersections[index_ray] - cam_pos[index_ray]) * location_normals).sum(axis=-1) <= 0
+ index_ray_front = index_ray[mask]
+
+
+ hits[index_ray_front] = True
+ miss[index_ray] = False
+ intersections[index_ray] = locations
+ normals[index_ray] = location_normals
+
+
+ if not no_permute:
+ assert len(batch) == 1, batch
+ permutation = np.random.permutation(*batch)
+ hits = hits [permutation]
+ miss = miss [permutation]
+ intersections = intersections[permutation, :]
+ normals = normals [permutation, :]
+ cam_pos = cam_pos [permutation, :]
+
+ # apply unit sphere transform
+ if not no_unit_sphere:
+ intersections = (intersections + translation) * scale
+ cam_pos = (cam_pos + translation) * scale
+
+ return SingleViewUVScan(
+ hits = hits,
+ miss = miss,
+ points = intersections,
+ normals = normals,
+ colors = None, # colors
+ distances = None,
+ cam_pos = cam_pos,
+ cam_mat4 = None,
+ proj_mat4 = None,
+ transforms = {},
+ )
+
+def distance_from_rays_to_point_cloud(
+ ray_origins : np.ndarray, # (*A, 3)
+ ray_dirs : np.ndarray, # (*A, 3)
+ points : np.ndarray, # (*B, 3)
+ dirs_normalized : bool = False,
+ n_steps : int = 40,
+ ) -> np.ndarray: # (A)
+
+ # anything outside of this volume will never constribute to the result
+ max_norm = max(
+ np.linalg.norm(ray_origins, axis=-1).max(),
+ np.linalg.norm(points, axis=-1).max(),
+ ) * 1.02
+
+ if not dirs_normalized:
+ ray_dirs = ray_dirs / np.linalg.norm(ray_dirs, axis=-1)[..., None]
+
+
+ # deal with single-view clouds
+ if ray_origins.shape != ray_dirs.shape:
+ ray_origins = np.broadcast_to(ray_origins, ray_dirs.shape)
+
+ n_points = np.product(points.shape[:-1])
+ use_faiss = n_points > 160000*4
+ if not use_faiss:
+ index = BallTree(points)
+ else:
+ # http://ann-benchmarks.com/index.html
+ assert np.issubdtype(points.dtype, np.float32)
+ assert np.issubdtype(ray_origins.dtype, np.float32)
+ assert np.issubdtype(ray_dirs.dtype, np.float32)
+ index = faiss.index_factory(points.shape[-1], "NSG32,Flat") # https://github.com/facebookresearch/faiss/wiki/The-index-factory
+
+ index.nprobe = 5 # 10 # default is 1
+ index.train(points)
+ index.add(points)
+
+ if not use_faiss:
+ min_d, min_n = index.query(ray_origins, k=1, return_distance=True)
+ else:
+ min_d, min_n = index.search(ray_origins, k=1)
+ min_d = np.sqrt(min_d)
+ acc_d = min_d.copy()
+
+ for step in range(1, n_steps+1):
+ query_points = ray_origins + acc_d * ray_dirs
+ if max_norm is not None:
+ qmask = np.linalg.norm(query_points, axis=-1) < max_norm
+ if not qmask.any(): break
+ query_points = query_points[qmask]
+ else:
+ qmask = slice(None)
+ if not use_faiss:
+ current_d, current_n = index.query(query_points, k=1, return_distance=True)
+ else:
+ current_d, current_n = index.search(query_points, k=1)
+ current_d = np.sqrt(current_d)
+ if max_norm is not None:
+ min_d[qmask] = np.minimum(current_d, min_d[qmask])
+ new_min_mask = min_d[qmask] == current_d
+ qmask2 = qmask.copy()
+ qmask2[qmask2] = new_min_mask[..., 0]
+ min_n[qmask2] = current_n[new_min_mask[..., 0]]
+ acc_d[qmask] += current_d * 0.25
+ else:
+ np.minimum(current_d, min_d, out=min_d)
+ new_min_mask = min_d == current_d
+ min_n[new_min_mask] = current_n[new_min_mask]
+ acc_d += current_d * 0.25
+
+ closest_points = points[min_n[:, 0], :] # k=1
+ distances = np.linalg.norm(np.cross(closest_points - ray_origins, ray_dirs, axis=-1), axis=-1)
+ return distances
+
+# helpers
+
+@compose(np.array) # make copy to avoid lru cache mutation
+@lru_cache(maxsize=1)
+def generate_equidistant_sphere_rays(n : int, **kw) -> np.ndarray: # output (n*n(-1)) rays, n may be off
+ sphere_points = points.generate_equidistant_sphere_points(n=n, **kw)
+
+ indices = np.indices((len(sphere_points),))[0] # (N)
+ # cartesian product
+ cprod = np.transpose([np.tile(indices, len(indices)), np.repeat(indices, len(indices))]) # (N**2, 2)
+ # filter repeated combinations
+ permutations = cprod[cprod[:, 0] != cprod[:, 1], :] # (N*(N-1), 2)
+ # lookup sphere points
+ two_sphere = sphere_points[permutations, :] # (N*(N-1), 2, 3)
+
+ return two_sphere
+
+def compute_unit_sphere_transform(mesh: Trimesh, *, dtype=type) -> tuple[np.ndarray, float]:
+ """
+ returns translation and scale which mesh_to_sdf applies to meshes before computing their SDF cloud
+ """
+ # the transformation applied by mesh_to_sdf.scale_to_unit_sphere(mesh)
+ translation = -mesh.bounding_box.centroid
+ scale = 1 / np.max(np.linalg.norm(mesh.vertices + translation, axis=1))
+ if dtype is not None:
+ translation = translation.astype(dtype)
+ scale = scale .astype(dtype)
+ return translation, scale
diff --git a/ifield/data/common/types.py b/ifield/data/common/types.py
new file mode 100644
index 0000000..3da8d31
--- /dev/null
+++ b/ifield/data/common/types.py
@@ -0,0 +1,6 @@
+__doc__ = """
+Some helper types.
+"""
+
+class MalformedMesh(Exception):
+ pass
diff --git a/ifield/data/config.py b/ifield/data/config.py
new file mode 100644
index 0000000..16cb0a9
--- /dev/null
+++ b/ifield/data/config.py
@@ -0,0 +1,28 @@
+from ..utils.helpers import make_relative
+from pathlib import Path
+from typing import Optional
+import os
+import warnings
+
+
+def data_path_get(dataset_name: str, no_warn: bool = False) -> Path:
+ dataset_envvar = f"IFIELD_DATA_MODELS_{dataset_name.replace(*'-_').upper()}"
+ if dataset_envvar in os.environ:
+ data_path = Path(os.environ[dataset_envvar])
+ elif "IFIELD_DATA_MODELS" in os.environ:
+ data_path = Path(os.environ["IFIELD_DATA_MODELS"]) / dataset_name
+ else:
+ data_path = Path(__file__).resolve().parent.parent.parent / "data" / "models" / dataset_name
+ if not data_path.is_dir() and not no_warn:
+ warnings.warn(f"{make_relative(data_path, Path.cwd()).__str__()!r} is not a directory!")
+ return data_path
+
+def data_path_persist(dataset_name: Optional[str], path: os.PathLike) -> os.PathLike:
+ "Persist the datapath, ensuring subprocesses also will use it. The path passes through."
+
+ if dataset_name is None:
+ os.environ["IFIELD_DATA_MODELS"] = str(path)
+ else:
+ os.environ[f"IFIELD_DATA_MODELS_{dataset_name.replace(*'-_').upper()}"] = str(path)
+
+ return path
diff --git a/ifield/data/coseg/__init__.py b/ifield/data/coseg/__init__.py
new file mode 100644
index 0000000..2ecc7c8
--- /dev/null
+++ b/ifield/data/coseg/__init__.py
@@ -0,0 +1,56 @@
+from ..config import data_path_get, data_path_persist
+from collections import namedtuple
+import os
+
+
+# Data source:
+# http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/ssd.htm
+
+__ALL__ = ["config", "Model", "MODELS"]
+
+Archive = namedtuple("Archive", "url fname download_size_str")
+
+@(lambda x: x()) # singleton
+class config:
+ DATA_PATH = property(
+ doc = """
+ Path to the dataset. The following envvars override it:
+ ${IFIELD_DATA_MODELS}/coseg
+ ${IFIELD_DATA_MODELS_COSEG}
+ """,
+ fget = lambda self: data_path_get ("coseg"),
+ fset = lambda self, path: data_path_persist("coseg", path),
+ )
+
+ @property
+ def IS_DOWNLOADED_DB(self) -> list[os.PathLike]:
+ return [
+ self.DATA_PATH / "downloaded.json",
+ ]
+
+ SHAPES: dict[str, Archive] = {
+ "candelabra" : Archive("http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/data/Candelabra/shapes.zip", "candelabra-shapes.zip", "3,3M"),
+ "chair" : Archive("http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/data/Chair/shapes.zip", "chair-shapes.zip", "3,2M"),
+ "four-legged" : Archive("http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/data/Four-legged/shapes.zip", "four-legged-shapes.zip", "2,9M"),
+ "goblets" : Archive("http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/data/Goblets/shapes.zip", "goblets-shapes.zip", "500K"),
+ "guitars" : Archive("http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/data/Guitars/shapes.zip", "guitars-shapes.zip", "1,9M"),
+ "lampes" : Archive("http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/data/Lampes/shapes.zip", "lampes-shapes.zip", "2,4M"),
+ "vases" : Archive("http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/data/Vases/shapes.zip", "vases-shapes.zip", "5,5M"),
+ "irons" : Archive("http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/data/Irons/shapes.zip", "irons-shapes.zip", "1,2M"),
+ "tele-aliens" : Archive("http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/data/Tele-aliens/shapes.zip", "tele-aliens-shapes.zip", "15M"),
+ "large-vases" : Archive("http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/data/Large-Vases/shapes.zip", "large-vases-shapes.zip", "6,2M"),
+ "large-chairs": Archive("http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/data/Large-Chairs/shapes.zip", "large-chairs-shapes.zip", "14M"),
+ }
+ GROUND_TRUTHS: dict[str, Archive] = {
+ "candelabra" : Archive("http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/data/Candelabra/gt.zip", "candelabra-gt.zip", "68K"),
+ "chair" : Archive("http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/data/Chair/gt.zip", "chair-gt.zip", "20K"),
+ "four-legged" : Archive("http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/data/Four-legged/gt.zip", "four-legged-gt.zip", "24K"),
+ "goblets" : Archive("http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/data/Goblets/gt.zip", "goblets-gt.zip", "4,0K"),
+ "guitars" : Archive("http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/data/Guitars/gt.zip", "guitars-gt.zip", "12K"),
+ "lampes" : Archive("http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/data/Lampes/gt.zip", "lampes-gt.zip", "60K"),
+ "vases" : Archive("http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/data/Vases/gt.zip", "vases-gt.zip", "40K"),
+ "irons" : Archive("http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/data/Irons/gt.zip", "irons-gt.zip", "8,0K"),
+ "tele-aliens" : Archive("http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/data/Tele-aliens/gt.zip", "tele-aliens-gt.zip", "72K"),
+ "large-vases" : Archive("http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/data/Large-Vases/gt.zip", "large-vases-gt.zip", "68K"),
+ "large-chairs": Archive("http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/data/Large-Chairs/gt.zip", "large-chairs-gt.zip", "116K"),
+ }
diff --git a/ifield/data/coseg/download.py b/ifield/data/coseg/download.py
new file mode 100644
index 0000000..47a4c3e
--- /dev/null
+++ b/ifield/data/coseg/download.py
@@ -0,0 +1,135 @@
+#!/usr/bin/env python3
+from . import config
+from ...utils.helpers import make_relative
+from ..common import download
+from pathlib import Path
+from textwrap import dedent
+import argparse
+import io
+import zipfile
+
+
+
+def is_downloaded(*a, **kw):
+ return download.is_downloaded(*a, dbfiles=config.IS_DOWNLOADED_DB, **kw)
+
+def download_and_extract(target_dir: Path, url_dict: dict[str, str], *, force=False, silent=False) -> bool:
+ target_dir.mkdir(parents=True, exist_ok=True)
+
+ ret = False
+ for url, fname in url_dict.items():
+ if not force:
+ if is_downloaded(target_dir, url): continue
+ if not download.check_url(url):
+ print("ERROR:", url)
+ continue
+ ret = True
+
+ if force or not (target_dir / "archives" / fname).is_file():
+
+ data = download.download_data(url, silent=silent, label=fname)
+ assert url.endswith(".zip")
+
+ print("writing...")
+
+ (target_dir / "archives").mkdir(parents=True, exist_ok=True)
+ with (target_dir / "archives" / fname).open("wb") as f:
+ f.write(data)
+ del data
+
+ print(f"extracting {fname}...")
+
+ with zipfile.ZipFile(target_dir / "archives" / fname, 'r') as f:
+ f.extractall(target_dir / Path(fname).stem.removesuffix("-shapes").removesuffix("-gt"))
+
+ is_downloaded(target_dir, url, add=True)
+
+ return ret
+
+def make_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(description=dedent("""
+ Download The COSEG Shape Dataset.
+ More info: http://irc.cs.sdu.edu.cn/~yunhai/public_html/ssl/ssd.htm
+
+ Example:
+
+ download-coseg --shapes chairs
+ """), formatter_class=argparse.RawTextHelpFormatter)
+
+ arg = parser.add_argument
+
+ arg("sets", nargs="*", default=[],
+ help="Which set to download, defaults to none.")
+ arg("--all", action="store_true",
+ help="Download all sets")
+ arg("--dir", default=str(config.DATA_PATH),
+ help=f"The target directory. Default is {make_relative(config.DATA_PATH, Path.cwd()).__str__()!r}")
+
+ arg("--shapes", action="store_true",
+ help="Download the 3d shapes for each chosen set")
+ arg("--gts", action="store_true",
+ help="Download the ground-truth segmentation data for each chosen set")
+
+ arg("--list", action="store_true",
+ help="Lists all the sets")
+ arg("--list-urls", action="store_true",
+ help="Lists the urls to download")
+ arg("--list-sizes", action="store_true",
+ help="Lists the download size of each set")
+ arg("--silent", action="store_true",
+ help="")
+ arg("--force", action="store_true",
+ help="Download again even if already downloaded")
+
+ return parser
+
+# entrypoint
+def cli(parser=make_parser()):
+ args = parser.parse_args()
+
+ assert set(config.SHAPES.keys()) == set(config.GROUND_TRUTHS.keys())
+
+ set_names = sorted(set(args.sets))
+ if args.all:
+ assert not set_names, "--all is mutually exclusive from manually selected sets"
+ set_names = sorted(config.SHAPES.keys())
+
+ if args.list:
+ print(*config.SHAPES.keys(), sep="\n")
+ exit()
+
+ if args.list_sizes:
+ print(*(f"{set_name:<15}{config.SHAPES[set_name].download_size_str}" for set_name in (set_names or config.SHAPES.keys())), sep="\n")
+ exit()
+
+ try:
+ url_dict \
+ = {config.SHAPES[set_name].url : config.SHAPES[set_name].fname for set_name in set_names if args.shapes} \
+ | {config.GROUND_TRUTHS[set_name].url : config.GROUND_TRUTHS[set_name].fname for set_name in set_names if args.gts}
+ except KeyError:
+ print("Error: unrecognized object name:", *set(set_names).difference(config.SHAPES.keys()), sep="\n")
+ exit(1)
+
+ if not url_dict:
+ if set_names and not (args.shapes or args.gts):
+ print("Error: Provide at least one of --shapes of --gts")
+ else:
+ print("Error: No object set was selected for download!")
+ exit(1)
+
+ if args.list_urls:
+ print(*url_dict.keys(), sep="\n")
+ exit()
+
+ print("Download start")
+ any_downloaded = download_and_extract(
+ target_dir = Path(args.dir),
+ url_dict = url_dict,
+ force = args.force,
+ silent = args.silent,
+ )
+ if not any_downloaded:
+ print("Everything has already been downloaded, skipping.")
+
+if __name__ == "__main__":
+ cli()
diff --git a/ifield/data/coseg/preprocess.py b/ifield/data/coseg/preprocess.py
new file mode 100644
index 0000000..65b0b70
--- /dev/null
+++ b/ifield/data/coseg/preprocess.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python3
+import os; os.environ.setdefault("PYOPENGL_PLATFORM", "egl")
+from . import config, read
+from ...utils.helpers import make_relative
+from pathlib import Path
+from textwrap import dedent
+import argparse
+
+
+def make_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(description=dedent("""
+ Preprocess the COSEG dataset. Depends on `download-coseg --shapes ...` having been run.
+ """), formatter_class=argparse.RawTextHelpFormatter)
+
+ arg = parser.add_argument # brevity
+
+ arg("items", nargs="*", default=[],
+ help="Which object-set[/model-id] to process, defaults to all downloaded. Format: OBJECT-SET[/MODEL-ID]")
+ arg("--dir", default=str(config.DATA_PATH),
+ help=f"The target directory. Default is {make_relative(config.DATA_PATH, Path.cwd()).__str__()!r}")
+ arg("--force", action="store_true",
+ help="Overwrite existing files")
+ arg("--list-models", action="store_true",
+ help="List the downloaded models available for preprocessing")
+ arg("--list-object-sets", action="store_true",
+ help="List the downloaded object-sets available for preprocessing")
+ arg("--list-pages", type=int, default=None,
+ help="List the downloaded models available for preprocessing, paginated into N pages.")
+ arg("--page", nargs=2, type=int, default=[0, 1],
+ help="Subset of parts to compute. Use to parallelize. (page, total), page is 0 indexed")
+
+ arg2 = parser.add_argument_group("preprocessing targets").add_argument # brevity
+ arg2("--precompute-mesh-sv-scan-clouds", action="store_true",
+ help="Compute single-view hit+miss point clouds from 100 synthetic scans.")
+ arg2("--precompute-mesh-sv-scan-uvs", action="store_true",
+ help="Compute single-view hit+miss UV clouds from 100 synthetic scans.")
+ arg2("--precompute-mesh-sphere-scan", action="store_true",
+ help="Compute a sphere-view hit+miss cloud cast from n to n unit sphere points.")
+
+ arg3 = parser.add_argument_group("modifiers").add_argument # brevity
+ arg3("--n-sphere-points", type=int, default=4000,
+ help="The number of unit-sphere points to sample rays from. Final result: n*(n-1).")
+ arg3("--compute-miss-distances", action="store_true",
+ help="Compute the distance to the nearest hit for each miss in the hit+miss clouds.")
+ arg3("--fill-missing-uv-points", action="store_true",
+ help="TODO")
+ arg3("--no-filter-backhits", action="store_true",
+ help="Do not filter scan hits on backside of mesh faces.")
+ arg3("--no-unit-sphere", action="store_true",
+ help="Do not center the objects to the unit sphere.")
+ arg3("--convert-ok", action="store_true",
+ help="Allow reusing point clouds for uv clouds and vice versa. (does not account for other hparams)")
+ arg3("--debug", action="store_true",
+ help="Abort on failiure.")
+
+ return parser
+
+# entrypoint
+def cli(parser=make_parser()):
+ args = parser.parse_args()
+ if not any(getattr(args, k) for k in dir(args) if k.startswith("precompute_")) and not (args.list_models or args.list_object_sets or args.list_pages):
+ parser.error("no preprocessing target selected") # exits
+
+ config.DATA_PATH = Path(args.dir)
+
+ object_sets = [i for i in args.items if "/" not in i]
+ models = [i.split("/") for i in args.items if "/" in i]
+
+ # convert/expand synsets to models
+ # they are mutually exclusive
+ if object_sets: assert not models
+ if models: assert not object_sets
+ if not models:
+ models = read.list_model_ids(tuple(object_sets) or None)
+
+ if args.list_models:
+ try:
+ print(*(f"{object_set_id}/{model_id}" for object_set_id, model_id in models), sep="\n")
+ except BrokenPipeError:
+ pass
+ parser.exit()
+
+ if args.list_object_sets:
+ try:
+ print(*sorted(set(object_set_id for object_set_id, model_id in models)), sep="\n")
+ except BrokenPipeError:
+ pass
+ parser.exit()
+
+ if args.list_pages is not None:
+ try:
+ print(*(
+ f"--page {i} {args.list_pages} {object_set_id}/{model_id}"
+ for object_set_id, model_id in models
+ for i in range(args.list_pages)
+ ), sep="\n")
+ except BrokenPipeError:
+ pass
+ parser.exit()
+
+ if args.precompute_mesh_sv_scan_clouds:
+ read.precompute_mesh_scan_point_clouds(
+ models,
+ compute_miss_distances = args.compute_miss_distances,
+ no_filter_backhits = args.no_filter_backhits,
+ no_unit_sphere = args.no_unit_sphere,
+ convert_ok = args.convert_ok,
+ page = args.page,
+ force = args.force,
+ debug = args.debug,
+ )
+ if args.precompute_mesh_sv_scan_uvs:
+ read.precompute_mesh_scan_uvs(
+ models,
+ compute_miss_distances = args.compute_miss_distances,
+ fill_missing_points = args.fill_missing_uv_points,
+ no_filter_backhits = args.no_filter_backhits,
+ no_unit_sphere = args.no_unit_sphere,
+ convert_ok = args.convert_ok,
+ page = args.page,
+ force = args.force,
+ debug = args.debug,
+ )
+ if args.precompute_mesh_sphere_scan:
+ read.precompute_mesh_sphere_scan(
+ models,
+ sphere_points = args.n_sphere_points,
+ compute_miss_distances = args.compute_miss_distances,
+ no_filter_backhits = args.no_filter_backhits,
+ no_unit_sphere = args.no_unit_sphere,
+ page = args.page,
+ force = args.force,
+ debug = args.debug,
+ )
+
+if __name__ == "__main__":
+ cli()
diff --git a/ifield/data/coseg/read.py b/ifield/data/coseg/read.py
new file mode 100644
index 0000000..1e3dd6e
--- /dev/null
+++ b/ifield/data/coseg/read.py
@@ -0,0 +1,290 @@
+from . import config
+from ..common import points
+from ..common import processing
+from ..common.scan import SingleViewScan, SingleViewUVScan
+from ..common.types import MalformedMesh
+from functools import lru_cache
+from typing import Optional, Iterable
+import numpy as np
+import trimesh
+import trimesh.transformations as T
+
+__doc__ = """
+Here are functions for reading and preprocessing coseg benchmark data
+
+There are essentially a few sets per object:
+ "img" - meaning the RGBD images (none found in coseg)
+ "mesh_scans" - meaning synthetic scans of a mesh
+"""
+
+MESH_TRANSFORM_SKYWARD = T.rotation_matrix(np.pi/2, (1, 0, 0)) # rotate to be upright in pyrender
+MESH_POSE_CORRECTIONS = { # to gain a shared canonical orientation
+ ("four-legged", 381): T.rotation_matrix( -1*np.pi/2, (0, 0, 1)),
+ ("four-legged", 382): T.rotation_matrix( 1*np.pi/2, (0, 0, 1)),
+ ("four-legged", 383): T.rotation_matrix( -1*np.pi/2, (0, 0, 1)),
+ ("four-legged", 384): T.rotation_matrix( -1*np.pi/2, (0, 0, 1)),
+ ("four-legged", 385): T.rotation_matrix( -1*np.pi/2, (0, 0, 1)),
+ ("four-legged", 386): T.rotation_matrix( 1*np.pi/2, (0, 0, 1)),
+ ("four-legged", 387): T.rotation_matrix(-0.2*np.pi/2, (0, 1, 0))@T.rotation_matrix(1*np.pi/2, (0, 0, 1)),
+ ("four-legged", 388): T.rotation_matrix( -1*np.pi/2, (0, 0, 1)),
+ ("four-legged", 389): T.rotation_matrix( -1*np.pi/2, (0, 0, 1)),
+ ("four-legged", 390): T.rotation_matrix( 0*np.pi/2, (0, 0, 1)),
+ ("four-legged", 391): T.rotation_matrix( 0*np.pi/2, (0, 0, 1)),
+ ("four-legged", 392): T.rotation_matrix( -1*np.pi/2, (0, 0, 1)),
+ ("four-legged", 393): T.rotation_matrix( 0*np.pi/2, (0, 0, 1)),
+ ("four-legged", 394): T.rotation_matrix( -1*np.pi/2, (0, 0, 1)),
+ ("four-legged", 395): T.rotation_matrix(-0.2*np.pi/2, (0, 1, 0))@T.rotation_matrix(1*np.pi/2, (0, 0, 1)),
+ ("four-legged", 396): T.rotation_matrix( 1*np.pi/2, (0, 0, 1)),
+ ("four-legged", 397): T.rotation_matrix( 0*np.pi/2, (0, 0, 1)),
+ ("four-legged", 398): T.rotation_matrix( -1*np.pi/2, (0, 0, 1)),
+ ("four-legged", 399): T.rotation_matrix( 0*np.pi/2, (0, 0, 1)),
+ ("four-legged", 400): T.rotation_matrix( 0*np.pi/2, (0, 0, 1)),
+}
+
+
+ModelUid = tuple[str, int]
+
+@lru_cache(maxsize=1)
+def list_object_sets() -> list[str]:
+ return sorted(
+ object_set.name
+ for object_set in config.DATA_PATH.iterdir()
+ if (object_set / "shapes").is_dir() and object_set.name != "archive"
+ )
+
+@lru_cache(maxsize=1)
+def list_model_ids(object_sets: Optional[tuple[str]] = None) -> list[ModelUid]:
+ return sorted(
+ (object_set.name, int(model.stem))
+ for object_set in config.DATA_PATH.iterdir()
+ if (object_set / "shapes").is_dir() and object_set.name != "archive" and (object_sets is None or object_set.name in object_sets)
+ for model in (object_set / "shapes").iterdir()
+ if model.is_file() and model.suffix == ".off"
+ )
+
+def list_model_id_strings(object_sets: Optional[tuple[str]] = None) -> list[str]:
+ return [model_uid_to_string(object_set_id, model_id) for object_set_id, model_id in list_model_ids(object_sets)]
+
+def model_uid_to_string(object_set_id: str, model_id: int) -> str:
+ return f"{object_set_id}-{model_id}"
+
+def model_id_string_to_uid(model_string_uid: str) -> ModelUid:
+ object_set, split, model = model_string_uid.rpartition("-")
+ assert split == "-"
+ return (object_set, int(model))
+
+@lru_cache(maxsize=1)
+def list_mesh_scan_sphere_coords(n_poses: int = 50) -> list[tuple[float, float]]: # (theta, phi)
+ return points.generate_equidistant_sphere_points(n_poses, compute_sphere_coordinates=True)
+
+def mesh_scan_identifier(*, phi: float, theta: float) -> str:
+ return (
+ f"{'np'[theta>=0]}{abs(theta):.2f}"
+ f"{'np'[phi >=0]}{abs(phi) :.2f}"
+ ).replace(".", "d")
+
+@lru_cache(maxsize=1)
+def list_mesh_scan_identifiers(n_poses: int = 50) -> list[str]:
+ out = [
+ mesh_scan_identifier(phi=phi, theta=theta)
+ for theta, phi in list_mesh_scan_sphere_coords(n_poses)
+ ]
+ assert len(out) == len(set(out))
+ return out
+
+# ===
+
+def read_mesh(object_set_id: str, model_id: int) -> trimesh.Trimesh:
+ path = config.DATA_PATH / object_set_id / "shapes" / f"{model_id}.off"
+ if not path.is_file():
+ raise FileNotFoundError(f"{path = }")
+ try:
+ mesh = trimesh.load(path, force="mesh")
+ except Exception as e:
+ raise MalformedMesh(f"Trimesh raised: {e.__class__.__name__}: {e}") from e
+
+ pose = MESH_POSE_CORRECTIONS.get((object_set_id, int(model_id)))
+ mesh.apply_transform(pose @ MESH_TRANSFORM_SKYWARD if pose is not None else MESH_TRANSFORM_SKYWARD)
+ return mesh
+
+# === single-view scan clouds
+
+def compute_mesh_scan_point_cloud(
+ object_set_id : str,
+ model_id : int,
+ phi : float,
+ theta : float,
+ *,
+ compute_miss_distances : bool = False,
+ fill_missing_points : bool = False,
+ compute_normals : bool = True,
+ convert_ok : bool = False,
+ **kw,
+ ) -> SingleViewScan:
+
+ if convert_ok:
+ try:
+ return read_mesh_scan_uv(object_set_id, model_id, phi=phi, theta=theta).to_scan()
+ except FileNotFoundError:
+ pass
+
+ mesh = read_mesh(object_set_id, model_id)
+ scan = SingleViewScan.from_mesh_single_view(mesh,
+ phi = phi,
+ theta = theta,
+ compute_normals = compute_normals,
+ **kw,
+ )
+ if compute_miss_distances:
+ scan.compute_miss_distances()
+ if fill_missing_points:
+ scan.fill_missing_points()
+
+ return scan
+
+def precompute_mesh_scan_point_clouds(models: Iterable[ModelUid], *, n_poses: int = 50, page: tuple[int, int] = (0, 1), force = False, debug = False, **kw):
+ "precomputes all single-view scan clouds and stores them as HDF5 datasets"
+ cam_poses = list_mesh_scan_sphere_coords(n_poses=n_poses)
+ pose_identifiers = list_mesh_scan_identifiers (n_poses=n_poses)
+ assert len(cam_poses) == len(pose_identifiers)
+ paths = list_mesh_scan_point_cloud_h5_fnames(models, pose_identifiers, n_poses=n_poses)
+ mlen_syn = max(len(object_set_id) for object_set_id, model_id in models)
+ mlen_mod = max(len(str(model_id)) for object_set_id, model_id in models)
+ pretty_identifiers = [
+ f"{object_set_id.ljust(mlen_syn)} @ {str(model_id).ljust(mlen_mod)} @ {i:>5} @ ({itentifier}: {theta:.2f}, {phi:.2f})"
+ for object_set_id, model_id in models
+ for i, (itentifier, (theta, phi)) in enumerate(zip(pose_identifiers, cam_poses))
+ ]
+ mesh_cache = []
+ def computer(pretty_identifier: str) -> SingleViewScan:
+ object_set_id, model_id, index, _ = map(str.strip, pretty_identifier.split("@"))
+ theta, phi = cam_poses[int(index)]
+ return compute_mesh_scan_point_cloud(object_set_id, int(model_id), phi=phi, theta=theta, _mesh_cache=mesh_cache, **kw)
+ return processing.precompute_data(computer, pretty_identifiers, paths, page=page, force=force, debug=debug)
+
+def read_mesh_scan_point_cloud(object_set_id: str, model_id: int, *, identifier: str = None, phi: float = None, theta: float = None) -> SingleViewScan:
+ if identifier is None:
+ if phi is None or theta is None:
+ raise ValueError("Provide either phi+theta or an identifier!")
+ identifier = mesh_scan_identifier(phi=phi, theta=theta)
+ file = config.DATA_PATH / object_set_id / "uv_scan_clouds" / f"{model_id}_normalized_{identifier}.h5"
+ return SingleViewScan.from_h5_file(file)
+
+def list_mesh_scan_point_cloud_h5_fnames(models: Iterable[ModelUid], identifiers: Optional[Iterable[str]] = None, **kw):
+ if identifiers is None:
+ identifiers = list_mesh_scan_identifiers(**kw)
+ return [
+ config.DATA_PATH / object_set_id / "uv_scan_clouds" / f"{model_id}_normalized_{identifier}.h5"
+ for object_set_id, model_id in models
+ for identifier in identifiers
+ ]
+
+
+# === single-view UV scan clouds
+
+def compute_mesh_scan_uv(
+ object_set_id : str,
+ model_id : int,
+ phi : float,
+ theta : float,
+ *,
+ compute_miss_distances : bool = False,
+ fill_missing_points : bool = False,
+ compute_normals : bool = True,
+ convert_ok : bool = False,
+ **kw,
+ ) -> SingleViewUVScan:
+
+ if convert_ok:
+ try:
+ return read_mesh_scan_point_cloud(object_set_id, model_id, phi=phi, theta=theta).to_uv_scan()
+ except FileNotFoundError:
+ pass
+
+ mesh = read_mesh(object_set_id, model_id)
+ scan = SingleViewUVScan.from_mesh_single_view(mesh,
+ phi = phi,
+ theta = theta,
+ compute_normals = compute_normals,
+ **kw,
+ )
+ if compute_miss_distances:
+ scan.compute_miss_distances()
+ if fill_missing_points:
+ scan.fill_missing_points()
+
+ return scan
+
+def precompute_mesh_scan_uvs(models: Iterable[ModelUid], *, n_poses: int = 50, page: tuple[int, int] = (0, 1), force = False, debug = False, **kw):
+ "precomputes all single-view scan clouds and stores them as HDF5 datasets"
+ cam_poses = list_mesh_scan_sphere_coords(n_poses=n_poses)
+ pose_identifiers = list_mesh_scan_identifiers (n_poses=n_poses)
+ assert len(cam_poses) == len(pose_identifiers)
+ paths = list_mesh_scan_uv_h5_fnames(models, pose_identifiers, n_poses=n_poses)
+ mlen_syn = max(len(object_set_id) for object_set_id, model_id in models)
+ mlen_mod = max(len(str(model_id)) for object_set_id, model_id in models)
+ pretty_identifiers = [
+ f"{object_set_id.ljust(mlen_syn)} @ {str(model_id).ljust(mlen_mod)} @ {i:>5} @ ({itentifier}: {theta:.2f}, {phi:.2f})"
+ for object_set_id, model_id in models
+ for i, (itentifier, (theta, phi)) in enumerate(zip(pose_identifiers, cam_poses))
+ ]
+ mesh_cache = []
+ def computer(pretty_identifier: str) -> SingleViewUVScan:
+ object_set_id, model_id, index, _ = map(str.strip, pretty_identifier.split("@"))
+ theta, phi = cam_poses[int(index)]
+ return compute_mesh_scan_uv(object_set_id, int(model_id), phi=phi, theta=theta, _mesh_cache=mesh_cache, **kw)
+ return processing.precompute_data(computer, pretty_identifiers, paths, page=page, force=force, debug=debug)
+
+def read_mesh_scan_uv(object_set_id: str, model_id: int, *, identifier: str = None, phi: float = None, theta: float = None) -> SingleViewUVScan:
+ if identifier is None:
+ if phi is None or theta is None:
+ raise ValueError("Provide either phi+theta or an identifier!")
+ identifier = mesh_scan_identifier(phi=phi, theta=theta)
+ file = config.DATA_PATH / object_set_id / "uv_scan_clouds" / f"{model_id}_normalized_{identifier}.h5"
+
+ return SingleViewUVScan.from_h5_file(file)
+
+def list_mesh_scan_uv_h5_fnames(models: Iterable[ModelUid], identifiers: Optional[Iterable[str]] = None, **kw):
+ if identifiers is None:
+ identifiers = list_mesh_scan_identifiers(**kw)
+ return [
+ config.DATA_PATH / object_set_id / "uv_scan_clouds" / f"{model_id}_normalized_{identifier}.h5"
+ for object_set_id, model_id in models
+ for identifier in identifiers
+ ]
+
+
+# === sphere-view (UV) scan clouds
+
+def compute_mesh_sphere_scan(
+ object_set_id : str,
+ model_id : int,
+ *,
+ compute_normals : bool = True,
+ **kw,
+ ) -> SingleViewUVScan:
+ mesh = read_mesh(object_set_id, model_id)
+ scan = SingleViewUVScan.from_mesh_sphere_view(mesh,
+ compute_normals = compute_normals,
+ **kw,
+ )
+ return scan
+
+def precompute_mesh_sphere_scan(models: Iterable[ModelUid], *, page: tuple[int, int] = (0, 1), force: bool = False, debug: bool = False, n_points: int = 4000, **kw):
+ "precomputes all sphere scan clouds and stores them as HDF5 datasets"
+ paths = list_mesh_sphere_scan_h5_fnames(models)
+ identifiers = [model_uid_to_string(*i) for i in models]
+ def computer(identifier: str) -> SingleViewScan:
+ object_set_id, model_id = model_id_string_to_uid(identifier)
+ return compute_mesh_sphere_scan(object_set_id, model_id, **kw)
+ return processing.precompute_data(computer, identifiers, paths, page=page, force=force, debug=debug)
+
+def read_mesh_mesh_sphere_scan(object_set_id: str, model_id: int) -> SingleViewUVScan:
+ file = config.DATA_PATH / object_set_id / "sphere_scan_clouds" / f"{model_id}_normalized.h5"
+ return SingleViewUVScan.from_h5_file(file)
+
+def list_mesh_sphere_scan_h5_fnames(models: Iterable[ModelUid]) -> list[str]:
+ return [
+ config.DATA_PATH / object_set_id / "sphere_scan_clouds" / f"{model_id}_normalized.h5"
+ for object_set_id, model_id in models
+ ]
diff --git a/ifield/data/stanford/__init__.py b/ifield/data/stanford/__init__.py
new file mode 100644
index 0000000..56ddf54
--- /dev/null
+++ b/ifield/data/stanford/__init__.py
@@ -0,0 +1,76 @@
+from ..config import data_path_get, data_path_persist
+from collections import namedtuple
+import os
+
+
+# Data source:
+# http://graphics.stanford.edu/data/3Dscanrep/
+
+__ALL__ = ["config", "Model", "MODELS"]
+
+@(lambda x: x()) # singleton
+class config:
+ DATA_PATH = property(
+ doc = """
+ Path to the dataset. The following envvars override it:
+ ${IFIELD_DATA_MODELS}/stanford
+ ${IFIELD_DATA_MODELS_STANFORD}
+ """,
+ fget = lambda self: data_path_get ("stanford"),
+ fset = lambda self, path: data_path_persist("stanford", path),
+ )
+
+ @property
+ def IS_DOWNLOADED_DB(self) -> list[os.PathLike]:
+ return [
+ self.DATA_PATH / "downloaded.json",
+ ]
+
+ Model = namedtuple("Model", "url mesh_fname download_size_str")
+ MODELS: dict[str, Model] = {
+ "bunny": Model(
+ "http://graphics.stanford.edu/pub/3Dscanrep/bunny.tar.gz",
+ "bunny/reconstruction/bun_zipper.ply",
+ "4.89M",
+ ),
+ "drill_bit": Model(
+ "http://graphics.stanford.edu/pub/3Dscanrep/drill.tar.gz",
+ "drill/reconstruction/drill_shaft_vrip.ply",
+ "555k",
+ ),
+ "happy_buddha": Model(
+ # religious symbol
+ "http://graphics.stanford.edu/pub/3Dscanrep/happy/happy_recon.tar.gz",
+ "happy_recon/happy_vrip.ply",
+ "14.5M",
+ ),
+ "dragon": Model(
+ # symbol of Chinese culture
+ "http://graphics.stanford.edu/pub/3Dscanrep/dragon/dragon_recon.tar.gz",
+ "dragon_recon/dragon_vrip.ply",
+ "11.2M",
+ ),
+ "armadillo": Model(
+ "http://graphics.stanford.edu/pub/3Dscanrep/armadillo/Armadillo.ply.gz",
+ "armadillo.ply.gz",
+ "3.87M",
+ ),
+ "lucy": Model(
+ # Christian angel
+ "http://graphics.stanford.edu/data/3Dscanrep/lucy.tar.gz",
+ "lucy.ply",
+ "322M",
+ ),
+ "asian_dragon": Model(
+ # symbol of Chinese culture
+ "http://graphics.stanford.edu/data/3Dscanrep/xyzrgb/xyzrgb_dragon.ply.gz",
+ "xyzrgb_dragon.ply.gz",
+ "70.5M",
+ ),
+ "thai_statue": Model(
+ # Hindu religious significance
+ "http://graphics.stanford.edu/data/3Dscanrep/xyzrgb/xyzrgb_statuette.ply.gz",
+ "xyzrgb_statuette.ply.gz",
+ "106M",
+ ),
+ }
diff --git a/ifield/data/stanford/download.py b/ifield/data/stanford/download.py
new file mode 100644
index 0000000..5307ec0
--- /dev/null
+++ b/ifield/data/stanford/download.py
@@ -0,0 +1,129 @@
+#!/usr/bin/env python3
+from . import config
+from ...utils.helpers import make_relative
+from ..common import download
+from pathlib import Path
+from textwrap import dedent
+from typing import Iterable
+import argparse
+import io
+import tarfile
+
+
+def is_downloaded(*a, **kw):
+ return download.is_downloaded(*a, dbfiles=config.IS_DOWNLOADED_DB, **kw)
+
+def download_and_extract(target_dir: Path, url_list: Iterable[str], *, force=False, silent=False) -> bool:
+ target_dir.mkdir(parents=True, exist_ok=True)
+
+ ret = False
+ for url in url_list:
+ if not force:
+ if is_downloaded(target_dir, url): continue
+ if not download.check_url(url):
+ print("ERROR:", url)
+ continue
+ ret = True
+
+ data = download.download_data(url, silent=silent, label=str(Path(url).name))
+
+ print("extracting...")
+ if url.endswith(".ply.gz"):
+ fname = target_dir / "meshes" / url.split("/")[-1].lower()
+ fname.parent.mkdir(parents=True, exist_ok=True)
+ with fname.open("wb") as f:
+ f.write(data)
+ elif url.endswith(".tar.gz"):
+ with tarfile.open(fileobj=io.BytesIO(data)) as tar:
+ for member in tar.getmembers():
+ if not member.isfile(): continue
+ if member.name.startswith("/"): continue
+ if member.name.startswith("."): continue
+ if Path(member.name).name.startswith("."): continue
+ tar.extract(member, target_dir / "meshes")
+ del tar
+ else:
+ raise NotImplementedError(f"Extraction for {str(Path(url).name)} unknown")
+
+ is_downloaded(target_dir, url, add=True)
+ del data
+
+ return ret
+
+def make_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(description=dedent("""
+ Download The Stanford 3D Scanning Repository models.
+ More info: http://graphics.stanford.edu/data/3Dscanrep/
+
+ Example:
+
+ download-stanford bunny
+ """), formatter_class=argparse.RawTextHelpFormatter)
+
+ arg = parser.add_argument
+
+ arg("objects", nargs="*", default=[],
+ help="Which objects to download, defaults to none.")
+ arg("--all", action="store_true",
+ help="Download all objects")
+ arg("--dir", default=str(config.DATA_PATH),
+ help=f"The target directory. Default is {make_relative(config.DATA_PATH, Path.cwd()).__str__()!r}")
+
+ arg("--list", action="store_true",
+ help="Lists all the objects")
+ arg("--list-urls", action="store_true",
+ help="Lists the urls to download")
+ arg("--list-sizes", action="store_true",
+ help="Lists the download size of each model")
+ arg("--silent", action="store_true",
+ help="")
+ arg("--force", action="store_true",
+ help="Download again even if already downloaded")
+
+ return parser
+
+# entrypoint
+def cli(parser=make_parser()):
+ args = parser.parse_args()
+
+ obj_names = sorted(set(args.objects))
+ if args.all:
+ assert not obj_names
+ obj_names = sorted(config.MODELS.keys())
+ if not obj_names and args.list_urls: config.MODELS.keys()
+
+ if args.list:
+ print(*config.MODELS.keys(), sep="\n")
+ exit()
+
+ if args.list_sizes:
+ print(*(f"{obj_name:<15}{config.MODELS[obj_name].download_size_str}" for obj_name in (obj_names or config.MODELS.keys())), sep="\n")
+ exit()
+
+ try:
+ url_list = [config.MODELS[obj_name].url for obj_name in obj_names]
+ except KeyError:
+ print("Error: unrecognized object name:", *set(obj_names).difference(config.MODELS.keys()), sep="\n")
+ exit(1)
+
+ if not url_list:
+ print("Error: No object set was selected for download!")
+ exit(1)
+
+ if args.list_urls:
+ print(*url_list, sep="\n")
+ exit()
+
+
+ print("Download start")
+ any_downloaded = download_and_extract(
+ target_dir = Path(args.dir),
+ url_list = url_list,
+ force = args.force,
+ silent = args.silent,
+ )
+ if not any_downloaded:
+ print("Everything has already been downloaded, skipping.")
+
+if __name__ == "__main__":
+ cli()
diff --git a/ifield/data/stanford/preprocess.py b/ifield/data/stanford/preprocess.py
new file mode 100644
index 0000000..b7363f7
--- /dev/null
+++ b/ifield/data/stanford/preprocess.py
@@ -0,0 +1,118 @@
+#!/usr/bin/env python3
+import os; os.environ.setdefault("PYOPENGL_PLATFORM", "egl")
+from . import config, read
+from ...utils.helpers import make_relative
+from pathlib import Path
+from textwrap import dedent
+import argparse
+
+
+
+def make_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(description=dedent("""
+ Preprocess the Stanford models. Depends on `download-stanford` having been run.
+ """), formatter_class=argparse.RawTextHelpFormatter)
+
+ arg = parser.add_argument # brevity
+
+ arg("objects", nargs="*", default=[],
+ help="Which objects to process, defaults to all downloaded")
+ arg("--dir", default=str(config.DATA_PATH),
+ help=f"The target directory. Default is {make_relative(config.DATA_PATH, Path.cwd()).__str__()!r}")
+ arg("--force", action="store_true",
+ help="Overwrite existing files")
+ arg("--list", action="store_true",
+ help="List the downloaded models available for preprocessing")
+ arg("--list-pages", type=int, default=None,
+ help="List the downloaded models available for preprocessing, paginated into N pages.")
+ arg("--page", nargs=2, type=int, default=[0, 1],
+ help="Subset of parts to compute. Use to parallelize. (page, total), page is 0 indexed")
+
+ arg2 = parser.add_argument_group("preprocessing targets").add_argument # brevity
+ arg2("--precompute-mesh-sv-scan-clouds", action="store_true",
+ help="Compute single-view hit+miss point clouds from 100 synthetic scans.")
+ arg2("--precompute-mesh-sv-scan-uvs", action="store_true",
+ help="Compute single-view hit+miss UV clouds from 100 synthetic scans.")
+ arg2("--precompute-mesh-sphere-scan", action="store_true",
+ help="Compute a sphere-view hit+miss cloud cast from n to n unit sphere points.")
+
+ arg3 = parser.add_argument_group("ray-scan modifiers").add_argument # brevity
+ arg3("--n-sphere-points", type=int, default=4000,
+ help="The number of unit-sphere points to sample rays from. Final result: n*(n-1).")
+ arg3("--compute-miss-distances", action="store_true",
+ help="Compute the distance to the nearest hit for each miss in the hit+miss clouds.")
+ arg3("--fill-missing-uv-points", action="store_true",
+ help="TODO")
+ arg3("--no-filter-backhits", action="store_true",
+ help="Do not filter scan hits on backside of mesh faces.")
+ arg3("--no-unit-sphere", action="store_true",
+ help="Do not center the objects to the unit sphere.")
+ arg3("--convert-ok", action="store_true",
+ help="Allow reusing point clouds for uv clouds and vice versa. (does not account for other hparams)")
+ arg3("--debug", action="store_true",
+ help="Abort on failiure.")
+
+ arg5 = parser.add_argument_group("Shared modifiers").add_argument # brevity
+ arg5("--scan-resolution", type=int, default=400,
+ help="The resolution of the depth map rendered to sample points. Becomes x*x")
+
+ return parser
+
+# entrypoint
+def cli(parser: argparse.ArgumentParser = make_parser()):
+ args = parser.parse_args()
+ if not any(getattr(args, k) for k in dir(args) if k.startswith("precompute_")) and not (args.list or args.list_pages):
+ parser.error("no preprocessing target selected") # exits
+
+ config.DATA_PATH = Path(args.dir)
+ obj_names = args.objects or read.list_object_names()
+
+ if args.list:
+ print(*obj_names, sep="\n")
+ parser.exit()
+
+ if args.list_pages is not None:
+ print(*(
+ f"--page {i} {args.list_pages} {obj_name}"
+ for obj_name in obj_names
+ for i in range(args.list_pages)
+ ), sep="\n")
+ parser.exit()
+
+ if args.precompute_mesh_sv_scan_clouds:
+ read.precompute_mesh_scan_point_clouds(
+ obj_names,
+ compute_miss_distances = args.compute_miss_distances,
+ no_filter_backhits = args.no_filter_backhits,
+ no_unit_sphere = args.no_unit_sphere,
+ convert_ok = args.convert_ok,
+ page = args.page,
+ force = args.force,
+ debug = args.debug,
+ )
+ if args.precompute_mesh_sv_scan_uvs:
+ read.precompute_mesh_scan_uvs(
+ obj_names,
+ compute_miss_distances = args.compute_miss_distances,
+ fill_missing_points = args.fill_missing_uv_points,
+ no_filter_backhits = args.no_filter_backhits,
+ no_unit_sphere = args.no_unit_sphere,
+ convert_ok = args.convert_ok,
+ page = args.page,
+ force = args.force,
+ debug = args.debug,
+ )
+ if args.precompute_mesh_sphere_scan:
+ read.precompute_mesh_sphere_scan(
+ obj_names,
+ sphere_points = args.n_sphere_points,
+ compute_miss_distances = args.compute_miss_distances,
+ no_filter_backhits = args.no_filter_backhits,
+ no_unit_sphere = args.no_unit_sphere,
+ page = args.page,
+ force = args.force,
+ debug = args.debug,
+ )
+
+if __name__ == "__main__":
+ cli()
diff --git a/ifield/data/stanford/read.py b/ifield/data/stanford/read.py
new file mode 100644
index 0000000..ae6c67b
--- /dev/null
+++ b/ifield/data/stanford/read.py
@@ -0,0 +1,251 @@
+from . import config
+from ..common import points
+from ..common import processing
+from ..common.scan import SingleViewScan, SingleViewUVScan
+from ..common.types import MalformedMesh
+from functools import lru_cache, wraps
+from typing import Optional, Iterable
+from pathlib import Path
+import gzip
+import numpy as np
+import trimesh
+import trimesh.transformations as T
+
+__doc__ = """
+Here are functions for reading and preprocessing shapenet benchmark data
+
+There are essentially a few sets per object:
+ "img" - meaning the RGBD images (none found in stanford)
+ "mesh_scans" - meaning synthetic scans of a mesh
+"""
+
+MESH_TRANSFORM_SKYWARD = T.rotation_matrix(np.pi/2, (1, 0, 0))
+MESH_TRANSFORM_CANONICAL = { # to gain a shared canonical orientation
+ "armadillo" : T.rotation_matrix(np.pi, (0, 0, 1)) @ MESH_TRANSFORM_SKYWARD,
+ "asian_dragon" : T.rotation_matrix(-np.pi/2, (0, 0, 1)) @ MESH_TRANSFORM_SKYWARD,
+ "bunny" : MESH_TRANSFORM_SKYWARD,
+ "dragon" : MESH_TRANSFORM_SKYWARD,
+ "drill_bit" : MESH_TRANSFORM_SKYWARD,
+ "happy_buddha" : MESH_TRANSFORM_SKYWARD,
+ "lucy" : T.rotation_matrix(np.pi, (0, 0, 1)),
+ "thai_statue" : MESH_TRANSFORM_SKYWARD,
+}
+
+def list_object_names() -> list[str]:
+ # downloaded only:
+ return [
+ i for i, v in config.MODELS.items()
+ if (config.DATA_PATH / "meshes" / v.mesh_fname).is_file()
+ ]
+
+@lru_cache(maxsize=1)
+def list_mesh_scan_sphere_coords(n_poses: int = 50) -> list[tuple[float, float]]: # (theta, phi)
+ return points.generate_equidistant_sphere_points(n_poses, compute_sphere_coordinates=True)#, shift_theta=True
+
+def mesh_scan_identifier(*, phi: float, theta: float) -> str:
+ return (
+ f"{'np'[theta>=0]}{abs(theta):.2f}"
+ f"{'np'[phi >=0]}{abs(phi) :.2f}"
+ ).replace(".", "d")
+
+@lru_cache(maxsize=1)
+def list_mesh_scan_identifiers(n_poses: int = 50) -> list[str]:
+ out = [
+ mesh_scan_identifier(phi=phi, theta=theta)
+ for theta, phi in list_mesh_scan_sphere_coords(n_poses)
+ ]
+ assert len(out) == len(set(out))
+ return out
+
+# ===
+
+@lru_cache(maxsize=1)
+def read_mesh(obj_name: str) -> trimesh.Trimesh:
+ path = config.DATA_PATH / "meshes" / config.MODELS[obj_name].mesh_fname
+ if not path.exists():
+ raise FileNotFoundError(f"{obj_name = } -> {str(path) = }")
+ try:
+ if path.suffixes[-1] == ".gz":
+ with gzip.open(path, "r") as f:
+ mesh = trimesh.load(f, file_type="".join(path.suffixes[:-1])[1:])
+ else:
+ mesh = trimesh.load(path)
+ except Exception as e:
+ raise MalformedMesh(f"Trimesh raised: {e.__class__.__name__}: {e}") from e
+
+ # rotate to be upright in pyrender
+ mesh.apply_transform(MESH_TRANSFORM_CANONICAL.get(obj_name, MESH_TRANSFORM_SKYWARD))
+
+ return mesh
+
+# === single-view scan clouds
+
+def compute_mesh_scan_point_cloud(
+ obj_name : str,
+ *,
+ phi : float,
+ theta : float,
+ compute_miss_distances : bool = False,
+ compute_normals : bool = True,
+ convert_ok : bool = False, # this does not respect the other hparams
+ **kw,
+ ) -> SingleViewScan:
+
+ if convert_ok:
+ try:
+ return read_mesh_scan_uv(obj_name, phi=phi, theta=theta).to_scan()
+ except FileNotFoundError:
+ pass
+
+ mesh = read_mesh(obj_name)
+ return SingleViewScan.from_mesh_single_view(mesh,
+ phi = phi,
+ theta = theta,
+ compute_normals = compute_normals,
+ compute_miss_distances = compute_miss_distances,
+ **kw,
+ )
+
+def precompute_mesh_scan_point_clouds(obj_names, *, page: tuple[int, int] = (0, 1), force: bool = False, debug: bool = False, n_poses: int = 50, **kw):
+ "precomputes all single-view scan clouds and stores them as HDF5 datasets"
+ cam_poses = list_mesh_scan_sphere_coords(n_poses)
+ pose_identifiers = list_mesh_scan_identifiers (n_poses)
+ assert len(cam_poses) == len(pose_identifiers)
+ paths = list_mesh_scan_point_cloud_h5_fnames(obj_names, pose_identifiers)
+ mlen = max(map(len, config.MODELS.keys()))
+ pretty_identifiers = [
+ f"{obj_name.ljust(mlen)} @ {i:>5} @ ({itentifier}: {theta:.2f}, {phi:.2f})"
+ for obj_name in obj_names
+ for i, (itentifier, (theta, phi)) in enumerate(zip(pose_identifiers, cam_poses))
+ ]
+ mesh_cache = []
+ @wraps(compute_mesh_scan_point_cloud)
+ def computer(pretty_identifier: str) -> SingleViewScan:
+ obj_name, index, _ = map(str.strip, pretty_identifier.split("@"))
+ theta, phi = cam_poses[int(index)]
+ return compute_mesh_scan_point_cloud(obj_name, phi=phi, theta=theta, _mesh_cache=mesh_cache, **kw)
+ return processing.precompute_data(computer, pretty_identifiers, paths, page=page, force=force, debug=debug)
+
+def read_mesh_scan_point_cloud(obj_name, *, identifier: str = None, phi: float = None, theta: float = None) -> SingleViewScan:
+ if identifier is None:
+ if phi is None or theta is None:
+ raise ValueError("Provide either phi+theta or an identifier!")
+ identifier = mesh_scan_identifier(phi=phi, theta=theta)
+ file = config.DATA_PATH / "clouds" / obj_name / f"mesh_scan_{identifier}_clouds.h5"
+ if not file.exists(): raise FileNotFoundError(str(file))
+ return SingleViewScan.from_h5_file(file)
+
+def list_mesh_scan_point_cloud_h5_fnames(obj_names: Iterable[str], identifiers: Optional[Iterable[str]] = None, **kw) -> list[Path]:
+ if identifiers is None:
+ identifiers = list_mesh_scan_identifiers(**kw)
+ return [
+ config.DATA_PATH / "clouds" / obj_name / f"mesh_scan_{identifier}_clouds.h5"
+ for obj_name in obj_names
+ for identifier in identifiers
+ ]
+
+# === single-view UV scan clouds
+
+def compute_mesh_scan_uv(
+ obj_name : str,
+ *,
+ phi : float,
+ theta : float,
+ compute_miss_distances : bool = False,
+ fill_missing_points : bool = False,
+ compute_normals : bool = True,
+ convert_ok : bool = False,
+ **kw,
+ ) -> SingleViewUVScan:
+
+ if convert_ok:
+ try:
+ return read_mesh_scan_point_cloud(obj_name, phi=phi, theta=theta).to_uv_scan()
+ except FileNotFoundError:
+ pass
+
+ mesh = read_mesh(obj_name)
+ scan = SingleViewUVScan.from_mesh_single_view(mesh,
+ phi = phi,
+ theta = theta,
+ compute_normals = compute_normals,
+ **kw,
+ )
+ if compute_miss_distances:
+ scan.compute_miss_distances()
+ if fill_missing_points:
+ scan.fill_missing_points()
+
+ return scan
+
+def precompute_mesh_scan_uvs(obj_names, *, page: tuple[int, int] = (0, 1), force: bool = False, debug: bool = False, n_poses: int = 50, **kw):
+ "precomputes all single-view scan clouds and stores them as HDF5 datasets"
+ cam_poses = list_mesh_scan_sphere_coords(n_poses)
+ pose_identifiers = list_mesh_scan_identifiers (n_poses)
+ assert len(cam_poses) == len(pose_identifiers)
+ paths = list_mesh_scan_uv_h5_fnames(obj_names, pose_identifiers)
+ mlen = max(map(len, config.MODELS.keys()))
+ pretty_identifiers = [
+ f"{obj_name.ljust(mlen)} @ {i:>5} @ ({itentifier}: {theta:.2f}, {phi:.2f})"
+ for obj_name in obj_names
+ for i, (itentifier, (theta, phi)) in enumerate(zip(pose_identifiers, cam_poses))
+ ]
+ mesh_cache = []
+ @wraps(compute_mesh_scan_uv)
+ def computer(pretty_identifier: str) -> SingleViewScan:
+ obj_name, index, _ = map(str.strip, pretty_identifier.split("@"))
+ theta, phi = cam_poses[int(index)]
+ return compute_mesh_scan_uv(obj_name, phi=phi, theta=theta, _mesh_cache=mesh_cache, **kw)
+ return processing.precompute_data(computer, pretty_identifiers, paths, page=page, force=force, debug=debug)
+
+def read_mesh_scan_uv(obj_name, *, identifier: str = None, phi: float = None, theta: float = None) -> SingleViewUVScan:
+ if identifier is None:
+ if phi is None or theta is None:
+ raise ValueError("Provide either phi+theta or an identifier!")
+ identifier = mesh_scan_identifier(phi=phi, theta=theta)
+ file = config.DATA_PATH / "clouds" / obj_name / f"mesh_scan_{identifier}_uv.h5"
+ if not file.exists(): raise FileNotFoundError(str(file))
+ return SingleViewUVScan.from_h5_file(file)
+
+def list_mesh_scan_uv_h5_fnames(obj_names: Iterable[str], identifiers: Optional[Iterable[str]] = None, **kw) -> list[Path]:
+ if identifiers is None:
+ identifiers = list_mesh_scan_identifiers(**kw)
+ return [
+ config.DATA_PATH / "clouds" / obj_name / f"mesh_scan_{identifier}_uv.h5"
+ for obj_name in obj_names
+ for identifier in identifiers
+ ]
+
+# === sphere-view (UV) scan clouds
+
+def compute_mesh_sphere_scan(
+ obj_name : str,
+ *,
+ compute_normals : bool = True,
+ **kw,
+ ) -> SingleViewUVScan:
+ mesh = read_mesh(obj_name)
+ scan = SingleViewUVScan.from_mesh_sphere_view(mesh,
+ compute_normals = compute_normals,
+ **kw,
+ )
+ return scan
+
+def precompute_mesh_sphere_scan(obj_names, *, page: tuple[int, int] = (0, 1), force: bool = False, debug: bool = False, n_points: int = 4000, **kw):
+ "precomputes all single-view scan clouds and stores them as HDF5 datasets"
+ paths = list_mesh_sphere_scan_h5_fnames(obj_names)
+ @wraps(compute_mesh_sphere_scan)
+ def computer(obj_name: str) -> SingleViewScan:
+ return compute_mesh_sphere_scan(obj_name, **kw)
+ return processing.precompute_data(computer, obj_names, paths, page=page, force=force, debug=debug)
+
+def read_mesh_mesh_sphere_scan(obj_name) -> SingleViewUVScan:
+ file = config.DATA_PATH / "clouds" / obj_name / "mesh_sphere_scan.h5"
+ if not file.exists(): raise FileNotFoundError(str(file))
+ return SingleViewUVScan.from_h5_file(file)
+
+def list_mesh_sphere_scan_h5_fnames(obj_names: Iterable[str]) -> list[Path]:
+ return [
+ config.DATA_PATH / "clouds" / obj_name / "mesh_sphere_scan.h5"
+ for obj_name in obj_names
+ ]
diff --git a/ifield/datasets/__init__.py b/ifield/datasets/__init__.py
new file mode 100644
index 0000000..f9c3db4
--- /dev/null
+++ b/ifield/datasets/__init__.py
@@ -0,0 +1,3 @@
+__doc__ = """
+Submodules defining various `torch.utils.data.Dataset`
+"""
diff --git a/ifield/datasets/common.py b/ifield/datasets/common.py
new file mode 100644
index 0000000..0e60162
--- /dev/null
+++ b/ifield/datasets/common.py
@@ -0,0 +1,196 @@
+from ..data.common.h5_dataclasses import H5Dataclass, PathLike
+from torch.utils.data import Dataset, IterableDataset
+from typing import Any, Iterable, Hashable, TypeVar, Iterator, Callable
+from functools import partial, lru_cache
+import inspect
+
+
+T = TypeVar("T")
+T_H5 = TypeVar("T_H5", bound=H5Dataclass)
+
+
+class TransformableDatasetMixin:
+ def __init_subclass__(cls):
+ if getattr(cls, "_transformable_mixin_no_override_getitem", False):
+ pass
+ elif issubclass(cls, Dataset):
+ if cls.__getitem__ is not cls._transformable_mixin_getitem_wrapper:
+ cls._transformable_mixin_inner_getitem = cls.__getitem__
+ cls.__getitem__ = cls._transformable_mixin_getitem_wrapper
+ elif issubclass(cls, IterableDataset):
+ if cls.__iter__ is not cls._transformable_mixin_iter_wrapper:
+ cls._transformable_mixin_inner_iter = cls.__iter__
+ cls.__iter__ = cls._transformable_mixin_iter_wrapper
+ else:
+ raise TypeError(f"{cls.__name__!r} is neither a Dataset nor a IterableDataset!")
+
+ def __init__(self, *a, **kw):
+ super().__init__(*a, **kw)
+ self._transforms = []
+
+ # works as a decorator
+ def map(self: T, func: callable = None, /, args=[], **kw) -> T:
+ def wrapper(func) -> T:
+ if args or kw:
+ func = partial(func, *args, **kw)
+ self._transforms.append(func)
+ return self
+
+ if func is None:
+ return wrapper
+ else:
+ return wrapper(func)
+
+
+ def _transformable_mixin_getitem_wrapper(self, index: int):
+ if not self._transforms:
+ out = self._transformable_mixin_inner_getitem(index) # (TransformableDatasetMixin, no transforms)
+ else:
+ out = self._transformable_mixin_inner_getitem(index) # (TransformableDatasetMixin, has transforms)
+ for f in self._transforms:
+ out = f(out) # (TransformableDatasetMixin)
+ return out
+
+ def _transformable_mixin_iter_wrapper(self):
+ if not self._transforms:
+ out = self._transformable_mixin_inner_iter() # (TransformableDatasetMixin, no transforms)
+ else:
+ out = self._transformable_mixin_inner_iter() # (TransformableDatasetMixin, has transforms)
+ for f in self._transforms:
+ out = map(f, out) # (TransformableDatasetMixin)
+ return out
+
+
+class TransformedDataset(Dataset, TransformableDatasetMixin):
+ # used to wrap an another dataset
+ def __init__(self, dataset: Dataset, transforms: Iterable[callable]):
+ super().__init__()
+ self.dataset = dataset
+ for i in transforms:
+ self.map(i)
+
+ def __len__(self):
+ return len(self.dataset)
+
+ def __getitem__(self, index: int):
+ return self.dataset[index] # (TransformedDataset)
+
+
+class TransformExtendedDataset(Dataset, TransformableDatasetMixin):
+ _transformable_mixin_no_override_getitem = True
+ def __init__(self, dataset: Dataset):
+ super().__init__()
+ self.dataset = dataset
+
+ def __len__(self):
+ return len(self.dataset) * len(self._transforms)
+
+ def __getitem__(self, index: int):
+ n = len(self._transforms)
+ assert n > 0, f"{len(self._transforms) = }"
+
+ item = index // n
+ transform = self._transforms[index % n]
+ return transform(self.dataset[item])
+
+
+class CachedDataset(Dataset):
+ # used to wrap an another dataset
+ def __init__(self, dataset: Dataset, cache_size: int | None):
+ super().__init__()
+ self.dataset = dataset
+ if cache_size is not None and cache_size > 0:
+ self.cached_getter = lru_cache(cache_size, self.dataset.__getitem__)
+ else:
+ self.cached_getter = self.dataset.__getitem__
+
+ def __len__(self):
+ return len(self.dataset)
+
+ def __getitem__(self, index: int):
+ return self.cached_getter(index)
+
+
+class AutodecoderDataset(Dataset, TransformableDatasetMixin):
+ def __init__(self,
+ keys : Iterable[Hashable],
+ dataset : Dataset,
+ ):
+ super().__init__()
+ self.ad_mapping = list(keys)
+ self.dataset = dataset
+ if len(self.ad_mapping) != len(dataset):
+ raise ValueError(f"__len__ mismatch between keys and dataset: {len(self.ad_mapping)} != {len(dataset)}")
+
+ def __len__(self) -> int:
+ return len(self.dataset)
+
+ def __getitem__(self, index: int) -> tuple[Hashable, Any]:
+ return self.ad_mapping[index], self.dataset[index] # (AutodecoderDataset)
+
+ def keys(self) -> list[Hashable]:
+ return self.ad_mapping
+
+ def values(self) -> Iterator:
+ return iter(self.dataset)
+
+ def items(self) -> Iterable[tuple[Hashable, Any]]:
+ return zip(self.ad_mapping, self.dataset)
+
+
+class FunctionDataset(Dataset, TransformableDatasetMixin):
+ def __init__(self,
+ getter : Callable[[Hashable], T],
+ keys : list[Hashable],
+ cache_size : int | None = None,
+ ):
+ super().__init__()
+ if cache_size is not None and cache_size > 0:
+ getter = lru_cache(cache_size)(getter)
+ self.getter = getter
+ self.keys = keys
+
+ def __len__(self) -> int:
+ return len(self.keys)
+
+ def __getitem__(self, index: int) -> T:
+ return self.getter(self.keys[index])
+
+class H5Dataset(FunctionDataset):
+ def __init__(self,
+ h5_dataclass_cls : type[T_H5],
+ fnames : list[PathLike],
+ **kw,
+ ):
+ super().__init__(
+ getter = h5_dataclass_cls.from_h5_file,
+ keys = fnames,
+ **kw,
+ )
+
+class PaginatedH5Dataset(Dataset, TransformableDatasetMixin):
+ def __init__(self,
+ h5_dataclass_cls : type[T_H5],
+ fnames : list[PathLike],
+ n_pages : int = 10,
+ require_even_pages : bool = True,
+ ):
+ super().__init__()
+ self.h5_dataclass_cls = h5_dataclass_cls
+ self.fnames = fnames
+ self.n_pages = n_pages
+ self.require_even_pages = require_even_pages
+
+ def __len__(self) -> int:
+ return len(self.fnames) * self.n_pages
+
+ def __getitem__(self, index: int) -> T_H5:
+ item = index // self.n_pages
+ page = index % self.n_pages
+
+ return self.h5_dataclass_cls.from_h5_file( # (PaginatedH5Dataset)
+ fname = self.fname[item],
+ page = page,
+ n_pages = self.n_pages,
+ require_even_pages = self.require_even_pages,
+ )
diff --git a/ifield/datasets/coseg.py b/ifield/datasets/coseg.py
new file mode 100644
index 0000000..a5d76f9
--- /dev/null
+++ b/ifield/datasets/coseg.py
@@ -0,0 +1,40 @@
+from . import common
+from ..data.coseg import config
+from ..data.coseg import read
+from ..data.common import scan
+from typing import Iterable, Optional, Union
+import os
+
+
+class SingleViewUVScanDataset(common.H5Dataset):
+ def __init__(self,
+ object_sets : tuple[str],
+ identifiers : Optional[Iterable[str]] = None,
+ data_path : Union[str, os.PathLike, None] = None,
+ ):
+ if not object_sets:
+ raise ValueError("'object_sets' cannot be empty!")
+ if identifiers is None:
+ identifiers = read.list_mesh_scan_identifiers()
+ if data_path is not None:
+ config.DATA_PATH = data_path
+ models = read.list_model_ids(object_sets)
+ fnames = read.list_mesh_scan_uv_h5_fnames(models, identifiers)
+ super().__init__(
+ h5_dataclass_cls = scan.SingleViewUVScan,
+ fnames = fnames,
+ )
+
+class AutodecoderSingleViewUVScanDataset(common.AutodecoderDataset):
+ def __init__(self,
+ object_sets : tuple[str],
+ identifiers : Optional[Iterable[str]] = None,
+ data_path : Union[str, os.PathLike, None] = None,
+ ):
+ if identifiers is None:
+ identifiers = read.list_mesh_scan_identifiers()
+ # here do this step first, such that all the duplicate strings reference the same object
+ super().__init__(
+ keys = [key for key in read.list_model_id_strings(object_sets) for _ in range(len(identifiers))],
+ dataset = SingleViewUVScanDataset(object_sets, identifiers, data_path=data_path),
+ )
diff --git a/ifield/datasets/stanford.py b/ifield/datasets/stanford.py
new file mode 100644
index 0000000..2b6f92b
--- /dev/null
+++ b/ifield/datasets/stanford.py
@@ -0,0 +1,64 @@
+from . import common
+from ..data.stanford import config
+from ..data.stanford import read
+from ..data.common import scan
+from typing import Iterable, Optional, Union
+import os
+
+
+class SingleViewUVScanDataset(common.H5Dataset):
+ def __init__(self,
+ obj_names : Iterable[str],
+ identifiers : Optional[Iterable[str]] = None,
+ data_path : Union[str, os.PathLike, None] = None,
+ ):
+ if not obj_names:
+ raise ValueError("'obj_names' cannot be empty!")
+ if identifiers is None:
+ identifiers = read.list_mesh_scan_identifiers()
+ if data_path is not None:
+ config.DATA_PATH = data_path
+ fnames = read.list_mesh_scan_uv_h5_fnames(obj_names, identifiers)
+ super().__init__(
+ h5_dataclass_cls = scan.SingleViewUVScan,
+ fnames = fnames,
+ )
+
+class AutodecoderSingleViewUVScanDataset(common.AutodecoderDataset):
+ def __init__(self,
+ obj_names : Iterable[str],
+ identifiers : Optional[Iterable[str]] = None,
+ data_path : Union[str, os.PathLike, None] = None,
+ ):
+ if identifiers is None:
+ identifiers = read.list_mesh_scan_identifiers()
+ super().__init__(
+ keys = [obj_name for obj_name in obj_names for _ in range(len(identifiers))],
+ dataset = SingleViewUVScanDataset(obj_names, identifiers, data_path=data_path),
+ )
+
+
+class SphereScanDataset(common.H5Dataset):
+ def __init__(self,
+ obj_names : Iterable[str],
+ data_path : Union[str, os.PathLike, None] = None,
+ ):
+ if not obj_names:
+ raise ValueError("'obj_names' cannot be empty!")
+ if data_path is not None:
+ config.DATA_PATH = data_path
+ fnames = read.list_mesh_sphere_scan_h5_fnames(obj_names)
+ super().__init__(
+ h5_dataclass_cls = scan.SingleViewUVScan,
+ fnames = fnames,
+ )
+
+class AutodecoderSphereScanDataset(common.AutodecoderDataset):
+ def __init__(self,
+ obj_names : Iterable[str],
+ data_path : Union[str, os.PathLike, None] = None,
+ ):
+ super().__init__(
+ keys = obj_names,
+ dataset = SphereScanDataset(obj_names, data_path=data_path),
+ )
diff --git a/ifield/logging.py b/ifield/logging.py
new file mode 100644
index 0000000..fcf2c41
--- /dev/null
+++ b/ifield/logging.py
@@ -0,0 +1,258 @@
+from . import param
+from dataclasses import dataclass
+from pathlib import Path
+from pytorch_lightning.utilities import rank_zero_only
+from pytorch_lightning.utilities.exceptions import MisconfigurationException
+from typing import Union, Literal, Optional, TypeVar
+import concurrent.futures
+import psutil
+import pytorch_lightning as pl
+import statistics
+import threading
+import time
+import torch
+import yaml
+
+# from https://github.com/yaml/pyyaml/issues/240#issuecomment-1018712495
+def str_presenter(dumper, data):
+ """configures yaml for dumping multiline strings
+ Ref: https://stackoverflow.com/questions/8640959/how-can-i-control-what-scalar-form-pyyaml-uses-for-my-data"""
+ if len(data.splitlines()) > 1: # check for multiline string
+ return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|')
+ return dumper.represent_scalar('tag:yaml.org,2002:str', data)
+yaml.add_representer(str, str_presenter)
+
+
+LoggerStr = Literal[
+ #"csv",
+ "tensorboard",
+ #"mlflow",
+ #"comet",
+ #"neptune",
+ #"wandb",
+ None]
+try:
+ Logger = TypeVar("L", bound=pl.loggers.Logger)
+except AttributeError:
+ Logger = TypeVar("L", bound=pl.loggers.base.LightningLoggerBase)
+
+def make_logger(
+ experiment_name : str,
+ default_root_dir : Union[str, Path], # from pl.Trainer
+ save_dir : Union[str, Path],
+ type : LoggerStr = "tensorboard",
+ project : str = "ifield",
+ ) -> Optional[Logger]:
+ if type is None:
+ return None
+ elif type == "tensorboard":
+ return pl.loggers.TensorBoardLogger(
+ name = "tensorboard",
+ save_dir = Path(default_root_dir) / save_dir,
+ version = experiment_name,
+ log_graph = True,
+ )
+ raise ValueError(f"make_logger({type=})")
+
+def make_jinja_template(*, save_dir: Union[None, str, Path], **kw) -> str:
+ return param.make_jinja_template(make_logger,
+ defaults = dict(
+ save_dir = save_dir,
+ ),
+ exclude_list = {
+ "experiment_name",
+ "default_root_dir",
+ },
+ **({"name": "logging"} | kw),
+ )
+
+def get_checkpoints(experiment_name, default_root_dir, save_dir, type, project) -> list[Path]:
+ if type is None:
+ return None
+ if type == "tensorboard":
+ folder = Path(default_root_dir) / save_dir / "tensorboard" / experiment_name
+ return folder.glob("*.ckpt")
+ if type == "mlflow":
+ raise NotImplementedError(f"{type=}")
+ if type == "wandb":
+ raise NotImplementedError(f"{type=}")
+ raise ValueError(f"get_checkpoint({type=})")
+
+
+def log_config(_logger: Logger, **kwargs: Union[str, dict, list, int, float]):
+ assert isinstance(_logger, pl.loggers.Logger) \
+ or isinstance(_logger, pl.loggers.base.LightningLoggerBase), _logger
+
+ _logger: pl.loggers.TensorBoardLogger
+ _logger.log_hyperparams(params=kwargs)
+
+@dataclass
+class ModelOutputMonitor(pl.callbacks.Callback):
+ log_training : bool = True
+ log_validation : bool = True
+
+ def setup(self, trainer: pl.Trainer, pl_module: pl.LightningModule, stage: Optional[str] = None) -> None:
+ if not trainer.loggers:
+ raise MisconfigurationException(f"Cannot use {self._class__.__name__} callback with Trainer that has no logger.")
+
+ @staticmethod
+ def _log_outputs(trainer: pl.Trainer, pl_module: pl.LightningModule, outputs, fname: str):
+ if outputs is None:
+ return
+ elif isinstance(outputs, list) or isinstance(outputs, tuple):
+ outputs = {
+ f"loss[{i}]": v
+ for i, v in enumerate(outputs)
+ }
+ elif isinstance(outputs, torch.Tensor):
+ outputs = {
+ "loss": outputs,
+ }
+ elif isinstance(outputs, dict):
+ pass
+ else:
+ raise ValueError
+ sep = trainer.logger.group_separator
+ pl_module.log_dict({
+ f"{pl_module.__class__.__qualname__}.{fname}{sep}{k}":
+ float(v.item()) if isinstance(v, torch.Tensor) else float(v)
+ for k, v in outputs.items()
+ }, sync_dist=True)
+
+ def on_train_batch_end(self, trainer: pl.Trainer, pl_module: pl.LightningModule, outputs, batch, batch_idx, unused=0):
+ if self.log_training:
+ self._log_outputs(trainer, pl_module, outputs, "training_step")
+
+ def on_validation_batch_end(self, trainer: pl.Trainer, pl_module: pl.LightningModule, outputs, batch, batch_idx, dataloader_idx=0):
+ if self.log_validation:
+ self._log_outputs(trainer, pl_module, outputs, "validation_step")
+
+class EpochTimeMonitor(pl.callbacks.Callback):
+ __slots__ = [
+ "epoch_start",
+ "epoch_start_train",
+ "epoch_start_validation",
+ "epoch_start_test",
+ "epoch_start_predict",
+ ]
+
+ def setup(self, trainer: pl.Trainer, pl_module: pl.LightningModule, stage: Optional[str] = None) -> None:
+ if not trainer.loggers:
+ raise MisconfigurationException(f"Cannot use {self._class__.__name__} callback with Trainer that has no logger.")
+
+
+ @rank_zero_only
+ def on_train_epoch_start(self, trainer: pl.Trainer, pl_module: pl.LightningModule):
+ self.epoch_start_train = time.time()
+
+ @rank_zero_only
+ def on_validation_epoch_start(self, trainer: pl.Trainer, pl_module: pl.LightningModule):
+ self.epoch_start_validation = time.time()
+
+ @rank_zero_only
+ def on_test_epoch_start(self, trainer: pl.Trainer, pl_module: pl.LightningModule):
+ self.epoch_start_test = time.time()
+
+ @rank_zero_only
+ def on_predict_epoch_start(self, trainer: pl.Trainer, pl_module: pl.LightningModule):
+ self.epoch_start_predict = time.time()
+
+ @rank_zero_only
+ def on_train_epoch_end(self, trainer: pl.Trainer, pl_module: pl.LightningModule):
+ t = time.time() - self.epoch_start_train
+ del self.epoch_start_train
+ sep = trainer.logger.group_separator
+ trainer.logger.log_metrics({f"{self.__class__.__qualname__}{sep}epoch_train_time" : t}, step=trainer.global_step)
+
+ @rank_zero_only
+ def on_validation_epoch_end(self, trainer: pl.Trainer, pl_module: pl.LightningModule):
+ t = time.time() - self.epoch_start_validation
+ del self.epoch_start_validation
+ sep = trainer.logger.group_separator
+ trainer.logger.log_metrics({f"{self.__class__.__qualname__}{sep}epoch_validation_time" : t}, step=trainer.global_step)
+
+ @rank_zero_only
+ def on_test_epoch_end(self, trainer: pl.Trainer, pl_module: pl.LightningModule):
+ t = time.time() - self.epoch_start_test
+ del self.epoch_start_validation
+ sep = trainer.logger.group_separator
+ trainer.logger.log_metrics({f"{self.__class__.__qualname__}{sep}epoch_test_time" : t}, step=trainer.global_step)
+
+ @rank_zero_only
+ def on_predict_epoch_end(self, trainer: pl.Trainer, pl_module: pl.LightningModule):
+ t = time.time() - self.epoch_start_predict
+ del self.epoch_start_validation
+ sep = trainer.logger.group_separator
+ trainer.logger.log_metrics({f"{self.__class__.__qualname__}{sep}epoch_predict_time" : t}, step=trainer.global_step)
+
+@dataclass
+class PsutilMonitor(pl.callbacks.Callback):
+ sample_rate : float = 0.2 # times per second
+
+ _should_stop = False
+
+ @rank_zero_only
+ def on_fit_start(self, trainer: pl.Trainer, pl_module: pl.LightningModule):
+ if not trainer.loggers:
+ raise MisconfigurationException(f"Cannot use {self._class__.__name__} callback with Trainer that has no logger.")
+ assert not hasattr(self, "_thread")
+
+ self._should_stop = False
+ self._thread = threading.Thread(
+ target = self.thread_target,
+ name = self.thread_target.__qualname__,
+ args = [trainer],
+ daemon=True,
+ )
+ self._thread.start()
+
+ @rank_zero_only
+ def on_fit_end(self, trainer: pl.Trainer, pl_module: pl.LightningModule):
+ assert getattr(self, "_thread", None) is not None
+ self._should_stop = True
+ del self._thread
+
+ def thread_target(self, trainer: pl.Trainer):
+ uses_gpu = isinstance(trainer.accelerator, (pl.accelerators.GPUAccelerator, pl.accelerators.CUDAAccelerator))
+ gpu_ids = trainer.device_ids
+
+ prefix = f"{self.__class__.__qualname__}{trainer.logger.group_separator}"
+
+ while not self._should_stop:
+ step = trainer.global_step
+ p = psutil.Process()
+
+ meminfo = p.memory_info()
+ rss_ram = meminfo.rss / 1024**2 # MB
+ vms_ram = meminfo.vms / 1024**2 # MB
+
+ util_per_cpu = psutil.cpu_percent(percpu=True)
+
+ util_per_cpu = [util_per_cpu[i] for i in p.cpu_affinity()]
+ util_total = statistics.mean(util_per_cpu)
+
+ if uses_gpu:
+ with concurrent.futures.ThreadPoolExecutor() as e:
+ if hasattr(pl.accelerators, "cuda"):
+ gpu_stats = e.map(pl.accelerators.cuda.get_nvidia_gpu_stats, gpu_ids)
+ else:
+ gpu_stats = e.map(pl.accelerators.gpu.get_nvidia_gpu_stats, gpu_ids)
+ trainer.logger.log_metrics({
+ f"{prefix}ram.rss" : rss_ram,
+ f"{prefix}ram.vms" : vms_ram,
+ f"{prefix}cpu.total" : util_total,
+ **{ f"{prefix}cpu.{i:03}.utilization" : stat for i, stat in enumerate(util_per_cpu) },
+ **{
+ f"{prefix}gpu.{gpu_idx:02}.{key.split(' ',1)[0]}" : stat
+ for gpu_idx, stats in zip(gpu_ids, gpu_stats)
+ for key, stat in stats.items()
+ },
+ }, step = step)
+ else:
+ trainer.logger.log_metrics({
+ f"{prefix}cpu.total" : util_total,
+ **{ f"{prefix}cpu.{i:03}.utilization" : stat for i, stat in enumerate(util_per_cpu) },
+ }, step = step)
+
+ time.sleep(1 / self.sample_rate)
+ print("DAEMON END")
diff --git a/ifield/models/__init__.py b/ifield/models/__init__.py
new file mode 100644
index 0000000..3771d5e
--- /dev/null
+++ b/ifield/models/__init__.py
@@ -0,0 +1,3 @@
+__doc__ = """
+Contains Pytorch Models
+"""
diff --git a/ifield/models/conditioning.py b/ifield/models/conditioning.py
new file mode 100644
index 0000000..d8821f2
--- /dev/null
+++ b/ifield/models/conditioning.py
@@ -0,0 +1,159 @@
+from abc import ABC, abstractmethod
+from torch import nn, Tensor
+from torch.nn.modules.module import _EXTRA_STATE_KEY_SUFFIX
+from typing import Hashable, Union, Optional, KeysView, ValuesView, ItemsView, Any, Sequence
+import torch
+
+
+class RequiresConditioner(nn.Module, ABC): # mixin
+
+ @property
+ @abstractmethod
+ def n_latent_features(self) -> int:
+ "This should provide the width of the conditioning feature vector"
+ ...
+
+ @property
+ @abstractmethod
+ def latent_embeddings_init_std(self) -> float:
+ "This should provide the standard deviation to initialize the latent features with. DeepSDF uses 0.01."
+ ...
+
+ @property
+ @abstractmethod
+ def latent_embeddings() -> Optional[Tensor]:
+ """This property should return a tensor cotnaining all stored embeddings, for use in computing auto-decoder losses"""
+ ...
+
+ @abstractmethod
+ def encode(self, batch: Any, batch_idx: int, optimizer_idx: int) -> Tensor:
+ "This should, given a training batch, return the encoded conditioning vector"
+ ...
+
+
+class AutoDecoderModuleMixin(RequiresConditioner, ABC):
+ """
+ Populates dunder methods making it behave as a mapping.
+ The mapping indexes into a stored set of learnable embedding vectors.
+
+ Based on the auto-decoder architecture of
+ J.J. Park, P. Florence, J. Straub, R. Newcombe, S. Lovegrove, DeepSDF:
+ Learning Continuous Signed Distance Functions for Shape Representation, in:
+ 2019 IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR),
+ IEEE, Long Beach, CA, USA, 2019: pp. 165–174.
+ https://doi.org/10.1109/CVPR.2019.00025.
+ """
+
+ _autodecoder_mapping: dict[Hashable, int]
+ autodecoder_embeddings: nn.Parameter
+
+ def __init__(self, *a, **kw):
+ super().__init__(*a, **kw)
+
+ @self._register_load_state_dict_pre_hook
+ def hook(state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs):
+ if f"{prefix}_autodecoder_mapping" in state_dict:
+ state_dict[f"{prefix}{_EXTRA_STATE_KEY_SUFFIX}"] = state_dict.pop(f"{prefix}_autodecoder_mapping")
+
+ class ICanBeLoadedFromCheckpointsAndChangeShapeStopBotheringMePyTorchAndSitInTheCornerIKnowWhatIAmDoing(nn.UninitializedParameter):
+ def copy_(self, other):
+ self.materialize(other.shape, other.device, other.dtype)
+ return self.copy_(other)
+ self.autodecoder_embeddings = ICanBeLoadedFromCheckpointsAndChangeShapeStopBotheringMePyTorchAndSitInTheCornerIKnowWhatIAmDoing()
+
+ # nn.Module interface
+
+ def get_extra_state(self):
+ return {
+ "ad_uids": getattr(self, "_autodecoder_mapping", {}),
+ }
+
+ def set_extra_state(self, obj):
+ if "ad_uids" not in obj: # backward compat
+ self._autodecoder_mapping = obj
+ else:
+ self._autodecoder_mapping = obj["ad_uids"]
+
+ # RequiresConditioner interface
+
+ @property
+ def latent_embeddings(self) -> Tensor:
+ return self.autodecoder_embeddings
+
+ # my interface
+
+ def set_observation_ids(self, z_uids: set[Hashable]):
+ assert self.latent_embeddings_init_std is not None, f"{self.__module__}.{self.__class__.__qualname__}.latent_embeddings_init_std"
+ assert self.n_latent_features is not None, f"{self.__module__}.{self.__class__.__qualname__}.n_latent_features"
+ assert self.latent_embeddings_init_std > 0, self.latent_embeddings_init_std
+ assert self.n_latent_features > 0, self.n_latent_features
+
+ self._autodecoder_mapping = {
+ k: i
+ for i, k in enumerate(sorted(set(z_uids)))
+ }
+
+ if not len(z_uids) == len(self._autodecoder_mapping):
+ raise ValueError(f"Observation identifiers are not unique! {z_uids = }")
+
+ self.autodecoder_embeddings = nn.Parameter(
+ torch.Tensor(len(self._autodecoder_mapping), self.n_latent_features)
+ .normal_(mean=0, std=self.latent_embeddings_init_std)
+ .to(self.device, self.dtype)
+ )
+
+ def add_key(self, z_uid: Hashable, z: Optional[Tensor] = None):
+ if z_uid in self._autodecoder_mapping:
+ raise ValueError(f"Observation identifier {z_uid!r} not unique!")
+
+ self._autodecoder_mapping[z_uid] = len(self._autodecoder_mapping)
+ self.autodecoder_embeddings
+ raise NotImplementedError
+
+ def __delitem__(self, z_uid: Hashable):
+ i = self._autodecoder_mapping.pop(z_uid)
+ for k, v in list(self._autodecoder_mapping.items()):
+ if v > i:
+ self._autodecoder_mapping[k] -= 1
+
+ with torch.no_grad():
+ self.autodecoder_embeddings = nn.Parameter(torch.cat((
+ self.autodecoder_embeddings.detach()[:i, :],
+ self.autodecoder_embeddings.detach()[i+1:, :],
+ ), dim=0))
+
+ def __contains__(self, z_uid: Hashable) -> bool:
+ return z_uid in self._autodecoder_mapping
+
+ def __getitem__(self, z_uids: Union[Hashable, Sequence[Hashable]]) -> Tensor:
+ if isinstance(z_uids, tuple) or isinstance(z_uids, list):
+ key = tuple(map(self._autodecoder_mapping.__getitem__, z_uids))
+ else:
+ key = self._autodecoder_mapping[z_uids]
+ return self.autodecoder_embeddings[key, :]
+
+ def __iter__(self):
+ return self._autodecoder_mapping.keys()
+
+ def keys(self) -> KeysView[Hashable]:
+ """
+ lists the identifiers of each code
+ """
+ return self._autodecoder_mapping.keys()
+
+ def values(self) -> ValuesView[Tensor]:
+ return list(self.autodecoder_embeddings)
+
+ def items(self) -> ItemsView[Hashable, Tensor]:
+ """
+ lists all the learned codes / latent vectors with their identifiers as keys
+ """
+ return {
+ k : self.autodecoder_embeddings[i]
+ for k, i in self._autodecoder_mapping.items()
+ }.items()
+
+class EncoderModuleMixin(RequiresConditioner, ABC):
+ @property
+ def latent_embeddings(self) -> None:
+ return None
diff --git a/ifield/models/intersection_fields.py b/ifield/models/intersection_fields.py
new file mode 100644
index 0000000..06a086d
--- /dev/null
+++ b/ifield/models/intersection_fields.py
@@ -0,0 +1,589 @@
+from .. import param
+from ..modules.dtype import DtypeMixin
+from ..utils import geometry
+from ..utils.helpers import compose
+from ..utils.loss import Schedulable, ensure_schedulables, HParamSchedule, HParamScheduleBase, Linear
+from ..utils.operators import diff
+from .conditioning import RequiresConditioner, AutoDecoderModuleMixin
+from .medial_atoms import MedialAtomNet
+from .orthogonal_plane import OrthogonalPlaneNet
+from pytorch_lightning.utilities.exceptions import MisconfigurationException
+from torch import Tensor
+from torch.nn import functional as F
+from typing import TypedDict, Literal, Union, Hashable, Optional
+import pytorch_lightning as pl
+import torch
+import os
+
+LOG_ALL_METRICS = bool(int(os.environ.get("IFIELD_LOG_ALL_METRICS", "1")))
+
+if __debug__:
+ def broadcast_tensors(*tensors: torch.Tensor) -> list[torch.Tensor]:
+ try:
+ return torch.broadcast_tensors(*tensors)
+ except RuntimeError as e:
+ shapes = ", ".join(f"{chr(c)}.size={tuple(t.shape)}" for c, t in enumerate(tensors, ord("a")))
+ raise ValueError(f"Could not broadcast tensors {shapes}.\n{str(e)}")
+else:
+ broadcast_tensors = torch.broadcast_tensors
+
+
+class ForwardDepthMapsBatch(TypedDict):
+ cam2world : Tensor # (B, 4, 4)
+ uv : Tensor # (B, H, W)
+ intrinsics : Tensor # (B, 3, 3)
+
+class ForwardScanRaysBatch(TypedDict):
+ origins : Tensor # (B, H, W, 3) or (B, 3)
+ dirs : Tensor # (B, H, W, 3)
+
+class LossBatch(TypedDict):
+ hits : Tensor # (B, H, W) dtype=bool
+ miss : Tensor # (B, H, W) dtype=bool
+ depths : Tensor # (B, H, W)
+ normals : Tensor # (B, H, W, 3) NaN if not hit
+ distances : Tensor # (B, H, W, 1) NaN if not miss
+
+class LabeledBatch(TypedDict):
+ z_uid : list[Hashable]
+
+ForwardBatch = Union[ForwardDepthMapsBatch, ForwardScanRaysBatch]
+TrainingBatch = Union[ForwardBatch, LossBatch, LabeledBatch]
+
+
+IntersectionMode = Literal[
+ "medial_sphere",
+ "orthogonal_plane",
+]
+
+class IntersectionFieldModel(pl.LightningModule, RequiresConditioner, DtypeMixin):
+ net: Union[MedialAtomNet, OrthogonalPlaneNet]
+
+ @ensure_schedulables
+ def __init__(self,
+ # mode
+ input_mode : geometry.RayEmbedding = "plucker",
+ output_mode : IntersectionMode = "medial_sphere",
+
+ # network
+ latent_features : int = 256,
+ hidden_features : int = 512,
+ hidden_layers : int = 8,
+ improve_miss_grads: bool = True,
+ normalize_ray_dirs: bool = False, # the dataset is usually already normalized, but this could still be important for backprop
+
+ # orthogonal plane
+ loss_hit_cross_entropy : Schedulable = 1.0,
+
+ # medial atoms
+ loss_intersection : Schedulable = 1,
+ loss_intersection_l2 : Schedulable = 0,
+ loss_intersection_proj : Schedulable = 0,
+ loss_intersection_proj_l2 : Schedulable = 0,
+ loss_normal_cossim : Schedulable = 0.25, # supervise target normal cosine similarity
+ loss_normal_euclid : Schedulable = 0, # supervise target normal l2 distance
+ loss_normal_cossim_proj : Schedulable = 0, # supervise target normal cosine similarity
+ loss_normal_euclid_proj : Schedulable = 0, # supervise target normal l2 distance
+ loss_hit_nodistance_l1 : Schedulable = 0, # constrain no miss distance for hits
+ loss_hit_nodistance_l2 : Schedulable = 32, # constrain no miss distance for hits
+ loss_miss_distance_l1 : Schedulable = 0, # supervise target miss distance for misses
+ loss_miss_distance_l2 : Schedulable = 0, # supervise target miss distance for misses
+ loss_inscription_hits : Schedulable = 0, # Penalize atom candidates using the supervision data of a different ray
+ loss_inscription_hits_l2: Schedulable = 0, # Penalize atom candidates using the supervision data of a different ray
+ loss_inscription_miss : Schedulable = 0, # Penalize atom candidates using the supervision data of a different ray
+ loss_inscription_miss_l2: Schedulable = 0, # Penalize atom candidates using the supervision data of a different ray
+ loss_sphere_grow_reg : Schedulable = 0, # maximialize sphere size
+ loss_sphere_grow_reg_hit: Schedulable = 0, # maximialize sphere size
+ loss_embedding_norm : Schedulable = "0.01**2 * Linear(15)", # DeepSDF schedules over 150 epochs. DeepSDF use 0.01**2, irobot uses 0.04**2
+ loss_multi_view_reg : Schedulable = 0, # minimize gradient w.r.t. delta ray dir, when ray origin = intersection
+ loss_atom_centroid_norm_std_reg : Schedulable = 0, # minimize per-atom centroid std
+
+ # optimization
+ opt_learning_rate : Schedulable = 1e-5,
+ opt_weight_decay : float = 0,
+ opt_warmup : float = 0,
+ **kw,
+ ):
+ super().__init__()
+ opt_warmup = Linear(opt_warmup)
+ opt_warmup._param_name = "opt_warmup"
+ self.save_hyperparameters()
+
+
+ if "half" in input_mode:
+ assert output_mode == "medial_sphere" and kw.get("n_atoms", 1) > 1
+
+ assert output_mode in ["medial_sphere", "orthogonal_plane"]
+ assert opt_weight_decay >= 0, opt_weight_decay
+
+ if output_mode == "orthogonal_plane":
+ self.net = OrthogonalPlaneNet(
+ in_features = self.n_input_embedding_features,
+ hidden_layers = hidden_layers,
+ hidden_features = hidden_features,
+ latent_features = latent_features,
+ **kw,
+ )
+ elif output_mode == "medial_sphere":
+ self.net = MedialAtomNet(
+ in_features = self.n_input_embedding_features,
+ hidden_layers = hidden_layers,
+ hidden_features = hidden_features,
+ latent_features = latent_features,
+ **kw,
+ )
+
+ def on_fit_start(self):
+ if __debug__:
+ for k, v in self.hparams.items():
+ if isinstance(v, HParamScheduleBase):
+ v.assert_positive(self.trainer.max_epochs)
+
+ @property
+ def n_input_embedding_features(self) -> int:
+ return geometry.ray_input_embedding_length(self.hparams.input_mode)
+
+ @property
+ def n_latent_features(self) -> int:
+ return self.hparams.latent_features
+
+ @property
+ def latent_embeddings_init_std(self) -> float:
+ return 0.01
+
+ @property
+ def is_conditioned(self):
+ return self.net.is_conditioned
+
+ @property
+ def is_double_backprop(self) -> bool:
+ return self.is_double_backprop_origins or self.is_double_backprop_dirs
+
+ @property
+ def is_double_backprop_origins(self) -> bool:
+ prif = self.hparams.output_mode == "orthogonal_plane"
+ return prif and self.hparams.loss_normal_cossim
+
+ @property
+ def is_double_backprop_dirs(self) -> bool:
+ return self.hparams.loss_multi_view_reg
+
+ @classmethod
+ @compose("\n".join)
+ def make_jinja_template(cls, *, exclude_list: set[str] = {}, top_level: bool = True, **kw) -> str:
+ yield param.make_jinja_template(cls, top_level=top_level, **kw)
+ yield MedialAtomNet.make_jinja_template(top_level=False, exclude_list={
+ "in_features",
+ "hidden_layers",
+ "hidden_features",
+ "latent_features",
+ })
+
+ def batch2rays(self, batch: ForwardBatch) -> tuple[Tensor, Tensor]:
+ if "uv" in batch:
+ raise NotImplementedError
+ assert not (self.hparams.loss_multi_view_reg and self.training)
+ ray_origins, \
+ ray_dirs, \
+ = geometry.camera_uv_to_rays(
+ cam2world = batch["cam2world"],
+ uv = batch["uv"],
+ intrinsics = batch["intrinsics"],
+ )
+ else:
+ ray_origins = batch["points" if self.hparams.loss_multi_view_reg and self.training else "origins"]
+ ray_dirs = batch["dirs"]
+ return ray_origins, ray_dirs
+
+ def forward(self,
+ batch : ForwardBatch,
+ z : Optional[Tensor] = None, # latent code
+ *,
+ return_input : bool = False,
+ allow_nans : bool = False, # in output
+ **kw,
+ ) -> tuple[torch.Tensor, ...]:
+ (
+ ray_origins, # (B, 3)
+ ray_dirs, # (B, H, W, 3)
+ ) = self.batch2rays(batch)
+
+ # Ensure rays are normalized
+ # NOTICE: this is slow, make sure to train with optimizations!
+ assert ray_dirs.detach().norm(dim=-1).allclose(torch.ones(ray_dirs.shape[:-1], **self.device_and_dtype)),\
+ ray_dirs.detach().norm(dim=-1)
+
+ if ray_origins.ndim + 2 == ray_dirs.ndim:
+ ray_origins = ray_origins[..., None, None, :]
+
+ ray_origins, ray_dirs = broadcast_tensors(ray_origins, ray_dirs)
+
+ if self.is_double_backprop and self.training:
+ if self.is_double_backprop_dirs:
+ ray_dirs.requires_grad = True
+ if self.is_double_backprop_origins:
+ ray_origins.requires_grad = True
+ assert ray_origins.requires_grad or ray_dirs.requires_grad
+
+ input = geometry.ray_input_embedding(
+ ray_origins, ray_dirs,
+ mode = self.hparams.input_mode,
+ normalize_dirs = self.hparams.normalize_ray_dirs,
+ is_training = self.training,
+ )
+ assert not input.detach().isnan().any()
+
+ predictions = self.net(input, z)
+
+ intersections = self.net.compute_intersections(
+ ray_origins, ray_dirs, predictions,
+ allow_nans = allow_nans and not self.training, **kw
+ )
+ if return_input:
+ return ray_origins, ray_dirs, input, intersections
+ else:
+ return intersections
+
+ def training_step(self, batch: TrainingBatch, batch_idx: int, *, is_validation=False) -> Tensor:
+ z = self.encode(batch) if self.is_conditioned else None
+ assert self.is_conditioned or len(set(batch["z_uid"])) <= 1, \
+ f"Network is unconditioned, but the batch has multiple uids: {set(batch['z_uid'])!r}"
+
+ # unpack
+ target_hits = batch["hits"] # (B, H, W) dtype=bool
+ target_miss = batch["miss"] # (B, H, W) dtype=bool
+ target_points = batch["points"] # (B, H, W, 3)
+ target_normals = batch["normals"] # (B, H, W, 3) NaN if not hit
+ target_distances = batch["distances"] # (B, H, W) NaN if not miss
+ assert not target_normals [target_hits].isnan().any()
+ assert not target_distances[target_miss].isnan().any()
+ target_normals[target_normals.isnan()] = 0
+ assert not target_normals .isnan().any()
+
+ # make z fit batch scheme
+ if z is not None:
+ z = z[..., None, None, :]
+
+ losses = {}
+ metrics = {}
+ zeros = torch.zeros_like(target_distances)
+
+ if self.hparams.output_mode == "medial_sphere":
+ assert isinstance(self.net, MedialAtomNet)
+ ray_origins, ray_dirs, plucker, (
+ depths, # (...) float, projection if not hit
+ silhouettes, # (...) float
+ intersections, # (..., 3) float, projection or NaN if not hit
+ intersection_normals, # (..., 3) float, rejection or NaN if not hit
+ is_intersecting, # (...) bool, true if hit
+ sphere_centers, # (..., 3) network output
+ sphere_radii, # (...) network output
+
+ atom_indices,
+ all_intersections, # (..., N_ATOMS) float, projection or NaN if not hit
+ all_intersection_normals, # (..., N_ATOMS, 3) float, rejection or NaN if not hit
+ all_depths, # (..., N_ATOMS) float, projection if not hit
+ all_silhouettes, # (..., N_ATOMS, 3) float, projection or NaN if not hit
+ all_is_intersecting, # (..., N_ATOMS) bool, true if hit
+ all_sphere_centers, # (..., N_ATOMS, 3) network output
+ all_sphere_radii, # (..., N_ATOMS) network output
+ ) = self(batch, z,
+ intersections_only = False,
+ return_all_atoms = True,
+ allow_nans = False,
+ return_input = True,
+ improve_miss_grads = True,
+ )
+
+ # target hit supervision
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_intersection: # scores true hits
+ losses["loss_intersection"] = (
+ (target_points - intersections).norm(dim=-1)
+ ).where(target_hits & is_intersecting, zeros).mean()
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_intersection_l2: # scores true hits
+ losses["loss_intersection_l2"] = (
+ (target_points - intersections).pow(2).sum(dim=-1)
+ ).where(target_hits & is_intersecting, zeros).mean()
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_intersection_proj: # scores misses as if they were hits, using the projection
+ losses["loss_intersection_proj"] = (
+ (target_points - intersections).norm(dim=-1)
+ ).where(target_hits, zeros).mean()
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_intersection_proj_l2: # scores misses as if they were hits, using the projection
+ losses["loss_intersection_proj_l2"] = (
+ (target_points - intersections).pow(2).sum(dim=-1)
+ ).where(target_hits, zeros).mean()
+
+ # target hit normal supervision
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_normal_cossim: # scores true hits
+ losses["loss_normal_cossim"] = (
+ 1 - torch.cosine_similarity(target_normals, intersection_normals, dim=-1)
+ ).where(target_hits & is_intersecting, zeros).mean()
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_normal_euclid: # scores true hits
+ losses["loss_normal_euclid"] = (
+ (target_normals - intersection_normals).norm(dim=-1)
+ ).where(target_hits & is_intersecting, zeros).mean()
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_normal_cossim_proj: # scores misses as if they were hits
+ losses["loss_normal_cossim_proj"] = (
+ 1 - torch.cosine_similarity(target_normals, intersection_normals, dim=-1)
+ ).where(target_hits, zeros).mean()
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_normal_euclid_proj: # scores misses as if they were hits
+ losses["loss_normal_euclid_proj"] = (
+ (target_normals - intersection_normals).norm(dim=-1)
+ ).where(target_hits, zeros).mean()
+
+ # target sufficient hit radius
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_hit_nodistance_l1: # ensures hits become hits, instead of relying on the projection being right
+ losses["loss_hit_nodistance_l1"] = (
+ silhouettes
+ ).where(target_hits & (silhouettes > 0), zeros).mean()
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_hit_nodistance_l2: # ensures hits become hits, instead of relying on the projection being right
+ losses["loss_hit_nodistance_l2"] = (
+ silhouettes
+ ).where(target_hits & (silhouettes > 0), zeros).pow(2).mean()
+
+ # target miss supervision
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_miss_distance_l1: # only positive misses reinforcement
+ losses["loss_miss_distance_l1"] = (
+ target_distances - silhouettes
+ ).where(target_miss, zeros).abs().mean()
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_miss_distance_l2: # only positive misses reinforcement
+ losses["loss_miss_distance_l2"] = (
+ target_distances - silhouettes
+ ).where(target_miss, zeros).pow(2).mean()
+
+ # incentivise maximal spheres
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_sphere_grow_reg: # all atoms
+ losses["loss_sphere_grow_reg"] = ((all_sphere_radii.detach() + 1) - all_sphere_radii).abs().mean()
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_sphere_grow_reg_hit: # true hits only
+ losses["loss_sphere_grow_reg_hit"] = ((sphere_radii.detach() + 1) - sphere_radii).where(target_hits & is_intersecting, zeros).abs().mean()
+
+ # spherical latent prior
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_embedding_norm:
+ losses["loss_embedding_norm"] = self.latent_embeddings.norm(dim=-1).mean()
+
+
+ is_grad_enabled = torch.is_grad_enabled()
+
+ # multi-view regularization: atom should not change when view changes
+ if self.hparams.loss_multi_view_reg and is_grad_enabled:
+ assert ray_dirs.requires_grad, ray_dirs
+ assert plucker.requires_grad, plucker
+ assert intersections.grad_fn is not None
+ assert intersection_normals.grad_fn is not None
+
+ *center_grads, radii_grads = diff.gradients(
+ sphere_centers[..., 0],
+ sphere_centers[..., 1],
+ sphere_centers[..., 2],
+ sphere_radii,
+ wrt=ray_dirs,
+ )
+
+ losses["loss_multi_view_reg"] = (
+ sum(
+ i.pow(2).sum(dim=-1)
+ for i in center_grads
+ ).where(target_hits & is_intersecting, zeros).mean()
+ +
+ radii_grads.pow(2).sum(dim=-1)
+ .where(target_hits & is_intersecting, zeros).mean()
+ )
+
+ # minimize the volume spanned by each atom
+ if self.hparams.loss_atom_centroid_norm_std_reg and self.net.n_atoms > 1:
+ assert len(all_sphere_centers.shape) == 5, all_sphere_centers.shape
+ losses["loss_atom_centroid_norm_std_reg"] \
+ = ((
+ all_sphere_centers
+ - all_sphere_centers
+ .mean(dim=(1, 2), keepdim=True)
+ ).pow(2).sum(dim=-1) - 0.05**2).clamp(0, None).mean()
+
+ # prif is l1, LSMAT is l2
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_inscription_hits or self.hparams.loss_inscription_miss or self.hparams.loss_inscription_hits_l2 or self.hparams.loss_inscription_miss_l2:
+ b = target_hits.shape[0] # number of objects
+ n = target_hits.shape[1:].numel() # rays per object
+ perm = torch.randperm(n, device=self.device) # ray2ray permutation
+ flatten = dict(start_dim=1, end_dim=len(target_hits.shape) - 1)
+
+ (
+ inscr_sphere_center_projs, # (b, n, n_atoms, 3)
+ inscr_intersections_near, # (b, n, n_atoms, 3)
+ inscr_intersections_far, # (b, n, n_atoms, 3)
+ inscr_is_intersecting, # (b, n, n_atoms) dtype=bool
+ ) = geometry.ray_sphere_intersect(
+ ray_origins.flatten(**flatten)[:, perm, None, :],
+ ray_dirs .flatten(**flatten)[:, perm, None, :],
+ all_sphere_centers.flatten(**flatten),
+ all_sphere_radii .flatten(**flatten),
+ return_parts = True,
+ allow_nans = False,
+ improve_miss_grads = self.hparams.improve_miss_grads,
+ )
+ assert inscr_sphere_center_projs.shape == (b, n, self.net.n_atoms, 3), \
+ (inscr_sphere_center_projs.shape, (b, n, self.net.n_atoms, 3))
+ inscr_silhouettes = (
+ inscr_sphere_center_projs - all_sphere_centers.flatten(**flatten)
+ ).norm(dim=-1) - all_sphere_radii.flatten(**flatten)
+
+ loss_inscription_hits = (
+ (
+ (inscr_intersections_near - target_points.flatten(**flatten)[:, perm, None, :])
+ * ray_dirs.flatten(**flatten)[:, perm, None, :]
+ ).sum(dim=-1)
+ ).where(target_hits.flatten(**flatten)[:, perm, None] & inscr_is_intersecting,
+ torch.zeros(inscr_intersections_near.shape[:-1], **self.device_and_dtype),
+ ).clamp(None, 0)
+ loss_inscription_miss = (
+ inscr_silhouettes - target_distances.flatten(**flatten)[:, perm, None]
+ ).where(target_miss.flatten(**flatten)[:, perm, None],
+ torch.zeros_like(inscr_silhouettes)
+ ).clamp(None, 0)
+
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_inscription_hits:
+ losses["loss_inscription_hits"] = loss_inscription_hits.neg().mean()
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_inscription_miss:
+ losses["loss_inscription_miss"] = loss_inscription_miss.neg().mean()
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_inscription_hits_l2:
+ losses["loss_inscription_hits_l2"] = loss_inscription_hits.pow(2).mean()
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_inscription_miss_l2:
+ losses["loss_inscription_miss_l2"] = loss_inscription_miss.pow(2).mean()
+
+ # metrics
+ metrics["iou"] = (
+ ((~target_miss) & is_intersecting.detach()).sum() /
+ ((~target_miss) | is_intersecting.detach()).sum()
+ )
+ metrics["radii"] = sphere_radii.detach().mean() # with the constant applied pressure, we need to measure it this way instead
+
+ elif self.hparams.output_mode == "orthogonal_plane":
+ assert isinstance(self.net, OrthogonalPlaneNet)
+ ray_origins, ray_dirs, input_embedding, (
+ intersections, # (..., 3) dtype=float
+ is_intersecting, # (...) dtype=float
+ ) = self(batch, z, return_input=True, normalize_origins=True)
+
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_intersection:
+ losses["loss_intersection"] = (
+ (intersections - target_points).norm(dim=-1)
+ ).where(target_hits, zeros).mean()
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_intersection_l2:
+ losses["loss_intersection_l2"] = (
+ (intersections - target_points).pow(2).sum(dim=-1)
+ ).where(target_hits, zeros).mean()
+
+ if (__debug__ or LOG_ALL_METRICS) or self.hparams.loss_hit_cross_entropy:
+ losses["loss_hit_cross_entropy"] = (
+ F.binary_cross_entropy_with_logits(is_intersecting, (~target_miss).to(self.dtype))
+ ).mean()
+
+ if self.hparams.loss_normal_cossim and torch.is_grad_enabled():
+ jac = diff.jacobian(intersections, ray_origins)
+ intersection_normals = self.compute_normals_from_intersection_origin_jacobian(jac, ray_dirs)
+ losses["loss_normal_cossim"] = (
+ 1 - torch.cosine_similarity(target_normals, intersection_normals, dim=-1)
+ ).where(target_hits, zeros).mean()
+
+ if self.hparams.loss_normal_euclid and torch.is_grad_enabled():
+ jac = diff.jacobian(intersections, ray_origins)
+ intersection_normals = self.compute_normals_from_intersection_origin_jacobian(jac, ray_dirs)
+ losses["loss_normal_euclid"] = (
+ (target_normals - intersection_normals).norm(dim=-1)
+ ).where(target_hits, zeros).mean()
+
+ if self.hparams.loss_multi_view_reg and torch.is_grad_enabled():
+ assert ray_dirs .requires_grad, ray_dirs
+ assert intersections.grad_fn is not None
+ grads = diff.gradients(
+ intersections[..., 0],
+ intersections[..., 1],
+ intersections[..., 2],
+ wrt=ray_dirs,
+ )
+ losses["loss_multi_view_reg"] = sum(
+ i.pow(2).sum(dim=-1)
+ for i in grads
+ ).where(target_hits, zeros).mean()
+
+ metrics["iou"] = (
+ ((~target_miss) & (is_intersecting>0.5).detach()).sum() /
+ ((~target_miss) | (is_intersecting>0.5).detach()).sum()
+ )
+ else:
+ raise NotImplementedError(self.hparams.output_mode)
+
+ # output losses and metrics
+
+ # apply scaling:
+ losses_unscaled = losses.copy() # shallow copy
+ for k in list(losses.keys()):
+ assert losses[k].numel() == 1, f"losses[{k!r}] shape: {losses[k].shape}"
+ val_schedule: HParamSchedule = self.hparams[k]
+ val = val_schedule.get(self)
+ if val == 0:
+ if (__debug__ or LOG_ALL_METRICS) and val_schedule.is_const:
+ del losses[k] # it was only added for unscaled logging, do not backprop
+ else:
+ losses[k] = 0
+ elif val != 1:
+ losses[k] = losses[k] * val
+
+ if not losses:
+ raise MisconfigurationException("no loss was computed")
+
+ losses["loss"] = sum(losses.values()) * self.hparams.opt_warmup.get(self)
+ losses.update({f"unscaled_{k}": v.detach() for k, v in losses_unscaled.items()})
+ losses.update({f"metric_{k}": v.detach() for k, v in metrics.items()})
+ return losses
+
+
+ # used by pl.callbacks.EarlyStopping, via cli.py
+ @property
+ def metric_early_stop(self): return (
+ "unscaled_loss_intersection_proj"
+ if self.hparams.output_mode == "medial_sphere" else
+ "unscaled_loss_intersection"
+ )
+
+ def validation_step(self, batch: TrainingBatch, batch_idx: int) -> dict[str, Tensor]:
+ losses = self.training_step(batch, batch_idx, is_validation=True)
+ return losses
+
+ def configure_optimizers(self):
+ adam = torch.optim.Adam(self.parameters(),
+ lr=1 if not self.hparams.opt_learning_rate.is_const else self.hparams.opt_learning_rate.get_train_value(0),
+ weight_decay=self.hparams.opt_weight_decay)
+ schedules = []
+ if not self.hparams.opt_learning_rate.is_const:
+ schedules = [
+ torch.optim.lr_scheduler.LambdaLR(adam,
+ lambda epoch: self.hparams.opt_learning_rate.get_train_value(epoch),
+ ),
+ ]
+ return [adam], schedules
+
+ @property
+ def example_input_array(self) -> tuple[dict[str, Tensor], Tensor]:
+ return (
+ { # see self.batch2rays
+ "origins" : torch.zeros(1, 3), # most commonly used
+ "points" : torch.zeros(1, 3), # used if self.training and self.hparams.loss_multi_view_reg
+ "dirs" : torch.ones(1, 3) * torch.rsqrt(torch.tensor(3)),
+ },
+ torch.ones(1, self.hparams.latent_features),
+ )
+
+ @staticmethod
+ def compute_normals_from_intersection_origin_jacobian(origin_jac: Tensor, ray_dirs: Tensor) -> Tensor:
+ normals = sum((
+ torch.cross(origin_jac[..., 0], origin_jac[..., 1], dim=-1) * -ray_dirs[..., [2]],
+ torch.cross(origin_jac[..., 1], origin_jac[..., 2], dim=-1) * -ray_dirs[..., [0]],
+ torch.cross(origin_jac[..., 2], origin_jac[..., 0], dim=-1) * -ray_dirs[..., [1]],
+ ))
+ return normals / normals.norm(dim=-1, keepdim=True)
+
+
+class IntersectionFieldAutoDecoderModel(IntersectionFieldModel, AutoDecoderModuleMixin):
+ def encode(self, batch: LabeledBatch) -> Tensor:
+ assert not isinstance(self.trainer.strategy, pl.strategies.DataParallelStrategy)
+ return self[batch["z_uid"]] # [N, Z_n]
diff --git a/ifield/models/medial_atoms.py b/ifield/models/medial_atoms.py
new file mode 100644
index 0000000..3f9b8dc
--- /dev/null
+++ b/ifield/models/medial_atoms.py
@@ -0,0 +1,186 @@
+from .. import param
+from ..modules import fc
+from ..data.common import points
+from ..utils import geometry
+from ..utils.helpers import compose
+from textwrap import indent, dedent
+from torch import nn, Tensor
+from typing import Optional
+import torch
+import warnings
+
+# generalize this into a HypoHyperConcat net? ConditionedNet?
+class MedialAtomNet(nn.Module):
+ def __init__(self,
+ in_features : int,
+ latent_features : int,
+ hidden_features : int,
+ hidden_layers : int,
+ n_atoms : int = 1,
+ final_init_wrr : tuple[float, float] | None = (0.05, 0.6, 0.1),
+ **kw,
+ ):
+ super().__init__()
+ assert n_atoms >= 1, n_atoms
+ self.n_atoms = n_atoms
+
+ self.fc = fc.FCBlock(
+ in_features = in_features,
+ hidden_layers = hidden_layers,
+ hidden_features = hidden_features,
+ out_features = n_atoms * 4, # n_atoms * (x, y, z, r)
+ outermost_linear = True,
+ latent_features = latent_features,
+ **kw,
+ )
+
+ if final_init_wrr is not None:
+ with torch.no_grad():
+ w, r1, r2 = final_init_wrr
+ if w != 1: self.fc[-1].linear.weight *= w
+ dtype = self.fc[-1].linear.bias.dtype
+ self.fc[-1].linear.bias[..., [4*n+i for n in range(n_atoms) for i in range(3)]] = torch.tensor(points.generate_random_sphere_points(n_atoms, radius=r1), dtype=dtype).flatten()
+ self.fc[-1].linear.bias[..., 3::4] = r2
+
+ @property
+ def is_conditioned(self):
+ return self.fc.is_conditioned
+
+ @classmethod
+ @compose("\n".join)
+ def make_jinja_template(cls, *, exclude_list: set[str] = {}, top_level: bool = True, **kw) -> str:
+ yield param.make_jinja_template(cls, top_level=top_level, exclude_list=exclude_list, **kw)
+ yield fc.FCBlock.make_jinja_template(top_level=False, exclude_list={
+ "in_features",
+ "hidden_layers",
+ "hidden_features",
+ "out_features",
+ "outermost_linear",
+ "latent_features",
+ })
+
+ def forward(self, x: Tensor, z: Optional[Tensor] = None):
+ if __debug__ and self.is_conditioned and z is None:
+ warnings.warn(f"{self.__class__.__qualname__} is conditioned, but the forward pass was not supplied with a conditioning tensor.")
+ return self.fc(x, z)
+
+ def compute_intersections(self,
+ ray_origins : Tensor, # (..., 3)
+ ray_dirs : Tensor, # (..., 3)
+ medial_atoms : Tensor, # (..., 4*self.n_atoms)
+ *,
+ intersections_only : bool = True,
+ return_all_atoms : bool = False, # only applies if intersections_only=False
+ allow_nans : bool = True,
+ improve_miss_grads : bool = False,
+ ) -> tuple[(Tensor,)*5]:
+ assert ray_origins.shape[:-1] == ray_dirs.shape[:-1] == medial_atoms.shape[:-1], \
+ (ray_origins.shape, ray_dirs.shape, medial_atoms.shape)
+ assert medial_atoms.shape[-1] % 4 == 0, \
+ medial_atoms.shape
+ assert ray_origins.shape[-1] == ray_dirs.shape[-1] == 3, \
+ (ray_origins.shape, ray_dirs.shape)
+
+ #n_atoms = medial_atoms.shape[-1] // 4
+ n_atoms = medial_atoms.shape[-1] >> 2
+
+ # reshape (..., n_atoms * d) to (..., n_atoms, d)
+ medial_atoms = medial_atoms.view(*medial_atoms.shape[:-1], n_atoms, 4)
+ ray_origins = ray_origins.unsqueeze(-2).broadcast_to([*ray_origins.shape[:-1], n_atoms, 3])
+ ray_dirs = ray_dirs .unsqueeze(-2).broadcast_to([*ray_dirs .shape[:-1], n_atoms, 3])
+
+ # unpack atoms
+ sphere_centers = medial_atoms[..., :3]
+ sphere_radii = medial_atoms[..., 3].abs()
+
+ assert not ray_origins .detach().isnan().any()
+ assert not ray_dirs .detach().isnan().any()
+ assert not sphere_centers.detach().isnan().any()
+ assert not sphere_radii .detach().isnan().any()
+
+ # compute intersections
+ (
+ sphere_center_projs, # (..., 3)
+ intersections_near, # (..., 3)
+ intersections_far, # (..., 3)
+ is_intersecting, # (...) bool
+ ) = geometry.ray_sphere_intersect(
+ ray_origins,
+ ray_dirs,
+ sphere_centers,
+ sphere_radii,
+ return_parts = True,
+ allow_nans = allow_nans,
+ improve_miss_grads = improve_miss_grads,
+ )
+
+ # early return
+ if intersections_only and n_atoms == 1:
+ return intersections_near.squeeze(-2), is_intersecting.squeeze(-1)
+
+ # compute how close each hit and miss are
+ depths = ((intersections_near - ray_origins) * ray_dirs).sum(-1)
+ silhouettes = torch.linalg.norm(sphere_center_projs - sphere_centers, dim=-1) - sphere_radii
+
+ if return_all_atoms:
+ intersections_near_all = intersections_near
+ depths_all = depths
+ silhouettes_all = silhouettes
+ is_intersecting_all = is_intersecting
+ sphere_centers_all = sphere_centers
+ sphere_radii_all = sphere_radii
+
+ # collapse n_atoms
+ if n_atoms > 1:
+ atom_indices = torch.where(is_intersecting.any(dim=-1, keepdim=True),
+ torch.where(is_intersecting, depths.detach(), depths.detach()+100).argmin(dim=-1, keepdim=True),
+ silhouettes.detach().argmin(dim=-1, keepdim=True),
+ )
+
+ intersections_near = intersections_near.take_along_dim(atom_indices[..., None], -2).squeeze(-2)
+ depths = depths .take_along_dim(atom_indices, -1).squeeze(-1)
+ silhouettes = silhouettes .take_along_dim(atom_indices, -1).squeeze(-1)
+ is_intersecting = is_intersecting .take_along_dim(atom_indices, -1).squeeze(-1)
+ sphere_centers = sphere_centers .take_along_dim(atom_indices[..., None], -2).squeeze(-2)
+ sphere_radii = sphere_radii .take_along_dim(atom_indices, -1).squeeze(-1)
+ else:
+ atom_indices = None
+ intersections_near = intersections_near.squeeze(-2)
+ depths = depths .squeeze(-1)
+ silhouettes = silhouettes .squeeze(-1)
+ is_intersecting = is_intersecting .squeeze(-1)
+ sphere_centers = sphere_centers .squeeze(-2)
+ sphere_radii = sphere_radii .squeeze(-1)
+
+ # early return
+ if intersections_only:
+ return intersections_near, is_intersecting
+
+ # compute sphere normals
+ intersection_normals = intersections_near - sphere_centers
+ intersection_normals = intersection_normals / (intersection_normals.norm(dim=-1)[..., None] + 1e-9)
+
+ if return_all_atoms:
+ intersection_normals_all = intersections_near_all - sphere_centers_all
+ intersection_normals_all = intersection_normals_all / (intersection_normals_all.norm(dim=-1)[..., None] + 1e-9)
+
+
+ return (
+ depths, # (...) valid if hit, based on 'intersections'
+ silhouettes, # (...) always valid
+ intersections_near, # (..., 3) valid if hit, projection if not
+ intersection_normals, # (..., 3) valid if hit, rejection if not
+ is_intersecting, # (...) dtype=bool
+ sphere_centers, # (..., 3) network output
+ sphere_radii, # (...) network output
+ *(() if not return_all_atoms else (
+
+ atom_indices,
+ intersections_near_all, # (..., N_ATOMS) valid if hit, based on 'intersections'
+ intersection_normals_all, # (..., N_ATOMS, 3) valid if hit, rejection if not
+ depths_all, # (..., N_ATOMS) always valid
+ silhouettes_all, # (..., N_ATOMS, 3) valid if hit, projection if not
+ is_intersecting_all, # (..., N_ATOMS) dtype=bool
+ sphere_centers_all, # (..., N_ATOMS, 3) network output
+ sphere_radii_all, # (..., N_ATOMS) network output
+ )))
diff --git a/ifield/models/orthogonal_plane.py b/ifield/models/orthogonal_plane.py
new file mode 100644
index 0000000..4c2b885
--- /dev/null
+++ b/ifield/models/orthogonal_plane.py
@@ -0,0 +1,101 @@
+from .. import param
+from ..modules import fc
+from ..utils import geometry
+from ..utils.helpers import compose
+from textwrap import indent, dedent
+from torch import nn, Tensor
+from typing import Optional
+import warnings
+
+class OrthogonalPlaneNet(nn.Module):
+ """
+
+ """
+
+ def __init__(self,
+ in_features : int,
+ latent_features : int,
+ hidden_features : int,
+ hidden_layers : int,
+ **kw,
+ ):
+ super().__init__()
+
+ self.fc = fc.FCBlock(
+ in_features = in_features,
+ hidden_layers = hidden_layers,
+ hidden_features = hidden_features,
+ out_features = 2, # (plane_offset, is_intersecting)
+ outermost_linear = True,
+ latent_features = latent_features,
+ **kw,
+ )
+
+ @property
+ def is_conditioned(self):
+ return self.fc.is_conditioned
+
+ @classmethod
+ @compose("\n".join)
+ def make_jinja_template(cls, *, exclude_list: set[str] = {}, top_level: bool = True, **kw) -> str:
+ yield param.make_jinja_template(cls, top_level=top_level, exclude_list=exclude_list, **kw)
+ yield param.make_jinja_template(fc.FCBlock, top_level=False, exclude_list={
+ "in_features",
+ "hidden_layers",
+ "hidden_features",
+ "out_features",
+ "outermost_linear",
+ })
+
+ def forward(self, x: Tensor, z: Optional[Tensor] = None) -> Tensor:
+ if __debug__ and self.is_conditioned and z is None:
+ warnings.warn(f"{self.__class__.__qualname__} is conditioned, but the forward pass was not supplied with a conditioning tensor.")
+ return self.fc(x, z)
+
+ @staticmethod
+ def compute_intersections(
+ ray_origins : Tensor, # (..., 3)
+ ray_dirs : Tensor, # (..., 3)
+ predictions : Tensor, # (..., 2)
+ *,
+ normalize_origins = True,
+ return_signed_displacements = False,
+ allow_nans = False, # MARF compat
+ atom_random_prob = None, # MARF compat
+ atom_dropout_prob = None, # MARF compat
+ ) -> tuple[(Tensor,)*5]:
+ assert ray_origins.shape[:-1] == ray_dirs.shape[:-1] == predictions.shape[:-1], \
+ (ray_origins.shape, ray_dirs.shape, predictions.shape)
+ assert predictions.shape[-1] == 2, \
+ predictions.shape
+
+ assert not allow_nans
+
+ if normalize_origins:
+ ray_origins = geometry.project_point_on_ray(0, ray_origins, ray_dirs)
+
+ # unpack predictions
+ signed_displacements = predictions[..., 0]
+ is_intersecting = predictions[..., 1]
+
+ # compute intersections
+ intersections = ray_origins - signed_displacements[..., None] * ray_dirs
+
+ return (
+ intersections,
+ is_intersecting,
+ *((signed_displacements,) if return_signed_displacements else ()),
+ )
+
+
+
+
+OrthogonalPlaneNet.__doc__ = __doc__ = f"""
+{dedent(OrthogonalPlaneNet.__doc__).strip()}
+
+# Config template:
+
+```yaml
+{OrthogonalPlaneNet.make_jinja_template()}
+```
+"""
diff --git a/ifield/modules/__init__.py b/ifield/modules/__init__.py
new file mode 100644
index 0000000..0ee5155
--- /dev/null
+++ b/ifield/modules/__init__.py
@@ -0,0 +1,3 @@
+__doc__ = """
+Contains Pytorch Modules
+"""
diff --git a/ifield/modules/dtype.py b/ifield/modules/dtype.py
new file mode 100644
index 0000000..b109149
--- /dev/null
+++ b/ifield/modules/dtype.py
@@ -0,0 +1,22 @@
+import pytorch_lightning as pl
+
+
+class DtypeMixin:
+ def __init_subclass__(cls):
+ assert issubclass(cls, pl.LightningModule), \
+ f"{cls.__name__!r} is not a subclass of 'pytorch_lightning.LightningModule'!"
+
+ @property
+ def device_and_dtype(self) -> dict:
+ """
+ Examples:
+ ```
+ torch.tensor(1337, **self.device_and_dtype)
+ some_tensor.to(**self.device_and_dtype)
+ ```
+ """
+
+ return {
+ "dtype": self.dtype,
+ "device": self.device,
+ }
diff --git a/ifield/modules/fc.py b/ifield/modules/fc.py
new file mode 100644
index 0000000..73345aa
--- /dev/null
+++ b/ifield/modules/fc.py
@@ -0,0 +1,424 @@
+from . import siren
+from .. import param
+from ..utils.helpers import compose, run_length_encode, MetaModuleProxy
+from collections import OrderedDict
+from pytorch_lightning.core.mixins import HyperparametersMixin
+from torch import nn, Tensor
+from torch.nn.utils.weight_norm import WeightNorm
+from torchmeta.modules import MetaModule, MetaSequential
+from typing import Iterable, Literal, Optional, Union, Callable
+import itertools
+import math
+import torch
+
+__doc__ = """
+`fc` is short for "Fully Connected"
+"""
+
+def broadcast_tensors_except(*tensors: Tensor, dim: int) -> list[Tensor]:
+ if dim == -1:
+ shapes = [ i.shape[:dim] for i in tensors ]
+ else:
+ shapes = [ (*i.shape[:dim], i.shape[dim+1:]) for i in tensors ]
+ target_shape = list(torch.broadcast_shapes(*shapes))
+ if dim == -1:
+ target_shape.append(-1)
+ elif dim < 0:
+ target_shape.insert(dim+1, -1)
+ else:
+ target_shape.insert(dim, -1)
+
+ return [ i.broadcast_to(target_shape) for i in tensors ]
+
+
+EPS = 1e-8
+
+Nonlinearity = Literal[
+ None,
+ "relu",
+ "leaky_relu",
+ "silu",
+ "softplus",
+ "elu",
+ "selu",
+ "sine",
+ "sigmoid",
+ "tanh",
+]
+
+Normalization = Literal[
+ None,
+ "batchnorm",
+ "batchnorm_na",
+ "layernorm",
+ "layernorm_na",
+ "weightnorm",
+]
+
+class ReprHyperparametersMixin(HyperparametersMixin):
+ def extra_repr(self):
+ this = ", ".join(f"{k}={v!r}" for k, v in self.hparams.items())
+ rest = super().extra_repr()
+ if rest:
+ return f"{this}, {rest}"
+ else:
+ return this
+
+class MultilineReprHyperparametersMixin(HyperparametersMixin):
+ def extra_repr(self):
+ items = [f"{k}={v!r}" for k, v in self.hparams.items()]
+ this = "\n".join(
+ ", ".join(filter(bool, i)) + ","
+ for i in itertools.zip_longest(items[0::3], items[1::3], items[2::3])
+ )
+ rest = super().extra_repr()
+ if rest:
+ return f"{this}, {rest}"
+ else:
+ return this
+
+
+class BatchLinear(nn.Linear):
+ """
+ A linear (meta-)layer that can deal with batched weight matrices and biases,
+ as for instance output by a hypernetwork.
+ """
+ __doc__ = nn.Linear.__doc__
+ _meta_forward_pre_hooks = None
+
+ def register_forward_pre_hook(self, hook: Callable) -> torch.utils.hooks.RemovableHandle:
+ if not isinstance(hook, WeightNorm):
+ return super().register_forward_pre_hook(hook)
+
+ if self._meta_forward_pre_hooks is None:
+ self._meta_forward_pre_hooks = OrderedDict()
+
+ handle = torch.utils.hooks.RemovableHandle(self._meta_forward_pre_hooks)
+ self._meta_forward_pre_hooks[handle.id] = hook
+ return handle
+
+ def forward(self, input: Tensor, params: Optional[dict[str, Tensor]]=None):
+ if params is None or not isinstance(self, MetaModule):
+ params = OrderedDict(self.named_parameters())
+ if self._meta_forward_pre_hooks is not None:
+ proxy = MetaModuleProxy(self, params)
+ for hook in self._meta_forward_pre_hooks.values():
+ hook(proxy, [input])
+
+ weight = params["weight"]
+ bias = params.get("bias", None)
+
+ # transpose weights
+ weight = weight.permute(*range(len(weight.shape) - 2), -1, -2) # does not jit
+
+ output = input.unsqueeze(-2).matmul(weight).squeeze(-2)
+
+ if bias is not None:
+ output = output + bias
+
+ return output
+
+
+class MetaBatchLinear(BatchLinear, MetaModule):
+ pass
+
+
+class CallbackConcatLayer(nn.Module):
+ "A tricky way to enable skip connections in sequentials models"
+ def __init__(self, tensor_getter: Callable[[], tuple[Tensor, ...]]):
+ super().__init__()
+ self.tensor_getter = tensor_getter
+
+ def forward(self, x):
+ ys = self.tensor_getter()
+ return torch.cat(broadcast_tensors_except(x, *ys, dim=-1), dim=-1)
+
+
+class ResidualSkipConnectionEndLayer(nn.Module):
+ """
+ Residual skip connections that can be added to a nn.Sequential
+ """
+
+ class ResidualSkipConnectionStartLayer(nn.Module):
+ def __init__(self):
+ super().__init__()
+ self._stored_tensor = None
+
+ def forward(self, x):
+ assert self._stored_tensor is None
+ self._stored_tensor = x
+ return x
+
+ def get(self):
+ assert self._stored_tensor is not None
+ x = self._stored_tensor
+ self._stored_tensor = None
+ return x
+
+ def __init__(self):
+ super().__init__()
+ self._stored_tensor = None
+ self._start = self.ResidualSkipConnectionStartLayer()
+
+ def forward(self, x):
+ skip = self._start.get()
+ return x + skip
+
+ @property
+ def start(self) -> ResidualSkipConnectionStartLayer:
+ return self._start
+
+ @property
+ def end(self) -> "ResidualSkipConnectionEndLayer":
+ return self
+
+
+ResidualMode = Literal[
+ None,
+ "identity",
+]
+
+class FCLayer(MultilineReprHyperparametersMixin, MetaSequential):
+ """
+ A single fully connected (FC) layer
+ """
+
+ def __init__(self,
+ in_features : int,
+ out_features : int,
+ *,
+ nonlinearity : Nonlinearity = "relu",
+ normalization : Normalization = None,
+ is_first : bool = False, # used for SIREN initialization
+ is_final : bool = False, # used for fan_out init
+ dropout_prob : float = 0.0,
+ negative_slope : float = 0.01, # only for nonlinearity="leaky_relu", default is normally 0.01
+ omega_0 : float = 30, # only for nonlinearity="sine"
+ residual_mode : ResidualMode = None,
+ _no_meta : bool = False, # set to true in hypernetworks
+ **_
+ ):
+ super().__init__()
+ self.save_hyperparameters()
+
+ # improve repr
+ if nonlinearity != "leaky_relu":
+ self.hparams.pop("negative_slope")
+ if nonlinearity != "sine":
+ self.hparams.pop("omega_0")
+
+ Linear = nn.Linear if _no_meta else MetaBatchLinear
+
+ def make_layer() -> Iterable[nn.Module]:
+ # residual start
+ if residual_mode is not None:
+ residual_layer = ResidualSkipConnectionEndLayer()
+ yield "res_a", residual_layer.start
+
+ linear = Linear(in_features, out_features)
+
+ # initialize
+ if nonlinearity in {"relu", "leaky_relu", "silu", "softplus"}:
+ nn.init.kaiming_uniform_(linear.weight, a=negative_slope, nonlinearity=nonlinearity, mode="fan_in" if not is_final else "fan_out")
+ elif nonlinearity == "elu":
+ nn.init.normal_(linear.weight, std=math.sqrt(1.5505188080679277) / math.sqrt(linear.weight.size(-1)))
+ elif nonlinearity == "selu":
+ nn.init.normal_(linear.weight, std=1 / math.sqrt(linear.weight.size(-1)))
+ elif nonlinearity == "sine":
+ siren.init_weights_(linear, omega_0, is_first)
+ elif nonlinearity in {"sigmoid", "tanh"}:
+ nn.init.xavier_normal_(linear.weight)
+ elif nonlinearity is None:
+ pass # this is effectively uniform(-1/sqrt(in_features), 1/sqrt(in_features))
+ else:
+ raise NotImplementedError(nonlinearity)
+
+ # linear + normalize
+ if normalization is None:
+ yield "linear", linear
+ elif normalization == "batchnorm":
+ yield "linear", linear
+ yield "norm", nn.BatchNorm1d(out_features, affine=True)
+ elif normalization == "batchnorm_na":
+ yield "linear", linear
+ yield "norm", nn.BatchNorm1d(out_features, affine=False)
+ elif normalization == "layernorm":
+ yield "linear", linear
+ yield "norm", nn.LayerNorm([out_features], elementwise_affine=True)
+ elif normalization == "layernorm_na":
+ yield "linear", linear
+ yield "norm", nn.LayerNorm([out_features], elementwise_affine=False)
+ elif normalization == "weightnorm":
+ yield "linear", nn.utils.weight_norm(linear)
+ else:
+ raise NotImplementedError(normalization)
+
+ # activation
+ inplace = False
+ if nonlinearity is None : pass
+ elif nonlinearity == "relu" : yield nonlinearity, nn.ReLU(inplace=inplace)
+ elif nonlinearity == "leaky_relu" : yield nonlinearity, nn.LeakyReLU(negative_slope=negative_slope, inplace=inplace)
+ elif nonlinearity == "silu" : yield nonlinearity, nn.SiLU(inplace=inplace)
+ elif nonlinearity == "softplus" : yield nonlinearity, nn.Softplus()
+ elif nonlinearity == "elu" : yield nonlinearity, nn.ELU(inplace=inplace)
+ elif nonlinearity == "selu" : yield nonlinearity, nn.SELU(inplace=inplace)
+ elif nonlinearity == "sine" : yield nonlinearity, siren.Sine(omega_0)
+ elif nonlinearity == "sigmoid" : yield nonlinearity, nn.Sigmoid()
+ elif nonlinearity == "tanh" : yield nonlinearity, nn.Tanh()
+ else : raise NotImplementedError(f"{nonlinearity=}")
+
+ # dropout
+ if dropout_prob > 0:
+ if nonlinearity == "selu":
+ yield "adropout", nn.AlphaDropout(p=dropout_prob)
+ else:
+ yield "dropout", nn.Dropout(p=dropout_prob)
+
+ # residual end
+ if residual_mode is not None:
+ yield "res_b", residual_layer.end
+
+ for name, module in make_layer():
+ self.add_module(name.replace("-", "_"), module)
+
+ @property
+ def nonlinearity(self) -> Optional[nn.Module]:
+ "alias to the activation function submodule"
+ if self.hparams.nonlinearity is None:
+ return None
+ return getattr(self, self.hparams.nonlinearity.replace("-", "_"))
+
+ def initialize_weights():
+ raise NotImplementedError
+
+
+class FCBlock(MultilineReprHyperparametersMixin, MetaSequential):
+ """
+ A block of FC layers
+ """
+ def __init__(self,
+ in_features : int,
+ hidden_features : int,
+ hidden_layers : int,
+ out_features : int,
+ normalization : Normalization = None,
+ nonlinearity : Nonlinearity = "relu",
+ dropout_prob : float = 0.0,
+ outermost_linear : bool = True, # whether last linear is nonlinear
+ latent_features : Optional[int] = None,
+ concat_skipped_layers : Union[list[int], bool] = [],
+ concat_conditioned_layers : Union[list[int], bool] = [],
+ **kw,
+ ):
+ super().__init__()
+ self.save_hyperparameters()
+
+ if isinstance(concat_skipped_layers, bool):
+ concat_skipped_layers = list(range(hidden_layers+2)) if concat_skipped_layers else []
+ if isinstance(concat_conditioned_layers, bool):
+ concat_conditioned_layers = list(range(hidden_layers+2)) if concat_conditioned_layers else []
+ if len(concat_conditioned_layers) != 0 and latent_features is None:
+ raise ValueError("Layers marked to be conditioned without known number of latent features")
+ concat_skipped_layers = [i if i >= 0 else hidden_layers+2-abs(i) for i in concat_skipped_layers]
+ concat_conditioned_layers = [i if i >= 0 else hidden_layers+2-abs(i) for i in concat_conditioned_layers]
+ self._concat_x_layers: frozenset[int] = frozenset(concat_skipped_layers)
+ self._concat_z_layers: frozenset[int] = frozenset(concat_conditioned_layers)
+ if len(self._concat_x_layers) != len(concat_skipped_layers):
+ raise ValueError(f"Duplicates found in {concat_skipped_layers = }")
+ if len(self._concat_z_layers) != len(concat_conditioned_layers):
+ raise ValueError(f"Duplicates found in {concat_conditioned_layers = }")
+ if not all(isinstance(i, int) for i in self._concat_x_layers):
+ raise TypeError(f"Expected only integers in {concat_skipped_layers = }")
+ if not all(isinstance(i, int) for i in self._concat_z_layers):
+ raise TypeError(f"Expected only integers in {concat_conditioned_layers = }")
+
+ def make_layers() -> Iterable[nn.Module]:
+ def make_concat_layer(*idxs: int) -> int:
+ x_condition_this_layer = any(idx in self._concat_x_layers for idx in idxs)
+ z_condition_this_layer = any(idx in self._concat_z_layers for idx in idxs)
+ if x_condition_this_layer and z_condition_this_layer:
+ yield CallbackConcatLayer(lambda: (self._current_x, self._current_z))
+ elif x_condition_this_layer:
+ yield CallbackConcatLayer(lambda: (self._current_x,))
+ elif z_condition_this_layer:
+ yield CallbackConcatLayer(lambda: (self._current_z,))
+
+ return in_features*x_condition_this_layer + (latent_features or 0)*z_condition_this_layer
+
+ added = yield from make_concat_layer(0)
+
+ yield FCLayer(
+ in_features = in_features + added,
+ out_features = hidden_features,
+ nonlinearity = nonlinearity,
+ normalization = normalization,
+ dropout_prob = dropout_prob,
+ is_first = True,
+ is_final = False,
+ **kw,
+ )
+
+ for i in range(hidden_layers):
+ added = yield from make_concat_layer(i+1)
+
+ yield FCLayer(
+ in_features = hidden_features + added,
+ out_features = hidden_features,
+ nonlinearity = nonlinearity,
+ normalization = normalization,
+ dropout_prob = dropout_prob,
+ is_first = False,
+ is_final = False,
+ **kw,
+ )
+
+ added = yield from make_concat_layer(hidden_layers+1)
+
+ nl = nonlinearity
+
+ yield FCLayer(
+ in_features = hidden_features + added,
+ out_features = out_features,
+ nonlinearity = None if outermost_linear else nl,
+ normalization = None if outermost_linear else normalization,
+ dropout_prob = 0.0 if outermost_linear else dropout_prob,
+ is_first = False,
+ is_final = True,
+ **kw,
+ )
+
+ for i, module in enumerate(make_layers()):
+ self.add_module(str(i), module)
+
+ @property
+ def is_conditioned(self) -> bool:
+ "Whether z is used or not"
+ return bool(self._concat_z_layers)
+
+ @classmethod
+ @compose("\n".join)
+ def make_jinja_template(cls, *, exclude_list: set[str] = {}, top_level: bool = True, **kw) -> str:
+ @compose(" ".join)
+ def as_jexpr(values: Union[list[int]]):
+ yield "{{"
+ for val, count in run_length_encode(values):
+ yield f"[{val!r}]*{count!r}"
+ yield "}}"
+ yield param.make_jinja_template(cls, top_level=top_level, exclude_list=exclude_list)
+ yield param.make_jinja_template(FCLayer, top_level=False, exclude_list=exclude_list | {
+ "in_features",
+ "out_features",
+ "nonlinearity",
+ "normalization",
+ "dropout_prob",
+ "is_first",
+ "is_final",
+ })
+
+ def forward(self, input: Tensor, z: Optional[Tensor] = None, *, params: Optional[dict[str, Tensor]]=None):
+ assert not self.is_conditioned or z is not None
+ if z is not None and z.ndim < input.ndim:
+ z = z[(*(None,)*(input.ndim - z.ndim), ...)]
+ self._current_x = input
+ self._current_z = z
+ return super().forward(input, params=params)
diff --git a/ifield/modules/siren.py b/ifield/modules/siren.py
new file mode 100644
index 0000000..0df337e
--- /dev/null
+++ b/ifield/modules/siren.py
@@ -0,0 +1,25 @@
+from math import sqrt
+from torch import nn
+import torch
+
+class Sine(nn.Module):
+ def __init__(self, omega_0: float):
+ super().__init__()
+ self.omega_0 = omega_0
+
+ def forward(self, input):
+ if self.omega_0 == 1:
+ return torch.sin(input)
+ else:
+ return torch.sin(input * self.omega_0)
+
+
+def init_weights_(module: nn.Linear, omega_0: float, is_first: bool = True):
+ assert isinstance(module, nn.Linear), module
+ with torch.no_grad():
+ mag = (
+ 1 / module.in_features
+ if is_first else
+ sqrt(6 / module.in_features) / omega_0
+ )
+ module.weight.uniform_(-mag, mag)
diff --git a/ifield/param.py b/ifield/param.py
new file mode 100644
index 0000000..4298048
--- /dev/null
+++ b/ifield/param.py
@@ -0,0 +1,231 @@
+from .utils.helpers import compose, elementwise_max
+from datetime import datetime
+from torch import nn
+from typing import Any, Literal, Iterable, Union, Callable, Optional
+import inspect
+import jinja2
+import json
+import os
+import random
+import re
+import shlex
+import string
+import sys
+import time
+import typing
+import warnings
+import yaml
+
+_UNDEFINED = " I AM UNDEFINED "
+
+def _yaml_encode_value(val) -> str:
+ if isinstance(val, tuple):
+ val = list(val)
+ elif isinstance(val, set):
+ val = list(val)
+ if isinstance(val, list):
+ return json.dumps(val)
+ elif isinstance(val, dict):
+ return json.dumps(val)
+ else:
+ return yaml.dump(val).removesuffix("\n...\n").rstrip("\n")
+
+def _raise(val: Union[Exception, str]):
+ if isinstance(val, str):
+ val = jinja2.TemplateError(val)
+ raise val
+
+def make_jinja_globals(*, enable_require_defined: bool) -> dict:
+ import builtins
+ import functools
+ import itertools
+ import operator
+ import json
+
+ def require_defined(name, value, *defaults, failed: bool = False, strict: bool=False, exchaustive=False):
+ if not defaults:
+ raise ValueError("`require_defined` requires at least one valid value provided")
+ if jinja2.is_undefined(value):
+ assert value._undefined_name == name, \
+ f"Name mismatch: {value._undefined_name=}, {name=}"
+ if failed or jinja2.is_undefined(value):
+ if enable_require_defined or strict:
+ raise ValueError(
+ f"Required variable {name!r} "
+ f"is {'incorrect' if failed else 'undefined'}! "
+ f"Try providing:\n" + "\n".join(
+ f"-O{shlex.quote(name)}={shlex.quote(str(default))}"
+ for default in defaults
+ )
+ )
+ else:
+ warnings.warn(
+ f"Required variable {name!r} "
+ f"is {'incorrect' if failed else 'undefined'}! "
+ f"Try providing:\n" + "\n".join(
+ f"-O{shlex.quote(name)}={shlex.quote(str(default))}"
+ for default in defaults
+ )
+ )
+ if exchaustive and not jinja2.is_undefined(value) and value not in defaults:
+ raise ValueError(
+ f"Variable {name!r} not in list of allowed values: {defaults!r}"
+ )
+
+ def gen_run_uid(n: int, _choice = random.Random(time.time_ns()).choice):
+ """
+ generates a UID for the experiment run, nice for regexes, grepping and timekeeping.
+ """
+ # we have _choice, since most likely, pl.seed_everything has been run by this point
+ # we store it as a default parameter to reuse it, on the off-chance of two calls to this function being run withion the same ns
+ code = ''.join(_choice(string.ascii_lowercase) for _ in range(n))
+ return f"{datetime.now():%Y-%m-%d-%H%M}-{code}"
+ return f"{datetime.now():%Y%m%d-%H%M}-{code}"
+
+ def cartesian_hparams(_map=None, **kw: dict[str, list]) -> Iterable[jinja2.utils.Namespace]:
+ "Use this to bypass the common error 'SyntaxError: too many statically nested blocks'"
+ if isinstance(_map, jinja2.utils.Namespace):
+ kw = _map._Namespace__attrs | kw
+ elif isinstance(_map, dict):
+ kw = _map._Namespace__attrs | kw
+ keys, vals = zip(*kw.items())
+ for i in itertools.product(*vals):
+ yield jinja2.utils.Namespace(zip(keys, i))
+
+ def ablation_hparams(_map=None, *, caartesian_keys: list[str] = None, **kw: dict[str, list]) -> Iterable[jinja2.utils.Namespace]:
+ "Use this to bypass the common error 'SyntaxError: too many statically nested blocks'"
+ if isinstance(_map, jinja2.utils.Namespace):
+ kw = _map._Namespace__attrs | kw
+ elif isinstance(_map, dict):
+ kw = _map._Namespace__attrs | kw
+ keys = list(kw.keys())
+
+ caartesian_keys = [k for k in keys if k in caartesian_keys] if caartesian_keys else []
+ ablation_keys = [k for k in keys if k not in caartesian_keys]
+ caartesian_vals = list(map(kw.__getitem__, caartesian_keys))
+ ablation_vals = list(map(kw.__getitem__, ablation_keys))
+
+ for base_vals in itertools.product(*caartesian_vals):
+ base = list(itertools.chain(zip(caartesian_keys, base_vals), zip(ablation_keys, [i[0] for i in ablation_vals])))
+ yield jinja2.utils.Namespace(base)
+ for ablation_key, ablation_val in zip(ablation_keys, ablation_vals):
+ for val in ablation_val[1:]:
+ yield jinja2.utils.Namespace(base, **{ablation_key: val}) # ablation variation
+
+ return {
+ **locals(),
+ **vars(builtins),
+ "argv": sys.argv,
+ "raise": _raise,
+ }
+
+def make_jinja_env(globals = make_jinja_globals(enable_require_defined=True), allow_undef=False) -> jinja2.Environment:
+ env = jinja2.Environment(
+ loader = jinja2.FileSystemLoader([os.getcwd(), "/"], followlinks=True),
+ autoescape = False,
+ trim_blocks = True,
+ lstrip_blocks = True,
+ undefined = jinja2.Undefined if allow_undef else jinja2.StrictUndefined,
+ extensions = [
+ "jinja2.ext.do", # statements with side-effects
+ "jinja2.ext.loopcontrols", # break and continue
+ ],
+ )
+ env.globals.update(globals)
+ env.filters.update({
+ "defined": lambda x: _raise(f"{x._undefined_name!r} is not defined!") if jinja2.is_undefined(x) else x,
+ "repr": repr,
+ "to_json": json.dumps,
+ "bool": lambda x: json.dumps(bool(x)),
+ "int": lambda x: json.dumps(int(x)),
+ "float": lambda x: json.dumps(float(x)),
+ "str": lambda x: json.dumps(str(x)),
+ })
+ return env
+
+def list_func_params(func: callable, exclude_list: set[str], defaults: dict={}) -> Iterable[tuple[str, Any, str]]:
+ signature = inspect.signature(func)
+ for i, (k, v) in enumerate(signature.parameters.items()):
+ if not i and k in {"self", "cls"}:
+ continue
+ if k in exclude_list:
+ continue
+ if k.startswith("_"):
+ continue
+ if v.kind is v.VAR_POSITIONAL or v.kind is v.VAR_KEYWORD:
+ continue
+ has_default = not defaults.get(k, v.default) is v.empty
+ has_annotation = not v.annotation is v.empty
+ allowed_literals = f"{{{', '.join(map(_yaml_encode_value, typing.get_args(v.annotation)))}}}" \
+ if typing.get_origin(v.annotation) is Literal else None
+
+ assert has_annotation, f"param {k!r} has no type annotation"
+ yield (
+ k,
+ defaults.get(k, v.default) if has_default else _UNDEFINED,
+ f"in {allowed_literals}" if allowed_literals else typing._type_repr(v.annotation),
+ )
+
+@compose("\n".join)
+def make_jinja_template(
+ network_cls: nn.Module,
+ *,
+ exclude_list: set[str] = set(),
+ defaults: dict[str, Any]={},
+ top_level: bool = True,
+ commented: bool = False,
+ name=None,
+ comment: Optional[str] = None,
+ special_encoders: dict[str, Callable[[Any], str]]={},
+ ) -> str:
+ c = "#" if commented else ""
+ if name is None:
+ name = network_cls.__name__
+
+ if comment is not None:
+ if "\n" in comment:
+ raise ValueError("newline in jinja template comment is not allowed")
+
+ hparams = [*list_func_params(network_cls, exclude_list, defaults=defaults)]
+ if not hparams:
+ if top_level:
+ yield f"{name}:"
+ else:
+ yield f" # {name}:"
+ return
+
+
+ encoded_hparams = [
+ (key, _yaml_encode_value(value) if value is not _UNDEFINED else "", comment)
+ if key not in special_encoders else
+ (key, special_encoders[key](value) if value is not _UNDEFINED else "", comment)
+ for key, value, comment in hparams
+ ]
+
+ ml_key, ml_value = elementwise_max(
+ (
+ len(key),
+ len(value),
+ )
+ for key, value, comment in encoded_hparams
+ )
+
+ if top_level:
+ yield f"{name}:" if not comment else f"{name}: # {comment}"
+ else:
+ yield f" # {name}:" if not comment else f" # {name}: # {comment}"
+
+ for key, value, comment in encoded_hparams:
+ if key in exclude_list:
+ continue
+ pad_key = ml_key - len(key)
+ pad_value = ml_value - len(value)
+
+ yield f" {c}{key}{' '*pad_key} : {value}{' '*pad_value} # {comment}"
+
+ yield ""
+
+# helpers:
+
+def squash_newlines(data: str) -> str:
+ return re.sub(r'\n\n\n+', '\n\n', data)
diff --git a/ifield/utils/__init__.py b/ifield/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/ifield/utils/geometry.py b/ifield/utils/geometry.py
new file mode 100644
index 0000000..a3e1f69
--- /dev/null
+++ b/ifield/utils/geometry.py
@@ -0,0 +1,197 @@
+from torch import Tensor
+from torch.nn import functional as F
+from typing import Optional, Literal
+import torch
+from .helpers import compose
+
+
+def get_ray_origins(cam2world: Tensor):
+ return cam2world[..., :3, 3]
+
+def camera_uv_to_rays(
+ cam2world : Tensor,
+ uv : Tensor,
+ intrinsics : Tensor,
+ ) -> tuple[Tensor, Tensor]:
+ """
+ Computes rays and origins from batched cam2world & intrinsics matrices, as well as pixel coordinates
+ cam2world: (..., 4, 4)
+ intrinsics: (..., 3, 3)
+ uv: (..., n, 2)
+ """
+ ray_dirs = get_ray_directions(uv, cam2world=cam2world, intrinsics=intrinsics)
+ ray_origins = get_ray_origins(cam2world)
+ ray_origins = ray_origins[..., None, :].expand([*uv.shape[:-1], 3])
+ return ray_origins, ray_dirs
+
+RayEmbedding = Literal[
+ "plucker", # LFN
+ "perp_foot", # PRIF
+ "both",
+]
+
+@compose(torch.cat, dim=-1)
+@compose(tuple)
+def ray_input_embedding(ray_origins: Tensor, ray_dirs: Tensor, mode: RayEmbedding = "plucker", normalize_dirs=False, is_training=False):
+ """
+ Computes the plucker coordinates / perpendicular foot from ray origins and directions, appending it to direction
+ """
+ assert ray_origins.shape[-1] == ray_dirs.shape[-1] == 3, \
+ f"{ray_dirs.shape = }, {ray_origins.shape = }"
+
+ if normalize_dirs:
+ ray_dirs = ray_dirs / ray_dirs.norm(dim=-1, keepdim=True)
+
+ yield ray_dirs
+
+ do_moment = mode in ("plucker", "both")
+ do_perp_feet = mode in ("perp_foot", "both")
+ assert do_moment or do_perp_feet
+
+ moment = torch.cross(ray_origins, ray_dirs, dim=-1)
+ if do_moment:
+ yield moment
+
+ if do_perp_feet:
+ perp_feet = torch.cross(ray_dirs, moment, dim=-1)
+ yield perp_feet
+
+def ray_input_embedding_length(mode: RayEmbedding = "plucker") -> int:
+ do_moment = mode in ("plucker", "both")
+ do_perp_feet = mode in ("perp_foot", "both")
+ assert do_moment or do_perp_feet
+
+ out = 3 # ray_dirs
+ if do_moment:
+ out += 3 # moment
+ if do_perp_feet:
+ out += 3 # perp foot
+ return out
+
+def parse_intrinsics(intrinsics, return_dict=False):
+ fx = intrinsics[..., 0, 0:1]
+ fy = intrinsics[..., 1, 1:2]
+ cx = intrinsics[..., 0, 2:3]
+ cy = intrinsics[..., 1, 2:3]
+ if return_dict:
+ return {"fx": fx, "fy": fy, "cx": cx, "cy": cy}
+ else:
+ return fx, fy, cx, cy
+
+def expand_as(x, y):
+ if len(x.shape) == len(y.shape):
+ return x
+
+ for i in range(len(y.shape) - len(x.shape)):
+ x = x.unsqueeze(-1)
+
+ return x
+
+def lift(x, y, z, intrinsics, homogeneous=False):
+ """
+
+ :param self:
+ :param x: Shape (batch_size, num_points)
+ :param y:
+ :param z:
+ :param intrinsics:
+ :return:
+ """
+ fx, fy, cx, cy = parse_intrinsics(intrinsics)
+
+ x_lift = (x - expand_as(cx, x)) / expand_as(fx, x) * z
+ y_lift = (y - expand_as(cy, y)) / expand_as(fy, y) * z
+
+ if homogeneous:
+ return torch.stack((x_lift, y_lift, z, torch.ones_like(z).to(x.device)), dim=-1)
+ else:
+ return torch.stack((x_lift, y_lift, z), dim=-1)
+
+def project(x, y, z, intrinsics):
+ """
+
+ :param self:
+ :param x: Shape (batch_size, num_points)
+ :param y:
+ :param z:
+ :param intrinsics:
+ :return:
+ """
+ fx, fy, cx, cy = parse_intrinsics(intrinsics)
+
+ x_proj = expand_as(fx, x) * x / z + expand_as(cx, x)
+ y_proj = expand_as(fy, y) * y / z + expand_as(cy, y)
+
+ return torch.stack((x_proj, y_proj, z), dim=-1)
+
+def world_from_xy_depth(xy, depth, cam2world, intrinsics):
+ batch_size, *_ = cam2world.shape
+
+ x_cam = xy[..., 0]
+ y_cam = xy[..., 1]
+ z_cam = depth
+
+ pixel_points_cam = lift(x_cam, y_cam, z_cam, intrinsics=intrinsics, homogeneous=True)
+ world_coords = torch.einsum("b...ij,b...kj->b...ki", cam2world, pixel_points_cam)[..., :3]
+
+ return world_coords
+
+def project_point_on_ray(projection_point, ray_origin, ray_dir):
+ dot = torch.einsum("...j,...j", projection_point-ray_origin, ray_dir)
+ return ray_origin + dot[..., None] * ray_dir
+
+def get_ray_directions(
+ xy : Tensor, # (..., N, 2)
+ cam2world : Tensor, # (..., 4, 4)
+ intrinsics : Tensor, # (..., 3, 3)
+ ):
+ z_cam = torch.ones(xy.shape[:-1]).to(xy.device)
+ pixel_points = world_from_xy_depth(xy, z_cam, intrinsics=intrinsics, cam2world=cam2world) # (batch, num_samples, 3)
+
+ cam_pos = cam2world[..., :3, 3]
+ ray_dirs = pixel_points - cam_pos[..., None, :] # (batch, num_samples, 3)
+ ray_dirs = F.normalize(ray_dirs, dim=-1)
+ return ray_dirs
+
+def ray_sphere_intersect(
+ ray_origins : Tensor, # (..., 3)
+ ray_dirs : Tensor, # (..., 3)
+ sphere_centers : Optional[Tensor] = None, # (..., 3)
+ sphere_radii : Optional[Tensor] = 1, # (...)
+ *,
+ return_parts : bool = False,
+ allow_nans : bool = True,
+ improve_miss_grads : bool = False,
+ ) -> tuple[Tensor, ...]:
+ if improve_miss_grads: assert not allow_nans, "improve_miss_grads does not work with allow_nans"
+ if sphere_centers is None:
+ ray_origins_centered = ray_origins #- torch.zeros_like(ray_origins)
+ else:
+ ray_origins_centered = ray_origins - sphere_centers
+
+ ray_dir_dot_origins = (ray_dirs * ray_origins_centered).sum(dim=-1, keepdim=True)
+ discriminants2 = ray_dir_dot_origins**2 - ((ray_origins_centered * ray_origins_centered).sum(dim=-1) - sphere_radii**2)[..., None]
+ if not allow_nans or return_parts:
+ is_intersecting = discriminants2 > 0
+ if allow_nans:
+ discriminants = torch.sqrt(discriminants2)
+ else:
+ discriminants = torch.sqrt(torch.where(is_intersecting, discriminants2,
+ discriminants2 - discriminants2.detach() + 0.001
+ if improve_miss_grads else
+ torch.zeros_like(discriminants2)
+ ))
+ assert not discriminants.detach().isnan().any() # slow, use optimizations!
+
+ if not return_parts:
+ return (
+ ray_origins + ray_dirs * (- ray_dir_dot_origins - discriminants),
+ ray_origins + ray_dirs * (- ray_dir_dot_origins + discriminants),
+ )
+ else:
+ return (
+ ray_origins + ray_dirs * (- ray_dir_dot_origins),
+ ray_origins + ray_dirs * (- ray_dir_dot_origins - discriminants),
+ ray_origins + ray_dirs * (- ray_dir_dot_origins + discriminants),
+ is_intersecting.squeeze(-1),
+ )
diff --git a/ifield/utils/helpers.py b/ifield/utils/helpers.py
new file mode 100644
index 0000000..0e119a9
--- /dev/null
+++ b/ifield/utils/helpers.py
@@ -0,0 +1,205 @@
+from functools import wraps, reduce, partial
+from itertools import zip_longest, groupby
+from pathlib import Path
+from typing import Iterable, TypeVar, Callable, Union, Optional, Mapping, Hashable
+import collections
+import operator
+import re
+
+Numeric = Union[int, float, complex]
+T = TypeVar("T")
+S = TypeVar("S")
+
+# decorator
+def compose(outer_func: Callable[[..., S], T], *outer_a, **outer_kw) -> Callable[..., T]:
+ def wrapper(inner_func: Callable[..., S]):
+ @wraps(inner_func)
+ def wrapped(*a, **kw):
+ return outer_func(*outer_a, inner_func(*a, **kw), **outer_kw)
+ return wrapped
+ return wrapper
+
+def compose_star(outer_func: Callable[[..., S], T], *outer_a, **outer_kw) -> Callable[..., T]:
+ def wrapper(inner_func: Callable[..., S]):
+ @wraps(inner_func)
+ def wrapped(*a, **kw):
+ return outer_func(*outer_a, *inner_func(*a, **kw), **outer_kw)
+ return wrapped
+ return wrapper
+
+
+# itertools
+
+def elementwise_max(iterable: Iterable[Iterable[T]]) -> Iterable[T]:
+ return reduce(lambda xs, ys: [*map(max, zip(xs, ys))], iterable)
+
+def prod(numbers: Iterable[T], initial: Optional[T] = None) -> T:
+ if initial is not None:
+ return reduce(operator.mul, numbers, initial)
+ else:
+ return reduce(operator.mul, numbers)
+
+def run_length_encode(data: Iterable[T]) -> Iterable[tuple[T, int]]:
+ return (
+ (x, len(y))
+ for x, y in groupby(data)
+ )
+
+
+# text conversion
+
+def camel_to_snake_case(text: str, sep: str = "_", join_abbreviations: bool = False) -> str:
+ parts = (
+ part.lower()
+ for part in re.split(r'(?=[A-Z])', text)
+ if part
+ )
+ if join_abbreviations:
+ parts = list(parts)
+ if len(parts) > 1:
+ for i, (a, b) in list(enumerate(zip(parts[:-1], parts[1:])))[::-1]:
+ if len(a) == len(b) == 1:
+ parts[i] = parts[i] + parts.pop(i+1)
+ return sep.join(parts)
+
+def snake_to_camel_case(text: str) -> str:
+ return "".join(
+ part.captialize()
+ for part in text.split("_")
+ if part
+ )
+
+
+# textwrap
+
+def columnize_dict(data: dict, n_columns=2, prefix="", sep=" ") -> str:
+ sub = (len(data) + 1) // n_columns
+ return reduce(partial(columnize, sep=sep),
+ (
+ columnize(
+ "\n".join([f"{'' if n else prefix}{i!r}" for i in data.keys() ][n*sub : (n+1)*sub]),
+ "\n".join([f": {i!r}," for i in data.values()][n*sub : (n+1)*sub]),
+ )
+ for n in range(n_columns)
+ )
+ )
+
+def columnize(left: str, right: str, prefix="", sep=" ") -> str:
+ left = left .split("\n")
+ right = right.split("\n")
+ width = max(map(len, left)) if left else 0
+ return "\n".join(
+ f"{prefix}{a.ljust(width)}{sep}{b}"
+ if b else
+ f"{prefix}{a}"
+ for a, b in zip_longest(left, right, fillvalue="")
+ )
+
+
+# pathlib
+
+def make_relative(path: Union[Path, str], parent: Path = None) -> Path:
+ if isinstance(path, str):
+ path = Path(path)
+ if parent is None:
+ parent = Path.cwd()
+ try:
+ return path.relative_to(parent)
+ except ValueError:
+ pass
+ try:
+ return ".." / path.relative_to(parent.parent)
+ except ValueError:
+ pass
+ return path
+
+
+# dictionaries
+
+def update_recursive(target: dict, source: dict):
+ """ Update two config dictionaries recursively. """
+ for k, v in source.items():
+ if isinstance(v, dict):
+ if k not in target:
+ target[k] = type(target)()
+ update_recursive(target[k], v)
+ else:
+ target[k] = v
+
+def map_tree(func: Callable[[T], S], val: Union[Mapping[Hashable, T], tuple[T, ...], list[T], T]) -> Union[Mapping[Hashable, S], tuple[S, ...], list[S], S]:
+ if isinstance(val, collections.abc.Mapping):
+ return {
+ k: map_tree(func, subval)
+ for k, subval in val.items()
+ }
+ elif isinstance(val, tuple):
+ return tuple(
+ map_tree(func, subval)
+ for subval in val
+ )
+ elif isinstance(val, list):
+ return [
+ map_tree(func, subval)
+ for subval in val
+ ]
+ else:
+ return func(val)
+
+def flatten_tree(val, *, sep=".", prefix=None):
+ if isinstance(val, collections.abc.Mapping):
+ return {
+ k: v
+ for subkey, subval in val.items()
+ for k, v in flatten_tree(subval, sep=sep, prefix=f"{prefix}{sep}{subkey}" if prefix else subkey).items()
+ }
+ elif isinstance(val, tuple) or isinstance(val, list):
+ return {
+ k: v
+ for index, subval in enumerate(val)
+ for k, v in flatten_tree(subval, sep=sep, prefix=f"{prefix}{sep}[{index}]" if prefix else f"[{index}]").items()
+ }
+ elif prefix:
+ return {prefix: val}
+ else:
+ return val
+
+# conversions
+
+def hex2tuple(data: str) -> tuple[int]:
+ data = data.removeprefix("#")
+ return (*(
+ int(data[i:i+2], 16)
+ for i in range(0, len(data), 2)
+ ),)
+
+
+# repr shims
+
+class CustomRepr:
+ def __init__(self, repr_str: str):
+ self.repr_str = repr_str
+ def __str__(self):
+ return self.repr_str
+ def __repr__(self):
+ return self.repr_str
+
+
+# Meta Params Module proxy
+
+class MetaModuleProxy:
+ def __init__(self, module, params):
+ self._module = module
+ self._params = params
+
+ def __getattr__(self, name):
+ params = super().__getattribute__("_params")
+ if name in params:
+ return params[name]
+ else:
+ return getattr(super().__getattribute__("_module"), name)
+
+ def __setattr__(self, name, value):
+ if name not in ("_params", "_module"):
+ super().__getattribute__("_params")[name] = value
+ else:
+ super().__setattr__(name, value)
diff --git a/ifield/utils/loss.py b/ifield/utils/loss.py
new file mode 100644
index 0000000..cdc2237
--- /dev/null
+++ b/ifield/utils/loss.py
@@ -0,0 +1,590 @@
+from abc import abstractmethod, ABC
+from dataclasses import dataclass, field, fields, MISSING
+from functools import wraps
+from matplotlib import pyplot as plt
+from matplotlib.artist import Artist
+from tabulate import tabulate
+from torch import nn
+from typing import Optional, TypeVar, Union
+import inspect
+import math
+import pytorch_lightning as pl
+import typing
+import warnings
+
+
+HParamSchedule = TypeVar("HParamSchedule", bound="HParamScheduleBase")
+Schedulable = Union[HParamSchedule, int, float, str]
+
+class HParamScheduleBase(ABC):
+ _subclasses = {} # shared reference intended
+ def __init_subclass__(cls):
+ if not cls.__name__.startswith("_"):
+ cls._subclasses[cls.__name__] = cls
+
+ _infix : Optional[str] = field(init=False, repr=False, default=None)
+ _param_name : Optional[str] = field(init=False, repr=False, default=None)
+ _expr : Optional[str] = field(init=False, repr=False, default=None)
+
+ def get(self, module: nn.Module, *, trainer: Optional[pl.Trainer] = None) -> float:
+ if module.training:
+ if trainer is None:
+ trainer = module.trainer # this assumes `module` is a pl.LightningModule
+ value = self.get_train_value(
+ epoch = trainer.current_epoch + (trainer.fit_loop.epoch_loop.batch_progress.current.processed / trainer.num_training_batches),
+ )
+ if trainer.logger is not None and self._param_name is not None and self.__class__ is not Const and trainer.global_step % 15 == 0:
+ trainer.logger.log_metrics({
+ f"HParamSchedule/{self._param_name}": value,
+ }, step=trainer.global_step)
+ return value
+ else:
+ return self.get_eval_value()
+
+ def _gen_data(self, n_epochs, steps_per_epoch=1000):
+ global_steps = 0
+ for epoch in range(n_epochs):
+ for step in range(steps_per_epoch):
+ yield (
+ epoch + step/steps_per_epoch,
+ self.get_train_value(epoch + step/steps_per_epoch),
+ )
+ global_steps += steps_per_epoch
+
+ def plot(self, *a, ax: Optional[plt.Axes] = None, **kw) -> Artist:
+ if ax is None: ax = plt.gca()
+ out = ax.plot(*zip(*self._gen_data(*a, **kw)), label=self._expr)
+ ax.set_title(self._param_name)
+ ax.set_xlabel("Epoch")
+ ax.set_ylabel("Value")
+ ax.legend()
+ return out
+
+ def assert_positive(self, *a, **kw):
+ for epoch, val in self._gen_data(*a, **kw):
+ assert val >= 0, f"{epoch=}, {val=}"
+
+ @abstractmethod
+ def get_eval_value(self) -> float:
+ ...
+
+ @abstractmethod
+ def get_train_value(self, epoch: float) -> float:
+ ...
+
+ def __add__(self, rhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "+":
+ return cls(self, rhs)
+ return NotImplemented
+
+ def __radd__(self, lhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "+":
+ return cls(lhs, self)
+ return NotImplemented
+
+ def __sub__(self, rhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "-":
+ return cls(self, rhs)
+ return NotImplemented
+
+ def __rsub__(self, lhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "-":
+ return cls(lhs, self)
+ return NotImplemented
+
+ def __mul__(self, rhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "*":
+ return cls(self, rhs)
+ return NotImplemented
+
+ def __rmul__(self, lhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "*":
+ return cls(lhs, self)
+ return NotImplemented
+
+ def __matmul__(self, rhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "@":
+ return cls(self, rhs)
+ return NotImplemented
+
+ def __rmatmul__(self, lhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "@":
+ return cls(lhs, self)
+ return NotImplemented
+
+ def __truediv__(self, rhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "/":
+ return cls(self, rhs)
+ return NotImplemented
+
+ def __rtruediv__(self, lhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "/":
+ return cls(lhs, self)
+ return NotImplemented
+
+ def __floordiv__(self, rhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "//":
+ return cls(self, rhs)
+ return NotImplemented
+
+ def __rfloordiv__(self, lhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "//":
+ return cls(lhs, self)
+ return NotImplemented
+
+ def __mod__(self, rhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "%":
+ return cls(self, rhs)
+ return NotImplemented
+
+ def __rmod__(self, lhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "%":
+ return cls(lhs, self)
+ return NotImplemented
+
+ def __pow__(self, rhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "**":
+ return cls(self, rhs)
+ return NotImplemented
+
+ def __rpow__(self, lhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "**":
+ return cls(lhs, self)
+ return NotImplemented
+
+ def __lshift__(self, rhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "<<":
+ return cls(self, rhs)
+ return NotImplemented
+
+ def __rlshift__(self, lhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "<<":
+ return cls(lhs, self)
+ return NotImplemented
+
+ def __rshift__(self, rhs):
+ for cls in self._subclasses.values():
+ if cls._infix == ">>":
+ return cls(self, rhs)
+ return NotImplemented
+
+ def __rrshift__(self, lhs):
+ for cls in self._subclasses.values():
+ if cls._infix == ">>":
+ return cls(lhs, self)
+ return NotImplemented
+
+ def __and__(self, rhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "&":
+ return cls(self, rhs)
+ return NotImplemented
+
+ def __rand__(self, lhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "&":
+ return cls(lhs, self)
+ return NotImplemented
+
+ def __xor__(self, rhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "^":
+ return cls(self, rhs)
+ return NotImplemented
+
+ def __rxor__(self, lhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "^":
+ return cls(lhs, self)
+ return NotImplemented
+
+ def __or__(self, rhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "|":
+ return cls(self, rhs)
+ return NotImplemented
+
+ def __ror__(self, lhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "|":
+ return cls(lhs, self)
+ return NotImplemented
+
+ def __ge__(self, rhs):
+ for cls in self._subclasses.values():
+ if cls._infix == ">=":
+ return cls(self, rhs)
+ return NotImplemented
+
+ def __gt__(self, rhs):
+ for cls in self._subclasses.values():
+ if cls._infix == ">":
+ return cls(self, rhs)
+ return NotImplemented
+
+ def __le__(self, rhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "<=":
+ return cls(self, rhs)
+ return NotImplemented
+
+ def __lt__(self, rhs):
+ for cls in self._subclasses.values():
+ if cls._infix == "<":
+ return cls(self, rhs)
+ return NotImplemented
+
+ def __bool__(self):
+ return True
+
+ def __neg__(self):
+ for cls in self._subclasses.values():
+ if cls._infix == "-":
+ return cls(0, self)
+ return NotImplemented
+
+ @property
+ def is_const(self) -> bool:
+ return False
+
+
+
+def parse_dsl(config: Schedulable, name=None) -> HParamSchedule:
+ if isinstance(config, HParamScheduleBase):
+ return config
+ elif isinstance(config, str):
+ out = eval(config, {"__builtins__": {}, "lg": math.log10}, HParamScheduleBase._subclasses)
+ if not isinstance(out, HParamScheduleBase):
+ out = Const(out)
+ else:
+ out = Const(config)
+ out._expr = config
+ out._param_name = name
+ return out
+
+
+# decorator
+def ensure_schedulables(func):
+ signature = inspect.signature(func)
+ module_name = func.__qualname__.removesuffix(".__init__")
+
+ @wraps(func)
+ def wrapper(*a, **kw):
+ bound_args = signature.bind(*a, **kw)
+
+ for param_name, param in signature.parameters.items():
+ type_origin = typing.get_origin(param.annotation)
+ type_args = typing.get_args (param.annotation)
+
+ if type_origin is HParamSchedule or (type_origin is Union and (HParamSchedule in type_args or HParamScheduleBase in type_args)):
+ if param_name in bound_args.arguments:
+ bound_args.arguments[param_name] = parse_dsl(bound_args.arguments[param_name], name=f"{module_name}.{param_name}")
+ elif param.default is not param.empty:
+ bound_args.arguments[param_name] = parse_dsl(param.default, name=f"{module_name}.{param_name}")
+
+ return func(
+ *bound_args.args,
+ **bound_args.kwargs,
+ )
+ return wrapper
+
+# https://easings.net/
+
+@dataclass
+class _InfixBase(HParamScheduleBase):
+ l : Union[HParamSchedule, int, float]
+ r : Union[HParamSchedule, int, float]
+
+ def _operation(self, l: float, r: float) -> float:
+ raise NotImplementedError
+
+ def get_eval_value(self) -> float:
+ return self._operation(
+ self.l.get_eval_value() if isinstance(self.l, HParamScheduleBase) else self.l,
+ self.r.get_eval_value() if isinstance(self.r, HParamScheduleBase) else self.r,
+ )
+
+ def get_train_value(self, epoch: float) -> float:
+ return self._operation(
+ self.l.get_train_value(epoch) if isinstance(self.l, HParamScheduleBase) else self.l,
+ self.r.get_train_value(epoch) if isinstance(self.r, HParamScheduleBase) else self.r,
+ )
+
+ def __bool__(self):
+ if self.is_const:
+ return bool(self.get_eval_value())
+ else:
+ return True
+
+ @property
+ def is_const(self) -> bool:
+ return (self.l.is_const if isinstance(self.l, HParamScheduleBase) else True) \
+ and (self.r.is_const if isinstance(self.r, HParamScheduleBase) else True)
+
+@dataclass
+class Add(_InfixBase):
+ """ adds the results of two schedules """
+ _infix : Optional[str] = field(init=False, repr=False, default="+")
+ def _operation(self, l: float, r: float) -> float:
+ return l + r
+
+
+@dataclass
+class Sub(_InfixBase):
+ """ subtracts the results of two schedules """
+ _infix : Optional[str] = field(init=False, repr=False, default="-")
+ def _operation(self, l: float, r: float) -> float:
+ return l - r
+
+
+@dataclass
+class Prod(_InfixBase):
+ """ multiplies the results of two schedules """
+ _infix : Optional[str] = field(init=False, repr=False, default="*")
+ def _operation(self, l: float, r: float) -> float:
+ return l * r
+ @property
+ def is_const(self) -> bool: # propagate identity
+ l = self.l.get_eval_value() if isinstance(self.l, HParamScheduleBase) and self.l.is_const else self.l
+ r = self.r.get_eval_value() if isinstance(self.r, HParamScheduleBase) and self.r.is_const else self.r
+ return l == 0 or r == 0 or super().is_const
+
+
+@dataclass
+class Div(_InfixBase):
+ """ divides the results of two schedules """
+ _infix : Optional[str] = field(init=False, repr=False, default="/")
+ def _operation(self, l: float, r: float) -> float:
+ return l / r
+
+
+@dataclass
+class Pow(_InfixBase):
+ """ raises the results of one schedule to the other """
+ _infix : Optional[str] = field(init=False, repr=False, default="**")
+ def _operation(self, l: float, r: float) -> float:
+ return l ** r
+
+
+@dataclass
+class Gt(_InfixBase):
+ """ compares the results of two schedules """
+ _infix : Optional[str] = field(init=False, repr=False, default=">")
+ def _operation(self, l: float, r: float) -> float:
+ return l > r
+
+
+@dataclass
+class Lt(_InfixBase):
+ """ compares the results of two schedules """
+ _infix : Optional[str] = field(init=False, repr=False, default="<")
+ def _operation(self, l: float, r: float) -> float:
+ return l < r
+
+
+@dataclass
+class Ge(_InfixBase):
+ """ compares the results of two schedules """
+ _infix : Optional[str] = field(init=False, repr=False, default=">=")
+ def _operation(self, l: float, r: float) -> float:
+ return l >= r
+
+
+@dataclass
+class Le(_InfixBase):
+ """ compares the results of two schedules """
+ _infix : Optional[str] = field(init=False, repr=False, default="<=")
+ def _operation(self, l: float, r: float) -> float:
+ return l <= r
+
+
+@dataclass
+class Const(HParamScheduleBase):
+ """ A way to ensure .get(...) exists """
+
+ c : Union[int, float]
+
+ def get_eval_value(self) -> float:
+ return self.c
+
+ def get_train_value(self, epoch: float) -> float:
+ return self.c
+
+ def __bool__(self):
+ return bool(self.get_eval_value())
+
+ @property
+ def is_const(self) -> bool:
+ return True
+
+@dataclass
+class Step(HParamScheduleBase):
+ """ steps from 0 to 1 at `epoch` """
+
+ epoch : float
+
+ def get_eval_value(self) -> float:
+ return 1
+
+ def get_train_value(self, epoch: float) -> float:
+ return 1 if epoch >= self.epoch else 0
+
+@dataclass
+class Linear(HParamScheduleBase):
+ """ linear from 0 to 1 over `n_epochs`, delayed by `offset` """
+
+ n_epochs : float
+ offset : float = 0
+
+ def get_eval_value(self) -> float:
+ return 1
+
+ def get_train_value(self, epoch: float) -> float:
+ if self.n_epochs <= 0: return 1
+ return min(max(epoch - self.offset, 0) / self.n_epochs, 1)
+
+@dataclass
+class EaseSin(HParamScheduleBase): # effectively 1-CosineAnnealing
+ """ sinusoidal ease in-out from 0 to 1 over `n_epochs`, delayed by `offset` """
+
+ n_epochs : float
+ offset : float = 0
+
+ def get_eval_value(self) -> float:
+ return 1
+
+ def get_train_value(self, epoch: float) -> float:
+ x = min(max(epoch - self.offset, 0) / self.n_epochs, 1)
+ return -(math.cos(math.pi * x) - 1) / 2
+
+@dataclass
+class EaseExp(HParamScheduleBase):
+ """ exponential ease in-out from 0 to 1 over `n_epochs`, delayed by `offset` """
+
+ n_epochs : float
+ offset : float = 0
+
+ def get_eval_value(self) -> float:
+ return 1
+
+ def get_train_value(self, epoch: float) -> float:
+ if (epoch-self.offset) < 0:
+ return 0
+ if (epoch-self.offset) > self.n_epochs:
+ return 1
+ x = min(max(epoch - self.offset, 0) / self.n_epochs, 1)
+ return (
+ 2**(20*x-10) / 2
+ if x < 0.5 else
+ (2 - 2**(-20*x+10)) / 2
+ )
+
+@dataclass
+class Steps(HParamScheduleBase):
+ """ Starts at 1, multiply by gamma every n epochs. Models StepLR in pytorch """
+ step_size: float
+ gamma: float = 0.1
+
+ def get_eval_value(self) -> float:
+ return 1
+ def get_train_value(self, epoch: float) -> float:
+ return self.gamma**int(epoch / self.step_size)
+
+@dataclass
+class MultiStep(HParamScheduleBase):
+ """ Starts at 1, multiply by gamma every milstone epoch. Models MultiStepLR in pytorch """
+ milestones: list[float]
+ gamma: float = 0.1
+
+ def get_eval_value(self) -> float:
+ return 1
+ def get_train_value(self, epoch: float) -> float:
+ for i, m in list(enumerate(self.milestones))[::-1]:
+ if epoch >= m:
+ return self.gamma**(i+1)
+ return 1
+
+@dataclass
+class Epoch(HParamScheduleBase):
+ """ The current epoch, starting at 0 """
+
+ def get_eval_value(self) -> float:
+ return 0
+ def get_train_value(self, epoch: float) -> float:
+ return epoch
+
+@dataclass
+class Offset(HParamScheduleBase):
+ """ Offsets the epoch for the subexpression, clamped above 0. Positive offsets makes it happen later """
+ expr : Union[HParamSchedule, int, float]
+ offset : float
+
+ def get_eval_value(self) -> float:
+ return self.expr.get_eval_value() if isinstance(self.expr, HParamScheduleBase) else self.expr
+ def get_train_value(self, epoch: float) -> float:
+ return self.expr.get_train_value(max(epoch - self.offset, 0)) if isinstance(self.expr, HParamScheduleBase) else self.expr
+
+@dataclass
+class Mod(HParamScheduleBase):
+ """ The epoch in the subexptression is subject to a modulus. Use for warm restarts """
+
+ modulus : float
+ expr : Union[HParamSchedule, int, float]
+
+ def get_eval_value(self) -> float:
+ return self.expr.get_eval_value() if isinstance(self.expr, HParamScheduleBase) else self.expr
+ def get_train_value(self, epoch: float) -> float:
+ return self.expr.get_train_value(epoch % self.modulus) if isinstance(self.expr, HParamScheduleBase) else self.expr
+
+
+def main():
+ import sys, rich.pretty
+ if not sys.argv[2:]:
+ print(f"Usage: {sys.argv[0]} n_epochs 'expression'")
+ print("Available operations:")
+ def mk_ops():
+ for name, cls in HParamScheduleBase._subclasses.items():
+ if isinstance(cls._infix, str):
+ yield (cls._infix, f"(infix) {cls.__doc__.strip()}")
+ else:
+ yield (
+ f"""{name}({', '.join(
+ i.name
+ if i.default is MISSING else
+ f"{i.name}={i.default!r}"
+ for i in fields(cls)
+ )})""",
+ cls.__doc__.strip(),
+ )
+ rich.print(tabulate(sorted(mk_ops()), tablefmt="plain"))
+ else:
+ n_epochs = int(sys.argv[1])
+ schedules = [parse_dsl(arg, name="cli arg") for arg in sys.argv[2:]]
+ ax = plt.gca()
+ print("[")
+ for schedule in schedules:
+ rich.print(f" {schedule}, # {schedule.is_const = }")
+ schedule.plot(n_epochs, ax=ax)
+ print("]")
+ plt.show()
+
+if __name__ == "__main__":
+ main()
diff --git a/ifield/utils/operators/__init__.py b/ifield/utils/operators/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/ifield/utils/operators/diff.py b/ifield/utils/operators/diff.py
new file mode 100644
index 0000000..8f2b194
--- /dev/null
+++ b/ifield/utils/operators/diff.py
@@ -0,0 +1,96 @@
+import torch
+from torch.autograd import grad
+
+
+def hessian(y: torch.Tensor, x: torch.Tensor, check=False, detach=False) -> torch.Tensor:
+ """
+ hessian of y wrt x
+ y: shape (..., Y)
+ x: shape (..., X)
+ return: shape (..., Y, X, X)
+ """
+ assert x.requires_grad
+ assert y.grad_fn
+
+ grad_y = torch.ones_like(y[..., 0]).to(y.device) # reuse -> less memory
+
+ hess = torch.stack([
+ # calculate hessian on y for each x value
+ torch.stack(
+ gradients(
+ *(dydx[..., j] for j in range(x.shape[-1])),
+ wrt=x,
+ grad_outputs=[grad_y]*x.shape[-1],
+ detach=detach,
+ ),
+ dim = -2,
+ )
+ # calculate dydx over batches for each feature value of y
+ for dydx in gradients(*(y[..., i] for i in range(y.shape[-1])), wrt=x)
+ ], dim=-3)
+
+ if check:
+ assert hess.isnan().any()
+ return hess
+
+def laplace(y: torch.Tensor, x: torch.Tensor) -> torch.Tensor:
+ return divergence(*gradients(y, wrt=x), x)
+
+def divergence(y: torch.Tensor, x: torch.Tensor) -> torch.Tensor:
+ assert x.requires_grad
+ assert y.grad_fn
+ return sum(
+ grad(
+ y[..., i],
+ x,
+ torch.ones_like(y[..., i]),
+ create_graph=True
+ )[0][..., i:i+1]
+ for i in range(y.shape[-1])
+ )
+
+def gradients(*ys, wrt, grad_outputs=None, detach=False) -> list[torch.Tensor]:
+ assert wrt.requires_grad
+ assert all(y.grad_fn for y in ys)
+ if grad_outputs is None:
+ grad_outputs = [torch.ones_like(y) for y in ys]
+
+ grads = (
+ grad(
+ [y],
+ [wrt],
+ grad_outputs=y_grad,
+ create_graph=True,
+ )[0]
+ for y, y_grad in zip(ys, grad_outputs)
+ )
+ if detach:
+ grads = map(torch.detach, grads)
+
+ return [*grads]
+
+def jacobian(y: torch.Tensor, x: torch.Tensor, check=False, detach=False) -> torch.Tensor:
+ """
+ jacobian of `y` w.r.t. `x`
+
+ y: shape (..., Y)
+ x: shape (..., X)
+ return: shape (..., Y, X)
+ """
+ assert x.requires_grad
+ assert y.grad_fn
+
+ y_grad = torch.ones_like(y[..., 0])
+ jac = torch.stack(
+ gradients(
+ *(y[..., i] for i in range(y.shape[-1])),
+ wrt=x,
+ grad_outputs=[y_grad]*x.shape[-1],
+ detach=detach,
+ ),
+ dim=-2,
+ )
+
+ if check:
+ assert jac.isnan().any()
+ return jac
diff --git a/ifield/viewer/__init__.py b/ifield/viewer/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/ifield/viewer/assets/texturify_pano-1-4.jpg b/ifield/viewer/assets/texturify_pano-1-4.jpg
new file mode 100644
index 0000000..c2abbc7
Binary files /dev/null and b/ifield/viewer/assets/texturify_pano-1-4.jpg differ
diff --git a/ifield/viewer/common.py b/ifield/viewer/common.py
new file mode 100644
index 0000000..b2f3bff
--- /dev/null
+++ b/ifield/viewer/common.py
@@ -0,0 +1,430 @@
+from ..utils import geometry
+from abc import ABC, abstractmethod
+from datetime import datetime
+from pathlib import Path
+from pytorch3d.transforms import euler_angles_to_matrix
+from tqdm import tqdm
+from typing import Sequence, Callable, TypedDict
+import imageio
+import shlex
+import json
+import numpy as np
+import os
+import time
+import torch
+os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "1"
+import pygame
+
+IVec2 = tuple[int, int]
+IVec3 = tuple[int, int, int]
+Vec2 = tuple[float|int, float|int]
+Vec3 = tuple[float|int, float|int, float|int]
+
+class CamState(TypedDict, total=False):
+ distance : float
+ pos_x : float
+ pos_y : float
+ pos_z : float
+ rot_x : float
+ rot_y : float
+ fov_y : float
+
+
+
+class InteractiveViewer(ABC):
+ constants = pygame.constants # saves an import
+
+ # realtime
+ t : float # time since start
+ td : float # time delta since last frame
+
+ # offline
+ is_headless : bool
+ fps : int
+ frame_idx : int
+
+ fill_color = (255, 255, 255)
+
+ def __init__(self, name: str, res: IVec2 = (640, 480), scale: int= 1, screenshot_dir: Path = "."):
+ self.name = name
+ self.res = res
+ self.scale = scale
+ self.screenshot_dir = Path(screenshot_dir)
+
+ self.is_headless = False
+
+ self.cam_distance = 2.0
+ self.cam_pos_x = 0.0 # look-at and rotation pivot
+ self.cam_pos_y = 0.0 # look-at and rotation pivot
+ self.cam_pos_z = 0.0 # look-at and rotation pivot
+ self.cam_rot_x = 0.5 * torch.pi # radians
+ self.cam_rot_y = -0.5 * torch.pi # radians
+ self.cam_fov_y = 60.0 / 180.0 * 3.1415 # radians
+ self.keep_rotating = False
+ self.initial_camera_state = self.cam_state
+ self.fps_cap = None
+
+ @property
+ def cam_state(self) -> CamState:
+ return dict(
+ distance = self.cam_distance,
+ pos_x = self.cam_pos_x,
+ pos_y = self.cam_pos_y,
+ pos_z = self.cam_pos_z,
+ rot_x = self.cam_rot_x,
+ rot_y = self.cam_rot_y,
+ fov_y = self.cam_fov_y,
+ )
+
+ @cam_state.setter
+ def cam_state(self, new_state: CamState):
+ self.cam_distance = new_state.get("distance", self.cam_distance)
+ self.cam_pos_x = new_state.get("pos_x", self.cam_pos_x)
+ self.cam_pos_y = new_state.get("pos_y", self.cam_pos_y)
+ self.cam_pos_z = new_state.get("pos_z", self.cam_pos_z)
+ self.cam_rot_x = new_state.get("rot_x", self.cam_rot_x)
+ self.cam_rot_y = new_state.get("rot_y", self.cam_rot_y)
+ self.cam_fov_y = new_state.get("fov_y", self.cam_fov_y)
+
+ @property
+ def scaled_res(self) -> IVec2:
+ return (
+ self.res[0] * self.scale,
+ self.res[1] * self.scale,
+ )
+
+ def setup(self):
+ pass
+
+ def teardown(self):
+ pass
+
+ @abstractmethod
+ def render_frame(self, pixel_view: np.ndarray): # (W, H, 3) dtype=uint8
+ ...
+
+ def handle_key_up(self, key: int, keys_pressed: Sequence[bool]):
+ pass
+
+ def handle_key_down(self, key: int, keys_pressed: Sequence[bool]):
+ mod = keys_pressed[pygame.K_LSHIFT] or keys_pressed[pygame.K_RSHIFT]
+ mod2 = keys_pressed[pygame.K_LCTRL] or keys_pressed[pygame.K_RCTRL]
+ if key == pygame.K_r:
+ self.keep_rotating = True
+ self.cam_rot_x += self.td
+ if key == pygame.K_MINUS:
+ self.scale += 1
+ if __debug__: print()
+ print(f"== Scale = {self.scale} ==")
+ if key == pygame.K_PLUS and self.scale > 1:
+ self.scale -= 1
+ if __debug__: print()
+ print(f"== Scale = {self.scale} ==")
+ if key == pygame.K_RETURN:
+ self.cam_state = self.initial_camera_state
+ if key == pygame.K_h:
+ if mod2:
+ print(shlex.quote(json.dumps(self.cam_state)))
+ elif mod:
+ with (self.screenshot_dir / "camera.json").open("w") as f:
+ json.dump(self.cam_state, f)
+ print("Wrote", self.screenshot_dir / "camera.json")
+ else:
+ with (self.screenshot_dir / "camera.json").open("r") as f:
+ self.cam_state = json.load(f)
+ print("Read", self.screenshot_dir / "camera.json")
+
+ def handle_keys_pressed(self, pressed: Sequence[bool]) -> float:
+ mod1 = pressed[pygame.K_LCTRL] or pressed[pygame.K_RCTRL]
+ mod2 = pressed[pygame.K_LSHIFT] or pressed[pygame.K_RSHIFT]
+ mod3 = pressed[pygame.K_LALT] or pressed[pygame.K_RALT]
+ td = self.td * (0.5 if mod2 else (6 if mod1 else 2))
+
+ if pressed[pygame.K_UP]: self.cam_rot_y += td
+ if pressed[pygame.K_DOWN]: self.cam_rot_y -= td
+ if pressed[pygame.K_LEFT]: self.cam_rot_x += td
+ if pressed[pygame.K_RIGHT]: self.cam_rot_x -= td
+ if pressed[pygame.K_PAGEUP] and mod3: self.cam_distance -= td
+ if pressed[pygame.K_PAGEDOWN] and mod3: self.cam_distance += td
+
+ if any(pressed[i] for i in [pygame.K_UP, pygame.K_DOWN, pygame.K_LEFT, pygame.K_RIGHT]):
+ self.keep_rotating = False
+ if self.keep_rotating: self.cam_rot_x += self.td * 0.25
+
+ if pressed[pygame.K_w]: self.cam_pos_x -= td * np.cos(-self.cam_rot_x)
+ if pressed[pygame.K_w]: self.cam_pos_y += td * np.sin(-self.cam_rot_x)
+ if pressed[pygame.K_s]: self.cam_pos_x += td * np.cos(-self.cam_rot_x)
+ if pressed[pygame.K_s]: self.cam_pos_y -= td * np.sin(-self.cam_rot_x)
+ if pressed[pygame.K_a]: self.cam_pos_x += td * np.sin(self.cam_rot_x)
+ if pressed[pygame.K_a]: self.cam_pos_y -= td * np.cos(self.cam_rot_x)
+ if pressed[pygame.K_d]: self.cam_pos_x -= td * np.sin(self.cam_rot_x)
+ if pressed[pygame.K_d]: self.cam_pos_y += td * np.cos(self.cam_rot_x)
+ if pressed[pygame.K_PAGEUP] and not mod3: self.cam_pos_z -= td
+ if pressed[pygame.K_PAGEDOWN] and not mod3: self.cam_pos_z += td
+
+ return td
+
+ def handle_mouse_button_up(self, pos: IVec2, button: int, keys_pressed: Sequence[bool]):
+ pass
+
+ def handle_mouse_button_down(self, pos: IVec2, button: int, keys_pressed: Sequence[bool]):
+ pass
+
+ def handle_mouse_motion(self, pos: IVec2, rel: IVec2, buttons: Sequence[bool], keys_pressed: Sequence[bool]):
+ pass
+
+ def handle_mousewheel(self, flipped: bool, x: int, y: int, keys_pressed: Sequence[bool]):
+ if keys_pressed[pygame.K_LALT] or keys_pressed[pygame.K_RALT]:
+ self.cam_fov_y -= y * 0.015
+ else:
+ self.cam_distance -= y * 0.2
+
+ _current_caption = None
+ def set_caption(self, title: str, *a, **kw):
+ if self._current_caption != title and not self.is_headless:
+ print(f"set_caption: {title!r}")
+ self._current_caption = title
+ return pygame.display.set_caption(title, *a, **kw)
+
+ @property
+ def mouse_position(self) -> IVec2:
+ mx, my = pygame.mouse.get_pos() if not self.is_headless else (0, 0)
+ return (
+ mx // self.scale,
+ my // self.scale,
+ )
+
+ @property
+ def uvs(self) -> torch.Tensor: # (w, h, 2) dtype=float32
+ res = tuple(self.res)
+ if not getattr(self, "_uvs_res", None) == res:
+ U, V = torch.meshgrid(
+ torch.arange(self.res[1]).to(torch.float32),
+ torch.arange(self.res[0]).to(torch.float32),
+ indexing="xy",
+ )
+ self._uvs_res, self._uvs = res, torch.stack((U, V), dim=-1)
+ return self._uvs
+
+ @property
+ def cam2world(self) -> torch.Tensor: # (4, 4) dtype=float32
+ if getattr(self, "_cam2world_cam_rot_y", None) is not self.cam_rot_y \
+ or getattr(self, "_cam2world_cam_rot_x", None) is not self.cam_rot_x \
+ or getattr(self, "_cam2world_cam_pos_x", None) is not self.cam_pos_x \
+ or getattr(self, "_cam2world_cam_pos_y", None) is not self.cam_pos_y \
+ or getattr(self, "_cam2world_cam_pos_z", None) is not self.cam_pos_z \
+ or getattr(self, "_cam2world_cam_distance", None) is not self.cam_distance:
+ self._cam2world_cam_rot_y = self.cam_rot_y
+ self._cam2world_cam_rot_x = self.cam_rot_x
+ self._cam2world_cam_pos_x = self.cam_pos_x
+ self._cam2world_cam_pos_y = self.cam_pos_y
+ self._cam2world_cam_pos_z = self.cam_pos_z
+ self._cam2world_cam_distance = self.cam_distance
+
+ a = torch.eye(4)
+ a[2, 3] = self.cam_distance
+ b = torch.eye(4)
+ b[:3, :3] = euler_angles_to_matrix(torch.tensor((self.cam_rot_x, self.cam_rot_y, 0)), "ZYX")
+ b[0:3, 3] -= torch.tensor(( self.cam_pos_x, self.cam_pos_y, self.cam_pos_z, ))
+ self._cam2world = b @ a
+
+ self._cam2world_inv = None
+ return self._cam2world
+
+ @property
+ def cam2world_inv(self) -> torch.Tensor: # (4, 4) dtype=float32
+ if getattr(self, "_cam2world_inv", None) is None:
+ self._cam2world_inv = torch.linalg.inv(self._cam2world)
+ return self._cam2world_inv
+
+ @property
+ def intrinsics(self) -> torch.Tensor: # (3, 3) dtype=float32
+ if getattr(self, "_intrinsics_res", None) is not self.res \
+ or getattr(self, "_intrinsics_cam_fov_y", None) is not self.cam_fov_y:
+ self._intrinsics_res = res = self.res
+ self._intrinsics_cam_fov_y = cam_fov_y = self.cam_fov_y
+
+ self._intrinsics = torch.eye(3)
+ p = torch.sin(torch.tensor(cam_fov_y / 2))
+ s = (res[1] / 2)
+ self._intrinsics[0, 0] = s/p # fx - focal length x
+ self._intrinsics[1, 1] = s/p # fy - focal length y
+ self._intrinsics[0, 2] = (res[1] - 1) / 2 # cx - optical center x
+ self._intrinsics[1, 2] = (res[0] - 1) / 2 # cy - optical center y
+ return self._intrinsics
+
+ @property
+ def raydirs_and_cam(self) -> tuple[torch.Tensor, torch.Tensor]: # (w, h, 3) and (3) dtype=float32
+ if getattr(self, "_raydirs_and_cam_cam2world", None) is not self.cam2world \
+ or getattr(self, "_raydirs_and_cam_intrinsics", None) is not self.intrinsics \
+ or getattr(self, "_raydirs_and_cam_uvs", None) is not self.uvs:
+ self._raydirs_and_cam_cam2world = cam2world = self.cam2world
+ self._raydirs_and_cam_intrinsics = intrinsics = self.intrinsics
+ self._raydirs_and_cam_uvs = uvs = self.uvs
+
+ #cam_pos = (cam2world @ torch.tensor([0, 0, 0, 1], dtype=torch.float32))[:3]
+ cam_pos = cam2world[:3, -1]
+
+ dirs = -geometry.get_ray_directions(uvs, cam2world[None, ...], intrinsics[None, ...]).squeeze(-1)
+
+ self._raydirs_and_cam = (dirs, cam_pos)
+ return (
+ self._raydirs_and_cam[0],
+ self._raydirs_and_cam[1],
+ )
+
+ def run(self):
+ self.is_headless = False
+ pygame.display.init() # we do not use the mixer, which often hangs on quit
+ try:
+ window = pygame.display.set_mode(self.scaled_res, flags=pygame.RESIZABLE)
+ buffer = pygame.surface.Surface(self.res)
+
+ window.fill(self.fill_color)
+ buffer.fill(self.fill_color)
+ pygame.display.flip()
+
+ pixel_view = pygame.surfarray.pixels3d(buffer) # (W, H, 3)
+
+ current_scale = self.scale
+ def remake_window_buffer(window_size: IVec2):
+ nonlocal buffer, pixel_view, current_scale
+ self.res = (
+ window_size[0] // self.scale,
+ window_size[1] // self.scale,
+ )
+ buffer = pygame.surface.Surface(self.res)
+ pixel_view = pygame.surfarray.pixels3d(buffer)
+ current_scale = self.scale
+
+ print()
+
+ self.setup()
+
+ is_running = True
+ clock = pygame.time.Clock()
+ epoch = t_prev = time.time()
+ self.frame_idx = -1
+ while is_running:
+ self.frame_idx += 1
+ if not self.fps_cap is None: clock.tick(self.fps_cap)
+ t = time.time()
+ self.td = t - t_prev
+ t_prev = t
+ self.t = t - epoch
+ print("\rFPS:", 1/self.td, " "*10, end="")
+
+ self.render_frame(pixel_view)
+
+ pygame.transform.scale(buffer, window.get_size(), window)
+ pygame.display.flip()
+
+ keys_pressed = pygame.key.get_pressed()
+ self.handle_keys_pressed(keys_pressed)
+
+ for event in pygame.event.get():
+ if event.type == pygame.VIDEORESIZE:
+ print()
+ print("== resize window ==")
+ remake_window_buffer(event.size)
+ elif event.type == pygame.QUIT:
+ is_running = False
+ elif event.type == pygame.KEYUP:
+ self.handle_key_up(event.key, keys_pressed)
+ elif event.type == pygame.KEYDOWN:
+ self.handle_key_down(event.key, keys_pressed)
+ if event.key == pygame.K_q:
+ is_running = False
+ elif event.key == pygame.K_y:
+ fname = self.mk_dump_fname("png")
+ fname.parent.mkdir(parents=True, exist_ok=True)
+ pygame.image.save(buffer.copy(), fname)
+ print()
+ print("Saved", fname)
+ elif event.type == pygame.MOUSEBUTTONUP:
+ self.handle_mouse_button_up(event.pos, event.button, keys_pressed)
+ elif event.type == pygame.MOUSEBUTTONDOWN:
+ self.handle_mouse_button_down(event.pos, event.button, keys_pressed)
+ elif event.type == pygame.MOUSEMOTION:
+ self.handle_mouse_motion(event.pos, event.rel, event.buttons, keys_pressed)
+ elif event.type == pygame.MOUSEWHEEL:
+ self.handle_mousewheel(event.flipped, event.x, event.y, keys_pressed)
+
+ if current_scale != self.scale:
+ remake_window_buffer(window.get_size())
+
+ finally:
+ self.teardown()
+ print()
+ pygame.quit()
+
+ def render_headless(self, output_path: str, *, n_frames: int, fps: int, state_callback: Callable[["InteractiveViewer", int], None] | None, resolution=None, bitrate=None, **kw):
+ self.is_headless = True
+ self.fps = fps
+
+ buffer = pygame.surface.Surface(self.res if resolution is None else resolution)
+ pixel_view = pygame.surfarray.pixels3d(buffer) # (W, H, 3)
+
+ def do():
+ try:
+ self.setup()
+ for frame in tqdm(range(n_frames), **kw, disable=n_frames==1):
+ self.frame_idx = frame
+ if state_callback is not None:
+ state_callback(self, frame)
+
+ self.render_frame(pixel_view)
+
+ yield pixel_view.copy().swapaxes(0,1)
+ finally:
+ self.teardown()
+
+ output_path = Path(output_path)
+ if output_path.suffix == ".png":
+ if n_frames > 1 and "%" not in output_path.name: raise ValueError
+ output_path.parent.mkdir(parents=True, exist_ok=True)
+ for i, framebuffer in enumerate(do()):
+ with imageio.get_writer(output_path.parent / output_path.name.replace("%", f"{i:04}")) as writer:
+ writer.append_data(framebuffer)
+ else: # ffmpeg - https://imageio.readthedocs.io/en/v2.9.0/format_ffmpeg.html#ffmpeg
+ with imageio.get_writer(output_path, fps=fps, bitrate=bitrate) as writer:
+ for framebuffer in do():
+ writer.append_data(framebuffer)
+
+ def load_sphere_map(self, fname):
+ self._sphere_surf = pygame.image.load(fname)
+ self._sphere_map = pygame.surfarray.pixels3d(self._sphere_surf)
+
+ def lookup_sphere_map_dirs(self, dirs, origins):
+ near, far = geometry.ray_sphere_intersect(
+ torch.tensor(origins),
+ torch.tensor(dirs),
+ sphere_radii = torch.tensor(origins).norm(dim=-1) * 2,
+ )
+ hits = far.detach()
+
+ x = hits[..., 0]
+ y = hits[..., 1]
+ z = hits[..., 2]
+ theta = (z / hits.norm(dim=-1)).acos()
+ phi = (y/x).atan()
+ phi[(x<0) & (y>=0)] += 3.14
+ phi[(x<0) & (y< 0)] -= 3.14
+
+ w, h = self._sphere_map.shape[:2]
+
+ return self._sphere_map[
+ ((phi / (2*torch.pi) * w).int() % w).cpu(),
+ ((theta / (1*torch.pi) * h).int() % h).cpu(),
+ ]
+
+ def blit_sphere_map_mask(self, pixel_view, mask=None):
+ dirs, origin = self.raydirs_and_cam
+ if mask is None: mask = (slice(None), slice(None))
+ pixel_view[mask] \
+ = self.lookup_sphere_map_dirs(dirs, origin[None, None, :])
+
+ def mk_dump_fname(self, suffix: str, uid=None) -> Path:
+ name = self.name.split("-")[-1] if len(self.name) > 160 else self.name
+ if uid is not None: name = f"{name}-{uid}"
+ return self.screenshot_dir / f"pygame-viewer-{datetime.now():%Y%m%d-%H%M%S}-{name}.{suffix}"
diff --git a/ifield/viewer/ray_field.py b/ifield/viewer/ray_field.py
new file mode 100644
index 0000000..a0919df
--- /dev/null
+++ b/ifield/viewer/ray_field.py
@@ -0,0 +1,792 @@
+from ..data.common.scan import SingleViewUVScan
+import mesh_to_sdf.scan as sdf_scan
+from ..models import intersection_fields
+from ..utils import geometry, helpers
+from ..utils.operators import diff
+from .common import InteractiveViewer
+from matplotlib import cm
+import matplotlib.colors as mcolors
+from concurrent.futures import ThreadPoolExecutor
+from textwrap import dedent
+from typing import Hashable, Optional, Callable
+from munch import Munch
+import functools
+import itertools
+import numpy as np
+import random
+from pathlib import Path
+import shutil
+import subprocess
+import torch
+from trimesh import Trimesh
+import trimesh.transformations as T
+
+
+class ModelViewer(InteractiveViewer):
+ lambertian_color = (1.0, 1.0, 1.0)
+ max_cols = 200
+ max_cols = 32
+
+ def __init__(self,
+ model : intersection_fields.IntersectionFieldAutoDecoderModel,
+ start_uid : Hashable,
+ skyward : str = "+Z",
+ mesh_gt_getter: Callable[[Hashable], Trimesh] | None = None,
+ *a, **kw):
+ self.model = model
+ self.model.eval()
+ self.current_uid = self._prev_uid = start_uid
+ self.all_uids = list(model.keys())
+
+ self.mesh_gt_getter = mesh_gt_getter
+ self.current_gt_mesh: tuple[Hashable, Trimesh] = (None, None)
+
+ self.display_mode_normals = self.vizmodes_normals .index("medial" if self.model.hparams.output_mode == "medial_sphere" else "analytical")
+ self.display_mode_shading = self.vizmodes_shading .index("lambertian")
+ self.display_mode_centroid = self.vizmodes_centroids.index("best-centroids-colored")
+ self.display_mode_spheres = self.vizmodes_spheres .index(None)
+ self.display_mode_variation = 0
+
+ self.display_sphere_map_bg = True
+ self.atom_radius_offset = 0
+ self.atom_index_solo = None
+ self.export_medial_surface_mesh = False
+
+ self.light_angle1 = 0
+ self.light_angle2 = 0
+
+ self.obj_rot = {
+ "-X": torch.tensor(T.rotation_matrix(angle= np.pi/2, direction=(0, 1, 0))[:3, :3], **model.device_and_dtype).T,
+ "+X": torch.tensor(T.rotation_matrix(angle=-np.pi/2, direction=(0, 1, 0))[:3, :3], **model.device_and_dtype).T,
+ "-Y": torch.tensor(T.rotation_matrix(angle= np.pi/2, direction=(1, 0, 0))[:3, :3], **model.device_and_dtype).T,
+ "+Y": torch.tensor(T.rotation_matrix(angle=-np.pi/2, direction=(1, 0, 0))[:3, :3], **model.device_and_dtype).T,
+ "-Z": torch.tensor(T.rotation_matrix(angle= np.pi, direction=(1, 0, 0))[:3, :3], **model.device_and_dtype).T,
+ "+Z": torch.eye(3, **model.device_and_dtype),
+ }[str(skyward).upper()]
+ self.obj_rot_inv = torch.linalg.inv(self.obj_rot)
+
+ super().__init__(*a, **kw)
+
+ vizmodes_normals = (
+ "medial",
+ "analytical",
+ "ground_truth",
+ )
+ vizmodes_shading = (
+ None, # just atoms or medial axis
+ "colored-lambertian",
+ "lambertian",
+ "shade-best-radii",
+ "shade-all-radii",
+ "translucent",
+ "normal",
+ "centroid-grad-norm", # backprop
+ "anisotropic", # backprop
+ "curvature", # backprop
+ "glass",
+ "double-glass",
+ )
+ vizmodes_centroids = (
+ None,
+ "best-centroids",
+ "all-centroids",
+ "best-centroids-colored",
+ "all-centroids-colored",
+ "miss-centroids-colored",
+ "all-miss-centroids-colored",
+ )
+ vizmodes_spheres = (
+ None,
+ "intersecting-sphere",
+ "intersecting-sphere-colored",
+ "best-sphere",
+ "best-sphere-colored",
+ "all-spheres-colored",
+ )
+
+ def get_display_mode(self) -> tuple[str, str, Optional[str], Optional[str]]:
+ MARF = self.model.hparams.output_mode == "medial_sphere"
+ if isinstance(self.display_mode_normals, str): self.display_mode_normals = self.vizmodes_shading .index(self.display_mode_normals)
+ if isinstance(self.display_mode_shading, str): self.display_mode_shading = self.vizmodes_shading .index(self.display_mode_shading)
+ if isinstance(self.display_mode_centroid, str): self.display_mode_centroid = self.vizmodes_centroids.index(self.display_mode_centroid)
+ if isinstance(self.display_mode_spheres, str): self.display_mode_spheres = self.vizmodes_spheres .index(self.display_mode_spheres)
+ out = (
+ self.vizmodes_normals [self.display_mode_normals % len(self.vizmodes_normals)],
+ self.vizmodes_shading [self.display_mode_shading % len(self.vizmodes_shading)],
+ self.vizmodes_centroids[self.display_mode_centroid % len(self.vizmodes_centroids)] if MARF else None,
+ self.vizmodes_spheres [self.display_mode_spheres % len(self.vizmodes_spheres)] if MARF else None,
+ )
+ self.set_caption(" & ".join(i for i in out if i is not None))
+ return out
+
+ @property
+ def cam_state(self):
+ return super().cam_state | {
+ "light_angle1" : self.light_angle1,
+ "light_angle2" : self.light_angle2,
+ }
+
+ @cam_state.setter
+ def cam_state(self, new_state):
+ InteractiveViewer.cam_state.fset(self, new_state)
+ self.light_angle1 = new_state.get("light_angle1", self.light_angle1)
+ self.light_angle2 = new_state.get("light_angle2", self.light_angle2)
+
+ def get_current_conditioning(self) -> Optional[torch.Tensor]:
+ if not self.model.is_conditioned:
+ return None
+
+ prev_uid = self._prev_uid # to determine if target has changed
+ next_z = self.model[prev_uid].detach() # interpolation target
+ prev_z = getattr(self, "_prev_z", next_z) # interpolation source
+ epoch = getattr(self, "_prev_epoch", 0) # interpolation factor
+
+ if not self.is_headless:
+ now = self.t
+ t = (now - epoch) / 1 # 1 second
+ else:
+ now = self.frame_idx
+ t = (now - epoch) / self.fps # 1 second
+ assert t >= 0
+
+ if t < 1:
+ next_z = next_z*t + prev_z*(1-t)
+
+ if prev_uid != self.current_uid:
+ self._prev_uid = self.current_uid
+ self._prev_z = next_z
+ self._prev_epoch = now
+
+ return next_z
+
+ def get_current_ground_truth(self) -> Trimesh | None:
+ if self.mesh_gt_getter is None:
+ return None
+ uid, mesh = self.current_gt_mesh
+ try:
+ if uid != self.current_uid:
+ print("Loading ground truth mesh...")
+ mesh = self.mesh_gt_getter(self.current_uid)
+ self.current_gt_mesh = self.current_uid, mesh
+ except NotImplementedError:
+ self.current_gt_mesh = self.current_uid, None
+ return None
+ return mesh
+
+ def handle_keys_pressed(self, pressed):
+ td = super().handle_keys_pressed(pressed)
+ mod = pressed[self.constants.K_LALT] or pressed[self.constants.K_RALT]
+ if not mod and pressed[self.constants.K_f]: self.light_angle1 -= td * 0.5
+ if not mod and pressed[self.constants.K_g]: self.light_angle1 += td * 0.5
+ if mod and pressed[self.constants.K_f]: self.light_angle2 += td * 0.5
+ if mod and pressed[self.constants.K_g]: self.light_angle2 -= td * 0.5
+ return td
+
+ def handle_key_down(self, key, keys_pressed):
+ super().handle_key_down(key, keys_pressed)
+ shift = keys_pressed[self.constants.K_LSHIFT] or keys_pressed[self.constants.K_RSHIFT]
+ if key == self.constants.K_o:
+ i = self.all_uids.index(self.current_uid)
+ i = (i - 1) % len(self.all_uids)
+ self.current_uid = self.all_uids[i]
+ print(self.current_uid)
+ if key == self.constants.K_p:
+ i = self.all_uids.index(self.current_uid)
+ i = (i + 1) % len(self.all_uids)
+ self.current_uid = self.all_uids[i]
+ print(self.current_uid)
+ if key == self.constants.K_SPACE:
+ self.display_sphere_map_bg = {
+ True : 255,
+ 255 : 0,
+ 0 : True,
+ }.get(self.display_sphere_map_bg, True)
+ if key == self.constants.K_u: self.export_medial_surface_mesh = True
+ if key == self.constants.K_x: self.display_mode_normals += -1 if shift else 1
+ if key == self.constants.K_c: self.display_mode_shading += -1 if shift else 1
+ if key == self.constants.K_v: self.display_mode_centroid += -1 if shift else 1
+ if key == self.constants.K_b: self.display_mode_spheres += -1 if shift else 1
+ if key == self.constants.K_e: self.display_mode_variation+= -1 if shift else 1
+ if key == self.constants.K_c: self.display_mode_variation = 0
+ if key == self.constants.K_0: self.atom_index_solo = None
+ if key == self.constants.K_1: self.atom_index_solo = 0 if self.atom_index_solo != 0 else None
+ if key == self.constants.K_2: self.atom_index_solo = 1 if self.atom_index_solo != 1 else None
+ if key == self.constants.K_3: self.atom_index_solo = 2 if self.atom_index_solo != 2 else None
+ if key == self.constants.K_4: self.atom_index_solo = 3 if self.atom_index_solo != 3 else None
+ if key == self.constants.K_5: self.atom_index_solo = 4 if self.atom_index_solo != 4 else None
+ if key == self.constants.K_6: self.atom_index_solo = 5 if self.atom_index_solo != 5 else None
+ if key == self.constants.K_7: self.atom_index_solo = 6 if self.atom_index_solo != 6 else None
+ if key == self.constants.K_8: self.atom_index_solo = 7 if self.atom_index_solo != 7 else None
+ if key == self.constants.K_9: self.atom_index_solo = self.atom_index_solo + (-1 if shift else 1) if self.atom_index_solo is not None else 0
+
+ def handle_mouse_button_down(self, pos, button, keys_pressed):
+ super().handle_mouse_button_down(pos, button, keys_pressed)
+ if button in (1, 3):
+ self.display_mode_spheres += 1 if button == 1 else -1
+
+ def handle_mousewheel(self, flipped, x, y, keys_pressed):
+ shift = keys_pressed[self.constants.K_LSHIFT] or keys_pressed[self.constants.K_RSHIFT]
+ if not shift:
+ super().handle_mousewheel(flipped, x, y, keys_pressed)
+ else:
+ self.atom_radius_offset += 0.005 * y
+ print()
+ print("atom_radius_offset:", self.atom_radius_offset)
+
+ def setup(self):
+ if not self.is_headless:
+ print(dedent("""
+ WASD + PG Up/Down - translate
+ ARROWS - rotate
+
+ (SHIFT+) C - Next/(Prev) shading mode
+ (SHIFT+) V - Next/(Prev) centroids mode
+ (SHIFT+) B - Next/(Prev) sphere mode
+ Mouse L/ R - Next/ Prev sphere mode
+ (SHIFT+) E - Next/(Prev) variation (for quick experimentation within a shading mode)
+ SHIFT + Scroll - Offset atom radius
+ ALT + Scroll - Modify FoV (_true_ zoom)
+ Mouse Scroll - Translate in/out ("zoom", moves camera to/from to point of focus)
+ Alt+PG Up/Down - Translate in/out ("zoom", moves camera to/from to point of focus)
+
+ F / G - rotate light left / right
+ ALT+ F / G - rotate light up / down
+ CTRL / SHIFT - faster/slower rotation
+ O / P - prev/next object
+ 1-9 - solo atom
+ 0 - show all atoms
+ + / - - decrease/increase pixel scale
+ R - rotate continuously
+ H / SHIFT+H / CTRL+H - load/save/print camera state
+ Enter - reset camera state
+ Y - save screenshot
+ U - save mesh of centroids
+ Space - cycle sphere map background
+ Q - quit
+ """).strip())
+
+ fname = Path(__file__).parent.resolve() / "assets/texturify_pano-1-4.jpg"
+ self.load_sphere_map(fname)
+
+ if self.model.hparams.output_mode == "medial_sphere":
+ @self.model.net.register_forward_hook
+ def atom_offset_radius_and_solo(model, input, output):
+ slice = (..., [i+3 for i in range(0, output.shape[-1], 4)])
+ output[slice] += self.atom_radius_offset * output[slice].sign()
+ if self.atom_index_solo is not None:
+ x = self.atom_index_solo * 4
+ x = x % output.shape[-1]
+ output = output[..., list(range(x, x+4))]
+ return output
+ self._atom_offset_radius_and_solo_hook = atom_offset_radius_and_solo
+
+ def teardown(self):
+ if hasattr(self, "_atom_offset_radius_and_solo_hook"):
+ self._atom_offset_radius_and_solo_hook.remove()
+ del self._atom_offset_radius_and_solo_hook
+
+ @torch.no_grad()
+ def render_frame(self, pixel_view: np.ndarray): # (W, H, 3) dtype=uint8
+ MARF = self.model.hparams.output_mode == "medial_sphere"
+ PRIF = self.model.hparams.output_mode == "orthogonal_plane"
+ assert (MARF or PRIF) and MARF != PRIF
+ device_and_dtype = self.model.device_and_dtype
+ device = self.model.device
+ dtype = self.model.dtype
+
+ (
+ vizmode_normals,
+ vizmode_shading,
+ vizmode_centroids,
+ vizmode_spheres,
+ ) = self.get_display_mode()
+
+ dirs, origins = self.raydirs_and_cam
+ origins = origins.detach().clone().to(**device_and_dtype)
+ dirs = dirs .detach().clone().to(**device_and_dtype)
+
+ if vizmode_normals != "ground_truth" or self.get_current_ground_truth() is None:
+
+ # enable grad or not
+ do_jac = PRIF or vizmode_normals == "analytical"
+ do_jac_medial = MARF and "centroid-grad-norm" in (vizmode_shading or "")
+ do_shape_operator = "anisotropic" in (vizmode_shading or "") or "curvature" in (vizmode_shading or "")
+ do_grad = do_jac or do_jac_medial or do_shape_operator
+ if do_grad:
+ origins = origins.broadcast_to(dirs.shape)
+
+ self.model.eval()
+ latent = self.get_current_conditioning()
+ if self.max_cols is None or self.max_cols > dirs.shape[0]:
+ chunks = [slice(None)]
+ else:
+ chunks = [slice(col, col+self.max_cols) for col in range(0, dirs.shape[0], self.max_cols)]
+ forward_chunks = []
+ for chunk in chunks:
+ self.model.zero_grad()
+ origins_chunk = origins[chunk if origins.ndim != 1 else slice(None)] @ self.obj_rot
+ dirs_chunk = dirs [chunk] @ self.obj_rot
+ if do_grad:
+ origins_chunk.requires_grad = dirs_chunk.requires_grad = True
+
+ @forward_chunks.append
+ @(lambda f: f(origins_chunk, dirs_chunk))
+ @torch.set_grad_enabled(do_grad)
+ def forward_chunk(origins, dirs) -> Munch:
+ if PRIF:
+ intersections, is_intersecting = self.model(dict(origins=origins, dirs=dirs), z=latent, normalize_origins=True)
+ is_intersecting = is_intersecting > 0.5
+ elif MARF:
+ (
+ depths, silhouettes, intersections,
+ intersection_normals, is_intersecting,
+ sphere_centers, sphere_radii,
+
+ atom_indices,
+ all_intersections, all_intersection_normals, all_depths, all_silhouettes, all_is_intersecting,
+ all_sphere_centers, all_sphere_radii,
+ ) = self.model.forward(dict(origins=origins, dirs=dirs), z=latent,
+ intersections_only = False,
+ return_all_atoms = True,
+ )
+
+ if do_jac:
+ jac = diff.jacobian(intersections, origins, detach=not do_shape_operator)
+ intersection_normals = self.model.compute_normals_from_intersection_origin_jacobian(jac, dirs.detach())
+
+ if do_jac_medial:
+ sphere_centers_jac = diff.jacobian(sphere_centers, origins, detach=True)
+
+ if do_shape_operator:
+ hess = diff.jacobian(intersection_normals, origins, detach=True)[is_intersecting, :, :]
+ N = intersection_normals.detach()[is_intersecting, :]
+ TM = (torch.eye(3, device=device) - N[..., None, :]*N[..., :, None]) # projection onto tangent plane
+ # shape operator, i.e. total derivative of the surface normal w.r.t. the tangent space
+ shape_operator = hess @ TM
+
+ return Munch((k, v.detach()) for k, v in locals().items() if isinstance(v, torch.Tensor))
+
+ intersections = torch.cat([chunk.intersections for chunk in forward_chunks], dim=0)
+ is_intersecting = torch.cat([chunk.is_intersecting for chunk in forward_chunks], dim=0)
+ intersection_normals = torch.cat([chunk.intersection_normals for chunk in forward_chunks], dim=0)
+ if MARF:
+ all_sphere_centers = torch.cat([chunk.all_sphere_centers for chunk in forward_chunks], dim=0)
+ all_sphere_radii = torch.cat([chunk.all_sphere_radii for chunk in forward_chunks], dim=0)
+ atom_indices = torch.cat([chunk.atom_indices for chunk in forward_chunks], dim=0)
+ silhouettes = torch.cat([chunk.silhouettes for chunk in forward_chunks], dim=0)
+ sphere_centers = torch.cat([chunk.sphere_centers for chunk in forward_chunks], dim=0)
+ sphere_radii = torch.cat([chunk.sphere_radii for chunk in forward_chunks], dim=0)
+ if do_jac_medial:
+ sphere_centers_jac = torch.cat([chunk.sphere_centers_jac for chunk in forward_chunks], dim=0)
+ if do_shape_operator:
+ shape_operator = torch.cat([chunk.shape_operator for chunk in forward_chunks], dim=0)
+
+ n_atoms = all_sphere_centers.shape[-2] if MARF else 1
+
+ intersections = intersections @ self.obj_rot_inv
+ intersection_normals = intersection_normals @ self.obj_rot_inv
+ sphere_centers = sphere_centers @ self.obj_rot_inv if sphere_centers is not None else None
+ all_sphere_centers = all_sphere_centers @ self.obj_rot_inv if all_sphere_centers is not None else None
+
+ else: # render ground truth mesh
+ # HACK: we use a thread to not break the pygame opengl context
+ with ThreadPoolExecutor(max_workers=1) as p:
+ scan = p.submit(sdf_scan.Scan, self.get_current_ground_truth(),
+ camera_transform = self.cam2world.numpy(),
+ resolution = self.res[1],
+ calculate_normals = True,
+ fov = self.cam_fov_y,
+ z_near = 0.001,
+ z_far = 50,
+ no_flip_backfaced_normals = True
+ ).result()
+ n_atoms, MARF, PRIF = 1, False, True
+ is_intersecting = torch.zeros(self.res, dtype=bool)
+ is_intersecting[ (self.res[0]-self.res[1]) // 2 : (self.res[0]-self.res[1]) // 2 + self.res[1], : ] = torch.tensor(scan.depth_buffer != 0, dtype=bool)
+ intersections = torch.zeros((*is_intersecting.shape, 3), dtype=dtype)
+ intersection_normals = torch.zeros((*is_intersecting.shape, 3), dtype=dtype)
+ intersections [is_intersecting] = torch.tensor(scan.points, dtype=dtype)
+ intersection_normals[is_intersecting] = torch.tensor(scan.normals, dtype=dtype)
+ is_intersecting = is_intersecting .flip(1).to(device)
+ intersections = intersections .flip(1).to(device)
+ intersection_normals = intersection_normals.flip(1).to(device)
+
+ mask = is_intersecting.cpu()
+
+ mx, my = self.mouse_position
+ w, h = dirs.shape[:2]
+
+ # fill white
+ if self.display_sphere_map_bg == True:
+ self.blit_sphere_map_mask(pixel_view)
+ else:
+ pixel_view[:] = self.display_sphere_map_bg
+
+ # draw to buffer
+
+ to_cam = -dirs.detach()
+
+ # light direction
+ extra = np.pi if vizmode_shading == "translucent" else 0
+ LM = torch.tensor(T.rotation_matrix(angle=self.light_angle2, direction=(0, 1, 0))[:3, :3], dtype=dtype)
+ LM = torch.tensor(T.rotation_matrix(angle=self.light_angle1 + extra, direction=(1, 0, 0))[:3, :3], dtype=dtype) @ LM
+ to_light = (self.cam2world[:3, :3] @ LM @ torch.tensor((1, 1, 3), dtype=dtype)).to(device)[None, :]
+ to_light = to_light / to_light.norm(dim=-1, keepdim=True)
+
+ # used to color different atom candidates
+ color_set = tuple(map(helpers.hex2tuple,
+ itertools.chain(
+ mcolors.TABLEAU_COLORS.values(),
+ #list(mcolors.TABLEAU_COLORS.values())[::-1],
+ #['#f8481c', '#c20078', '#35530a', '#010844', '#a8ff04'],
+ mcolors.XKCD_COLORS.values(),
+ )
+ ))
+ color_per_atom = (*zip(*zip(range(n_atoms), itertools.cycle(color_set))),)[1]
+
+
+ # shade hits
+
+ if vizmode_shading is None:
+ pass
+ elif vizmode_shading == "colored-lambertian":
+ if n_atoms > 1:
+ color = torch.tensor(color_per_atom, device=device)[(*atom_indices[is_intersecting].T,)]
+ else:
+ color = torch.tensor(color_set[(0 if self.atom_index_solo is None else self.atom_index_solo) % len(color_set)], device=device)
+ lambertian = torch.einsum("id,id->i",
+ intersection_normals[is_intersecting, :],
+ to_light,
+ )[..., None]
+
+ pixel_view[mask, :] = (color *
+ torch.einsum("id,id->i",
+ intersection_normals[is_intersecting, :],
+ to_cam[is_intersecting, :],
+ )[..., None]).int().cpu()
+ pixel_view[mask, :] = (
+ 255 * lambertian.clamp(0, 1).pow(32) +
+ color * (lambertian + 0.25).clamp(0, 1) * (1-lambertian.clamp(0, 1).pow(32))
+ ).cpu()
+ elif vizmode_shading == "lambertian":
+ lambertian = torch.einsum("id,id->i",
+ intersection_normals[is_intersecting, :],
+ to_light,
+ )[..., None].clamp(0, 1)
+
+ if self.lambertian_color == (1.0, 1.0, 1.0):
+ pixel_view[mask, :] = (255 * lambertian).cpu()
+ else:
+ color = 255*torch.tensor(self.lambertian_color, device=device)
+ pixel_view[mask, :] = (color *
+ torch.einsum("id,id->i",
+ intersection_normals[is_intersecting, :],
+ to_cam[is_intersecting, :],
+ )[..., None]).int().cpu()
+ pixel_view[mask, :] = (
+ 255 * lambertian.clamp(0, 1).pow(32) +
+ color * (lambertian + 0.25).clamp(0, 1) * (1-lambertian.clamp(0, 1).pow(32))
+ ).cpu()
+ elif vizmode_shading == "translucent" and MARF:
+ lambertian = torch.einsum("id,id->i",
+ intersection_normals[is_intersecting, :],
+ to_light,
+ )[..., None].abs().clamp(0, 1)
+
+ distortion = 0.08
+ power = 16
+ ambient = 0
+ thickness = sphere_radii[is_intersecting].detach()
+ if self.display_mode_variation % 2:
+ thickness = thickness.mean()
+
+ color1 = torch.tensor((1, 0.5, 0.5), **device_and_dtype) # subsurface
+ color2 = torch.tensor((0, 1, 1), **device_and_dtype) # diffuse
+
+ l = to_light + intersection_normals[is_intersecting, :] * distortion
+ d = (to_cam[is_intersecting, :] * -l).sum(dim=-1).clamp(0, None).pow(power)
+ f = (d + ambient) * (1/(0.05 + thickness))
+
+ pixel_view[((dirs * to_light).sum(dim=-1) > 0.99).cpu(), :] = 255 # draw light source
+
+ pixel_view[mask, :] = (255 * (
+ color2 * (0.05 + lambertian*0.15) +
+ color1 * 0.3 * f[..., None]
+ ).clamp(0, 1)).cpu()
+ elif vizmode_shading == "anisotropic" and vizmode_normals != "ground_truth":
+ eigvals, eigvecs = torch.linalg.eig(shape_operator.mT) # slow, complex output, not sorted
+ eigvals, indices = eigvals.abs().sort(dim=-1)
+ eigvecs = (eigvecs.abs() * eigvecs.real.sign()).take_along_dim(indices[..., None, :], dim=-1)
+ eigvecs = eigvecs.mT
+
+ s = self.display_mode_variation % 5
+ if s in (0, 1):
+ # try to keep these below 0.2:
+ if s == 0: a1, a2 = 0.05, 0.3
+ if s == 1: a1, a2 = 0.3, 0.05
+
+ # == Ward anisotropic specular reflectance ==
+
+ # G.J. Ward, Measuring and modeling anisotropic reflection, in:
+ # Proceedings of the 19th Annual Conference on Computer Graphics and
+ # Interactive Techniques, 1992: pp. 265–272.
+
+ eigvecs /= eigvecs.norm(dim=-1, keepdim=True)
+
+ N = intersection_normals[is_intersecting, :]
+ H = to_cam[is_intersecting, :] + to_light
+ H = H / H.norm(dim=-1, keepdim=True)
+ specular = (1/(4*torch.pi * a1*a2 * torch.sqrt((
+ (N * to_cam[is_intersecting, :]).sum(dim=-1) *
+ (N * to_light ).sum(dim=-1)
+ )))) * torch.exp(
+ -2 * (
+ ((H * eigvecs[..., 2, :]).sum(dim=-1) / a1).pow(2)
+ +
+ ((H * eigvecs[..., 1, :]).sum(dim=-1) / a2).pow(2)
+ ) / (
+ 1 + (N * H).sum(dim=-1)
+ )
+ )
+ specular = specular.clamp(0, None).nan_to_num(0, 0, 0)
+ lambertian = torch.einsum("id,id->i", N, to_light ).clamp(0, None)
+
+ color1 = 0.4 * torch.tensor((1, 1, 1), **device_and_dtype) # specular
+ color2 = 0.4 * torch.tensor((0, 1, 1), **device_and_dtype) # diffuse
+ pixel_view[mask, :] = (255 * (
+ color1 * specular [..., None] +
+ color2 * lambertian[..., None]
+ ).clamp(0, 1)).int().cpu()
+ if s == 2:
+ pixel_view[mask, :] = (255 * (
+ eigvecs[..., 2, :].abs().clamp(0, 1) # orientation only
+ )).int().cpu()
+ elif s == 3:
+ pixel_view[mask, :] = (255 * (
+ eigvecs[..., 1, :].abs().clamp(0, 1) # orientation only
+ )).int().cpu()
+ elif s == 4:
+ pixel_view[mask, :] = (255 * (
+ eigvecs[..., 0, :].abs().clamp(0, 1) # orientation only
+ )).int().cpu()
+ elif vizmode_shading == "shade-best-radii" and MARF:
+ lambertian = torch.einsum("id,id->i",
+ intersection_normals[is_intersecting, :],
+ to_light,
+ )[..., None]
+
+ radii = sphere_radii[is_intersecting]
+ radii = radii - 0.04
+ radii = radii / 0.4
+
+ colors = cm.plasma(radii.clamp(0, 1).cpu())[..., :3]
+ pixel_view[mask, :] = 255 * (
+ lambertian.pow(32).clamp(0, 1).cpu().numpy() +
+ colors * (lambertian + 0.25).clamp(0, 1).cpu().numpy() * (1-lambertian.pow(32).clamp(0, 1)).cpu().numpy()
+ )
+ elif vizmode_shading == "shade-all-radii" and MARF:
+ radii = sphere_radii[is_intersecting][..., None]
+ radii /= radii.max()
+ if n_atoms > 1:
+ color = torch.tensor(color_per_atom, device=device)[(*atom_indices[is_intersecting].T,)]
+ else:
+ color = torch.tensor(color_set[(0 if self.atom_index_solo is None else self.atom_index_solo) % len(color_set)], device=device)
+ pixel_view[mask, :] = (color * radii).int().cpu()
+ elif vizmode_shading == "normal":
+ normal = intersection_normals[is_intersecting, :]
+ pixel_view[mask, :] = (255 * (normal * 0.5 + 0.5) ).int().cpu()
+ elif vizmode_shading == "curvature" and vizmode_normals != "ground_truth":
+ eigvals = torch.linalg.eigvals(shape_operator.mT) # complex output, not sorted
+
+ # we sort them by absolute magnitude, not the real component
+ _, indices = (eigvals.abs() * eigvals.real.sign()).sort(dim=-1)
+ eigvals = eigvals.real.take_along_dim(indices, dim=-1)
+
+ s = self.display_mode_variation % (6 if MARF else 5)
+ if s==0: out = (eigvals[..., [0, 2]].mean(dim=-1, keepdim=True) / 25).tanh() # mean curvature
+ if s==1: out = (eigvals[..., [0, 2]].prod(dim=-1, keepdim=True) / 25).tanh() # gaussian curvature
+ if s==2: out = (eigvals[..., [2]] / 25).tanh() # maximum principal curvature - k1
+ if s==3: out = (eigvals[..., [1]] / 25).tanh() # some curvature
+ if s==4: out = (eigvals[..., [0]] / 25).tanh() # minimum principal curvature - k2
+ if s==5: out = ((sphere_radii[is_intersecting][..., None].detach() - 1 / eigvals[..., [2]].clamp(1e-8, None)) * 5).tanh().clamp(0, None)
+
+ lambertian = torch.einsum("id,id->i",
+ intersection_normals[is_intersecting, :],
+ to_light,
+ )[..., None]
+
+ pixel_view[mask, :] = (255 * (lambertian+0.5).clamp(0, 1) * torch.cat((
+ 1+out.clamp(-1, 0),
+ 1-out.abs(),
+ 1-out.clamp(0, 1),
+ ), dim=-1)).int().cpu()
+ elif vizmode_shading == "centroid-grad-norm" and MARF:
+ asd = sphere_centers_jac[is_intersecting, :, :].norm(dim=-2).mean(dim=-1, keepdim=True)
+ asd -= asd.min()
+ asd /= asd.max()
+ pixel_view[mask, :] = (255 * asd).cpu()
+ elif "glass" in vizmode_shading:
+ normals = intersection_normals[is_intersecting, :]
+ to_cam_ = to_cam [is_intersecting, :]
+ # "Empiricial Approximation" of fresnel
+ # https://developer.download.nvidia.com/CgTutorial/cg_tutorial_chapter07.html via
+ # http://kylehalladay.com/blog/tutorial/2014/02/18/Fresnel-Shaders-From-The-Ground-Up.html
+ cos = torch.einsum("id,id->i", normals, to_cam_ )[..., None]
+ bias, scale, power = 0, 4, 3
+ fresnel = (bias + scale*(1-cos)**power).clamp(0, 1)
+
+ #reflection
+ reflection = -to_cam_ - 2*(-cos)*normals
+
+ #refraction
+ r = 1 / 1.5 # refractive index, air -> glass
+ refraction = -r*to_cam_ + (r*cos - (1-r**2*(1-cos**2)).sqrt()) * normals
+ exit_point = intersections[is_intersecting, :]
+
+ # reflect the refraction over the plane defined by the refraction direction and the sphere center, resulting in the second refraction
+ if vizmode_shading == "double-glass" and MARF:
+ cos2 = torch.einsum("id,id->i", refraction, -to_cam_ )[..., None]
+ pn = -to_cam_ - cos2*refraction
+ pn /= pn.norm(dim=-1, keepdim=True)
+
+ refraction = -to_cam_ - 2*torch.einsum("id,id->i", pn, -to_cam_ )[..., None]*pn
+
+ exit_point -= sphere_centers[is_intersecting, :]
+ exit_point = exit_point - 2*torch.einsum("id,id->i", pn, exit_point )[..., None]*pn
+ exit_point += sphere_centers[is_intersecting, :]
+
+ fresnel = np.asanyarray(fresnel.cpu())
+ pixel_view[mask, :] \
+ = self.lookup_sphere_map_dirs(reflection, intersections[is_intersecting, :]) * fresnel \
+ + self.lookup_sphere_map_dirs(refraction, exit_point) * (1-fresnel)
+ else: # flat
+ pixel_view[mask, :] = 80
+
+ if not MARF: return
+
+ # overlay medial atoms
+
+ if vizmode_spheres is not None:
+ # show miss distance in red
+ s = silhouettes.detach()[~is_intersecting].clamp(0, 1)
+ s /= s.max()
+ pixel_view[~mask, 1] = (s * 255).cpu()
+ pixel_view[:, 2] = pixel_view[:, 1]
+
+ mouse_hits = 0 <= mx < w and 0 <= my < h and mask[mx, my]
+ draw_intersecting = "intersecting-sphere" in vizmode_spheres
+ draw_best = "best-sphere" in vizmode_spheres
+ draw_color = "-sphere-colored" in vizmode_spheres
+ draw_all = "all-spheres-colored" in vizmode_spheres
+
+ def get_nears():
+ if draw_all:
+ projected, near, far, is_intersecting = geometry.ray_sphere_intersect(
+ torch.tensor(origins),
+ torch.tensor(dirs[..., None, :]),
+ sphere_centers = all_sphere_centers[mx, my][None, None, ...],
+ sphere_radii = all_sphere_radii [mx, my][None, None, ...],
+ allow_nans = False,
+ return_parts = True,
+ )
+
+ depths = (near - origins).norm(dim=-1)
+ atom_indices_ = torch.where(is_intersecting, depths.detach(), depths.detach()+100).argmin(dim=-1, keepdim=True)
+ is_intersecting = is_intersecting.any(dim=-1)
+ projected = None
+ near = near.take_along_dim(atom_indices_[..., None], -2).squeeze(-2)
+ far = None
+ sphere_centers_ = all_sphere_centers[mx, my][None, None, ...].take_along_dim(atom_indices_[..., None], -2).squeeze(-2)
+
+ normals = near[is_intersecting, :] - sphere_centers_[is_intersecting, :]
+ normals /= torch.linalg.norm(normals, dim=-1)[..., None]
+
+ color = torch.tensor(color_per_atom, device=device)[(*atom_indices_[is_intersecting].T,)]
+ yield color, projected, near, far, is_intersecting, normals
+
+ if (mouse_hits and draw_intersecting) or draw_best:
+ projected, near, far, is_intersecting = geometry.ray_sphere_intersect(
+ torch.tensor(origins),
+ torch.tensor(dirs),
+ # unit-sphere by default
+ sphere_centers = sphere_centers[mx, my][None, None, ...],
+ sphere_radii = sphere_radii [mx, my][None, None, ...],
+ return_parts = True,
+ )
+
+ normals = near[is_intersecting, :] - sphere_centers[mx, my][None, ...]
+ normals /= torch.linalg.norm(normals, dim=-1)[..., None]
+ color = (255, 255, 255) if not draw_color else color_per_atom[atom_indices[mx, my]]
+ yield torch.tensor(color, device=device), projected, near, far, is_intersecting, normals
+
+ # draw sphere with lambertian shading
+ for color, projected, near, far, is_intersecting_2, normals in get_nears():
+ lambertian = torch.einsum("...id,...id->...i", normals, to_light )[..., None]
+ pixel_view[is_intersecting_2.cpu(), :] = (
+ 255*lambertian.pow(32).clamp(0, 1) +
+ color * (lambertian + 0.25).clamp(0, 1) * (1-lambertian.pow(32).clamp(0, 1))
+ ).cpu()
+
+ # overlay points / sphere centers
+
+ if vizmode_centroids is not None:
+ cam2world_inv = torch.tensor(self.cam2world_inv, **device_and_dtype)
+ intrinsics = torch.tensor(self.intrinsics, **device_and_dtype)
+
+ def get_coords():
+ miss_centroid = "miss-centroids" in vizmode_centroids
+ mask = is_intersecting if not miss_centroid else ~is_intersecting
+ if vizmode_centroids in ("all-centroids-colored", "all-miss-centroids-colored"):
+ # we use temporal dithering to the show all overlapping centers
+ for color, atom_index in sorted(zip(itertools.chain(color_set), range(n_atoms)), key=lambda x: random.random()):
+ yield color, all_sphere_centers[..., atom_index, :][mask], mask
+ elif "all-centroids" in vizmode_centroids:
+ yield (80, 150, 80), all_sphere_centers[mask].reshape(-1, 3), mask # [:, 3]
+
+ if "centroids-colored" in vizmode_centroids:
+ if n_atoms == 1:
+ color = color_set[(0 if self.atom_index_solo is None else self.atom_index_solo) % len(color_set)]
+ else:
+ color = torch.tensor(color_per_atom, device=device)[(*atom_indices[mask].T,)].cpu()
+ else:
+ color = (0, 0, 0)
+ yield color, sphere_centers[mask], mask
+
+ for i, (color, coords, coord_mask) in enumerate(get_coords()):
+ if self.export_medial_surface_mesh:
+ fname = self.mk_dump_fname("ply", uid=i)
+ p = torch.zeros_like(sphere_centers)
+ c = torch.zeros_like(sphere_centers)
+ p[coord_mask, :] = coords
+ c[coord_mask, :] = torch.tensor(color, device=p.device) / 255
+ SingleViewUVScan(
+ hits = ( mask).numpy(),
+ miss = (~mask).numpy(),
+ points = p.cpu().numpy(),
+ colors = c.cpu().numpy(),
+ normals=None, distances=None, cam_pos=None,
+ cam_mat4=None, proj_mat4=None, transforms=None,
+ ).to_mesh().export(str(fname), file_type="ply")
+ print("dumped", fname)
+ if shutil.which("f3d"):
+ subprocess.Popen(["f3d", "-gsy", "--up=+z", "--bg-color=1,1,1", fname], close_fds=True)
+
+ coords = torch.cat((coords, torch.ones((*coords.shape[:-1], 1), **device_and_dtype)), dim=-1)
+
+ coords = torch.einsum("...ij,...kj->...ki", cam2world_inv, coords)[..., :3]
+ coords = geometry.project(coords[..., 0], coords[..., 1], coords[..., 2], intrinsics)
+
+ in_view = functools.reduce(torch.mul, (
+ coords[:, 0] < pixel_view.shape[1],
+ coords[:, 0] >= 0,
+ coords[:, 1] < pixel_view.shape[0],
+ coords[:, 1] >= 0,
+ )).cpu()
+
+ coords = coords[in_view, :]
+ if not isinstance(color, tuple):
+ color = color[in_view, :]
+
+ pixel_view[(*coords[..., [1, 0]].int().T.cpu(),)] = color
+
+ self.export_medial_surface_mesh = False
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..96a9658
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,6369 @@
+# This file is automatically @generated by Poetry and should not be changed by hand.
+
+[[package]]
+name = "absl-py"
+version = "1.4.0"
+description = "Abseil Python Common Libraries, see https://github.com/abseil/abseil-py."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "absl-py-1.4.0.tar.gz", hash = "sha256:d2c244d01048ba476e7c080bd2c6df5e141d211de80223460d5b3b8a2a58433d"},
+ {file = "absl_py-1.4.0-py3-none-any.whl", hash = "sha256:0d3fe606adfa4f7db64792dd4c7aee4ee0c38ab75dfd353b7a83ed3e957fcb47"},
+]
+
+[[package]]
+name = "aiofiles"
+version = "22.1.0"
+description = "File support for asyncio."
+category = "dev"
+optional = false
+python-versions = ">=3.7,<4.0"
+files = [
+ {file = "aiofiles-22.1.0-py3-none-any.whl", hash = "sha256:1142fa8e80dbae46bb6339573ad4c8c0841358f79c6eb50a493dceca14621bad"},
+ {file = "aiofiles-22.1.0.tar.gz", hash = "sha256:9107f1ca0b2a5553987a94a3c9959fe5b491fdf731389aa5b7b1bd0733e32de6"},
+]
+
+[[package]]
+name = "aiohttp"
+version = "3.8.4"
+description = "Async http client/server framework (asyncio)"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "aiohttp-3.8.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5ce45967538fb747370308d3145aa68a074bdecb4f3a300869590f725ced69c1"},
+ {file = "aiohttp-3.8.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b744c33b6f14ca26b7544e8d8aadff6b765a80ad6164fb1a430bbadd593dfb1a"},
+ {file = "aiohttp-3.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a45865451439eb320784918617ba54b7a377e3501fb70402ab84d38c2cd891b"},
+ {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a86d42d7cba1cec432d47ab13b6637bee393a10f664c425ea7b305d1301ca1a3"},
+ {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee3c36df21b5714d49fc4580247947aa64bcbe2939d1b77b4c8dcb8f6c9faecc"},
+ {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:176a64b24c0935869d5bbc4c96e82f89f643bcdf08ec947701b9dbb3c956b7dd"},
+ {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c844fd628851c0bc309f3c801b3a3d58ce430b2ce5b359cd918a5a76d0b20cb5"},
+ {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5393fb786a9e23e4799fec788e7e735de18052f83682ce2dfcabaf1c00c2c08e"},
+ {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e4b09863aae0dc965c3ef36500d891a3ff495a2ea9ae9171e4519963c12ceefd"},
+ {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:adfbc22e87365a6e564c804c58fc44ff7727deea782d175c33602737b7feadb6"},
+ {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:147ae376f14b55f4f3c2b118b95be50a369b89b38a971e80a17c3fd623f280c9"},
+ {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:eafb3e874816ebe2a92f5e155f17260034c8c341dad1df25672fb710627c6949"},
+ {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c6cc15d58053c76eacac5fa9152d7d84b8d67b3fde92709195cb984cfb3475ea"},
+ {file = "aiohttp-3.8.4-cp310-cp310-win32.whl", hash = "sha256:59f029a5f6e2d679296db7bee982bb3d20c088e52a2977e3175faf31d6fb75d1"},
+ {file = "aiohttp-3.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:fe7ba4a51f33ab275515f66b0a236bcde4fb5561498fe8f898d4e549b2e4509f"},
+ {file = "aiohttp-3.8.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d8ef1a630519a26d6760bc695842579cb09e373c5f227a21b67dc3eb16cfea4"},
+ {file = "aiohttp-3.8.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b3f2e06a512e94722886c0827bee9807c86a9f698fac6b3aee841fab49bbfb4"},
+ {file = "aiohttp-3.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a80464982d41b1fbfe3154e440ba4904b71c1a53e9cd584098cd41efdb188ef"},
+ {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b631e26df63e52f7cce0cce6507b7a7f1bc9b0c501fcde69742130b32e8782f"},
+ {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f43255086fe25e36fd5ed8f2ee47477408a73ef00e804cb2b5cba4bf2ac7f5e"},
+ {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d347a172f866cd1d93126d9b239fcbe682acb39b48ee0873c73c933dd23bd0f"},
+ {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3fec6a4cb5551721cdd70473eb009d90935b4063acc5f40905d40ecfea23e05"},
+ {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80a37fe8f7c1e6ce8f2d9c411676e4bc633a8462844e38f46156d07a7d401654"},
+ {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d1e6a862b76f34395a985b3cd39a0d949ca80a70b6ebdea37d3ab39ceea6698a"},
+ {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cd468460eefef601ece4428d3cf4562459157c0f6523db89365202c31b6daebb"},
+ {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:618c901dd3aad4ace71dfa0f5e82e88b46ef57e3239fc7027773cb6d4ed53531"},
+ {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:652b1bff4f15f6287550b4670546a2947f2a4575b6c6dff7760eafb22eacbf0b"},
+ {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80575ba9377c5171407a06d0196b2310b679dc752d02a1fcaa2bc20b235dbf24"},
+ {file = "aiohttp-3.8.4-cp311-cp311-win32.whl", hash = "sha256:bbcf1a76cf6f6dacf2c7f4d2ebd411438c275faa1dc0c68e46eb84eebd05dd7d"},
+ {file = "aiohttp-3.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:6e74dd54f7239fcffe07913ff8b964e28b712f09846e20de78676ce2a3dc0bfc"},
+ {file = "aiohttp-3.8.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:880e15bb6dad90549b43f796b391cfffd7af373f4646784795e20d92606b7a51"},
+ {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb96fa6b56bb536c42d6a4a87dfca570ff8e52de2d63cabebfd6fb67049c34b6"},
+ {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a6cadebe132e90cefa77e45f2d2f1a4b2ce5c6b1bfc1656c1ddafcfe4ba8131"},
+ {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f352b62b45dff37b55ddd7b9c0c8672c4dd2eb9c0f9c11d395075a84e2c40f75"},
+ {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ab43061a0c81198d88f39aaf90dae9a7744620978f7ef3e3708339b8ed2ef01"},
+ {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9cb1565a7ad52e096a6988e2ee0397f72fe056dadf75d17fa6b5aebaea05622"},
+ {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:1b3ea7edd2d24538959c1c1abf97c744d879d4e541d38305f9bd7d9b10c9ec41"},
+ {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:7c7837fe8037e96b6dd5cfcf47263c1620a9d332a87ec06a6ca4564e56bd0f36"},
+ {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:3b90467ebc3d9fa5b0f9b6489dfb2c304a1db7b9946fa92aa76a831b9d587e99"},
+ {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:cab9401de3ea52b4b4c6971db5fb5c999bd4260898af972bf23de1c6b5dd9d71"},
+ {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d1f9282c5f2b5e241034a009779e7b2a1aa045f667ff521e7948ea9b56e0c5ff"},
+ {file = "aiohttp-3.8.4-cp36-cp36m-win32.whl", hash = "sha256:5e14f25765a578a0a634d5f0cd1e2c3f53964553a00347998dfdf96b8137f777"},
+ {file = "aiohttp-3.8.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4c745b109057e7e5f1848c689ee4fb3a016c8d4d92da52b312f8a509f83aa05e"},
+ {file = "aiohttp-3.8.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:aede4df4eeb926c8fa70de46c340a1bc2c6079e1c40ccf7b0eae1313ffd33519"},
+ {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ddaae3f3d32fc2cb4c53fab020b69a05c8ab1f02e0e59665c6f7a0d3a5be54f"},
+ {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4eb3b82ca349cf6fadcdc7abcc8b3a50ab74a62e9113ab7a8ebc268aad35bb9"},
+ {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bcb89336efa095ea21b30f9e686763f2be4478f1b0a616969551982c4ee4c3b"},
+ {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c08e8ed6fa3d477e501ec9db169bfac8140e830aa372d77e4a43084d8dd91ab"},
+ {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6cd05ea06daca6ad6a4ca3ba7fe7dc5b5de063ff4daec6170ec0f9979f6c332"},
+ {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7a00a9ed8d6e725b55ef98b1b35c88013245f35f68b1b12c5cd4100dddac333"},
+ {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:de04b491d0e5007ee1b63a309956eaed959a49f5bb4e84b26c8f5d49de140fa9"},
+ {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:40653609b3bf50611356e6b6554e3a331f6879fa7116f3959b20e3528783e699"},
+ {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dbf3a08a06b3f433013c143ebd72c15cac33d2914b8ea4bea7ac2c23578815d6"},
+ {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854f422ac44af92bfe172d8e73229c270dc09b96535e8a548f99c84f82dde241"},
+ {file = "aiohttp-3.8.4-cp37-cp37m-win32.whl", hash = "sha256:aeb29c84bb53a84b1a81c6c09d24cf33bb8432cc5c39979021cc0f98c1292a1a"},
+ {file = "aiohttp-3.8.4-cp37-cp37m-win_amd64.whl", hash = "sha256:db3fc6120bce9f446d13b1b834ea5b15341ca9ff3f335e4a951a6ead31105480"},
+ {file = "aiohttp-3.8.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fabb87dd8850ef0f7fe2b366d44b77d7e6fa2ea87861ab3844da99291e81e60f"},
+ {file = "aiohttp-3.8.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91f6d540163f90bbaef9387e65f18f73ffd7c79f5225ac3d3f61df7b0d01ad15"},
+ {file = "aiohttp-3.8.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d265f09a75a79a788237d7f9054f929ced2e69eb0bb79de3798c468d8a90f945"},
+ {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d89efa095ca7d442a6d0cbc755f9e08190ba40069b235c9886a8763b03785da"},
+ {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dac314662f4e2aa5009977b652d9b8db7121b46c38f2073bfeed9f4049732cd"},
+ {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe11310ae1e4cd560035598c3f29d86cef39a83d244c7466f95c27ae04850f10"},
+ {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ddb2a2026c3f6a68c3998a6c47ab6795e4127315d2e35a09997da21865757f8"},
+ {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e75b89ac3bd27d2d043b234aa7b734c38ba1b0e43f07787130a0ecac1e12228a"},
+ {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6e601588f2b502c93c30cd5a45bfc665faaf37bbe835b7cfd461753068232074"},
+ {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a5d794d1ae64e7753e405ba58e08fcfa73e3fad93ef9b7e31112ef3c9a0efb52"},
+ {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a1f4689c9a1462f3df0a1f7e797791cd6b124ddbee2b570d34e7f38ade0e2c71"},
+ {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3032dcb1c35bc330134a5b8a5d4f68c1a87252dfc6e1262c65a7e30e62298275"},
+ {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8189c56eb0ddbb95bfadb8f60ea1b22fcfa659396ea36f6adcc521213cd7b44d"},
+ {file = "aiohttp-3.8.4-cp38-cp38-win32.whl", hash = "sha256:33587f26dcee66efb2fff3c177547bd0449ab7edf1b73a7f5dea1e38609a0c54"},
+ {file = "aiohttp-3.8.4-cp38-cp38-win_amd64.whl", hash = "sha256:e595432ac259af2d4630008bf638873d69346372d38255774c0e286951e8b79f"},
+ {file = "aiohttp-3.8.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5a7bdf9e57126dc345b683c3632e8ba317c31d2a41acd5800c10640387d193ed"},
+ {file = "aiohttp-3.8.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:22f6eab15b6db242499a16de87939a342f5a950ad0abaf1532038e2ce7d31567"},
+ {file = "aiohttp-3.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7235604476a76ef249bd64cb8274ed24ccf6995c4a8b51a237005ee7a57e8643"},
+ {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea9eb976ffdd79d0e893869cfe179a8f60f152d42cb64622fca418cd9b18dc2a"},
+ {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92c0cea74a2a81c4c76b62ea1cac163ecb20fb3ba3a75c909b9fa71b4ad493cf"},
+ {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:493f5bc2f8307286b7799c6d899d388bbaa7dfa6c4caf4f97ef7521b9cb13719"},
+ {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a63f03189a6fa7c900226e3ef5ba4d3bd047e18f445e69adbd65af433add5a2"},
+ {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10c8cefcff98fd9168cdd86c4da8b84baaa90bf2da2269c6161984e6737bf23e"},
+ {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bca5f24726e2919de94f047739d0a4fc01372801a3672708260546aa2601bf57"},
+ {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:03baa76b730e4e15a45f81dfe29a8d910314143414e528737f8589ec60cf7391"},
+ {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8c29c77cc57e40f84acef9bfb904373a4e89a4e8b74e71aa8075c021ec9078c2"},
+ {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:03543dcf98a6619254b409be2d22b51f21ec66272be4ebda7b04e6412e4b2e14"},
+ {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:17b79c2963db82086229012cff93ea55196ed31f6493bb1ccd2c62f1724324e4"},
+ {file = "aiohttp-3.8.4-cp39-cp39-win32.whl", hash = "sha256:34ce9f93a4a68d1272d26030655dd1b58ff727b3ed2a33d80ec433561b03d67a"},
+ {file = "aiohttp-3.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:41a86a69bb63bb2fc3dc9ad5ea9f10f1c9c8e282b471931be0268ddd09430b04"},
+ {file = "aiohttp-3.8.4.tar.gz", hash = "sha256:bf2e1a9162c1e441bf805a1fd166e249d574ca04e03b34f97e2928769e91ab5c"},
+]
+
+[package.dependencies]
+aiosignal = ">=1.1.2"
+async-timeout = ">=4.0.0a3,<5.0"
+attrs = ">=17.3.0"
+charset-normalizer = ">=2.0,<4.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"
+category = "main"
+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 = "aiosqlite"
+version = "0.18.0"
+description = "asyncio bridge to the standard sqlite3 module"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "aiosqlite-0.18.0-py3-none-any.whl", hash = "sha256:c3511b841e3a2c5614900ba1d179f366826857586f78abd75e7cbeb88e75a557"},
+ {file = "aiosqlite-0.18.0.tar.gz", hash = "sha256:faa843ef5fb08bafe9a9b3859012d3d9d6f77ce3637899de20606b7fc39aa213"},
+]
+
+[[package]]
+name = "ansiwrap"
+version = "0.8.4"
+description = "textwrap, but savvy to ANSI colors and styles"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "ansiwrap-0.8.4-py2.py3-none-any.whl", hash = "sha256:7b053567c88e1ad9eed030d3ac41b722125e4c1271c8a99ade797faff1f49fb1"},
+ {file = "ansiwrap-0.8.4.zip", hash = "sha256:ca0c740734cde59bf919f8ff2c386f74f9a369818cdc60efe94893d01ea8d9b7"},
+]
+
+[package.dependencies]
+textwrap3 = ">=0.9.2"
+
+[[package]]
+name = "anyio"
+version = "3.6.2"
+description = "High level compatibility layer for multiple asynchronous event loop implementations"
+category = "dev"
+optional = false
+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 = "appdirs"
+version = "1.4.4"
+description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
+ {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
+]
+
+[[package]]
+name = "appnope"
+version = "0.1.3"
+description = "Disable App Nap on macOS >= 10.9"
+category = "dev"
+optional = false
+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."
+category = "dev"
+optional = false
+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"
+category = "dev"
+optional = false
+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"
+category = "dev"
+optional = false
+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 = "astroid"
+version = "2.15.0"
+description = "An abstract syntax tree for Python with inference support."
+category = "dev"
+optional = false
+python-versions = ">=3.7.2"
+files = [
+ {file = "astroid-2.15.0-py3-none-any.whl", hash = "sha256:e3e4d0ffc2d15d954065579689c36aac57a339a4679a679579af6401db4d3fdb"},
+ {file = "astroid-2.15.0.tar.gz", hash = "sha256:525f126d5dc1b8b0b6ee398b33159105615d92dc4a17f2cd064125d57f6186fa"},
+]
+
+[package.dependencies]
+lazy-object-proxy = ">=1.4.0"
+typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""}
+wrapt = {version = ">=1.11,<2", markers = "python_version < \"3.11\""}
+
+[[package]]
+name = "asttokens"
+version = "2.2.1"
+description = "Annotate AST trees with source code positions"
+category = "dev"
+optional = false
+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 = "async-timeout"
+version = "4.0.2"
+description = "Timeout context manager for asyncio programs"
+category = "main"
+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.2.0"
+description = "Classes Without Boilerplate"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"},
+ {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"},
+]
+
+[package.extras]
+cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"]
+dev = ["attrs[docs,tests]"]
+docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"]
+tests = ["attrs[tests-no-zope]", "zope.interface"]
+tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"]
+
+[[package]]
+name = "autopep8"
+version = "1.6.0"
+description = "A tool that automatically formats Python code to conform to the PEP 8 style guide"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "autopep8-1.6.0-py2.py3-none-any.whl", hash = "sha256:ed77137193bbac52d029a52c59bec1b0629b5a186c495f1eb21b126ac466083f"},
+ {file = "autopep8-1.6.0.tar.gz", hash = "sha256:44f0932855039d2c15c4510d6df665e4730f2b8582704fa48f9c55bd3e17d979"},
+]
+
+[package.dependencies]
+pycodestyle = ">=2.8.0"
+toml = "*"
+
+[[package]]
+name = "babel"
+version = "2.12.1"
+description = "Internationalization utilities"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"},
+ {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"},
+]
+
+[[package]]
+name = "backcall"
+version = "0.2.0"
+description = "Specifications for callback functions passed in to an API"
+category = "dev"
+optional = false
+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 = "baron"
+version = "0.10.1"
+description = "Full Syntax Tree for python to make writing refactoring code a realist task"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "baron-0.10.1-py2.py3-none-any.whl", hash = "sha256:befb33f4b9e832c7cd1e3cf0eafa6dd3cb6ed4cb2544245147c019936f4e0a8a"},
+ {file = "baron-0.10.1.tar.gz", hash = "sha256:af822ad44d4eb425c8516df4239ac4fdba9fdb398ef77e4924cd7c9b4045bc2f"},
+]
+
+[package.dependencies]
+rply = "*"
+
+[[package]]
+name = "beautifulsoup4"
+version = "4.12.0"
+description = "Screen-scraping library"
+category = "dev"
+optional = false
+python-versions = ">=3.6.0"
+files = [
+ {file = "beautifulsoup4-4.12.0-py3-none-any.whl", hash = "sha256:2130a5ad7f513200fae61a17abb5e338ca980fa28c439c0571014bc0217e9591"},
+ {file = "beautifulsoup4-4.12.0.tar.gz", hash = "sha256:c5fceeaec29d09c84970e47c65f2f0efe57872f7cff494c9691a26ec0ff13234"},
+]
+
+[package.dependencies]
+soupsieve = ">1.2"
+
+[package.extras]
+html5lib = ["html5lib"]
+lxml = ["lxml"]
+
+[[package]]
+name = "bleach"
+version = "6.0.0"
+description = "An easy safelist-based HTML-sanitizing tool."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "bleach-6.0.0-py3-none-any.whl", hash = "sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4"},
+ {file = "bleach-6.0.0.tar.gz", hash = "sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414"},
+]
+
+[package.dependencies]
+six = ">=1.9.0"
+webencodings = "*"
+
+[package.extras]
+css = ["tinycss2 (>=1.1.0,<1.2)"]
+
+[[package]]
+name = "cachetools"
+version = "5.3.0"
+description = "Extensible memoizing collections and decorators"
+category = "dev"
+optional = false
+python-versions = "~=3.7"
+files = [
+ {file = "cachetools-5.3.0-py3-none-any.whl", hash = "sha256:429e1a1e845c008ea6c85aa35d4b98b65d6a9763eeef3e37e92728a12d1de9d4"},
+ {file = "cachetools-5.3.0.tar.gz", hash = "sha256:13dfddc7b8df938c21a940dfa6557ce6e94a2f1cdfa58eb90c805721d58f2c14"},
+]
+
+[[package]]
+name = "certifi"
+version = "2022.12.7"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "main"
+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."
+category = "dev"
+optional = false
+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 = "charset-normalizer"
+version = "3.1.0"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+category = "main"
+optional = false
+python-versions = ">=3.7.0"
+files = [
+ {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"},
+ {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"},
+]
+
+[[package]]
+name = "click"
+version = "8.1.3"
+description = "Composable command line interface toolkit"
+category = "main"
+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 = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+category = "main"
+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.3"
+description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "comm-0.1.3-py3-none-any.whl", hash = "sha256:16613c6211e20223f215fc6d3b266a247b6e2641bf4e0a3ad34cb1aff2aa3f37"},
+ {file = "comm-0.1.3.tar.gz", hash = "sha256:a61efa9daffcfbe66fd643ba966f846a624e4e6d6767eda9cf6e993aadaab93e"},
+]
+
+[package.dependencies]
+traitlets = ">=5.3"
+
+[package.extras]
+lint = ["black (>=22.6.0)", "mdformat (>0.7)", "mdformat-gfm (>=0.3.5)", "ruff (>=0.0.156)"]
+test = ["pytest"]
+typing = ["mypy (>=0.990)"]
+
+[[package]]
+name = "contourpy"
+version = "1.0.7"
+description = "Python library for calculating contours of 2D quadrilateral grids"
+category = "main"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "contourpy-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:95c3acddf921944f241b6773b767f1cbce71d03307270e2d769fd584d5d1092d"},
+ {file = "contourpy-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc1464c97579da9f3ab16763c32e5c5d5bb5fa1ec7ce509a4ca6108b61b84fab"},
+ {file = "contourpy-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8acf74b5d383414401926c1598ed77825cd530ac7b463ebc2e4f46638f56cce6"},
+ {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c71fdd8f1c0f84ffd58fca37d00ca4ebaa9e502fb49825484da075ac0b0b803"},
+ {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f99e9486bf1bb979d95d5cffed40689cb595abb2b841f2991fc894b3452290e8"},
+ {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87f4d8941a9564cda3f7fa6a6cd9b32ec575830780677932abdec7bcb61717b0"},
+ {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9e20e5a1908e18aaa60d9077a6d8753090e3f85ca25da6e25d30dc0a9e84c2c6"},
+ {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a877ada905f7d69b2a31796c4b66e31a8068b37aa9b78832d41c82fc3e056ddd"},
+ {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6381fa66866b0ea35e15d197fc06ac3840a9b2643a6475c8fff267db8b9f1e69"},
+ {file = "contourpy-1.0.7-cp310-cp310-win32.whl", hash = "sha256:3c184ad2433635f216645fdf0493011a4667e8d46b34082f5a3de702b6ec42e3"},
+ {file = "contourpy-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:3caea6365b13119626ee996711ab63e0c9d7496f65641f4459c60a009a1f3e80"},
+ {file = "contourpy-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed33433fc3820263a6368e532f19ddb4c5990855e4886088ad84fd7c4e561c71"},
+ {file = "contourpy-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:38e2e577f0f092b8e6774459317c05a69935a1755ecfb621c0a98f0e3c09c9a5"},
+ {file = "contourpy-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ae90d5a8590e5310c32a7630b4b8618cef7563cebf649011da80874d0aa8f414"},
+ {file = "contourpy-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130230b7e49825c98edf0b428b7aa1125503d91732735ef897786fe5452b1ec2"},
+ {file = "contourpy-1.0.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58569c491e7f7e874f11519ef46737cea1d6eda1b514e4eb5ac7dab6aa864d02"},
+ {file = "contourpy-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d43960d809c4c12508a60b66cb936e7ed57d51fb5e30b513934a4a23874fae"},
+ {file = "contourpy-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:152fd8f730c31fd67fe0ffebe1df38ab6a669403da93df218801a893645c6ccc"},
+ {file = "contourpy-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9056c5310eb1daa33fc234ef39ebfb8c8e2533f088bbf0bc7350f70a29bde1ac"},
+ {file = "contourpy-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a9d7587d2fdc820cc9177139b56795c39fb8560f540bba9ceea215f1f66e1566"},
+ {file = "contourpy-1.0.7-cp311-cp311-win32.whl", hash = "sha256:4ee3ee247f795a69e53cd91d927146fb16c4e803c7ac86c84104940c7d2cabf0"},
+ {file = "contourpy-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:5caeacc68642e5f19d707471890f037a13007feba8427eb7f2a60811a1fc1350"},
+ {file = "contourpy-1.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fd7dc0e6812b799a34f6d12fcb1000539098c249c8da54f3566c6a6461d0dbad"},
+ {file = "contourpy-1.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0f9d350b639db6c2c233d92c7f213d94d2e444d8e8fc5ca44c9706cf72193772"},
+ {file = "contourpy-1.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e96a08b62bb8de960d3a6afbc5ed8421bf1a2d9c85cc4ea73f4bc81b4910500f"},
+ {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:031154ed61f7328ad7f97662e48660a150ef84ee1bc8876b6472af88bf5a9b98"},
+ {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e9ebb4425fc1b658e13bace354c48a933b842d53c458f02c86f371cecbedecc"},
+ {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efb8f6d08ca7998cf59eaf50c9d60717f29a1a0a09caa46460d33b2924839dbd"},
+ {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6c180d89a28787e4b73b07e9b0e2dac7741261dbdca95f2b489c4f8f887dd810"},
+ {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b8d587cc39057d0afd4166083d289bdeff221ac6d3ee5046aef2d480dc4b503c"},
+ {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:769eef00437edf115e24d87f8926955f00f7704bede656ce605097584f9966dc"},
+ {file = "contourpy-1.0.7-cp38-cp38-win32.whl", hash = "sha256:62398c80ef57589bdbe1eb8537127321c1abcfdf8c5f14f479dbbe27d0322e66"},
+ {file = "contourpy-1.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:57119b0116e3f408acbdccf9eb6ef19d7fe7baf0d1e9aaa5381489bc1aa56556"},
+ {file = "contourpy-1.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30676ca45084ee61e9c3da589042c24a57592e375d4b138bd84d8709893a1ba4"},
+ {file = "contourpy-1.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e927b3868bd1e12acee7cc8f3747d815b4ab3e445a28d2e5373a7f4a6e76ba1"},
+ {file = "contourpy-1.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:366a0cf0fc079af5204801786ad7a1c007714ee3909e364dbac1729f5b0849e5"},
+ {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ba9bb365446a22411f0673abf6ee1fea3b2cf47b37533b970904880ceb72f3"},
+ {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:71b0bf0c30d432278793d2141362ac853859e87de0a7dee24a1cea35231f0d50"},
+ {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7281244c99fd7c6f27c1c6bfafba878517b0b62925a09b586d88ce750a016d2"},
+ {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b6d0f9e1d39dbfb3977f9dd79f156c86eb03e57a7face96f199e02b18e58d32a"},
+ {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7f6979d20ee5693a1057ab53e043adffa1e7418d734c1532e2d9e915b08d8ec2"},
+ {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5dd34c1ae752515318224cba7fc62b53130c45ac6a1040c8b7c1a223c46e8967"},
+ {file = "contourpy-1.0.7-cp39-cp39-win32.whl", hash = "sha256:c5210e5d5117e9aec8c47d9156d1d3835570dd909a899171b9535cb4a3f32693"},
+ {file = "contourpy-1.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:60835badb5ed5f4e194a6f21c09283dd6e007664a86101431bf870d9e86266c4"},
+ {file = "contourpy-1.0.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ce41676b3d0dd16dbcfabcc1dc46090aaf4688fd6e819ef343dbda5a57ef0161"},
+ {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a011cf354107b47c58ea932d13b04d93c6d1d69b8b6dce885e642531f847566"},
+ {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31a55dccc8426e71817e3fe09b37d6d48ae40aae4ecbc8c7ad59d6893569c436"},
+ {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69f8ff4db108815addd900a74df665e135dbbd6547a8a69333a68e1f6e368ac2"},
+ {file = "contourpy-1.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efe99298ba37e37787f6a2ea868265465410822f7bea163edcc1bd3903354ea9"},
+ {file = "contourpy-1.0.7-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a1e97b86f73715e8670ef45292d7cc033548266f07d54e2183ecb3c87598888f"},
+ {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc331c13902d0f50845099434cd936d49d7a2ca76cb654b39691974cb1e4812d"},
+ {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24847601071f740837aefb730e01bd169fbcaa610209779a78db7ebb6e6a7051"},
+ {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abf298af1e7ad44eeb93501e40eb5a67abbf93b5d90e468d01fc0c4451971afa"},
+ {file = "contourpy-1.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:64757f6460fc55d7e16ed4f1de193f362104285c667c112b50a804d482777edd"},
+ {file = "contourpy-1.0.7.tar.gz", hash = "sha256:d8165a088d31798b59e91117d1f5fc3df8168d8b48c4acc10fc0df0d0bdbcc5e"},
+]
+
+[package.dependencies]
+numpy = ">=1.16"
+
+[package.extras]
+bokeh = ["bokeh", "chromedriver", "selenium"]
+docs = ["furo", "sphinx-copybutton"]
+mypy = ["contourpy[bokeh]", "docutils-stubs", "mypy (==0.991)", "types-Pillow"]
+test = ["Pillow", "matplotlib", "pytest"]
+test-no-images = ["pytest"]
+
+[[package]]
+name = "cycler"
+version = "0.11.0"
+description = "Composable style cycles"
+category = "main"
+optional = false
+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 = "cython"
+version = "0.29.33"
+description = "The Cython compiler for writing C extensions for the Python language."
+category = "main"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+files = [
+ {file = "Cython-0.29.33-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:286cdfb193e23799e113b7bd5ac74f58da5e9a77c70e3b645b078836b896b165"},
+ {file = "Cython-0.29.33-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8507279a4f86ed8365b96603d5ad155888d4d01b72a9bbf0615880feda5a11d4"},
+ {file = "Cython-0.29.33-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5bf5ffd96957a595441cca2fc78470d93fdc40dfe5449881b812ea6045d7e9be"},
+ {file = "Cython-0.29.33-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d2019a7e54ba8b253f44411863b8f8c0b6cd623f7a92dc0ccb83892358c4283a"},
+ {file = "Cython-0.29.33-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:190e60b7505d3b9b60130bcc2251c01b9ef52603420829c19d3c3ede4ac2763a"},
+ {file = "Cython-0.29.33-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0168482495b75fea1c97a9641a95bac991f313e85f378003f9a4909fdeb3d454"},
+ {file = "Cython-0.29.33-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:090556e41f2b30427dd3a1628d3613177083f47567a30148b6b7b8c7a5862187"},
+ {file = "Cython-0.29.33-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:19c9913e9304bf97f1d2c357438895466f99aa2707d3c7a5e9de60c259e1ca1d"},
+ {file = "Cython-0.29.33-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:afc9b6ab20889676c76e700ae6967aa6886a7efe5b05ef6d5b744a6ca793cc43"},
+ {file = "Cython-0.29.33-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:49fb45b2bf12d6e2060bbd64506c06ac90e254f3a4bceb32c717f4964a1ae812"},
+ {file = "Cython-0.29.33-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:5430f38d3d01c4715ec2aef5c41e02a2441c1c3a0149359c7a498e4c605b8e6c"},
+ {file = "Cython-0.29.33-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c4d315443c7f4c61180b6c3ea9a9717ee7c901cc9db8d1d46fdf6556613840ed"},
+ {file = "Cython-0.29.33-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6b4e6481e3e7e4d345640fe2fdc6dc57c94369b467f3dc280949daa8e9fd13b9"},
+ {file = "Cython-0.29.33-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:060a2568ef80116a0a9dcaf3218a61c6007be0e0b77c5752c094ce5187a4d63c"},
+ {file = "Cython-0.29.33-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:b67ddd32eaa2932a66bf8121accc36a7b3078593805519b0f00040f2b10a6a52"},
+ {file = "Cython-0.29.33-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:1b507236ba3ca94170ce0a504dd03acf77307d4bfbc5a010a8031673f6b213a9"},
+ {file = "Cython-0.29.33-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:581efc0622a9be05714222f2b4ac96a5419de58d5949517282d8df38155c8b9d"},
+ {file = "Cython-0.29.33-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6b8bcbf8f1c3c46d6184be1e559e3a3fb8cdf27c6d507d8bc8ae04cfcbfd75f5"},
+ {file = "Cython-0.29.33-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1ca93bbe584aee92094fd4fb6acc5cb6500acf98d4f57cc59244f0a598b0fcf6"},
+ {file = "Cython-0.29.33-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:da490129e1e4ffaf3f88bfb46d338549a2150f60f809a63d385b83e00960d11a"},
+ {file = "Cython-0.29.33-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4cadf5250eda0c5cdaf4c3a29b52be3e0695f4a2bf1ccd49b638d239752ea513"},
+ {file = "Cython-0.29.33-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:bcb1a84fd2bd7885d572adc180e24fd8a7d4b0c104c144e33ccf84a1ab4eb2b8"},
+ {file = "Cython-0.29.33-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:d78147ad8a3417ae6b371bbc5bfc6512f6ad4ad3fb71f5eef42e136e4ed14970"},
+ {file = "Cython-0.29.33-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dd96b06b93c0e5fa4fc526c5be37c13a93e2fe7c372b5f358277ebe9e1620957"},
+ {file = "Cython-0.29.33-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:959f0092d58e7fa00fd3434f7ff32fb78be7c2fa9f8e0096326343159477fe45"},
+ {file = "Cython-0.29.33-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0455d5b92f461218bcf173a149a88b7396c3a109066274ccab5eff58db0eae32"},
+ {file = "Cython-0.29.33-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:a9b0b890656e9d18a18e1efe26ea3d2d0f3e525a07a2a853592b0afc56a15c89"},
+ {file = "Cython-0.29.33-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:b5e8ce3039ff64000d58cd45b3f6f83e13f032dde7f27bb1ab96070d9213550b"},
+ {file = "Cython-0.29.33-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:e8922fa3d7e76b7186bbd0810e170ca61f83661ab1b29dc75e88ff2327aaf49d"},
+ {file = "Cython-0.29.33-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f67b7306fd00d55f271009335cecadc506d144205c7891070aad889928d85750"},
+ {file = "Cython-0.29.33-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f271f90005064c49b47a93f456dc6cf0a21d21ef835bd33ac1e0db10ad51f84f"},
+ {file = "Cython-0.29.33-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d4457d417ffbb94abc42adcd63a03b24ff39cf090f3e9eca5e10cfb90766cbe3"},
+ {file = "Cython-0.29.33-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:0b53e017522feb8dcc2189cf1d2d344bab473c5bba5234390b5666d822992c7c"},
+ {file = "Cython-0.29.33-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:4f88c2dc0653eef6468848eb8022faf64115b39734f750a1c01a7ba7eb04d89f"},
+ {file = "Cython-0.29.33-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:1900d862a4a537d2125706740e9f3b016e80f7bbf7b54db6b3cc3d0bdf0f5c3a"},
+ {file = "Cython-0.29.33-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37bfca4f9f26361343d8c678f8178321e4ae5b919523eed05d2cd8ddbe6b06ec"},
+ {file = "Cython-0.29.33-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a9863f8238642c0b1ef8069d99da5ade03bfe2225a64b00c5ae006d95f142a73"},
+ {file = "Cython-0.29.33-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1dd503408924723b0bb10c0013b76e324eeee42db6deced9b02b648f1415d94c"},
+ {file = "Cython-0.29.33-py2.py3-none-any.whl", hash = "sha256:8b99252bde8ff51cd06a3fe4aeacd3af9b4ff4a4e6b701ac71bddc54f5da61d6"},
+ {file = "Cython-0.29.33.tar.gz", hash = "sha256:5040764c4a4d2ce964a395da24f0d1ae58144995dab92c6b96f44c3f4d72286a"},
+]
+
+[[package]]
+name = "debugpy"
+version = "1.6.6"
+description = "An implementation of the Debug Adapter Protocol for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "debugpy-1.6.6-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:0ea1011e94416e90fb3598cc3ef5e08b0a4dd6ce6b9b33ccd436c1dffc8cd664"},
+ {file = "debugpy-1.6.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dff595686178b0e75580c24d316aa45a8f4d56e2418063865c114eef651a982e"},
+ {file = "debugpy-1.6.6-cp310-cp310-win32.whl", hash = "sha256:87755e173fcf2ec45f584bb9d61aa7686bb665d861b81faa366d59808bbd3494"},
+ {file = "debugpy-1.6.6-cp310-cp310-win_amd64.whl", hash = "sha256:72687b62a54d9d9e3fb85e7a37ea67f0e803aaa31be700e61d2f3742a5683917"},
+ {file = "debugpy-1.6.6-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:78739f77c58048ec006e2b3eb2e0cd5a06d5f48c915e2fc7911a337354508110"},
+ {file = "debugpy-1.6.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23c29e40e39ad7d869d408ded414f6d46d82f8a93b5857ac3ac1e915893139ca"},
+ {file = "debugpy-1.6.6-cp37-cp37m-win32.whl", hash = "sha256:7aa7e103610e5867d19a7d069e02e72eb2b3045b124d051cfd1538f1d8832d1b"},
+ {file = "debugpy-1.6.6-cp37-cp37m-win_amd64.whl", hash = "sha256:f6383c29e796203a0bba74a250615ad262c4279d398e89d895a69d3069498305"},
+ {file = "debugpy-1.6.6-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:23363e6d2a04d726bbc1400bd4e9898d54419b36b2cdf7020e3e215e1dcd0f8e"},
+ {file = "debugpy-1.6.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b5d1b13d7c7bf5d7cf700e33c0b8ddb7baf030fcf502f76fc061ddd9405d16c"},
+ {file = "debugpy-1.6.6-cp38-cp38-win32.whl", hash = "sha256:70ab53918fd907a3ade01909b3ed783287ede362c80c75f41e79596d5ccacd32"},
+ {file = "debugpy-1.6.6-cp38-cp38-win_amd64.whl", hash = "sha256:c05349890804d846eca32ce0623ab66c06f8800db881af7a876dc073ac1c2225"},
+ {file = "debugpy-1.6.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a771739902b1ae22a120dbbb6bd91b2cae6696c0e318b5007c5348519a4211c6"},
+ {file = "debugpy-1.6.6-cp39-cp39-win32.whl", hash = "sha256:549ae0cb2d34fc09d1675f9b01942499751d174381b6082279cf19cdb3c47cbe"},
+ {file = "debugpy-1.6.6-cp39-cp39-win_amd64.whl", hash = "sha256:de4a045fbf388e120bb6ec66501458d3134f4729faed26ff95de52a754abddb1"},
+ {file = "debugpy-1.6.6-py2.py3-none-any.whl", hash = "sha256:be596b44448aac14eb3614248c91586e2bc1728e020e82ef3197189aae556115"},
+ {file = "debugpy-1.6.6.zip", hash = "sha256:b9c2130e1c632540fbf9c2c88341493797ddf58016e7cba02e311de9b0a96b67"},
+]
+
+[[package]]
+name = "decorator"
+version = "5.1.1"
+description = "Decorators for Humans"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
+ {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
+]
+
+[[package]]
+name = "defusedxml"
+version = "0.7.1"
+description = "XML bomb protection for Python stdlib modules"
+category = "dev"
+optional = false
+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"
+category = "dev"
+optional = false
+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 = "docstring-to-markdown"
+version = "0.12"
+description = "On the fly conversion of Python docstrings to markdown"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "docstring-to-markdown-0.12.tar.gz", hash = "sha256:40004224b412bd6f64c0f3b85bb357a41341afd66c4b4896709efa56827fb2bb"},
+ {file = "docstring_to_markdown-0.12-py3-none-any.whl", hash = "sha256:7df6311a887dccf9e770f51242ec002b19f0591994c4783be49d24cdc1df3737"},
+]
+
+[[package]]
+name = "entrypoints"
+version = "0.4"
+description = "Discover and load entry points from installed packages."
+category = "dev"
+optional = false
+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 = "executing"
+version = "1.2.0"
+description = "Get the currently executing AST node of a frame, and other information"
+category = "dev"
+optional = false
+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 = "faiss-cpu"
+version = "1.7.3"
+description = "A library for efficient similarity search and clustering of dense vectors."
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "faiss-cpu-1.7.3.tar.gz", hash = "sha256:cb71fe3f2934732d157d9d8cfb6ed2dd4020a0065571c84842ff6a3f0beab310"},
+ {file = "faiss_cpu-1.7.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:343f025e0846239d987d0c719772387ad685b74e5ef62b2e5616cabef9062729"},
+ {file = "faiss_cpu-1.7.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8b7b1cf693d7c24b5a633ff024717bd715fec501af4854357da0805b4899bcec"},
+ {file = "faiss_cpu-1.7.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c37e5fc0a266839844798a53dd42dd6afbee0c5905611f3f278297053fccbd7"},
+ {file = "faiss_cpu-1.7.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0628f7b0c6263ef4431995bb4f5f39833f999e96e6663935cbf0a1f2243dc4ac"},
+ {file = "faiss_cpu-1.7.3-cp310-cp310-win_amd64.whl", hash = "sha256:e22d1887c617156a673665c913ee82a30bfc1a3bc939ba8500b61328bce5a625"},
+ {file = "faiss_cpu-1.7.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6d411449a5f3c3abfcafadaac3190ab1ab206023fc9110da86649506dcbe8a27"},
+ {file = "faiss_cpu-1.7.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a10ea8622908f9f9ca4003e66da809dfad4af5c7d9fb7f582722d703bbc6c8bd"},
+ {file = "faiss_cpu-1.7.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5ced43ae058a62f63b12194ec9aa4c34066b0ea813ecbd936c65b7d52848c8"},
+ {file = "faiss_cpu-1.7.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3df6371012248dea8e9509949e2d2c6d73dea7c1bdaa4ba4563eb1c3cd8021a6"},
+ {file = "faiss_cpu-1.7.3-cp311-cp311-win_amd64.whl", hash = "sha256:8b6ff7854c3f46104718c6b34e81cd48c156d970dd87703c5122ca90217bb8dc"},
+ {file = "faiss_cpu-1.7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ab6314a8fbcce11dc3ecb6f48dda8c4ec274ed11c1f336f599f480bf0561442c"},
+ {file = "faiss_cpu-1.7.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:877c0bbf4c4a1806d88e091aba4c91ff3fa35c3ede5663b7fafc5b39247a369e"},
+ {file = "faiss_cpu-1.7.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6f199be10d30ecc6ed65350931006eca01b7bb8faa27d63069318eea0f6a0c1"},
+ {file = "faiss_cpu-1.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:1ca2b7cdbfdcc6a2e8fa75a09594916b50ec8260913ca48334dc3ce797179b5f"},
+ {file = "faiss_cpu-1.7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7b3f91856c19cfb8464178bab7e8ea94a391f6947b556be6754f9fc10b3c25fb"},
+ {file = "faiss_cpu-1.7.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7a238a0ef4d36c614d6f60e1ea308288b3920091638a3687f708de6071d007c1"},
+ {file = "faiss_cpu-1.7.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af53bee502c629eaaaf8b5ec648484a726be0fd2768ad4ef2bd4b829384b2682"},
+ {file = "faiss_cpu-1.7.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:441d1c305595d925138f2cde63dabe8c10ee05fc8ad66bf750e278a7e8c409bd"},
+ {file = "faiss_cpu-1.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:2766cc14b9004c1aae3b3943e693c3a9566eb1a25168b681981f9048276fe1e7"},
+ {file = "faiss_cpu-1.7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20ef191bb6164c8e794b11d20427568a75d15980b6d66732071e9aa57ea06e2d"},
+ {file = "faiss_cpu-1.7.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c57c293c4682066955626c2a2956be9a3b92594f69ed1a33abd72260a6911b69"},
+ {file = "faiss_cpu-1.7.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd128170446ff3c3e28d89e813d32cd04f17fa3025794778a01a0d81524275dc"},
+ {file = "faiss_cpu-1.7.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a14d832b5361ce9af21977eb1dcdebe23b9edcc12aad40316df7ca1bd86bc6b5"},
+ {file = "faiss_cpu-1.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:52df8895c5e59d1c9eda368a63790381a6f7fceddb22bed08f9c90a706d8a148"},
+]
+
+[[package]]
+name = "fastjsonschema"
+version = "2.16.3"
+description = "Fastest Python implementation of JSON schema"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "fastjsonschema-2.16.3-py3-none-any.whl", hash = "sha256:04fbecc94300436f628517b05741b7ea009506ce8f946d40996567c669318490"},
+ {file = "fastjsonschema-2.16.3.tar.gz", hash = "sha256:4a30d6315a68c253cfa8f963b9697246315aa3db89f98b97235e345dedfb0b8e"},
+]
+
+[package.extras]
+devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"]
+
+[[package]]
+name = "fix-my-functions"
+version = "0.1.3"
+description = ""
+category = "dev"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "fix-my-functions-0.1.3.tar.gz", hash = "sha256:3668905cf84f76f6e3f72059881456b47af4ae3763da0cd2a37200c87ad75adf"},
+ {file = "fix_my_functions-0.1.3-py3-none-any.whl", hash = "sha256:ace77267430050e979615c944f69adae6f80542e138e1be03deddfd4143ff9c9"},
+]
+
+[package.dependencies]
+colorama = ">=0.4.4"
+redbaron = ">=0.9.2"
+
+[[package]]
+name = "flake8"
+version = "6.0.0"
+description = "the modular source code checker: pep8 pyflakes and co"
+category = "dev"
+optional = false
+python-versions = ">=3.8.1"
+files = [
+ {file = "flake8-6.0.0-py2.py3-none-any.whl", hash = "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7"},
+ {file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"},
+]
+
+[package.dependencies]
+mccabe = ">=0.7.0,<0.8.0"
+pycodestyle = ">=2.10.0,<2.11.0"
+pyflakes = ">=3.0.0,<3.1.0"
+
+[[package]]
+name = "fonttools"
+version = "4.39.2"
+description = "Tools to manipulate font files"
+category = "main"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "fonttools-4.39.2-py3-none-any.whl", hash = "sha256:85245aa2fd4cf502a643c9a9a2b5a393703e150a6eaacc3e0e84bb448053f061"},
+ {file = "fonttools-4.39.2.zip", hash = "sha256:e2d9f10337c9e3b17f9bce17a60a16a885a7d23b59b7f45ce07ea643e5580439"},
+]
+
+[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 (>=15.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 (>=15.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"
+category = "dev"
+optional = false
+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 = "freetype-py"
+version = "2.3.0"
+description = "Freetype python bindings"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "freetype-py-2.3.0.zip", hash = "sha256:f9b64ce3272a5c358dcee824800a32d70997fb872a0965a557adca20fce7a5d0"},
+ {file = "freetype_py-2.3.0-py3-none-macosx_10_9_universal2.whl", hash = "sha256:ca7155de937af6f26bfd9f9089a6e9b01fa8f9d3040a3ddc0aeb3a53cf88f428"},
+ {file = "freetype_py-2.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccdb1616794a8ad48beaa9e29d3494e6643d24d8e925cc39263de21c062ea5a7"},
+ {file = "freetype_py-2.3.0-py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c8f17c3ac35dc7cc9571ac37a00a6daa428a1a6d0fe6926a77d16066865ed5ef"},
+ {file = "freetype_py-2.3.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:89cee8f4e7cf0a37b73a43a08c88703d84e3b9f9243fc665d8dc0b72a5d206a8"},
+ {file = "freetype_py-2.3.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:b95ccd52ff7e9bef34505f8af724cee114a3c3cc9cf13e0fd406fa0cc92b988a"},
+ {file = "freetype_py-2.3.0-py3-none-win_amd64.whl", hash = "sha256:3a552265b06c2cb3fa54f86ed6fcbf045d8dc8176f9475bedddf9a1b31f5402f"},
+]
+
+[[package]]
+name = "frozenlist"
+version = "1.3.3"
+description = "A list-like structure which implements collections.abc.MutableSequence"
+category = "main"
+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 = "2023.3.0"
+description = "File-system specification"
+category = "main"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "fsspec-2023.3.0-py3-none-any.whl", hash = "sha256:bf57215e19dbfa4fe7edae53040cc1deef825e3b1605cca9a8d2c2fadd2328a0"},
+ {file = "fsspec-2023.3.0.tar.gz", hash = "sha256:24e635549a590d74c6c18274ddd3ffab4753341753e923408b1904eaabafe04d"},
+]
+
+[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"]
+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 = "fvcore"
+version = "0.1.5.post20221221"
+description = "Collection of common code shared among different research projects in FAIR computer vision team"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "fvcore-0.1.5.post20221221.tar.gz", hash = "sha256:f2fb0bb90572ae651c11c78e20493ed19b2240550a7e4bbb2d6de87bdd037860"},
+]
+
+[package.dependencies]
+iopath = ">=0.1.7"
+numpy = "*"
+Pillow = "*"
+pyyaml = ">=5.1"
+tabulate = "*"
+termcolor = ">=1.1"
+tqdm = "*"
+yacs = ">=0.1.6"
+
+[package.extras]
+all = ["shapely"]
+
+[[package]]
+name = "geomloss"
+version = "0.2.4"
+description = "Geometric loss functions between point clouds, images and volumes."
+category = "main"
+optional = false
+python-versions = ">=3"
+files = [
+ {file = "geomloss-0.2.4-py3-none-any.whl", hash = "sha256:1e4b4a53d1798f7927f2a15f6fb533675d8f172d9de4f0eaf959eb120ab8c9f5"},
+ {file = "geomloss-0.2.4.tar.gz", hash = "sha256:3d6cc5a358b854429619fc180f1e7a3ab31a0b50742d7196042adf5134065dfa"},
+]
+
+[package.dependencies]
+numpy = "*"
+
+[package.extras]
+full = ["cmake (>=3.18)", "pykeops[full]"]
+
+[[package]]
+name = "google-auth"
+version = "2.16.2"
+description = "Google Authentication Library"
+category = "dev"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*"
+files = [
+ {file = "google-auth-2.16.2.tar.gz", hash = "sha256:07e14f34ec288e3f33e00e2e3cc40c8942aa5d4ceac06256a28cd8e786591420"},
+ {file = "google_auth-2.16.2-py2.py3-none-any.whl", hash = "sha256:2fef3cf94876d1a0e204afece58bb4d83fb57228aaa366c64045039fda6770a2"},
+]
+
+[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)"]
+requests = ["requests (>=2.20.0,<3.0.0dev)"]
+
+[[package]]
+name = "google-auth-oauthlib"
+version = "0.4.6"
+description = "Google Authentication Library"
+category = "dev"
+optional = false
+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 = "graphviz"
+version = "0.20.1"
+description = "Simple Python interface for Graphviz"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "graphviz-0.20.1-py3-none-any.whl", hash = "sha256:587c58a223b51611c0cf461132da386edd896a029524ca61a1462b880bf97977"},
+ {file = "graphviz-0.20.1.zip", hash = "sha256:8c58f14adaa3b947daf26c19bc1e98c4e0702cdc31cf99153e6f06904d492bf8"},
+]
+
+[package.extras]
+dev = ["flake8", "pep8-naming", "tox (>=3)", "twine", "wheel"]
+docs = ["sphinx (>=5)", "sphinx-autodoc-typehints", "sphinx-rtd-theme"]
+test = ["coverage", "mock (>=4)", "pytest (>=7)", "pytest-cov", "pytest-mock (>=3)"]
+
+[[package]]
+name = "grpcio"
+version = "1.51.3"
+description = "HTTP/2-based RPC framework"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "grpcio-1.51.3-cp310-cp310-linux_armv7l.whl", hash = "sha256:f601aaeae18dab81930fb8d4f916b0da21e89bb4b5f7367ef793f46b4a76b7b0"},
+ {file = "grpcio-1.51.3-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:eef0450a4b5ed11feab639bf3eb1b6e23d0efa9b911bf7b06fb60e14f5f8a585"},
+ {file = "grpcio-1.51.3-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:82b0ad8ac825d4bb31bff9f638557c045f4a6d824d84b21e893968286f88246b"},
+ {file = "grpcio-1.51.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3667c06e37d6cd461afdd51cefe6537702f3d1dc5ff4cac07e88d8b4795dc16f"},
+ {file = "grpcio-1.51.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3709048fe0aa23dda09b3e69849a12055790171dab9e399a72ea8f9dfbf9ac80"},
+ {file = "grpcio-1.51.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:200d69857f9910f7458b39b9bcf83ee4a180591b40146ba9e49314e3a7419313"},
+ {file = "grpcio-1.51.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cd9a5e68e79c5f031500e67793048a90209711e0854a9ddee8a3ce51728de4e5"},
+ {file = "grpcio-1.51.3-cp310-cp310-win32.whl", hash = "sha256:6604f614016127ae10969176bbf12eb0e03d2fb3d643f050b3b69e160d144fb4"},
+ {file = "grpcio-1.51.3-cp310-cp310-win_amd64.whl", hash = "sha256:e95c7ccd4c5807adef1602005513bf7c7d14e5a41daebcf9d8d30d8bf51b8f81"},
+ {file = "grpcio-1.51.3-cp311-cp311-linux_armv7l.whl", hash = "sha256:5e77ee138100f0bb55cbd147840f87ee6241dbd25f09ea7cd8afe7efff323449"},
+ {file = "grpcio-1.51.3-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:68a7514b754e38e8de9075f7bb4dee919919515ec68628c43a894027e40ddec4"},
+ {file = "grpcio-1.51.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c1b9f8afa62ff265d86a4747a2990ec5a96e4efce5d5888f245a682d66eca47"},
+ {file = "grpcio-1.51.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8de30f0b417744288cec65ec8cf84b8a57995cf7f1e84ccad2704d93f05d0aae"},
+ {file = "grpcio-1.51.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b69c7adc7ed60da1cb1b502853db61f453fc745f940cbcc25eb97c99965d8f41"},
+ {file = "grpcio-1.51.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d81528ffe0e973dc840ec73a4132fd18b8203ad129d7410155d951a0a7e4f5d0"},
+ {file = "grpcio-1.51.3-cp311-cp311-win32.whl", hash = "sha256:040eb421613b57c696063abde405916dd830203c184c9000fc8c3b3b3c950325"},
+ {file = "grpcio-1.51.3-cp311-cp311-win_amd64.whl", hash = "sha256:2a8e17286c4240137d933b8ca506465472248b4ce0fe46f3404459e708b65b68"},
+ {file = "grpcio-1.51.3-cp37-cp37m-linux_armv7l.whl", hash = "sha256:d5cd1389669a847555df54177b911d9ff6f17345b2a6f19388707b7a9f724c88"},
+ {file = "grpcio-1.51.3-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:be1bf35ce82cdbcac14e39d5102d8de4079a1c1a6a06b68e41fcd9ef64f9dd28"},
+ {file = "grpcio-1.51.3-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:5eed34994c095e2bf7194ffac7381c6068b057ef1e69f8f08db77771350a7566"},
+ {file = "grpcio-1.51.3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f9a7d88082b2a17ae7bd3c2354d13bab0453899e0851733f6afa6918373f476"},
+ {file = "grpcio-1.51.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c8abbc5f837111e7bd619612eedc223c290b0903b952ce0c7b00840ea70f14"},
+ {file = "grpcio-1.51.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:165b05af77e6aecb4210ae7663e25acf234ba78a7c1c157fa5f2efeb0d6ec53c"},
+ {file = "grpcio-1.51.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:54e36c2ee304ff15f2bfbdc43d2b56c63331c52d818c364e5b5214e5bc2ad9f6"},
+ {file = "grpcio-1.51.3-cp37-cp37m-win32.whl", hash = "sha256:cd0daac21d9ef5e033a5100c1d3aa055bbed28bfcf070b12d8058045c4e821b1"},
+ {file = "grpcio-1.51.3-cp37-cp37m-win_amd64.whl", hash = "sha256:2fdd6333ce96435408565a9dbbd446212cd5d62e4d26f6a3c0feb1e3c35f1cc8"},
+ {file = "grpcio-1.51.3-cp38-cp38-linux_armv7l.whl", hash = "sha256:54b0c29bdd9a3b1e1b61443ab152f060fc719f1c083127ab08d03fac5efd51be"},
+ {file = "grpcio-1.51.3-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:ffaaf7e93fcb437356b5a4b23bf36e8a3d0221399ff77fd057e4bc77776a24be"},
+ {file = "grpcio-1.51.3-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:eafbe7501a3268d05f2e450e1ddaffb950d842a8620c13ec328b501d25d2e2c3"},
+ {file = "grpcio-1.51.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:881ecb34feabf31c6b3b9bbbddd1a5b57e69f805041e5a2c6c562a28574f71c4"},
+ {file = "grpcio-1.51.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e860a3222139b41d430939bbec2ec9c3f6c740938bf7a04471a9a8caaa965a2e"},
+ {file = "grpcio-1.51.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:49ede0528e9dac7e8a9fe30b16c73b630ddd9a576bf4b675eb6b0c53ee5ca00f"},
+ {file = "grpcio-1.51.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6972b009638b40a448d10e1bc18e2223143b8a7aa20d7def0d78dd4af4126d12"},
+ {file = "grpcio-1.51.3-cp38-cp38-win32.whl", hash = "sha256:5694448256e3cdfe5bd358f1574a3f2f51afa20cc834713c4b9788d60b7cc646"},
+ {file = "grpcio-1.51.3-cp38-cp38-win_amd64.whl", hash = "sha256:3ea4341efe603b049e8c9a5f13c696ca37fcdf8a23ca35f650428ad3606381d9"},
+ {file = "grpcio-1.51.3-cp39-cp39-linux_armv7l.whl", hash = "sha256:6c677581ce129f5fa228b8f418cee10bd28dd449f3a544ea73c8ba590ee49d0b"},
+ {file = "grpcio-1.51.3-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:30e09b5e0531685e176f49679b6a3b190762cc225f4565e55a899f5e14b3aa62"},
+ {file = "grpcio-1.51.3-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:c831f31336e81243f85b6daff3e5e8a123302ce0ea1f2726ad752fd7a59f3aee"},
+ {file = "grpcio-1.51.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2cd2e4cefb724cab1ba2df4b7535a9980531b9ec51b4dbb5f137a1f3a3754ef0"},
+ {file = "grpcio-1.51.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7a0d0bf44438869d307f85a54f25a896ad6b4b0ca12370f76892ad732928d87"},
+ {file = "grpcio-1.51.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c02abd55409bfb293371554adf6a4401197ec2133dd97727c01180889014ba4d"},
+ {file = "grpcio-1.51.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2f8ff75e61e1227ba7a3f16b2eadbcc11d0a54096d52ab75a6b88cfbe56f55d1"},
+ {file = "grpcio-1.51.3-cp39-cp39-win32.whl", hash = "sha256:6c99a73a6260bdf844b2e5ddad02dcd530310f80e1fa72c300fa19c1c7496962"},
+ {file = "grpcio-1.51.3-cp39-cp39-win_amd64.whl", hash = "sha256:22bdfac4f7f27acdd4da359b5e7e1973dc74bf1ed406729b07d0759fde2f064b"},
+ {file = "grpcio-1.51.3.tar.gz", hash = "sha256:be7b2265b7527bb12109a7727581e274170766d5b3c9258d4e466f4872522d7a"},
+]
+
+[package.extras]
+protobuf = ["grpcio-tools (>=1.51.3)"]
+
+[[package]]
+name = "h5py"
+version = "3.8.0"
+description = "Read and write HDF5 files from Python"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "h5py-3.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:533d7dad466ddb7e3b30af274b630eb7c1a6e4ddf01d1c373a0334dc2152110a"},
+ {file = "h5py-3.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c873ba9fd4fa875ad62ce0e4891725e257a8fe7f5abdbc17e51a5d54819be55c"},
+ {file = "h5py-3.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98a240cd4c1bfd568aaa52ec42d263131a2582dab82d74d3d42a0d954cac12be"},
+ {file = "h5py-3.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3389b63222b1c7a158bb7fe69d11ca00066740ec5574596d47a2fe5317f563a"},
+ {file = "h5py-3.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:7f3350fc0a8407d668b13247861c2acd23f7f5fe7d060a3ad9b0820f5fcbcae0"},
+ {file = "h5py-3.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db03e3f2c716205fbdabb34d0848459840585225eb97b4f08998c743821ca323"},
+ {file = "h5py-3.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:36761693efbe53df179627a775476dcbc37727d6e920958277a7efbc18f1fb73"},
+ {file = "h5py-3.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a506fc223def428f4329e7e1f9fe1c8c593eab226e7c0942c8d75308ad49950"},
+ {file = "h5py-3.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33b15aae79e9147aebe1d0e54099cbcde8d65e3e227cd5b59e49b1272aa0e09d"},
+ {file = "h5py-3.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:9f6f6ffadd6bfa9b2c5b334805eb4b19ca0a5620433659d8f7fb86692c40a359"},
+ {file = "h5py-3.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8f55d9c6c84d7d09c79fb85979e97b81ec6071cc776a97eb6b96f8f6ec767323"},
+ {file = "h5py-3.8.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b685453e538b2b5934c58a644ac3f3b3d0cec1a01b6fb26d57388e9f9b674ad0"},
+ {file = "h5py-3.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:377865821fe80ad984d003723d6f8890bd54ceeb5981b43c0313b9df95411b30"},
+ {file = "h5py-3.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:0fef76e10b9216657fa37e7edff6d8be0709b25bd5066474c229b56cf0098df9"},
+ {file = "h5py-3.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:26ffc344ec9984d2cd3ca0265007299a8bac8d85c1ad48f4639d8d3aed2af171"},
+ {file = "h5py-3.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bacaa1c16810dd2b3e4417f8e730971b7c4d53d234de61fe4a918db78e80e1e4"},
+ {file = "h5py-3.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bae730580ae928de409d63cbe4fdca4c82c3ad2bed30511d19d34e995d63c77e"},
+ {file = "h5py-3.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f47f757d1b76f0ecb8aa0508ec8d1b390df67a8b67ee2515dc1b046f3a1596ea"},
+ {file = "h5py-3.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f891b17e3a3e974e93f9e34e7cca9f530806543571ce078998676a555837d91d"},
+ {file = "h5py-3.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:290e00fa2de74a10688d1bac98d5a9cdd43f14f58e562c580b5b3dfbd358ecae"},
+ {file = "h5py-3.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:03890b1c123d024fb0239a3279737d5432498c1901c354f8b10d8221d1d16235"},
+ {file = "h5py-3.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7865de06779b14d98068da387333ad9bf2756b5b579cc887fac169bc08f87c3"},
+ {file = "h5py-3.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49bc857635f935fa30e92e61ac1e87496df8f260a6945a3235e43a9890426866"},
+ {file = "h5py-3.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:5fd2252d1fc364ba0e93dd0b7089f4906b66805cb4e6aca7fa8874ac08649647"},
+ {file = "h5py-3.8.0.tar.gz", hash = "sha256:6fead82f0c4000cf38d53f9c030780d81bfa0220218aee13b90b7701c937d95f"},
+]
+
+[package.dependencies]
+numpy = ">=1.14.5"
+
+[[package]]
+name = "hdf5plugin"
+version = "4.1.1"
+description = "HDF5 Plugins for Windows, MacOS, and Linux"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "hdf5plugin-4.1.1-py3-none-macosx_10_9_universal2.whl", hash = "sha256:02fa82fbe5b6608b8c5371bd2a2dbe780abdafe30f2830e0ca658b6b59da6225"},
+ {file = "hdf5plugin-4.1.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a23463b1f70c3bfe3654299a397e97d074f1f8122d60fc4fc72c0005abf3900f"},
+ {file = "hdf5plugin-4.1.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a6906054df13341999c56f2301ae55f86499aebeec286612b6479453868d335"},
+ {file = "hdf5plugin-4.1.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16a8936f38890ce3a4dc10375adfb19dbf315e44cc7f099aafd1232e7ec75854"},
+ {file = "hdf5plugin-4.1.1-py3-none-win_amd64.whl", hash = "sha256:11c3fffe14aaf8ebd84729c2f110a3f2e4965b164484c0ef67005dc376f82b10"},
+ {file = "hdf5plugin-4.1.1.tar.gz", hash = "sha256:96a989679f1f38251e0dcae363180d382ba402f6c89aab73ca351a391ac23b36"},
+]
+
+[package.dependencies]
+h5py = "*"
+
+[package.extras]
+dev = ["sphinx", "sphinx-rtd-theme"]
+
+[[package]]
+name = "idna"
+version = "3.4"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
+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.26.1"
+description = "Library for reading and writing a wide range of image, video, scientific, and volumetric data formats."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "imageio-2.26.1-py3-none-any.whl", hash = "sha256:df56ade8a9b43476ce020be0202b09a3a4cc0c7a079f8fe5ee4b87105eff4237"},
+ {file = "imageio-2.26.1.tar.gz", hash = "sha256:7f7bc13254a311f298bc64d60c2690dd3460fd412532717e2c51715daed17fc5"},
+]
+
+[package.dependencies]
+numpy = "*"
+pillow = ">=8.3.2"
+
+[package.extras]
+all-plugins = ["astropy", "av", "imageio-ffmpeg", "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 (<6)"]
+ffmpeg = ["imageio-ffmpeg", "psutil"]
+fits = ["astropy"]
+full = ["astropy", "av", "black", "flake8", "fsspec[github]", "gdal", "imageio-ffmpeg", "invoke", "itk", "numpydoc", "psutil", "pydata-sphinx-theme", "pytest", "pytest-cov", "sphinx (<6)", "tifffile", "wheel"]
+gdal = ["gdal"]
+itk = ["itk"]
+linting = ["black", "flake8"]
+pyav = ["av"]
+test = ["fsspec[github]", "invoke", "pytest", "pytest-cov"]
+tifffile = ["tifffile"]
+
+[[package]]
+name = "imageio-ffmpeg"
+version = "0.4.8"
+description = "FFMPEG wrapper for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "imageio-ffmpeg-0.4.8.tar.gz", hash = "sha256:fdaa05ad10fe070b7fa8e5f615cb0d28f3b9b791d00af6d2a11e694158d10aa9"},
+ {file = "imageio_ffmpeg-0.4.8-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:dba439a303d65061aef17d2ee9324ecfa9c6b4752bd0953b309fdbb79b38451e"},
+ {file = "imageio_ffmpeg-0.4.8-py3-none-manylinux2010_x86_64.whl", hash = "sha256:7caa9ce9fc0d7e2f3160ce8cb70a115e5211e0f048e5c1509163d8f89d1080df"},
+ {file = "imageio_ffmpeg-0.4.8-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dd3ef9835df91570a1cbd9e36dfbc7d228fca42dbb11636e20df75d719de2949"},
+ {file = "imageio_ffmpeg-0.4.8-py3-none-win32.whl", hash = "sha256:0e2688120b3bdb367897450d07c1b1300e96a0bace03ba7de2eb8d738237ea9a"},
+ {file = "imageio_ffmpeg-0.4.8-py3-none-win_amd64.whl", hash = "sha256:120d70e6448617cad6213e47dee3a3310117c230f532dd614ed3059a78acf13a"},
+]
+
+[[package]]
+name = "importlib-metadata"
+version = "6.1.0"
+description = "Read metadata from Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "importlib_metadata-6.1.0-py3-none-any.whl", hash = "sha256:ff80f3b5394912eb1b108fcfd444dc78b7f1f3e16b16188054bd01cb9cb86f09"},
+ {file = "importlib_metadata-6.1.0.tar.gz", hash = "sha256:43ce9281e097583d758c2c708c4376371261a02c34682491a8e98352365aad20"},
+]
+
+[package.dependencies]
+zipp = ">=0.5"
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+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 = "iopath"
+version = "0.1.10"
+description = "A library for providing I/O abstraction."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "iopath-0.1.10.tar.gz", hash = "sha256:3311c16a4d9137223e20f141655759933e1eda24f8bff166af834af3c645ef01"},
+]
+
+[package.dependencies]
+portalocker = "*"
+tqdm = "*"
+typing_extensions = "*"
+
+[package.extras]
+aws = ["boto3"]
+
+[[package]]
+name = "ipykernel"
+version = "6.22.0"
+description = "IPython Kernel for Jupyter"
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "ipykernel-6.22.0-py3-none-any.whl", hash = "sha256:1ae6047c1277508933078163721bbb479c3e7292778a04b4bacf0874550977d6"},
+ {file = "ipykernel-6.22.0.tar.gz", hash = "sha256:302558b81f1bc22dc259fb2a0c5c7cf2f4c0bdb21b50484348f7bafe7fb71421"},
+]
+
+[package.dependencies]
+appnope = {version = "*", markers = "platform_system == \"Darwin\""}
+comm = ">=0.1.1"
+debugpy = ">=1.6.5"
+ipython = ">=7.23.1"
+jupyter-client = ">=6.1.12"
+jupyter-core = ">=4.12,<5.0.0 || >=5.1.0"
+matplotlib-inline = ">=0.1"
+nest-asyncio = "*"
+packaging = "*"
+psutil = "*"
+pyzmq = ">=20"
+tornado = ">=6.1"
+traitlets = ">=5.4.0"
+
+[package.extras]
+cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"]
+docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"]
+pyqt5 = ["pyqt5"]
+pyside6 = ["pyside6"]
+test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio", "pytest-cov", "pytest-timeout"]
+
+[[package]]
+name = "ipython"
+version = "8.11.0"
+description = "IPython: Productive Interactive Computing"
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "ipython-8.11.0-py3-none-any.whl", hash = "sha256:5b54478e459155a326bf5f42ee4f29df76258c0279c36f21d71ddb560f88b156"},
+ {file = "ipython-8.11.0.tar.gz", hash = "sha256:735cede4099dbc903ee540307b9171fbfef4aa75cfcacc5a273b2cda2f02be04"},
+]
+
+[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.30,<3.0.37 || >3.0.37,<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.21)", "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.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"]
+
+[[package]]
+name = "ipython-genutils"
+version = "0.2.0"
+description = "Vestigial utilities from IPython"
+category = "dev"
+optional = false
+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 = "ipywidgets"
+version = "8.0.5"
+description = "Jupyter interactive widgets"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "ipywidgets-8.0.5-py3-none-any.whl", hash = "sha256:a6e5c0392f86207fae304688a670afb26b2fd819592cfc0812777c2fdf22dbad"},
+ {file = "ipywidgets-8.0.5.tar.gz", hash = "sha256:89a1930b9ef255838571a2415cc4a15e824e4316b8f067805d1d03b98b6a8c5f"},
+]
+
+[package.dependencies]
+ipython = ">=6.1.0"
+jupyterlab-widgets = ">=3.0,<4.0"
+traitlets = ">=4.3.1"
+widgetsnbextension = ">=4.0,<5.0"
+
+[package.extras]
+test = ["ipykernel", "jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"]
+
+[[package]]
+name = "isoduration"
+version = "20.11.0"
+description = "Operations with ISO 8601 durations"
+category = "dev"
+optional = false
+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."
+category = "dev"
+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."
+category = "dev"
+optional = false
+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."
+category = "main"
+optional = false
+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.2.0"
+description = "Lightweight pipelining with Python functions"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "joblib-1.2.0-py3-none-any.whl", hash = "sha256:091138ed78f800342968c523bdde947e7a305b8594b910a0fea2ab83c3c6d385"},
+ {file = "joblib-1.2.0.tar.gz", hash = "sha256:e1cee4a79e4af22881164f218d4311f60074197fb707e082e803b61f6d137018"},
+]
+
+[[package]]
+name = "json5"
+version = "0.9.11"
+description = "A Python implementation of the JSON5 data format."
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "json5-0.9.11-py2.py3-none-any.whl", hash = "sha256:1aa54b80b5e507dfe31d12b7743a642e2ffa6f70bf73b8e3d7d1d5fba83d99bd"},
+ {file = "json5-0.9.11.tar.gz", hash = "sha256:4f1e196acc55b83985a51318489f345963c7ba84aa37607e49073066c562e99b"},
+]
+
+[package.extras]
+dev = ["hypothesis"]
+
+[[package]]
+name = "jsonpointer"
+version = "2.3"
+description = "Identify specific nodes in a JSON document (RFC 6901)"
+category = "dev"
+optional = false
+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"
+category = "dev"
+optional = false
+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\""}
+isoduration = {version = "*", optional = true, markers = "extra == \"format-nongpl\""}
+jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format-nongpl\""}
+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"
+version = "1.0.0"
+description = "Jupyter metapackage. Install all the Jupyter components in one go."
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "jupyter-1.0.0-py2.py3-none-any.whl", hash = "sha256:5b290f93b98ffbc21c0c7e749f054b3267782166d72fa5e3ed1ed4eaf34a2b78"},
+ {file = "jupyter-1.0.0.tar.gz", hash = "sha256:d9dc4b3318f310e34c82951ea5d6683f67bed7def4b259fafbfe4f1beb1d8e5f"},
+ {file = "jupyter-1.0.0.zip", hash = "sha256:3e1f86076bbb7c8c207829390305a2b1fe836d471ed54be66a3b8c41e7f46cc7"},
+]
+
+[package.dependencies]
+ipykernel = "*"
+ipywidgets = "*"
+jupyter-console = "*"
+nbconvert = "*"
+notebook = "*"
+qtconsole = "*"
+
+[[package]]
+name = "jupyter-client"
+version = "8.1.0"
+description = "Jupyter protocol implementation and client libraries"
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "jupyter_client-8.1.0-py3-none-any.whl", hash = "sha256:d5b8e739d7816944be50f81121a109788a3d92732ecf1ad1e4dadebc948818fe"},
+ {file = "jupyter_client-8.1.0.tar.gz", hash = "sha256:3fbab64100a0dcac7701b1e0f1a4412f1ccb45546ff2ad9bc4fcbe4e19804811"},
+]
+
+[package.dependencies]
+jupyter-core = ">=4.12,<5.0.0 || >=5.1.0"
+python-dateutil = ">=2.8.2"
+pyzmq = ">=23.0"
+tornado = ">=6.2"
+traitlets = ">=5.3"
+
+[package.extras]
+docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"]
+test = ["codecov", "coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"]
+
+[[package]]
+name = "jupyter-console"
+version = "6.6.3"
+description = "Jupyter terminal console"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jupyter_console-6.6.3-py3-none-any.whl", hash = "sha256:309d33409fcc92ffdad25f0bcdf9a4a9daa61b6f341177570fdac03de5352485"},
+ {file = "jupyter_console-6.6.3.tar.gz", hash = "sha256:566a4bf31c87adbfadf22cdf846e3069b59a71ed5da71d6ba4d8aaad14a53539"},
+]
+
+[package.dependencies]
+ipykernel = ">=6.14"
+ipython = "*"
+jupyter-client = ">=7.0.0"
+jupyter-core = ">=4.12,<5.0.0 || >=5.1.0"
+prompt-toolkit = ">=3.0.30"
+pygments = "*"
+pyzmq = ">=17"
+traitlets = ">=5.4"
+
+[package.extras]
+test = ["flaky", "pexpect", "pytest"]
+
+[[package]]
+name = "jupyter-contrib-core"
+version = "0.4.2"
+description = "Common utilities for jupyter-contrib projects."
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "jupyter_contrib_core-0.4.2.tar.gz", hash = "sha256:1887212f3ca9d4487d624c0705c20dfdf03d5a0b9ea2557d3aaeeb4c38bdcabb"},
+]
+
+[package.dependencies]
+jupyter_core = "*"
+notebook = ">=4.0"
+setuptools = "*"
+tornado = "*"
+traitlets = "*"
+
+[package.extras]
+testing-utils = ["mock", "nose"]
+
+[[package]]
+name = "jupyter-contrib-nbextensions"
+version = "0.7.0"
+description = "A collection of Jupyter nbextensions."
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "jupyter_contrib_nbextensions-0.7.0.tar.gz", hash = "sha256:06e33f005885eb92f89cbe82711e921278201298d08ab0d886d1ba09e8c3e9ca"},
+]
+
+[package.dependencies]
+ipython_genutils = "*"
+jupyter_contrib_core = ">=0.3.3"
+jupyter_core = "*"
+jupyter_highlight_selected_word = ">=0.1.1"
+jupyter_nbextensions_configurator = ">=0.4.0"
+lxml = "*"
+nbconvert = ">=6.0"
+notebook = ">=6.0"
+tornado = "*"
+traitlets = ">=4.1"
+
+[package.extras]
+test = ["mock", "nbformat", "nose", "pip", "requests"]
+
+[[package]]
+name = "jupyter-core"
+version = "5.3.0"
+description = "Jupyter core package. A base package on which Jupyter projects rely."
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "jupyter_core-5.3.0-py3-none-any.whl", hash = "sha256:d4201af84559bc8c70cead287e1ab94aeef3c512848dde077b7684b54d67730d"},
+ {file = "jupyter_core-5.3.0.tar.gz", hash = "sha256:6db75be0c83edbf1b7c9f91ec266a9a24ef945da630f3120e1a0046dc13713fc"},
+]
+
+[package.dependencies]
+platformdirs = ">=2.5"
+pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""}
+traitlets = ">=5.3"
+
+[package.extras]
+docs = ["myst-parser", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"]
+test = ["ipykernel", "pre-commit", "pytest", "pytest-cov", "pytest-timeout"]
+
+[[package]]
+name = "jupyter-events"
+version = "0.6.3"
+description = "Jupyter Event System library"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jupyter_events-0.6.3-py3-none-any.whl", hash = "sha256:57a2749f87ba387cd1bfd9b22a0875b889237dbf2edc2121ebb22bde47036c17"},
+ {file = "jupyter_events-0.6.3.tar.gz", hash = "sha256:9a6e9995f75d1b7146b436ea24d696ce3a35bfa8bfe45e0c33c334c79464d0b3"},
+]
+
+[package.dependencies]
+jsonschema = {version = ">=3.2.0", extras = ["format-nongpl"]}
+python-json-logger = ">=2.0.4"
+pyyaml = ">=5.3"
+rfc3339-validator = "*"
+rfc3986-validator = ">=0.1.1"
+traitlets = ">=5.3"
+
+[package.extras]
+cli = ["click", "rich"]
+docs = ["jupyterlite-sphinx", "myst-parser", "pydata-sphinx-theme", "sphinxcontrib-spelling"]
+test = ["click", "coverage", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.19.0)", "pytest-console-scripts", "pytest-cov", "rich"]
+
+[[package]]
+name = "jupyter-highlight-selected-word"
+version = "0.2.0"
+description = "Jupyter notebook extension that enables highlighting every instance of the current word in the notebook."
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "jupyter_highlight_selected_word-0.2.0-py2.py3-none-any.whl", hash = "sha256:9545dfa9cb057eebe3a5795604dcd3a5294ea18637e553f61a0b67c1b5903c58"},
+ {file = "jupyter_highlight_selected_word-0.2.0.tar.gz", hash = "sha256:9fa740424859a807950ca08d2bfd28a35154cd32dd6d50ac4e0950022adc0e7b"},
+]
+
+[[package]]
+name = "jupyter-nbextensions-configurator"
+version = "0.6.1"
+description = "jupyter serverextension providing configuration interfaces for nbextensions."
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "jupyter_nbextensions_configurator-0.6.1.tar.gz", hash = "sha256:4b9e1270ccc1f8e0a421efb8979a737f586813023a4855b9453f61c3ca599b82"},
+]
+
+[package.dependencies]
+jupyter_contrib_core = ">=0.3.3"
+jupyter_core = "*"
+notebook = ">=6.0"
+pyyaml = "*"
+tornado = "*"
+traitlets = "*"
+
+[package.extras]
+test = ["jupyter_contrib_core[testing-utils]", "mock", "nose", "requests", "selenium"]
+
+[[package]]
+name = "jupyter-server"
+version = "2.5.0"
+description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications."
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "jupyter_server-2.5.0-py3-none-any.whl", hash = "sha256:e6bc1e9e96d7c55b9ce9699ff6cb9a910581fe7349e27c40389acb67632e24c0"},
+ {file = "jupyter_server-2.5.0.tar.gz", hash = "sha256:9fde612791f716fd34d610cd939704a9639643744751ba66e7ee8fdc9cead07e"},
+]
+
+[package.dependencies]
+anyio = ">=3.1.0"
+argon2-cffi = "*"
+jinja2 = "*"
+jupyter-client = ">=7.4.4"
+jupyter-core = ">=4.12,<5.0.0 || >=5.1.0"
+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", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-openapi", "sphinxcontrib-spelling", "sphinxemoji", "tornado", "typing-extensions"]
+test = ["ipykernel", "pre-commit", "pytest (>=7.0)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.4)", "pytest-timeout", "requests"]
+
+[[package]]
+name = "jupyter-server-fileid"
+version = "0.8.0"
+description = ""
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jupyter_server_fileid-0.8.0-py3-none-any.whl", hash = "sha256:6092ef114eddccf6cba69c0f0feb612c2f476f2e9467828809edb854c18806bb"},
+ {file = "jupyter_server_fileid-0.8.0.tar.gz", hash = "sha256:1e0816d0857f490fadea11348570f0cba03f70f315c9842225aecfa45882b6af"},
+]
+
+[package.dependencies]
+jupyter-events = ">=0.5.0"
+jupyter-server = ">=1.15,<3"
+
+[package.extras]
+cli = ["click"]
+test = ["jupyter-server[test] (>=1.15,<3)", "pytest", "pytest-cov"]
+
+[[package]]
+name = "jupyter-server-terminals"
+version = "0.4.4"
+description = "A Jupyter Server Extension Providing Terminals."
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "jupyter_server_terminals-0.4.4-py3-none-any.whl", hash = "sha256:75779164661cec02a8758a5311e18bb8eb70c4e86c6b699403100f1585a12a36"},
+ {file = "jupyter_server_terminals-0.4.4.tar.gz", hash = "sha256:57ab779797c25a7ba68e97bcfb5d7740f2b5e8a83b5e8102b10438041a7eac5d"},
+]
+
+[package.dependencies]
+pywinpty = {version = ">=2.0.3", markers = "os_name == \"nt\""}
+terminado = ">=0.8.3"
+
+[package.extras]
+docs = ["jinja2", "jupyter-server", "mistune (<3.0)", "myst-parser", "nbformat", "packaging", "pydata-sphinx-theme", "sphinxcontrib-github-alt", "sphinxcontrib-openapi", "sphinxcontrib-spelling", "sphinxemoji", "tornado"]
+test = ["coverage", "jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-cov", "pytest-jupyter[server] (>=0.5.3)", "pytest-timeout"]
+
+[[package]]
+name = "jupyter-server-ydoc"
+version = "0.8.0"
+description = "A Jupyter Server Extension Providing Y Documents."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jupyter_server_ydoc-0.8.0-py3-none-any.whl", hash = "sha256:969a3a1a77ed4e99487d60a74048dc9fa7d3b0dcd32e60885d835bbf7ba7be11"},
+ {file = "jupyter_server_ydoc-0.8.0.tar.gz", hash = "sha256:a6fe125091792d16c962cc3720c950c2b87fcc8c3ecf0c54c84e9a20b814526c"},
+]
+
+[package.dependencies]
+jupyter-server-fileid = ">=0.6.0,<1"
+jupyter-ydoc = ">=0.2.0,<0.4.0"
+ypy-websocket = ">=0.8.2,<0.9.0"
+
+[package.extras]
+test = ["coverage", "jupyter-server[test] (>=2.0.0a0)", "pytest (>=7.0)", "pytest-cov", "pytest-timeout", "pytest-tornasync"]
+
+[[package]]
+name = "jupyter-ydoc"
+version = "0.2.3"
+description = "Document structures for collaborative editing using Ypy"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jupyter_ydoc-0.2.3-py3-none-any.whl", hash = "sha256:3ac51abfe378c6aeb62a449e8f0241bede1205f0199b0d27429140cbba950f79"},
+ {file = "jupyter_ydoc-0.2.3.tar.gz", hash = "sha256:98db7785215873c64d7dfcb1b741f41df11994c4b3d7e2957e004b392d6f11ea"},
+]
+
+[package.dependencies]
+y-py = ">=0.5.3,<0.6.0"
+
+[package.extras]
+dev = ["click", "jupyter-releaser"]
+test = ["pre-commit", "pytest", "pytest-asyncio", "websockets (>=10.0)", "ypy-websocket (>=0.3.1,<0.4.0)"]
+
+[[package]]
+name = "jupyterlab"
+version = "3.6.2"
+description = "JupyterLab computational environment"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jupyterlab-3.6.2-py3-none-any.whl", hash = "sha256:6c87c5910f886a14009352bc63f640961b206f73ed650dcf94d65f9dfcb30f95"},
+ {file = "jupyterlab-3.6.2.tar.gz", hash = "sha256:e55bc40c36c2a52b76cf301138507a5488eb769137dd39d9f31a6259a00c6b03"},
+]
+
+[package.dependencies]
+ipython = "*"
+jinja2 = ">=2.1"
+jupyter-core = "*"
+jupyter-server = ">=1.16.0,<3"
+jupyter-server-ydoc = ">=0.8.0,<0.9.0"
+jupyter-ydoc = ">=0.2.3,<0.3.0"
+jupyterlab-server = ">=2.19,<3.0"
+nbclassic = "*"
+notebook = "<7"
+packaging = "*"
+tomli = {version = "*", markers = "python_version < \"3.11\""}
+tornado = ">=6.1.0"
+
+[package.extras]
+test = ["check-manifest", "coverage", "jupyterlab-server[test]", "pre-commit", "pytest (>=6.0)", "pytest-check-links (>=0.5)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "requests", "requests-cache", "virtualenv"]
+
+[[package]]
+name = "jupyterlab-pygments"
+version = "0.2.2"
+description = "Pygments theme using JupyterLab CSS variables"
+category = "dev"
+optional = false
+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 = "jupyterlab-server"
+version = "2.21.0"
+description = "A set of server components for JupyterLab and JupyterLab like applications."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jupyterlab_server-2.21.0-py3-none-any.whl", hash = "sha256:ff1e7a81deb2dcb433215d469000988590fd5a5733574aa2698d643a6c9b3ace"},
+ {file = "jupyterlab_server-2.21.0.tar.gz", hash = "sha256:b4f5b48eaae1be83e2fd6fb77ac49d9b639be4ca4bd2e05b5368d29632a93725"},
+]
+
+[package.dependencies]
+babel = ">=2.10"
+jinja2 = ">=3.0.3"
+json5 = ">=0.9.0"
+jsonschema = ">=4.17.3"
+jupyter-server = ">=1.21,<3"
+packaging = ">=21.3"
+requests = ">=2.28"
+
+[package.extras]
+docs = ["autodoc-traits", "docutils (<0.20)", "jinja2 (<3.2.0)", "mistune (<3)", "myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-copybutton", "sphinxcontrib-openapi"]
+openapi = ["openapi-core (>=0.16.1,<0.17.0)", "ruamel-yaml"]
+test = ["codecov", "hatch", "ipykernel", "jupyterlab-server[openapi]", "openapi-spec-validator (>=0.5.1,<0.6.0)", "pytest (>=7.0)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter[server] (>=0.6.2)", "pytest-timeout", "requests-mock", "sphinxcontrib-spelling", "strict-rfc3339", "werkzeug"]
+
+[[package]]
+name = "jupyterlab-widgets"
+version = "3.0.6"
+description = "Jupyter interactive widgets for JupyterLab"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jupyterlab_widgets-3.0.6-py3-none-any.whl", hash = "sha256:e95d08adf4f9c37a57da5fff8a65d00480199885fd2ecd2583fd9560b594b4e9"},
+ {file = "jupyterlab_widgets-3.0.6.tar.gz", hash = "sha256:a464d68a7b9ebabdc135196389381412a39503d89302be0867d0ff3b2428ebb8"},
+]
+
+[[package]]
+name = "jupyterthemes"
+version = "0.20.0"
+description = "Select and install a Jupyter notebook theme"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "jupyterthemes-0.20.0-py2.py3-none-any.whl", hash = "sha256:4bd42fc88a06e3afabbe70c2ee25e6467147512993a3cbd9bec57ae3fd2e2fb1"},
+ {file = "jupyterthemes-0.20.0.tar.gz", hash = "sha256:2a8ebc0c84b212ab99b9f1757fc0582a3f53930d3a75b2492d91a7c8b36ab41e"},
+]
+
+[package.dependencies]
+ipython = ">=5.4.1"
+jupyter-core = "*"
+lesscpy = ">=0.11.2"
+matplotlib = ">=1.4.3"
+notebook = ">=5.6.0"
+
+[[package]]
+name = "keopscore"
+version = "2.1.1"
+description = "keopscore is the KeOps meta programming engine. This python module should be used through a binder (e.g. pykeops or rkeops)"
+category = "main"
+optional = false
+python-versions = ">=3"
+files = [
+ {file = "keopscore-2.1.1.tar.gz", hash = "sha256:07b4d254a28a9d4a43153663856677263dd7112912efacbad83c2a76ea0836f0"},
+]
+
+[[package]]
+name = "kiwisolver"
+version = "1.4.4"
+description = "A fast implementation of the Cassowary constraint solver"
+category = "main"
+optional = false
+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 = "lazy-object-proxy"
+version = "1.9.0"
+description = "A fast and thorough lazy object proxy."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"},
+ {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"},
+ {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"},
+ {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd"},
+ {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701"},
+ {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46"},
+ {file = "lazy_object_proxy-1.9.0-cp310-cp310-win32.whl", hash = "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455"},
+ {file = "lazy_object_proxy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e"},
+ {file = "lazy_object_proxy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07"},
+ {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a"},
+ {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"},
+ {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4"},
+ {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9"},
+ {file = "lazy_object_proxy-1.9.0-cp311-cp311-win32.whl", hash = "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586"},
+ {file = "lazy_object_proxy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb"},
+ {file = "lazy_object_proxy-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e"},
+ {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8"},
+ {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2"},
+ {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8"},
+ {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda"},
+ {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win32.whl", hash = "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734"},
+ {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671"},
+ {file = "lazy_object_proxy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63"},
+ {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171"},
+ {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be"},
+ {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30"},
+ {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11"},
+ {file = "lazy_object_proxy-1.9.0-cp38-cp38-win32.whl", hash = "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82"},
+ {file = "lazy_object_proxy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b"},
+ {file = "lazy_object_proxy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b"},
+ {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4"},
+ {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006"},
+ {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494"},
+ {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382"},
+ {file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"},
+ {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"},
+]
+
+[[package]]
+name = "lesscpy"
+version = "0.15.1"
+description = "Python LESS compiler"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "lesscpy-0.15.1-py2.py3-none-any.whl", hash = "sha256:8d26e58ed4812b345c2896daea435a28cb3182f87ae3391157085255d4c37dff"},
+ {file = "lesscpy-0.15.1.tar.gz", hash = "sha256:1045d17a98f688646ca758dff254e6e9c03745648e051a081b0395c3b77c824c"},
+]
+
+[package.dependencies]
+ply = "*"
+
+[[package]]
+name = "lightning-utilities"
+version = "0.8.0"
+description = "PyTorch Lightning Sample project."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "lightning-utilities-0.8.0.tar.gz", hash = "sha256:8e5d95c7c57f026cdfed7c154303e88c93a7a5e868c9944cb02cf71f1db29720"},
+ {file = "lightning_utilities-0.8.0-py3-none-any.whl", hash = "sha256:22aa107b51c8f50ccef54d08885eb370903eb04148cddb2891b9c65c59de2a6e"},
+]
+
+[package.dependencies]
+packaging = ">=17.1"
+typing-extensions = "*"
+
+[package.extras]
+cli = ["fire"]
+docs = ["sphinx (>=4.0,<5.0)"]
+test = ["coverage (==6.5.0)"]
+typing = ["mypy (>=1.0.0)"]
+
+[[package]]
+name = "llvmlite"
+version = "0.39.1"
+description = "lightweight wrapper around basic LLVM functionality"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "llvmlite-0.39.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6717c7a6e93c9d2c3d07c07113ec80ae24af45cde536b34363d4bcd9188091d9"},
+ {file = "llvmlite-0.39.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ddab526c5a2c4ccb8c9ec4821fcea7606933dc53f510e2a6eebb45a418d3488a"},
+ {file = "llvmlite-0.39.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3f331a323d0f0ada6b10d60182ef06c20a2f01be21699999d204c5750ffd0b4"},
+ {file = "llvmlite-0.39.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c00ff204afa721b0bb9835b5bf1ba7fba210eefcec5552a9e05a63219ba0dc"},
+ {file = "llvmlite-0.39.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16f56eb1eec3cda3a5c526bc3f63594fc24e0c8d219375afeb336f289764c6c7"},
+ {file = "llvmlite-0.39.1-cp310-cp310-win32.whl", hash = "sha256:d0bfd18c324549c0fec2c5dc610fd024689de6f27c6cc67e4e24a07541d6e49b"},
+ {file = "llvmlite-0.39.1-cp310-cp310-win_amd64.whl", hash = "sha256:7ebf1eb9badc2a397d4f6a6c8717447c81ac011db00064a00408bc83c923c0e4"},
+ {file = "llvmlite-0.39.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6546bed4e02a1c3d53a22a0bced254b3b6894693318b16c16c8e43e29d6befb6"},
+ {file = "llvmlite-0.39.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1578f5000fdce513712e99543c50e93758a954297575610f48cb1fd71b27c08a"},
+ {file = "llvmlite-0.39.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3803f11ad5f6f6c3d2b545a303d68d9fabb1d50e06a8d6418e6fcd2d0df00959"},
+ {file = "llvmlite-0.39.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50aea09a2b933dab7c9df92361b1844ad3145bfb8dd2deb9cd8b8917d59306fb"},
+ {file = "llvmlite-0.39.1-cp37-cp37m-win32.whl", hash = "sha256:b1a0bbdb274fb683f993198775b957d29a6f07b45d184c571ef2a721ce4388cf"},
+ {file = "llvmlite-0.39.1-cp37-cp37m-win_amd64.whl", hash = "sha256:e172c73fccf7d6db4bd6f7de963dedded900d1a5c6778733241d878ba613980e"},
+ {file = "llvmlite-0.39.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e31f4b799d530255aaf0566e3da2df5bfc35d3cd9d6d5a3dcc251663656c27b1"},
+ {file = "llvmlite-0.39.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:62c0ea22e0b9dffb020601bb65cb11dd967a095a488be73f07d8867f4e327ca5"},
+ {file = "llvmlite-0.39.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ffc84ade195abd4abcf0bd3b827b9140ae9ef90999429b9ea84d5df69c9058c"},
+ {file = "llvmlite-0.39.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0f158e4708dda6367d21cf15afc58de4ebce979c7a1aa2f6b977aae737e2a54"},
+ {file = "llvmlite-0.39.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22d36591cd5d02038912321d9ab8e4668e53ae2211da5523f454e992b5e13c36"},
+ {file = "llvmlite-0.39.1-cp38-cp38-win32.whl", hash = "sha256:4c6ebace910410daf0bebda09c1859504fc2f33d122e9a971c4c349c89cca630"},
+ {file = "llvmlite-0.39.1-cp38-cp38-win_amd64.whl", hash = "sha256:fb62fc7016b592435d3e3a8f680e3ea8897c3c9e62e6e6cc58011e7a4801439e"},
+ {file = "llvmlite-0.39.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa9b26939ae553bf30a9f5c4c754db0fb2d2677327f2511e674aa2f5df941789"},
+ {file = "llvmlite-0.39.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e4f212c018db951da3e1dc25c2651abc688221934739721f2dad5ff1dd5f90e7"},
+ {file = "llvmlite-0.39.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39dc2160aed36e989610fc403487f11b8764b6650017ff367e45384dff88ffbf"},
+ {file = "llvmlite-0.39.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ec3d70b3e507515936e475d9811305f52d049281eaa6c8273448a61c9b5b7e2"},
+ {file = "llvmlite-0.39.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60f8dd1e76f47b3dbdee4b38d9189f3e020d22a173c00f930b52131001d801f9"},
+ {file = "llvmlite-0.39.1-cp39-cp39-win32.whl", hash = "sha256:03aee0ccd81735696474dc4f8b6be60774892a2929d6c05d093d17392c237f32"},
+ {file = "llvmlite-0.39.1-cp39-cp39-win_amd64.whl", hash = "sha256:3fc14e757bc07a919221f0cbaacb512704ce5774d7fcada793f1996d6bc75f2a"},
+ {file = "llvmlite-0.39.1.tar.gz", hash = "sha256:b43abd7c82e805261c425d50335be9a6c4f84264e34d6d6e475207300005d572"},
+]
+
+[[package]]
+name = "lxml"
+version = "4.9.2"
+description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*"
+files = [
+ {file = "lxml-4.9.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:76cf573e5a365e790396a5cc2b909812633409306c6531a6877c59061e42c4f2"},
+ {file = "lxml-4.9.2-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1f42b6921d0e81b1bcb5e395bc091a70f41c4d4e55ba99c6da2b31626c44892"},
+ {file = "lxml-4.9.2-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9f102706d0ca011de571de32c3247c6476b55bb6bc65a20f682f000b07a4852a"},
+ {file = "lxml-4.9.2-cp27-cp27m-win32.whl", hash = "sha256:8d0b4612b66ff5d62d03bcaa043bb018f74dfea51184e53f067e6fdcba4bd8de"},
+ {file = "lxml-4.9.2-cp27-cp27m-win_amd64.whl", hash = "sha256:4c8f293f14abc8fd3e8e01c5bd86e6ed0b6ef71936ded5bf10fe7a5efefbaca3"},
+ {file = "lxml-4.9.2-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2899456259589aa38bfb018c364d6ae7b53c5c22d8e27d0ec7609c2a1ff78b50"},
+ {file = "lxml-4.9.2-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6749649eecd6a9871cae297bffa4ee76f90b4504a2a2ab528d9ebe912b101975"},
+ {file = "lxml-4.9.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:a08cff61517ee26cb56f1e949cca38caabe9ea9fbb4b1e10a805dc39844b7d5c"},
+ {file = "lxml-4.9.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:85cabf64adec449132e55616e7ca3e1000ab449d1d0f9d7f83146ed5bdcb6d8a"},
+ {file = "lxml-4.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8340225bd5e7a701c0fa98284c849c9b9fc9238abf53a0ebd90900f25d39a4e4"},
+ {file = "lxml-4.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:1ab8f1f932e8f82355e75dda5413a57612c6ea448069d4fb2e217e9a4bed13d4"},
+ {file = "lxml-4.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:699a9af7dffaf67deeae27b2112aa06b41c370d5e7633e0ee0aea2e0b6c211f7"},
+ {file = "lxml-4.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9cc34af337a97d470040f99ba4282f6e6bac88407d021688a5d585e44a23184"},
+ {file = "lxml-4.9.2-cp310-cp310-win32.whl", hash = "sha256:d02a5399126a53492415d4906ab0ad0375a5456cc05c3fc0fc4ca11771745cda"},
+ {file = "lxml-4.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:a38486985ca49cfa574a507e7a2215c0c780fd1778bb6290c21193b7211702ab"},
+ {file = "lxml-4.9.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c83203addf554215463b59f6399835201999b5e48019dc17f182ed5ad87205c9"},
+ {file = "lxml-4.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:2a87fa548561d2f4643c99cd13131acb607ddabb70682dcf1dff5f71f781a4bf"},
+ {file = "lxml-4.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:d6b430a9938a5a5d85fc107d852262ddcd48602c120e3dbb02137c83d212b380"},
+ {file = "lxml-4.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3efea981d956a6f7173b4659849f55081867cf897e719f57383698af6f618a92"},
+ {file = "lxml-4.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:df0623dcf9668ad0445e0558a21211d4e9a149ea8f5666917c8eeec515f0a6d1"},
+ {file = "lxml-4.9.2-cp311-cp311-win32.whl", hash = "sha256:da248f93f0418a9e9d94b0080d7ebc407a9a5e6d0b57bb30db9b5cc28de1ad33"},
+ {file = "lxml-4.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:3818b8e2c4b5148567e1b09ce739006acfaa44ce3156f8cbbc11062994b8e8dd"},
+ {file = "lxml-4.9.2-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca989b91cf3a3ba28930a9fc1e9aeafc2a395448641df1f387a2d394638943b0"},
+ {file = "lxml-4.9.2-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:822068f85e12a6e292803e112ab876bc03ed1f03dddb80154c395f891ca6b31e"},
+ {file = "lxml-4.9.2-cp35-cp35m-win32.whl", hash = "sha256:be7292c55101e22f2a3d4d8913944cbea71eea90792bf914add27454a13905df"},
+ {file = "lxml-4.9.2-cp35-cp35m-win_amd64.whl", hash = "sha256:998c7c41910666d2976928c38ea96a70d1aa43be6fe502f21a651e17483a43c5"},
+ {file = "lxml-4.9.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:b26a29f0b7fc6f0897f043ca366142d2b609dc60756ee6e4e90b5f762c6adc53"},
+ {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ab323679b8b3030000f2be63e22cdeea5b47ee0abd2d6a1dc0c8103ddaa56cd7"},
+ {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:689bb688a1db722485e4610a503e3e9210dcc20c520b45ac8f7533c837be76fe"},
+ {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f49e52d174375a7def9915c9f06ec4e569d235ad428f70751765f48d5926678c"},
+ {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:36c3c175d34652a35475a73762b545f4527aec044910a651d2bf50de9c3352b1"},
+ {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a35f8b7fa99f90dd2f5dc5a9fa12332642f087a7641289ca6c40d6e1a2637d8e"},
+ {file = "lxml-4.9.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:58bfa3aa19ca4c0f28c5dde0ff56c520fbac6f0daf4fac66ed4c8d2fb7f22e74"},
+ {file = "lxml-4.9.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc718cd47b765e790eecb74d044cc8d37d58562f6c314ee9484df26276d36a38"},
+ {file = "lxml-4.9.2-cp36-cp36m-win32.whl", hash = "sha256:d5bf6545cd27aaa8a13033ce56354ed9e25ab0e4ac3b5392b763d8d04b08e0c5"},
+ {file = "lxml-4.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:3ab9fa9d6dc2a7f29d7affdf3edebf6ece6fb28a6d80b14c3b2fb9d39b9322c3"},
+ {file = "lxml-4.9.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:05ca3f6abf5cf78fe053da9b1166e062ade3fa5d4f92b4ed688127ea7d7b1d03"},
+ {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:a5da296eb617d18e497bcf0a5c528f5d3b18dadb3619fbdadf4ed2356ef8d941"},
+ {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:04876580c050a8c5341d706dd464ff04fd597095cc8c023252566a8826505726"},
+ {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c9ec3eaf616d67db0764b3bb983962b4f385a1f08304fd30c7283954e6a7869b"},
+ {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2a29ba94d065945944016b6b74e538bdb1751a1db6ffb80c9d3c2e40d6fa9894"},
+ {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a82d05da00a58b8e4c0008edbc8a4b6ec5a4bc1e2ee0fb6ed157cf634ed7fa45"},
+ {file = "lxml-4.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:223f4232855ade399bd409331e6ca70fb5578efef22cf4069a6090acc0f53c0e"},
+ {file = "lxml-4.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d17bc7c2ccf49c478c5bdd447594e82692c74222698cfc9b5daae7ae7e90743b"},
+ {file = "lxml-4.9.2-cp37-cp37m-win32.whl", hash = "sha256:b64d891da92e232c36976c80ed7ebb383e3f148489796d8d31a5b6a677825efe"},
+ {file = "lxml-4.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a0a336d6d3e8b234a3aae3c674873d8f0e720b76bc1d9416866c41cd9500ffb9"},
+ {file = "lxml-4.9.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:da4dd7c9c50c059aba52b3524f84d7de956f7fef88f0bafcf4ad7dde94a064e8"},
+ {file = "lxml-4.9.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:821b7f59b99551c69c85a6039c65b75f5683bdc63270fec660f75da67469ca24"},
+ {file = "lxml-4.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:e5168986b90a8d1f2f9dc1b841467c74221bd752537b99761a93d2d981e04889"},
+ {file = "lxml-4.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8e20cb5a47247e383cf4ff523205060991021233ebd6f924bca927fcf25cf86f"},
+ {file = "lxml-4.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13598ecfbd2e86ea7ae45ec28a2a54fb87ee9b9fdb0f6d343297d8e548392c03"},
+ {file = "lxml-4.9.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:880bbbcbe2fca64e2f4d8e04db47bcdf504936fa2b33933efd945e1b429bea8c"},
+ {file = "lxml-4.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7d2278d59425777cfcb19735018d897ca8303abe67cc735f9f97177ceff8027f"},
+ {file = "lxml-4.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5344a43228767f53a9df6e5b253f8cdca7dfc7b7aeae52551958192f56d98457"},
+ {file = "lxml-4.9.2-cp38-cp38-win32.whl", hash = "sha256:925073b2fe14ab9b87e73f9a5fde6ce6392da430f3004d8b72cc86f746f5163b"},
+ {file = "lxml-4.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:9b22c5c66f67ae00c0199f6055705bc3eb3fcb08d03d2ec4059a2b1b25ed48d7"},
+ {file = "lxml-4.9.2-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5f50a1c177e2fa3ee0667a5ab79fdc6b23086bc8b589d90b93b4bd17eb0e64d1"},
+ {file = "lxml-4.9.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:090c6543d3696cbe15b4ac6e175e576bcc3f1ccfbba970061b7300b0c15a2140"},
+ {file = "lxml-4.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:63da2ccc0857c311d764e7d3d90f429c252e83b52d1f8f1d1fe55be26827d1f4"},
+ {file = "lxml-4.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:5b4545b8a40478183ac06c073e81a5ce4cf01bf1734962577cf2bb569a5b3bbf"},
+ {file = "lxml-4.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2e430cd2824f05f2d4f687701144556646bae8f249fd60aa1e4c768ba7018947"},
+ {file = "lxml-4.9.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6804daeb7ef69e7b36f76caddb85cccd63d0c56dedb47555d2fc969e2af6a1a5"},
+ {file = "lxml-4.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a6e441a86553c310258aca15d1c05903aaf4965b23f3bc2d55f200804e005ee5"},
+ {file = "lxml-4.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca34efc80a29351897e18888c71c6aca4a359247c87e0b1c7ada14f0ab0c0fb2"},
+ {file = "lxml-4.9.2-cp39-cp39-win32.whl", hash = "sha256:6b418afe5df18233fc6b6093deb82a32895b6bb0b1155c2cdb05203f583053f1"},
+ {file = "lxml-4.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:f1496ea22ca2c830cbcbd473de8f114a320da308438ae65abad6bab7867fe38f"},
+ {file = "lxml-4.9.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:b264171e3143d842ded311b7dccd46ff9ef34247129ff5bf5066123c55c2431c"},
+ {file = "lxml-4.9.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0dc313ef231edf866912e9d8f5a042ddab56c752619e92dfd3a2c277e6a7299a"},
+ {file = "lxml-4.9.2-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:16efd54337136e8cd72fb9485c368d91d77a47ee2d42b057564aae201257d419"},
+ {file = "lxml-4.9.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0f2b1e0d79180f344ff9f321327b005ca043a50ece8713de61d1cb383fb8ac05"},
+ {file = "lxml-4.9.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7b770ed79542ed52c519119473898198761d78beb24b107acf3ad65deae61f1f"},
+ {file = "lxml-4.9.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efa29c2fe6b4fdd32e8ef81c1528506895eca86e1d8c4657fda04c9b3786ddf9"},
+ {file = "lxml-4.9.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7e91ee82f4199af8c43d8158024cbdff3d931df350252288f0d4ce656df7f3b5"},
+ {file = "lxml-4.9.2-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:b23e19989c355ca854276178a0463951a653309fb8e57ce674497f2d9f208746"},
+ {file = "lxml-4.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:01d36c05f4afb8f7c20fd9ed5badca32a2029b93b1750f571ccc0b142531caf7"},
+ {file = "lxml-4.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7b515674acfdcadb0eb5d00d8a709868173acece5cb0be3dd165950cbfdf5409"},
+ {file = "lxml-4.9.2.tar.gz", hash = "sha256:2455cfaeb7ac70338b3257f41e21f0724f4b5b0c0e7702da67ee6c3640835b67"},
+]
+
+[package.extras]
+cssselect = ["cssselect (>=0.7)"]
+html5 = ["html5lib"]
+htmlsoup = ["BeautifulSoup4"]
+source = ["Cython (>=0.29.7)"]
+
+[[package]]
+name = "mako"
+version = "1.2.4"
+description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"},
+ {file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=0.9.2"
+
+[package.extras]
+babel = ["Babel"]
+lingua = ["lingua"]
+testing = ["pytest"]
+
+[[package]]
+name = "markdown"
+version = "3.4.3"
+description = "Python implementation of John Gruber's Markdown."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "Markdown-3.4.3-py3-none-any.whl", hash = "sha256:065fd4df22da73a625f14890dd77eb8040edcbd68794bcd35943be14490608b2"},
+ {file = "Markdown-3.4.3.tar.gz", hash = "sha256:8bf101198e004dc93e84a12a7395e31aac6a9c9942848ae1d99b9d72cf9b3520"},
+]
+
+[package.extras]
+testing = ["coverage", "pyyaml"]
+
+[[package]]
+name = "markdown-it-py"
+version = "2.2.0"
+description = "Python port of markdown-it. Markdown parsing, done right!"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"},
+ {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"},
+]
+
+[package.dependencies]
+mdurl = ">=0.1,<1.0"
+
+[package.extras]
+benchmarking = ["psutil", "pytest", "pytest-benchmark"]
+code-style = ["pre-commit (>=3.0,<4.0)"]
+compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
+linkify = ["linkify-it-py (>=1,<3)"]
+plugins = ["mdit-py-plugins"]
+profiling = ["gprof2dot"]
+rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
+testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
+
+[[package]]
+name = "markupsafe"
+version = "2.1.2"
+description = "Safely add untrusted strings to HTML/XML markup."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"},
+ {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"},
+ {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"},
+ {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"},
+ {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"},
+ {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"},
+ {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"},
+ {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"},
+ {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"},
+ {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"},
+ {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"},
+ {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"},
+ {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"},
+ {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"},
+ {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"},
+ {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"},
+ {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"},
+ {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"},
+ {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"},
+ {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"},
+ {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"},
+ {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"},
+ {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"},
+ {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"},
+ {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"},
+ {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"},
+ {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"},
+ {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"},
+ {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"},
+ {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"},
+ {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"},
+ {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"},
+ {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"},
+ {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"},
+ {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"},
+ {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"},
+ {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"},
+ {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"},
+ {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"},
+ {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"},
+ {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"},
+ {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"},
+ {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"},
+ {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"},
+ {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"},
+ {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"},
+ {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"},
+ {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"},
+ {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"},
+ {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"},
+]
+
+[[package]]
+name = "matplotlib"
+version = "3.7.1"
+description = "Python plotting package"
+category = "main"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "matplotlib-3.7.1-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:95cbc13c1fc6844ab8812a525bbc237fa1470863ff3dace7352e910519e194b1"},
+ {file = "matplotlib-3.7.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:08308bae9e91aca1ec6fd6dda66237eef9f6294ddb17f0d0b3c863169bf82353"},
+ {file = "matplotlib-3.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:544764ba51900da4639c0f983b323d288f94f65f4024dc40ecb1542d74dc0500"},
+ {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d94989191de3fcc4e002f93f7f1be5da476385dde410ddafbb70686acf00ea"},
+ {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99bc9e65901bb9a7ce5e7bb24af03675cbd7c70b30ac670aa263240635999a4"},
+ {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb7d248c34a341cd4c31a06fd34d64306624c8cd8d0def7abb08792a5abfd556"},
+ {file = "matplotlib-3.7.1-cp310-cp310-win32.whl", hash = "sha256:ce463ce590f3825b52e9fe5c19a3c6a69fd7675a39d589e8b5fbe772272b3a24"},
+ {file = "matplotlib-3.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:3d7bc90727351fb841e4d8ae620d2d86d8ed92b50473cd2b42ce9186104ecbba"},
+ {file = "matplotlib-3.7.1-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:770a205966d641627fd5cf9d3cb4b6280a716522cd36b8b284a8eb1581310f61"},
+ {file = "matplotlib-3.7.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f67bfdb83a8232cb7a92b869f9355d677bce24485c460b19d01970b64b2ed476"},
+ {file = "matplotlib-3.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2bf092f9210e105f414a043b92af583c98f50050559616930d884387d0772aba"},
+ {file = "matplotlib-3.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89768d84187f31717349c6bfadc0e0d8c321e8eb34522acec8a67b1236a66332"},
+ {file = "matplotlib-3.7.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83111e6388dec67822e2534e13b243cc644c7494a4bb60584edbff91585a83c6"},
+ {file = "matplotlib-3.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a867bf73a7eb808ef2afbca03bcdb785dae09595fbe550e1bab0cd023eba3de0"},
+ {file = "matplotlib-3.7.1-cp311-cp311-win32.whl", hash = "sha256:fbdeeb58c0cf0595efe89c05c224e0a502d1aa6a8696e68a73c3efc6bc354304"},
+ {file = "matplotlib-3.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:c0bd19c72ae53e6ab979f0ac6a3fafceb02d2ecafa023c5cca47acd934d10be7"},
+ {file = "matplotlib-3.7.1-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:6eb88d87cb2c49af00d3bbc33a003f89fd9f78d318848da029383bfc08ecfbfb"},
+ {file = "matplotlib-3.7.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:cf0e4f727534b7b1457898c4f4ae838af1ef87c359b76dcd5330fa31893a3ac7"},
+ {file = "matplotlib-3.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:46a561d23b91f30bccfd25429c3c706afe7d73a5cc64ef2dfaf2b2ac47c1a5dc"},
+ {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8704726d33e9aa8a6d5215044b8d00804561971163563e6e6591f9dcf64340cc"},
+ {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4cf327e98ecf08fcbb82685acaf1939d3338548620ab8dfa02828706402c34de"},
+ {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:617f14ae9d53292ece33f45cba8503494ee199a75b44de7717964f70637a36aa"},
+ {file = "matplotlib-3.7.1-cp38-cp38-win32.whl", hash = "sha256:7c9a4b2da6fac77bcc41b1ea95fadb314e92508bf5493ceff058e727e7ecf5b0"},
+ {file = "matplotlib-3.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:14645aad967684e92fc349493fa10c08a6da514b3d03a5931a1bac26e6792bd1"},
+ {file = "matplotlib-3.7.1-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:81a6b377ea444336538638d31fdb39af6be1a043ca5e343fe18d0f17e098770b"},
+ {file = "matplotlib-3.7.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:28506a03bd7f3fe59cd3cd4ceb2a8d8a2b1db41afede01f66c42561b9be7b4b7"},
+ {file = "matplotlib-3.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8c587963b85ce41e0a8af53b9b2de8dddbf5ece4c34553f7bd9d066148dc719c"},
+ {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8bf26ade3ff0f27668989d98c8435ce9327d24cffb7f07d24ef609e33d582439"},
+ {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:def58098f96a05f90af7e92fd127d21a287068202aa43b2a93476170ebd99e87"},
+ {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f883a22a56a84dba3b588696a2b8a1ab0d2c3d41be53264115c71b0a942d8fdb"},
+ {file = "matplotlib-3.7.1-cp39-cp39-win32.whl", hash = "sha256:4f99e1b234c30c1e9714610eb0c6d2f11809c9c78c984a613ae539ea2ad2eb4b"},
+ {file = "matplotlib-3.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:3ba2af245e36990facf67fde840a760128ddd71210b2ab6406e640188d69d136"},
+ {file = "matplotlib-3.7.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3032884084f541163f295db8a6536e0abb0db464008fadca6c98aaf84ccf4717"},
+ {file = "matplotlib-3.7.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a2cb34336110e0ed8bb4f650e817eed61fa064acbefeb3591f1b33e3a84fd96"},
+ {file = "matplotlib-3.7.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b867e2f952ed592237a1828f027d332d8ee219ad722345b79a001f49df0936eb"},
+ {file = "matplotlib-3.7.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:57bfb8c8ea253be947ccb2bc2d1bb3862c2bccc662ad1b4626e1f5e004557042"},
+ {file = "matplotlib-3.7.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:438196cdf5dc8d39b50a45cb6e3f6274edbcf2254f85fa9b895bf85851c3a613"},
+ {file = "matplotlib-3.7.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:21e9cff1a58d42e74d01153360de92b326708fb205250150018a52c70f43c290"},
+ {file = "matplotlib-3.7.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75d4725d70b7c03e082bbb8a34639ede17f333d7247f56caceb3801cb6ff703d"},
+ {file = "matplotlib-3.7.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:97cc368a7268141afb5690760921765ed34867ffb9655dd325ed207af85c7529"},
+ {file = "matplotlib-3.7.1.tar.gz", hash = "sha256:7b73305f25eab4541bd7ee0b96d87e53ae9c9f1823be5659b806cd85786fe882"},
+]
+
+[package.dependencies]
+contourpy = ">=1.0.1"
+cycler = ">=0.10"
+fonttools = ">=4.22.0"
+kiwisolver = ">=1.0.1"
+numpy = ">=1.20"
+packaging = ">=20.0"
+pillow = ">=6.2.0"
+pyparsing = ">=2.3.1"
+python-dateutil = ">=2.7"
+
+[[package]]
+name = "matplotlib-inline"
+version = "0.1.6"
+description = "Inline Matplotlib backend for Jupyter"
+category = "dev"
+optional = false
+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 = "mccabe"
+version = "0.7.0"
+description = "McCabe checker, plugin for flake8"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
+ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+description = "Markdown URL utilities"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
+ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
+]
+
+[[package]]
+name = "mesh-to-sdf"
+version = "0.0.14"
+description = "Calculate signed distance fields for arbitrary meshes"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+files = []
+develop = false
+
+[package.dependencies]
+pyopengl = "*"
+pyrender = "*"
+scikit-image = "*"
+scikit-learn = "*"
+
+[package.source]
+type = "git"
+url = "https://github.com/pbsds/mesh_to_sdf"
+reference = "no_flip_normals"
+resolved_reference = "c5e9a53425108008065c66f927ffe68d9c01453e"
+
+[[package]]
+name = "methodtools"
+version = "0.4.7"
+description = "Expand standard functools to methods"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "methodtools-0.4.7.tar.gz", hash = "sha256:e213439dd64cfe60213f7015da6efe5dd4003fd89376db3baa09fe13ec2bb0ba"},
+]
+
+[package.dependencies]
+wirerope = ">=0.4.7"
+
+[package.extras]
+doc = ["sphinx"]
+test = ["functools32 (>=3.2.3-2)", "pytest (>=4.6.7)", "pytest-cov (>=2.6.1)"]
+
+[[package]]
+name = "mistune"
+version = "0.8.4"
+description = "The fastest markdown parser in pure Python"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"},
+ {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"},
+]
+
+[[package]]
+name = "more-itertools"
+version = "9.1.0"
+description = "More routines for operating on iterables, beyond itertools"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "more-itertools-9.1.0.tar.gz", hash = "sha256:cabaa341ad0389ea83c17a94566a53ae4c9d07349861ecb14dc6d0345cf9ac5d"},
+ {file = "more_itertools-9.1.0-py3-none-any.whl", hash = "sha256:d2bc7f02446e86a68911e58ded76d6561eea00cddfb2a91e7019bbb586c799f3"},
+]
+
+[[package]]
+name = "mpmath"
+version = "1.3.0"
+description = "Python library for arbitrary-precision floating-point arithmetic"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"},
+ {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"},
+]
+
+[package.extras]
+develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"]
+docs = ["sphinx"]
+gmpy = ["gmpy2 (>=2.1.0a4)"]
+tests = ["pytest (>=4.6)"]
+
+[[package]]
+name = "multidict"
+version = "6.0.4"
+description = "multidict implementation"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"},
+ {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"},
+ {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"},
+ {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"},
+ {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"},
+ {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"},
+ {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"},
+ {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"},
+ {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"},
+ {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"},
+ {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"},
+ {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"},
+ {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"},
+ {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"},
+ {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"},
+ {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"},
+ {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"},
+ {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"},
+ {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"},
+ {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"},
+ {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"},
+ {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"},
+ {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"},
+ {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"},
+ {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"},
+ {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"},
+ {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"},
+ {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"},
+ {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"},
+ {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"},
+ {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"},
+ {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"},
+ {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"},
+ {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"},
+ {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"},
+ {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"},
+ {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"},
+ {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"},
+ {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"},
+ {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"},
+ {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"},
+ {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"},
+ {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"},
+ {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"},
+ {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"},
+ {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"},
+ {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"},
+ {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"},
+ {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"},
+ {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"},
+ {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"},
+ {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"},
+ {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"},
+ {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"},
+ {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"},
+ {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"},
+ {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"},
+ {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"},
+ {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"},
+ {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"},
+ {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"},
+ {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"},
+ {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"},
+ {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"},
+ {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"},
+ {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"},
+ {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"},
+ {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"},
+ {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"},
+ {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"},
+ {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"},
+ {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"},
+ {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"},
+ {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"},
+]
+
+[[package]]
+name = "munch"
+version = "2.5.0"
+description = "A dot-accessible dictionary (a la JavaScript objects)"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "munch-2.5.0-py2.py3-none-any.whl", hash = "sha256:6f44af89a2ce4ed04ff8de41f70b226b984db10a91dcc7b9ac2efc1c77022fdd"},
+ {file = "munch-2.5.0.tar.gz", hash = "sha256:2d735f6f24d4dba3417fa448cae40c6e896ec1fdab6cdb5e6510999758a4dbd2"},
+]
+
+[package.dependencies]
+six = "*"
+
+[package.extras]
+testing = ["astroid (>=1.5.3,<1.6.0)", "astroid (>=2.0)", "coverage", "pylint (>=1.7.2,<1.8.0)", "pylint (>=2.3.1,<2.4.0)", "pytest"]
+yaml = ["PyYAML (>=5.1.0)"]
+
+[[package]]
+name = "nbclassic"
+version = "0.5.3"
+description = "Jupyter Notebook as a Jupyter Server extension."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "nbclassic-0.5.3-py3-none-any.whl", hash = "sha256:e849277872d9ffd8fe4b39a8038d01ba82d6a1def9ce11b1b3c26c9546ed5131"},
+ {file = "nbclassic-0.5.3.tar.gz", hash = "sha256:889772a7ba524eb781d2901f396540bcad41151e1f7e043f12ebc14a6540d342"},
+]
+
+[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-jupyter", "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."
+category = "dev"
+optional = false
+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.0 || >=5.1.0"
+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 = "6.5.0"
+description = "Converting Jupyter Notebooks"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "nbconvert-6.5.0-py3-none-any.whl", hash = "sha256:c56dd0b8978a1811a5654f74c727ff16ca87dd5a43abd435a1c49b840fcd8360"},
+ {file = "nbconvert-6.5.0.tar.gz", hash = "sha256:223e46e27abe8596b8aed54301fadbba433b7ffea8196a68fd7b1ff509eee99d"},
+]
+
+[package.dependencies]
+beautifulsoup4 = "*"
+bleach = "*"
+defusedxml = "*"
+entrypoints = ">=0.2.2"
+jinja2 = ">=3.0"
+jupyter-core = ">=4.7"
+jupyterlab-pygments = "*"
+MarkupSafe = ">=2.0"
+mistune = ">=0.8.1,<2"
+nbclient = ">=0.5.0"
+nbformat = ">=5.1"
+packaging = "*"
+pandocfilters = ">=1.4.1"
+pygments = ">=2.4.1"
+tinycss2 = "*"
+traitlets = ">=5.0"
+
+[package.extras]
+all = ["ipykernel", "ipython", "ipywidgets (>=7)", "nbsphinx (>=0.2.12)", "pre-commit", "pyppeteer (>=1,<1.1)", "pytest", "pytest-cov", "pytest-dependency", "sphinx (>=1.5.1)", "sphinx-rtd-theme", "tornado (>=6.1)"]
+docs = ["ipython", "nbsphinx (>=0.2.12)", "sphinx (>=1.5.1)", "sphinx-rtd-theme"]
+serve = ["tornado (>=6.1)"]
+test = ["ipykernel", "ipywidgets (>=7)", "pre-commit", "pyppeteer (>=1,<1.1)", "pytest", "pytest-cov", "pytest-dependency"]
+webpdf = ["pyppeteer (>=1,<1.1)"]
+
+[[package]]
+name = "nbformat"
+version = "5.8.0"
+description = "The Jupyter Notebook format"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "nbformat-5.8.0-py3-none-any.whl", hash = "sha256:d910082bd3e0bffcf07eabf3683ed7dda0727a326c446eeb2922abe102e65162"},
+ {file = "nbformat-5.8.0.tar.gz", hash = "sha256:46dac64c781f1c34dfd8acba16547024110348f9fc7eab0f31981c2a3dc48d1f"},
+]
+
+[package.dependencies]
+fastjsonschema = "*"
+jsonschema = ">=2.6"
+jupyter-core = "*"
+traitlets = ">=5.1"
+
+[package.extras]
+docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"]
+test = ["pep440", "pre-commit", "pytest", "testpath"]
+
+[[package]]
+name = "nest-asyncio"
+version = "1.5.6"
+description = "Patch asyncio to allow nested event loops"
+category = "dev"
+optional = false
+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 = "networkx"
+version = "3.0"
+description = "Python package for creating and manipulating graphs and networks"
+category = "main"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "networkx-3.0-py3-none-any.whl", hash = "sha256:58058d66b1818043527244fab9d41a51fcd7dcc271748015f3c181b8a90c8e2e"},
+ {file = "networkx-3.0.tar.gz", hash = "sha256:9a9992345353618ae98339c2b63d8201c381c2944f38a2ab49cb45a4c667e412"},
+]
+
+[package.extras]
+default = ["matplotlib (>=3.4)", "numpy (>=1.20)", "pandas (>=1.3)", "scipy (>=1.8)"]
+developer = ["mypy (>=0.991)", "pre-commit (>=2.20)"]
+doc = ["nb2plots (>=0.6)", "numpydoc (>=1.5)", "pillow (>=9.2)", "pydata-sphinx-theme (>=0.11)", "sphinx (==5.2.3)", "sphinx-gallery (>=0.11)", "texext (>=0.6.7)"]
+extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.10)", "sympy (>=1.10)"]
+test = ["codecov (>=2.1)", "pytest (>=7.2)", "pytest-cov (>=4.0)"]
+
+[[package]]
+name = "notebook"
+version = "6.5.3"
+description = "A web-based notebook environment for interactive computing"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "notebook-6.5.3-py3-none-any.whl", hash = "sha256:50a334ad9d60b30cb759405168ef6fc3d60350ab5439fb1631544bb09dcb2cce"},
+ {file = "notebook-6.5.3.tar.gz", hash = "sha256:b12bee3292211d85dd7e588a790ddce30cb3e8fbcfa1e803522a207f60819e05"},
+]
+
+[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"
+category = "dev"
+optional = false
+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 = "numba"
+version = "0.56.4"
+description = "compiling Python code using LLVM"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "numba-0.56.4-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:9f62672145f8669ec08762895fe85f4cf0ead08ce3164667f2b94b2f62ab23c3"},
+ {file = "numba-0.56.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c602d015478b7958408d788ba00a50272649c5186ea8baa6cf71d4a1c761bba1"},
+ {file = "numba-0.56.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:85dbaed7a05ff96492b69a8900c5ba605551afb9b27774f7f10511095451137c"},
+ {file = "numba-0.56.4-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f4cfc3a19d1e26448032049c79fc60331b104f694cf570a9e94f4e2c9d0932bb"},
+ {file = "numba-0.56.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e08e203b163ace08bad500b0c16f6092b1eb34fd1fce4feaf31a67a3a5ecf3b"},
+ {file = "numba-0.56.4-cp310-cp310-win32.whl", hash = "sha256:0611e6d3eebe4cb903f1a836ffdb2bda8d18482bcd0a0dcc56e79e2aa3fefef5"},
+ {file = "numba-0.56.4-cp310-cp310-win_amd64.whl", hash = "sha256:fbfb45e7b297749029cb28694abf437a78695a100e7c2033983d69f0ba2698d4"},
+ {file = "numba-0.56.4-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:3cb1a07a082a61df80a468f232e452d818f5ae254b40c26390054e4e868556e0"},
+ {file = "numba-0.56.4-cp37-cp37m-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d69ad934e13c15684e7887100a8f5f0f61d7a8e57e0fd29d9993210089a5b531"},
+ {file = "numba-0.56.4-cp37-cp37m-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:dbcc847bac2d225265d054993a7f910fda66e73d6662fe7156452cac0325b073"},
+ {file = "numba-0.56.4-cp37-cp37m-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8a95ca9cc77ea4571081f6594e08bd272b66060634b8324e99cd1843020364f9"},
+ {file = "numba-0.56.4-cp37-cp37m-win32.whl", hash = "sha256:fcdf84ba3ed8124eb7234adfbb8792f311991cbf8aed1cad4b1b1a7ee08380c1"},
+ {file = "numba-0.56.4-cp37-cp37m-win_amd64.whl", hash = "sha256:42f9e1be942b215df7e6cc9948cf9c15bb8170acc8286c063a9e57994ef82fd1"},
+ {file = "numba-0.56.4-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:553da2ce74e8862e18a72a209ed3b6d2924403bdd0fb341fa891c6455545ba7c"},
+ {file = "numba-0.56.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4373da9757049db7c90591e9ec55a2e97b2b36ba7ae3bf9c956a513374077470"},
+ {file = "numba-0.56.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a993349b90569518739009d8f4b523dfedd7e0049e6838c0e17435c3e70dcc4"},
+ {file = "numba-0.56.4-cp38-cp38-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:720886b852a2d62619ae3900fe71f1852c62db4f287d0c275a60219e1643fc04"},
+ {file = "numba-0.56.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e64d338b504c9394a4a34942df4627e1e6cb07396ee3b49fe7b8d6420aa5104f"},
+ {file = "numba-0.56.4-cp38-cp38-win32.whl", hash = "sha256:03fe94cd31e96185cce2fae005334a8cc712fc2ba7756e52dff8c9400718173f"},
+ {file = "numba-0.56.4-cp38-cp38-win_amd64.whl", hash = "sha256:91f021145a8081f881996818474ef737800bcc613ffb1e618a655725a0f9e246"},
+ {file = "numba-0.56.4-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:d0ae9270a7a5cc0ede63cd234b4ff1ce166c7a749b91dbbf45e0000c56d3eade"},
+ {file = "numba-0.56.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c75e8a5f810ce80a0cfad6e74ee94f9fde9b40c81312949bf356b7304ef20740"},
+ {file = "numba-0.56.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a12ef323c0f2101529d455cfde7f4135eaa147bad17afe10b48634f796d96abd"},
+ {file = "numba-0.56.4-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:03634579d10a6129181129de293dd6b5eaabee86881369d24d63f8fe352dd6cb"},
+ {file = "numba-0.56.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0240f9026b015e336069329839208ebd70ec34ae5bfbf402e4fcc8e06197528e"},
+ {file = "numba-0.56.4-cp39-cp39-win32.whl", hash = "sha256:14dbbabf6ffcd96ee2ac827389afa59a70ffa9f089576500434c34abf9b054a4"},
+ {file = "numba-0.56.4-cp39-cp39-win_amd64.whl", hash = "sha256:0da583c532cd72feefd8e551435747e0e0fbb3c0530357e6845fcc11e38d6aea"},
+ {file = "numba-0.56.4.tar.gz", hash = "sha256:32d9fef412c81483d7efe0ceb6cf4d3310fde8b624a9cecca00f790573ac96ee"},
+]
+
+[package.dependencies]
+llvmlite = ">=0.39.0dev0,<0.40"
+numpy = ">=1.18,<1.24"
+setuptools = "*"
+
+[[package]]
+name = "numpy"
+version = "1.23.5"
+description = "NumPy is the fundamental package for array computing with Python."
+category = "main"
+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"
+category = "main"
+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"
+category = "main"
+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"
+category = "main"
+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"
+category = "main"
+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"
+category = "dev"
+optional = false
+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 = "ordered-set"
+version = "4.1.0"
+description = "An OrderedSet is a custom MutableSet that remembers its order, so that every"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8"},
+ {file = "ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562"},
+]
+
+[package.extras]
+dev = ["black", "mypy", "pytest"]
+
+[[package]]
+name = "packaging"
+version = "23.0"
+description = "Core utilities for Python packages"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"},
+ {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"},
+]
+
+[[package]]
+name = "pandas"
+version = "1.5.3"
+description = "Powerful data structures for data analysis, time series, and statistics"
+category = "main"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3749077d86e3a2f0ed51367f30bf5b82e131cc0f14260c4d3e499186fccc4406"},
+ {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:972d8a45395f2a2d26733eb8d0f629b2f90bebe8e8eddbb8829b180c09639572"},
+ {file = "pandas-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50869a35cbb0f2e0cd5ec04b191e7b12ed688874bd05dd777c19b28cbea90996"},
+ {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ac844a0fe00bfaeb2c9b51ab1424e5c8744f89860b138434a363b1f620f354"},
+ {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0a56cef15fd1586726dace5616db75ebcfec9179a3a55e78f72c5639fa2a23"},
+ {file = "pandas-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:478ff646ca42b20376e4ed3fa2e8d7341e8a63105586efe54fa2508ee087f328"},
+ {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6973549c01ca91ec96199e940495219c887ea815b2083722821f1d7abfa2b4dc"},
+ {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c39a8da13cede5adcd3be1182883aea1c925476f4e84b2807a46e2775306305d"},
+ {file = "pandas-1.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f76d097d12c82a535fda9dfe5e8dd4127952b45fea9b0276cb30cca5ea313fbc"},
+ {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e474390e60ed609cec869b0da796ad94f420bb057d86784191eefc62b65819ae"},
+ {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f2b952406a1588ad4cad5b3f55f520e82e902388a6d5a4a91baa8d38d23c7f6"},
+ {file = "pandas-1.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc4c368f42b551bf72fac35c5128963a171b40dce866fb066540eeaf46faa003"},
+ {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14e45300521902689a81f3f41386dc86f19b8ba8dd5ac5a3c7010ef8d2932813"},
+ {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9842b6f4b8479e41968eced654487258ed81df7d1c9b7b870ceea24ed9459b31"},
+ {file = "pandas-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:26d9c71772c7afb9d5046e6e9cf42d83dd147b5cf5bcb9d97252077118543792"},
+ {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fbcb19d6fceb9e946b3e23258757c7b225ba450990d9ed63ccceeb8cae609f7"},
+ {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:565fa34a5434d38e9d250af3c12ff931abaf88050551d9fbcdfafca50d62babf"},
+ {file = "pandas-1.5.3-cp38-cp38-win32.whl", hash = "sha256:87bd9c03da1ac870a6d2c8902a0e1fd4267ca00f13bc494c9e5a9020920e1d51"},
+ {file = "pandas-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:41179ce559943d83a9b4bbacb736b04c928b095b5f25dd2b7389eda08f46f373"},
+ {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c74a62747864ed568f5a82a49a23a8d7fe171d0c69038b38cedf0976831296fa"},
+ {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c4c00e0b0597c8e4f59e8d461f797e5d70b4d025880516a8261b2817c47759ee"},
+ {file = "pandas-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a50d9a4336a9621cab7b8eb3fb11adb82de58f9b91d84c2cd526576b881a0c5a"},
+ {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd05f7783b3274aa206a1af06f0ceed3f9b412cf665b7247eacd83be41cf7bf0"},
+ {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f69c4029613de47816b1bb30ff5ac778686688751a5e9c99ad8c7031f6508e5"},
+ {file = "pandas-1.5.3-cp39-cp39-win32.whl", hash = "sha256:7cec0bee9f294e5de5bbfc14d0573f65526071029d036b753ee6507d2a21480a"},
+ {file = "pandas-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:dfd681c5dc216037e0b0a2c821f5ed99ba9f03ebcf119c7dac0e9a7b960b9ec9"},
+ {file = "pandas-1.5.3.tar.gz", hash = "sha256:74a3fd7e5a7ec052f183273dc7b0acd3a863edf7520f5d3a1765c04ffdb3b0b1"},
+]
+
+[package.dependencies]
+numpy = {version = ">=1.21.0", markers = "python_version >= \"3.10\""}
+python-dateutil = ">=2.8.1"
+pytz = ">=2020.1"
+
+[package.extras]
+test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"]
+
+[[package]]
+name = "pandocfilters"
+version = "1.5.0"
+description = "Utilities for writing pandoc filters in python"
+category = "dev"
+optional = false
+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 = "papermill"
+version = "2.4.0"
+description = "Parametrize and run Jupyter and nteract Notebooks"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "papermill-2.4.0-py3-none-any.whl", hash = "sha256:baa76f0441257d9a25b3ad7c895e761341b94f9a70ca98cf419247fc728932d9"},
+ {file = "papermill-2.4.0.tar.gz", hash = "sha256:6f8f8a9b06b39677f207c09100c8d386bcf592f0cbbdda9f0f50e81445697627"},
+]
+
+[package.dependencies]
+ansiwrap = "*"
+click = "*"
+entrypoints = "*"
+nbclient = ">=0.2.0"
+nbformat = ">=5.1.2"
+pyyaml = "*"
+requests = "*"
+tenacity = "*"
+tqdm = ">=4.32.2"
+
+[package.extras]
+all = ["azure-datalake-store (>=0.0.30)", "azure-storage-blob (>=12.1.0)", "black (>=19.3b0)", "boto3", "gcsfs (>=0.2.0)", "pyarrow (>=2.0)", "requests (>=2.21.0)"]
+azure = ["azure-datalake-store (>=0.0.30)", "azure-storage-blob (>=12.1.0)", "requests (>=2.21.0)"]
+black = ["black (>=19.3b0)"]
+dev = ["attrs (>=17.4.0)", "azure-datalake-store (>=0.0.30)", "azure-storage-blob (>=12.1.0)", "black (>=19.3b0)", "boto3", "botocore", "bumpversion", "check-manifest", "codecov", "coverage", "flake8", "gcsfs (>=0.2.0)", "google-compute-engine", "ipython (>=5.0)", "ipywidgets", "moto", "notebook", "pip (>=18.1)", "pre-commit", "pyarrow (>=2.0)", "pytest (>=4.1)", "pytest-cov (>=2.6.1)", "pytest-env (>=0.6.2)", "pytest-mock (>=1.10)", "recommonmark", "requests (>=2.21.0)", "setuptools (>=38.6.0)", "tox", "twine (>=1.11.0)", "wheel (>=0.31.0)"]
+gcs = ["gcsfs (>=0.2.0)"]
+github = ["PyGithub (>=1.55)"]
+hdfs = ["pyarrow (>=2.0)"]
+s3 = ["boto3"]
+test = ["attrs (>=17.4.0)", "azure-datalake-store (>=0.0.30)", "azure-storage-blob (>=12.1.0)", "black (>=19.3b0)", "boto3", "botocore", "bumpversion", "check-manifest", "codecov", "coverage", "flake8", "gcsfs (>=0.2.0)", "google-compute-engine", "ipython (>=5.0)", "ipywidgets", "moto", "notebook", "pip (>=18.1)", "pre-commit", "pyarrow (>=2.0)", "pytest (>=4.1)", "pytest-cov (>=2.6.1)", "pytest-env (>=0.6.2)", "pytest-mock (>=1.10)", "recommonmark", "requests (>=2.21.0)", "setuptools (>=38.6.0)", "tox", "twine (>=1.11.0)", "wheel (>=0.31.0)"]
+
+[[package]]
+name = "parso"
+version = "0.8.3"
+description = "A Python Parser"
+category = "dev"
+optional = false
+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 = "pdoc"
+version = "12.3.1"
+description = "API Documentation for Python Projects"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pdoc-12.3.1-py3-none-any.whl", hash = "sha256:c3f24f31286e634de9c76fa6e67bd5c0c5e74360b41dc91e6b82499831eb52d8"},
+ {file = "pdoc-12.3.1.tar.gz", hash = "sha256:453236f225feddb8a9071428f1982a78d74b9b3da4bc4433aedb64dbd0cc87ab"},
+]
+
+[package.dependencies]
+Jinja2 = ">=2.11.0"
+MarkupSafe = "*"
+pygments = ">=2.12.0"
+
+[package.extras]
+dev = ["black", "hypothesis", "mypy", "pytest", "pytest-cov", "pytest-timeout", "ruff", "tox", "types-pygments"]
+
+[[package]]
+name = "pdoc3"
+version = "0.10.0"
+description = "Auto-generate API documentation for Python projects."
+category = "dev"
+optional = false
+python-versions = ">= 3.6"
+files = [
+ {file = "pdoc3-0.10.0-py3-none-any.whl", hash = "sha256:ba45d1ada1bd987427d2bf5cdec30b2631a3ff5fb01f6d0e77648a572ce6028b"},
+ {file = "pdoc3-0.10.0.tar.gz", hash = "sha256:5f22e7bcb969006738e1aa4219c75a32f34c2d62d46dc9d2fb2d3e0b0287e4b7"},
+]
+
+[package.dependencies]
+mako = "*"
+markdown = ">=3.0"
+
+[[package]]
+name = "pexpect"
+version = "4.8.0"
+description = "Pexpect allows easy control of interactive console applications."
+category = "dev"
+optional = false
+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"
+category = "dev"
+optional = false
+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.4.0"
+description = "Python Imaging Library (Fork)"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "Pillow-9.4.0-1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1"},
+ {file = "Pillow-9.4.0-1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12"},
+ {file = "Pillow-9.4.0-1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd"},
+ {file = "Pillow-9.4.0-1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9"},
+ {file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"},
+ {file = "Pillow-9.4.0-1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab"},
+ {file = "Pillow-9.4.0-1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9"},
+ {file = "Pillow-9.4.0-2-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0"},
+ {file = "Pillow-9.4.0-2-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f"},
+ {file = "Pillow-9.4.0-2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c"},
+ {file = "Pillow-9.4.0-2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848"},
+ {file = "Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1"},
+ {file = "Pillow-9.4.0-2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33"},
+ {file = "Pillow-9.4.0-2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9"},
+ {file = "Pillow-9.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157"},
+ {file = "Pillow-9.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47"},
+ {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"},
+ {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d"},
+ {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16a8df99701f9095bea8a6c4b3197da105df6f74e6176c5b410bc2df2fd29a57"},
+ {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5"},
+ {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed3e4b4e1e6de75fdc16d3259098de7c6571b1a6cc863b1a49e7d3d53e036070"},
+ {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5b2f8a31bd43e0f18172d8ac82347c8f37ef3e0b414431157718aa234991b28"},
+ {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35"},
+ {file = "Pillow-9.4.0-cp310-cp310-win32.whl", hash = "sha256:f09598b416ba39a8f489c124447b007fe865f786a89dbfa48bb5cf395693132a"},
+ {file = "Pillow-9.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6e78171be3fb7941f9910ea15b4b14ec27725865a73c15277bc39f5ca4f8391"},
+ {file = "Pillow-9.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:3fa1284762aacca6dc97474ee9c16f83990b8eeb6697f2ba17140d54b453e133"},
+ {file = "Pillow-9.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eaef5d2de3c7e9b21f1e762f289d17b726c2239a42b11e25446abf82b26ac132"},
+ {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4dfdae195335abb4e89cc9762b2edc524f3c6e80d647a9a81bf81e17e3fb6f0"},
+ {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6abfb51a82e919e3933eb137e17c4ae9c0475a25508ea88993bb59faf82f3b35"},
+ {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451f10ef963918e65b8869e17d67db5e2f4ab40e716ee6ce7129b0cde2876eab"},
+ {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6663977496d616b618b6cfa43ec86e479ee62b942e1da76a2c3daa1c75933ef4"},
+ {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60e7da3a3ad1812c128750fc1bc14a7ceeb8d29f77e0a2356a8fb2aa8925287d"},
+ {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:19005a8e58b7c1796bc0167862b1f54a64d3b44ee5d48152b06bb861458bc0f8"},
+ {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a"},
+ {file = "Pillow-9.4.0-cp311-cp311-win32.whl", hash = "sha256:b222090c455d6d1a64e6b7bb5f4035c4dff479e22455c9eaa1bdd4c75b52c80c"},
+ {file = "Pillow-9.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba6612b6548220ff5e9df85261bddc811a057b0b465a1226b39bfb8550616aee"},
+ {file = "Pillow-9.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5f532a2ad4d174eb73494e7397988e22bf427f91acc8e6ebf5bb10597b49c493"},
+ {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dd5a9c3091a0f414a963d427f920368e2b6a4c2f7527fdd82cde8ef0bc7a327"},
+ {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef21af928e807f10bf4141cad4746eee692a0dd3ff56cfb25fce076ec3cc8abe"},
+ {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:847b114580c5cc9ebaf216dd8c8dbc6b00a3b7ab0131e173d7120e6deade1f57"},
+ {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:653d7fb2df65efefbcbf81ef5fe5e5be931f1ee4332c2893ca638c9b11a409c4"},
+ {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:46f39cab8bbf4a384ba7cb0bc8bae7b7062b6a11cfac1ca4bc144dea90d4a9f5"},
+ {file = "Pillow-9.4.0-cp37-cp37m-win32.whl", hash = "sha256:7ac7594397698f77bce84382929747130765f66406dc2cd8b4ab4da68ade4c6e"},
+ {file = "Pillow-9.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:46c259e87199041583658457372a183636ae8cd56dbf3f0755e0f376a7f9d0e6"},
+ {file = "Pillow-9.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:0e51f608da093e5d9038c592b5b575cadc12fd748af1479b5e858045fff955a9"},
+ {file = "Pillow-9.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:765cb54c0b8724a7c12c55146ae4647e0274a839fb6de7bcba841e04298e1011"},
+ {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:519e14e2c49fcf7616d6d2cfc5c70adae95682ae20f0395e9280db85e8d6c4df"},
+ {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d197df5489004db87d90b918033edbeee0bd6df3848a204bca3ff0a903bef837"},
+ {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b"},
+ {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:e1339790c083c5a4de48f688b4841f18df839eb3c9584a770cbd818b33e26d5d"},
+ {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:a96e6e23f2b79433390273eaf8cc94fec9c6370842e577ab10dabdcc7ea0a66b"},
+ {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f"},
+ {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d7081c084ceb58278dd3cf81f836bc818978c0ccc770cbbb202125ddabec6628"},
+ {file = "Pillow-9.4.0-cp38-cp38-win32.whl", hash = "sha256:df41112ccce5d47770a0c13651479fbcd8793f34232a2dd9faeccb75eb5d0d0d"},
+ {file = "Pillow-9.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a"},
+ {file = "Pillow-9.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0f3269304c1a7ce82f1759c12ce731ef9b6e95b6df829dccd9fe42912cc48569"},
+ {file = "Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cb362e3b0976dc994857391b776ddaa8c13c28a16f80ac6522c23d5257156bed"},
+ {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815"},
+ {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28676836c7796805914b76b1837a40f76827ee0d5398f72f7dcc634bae7c6264"},
+ {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e"},
+ {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:53dcb50fbdc3fb2c55431a9b30caeb2f7027fcd2aeb501459464f0214200a503"},
+ {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:e8c5cf126889a4de385c02a2c3d3aba4b00f70234bfddae82a5eaa3ee6d5e3e6"},
+ {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c6b1389ed66cdd174d040105123a5a1bc91d0aa7059c7261d20e583b6d8cbd2"},
+ {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0dd4c681b82214b36273c18ca7ee87065a50e013112eea7d78c7a1b89a739153"},
+ {file = "Pillow-9.4.0-cp39-cp39-win32.whl", hash = "sha256:6d9dfb9959a3b0039ee06c1a1a90dc23bac3b430842dcb97908ddde05870601c"},
+ {file = "Pillow-9.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:54614444887e0d3043557d9dbc697dbb16cfb5a35d672b7a0fcc1ed0cf1c600b"},
+ {file = "Pillow-9.4.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b9b752ab91e78234941e44abdecc07f1f0d8f51fb62941d32995b8161f68cfe5"},
+ {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3b56206244dc8711f7e8b7d6cad4663917cd5b2d950799425076681e8766286"},
+ {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aabdab8ec1e7ca7f1434d042bf8b1e92056245fb179790dc97ed040361f16bfd"},
+ {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db74f5562c09953b2c5f8ec4b7dfd3f5421f31811e97d1dbc0a7c93d6e3a24df"},
+ {file = "Pillow-9.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e9d7747847c53a16a729b6ee5e737cf170f7a16611c143d95aa60a109a59c336"},
+ {file = "Pillow-9.4.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b52ff4f4e002f828ea6483faf4c4e8deea8d743cf801b74910243c58acc6eda3"},
+ {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:575d8912dca808edd9acd6f7795199332696d3469665ef26163cd090fa1f8bfa"},
+ {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c4ed2ff6760e98d262e0cc9c9a7f7b8a9f61aa4d47c58835cdaf7b0b8811bb"},
+ {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e621b0246192d3b9cb1dc62c78cfa4c6f6d2ddc0ec207d43c0dedecb914f152a"},
+ {file = "Pillow-9.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9"},
+ {file = "Pillow-9.4.0.tar.gz", hash = "sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e"},
+]
+
+[package.extras]
+docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "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 = "platformdirs"
+version = "3.1.1"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "platformdirs-3.1.1-py3-none-any.whl", hash = "sha256:e5986afb596e4bb5bde29a79ac9061aa955b94fca2399b7aaac4090860920dd8"},
+ {file = "platformdirs-3.1.1.tar.gz", hash = "sha256:024996549ee88ec1a9aa99ff7f8fc819bb59e2c3477b410d90a16d32d6e707aa"},
+]
+
+[package.extras]
+docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
+
+[[package]]
+name = "pluggy"
+version = "1.0.0"
+description = "plugin and hook calling mechanisms for python"
+category = "dev"
+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 = "ply"
+version = "3.11"
+description = "Python Lex & Yacc"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"},
+ {file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"},
+]
+
+[[package]]
+name = "portalocker"
+version = "2.7.0"
+description = "Wraps the portalocker recipe for easy usage"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "portalocker-2.7.0-py2.py3-none-any.whl", hash = "sha256:a07c5b4f3985c3cf4798369631fb7011adb498e2a46d8440efc75a8f29a0f983"},
+ {file = "portalocker-2.7.0.tar.gz", hash = "sha256:032e81d534a88ec1736d03f780ba073f047a06c478b06e2937486f334e955c51"},
+]
+
+[package.dependencies]
+pywin32 = {version = ">=226", markers = "platform_system == \"Windows\""}
+
+[package.extras]
+docs = ["sphinx (>=1.7.1)"]
+redis = ["redis"]
+tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "pytest-timeout (>=2.1.0)", "redis", "sphinx (>=6.0.0)"]
+
+[[package]]
+name = "prometheus-client"
+version = "0.16.0"
+description = "Python client for the Prometheus monitoring system."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "prometheus_client-0.16.0-py3-none-any.whl", hash = "sha256:0836af6eb2c8f4fed712b2f279f6c0a8bbab29f9f4aa15276b91c7cb0d1616ab"},
+ {file = "prometheus_client-0.16.0.tar.gz", hash = "sha256:a03e35b359f14dd1630898543e2120addfdeacd1a6069c1367ae90fd93ad3f48"},
+]
+
+[package.extras]
+twisted = ["twisted"]
+
+[[package]]
+name = "prompt-toolkit"
+version = "3.0.38"
+description = "Library for building powerful interactive command lines in Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7.0"
+files = [
+ {file = "prompt_toolkit-3.0.38-py3-none-any.whl", hash = "sha256:45ea77a2f7c60418850331366c81cf6b5b9cf4c7fd34616f733c5427e6abbb1f"},
+ {file = "prompt_toolkit-3.0.38.tar.gz", hash = "sha256:23ac5d50538a9a38c8bde05fecb47d0b403ecd0662857a86f886f798563d5b9b"},
+]
+
+[package.dependencies]
+wcwidth = "*"
+
+[[package]]
+name = "protobuf"
+version = "4.22.1"
+description = ""
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "protobuf-4.22.1-cp310-abi3-win32.whl", hash = "sha256:85aa9acc5a777adc0c21b449dafbc40d9a0b6413ff3a4f77ef9df194be7f975b"},
+ {file = "protobuf-4.22.1-cp310-abi3-win_amd64.whl", hash = "sha256:8bc971d76c03f1dd49f18115b002254f2ddb2d4b143c583bb860b796bb0d399e"},
+ {file = "protobuf-4.22.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:5917412347e1da08ce2939eb5cd60650dfb1a9ab4606a415b9278a1041fb4d19"},
+ {file = "protobuf-4.22.1-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:9e12e2810e7d297dbce3c129ae5e912ffd94240b050d33f9ecf023f35563b14f"},
+ {file = "protobuf-4.22.1-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:953fc7904ef46900262a26374b28c2864610b60cdc8b272f864e22143f8373c4"},
+ {file = "protobuf-4.22.1-cp37-cp37m-win32.whl", hash = "sha256:6e100f7bc787cd0a0ae58dbf0ab8bbf1ee7953f862b89148b6cf5436d5e9eaa1"},
+ {file = "protobuf-4.22.1-cp37-cp37m-win_amd64.whl", hash = "sha256:87a6393fa634f294bf24d1cfe9fdd6bb605cbc247af81b9b10c4c0f12dfce4b3"},
+ {file = "protobuf-4.22.1-cp38-cp38-win32.whl", hash = "sha256:e3fb58076bdb550e75db06ace2a8b3879d4c4f7ec9dd86e4254656118f4a78d7"},
+ {file = "protobuf-4.22.1-cp38-cp38-win_amd64.whl", hash = "sha256:651113695bc2e5678b799ee5d906b5d3613f4ccfa61b12252cfceb6404558af0"},
+ {file = "protobuf-4.22.1-cp39-cp39-win32.whl", hash = "sha256:67b7d19da0fda2733702c2299fd1ef6cb4b3d99f09263eacaf1aa151d9d05f02"},
+ {file = "protobuf-4.22.1-cp39-cp39-win_amd64.whl", hash = "sha256:b8700792f88e59ccecfa246fa48f689d6eee6900eddd486cdae908ff706c482b"},
+ {file = "protobuf-4.22.1-py3-none-any.whl", hash = "sha256:3e19dcf4adbf608924d3486ece469dd4f4f2cf7d2649900f0efcd1a84e8fd3ba"},
+ {file = "protobuf-4.22.1.tar.gz", hash = "sha256:dce7a55d501c31ecf688adb2f6c3f763cf11bc0be815d1946a84d74772ab07a7"},
+]
+
+[[package]]
+name = "psutil"
+version = "5.9.4"
+description = "Cross-platform lib for process and system monitoring in Python."
+category = "dev"
+optional = false
+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 = "ptpython"
+version = "3.0.23"
+description = "Python REPL build on top of prompt_toolkit"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "ptpython-3.0.23-py2.py3-none-any.whl", hash = "sha256:51069503684169b21e1980734a9ba2e104643b7e6a50d3ca0e5669ea70d9e21c"},
+ {file = "ptpython-3.0.23.tar.gz", hash = "sha256:9fc9bec2cc51bc4000c1224d8c56241ce8a406b3d49ec8dc266f78cd3cd04ba4"},
+]
+
+[package.dependencies]
+appdirs = "*"
+jedi = ">=0.16.0"
+prompt-toolkit = ">=3.0.28,<3.1.0"
+pygments = "*"
+
+[package.extras]
+all = ["black"]
+ptipython = ["ipython"]
+
+[[package]]
+name = "ptyprocess"
+version = "0.7.0"
+description = "Run a subprocess in a pseudo terminal"
+category = "dev"
+optional = false
+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 = "pudb"
+version = "2022.1.3"
+description = "A full-screen, console-based Python debugger"
+category = "dev"
+optional = false
+python-versions = "~=3.6"
+files = [
+ {file = "pudb-2022.1.3.tar.gz", hash = "sha256:58e83ada9e19ffe92c1fdc78ae5458ef91aeb892a5b8f0e7379e6fa61e0e664a"},
+]
+
+[package.dependencies]
+jedi = ">=0.18,<1"
+packaging = ">=20.0"
+pygments = ">=2.7.4"
+urwid = ">=1.1.1"
+urwid_readline = "*"
+
+[package.extras]
+completion = ["shtab"]
+
+[[package]]
+name = "pure-eval"
+version = "0.2.2"
+description = "Safely evaluate AST nodes without side effects"
+category = "dev"
+optional = false
+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 = "pyasn1"
+version = "0.4.8"
+description = "ASN.1 types and codecs"
+category = "dev"
+optional = false
+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."
+category = "dev"
+optional = false
+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 = "pybind11"
+version = "2.10.4"
+description = "Seamless operability between C++11 and Python"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "pybind11-2.10.4-py3-none-any.whl", hash = "sha256:ec9be0c45061c829648d7e8c98a7d041768b768c934acd15196e0f1943d9a818"},
+ {file = "pybind11-2.10.4.tar.gz", hash = "sha256:0bb621d3c45a049aa5923debb87c5c0e2668227905c55ebe8af722608d8ed927"},
+]
+
+[package.extras]
+global = ["pybind11-global (==2.10.4)"]
+
+[[package]]
+name = "pycodestyle"
+version = "2.10.0"
+description = "Python style guide checker"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"},
+ {file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"},
+]
+
+[[package]]
+name = "pycparser"
+version = "2.21"
+description = "C parser in Python"
+category = "dev"
+optional = false
+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 = "pydantic"
+version = "1.10.7"
+description = "Data validation and settings management using python type hints"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pydantic-1.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d"},
+ {file = "pydantic-1.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e"},
+ {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a"},
+ {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f"},
+ {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209"},
+ {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af"},
+ {file = "pydantic-1.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a"},
+ {file = "pydantic-1.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1"},
+ {file = "pydantic-1.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe"},
+ {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd"},
+ {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb"},
+ {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b"},
+ {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca"},
+ {file = "pydantic-1.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d"},
+ {file = "pydantic-1.10.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918"},
+ {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe"},
+ {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee"},
+ {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1"},
+ {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a"},
+ {file = "pydantic-1.10.7-cp37-cp37m-win_amd64.whl", hash = "sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914"},
+ {file = "pydantic-1.10.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd"},
+ {file = "pydantic-1.10.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245"},
+ {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d"},
+ {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3"},
+ {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52"},
+ {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209"},
+ {file = "pydantic-1.10.7-cp38-cp38-win_amd64.whl", hash = "sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e"},
+ {file = "pydantic-1.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143"},
+ {file = "pydantic-1.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e"},
+ {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d"},
+ {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f"},
+ {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd"},
+ {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5"},
+ {file = "pydantic-1.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e"},
+ {file = "pydantic-1.10.7-py3-none-any.whl", hash = "sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6"},
+ {file = "pydantic-1.10.7.tar.gz", hash = "sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e"},
+]
+
+[package.dependencies]
+typing-extensions = ">=4.2.0"
+
+[package.extras]
+dotenv = ["python-dotenv (>=0.10.4)"]
+email = ["email-validator (>=1.0.3)"]
+
+[[package]]
+name = "pydocstyle"
+version = "6.2.3"
+description = "Python docstring style checker"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "pydocstyle-6.2.3-py3-none-any.whl", hash = "sha256:a04ed1e6fe0be0970eddbb1681a7ab59b11eb92729fdb4b9b24f0eb11a25629e"},
+ {file = "pydocstyle-6.2.3.tar.gz", hash = "sha256:d867acad25e48471f2ad8a40ef9813125e954ad675202245ca836cb6e28b2297"},
+]
+
+[package.dependencies]
+snowballstemmer = ">=2.2.0"
+
+[package.extras]
+toml = ["tomli (>=1.2.3)"]
+
+[[package]]
+name = "pyembree"
+version = "0.2.11"
+description = "Python wrapper for Intel Embree 2.17.7"
+category = "main"
+optional = false
+python-versions = ">=3.8,<3.11"
+files = [
+ {file = "pyembree-0.2.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fea0e9004fff0ca26854bd2bc5baa06fe7cc7816e6713f9505f652c25d8d8c9"},
+]
+
+[package.dependencies]
+Cython = ">=0.29.28,<0.30.0"
+numpy = ">=1.22.2,<2.0.0"
+Rtree = ">=1.0.0,<2.0.0"
+setuptools = ">=60.9.3,<61.0.0"
+trimesh = ">=3.10.7,<4.0.0"
+wheel = ">=0.37.1,<0.38.0"
+
+[package.source]
+type = "url"
+url = "https://folk.ntnu.no/pederbs/pypy/pep503/pyembree/pyembree-0.2.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
+
+[[package]]
+name = "pyflakes"
+version = "3.0.1"
+description = "passive checker of Python programs"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"},
+ {file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"},
+]
+
+[[package]]
+name = "pygame"
+version = "2.3.0"
+description = "Python Game Development"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "pygame-2.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3e9535cf1af0c6ca38d94e0b492fc41057d7bf05e9bd64d3ed3e216d336d6d11"},
+ {file = "pygame-2.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:23bd3c3a6d4e8acddee2297d609dbc5953d6ba99b0f0cc5ccc2f567889db3785"},
+ {file = "pygame-2.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:619eed2d97f28af9d4cdb217a5517fd6f59b873f2f1d31b4489ed852b9a175c3"},
+ {file = "pygame-2.3.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9ccac73a8c913809ba2c1408d750abf14e45666b3c83493370441c52e99222b4"},
+ {file = "pygame-2.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ec8e691407b6c91525b2d7c8386fd6232b97d8f8c33d134ec0c0165b1d52c24"},
+ {file = "pygame-2.3.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8308b21804d137a3b7cafbd020d2159eb5bcc18ffc9c3993b20311069c326a2c"},
+ {file = "pygame-2.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1d737db18f4c94b620613c6a047a3a1eecc0f36df7d5da4070de575930cc5f0"},
+ {file = "pygame-2.3.0-cp310-cp310-win32.whl", hash = "sha256:788717d0b9a0d0828a763381e1eb6a127ceef815f9a91ff52217ed4b78df62fc"},
+ {file = "pygame-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:e3948be800b5f251a0741ec3aab3ca508dfc391095726a69af7064fa4d3e0547"},
+ {file = "pygame-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:82e5806fd797bd1b27fae705683f6822ae5276ec9cda42e6e21bba61985b763a"},
+ {file = "pygame-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fab0457ab07e8abb99de2b83c0a71f98bdf79afb01ff611873e4333fd8649f02"},
+ {file = "pygame-2.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad8fa7a91fa8f2a4fa46366142763675a0a11b7c34b06dfc20b1095d116da820"},
+ {file = "pygame-2.3.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfff49dbb7fcc2a9a88e3f25fda7f181ee4957fd89df78c47fa64c689d19b8a9"},
+ {file = "pygame-2.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5afd712bd7307d034e6940f3025c4b769656fd4cbb38fbdbd6af0f93d6c8386"},
+ {file = "pygame-2.3.0-cp311-cp311-win32.whl", hash = "sha256:fa18acc2d6f0d09575802e1db11845fc0f83f9777cc385c51380125df92f3dc9"},
+ {file = "pygame-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:101c57141d705ca1930377c324d2c7acd3099f1b4ac676981bdf5d5b329842c8"},
+ {file = "pygame-2.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:17730a2ed1001e5876745702c92906ad31ecedc13825efba56a0cba92e273b7a"},
+ {file = "pygame-2.3.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4b334f6dd6c1412dd4b161a8562b7a422db957f67b7eb93e927606e2dd435882"},
+ {file = "pygame-2.3.0-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4db1b103025fd4b451dfa409c0da16d2ff31714ae82bdf45b1434863cd69370b"},
+ {file = "pygame-2.3.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d339f90cc30de4b013670de84abd46de4be602d5c52bbe4e569fa15d17b204ca"},
+ {file = "pygame-2.3.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7390815dad55a2db9f8daac6f2c2e593801daea2d674433a72b91ea1caee0d3"},
+ {file = "pygame-2.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a1e473c627acf369b30bb52fb5f39d1f68f8c204aa857578b72f07a23c952b"},
+ {file = "pygame-2.3.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:228514c0d034c840b8ee6bf99185df34ac15e6a6a99684b8a3900124417c8d8f"},
+ {file = "pygame-2.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a8b315203925724f89a81a741682589ba1c36ec858d98e6accb7501ece9e99a3"},
+ {file = "pygame-2.3.0-cp36-cp36m-win32.whl", hash = "sha256:38642c6cc6477db6ebddd52be39bad0a9e19cf097f83feaaf8e7573b9a9d2405"},
+ {file = "pygame-2.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:525e11a2b9182ec84d690634016009e382ab8b488593c3f150a0b8aae28aa165"},
+ {file = "pygame-2.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:32bdf1d5d9e0763779d0b915d4617253949a6c118c4c6b5ae1a77cf1df964e4c"},
+ {file = "pygame-2.3.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f57b1ee40387e43ab5c3cf20437283477b5ef52ead4bb1d9bff254ef9ee70623"},
+ {file = "pygame-2.3.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6ccde93b51d2393216f98e8f81cf5cc628513d837c89dcf5b588f52031659c09"},
+ {file = "pygame-2.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c60be419d7cca1222895dfe9d520628b7346015208382a19fa678356a22664b3"},
+ {file = "pygame-2.3.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43f238229b3a9e5692ba5a31638f1c148257b37a49ef21f03b23b34d7f00b2d9"},
+ {file = "pygame-2.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d628637d4f0c55613f258b84eef932faf89e683aa842f4fd483a676f44a38606"},
+ {file = "pygame-2.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:35f5a9cc7a9a2ea3d048e418e79f30e1506cb47015939330903026c636761aab"},
+ {file = "pygame-2.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:703d5def9d4dbe9c358f63151bee4a55e328dd7737e692f52522bc44be7c7c8c"},
+ {file = "pygame-2.3.0-cp37-cp37m-win32.whl", hash = "sha256:53e9418c457fa549294feee7947bc0b24b048b4eba133f0e757dd2348d15af3b"},
+ {file = "pygame-2.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:0a664cd6c50870f6749c389a8844318afc8a2d02f8cb7b05d67930fdf99252bd"},
+ {file = "pygame-2.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf236758429d9b9cdadd1fcf40901588818ee440178b932409c40157ab41e902"},
+ {file = "pygame-2.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d035ba196c258876a87451fa7de65b62c087d7016e51000e8d95bc67c8584f7"},
+ {file = "pygame-2.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:57180b3aabbe17d8017aa724887019943d96ea69810f4315f5c1b7d4f64861f9"},
+ {file = "pygame-2.3.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:246f75f67d2ad4c2dad21b1f35c6092d67c4c0db13b2fa0a42d794e6e2794f47"},
+ {file = "pygame-2.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:033352321cc49d60fdc3c3ae4b3e10ecb6614846fb2eb3453c729aba48a2874d"},
+ {file = "pygame-2.3.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ee86606c6c7f61176ed24b427fa230fe4fc9f552aa555b8db21ddb608b4ce88"},
+ {file = "pygame-2.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d949e93fbdaf5b43f69a484639104c07028f93686c8305afb0d8e382fde8ff5d"},
+ {file = "pygame-2.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2acf958513bd1612960ec68aa5e388262218f7365db59e54e1ee68a55bc544b"},
+ {file = "pygame-2.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6c5d33355dfb66382bcac1fcf3db64ba71bc9e97082db3ae45a7a0d335e73268"},
+ {file = "pygame-2.3.0-cp38-cp38-win32.whl", hash = "sha256:1eda9f30d376d4205e8204e542ab1348dcbb31755c8ba38772e48a3b2f91b2fc"},
+ {file = "pygame-2.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b507df9ea606a87c29e5028b8de9f35066a15f6a5d7f3e5b47b3719e9403f924"},
+ {file = "pygame-2.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25c1b1819211aaa0f98264e6b670a496a9975079d5ae2dffd304b0aca6b1aa3c"},
+ {file = "pygame-2.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e516bc6bba5455817bbb0038f4c44d1914aac13c7f7954dee9213c9ae28bd9ac"},
+ {file = "pygame-2.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:740b9f311c693b00d86a89cc6846afc1d1e013b006975eb8be0b18d5481c5b32"},
+ {file = "pygame-2.3.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:932034e1738873a55c4e2eb83b6e8c03f9a55feaa6a04a7da7b1e0e5a5050b4a"},
+ {file = "pygame-2.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:774233845099d632de676ad4d4dd08ba27ebce5bfa550b1dc9f6cce145e21c35"},
+ {file = "pygame-2.3.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f79a3c5e7f24474d6e722d597ee03d2b0d17958c77d4307787147cf339b4ad9"},
+ {file = "pygame-2.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84fad9538012f1d6b298dcf690c4336e0317fe97ac10993b4d847ff547e919dd"},
+ {file = "pygame-2.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:910678441d02c3b55ac59fcbc4220a824b094407de084734b5d84e0900d6448b"},
+ {file = "pygame-2.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:653ec5102b9cb13a24e26663a81d7810790e56b88113b90aa5fdca681c01a5b9"},
+ {file = "pygame-2.3.0-cp39-cp39-win32.whl", hash = "sha256:e62607c86e02d29ba5cb00837f73b1dce7b325a1f1f6d93150a0f96fa68da1a1"},
+ {file = "pygame-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:90931a210325274184860d898df4e87a0972654edbb2a6185afcdce32244dfb6"},
+ {file = "pygame-2.3.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:1dc89d825e0ccba5ba3605abbd83be1401e0a32de7ab64b9647a6bb1ecb0a4f7"},
+ {file = "pygame-2.3.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e323b75abda43345aff5ab2f6b1c017135f937f8a114d7aac8d95a07d200e19f"},
+ {file = "pygame-2.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e13de2947c496fcb600fa4b5cd00a5fa33d4b3af9d13c169a5f79268268de0a8"},
+ {file = "pygame-2.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:555234ed6b08242af95406fd3eb43255c3ce8e915e8c751f2d411bd40d574df4"},
+ {file = "pygame-2.3.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:858d3968aebaca5015ef0ec82c513114a3c3fe64ce910222cfa852a39f03b135"},
+ {file = "pygame-2.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:250b3ec3f90b05ad50cb0070d994a0a1f39fffe8181fc9508b8749884c313431"},
+ {file = "pygame-2.3.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a5e83bd89da26f8360e02d5de2d2575981b0ebad81ea6d48aba610dabf167b88"},
+ {file = "pygame-2.3.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2961d44593aaa99580971e4123db00d4ca72fb4b30fa56350b3f6792331a41e"},
+ {file = "pygame-2.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:385163fd1ed8809a72be68fddc9c76876c304e8712695aff2ea49adf3831caf9"},
+ {file = "pygame-2.3.0.tar.gz", hash = "sha256:884b92c9cbf0bfaf8b8dd0f75a746613c55447d307ddd1addf903709b3b9f89f"},
+]
+
+[[package]]
+name = "pyglet"
+version = "2.0.5"
+description = "Cross-platform windowing and multimedia library"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pyglet-2.0.5-py3-none-any.whl", hash = "sha256:b0b94d0a3f9d0016bee506566fd13d7c62b5b9d10b6e16d32765d654959ba4dc"},
+ {file = "pyglet-2.0.5.zip", hash = "sha256:c47ff4eded95104d030e0697eedd6082b61dc987460bbca83ec47b6e7cbfd38a"},
+]
+
+[[package]]
+name = "pygments"
+version = "2.14.0"
+description = "Pygments is a syntax highlighting package written in Python."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"},
+ {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"},
+]
+
+[package.extras]
+plugins = ["importlib-metadata"]
+
+[[package]]
+name = "pykeops"
+version = "2.1.1"
+description = "Python bindings of KeOps: KErnel OPerationS, on CPUs and GPUs, with autodiff and without memory overflows"
+category = "main"
+optional = false
+python-versions = ">=3"
+files = [
+ {file = "pykeops-2.1.1.tar.gz", hash = "sha256:1931823c746345ce5a5805adad6baa1add772c6fe1800375f7f9a3ddb38b6f71"},
+]
+
+[package.dependencies]
+keopscore = "2.1.1"
+numpy = "*"
+pybind11 = "*"
+
+[package.extras]
+full = ["breathe", "faiss", "gpytorch", "h5py", "imageio", "jax", "jaxlib", "matplotlib", "multiprocess", "recommonmark", "scikit-learn", "sphinx", "sphinx-gallery", "sphinx-prompt", "sphinx_rtd_theme", "sphinxcontrib-httpdomain", "torch"]
+test = ["numpy", "pytest", "torch"]
+
+[[package]]
+name = "pylint"
+version = "2.17.1"
+description = "python code static checker"
+category = "dev"
+optional = false
+python-versions = ">=3.7.2"
+files = [
+ {file = "pylint-2.17.1-py3-none-any.whl", hash = "sha256:8660a54e3f696243d644fca98f79013a959c03f979992c1ab59c24d3f4ec2700"},
+ {file = "pylint-2.17.1.tar.gz", hash = "sha256:d4d009b0116e16845533bc2163493d6681846ac725eab8ca8014afb520178ddd"},
+]
+
+[package.dependencies]
+astroid = ">=2.15.0,<=2.17.0-dev0"
+colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
+dill = {version = ">=0.2", markers = "python_version < \"3.11\""}
+isort = ">=4.2.5,<6"
+mccabe = ">=0.6,<0.8"
+platformdirs = ">=2.2.0"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+tomlkit = ">=0.10.1"
+
+[package.extras]
+spelling = ["pyenchant (>=3.2,<4.0)"]
+testutils = ["gitpython (>3)"]
+
+[[package]]
+name = "pyopengl"
+version = "3.1.0"
+description = "Standard OpenGL bindings for Python"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "PyOpenGL-3.1.0.tar.gz", hash = "sha256:9b47c5c3a094fa518ca88aeed35ae75834d53e4285512c61879f67a48c94ddaf"},
+ {file = "PyOpenGL-3.1.0.zip", hash = "sha256:efa4e39a49b906ccbe66758812ca81ced13a6f26931ab2ba2dba2750c016c0d0"},
+]
+
+[[package]]
+name = "pyparsing"
+version = "3.0.9"
+description = "pyparsing module - Classes and methods to define and execute parsing grammars"
+category = "main"
+optional = false
+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 = "pyqt5"
+version = "5.15.9"
+description = "Python bindings for the Qt cross platform application toolkit"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "PyQt5-5.15.9-cp37-abi3-macosx_10_13_x86_64.whl", hash = "sha256:883ba5c8a348be78c8be6a3d3ba014c798e679503bce00d76c666c2dc6afe828"},
+ {file = "PyQt5-5.15.9-cp37-abi3-manylinux_2_17_x86_64.whl", hash = "sha256:dd5ce10e79fbf1df29507d2daf99270f2057cdd25e4de6fbf2052b46c652e3a5"},
+ {file = "PyQt5-5.15.9-cp37-abi3-win32.whl", hash = "sha256:e45c5cc15d4fd26ab5cb0e5cdba60691a3e9086411f8e3662db07a5a4222a696"},
+ {file = "PyQt5-5.15.9-cp37-abi3-win_amd64.whl", hash = "sha256:e030d795df4cbbfcf4f38b18e2e119bcc9e177ef658a5094b87bb16cac0ce4c5"},
+ {file = "PyQt5-5.15.9.tar.gz", hash = "sha256:dc41e8401a90dc3e2b692b411bd5492ab559ae27a27424eed4bd3915564ec4c0"},
+]
+
+[package.dependencies]
+PyQt5-Qt5 = ">=5.15.2"
+PyQt5-sip = ">=12.11,<13"
+
+[[package]]
+name = "pyqt5-qt5"
+version = "5.15.2"
+description = "The subset of a Qt installation needed by PyQt5."
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "PyQt5_Qt5-5.15.2-py3-none-macosx_10_13_intel.whl", hash = "sha256:76980cd3d7ae87e3c7a33bfebfaee84448fd650bad6840471d6cae199b56e154"},
+ {file = "PyQt5_Qt5-5.15.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a"},
+ {file = "PyQt5_Qt5-5.15.2-py3-none-win32.whl", hash = "sha256:9cc7a768b1921f4b982ebc00a318ccb38578e44e45316c7a4a850e953e1dd327"},
+ {file = "PyQt5_Qt5-5.15.2-py3-none-win_amd64.whl", hash = "sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962"},
+]
+
+[[package]]
+name = "pyqt5-sip"
+version = "12.11.1"
+description = "The sip module support for PyQt5"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "PyQt5_sip-12.11.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a40a39a6136a90e10c31510295c2be924564fc6260691501cdde669bdc5edea5"},
+ {file = "PyQt5_sip-12.11.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:19b06164793177146c7f7604fe8389f44221a7bde196f2182457eb3e4229fa88"},
+ {file = "PyQt5_sip-12.11.1-cp310-cp310-win32.whl", hash = "sha256:3afb1d1c07adcfef5c8bb12356a2ec2ec094f324af4417735d43b1ecaf1bb1a4"},
+ {file = "PyQt5_sip-12.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:54dad6c2e5dab14e46f6822a889bbb1515bbd2061762273af10d26566d649bd9"},
+ {file = "PyQt5_sip-12.11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7218f6a1cefeb0b2fc26b89f15011f841aa4cd77786ccd863bf9792347fa38a8"},
+ {file = "PyQt5_sip-12.11.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6b1113538082a7dd63b908587f61ce28ba4c7b8341e801fdf305d53a50a878ab"},
+ {file = "PyQt5_sip-12.11.1-cp311-cp311-win32.whl", hash = "sha256:ac5f7ed06213d3bb203e33037f7c1a0716584c21f4f0922dcc044750e3659b80"},
+ {file = "PyQt5_sip-12.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:4f0497e2f5eeaea9f5a67b0e55c501168efa86df4e53aace2a46498b87bc55c1"},
+ {file = "PyQt5_sip-12.11.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b355d56483edc79dcba30be947a6b700856bb74beb90539e14cc4d92b9bad152"},
+ {file = "PyQt5_sip-12.11.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dd163d9cffc4a56ebb9dd6908c0f0cb0caff8080294d41f4fb60fc3be63ca434"},
+ {file = "PyQt5_sip-12.11.1-cp37-cp37m-win32.whl", hash = "sha256:b714f550ea6ddae94fd7acae531971e535f4a4e7277b62eb44e7c649cf3f03d0"},
+ {file = "PyQt5_sip-12.11.1-cp37-cp37m-win_amd64.whl", hash = "sha256:d09b2586235deab7a5f2e28e4bde9a70c0b3730fa84f2590804a9932414136a3"},
+ {file = "PyQt5_sip-12.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9a6f9c058564d0ac573561036299f54c452ae78b7d2a65d7c2d01685e6dca50d"},
+ {file = "PyQt5_sip-12.11.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fc920c0e0d5050474d2d6282b478e4957548bf1dce58e1b0678914514dc70064"},
+ {file = "PyQt5_sip-12.11.1-cp38-cp38-win32.whl", hash = "sha256:3358c584832f0ac9fd1ad3623d8a346c705f43414df1fcd0cb285a6ef51fec08"},
+ {file = "PyQt5_sip-12.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:f9691c6f4d899ca762dd54442a1be158c3e52017f583183da6ef37d5bae86595"},
+ {file = "PyQt5_sip-12.11.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0bc81cb9e171d29302d393775f95cfa01b7a15f61b199ab1812976e5c4cb2cb9"},
+ {file = "PyQt5_sip-12.11.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b077fb4383536f51382f5516f0347328a4f338c6ccc4c268cc358643bef1b838"},
+ {file = "PyQt5_sip-12.11.1-cp39-cp39-win32.whl", hash = "sha256:5c152878443c3e951d5db7df53509d444708dc06a121c267b548146be06b87f8"},
+ {file = "PyQt5_sip-12.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:bd935cc46dfdbb89c21042c1db2e46a71f25693af57272f146d6d9418e2934f1"},
+ {file = "PyQt5_sip-12.11.1.tar.gz", hash = "sha256:97d3fbda0f61edb1be6529ec2d5c7202ae83aee4353e4b264a159f8c9ada4369"},
+]
+
+[[package]]
+name = "pyrender"
+version = "0.1.45"
+description = "Easy-to-use Python renderer for 3D visualization"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pyrender-0.1.45-py3-none-any.whl", hash = "sha256:5cf751d1f21fba4640e830cef3a0b5a95ed0f05677bf92c6b8330056b4023aeb"},
+ {file = "pyrender-0.1.45.tar.gz", hash = "sha256:284b2432bf6832f05c5216c4b979ceb514ea78163bf53b8ce2bdf0069cb3b92e"},
+]
+
+[package.dependencies]
+freetype-py = "*"
+imageio = "*"
+networkx = "*"
+numpy = "*"
+Pillow = "*"
+pyglet = ">=1.4.10"
+PyOpenGL = "3.1.0"
+scipy = "*"
+six = "*"
+trimesh = "*"
+
+[package.extras]
+dev = ["flake8", "pre-commit", "pytest", "pytest-cov", "tox"]
+docs = ["sphinx", "sphinx-automodapi", "sphinx-rtd-theme"]
+
+[[package]]
+name = "pyrsistent"
+version = "0.19.3"
+description = "Persistent/Functional/Immutable data structures"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pyrsistent-0.19.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a"},
+ {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64"},
+ {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf"},
+ {file = "pyrsistent-0.19.3-cp310-cp310-win32.whl", hash = "sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a"},
+ {file = "pyrsistent-0.19.3-cp310-cp310-win_amd64.whl", hash = "sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da"},
+ {file = "pyrsistent-0.19.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9"},
+ {file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393"},
+ {file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19"},
+ {file = "pyrsistent-0.19.3-cp311-cp311-win32.whl", hash = "sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3"},
+ {file = "pyrsistent-0.19.3-cp311-cp311-win_amd64.whl", hash = "sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8"},
+ {file = "pyrsistent-0.19.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28"},
+ {file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf"},
+ {file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9"},
+ {file = "pyrsistent-0.19.3-cp37-cp37m-win32.whl", hash = "sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1"},
+ {file = "pyrsistent-0.19.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b"},
+ {file = "pyrsistent-0.19.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8"},
+ {file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a"},
+ {file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c"},
+ {file = "pyrsistent-0.19.3-cp38-cp38-win32.whl", hash = "sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c"},
+ {file = "pyrsistent-0.19.3-cp38-cp38-win_amd64.whl", hash = "sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7"},
+ {file = "pyrsistent-0.19.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc"},
+ {file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2"},
+ {file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3"},
+ {file = "pyrsistent-0.19.3-cp39-cp39-win32.whl", hash = "sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2"},
+ {file = "pyrsistent-0.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98"},
+ {file = "pyrsistent-0.19.3-py3-none-any.whl", hash = "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64"},
+ {file = "pyrsistent-0.19.3.tar.gz", hash = "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440"},
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.8.2"
+description = "Extensions to the standard Python datetime module"
+category = "main"
+optional = false
+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.7"
+description = "A python library adding a json log formatter"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "python-json-logger-2.0.7.tar.gz", hash = "sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c"},
+ {file = "python_json_logger-2.0.7-py3-none-any.whl", hash = "sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd"},
+]
+
+[[package]]
+name = "python-lsp-jsonrpc"
+version = "1.0.0"
+description = "JSON RPC 2.0 server library"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "python-lsp-jsonrpc-1.0.0.tar.gz", hash = "sha256:7bec170733db628d3506ea3a5288ff76aa33c70215ed223abdb0d95e957660bd"},
+ {file = "python_lsp_jsonrpc-1.0.0-py3-none-any.whl", hash = "sha256:079b143be64b0a378bdb21dff5e28a8c1393fe7e8a654ef068322d754e545fc7"},
+]
+
+[package.dependencies]
+ujson = ">=3.0.0"
+
+[package.extras]
+test = ["coverage", "pycodestyle", "pyflakes", "pylint", "pytest", "pytest-cov"]
+
+[[package]]
+name = "python-lsp-server"
+version = "1.7.1"
+description = "Python Language Server for the Language Server Protocol"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "python-lsp-server-1.7.1.tar.gz", hash = "sha256:67473bb301f35434b5fa8b21fc5ed5fac27dc8a8446ccec8bae456af52a0aef6"},
+ {file = "python_lsp_server-1.7.1-py3-none-any.whl", hash = "sha256:8f8b382868b161199aa385659b28427890be628d86f54810463a4d0ee0d6d091"},
+]
+
+[package.dependencies]
+autopep8 = {version = ">=1.6.0,<1.7.0", optional = true, markers = "extra == \"all\""}
+docstring-to-markdown = "*"
+flake8 = {version = ">=5.0.0,<7", optional = true, markers = "extra == \"all\""}
+jedi = ">=0.17.2,<0.19.0"
+mccabe = {version = ">=0.7.0,<0.8.0", optional = true, markers = "extra == \"all\""}
+pluggy = ">=1.0.0"
+pycodestyle = {version = ">=2.9.0,<2.11.0", optional = true, markers = "extra == \"all\""}
+pydocstyle = {version = ">=6.2.0,<6.3.0", optional = true, markers = "extra == \"all\""}
+pyflakes = {version = ">=2.5.0,<3.1.0", optional = true, markers = "extra == \"all\""}
+pylint = {version = ">=2.5.0,<3", optional = true, markers = "extra == \"all\""}
+python-lsp-jsonrpc = ">=1.0.0"
+rope = {version = ">1.2.0", optional = true, markers = "extra == \"all\""}
+setuptools = ">=39.0.0"
+ujson = ">=3.0.0"
+whatthepatch = {version = ">=1.0.2,<2.0.0", optional = true, markers = "extra == \"all\""}
+yapf = {version = "*", optional = true, markers = "extra == \"all\""}
+
+[package.extras]
+all = ["autopep8 (>=1.6.0,<1.7.0)", "flake8 (>=5.0.0,<7)", "mccabe (>=0.7.0,<0.8.0)", "pycodestyle (>=2.9.0,<2.11.0)", "pydocstyle (>=6.2.0,<6.3.0)", "pyflakes (>=2.5.0,<3.1.0)", "pylint (>=2.5.0,<3)", "rope (>1.2.0)", "whatthepatch (>=1.0.2,<2.0.0)", "yapf"]
+autopep8 = ["autopep8 (>=1.6.0,<1.7.0)"]
+flake8 = ["flake8 (>=5.0.0,<7)"]
+mccabe = ["mccabe (>=0.7.0,<0.8.0)"]
+pycodestyle = ["pycodestyle (>=2.9.0,<2.11.0)"]
+pydocstyle = ["pydocstyle (>=6.2.0,<6.3.0)"]
+pyflakes = ["pyflakes (>=2.5.0,<3.1.0)"]
+pylint = ["pylint (>=2.5.0,<3)"]
+rope = ["rope (>1.2.0)"]
+test = ["coverage", "flaky", "matplotlib", "numpy", "pandas", "pylint (>=2.5.0,<3)", "pyqt5", "pytest", "pytest-cov"]
+websockets = ["websockets (>=10.3)"]
+yapf = ["whatthepatch (>=1.0.2,<2.0.0)", "yapf"]
+
+[[package]]
+name = "pytoolconfig"
+version = "1.2.5"
+description = "Python tool configuration"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pytoolconfig-1.2.5-py3-none-any.whl", hash = "sha256:239ba9d3e537b91d0243275a497700ea39a5e259ddb80421c366e3b288bf30fe"},
+ {file = "pytoolconfig-1.2.5.tar.gz", hash = "sha256:a50f9dfe23b03a9d40414c1fdf902fefbeae12f2ac75a3c8f915944d6ffac279"},
+]
+
+[package.dependencies]
+packaging = ">=22.0"
+platformdirs = {version = ">=1.4.4", optional = true, markers = "extra == \"global\""}
+tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
+
+[package.extras]
+doc = ["sphinx (>=4.5.0)", "tabulate (>=0.8.9)"]
+gendocs = ["pytoolconfig[doc]", "sphinx (>=4.5.0)", "sphinx-autodoc-typehints (>=1.18.1)", "sphinx-rtd-theme (>=1.0.0)"]
+global = ["platformdirs (>=1.4.4)"]
+validation = ["pydantic (>=1.7.4)"]
+
+[[package]]
+name = "pytorch-lightning"
+version = "1.9.4"
+description = "PyTorch Lightning is the lightweight PyTorch wrapper for ML researchers. Scale your models. Write less boilerplate."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pytorch-lightning-1.9.4.tar.gz", hash = "sha256:188a7f4468acf23512e7f4903253d86fc7929a49f0c09d699872e364162001e8"},
+ {file = "pytorch_lightning-1.9.4-py3-none-any.whl", hash = "sha256:a2d2bd7657716087c294b076fe385ed17879764d6daaad0a541394a8f7164f93"},
+]
+
+[package.dependencies]
+fsspec = {version = ">2021.06.0", extras = ["http"]}
+lightning-utilities = ">=0.6.0.post0"
+numpy = ">=1.17.2"
+packaging = ">=17.1"
+PyYAML = ">=5.4"
+torch = ">=1.10.0"
+torchmetrics = ">=0.7.0"
+tqdm = ">=4.57.0"
+typing-extensions = ">=4.0.0"
+
+[package.extras]
+all = ["colossalai (>=0.2.0)", "deepspeed (>=0.6.0)", "fairscale (>=0.4.5)", "gym[classic-control] (>=0.17.0)", "hivemind (==1.1.5)", "horovod (>=0.21.2,!=0.24.0)", "hydra-core (>=1.0.5)", "ipython[all] (<8.7.1)", "jsonargparse[signatures] (>=4.18.0)", "matplotlib (>3.1)", "omegaconf (>=2.0.5)", "rich (>=10.14.0,!=10.15.0.a)", "tensorboardX (>=2.2)", "torchvision (>=0.11.1)"]
+colossalai = ["colossalai (>=0.2.0)"]
+deepspeed = ["deepspeed (>=0.6.0)"]
+dev = ["cloudpickle (>=1.3)", "codecov (==2.1.12)", "colossalai (>=0.2.0)", "coverage (==6.5.0)", "deepspeed (>=0.6.0)", "fairscale (>=0.4.5)", "fastapi (<0.87.0)", "gym[classic-control] (>=0.17.0)", "hivemind (==1.1.5)", "horovod (>=0.21.2,!=0.24.0)", "hydra-core (>=1.0.5)", "ipython[all] (<8.7.1)", "jsonargparse[signatures] (>=4.18.0)", "matplotlib (>3.1)", "omegaconf (>=2.0.5)", "onnxruntime (<1.14.0)", "pandas (>1.0)", "pre-commit (==2.20.0)", "protobuf (<=3.20.1)", "psutil (<5.9.5)", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-forked (==1.4.0)", "pytest-rerunfailures (==10.3)", "rich (>=10.14.0,!=10.15.0.a)", "scikit-learn (>0.22.1)", "tensorboard (>=2.9.1)", "tensorboardX (>=2.2)", "torchvision (>=0.11.1)", "uvicorn (<0.19.1)"]
+examples = ["gym[classic-control] (>=0.17.0)", "ipython[all] (<8.7.1)", "torchvision (>=0.11.1)"]
+extra = ["hydra-core (>=1.0.5)", "jsonargparse[signatures] (>=4.18.0)", "matplotlib (>3.1)", "omegaconf (>=2.0.5)", "rich (>=10.14.0,!=10.15.0.a)", "tensorboardX (>=2.2)"]
+fairscale = ["fairscale (>=0.4.5)"]
+hivemind = ["hivemind (==1.1.5)"]
+horovod = ["horovod (>=0.21.2,!=0.24.0)"]
+strategies = ["colossalai (>=0.2.0)", "deepspeed (>=0.6.0)", "fairscale (>=0.4.5)", "hivemind (==1.1.5)", "horovod (>=0.21.2,!=0.24.0)"]
+test = ["cloudpickle (>=1.3)", "codecov (==2.1.12)", "coverage (==6.5.0)", "fastapi (<0.87.0)", "onnxruntime (<1.14.0)", "pandas (>1.0)", "pre-commit (==2.20.0)", "protobuf (<=3.20.1)", "psutil (<5.9.5)", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-forked (==1.4.0)", "pytest-rerunfailures (==10.3)", "scikit-learn (>0.22.1)", "tensorboard (>=2.9.1)", "uvicorn (<0.19.1)"]
+
+[[package]]
+name = "pytorch3d"
+version = "0.7.2"
+description = "PyTorch3D is FAIR's library of reusable components for deep Learning with 3D data."
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pytorch3d-0.7.2-cp310-cp310-linux_x86_64.whl", hash = "sha256:52f9af847687efa608825452d6c54d398af56bccf72cfcb0a44bd06a67b8ee70"},
+]
+
+[package.dependencies]
+fvcore = "*"
+iopath = "*"
+
+[package.extras]
+all = ["imageio", "ipywidgets", "matplotlib", "tqdm (>4.29.0)"]
+dev = ["flake8", "usort"]
+implicitron = ["accelerate", "hydra-core (>=1.1)", "lpips", "matplotlib", "tqdm (>4.29.0)", "visdom"]
+
+[package.source]
+type = "url"
+url = "https://dl.fbaipublicfiles.com/pytorch3d/packaging/wheels/py310_cu116_pyt1130/pytorch3d-0.7.2-cp310-cp310-linux_x86_64.whl"
+
+[[package]]
+name = "pytz"
+version = "2022.7.1"
+description = "World timezone definitions, modern and historical"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"},
+ {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"},
+]
+
+[[package]]
+name = "pywavelets"
+version = "1.4.1"
+description = "PyWavelets, wavelet transform module"
+category = "main"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "PyWavelets-1.4.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:d854411eb5ee9cb4bc5d0e66e3634aeb8f594210f6a1bed96dbed57ec70f181c"},
+ {file = "PyWavelets-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:231b0e0b1cdc1112f4af3c24eea7bf181c418d37922a67670e9bf6cfa2d544d4"},
+ {file = "PyWavelets-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:754fa5085768227c4f4a26c1e0c78bc509a266d9ebd0eb69a278be7e3ece943c"},
+ {file = "PyWavelets-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da7b9c006171be1f9ddb12cc6e0d3d703b95f7f43cb5e2c6f5f15d3233fcf202"},
+ {file = "PyWavelets-1.4.1-cp310-cp310-win32.whl", hash = "sha256:67a0d28a08909f21400cb09ff62ba94c064882ffd9e3a6b27880a111211d59bd"},
+ {file = "PyWavelets-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:91d3d393cffa634f0e550d88c0e3f217c96cfb9e32781f2960876f1808d9b45b"},
+ {file = "PyWavelets-1.4.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:64c6bac6204327321db30b775060fbe8e8642316e6bff17f06b9f34936f88875"},
+ {file = "PyWavelets-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f19327f2129fb7977bc59b966b4974dfd72879c093e44a7287500a7032695de"},
+ {file = "PyWavelets-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad987748f60418d5f4138db89d82ba0cb49b086e0cbb8fd5c3ed4a814cfb705e"},
+ {file = "PyWavelets-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:875d4d620eee655346e3589a16a73790cf9f8917abba062234439b594e706784"},
+ {file = "PyWavelets-1.4.1-cp311-cp311-win32.whl", hash = "sha256:7231461d7a8eb3bdc7aa2d97d9f67ea5a9f8902522818e7e2ead9c2b3408eeb1"},
+ {file = "PyWavelets-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:daf0aa79842b571308d7c31a9c43bc99a30b6328e6aea3f50388cd8f69ba7dbc"},
+ {file = "PyWavelets-1.4.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:ab7da0a17822cd2f6545626946d3b82d1a8e106afc4b50e3387719ba01c7b966"},
+ {file = "PyWavelets-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:578af438a02a86b70f1975b546f68aaaf38f28fb082a61ceb799816049ed18aa"},
+ {file = "PyWavelets-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb5ca8d11d3f98e89e65796a2125be98424d22e5ada360a0dbabff659fca0fc"},
+ {file = "PyWavelets-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:058b46434eac4c04dd89aeef6fa39e4b6496a951d78c500b6641fd5b2cc2f9f4"},
+ {file = "PyWavelets-1.4.1-cp38-cp38-win32.whl", hash = "sha256:de7cd61a88a982edfec01ea755b0740e94766e00a1ceceeafef3ed4c85c605cd"},
+ {file = "PyWavelets-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:7ab8d9db0fe549ab2ee0bea61f614e658dd2df419d5b75fba47baa761e95f8f2"},
+ {file = "PyWavelets-1.4.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:23bafd60350b2b868076d976bdd92f950b3944f119b4754b1d7ff22b7acbf6c6"},
+ {file = "PyWavelets-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d0e56cd7a53aed3cceca91a04d62feb3a0aca6725b1912d29546c26f6ea90426"},
+ {file = "PyWavelets-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:030670a213ee8fefa56f6387b0c8e7d970c7f7ad6850dc048bd7c89364771b9b"},
+ {file = "PyWavelets-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71ab30f51ee4470741bb55fc6b197b4a2b612232e30f6ac069106f0156342356"},
+ {file = "PyWavelets-1.4.1-cp39-cp39-win32.whl", hash = "sha256:47cac4fa25bed76a45bc781a293c26ac63e8eaae9eb8f9be961758d22b58649c"},
+ {file = "PyWavelets-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:88aa5449e109d8f5e7f0adef85f7f73b1ab086102865be64421a3a3d02d277f4"},
+ {file = "PyWavelets-1.4.1.tar.gz", hash = "sha256:6437af3ddf083118c26d8f97ab43b0724b956c9f958e9ea788659f6a2834ba93"},
+]
+
+[package.dependencies]
+numpy = ">=1.17.3"
+
+[[package]]
+name = "pywin32"
+version = "305"
+description = "Python for Window Extensions"
+category = "main"
+optional = false
+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.10"
+description = "Pseudo terminal support for Windows from Python."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pywinpty-2.0.10-cp310-none-win_amd64.whl", hash = "sha256:4c7d06ad10f6e92bc850a467f26d98f4f30e73d2fe5926536308c6ae0566bc16"},
+ {file = "pywinpty-2.0.10-cp311-none-win_amd64.whl", hash = "sha256:7ffbd66310b83e42028fc9df7746118978d94fba8c1ebf15a7c1275fdd80b28a"},
+ {file = "pywinpty-2.0.10-cp37-none-win_amd64.whl", hash = "sha256:38cb924f2778b5751ef91a75febd114776b3af0ae411bc667be45dd84fc881d3"},
+ {file = "pywinpty-2.0.10-cp38-none-win_amd64.whl", hash = "sha256:902d79444b29ad1833b8d5c3c9aabdfd428f4f068504430df18074007c8c0de8"},
+ {file = "pywinpty-2.0.10-cp39-none-win_amd64.whl", hash = "sha256:3c46aef80dd50979aff93de199e4a00a8ee033ba7a03cadf0a91fed45f0c39d7"},
+ {file = "pywinpty-2.0.10.tar.gz", hash = "sha256:cdbb5694cf8c7242c2ecfaca35c545d31fa5d5814c3d67a4e628f803f680ebea"},
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0"
+description = "YAML parser and emitter for Python"
+category = "main"
+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 = "25.0.2"
+description = "Python bindings for 0MQ"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "pyzmq-25.0.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:ac178e666c097c8d3deb5097b58cd1316092fc43e8ef5b5fdb259b51da7e7315"},
+ {file = "pyzmq-25.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:659e62e1cbb063151c52f5b01a38e1df6b54feccfa3e2509d44c35ca6d7962ee"},
+ {file = "pyzmq-25.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8280ada89010735a12b968ec3ea9a468ac2e04fddcc1cede59cb7f5178783b9c"},
+ {file = "pyzmq-25.0.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b5eeb5278a8a636bb0abdd9ff5076bcbb836cd2302565df53ff1fa7d106d54"},
+ {file = "pyzmq-25.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a2e5fe42dfe6b73ca120b97ac9f34bfa8414feb15e00e37415dbd51cf227ef6"},
+ {file = "pyzmq-25.0.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:827bf60e749e78acb408a6c5af6688efbc9993e44ecc792b036ec2f4b4acf485"},
+ {file = "pyzmq-25.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7b504ae43d37e282301da586529e2ded8b36d4ee2cd5e6db4386724ddeaa6bbc"},
+ {file = "pyzmq-25.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb1f69a0a2a2b1aae8412979dd6293cc6bcddd4439bf07e4758d864ddb112354"},
+ {file = "pyzmq-25.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b9c9cc965cdf28381e36da525dcb89fc1571d9c54800fdcd73e3f73a2fc29bd"},
+ {file = "pyzmq-25.0.2-cp310-cp310-win32.whl", hash = "sha256:24abbfdbb75ac5039205e72d6c75f10fc39d925f2df8ff21ebc74179488ebfca"},
+ {file = "pyzmq-25.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6a821a506822fac55d2df2085a52530f68ab15ceed12d63539adc32bd4410f6e"},
+ {file = "pyzmq-25.0.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:9af0bb0277e92f41af35e991c242c9c71920169d6aa53ade7e444f338f4c8128"},
+ {file = "pyzmq-25.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54a96cf77684a3a537b76acfa7237b1e79a8f8d14e7f00e0171a94b346c5293e"},
+ {file = "pyzmq-25.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88649b19ede1cab03b96b66c364cbbf17c953615cdbc844f7f6e5f14c5e5261c"},
+ {file = "pyzmq-25.0.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:715cff7644a80a7795953c11b067a75f16eb9fc695a5a53316891ebee7f3c9d5"},
+ {file = "pyzmq-25.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:312b3f0f066b4f1d17383aae509bacf833ccaf591184a1f3c7a1661c085063ae"},
+ {file = "pyzmq-25.0.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d488c5c8630f7e782e800869f82744c3aca4aca62c63232e5d8c490d3d66956a"},
+ {file = "pyzmq-25.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:38d9f78d69bcdeec0c11e0feb3bc70f36f9b8c44fc06e5d06d91dc0a21b453c7"},
+ {file = "pyzmq-25.0.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3059a6a534c910e1d5d068df42f60d434f79e6cc6285aa469b384fa921f78cf8"},
+ {file = "pyzmq-25.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6526d097b75192f228c09d48420854d53dfbc7abbb41b0e26f363ccb26fbc177"},
+ {file = "pyzmq-25.0.2-cp311-cp311-win32.whl", hash = "sha256:5c5fbb229e40a89a2fe73d0c1181916f31e30f253cb2d6d91bea7927c2e18413"},
+ {file = "pyzmq-25.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed15e3a2c3c2398e6ae5ce86d6a31b452dfd6ad4cd5d312596b30929c4b6e182"},
+ {file = "pyzmq-25.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:032f5c8483c85bf9c9ca0593a11c7c749d734ce68d435e38c3f72e759b98b3c9"},
+ {file = "pyzmq-25.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:374b55516393bfd4d7a7daa6c3b36d6dd6a31ff9d2adad0838cd6a203125e714"},
+ {file = "pyzmq-25.0.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:08bfcc21b5997a9be4fefa405341320d8e7f19b4d684fb9c0580255c5bd6d695"},
+ {file = "pyzmq-25.0.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1a843d26a8da1b752c74bc019c7b20e6791ee813cd6877449e6a1415589d22ff"},
+ {file = "pyzmq-25.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:b48616a09d7df9dbae2f45a0256eee7b794b903ddc6d8657a9948669b345f220"},
+ {file = "pyzmq-25.0.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d4427b4a136e3b7f85516c76dd2e0756c22eec4026afb76ca1397152b0ca8145"},
+ {file = "pyzmq-25.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:26b0358e8933990502f4513c991c9935b6c06af01787a36d133b7c39b1df37fa"},
+ {file = "pyzmq-25.0.2-cp36-cp36m-win32.whl", hash = "sha256:c8fedc3ccd62c6b77dfe6f43802057a803a411ee96f14e946f4a76ec4ed0e117"},
+ {file = "pyzmq-25.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:2da6813b7995b6b1d1307329c73d3e3be2fd2d78e19acfc4eff2e27262732388"},
+ {file = "pyzmq-25.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a35960c8b2f63e4ef67fd6731851030df68e4b617a6715dd11b4b10312d19fef"},
+ {file = "pyzmq-25.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eef2a0b880ab40aca5a878933376cb6c1ec483fba72f7f34e015c0f675c90b20"},
+ {file = "pyzmq-25.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:85762712b74c7bd18e340c3639d1bf2f23735a998d63f46bb6584d904b5e401d"},
+ {file = "pyzmq-25.0.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:64812f29d6eee565e129ca14b0c785744bfff679a4727137484101b34602d1a7"},
+ {file = "pyzmq-25.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:510d8e55b3a7cd13f8d3e9121edf0a8730b87d925d25298bace29a7e7bc82810"},
+ {file = "pyzmq-25.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b164cc3c8acb3d102e311f2eb6f3c305865ecb377e56adc015cb51f721f1dda6"},
+ {file = "pyzmq-25.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:28fdb9224a258134784a9cf009b59265a9dde79582fb750d4e88a6bcbc6fa3dc"},
+ {file = "pyzmq-25.0.2-cp37-cp37m-win32.whl", hash = "sha256:dd771a440effa1c36d3523bc6ba4e54ff5d2e54b4adcc1e060d8f3ca3721d228"},
+ {file = "pyzmq-25.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:9bdc40efb679b9dcc39c06d25629e55581e4c4f7870a5e88db4f1c51ce25e20d"},
+ {file = "pyzmq-25.0.2-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:1f82906a2d8e4ee310f30487b165e7cc8ed09c009e4502da67178b03083c4ce0"},
+ {file = "pyzmq-25.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:21ec0bf4831988af43c8d66ba3ccd81af2c5e793e1bf6790eb2d50e27b3c570a"},
+ {file = "pyzmq-25.0.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:abbce982a17c88d2312ec2cf7673985d444f1beaac6e8189424e0a0e0448dbb3"},
+ {file = "pyzmq-25.0.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9e1d2f2d86fc75ed7f8845a992c5f6f1ab5db99747fb0d78b5e4046d041164d2"},
+ {file = "pyzmq-25.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e92ff20ad5d13266bc999a29ed29a3b5b101c21fdf4b2cf420c09db9fb690e"},
+ {file = "pyzmq-25.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edbbf06cc2719889470a8d2bf5072bb00f423e12de0eb9ffec946c2c9748e149"},
+ {file = "pyzmq-25.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:77942243ff4d14d90c11b2afd8ee6c039b45a0be4e53fb6fa7f5e4fd0b59da39"},
+ {file = "pyzmq-25.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ab046e9cb902d1f62c9cc0eca055b1d11108bdc271caf7c2171487298f229b56"},
+ {file = "pyzmq-25.0.2-cp38-cp38-win32.whl", hash = "sha256:ad761cfbe477236802a7ab2c080d268c95e784fe30cafa7e055aacd1ca877eb0"},
+ {file = "pyzmq-25.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8560756318ec7c4c49d2c341012167e704b5a46d9034905853c3d1ade4f55bee"},
+ {file = "pyzmq-25.0.2-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:ab2c056ac503f25a63f6c8c6771373e2a711b98b304614151dfb552d3d6c81f6"},
+ {file = "pyzmq-25.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cca8524b61c0eaaa3505382dc9b9a3bc8165f1d6c010fdd1452c224225a26689"},
+ {file = "pyzmq-25.0.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cfb9f7eae02d3ac42fbedad30006b7407c984a0eb4189a1322241a20944d61e5"},
+ {file = "pyzmq-25.0.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5eaeae038c68748082137d6896d5c4db7927e9349237ded08ee1bbd94f7361c9"},
+ {file = "pyzmq-25.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a31992a8f8d51663ebf79df0df6a04ffb905063083d682d4380ab8d2c67257c"},
+ {file = "pyzmq-25.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6a979e59d2184a0c8f2ede4b0810cbdd86b64d99d9cc8a023929e40dce7c86cc"},
+ {file = "pyzmq-25.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1f124cb73f1aa6654d31b183810febc8505fd0c597afa127c4f40076be4574e0"},
+ {file = "pyzmq-25.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:65c19a63b4a83ae45d62178b70223adeee5f12f3032726b897431b6553aa25af"},
+ {file = "pyzmq-25.0.2-cp39-cp39-win32.whl", hash = "sha256:83d822e8687621bed87404afc1c03d83fa2ce39733d54c2fd52d8829edb8a7ff"},
+ {file = "pyzmq-25.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:24683285cc6b7bf18ad37d75b9db0e0fefe58404e7001f1d82bf9e721806daa7"},
+ {file = "pyzmq-25.0.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a4b4261eb8f9ed71f63b9eb0198dd7c934aa3b3972dac586d0ef502ba9ab08b"},
+ {file = "pyzmq-25.0.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:62ec8d979f56c0053a92b2b6a10ff54b9ec8a4f187db2b6ec31ee3dd6d3ca6e2"},
+ {file = "pyzmq-25.0.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:affec1470351178e892121b3414c8ef7803269f207bf9bef85f9a6dd11cde264"},
+ {file = "pyzmq-25.0.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffc71111433bd6ec8607a37b9211f4ef42e3d3b271c6d76c813669834764b248"},
+ {file = "pyzmq-25.0.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:6fadc60970714d86eff27821f8fb01f8328dd36bebd496b0564a500fe4a9e354"},
+ {file = "pyzmq-25.0.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:269968f2a76c0513490aeb3ba0dc3c77b7c7a11daa894f9d1da88d4a0db09835"},
+ {file = "pyzmq-25.0.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f7c8b8368e84381ae7c57f1f5283b029c888504aaf4949c32e6e6fb256ec9bf0"},
+ {file = "pyzmq-25.0.2-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25e6873a70ad5aa31e4a7c41e5e8c709296edef4a92313e1cd5fc87bbd1874e2"},
+ {file = "pyzmq-25.0.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b733076ff46e7db5504c5e7284f04a9852c63214c74688bdb6135808531755a3"},
+ {file = "pyzmq-25.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a6f6ae12478fdc26a6d5fdb21f806b08fa5403cd02fd312e4cb5f72df078f96f"},
+ {file = "pyzmq-25.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:67da1c213fbd208906ab3470cfff1ee0048838365135a9bddc7b40b11e6d6c89"},
+ {file = "pyzmq-25.0.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:531e36d9fcd66f18de27434a25b51d137eb546931033f392e85674c7a7cea853"},
+ {file = "pyzmq-25.0.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34a6fddd159ff38aa9497b2e342a559f142ab365576284bc8f77cb3ead1f79c5"},
+ {file = "pyzmq-25.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b491998ef886662c1f3d49ea2198055a9a536ddf7430b051b21054f2a5831800"},
+ {file = "pyzmq-25.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5d496815074e3e3d183fe2c7fcea2109ad67b74084c254481f87b64e04e9a471"},
+ {file = "pyzmq-25.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:56a94ab1d12af982b55ca96c6853db6ac85505e820d9458ac76364c1998972f4"},
+ {file = "pyzmq-25.0.2.tar.gz", hash = "sha256:6b8c1bbb70e868dc88801aa532cae6bd4e3b5233784692b786f17ad2962e5149"},
+]
+
+[package.dependencies]
+cffi = {version = "*", markers = "implementation_name == \"pypy\""}
+
+[[package]]
+name = "qtconsole"
+version = "5.4.1"
+description = "Jupyter Qt console"
+category = "dev"
+optional = false
+python-versions = ">= 3.7"
+files = [
+ {file = "qtconsole-5.4.1-py3-none-any.whl", hash = "sha256:bae8c7e10170cdcdcaf7e6d53ad7d6a7412249b9b8310a0eaa6b6f3b260f32db"},
+ {file = "qtconsole-5.4.1.tar.gz", hash = "sha256:f67a03f40f722e13261791280f73068dbaf9dafcc335cbba644ccc8f892640e5"},
+]
+
+[package.dependencies]
+ipykernel = ">=4.1"
+ipython-genutils = "*"
+jupyter-client = ">=4.1"
+jupyter-core = "*"
+packaging = "*"
+pygments = "*"
+pyzmq = ">=17.1"
+qtpy = ">=2.0.1"
+traitlets = "<5.2.1 || >5.2.1,<5.2.2 || >5.2.2"
+
+[package.extras]
+doc = ["Sphinx (>=1.3)"]
+test = ["flaky", "pytest", "pytest-qt"]
+
+[[package]]
+name = "qtpy"
+version = "2.3.0"
+description = "Provides an abstraction layer on top of the various Qt bindings (PyQt5/6 and PySide2/6)."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "QtPy-2.3.0-py3-none-any.whl", hash = "sha256:8d6d544fc20facd27360ea189592e6135c614785f0dec0b4f083289de6beb408"},
+ {file = "QtPy-2.3.0.tar.gz", hash = "sha256:0603c9c83ccc035a4717a12908bf6bc6cb22509827ea2ec0e94c2da7c9ed57c5"},
+]
+
+[package.dependencies]
+packaging = "*"
+
+[package.extras]
+test = ["pytest (>=6,!=7.0.0,!=7.0.1)", "pytest-cov (>=3.0.0)", "pytest-qt"]
+
+[[package]]
+name = "redbaron"
+version = "0.9.2"
+description = "Abstraction on top of baron, a FST for python to make writing refactoring code a realistic task"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "redbaron-0.9.2-py2.py3-none-any.whl", hash = "sha256:d01032b6a848b5521a8d6ef72486315c2880f420956870cdd742e2b5a09b9bab"},
+ {file = "redbaron-0.9.2.tar.gz", hash = "sha256:472d0739ca6b2240bb2278ae428604a75472c9c12e86c6321e8c016139c0132f"},
+]
+
+[package.dependencies]
+baron = ">=0.7"
+
+[package.extras]
+notebook = ["pygments"]
+
+[[package]]
+name = "remote-exec"
+version = "1.11.0"
+description = "A CLI to sync codebases and execute commands remotely"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = []
+develop = false
+
+[package.dependencies]
+click = ">=7.1.1"
+pydantic = ">=1.5.1"
+toml = ">=0.10.0"
+watchdog = ">=0.10.3"
+
+[package.source]
+type = "git"
+url = "https://github.com/pbsds/remote"
+reference = "whitespace-push"
+resolved_reference = "84c9d9917f233e2acbded75692b7f7a235a169aa"
+
+[[package]]
+name = "requests"
+version = "2.28.2"
+description = "Python HTTP for Humans."
+category = "main"
+optional = false
+python-versions = ">=3.7, <4"
+files = [
+ {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"},
+ {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"},
+]
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = ">=2,<4"
+idna = ">=2.5,<4"
+urllib3 = ">=1.21.1,<1.27"
+
+[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."
+category = "dev"
+optional = false
+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"
+category = "dev"
+optional = false
+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"
+category = "dev"
+optional = false
+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 = "rich"
+version = "13.3.2"
+description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
+category = "main"
+optional = false
+python-versions = ">=3.7.0"
+files = [
+ {file = "rich-13.3.2-py3-none-any.whl", hash = "sha256:a104f37270bf677148d8acb07d33be1569eeee87e2d1beb286a4e9113caf6f2f"},
+ {file = "rich-13.3.2.tar.gz", hash = "sha256:91954fe80cfb7985727a467ca98a7618e5dd15178cc2da10f553b36a93859001"},
+]
+
+[package.dependencies]
+markdown-it-py = ">=2.2.0,<3.0.0"
+pygments = ">=2.13.0,<3.0.0"
+
+[package.extras]
+jupyter = ["ipywidgets (>=7.5.1,<9)"]
+
+[[package]]
+name = "rope"
+version = "1.7.0"
+description = "a python refactoring library..."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "rope-1.7.0-py3-none-any.whl", hash = "sha256:893dd80ba7077fc9f6f42b0a849372076b70f1d6e405b9f0cc52781ffa0e6890"},
+ {file = "rope-1.7.0.tar.gz", hash = "sha256:ba39581d0f8dee4ae8b5b5e82e35d03cebad965ccb127b7eaab9755cdc85e85a"},
+]
+
+[package.dependencies]
+pytoolconfig = {version = ">=1.2.2", extras = ["global"]}
+
+[package.extras]
+dev = ["build (>=0.7.0)", "pre-commit (>=2.20.0)", "pytest (>=7.0.1)", "pytest-timeout (>=2.1.0)"]
+doc = ["pytoolconfig[doc]", "sphinx (>=4.5.0)", "sphinx-autodoc-typehints (>=1.18.1)", "sphinx-rtd-theme (>=1.0.0)"]
+
+[[package]]
+name = "rply"
+version = "0.7.8"
+description = "A pure Python Lex/Yacc that works with RPython"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "rply-0.7.8-py2.py3-none-any.whl", hash = "sha256:28ffd11d656c48aeb8c508eb382acd6a0bd906662624b34388751732a27807e7"},
+ {file = "rply-0.7.8.tar.gz", hash = "sha256:2a808ac25a4580a9991fc304d64434e299a8fc75760574492f242cbb5bb301c9"},
+]
+
+[package.dependencies]
+appdirs = "*"
+
+[[package]]
+name = "rsa"
+version = "4.9"
+description = "Pure-Python RSA implementation"
+category = "dev"
+optional = false
+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 = "rtree"
+version = "1.0.1"
+description = "R-Tree spatial index for Python GIS"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "Rtree-1.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9855b8f11cdad99c56eb361b7b632a4fbd3d8cbe3f2081426b445f0cfb7fdca9"},
+ {file = "Rtree-1.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:18ce7e4d04b85c48f2d364835620b3b20e38e199639746e7b12f07a2303e18ff"},
+ {file = "Rtree-1.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:784efa6b7be9e99b33613ae8495931032689441eabb6120c9b3eb91188c33794"},
+ {file = "Rtree-1.0.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:157207191aebdacbbdbb369e698cfbfebce53bc97114e96c8af5bed3126475f1"},
+ {file = "Rtree-1.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5fb3671a8d440c24b1dd29ec621d4345ced7185e26f02abe98e85a6629fcb50"},
+ {file = "Rtree-1.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:11d16f51cf9205cd6995af36e24efe8f184270f667fb49bb69b09fc46b97e7d4"},
+ {file = "Rtree-1.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6db6a0a93e41594ffc14b053f386dd414ab5a82535bbd9aedafa6ac8dc0650d8"},
+ {file = "Rtree-1.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c6e29e5eb3083ad12ac5c1ce6e37465ea3428d894d3466cc9c9e2ee4bf768e53"},
+ {file = "Rtree-1.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:656b148589c0b5bab4a7db4d033634329f42a5feaac10ca40aceeca109d83c1f"},
+ {file = "Rtree-1.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b2c15f9373ba314c83a8df5cb6d99b4e3af23c376c6b1317add995432dd0970"},
+ {file = "Rtree-1.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93c5e0bf31e76b4f92a6eec3d2891e938408774c75a8ed6ac3d2c8db04a2be33"},
+ {file = "Rtree-1.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6792de0e3c2fd3ad7e069445027603bec7a47000432f49c80246886311f4f152"},
+ {file = "Rtree-1.0.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:004e131b570dc360a49e7f3b60e7bc6517943a54df056587964d1cb903889e7e"},
+ {file = "Rtree-1.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:becd711fe97c2e09b1b7969e83080a3c8012bce2d30f6db879aade255fcba5c1"},
+ {file = "Rtree-1.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:015df09e1bc55ddf7c88799bf1515d058cd0ee78eacf4cd443a32876d3b3a863"},
+ {file = "Rtree-1.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c2973b76f61669a85e160b4ad09879c4089fc0e3f20fd99adf161ca298fe8374"},
+ {file = "Rtree-1.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e4335e131a58952635560a003458011d97f9ea6f3c010dc24906050b42ee2c03"},
+ {file = "Rtree-1.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:e7ca5d743f6a1dc62653dfac8ee7ce2e1ba91be7cf97916a7f60b7cbe48fb48d"},
+ {file = "Rtree-1.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2ee7165e9872a026ccb868c021711eba39cedf7d1820763c9de52d5324691a92"},
+ {file = "Rtree-1.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8de99f28af0f1783eefb80918959903b4b18112f6a12b48f296ecb162804e69d"},
+ {file = "Rtree-1.0.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a94e2f4bf74bd202ea8b67ea3d7c71e763ad41f79be1d6b72aa2c8d5a8e92c4"},
+ {file = "Rtree-1.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5120da3a1b96f3a7a17dd6af0afdd4e6f3cc9baa87e9ee0a272882f01f980bb"},
+ {file = "Rtree-1.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7e3d5f0e7b28250afbb290ab88b49aa0f121c9714d0da2080581783690347507"},
+ {file = "Rtree-1.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:296203e933b6ec0dd07f6a7456c4f1492def95b6993f20cc61c92b0fee0aecc5"},
+ {file = "Rtree-1.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:77908cd7acdd519a731979ebf5baff8afd102109c2f52864c1e6ee75d3ea2d87"},
+ {file = "Rtree-1.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1a213e5d385278ca7668bc5b27083f8d6e39996a9bd59b6528f3a30009dae4ed"},
+ {file = "Rtree-1.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cfa8cffec5cb9fed494c4bb335ebdb69b3c26178b0b685f67f79296c6b3d800c"},
+ {file = "Rtree-1.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b31fd22d214160859d038da7cb2aaa27acb71efc24a7bcc75c84b5e502721549"},
+ {file = "Rtree-1.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d68a81ad419d5c2ea5fecc677e6c178666c057e2c7b24100a6c48392196f1e9"},
+ {file = "Rtree-1.0.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62f38020af47b765adc6b0bc7c4e810c6c3d1eab44ba339b592ff25a4c0dc0a7"},
+ {file = "Rtree-1.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50b658a6707f215a0056d52e9f83a97148c0af62dea07cf29b3789a2c429e78a"},
+ {file = "Rtree-1.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3573cbb0de872f54d0a0c29596a84e8ac3939c47ca3bece4a82e92775730a0d0"},
+ {file = "Rtree-1.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5abe5a19d943a88bea14901970e4c53e4579fc2662404cdea6163bf4c04d49a"},
+ {file = "Rtree-1.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1e894112cef4de6c518bdea0b43eada65f12888c3645cc437c3a677aa023039f"},
+ {file = "Rtree-1.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:582854252b8fd5c8472478af060635434931fb55edd269bac128cbf2eef43620"},
+ {file = "Rtree-1.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b54057e8a8ad92c1d8e9eaa5cf32aad70dde454abbf9b638e9d6024520a52c02"},
+ {file = "Rtree-1.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:698de8ce6c62e159d93b35bacf64bcf3619077b5367bc88cd2cff5e0bc36169b"},
+ {file = "Rtree-1.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:273ee61783de3a1664e5f868feebf5eea4629447137751bfa4087b0f82093082"},
+ {file = "Rtree-1.0.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16900ee02cf5c198a42b03635268a80f606aa102f3f7618b89f75023d406da1c"},
+ {file = "Rtree-1.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ce4a6fdb63254a4c1efebe7a4f7a59b1c333c703bde4ae715d9ad88c833e10b"},
+ {file = "Rtree-1.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5b20f69e040a05503b22297af223f336fe7047909b57e4b207b98292f33a229f"},
+ {file = "Rtree-1.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:57128293dd625cb1f07726f32208097953e8854d70ab1fc55d6858733618b9ed"},
+ {file = "Rtree-1.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e898d7409ab645c25e06d4e058f99271182601d70b2887aba3351bf08e09a0c6"},
+ {file = "Rtree-1.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:ad9912faeddb1ddcec5e26b33089166d58a107af6862d8b7f1bb2b7c0002ab39"},
+ {file = "Rtree-1.0.1.tar.gz", hash = "sha256:222121699c303a64065d849bf7038b1ecabc37b65c7fa340bedb38ef0e805429"},
+]
+
+[[package]]
+name = "scikit-image"
+version = "0.19.3"
+description = "Image processing in Python"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "scikit-image-0.19.3.tar.gz", hash = "sha256:24b5367de1762da6ee126dd8f30cc4e7efda474e0d7d70685433f0e3aa2ec450"},
+ {file = "scikit_image-0.19.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:3a01372ae4bca223873304b0bff79b9d92446ac6d6177f73d89b45561e2d09d8"},
+ {file = "scikit_image-0.19.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:fdf48d9b1f13af69e4e2c78e05067e322e9c8c97463c315cd0ecb47a94e259fc"},
+ {file = "scikit_image-0.19.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b6a8f98f2ac9bb73706461fd1dec875f6a5141759ed526850a5a49e90003d19"},
+ {file = "scikit_image-0.19.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfbb073f23deb48e0e60c47f8741d8089121d89cc78629ea8c5b51096efc5be7"},
+ {file = "scikit_image-0.19.3-cp310-cp310-win_amd64.whl", hash = "sha256:cc24177de3fdceca5d04807ad9c87d665f0bf01032ed94a9055cd1ed2b3f33e9"},
+ {file = "scikit_image-0.19.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:fd9dd3994bb6f9f7a35f228323f3c4dc44b3cf2ff15fd72d895216e9333550c6"},
+ {file = "scikit_image-0.19.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad5d8000207a264d1a55681a9276e6a739d3f05cf4429004ad00d61d1892235f"},
+ {file = "scikit_image-0.19.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:84baa3179f3ae983c3a5d81c1e404bc92dcf7daeb41bfe9369badcda3fb22b92"},
+ {file = "scikit_image-0.19.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f9f8a1387afc6c70f2bed007c3854a2d7489f9f7713c242f16f32ee05934bc2"},
+ {file = "scikit_image-0.19.3-cp37-cp37m-win32.whl", hash = "sha256:9fb0923a3bfa99457c5e17888f27b3b8a83a3600b4fef317992e7b7234764732"},
+ {file = "scikit_image-0.19.3-cp37-cp37m-win_amd64.whl", hash = "sha256:ce3d2207f253b8eb2c824e30d145a9f07a34a14212d57f3beca9f7e03c383cbe"},
+ {file = "scikit_image-0.19.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:2a02d1bd0e2b53e36b952bd5fd6118d9ccc3ee51de35705d63d8eb1f2e86adef"},
+ {file = "scikit_image-0.19.3-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:03779a7e1736fdf89d83c0ba67d44110496edd736a3bfce61a2b5177a1c8a099"},
+ {file = "scikit_image-0.19.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19a21a101a20c587a3b611a2cf6f86c35aae9f8d9563279b987e83ee1c9a9790"},
+ {file = "scikit_image-0.19.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f50b923f8099c1045fcde7418d86b206c87e333e43da980f41d8577b9605245"},
+ {file = "scikit_image-0.19.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e207c6ce5ce121d7d9b9d2b61b9adca57d1abed112c902d8ffbfdc20fb42c12b"},
+ {file = "scikit_image-0.19.3-cp38-cp38-win32.whl", hash = "sha256:a7c3985c68bfe05f7571167ee021d14f5b8d1a4a250c91f0b13be7fb07e6af34"},
+ {file = "scikit_image-0.19.3-cp38-cp38-win_amd64.whl", hash = "sha256:651de1c2ce1fbee834753b46b8e7d81cb12a5594898babba63ac82b30ddad49d"},
+ {file = "scikit_image-0.19.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:8d8917fcf85b987b1f287f823f3a1a7dac38b70aaca759bc0200f3bc292d5ced"},
+ {file = "scikit_image-0.19.3-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:0b0a199157ce8487c77de4fde0edc0b42d6d42818881c11f459262351d678b2d"},
+ {file = "scikit_image-0.19.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33dfd463ee6cc509defa279b963829f2230c9e0639ccd3931045be055878eea6"},
+ {file = "scikit_image-0.19.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8714348ddd671f819457a797c97d4c672166f093def66d66c3254cbd1d43f83"},
+ {file = "scikit_image-0.19.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff3b1025356508d41f4fe48528e509d95f9e4015e90cf158cd58c56dc63e0ac5"},
+ {file = "scikit_image-0.19.3-cp39-cp39-win32.whl", hash = "sha256:9439e5294de3f18d6e82ec8eee2c46590231cf9c690da80545e83a0733b7a69e"},
+ {file = "scikit_image-0.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:32fb88cc36203b99c9672fb972c9ef98635deaa5fc889fe969f3e11c44f22919"},
+]
+
+[package.dependencies]
+imageio = ">=2.4.1"
+networkx = ">=2.2"
+numpy = ">=1.17.0"
+packaging = ">=20.0"
+pillow = ">=6.1.0,<7.1.0 || >7.1.0,<7.1.1 || >7.1.1,<8.3.0 || >8.3.0"
+PyWavelets = ">=1.1.1"
+scipy = ">=1.4.1"
+tifffile = ">=2019.7.26"
+
+[package.extras]
+data = ["pooch (>=1.3.0)"]
+docs = ["cloudpickle (>=0.2.1)", "dask[array] (>=0.15.0,!=2.17.0)", "ipywidgets", "kaleido", "matplotlib (>=3.3)", "myst-parser", "numpydoc (>=1.0)", "pandas (>=0.23.0)", "plotly (>=4.14.0)", "pooch (>=1.3.0)", "pytest-runner", "scikit-learn", "seaborn (>=0.7.1)", "sphinx (>=1.8)", "sphinx-copybutton", "sphinx-gallery (>=0.10.1)", "tifffile (>=2020.5.30)"]
+optional = ["SimpleITK", "astropy (>=3.1.2)", "cloudpickle (>=0.2.1)", "dask[array] (>=1.0.0,!=2.17.0)", "matplotlib (>=3.0.3)", "pooch (>=1.3.0)", "pyamg", "qtpy"]
+test = ["asv", "codecov", "flake8", "matplotlib (>=3.0.3)", "pooch (>=1.3.0)", "pytest (>=5.2.0)", "pytest-cov (>=2.7.0)", "pytest-faulthandler", "pytest-localserver"]
+
+[[package]]
+name = "scikit-learn"
+version = "1.2.2"
+description = "A set of python modules for machine learning and data mining"
+category = "main"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "scikit-learn-1.2.2.tar.gz", hash = "sha256:8429aea30ec24e7a8c7ed8a3fa6213adf3814a6efbea09e16e0a0c71e1a1a3d7"},
+ {file = "scikit_learn-1.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99cc01184e347de485bf253d19fcb3b1a3fb0ee4cea5ee3c43ec0cc429b6d29f"},
+ {file = "scikit_learn-1.2.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e6e574db9914afcb4e11ade84fab084536a895ca60aadea3041e85b8ac963edb"},
+ {file = "scikit_learn-1.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fe83b676f407f00afa388dd1fdd49e5c6612e551ed84f3b1b182858f09e987d"},
+ {file = "scikit_learn-1.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2642baa0ad1e8f8188917423dd73994bf25429f8893ddbe115be3ca3183584"},
+ {file = "scikit_learn-1.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:ad66c3848c0a1ec13464b2a95d0a484fd5b02ce74268eaa7e0c697b904f31d6c"},
+ {file = "scikit_learn-1.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dfeaf8be72117eb61a164ea6fc8afb6dfe08c6f90365bde2dc16456e4bc8e45f"},
+ {file = "scikit_learn-1.2.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:fe0aa1a7029ed3e1dcbf4a5bc675aa3b1bc468d9012ecf6c6f081251ca47f590"},
+ {file = "scikit_learn-1.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:065e9673e24e0dc5113e2dd2b4ca30c9d8aa2fa90f4c0597241c93b63130d233"},
+ {file = "scikit_learn-1.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf036ea7ef66115e0d49655f16febfa547886deba20149555a41d28f56fd6d3c"},
+ {file = "scikit_learn-1.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:8b0670d4224a3c2d596fd572fb4fa673b2a0ccfb07152688ebd2ea0b8c61025c"},
+ {file = "scikit_learn-1.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9c710ff9f9936ba8a3b74a455ccf0dcf59b230caa1e9ba0223773c490cab1e51"},
+ {file = "scikit_learn-1.2.2-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:2dd3ffd3950e3d6c0c0ef9033a9b9b32d910c61bd06cb8206303fb4514b88a49"},
+ {file = "scikit_learn-1.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44b47a305190c28dd8dd73fc9445f802b6ea716669cfc22ab1eb97b335d238b1"},
+ {file = "scikit_learn-1.2.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:953236889928d104c2ef14027539f5f2609a47ebf716b8cbe4437e85dce42744"},
+ {file = "scikit_learn-1.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:7f69313884e8eb311460cc2f28676d5e400bd929841a2c8eb8742ae78ebf7c20"},
+ {file = "scikit_learn-1.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8156db41e1c39c69aa2d8599ab7577af53e9e5e7a57b0504e116cc73c39138dd"},
+ {file = "scikit_learn-1.2.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fe175ee1dab589d2e1033657c5b6bec92a8a3b69103e3dd361b58014729975c3"},
+ {file = "scikit_learn-1.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d5312d9674bed14f73773d2acf15a3272639b981e60b72c9b190a0cffed5bad"},
+ {file = "scikit_learn-1.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea061bf0283bf9a9f36ea3c5d3231ba2176221bbd430abd2603b1c3b2ed85c89"},
+ {file = "scikit_learn-1.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:6477eed40dbce190f9f9e9d0d37e020815825b300121307942ec2110302b66a3"},
+]
+
+[package.dependencies]
+joblib = ">=1.1.1"
+numpy = ">=1.17.3"
+scipy = ">=1.3.2"
+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.10.0)", "pooch (>=1.6.0)", "scikit-image (>=0.16.2)", "seaborn (>=0.9.0)", "sphinx (>=4.0.1)", "sphinx-gallery (>=0.7.0)", "sphinx-prompt (>=1.3.0)", "sphinxext-opengraph (>=0.4.2)"]
+examples = ["matplotlib (>=3.1.3)", "pandas (>=1.0.5)", "plotly (>=5.10.0)", "pooch (>=1.6.0)", "scikit-image (>=0.16.2)", "seaborn (>=0.9.0)"]
+tests = ["black (>=22.3.0)", "flake8 (>=3.8.2)", "matplotlib (>=3.1.3)", "mypy (>=0.961)", "numpydoc (>=1.2.0)", "pandas (>=1.0.5)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pytest (>=5.3.1)", "pytest-cov (>=2.9.0)", "scikit-image (>=0.16.2)"]
+
+[[package]]
+name = "scipy"
+version = "1.10.1"
+description = "Fundamental algorithms for scientific computing in Python"
+category = "main"
+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.12.2"
+description = "Statistical data visualization"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "seaborn-0.12.2-py3-none-any.whl", hash = "sha256:ebf15355a4dba46037dfd65b7350f014ceb1f13c05e814eda2c9f5fd731afc08"},
+ {file = "seaborn-0.12.2.tar.gz", hash = "sha256:374645f36509d0dcab895cba5b47daf0586f77bfe3b36c97c607db7da5be0139"},
+]
+
+[package.dependencies]
+matplotlib = ">=3.1,<3.6.1 || >3.6.1"
+numpy = ">=1.17,<1.24.0 || >1.24.0"
+pandas = ">=0.25"
+
+[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-copybutton", "sphinx-design", "sphinx-issues"]
+stats = ["scipy (>=1.3)", "statsmodels (>=0.10)"]
+
+[[package]]
+name = "send2trash"
+version = "1.8.0"
+description = "Send file to trash natively under Mac OS X, Windows and Linux."
+category = "dev"
+optional = false
+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 = "serve-me-once"
+version = "0.1.2"
+description = ""
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "serve-me-once-0.1.2.tar.gz", hash = "sha256:d2dcf4b218ccd6e47374dd7ceffdd349236fcbec8a0d4c9116311e4a0018ed21"},
+ {file = "serve_me_once-0.1.2-py3-none-any.whl", hash = "sha256:de5f0eb96a1eedd9f1e3f36edd79f6f3416ce11febd39b423d59e2efe24b97cd"},
+]
+
+[[package]]
+name = "setuptools"
+version = "60.10.0"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "setuptools-60.10.0-py3-none-any.whl", hash = "sha256:782ef48d58982ddb49920c11a0c5c9c0b02e7d7d1c2ad0aa44e1a1e133051c96"},
+ {file = "setuptools-60.10.0.tar.gz", hash = "sha256:6599055eeb23bfef457d5605d33a4d68804266e6cb430b0fb12417c5efeae36c"},
+]
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx", "sphinx-favicon", "sphinx-inline-tabs", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "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 = "shapely"
+version = "2.0.1"
+description = "Manipulation and analysis of geometric objects"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "shapely-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b06d031bc64149e340448fea25eee01360a58936c89985cf584134171e05863f"},
+ {file = "shapely-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9a6ac34c16f4d5d3c174c76c9d7614ec8fe735f8f82b6cc97a46b54f386a86bf"},
+ {file = "shapely-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:865bc3d7cc0ea63189d11a0b1120d1307ed7a64720a8bfa5be2fde5fc6d0d33f"},
+ {file = "shapely-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45b4833235b90bc87ee26c6537438fa77559d994d2d3be5190dd2e54d31b2820"},
+ {file = "shapely-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce88ec79df55430e37178a191ad8df45cae90b0f6972d46d867bf6ebbb58cc4d"},
+ {file = "shapely-2.0.1-cp310-cp310-win32.whl", hash = "sha256:01224899ff692a62929ef1a3f5fe389043e262698a708ab7569f43a99a48ae82"},
+ {file = "shapely-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:da71de5bf552d83dcc21b78cc0020e86f8d0feea43e202110973987ffa781c21"},
+ {file = "shapely-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:502e0a607f1dcc6dee0125aeee886379be5242c854500ea5fd2e7ac076b9ce6d"},
+ {file = "shapely-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7d3bbeefd8a6a1a1017265d2d36f8ff2d79d0162d8c141aa0d37a87063525656"},
+ {file = "shapely-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f470a130d6ddb05b810fc1776d918659407f8d025b7f56d2742a596b6dffa6c7"},
+ {file = "shapely-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4641325e065fd3e07d55677849c9ddfd0cf3ee98f96475126942e746d55b17c8"},
+ {file = "shapely-2.0.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:90cfa4144ff189a3c3de62e2f3669283c98fb760cfa2e82ff70df40f11cadb39"},
+ {file = "shapely-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70a18fc7d6418e5aea76ac55dce33f98e75bd413c6eb39cfed6a1ba36469d7d4"},
+ {file = "shapely-2.0.1-cp311-cp311-win32.whl", hash = "sha256:09d6c7763b1bee0d0a2b84bb32a4c25c6359ad1ac582a62d8b211e89de986154"},
+ {file = "shapely-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:d8f55f355be7821dade839df785a49dc9f16d1af363134d07eb11e9207e0b189"},
+ {file = "shapely-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:83a8ec0ee0192b6e3feee9f6a499d1377e9c295af74d7f81ecba5a42a6b195b7"},
+ {file = "shapely-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a529218e72a3dbdc83676198e610485fdfa31178f4be5b519a8ae12ea688db14"},
+ {file = "shapely-2.0.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91575d97fd67391b85686573d758896ed2fc7476321c9d2e2b0c398b628b961c"},
+ {file = "shapely-2.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8b0d834b11be97d5ab2b4dceada20ae8e07bcccbc0f55d71df6729965f406ad"},
+ {file = "shapely-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:b4f0711cc83734c6fad94fc8d4ec30f3d52c1787b17d9dca261dc841d4731c64"},
+ {file = "shapely-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:05c51a29336e604c084fb43ae5dbbfa2c0ef9bd6fedeae0a0d02c7b57a56ba46"},
+ {file = "shapely-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b519cf3726ddb6c67f6a951d1bb1d29691111eaa67ea19ddca4d454fbe35949c"},
+ {file = "shapely-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:193a398d81c97a62fc3634a1a33798a58fd1dcf4aead254d080b273efbb7e3ff"},
+ {file = "shapely-2.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e55698e0ed95a70fe9ff9a23c763acfe0bf335b02df12142f74e4543095e9a9b"},
+ {file = "shapely-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f32a748703e7bf6e92dfa3d2936b2fbfe76f8ce5f756e24f49ef72d17d26ad02"},
+ {file = "shapely-2.0.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a34a23d6266ca162499e4a22b79159dc0052f4973d16f16f990baa4d29e58b6"},
+ {file = "shapely-2.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d173d24e85e51510e658fb108513d5bc11e3fd2820db6b1bd0522266ddd11f51"},
+ {file = "shapely-2.0.1-cp38-cp38-win32.whl", hash = "sha256:3cb256ae0c01b17f7bc68ee2ffdd45aebf42af8992484ea55c29a6151abe4386"},
+ {file = "shapely-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:c7eed1fb3008a8a4a56425334b7eb82651a51f9e9a9c2f72844a2fb394f38a6c"},
+ {file = "shapely-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ac1dfc397475d1de485e76de0c3c91cc9d79bd39012a84bb0f5e8a199fc17bef"},
+ {file = "shapely-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:33403b8896e1d98aaa3a52110d828b18985d740cc9f34f198922018b1e0f8afe"},
+ {file = "shapely-2.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2569a4b91caeef54dd5ae9091ae6f63526d8ca0b376b5bb9fd1a3195d047d7d4"},
+ {file = "shapely-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a70a614791ff65f5e283feed747e1cc3d9e6c6ba91556e640636bbb0a1e32a71"},
+ {file = "shapely-2.0.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c43755d2c46b75a7b74ac6226d2cc9fa2a76c3263c5ae70c195c6fb4e7b08e79"},
+ {file = "shapely-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad81f292fffbd568ae71828e6c387da7eb5384a79db9b4fde14dd9fdeffca9a"},
+ {file = "shapely-2.0.1-cp39-cp39-win32.whl", hash = "sha256:b50c401b64883e61556a90b89948297f1714dbac29243d17ed9284a47e6dd731"},
+ {file = "shapely-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:bca57b683e3d94d0919e2f31e4d70fdfbb7059650ef1b431d9f4e045690edcd5"},
+ {file = "shapely-2.0.1.tar.gz", hash = "sha256:66a6b1a3e72ece97fc85536a281476f9b7794de2e646ca8a4517e2e3c1446893"},
+]
+
+[package.dependencies]
+numpy = ">=1.14"
+
+[package.extras]
+docs = ["matplotlib", "numpydoc (>=1.1.0,<1.2.0)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"]
+test = ["pytest", "pytest-cov"]
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+category = "main"
+optional = false
+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 = "sniffio"
+version = "1.3.0"
+description = "Sniff out which async library your code is running under"
+category = "dev"
+optional = false
+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 = "snowballstemmer"
+version = "2.2.0"
+description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"},
+ {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
+]
+
+[[package]]
+name = "soupsieve"
+version = "2.4"
+description = "A modern CSS selector implementation for Beautiful Soup."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "soupsieve-2.4-py3-none-any.whl", hash = "sha256:49e5368c2cda80ee7e84da9dbe3e110b70a4575f196efb74e51b94549d921955"},
+ {file = "soupsieve-2.4.tar.gz", hash = "sha256:e28dba9ca6c7c00173e34e4ba57448f0688bb681b7c5e8bf4971daafc093d69a"},
+]
+
+[[package]]
+name = "stack-data"
+version = "0.6.2"
+description = "Extract data from python stack frames and tracebacks for informative displays"
+category = "dev"
+optional = false
+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 = "sympy"
+version = "1.11.1"
+description = "Computer algebra system (CAS) in Python"
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "sympy-1.11.1-py3-none-any.whl", hash = "sha256:938f984ee2b1e8eae8a07b884c8b7a1146010040fccddc6539c54f401c8f6fcf"},
+ {file = "sympy-1.11.1.tar.gz", hash = "sha256:e32380dce63cb7c0108ed525570092fd45168bdae2faa17e528221ef72e88658"},
+]
+
+[package.dependencies]
+mpmath = ">=0.19"
+
+[[package]]
+name = "tabulate"
+version = "0.9.0"
+description = "Pretty-print tabular data"
+category = "main"
+optional = false
+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 = "tenacity"
+version = "8.2.2"
+description = "Retry code until it succeeds"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "tenacity-8.2.2-py3-none-any.whl", hash = "sha256:2f277afb21b851637e8f52e6a613ff08734c347dc19ade928e519d7d2d8569b0"},
+ {file = "tenacity-8.2.2.tar.gz", hash = "sha256:43af037822bd0029025877f3b2d97cc4d7bb0c2991000a3d59d71517c5c969e0"},
+]
+
+[package.extras]
+doc = ["reno", "sphinx", "tornado (>=4.5)"]
+
+[[package]]
+name = "tensorboard"
+version = "2.12.0"
+description = "TensorBoard lets you watch Tensors Flow"
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "tensorboard-2.12.0-py3-none-any.whl", hash = "sha256:3cbdc32448d7a28dc1bf0b1754760c08b8e0e2e37c451027ebd5ff4896613012"},
+]
+
+[package.dependencies]
+absl-py = ">=0.4"
+google-auth = ">=1.6.3,<3"
+google-auth-oauthlib = ">=0.4.1,<0.5"
+grpcio = ">=1.48.2"
+markdown = ">=2.6.8"
+numpy = ">=1.12.0"
+protobuf = ">=3.19.6"
+requests = ">=2.21.0,<3"
+setuptools = ">=41.0.0"
+tensorboard-data-server = ">=0.7.0,<0.8.0"
+tensorboard-plugin-wit = ">=1.6.0"
+werkzeug = ">=1.0.1"
+wheel = ">=0.26"
+
+[[package]]
+name = "tensorboard-data-server"
+version = "0.7.0"
+description = "Fast data loading for TensorBoard"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tensorboard_data_server-0.7.0-py3-none-any.whl", hash = "sha256:753d4214799b31da7b6d93837959abebbc6afa86e69eacf1e9a317a48daa31eb"},
+ {file = "tensorboard_data_server-0.7.0-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:eb7fa518737944dbf4f0cf83c2e40a7ac346bf91be2e6a0215de98be74e85454"},
+ {file = "tensorboard_data_server-0.7.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:64aa1be7c23e80b1a42c13b686eb0875bb70f5e755f4d2b8de5c1d880cf2267f"},
+]
+
+[[package]]
+name = "tensorboard-plugin-wit"
+version = "1.8.1"
+description = "What-If Tool TensorBoard plugin."
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "tensorboard_plugin_wit-1.8.1-py3-none-any.whl", hash = "sha256:ff26bdd583d155aa951ee3b152b3d0cffae8005dc697f72b44a8e8c2a77a8cbe"},
+]
+
+[[package]]
+name = "termcolor"
+version = "2.2.0"
+description = "ANSI color formatting for output in terminal"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "termcolor-2.2.0-py3-none-any.whl", hash = "sha256:91ddd848e7251200eac969846cbae2dacd7d71c2871e92733289e7e3666f48e7"},
+ {file = "termcolor-2.2.0.tar.gz", hash = "sha256:dfc8ac3f350788f23b2947b3e6cfa5a53b630b612e6cd8965a015a776020b99a"},
+]
+
+[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."
+category = "dev"
+optional = false
+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 = "textwrap3"
+version = "0.9.2"
+description = "textwrap from Python 3.6 backport (plus a few tweaks)"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "textwrap3-0.9.2-py2.py3-none-any.whl", hash = "sha256:bf5f4c40faf2a9ff00a9e0791fed5da7415481054cef45bb4a3cfb1f69044ae0"},
+ {file = "textwrap3-0.9.2.zip", hash = "sha256:5008eeebdb236f6303dcd68f18b856d355f6197511d952ba74bc75e40e0c3414"},
+]
+
+[[package]]
+name = "threadpoolctl"
+version = "3.1.0"
+description = "threadpoolctl"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "threadpoolctl-3.1.0-py3-none-any.whl", hash = "sha256:8b99adda265feb6773280df41eece7b2e6561b772d21ffd52e372f999024907b"},
+ {file = "threadpoolctl-3.1.0.tar.gz", hash = "sha256:a335baacfaa4400ae1f0d8e3a58d6674d2f8828e3716bb2802c44955ad391380"},
+]
+
+[[package]]
+name = "tifffile"
+version = "2023.3.21"
+description = "Read and write TIFF files"
+category = "main"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "tifffile-2023.3.21-py3-none-any.whl", hash = "sha256:b83af3dbe914663aeaf250e9751ae503154f2599dd27ed7a4df1b2dc7c148efa"},
+ {file = "tifffile-2023.3.21.tar.gz", hash = "sha256:16027be65e9d5a1b26bf106a98a639345fecf83ebac9004dc7e617647e4dbfeb"},
+]
+
+[package.dependencies]
+numpy = "*"
+
+[package.extras]
+all = ["defusedxml", "fsspec", "imagecodecs (>=2023.1.23)", "lxml", "matplotlib", "zarr"]
+
+[[package]]
+name = "tinycss2"
+version = "1.2.1"
+description = "A tiny CSS parser"
+category = "dev"
+optional = false
+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"
+category = "dev"
+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"
+category = "dev"
+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 = "tomlkit"
+version = "0.11.6"
+description = "Style preserving TOML library"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"},
+ {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"},
+]
+
+[[package]]
+name = "torch"
+version = "1.13.1"
+description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration"
+category = "main"
+optional = false
+python-versions = ">=3.7.0"
+files = [
+ {file = "torch-1.13.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:fd12043868a34a8da7d490bf6db66991108b00ffbeecb034228bfcbbd4197143"},
+ {file = "torch-1.13.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:d9fe785d375f2e26a5d5eba5de91f89e6a3be5d11efb497e76705fdf93fa3c2e"},
+ {file = "torch-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:98124598cdff4c287dbf50f53fb455f0c1e3a88022b39648102957f3445e9b76"},
+ {file = "torch-1.13.1-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:393a6273c832e047581063fb74335ff50b4c566217019cc6ace318cd79eb0566"},
+ {file = "torch-1.13.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:0122806b111b949d21fa1a5f9764d1fd2fcc4a47cb7f8ff914204fd4fc752ed5"},
+ {file = "torch-1.13.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:22128502fd8f5b25ac1cd849ecb64a418382ae81dd4ce2b5cebaa09ab15b0d9b"},
+ {file = "torch-1.13.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:76024be052b659ac1304ab8475ab03ea0a12124c3e7626282c9c86798ac7bc11"},
+ {file = "torch-1.13.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:ea8dda84d796094eb8709df0fcd6b56dc20b58fdd6bc4e8d7109930dafc8e419"},
+ {file = "torch-1.13.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2ee7b81e9c457252bddd7d3da66fb1f619a5d12c24d7074de91c4ddafb832c93"},
+ {file = "torch-1.13.1-cp37-none-macosx_10_9_x86_64.whl", hash = "sha256:0d9b8061048cfb78e675b9d2ea8503bfe30db43d583599ae8626b1263a0c1380"},
+ {file = "torch-1.13.1-cp37-none-macosx_11_0_arm64.whl", hash = "sha256:f402ca80b66e9fbd661ed4287d7553f7f3899d9ab54bf5c67faada1555abde28"},
+ {file = "torch-1.13.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:727dbf00e2cf858052364c0e2a496684b9cb5aa01dc8a8bc8bbb7c54502bdcdd"},
+ {file = "torch-1.13.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:df8434b0695e9ceb8cc70650afc1310d8ba949e6db2a0525ddd9c3b2b181e5fe"},
+ {file = "torch-1.13.1-cp38-cp38-win_amd64.whl", hash = "sha256:5e1e722a41f52a3f26f0c4fcec227e02c6c42f7c094f32e49d4beef7d1e213ea"},
+ {file = "torch-1.13.1-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:33e67eea526e0bbb9151263e65417a9ef2d8fa53cbe628e87310060c9dcfa312"},
+ {file = "torch-1.13.1-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:eeeb204d30fd40af6a2d80879b46a7efbe3cf43cdbeb8838dd4f3d126cc90b2b"},
+ {file = "torch-1.13.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:50ff5e76d70074f6653d191fe4f6a42fdbe0cf942fbe2a3af0b75eaa414ac038"},
+ {file = "torch-1.13.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:2c3581a3fd81eb1f0f22997cddffea569fea53bafa372b2c0471db373b26aafc"},
+ {file = "torch-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:0aa46f0ac95050c604bcf9ef71da9f1172e5037fdf2ebe051962d47b123848e7"},
+ {file = "torch-1.13.1-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:6930791efa8757cb6974af73d4996b6b50c592882a324b8fb0589c6a9ba2ddaf"},
+ {file = "torch-1.13.1-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:e0df902a7c7dd6c795698532ee5970ce898672625635d885eade9976e5a04949"},
+]
+
+[package.dependencies]
+nvidia-cublas-cu11 = {version = "11.10.3.66", markers = "platform_system == \"Linux\""}
+nvidia-cuda-nvrtc-cu11 = {version = "11.7.99", markers = "platform_system == \"Linux\""}
+nvidia-cuda-runtime-cu11 = {version = "11.7.99", markers = "platform_system == \"Linux\""}
+nvidia-cudnn-cu11 = {version = "8.5.0.96", markers = "platform_system == \"Linux\""}
+typing-extensions = "*"
+
+[package.extras]
+opt-einsum = ["opt-einsum (>=3.3)"]
+
+[[package]]
+name = "torchmeta"
+version = "1.8.0"
+description = "Dataloaders for meta-learning in Pytorch"
+category = "main"
+optional = false
+python-versions = "*"
+files = []
+develop = false
+
+[package.dependencies]
+h5py = "*"
+numpy = ">=1.14.0"
+ordered-set = "*"
+Pillow = ">=7.0.0"
+requests = "*"
+torch = ">=1.4.0,<1.15.0"
+torchvision = ">=0.5.0,<0.16.0"
+tqdm = ">=4.0.0"
+
+[package.extras]
+tcga = ["academictorrents (>=2.1.0,<2.2.0)", "pandas (>=0.24.0,<0.25.0)", "six (>=1.11.0,<1.12.0)"]
+test = ["flaky"]
+
+[package.source]
+type = "git"
+url = "https://github.com/pbsds/pytorch-meta"
+reference = "upgrade"
+resolved_reference = "ef53cc17c9f645902c96ffc611ce3d2b5cd08e99"
+
+[[package]]
+name = "torchmetrics"
+version = "0.11.4"
+description = "PyTorch native Metrics"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "torchmetrics-0.11.4-py3-none-any.whl", hash = "sha256:45f892f3534e91f3ad9e2488d1b05a93b7cb76b7d037969435a41a1f24750d9a"},
+ {file = "torchmetrics-0.11.4.tar.gz", hash = "sha256:1fe45a14b44dd65d90199017dd5a4b5a128d56a8a311da7916c402c18c671494"},
+]
+
+[package.dependencies]
+numpy = ">=1.17.2"
+packaging = "*"
+torch = ">=1.8.1"
+
+[package.extras]
+all = ["lpips (<=0.1.4)", "nltk (>=3.6)", "pycocotools (>2.0.0)", "pystoi (<=0.3.3)", "regex (>=2021.9.24)", "scipy (>1.0.0)", "torch-fidelity (<=0.3.0)", "torchvision (>=0.8)", "tqdm (>=4.41.0)", "transformers (>=4.10.0)"]
+audio = ["pystoi (<=0.3.3)"]
+detection = ["pycocotools (>2.0.0)", "torchvision (>=0.8)"]
+image = ["lpips (<=0.1.4)", "scipy (>1.0.0)", "torch-fidelity (<=0.3.0)", "torchvision (>=0.8)"]
+multimodal = ["transformers (>=4.10.0)"]
+test = ["bert-score (==0.3.13)", "cloudpickle (>1.3)", "coverage (>5.2)", "dython (<=0.7.3)", "fast-bss-eval (>=0.1.0)", "fire (<=0.5.0)", "huggingface-hub (<0.7)", "jiwer (>=2.3.0)", "kornia (>=0.6.7)", "mir-eval (>=0.6)", "mypy (==0.982)", "netcal (>1.0.0)", "pandas (>1.0.0)", "phmdoctest (>=1.1.1)", "psutil (<=5.9.4)", "pypesq (>1.2)", "pytest (>=6.0.0)", "pytest-cov (>2.10)", "pytest-doctestplus (>=0.9.0)", "pytest-rerunfailures (>=10.0)", "pytest-timeout (<=2.1.0)", "pytorch-msssim (==0.2.1)", "requests (<=2.28.2)", "rouge-score (>0.1.0)", "sacrebleu (>=2.0.0)", "scikit-image (>0.17.1)", "scikit-learn (>1.0)", "scipy (>1.0.0)", "torch-complex (<=0.4.3)", "transformers (>4.4.0)", "types-PyYAML", "types-emoji", "types-protobuf", "types-requests", "types-setuptools", "types-six", "types-tabulate"]
+text = ["nltk (>=3.6)", "regex (>=2021.9.24)", "tqdm (>=4.41.0)"]
+
+[[package]]
+name = "torchvision"
+version = "0.14.1"
+description = "image and video datasets and models for torch deep learning"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "torchvision-0.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb05dd9dd3af5428fee525400759daf8da8e4caec45ddd6908cfb36571f6433"},
+ {file = "torchvision-0.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d0766ea92affa7af248e327dd85f7c9cfdf51a57530b43212d4e1858548e9d7"},
+ {file = "torchvision-0.14.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:6d7b35653113664ea3fdcb71f515cfbf29d2fe393000fd8aaff27a1284de6908"},
+ {file = "torchvision-0.14.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:8a9eb773a2fa8f516e404ac09c059fb14e6882c48fdbb9c946327d2ce5dba6cd"},
+ {file = "torchvision-0.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:13986f0c15377ff23039e1401012ccb6ecf71024ce53def27139e4eac5a57592"},
+ {file = "torchvision-0.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb7a793fd33ce1abec24b42778419a3fb1e3159d7dfcb274a3ca8fb8cbc408dc"},
+ {file = "torchvision-0.14.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:89fb0419780ec9a9eb9f7856a0149f6ac9f956b28f44b0c0080c6b5b48044db7"},
+ {file = "torchvision-0.14.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a2d4237d3c9705d7729eb4534e4eb06f1d6be7ff1df391204dfb51586d9b0ecb"},
+ {file = "torchvision-0.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:92a324712a87957443cc34223274298ae9496853f115c252f8fc02b931f2340e"},
+ {file = "torchvision-0.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:68ed03359dcd3da9cd21b8ab94da21158df8a6a0c5bad0bf4a42f0e448d28cb3"},
+ {file = "torchvision-0.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:30fcf0e9fe57d4ac4ce6426659a57dce199637ccb6c70be1128670f177692624"},
+ {file = "torchvision-0.14.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0ed02aefd09bf1114d35f1aa7dce55aa61c2c7e57f9aa02dce362860be654e85"},
+ {file = "torchvision-0.14.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a541e49fc3c4e90e49e6988428ab047415ed52ea97d0c0bfd147d8bacb8f4df8"},
+ {file = "torchvision-0.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:6099b3191dc2516099a32ae38a5fb349b42e863872a13545ab1a524b6567be60"},
+ {file = "torchvision-0.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c5e744f56e5f5b452deb5fc0f3f2ba4d2f00612d14d8da0dbefea8f09ac7690b"},
+ {file = "torchvision-0.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:758b20d079e810b4740bd60d1eb16e49da830e3360f9be379eb177ee221fa5d4"},
+ {file = "torchvision-0.14.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:83045507ef8d3c015d4df6be79491375b2f901352cfca6e72b4723e9c4f9a55d"},
+ {file = "torchvision-0.14.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:eaed58cf454323ed9222d4e0dd5fb897064f454b400696e03a5200e65d3a1e76"},
+ {file = "torchvision-0.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:b337e1245ca4353623dd563c03cd8f020c2496a7c5d12bba4d2e381999c766e0"},
+]
+
+[package.dependencies]
+numpy = "*"
+pillow = ">=5.3.0,<8.3.0 || >=8.4.0"
+requests = "*"
+torch = "1.13.1"
+typing-extensions = "*"
+
+[package.extras]
+scipy = ["scipy"]
+
+[[package]]
+name = "torchviz"
+version = "0.0.2"
+description = "A small package to create visualizations of PyTorch execution graphs"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "torchviz-0.0.2.tar.gz", hash = "sha256:c790b4c993f783433604bf610cfa58bb0a031260be4ee5196a00c0884e768051"},
+]
+
+[package.dependencies]
+graphviz = "*"
+torch = "*"
+
+[[package]]
+name = "tornado"
+version = "6.2"
+description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
+category = "dev"
+optional = false
+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.65.0"
+description = "Fast, Extensible Progress Meter"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tqdm-4.65.0-py3-none-any.whl", hash = "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671"},
+ {file = "tqdm-4.65.0.tar.gz", hash = "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5"},
+]
+
+[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.9.0"
+description = "Traitlets Python configuration system"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "traitlets-5.9.0-py3-none-any.whl", hash = "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8"},
+ {file = "traitlets-5.9.0.tar.gz", hash = "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9"},
+]
+
+[package.extras]
+docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
+test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"]
+
+[[package]]
+name = "trimesh"
+version = "3.21.0"
+description = "Import, export, process, analyze and view triangular meshes."
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "trimesh-3.21.0-py3-none-any.whl", hash = "sha256:459c811448facf410247bd9f284cd3026bee71804d0b0704b90352bbb069944c"},
+ {file = "trimesh-3.21.0.tar.gz", hash = "sha256:2aa8c312900e24a7487744a7e9d47e794bd6e79ad629aaa48cde0d37ee63a094"},
+]
+
+[package.dependencies]
+numpy = "*"
+
+[package.extras]
+all = ["chardet", "colorlog", "glooey", "jsonschema", "lxml", "mapbox-earcut", "meshio", "networkx", "pillow", "psutil", "pycollada", "pyglet (<2)", "python-fcl", "requests", "rtree", "scikit-image", "scipy", "setuptools", "shapely", "svg.path", "sympy", "xatlas", "xxhash"]
+easy = ["chardet", "colorlog", "jsonschema", "lxml", "mapbox-earcut", "networkx", "pillow", "pycollada", "requests", "rtree", "scipy", "setuptools", "shapely", "svg.path", "sympy", "xxhash"]
+test = ["autopep8", "coveralls", "ezdxf", "pyinstrument", "pytest", "pytest-cov", "ruff"]
+
+[[package]]
+name = "typer"
+version = "0.7.0"
+description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "typer-0.7.0-py3-none-any.whl", hash = "sha256:b5e704f4e48ec263de1c0b3a2387cd405a13767d2f907f44c1a08cbad96f606d"},
+ {file = "typer-0.7.0.tar.gz", hash = "sha256:ff797846578a9f2a201b53442aedeb543319466870fbe1c701eab66dd7681165"},
+]
+
+[package.dependencies]
+click = ">=7.1.1,<9.0.0"
+
+[package.extras]
+all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"]
+dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"]
+doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"]
+test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"]
+
+[[package]]
+name = "typing-extensions"
+version = "4.5.0"
+description = "Backported and Experimental Type Hints for Python 3.7+"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"},
+ {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"},
+]
+
+[[package]]
+name = "ujson"
+version = "5.7.0"
+description = "Ultra fast JSON encoder and decoder for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "ujson-5.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5eba5e69e4361ac3a311cf44fa71bc619361b6e0626768a494771aacd1c2f09b"},
+ {file = "ujson-5.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aae4d9e1b4c7b61780f0a006c897a4a1904f862fdab1abb3ea8f45bd11aa58f3"},
+ {file = "ujson-5.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2e43ccdba1cb5c6d3448eadf6fc0dae7be6c77e357a3abc968d1b44e265866d"},
+ {file = "ujson-5.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54384ce4920a6d35fa9ea8e580bc6d359e3eb961fa7e43f46c78e3ed162d56ff"},
+ {file = "ujson-5.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24ad1aa7fc4e4caa41d3d343512ce68e41411fb92adf7f434a4d4b3749dc8f58"},
+ {file = "ujson-5.7.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:afff311e9f065a8f03c3753db7011bae7beb73a66189c7ea5fcb0456b7041ea4"},
+ {file = "ujson-5.7.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e80f0d03e7e8646fc3d79ed2d875cebd4c83846e129737fdc4c2532dbd43d9e"},
+ {file = "ujson-5.7.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:137831d8a0db302fb6828ee21c67ad63ac537bddc4376e1aab1c8573756ee21c"},
+ {file = "ujson-5.7.0-cp310-cp310-win32.whl", hash = "sha256:7df3fd35ebc14dafeea031038a99232b32f53fa4c3ecddb8bed132a43eefb8ad"},
+ {file = "ujson-5.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:af4639f684f425177d09ae409c07602c4096a6287027469157bfb6f83e01448b"},
+ {file = "ujson-5.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b0f2680ce8a70f77f5d70aaf3f013d53e6af6d7058727a35d8ceb4a71cdd4e9"},
+ {file = "ujson-5.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a19fd8e7d8cc58a169bea99fed5666023adf707a536d8f7b0a3c51dd498abf"},
+ {file = "ujson-5.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6abb8e6d8f1ae72f0ed18287245f5b6d40094e2656d1eab6d99d666361514074"},
+ {file = "ujson-5.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8cd622c069368d5074bd93817b31bdb02f8d818e57c29e206f10a1f9c6337dd"},
+ {file = "ujson-5.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14f9082669f90e18e64792b3fd0bf19f2b15e7fe467534a35ea4b53f3bf4b755"},
+ {file = "ujson-5.7.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d7ff6ebb43bc81b057724e89550b13c9a30eda0f29c2f506f8b009895438f5a6"},
+ {file = "ujson-5.7.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f7f241488879d91a136b299e0c4ce091996c684a53775e63bb442d1a8e9ae22a"},
+ {file = "ujson-5.7.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5593263a7fcfb934107444bcfba9dde8145b282de0ee9f61e285e59a916dda0f"},
+ {file = "ujson-5.7.0-cp311-cp311-win32.whl", hash = "sha256:26c2b32b489c393106e9cb68d0a02e1a7b9d05a07429d875c46b94ee8405bdb7"},
+ {file = "ujson-5.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:ed24406454bb5a31df18f0a423ae14beb27b28cdfa34f6268e7ebddf23da807e"},
+ {file = "ujson-5.7.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18679484e3bf9926342b1c43a3bd640f93a9eeeba19ef3d21993af7b0c44785d"},
+ {file = "ujson-5.7.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ee295761e1c6c30400641f0a20d381633d7622633cdf83a194f3c876a0e4b7e"},
+ {file = "ujson-5.7.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b738282e12a05f400b291966630a98d622da0938caa4bc93cf65adb5f4281c60"},
+ {file = "ujson-5.7.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00343501dbaa5172e78ef0e37f9ebd08040110e11c12420ff7c1f9f0332d939e"},
+ {file = "ujson-5.7.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c0d1f7c3908357ee100aa64c4d1cf91edf99c40ac0069422a4fd5fd23b263263"},
+ {file = "ujson-5.7.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a5d2f44331cf04689eafac7a6596c71d6657967c07ac700b0ae1c921178645da"},
+ {file = "ujson-5.7.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:16b2254a77b310f118717715259a196662baa6b1f63b1a642d12ab1ff998c3d7"},
+ {file = "ujson-5.7.0-cp37-cp37m-win32.whl", hash = "sha256:6faf46fa100b2b89e4db47206cf8a1ffb41542cdd34dde615b2fc2288954f194"},
+ {file = "ujson-5.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ff0004c3f5a9a6574689a553d1b7819d1a496b4f005a7451f339dc2d9f4cf98c"},
+ {file = "ujson-5.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:75204a1dd7ec6158c8db85a2f14a68d2143503f4bafb9a00b63fe09d35762a5e"},
+ {file = "ujson-5.7.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7312731c7826e6c99cdd3ac503cd9acd300598e7a80bcf41f604fee5f49f566c"},
+ {file = "ujson-5.7.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b9dc5a90e2149643df7f23634fe202fed5ebc787a2a1be95cf23632b4d90651"},
+ {file = "ujson-5.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6a6961fc48821d84b1198a09516e396d56551e910d489692126e90bf4887d29"},
+ {file = "ujson-5.7.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b01a9af52a0d5c46b2c68e3f258fdef2eacaa0ce6ae3e9eb97983f5b1166edb6"},
+ {file = "ujson-5.7.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7316d3edeba8a403686cdcad4af737b8415493101e7462a70ff73dd0609eafc"},
+ {file = "ujson-5.7.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4ee997799a23227e2319a3f8817ce0b058923dbd31904761b788dc8f53bd3e30"},
+ {file = "ujson-5.7.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dda9aa4c33435147262cd2ea87c6b7a1ca83ba9b3933ff7df34e69fee9fced0c"},
+ {file = "ujson-5.7.0-cp38-cp38-win32.whl", hash = "sha256:bea8d30e362180aafecabbdcbe0e1f0b32c9fa9e39c38e4af037b9d3ca36f50c"},
+ {file = "ujson-5.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:c96e3b872bf883090ddf32cc41957edf819c5336ab0007d0cf3854e61841726d"},
+ {file = "ujson-5.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6411aea4c94a8e93c2baac096fbf697af35ba2b2ed410b8b360b3c0957a952d3"},
+ {file = "ujson-5.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d3b3499c55911f70d4e074c626acdb79a56f54262c3c83325ffb210fb03e44d"},
+ {file = "ujson-5.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:341f891d45dd3814d31764626c55d7ab3fd21af61fbc99d070e9c10c1190680b"},
+ {file = "ujson-5.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f242eec917bafdc3f73a1021617db85f9958df80f267db69c76d766058f7b19"},
+ {file = "ujson-5.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3af9f9f22a67a8c9466a32115d9073c72a33ae627b11de6f592df0ee09b98b6"},
+ {file = "ujson-5.7.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4a3d794afbf134df3056a813e5c8a935208cddeae975bd4bc0ef7e89c52f0ce0"},
+ {file = "ujson-5.7.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:800bf998e78dae655008dd10b22ca8dc93bdcfcc82f620d754a411592da4bbf2"},
+ {file = "ujson-5.7.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b5ac3d5c5825e30b438ea92845380e812a476d6c2a1872b76026f2e9d8060fc2"},
+ {file = "ujson-5.7.0-cp39-cp39-win32.whl", hash = "sha256:cd90027e6d93e8982f7d0d23acf88c896d18deff1903dd96140613389b25c0dd"},
+ {file = "ujson-5.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:523ee146cdb2122bbd827f4dcc2a8e66607b3f665186bce9e4f78c9710b6d8ab"},
+ {file = "ujson-5.7.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e87cec407ec004cf1b04c0ed7219a68c12860123dfb8902ef880d3d87a71c172"},
+ {file = "ujson-5.7.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bab10165db6a7994e67001733f7f2caf3400b3e11538409d8756bc9b1c64f7e8"},
+ {file = "ujson-5.7.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b522be14a28e6ac1cf818599aeff1004a28b42df4ed4d7bc819887b9dac915fc"},
+ {file = "ujson-5.7.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7592f40175c723c032cdbe9fe5165b3b5903604f774ab0849363386e99e1f253"},
+ {file = "ujson-5.7.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ed22f9665327a981f288a4f758a432824dc0314e4195a0eaeb0da56a477da94d"},
+ {file = "ujson-5.7.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:adf445a49d9a97a5a4c9bb1d652a1528de09dd1c48b29f79f3d66cea9f826bf6"},
+ {file = "ujson-5.7.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64772a53f3c4b6122ed930ae145184ebaed38534c60f3d859d8c3f00911eb122"},
+ {file = "ujson-5.7.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35209cb2c13fcb9d76d249286105b4897b75a5e7f0efb0c0f4b90f222ce48910"},
+ {file = "ujson-5.7.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:90712dfc775b2c7a07d4d8e059dd58636bd6ff1776d79857776152e693bddea6"},
+ {file = "ujson-5.7.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:0e4e8981c6e7e9e637e637ad8ffe948a09e5434bc5f52ecbb82b4b4cfc092bfb"},
+ {file = "ujson-5.7.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:581c945b811a3d67c27566539bfcb9705ea09cb27c4be0002f7a553c8886b817"},
+ {file = "ujson-5.7.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d36a807a24c7d44f71686685ae6fbc8793d784bca1adf4c89f5f780b835b6243"},
+ {file = "ujson-5.7.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b4257307e3662aa65e2644a277ca68783c5d51190ed9c49efebdd3cbfd5fa44"},
+ {file = "ujson-5.7.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea7423d8a2f9e160c5e011119741682414c5b8dce4ae56590a966316a07a4618"},
+ {file = "ujson-5.7.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c592eb91a5968058a561d358d0fef59099ed152cfb3e1cd14eee51a7a93879e"},
+ {file = "ujson-5.7.0.tar.gz", hash = "sha256:e788e5d5dcae8f6118ac9b45d0b891a0d55f7ac480eddcb7f07263f2bcf37b23"},
+]
+
+[[package]]
+name = "uri-template"
+version = "1.2.0"
+description = "RFC 6570 URI Template Processor"
+category = "dev"
+optional = false
+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.15"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+files = [
+ {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"},
+ {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"},
+]
+
+[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 = "urwid"
+version = "2.1.2"
+description = "A full-featured console (xterm et al.) user interface library"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "urwid-2.1.2.tar.gz", hash = "sha256:588bee9c1cb208d0906a9f73c613d2bd32c3ed3702012f51efe318a3f2127eae"},
+]
+
+[[package]]
+name = "urwid-readline"
+version = "0.13"
+description = "A textbox edit widget for urwid that supports readline shortcuts"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "urwid_readline-0.13.tar.gz", hash = "sha256:018020cbc864bb5ed87be17dc26b069eae2755cb29f3a9c569aac3bded1efaf4"},
+]
+
+[package.dependencies]
+urwid = "*"
+
+[package.extras]
+dev = ["black", "pytest"]
+
+[[package]]
+name = "visidata"
+version = "2.11"
+description = "terminal interface for exploring and arranging tabular data"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "visidata-2.11-py3-none-any.whl", hash = "sha256:84fd9c31ff3bce07055bfe735d0455fca69db462db284c61b53e88e55debba77"},
+ {file = "visidata-2.11.tar.gz", hash = "sha256:c09ecb65025dc9d6513bfd5e5aff9c430ffc325cc183b81abf5f2df5a96b66b3"},
+]
+
+[package.dependencies]
+importlib-metadata = ">=3.6"
+python-dateutil = "*"
+windows-curses = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "watchdog"
+version = "3.0.0"
+description = "Filesystem events monitoring"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41"},
+ {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397"},
+ {file = "watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96"},
+ {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae"},
+ {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9"},
+ {file = "watchdog-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7"},
+ {file = "watchdog-3.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674"},
+ {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f"},
+ {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc"},
+ {file = "watchdog-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3"},
+ {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3"},
+ {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0"},
+ {file = "watchdog-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8"},
+ {file = "watchdog-3.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100"},
+ {file = "watchdog-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346"},
+ {file = "watchdog-3.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64"},
+ {file = "watchdog-3.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a"},
+ {file = "watchdog-3.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44"},
+ {file = "watchdog-3.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a"},
+ {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709"},
+ {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83"},
+ {file = "watchdog-3.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d"},
+ {file = "watchdog-3.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33"},
+ {file = "watchdog-3.0.0-py3-none-win32.whl", hash = "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f"},
+ {file = "watchdog-3.0.0-py3-none-win_amd64.whl", hash = "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c"},
+ {file = "watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759"},
+ {file = "watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9"},
+]
+
+[package.extras]
+watchmedo = ["PyYAML (>=3.10)"]
+
+[[package]]
+name = "wcwidth"
+version = "0.2.6"
+description = "Measures the displayed width of unicode strings in a terminal"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"},
+ {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"},
+]
+
+[[package]]
+name = "webcolors"
+version = "1.12"
+description = "A library for working with color names and color values formats defined by HTML and CSS."
+category = "dev"
+optional = false
+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 = "webencodings"
+version = "0.5.1"
+description = "Character encoding aliases for legacy web content"
+category = "dev"
+optional = false
+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.5.1"
+description = "WebSocket client for Python with low level API options"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "websocket-client-1.5.1.tar.gz", hash = "sha256:3f09e6d8230892547132177f575a4e3e73cfdf06526e20cc02aa1c3b47184d40"},
+ {file = "websocket_client-1.5.1-py3-none-any.whl", hash = "sha256:cdf5877568b7e83aa7cf2244ab56a3213de587bbe0ce9d8b9600fc77b455d89e"},
+]
+
+[package.extras]
+docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"]
+optional = ["python-socks", "wsaccel"]
+test = ["websockets"]
+
+[[package]]
+name = "werkzeug"
+version = "2.2.3"
+description = "The comprehensive WSGI web application library."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "Werkzeug-2.2.3-py3-none-any.whl", hash = "sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612"},
+ {file = "Werkzeug-2.2.3.tar.gz", hash = "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.1.1"
+
+[package.extras]
+watchdog = ["watchdog"]
+
+[[package]]
+name = "whatthepatch"
+version = "1.0.4"
+description = "A patch parsing and application library."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "whatthepatch-1.0.4-py3-none-any.whl", hash = "sha256:e1261068ba4df71f72ac1b0dfd2271362691c25e8843c1fcbd170166c219f3aa"},
+ {file = "whatthepatch-1.0.4.tar.gz", hash = "sha256:e95c108087845b09258ddfaf82aa13cf83ba8319475117c0909754ca8b54d742"},
+]
+
+[[package]]
+name = "wheel"
+version = "0.37.1"
+description = "A built-package format for Python"
+category = "main"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+files = [
+ {file = "wheel-0.37.1-py2.py3-none-any.whl", hash = "sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a"},
+ {file = "wheel-0.37.1.tar.gz", hash = "sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4"},
+]
+
+[package.extras]
+test = ["pytest (>=3.0.0)", "pytest-cov"]
+
+[[package]]
+name = "widgetsnbextension"
+version = "4.0.6"
+description = "Jupyter interactive widgets for Jupyter Notebook"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "widgetsnbextension-4.0.6-py3-none-any.whl", hash = "sha256:7df2bffa274b0b416c1fa0789e321451858a9e276e1220b40a16cc994192e2b7"},
+ {file = "widgetsnbextension-4.0.6.tar.gz", hash = "sha256:1a07d06c881a7c16ca7ab4541b476edbe2e404f5c5f0cf524ffa2406a8bd7c80"},
+]
+
+[[package]]
+name = "windows-curses"
+version = "2.3.1"
+description = "Support for the standard curses module on Windows"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "windows_curses-2.3.1-cp310-cp310-win32.whl", hash = "sha256:2644f4547ae5124ce5129b66faa59ee0995b7b7205ed5e3920f6ecfef2e46275"},
+ {file = "windows_curses-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:8b541520895649c0835771775034a2b4edf36da3c3d9381c5022b5b4f9a5014e"},
+ {file = "windows_curses-2.3.1-cp311-cp311-win32.whl", hash = "sha256:25e7ff3d77aed6c747456b06fbc1528d67fc59d1ef3be9ca244774e65e6bdbb2"},
+ {file = "windows_curses-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:395656bfe88d6f60cb18604605d423e0f2d1c3a8f550507dca5877a9d0b3a0f3"},
+ {file = "windows_curses-2.3.1-cp36-cp36m-win32.whl", hash = "sha256:6ea8e1c4536fee248ee3f88e5010871df749932b7e829e2f012e5d23bd2fe31d"},
+ {file = "windows_curses-2.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:59856b41676c4b3eb527eb6b1478803d4dc92413b2e63aea762407807ffcd3ac"},
+ {file = "windows_curses-2.3.1-cp37-cp37m-win32.whl", hash = "sha256:9cd0ba6efde23930736eff45a0aa0af6fd82e60b4787a46157ef4956d2c52b06"},
+ {file = "windows_curses-2.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f9a7fcd03934e40238f9bbeddae51e3fdc442f28bca50afccdc521245ed39439"},
+ {file = "windows_curses-2.3.1-cp38-cp38-win32.whl", hash = "sha256:5c55ebafdb402cfa927174a03d651cd1b1e76d6e6cf71818f9d3378636c00e74"},
+ {file = "windows_curses-2.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:a551aaa09d6ec28f64ade8e85fd0c52880c8e9114729a79c34803104e49bed71"},
+ {file = "windows_curses-2.3.1-cp39-cp39-win32.whl", hash = "sha256:aab7e28133bf81769cddf8b3c3c8ab89e76cd43effd371c6370e918b6dfccf1b"},
+ {file = "windows_curses-2.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:85675de4ae7058348140daae83a8a7b81147a84ef9ab699307b3168f9490292f"},
+]
+
+[[package]]
+name = "wirerope"
+version = "0.4.7"
+description = "'Turn functions and methods into fully controllable objects'"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "wirerope-0.4.7.tar.gz", hash = "sha256:f3961039218276283c5037da0fa164619def0327595f10892d562a61a8603990"},
+]
+
+[package.dependencies]
+six = ">=1.11.0"
+
+[package.extras]
+doc = ["sphinx"]
+test = ["pytest (>=4.6.7)", "pytest-cov (>=2.6.1)"]
+
+[[package]]
+name = "wrapt"
+version = "1.15.0"
+description = "Module for decorators, wrappers and monkey patching."
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+files = [
+ {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"},
+ {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"},
+ {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"},
+ {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"},
+ {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"},
+ {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"},
+ {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"},
+ {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"},
+ {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"},
+ {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"},
+ {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"},
+ {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"},
+ {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"},
+ {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"},
+ {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"},
+ {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"},
+ {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"},
+ {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"},
+ {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"},
+ {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"},
+ {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"},
+ {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"},
+ {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"},
+ {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"},
+ {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"},
+ {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"},
+ {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"},
+ {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"},
+ {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"},
+ {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"},
+ {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"},
+ {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"},
+ {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"},
+ {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"},
+ {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"},
+ {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"},
+ {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"},
+ {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"},
+ {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"},
+ {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"},
+ {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"},
+ {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"},
+ {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"},
+ {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"},
+ {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"},
+ {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"},
+ {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"},
+ {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"},
+ {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"},
+ {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"},
+ {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"},
+ {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"},
+ {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"},
+ {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"},
+ {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"},
+ {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"},
+ {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"},
+ {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"},
+ {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"},
+ {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"},
+ {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"},
+ {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"},
+ {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"},
+ {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"},
+ {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"},
+ {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"},
+ {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"},
+ {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"},
+ {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"},
+ {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"},
+ {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"},
+ {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"},
+ {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"},
+ {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"},
+ {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"},
+]
+
+[[package]]
+name = "y-py"
+version = "0.5.9"
+description = "Python bindings for the Y-CRDT built from yrs (Rust)"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "y_py-0.5.9-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:afa9a11aa2880dd8689894f3269b653e6d3bd1956963d5329be9a5bf021dab62"},
+ {file = "y_py-0.5.9-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:e370ce076781adea161b04d2f666e8b4f89bc7e8927ef842fbb0283d3bfa73e0"},
+ {file = "y_py-0.5.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b67dad339f9b6701f74ff7a6e901c7909eca4eea02cf955b28d87a42650bd1be"},
+ {file = "y_py-0.5.9-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae82a6d9cbaff8cb7505e81b5b7f9cd7756bb7e7110aef7914375fe56b012a90"},
+ {file = "y_py-0.5.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c7ca64a2a97f708569dcabd55865915943e30267bf6d26c4d212d005951efe62"},
+ {file = "y_py-0.5.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55098440e32339c2dc3d652fb36bb77a4927dee5fd4ab0cb1fe12fdd163fd4f5"},
+ {file = "y_py-0.5.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9052a814e8b7ec756371a191f38de68b956437e0bb429c2dd503e658f298f9"},
+ {file = "y_py-0.5.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:95d13b38c9055d607565b77cbae12e2bf0c1671c5cb8f2ee2e1230d41d2d6d34"},
+ {file = "y_py-0.5.9-cp310-none-win32.whl", hash = "sha256:5dbd8d177ec7b9fef4a7b6d22eb2f8d5606fd5aac31cf2eab0dc18f0b3504c7c"},
+ {file = "y_py-0.5.9-cp310-none-win_amd64.whl", hash = "sha256:d373c6bb8e21d5f7ec0833b76fa1ab480086ada602ef5bbf4724a25a21a00b6a"},
+ {file = "y_py-0.5.9-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:f8f238144a302f17eb26b122cad9382fcff5ec6653b8a562130b9a5e44010098"},
+ {file = "y_py-0.5.9-cp311-cp311-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:25637e3d011ca6f877a24f3083ff2549d1d619406d7e8a1455c445527205046c"},
+ {file = "y_py-0.5.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ffebe5e62cbfee6e24593927dedba77dc13ac4cfb9c822074ab566b1fb63d59"},
+ {file = "y_py-0.5.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b0ed760e6aa5316227a0ba2d5d29634a4ef2d72c8bc55169ac01664e17e4b536"},
+ {file = "y_py-0.5.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91be189fae8ba242528333e266e38d65cae3d9a09fe45867fab8578a3ddf2ea2"},
+ {file = "y_py-0.5.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3ae6d22b7cc599220a26b06da6ead9fd582eea5fdb6273b06fa3f060d0a26a7"},
+ {file = "y_py-0.5.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:065f90501cf008375d70be6ce72dd41745e09d088f0b545f5f914d2c3f04f7ae"},
+ {file = "y_py-0.5.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:742c486d5b792c4ad76e09426161302edddca85efe826fa01dcee50907326cd7"},
+ {file = "y_py-0.5.9-cp311-none-win32.whl", hash = "sha256:2692c808bf28f797f8d693f45dc86563ac3b1626579f67ce9546dca69644d687"},
+ {file = "y_py-0.5.9-cp311-none-win_amd64.whl", hash = "sha256:c1f5f287cc7ae127ed6a2fb1546e631b316a41d087d7d2db9caa3e5f59906dcf"},
+ {file = "y_py-0.5.9-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:9a59603cf42c20d02ee5add2e3d0ce48e89c480a2a02f642fb77f142c4f37958"},
+ {file = "y_py-0.5.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b44473bb32217c78e18db66f497f6c8be33e339bab5f52398bb2468c904d5140"},
+ {file = "y_py-0.5.9-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1906f13e8d5ebfbd9c7948f57bc6f6f53b451b19c99350f42a0f648147a8acfe"},
+ {file = "y_py-0.5.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:202b2a3e42e0a1eaedee26f8a3bc73cd9f994c4c2b15511ea56b9838178eb380"},
+ {file = "y_py-0.5.9-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13b9d2959d9a26536b6ad118fb026ff19bd79da52e4addf6f3a562e7c01d516e"},
+ {file = "y_py-0.5.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff3ddedaa95284f4f22a92b362f658f3d92f272d8c0fa009051bd5490c4d5a04"},
+ {file = "y_py-0.5.9-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:85585e669d7679126e4a04e4bc0a063a641175a74eecfe47539e8da3e5b1da6e"},
+ {file = "y_py-0.5.9-cp37-none-win32.whl", hash = "sha256:caf9b1feb69379d424a1d3d7c899b8e0389a3fb3131d39c3c03dcc3d4a93dbdc"},
+ {file = "y_py-0.5.9-cp37-none-win_amd64.whl", hash = "sha256:7353af0e9c1f42fbf0ab340e253eeb333d58c890fa91d3eadb1b9adaf9336732"},
+ {file = "y_py-0.5.9-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:ed0fd5265905cc7e23709479bc152d69f4972dec32fa322d20cb77f749707e78"},
+ {file = "y_py-0.5.9-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:db1ac7f2d1862eb4c448cf76183399d555a63dbe2452bafecb1c2f691e36d687"},
+ {file = "y_py-0.5.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa685f7e43ce490dfb1e392ac48f584b75cd21f05dc526c160d15308236ce8a0"},
+ {file = "y_py-0.5.9-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c42f3a6cd20153925b00c49af855a3277989d411bb8ea849095be943ee160821"},
+ {file = "y_py-0.5.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:753aaae817d658a1e9d271663439d8e83d9d8effa45590ecdcadc600c7cf77e3"},
+ {file = "y_py-0.5.9-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc8e5f38842a4b043c9592bfa9a740147ddb8fac2d7a5b7bf6d52466c090ec23"},
+ {file = "y_py-0.5.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecd3cb0d13ac92e7b9235d1024dba9af0788161246f12dcf1f635d634ccb206a"},
+ {file = "y_py-0.5.9-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9983e99e3a61452b39ffce98206c7e4c6d260f4e917c8fe53fb54aaf25df89a3"},
+ {file = "y_py-0.5.9-cp38-none-win32.whl", hash = "sha256:63ef8e5b76cd54578a7fd5f72d8c698d9ccd7c555c7900ebfd38a24d397c3b15"},
+ {file = "y_py-0.5.9-cp38-none-win_amd64.whl", hash = "sha256:fe70d0134fe2115c08866f0cac0eb5c0788093872b5026eb438a74e1ebafd659"},
+ {file = "y_py-0.5.9-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:05f805b58422d5d7c8e7e8e2141d1c3cac4daaa4557ae6a9b84b141fe8d6289e"},
+ {file = "y_py-0.5.9-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:a7977eeaceaeb0dfffcc5643c985c337ebc33a0b1d792ae0a9b1331cdd97366f"},
+ {file = "y_py-0.5.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:800e73d2110b97a74c52db2c8ce03a78e96f0d66a7e0c87d8254170a67c2db0e"},
+ {file = "y_py-0.5.9-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add793f5f5c7c7a3eb1b09ffc771bdaae10a0bd482a370bf696b83f8dee8d1b4"},
+ {file = "y_py-0.5.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8b67ae37af8aac6160fda66c0f73bcdf65c06da9022eb76192c3fc45cfab994"},
+ {file = "y_py-0.5.9-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2532ea5aefb223fd688c93860199d348a7601d814aac9e8784d816314588ddeb"},
+ {file = "y_py-0.5.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df78a0409dca11554a4b6442d7a8e61f762c3cfc78d55d98352392869a6b9ae0"},
+ {file = "y_py-0.5.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2da2a9e28dceab4832945a745cad507579f52b4d0c9e2f54ae156eb56875861"},
+ {file = "y_py-0.5.9-cp39-none-win32.whl", hash = "sha256:fdafb93bfd5532b13a53c4090675bcd31724160017ecc73e492dc1211bc0377a"},
+ {file = "y_py-0.5.9-cp39-none-win_amd64.whl", hash = "sha256:73200c59bb253b880825466717941ac57267f2f685b053e183183cb6fe82874d"},
+ {file = "y_py-0.5.9-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:af6df5ec1d66ee2d962026635d60e84ad35fc01b2a1e36b993360c0ce60ae349"},
+ {file = "y_py-0.5.9-pp38-pypy38_pp73-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:0c0e333c20b0a6ce4a5851203d45898ab93f16426c342420b931e190c5b71d3d"},
+ {file = "y_py-0.5.9-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7434c77cd23592973ed63341b8d337e6aebaba5ed40d7f22e2d43dfd0c3a56e"},
+ {file = "y_py-0.5.9-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e30fe2491d095c6d695a2c96257967fd3e2497f0f777030c8492d03c18d46e2a"},
+ {file = "y_py-0.5.9-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a57d81260e048caacf43a2f851766687f53e8a8356df6947fb0eee7336a7e2de"},
+ {file = "y_py-0.5.9-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d4dfc276f988175baaa4ab321c3321a16ce33db3356c9bc5f4dea0db3de55aa"},
+ {file = "y_py-0.5.9-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb68445414940efe547291340e91604c7b8379b60822678ef29f4fc2a0e11c62"},
+ {file = "y_py-0.5.9-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cd6f373dbf592ad83aaf95c16abebc8678928e49bd509ebd593259e1908345ae"},
+ {file = "y_py-0.5.9-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:76b3480e7037ac9390c450e2aff9e46e2c9e61520c0d88afe228110ec728adc5"},
+ {file = "y_py-0.5.9-pp39-pypy39_pp73-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:9484a3fc33f812234e58a5ee834b42bb0a628054d61b5c06c323aa56c12e557d"},
+ {file = "y_py-0.5.9-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6d87d0c2e87990bc00c049742d36a5dbbb1510949459af17198728890ee748a"},
+ {file = "y_py-0.5.9-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fce5feb57f6231376eb10d1fb68c60da106ffa0b520b3129471c466eff0304cc"},
+ {file = "y_py-0.5.9-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27c1e9a866146d250e9e16d99fe22a40c82f5b592ab85da97e5679fc3841c7ce"},
+ {file = "y_py-0.5.9-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d722d6a27230c1f395535da5cee6a9a16497c6343afd262c846090075c083009"},
+ {file = "y_py-0.5.9-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f54625b9ed4e787872c45d3044dcfd04c0da4258d9914f3d32308830b35246c"},
+ {file = "y_py-0.5.9-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9513ae81fcc805671ae134c4c7421ca322acf92ce8b33817e1775ea8c0176973"},
+ {file = "y_py-0.5.9.tar.gz", hash = "sha256:50cfa0532bcee27edb8c64743b49570e28bb76a00cd384ead1d84b6f052d9368"},
+]
+
+[[package]]
+name = "yacs"
+version = "0.1.8"
+description = "Yet Another Configuration System"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "yacs-0.1.8-py2-none-any.whl", hash = "sha256:d43d1854c1ffc4634c5b349d1c1120f86f05c3a294c9d141134f282961ab5d94"},
+ {file = "yacs-0.1.8-py3-none-any.whl", hash = "sha256:99f893e30497a4b66842821bac316386f7bd5c4f47ad35c9073ef089aa33af32"},
+ {file = "yacs-0.1.8.tar.gz", hash = "sha256:efc4c732942b3103bea904ee89af98bcd27d01f0ac12d8d4d369f1e7a2914384"},
+]
+
+[package.dependencies]
+PyYAML = "*"
+
+[[package]]
+name = "yapf"
+version = "0.32.0"
+description = "A formatter for Python code."
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "yapf-0.32.0-py2.py3-none-any.whl", hash = "sha256:8fea849025584e486fd06d6ba2bed717f396080fd3cc236ba10cb97c4c51cf32"},
+ {file = "yapf-0.32.0.tar.gz", hash = "sha256:a3f5085d37ef7e3e004c4ba9f9b3e40c54ff1901cd111f05145ae313a7c67d1b"},
+]
+
+[[package]]
+name = "yarl"
+version = "1.8.2"
+description = "Yet another URL library"
+category = "main"
+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 = "ypy-websocket"
+version = "0.8.2"
+description = "WebSocket connector for Ypy"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "ypy_websocket-0.8.2-py3-none-any.whl", hash = "sha256:9049d5a7d61c26c2b5a39757c9ffcbe2274bf3553adeea8de7fe1c04671d4145"},
+ {file = "ypy_websocket-0.8.2.tar.gz", hash = "sha256:491b2cc4271df4dde9be83017c15f4532b597dc43148472eb20c5aeb838a5b46"},
+]
+
+[package.dependencies]
+aiofiles = ">=22.1.0,<23"
+aiosqlite = ">=0.17.0,<1"
+y-py = ">=0.5.3,<0.6.0"
+
+[package.extras]
+test = ["mypy", "pre-commit", "pytest", "pytest-asyncio", "websockets (>=10.0)"]
+
+[[package]]
+name = "zipp"
+version = "3.15.0"
+description = "Backport of pathlib-compatible object wrapper for zip files"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"},
+ {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"},
+]
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+testing = ["big-O", "flake8 (<5)", "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)"]
+
+[metadata]
+lock-version = "2.0"
+python-versions = ">=3.10,<3.11"
+content-hash = "ac011d99b8b2769be7c7b851ba1af8f48658b7676aa4241978c66761abcf71eb"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..2776600
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,80 @@
+[tool.poetry]
+name = "ifield"
+version = "0.2.0"
+description = ""
+authors = ["Peder Bergebakken Sundt "]
+
+[build-system]
+requires = ["poetry-core>=1.0.0", "setuptools>=60"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.poetry.dependencies]
+python = ">=3.10,<3.11"
+faiss-cpu = "^1.7.3"
+geomloss = "0.2.4" # 0.2.5 has no bdist on pypi
+h5py = "^3.7.0"
+hdf5plugin = "^4.0.1"
+imageio = "^2.23.0"
+jinja2 = "^3.1.2"
+matplotlib = "^3.6.2"
+mesh-to-sdf = {git = "https://github.com/pbsds/mesh_to_sdf", rev = "no_flip_normals"}
+methodtools = "^0.4.5"
+more-itertools = "^9.1.0"
+munch = "^2.5.0"
+numpy = "^1.23.0"
+pyembree = {url = "https://folk.ntnu.no/pederbs/pypy/pep503/pyembree/pyembree-0.2.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"}
+pygame = "^2.1.2"
+pykeops = "^2.1.1"
+pytorch3d = [
+ {url = "https://dl.fbaipublicfiles.com/pytorch3d/packaging/wheels/py310_cu116_pyt1130/pytorch3d-0.7.2-cp310-cp310-linux_x86_64.whl"},
+]
+pyqt5 = "^5.15.7"
+pyrender = "^0.1.45"
+pytorch-lightning = "^1.8.6"
+pyyaml = "^6.0"
+rich = "^13.3.2"
+rtree = "^1.0.1"
+scikit-image = "^0.19.3"
+scikit-learn = "^1.2.0"
+seaborn = "^0.12.1"
+serve-me-once = "^0.1.2"
+torch = "^1.13.0"
+torchmeta = {git = "https://github.com/pbsds/pytorch-meta", rev = "upgrade"}
+torchviz = "^0.0.2"
+tqdm = "^4.64.1"
+trimesh = "^3.17.1"
+typer = "^0.7.0"
+
+
+[tool.poetry.dev-dependencies]
+python-lsp-server = {extras = ["all"], version = "^1.6.0"}
+fix-my-functions = "^0.1.3"
+imageio-ffmpeg = "^0.4.7"
+jupyter = "^1.0.0"
+jupyter-contrib-nbextensions = "^0.7.0"
+jupyterlab = "^3.5.2"
+jupyterthemes = "^0.20.0"
+llvmlite = "^0.39.1" # only to make poetry install the python3.10 wheels instead of building them
+nbconvert = "<=6.5.0" # https://github.com/jupyter/nbconvert/issues/1894
+numba = "^0.56.4" # only to make poetry install the python3.10 wheels instead of building them
+papermill = "^2.4.0"
+pdoc = "^12.3.0"
+pdoc3 = "^0.10.0"
+ptpython = "^3.0.22"
+pudb = "^2022.1.3"
+remote-exec = {git = "https://github.com/pbsds/remote", rev = "whitespace-push"} # https://github.com/remote-cli/remote/pull/52
+shapely = "^2.0.0"
+sympy = "^1.11.1"
+tensorboard = "^2.11.0"
+visidata = "^2.11"
+
+[tool.poetry.scripts]
+show-schedule = 'ifield.utils.loss:main'
+show-h5-items = 'ifield.cli_utils:show_h5_items'
+show-h5-img = 'ifield.cli_utils:show_h5_img'
+show-h5-scan-cloud = 'ifield.cli_utils:show_h5_scan_cloud'
+show-model = 'ifield.cli_utils:show_model'
+download-stanford = 'ifield.data.stanford.download:cli'
+download-coseg = 'ifield.data.coseg.download:cli'
+preprocess-stanford = 'ifield.data.stanford.preprocess:cli'
+preprocess-coseg = 'ifield.data.coseg.preprocess:cli'