diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..929c8209de31142c01f071256e3992ed8cc1e9f8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,147 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# Logging into tensorboard and wandb
+runs/*
+wandb
+
+# macOS
+.DS_STORE
+
+# Local scratch work
+scratch/*
+
+# vscode
+.vscode/
+
+# Don't bother tracking saved_models or videos
+saved_models/*
+downloaded_models/*
+videos/*
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..c691634b2b31bc3198edb031b80a2c0ec7d1cd20
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Scott Goodfriend
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..8728c46dd9a3e032e504b9312e1283b2950c2f2b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,130 @@
+---
+library_name: rl-algo-impls
+tags:
+- PongNoFrameskip-v4
+- a2c
+- deep-reinforcement-learning
+- reinforcement-learning
+model-index:
+- name: a2c
+ results:
+ - metrics:
+ - type: mean_reward
+ value: 21.0 +/- 0.0
+ name: mean_reward
+ task:
+ type: reinforcement-learning
+ name: reinforcement-learning
+ dataset:
+ name: PongNoFrameskip-v4
+ type: PongNoFrameskip-v4
+---
+# **A2C** Agent playing **PongNoFrameskip-v4**
+
+This is a trained model of a **A2C** agent playing **PongNoFrameskip-v4** using the [/sgoodfriend/rl-algo-impls](https://github.com/sgoodfriend/rl-algo-impls) repo.
+
+All models trained at this commit can be found at https://api.wandb.ai/links/sgoodfriend/eyvb72mv.
+
+## Training Results
+
+This model was trained from 3 trainings of **A2C** agents using different initial seeds. These agents were trained by checking out [0760ef7](https://github.com/sgoodfriend/rl-algo-impls/tree/0760ef7d52b17f30219a27c18ba52c8895025ae3). The best and last models were kept from each training. This submission has loaded the best models from each training, reevaluates them, and selects the best model from these latest evaluations (mean - std).
+
+| algo | env | seed | reward_mean | reward_std | eval_episodes | best | wandb_url |
+|:-------|:-------------------|-------:|--------------:|-------------:|----------------:|:-------|:-----------------------------------------------------------------------------|
+| a2c | PongNoFrameskip-v4 | 1 | 21 | 0 | 16 | | [wandb](https://wandb.ai/sgoodfriend/rl-algo-impls-benchmarks/runs/0qr8uh8d) |
+| a2c | PongNoFrameskip-v4 | 2 | 21 | 0 | 16 | | [wandb](https://wandb.ai/sgoodfriend/rl-algo-impls-benchmarks/runs/wgl9ivm1) |
+| a2c | PongNoFrameskip-v4 | 3 | 21 | 0 | 16 | * | [wandb](https://wandb.ai/sgoodfriend/rl-algo-impls-benchmarks/runs/xwzoc2uw) |
+
+
+### Prerequisites: Weights & Biases (WandB)
+Training and benchmarking assumes you have a Weights & Biases project to upload runs to.
+By default training goes to a rl-algo-impls project while benchmarks go to
+rl-algo-impls-benchmarks. During training and benchmarking runs, videos of the best
+models and the model weights are uploaded to WandB.
+
+Before doing anything below, you'll need to create a wandb account and run `wandb
+login`.
+
+
+
+## Usage
+/sgoodfriend/rl-algo-impls: https://github.com/sgoodfriend/rl-algo-impls
+
+Note: While the model state dictionary and hyperaparameters are saved, the latest
+implementation could be sufficiently different to not be able to reproduce similar
+results. You might need to checkout the commit the agent was trained on:
+[0760ef7](https://github.com/sgoodfriend/rl-algo-impls/tree/0760ef7d52b17f30219a27c18ba52c8895025ae3).
+```
+# Downloads the model, sets hyperparameters, and runs agent for 3 episodes
+python enjoy.py --wandb-run-path=sgoodfriend/rl-algo-impls-benchmarks/xwzoc2uw
+```
+
+Setup hasn't been completely worked out yet, so you might be best served by using Google
+Colab starting from the
+[colab_enjoy.ipynb](https://github.com/sgoodfriend/rl-algo-impls/blob/main/colab_enjoy.ipynb)
+notebook.
+
+
+
+## Training
+If you want the highest chance to reproduce these results, you'll want to checkout the
+commit the agent was trained on: [0760ef7](https://github.com/sgoodfriend/rl-algo-impls/tree/0760ef7d52b17f30219a27c18ba52c8895025ae3). While
+training is deterministic, different hardware will give different results.
+
+```
+python train.py --algo a2c --env PongNoFrameskip-v4 --seed 3
+```
+
+Setup hasn't been completely worked out yet, so you might be best served by using Google
+Colab starting from the
+[colab_train.ipynb](https://github.com/sgoodfriend/rl-algo-impls/blob/main/colab_train.ipynb)
+notebook.
+
+
+
+## Benchmarking (with Lambda Labs instance)
+This and other models from https://api.wandb.ai/links/sgoodfriend/eyvb72mv were generated by running a script on a Lambda
+Labs instance. In a Lambda Labs instance terminal:
+```
+git clone git@github.com:sgoodfriend/rl-algo-impls.git
+cd rl-algo-impls
+bash ./lambda_labs/setup.sh
+wandb login
+bash ./lambda_labs/benchmark.sh
+```
+
+### Alternative: Google Colab Pro+
+As an alternative,
+[colab_benchmark.ipynb](https://github.com/sgoodfriend/rl-algo-impls/tree/main/benchmarks#:~:text=colab_benchmark.ipynb),
+can be used. However, this requires a Google Colab Pro+ subscription and running across
+4 separate instances because otherwise running all jobs will exceed the 24-hour limit.
+
+
+
+## Hyperparameters
+This isn't exactly the format of hyperparams in hyperparams/a2c.yml, but instead the Wandb Run Config. However, it's very
+close and has some additional data:
+```
+algo: a2c
+algo_hyperparams:
+ ent_coef: 0.01
+ vf_coef: 0.25
+env: PongNoFrameskip-v4
+env_hyperparams:
+ frame_stack: 4
+ n_envs: 16
+ no_reward_fire_steps: 500
+ no_reward_timeout_steps: 1000
+ vec_env_class: async
+n_timesteps: 10000000
+policy_hyperparams:
+ activation_fn: relu
+seed: 3
+use_deterministic_algorithms: true
+wandb_entity: null
+wandb_project_name: rl-algo-impls-benchmarks
+wandb_tags:
+- benchmark_0760ef7
+- host_192-9-248-209
+
+```
diff --git a/a2c/a2c.py b/a2c/a2c.py
new file mode 100644
index 0000000000000000000000000000000000000000..ab4430b9b9eb61bbc99aedbae7d4dd5d44f71bd9
--- /dev/null
+++ b/a2c/a2c.py
@@ -0,0 +1,201 @@
+import numpy as np
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+
+from dataclasses import asdict, dataclass, field
+from time import perf_counter
+from torch.utils.tensorboard.writer import SummaryWriter
+from typing import List, Optional, Sequence, NamedTuple, TypeVar
+
+from shared.algorithm import Algorithm
+from shared.callbacks.callback import Callback
+from shared.gae import compute_advantage, compute_rtg_and_advantage
+from shared.policy.on_policy import ActorCritic
+from shared.schedule import schedule, update_learning_rate
+from shared.stats import log_scalars
+from shared.trajectory import Trajectory, TrajectoryAccumulator
+from wrappers.vectorable_wrapper import (
+ VecEnv,
+ VecEnvObs,
+ single_observation_space,
+ single_action_space,
+)
+
+A2CSelf = TypeVar("A2CSelf", bound="A2C")
+
+
+class A2C(Algorithm):
+ def __init__(
+ self,
+ policy: ActorCritic,
+ env: VecEnv,
+ device: torch.device,
+ tb_writer: SummaryWriter,
+ learning_rate: float = 7e-4,
+ learning_rate_decay: str = "none",
+ n_steps: int = 5,
+ gamma: float = 0.99,
+ gae_lambda: float = 1.0,
+ ent_coef: float = 0.0,
+ ent_coef_decay: str = "none",
+ vf_coef: float = 0.5,
+ max_grad_norm: float = 0.5,
+ rms_prop_eps: float = 1e-5,
+ use_rms_prop: bool = True,
+ sde_sample_freq: int = -1,
+ normalize_advantage: bool = False,
+ ) -> None:
+ super().__init__(policy, env, device, tb_writer)
+ self.policy = policy
+
+ self.lr_schedule = schedule(learning_rate_decay, learning_rate)
+ if use_rms_prop:
+ self.optimizer = torch.optim.RMSprop(
+ policy.parameters(), lr=learning_rate, eps=rms_prop_eps
+ )
+ else:
+ self.optimizer = torch.optim.Adam(policy.parameters(), lr=learning_rate)
+
+ self.n_steps = n_steps
+
+ self.gamma = gamma
+ self.gae_lambda = gae_lambda
+
+ self.vf_coef = vf_coef
+ self.ent_coef_schedule = schedule(ent_coef_decay, ent_coef)
+ self.max_grad_norm = max_grad_norm
+
+ self.sde_sample_freq = sde_sample_freq
+ self.normalize_advantage = normalize_advantage
+
+ def learn(
+ self: A2CSelf, total_timesteps: int, callback: Optional[Callback] = None
+ ) -> A2CSelf:
+ epoch_dim = (self.n_steps, self.env.num_envs)
+ step_dim = (self.env.num_envs,)
+ obs_space = single_observation_space(self.env)
+ act_space = single_action_space(self.env)
+
+ obs = np.zeros(epoch_dim + obs_space.shape, dtype=obs_space.dtype)
+ actions = np.zeros(epoch_dim + act_space.shape, dtype=act_space.dtype)
+ rewards = np.zeros(epoch_dim, dtype=np.float32)
+ episode_starts = np.zeros(epoch_dim, dtype=np.byte)
+ values = np.zeros(epoch_dim, dtype=np.float32)
+ logprobs = np.zeros(epoch_dim, dtype=np.float32)
+
+ next_obs = self.env.reset()
+ next_episode_starts = np.ones(step_dim, dtype=np.byte)
+
+ timesteps_elapsed = 0
+ while timesteps_elapsed < total_timesteps:
+ start_time = perf_counter()
+
+ progress = timesteps_elapsed / total_timesteps
+ ent_coef = self.ent_coef_schedule(progress)
+ learning_rate = self.lr_schedule(progress)
+ update_learning_rate(self.optimizer, learning_rate)
+ log_scalars(
+ self.tb_writer,
+ "charts",
+ {
+ "ent_coef": ent_coef,
+ "learning_rate": learning_rate,
+ },
+ timesteps_elapsed,
+ )
+
+ self.policy.eval()
+ self.policy.reset_noise()
+ for s in range(self.n_steps):
+ timesteps_elapsed += self.env.num_envs
+ if self.sde_sample_freq > 0 and s > 0 and s % self.sde_sample_freq == 0:
+ self.policy.reset_noise()
+
+ obs[s] = next_obs
+ episode_starts[s] = next_episode_starts
+
+ actions[s], values[s], logprobs[s], clamped_action = self.policy.step(
+ next_obs
+ )
+ next_obs, rewards[s], next_episode_starts, _ = self.env.step(
+ clamped_action
+ )
+
+ advantages = np.zeros(epoch_dim, dtype=np.float32)
+ last_gae_lam = 0
+ for t in reversed(range(self.n_steps)):
+ if t == self.n_steps - 1:
+ next_nonterminal = 1.0 - next_episode_starts
+ next_value = self.policy.value(next_obs)
+ else:
+ next_nonterminal = 1.0 - episode_starts[t + 1]
+ next_value = values[t + 1]
+ delta = (
+ rewards[t] + self.gamma * next_value * next_nonterminal - values[t]
+ )
+ last_gae_lam = (
+ delta
+ + self.gamma * self.gae_lambda * next_nonterminal * last_gae_lam
+ )
+ advantages[t] = last_gae_lam
+ returns = advantages + values
+
+ b_obs = torch.tensor(obs.reshape((-1,) + obs_space.shape)).to(self.device)
+ b_actions = torch.tensor(actions.reshape((-1,) + act_space.shape)).to(
+ self.device
+ )
+ b_advantages = torch.tensor(advantages.reshape(-1)).to(self.device)
+ b_returns = torch.tensor(returns.reshape(-1)).to(self.device)
+
+ if self.normalize_advantage:
+ b_advantages = (b_advantages - b_advantages.mean()) / (
+ b_advantages.std() + 1e-8
+ )
+
+ self.policy.train()
+ logp_a, entropy, v = self.policy(b_obs, b_actions)
+
+ pi_loss = -(b_advantages * logp_a).mean()
+ value_loss = F.mse_loss(b_returns, v)
+ entropy_loss = -entropy.mean()
+
+ loss = pi_loss + self.vf_coef * value_loss + ent_coef * entropy_loss
+
+ self.optimizer.zero_grad()
+ loss.backward()
+ nn.utils.clip_grad_norm_(self.policy.parameters(), self.max_grad_norm)
+ self.optimizer.step()
+
+ y_pred = values.reshape(-1)
+ y_true = returns.reshape(-1)
+ var_y = np.var(y_true).item()
+ explained_var = (
+ np.nan if var_y == 0 else 1 - np.var(y_true - y_pred).item() / var_y
+ )
+
+ end_time = perf_counter()
+ rollout_steps = self.n_steps * self.env.num_envs
+ self.tb_writer.add_scalar(
+ "train/steps_per_second",
+ (rollout_steps) / (end_time - start_time),
+ timesteps_elapsed,
+ )
+
+ log_scalars(
+ self.tb_writer,
+ "losses",
+ {
+ "loss": loss.item(),
+ "pi_loss": pi_loss.item(),
+ "v_loss": value_loss.item(),
+ "entropy_loss": entropy_loss.item(),
+ "explained_var": explained_var,
+ },
+ timesteps_elapsed,
+ )
+
+ if callback:
+ callback.on_step(timesteps_elapsed=rollout_steps)
+
+ return self
diff --git a/benchmark_publish.py b/benchmark_publish.py
new file mode 100644
index 0000000000000000000000000000000000000000..55fa566837baaba44c161a2fa728120115e86d25
--- /dev/null
+++ b/benchmark_publish.py
@@ -0,0 +1,107 @@
+import argparse
+import subprocess
+import wandb
+import wandb.apis.public
+
+from collections import defaultdict
+from multiprocessing.pool import ThreadPool
+from typing import List, NamedTuple
+
+
+class RunGroup(NamedTuple):
+ algo: str
+ env_id: str
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--wandb-project-name",
+ type=str,
+ default="rl-algo-impls-benchmarks",
+ help="WandB project name to load runs from",
+ )
+ parser.add_argument(
+ "--wandb-entity",
+ type=str,
+ default=None,
+ help="WandB team of project. None uses default entity",
+ )
+ parser.add_argument("--wandb-tags", type=str, nargs="+", help="WandB tags")
+ parser.add_argument("--wandb-report-url", type=str, help="Link to WandB report")
+ parser.add_argument(
+ "--envs", type=str, nargs="*", help="Optional filter down to these envs"
+ )
+ parser.add_argument(
+ "--exclude-envs",
+ type=str,
+ nargs="*",
+ help="Environments to exclude from publishing",
+ )
+ parser.add_argument(
+ "--huggingface-user",
+ type=str,
+ default=None,
+ help="Huggingface user or team to upload model cards. Defaults to huggingface-cli login user",
+ )
+ parser.add_argument(
+ "--pool-size",
+ type=int,
+ default=3,
+ help="How many publish jobs can run in parallel",
+ )
+ parser.add_argument(
+ "--virtual-display", action="store_true", help="Use headless virtual display"
+ )
+ # parser.set_defaults(
+ # wandb_tags=["benchmark_e47a44c", "host_129-146-2-230"],
+ # wandb_report_url="https://api.wandb.ai/links/sgoodfriend/v4wd7cp5",
+ # envs=[],
+ # exclude_envs=[],
+ # )
+ args = parser.parse_args()
+ print(args)
+
+ api = wandb.Api()
+ all_runs = api.runs(
+ f"{args.wandb_entity or api.default_entity}/{args.wandb_project_name}"
+ )
+
+ required_tags = set(args.wandb_tags)
+ runs: List[wandb.apis.public.Run] = [
+ r
+ for r in all_runs
+ if required_tags.issubset(set(r.config.get("wandb_tags", [])))
+ ]
+
+ runs_paths_by_group = defaultdict(list)
+ for r in runs:
+ if r.state != "finished":
+ continue
+ algo = r.config["algo"]
+ env = r.config["env"]
+ if args.envs and env not in args.envs:
+ continue
+ if args.exclude_envs and env in args.exclude_envs:
+ continue
+ run_group = RunGroup(algo, env)
+ runs_paths_by_group[run_group].append("/".join(r.path))
+
+ def run(run_paths: List[str]) -> None:
+ publish_args = ["python", "huggingface_publish.py"]
+ publish_args.append("--wandb-run-paths")
+ publish_args.extend(run_paths)
+ publish_args.append("--wandb-report-url")
+ publish_args.append(args.wandb_report_url)
+ if args.huggingface_user:
+ publish_args.append("--huggingface-user")
+ publish_args.append(args.huggingface_user)
+ if args.virtual_display:
+ publish_args.append("--virtual-display")
+ subprocess.run(publish_args)
+
+ tp = ThreadPool(args.pool_size)
+ for run_paths in runs_paths_by_group.values():
+ tp.apply_async(run, (run_paths,))
+ tp.close()
+ tp.join()
diff --git a/benchmarks/benchmark_test.sh b/benchmarks/benchmark_test.sh
new file mode 100644
index 0000000000000000000000000000000000000000..5ab766df564f86a5dad2737dfb96a421bc48067c
--- /dev/null
+++ b/benchmarks/benchmark_test.sh
@@ -0,0 +1,32 @@
+source benchmarks/train_loop.sh
+
+export WANDB_PROJECT_NAME="rl-algo-impls"
+
+BENCHMARK_MAX_PROCS="${BENCHMARK_MAX_PROCS:-3}"
+
+ALGOS=(
+ # "vpg"
+ "dqn"
+ # "ppo"
+)
+ENVS=(
+ # Basic
+ "CartPole-v1"
+ "MountainCar-v0"
+ # "MountainCarContinuous-v0"
+ "Acrobot-v1"
+ "LunarLander-v2"
+ # # PyBullet
+ # "HalfCheetahBulletEnv-v0"
+ # "AntBulletEnv-v0"
+ # "HopperBulletEnv-v0"
+ # "Walker2DBulletEnv-v0"
+ # # CarRacing
+ # "CarRacing-v0"
+ # Atari
+ "PongNoFrameskip-v4"
+ "BreakoutNoFrameskip-v4"
+ "SpaceInvadersNoFrameskip-v4"
+ "QbertNoFrameskip-v4"
+)
+train_loop "${ALGOS[*]}" "${ENVS[*]}" | xargs -I CMD -P $BENCHMARK_MAX_PROCS bash -c CMD
diff --git a/benchmarks/colab_atari1.sh b/benchmarks/colab_atari1.sh
new file mode 100644
index 0000000000000000000000000000000000000000..ac3c646314420c360191f50135510c75e720a810
--- /dev/null
+++ b/benchmarks/colab_atari1.sh
@@ -0,0 +1,5 @@
+source benchmarks/train_loop.sh
+ALGOS="ppo"
+ENVS="PongNoFrameskip-v4 BreakoutNoFrameskip-v4"
+BENCHMARK_MAX_PROCS="${BENCHMARK_MAX_PROCS:-3}"
+train_loop $ALGOS "$ENVS" | xargs -I CMD -P $BENCHMARK_MAX_PROCS bash -c CMD
\ No newline at end of file
diff --git a/benchmarks/colab_atari2.sh b/benchmarks/colab_atari2.sh
new file mode 100644
index 0000000000000000000000000000000000000000..5e7b79e72b626a8e93f635a45346670e07b20c34
--- /dev/null
+++ b/benchmarks/colab_atari2.sh
@@ -0,0 +1,5 @@
+source benchmarks/train_loop.sh
+ALGOS="ppo"
+ENVS="SpaceInvadersNoFrameskip-v4 QbertNoFrameskip-v4"
+BENCHMARK_MAX_PROCS="${BENCHMARK_MAX_PROCS:-3}"
+train_loop $ALGOS "$ENVS" | xargs -I CMD -P $BENCHMARK_MAX_PROCS bash -c CMD
\ No newline at end of file
diff --git a/benchmarks/colab_basic.sh b/benchmarks/colab_basic.sh
new file mode 100644
index 0000000000000000000000000000000000000000..affe598a08c94ea23cecacfab44931170ecfd29f
--- /dev/null
+++ b/benchmarks/colab_basic.sh
@@ -0,0 +1,5 @@
+source benchmarks/train_loop.sh
+ALGOS="ppo"
+ENVS="CartPole-v1 MountainCar-v0 MountainCarContinuous-v0 Acrobot-v1 LunarLander-v2"
+BENCHMARK_MAX_PROCS="${BENCHMARK_MAX_PROCS:-3}"
+train_loop $ALGOS "$ENVS" | xargs -I CMD -P $BENCHMARK_MAX_PROCS bash -c CMD
diff --git a/benchmarks/colab_benchmark.ipynb b/benchmarks/colab_benchmark.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..a331841fd9fdd9b85c029c9c573652381d003483
--- /dev/null
+++ b/benchmarks/colab_benchmark.ipynb
@@ -0,0 +1,195 @@
+{
+ "nbformat": 4,
+ "nbformat_minor": 0,
+ "metadata": {
+ "colab": {
+ "provenance": [],
+ "machine_shape": "hm",
+ "authorship_tag": "ABX9TyOGIH7rqgasim3Sz7b1rpoE",
+ "include_colab_link": true
+ },
+ "kernelspec": {
+ "name": "python3",
+ "display_name": "Python 3"
+ },
+ "language_info": {
+ "name": "python"
+ },
+ "gpuClass": "standard",
+ "accelerator": "GPU"
+ },
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "view-in-github",
+ "colab_type": "text"
+ },
+ "source": [
+ ""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "# [sgoodfriend/rl-algo-impls](https://github.com/sgoodfriend/rl-algo-impls) in Google Colaboratory\n",
+ "## Parameters\n",
+ "\n",
+ "\n",
+ "1. Wandb\n",
+ "\n"
+ ],
+ "metadata": {
+ "id": "S-tXDWP8WTLc"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "from getpass import getpass\n",
+ "import os\n",
+ "os.environ[\"WANDB_API_KEY\"] = getpass(\"Wandb API key to upload metrics, videos, and models: \")"
+ ],
+ "metadata": {
+ "id": "1ZtdYgxWNGwZ"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Setup\n",
+ "Clone [sgoodfriend/rl-algo-impls](https://github.com/sgoodfriend/rl-algo-impls) "
+ ],
+ "metadata": {
+ "id": "bsG35Io0hmKG"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "%%capture\n",
+ "!git clone https://github.com/sgoodfriend/rl-algo-impls.git"
+ ],
+ "metadata": {
+ "id": "k5ynTV25hdAf"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "Installing the correct packages:\n",
+ "\n",
+ "While conda and poetry are generally used for package management, the mismatch in Python versions (3.10 in the project file vs 3.8 in Colab) makes using the package yml files difficult to use. For now, instead I'm going to specify the list of requirements manually below:"
+ ],
+ "metadata": {
+ "id": "jKxGok-ElYQ7"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "%%capture\n",
+ "!apt install python-opengl\n",
+ "!apt install ffmpeg\n",
+ "!apt install xvfb\n",
+ "!apt install swig"
+ ],
+ "metadata": {
+ "id": "nn6EETTc2Ewf"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "%%capture\n",
+ "%cd /content/rl-algo-impls\n",
+ "!pip install -r colab_requirements.txt"
+ ],
+ "metadata": {
+ "id": "AfZh9rH3yQii"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Run Once Per Runtime"
+ ],
+ "metadata": {
+ "id": "4o5HOLjc4wq7"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "import wandb\n",
+ "wandb.login()"
+ ],
+ "metadata": {
+ "id": "PCXa5tdS2qFX"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Restart Session beteween runs"
+ ],
+ "metadata": {
+ "id": "AZBZfSUV43JQ"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "%%capture\n",
+ "from pyvirtualdisplay import Display\n",
+ "\n",
+ "virtual_display = Display(visible=0, size=(1400, 900))\n",
+ "virtual_display.start()"
+ ],
+ "metadata": {
+ "id": "VzemeQJP2NO9"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "The below 5 bash scripts train agents on environments with 3 seeds each:\n",
+ "- colab_basic.sh and colab_pybullet.sh test on a set of basic gym environments and 4 PyBullet environments. Running both together will likely take about 18 hours. This is likely to run into runtime limits for free Colab and Colab Pro, but is fine for Colab Pro+.\n",
+ "- colab_carracing.sh only trains 3 seeds on CarRacing-v0, which takes almost 22 hours on Colab Pro+ on high-RAM, standard GPU.\n",
+ "- colab_atari1.sh and colab_atari2.sh likely need to be run separately because each takes about 19 hours on high-RAM, standard GPU."
+ ],
+ "metadata": {
+ "id": "nSHfna0hLlO1"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "%cd /content/rl-algo-impls\n",
+ "os.environ[\"BENCHMARK_MAX_PROCS\"] = str(1) # Can't reliably raise this to 2+, but would make it faster.\n",
+ "!./benchmarks/colab_basic.sh\n",
+ "!./benchmarks/colab_pybullet.sh\n",
+ "# !./benchmarks/colab_carracing.sh\n",
+ "# !./benchmarks/colab_atari1.sh\n",
+ "# !./benchmarks/colab_atari2.sh"
+ ],
+ "metadata": {
+ "id": "07aHYFH1zfXa"
+ },
+ "execution_count": null,
+ "outputs": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/benchmarks/colab_carracing.sh b/benchmarks/colab_carracing.sh
new file mode 100644
index 0000000000000000000000000000000000000000..7101140eba9fa2ee8b953ad07d7662fba47416f9
--- /dev/null
+++ b/benchmarks/colab_carracing.sh
@@ -0,0 +1,5 @@
+source benchmarks/train_loop.sh
+ALGOS="ppo"
+ENVS="CarRacing-v0"
+BENCHMARK_MAX_PROCS="${BENCHMARK_MAX_PROCS:-3}"
+train_loop $ALGOS "$ENVS" | xargs -I CMD -P $BENCHMARK_MAX_PROCS bash -c CMD
\ No newline at end of file
diff --git a/benchmarks/colab_pybullet.sh b/benchmarks/colab_pybullet.sh
new file mode 100644
index 0000000000000000000000000000000000000000..69e36bac610748c5bd18f5a58a3e79bd39b8b181
--- /dev/null
+++ b/benchmarks/colab_pybullet.sh
@@ -0,0 +1,5 @@
+source benchmarks/train_loop.sh
+ALGOS="ppo"
+ENVS="HalfCheetahBulletEnv-v0 AntBulletEnv-v0 HopperBulletEnv-v0 Walker2DBulletEnv-v0"
+BENCHMARK_MAX_PROCS="${BENCHMARK_MAX_PROCS:-3}"
+train_loop $ALGOS "$ENVS" | xargs -I CMD -P $BENCHMARK_MAX_PROCS bash -c CMD
\ No newline at end of file
diff --git a/benchmarks/train_loop.sh b/benchmarks/train_loop.sh
new file mode 100644
index 0000000000000000000000000000000000000000..9ad750dcf09b706a045ed24cbc2c0a91575a6b4e
--- /dev/null
+++ b/benchmarks/train_loop.sh
@@ -0,0 +1,15 @@
+train_loop () {
+ local WANDB_TAGS="benchmark_$(git rev-parse --short HEAD) host_$(hostname)"
+ local algo
+ local env
+ local seed
+ local WANDB_PROJECT_NAME="${WANDB_PROJECT_NAME:-rl-algo-impls-benchmarks}"
+ local SEEDS="${SEEDS:-1 2 3}"
+ for algo in $(echo $1); do
+ for env in $(echo $2); do
+ for seed in $SEEDS; do
+ echo python train.py --algo $algo --env $env --seed $seed --pool-size 1 --wandb-tags $WANDB_TAGS --wandb-project-name $WANDB_PROJECT_NAME
+ done
+ done
+ done
+}
\ No newline at end of file
diff --git a/colab_enjoy.ipynb b/colab_enjoy.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..57daecf4ee497e489387bbb338cd2baa4b01a67a
--- /dev/null
+++ b/colab_enjoy.ipynb
@@ -0,0 +1,198 @@
+{
+ "nbformat": 4,
+ "nbformat_minor": 0,
+ "metadata": {
+ "colab": {
+ "provenance": [],
+ "machine_shape": "hm",
+ "authorship_tag": "ABX9TyN6S7kyJKrM5x0OOiN+CgTc",
+ "include_colab_link": true
+ },
+ "kernelspec": {
+ "name": "python3",
+ "display_name": "Python 3"
+ },
+ "language_info": {
+ "name": "python"
+ },
+ "gpuClass": "standard",
+ "accelerator": "GPU"
+ },
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "view-in-github",
+ "colab_type": "text"
+ },
+ "source": [
+ ""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "# [sgoodfriend/rl-algo-impls](https://github.com/sgoodfriend/rl-algo-impls) in Google Colaboratory\n",
+ "## Parameters\n",
+ "\n",
+ "\n",
+ "1. Wandb\n",
+ "\n"
+ ],
+ "metadata": {
+ "id": "S-tXDWP8WTLc"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "from getpass import getpass\n",
+ "import os\n",
+ "os.environ[\"WANDB_API_KEY\"] = getpass(\"Wandb API key to upload metrics, videos, and models: \")"
+ ],
+ "metadata": {
+ "id": "1ZtdYgxWNGwZ"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "2. enjoy.py parameters"
+ ],
+ "metadata": {
+ "id": "ao0nAh3MOdN7"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "WANDB_RUN_PATH=\"sgoodfriend/rl-algo-impls-benchmarks/rd0lisee\""
+ ],
+ "metadata": {
+ "id": "jKL_NFhVOjSc"
+ },
+ "execution_count": 2,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Setup\n",
+ "Clone [sgoodfriend/rl-algo-impls](https://github.com/sgoodfriend/rl-algo-impls) "
+ ],
+ "metadata": {
+ "id": "bsG35Io0hmKG"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "%%capture\n",
+ "!git clone https://github.com/sgoodfriend/rl-algo-impls.git"
+ ],
+ "metadata": {
+ "id": "k5ynTV25hdAf"
+ },
+ "execution_count": 3,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "Installing the correct packages:\n",
+ "\n",
+ "While conda and poetry are generally used for package management, the mismatch in Python versions (3.10 in the project file vs 3.8 in Colab) makes using the package yml files difficult to use. For now, instead I'm going to specify the list of requirements manually below:"
+ ],
+ "metadata": {
+ "id": "jKxGok-ElYQ7"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "%%capture\n",
+ "!apt install python-opengl\n",
+ "!apt install ffmpeg\n",
+ "!apt install xvfb\n",
+ "!apt install swig"
+ ],
+ "metadata": {
+ "id": "nn6EETTc2Ewf"
+ },
+ "execution_count": 4,
+ "outputs": []
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "%%capture\n",
+ "%cd /content/rl-algo-impls\n",
+ "!pip install -r colab_requirements.txt"
+ ],
+ "metadata": {
+ "id": "AfZh9rH3yQii"
+ },
+ "execution_count": 5,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Run Once Per Runtime"
+ ],
+ "metadata": {
+ "id": "4o5HOLjc4wq7"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "import wandb\n",
+ "wandb.login()"
+ ],
+ "metadata": {
+ "id": "PCXa5tdS2qFX"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Restart Session beteween runs"
+ ],
+ "metadata": {
+ "id": "AZBZfSUV43JQ"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "%%capture\n",
+ "from pyvirtualdisplay import Display\n",
+ "\n",
+ "virtual_display = Display(visible=0, size=(1400, 900))\n",
+ "virtual_display.start()"
+ ],
+ "metadata": {
+ "id": "VzemeQJP2NO9"
+ },
+ "execution_count": 7,
+ "outputs": []
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "%cd /content/rl-algo-impls\n",
+ "!python enjoy.py --wandb-run-path={WANDB_RUN_PATH}"
+ ],
+ "metadata": {
+ "id": "07aHYFH1zfXa"
+ },
+ "execution_count": null,
+ "outputs": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/colab_requirements.txt b/colab_requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..798b0a43c5831756303e88f525d1f54bf3384851
--- /dev/null
+++ b/colab_requirements.txt
@@ -0,0 +1,14 @@
+AutoROM.accept-rom-license >= 0.4.2, < 0.5
+stable-baselines3[extra] >= 1.7.0, < 1.8
+gym[box2d] >= 0.21.0, < 0.22
+pyglet == 1.5.27
+wandb >= 0.13.10, < 0.14
+pyvirtualdisplay == 3.0
+pybullet >= 3.2.5, < 3.3
+tabulate >= 0.9.0, < 0.10
+huggingface-hub >= 0.12.0, < 0.13
+numexpr >= 2.8.4, < 2.9
+gym3 >= 0.3.3, < 0.4
+glfw >= 1.12.0, < 1.13
+procgen >= 0.10.7, < 0.11
+ipython >= 8.10.0, < 8.11
\ No newline at end of file
diff --git a/colab_train.ipynb b/colab_train.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..7f981493d0545cfabd5b73db13f1f3bb30114855
--- /dev/null
+++ b/colab_train.ipynb
@@ -0,0 +1,200 @@
+{
+ "nbformat": 4,
+ "nbformat_minor": 0,
+ "metadata": {
+ "colab": {
+ "provenance": [],
+ "machine_shape": "hm",
+ "authorship_tag": "ABX9TyMmemQnx6G7GOnn6XBdjgxY",
+ "include_colab_link": true
+ },
+ "kernelspec": {
+ "name": "python3",
+ "display_name": "Python 3"
+ },
+ "language_info": {
+ "name": "python"
+ },
+ "gpuClass": "standard",
+ "accelerator": "GPU"
+ },
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "view-in-github",
+ "colab_type": "text"
+ },
+ "source": [
+ ""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "# [sgoodfriend/rl-algo-impls](https://github.com/sgoodfriend/rl-algo-impls) in Google Colaboratory\n",
+ "## Parameters\n",
+ "\n",
+ "\n",
+ "1. Wandb\n",
+ "\n"
+ ],
+ "metadata": {
+ "id": "S-tXDWP8WTLc"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "from getpass import getpass\n",
+ "import os\n",
+ "os.environ[\"WANDB_API_KEY\"] = getpass(\"Wandb API key to upload metrics, videos, and models: \")"
+ ],
+ "metadata": {
+ "id": "1ZtdYgxWNGwZ"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "2. train run parameters"
+ ],
+ "metadata": {
+ "id": "ao0nAh3MOdN7"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "ALGO = \"ppo\"\n",
+ "ENV = \"CartPole-v1\"\n",
+ "SEED = 1"
+ ],
+ "metadata": {
+ "id": "jKL_NFhVOjSc"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Setup\n",
+ "Clone [sgoodfriend/rl-algo-impls](https://github.com/sgoodfriend/rl-algo-impls) "
+ ],
+ "metadata": {
+ "id": "bsG35Io0hmKG"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "%%capture\n",
+ "!git clone https://github.com/sgoodfriend/rl-algo-impls.git"
+ ],
+ "metadata": {
+ "id": "k5ynTV25hdAf"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "Installing the correct packages:\n",
+ "\n",
+ "While conda and poetry are generally used for package management, the mismatch in Python versions (3.10 in the project file vs 3.8 in Colab) makes using the package yml files difficult to use. For now, instead I'm going to specify the list of requirements manually below:"
+ ],
+ "metadata": {
+ "id": "jKxGok-ElYQ7"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "%%capture\n",
+ "!apt install python-opengl\n",
+ "!apt install ffmpeg\n",
+ "!apt install xvfb\n",
+ "!apt install swig"
+ ],
+ "metadata": {
+ "id": "nn6EETTc2Ewf"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "%%capture\n",
+ "%cd /content/rl-algo-impls\n",
+ "!pip install -r colab_requirements.txt"
+ ],
+ "metadata": {
+ "id": "AfZh9rH3yQii"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Run Once Per Runtime"
+ ],
+ "metadata": {
+ "id": "4o5HOLjc4wq7"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "import wandb\n",
+ "wandb.login()"
+ ],
+ "metadata": {
+ "id": "PCXa5tdS2qFX"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Restart Session beteween runs"
+ ],
+ "metadata": {
+ "id": "AZBZfSUV43JQ"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "%%capture\n",
+ "from pyvirtualdisplay import Display\n",
+ "\n",
+ "virtual_display = Display(visible=0, size=(1400, 900))\n",
+ "virtual_display.start()"
+ ],
+ "metadata": {
+ "id": "VzemeQJP2NO9"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "%cd /content/rl-algo-impls\n",
+ "!python train.py --algo {ALGO} --env {ENV} --seed {SEED}"
+ ],
+ "metadata": {
+ "id": "07aHYFH1zfXa"
+ },
+ "execution_count": null,
+ "outputs": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/compare_runs.py b/compare_runs.py
new file mode 100644
index 0000000000000000000000000000000000000000..d92a62dfbfa10a208cc6f64c62fd45a300004071
--- /dev/null
+++ b/compare_runs.py
@@ -0,0 +1,187 @@
+import argparse
+import itertools
+import numpy as np
+import pandas as pd
+import wandb
+import wandb.apis.public
+
+from collections import defaultdict
+from dataclasses import dataclass
+from typing import Dict, Iterable, List, TypeVar
+
+from benchmark_publish import RunGroup
+
+
+@dataclass
+class Comparison:
+ control_values: List[float]
+ experiment_values: List[float]
+
+ def mean_diff_percentage(self) -> float:
+ return self._diff_percentage(
+ np.mean(self.control_values).item(), np.mean(self.experiment_values).item()
+ )
+
+ def median_diff_percentage(self) -> float:
+ return self._diff_percentage(
+ np.median(self.control_values).item(),
+ np.median(self.experiment_values).item(),
+ )
+
+ def _diff_percentage(self, c: float, e: float) -> float:
+ if c == e:
+ return 0
+ elif c == 0:
+ return float("inf") if e > 0 else float("-inf")
+ return 100 * (e - c) / c
+
+ def score(self) -> float:
+ return (
+ np.sum(
+ np.sign((self.mean_diff_percentage(), self.median_diff_percentage()))
+ ).item()
+ / 2
+ )
+
+
+RunGroupRunsSelf = TypeVar("RunGroupRunsSelf", bound="RunGroupRuns")
+
+
+class RunGroupRuns:
+ def __init__(
+ self,
+ run_group: RunGroup,
+ control: List[str],
+ experiment: List[str],
+ summary_stats: List[str] = ["best_eval", "eval", "train_rolling"],
+ summary_metrics: List[str] = ["mean", "result"],
+ ) -> None:
+ self.algo = run_group.algo
+ self.env = run_group.env_id
+ self.control = set(control)
+ self.experiment = set(experiment)
+
+ self.summary_stats = summary_stats
+ self.summary_metrics = summary_metrics
+
+ self.control_runs = []
+ self.experiment_runs = []
+
+ def add_run(self, run: wandb.apis.public.Run) -> None:
+ wandb_tags = set(run.config.get("wandb_tags", []))
+ if self.control & wandb_tags:
+ self.control_runs.append(run)
+ elif self.experiment & wandb_tags:
+ self.experiment_runs.append(run)
+
+ def comparisons_by_metric(self) -> Dict[str, Comparison]:
+ c_by_m = {}
+ for metric in (
+ f"{s}/{m}"
+ for s, m in itertools.product(self.summary_stats, self.summary_metrics)
+ ):
+ c_by_m[metric] = Comparison(
+ [c.summary[metric] for c in self.control_runs],
+ [e.summary[metric] for e in self.experiment_runs],
+ )
+ return c_by_m
+
+ @staticmethod
+ def data_frame(rows: Iterable[RunGroupRunsSelf]) -> pd.DataFrame:
+ results = defaultdict(list)
+ for r in rows:
+ if not r.control_runs or not r.experiment_runs:
+ continue
+ results["algo"].append(r.algo)
+ results["env"].append(r.env)
+ results["control"].append(r.control)
+ results["expierment"].append(r.experiment)
+ c_by_m = r.comparisons_by_metric()
+ results["score"].append(
+ sum(m.score() for m in c_by_m.values()) / len(c_by_m)
+ )
+ for m, c in c_by_m.items():
+ results[f"{m}_mean"].append(c.mean_diff_percentage())
+ results[f"{m}_median"].append(c.median_diff_percentage())
+ return pd.DataFrame(results)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "-p",
+ "--wandb-project-name",
+ type=str,
+ default="rl-algo-impls-benchmarks",
+ help="WandB project name to load runs from",
+ )
+ parser.add_argument(
+ "--wandb-entity",
+ type=str,
+ default=None,
+ help="WandB team. None uses default entity",
+ )
+ parser.add_argument(
+ "-n",
+ "--wandb-hostname-tag",
+ type=str,
+ nargs="*",
+ help="WandB tags for hostname (i.e. host_192-9-145-26)",
+ )
+ parser.add_argument(
+ "-c",
+ "--wandb-control-tag",
+ type=str,
+ nargs="+",
+ help="WandB tag for control commit (i.e. benchmark_5598ebc)",
+ )
+ parser.add_argument(
+ "-e",
+ "--wandb-experiment-tag",
+ type=str,
+ nargs="+",
+ help="WandB tag for experiment commit (i.e. benchmark_5540e1f)",
+ )
+ parser.add_argument(
+ "--exclude-envs",
+ type=str,
+ nargs="*",
+ help="Environments to exclude from comparison",
+ )
+ # parser.set_defaults(
+ # wandb_hostname_tag=["host_150-230-44-105", "host_155-248-214-128"],
+ # wandb_control_tag=["benchmark_fbc943f"],
+ # wandb_experiment_tag=["benchmark_f59bf74"],
+ # exclude_envs=[],
+ # )
+ args = parser.parse_args()
+ print(args)
+
+ api = wandb.Api()
+ all_runs = api.runs(
+ path=f"{args.wandb_entity or api.default_entity}/{args.wandb_project_name}",
+ order="+created_at",
+ )
+
+ runs_by_run_group: Dict[RunGroup, RunGroupRuns] = {}
+ wandb_hostname_tags = set(args.wandb_hostname_tag)
+ for r in all_runs:
+ if r.state != "finished":
+ continue
+ wandb_tags = set(r.config.get("wandb_tags", []))
+ if not wandb_tags or not wandb_hostname_tags & wandb_tags:
+ continue
+ rg = RunGroup(r.config["algo"], r.config.get("env_id") or r.config["env"])
+ if args.exclude_envs and rg.env_id in args.exclude_envs:
+ continue
+ if rg not in runs_by_run_group:
+ runs_by_run_group[rg] = RunGroupRuns(
+ rg,
+ args.wandb_control_tag,
+ args.wandb_experiment_tag,
+ )
+ runs_by_run_group[rg].add_run(r)
+ df = RunGroupRuns.data_frame(runs_by_run_group.values()).round(decimals=2)
+ print(f"**Total Score: {sum(df.score)}**")
+ df.loc["mean"] = df.mean(numeric_only=True)
+ print(df.to_markdown())
diff --git a/dqn/dqn.py b/dqn/dqn.py
new file mode 100644
index 0000000000000000000000000000000000000000..d057178294f661ff8ea83a1b023943480b6faf9e
--- /dev/null
+++ b/dqn/dqn.py
@@ -0,0 +1,182 @@
+import copy
+import numpy as np
+import random
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+
+from collections import deque
+from torch.optim import Adam
+from torch.utils.tensorboard.writer import SummaryWriter
+from typing import List, NamedTuple, Optional, TypeVar
+
+from dqn.policy import DQNPolicy
+from shared.algorithm import Algorithm
+from shared.callbacks.callback import Callback
+from shared.schedule import linear_schedule
+from wrappers.vectorable_wrapper import VecEnv, VecEnvObs
+
+
+class Transition(NamedTuple):
+ obs: np.ndarray
+ action: np.ndarray
+ reward: float
+ done: bool
+ next_obs: np.ndarray
+
+
+class Batch(NamedTuple):
+ obs: np.ndarray
+ actions: np.ndarray
+ rewards: np.ndarray
+ dones: np.ndarray
+ next_obs: np.ndarray
+
+
+class ReplayBuffer:
+ def __init__(self, num_envs: int, maxlen: int) -> None:
+ self.num_envs = num_envs
+ self.buffer = deque(maxlen=maxlen)
+
+ def add(
+ self,
+ obs: VecEnvObs,
+ action: np.ndarray,
+ reward: np.ndarray,
+ done: np.ndarray,
+ next_obs: VecEnvObs,
+ ) -> None:
+ assert isinstance(obs, np.ndarray)
+ assert isinstance(next_obs, np.ndarray)
+ for i in range(self.num_envs):
+ self.buffer.append(
+ Transition(obs[i], action[i], reward[i], done[i], next_obs[i])
+ )
+
+ def sample(self, batch_size: int) -> Batch:
+ ts = random.sample(self.buffer, batch_size)
+ return Batch(
+ obs=np.array([t.obs for t in ts]),
+ actions=np.array([t.action for t in ts]),
+ rewards=np.array([t.reward for t in ts]),
+ dones=np.array([t.done for t in ts]),
+ next_obs=np.array([t.next_obs for t in ts]),
+ )
+
+ def __len__(self) -> int:
+ return len(self.buffer)
+
+
+DQNSelf = TypeVar("DQNSelf", bound="DQN")
+
+
+class DQN(Algorithm):
+ def __init__(
+ self,
+ policy: DQNPolicy,
+ env: VecEnv,
+ device: torch.device,
+ tb_writer: SummaryWriter,
+ learning_rate: float = 1e-4,
+ buffer_size: int = 1_000_000,
+ learning_starts: int = 50_000,
+ batch_size: int = 32,
+ tau: float = 1.0,
+ gamma: float = 0.99,
+ train_freq: int = 4,
+ gradient_steps: int = 1,
+ target_update_interval: int = 10_000,
+ exploration_fraction: float = 0.1,
+ exploration_initial_eps: float = 1.0,
+ exploration_final_eps: float = 0.05,
+ max_grad_norm: float = 10.0,
+ ) -> None:
+ super().__init__(policy, env, device, tb_writer)
+ self.policy = policy
+
+ self.optimizer = Adam(self.policy.q_net.parameters(), lr=learning_rate)
+
+ self.target_q_net = copy.deepcopy(self.policy.q_net).to(self.device)
+ self.target_q_net.train(False)
+ self.tau = tau
+ self.target_update_interval = target_update_interval
+
+ self.replay_buffer = ReplayBuffer(self.env.num_envs, buffer_size)
+ self.batch_size = batch_size
+
+ self.learning_starts = learning_starts
+ self.train_freq = train_freq
+ self.gradient_steps = gradient_steps
+
+ self.gamma = gamma
+ self.exploration_eps_schedule = linear_schedule(
+ exploration_initial_eps,
+ exploration_final_eps,
+ end_fraction=exploration_fraction,
+ )
+
+ self.max_grad_norm = max_grad_norm
+
+ def learn(
+ self: DQNSelf, total_timesteps: int, callback: Optional[Callback] = None
+ ) -> DQNSelf:
+ self.policy.train(True)
+ obs = self.env.reset()
+ obs = self._collect_rollout(self.learning_starts, obs, 1)
+ learning_steps = total_timesteps - self.learning_starts
+ timesteps_elapsed = 0
+ steps_since_target_update = 0
+ while timesteps_elapsed < learning_steps:
+ progress = timesteps_elapsed / learning_steps
+ eps = self.exploration_eps_schedule(progress)
+ obs = self._collect_rollout(self.train_freq, obs, eps)
+ rollout_steps = self.train_freq
+ timesteps_elapsed += rollout_steps
+ for _ in range(
+ self.gradient_steps if self.gradient_steps > 0 else self.train_freq
+ ):
+ self.train()
+ steps_since_target_update += rollout_steps
+ if steps_since_target_update >= self.target_update_interval:
+ self._update_target()
+ steps_since_target_update = 0
+ if callback:
+ callback.on_step(timesteps_elapsed=rollout_steps)
+ return self
+
+ def train(self) -> None:
+ if len(self.replay_buffer) < self.batch_size:
+ return
+ o, a, r, d, next_o = self.replay_buffer.sample(self.batch_size)
+ o = torch.as_tensor(o, device=self.device)
+ a = torch.as_tensor(a, device=self.device).unsqueeze(1)
+ r = torch.as_tensor(r, dtype=torch.float32, device=self.device)
+ d = torch.as_tensor(d, dtype=torch.long, device=self.device)
+ next_o = torch.as_tensor(next_o, device=self.device)
+
+ with torch.no_grad():
+ target = r + (1 - d) * self.gamma * self.target_q_net(next_o).max(1).values
+ current = self.policy.q_net(o).gather(dim=1, index=a).squeeze(1)
+ loss = F.smooth_l1_loss(current, target)
+
+ self.optimizer.zero_grad()
+ loss.backward()
+ if self.max_grad_norm:
+ nn.utils.clip_grad_norm_(self.policy.q_net.parameters(), self.max_grad_norm)
+ self.optimizer.step()
+
+ def _collect_rollout(self, timesteps: int, obs: VecEnvObs, eps: float) -> VecEnvObs:
+ for _ in range(0, timesteps, self.env.num_envs):
+ action = self.policy.act(obs, eps, deterministic=False)
+ next_obs, reward, done, _ = self.env.step(action)
+ self.replay_buffer.add(obs, action, reward, done, next_obs)
+ obs = next_obs
+ return obs
+
+ def _update_target(self) -> None:
+ for target_param, param in zip(
+ self.target_q_net.parameters(), self.policy.q_net.parameters()
+ ):
+ target_param.data.copy_(
+ self.tau * param.data + (1 - self.tau) * target_param.data
+ )
diff --git a/dqn/policy.py b/dqn/policy.py
new file mode 100644
index 0000000000000000000000000000000000000000..ea6c28b5db004676e0bd22eccc3827e781b7b1a8
--- /dev/null
+++ b/dqn/policy.py
@@ -0,0 +1,52 @@
+import numpy as np
+import os
+import torch
+
+from typing import Optional, Sequence, TypeVar
+
+from dqn.q_net import QNetwork
+from shared.policy.policy import Policy
+from wrappers.vectorable_wrapper import (
+ VecEnv,
+ VecEnvObs,
+ single_observation_space,
+ single_action_space,
+)
+
+DQNPolicySelf = TypeVar("DQNPolicySelf", bound="DQNPolicy")
+
+
+class DQNPolicy(Policy):
+ def __init__(
+ self,
+ env: VecEnv,
+ hidden_sizes: Sequence[int] = [],
+ cnn_feature_dim: int = 512,
+ cnn_style: str = "nature",
+ cnn_layers_init_orthogonal: Optional[bool] = None,
+ impala_channels: Sequence[int] = (16, 32, 32),
+ **kwargs,
+ ) -> None:
+ super().__init__(env, **kwargs)
+ self.q_net = QNetwork(
+ single_observation_space(env),
+ single_action_space(env),
+ hidden_sizes,
+ cnn_feature_dim=cnn_feature_dim,
+ cnn_style=cnn_style,
+ cnn_layers_init_orthogonal=cnn_layers_init_orthogonal,
+ impala_channels=impala_channels,
+ )
+
+ def act(
+ self, obs: VecEnvObs, eps: float = 0, deterministic: bool = True
+ ) -> np.ndarray:
+ assert eps == 0 if deterministic else eps >= 0
+ if not deterministic and np.random.random() < eps:
+ return np.array(
+ [self.env.action_space.sample() for _ in range(self.env.num_envs)]
+ )
+ else:
+ o = self._as_tensor(obs)
+ with torch.no_grad():
+ return self.q_net(o).argmax(axis=1).cpu().numpy()
diff --git a/dqn/q_net.py b/dqn/q_net.py
new file mode 100644
index 0000000000000000000000000000000000000000..37859f0c22708392713c95d87964de2c494cbeb9
--- /dev/null
+++ b/dqn/q_net.py
@@ -0,0 +1,41 @@
+import gym
+import torch as th
+import torch.nn as nn
+
+from gym.spaces import Discrete
+from typing import Optional, Sequence, Type
+
+from shared.module.feature_extractor import FeatureExtractor
+from shared.module.module import mlp
+
+
+class QNetwork(nn.Module):
+ def __init__(
+ self,
+ observation_space: gym.Space,
+ action_space: gym.Space,
+ hidden_sizes: Sequence[int] = [],
+ activation: Type[nn.Module] = nn.ReLU, # Used by stable-baselines3
+ cnn_feature_dim: int = 512,
+ cnn_style: str = "nature",
+ cnn_layers_init_orthogonal: Optional[bool] = None,
+ impala_channels: Sequence[int] = (16, 32, 32),
+ ) -> None:
+ super().__init__()
+ assert isinstance(action_space, Discrete)
+ self._feature_extractor = FeatureExtractor(
+ observation_space,
+ activation,
+ cnn_feature_dim=cnn_feature_dim,
+ cnn_style=cnn_style,
+ cnn_layers_init_orthogonal=cnn_layers_init_orthogonal,
+ impala_channels=impala_channels,
+ )
+ layer_sizes = (
+ (self._feature_extractor.out_dim,) + tuple(hidden_sizes) + (action_space.n,)
+ )
+ self._fc = mlp(layer_sizes, activation)
+
+ def forward(self, obs: th.Tensor) -> th.Tensor:
+ x = self._feature_extractor(obs)
+ return self._fc(x)
diff --git a/enjoy.py b/enjoy.py
new file mode 100644
index 0000000000000000000000000000000000000000..fd004de69152d374d292d4910e9854364a4a08e5
--- /dev/null
+++ b/enjoy.py
@@ -0,0 +1,30 @@
+# Support for PyTorch mps mode (https://pytorch.org/docs/stable/notes/mps.html)
+import os
+
+os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
+
+from runner.evaluate import EvalArgs, evaluate_model
+from runner.running_utils import base_parser
+
+
+if __name__ == "__main__":
+ parser = base_parser(multiple=False)
+ parser.add_argument("--render", default=True, type=bool)
+ parser.add_argument("--best", default=True, type=bool)
+ parser.add_argument("--n_envs", default=1, type=int)
+ parser.add_argument("--n_episodes", default=3, type=int)
+ parser.add_argument("--deterministic-eval", default=None, type=bool)
+ parser.add_argument(
+ "--no-print-returns", action="store_true", help="Limit printing"
+ )
+ # wandb-run-path overrides base RunArgs
+ parser.add_argument("--wandb-run-path", default=None, type=str)
+ parser.set_defaults(
+ algo=["ppo"],
+ )
+ args = parser.parse_args()
+ args.algo = args.algo[0]
+ args.env = args.env[0]
+ args = EvalArgs(**vars(args))
+
+ evaluate_model(args, os.path.dirname(__file__))
diff --git a/environment.yml b/environment.yml
new file mode 100644
index 0000000000000000000000000000000000000000..969f19408426ae75cf64779f01acd2418adbfd85
--- /dev/null
+++ b/environment.yml
@@ -0,0 +1,17 @@
+name: rl_algo_impls
+channels:
+ - pytorch
+ - conda-forge
+ - nodefaults
+dependencies:
+ - python=3.10.*
+ - mamba
+ - pip
+ - poetry
+ - pytorch
+ - torchvision
+ - torchaudio
+ - cmake
+ - swig
+ - ipywidgets
+ - black
diff --git a/huggingface_publish.py b/huggingface_publish.py
new file mode 100644
index 0000000000000000000000000000000000000000..cc481c73f978df4af466228d4e1d9ec8701c5398
--- /dev/null
+++ b/huggingface_publish.py
@@ -0,0 +1,189 @@
+import os
+
+os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
+
+import argparse
+import requests
+import shutil
+import subprocess
+import tempfile
+import wandb
+import wandb.apis.public
+
+from typing import List, Optional
+
+from huggingface_hub.hf_api import HfApi, upload_folder
+from huggingface_hub.repocard import metadata_save
+from pyvirtualdisplay.display import Display
+
+from publish.markdown_format import EvalTableData, model_card_text
+from runner.config import EnvHyperparams
+from runner.evaluate import EvalArgs, evaluate_model
+from runner.env import make_eval_env
+from shared.callbacks.eval_callback import evaluate
+from wrappers.vec_episode_recorder import VecEpisodeRecorder
+
+
+def publish(
+ wandb_run_paths: List[str],
+ wandb_report_url: str,
+ huggingface_user: Optional[str] = None,
+ huggingface_token: Optional[str] = None,
+ virtual_display: bool = False,
+) -> None:
+ if virtual_display:
+ display = Display(visible=False, size=(1400, 900))
+ display.start()
+
+ api = wandb.Api()
+ runs = [api.run(rp) for rp in wandb_run_paths]
+ algo = runs[0].config["algo"]
+ hyperparam_id = runs[0].config["env"]
+ evaluations = [
+ evaluate_model(
+ EvalArgs(
+ algo,
+ hyperparam_id,
+ seed=r.config.get("seed", None),
+ render=False,
+ best=True,
+ n_envs=None,
+ n_episodes=10,
+ no_print_returns=True,
+ wandb_run_path="/".join(r.path),
+ ),
+ os.path.dirname(__file__),
+ )
+ for r in runs
+ ]
+ run_metadata = requests.get(runs[0].file("wandb-metadata.json").url).json()
+ table_data = list(EvalTableData(r, e) for r, e in zip(runs, evaluations))
+ best_eval = sorted(
+ table_data, key=lambda d: d.evaluation.stats.score, reverse=True
+ )[0]
+
+ with tempfile.TemporaryDirectory() as tmpdirname:
+ _, (policy, stats, config) = best_eval
+
+ repo_name = config.model_name(include_seed=False)
+ repo_dir_path = os.path.join(tmpdirname, repo_name)
+ # Locally clone this repo to a temp directory
+ subprocess.run(["git", "clone", ".", repo_dir_path])
+ shutil.rmtree(os.path.join(repo_dir_path, ".git"))
+ model_path = config.model_dir_path(best=True, downloaded=True)
+ shutil.copytree(
+ model_path,
+ os.path.join(
+ repo_dir_path, "saved_models", config.model_dir_name(best=True)
+ ),
+ )
+
+ github_url = "https://github.com/sgoodfriend/rl-algo-impls"
+ commit_hash = run_metadata.get("git", {}).get("commit", None)
+ env_id = runs[0].config.get("env_id") or runs[0].config["env"]
+ card_text = model_card_text(
+ algo,
+ env_id,
+ github_url,
+ commit_hash,
+ wandb_report_url,
+ table_data,
+ best_eval,
+ )
+ readme_filepath = os.path.join(repo_dir_path, "README.md")
+ os.remove(readme_filepath)
+ with open(readme_filepath, "w") as f:
+ f.write(card_text)
+
+ metadata = {
+ "library_name": "rl-algo-impls",
+ "tags": [
+ env_id,
+ algo,
+ "deep-reinforcement-learning",
+ "reinforcement-learning",
+ ],
+ "model-index": [
+ {
+ "name": algo,
+ "results": [
+ {
+ "metrics": [
+ {
+ "type": "mean_reward",
+ "value": str(stats.score),
+ "name": "mean_reward",
+ }
+ ],
+ "task": {
+ "type": "reinforcement-learning",
+ "name": "reinforcement-learning",
+ },
+ "dataset": {
+ "name": env_id,
+ "type": env_id,
+ },
+ }
+ ],
+ }
+ ],
+ }
+ metadata_save(readme_filepath, metadata)
+
+ video_env = VecEpisodeRecorder(
+ make_eval_env(
+ config,
+ EnvHyperparams(**config.env_hyperparams),
+ override_n_envs=1,
+ normalize_load_path=model_path,
+ ),
+ os.path.join(repo_dir_path, "replay"),
+ max_video_length=3600,
+ )
+ evaluate(
+ video_env,
+ policy,
+ 1,
+ deterministic=config.eval_params.get("deterministic", True),
+ )
+
+ api = HfApi()
+ huggingface_user = huggingface_user or api.whoami()["name"]
+ huggingface_repo = f"{huggingface_user}/{repo_name}"
+ api.create_repo(
+ token=huggingface_token,
+ repo_id=huggingface_repo,
+ private=False,
+ exist_ok=True,
+ )
+ repo_url = upload_folder(
+ repo_id=huggingface_repo,
+ folder_path=repo_dir_path,
+ path_in_repo="",
+ commit_message=f"{algo.upper()} playing {env_id} from {github_url}/tree/{commit_hash}",
+ token=huggingface_token,
+ )
+ print(f"Pushed model to the hub: {repo_url}")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--wandb-run-paths",
+ type=str,
+ nargs="+",
+ help="Run paths of the form entity/project/run_id",
+ )
+ parser.add_argument("--wandb-report-url", type=str, help="Link to WandB report")
+ parser.add_argument(
+ "--huggingface-user",
+ type=str,
+ help="Huggingface user or team to upload model cards",
+ default=None,
+ )
+ parser.add_argument(
+ "--virtual-display", action="store_true", help="Use headless virtual display"
+ )
+ args = parser.parse_args()
+ print(args)
+ publish(**vars(args))
diff --git a/hyperparams/a2c.yml b/hyperparams/a2c.yml
new file mode 100644
index 0000000000000000000000000000000000000000..508c17d3442592e5cdedca7e16e2bf1bd466ee78
--- /dev/null
+++ b/hyperparams/a2c.yml
@@ -0,0 +1,127 @@
+CartPole-v1: &cartpole-defaults
+ n_timesteps: !!float 5e5
+ env_hyperparams:
+ n_envs: 8
+
+CartPole-v0:
+ <<: *cartpole-defaults
+
+MountainCar-v0:
+ n_timesteps: !!float 1e6
+ env_hyperparams:
+ n_envs: 16
+ normalize: true
+
+MountainCarContinuous-v0:
+ n_timesteps: !!float 1e5
+ env_hyperparams:
+ n_envs: 4
+ normalize: true
+ # policy_hyperparams:
+ # use_sde: true
+ # log_std_init: 0.0
+ # init_layers_orthogonal: false
+ algo_hyperparams:
+ n_steps: 100
+ sde_sample_freq: 16
+
+Acrobot-v1:
+ n_timesteps: !!float 5e5
+ env_hyperparams:
+ normalize: true
+ n_envs: 16
+
+LunarLander-v2:
+ n_timesteps: !!float 1e6
+ env_hyperparams:
+ n_envs: 8
+ normalize: true
+ algo_hyperparams:
+ n_steps: 5
+ gamma: 0.995
+ learning_rate: !!float 8.3e-4
+ learning_rate_decay: linear
+ ent_coef: !!float 1e-5
+
+BipedalWalker-v3:
+ n_timesteps: !!float 5e6
+ env_hyperparams:
+ n_envs: 16
+ normalize: true
+ policy_hyperparams:
+ use_sde: true
+ log_std_init: -2
+ init_layers_orthogonal: false
+ algo_hyperparams:
+ ent_coef: 0
+ max_grad_norm: 0.5
+ n_steps: 8
+ gae_lambda: 0.9
+ vf_coef: 0.4
+ gamma: 0.99
+ learning_rate: !!float 9.6e-4
+ learning_rate_decay: linear
+
+HalfCheetahBulletEnv-v0: &pybullet-defaults
+ n_timesteps: !!float 2e6
+ env_hyperparams:
+ n_envs: 4
+ normalize: true
+ policy_hyperparams:
+ use_sde: true
+ log_std_init: -2
+ init_layers_orthogonal: false
+ algo_hyperaparms: &pybullet-algo-defaults
+ n_steps: 8
+ ent_coef: 0
+ max_grad_norm: 0.5
+ gae_lambda: 0.9
+ gamma: 0.99
+ vf_coef: 0.4
+ learning_rate: !!float 9.6e-4
+ learning_rate_decay: linear
+
+AntBulletEnv-v0:
+ <<: *pybullet-defaults
+
+Walker2DBulletEnv-v0:
+ <<: *pybullet-defaults
+
+HopperBulletEnv-v0:
+ <<: *pybullet-defaults
+
+CarRacing-v0:
+ n_timesteps: !!float 4e6
+ env_hyperparams:
+ n_envs: 8
+ frame_stack: 4
+ policy_hyperparams:
+ use_sde: true
+ log_std_init: -2
+ init_layers_orthogonal: false
+ activation_fn: relu
+ share_features_extractor: false
+ cnn_feature_dim: 256
+ hidden_sizes: [256]
+ algo_hyperparams:
+ n_steps: 8
+ learning_rate: !!float 5e-5
+ learning_rate_decay: linear
+ gamma: 0.99
+ gae_lambda: 0.95
+ ent_coef: 0
+ sde_sample_freq: 4
+
+_atari: &atari-defaults
+ n_timesteps: !!float 1e7
+ env_hyperparams: &atari-env-defaults
+ n_envs: 16
+ frame_stack: 4
+ no_reward_timeout_steps: 1000
+ no_reward_fire_steps: 500
+ vec_env_class: async
+ policy_hyperparams: &atari-policy-defaults
+ activation_fn: relu
+ algo_hyperparams:
+ ent_coef: 0.01
+ vf_coef: 0.25
diff --git a/hyperparams/dqn.yml b/hyperparams/dqn.yml
new file mode 100644
index 0000000000000000000000000000000000000000..66003a67dd8c7865fd9ea269f6fc84b5d95fb428
--- /dev/null
+++ b/hyperparams/dqn.yml
@@ -0,0 +1,130 @@
+CartPole-v1: &cartpole-defaults
+ n_timesteps: !!float 5e4
+ env_hyperparams:
+ rolling_length: 50
+ policy_hyperparams:
+ hidden_sizes: [256, 256]
+ algo_hyperparams:
+ learning_rate: !!float 2.3e-3
+ batch_size: 64
+ buffer_size: 100000
+ learning_starts: 1000
+ gamma: 0.99
+ target_update_interval: 10
+ train_freq: 256
+ gradient_steps: 128
+ exploration_fraction: 0.16
+ exploration_final_eps: 0.04
+ eval_params:
+ step_freq: !!float 1e4
+
+CartPole-v0:
+ <<: *cartpole-defaults
+ n_timesteps: !!float 4e4
+
+MountainCar-v0:
+ n_timesteps: !!float 1.2e5
+ env_hyperparams:
+ rolling_length: 50
+ policy_hyperparams:
+ hidden_sizes: [256, 256]
+ algo_hyperparams:
+ learning_rate: !!float 4e-3
+ batch_size: 128
+ buffer_size: 10000
+ learning_starts: 1000
+ gamma: 0.98
+ target_update_interval: 600
+ train_freq: 16
+ gradient_steps: 8
+ exploration_fraction: 0.2
+ exploration_final_eps: 0.07
+
+Acrobot-v1:
+ n_timesteps: !!float 1e5
+ env_hyperparams:
+ rolling_length: 50
+ policy_hyperparams:
+ hidden_sizes: [256, 256]
+ algo_hyperparams:
+ learning_rate: !!float 6.3e-4
+ batch_size: 128
+ buffer_size: 50000
+ learning_starts: 0
+ gamma: 0.99
+ target_update_interval: 250
+ train_freq: 4
+ gradient_steps: -1
+ exploration_fraction: 0.12
+ exploration_final_eps: 0.1
+
+LunarLander-v2:
+ n_timesteps: !!float 5e5
+ env_hyperparams:
+ rolling_length: 50
+ policy_hyperparams:
+ hidden_sizes: [256, 256]
+ algo_hyperparams:
+ learning_rate: !!float 1e-4
+ batch_size: 256
+ buffer_size: 100000
+ learning_starts: 10000
+ gamma: 0.99
+ target_update_interval: 250
+ train_freq: 8
+ gradient_steps: -1
+ exploration_fraction: 0.12
+ exploration_final_eps: 0.1
+ max_grad_norm: 0.5
+ eval_params:
+ step_freq: 25_000
+
+_atari: &atari-defaults
+ n_timesteps: !!float 1e7
+ env_hyperparams:
+ frame_stack: 4
+ no_reward_timeout_steps: 1_000
+ no_reward_fire_steps: 500
+ n_envs: 8
+ vec_env_class: async
+ algo_hyperparams:
+ buffer_size: 100000
+ learning_rate: !!float 1e-4
+ batch_size: 32
+ learning_starts: 100000
+ target_update_interval: 1000
+ train_freq: 8
+ gradient_steps: 2
+ exploration_fraction: 0.1
+ exploration_final_eps: 0.01
+ eval_params:
+ deterministic: false
+
+PongNoFrameskip-v4:
+ <<: *atari-defaults
+ n_timesteps: !!float 2.5e6
+
+_impala-atari: &impala-atari-defaults
+ <<: *atari-defaults
+ policy_hyperparams:
+ cnn_style: impala
+ cnn_feature_dim: 256
+ init_layers_orthogonal: true
+ cnn_layers_init_orthogonal: false
+
+impala-PongNoFrameskip-v4:
+ <<: *impala-atari-defaults
+ env_id: PongNoFrameskip-v4
+ n_timesteps: !!float 2.5e6
+
+impala-BreakoutNoFrameskip-v4:
+ <<: *impala-atari-defaults
+ env_id: BreakoutNoFrameskip-v4
+
+impala-SpaceInvadersNoFrameskip-v4:
+ <<: *impala-atari-defaults
+ env_id: SpaceInvadersNoFrameskip-v4
+
+impala-QbertNoFrameskip-v4:
+ <<: *impala-atari-defaults
+ env_id: QbertNoFrameskip-v4
diff --git a/hyperparams/ppo.yml b/hyperparams/ppo.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0136fc46b019f9722aaad54bfe93f36ed88f4bc8
--- /dev/null
+++ b/hyperparams/ppo.yml
@@ -0,0 +1,383 @@
+CartPole-v1: &cartpole-defaults
+ n_timesteps: !!float 1e5
+ env_hyperparams:
+ n_envs: 8
+ algo_hyperparams:
+ n_steps: 32
+ batch_size: 256
+ n_epochs: 20
+ gae_lambda: 0.8
+ gamma: 0.98
+ ent_coef: 0.0
+ learning_rate: 0.001
+ learning_rate_decay: linear
+ clip_range: 0.2
+ clip_range_decay: linear
+ eval_params:
+ step_freq: !!float 2.5e4
+
+CartPole-v0:
+ <<: *cartpole-defaults
+ n_timesteps: !!float 5e4
+
+MountainCar-v0:
+ n_timesteps: !!float 1e6
+ env_hyperparams:
+ normalize: true
+ n_envs: 16
+ algo_hyperparams:
+ n_steps: 16
+ n_epochs: 4
+ gae_lambda: 0.98
+ gamma: 0.99
+ ent_coef: 0.0
+
+MountainCarContinuous-v0:
+ n_timesteps: !!float 1e5
+ env_hyperparams:
+ normalize: true
+ n_envs: 4
+ # policy_hyperparams:
+ # init_layers_orthogonal: false
+ # log_std_init: -3.29
+ # use_sde: true
+ algo_hyperparams:
+ n_steps: 512
+ batch_size: 256
+ n_epochs: 10
+ learning_rate: !!float 7.77e-5
+ ent_coef: 0.01 # 0.00429
+ ent_coef_decay: linear
+ clip_range: 0.1
+ gae_lambda: 0.9
+ max_grad_norm: 5
+ vf_coef: 0.19
+ eval_params:
+ step_freq: 5000
+
+Acrobot-v1:
+ n_timesteps: !!float 1e6
+ env_hyperparams:
+ n_envs: 16
+ normalize: true
+ algo_hyperparams:
+ n_steps: 256
+ n_epochs: 4
+ gae_lambda: 0.94
+ gamma: 0.99
+ ent_coef: 0.0
+
+LunarLander-v2:
+ n_timesteps: !!float 4e6
+ env_hyperparams:
+ n_envs: 16
+ algo_hyperparams:
+ n_steps: 1024
+ batch_size: 64
+ n_epochs: 4
+ gae_lambda: 0.98
+ gamma: 0.999
+ learning_rate: !!float 5e-4
+ learning_rate_decay: linear
+ clip_range: 0.2
+ clip_range_decay: linear
+ ent_coef: 0.01
+ normalize_advantage: false
+
+BipedalWalker-v3:
+ n_timesteps: !!float 10e6
+ env_hyperparams:
+ n_envs: 16
+ normalize: true
+ algo_hyperparams:
+ n_steps: 2048
+ batch_size: 64
+ gae_lambda: 0.95
+ gamma: 0.99
+ n_epochs: 10
+ ent_coef: 0.001
+ learning_rate: !!float 2.5e-4
+ learning_rate_decay: linear
+ clip_range: 0.2
+ clip_range_decay: linear
+
+CarRacing-v0: &carracing-defaults
+ n_timesteps: !!float 4e6
+ env_hyperparams:
+ n_envs: 8
+ frame_stack: 4
+ policy_hyperparams: &carracing-policy-defaults
+ use_sde: true
+ log_std_init: -2
+ init_layers_orthogonal: false
+ activation_fn: relu
+ share_features_extractor: false
+ cnn_feature_dim: 256
+ hidden_sizes: [256]
+ algo_hyperparams:
+ n_steps: 512
+ batch_size: 128
+ n_epochs: 10
+ learning_rate: !!float 1e-4
+ learning_rate_decay: linear
+ gamma: 0.99
+ gae_lambda: 0.95
+ ent_coef: 0.0
+ sde_sample_freq: 4
+ max_grad_norm: 0.5
+ vf_coef: 0.5
+ clip_range: 0.2
+
+impala-CarRacing-v0:
+ <<: *carracing-defaults
+ env_id: CarRacing-v0
+ policy_hyperparams:
+ <<: *carracing-policy-defaults
+ cnn_style: impala
+ init_layers_orthogonal: true
+ cnn_layers_init_orthogonal: false
+ hidden_sizes: []
+
+# BreakoutNoFrameskip-v4
+# PongNoFrameskip-v4
+# SpaceInvadersNoFrameskip-v4
+# QbertNoFrameskip-v4
+_atari: &atari-defaults
+ n_timesteps: !!float 1e7
+ env_hyperparams: &atari-env-defaults
+ n_envs: 8
+ frame_stack: 4
+ no_reward_timeout_steps: 1000
+ no_reward_fire_steps: 500
+ vec_env_class: async
+ policy_hyperparams: &atari-policy-defaults
+ activation_fn: relu
+ algo_hyperparams:
+ n_steps: 128
+ batch_size: 256
+ n_epochs: 4
+ learning_rate: !!float 2.5e-4
+ learning_rate_decay: linear
+ clip_range: 0.1
+ clip_range_decay: linear
+ vf_coef: 0.5
+ ent_coef: 0.01
+ eval_params:
+ deterministic: false
+
+_norm-rewards-atari: &norm-rewards-atari-default
+ <<: *atari-defaults
+ env_hyperparams:
+ <<: *atari-env-defaults
+ clip_atari_rewards: false
+ normalize: true
+ normalize_kwargs:
+ norm_obs: false
+ norm_reward: true
+
+norm-rewards-BreakoutNoFrameskip-v4:
+ <<: *norm-rewards-atari-default
+ env_id: BreakoutNoFrameskip-v4
+
+debug-PongNoFrameskip-v4:
+ <<: *atari-defaults
+ device: cpu
+ env_id: PongNoFrameskip-v4
+ env_hyperparams:
+ <<: *atari-env-defaults
+ vec_env_class: sync
+
+_impala-atari: &impala-atari-defaults
+ <<: *atari-defaults
+ policy_hyperparams:
+ <<: *atari-policy-defaults
+ cnn_style: impala
+ cnn_feature_dim: 256
+ init_layers_orthogonal: true
+ cnn_layers_init_orthogonal: false
+
+impala-PongNoFrameskip-v4:
+ <<: *impala-atari-defaults
+ env_id: PongNoFrameskip-v4
+
+impala-BreakoutNoFrameskip-v4:
+ <<: *impala-atari-defaults
+ env_id: BreakoutNoFrameskip-v4
+
+impala-SpaceInvadersNoFrameskip-v4:
+ <<: *impala-atari-defaults
+ env_id: SpaceInvadersNoFrameskip-v4
+
+impala-QbertNoFrameskip-v4:
+ <<: *impala-atari-defaults
+ env_id: QbertNoFrameskip-v4
+
+HalfCheetahBulletEnv-v0: &pybullet-defaults
+ n_timesteps: !!float 2e6
+ env_hyperparams: &pybullet-env-defaults
+ n_envs: 16
+ normalize: true
+ policy_hyperparams: &pybullet-policy-defaults
+ pi_hidden_sizes: [256, 256]
+ v_hidden_sizes: [256, 256]
+ activation_fn: relu
+ algo_hyperparams: &pybullet-algo-defaults
+ n_steps: 512
+ batch_size: 128
+ n_epochs: 20
+ gamma: 0.99
+ gae_lambda: 0.9
+ ent_coef: 0.0
+ max_grad_norm: 0.5
+ vf_coef: 0.5
+ learning_rate: !!float 3e-5
+ clip_range: 0.4
+
+AntBulletEnv-v0:
+ <<: *pybullet-defaults
+ policy_hyperparams:
+ <<: *pybullet-policy-defaults
+ algo_hyperparams:
+ <<: *pybullet-algo-defaults
+
+Walker2DBulletEnv-v0:
+ <<: *pybullet-defaults
+ algo_hyperparams:
+ <<: *pybullet-algo-defaults
+ clip_range_decay: linear
+
+HopperBulletEnv-v0:
+ <<: *pybullet-defaults
+ algo_hyperparams:
+ <<: *pybullet-algo-defaults
+ clip_range_decay: linear
+
+HumanoidBulletEnv-v0:
+ <<: *pybullet-defaults
+ n_timesteps: !!float 1e7
+ env_hyperparams:
+ <<: *pybullet-env-defaults
+ n_envs: 8
+ policy_hyperparams:
+ <<: *pybullet-policy-defaults
+ # log_std_init: -1
+ algo_hyperparams:
+ <<: *pybullet-algo-defaults
+ n_steps: 2048
+ batch_size: 64
+ n_epochs: 10
+ gae_lambda: 0.95
+ learning_rate: !!float 2.5e-4
+ clip_range: 0.2
+
+_procgen: &procgen-defaults
+ env_hyperparams: &procgen-env-defaults
+ env_type: procgen
+ n_envs: 64
+ # grayscale: false
+ # frame_stack: 4
+ normalize: true # procgen only normalizes reward
+ make_kwargs: &procgen-make-kwargs-defaults
+ num_threads: 8
+ policy_hyperparams: &procgen-policy-defaults
+ activation_fn: relu
+ cnn_style: impala
+ cnn_feature_dim: 256
+ init_layers_orthogonal: true
+ cnn_layers_init_orthogonal: false
+ algo_hyperparams: &procgen-algo-defaults
+ gamma: 0.999
+ gae_lambda: 0.95
+ n_steps: 256
+ batch_size: 2048
+ n_epochs: 3
+ ent_coef: 0.01
+ clip_range: 0.2
+ # clip_range_decay: linear
+ clip_range_vf: 0.2
+ learning_rate: !!float 5e-4
+ # learning_rate_decay: linear
+ vf_coef: 0.5
+ eval_params: &procgen-eval-defaults
+ ignore_first_episode: true
+ # deterministic: false
+ step_freq: !!float 1e5
+
+_procgen-easy: &procgen-easy-defaults
+ <<: *procgen-defaults
+ n_timesteps: !!float 25e6
+ env_hyperparams: &procgen-easy-env-defaults
+ <<: *procgen-env-defaults
+ make_kwargs:
+ <<: *procgen-make-kwargs-defaults
+ distribution_mode: easy
+
+procgen-coinrun-easy: &coinrun-easy-defaults
+ <<: *procgen-easy-defaults
+ env_id: coinrun
+
+debug-procgen-coinrun:
+ <<: *coinrun-easy-defaults
+ device: cpu
+
+procgen-starpilot-easy:
+ <<: *procgen-easy-defaults
+ env_id: starpilot
+
+procgen-bossfight-easy:
+ <<: *procgen-easy-defaults
+ env_id: bossfight
+
+procgen-bigfish-easy:
+ <<: *procgen-easy-defaults
+ env_id: bigfish
+
+_procgen-hard: &procgen-hard-defaults
+ <<: *procgen-defaults
+ n_timesteps: !!float 200e6
+ env_hyperparams: &procgen-hard-env-defaults
+ <<: *procgen-env-defaults
+ n_envs: 256
+ make_kwargs:
+ <<: *procgen-make-kwargs-defaults
+ distribution_mode: hard
+ algo_hyperparams: &procgen-hard-algo-defaults
+ <<: *procgen-algo-defaults
+ batch_size: 8192
+ clip_range_decay: linear
+ learning_rate_decay: linear
+ eval_params:
+ <<: *procgen-eval-defaults
+ step_freq: !!float 5e5
+
+procgen-starpilot-hard: &procgen-starpilot-hard-defaults
+ <<: *procgen-hard-defaults
+ env_id: starpilot
+
+procgen-starpilot-hard-2xIMPALA:
+ <<: *procgen-starpilot-hard-defaults
+ policy_hyperparams:
+ <<: *procgen-policy-defaults
+ impala_channels: [32, 64, 64]
+ algo_hyperparams:
+ <<: *procgen-hard-algo-defaults
+ learning_rate: !!float 3.3e-4
+
+procgen-starpilot-hard-2xIMPALA-fat:
+ <<: *procgen-starpilot-hard-defaults
+ policy_hyperparams:
+ <<: *procgen-policy-defaults
+ impala_channels: [32, 64, 64]
+ cnn_feature_dim: 512
+ algo_hyperparams:
+ <<: *procgen-hard-algo-defaults
+ learning_rate: !!float 2.5e-4
+
+procgen-starpilot-hard-4xIMPALA:
+ <<: *procgen-starpilot-hard-defaults
+ policy_hyperparams:
+ <<: *procgen-policy-defaults
+ impala_channels: [64, 128, 128]
+ algo_hyperparams:
+ <<: *procgen-hard-algo-defaults
+ learning_rate: !!float 2.1e-4
diff --git a/hyperparams/vpg.yml b/hyperparams/vpg.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e472a9226b830c127f044718672d6d0c9e8c83dc
--- /dev/null
+++ b/hyperparams/vpg.yml
@@ -0,0 +1,197 @@
+CartPole-v1: &cartpole-defaults
+ n_timesteps: !!float 4e5
+ algo_hyperparams:
+ n_steps: 4096
+ pi_lr: 0.01
+ gamma: 0.99
+ gae_lambda: 1
+ val_lr: 0.01
+ train_v_iters: 80
+ eval_params:
+ step_freq: !!float 2.5e4
+
+CartPole-v0:
+ <<: *cartpole-defaults
+ n_timesteps: !!float 1e5
+ algo_hyperparams:
+ n_steps: 1024
+ pi_lr: 0.01
+ gamma: 0.99
+ gae_lambda: 1
+ val_lr: 0.01
+ train_v_iters: 80
+
+MountainCar-v0:
+ n_timesteps: !!float 1e6
+ env_hyperparams:
+ normalize: true
+ n_envs: 16
+ algo_hyperparams:
+ n_steps: 200
+ pi_lr: 0.005
+ gamma: 0.99
+ gae_lambda: 0.97
+ val_lr: 0.01
+ train_v_iters: 80
+ max_grad_norm: 0.5
+
+MountainCarContinuous-v0:
+ n_timesteps: !!float 3e5
+ env_hyperparams:
+ normalize: true
+ n_envs: 4
+ # policy_hyperparams:
+ # init_layers_orthogonal: false
+ # log_std_init: -3.29
+ # use_sde: true
+ algo_hyperparams:
+ n_steps: 1000
+ pi_lr: !!float 5e-4
+ gamma: 0.99
+ gae_lambda: 0.9
+ val_lr: !!float 1e-3
+ train_v_iters: 80
+ max_grad_norm: 5
+ eval_params:
+ step_freq: 5000
+
+Acrobot-v1:
+ n_timesteps: !!float 2e5
+ algo_hyperparams:
+ n_steps: 2048
+ pi_lr: 0.005
+ gamma: 0.99
+ gae_lambda: 0.97
+ val_lr: 0.01
+ train_v_iters: 80
+ max_grad_norm: 0.5
+
+LunarLander-v2:
+ n_timesteps: !!float 4e6
+ policy_hyperparams:
+ hidden_sizes: [256, 256]
+ algo_hyperparams:
+ n_steps: 2048
+ pi_lr: 0.0001
+ gamma: 0.999
+ gae_lambda: 0.97
+ val_lr: 0.0001
+ train_v_iters: 80
+ max_grad_norm: 0.5
+ eval_params:
+ deterministic: false
+
+BipedalWalker-v3:
+ n_timesteps: !!float 10e6
+ env_hyperparams:
+ n_envs: 16
+ normalize: true
+ policy_hyperparams:
+ hidden_sizes: [256, 256]
+ algo_hyperparams:
+ n_steps: 1600
+ gae_lambda: 0.95
+ gamma: 0.99
+ pi_lr: !!float 1e-4
+ val_lr: !!float 1e-4
+ train_v_iters: 80
+ max_grad_norm: 0.5
+ eval_params:
+ deterministic: false
+
+CarRacing-v0:
+ n_timesteps: !!float 4e6
+ env_hyperparams:
+ frame_stack: 4
+ n_envs: 4
+ vec_env_class: sync
+ policy_hyperparams:
+ use_sde: true
+ log_std_init: -2
+ init_layers_orthogonal: false
+ activation_fn: relu
+ cnn_feature_dim: 256
+ hidden_sizes: [256]
+ algo_hyperparams:
+ n_steps: 1000
+ pi_lr: !!float 5e-5
+ gamma: 0.99
+ gae_lambda: 0.95
+ val_lr: !!float 1e-4
+ train_v_iters: 40
+ max_grad_norm: 0.5
+ sde_sample_freq: 4
+
+HalfCheetahBulletEnv-v0: &pybullet-defaults
+ n_timesteps: !!float 2e6
+ env_hyperparams: &pybullet-env-defaults
+ normalize: true
+ policy_hyperparams: &pybullet-policy-defaults
+ hidden_sizes: [256, 256]
+ algo_hyperparams: &pybullet-algo-defaults
+ n_steps: 4000
+ pi_lr: !!float 3e-4
+ gamma: 0.99
+ gae_lambda: 0.97
+ val_lr: !!float 1e-3
+ train_v_iters: 80
+ max_grad_norm: 0.5
+
+AntBulletEnv-v0:
+ <<: *pybullet-defaults
+ policy_hyperparams:
+ <<: *pybullet-policy-defaults
+ hidden_sizes: [400, 300]
+ algo_hyperparams:
+ <<: *pybullet-algo-defaults
+ pi_lr: !!float 7e-4
+ val_lr: !!float 7e-3
+
+HopperBulletEnv-v0:
+ <<: *pybullet-defaults
+
+Walker2DBulletEnv-v0:
+ <<: *pybullet-defaults
+
+FrozenLake-v1:
+ n_timesteps: !!float 8e5
+ env_params:
+ make_kwargs:
+ map_name: 8x8
+ is_slippery: true
+ policy_hyperparams:
+ hidden_sizes: [64]
+ algo_hyperparams:
+ n_steps: 2048
+ pi_lr: 0.01
+ gamma: 0.99
+ gae_lambda: 0.98
+ val_lr: 0.01
+ train_v_iters: 80
+ max_grad_norm: 0.5
+ eval_params:
+ step_freq: !!float 5e4
+ n_episodes: 10
+ save_best: true
+
+_atari: &atari-defaults
+ n_timesteps: !!float 25e6
+ env_hyperparams:
+ n_envs: 4
+ frame_stack: 4
+ no_reward_timeout_steps: 1000
+ no_reward_fire_steps: 500
+ vec_env_class: async
+ policy_hyperparams:
+ activation_fn: relu
+ algo_hyperparams:
+ n_steps: 2048
+ pi_lr: !!float 5e-5
+ gamma: 0.99
+ gae_lambda: 0.95
+ val_lr: !!float 1e-4
+ train_v_iters: 80
+ max_grad_norm: 0.5
+ ent_coef: 0.01
+ eval_params:
+ deterministic: false
diff --git a/lambda_labs/benchmark.sh b/lambda_labs/benchmark.sh
new file mode 100644
index 0000000000000000000000000000000000000000..624abe32380f7dfd37cffb1cf2693980a7eeeddd
--- /dev/null
+++ b/lambda_labs/benchmark.sh
@@ -0,0 +1,34 @@
+source benchmarks/train_loop.sh
+
+# export WANDB_PROJECT_NAME="rl-algo-impls"
+
+BENCHMARK_MAX_PROCS="${BENCHMARK_MAX_PROCS:-6}"
+
+ALGOS=(
+ # "vpg"
+ # "dqn"
+ # "ppo"
+ "a2c"
+)
+ENVS=(
+ # Basic
+ "CartPole-v1"
+ "MountainCar-v0"
+ "MountainCarContinuous-v0"
+ "Acrobot-v1"
+ "LunarLander-v2"
+ "BipedalWalker-v3"
+ # PyBullet
+ "HalfCheetahBulletEnv-v0"
+ "AntBulletEnv-v0"
+ "HopperBulletEnv-v0"
+ "Walker2DBulletEnv-v0"
+ # CarRacing
+ "CarRacing-v0"
+ # Atari
+ "PongNoFrameskip-v4"
+ "BreakoutNoFrameskip-v4"
+ "SpaceInvadersNoFrameskip-v4"
+ "QbertNoFrameskip-v4"
+)
+train_loop "${ALGOS[*]}" "${ENVS[*]}" | xargs -I CMD -P $BENCHMARK_MAX_PROCS bash -c CMD
diff --git a/lambda_labs/impala_atari_benchmark.sh b/lambda_labs/impala_atari_benchmark.sh
new file mode 100644
index 0000000000000000000000000000000000000000..a9ae78022ef1ae1995ec14495fdd8af287e30e89
--- /dev/null
+++ b/lambda_labs/impala_atari_benchmark.sh
@@ -0,0 +1,19 @@
+source benchmarks/train_loop.sh
+
+# export WANDB_PROJECT_NAME="rl-algo-impls"
+
+BENCHMARK_MAX_PROCS="${BENCHMARK_MAX_PROCS:-5}"
+
+ALGOS=(
+ # "vpg"
+ # "dqn"
+ "ppo"
+)
+ENVS=(
+ "impala-PongNoFrameskip-v4"
+ "impala-BreakoutNoFrameskip-v4"
+ "impala-SpaceInvadersNoFrameskip-v4"
+ "impala-QbertNoFrameskip-v4"
+ "impala-CarRacing-v0"
+)
+train_loop "${ALGOS[*]}" "${ENVS[*]}" | xargs -I CMD -P $BENCHMARK_MAX_PROCS bash -c CMD
diff --git a/lambda_labs/lambda_requirements.txt b/lambda_labs/lambda_requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..62e1bc45d79e95d758bc92915d99ee612092f56e
--- /dev/null
+++ b/lambda_labs/lambda_requirements.txt
@@ -0,0 +1,16 @@
+scipy >= 1.10.0, < 1.11
+tensorboard >= ^2.11.0, < 2.12
+AutoROM.accept-rom-license >= 0.4.2, < 0.5
+stable-baselines3[extra] >= 1.7.0, < 1.8
+gym[box2d] >= 0.21.0, < 0.22
+pyglet == 1.5.27
+wandb >= 0.13.10, < 0.14
+pyvirtualdisplay == 3.0
+pybullet >= 3.2.5, < 3.3
+tabulate >= 0.9.0, < 0.10
+huggingface-hub >= 0.12.0, < 0.13
+numexpr >= 2.8.4, < 2.9
+gym3 >= 0.3.3, < 0.4
+glfw >= 1.12.0, < 1.13
+procgen >= 0.10.7, < 0.11
+ipython >= 8.10.0, < 8.11
\ No newline at end of file
diff --git a/lambda_labs/procgen_benchmark.sh b/lambda_labs/procgen_benchmark.sh
new file mode 100644
index 0000000000000000000000000000000000000000..9e58f69ed4fcf6bbc58cca335ce3badbd9e3ca58
--- /dev/null
+++ b/lambda_labs/procgen_benchmark.sh
@@ -0,0 +1,18 @@
+source benchmarks/train_loop.sh
+
+# export WANDB_PROJECT_NAME="rl-algo-impls"
+
+BENCHMARK_MAX_PROCS="${BENCHMARK_MAX_PROCS:-3}"
+
+ALGOS=(
+ # "vpg"
+ # "dqn"
+ "ppo"
+)
+ENVS=(
+ "procgen-coinrun-easy"
+ "procgen-starpilot-easy"
+ "procgen-bossfight-easy"
+ "procgen-bigfish-easy"
+)
+train_loop "${ALGOS[*]}" "${ENVS[*]}" | xargs -I CMD -P $BENCHMARK_MAX_PROCS bash -c CMD
diff --git a/lambda_labs/setup.sh b/lambda_labs/setup.sh
new file mode 100644
index 0000000000000000000000000000000000000000..79d7f0a0b9bcc087b3b40065830bc8afef29ec18
--- /dev/null
+++ b/lambda_labs/setup.sh
@@ -0,0 +1,10 @@
+sudo apt update
+sudo apt install -y python-opengl
+sudo apt install -y ffmpeg
+sudo apt install -y xvfb
+sudo apt install -y swig
+
+python3 -m pip install --upgrade pip
+pip install --upgrade torch torchvision torchaudio
+
+pip install --upgrade -r ~/rl-algo-impls/lambda_labs/lambda_requirements.txt
\ No newline at end of file
diff --git a/lambda_labs/starpilot_hard_benchmark.sh b/lambda_labs/starpilot_hard_benchmark.sh
new file mode 100644
index 0000000000000000000000000000000000000000..da6897b33e0f85917288ba743ba6eb06d20e2176
--- /dev/null
+++ b/lambda_labs/starpilot_hard_benchmark.sh
@@ -0,0 +1,16 @@
+source benchmarks/train_loop.sh
+
+# export WANDB_PROJECT_NAME="rl-algo-impls"
+
+BENCHMARK_MAX_PROCS="${BENCHMARK_MAX_PROCS:-1}"
+
+ALGOS=(
+ "ppo"
+)
+ENVS=(
+ "procgen-starpilot-hard"
+ "procgen-starpilot-hard-2xIMPALA"
+ "procgen-starpilot-hard-2xIMPALA-fat"
+ "procgen-starpilot-hard-4xIMPALA"
+)
+train_loop "${ALGOS[*]}" "${ENVS[*]}" | xargs -I CMD -P $BENCHMARK_MAX_PROCS bash -c CMD
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000000000000000000000000000000000000..da115fe5e6e3297f30cd68c4a85ca151fe1414aa
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,4466 @@
+# 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 = "main"
+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 = "ale-py"
+version = "0.7.4"
+description = "The Arcade Learning Environment (ALE) - a platform for AI research."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "ale_py-0.7.4-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:418eea1539c2669c799274fedead4d44d05dfc3dcd6c536378d5984c42bc340b"},
+ {file = "ale_py-0.7.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:38e4823be04761a2ebc0167ed710a318cc9f0fec3815576c45030fe8e67f9c98"},
+ {file = "ale_py-0.7.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9af49488ec1b4facb299975a665e9e706921dd2d756daad813e2897debc5fc3c"},
+ {file = "ale_py-0.7.4-cp310-cp310-win_amd64.whl", hash = "sha256:f600c55d6a7c6c30f5592b30afc34366101fc7561842bdd5740d5bca390201eb"},
+ {file = "ale_py-0.7.4-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:da3e1400e02fb46659dfb3af92e8a4cf4c5b2d4f9d19a008ce9d5fa8eebb4ab6"},
+ {file = "ale_py-0.7.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c073005b68901f0003ffe871d56021245eda9e88f27cc91745627c099932499f"},
+ {file = "ale_py-0.7.4-cp37-cp37m-win_amd64.whl", hash = "sha256:913394ad1dbe22a8d489378d702f296234721ca0a0e76e5354764e8bf40bc623"},
+ {file = "ale_py-0.7.4-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:4841f395e3166d4a7b1e9207cafab08de4b9e9b4178afd97a36f53844ade98a2"},
+ {file = "ale_py-0.7.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5b2899b4cf659bc14a20047455e681e991cb96ceed937d22a5dac1a97a16bf3e"},
+ {file = "ale_py-0.7.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9aff7a8ce37d00a87ef4114666db0b45d499744d08f5ff1683dbbbcac4783569"},
+ {file = "ale_py-0.7.4-cp38-cp38-win_amd64.whl", hash = "sha256:a23f4c858a2c5cbfa3c0cb2c9ab167359c368104b67e19b332710c19b43c6091"},
+ {file = "ale_py-0.7.4-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:0b9ab62f12a325e92ba2af99c5b231ad3b219a46913b14068c857d37837025fb"},
+ {file = "ale_py-0.7.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:269dcf94024ba7a8276d4dcf04c526df695cb383aa2372e9903a08ec6f679262"},
+ {file = "ale_py-0.7.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f65371c180779b115d8600d99780e9e83b229812e94c6b49be1686ce4d82573"},
+ {file = "ale_py-0.7.4-cp39-cp39-win_amd64.whl", hash = "sha256:b53e7d0c8f8e8610ebaec88887da2427ce16811f9697ccbb39588ec784bea145"},
+]
+
+[package.dependencies]
+importlib-resources = "*"
+numpy = "*"
+
+[package.extras]
+test = ["gym", "pytest"]
+
+[[package]]
+name = "anyio"
+version = "3.6.2"
+description = "High level compatibility layer for multiple asynchronous event loop implementations"
+category = "main"
+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 = "main"
+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 = "main"
+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 = "main"
+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 = "main"
+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 = "main"
+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 = "asttokens"
+version = "2.2.1"
+description = "Annotate AST trees with source code positions"
+category = "main"
+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 = "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 = "autorom"
+version = "0.4.2"
+description = "Automated installation of Atari ROMs for Gym/ALE-Py"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "AutoROM-0.4.2-py3-none-any.whl", hash = "sha256:719c9d363ef08391fdb7003d70df235b68f36de628d289a946c4a59a3adefa13"},
+ {file = "AutoROM-0.4.2.tar.gz", hash = "sha256:b426a39bc0ee3781c7791f28963a9b2e4385b6421eeaf2f368edc00c761d428a"},
+]
+
+[package.dependencies]
+"AutoROM.accept-rom-license" = {version = "*", optional = true, markers = "extra == \"accept-rom-license\""}
+click = "*"
+requests = "*"
+tqdm = "*"
+
+[package.extras]
+accept-rom-license = ["AutoROM.accept-rom-license"]
+
+[[package]]
+name = "autorom-accept-rom-license"
+version = "0.4.2"
+description = "Automated installation of Atari ROMs for Gym/ALE-Py"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "AutoROM.accept-rom-license-0.4.2.tar.gz", hash = "sha256:f08654c9d2a6837c5ec951c0364bda352510920ea0523005a2d4460c7d2be7f2"},
+]
+
+[package.dependencies]
+click = "*"
+requests = "*"
+tqdm = "*"
+
+[package.extras]
+tests = ["ale_py", "multi_agent_ale_py"]
+
+[[package]]
+name = "backcall"
+version = "0.2.0"
+description = "Specifications for callback functions passed in to an API"
+category = "main"
+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 = "beautifulsoup4"
+version = "4.11.1"
+description = "Screen-scraping library"
+category = "main"
+optional = false
+python-versions = ">=3.6.0"
+files = [
+ {file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"},
+ {file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"},
+]
+
+[package.dependencies]
+soupsieve = ">1.2"
+
+[package.extras]
+html5lib = ["html5lib"]
+lxml = ["lxml"]
+
+[[package]]
+name = "bleach"
+version = "5.0.1"
+description = "An easy safelist-based HTML-sanitizing tool."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "bleach-5.0.1-py3-none-any.whl", hash = "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a"},
+ {file = "bleach-5.0.1.tar.gz", hash = "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c"},
+]
+
+[package.dependencies]
+six = ">=1.9.0"
+webencodings = "*"
+
+[package.extras]
+css = ["tinycss2 (>=1.1.0,<1.2)"]
+dev = ["Sphinx (==4.3.2)", "black (==22.3.0)", "build (==0.8.0)", "flake8 (==4.0.1)", "hashin (==0.17.0)", "mypy (==0.961)", "pip-tools (==6.6.2)", "pytest (==7.1.2)", "tox (==3.25.0)", "twine (==4.0.1)", "wheel (==0.37.1)"]
+
+[[package]]
+name = "box2d-py"
+version = "2.3.5"
+description = "Python Box2D"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "box2d-py-2.3.5.tar.gz", hash = "sha256:b37dc38844bcd7def48a97111d2b082e4f81cca3cece7460feb3eacda0da2207"},
+ {file = "box2d_py-2.3.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:287aa54005c0644b47bf7ad72966e4068d66e56bcf8458f5b4a653ffe42a2618"},
+ {file = "box2d_py-2.3.5-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:483b3f9acd5d156b72bf2013f93cf7f8ca0ee1562e43d2353ab4c0cbec4ee49a"},
+ {file = "box2d_py-2.3.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:a294c2d7cc73cc05dd491287079e15419eb98caa3158df94f40faf85eeb4b6e9"},
+ {file = "box2d_py-2.3.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0d46068eb8d29e366ed698ab2a4833d4d2d34ed035ebd6a685888007dda05f64"},
+]
+
+[[package]]
+name = "cachecontrol"
+version = "0.12.11"
+description = "httplib2 caching for requests"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "CacheControl-0.12.11-py2.py3-none-any.whl", hash = "sha256:2c75d6a8938cb1933c75c50184549ad42728a27e9f6b92fd677c3151aa72555b"},
+ {file = "CacheControl-0.12.11.tar.gz", hash = "sha256:a5b9fcc986b184db101aa280b42ecdcdfc524892596f606858e0b7a8b4d9e144"},
+]
+
+[package.dependencies]
+lockfile = {version = ">=0.9", optional = true, markers = "extra == \"filecache\""}
+msgpack = ">=0.5.2"
+requests = "*"
+
+[package.extras]
+filecache = ["lockfile (>=0.9)"]
+redis = ["redis (>=2.10.5)"]
+
+[[package]]
+name = "cachetools"
+version = "5.2.1"
+description = "Extensible memoizing collections and decorators"
+category = "main"
+optional = false
+python-versions = "~=3.7"
+files = [
+ {file = "cachetools-5.2.1-py3-none-any.whl", hash = "sha256:8462eebf3a6c15d25430a8c27c56ac61340b2ecf60c9ce57afc2b97e450e47da"},
+ {file = "cachetools-5.2.1.tar.gz", hash = "sha256:5991bc0e08a1319bb618d3195ca5b6bc76646a49c21d55962977197b301cc1fe"},
+]
+
+[[package]]
+name = "cachy"
+version = "0.3.0"
+description = "Cachy provides a simple yet effective caching library."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+ {file = "cachy-0.3.0-py2.py3-none-any.whl", hash = "sha256:338ca09c8860e76b275aff52374330efedc4d5a5e45dc1c5b539c1ead0786fe7"},
+ {file = "cachy-0.3.0.tar.gz", hash = "sha256:186581f4ceb42a0bbe040c407da73c14092379b1e4c0e327fdb72ae4a9b269b1"},
+]
+
+[package.extras]
+memcached = ["python-memcached (>=1.59,<2.0)"]
+msgpack = ["msgpack-python (>=0.5,<0.6)"]
+redis = ["redis (>=3.3.6,<4.0.0)"]
+
+[[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 = "main"
+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.0.1"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "charset-normalizer-3.0.1.tar.gz", hash = "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f"},
+ {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6"},
+ {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db"},
+ {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539"},
+ {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d"},
+ {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"},
+ {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e"},
+ {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d"},
+ {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783"},
+ {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555"},
+ {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639"},
+ {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76"},
+ {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6"},
+ {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e"},
+ {file = "charset_normalizer-3.0.1-cp310-cp310-win32.whl", hash = "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b"},
+ {file = "charset_normalizer-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59"},
+ {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d"},
+ {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3"},
+ {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c"},
+ {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b"},
+ {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1"},
+ {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317"},
+ {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603"},
+ {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18"},
+ {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a"},
+ {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7"},
+ {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc"},
+ {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b"},
+ {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6"},
+ {file = "charset_normalizer-3.0.1-cp311-cp311-win32.whl", hash = "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3"},
+ {file = "charset_normalizer-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c"},
+ {file = "charset_normalizer-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a"},
+ {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea"},
+ {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72"},
+ {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478"},
+ {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d"},
+ {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837"},
+ {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6"},
+ {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc"},
+ {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a"},
+ {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c"},
+ {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a"},
+ {file = "charset_normalizer-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41"},
+ {file = "charset_normalizer-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602"},
+ {file = "charset_normalizer-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14"},
+ {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d"},
+ {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc"},
+ {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3"},
+ {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866"},
+ {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b"},
+ {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087"},
+ {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8"},
+ {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b"},
+ {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918"},
+ {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d"},
+ {file = "charset_normalizer-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154"},
+ {file = "charset_normalizer-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5"},
+ {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42"},
+ {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c"},
+ {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e"},
+ {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58"},
+ {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174"},
+ {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753"},
+ {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b"},
+ {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1"},
+ {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a"},
+ {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed"},
+ {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479"},
+ {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291"},
+ {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820"},
+ {file = "charset_normalizer-3.0.1-cp38-cp38-win32.whl", hash = "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e"},
+ {file = "charset_normalizer-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af"},
+ {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f"},
+ {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678"},
+ {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be"},
+ {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b"},
+ {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca"},
+ {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e"},
+ {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3"},
+ {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541"},
+ {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d"},
+ {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579"},
+ {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4"},
+ {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c"},
+ {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786"},
+ {file = "charset_normalizer-3.0.1-cp39-cp39-win32.whl", hash = "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8"},
+ {file = "charset_normalizer-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59"},
+ {file = "charset_normalizer-3.0.1-py3-none-any.whl", hash = "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24"},
+]
+
+[[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 = "click-default-group"
+version = "1.2.2"
+description = "Extends click.Group to invoke a command without explicit subcommand name"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "click-default-group-1.2.2.tar.gz", hash = "sha256:d9560e8e8dfa44b3562fbc9425042a0fd6d21956fcc2db0077f63f34253ab904"},
+]
+
+[package.dependencies]
+click = "*"
+
+[[package]]
+name = "clikit"
+version = "0.6.2"
+description = "CliKit is a group of utilities to build beautiful and testable command line interfaces."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+ {file = "clikit-0.6.2-py2.py3-none-any.whl", hash = "sha256:71268e074e68082306e23d7369a7b99f824a0ef926e55ba2665e911f7208489e"},
+ {file = "clikit-0.6.2.tar.gz", hash = "sha256:442ee5db9a14120635c5990bcdbfe7c03ada5898291f0c802f77be71569ded59"},
+]
+
+[package.dependencies]
+crashtest = {version = ">=0.3.0,<0.4.0", markers = "python_version >= \"3.6\" and python_version < \"4.0\""}
+pastel = ">=0.2.0,<0.3.0"
+pylev = ">=1.3,<2.0"
+
+[[package]]
+name = "cloudpickle"
+version = "2.2.0"
+description = "Extended pickling support for Python objects"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "cloudpickle-2.2.0-py3-none-any.whl", hash = "sha256:7428798d5926d8fcbfd092d18d01a2a03daf8237d8fcdc8095d256b8490796f0"},
+ {file = "cloudpickle-2.2.0.tar.gz", hash = "sha256:3f4219469c55453cfe4737e564b67c2a149109dabf7f242478948b895f61106f"},
+]
+
+[[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.2"
+description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "comm-0.1.2-py3-none-any.whl", hash = "sha256:9f3abf3515112fa7c55a42a6a5ab358735c9dccc8b5910a9d8e3ef5998130666"},
+ {file = "comm-0.1.2.tar.gz", hash = "sha256:3e2f5826578e683999b93716285b3b1f344f157bf75fa9ce0a797564e742f062"},
+]
+
+[package.dependencies]
+traitlets = ">=5.3"
+
+[package.extras]
+test = ["pytest"]
+
+[[package]]
+name = "commonmark"
+version = "0.9.1"
+description = "Python parser for the CommonMark Markdown spec"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
+ {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
+]
+
+[package.extras]
+test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"]
+
+[[package]]
+name = "conda-lock"
+version = "1.3.0"
+description = "Lockfiles for conda"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "conda_lock-1.3.0-py3-none-any.whl", hash = "sha256:a4d5676bb6a422f17d4f4485de2c84ba9bf3d95592655382c95e6e76203d83d8"},
+ {file = "conda_lock-1.3.0.tar.gz", hash = "sha256:8cf4b3a4e6037dadd74cc2b5325e2e76e0a42f3da31b5fd657a8030bce74ee06"},
+]
+
+[package.dependencies]
+cachecontrol = {version = ">=0.12.9", extras = ["filecache"]}
+cachy = ">=0.3.0"
+click = ">=8.0"
+click-default-group = "*"
+clikit = ">=0.6.2"
+crashtest = ">=0.3.0"
+ensureconda = ">=1.3"
+gitpython = "*"
+html5lib = ">=1.0"
+jinja2 = "*"
+keyring = ">=21.2.0"
+packaging = ">=20.4"
+pkginfo = ">=1.4"
+pydantic = ">=1.8.1"
+pyyaml = ">=5.1"
+requests = ">=2.18"
+ruamel-yaml = "*"
+tomli = {version = "*", markers = "python_version < \"3.11\""}
+tomlkit = ">=0.7.0"
+toolz = ">=0.12.0,<1.0.0"
+typing-extensions = "*"
+virtualenv = ">=20.0.26"
+
+[package.extras]
+pip-support = ["poetry (>=1.1.0,<1.2.0)"]
+
+[[package]]
+name = "contourpy"
+version = "1.0.6"
+description = "Python library for calculating contours of 2D quadrilateral grids"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "contourpy-1.0.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:613c665529899b5d9fade7e5d1760111a0b011231277a0d36c49f0d3d6914bd6"},
+ {file = "contourpy-1.0.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:78ced51807ccb2f45d4ea73aca339756d75d021069604c2fccd05390dc3c28eb"},
+ {file = "contourpy-1.0.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3b1bd7577c530eaf9d2bc52d1a93fef50ac516a8b1062c3d1b9bcec9ebe329b"},
+ {file = "contourpy-1.0.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8834c14b8c3dd849005e06703469db9bf96ba2d66a3f88ecc539c9a8982e0ee"},
+ {file = "contourpy-1.0.6-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4052a8a4926d4468416fc7d4b2a7b2a3e35f25b39f4061a7e2a3a2748c4fc48"},
+ {file = "contourpy-1.0.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c0e1308307a75e07d1f1b5f0f56b5af84538a5e9027109a7bcf6cb47c434e72"},
+ {file = "contourpy-1.0.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fc4e7973ed0e1fe689435842a6e6b330eb7ccc696080dda9a97b1a1b78e41db"},
+ {file = "contourpy-1.0.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:08e8d09d96219ace6cb596506fb9b64ea5f270b2fb9121158b976d88871fcfd1"},
+ {file = "contourpy-1.0.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f33da6b5d19ad1bb5e7ad38bb8ba5c426d2178928bc2b2c44e8823ea0ecb6ff3"},
+ {file = "contourpy-1.0.6-cp310-cp310-win32.whl", hash = "sha256:12a7dc8439544ed05c6553bf026d5e8fa7fad48d63958a95d61698df0e00092b"},
+ {file = "contourpy-1.0.6-cp310-cp310-win_amd64.whl", hash = "sha256:eadad75bf91897f922e0fb3dca1b322a58b1726a953f98c2e5f0606bd8408621"},
+ {file = "contourpy-1.0.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:913bac9d064cff033cf3719e855d4f1db9f1c179e0ecf3ba9fdef21c21c6a16a"},
+ {file = "contourpy-1.0.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46deb310a276cc5c1fd27958e358cce68b1e8a515fa5a574c670a504c3a3fe30"},
+ {file = "contourpy-1.0.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b64f747e92af7da3b85631a55d68c45a2d728b4036b03cdaba4bd94bcc85bd6f"},
+ {file = "contourpy-1.0.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50627bf76abb6ba291ad08db583161939c2c5fab38c38181b7833423ab9c7de3"},
+ {file = "contourpy-1.0.6-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:358f6364e4873f4d73360b35da30066f40387dd3c427a3e5432c6b28dd24a8fa"},
+ {file = "contourpy-1.0.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c78bfbc1a7bff053baf7e508449d2765964d67735c909b583204e3240a2aca45"},
+ {file = "contourpy-1.0.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e43255a83835a129ef98f75d13d643844d8c646b258bebd11e4a0975203e018f"},
+ {file = "contourpy-1.0.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:375d81366afd547b8558c4720337218345148bc2fcffa3a9870cab82b29667f2"},
+ {file = "contourpy-1.0.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b98c820608e2dca6442e786817f646d11057c09a23b68d2b3737e6dcb6e4a49b"},
+ {file = "contourpy-1.0.6-cp311-cp311-win32.whl", hash = "sha256:0e4854cc02006ad6684ce092bdadab6f0912d131f91c2450ce6dbdea78ee3c0b"},
+ {file = "contourpy-1.0.6-cp311-cp311-win_amd64.whl", hash = "sha256:d2eff2af97ea0b61381828b1ad6cd249bbd41d280e53aea5cccd7b2b31b8225c"},
+ {file = "contourpy-1.0.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5b117d29433fc8393b18a696d794961464e37afb34a6eeb8b2c37b5f4128a83e"},
+ {file = "contourpy-1.0.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:341330ed19074f956cb20877ad8d2ae50e458884bfa6a6df3ae28487cc76c768"},
+ {file = "contourpy-1.0.6-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:371f6570a81dfdddbb837ba432293a63b4babb942a9eb7aaa699997adfb53278"},
+ {file = "contourpy-1.0.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9447c45df407d3ecb717d837af3b70cfef432138530712263730783b3d016512"},
+ {file = "contourpy-1.0.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:730c27978a0003b47b359935478b7d63fd8386dbb2dcd36c1e8de88cbfc1e9de"},
+ {file = "contourpy-1.0.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:da1ef35fd79be2926ba80fbb36327463e3656c02526e9b5b4c2b366588b74d9a"},
+ {file = "contourpy-1.0.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:cd2bc0c8f2e8de7dd89a7f1c10b8844e291bca17d359373203ef2e6100819edd"},
+ {file = "contourpy-1.0.6-cp37-cp37m-win32.whl", hash = "sha256:3a1917d3941dd58732c449c810fa7ce46cc305ce9325a11261d740118b85e6f3"},
+ {file = "contourpy-1.0.6-cp37-cp37m-win_amd64.whl", hash = "sha256:06ca79e1efbbe2df795822df2fa173d1a2b38b6e0f047a0ec7903fbca1d1847e"},
+ {file = "contourpy-1.0.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e626cefff8491bce356221c22af5a3ea528b0b41fbabc719c00ae233819ea0bf"},
+ {file = "contourpy-1.0.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dbe6fe7a1166b1ddd7b6d887ea6fa8389d3f28b5ed3f73a8f40ece1fc5a3d340"},
+ {file = "contourpy-1.0.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e13b31d1b4b68db60b3b29f8e337908f328c7f05b9add4b1b5c74e0691180109"},
+ {file = "contourpy-1.0.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a79d239fc22c3b8d9d3de492aa0c245533f4f4c7608e5749af866949c0f1b1b9"},
+ {file = "contourpy-1.0.6-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e8e686a6db92a46111a1ee0ee6f7fbfae4048f0019de207149f43ac1812cf95"},
+ {file = "contourpy-1.0.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2bd02f1a7adff3a1f33e431eb96ab6d7987b039d2946a9b39fe6fb16a1036"},
+ {file = "contourpy-1.0.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:03d1b9c6b44a9e30d554654c72be89af94fab7510b4b9f62356c64c81cec8b7d"},
+ {file = "contourpy-1.0.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b48d94386f1994db7c70c76b5808c12e23ed7a4ee13693c2fc5ab109d60243c0"},
+ {file = "contourpy-1.0.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:208bc904889c910d95aafcf7be9e677726df9ef71e216780170dbb7e37d118fa"},
+ {file = "contourpy-1.0.6-cp38-cp38-win32.whl", hash = "sha256:444fb776f58f4906d8d354eb6f6ce59d0a60f7b6a720da6c1ccb839db7c80eb9"},
+ {file = "contourpy-1.0.6-cp38-cp38-win_amd64.whl", hash = "sha256:9bc407a6af672da20da74823443707e38ece8b93a04009dca25856c2d9adadb1"},
+ {file = "contourpy-1.0.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:aa4674cf3fa2bd9c322982644967f01eed0c91bb890f624e0e0daf7a5c3383e9"},
+ {file = "contourpy-1.0.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f56515e7c6fae4529b731f6c117752247bef9cdad2b12fc5ddf8ca6a50965a5"},
+ {file = "contourpy-1.0.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:344cb3badf6fc7316ad51835f56ac387bdf86c8e1b670904f18f437d70da4183"},
+ {file = "contourpy-1.0.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b1e66346acfb17694d46175a0cea7d9036f12ed0c31dfe86f0f405eedde2bdd"},
+ {file = "contourpy-1.0.6-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8468b40528fa1e15181cccec4198623b55dcd58306f8815a793803f51f6c474a"},
+ {file = "contourpy-1.0.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dedf4c64185a216c35eb488e6f433297c660321275734401760dafaeb0ad5c2"},
+ {file = "contourpy-1.0.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:494efed2c761f0f37262815f9e3c4bb9917c5c69806abdee1d1cb6611a7174a0"},
+ {file = "contourpy-1.0.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:75a2e638042118118ab39d337da4c7908c1af74a8464cad59f19fbc5bbafec9b"},
+ {file = "contourpy-1.0.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a628bba09ba72e472bf7b31018b6281fd4cc903f0888049a3724afba13b6e0b8"},
+ {file = "contourpy-1.0.6-cp39-cp39-win32.whl", hash = "sha256:e1739496c2f0108013629aa095cc32a8c6363444361960c07493818d0dea2da4"},
+ {file = "contourpy-1.0.6-cp39-cp39-win_amd64.whl", hash = "sha256:a457ee72d9032e86730f62c5eeddf402e732fdf5ca8b13b41772aa8ae13a4563"},
+ {file = "contourpy-1.0.6-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d912f0154a20a80ea449daada904a7eb6941c83281a9fab95de50529bfc3a1da"},
+ {file = "contourpy-1.0.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4081918147fc4c29fad328d5066cfc751da100a1098398742f9f364be63803fc"},
+ {file = "contourpy-1.0.6-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0537cc1195245bbe24f2913d1f9211b8f04eb203de9044630abd3664c6cc339c"},
+ {file = "contourpy-1.0.6-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcd556c8fc37a342dd636d7eef150b1399f823a4462f8c968e11e1ebeabee769"},
+ {file = "contourpy-1.0.6-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f6ca38dd8d988eca8f07305125dec6f54ac1c518f1aaddcc14d08c01aebb6efc"},
+ {file = "contourpy-1.0.6-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c1baa49ab9fedbf19d40d93163b7d3e735d9cd8d5efe4cce9907902a6dad391f"},
+ {file = "contourpy-1.0.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:211dfe2bd43bf5791d23afbe23a7952e8ac8b67591d24be3638cabb648b3a6eb"},
+ {file = "contourpy-1.0.6-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c38c6536c2d71ca2f7e418acaf5bca30a3af7f2a2fa106083c7d738337848dbe"},
+ {file = "contourpy-1.0.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b1ee48a130da4dd0eb8055bbab34abf3f6262957832fd575e0cab4979a15a41"},
+ {file = "contourpy-1.0.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5641927cc5ae66155d0c80195dc35726eae060e7defc18b7ab27600f39dd1fe7"},
+ {file = "contourpy-1.0.6-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ee394502026d68652c2824348a40bf50f31351a668977b51437131a90d777ea"},
+ {file = "contourpy-1.0.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b97454ed5b1368b66ed414c754cba15b9750ce69938fc6153679787402e4cdf"},
+ {file = "contourpy-1.0.6-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0236875c5a0784215b49d00ebbe80c5b6b5d5244b3655a36dda88105334dea17"},
+ {file = "contourpy-1.0.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84c593aeff7a0171f639da92cb86d24954bbb61f8a1b530f74eb750a14685832"},
+ {file = "contourpy-1.0.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9b0e7fe7f949fb719b206548e5cde2518ffb29936afa4303d8a1c4db43dcb675"},
+ {file = "contourpy-1.0.6.tar.gz", hash = "sha256:6e459ebb8bb5ee4c22c19cc000174f8059981971a33ce11e17dddf6aca97a142"},
+]
+
+[package.dependencies]
+numpy = ">=1.16"
+
+[package.extras]
+bokeh = ["bokeh", "selenium"]
+docs = ["docutils (<0.18)", "sphinx (<=5.2.0)", "sphinx-rtd-theme"]
+test = ["Pillow", "flake8", "isort", "matplotlib", "pytest"]
+test-minimal = ["pytest"]
+test-no-codebase = ["Pillow", "matplotlib", "pytest"]
+
+[[package]]
+name = "crashtest"
+version = "0.3.1"
+description = "Manage Python errors with ease"
+category = "main"
+optional = false
+python-versions = ">=3.6,<4.0"
+files = [
+ {file = "crashtest-0.3.1-py3-none-any.whl", hash = "sha256:300f4b0825f57688b47b6d70c6a31de33512eb2fa1ac614f780939aa0cf91680"},
+ {file = "crashtest-0.3.1.tar.gz", hash = "sha256:42ca7b6ce88b6c7433e2ce47ea884e91ec93104a4b754998be498a8e6c3d37dd"},
+]
+
+[[package]]
+name = "cryptography"
+version = "39.0.1"
+description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "cryptography-39.0.1-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:6687ef6d0a6497e2b58e7c5b852b53f62142cfa7cd1555795758934da363a965"},
+ {file = "cryptography-39.0.1-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:706843b48f9a3f9b9911979761c91541e3d90db1ca905fd63fee540a217698bc"},
+ {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:5d2d8b87a490bfcd407ed9d49093793d0f75198a35e6eb1a923ce1ee86c62b41"},
+ {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e17b26de248c33f3acffb922748151d71827d6021d98c70e6c1a25ddd78505"},
+ {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e124352fd3db36a9d4a21c1aa27fd5d051e621845cb87fb851c08f4f75ce8be6"},
+ {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:5aa67414fcdfa22cf052e640cb5ddc461924a045cacf325cd164e65312d99502"},
+ {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:35f7c7d015d474f4011e859e93e789c87d21f6f4880ebdc29896a60403328f1f"},
+ {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f24077a3b5298a5a06a8e0536e3ea9ec60e4c7ac486755e5fb6e6ea9b3500106"},
+ {file = "cryptography-39.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f0c64d1bd842ca2633e74a1a28033d139368ad959872533b1bab8c80e8240a0c"},
+ {file = "cryptography-39.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0f8da300b5c8af9f98111ffd512910bc792b4c77392a9523624680f7956a99d4"},
+ {file = "cryptography-39.0.1-cp36-abi3-win32.whl", hash = "sha256:fe913f20024eb2cb2f323e42a64bdf2911bb9738a15dba7d3cce48151034e3a8"},
+ {file = "cryptography-39.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ced4e447ae29ca194449a3f1ce132ded8fcab06971ef5f618605aacaa612beac"},
+ {file = "cryptography-39.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:807ce09d4434881ca3a7594733669bd834f5b2c6d5c7e36f8c00f691887042ad"},
+ {file = "cryptography-39.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:96f1157a7c08b5b189b16b47bc9db2332269d6680a196341bf30046330d15388"},
+ {file = "cryptography-39.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e422abdec8b5fa8462aa016786680720d78bdce7a30c652b7fadf83a4ba35336"},
+ {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:b0afd054cd42f3d213bf82c629efb1ee5f22eba35bf0eec88ea9ea7304f511a2"},
+ {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:6f8ba7f0328b79f08bdacc3e4e66fb4d7aab0c3584e0bd41328dce5262e26b2e"},
+ {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ef8b72fa70b348724ff1218267e7f7375b8de4e8194d1636ee60510aae104cd0"},
+ {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:aec5a6c9864be7df2240c382740fcf3b96928c46604eaa7f3091f58b878c0bb6"},
+ {file = "cryptography-39.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdd188c8a6ef8769f148f88f859884507b954cc64db6b52f66ef199bb9ad660a"},
+ {file = "cryptography-39.0.1.tar.gz", hash = "sha256:d1f6198ee6d9148405e49887803907fe8962a23e6c6f83ea7d98f1c0de375695"},
+]
+
+[package.dependencies]
+cffi = ">=1.12"
+
+[package.extras]
+docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
+docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"]
+pep8test = ["black", "check-manifest", "mypy", "ruff", "types-pytz", "types-requests"]
+sdist = ["setuptools-rust (>=0.11.4)"]
+ssh = ["bcrypt (>=3.1.5)"]
+test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist", "pytz"]
+test-randomorder = ["pytest-randomly"]
+tox = ["tox"]
+
+[[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 = "debugpy"
+version = "1.6.5"
+description = "An implementation of the Debug Adapter Protocol for Python"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "debugpy-1.6.5-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:696165f021a6a17da08163eaae84f3faf5d8be68fb78cd78488dd347e625279c"},
+ {file = "debugpy-1.6.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17039e392d6f38388a68bd02c5f823b32a92142a851e96ba3ec52aeb1ce9d900"},
+ {file = "debugpy-1.6.5-cp310-cp310-win32.whl", hash = "sha256:62a06eb78378292ba6c427d861246574dc8b84471904973797b29dd33c7c2495"},
+ {file = "debugpy-1.6.5-cp310-cp310-win_amd64.whl", hash = "sha256:9984fc00ab372c97f63786c400107f54224663ea293daab7b365a5b821d26309"},
+ {file = "debugpy-1.6.5-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:048368f121c08b00bbded161e8583817af5055982d2722450a69efe2051621c2"},
+ {file = "debugpy-1.6.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74e4eca42055759032e3f1909d1374ba1d729143e0c2729bb8cb5e8b5807c458"},
+ {file = "debugpy-1.6.5-cp37-cp37m-win32.whl", hash = "sha256:0f9afcc8cad6424695f3356dc9a7406d5b18e37ee2e73f34792881a44b02cc50"},
+ {file = "debugpy-1.6.5-cp37-cp37m-win_amd64.whl", hash = "sha256:b5a74ecebe5253344501d9b23f74459c46428b30437fa9254cfb8cb129943242"},
+ {file = "debugpy-1.6.5-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:9e809ef787802c808995e5b6ade714a25fa187f892b41a412d418a15a9c4a432"},
+ {file = "debugpy-1.6.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:947c686e8adb46726f3d5f19854f6aebf66c2edb91225643c7f44b40b064a235"},
+ {file = "debugpy-1.6.5-cp38-cp38-win32.whl", hash = "sha256:377391341c4b86f403d93e467da8e2d05c22b683f08f9af3e16d980165b06b90"},
+ {file = "debugpy-1.6.5-cp38-cp38-win_amd64.whl", hash = "sha256:286ae0c2def18ee0dc8a61fa76d51039ca8c11485b6ed3ef83e3efe8a23926ae"},
+ {file = "debugpy-1.6.5-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:500dd4a9ff818f5c52dddb4a608c7de5371c2d7d905c505eb745556c579a9f11"},
+ {file = "debugpy-1.6.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f3fab217fe7e2acb2d90732af1a871947def4e2b6654945ba1ebd94bd0bea26"},
+ {file = "debugpy-1.6.5-cp39-cp39-win32.whl", hash = "sha256:15bc5febe0edc79726517b1f8d57d7ac7c784567b5ba804aab8b1c9d07a57018"},
+ {file = "debugpy-1.6.5-cp39-cp39-win_amd64.whl", hash = "sha256:7e84d9e4420122384cb2cc762a00b4e17cbf998022890f89b195ce178f78ff47"},
+ {file = "debugpy-1.6.5-py2.py3-none-any.whl", hash = "sha256:8116e40a1cd0593bd2aba01d4d560ee08f018da8e8fbd4cbd24ff09b5f0e41ef"},
+ {file = "debugpy-1.6.5.zip", hash = "sha256:5e55e6c79e215239dd0794ee0bf655412b934735a58e9d705e5c544f596f1603"},
+]
+
+[[package]]
+name = "decorator"
+version = "5.1.1"
+description = "Decorators for Humans"
+category = "main"
+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 = "main"
+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 = "distlib"
+version = "0.3.6"
+description = "Distribution utilities"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
+ {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
+]
+
+[[package]]
+name = "docker-pycreds"
+version = "0.4.0"
+description = "Python bindings for the docker credentials store API"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "docker-pycreds-0.4.0.tar.gz", hash = "sha256:6ce3270bcaf404cc4c3e27e4b6c70d3521deae82fb508767870fdbf772d584d4"},
+ {file = "docker_pycreds-0.4.0-py2.py3-none-any.whl", hash = "sha256:7266112468627868005106ec19cd0d722702d2b7d5912a28e19b826c3d37af49"},
+]
+
+[package.dependencies]
+six = ">=1.4.0"
+
+[[package]]
+name = "ensureconda"
+version = "1.4.3"
+description = "Lightweight bootstrapper for a conda executable"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "ensureconda-1.4.3-py3-none-any.whl", hash = "sha256:a746630675e8d5a6a3fc1e8d2ec9c80459e1e722df94365a5099eec0079572a8"},
+ {file = "ensureconda-1.4.3.tar.gz", hash = "sha256:e04ae2e2f872869df7e7da22dcca9bd7c42f839a55155dddb249bcdc9e6aae48"},
+]
+
+[package.dependencies]
+appdirs = "*"
+click = ">=5.1"
+filelock = "*"
+requests = ">=2"
+
+[[package]]
+name = "entrypoints"
+version = "0.4"
+description = "Discover and load entry points from installed packages."
+category = "main"
+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 = "main"
+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 = "fastjsonschema"
+version = "2.16.2"
+description = "Fastest Python implementation of JSON schema"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "fastjsonschema-2.16.2-py3-none-any.whl", hash = "sha256:21f918e8d9a1a4ba9c22e09574ba72267a6762d47822db9add95f6454e51cc1c"},
+ {file = "fastjsonschema-2.16.2.tar.gz", hash = "sha256:01e366f25d9047816fe3d288cbfc3e10541daf0af2044763f3d0ade42476da18"},
+]
+
+[package.extras]
+devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"]
+
+[[package]]
+name = "filelock"
+version = "3.9.0"
+description = "A platform independent file lock."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"},
+ {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"},
+]
+
+[package.extras]
+docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"]
+testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"]
+
+[[package]]
+name = "fonttools"
+version = "4.38.0"
+description = "Tools to manipulate font files"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "fonttools-4.38.0-py3-none-any.whl", hash = "sha256:820466f43c8be8c3009aef8b87e785014133508f0de64ec469e4efb643ae54fb"},
+ {file = "fonttools-4.38.0.zip", hash = "sha256:2bb244009f9bf3fa100fc3ead6aeb99febe5985fa20afbfbaa2f8946c2fbdaf1"},
+]
+
+[package.extras]
+all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=14.0.0)", "xattr", "zopfli (>=0.1.4)"]
+graphite = ["lz4 (>=1.7.4.2)"]
+interpolatable = ["munkres", "scipy"]
+lxml = ["lxml (>=4.0,<5)"]
+pathops = ["skia-pathops (>=0.5.0)"]
+plot = ["matplotlib"]
+repacker = ["uharfbuzz (>=0.23.0)"]
+symfont = ["sympy"]
+type1 = ["xattr"]
+ufo = ["fs (>=2.2.0,<3)"]
+unicode = ["unicodedata2 (>=14.0.0)"]
+woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"]
+
+[[package]]
+name = "fqdn"
+version = "1.5.1"
+description = "Validates fully-qualified domain names against RFC 1123, so that they are acceptable to modern bowsers"
+category = "main"
+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 = "gitdb"
+version = "4.0.10"
+description = "Git Object Database"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"},
+ {file = "gitdb-4.0.10.tar.gz", hash = "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a"},
+]
+
+[package.dependencies]
+smmap = ">=3.0.1,<6"
+
+[[package]]
+name = "gitpython"
+version = "3.1.30"
+description = "GitPython is a python library used to interact with Git repositories"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "GitPython-3.1.30-py3-none-any.whl", hash = "sha256:cd455b0000615c60e286208ba540271af9fe531fa6a87cc590a7298785ab2882"},
+ {file = "GitPython-3.1.30.tar.gz", hash = "sha256:769c2d83e13f5d938b7688479da374c4e3d49f71549aaf462b646db9602ea6f8"},
+]
+
+[package.dependencies]
+gitdb = ">=4.0.1,<5"
+
+[[package]]
+name = "glcontext"
+version = "2.3.7"
+description = "Portable OpenGL Context"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "glcontext-2.3.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8ece87d8616bf12e55a08a05159f4303c8b82d348c2c43c7297c85d8e95dfa3e"},
+ {file = "glcontext-2.3.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5dcd68b23b1a549a3b0851d3621630e492ff9015a18f29f2512088b4e03e4d9"},
+ {file = "glcontext-2.3.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dc6a6133bffc33cb75bbc79dc08bd1e206017ac69ec68f703227aaf5f5129bb"},
+ {file = "glcontext-2.3.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc906a19be96d2820dee8e681ca1d3129821eb6e5c4f1544db723edf0c0696bd"},
+ {file = "glcontext-2.3.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89869925f4e1762878561fa1e3cbd1ee5ce73e5597275b5fc8bc054dd894fca4"},
+ {file = "glcontext-2.3.7-cp310-cp310-win32.whl", hash = "sha256:088482e07aed6229a34fbb1d0c5fbe0ad9c413dbddb5eaaa8e5c83d933cbe8d6"},
+ {file = "glcontext-2.3.7-cp310-cp310-win_amd64.whl", hash = "sha256:03b505fc8ce2dfcf800feac0e20cbb7b1899a5ef7407fa0cccb3267a5b2abbdb"},
+ {file = "glcontext-2.3.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:155154084bdedfc8904524d8bd212e5896cc5d5caf1d45c19d13dc34aee4b5ab"},
+ {file = "glcontext-2.3.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:65bf63b2068e13183e34a4beaf921f20cd144a25cebed0fa9a46f25e8b47577d"},
+ {file = "glcontext-2.3.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51e04b162529f99c7b764129e07aaa3ec8edfc63ca7a212b71e348319f8b821b"},
+ {file = "glcontext-2.3.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0961811d85ac551b1ce1f197296a8e5f497b35a149cfc6e128f74dfaef5e592f"},
+ {file = "glcontext-2.3.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa4595600a699ed13e854b87116a1519a25e47a10100df01650c1be3532bd629"},
+ {file = "glcontext-2.3.7-cp311-cp311-win32.whl", hash = "sha256:7dc827f119ccc3ea55b7bec73573516117c55319edc93bc2bbcf389bf1e7acfe"},
+ {file = "glcontext-2.3.7-cp311-cp311-win_amd64.whl", hash = "sha256:a22a3fbb3abefd7a9f5a672af8fccb8d8d996b2eae2075ac9d8ca10f4a6f6653"},
+ {file = "glcontext-2.3.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6df4cf354adb911a9ca58bc5c60fb1ae27544527878bc3ddf8f7ea56946c6fcc"},
+ {file = "glcontext-2.3.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f1656e931c937f8bdce12c551fa0077db814b123e7f16b6db26e1e7c89dae16"},
+ {file = "glcontext-2.3.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:058bf839884b5d5d8488978ed804023be64fc9bafb674a0ede1ba26c05bd9146"},
+ {file = "glcontext-2.3.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f17be52c99e3eaeefaaac780bde40bfa99be3ad32bbfed346bb347c9d0b01967"},
+ {file = "glcontext-2.3.7-cp37-cp37m-win32.whl", hash = "sha256:5a4cc4fef74dcab0b428ef750fad3c05311657ffb4f1dd3d4afa75e664551588"},
+ {file = "glcontext-2.3.7-cp37-cp37m-win_amd64.whl", hash = "sha256:fd03d6d8dbfdd9bab97ada98759e345b29d50f690cec95dd01d22d02f616bfea"},
+ {file = "glcontext-2.3.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:03b3925472771607d13feb9a0de93b04408ae86c91eee3f5e09e43744f90b1af"},
+ {file = "glcontext-2.3.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f234ebcc3355155811389c320974056ce20233770205fc7cb41d8653d6137efa"},
+ {file = "glcontext-2.3.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46ef33b616027a616dcceba33bc48e589ba24fa84ee43c5b8611c5b57d2dace3"},
+ {file = "glcontext-2.3.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ff822473d498d606424f92a341d01121562af35bf1d3d0e2ccd1f9c2f86859b"},
+ {file = "glcontext-2.3.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c90b525296c4930b1f74bf460b97af052c3cc9ba47d811f416ed82e1b16b03"},
+ {file = "glcontext-2.3.7-cp38-cp38-win32.whl", hash = "sha256:f1444229f84a7aea48ce3f1143147acee92eee264826db4c41ea38c6b0a924a9"},
+ {file = "glcontext-2.3.7-cp38-cp38-win_amd64.whl", hash = "sha256:59580776fd7e520995b82a6134c8ca7152a7881e174077fc785f4cc69c476d69"},
+ {file = "glcontext-2.3.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8aa90a648f17bacacef95b09a5fab368e8feff3714fc4b81eb9374bd439850e6"},
+ {file = "glcontext-2.3.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:867fe03c1c241d2416b719e23d1671537e34e03bab741dcc50d49298c1397073"},
+ {file = "glcontext-2.3.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae930d226f9145ec580f3fe10fc23262b8c21a6a0cd6fbc081a6606e9000ce74"},
+ {file = "glcontext-2.3.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc73099fa7525a20e2021a2f2befa61e9ef306364838c1859ba79f5bd8eda33a"},
+ {file = "glcontext-2.3.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:755698083c6119e771ea3f5837143324636700e1e5b397885c05085a837d5876"},
+ {file = "glcontext-2.3.7-cp39-cp39-win32.whl", hash = "sha256:ab8147607af85fc2ec2e02b4364ff36b636f63781295e74220dc5c5856794e07"},
+ {file = "glcontext-2.3.7-cp39-cp39-win_amd64.whl", hash = "sha256:2fae2d4bcb0564e0eb8e72c97e149faebfad369aeaef74ed7fd17f5f84a07428"},
+ {file = "glcontext-2.3.7-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e48550269c3baff04cc46ca79bd9d2d5a62216665751b10aa86d95ebe182d319"},
+ {file = "glcontext-2.3.7-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82eff3e5664c5a17fc0cbb1dae2c32088cdd3c3bfbfe4b9c71012275c2a63e8e"},
+ {file = "glcontext-2.3.7-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44f7dbf800e6f933a5c56e07b18ef70f44949f34bf57f5d5318e2199c12cbfbc"},
+ {file = "glcontext-2.3.7-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d18b3e9e9259595dd5c538c1fd9238f8b26c22d6351397e721ef8a89ad55f12"},
+ {file = "glcontext-2.3.7-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:376e12d230fd198a329dfe253b41480b0a015a2dabbac5eecf6b279fe3afb1b3"},
+ {file = "glcontext-2.3.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:84dc3b831af386cb20cae8fb10ac78d8007bb29118730db2e9f21c329a528028"},
+ {file = "glcontext-2.3.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c873315741dbc208c199cbe449aa77d1831551dd78d9b3d67e0a6f9eb576d"},
+ {file = "glcontext-2.3.7-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e47dd8cf39cabe20b41dd0c4c6589f0c7a4de2a5bad8e51ab0fc0b4a26ae6b"},
+ {file = "glcontext-2.3.7-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79e561b67e606b6e13ba58e6ae3e688e3429dbb5d60e551ba40d649432044f37"},
+ {file = "glcontext-2.3.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d986976c9b758d60d966fbaf8bdff129d125e8b2c58889d2220ca96991f1071e"},
+ {file = "glcontext-2.3.7-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:891b56a3bbaf3470595c218e847e79448e95cecb412224c8585da640c61cf29a"},
+ {file = "glcontext-2.3.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a349317c9d634aa56e30aae9ad408bc1b9de281af0e4f87de682b454ebaf540e"},
+ {file = "glcontext-2.3.7-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1716d21d423a1a2261cd717bc66714eeb5464d6a061b92678f356ca69cfd1255"},
+ {file = "glcontext-2.3.7-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:440ff5f59f318ce495c6bdddfa01a23dd64713fb960ceb87c3a9423745781d47"},
+ {file = "glcontext-2.3.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ef0c7e534e53f14b7b09dc3fe1e207243c9bb3eb2543d9876ed253156ca7a8bf"},
+ {file = "glcontext-2.3.7.tar.gz", hash = "sha256:bb2d0503f45ad85ca7319bd37fd983e374b3f824c38a450b5f72cfc974114156"},
+]
+
+[[package]]
+name = "glfw"
+version = "1.12.0"
+description = "A ctypes-based wrapper for GLFW3."
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "glfw-1.12.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38-none-macosx_10_6_intel.whl", hash = "sha256:88bd1cd2ace036d275e9af8312993068d94b3ac19248421eedc35a4baf53bb8c"},
+ {file = "glfw-1.12.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38-none-manylinux2010_i686.whl", hash = "sha256:bb63f6121c40f5f17cd78328c040b40aeaca9ed748f440c40d2fcad824107a74"},
+ {file = "glfw-1.12.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38-none-manylinux2010_x86_64.whl", hash = "sha256:6324fed2c4fd1762ae580277c534b5dab6f360b8fb9aed3e1547cf33f63d207c"},
+ {file = "glfw-1.12.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38-none-manylinux2014_x86_64.whl", hash = "sha256:20c918a1ae413a009f7559be9559bc2ec1d6251888f588ffa7a174d6db69a942"},
+ {file = "glfw-1.12.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38-none-win32.whl", hash = "sha256:fe9622a48ec9dc436e67de0d0bb4c9443996b5bd5564df1734d3a4280b728d38"},
+ {file = "glfw-1.12.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38-none-win_amd64.whl", hash = "sha256:3723db5e5628b2cd8729421fb6fc5fed3e5d25c7d1631f72f13301b218ee1600"},
+ {file = "glfw-1.12.0.tar.gz", hash = "sha256:f195ed7a43475e4f1603903d6999f3a6b470fda88bd1749ff10adc520abe8fb1"},
+]
+
+[[package]]
+name = "google-auth"
+version = "2.16.0"
+description = "Google Authentication Library"
+category = "main"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*"
+files = [
+ {file = "google-auth-2.16.0.tar.gz", hash = "sha256:ed7057a101af1146f0554a769930ac9de506aeca4fd5af6543ebe791851a9fbd"},
+ {file = "google_auth-2.16.0-py2.py3-none-any.whl", hash = "sha256:5045648c821fb72384cdc0e82cc326df195f113a33049d9b62b74589243d2acc"},
+]
+
+[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 = "main"
+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 = "grpcio"
+version = "1.51.1"
+description = "HTTP/2-based RPC framework"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "grpcio-1.51.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:cc2bece1737b44d878cc1510ea04469a8073dbbcdd762175168937ae4742dfb3"},
+ {file = "grpcio-1.51.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:e223a9793522680beae44671b9ed8f6d25bbe5ddf8887e66aebad5e0686049ef"},
+ {file = "grpcio-1.51.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:24ac1154c4b2ab4a0c5326a76161547e70664cd2c39ba75f00fc8a2170964ea2"},
+ {file = "grpcio-1.51.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4ef09f8997c4be5f3504cefa6b5c6cc3cf648274ce3cede84d4342a35d76db6"},
+ {file = "grpcio-1.51.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0b77e992c64880e6efbe0086fe54dfc0bbd56f72a92d9e48264dcd2a3db98"},
+ {file = "grpcio-1.51.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:eacad297ea60c72dd280d3353d93fb1dcca952ec11de6bb3c49d12a572ba31dd"},
+ {file = "grpcio-1.51.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:16c71740640ba3a882f50b01bf58154681d44b51f09a5728180a8fdc66c67bd5"},
+ {file = "grpcio-1.51.1-cp310-cp310-win32.whl", hash = "sha256:29cb97d41a4ead83b7bcad23bdb25bdd170b1e2cba16db6d3acbb090bc2de43c"},
+ {file = "grpcio-1.51.1-cp310-cp310-win_amd64.whl", hash = "sha256:9ff42c5620b4e4530609e11afefa4a62ca91fa0abb045a8957e509ef84e54d30"},
+ {file = "grpcio-1.51.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:bc59f7ba87972ab236f8669d8ca7400f02a0eadf273ca00e02af64d588046f02"},
+ {file = "grpcio-1.51.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:3c2b3842dcf870912da31a503454a33a697392f60c5e2697c91d133130c2c85d"},
+ {file = "grpcio-1.51.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22b011674090594f1f3245960ced7386f6af35485a38901f8afee8ad01541dbd"},
+ {file = "grpcio-1.51.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49d680356a975d9c66a678eb2dde192d5dc427a7994fb977363634e781614f7c"},
+ {file = "grpcio-1.51.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:094e64236253590d9d4075665c77b329d707b6fca864dd62b144255e199b4f87"},
+ {file = "grpcio-1.51.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:257478300735ce3c98d65a930bbda3db172bd4e00968ba743e6a1154ea6edf10"},
+ {file = "grpcio-1.51.1-cp311-cp311-win32.whl", hash = "sha256:5a6ebcdef0ef12005d56d38be30f5156d1cb3373b52e96f147f4a24b0ddb3a9d"},
+ {file = "grpcio-1.51.1-cp311-cp311-win_amd64.whl", hash = "sha256:3f9b0023c2c92bebd1be72cdfca23004ea748be1813a66d684d49d67d836adde"},
+ {file = "grpcio-1.51.1-cp37-cp37m-linux_armv7l.whl", hash = "sha256:cd3baccea2bc5c38aeb14e5b00167bd4e2373a373a5e4d8d850bd193edad150c"},
+ {file = "grpcio-1.51.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:17ec9b13cec4a286b9e606b48191e560ca2f3bbdf3986f91e480a95d1582e1a7"},
+ {file = "grpcio-1.51.1-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:fbdbe9a849854fe484c00823f45b7baab159bdd4a46075302281998cb8719df5"},
+ {file = "grpcio-1.51.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31bb6bc7ff145e2771c9baf612f4b9ebbc9605ccdc5f3ff3d5553de7fc0e0d79"},
+ {file = "grpcio-1.51.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e473525c28251558337b5c1ad3fa969511e42304524a4e404065e165b084c9e4"},
+ {file = "grpcio-1.51.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6f0b89967ee11f2b654c23b27086d88ad7bf08c0b3c2a280362f28c3698b2896"},
+ {file = "grpcio-1.51.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7942b32a291421460d6a07883033e392167d30724aa84987e6956cd15f1a21b9"},
+ {file = "grpcio-1.51.1-cp37-cp37m-win32.whl", hash = "sha256:f96ace1540223f26fbe7c4ebbf8a98e3929a6aa0290c8033d12526847b291c0f"},
+ {file = "grpcio-1.51.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f1fec3abaf274cdb85bf3878167cfde5ad4a4d97c68421afda95174de85ba813"},
+ {file = "grpcio-1.51.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:0e1a9e1b4a23808f1132aa35f968cd8e659f60af3ffd6fb00bcf9a65e7db279f"},
+ {file = "grpcio-1.51.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:6df3b63538c362312bc5fa95fb965069c65c3ea91d7ce78ad9c47cab57226f54"},
+ {file = "grpcio-1.51.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:172405ca6bdfedd6054c74c62085946e45ad4d9cec9f3c42b4c9a02546c4c7e9"},
+ {file = "grpcio-1.51.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:506b9b7a4cede87d7219bfb31014d7b471cfc77157da9e820a737ec1ea4b0663"},
+ {file = "grpcio-1.51.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fb93051331acbb75b49a2a0fd9239c6ba9528f6bdc1dd400ad1cb66cf864292"},
+ {file = "grpcio-1.51.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5dca372268c6ab6372d37d6b9f9343e7e5b4bc09779f819f9470cd88b2ece3c3"},
+ {file = "grpcio-1.51.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:471d39d3370ca923a316d49c8aac66356cea708a11e647e3bdc3d0b5de4f0a40"},
+ {file = "grpcio-1.51.1-cp38-cp38-win32.whl", hash = "sha256:75e29a90dc319f0ad4d87ba6d20083615a00d8276b51512e04ad7452b5c23b04"},
+ {file = "grpcio-1.51.1-cp38-cp38-win_amd64.whl", hash = "sha256:f1158bccbb919da42544a4d3af5d9296a3358539ffa01018307337365a9a0c64"},
+ {file = "grpcio-1.51.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:59dffade859f157bcc55243714d57b286da6ae16469bf1ac0614d281b5f49b67"},
+ {file = "grpcio-1.51.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:dad6533411d033b77f5369eafe87af8583178efd4039c41d7515d3336c53b4f1"},
+ {file = "grpcio-1.51.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:4c4423ea38a7825b8fed8934d6d9aeebdf646c97e3c608c3b0bcf23616f33877"},
+ {file = "grpcio-1.51.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0dc5354e38e5adf2498312f7241b14c7ce3484eefa0082db4297189dcbe272e6"},
+ {file = "grpcio-1.51.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97d67983189e2e45550eac194d6234fc38b8c3b5396c153821f2d906ed46e0ce"},
+ {file = "grpcio-1.51.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:538d981818e49b6ed1e9c8d5e5adf29f71c4e334e7d459bf47e9b7abb3c30e09"},
+ {file = "grpcio-1.51.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9235dcd5144a83f9ca6f431bd0eccc46b90e2c22fe27b7f7d77cabb2fb515595"},
+ {file = "grpcio-1.51.1-cp39-cp39-win32.whl", hash = "sha256:aacb54f7789ede5cbf1d007637f792d3e87f1c9841f57dd51abf89337d1b8472"},
+ {file = "grpcio-1.51.1-cp39-cp39-win_amd64.whl", hash = "sha256:2b170eaf51518275c9b6b22ccb59450537c5a8555326fd96ff7391b5dd75303c"},
+ {file = "grpcio-1.51.1.tar.gz", hash = "sha256:e6dfc2b6567b1c261739b43d9c59d201c1b89e017afd9e684d85aa7a186c9f7a"},
+]
+
+[package.extras]
+protobuf = ["grpcio-tools (>=1.51.1)"]
+
+[[package]]
+name = "gym"
+version = "0.21.0"
+description = "Gym: A universal API for reinforcement learning environments."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "gym-0.21.0.tar.gz", hash = "sha256:0fd1ce165c754b4017e37a617b097c032b8c3feb8a0394ccc8777c7c50dddff3"},
+]
+
+[package.dependencies]
+box2d-py = {version = "2.3.5", optional = true, markers = "extra == \"box2d\""}
+cloudpickle = ">=1.2.0"
+numpy = ">=1.18.0"
+pyglet = {version = ">=1.4.0", optional = true, markers = "extra == \"box2d\""}
+
+[package.extras]
+accept-rom-license = ["autorom[accept-rom-license] (>=0.4.2,<0.5.0)"]
+all = ["ale-py (>=0.7.1,<0.8.0)", "ale-py (>=0.7.1,<0.8.0)", "box2d-py (==2.3.5)", "box2d-py (==2.3.5)", "lz4 (>=3.1.0)", "lz4 (>=3.1.0)", "mujoco_py (>=1.50,<2.0)", "mujoco_py (>=1.50,<2.0)", "pyglet (>=1.4.0)", "pyglet (>=1.4.0)", "pyglet (>=1.4.0)", "pyglet (>=1.4.0)", "scipy (>=1.4.1)", "scipy (>=1.4.1)"]
+atari = ["ale-py (>=0.7.1,<0.8.0)"]
+box2d = ["box2d-py (==2.3.5)", "pyglet (>=1.4.0)"]
+classic-control = ["pyglet (>=1.4.0)"]
+mujoco = ["mujoco_py (>=1.50,<2.0)"]
+nomujoco = ["ale-py (>=0.7.1,<0.8.0)", "box2d-py (==2.3.5)", "lz4 (>=3.1.0)", "pyglet (>=1.4.0)", "pyglet (>=1.4.0)", "scipy (>=1.4.1)"]
+other = ["lz4 (>=3.1.0)"]
+robotics = ["mujoco_py (>=1.50,<2.0)"]
+toy-text = ["scipy (>=1.4.1)"]
+
+[[package]]
+name = "gym3"
+version = "0.3.3"
+description = "Vectorized Reinforcement Learning Environment Interface"
+category = "main"
+optional = false
+python-versions = ">=3.6.0"
+files = [
+ {file = "gym3-0.3.3-py3-none-any.whl", hash = "sha256:bacc0e124f8ce5e1d8a9dceb85725123332954f13ef4e226133506597548838a"},
+]
+
+[package.dependencies]
+cffi = ">=1.13.0,<2.0.0"
+glfw = ">=1.8.6,<2.0.0"
+imageio = ">=2.6.0,<3.0.0"
+imageio-ffmpeg = ">=0.3.0,<0.4.0"
+moderngl = ">=5.5.4,<6.0.0"
+numpy = ">=1.11.0,<2.0.0"
+
+[package.extras]
+test = ["gym (==0.17.2)", "gym-retro (==0.8.0)", "mpi4py (==3.0.3)", "pytest (==5.2.1)", "pytest-benchmark (==3.2.2)", "tensorflow (==1.15.0)"]
+
+[[package]]
+name = "html5lib"
+version = "1.1"
+description = "HTML parser based on the WHATWG HTML specification"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+files = [
+ {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"},
+ {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"},
+]
+
+[package.dependencies]
+six = ">=1.9"
+webencodings = "*"
+
+[package.extras]
+all = ["chardet (>=2.2)", "genshi", "lxml"]
+chardet = ["chardet (>=2.2)"]
+genshi = ["genshi"]
+lxml = ["lxml"]
+
+[[package]]
+name = "huggingface-hub"
+version = "0.12.0"
+description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
+category = "main"
+optional = false
+python-versions = ">=3.7.0"
+files = [
+ {file = "huggingface_hub-0.12.0-py3-none-any.whl", hash = "sha256:93809eabbfb2058a808bddf8b2a70f645de3f9df73ce87ddf5163d4c74b71c0c"},
+ {file = "huggingface_hub-0.12.0.tar.gz", hash = "sha256:da82c9ec8f9d8f976ffd3fd8249d20bb35c2dd3145a9f7ca1106f0ebefd9afa0"},
+]
+
+[package.dependencies]
+filelock = "*"
+packaging = ">=20.9"
+pyyaml = ">=5.1"
+requests = "*"
+tqdm = ">=4.42.1"
+typing-extensions = ">=3.7.4.3"
+
+[package.extras]
+all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "black (==22.3)", "flake8 (>=3.8.3)", "flake8-bugbear", "isort (>=5.5.4)", "jedi", "mypy (==0.982)", "pytest", "pytest-cov", "pytest-env", "pytest-xdist", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3"]
+cli = ["InquirerPy (==0.3.4)"]
+dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "black (==22.3)", "flake8 (>=3.8.3)", "flake8-bugbear", "isort (>=5.5.4)", "jedi", "mypy (==0.982)", "pytest", "pytest-cov", "pytest-env", "pytest-xdist", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3"]
+fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"]
+quality = ["black (==22.3)", "flake8 (>=3.8.3)", "flake8-bugbear", "isort (>=5.5.4)", "mypy (==0.982)"]
+tensorflow = ["graphviz", "pydot", "tensorflow"]
+testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "isort (>=5.5.4)", "jedi", "pytest", "pytest-cov", "pytest-env", "pytest-xdist", "soundfile"]
+torch = ["torch"]
+typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3"]
+
+[[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.25.0"
+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.25.0-py3-none-any.whl", hash = "sha256:9ef2fdef1235eef849b1aea399f08508493624a2a2c8cc3bba957dabb7d0b79f"},
+ {file = "imageio-2.25.0.tar.gz", hash = "sha256:b80796a1f8c38c697a940a2ad7397ee28900d5c4e51061b9a67d16aca867f33e"},
+]
+
+[package.dependencies]
+numpy = "*"
+pillow = ">=8.3.2"
+
+[package.extras]
+all-plugins = ["astropy", "av", "imageio-ffmpeg", "opencv-python", "psutil", "tifffile"]
+all-plugins-pypy = ["av", "imageio-ffmpeg", "psutil", "tifffile"]
+build = ["wheel"]
+dev = ["black", "flake8", "fsspec[github]", "invoke", "pytest", "pytest-cov"]
+docs = ["numpydoc", "pydata-sphinx-theme", "sphinx (<6)"]
+ffmpeg = ["imageio-ffmpeg", "psutil"]
+fits = ["astropy"]
+full = ["astropy", "av", "black", "flake8", "fsspec[github]", "gdal", "imageio-ffmpeg", "invoke", "itk", "numpydoc", "opencv-python", "psutil", "pydata-sphinx-theme", "pytest", "pytest-cov", "sphinx (<6)", "tifffile", "wheel"]
+gdal = ["gdal"]
+itk = ["itk"]
+linting = ["black", "flake8"]
+opencv = ["opencv-python"]
+pyav = ["av"]
+test = ["fsspec[github]", "invoke", "pytest", "pytest-cov"]
+tifffile = ["tifffile"]
+
+[[package]]
+name = "imageio-ffmpeg"
+version = "0.3.0"
+description = "FFMPEG wrapper for Python"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "imageio-ffmpeg-0.3.0.tar.gz", hash = "sha256:28500544acdebc195159d53a4670b76d596f368b218317bad5d3cbf43b00d6c2"},
+ {file = "imageio_ffmpeg-0.3.0-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:f99476aa42aac2ca0657483b417874a825b2f526aaa8a7a823b8a803f366caab"},
+ {file = "imageio_ffmpeg-0.3.0-py3-none-manylinux2010_x86_64.whl", hash = "sha256:f3d4096bf6e211540c4f6b9628c56d8e700cf12027ed842724173ab9c6666d1a"},
+ {file = "imageio_ffmpeg-0.3.0-py3-none-win32.whl", hash = "sha256:400345dd194f2cb2b424294aa0f3c90afce1de96879ffe3266afeece3494d93c"},
+ {file = "imageio_ffmpeg-0.3.0-py3-none-win_amd64.whl", hash = "sha256:991416c0eed0d221229e67342b8264a8b9defdec61d8a9e7688b90dbb838fb1e"},
+]
+
+[[package]]
+name = "importlib-metadata"
+version = "4.13.0"
+description = "Read metadata from Python packages"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "importlib_metadata-4.13.0-py3-none-any.whl", hash = "sha256:8a8a81bcf996e74fee46f0d16bd3eaa382a7eb20fd82445c3ad11f4090334116"},
+ {file = "importlib_metadata-4.13.0.tar.gz", hash = "sha256:dd0173e8f150d6815e098fd354f6414b0f079af4644ddfe90c71e2fc6174346d"},
+]
+
+[package.dependencies]
+zipp = ">=0.5"
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
+perf = ["ipython"]
+testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
+
+[[package]]
+name = "importlib-resources"
+version = "5.10.2"
+description = "Read resources from Python packages"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "importlib_resources-5.10.2-py3-none-any.whl", hash = "sha256:7d543798b0beca10b6a01ac7cafda9f822c54db9e8376a6bf57e0cbd74d486b6"},
+ {file = "importlib_resources-5.10.2.tar.gz", hash = "sha256:e4a96c8cc0339647ff9a5e0550d9f276fc5a01ffa276012b58ec108cfd7b8484"},
+]
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
+
+[[package]]
+name = "ipykernel"
+version = "6.20.2"
+description = "IPython Kernel for Jupyter"
+category = "main"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "ipykernel-6.20.2-py3-none-any.whl", hash = "sha256:5d0675d5f48bf6a95fd517d7b70bcb3b2c5631b2069949b5c2d6e1d7477fb5a0"},
+ {file = "ipykernel-6.20.2.tar.gz", hash = "sha256:1893c5b847033cd7a58f6843b04a9349ffb1031bc6588401cadc9adb58da428e"},
+]
+
+[package.dependencies]
+appnope = {version = "*", markers = "platform_system == \"Darwin\""}
+comm = ">=0.1.1"
+debugpy = ">=1.0"
+ipython = ">=7.23.1"
+jupyter-client = ">=6.1.12"
+matplotlib-inline = ">=0.1"
+nest-asyncio = "*"
+packaging = "*"
+psutil = "*"
+pyzmq = ">=17"
+tornado = ">=6.1"
+traitlets = ">=5.4.0"
+
+[package.extras]
+cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"]
+docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "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.10.0"
+description = "IPython: Productive Interactive Computing"
+category = "main"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "ipython-8.10.0-py3-none-any.whl", hash = "sha256:b38c31e8fc7eff642fc7c597061fff462537cf2314e3225a19c906b7b0d8a345"},
+ {file = "ipython-8.10.0.tar.gz", hash = "sha256:b13a1d6c1f5818bd388db53b7107d17454129a70de2b87481d555daede5eb49e"},
+]
+
+[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.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 = "main"
+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.4"
+description = "Jupyter interactive widgets"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "ipywidgets-8.0.4-py3-none-any.whl", hash = "sha256:ebb195e743b16c3947fe8827190fb87b4d00979c0fbf685afe4d2c4927059fa1"},
+ {file = "ipywidgets-8.0.4.tar.gz", hash = "sha256:c0005a77a47d77889cafed892b58e33b4a2a96712154404c6548ec22272811ea"},
+]
+
+[package.dependencies]
+ipykernel = ">=4.5.1"
+ipython = ">=6.1.0"
+jupyterlab-widgets = ">=3.0,<4.0"
+traitlets = ">=4.3.1"
+widgetsnbextension = ">=4.0,<5.0"
+
+[package.extras]
+test = ["jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"]
+
+[[package]]
+name = "isoduration"
+version = "20.11.0"
+description = "Operations with ISO 8601 durations"
+category = "main"
+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 = "jaraco-classes"
+version = "3.2.3"
+description = "Utility functions for Python class constructs"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jaraco.classes-3.2.3-py3-none-any.whl", hash = "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158"},
+ {file = "jaraco.classes-3.2.3.tar.gz", hash = "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a"},
+]
+
+[package.dependencies]
+more-itertools = "*"
+
+[package.extras]
+docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
+testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
+
+[[package]]
+name = "jedi"
+version = "0.18.2"
+description = "An autocompletion tool for Python that can be used for text editors."
+category = "main"
+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 = "jeepney"
+version = "0.8.0"
+description = "Low-level, pure Python DBus protocol wrapper."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"},
+ {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"},
+]
+
+[package.extras]
+test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"]
+trio = ["async_generator", "trio"]
+
+[[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 = "jsonpointer"
+version = "2.3"
+description = "Identify specific nodes in a JSON document (RFC 6901)"
+category = "main"
+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 = "main"
+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 = "main"
+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 = "7.4.9"
+description = "Jupyter protocol implementation and client libraries"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jupyter_client-7.4.9-py3-none-any.whl", hash = "sha256:214668aaea208195f4c13d28eb272ba79f945fc0cf3f11c7092c20b2ca1980e7"},
+ {file = "jupyter_client-7.4.9.tar.gz", hash = "sha256:52be28e04171f07aed8f20e1616a5a552ab9fee9cbbe6c1896ae170c3880d392"},
+]
+
+[package.dependencies]
+entrypoints = "*"
+jupyter-core = ">=4.9.2"
+nest-asyncio = ">=1.5.4"
+python-dateutil = ">=2.8.2"
+pyzmq = ">=23.0"
+tornado = ">=6.2"
+traitlets = "*"
+
+[package.extras]
+doc = ["ipykernel", "myst-parser", "sphinx (>=1.3.6)", "sphinx-rtd-theme", "sphinxcontrib-github-alt"]
+test = ["codecov", "coverage", "ipykernel (>=6.12)", "ipython", "mypy", "pre-commit", "pytest", "pytest-asyncio (>=0.18)", "pytest-cov", "pytest-timeout"]
+
+[[package]]
+name = "jupyter-console"
+version = "6.4.4"
+description = "Jupyter terminal console"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jupyter_console-6.4.4-py3-none-any.whl", hash = "sha256:756df7f4f60c986e7bc0172e4493d3830a7e6e75c08750bbe59c0a5403ad6dee"},
+ {file = "jupyter_console-6.4.4.tar.gz", hash = "sha256:172f5335e31d600df61613a97b7f0352f2c8250bbd1092ef2d658f77249f89fb"},
+]
+
+[package.dependencies]
+ipykernel = "*"
+ipython = "*"
+jupyter-client = ">=7.0.0"
+prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0"
+pygments = "*"
+
+[package.extras]
+test = ["pexpect"]
+
+[[package]]
+name = "jupyter-core"
+version = "5.1.3"
+description = "Jupyter core package. A base package on which Jupyter projects rely."
+category = "main"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "jupyter_core-5.1.3-py3-none-any.whl", hash = "sha256:d23ab7db81ca1759f13780cd6b65f37f59bf8e0186ac422d5ca4982cc7d56716"},
+ {file = "jupyter_core-5.1.3.tar.gz", hash = "sha256:82e1cff0ef804c38677eff7070d5ff1d45037fef01a2d9ba9e6b7b8201831e9f"},
+]
+
+[package.dependencies]
+platformdirs = ">=2.5"
+pywin32 = {version = ">=1.0", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""}
+traitlets = ">=5.3"
+
+[package.extras]
+docs = ["myst-parser", "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 = "main"
+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-server"
+version = "2.1.0"
+description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications."
+category = "main"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "jupyter_server-2.1.0-py3-none-any.whl", hash = "sha256:90cd6f2bd0581ddd9b2dbe82026a0f4c228a1d95c86e22460efbfdfc931fcf56"},
+ {file = "jupyter_server-2.1.0.tar.gz", hash = "sha256:efaae5e4f0d5f22c7f2f2dc848635036ee74a2df02abed52d30d9d95121ad382"},
+]
+
+[package.dependencies]
+anyio = ">=3.1.0,<4"
+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"]
+test = ["ipykernel", "pre-commit", "pytest (>=7.0)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.4)", "pytest-timeout", "requests"]
+
+[[package]]
+name = "jupyter-server-terminals"
+version = "0.4.4"
+description = "A Jupyter Server Extension Providing Terminals."
+category = "main"
+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 = "jupyterlab-pygments"
+version = "0.2.2"
+description = "Pygments theme using JupyterLab CSS variables"
+category = "main"
+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-widgets"
+version = "3.0.5"
+description = "Jupyter interactive widgets for JupyterLab"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jupyterlab_widgets-3.0.5-py3-none-any.whl", hash = "sha256:a04a42e50231b355b7087e16a818f541e53589f7647144ea0344c4bf16f300e5"},
+ {file = "jupyterlab_widgets-3.0.5.tar.gz", hash = "sha256:eeaecdeaf6c03afc960ddae201ced88d5979b4ca9c3891bcb8f6631af705f5ef"},
+]
+
+[[package]]
+name = "keyring"
+version = "23.13.1"
+description = "Store and access your passwords safely."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "keyring-23.13.1-py3-none-any.whl", hash = "sha256:771ed2a91909389ed6148631de678f82ddc73737d85a927f382a8a1b157898cd"},
+ {file = "keyring-23.13.1.tar.gz", hash = "sha256:ba2e15a9b35e21908d0aaf4e0a47acc52d6ae33444df0da2b49d41a46ef6d678"},
+]
+
+[package.dependencies]
+importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""}
+"jaraco.classes" = "*"
+jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""}
+pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""}
+SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""}
+
+[package.extras]
+completion = ["shtab"]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
+testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
+
+[[package]]
+name = "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 = "lockfile"
+version = "0.12.2"
+description = "Platform-independent file locking module"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "lockfile-0.12.2-py2.py3-none-any.whl", hash = "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa"},
+ {file = "lockfile-0.12.2.tar.gz", hash = "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799"},
+]
+
+[[package]]
+name = "markdown"
+version = "3.4.1"
+description = "Python implementation of Markdown."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "Markdown-3.4.1-py3-none-any.whl", hash = "sha256:08fb8465cffd03d10b9dd34a5c3fea908e20391a2a90b88d66362cb05beed186"},
+ {file = "Markdown-3.4.1.tar.gz", hash = "sha256:3b809086bb6efad416156e00a0da66fe47618a5d6918dd688f53f40c8e4cfeff"},
+]
+
+[package.extras]
+testing = ["coverage", "pyyaml"]
+
+[[package]]
+name = "markupsafe"
+version = "2.1.1"
+description = "Safely add untrusted strings to HTML/XML markup."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
+ {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
+]
+
+[[package]]
+name = "matplotlib"
+version = "3.6.3"
+description = "Python plotting package"
+category = "main"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "matplotlib-3.6.3-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:80c166a0e28512e26755f69040e6bf2f946a02ffdb7c00bf6158cca3d2b146e6"},
+ {file = "matplotlib-3.6.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eb9421c403ffd387fbe729de6d9a03005bf42faba5e8432f4e51e703215b49fc"},
+ {file = "matplotlib-3.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5223affa21050fb6118353c1380c15e23aedfb436bf3e162c26dc950617a7519"},
+ {file = "matplotlib-3.6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00c248ab6b92bea3f8148714837937053a083ff03b4c5e30ed37e28fc0e7e56"},
+ {file = "matplotlib-3.6.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca94f0362f6b6f424b555b956971dcb94b12d0368a6c3e07dc7a40d32d6d873d"},
+ {file = "matplotlib-3.6.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59400cc9451094b7f08cc3f321972e6e1db4cd37a978d4e8a12824bf7fd2f03b"},
+ {file = "matplotlib-3.6.3-cp310-cp310-win32.whl", hash = "sha256:57ad1aee29043163374bfa8990e1a2a10ff72c9a1bfaa92e9c46f6ea59269121"},
+ {file = "matplotlib-3.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:1fcc4cad498533d3c393a160975acc9b36ffa224d15a6b90ae579eacee5d8579"},
+ {file = "matplotlib-3.6.3-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:d2cfaa7fd62294d945b8843ea24228a27c8e7c5b48fa634f3c168153b825a21b"},
+ {file = "matplotlib-3.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c3f08df2ac4636249b8bc7a85b8b82c983bef1441595936f62c2918370ca7e1d"},
+ {file = "matplotlib-3.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff2aa84e74f80891e6bcf292ebb1dd57714ffbe13177642d65fee25384a30894"},
+ {file = "matplotlib-3.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11011c97d62c1db7bc20509572557842dbb8c2a2ddd3dd7f20501aa1cde3e54e"},
+ {file = "matplotlib-3.6.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c235bf9be052347373f589e018988cad177abb3f997ab1a2e2210c41562cc0c"},
+ {file = "matplotlib-3.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bebcff4c3ed02c6399d47329f3554193abd824d3d53b5ca02cf583bcd94470e2"},
+ {file = "matplotlib-3.6.3-cp311-cp311-win32.whl", hash = "sha256:d5f18430f5cfa5571ab8f4c72c89af52aa0618e864c60028f11a857d62200cba"},
+ {file = "matplotlib-3.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:dfba7057609ca9567b9704626756f0142e97ec8c5ba2c70c6e7bd1c25ef99f06"},
+ {file = "matplotlib-3.6.3-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:9fb8fb19d03abf3c5dab89a8677e62c4023632f919a62b6dd1d6d2dbf42cd9f5"},
+ {file = "matplotlib-3.6.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:bbf269e1d24bc25247095d71c7a969813f7080e2a7c6fa28931a603f747ab012"},
+ {file = "matplotlib-3.6.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:994637e2995b0342699b396a320698b07cd148bbcf2dd2fa2daba73f34dd19f2"},
+ {file = "matplotlib-3.6.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:77b384cee7ab8cf75ffccbfea351a09b97564fc62d149827a5e864bec81526e5"},
+ {file = "matplotlib-3.6.3-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:73b93af33634ed919e72811c9703e1105185cd3fb46d76f30b7f4cfbbd063f89"},
+ {file = "matplotlib-3.6.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:debeab8e2ab07e5e3dac33e12456da79c7e104270d2b2d1df92b9e40347cca75"},
+ {file = "matplotlib-3.6.3-cp38-cp38-win32.whl", hash = "sha256:acc3b1a4bddbf56fe461e36fb9ef94c2cb607fc90d24ccc650040bfcc7610de4"},
+ {file = "matplotlib-3.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:1183877d008c752d7d535396096c910f4663e4b74a18313adee1213328388e1e"},
+ {file = "matplotlib-3.6.3-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:6adc441b5b2098a4b904bbf9d9e92fb816fef50c55aa2ea6a823fc89b94bb838"},
+ {file = "matplotlib-3.6.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:6d81b11ede69e3a751424b98dc869c96c10256b2206bfdf41f9c720eee86844c"},
+ {file = "matplotlib-3.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:29f17b7f2e068dc346687cbdf80b430580bab42346625821c2d3abf3a1ec5417"},
+ {file = "matplotlib-3.6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f56a7252eee8f3438447f75f5e1148a1896a2756a92285fe5d73bed6deebff4"},
+ {file = "matplotlib-3.6.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbddfeb1495484351fb5b30cf5bdf06b3de0bc4626a707d29e43dfd61af2a780"},
+ {file = "matplotlib-3.6.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:809119d1cba3ece3c9742eb01827fe7a0e781ea3c5d89534655a75e07979344f"},
+ {file = "matplotlib-3.6.3-cp39-cp39-win32.whl", hash = "sha256:e0a64d7cc336b52e90f59e6d638ae847b966f68582a7af041e063d568e814740"},
+ {file = "matplotlib-3.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:79e501eb847f4a489eb7065bb8d3187117f65a4c02d12ea3a19d6c5bef173bcc"},
+ {file = "matplotlib-3.6.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2787a16df07370dcba385fe20cdd0cc3cfaabd3c873ddabca78c10514c799721"},
+ {file = "matplotlib-3.6.3-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68d94a436f62b8a861bf3ace82067a71bafb724b4e4f9133521e4d8012420dd7"},
+ {file = "matplotlib-3.6.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81b409b2790cf8d7c1ef35920f01676d2ae7afa8241844e7aa5484fdf493a9a0"},
+ {file = "matplotlib-3.6.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:faff486b36530a836a6b4395850322e74211cd81fc17f28b4904e1bd53668e3e"},
+ {file = "matplotlib-3.6.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:38d38cb1ea1d80ee0f6351b65c6f76cad6060bbbead015720ba001348ae90f0c"},
+ {file = "matplotlib-3.6.3-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12f999661589981e74d793ee2f41b924b3b87d65fd929f6153bf0f30675c59b1"},
+ {file = "matplotlib-3.6.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01b7f521a9a73c383825813af255f8c4485d1706e4f3e2ed5ae771e4403a40ab"},
+ {file = "matplotlib-3.6.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9ceebaf73f1a3444fa11014f38b9da37ff7ea328d6efa1652241fe3777bfdab9"},
+ {file = "matplotlib-3.6.3.tar.gz", hash = "sha256:1f4d69707b1677560cd952544ee4962f68ff07952fb9069ff8c12b56353cb8c9"},
+]
+
+[package.dependencies]
+contourpy = ">=1.0.1"
+cycler = ">=0.10"
+fonttools = ">=4.22.0"
+kiwisolver = ">=1.0.1"
+numpy = ">=1.19"
+packaging = ">=20.0"
+pillow = ">=6.2.0"
+pyparsing = ">=2.2.1"
+python-dateutil = ">=2.7"
+
+[[package]]
+name = "matplotlib-inline"
+version = "0.1.6"
+description = "Inline Matplotlib backend for Jupyter"
+category = "main"
+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 = "mistune"
+version = "2.0.4"
+description = "A sane Markdown parser with useful plugins and renderers"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "mistune-2.0.4-py2.py3-none-any.whl", hash = "sha256:182cc5ee6f8ed1b807de6b7bb50155df7b66495412836b9a74c8fbdfc75fe36d"},
+ {file = "mistune-2.0.4.tar.gz", hash = "sha256:9ee0a66053e2267aba772c71e06891fa8f1af6d4b01d5e84e267b4570d4d9808"},
+]
+
+[[package]]
+name = "moderngl"
+version = "5.7.4"
+description = "ModernGL: High performance rendering for Python 3"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "moderngl-5.7.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1719892e83bda0b433b254f1be22a41745ee6b5a4fd3a6ee5aa799756383d4f"},
+ {file = "moderngl-5.7.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c6fdea10f5176b8018d2e5a05ed9fa5010b8ab24fcbabe64c05a943d50b9ba7d"},
+ {file = "moderngl-5.7.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:396b2bc0a09d409b40e7c559bb1092ee327f5394b125e2ebfa9fc2951e27550d"},
+ {file = "moderngl-5.7.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8eea6a083e8737bbe666e83c460a2628556f7df489987405fe33b27db3df386c"},
+ {file = "moderngl-5.7.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca77be80c96c0585d7dd0f0b65815883fb5416f22de60875bec0f87f8c1c2b6b"},
+ {file = "moderngl-5.7.4-cp310-cp310-win32.whl", hash = "sha256:48c3677f0303a2c5887dccd4ff3c06cc60bc54f05646bb4575f87dd69f65f83b"},
+ {file = "moderngl-5.7.4-cp310-cp310-win_amd64.whl", hash = "sha256:7d28d49892923c2cc8eb145266af95ed99a0c4f3fc686228af941d1e0cdbd9bb"},
+ {file = "moderngl-5.7.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dd2446134c7bd71c8d47606545ca7820cb411723edf77d2b09301651a2356e91"},
+ {file = "moderngl-5.7.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d549c518624975652ad511659d2b27827d2aa9ccbf09b17b4b53afe4226b68c7"},
+ {file = "moderngl-5.7.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4441cc13384c41b7434eb1eab42f1c305f6c9b3dd8665e98ad8dc1870ff83d38"},
+ {file = "moderngl-5.7.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc921e7b716a0260eeb352d3c6fd1c0fa5edb434de89e0325fc58611d3929f9e"},
+ {file = "moderngl-5.7.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdab10cbe2b87394440a3678c1445fe7fe687e82fd6b3ae7ddd76ec68766115a"},
+ {file = "moderngl-5.7.4-cp311-cp311-win32.whl", hash = "sha256:631876e886aebf293a9afdf54160d46f1dc2646631750837ef04518044c4de5f"},
+ {file = "moderngl-5.7.4-cp311-cp311-win_amd64.whl", hash = "sha256:38c5409df54f6601b4df94f61609ef7519bb570061333ba2f7d58bddef0963df"},
+ {file = "moderngl-5.7.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:875fe5701061a7bbe833cbd42cd779c68748ed277b2ab1ec06be61972e3e846f"},
+ {file = "moderngl-5.7.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb49672482406f539968b10f30d982578242a850dd7f071dc57b34fa88aebe3d"},
+ {file = "moderngl-5.7.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f502d3ac58a776a57f1f524efbd05b6b95a601e087082dc8cd5cd71cb315c577"},
+ {file = "moderngl-5.7.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:918f11262c4724e2515c4afc76ad57c6148d486fd6e7c3b1c04470f43f57f97b"},
+ {file = "moderngl-5.7.4-cp37-cp37m-win32.whl", hash = "sha256:dce7cb87d7faced6f78e06c7e2fa607a79a50cb8ff0aed3dc9bca77b814ee042"},
+ {file = "moderngl-5.7.4-cp37-cp37m-win_amd64.whl", hash = "sha256:dd010370e30bc52e65baee5061eb2ec6965adc2b08e81c232ccf7ec76553af5e"},
+ {file = "moderngl-5.7.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:49546570a7eb4594cc1a4109fdef47a035ffb0b11cc3d38aafe92ab81dc6d50f"},
+ {file = "moderngl-5.7.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:824f6dc34316530a617fea9e6a46d008cfe82a01e9d34ae308ad51ef127720a7"},
+ {file = "moderngl-5.7.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cb8c76c77c47cf05c29bd1940d95a641ad1b6052b5b0d29ffca816295ca364e"},
+ {file = "moderngl-5.7.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b923f6644989cde6675394f63027914546fc33a380148cec449176980c25e47"},
+ {file = "moderngl-5.7.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe9a40751133744018dcb19289a1f798908b1eff1a8455803dfe1bc4028f0f34"},
+ {file = "moderngl-5.7.4-cp38-cp38-win32.whl", hash = "sha256:fcc0e5b557a67137a5c6944faf4dde096cf5afab296d11e86bbf20ead043eddf"},
+ {file = "moderngl-5.7.4-cp38-cp38-win_amd64.whl", hash = "sha256:8ffbad06fb39116406a8988d19fd073bbbf973513a376b3c6b181ae67408237b"},
+ {file = "moderngl-5.7.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:82cd399b152f3a775c1f93ef3192980a9d3d2d1c51a9df07a329ce4e4fc82430"},
+ {file = "moderngl-5.7.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eb6ad78ecfc06c0aab0be9706b71be5ee0ad443efbc4f6578a315da88dc3d4ef"},
+ {file = "moderngl-5.7.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1d6c79fad12df622fa0dc8b832a2c624368cd949a0326f65ebc234891726068"},
+ {file = "moderngl-5.7.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9630a42cefafaeb19a816e23de2869d855af39b880c38d9efc16b50e354297c4"},
+ {file = "moderngl-5.7.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:395f96f81ea9b3c8fcd9cf1e5aedd9fe81ef0a2475922fe8f1657fbfdbd03397"},
+ {file = "moderngl-5.7.4-cp39-cp39-win32.whl", hash = "sha256:512d9287a9e80bdc706c6f54b190820d8f3887619a574d5f996eb27ff2385777"},
+ {file = "moderngl-5.7.4-cp39-cp39-win_amd64.whl", hash = "sha256:664a0089abc845bba2198fcadbe6d78adf65e4e5f8e117147f0d36353e9a2090"},
+ {file = "moderngl-5.7.4-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2f8ec6ddb6141751f35734e8ce2222868da3fca57fbcb83ce4f28c93a5fcb91c"},
+ {file = "moderngl-5.7.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4660a33a093d44237d40adf884a4b678dc7c1237a6d634a2ffa4652cd476aa19"},
+ {file = "moderngl-5.7.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70477f355e45a8c2cce4f97818191ab28313e4203b15554214a417853ee8cfcb"},
+ {file = "moderngl-5.7.4-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35f5e916d3d33753ec7a0611b5cf5addbe88a8a1aa3d292f2693808381bb19c5"},
+ {file = "moderngl-5.7.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a2e50913ab8e53b3c8da792f4b75ff7afcd117e596346110ec085de535d03ef1"},
+ {file = "moderngl-5.7.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9dc27da2e9225fc9df21fa0522cd77e781ee0a2d7b75a03449c963516fb9b96c"},
+ {file = "moderngl-5.7.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bfd84ac24d141b490293487e63eacebc0f198d12049adfe44d08168fc89f979a"},
+ {file = "moderngl-5.7.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3cde28a2091222f51a743561cebc8d80314042a7db9a8e723e8fa5f1ad6618d"},
+ {file = "moderngl-5.7.4-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7579e66c345342f9b3fdf33e23107b874499979577b14014249856e73071c06"},
+ {file = "moderngl-5.7.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3a6d180fab423cbbaac1ee37e4d76821bf3a13e79776597c85199876f3053635"},
+ {file = "moderngl-5.7.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9c3e825088a0acfb0080f3ccc0332584f99868ed39bf986e44f03581f6d33128"},
+ {file = "moderngl-5.7.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44a69c62e468fd71c84384041242194f81a88fdfe08799a5f01393729c9928bd"},
+ {file = "moderngl-5.7.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7963e49cadb621b7ffbd5effe43ce6472afb535dd5c074924b95fdce54d289e"},
+ {file = "moderngl-5.7.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c226cb05022372b4e6b841b47d7c67d9da026d93dd38801351ce679e70beacf"},
+ {file = "moderngl-5.7.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a20a575d9a5cab62554fbcb7b53e5459a13f380ee2e5c4888e9b537ef575f1ca"},
+ {file = "moderngl-5.7.4.tar.gz", hash = "sha256:20f821bf66b2811bc8648d7cf7f64402afff7619fea271f42a6ee85fe03e4041"},
+]
+
+[package.dependencies]
+glcontext = ">=2.3.6,<3"
+
+[[package]]
+name = "more-itertools"
+version = "9.0.0"
+description = "More routines for operating on iterables, beyond itertools"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "more-itertools-9.0.0.tar.gz", hash = "sha256:5a6257e40878ef0520b1803990e3e22303a41b5714006c32a3fd8304b26ea1ab"},
+ {file = "more_itertools-9.0.0-py3-none-any.whl", hash = "sha256:250e83d7e81d0c87ca6bd942e6aeab8cc9daa6096d12c5308f3f92fa5e5c1f41"},
+]
+
+[[package]]
+name = "msgpack"
+version = "1.0.4"
+description = "MessagePack serializer"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "msgpack-1.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4ab251d229d10498e9a2f3b1e68ef64cb393394ec477e3370c457f9430ce9250"},
+ {file = "msgpack-1.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:112b0f93202d7c0fef0b7810d465fde23c746a2d482e1e2de2aafd2ce1492c88"},
+ {file = "msgpack-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:002b5c72b6cd9b4bafd790f364b8480e859b4712e91f43014fe01e4f957b8467"},
+ {file = "msgpack-1.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35bc0faa494b0f1d851fd29129b2575b2e26d41d177caacd4206d81502d4c6a6"},
+ {file = "msgpack-1.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4733359808c56d5d7756628736061c432ded018e7a1dff2d35a02439043321aa"},
+ {file = "msgpack-1.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb514ad14edf07a1dbe63761fd30f89ae79b42625731e1ccf5e1f1092950eaa6"},
+ {file = "msgpack-1.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c23080fdeec4716aede32b4e0ef7e213c7b1093eede9ee010949f2a418ced6ba"},
+ {file = "msgpack-1.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:49565b0e3d7896d9ea71d9095df15b7f75a035c49be733051c34762ca95bbf7e"},
+ {file = "msgpack-1.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:aca0f1644d6b5a73eb3e74d4d64d5d8c6c3d577e753a04c9e9c87d07692c58db"},
+ {file = "msgpack-1.0.4-cp310-cp310-win32.whl", hash = "sha256:0dfe3947db5fb9ce52aaea6ca28112a170db9eae75adf9339a1aec434dc954ef"},
+ {file = "msgpack-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dea20515f660aa6b7e964433b1808d098dcfcabbebeaaad240d11f909298075"},
+ {file = "msgpack-1.0.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e83f80a7fec1a62cf4e6c9a660e39c7f878f603737a0cdac8c13131d11d97f52"},
+ {file = "msgpack-1.0.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c11a48cf5e59026ad7cb0dc29e29a01b5a66a3e333dc11c04f7e991fc5510a9"},
+ {file = "msgpack-1.0.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1276e8f34e139aeff1c77a3cefb295598b504ac5314d32c8c3d54d24fadb94c9"},
+ {file = "msgpack-1.0.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c9566f2c39ccced0a38d37c26cc3570983b97833c365a6044edef3574a00c08"},
+ {file = "msgpack-1.0.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fcb8a47f43acc113e24e910399376f7277cf8508b27e5b88499f053de6b115a8"},
+ {file = "msgpack-1.0.4-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:76ee788122de3a68a02ed6f3a16bbcd97bc7c2e39bd4d94be2f1821e7c4a64e6"},
+ {file = "msgpack-1.0.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0a68d3ac0104e2d3510de90a1091720157c319ceeb90d74f7b5295a6bee51bae"},
+ {file = "msgpack-1.0.4-cp36-cp36m-win32.whl", hash = "sha256:85f279d88d8e833ec015650fd15ae5eddce0791e1e8a59165318f371158efec6"},
+ {file = "msgpack-1.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:c1683841cd4fa45ac427c18854c3ec3cd9b681694caf5bff04edb9387602d661"},
+ {file = "msgpack-1.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a75dfb03f8b06f4ab093dafe3ddcc2d633259e6c3f74bb1b01996f5d8aa5868c"},
+ {file = "msgpack-1.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9667bdfdf523c40d2511f0e98a6c9d3603be6b371ae9a238b7ef2dc4e7a427b0"},
+ {file = "msgpack-1.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11184bc7e56fd74c00ead4f9cc9a3091d62ecb96e97653add7a879a14b003227"},
+ {file = "msgpack-1.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac5bd7901487c4a1dd51a8c58f2632b15d838d07ceedaa5e4c080f7190925bff"},
+ {file = "msgpack-1.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1e91d641d2bfe91ba4c52039adc5bccf27c335356055825c7f88742c8bb900dd"},
+ {file = "msgpack-1.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2a2df1b55a78eb5f5b7d2a4bb221cd8363913830145fad05374a80bf0877cb1e"},
+ {file = "msgpack-1.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:545e3cf0cf74f3e48b470f68ed19551ae6f9722814ea969305794645da091236"},
+ {file = "msgpack-1.0.4-cp37-cp37m-win32.whl", hash = "sha256:2cc5ca2712ac0003bcb625c96368fd08a0f86bbc1a5578802512d87bc592fe44"},
+ {file = "msgpack-1.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:eba96145051ccec0ec86611fe9cf693ce55f2a3ce89c06ed307de0e085730ec1"},
+ {file = "msgpack-1.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7760f85956c415578c17edb39eed99f9181a48375b0d4a94076d84148cf67b2d"},
+ {file = "msgpack-1.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:449e57cc1ff18d3b444eb554e44613cffcccb32805d16726a5494038c3b93dab"},
+ {file = "msgpack-1.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d603de2b8d2ea3f3bcb2efe286849aa7a81531abc52d8454da12f46235092bcb"},
+ {file = "msgpack-1.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f5d88c99f64c456413d74a975bd605a9b0526293218a3b77220a2c15458ba9"},
+ {file = "msgpack-1.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916c78f33602ecf0509cc40379271ba0f9ab572b066bd4bdafd7434dee4bc6e"},
+ {file = "msgpack-1.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81fc7ba725464651190b196f3cd848e8553d4d510114a954681fd0b9c479d7e1"},
+ {file = "msgpack-1.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d5b5b962221fa2c5d3a7f8133f9abffc114fe218eb4365e40f17732ade576c8e"},
+ {file = "msgpack-1.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:77ccd2af37f3db0ea59fb280fa2165bf1b096510ba9fe0cc2bf8fa92a22fdb43"},
+ {file = "msgpack-1.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b17be2478b622939e39b816e0aa8242611cc8d3583d1cd8ec31b249f04623243"},
+ {file = "msgpack-1.0.4-cp38-cp38-win32.whl", hash = "sha256:2bb8cdf50dd623392fa75525cce44a65a12a00c98e1e37bf0fb08ddce2ff60d2"},
+ {file = "msgpack-1.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:26b8feaca40a90cbe031b03d82b2898bf560027160d3eae1423f4a67654ec5d6"},
+ {file = "msgpack-1.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:462497af5fd4e0edbb1559c352ad84f6c577ffbbb708566a0abaaa84acd9f3ae"},
+ {file = "msgpack-1.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2999623886c5c02deefe156e8f869c3b0aaeba14bfc50aa2486a0415178fce55"},
+ {file = "msgpack-1.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f0029245c51fd9473dc1aede1160b0a29f4a912e6b1dd353fa6d317085b219da"},
+ {file = "msgpack-1.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed6f7b854a823ea44cf94919ba3f727e230da29feb4a99711433f25800cf747f"},
+ {file = "msgpack-1.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df96d6eaf45ceca04b3f3b4b111b86b33785683d682c655063ef8057d61fd92"},
+ {file = "msgpack-1.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a4192b1ab40f8dca3f2877b70e63799d95c62c068c84dc028b40a6cb03ccd0f"},
+ {file = "msgpack-1.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e3590f9fb9f7fbc36df366267870e77269c03172d086fa76bb4eba8b2b46624"},
+ {file = "msgpack-1.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1576bd97527a93c44fa856770197dec00d223b0b9f36ef03f65bac60197cedf8"},
+ {file = "msgpack-1.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:63e29d6e8c9ca22b21846234913c3466b7e4ee6e422f205a2988083de3b08cae"},
+ {file = "msgpack-1.0.4-cp39-cp39-win32.whl", hash = "sha256:fb62ea4b62bfcb0b380d5680f9a4b3f9a2d166d9394e9bbd9666c0ee09a3645c"},
+ {file = "msgpack-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:4d5834a2a48965a349da1c5a79760d94a1a0172fbb5ab6b5b33cbf8447e109ce"},
+ {file = "msgpack-1.0.4.tar.gz", hash = "sha256:f5d869c18f030202eb412f08b28d2afeea553d6613aee89e200d7aca7ef01f5f"},
+]
+
+[[package]]
+name = "nbclassic"
+version = "0.4.8"
+description = "A web-based notebook environment for interactive computing"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "nbclassic-0.4.8-py3-none-any.whl", hash = "sha256:cbf05df5842b420d5cece0143462380ea9d308ff57c2dc0eb4d6e035b18fbfb3"},
+ {file = "nbclassic-0.4.8.tar.gz", hash = "sha256:c74d8a500f8e058d46b576a41e5bc640711e1032cf7541dde5f73ea49497e283"},
+]
+
+[package.dependencies]
+argon2-cffi = "*"
+ipykernel = "*"
+ipython-genutils = "*"
+jinja2 = "*"
+jupyter-client = ">=6.1.1"
+jupyter-core = ">=4.6.1"
+jupyter-server = ">=1.8"
+nbconvert = ">=5"
+nbformat = "*"
+nest-asyncio = ">=1.5"
+notebook-shim = ">=0.1.0"
+prometheus-client = "*"
+pyzmq = ">=17"
+Send2Trash = ">=1.8.0"
+terminado = ">=0.8.3"
+tornado = ">=6.1"
+traitlets = ">=4.2.1"
+
+[package.extras]
+docs = ["myst-parser", "nbsphinx", "sphinx", "sphinx-rtd-theme", "sphinxcontrib-github-alt"]
+json-logging = ["json-logging"]
+test = ["coverage", "nbval", "pytest", "pytest-cov", "pytest-playwright", "pytest-tornasync", "requests", "requests-unixsocket", "testpath"]
+
+[[package]]
+name = "nbclient"
+version = "0.7.2"
+description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor."
+category = "main"
+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 = "7.2.8"
+description = "Converting Jupyter Notebooks"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "nbconvert-7.2.8-py3-none-any.whl", hash = "sha256:ac57f2812175441a883f50c8ff113133ca65fe7ae5a9f1e3da3bfd1a70dce2ee"},
+ {file = "nbconvert-7.2.8.tar.gz", hash = "sha256:ccedacde57a972836bfb46466485be29ed1364ed7c2f379f62bad47d340ece99"},
+]
+
+[package.dependencies]
+beautifulsoup4 = "*"
+bleach = "*"
+defusedxml = "*"
+jinja2 = ">=3.0"
+jupyter-core = ">=4.7"
+jupyterlab-pygments = "*"
+markupsafe = ">=2.0"
+mistune = ">=2.0.3,<3"
+nbclient = ">=0.5.0"
+nbformat = ">=5.1"
+packaging = "*"
+pandocfilters = ">=1.4.1"
+pygments = ">=2.4.1"
+tinycss2 = "*"
+traitlets = ">=5.0"
+
+[package.extras]
+all = ["nbconvert[docs,qtpdf,serve,test,webpdf]"]
+docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)", "sphinxcontrib-spelling"]
+qtpdf = ["nbconvert[qtpng]"]
+qtpng = ["pyqtwebengine (>=5.15)"]
+serve = ["tornado (>=6.1)"]
+test = ["ipykernel", "ipywidgets (>=7)", "pre-commit", "pytest", "pytest-dependency"]
+webpdf = ["pyppeteer (>=1,<1.1)"]
+
+[[package]]
+name = "nbformat"
+version = "5.7.3"
+description = "The Jupyter Notebook format"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "nbformat-5.7.3-py3-none-any.whl", hash = "sha256:22a98a6516ca216002b0a34591af5bcb8072ca6c63910baffc901cfa07fefbf0"},
+ {file = "nbformat-5.7.3.tar.gz", hash = "sha256:4b021fca24d3a747bf4e626694033d792d594705829e5e35b14ee3369f9f6477"},
+]
+
+[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 = "main"
+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 = "notebook"
+version = "6.5.2"
+description = "A web-based notebook environment for interactive computing"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "notebook-6.5.2-py3-none-any.whl", hash = "sha256:e04f9018ceb86e4fa841e92ea8fb214f8d23c1cedfde530cc96f92446924f0e4"},
+ {file = "notebook-6.5.2.tar.gz", hash = "sha256:c1897e5317e225fc78b45549a6ab4b668e4c996fd03a04e938fe5e7af2bfffd0"},
+]
+
+[package.dependencies]
+argon2-cffi = "*"
+ipykernel = "*"
+ipython-genutils = "*"
+jinja2 = "*"
+jupyter-client = ">=5.3.4"
+jupyter-core = ">=4.6.1"
+nbclassic = ">=0.4.7"
+nbconvert = ">=5"
+nbformat = "*"
+nest-asyncio = ">=1.5"
+prometheus-client = "*"
+pyzmq = ">=17"
+Send2Trash = ">=1.8.0"
+terminado = ">=0.8.3"
+tornado = ">=6.1"
+traitlets = ">=4.2.1"
+
+[package.extras]
+docs = ["myst-parser", "nbsphinx", "sphinx", "sphinx-rtd-theme", "sphinxcontrib-github-alt"]
+json-logging = ["json-logging"]
+test = ["coverage", "nbval", "pytest", "pytest-cov", "requests", "requests-unixsocket", "selenium (==4.1.5)", "testpath"]
+
+[[package]]
+name = "notebook-shim"
+version = "0.2.2"
+description = "A shim layer for notebook traits and config"
+category = "main"
+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 = "numexpr"
+version = "2.8.4"
+description = "Fast numerical expression evaluator for NumPy"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "numexpr-2.8.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a75967d46b6bd56455dd32da6285e5ffabe155d0ee61eef685bbfb8dafb2e484"},
+ {file = "numexpr-2.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db93cf1842f068247de631bfc8af20118bf1f9447cd929b531595a5e0efc9346"},
+ {file = "numexpr-2.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bca95f4473b444428061d4cda8e59ac564dc7dc6a1dea3015af9805c6bc2946"},
+ {file = "numexpr-2.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e34931089a6bafc77aaae21f37ad6594b98aa1085bb8b45d5b3cd038c3c17d9"},
+ {file = "numexpr-2.8.4-cp310-cp310-win32.whl", hash = "sha256:f3a920bfac2645017110b87ddbe364c9c7a742870a4d2f6120b8786c25dc6db3"},
+ {file = "numexpr-2.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:6931b1e9d4f629f43c14b21d44f3f77997298bea43790cfcdb4dd98804f90783"},
+ {file = "numexpr-2.8.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9400781553541f414f82eac056f2b4c965373650df9694286b9bd7e8d413f8d8"},
+ {file = "numexpr-2.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ee9db7598dd4001138b482342b96d78110dd77cefc051ec75af3295604dde6a"},
+ {file = "numexpr-2.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff5835e8af9a212e8480003d731aad1727aaea909926fd009e8ae6a1cba7f141"},
+ {file = "numexpr-2.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:655d84eb09adfee3c09ecf4a89a512225da153fdb7de13c447404b7d0523a9a7"},
+ {file = "numexpr-2.8.4-cp311-cp311-win32.whl", hash = "sha256:5538b30199bfc68886d2be18fcef3abd11d9271767a7a69ff3688defe782800a"},
+ {file = "numexpr-2.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:3f039321d1c17962c33079987b675fb251b273dbec0f51aac0934e932446ccc3"},
+ {file = "numexpr-2.8.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c867cc36cf815a3ec9122029874e00d8fbcef65035c4a5901e9b120dd5d626a2"},
+ {file = "numexpr-2.8.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:059546e8f6283ccdb47c683101a890844f667fa6d56258d48ae2ecf1b3875957"},
+ {file = "numexpr-2.8.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:845a6aa0ed3e2a53239b89c1ebfa8cf052d3cc6e053c72805e8153300078c0b1"},
+ {file = "numexpr-2.8.4-cp37-cp37m-win32.whl", hash = "sha256:a38664e699526cb1687aefd9069e2b5b9387da7feac4545de446141f1ef86f46"},
+ {file = "numexpr-2.8.4-cp37-cp37m-win_amd64.whl", hash = "sha256:eaec59e9bf70ff05615c34a8b8d6c7bd042bd9f55465d7b495ea5436f45319d0"},
+ {file = "numexpr-2.8.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b318541bf3d8326682ebada087ba0050549a16d8b3fa260dd2585d73a83d20a7"},
+ {file = "numexpr-2.8.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b076db98ca65eeaf9bd224576e3ac84c05e451c0bd85b13664b7e5f7b62e2c70"},
+ {file = "numexpr-2.8.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90f12cc851240f7911a47c91aaf223dba753e98e46dff3017282e633602e76a7"},
+ {file = "numexpr-2.8.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c368aa35ae9b18840e78b05f929d3a7b3abccdba9630a878c7db74ca2368339"},
+ {file = "numexpr-2.8.4-cp38-cp38-win32.whl", hash = "sha256:b96334fc1748e9ec4f93d5fadb1044089d73fb08208fdb8382ed77c893f0be01"},
+ {file = "numexpr-2.8.4-cp38-cp38-win_amd64.whl", hash = "sha256:a6d2d7740ae83ba5f3531e83afc4b626daa71df1ef903970947903345c37bd03"},
+ {file = "numexpr-2.8.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:77898fdf3da6bb96aa8a4759a8231d763a75d848b2f2e5c5279dad0b243c8dfe"},
+ {file = "numexpr-2.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df35324666b693f13a016bc7957de7cc4d8801b746b81060b671bf78a52b9037"},
+ {file = "numexpr-2.8.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17ac9cfe6d0078c5fc06ba1c1bbd20b8783f28c6f475bbabd3cad53683075cab"},
+ {file = "numexpr-2.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df3a1f6b24214a1ab826e9c1c99edf1686c8e307547a9aef33910d586f626d01"},
+ {file = "numexpr-2.8.4-cp39-cp39-win32.whl", hash = "sha256:7d71add384adc9119568d7e9ffa8a35b195decae81e0abf54a2b7779852f0637"},
+ {file = "numexpr-2.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:9f096d707290a6a00b6ffdaf581ee37331109fb7b6c8744e9ded7c779a48e517"},
+ {file = "numexpr-2.8.4.tar.gz", hash = "sha256:d5432537418d18691b9115d615d6daa17ee8275baef3edf1afbbf8bc69806147"},
+]
+
+[package.dependencies]
+numpy = ">=1.13.3"
+
+[[package]]
+name = "numpy"
+version = "1.24.1"
+description = "Fundamental package for array computing in Python"
+category = "main"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "numpy-1.24.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:179a7ef0889ab769cc03573b6217f54c8bd8e16cef80aad369e1e8185f994cd7"},
+ {file = "numpy-1.24.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b09804ff570b907da323b3d762e74432fb07955701b17b08ff1b5ebaa8cfe6a9"},
+ {file = "numpy-1.24.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1b739841821968798947d3afcefd386fa56da0caf97722a5de53e07c4ccedc7"},
+ {file = "numpy-1.24.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e3463e6ac25313462e04aea3fb8a0a30fb906d5d300f58b3bc2c23da6a15398"},
+ {file = "numpy-1.24.1-cp310-cp310-win32.whl", hash = "sha256:b31da69ed0c18be8b77bfce48d234e55d040793cebb25398e2a7d84199fbc7e2"},
+ {file = "numpy-1.24.1-cp310-cp310-win_amd64.whl", hash = "sha256:b07b40f5fb4fa034120a5796288f24c1fe0e0580bbfff99897ba6267af42def2"},
+ {file = "numpy-1.24.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7094891dcf79ccc6bc2a1f30428fa5edb1e6fb955411ffff3401fb4ea93780a8"},
+ {file = "numpy-1.24.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e418681372520c992805bb723e29d69d6b7aa411065f48216d8329d02ba032"},
+ {file = "numpy-1.24.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e274f0f6c7efd0d577744f52032fdd24344f11c5ae668fe8d01aac0422611df1"},
+ {file = "numpy-1.24.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0044f7d944ee882400890f9ae955220d29b33d809a038923d88e4e01d652acd9"},
+ {file = "numpy-1.24.1-cp311-cp311-win32.whl", hash = "sha256:442feb5e5bada8408e8fcd43f3360b78683ff12a4444670a7d9e9824c1817d36"},
+ {file = "numpy-1.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:de92efa737875329b052982e37bd4371d52cabf469f83e7b8be9bb7752d67e51"},
+ {file = "numpy-1.24.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b162ac10ca38850510caf8ea33f89edcb7b0bb0dfa5592d59909419986b72407"},
+ {file = "numpy-1.24.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:26089487086f2648944f17adaa1a97ca6aee57f513ba5f1c0b7ebdabbe2b9954"},
+ {file = "numpy-1.24.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caf65a396c0d1f9809596be2e444e3bd4190d86d5c1ce21f5fc4be60a3bc5b36"},
+ {file = "numpy-1.24.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0677a52f5d896e84414761531947c7a330d1adc07c3a4372262f25d84af7bf7"},
+ {file = "numpy-1.24.1-cp38-cp38-win32.whl", hash = "sha256:dae46bed2cb79a58d6496ff6d8da1e3b95ba09afeca2e277628171ca99b99db1"},
+ {file = "numpy-1.24.1-cp38-cp38-win_amd64.whl", hash = "sha256:6ec0c021cd9fe732e5bab6401adea5a409214ca5592cd92a114f7067febcba0c"},
+ {file = "numpy-1.24.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:28bc9750ae1f75264ee0f10561709b1462d450a4808cd97c013046073ae64ab6"},
+ {file = "numpy-1.24.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:84e789a085aabef2f36c0515f45e459f02f570c4b4c4c108ac1179c34d475ed7"},
+ {file = "numpy-1.24.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e669fbdcdd1e945691079c2cae335f3e3a56554e06bbd45d7609a6cf568c700"},
+ {file = "numpy-1.24.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef85cf1f693c88c1fd229ccd1055570cb41cdf4875873b7728b6301f12cd05bf"},
+ {file = "numpy-1.24.1-cp39-cp39-win32.whl", hash = "sha256:87a118968fba001b248aac90e502c0b13606721b1343cdaddbc6e552e8dfb56f"},
+ {file = "numpy-1.24.1-cp39-cp39-win_amd64.whl", hash = "sha256:ddc7ab52b322eb1e40521eb422c4e0a20716c271a306860979d450decbb51b8e"},
+ {file = "numpy-1.24.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed5fb71d79e771ec930566fae9c02626b939e37271ec285e9efaf1b5d4370e7d"},
+ {file = "numpy-1.24.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad2925567f43643f51255220424c23d204024ed428afc5aad0f86f3ffc080086"},
+ {file = "numpy-1.24.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cfa1161c6ac8f92dea03d625c2d0c05e084668f4a06568b77a25a89111621566"},
+ {file = "numpy-1.24.1.tar.gz", hash = "sha256:2386da9a471cc00a1f47845e27d916d5ec5346ae9696e01a8a34760858fe9dd2"},
+]
+
+[[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 = "main"
+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 = "opencv-python"
+version = "4.7.0.68"
+description = "Wrapper package for OpenCV python bindings."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "opencv-python-4.7.0.68.tar.gz", hash = "sha256:9829e6efedde1d1b8419c5bd4d62d289ecbf44ae35b843c6da9e3cbcba1a9a8a"},
+ {file = "opencv_python-4.7.0.68-cp37-abi3-macosx_10_13_x86_64.whl", hash = "sha256:abc6adfa8694f71a4caffa922b279bd9d96954a37eee40b147f613c64310b411"},
+ {file = "opencv_python-4.7.0.68-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:86f4b60b9536948f16d2170ba3a9b22d3955a957dc61a9bc56e53692c6db2c7e"},
+ {file = "opencv_python-4.7.0.68-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d1c993811f92ddd7919314ada7b9be1f23db1c73f1384915c834dee8549c0b9"},
+ {file = "opencv_python-4.7.0.68-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a00e12546e5578f6bb7ed408c37fcfea533d74e9691cfaf40926f6b43295577"},
+ {file = "opencv_python-4.7.0.68-cp37-abi3-win32.whl", hash = "sha256:e770e9f653a0e5e72b973adb8213fae2df4642730ba1faf31e73a54287a4d5d4"},
+ {file = "opencv_python-4.7.0.68-cp37-abi3-win_amd64.whl", hash = "sha256:7a08f9d1f9dd52de63a7bb448ab7d6d4a1a85b767c2358501d968d1e4d95098d"},
+]
+
+[package.dependencies]
+numpy = [
+ {version = ">=1.21.2", markers = "python_version >= \"3.10\""},
+ {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\""},
+ {version = ">=1.19.3", markers = "python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\" or python_version >= \"3.9\""},
+ {version = ">=1.17.0", markers = "python_version >= \"3.7\""},
+ {version = ">=1.17.3", markers = "python_version >= \"3.8\""},
+]
+
+[[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.2"
+description = "Powerful data structures for data analysis, time series, and statistics"
+category = "main"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pandas-1.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e9dbacd22555c2d47f262ef96bb4e30880e5956169741400af8b306bbb24a273"},
+ {file = "pandas-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e2b83abd292194f350bb04e188f9379d36b8dfac24dd445d5c87575f3beaf789"},
+ {file = "pandas-1.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2552bffc808641c6eb471e55aa6899fa002ac94e4eebfa9ec058649122db5824"},
+ {file = "pandas-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fc87eac0541a7d24648a001d553406f4256e744d92df1df8ebe41829a915028"},
+ {file = "pandas-1.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0d8fd58df5d17ddb8c72a5075d87cd80d71b542571b5f78178fb067fa4e9c72"},
+ {file = "pandas-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:4aed257c7484d01c9a194d9a94758b37d3d751849c05a0050c087a358c41ad1f"},
+ {file = "pandas-1.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:375262829c8c700c3e7cbb336810b94367b9c4889818bbd910d0ecb4e45dc261"},
+ {file = "pandas-1.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc3cd122bea268998b79adebbb8343b735a5511ec14efb70a39e7acbc11ccbdc"},
+ {file = "pandas-1.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4f5a82afa4f1ff482ab8ded2ae8a453a2cdfde2001567b3ca24a4c5c5ca0db3"},
+ {file = "pandas-1.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8092a368d3eb7116e270525329a3e5c15ae796ccdf7ccb17839a73b4f5084a39"},
+ {file = "pandas-1.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6257b314fc14958f8122779e5a1557517b0f8e500cfb2bd53fa1f75a8ad0af2"},
+ {file = "pandas-1.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:82ae615826da838a8e5d4d630eb70c993ab8636f0eff13cb28aafc4291b632b5"},
+ {file = "pandas-1.5.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:457d8c3d42314ff47cc2d6c54f8fc0d23954b47977b2caed09cd9635cb75388b"},
+ {file = "pandas-1.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c009a92e81ce836212ce7aa98b219db7961a8b95999b97af566b8dc8c33e9519"},
+ {file = "pandas-1.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:71f510b0efe1629bf2f7c0eadb1ff0b9cf611e87b73cd017e6b7d6adb40e2b3a"},
+ {file = "pandas-1.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a40dd1e9f22e01e66ed534d6a965eb99546b41d4d52dbdb66565608fde48203f"},
+ {file = "pandas-1.5.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ae7e989f12628f41e804847a8cc2943d362440132919a69429d4dea1f164da0"},
+ {file = "pandas-1.5.2-cp38-cp38-win32.whl", hash = "sha256:530948945e7b6c95e6fa7aa4be2be25764af53fba93fe76d912e35d1c9ee46f5"},
+ {file = "pandas-1.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:73f219fdc1777cf3c45fde7f0708732ec6950dfc598afc50588d0d285fddaefc"},
+ {file = "pandas-1.5.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9608000a5a45f663be6af5c70c3cbe634fa19243e720eb380c0d378666bc7702"},
+ {file = "pandas-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:315e19a3e5c2ab47a67467fc0362cb36c7c60a93b6457f675d7d9615edad2ebe"},
+ {file = "pandas-1.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e18bc3764cbb5e118be139b3b611bc3fbc5d3be42a7e827d1096f46087b395eb"},
+ {file = "pandas-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0183cb04a057cc38fde5244909fca9826d5d57c4a5b7390c0cc3fa7acd9fa883"},
+ {file = "pandas-1.5.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:344021ed3e639e017b452aa8f5f6bf38a8806f5852e217a7594417fb9bbfa00e"},
+ {file = "pandas-1.5.2-cp39-cp39-win32.whl", hash = "sha256:e7469271497960b6a781eaa930cba8af400dd59b62ec9ca2f4d31a19f2f91090"},
+ {file = "pandas-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:c218796d59d5abd8780170c937b812c9637e84c32f8271bbf9845970f8c1351f"},
+ {file = "pandas-1.5.2.tar.gz", hash = "sha256:220b98d15cee0b2cd839a6358bd1f273d0356bf964c1a1aeb32d47db0215488b"},
+]
+
+[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 = "main"
+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 = "parso"
+version = "0.8.3"
+description = "A Python Parser"
+category = "main"
+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 = "pastel"
+version = "0.2.1"
+description = "Bring colors to your terminal."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+ {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"},
+ {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"},
+]
+
+[[package]]
+name = "pathtools"
+version = "0.1.2"
+description = "File system general utilities"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pathtools-0.1.2.tar.gz", hash = "sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0"},
+]
+
+[[package]]
+name = "pexpect"
+version = "4.8.0"
+description = "Pexpect allows easy control of interactive console applications."
+category = "main"
+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 = "main"
+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-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 = "pkginfo"
+version = "1.9.6"
+description = "Query metadata from sdists / bdists / installed packages."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "pkginfo-1.9.6-py3-none-any.whl", hash = "sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546"},
+ {file = "pkginfo-1.9.6.tar.gz", hash = "sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046"},
+]
+
+[package.extras]
+testing = ["pytest", "pytest-cov"]
+
+[[package]]
+name = "platformdirs"
+version = "2.6.2"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"},
+ {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"},
+]
+
+[package.extras]
+docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
+
+[[package]]
+name = "prometheus-client"
+version = "0.15.0"
+description = "Python client for the Prometheus monitoring system."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "prometheus_client-0.15.0-py3-none-any.whl", hash = "sha256:db7c05cbd13a0f79975592d112320f2605a325969b270a94b71dcabc47b931d2"},
+ {file = "prometheus_client-0.15.0.tar.gz", hash = "sha256:be26aa452490cfcf6da953f9436e95a9f2b4d578ca80094b4458930e5f584ab1"},
+]
+
+[package.extras]
+twisted = ["twisted"]
+
+[[package]]
+name = "prompt-toolkit"
+version = "3.0.36"
+description = "Library for building powerful interactive command lines in Python"
+category = "main"
+optional = false
+python-versions = ">=3.6.2"
+files = [
+ {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"},
+ {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"},
+]
+
+[package.dependencies]
+wcwidth = "*"
+
+[[package]]
+name = "protobuf"
+version = "3.20.3"
+description = "Protocol Buffers"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "protobuf-3.20.3-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99"},
+ {file = "protobuf-3.20.3-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e"},
+ {file = "protobuf-3.20.3-cp310-cp310-win32.whl", hash = "sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c"},
+ {file = "protobuf-3.20.3-cp310-cp310-win_amd64.whl", hash = "sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7"},
+ {file = "protobuf-3.20.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469"},
+ {file = "protobuf-3.20.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4"},
+ {file = "protobuf-3.20.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4"},
+ {file = "protobuf-3.20.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454"},
+ {file = "protobuf-3.20.3-cp37-cp37m-win32.whl", hash = "sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905"},
+ {file = "protobuf-3.20.3-cp37-cp37m-win_amd64.whl", hash = "sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c"},
+ {file = "protobuf-3.20.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7"},
+ {file = "protobuf-3.20.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee"},
+ {file = "protobuf-3.20.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050"},
+ {file = "protobuf-3.20.3-cp38-cp38-win32.whl", hash = "sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86"},
+ {file = "protobuf-3.20.3-cp38-cp38-win_amd64.whl", hash = "sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9"},
+ {file = "protobuf-3.20.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b"},
+ {file = "protobuf-3.20.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b"},
+ {file = "protobuf-3.20.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402"},
+ {file = "protobuf-3.20.3-cp39-cp39-win32.whl", hash = "sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480"},
+ {file = "protobuf-3.20.3-cp39-cp39-win_amd64.whl", hash = "sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7"},
+ {file = "protobuf-3.20.3-py2.py3-none-any.whl", hash = "sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db"},
+ {file = "protobuf-3.20.3.tar.gz", hash = "sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2"},
+]
+
+[[package]]
+name = "psutil"
+version = "5.9.4"
+description = "Cross-platform lib for process and system monitoring in Python."
+category = "main"
+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 = "ptyprocess"
+version = "0.7.0"
+description = "Run a subprocess in a pseudo terminal"
+category = "main"
+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 = "pure-eval"
+version = "0.2.2"
+description = "Safely evaluate AST nodes without side effects"
+category = "main"
+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 = "main"
+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 = "main"
+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 = "pybullet"
+version = "3.2.5"
+description = "Official Python Interface for the Bullet Physics SDK specialized for Robotics Simulation and Reinforcement Learning"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pybullet-3.2.5-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4970aec0dd968924f6b1820655a20f80650da2f85ba38b641937c9701a8a2b14"},
+ {file = "pybullet-3.2.5-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b64e4523a11d03729035e0a5baa0ce4d2ca58de8d0a242c0b91e8253781b24c4"},
+ {file = "pybullet-3.2.5-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:49e80fd708a3ffd1d0dac3149e13852bd59cca056bb328bf35b25ea26a8bf504"},
+ {file = "pybullet-3.2.5-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:56456b7b53ab00f33d52a3eb96fb0d7b4b8e16f21987d727b34baecc2019702f"},
+ {file = "pybullet-3.2.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3e22fdb949d0a67e18cc3e248d6199ff788704c68c3edbfc3b5c02fc58f52f9a"},
+ {file = "pybullet-3.2.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3f9c4289f1773b55915f4efb7514b088539d59b4a082465d68ee7caac11355d1"},
+ {file = "pybullet-3.2.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9adcaa00de674a02549949f6f8d51b485bd7a23fbc87a1defb2067e1364f8202"},
+ {file = "pybullet-3.2.5.tar.gz", hash = "sha256:1bcb9afb87a086be1b2de18f084d1fdab8194da1bf71f264743ca26baa39c351"},
+]
+
+[[package]]
+name = "pycparser"
+version = "2.21"
+description = "C parser in Python"
+category = "main"
+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.4"
+description = "Data validation and settings management using python type hints"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pydantic-1.10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854"},
+ {file = "pydantic-1.10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6dc1cc241440ed7ca9ab59d9929075445da6b7c94ced281b3dd4cfe6c8cff817"},
+ {file = "pydantic-1.10.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51bdeb10d2db0f288e71d49c9cefa609bca271720ecd0c58009bd7504a0c464c"},
+ {file = "pydantic-1.10.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78cec42b95dbb500a1f7120bdf95c401f6abb616bbe8785ef09887306792e66e"},
+ {file = "pydantic-1.10.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8775d4ef5e7299a2f4699501077a0defdaac5b6c4321173bcb0f3c496fbadf85"},
+ {file = "pydantic-1.10.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:572066051eeac73d23f95ba9a71349c42a3e05999d0ee1572b7860235b850cc6"},
+ {file = "pydantic-1.10.4-cp310-cp310-win_amd64.whl", hash = "sha256:7feb6a2d401f4d6863050f58325b8d99c1e56f4512d98b11ac64ad1751dc647d"},
+ {file = "pydantic-1.10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39f4a73e5342b25c2959529f07f026ef58147249f9b7431e1ba8414a36761f53"},
+ {file = "pydantic-1.10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:983e720704431a6573d626b00662eb78a07148c9115129f9b4351091ec95ecc3"},
+ {file = "pydantic-1.10.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75d52162fe6b2b55964fbb0af2ee58e99791a3138588c482572bb6087953113a"},
+ {file = "pydantic-1.10.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fdf8d759ef326962b4678d89e275ffc55b7ce59d917d9f72233762061fd04a2d"},
+ {file = "pydantic-1.10.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05a81b006be15655b2a1bae5faa4280cf7c81d0e09fcb49b342ebf826abe5a72"},
+ {file = "pydantic-1.10.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d88c4c0e5c5dfd05092a4b271282ef0588e5f4aaf345778056fc5259ba098857"},
+ {file = "pydantic-1.10.4-cp311-cp311-win_amd64.whl", hash = "sha256:6a05a9db1ef5be0fe63e988f9617ca2551013f55000289c671f71ec16f4985e3"},
+ {file = "pydantic-1.10.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:887ca463c3bc47103c123bc06919c86720e80e1214aab79e9b779cda0ff92a00"},
+ {file = "pydantic-1.10.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdf88ab63c3ee282c76d652fc86518aacb737ff35796023fae56a65ced1a5978"},
+ {file = "pydantic-1.10.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a48f1953c4a1d9bd0b5167ac50da9a79f6072c63c4cef4cf2a3736994903583e"},
+ {file = "pydantic-1.10.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a9f2de23bec87ff306aef658384b02aa7c32389766af3c5dee9ce33e80222dfa"},
+ {file = "pydantic-1.10.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:cd8702c5142afda03dc2b1ee6bc358b62b3735b2cce53fc77b31ca9f728e4bc8"},
+ {file = "pydantic-1.10.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6e7124d6855b2780611d9f5e1e145e86667eaa3bd9459192c8dc1a097f5e9903"},
+ {file = "pydantic-1.10.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b53e1d41e97063d51a02821b80538053ee4608b9a181c1005441f1673c55423"},
+ {file = "pydantic-1.10.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:55b1625899acd33229c4352ce0ae54038529b412bd51c4915349b49ca575258f"},
+ {file = "pydantic-1.10.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:301d626a59edbe5dfb48fcae245896379a450d04baeed50ef40d8199f2733b06"},
+ {file = "pydantic-1.10.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6f9d649892a6f54a39ed56b8dfd5e08b5f3be5f893da430bed76975f3735d15"},
+ {file = "pydantic-1.10.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d7b5a3821225f5c43496c324b0d6875fde910a1c2933d726a743ce328fbb2a8c"},
+ {file = "pydantic-1.10.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f2f7eb6273dd12472d7f218e1fef6f7c7c2f00ac2e1ecde4db8824c457300416"},
+ {file = "pydantic-1.10.4-cp38-cp38-win_amd64.whl", hash = "sha256:4b05697738e7d2040696b0a66d9f0a10bec0efa1883ca75ee9e55baf511909d6"},
+ {file = "pydantic-1.10.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a9a6747cac06c2beb466064dda999a13176b23535e4c496c9d48e6406f92d42d"},
+ {file = "pydantic-1.10.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eb992a1ef739cc7b543576337bebfc62c0e6567434e522e97291b251a41dad7f"},
+ {file = "pydantic-1.10.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:990406d226dea0e8f25f643b370224771878142155b879784ce89f633541a024"},
+ {file = "pydantic-1.10.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e82a6d37a95e0b1b42b82ab340ada3963aea1317fd7f888bb6b9dfbf4fff57c"},
+ {file = "pydantic-1.10.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9193d4f4ee8feca58bc56c8306bcb820f5c7905fd919e0750acdeeeef0615b28"},
+ {file = "pydantic-1.10.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2b3ce5f16deb45c472dde1a0ee05619298c864a20cded09c4edd820e1454129f"},
+ {file = "pydantic-1.10.4-cp39-cp39-win_amd64.whl", hash = "sha256:9cbdc268a62d9a98c56e2452d6c41c0263d64a2009aac69246486f01b4f594c4"},
+ {file = "pydantic-1.10.4-py3-none-any.whl", hash = "sha256:4948f264678c703f3877d1c8877c4e3b2e12e549c57795107f08cf70c6ec7774"},
+ {file = "pydantic-1.10.4.tar.gz", hash = "sha256:b9a3859f24eb4e097502a3be1fb4b2abb79b6103dd9e2e0edb70613a4459a648"},
+]
+
+[package.dependencies]
+typing-extensions = ">=4.2.0"
+
+[package.extras]
+dotenv = ["python-dotenv (>=0.10.4)"]
+email = ["email-validator (>=1.0.3)"]
+
+[[package]]
+name = "pyglet"
+version = "1.5.27"
+description = "Cross-platform windowing and multimedia library"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pyglet-1.5.27-py3-none-any.whl", hash = "sha256:7ce2eb5a299cda92fcc099f533520cdaa2f157a175d6c1442a2ae4d769284d2d"},
+ {file = "pyglet-1.5.27.zip", hash = "sha256:4d00e067451f3b10fd51b69764fddab65444372a2da344ee2b35f0a8e6ebf005"},
+]
+
+[[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 = "pylev"
+version = "1.4.0"
+description = "A pure Python Levenshtein implementation that's not freaking GPL'd."
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pylev-1.4.0-py2.py3-none-any.whl", hash = "sha256:7b2e2aa7b00e05bb3f7650eb506fc89f474f70493271a35c242d9a92188ad3dd"},
+ {file = "pylev-1.4.0.tar.gz", hash = "sha256:9e77e941042ad3a4cc305dcdf2b2dec1aec2fbe3dd9015d2698ad02b173006d1"},
+]
+
+[[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 = "pyrsistent"
+version = "0.19.3"
+description = "Persistent/Functional/Immutable data structures"
+category = "main"
+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.4"
+description = "A python library adding a json log formatter"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "python-json-logger-2.0.4.tar.gz", hash = "sha256:764d762175f99fcc4630bd4853b09632acb60a6224acb27ce08cd70f0b1b81bd"},
+ {file = "python_json_logger-2.0.4-py3-none-any.whl", hash = "sha256:3b03487b14eb9e4f77e4fc2a023358b5394b82fd89cecf5586259baed57d8c6f"},
+]
+
+[[package]]
+name = "pytz"
+version = "2022.7"
+description = "World timezone definitions, modern and historical"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pytz-2022.7-py2.py3-none-any.whl", hash = "sha256:93007def75ae22f7cd991c84e02d434876818661f8df9ad5df9e950ff4e52cfd"},
+ {file = "pytz-2022.7.tar.gz", hash = "sha256:7ccfae7b4b2c067464a6733c6261673fdb8fd1be905460396b97a073e9fa683a"},
+]
+
+[[package]]
+name = "pyvirtualdisplay"
+version = "3.0"
+description = "python wrapper for Xvfb, Xephyr and Xvnc"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "PyVirtualDisplay-3.0-py3-none-any.whl", hash = "sha256:40d4b8dfe4b8de8552e28eb367647f311f88a130bf837fe910e7f180d5477f0e"},
+ {file = "PyVirtualDisplay-3.0.tar.gz", hash = "sha256:09755bc3ceb6eb725fb07eca5425f43f2358d3bf08e00d2a9b792a1aedd16159"},
+]
+
+[[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 = "pywin32-ctypes"
+version = "0.2.0"
+description = ""
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"},
+ {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"},
+]
+
+[[package]]
+name = "pywinpty"
+version = "2.0.10"
+description = "Pseudo terminal support for Windows from Python."
+category = "main"
+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.0"
+description = "Python bindings for 0MQ"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "pyzmq-25.0.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:2d05d904f03ddf1e0d83d97341354dfe52244a619b5a1440a5f47a5b3451e84e"},
+ {file = "pyzmq-25.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a154ef810d44f9d28868be04641f837374a64e7449df98d9208e76c260c7ef1"},
+ {file = "pyzmq-25.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:487305c2a011fdcf3db1f24e8814bb76d23bc4d2f46e145bc80316a59a9aa07d"},
+ {file = "pyzmq-25.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e7b87638ee30ab13230e37ce5331b3e730b1e0dda30120b9eeec3540ed292c8"},
+ {file = "pyzmq-25.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75243e422e85a62f0ab7953dc315452a56b2c6a7e7d1a3c3109ac3cc57ed6b47"},
+ {file = "pyzmq-25.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:31e523d067ce44a04e876bed3ff9ea1ff8d1b6636d16e5fcace9d22f8c564369"},
+ {file = "pyzmq-25.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8539216173135e9e89f6b1cc392e74e6b935b91e8c76106cf50e7a02ab02efe5"},
+ {file = "pyzmq-25.0.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2754fa68da08a854f4816e05160137fa938a2347276471103d31e04bcee5365c"},
+ {file = "pyzmq-25.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4a1bc30f0c18444d51e9b0d0dd39e3a4e7c53ee74190bebef238cd58de577ea9"},
+ {file = "pyzmq-25.0.0-cp310-cp310-win32.whl", hash = "sha256:01d53958c787cfea34091fcb8ef36003dbb7913b8e9f8f62a0715234ebc98b70"},
+ {file = "pyzmq-25.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:58fc3ad5e1cfd2e6d24741fbb1e216b388115d31b0ca6670f894187f280b6ba6"},
+ {file = "pyzmq-25.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:e4bba04ea779a3d7ef25a821bb63fd0939142c88e7813e5bd9c6265a20c523a2"},
+ {file = "pyzmq-25.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:af1fbfb7ad6ac0009ccee33c90a1d303431c7fb594335eb97760988727a37577"},
+ {file = "pyzmq-25.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85456f0d8f3268eecd63dede3b99d5bd8d3b306310c37d4c15141111d22baeaf"},
+ {file = "pyzmq-25.0.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0645b5a2d2a06fd8eb738018490c514907f7488bf9359c6ee9d92f62e844b76f"},
+ {file = "pyzmq-25.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f72ea279b2941a5203e935a4588b9ba8a48aeb9a926d9dfa1986278bd362cb8"},
+ {file = "pyzmq-25.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:4e295f7928a31ae0f657e848c5045ba6d693fe8921205f408ca3804b1b236968"},
+ {file = "pyzmq-25.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ac97e7d647d5519bcef48dd8d3d331f72975afa5c4496c95f6e854686f45e2d9"},
+ {file = "pyzmq-25.0.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:656281d496aaf9ca4fd4cea84e6d893e3361057c4707bd38618f7e811759103c"},
+ {file = "pyzmq-25.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1f6116991568aac48b94d6d8aaed6157d407942ea385335a6ed313692777fb9d"},
+ {file = "pyzmq-25.0.0-cp311-cp311-win32.whl", hash = "sha256:0282bba9aee6e0346aa27d6c69b5f7df72b5a964c91958fc9e0c62dcae5fdcdc"},
+ {file = "pyzmq-25.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:526f884a27e8bba62fe1f4e07c62be2cfe492b6d432a8fdc4210397f8cf15331"},
+ {file = "pyzmq-25.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ccb3e1a863222afdbda42b7ca8ac8569959593d7abd44f5a709177d6fa27d266"},
+ {file = "pyzmq-25.0.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4046d03100aca266e70d54a35694cb35d6654cfbef633e848b3c4a8d64b9d187"},
+ {file = "pyzmq-25.0.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3100dddcada66ec5940ed6391ebf9d003cc3ede3d320748b2737553019f58230"},
+ {file = "pyzmq-25.0.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7877264aa851c19404b1bb9dbe6eed21ea0c13698be1eda3784aab3036d1c861"},
+ {file = "pyzmq-25.0.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5049e75cc99db65754a3da5f079230fb8889230cf09462ec972d884d1704a3ed"},
+ {file = "pyzmq-25.0.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:81f99fb1224d36eb91557afec8cdc2264e856f3464500b55749020ce4c848ef2"},
+ {file = "pyzmq-25.0.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:a1cd4a95f176cdc0ee0a82d49d5830f13ae6015d89decbf834c273bc33eeb3d3"},
+ {file = "pyzmq-25.0.0-cp36-cp36m-win32.whl", hash = "sha256:926236ca003aec70574754f39703528947211a406f5c6c8b3e50eca04a9e87fc"},
+ {file = "pyzmq-25.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:94f0a7289d0f5c80807c37ebb404205e7deb737e8763eb176f4770839ee2a287"},
+ {file = "pyzmq-25.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f3f96d452e9580cb961ece2e5a788e64abaecb1232a80e61deffb28e105ff84a"},
+ {file = "pyzmq-25.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:930e6ad4f2eaac31a3d0c2130619d25db754b267487ebc186c6ad18af2a74018"},
+ {file = "pyzmq-25.0.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e1081d7030a1229c8ff90120346fb7599b54f552e98fcea5170544e7c6725aab"},
+ {file = "pyzmq-25.0.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:531866c491aee5a1e967c286cfa470dffac1e2a203b1afda52d62b58782651e9"},
+ {file = "pyzmq-25.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fc7c1421c5b1c916acf3128bf3cc7ea7f5018b58c69a6866d70c14190e600ce9"},
+ {file = "pyzmq-25.0.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9a2d5e419bd39a1edb6cdd326d831f0120ddb9b1ff397e7d73541bf393294973"},
+ {file = "pyzmq-25.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:183e18742be3621acf8908903f689ec520aee3f08449bfd29f583010ca33022b"},
+ {file = "pyzmq-25.0.0-cp37-cp37m-win32.whl", hash = "sha256:02f5cb60a7da1edd5591a15efa654ffe2303297a41e1b40c3c8942f8f11fc17c"},
+ {file = "pyzmq-25.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:cac602e02341eaaf4edfd3e29bd3fdef672e61d4e6dfe5c1d065172aee00acee"},
+ {file = "pyzmq-25.0.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:e14df47c1265356715d3d66e90282a645ebc077b70b3806cf47efcb7d1d630cb"},
+ {file = "pyzmq-25.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:293a7c2128690f496057f1f1eb6074f8746058d13588389981089ec45d8fdc77"},
+ {file = "pyzmq-25.0.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:731b208bc9412deeb553c9519dca47136b5a01ca66667cafd8733211941b17e4"},
+ {file = "pyzmq-25.0.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b055a1cddf8035966ad13aa51edae5dc8f1bba0b5d5e06f7a843d8b83dc9b66b"},
+ {file = "pyzmq-25.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17e1cb97d573ea84d7cd97188b42ca6f611ab3ee600f6a75041294ede58e3d20"},
+ {file = "pyzmq-25.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:60ecbfe7669d3808ffa8a7dd1487d6eb8a4015b07235e3b723d4b2a2d4de7203"},
+ {file = "pyzmq-25.0.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4c25c95416133942280faaf068d0fddfd642b927fb28aaf4ab201a738e597c1e"},
+ {file = "pyzmq-25.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:be05504af0619d1cffa500af1e0ede69fb683f301003851f5993b5247cc2c576"},
+ {file = "pyzmq-25.0.0-cp38-cp38-win32.whl", hash = "sha256:6bf3842af37af43fa953e96074ebbb5315f6a297198f805d019d788a1021dbc8"},
+ {file = "pyzmq-25.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:b90bb8dfbbd138558f1f284fecfe328f7653616ff9a972433a00711d9475d1a9"},
+ {file = "pyzmq-25.0.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:62b9e80890c0d2408eb42d5d7e1fc62a5ce71be3288684788f74cf3e59ffd6e2"},
+ {file = "pyzmq-25.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:484c2c4ee02c1edc07039f42130bd16e804b1fe81c4f428e0042e03967f40c20"},
+ {file = "pyzmq-25.0.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9ca6db34b26c4d3e9b0728841ec9aa39484eee272caa97972ec8c8e231b20c7e"},
+ {file = "pyzmq-25.0.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:610d2d112acd4e5501fac31010064a6c6efd716ceb968e443cae0059eb7b86de"},
+ {file = "pyzmq-25.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3594c0ff604e685d7e907860b61d0e10e46c74a9ffca168f6e9e50ea934ee440"},
+ {file = "pyzmq-25.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c21a5f4e54a807df5afdef52b6d24ec1580153a6bcf0607f70a6e1d9fa74c5c3"},
+ {file = "pyzmq-25.0.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4725412e27612f0d7d7c2f794d89807ad0227c2fc01dd6146b39ada49c748ef9"},
+ {file = "pyzmq-25.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4d3d604fe0a67afd1aff906e54da557a5203368a99dcc50a70eef374f1d2abef"},
+ {file = "pyzmq-25.0.0-cp39-cp39-win32.whl", hash = "sha256:3670e8c5644768f214a3b598fe46378a4a6f096d5fb82a67dfd3440028460565"},
+ {file = "pyzmq-25.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:e99629a976809fe102ef73e856cf4b2660acd82a412a51e80ba2215e523dfd0a"},
+ {file = "pyzmq-25.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:66509c48f7446b640eeae24b60c9c1461799a27b1b0754e438582e36b5af3315"},
+ {file = "pyzmq-25.0.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9c464cc508177c09a5a6122b67f978f20e2954a21362bf095a0da4647e3e908"},
+ {file = "pyzmq-25.0.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:28bcb2e66224a7ac2843eb632e4109d6b161479e7a2baf24e37210461485b4f1"},
+ {file = "pyzmq-25.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0e7ef9ac807db50b4eb6f534c5dcc22f998f5dae920cc28873d2c1d080a4fc9"},
+ {file = "pyzmq-25.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:5050f5c50b58a6e38ccaf9263a356f74ef1040f5ca4030225d1cb1a858c5b7b6"},
+ {file = "pyzmq-25.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2a73af6504e0d2805e926abf136ebf536735a13c22f709be7113c2ec65b4bec3"},
+ {file = "pyzmq-25.0.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0e8d00228db627ddd1b418c7afd81820b38575f237128c9650365f2dd6ac3443"},
+ {file = "pyzmq-25.0.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5605621f2181f20b71f13f698944deb26a0a71af4aaf435b34dd90146092d530"},
+ {file = "pyzmq-25.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6136bfb0e5a9cf8c60c6ac763eb21f82940a77e6758ea53516c8c7074f4ff948"},
+ {file = "pyzmq-25.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:0a90b2480a26aef7c13cff18703ba8d68e181facb40f78873df79e6d42c1facc"},
+ {file = "pyzmq-25.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00c94fd4c9dd3c95aace0c629a7fa713627a5c80c1819326b642adf6c4b8e2a2"},
+ {file = "pyzmq-25.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20638121b0bdc80777ce0ec8c1f14f1ffec0697a1f88f0b564fa4a23078791c4"},
+ {file = "pyzmq-25.0.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6f75b4b8574f3a8a0d6b4b52606fc75b82cb4391471be48ab0b8677c82f9ed4"},
+ {file = "pyzmq-25.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cbb885f347eba7ab7681c450dee5b14aed9f153eec224ec0c3f299273d9241f"},
+ {file = "pyzmq-25.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c48f257da280b3be6c94e05bd575eddb1373419dbb1a72c3ce64e88f29d1cd6d"},
+ {file = "pyzmq-25.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:866eabf7c1315ef2e93e34230db7cbf672e0d7c626b37c11f7e870c8612c3dcc"},
+ {file = "pyzmq-25.0.0.tar.gz", hash = "sha256:f330a1a2c7f89fd4b0aa4dcb7bf50243bf1c8da9a2f1efc31daf57a2046b31f2"},
+]
+
+[package.dependencies]
+cffi = {version = "*", markers = "implementation_name == \"pypy\""}
+
+[[package]]
+name = "qtconsole"
+version = "5.4.0"
+description = "Jupyter Qt console"
+category = "main"
+optional = false
+python-versions = ">= 3.7"
+files = [
+ {file = "qtconsole-5.4.0-py3-none-any.whl", hash = "sha256:be13560c19bdb3b54ed9741a915aa701a68d424519e8341ac479a91209e694b2"},
+ {file = "qtconsole-5.4.0.tar.gz", hash = "sha256:57748ea2fd26320a0b77adba20131cfbb13818c7c96d83fafcb110ff55f58b35"},
+]
+
+[package.dependencies]
+ipykernel = ">=4.1"
+ipython-genutils = "*"
+jupyter-client = ">=4.1"
+jupyter-core = "*"
+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 = "main"
+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 = "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 = "main"
+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 = "main"
+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 = "main"
+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.0.1"
+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.0.1-py3-none-any.whl", hash = "sha256:41fe1d05f433b0f4724cda8345219213d2bfa472ef56b2f64f415b5b94d51b04"},
+ {file = "rich-13.0.1.tar.gz", hash = "sha256:25f83363f636995627a99f6e4abc52ed0970ebbd544960cc63cbb43aaac3d6f0"},
+]
+
+[package.dependencies]
+commonmark = ">=0.9.0,<0.10.0"
+pygments = ">=2.6.0,<3.0.0"
+
+[package.extras]
+jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
+
+[[package]]
+name = "rsa"
+version = "4.9"
+description = "Pure-Python RSA implementation"
+category = "main"
+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 = "ruamel-yaml"
+version = "0.17.21"
+description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
+category = "main"
+optional = false
+python-versions = ">=3"
+files = [
+ {file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"},
+ {file = "ruamel.yaml-0.17.21.tar.gz", hash = "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af"},
+]
+
+[package.dependencies]
+"ruamel.yaml.clib" = {version = ">=0.2.6", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""}
+
+[package.extras]
+docs = ["ryd"]
+jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"]
+
+[[package]]
+name = "ruamel-yaml-clib"
+version = "0.2.7"
+description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5859983f26d8cd7bb5c287ef452e8aacc86501487634573d260968f753e1d71"},
+ {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:debc87a9516b237d0466a711b18b6ebeb17ba9f391eb7f91c649c5c4ec5006c7"},
+ {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:df5828871e6648db72d1c19b4bd24819b80a755c4541d3409f0f7acd0f335c80"},
+ {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:efa08d63ef03d079dcae1dfe334f6c8847ba8b645d08df286358b1f5293d24ab"},
+ {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win32.whl", hash = "sha256:763d65baa3b952479c4e972669f679fe490eee058d5aa85da483ebae2009d231"},
+ {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:d000f258cf42fec2b1bbf2863c61d7b8918d31ffee905da62dede869254d3b8a"},
+ {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e"},
+ {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_12_6_arm64.whl", hash = "sha256:721bc4ba4525f53f6a611ec0967bdcee61b31df5a56801281027a3a6d1c2daf5"},
+ {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:41d0f1fa4c6830176eef5b276af04c89320ea616655d01327d5ce65e50575c94"},
+ {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4b3a93bb9bc662fc1f99c5c3ea8e623d8b23ad22f861eb6fce9377ac07ad6072"},
+ {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_12_0_arm64.whl", hash = "sha256:a234a20ae07e8469da311e182e70ef6b199d0fbeb6c6cc2901204dd87fb867e8"},
+ {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:15910ef4f3e537eea7fe45f8a5d19997479940d9196f357152a09031c5be59f3"},
+ {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:370445fd795706fd291ab00c9df38a0caed0f17a6fb46b0f607668ecb16ce763"},
+ {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-win32.whl", hash = "sha256:ecdf1a604009bd35c674b9225a8fa609e0282d9b896c03dd441a91e5f53b534e"},
+ {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-win_amd64.whl", hash = "sha256:f34019dced51047d6f70cb9383b2ae2853b7fc4dce65129a5acd49f4f9256646"},
+ {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2aa261c29a5545adfef9296b7e33941f46aa5bbd21164228e833412af4c9c75f"},
+ {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f01da5790e95815eb5a8a138508c01c758e5f5bc0ce4286c4f7028b8dd7ac3d0"},
+ {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:40d030e2329ce5286d6b231b8726959ebbe0404c92f0a578c0e2482182e38282"},
+ {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c3ca1fbba4ae962521e5eb66d72998b51f0f4d0f608d3c0347a48e1af262efa7"},
+ {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-win32.whl", hash = "sha256:7bdb4c06b063f6fd55e472e201317a3bb6cdeeee5d5a38512ea5c01e1acbdd93"},
+ {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:be2a7ad8fd8f7442b24323d24ba0b56c51219513cfa45b9ada3b87b76c374d4b"},
+ {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91a789b4aa0097b78c93e3dc4b40040ba55bef518f84a40d4442f713b4094acb"},
+ {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:99e77daab5d13a48a4054803d052ff40780278240a902b880dd37a51ba01a307"},
+ {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:3243f48ecd450eddadc2d11b5feb08aca941b5cd98c9b1db14b2fd128be8c697"},
+ {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8831a2cedcd0f0927f788c5bdf6567d9dc9cc235646a434986a852af1cb54b4b"},
+ {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-win32.whl", hash = "sha256:3110a99e0f94a4a3470ff67fc20d3f96c25b13d24c6980ff841e82bafe827cac"},
+ {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:92460ce908546ab69770b2e576e4f99fbb4ce6ab4b245345a3869a0a0410488f"},
+ {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5bc0667c1eb8f83a3752b71b9c4ba55ef7c7058ae57022dd9b29065186a113d9"},
+ {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:4a4d8d417868d68b979076a9be6a38c676eca060785abaa6709c7b31593c35d1"},
+ {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bf9a6bc4a0221538b1a7de3ed7bca4c93c02346853f44e1cd764be0023cd3640"},
+ {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a7b301ff08055d73223058b5c46c55638917f04d21577c95e00e0c4d79201a6b"},
+ {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win32.whl", hash = "sha256:d5e51e2901ec2366b79f16c2299a03e74ba4531ddcfacc1416639c557aef0ad8"},
+ {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:184faeaec61dbaa3cace407cffc5819f7b977e75360e8d5ca19461cd851a5fc5"},
+ {file = "ruamel.yaml.clib-0.2.7.tar.gz", hash = "sha256:1f08fd5a2bea9c4180db71678e850b995d2a5f4537be0e94557668cf0f5f9497"},
+]
+
+[[package]]
+name = "scipy"
+version = "1.10.0"
+description = "Fundamental algorithms for scientific computing in Python"
+category = "main"
+optional = false
+python-versions = "<3.12,>=3.8"
+files = [
+ {file = "scipy-1.10.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b901b423c91281a974f6cd1c36f5c6c523e665b5a6d5e80fcb2334e14670eefd"},
+ {file = "scipy-1.10.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:16ba05d3d1b9f2141004f3f36888e05894a525960b07f4c2bfc0456b955a00be"},
+ {file = "scipy-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:151f066fe7d6653c3ffefd489497b8fa66d7316e3e0d0c0f7ff6acca1b802809"},
+ {file = "scipy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f9ea0a37aca111a407cb98aa4e8dfde6e5d9333bae06dfa5d938d14c80bb5c3"},
+ {file = "scipy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:27e548276b5a88b51212b61f6dda49a24acf5d770dff940bd372b3f7ced8c6c2"},
+ {file = "scipy-1.10.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:42ab8b9e7dc1ebe248e55f54eea5307b6ab15011a7883367af48dd781d1312e4"},
+ {file = "scipy-1.10.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e096b062d2efdea57f972d232358cb068413dc54eec4f24158bcbb5cb8bddfd8"},
+ {file = "scipy-1.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4df25a28bd22c990b22129d3c637fd5c3be4b7c94f975dca909d8bab3309b694"},
+ {file = "scipy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ad449db4e0820e4b42baccefc98ec772ad7818dcbc9e28b85aa05a536b0f1a2"},
+ {file = "scipy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:6faf86ef7717891195ae0537e48da7524d30bc3b828b30c9b115d04ea42f076f"},
+ {file = "scipy-1.10.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:4bd0e3278126bc882d10414436e58fa3f1eca0aa88b534fcbf80ed47e854f46c"},
+ {file = "scipy-1.10.0-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:38bfbd18dcc69eeb589811e77fae552fa923067fdfbb2e171c9eac749885f210"},
+ {file = "scipy-1.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ab2a58064836632e2cec31ca197d3695c86b066bc4818052b3f5381bfd2a728"},
+ {file = "scipy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cd7a30970c29d9768a7164f564d1fbf2842bfc77b7d114a99bc32703ce0bf48"},
+ {file = "scipy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:9b878c671655864af59c108c20e4da1e796154bd78c0ed6bb02bc41c84625686"},
+ {file = "scipy-1.10.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:3afcbddb4488ac950ce1147e7580178b333a29cd43524c689b2e3543a080a2c8"},
+ {file = "scipy-1.10.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:6e4497e5142f325a5423ff5fda2fff5b5d953da028637ff7c704378c8c284ea7"},
+ {file = "scipy-1.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:441cab2166607c82e6d7a8683779cb89ba0f475b983c7e4ab88f3668e268c143"},
+ {file = "scipy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0490dc499fe23e4be35b8b6dd1e60a4a34f0c4adb30ac671e6332446b3cbbb5a"},
+ {file = "scipy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:954ff69d2d1bf666b794c1d7216e0a746c9d9289096a64ab3355a17c7c59db54"},
+ {file = "scipy-1.10.0.tar.gz", hash = "sha256:c8b3cbc636a87a89b770c6afc999baa6bcbb01691b5ccbbc1b1791c7c0a07540"},
+]
+
+[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 = "secretstorage"
+version = "3.3.3"
+description = "Python bindings to FreeDesktop.org Secret Service API"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"},
+ {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"},
+]
+
+[package.dependencies]
+cryptography = ">=2.0"
+jeepney = ">=0.6"
+
+[[package]]
+name = "send2trash"
+version = "1.8.0"
+description = "Send file to trash natively under Mac OS X, Windows and Linux."
+category = "main"
+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 = "sentry-sdk"
+version = "1.13.0"
+description = "Python client for Sentry (https://sentry.io)"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "sentry-sdk-1.13.0.tar.gz", hash = "sha256:72da0766c3069a3941eadbdfa0996f83f5a33e55902a19ba399557cfee1dddcc"},
+ {file = "sentry_sdk-1.13.0-py2.py3-none-any.whl", hash = "sha256:b7ff6318183e551145b5c4766eb65b59ad5b63ff234dffddc5fb50340cad6729"},
+]
+
+[package.dependencies]
+certifi = "*"
+urllib3 = {version = ">=1.26.11", markers = "python_version >= \"3.6\""}
+
+[package.extras]
+aiohttp = ["aiohttp (>=3.5)"]
+beam = ["apache-beam (>=2.12)"]
+bottle = ["bottle (>=0.12.13)"]
+celery = ["celery (>=3)"]
+chalice = ["chalice (>=1.16.0)"]
+django = ["django (>=1.8)"]
+falcon = ["falcon (>=1.4)"]
+fastapi = ["fastapi (>=0.79.0)"]
+flask = ["blinker (>=1.1)", "flask (>=0.11)"]
+httpx = ["httpx (>=0.16.0)"]
+opentelemetry = ["opentelemetry-distro (>=0.350b0)"]
+pure-eval = ["asttokens", "executing", "pure-eval"]
+pymongo = ["pymongo (>=3.1)"]
+pyspark = ["pyspark (>=2.4.4)"]
+quart = ["blinker (>=1.1)", "quart (>=0.16.1)"]
+rq = ["rq (>=0.6)"]
+sanic = ["sanic (>=0.8)"]
+sqlalchemy = ["sqlalchemy (>=1.2)"]
+starlette = ["starlette (>=0.19.1)"]
+starlite = ["starlite (>=1.48)"]
+tornado = ["tornado (>=5)"]
+
+[[package]]
+name = "setproctitle"
+version = "1.3.2"
+description = "A Python module to customize the process title"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "setproctitle-1.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:288943dec88e178bb2fd868adf491197cc0fc8b6810416b1c6775e686bab87fe"},
+ {file = "setproctitle-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:630f6fe5e24a619ccf970c78e084319ee8be5be253ecc9b5b216b0f474f5ef18"},
+ {file = "setproctitle-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c877691b90026670e5a70adfbcc735460a9f4c274d35ec5e8a43ce3f8443005"},
+ {file = "setproctitle-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a55fe05f15c10e8c705038777656fe45e3bd676d49ad9ac8370b75c66dd7cd7"},
+ {file = "setproctitle-1.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab45146c71ca6592c9cc8b354a2cc9cc4843c33efcbe1d245d7d37ce9696552d"},
+ {file = "setproctitle-1.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e00c9d5c541a2713ba0e657e0303bf96ddddc412ef4761676adc35df35d7c246"},
+ {file = "setproctitle-1.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:265ecbe2c6eafe82e104f994ddd7c811520acdd0647b73f65c24f51374cf9494"},
+ {file = "setproctitle-1.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c2c46200656280a064073447ebd363937562debef329482fd7e570c8d498f806"},
+ {file = "setproctitle-1.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:fa2f50678f04fda7a75d0fe5dd02bbdd3b13cbe6ed4cf626e4472a7ccf47ae94"},
+ {file = "setproctitle-1.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7f2719a398e1a2c01c2a63bf30377a34d0b6ef61946ab9cf4d550733af8f1ef1"},
+ {file = "setproctitle-1.3.2-cp310-cp310-win32.whl", hash = "sha256:e425be62524dc0c593985da794ee73eb8a17abb10fe692ee43bb39e201d7a099"},
+ {file = "setproctitle-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:e85e50b9c67854f89635a86247412f3ad66b132a4d8534ac017547197c88f27d"},
+ {file = "setproctitle-1.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2a97d51c17d438cf5be284775a322d57b7ca9505bb7e118c28b1824ecaf8aeaa"},
+ {file = "setproctitle-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:587c7d6780109fbd8a627758063d08ab0421377c0853780e5c356873cdf0f077"},
+ {file = "setproctitle-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d17c8bd073cbf8d141993db45145a70b307385b69171d6b54bcf23e5d644de"},
+ {file = "setproctitle-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e932089c35a396dc31a5a1fc49889dd559548d14cb2237adae260382a090382e"},
+ {file = "setproctitle-1.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e4f8f12258a8739c565292a551c3db62cca4ed4f6b6126664e2381acb4931bf"},
+ {file = "setproctitle-1.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:570d255fd99c7f14d8f91363c3ea96bd54f8742275796bca67e1414aeca7d8c3"},
+ {file = "setproctitle-1.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a8e0881568c5e6beff91ef73c0ec8ac2a9d3ecc9edd6bd83c31ca34f770910c4"},
+ {file = "setproctitle-1.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4bba3be4c1fabf170595b71f3af46c6d482fbe7d9e0563999b49999a31876f77"},
+ {file = "setproctitle-1.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:37ece938110cab2bb3957e3910af8152ca15f2b6efdf4f2612e3f6b7e5459b80"},
+ {file = "setproctitle-1.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db684d6bbb735a80bcbc3737856385b55d53f8a44ce9b46e9a5682c5133a9bf7"},
+ {file = "setproctitle-1.3.2-cp311-cp311-win32.whl", hash = "sha256:ca58cd260ea02759238d994cfae844fc8b1e206c684beb8f38877dcab8451dfc"},
+ {file = "setproctitle-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:88486e6cce2a18a033013d17b30a594f1c5cb42520c49c19e6ade40b864bb7ff"},
+ {file = "setproctitle-1.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:92c626edc66169a1b09e9541b9c0c9f10488447d8a2b1d87c8f0672e771bc927"},
+ {file = "setproctitle-1.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:710e16fa3bade3b026907e4a5e841124983620046166f355bbb84be364bf2a02"},
+ {file = "setproctitle-1.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f29b75e86260b0ab59adb12661ef9f113d2f93a59951373eb6d68a852b13e83"},
+ {file = "setproctitle-1.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c8d9650154afaa86a44ff195b7b10d683c73509d085339d174e394a22cccbb9"},
+ {file = "setproctitle-1.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0452282258dfcc01697026a8841258dd2057c4438b43914b611bccbcd048f10"},
+ {file = "setproctitle-1.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:e49ae693306d7624015f31cb3e82708916759d592c2e5f72a35c8f4cc8aef258"},
+ {file = "setproctitle-1.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1ff863a20d1ff6ba2c24e22436a3daa3cd80be1dfb26891aae73f61b54b04aca"},
+ {file = "setproctitle-1.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:55ce1e9925ce1765865442ede9dca0ba9bde10593fcd570b1f0fa25d3ec6b31c"},
+ {file = "setproctitle-1.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7fe9df7aeb8c64db6c34fc3b13271a363475d77bc157d3f00275a53910cb1989"},
+ {file = "setproctitle-1.3.2-cp37-cp37m-win32.whl", hash = "sha256:e5c50e164cd2459bc5137c15288a9ef57160fd5cbf293265ea3c45efe7870865"},
+ {file = "setproctitle-1.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a499fff50387c1520c085a07578a000123f519e5f3eee61dd68e1d301659651f"},
+ {file = "setproctitle-1.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5b932c3041aa924163f4aab970c2f0e6b4d9d773f4d50326e0ea1cd69240e5c5"},
+ {file = "setproctitle-1.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f4bfc89bd33ebb8e4c0e9846a09b1f5a4a86f5cb7a317e75cc42fee1131b4f4f"},
+ {file = "setproctitle-1.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcd3cf4286a60fdc95451d8d14e0389a6b4f5cebe02c7f2609325eb016535963"},
+ {file = "setproctitle-1.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fb4f769c02f63fac90989711a3fee83919f47ae9afd4758ced5d86596318c65"},
+ {file = "setproctitle-1.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5194b4969f82ea842a4f6af2f82cd16ebdc3f1771fb2771796e6add9835c1973"},
+ {file = "setproctitle-1.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f0cde41857a644b7353a0060b5f94f7ba7cf593ebde5a1094da1be581ac9a31"},
+ {file = "setproctitle-1.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9124bedd8006b0e04d4e8a71a0945da9b67e7a4ab88fdad7b1440dc5b6122c42"},
+ {file = "setproctitle-1.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c8a09d570b39517de10ee5b718730e171251ce63bbb890c430c725c8c53d4484"},
+ {file = "setproctitle-1.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:8ff3c8cb26afaed25e8bca7b9dd0c1e36de71f35a3a0706b5c0d5172587a3827"},
+ {file = "setproctitle-1.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:589be87172b238f839e19f146b9ea47c71e413e951ef0dc6db4218ddacf3c202"},
+ {file = "setproctitle-1.3.2-cp38-cp38-win32.whl", hash = "sha256:4749a2b0c9ac52f864d13cee94546606f92b981b50e46226f7f830a56a9dc8e1"},
+ {file = "setproctitle-1.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:e43f315c68aa61cbdef522a2272c5a5b9b8fd03c301d3167b5e1343ef50c676c"},
+ {file = "setproctitle-1.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:de3a540cd1817ede31f530d20e6a4935bbc1b145fd8f8cf393903b1e02f1ae76"},
+ {file = "setproctitle-1.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4058564195b975ddc3f0462375c533cce310ccdd41b80ac9aed641c296c3eff4"},
+ {file = "setproctitle-1.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c5d5dad7c28bdd1ec4187d818e43796f58a845aa892bb4481587010dc4d362b"},
+ {file = "setproctitle-1.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffc61a388a5834a97953d6444a2888c24a05f2e333f9ed49f977a87bb1ad4761"},
+ {file = "setproctitle-1.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fa1a0fbee72b47dc339c87c890d3c03a72ea65c061ade3204f285582f2da30f"},
+ {file = "setproctitle-1.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe8a988c7220c002c45347430993830666e55bc350179d91fcee0feafe64e1d4"},
+ {file = "setproctitle-1.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bae283e85fc084b18ffeb92e061ff7ac5af9e183c9d1345c93e178c3e5069cbe"},
+ {file = "setproctitle-1.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:fed18e44711c5af4b681c2b3b18f85e6f0f1b2370a28854c645d636d5305ccd8"},
+ {file = "setproctitle-1.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:b34baef93bfb20a8ecb930e395ccd2ae3268050d8cf4fe187de5e2bd806fd796"},
+ {file = "setproctitle-1.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7f0bed90a216ef28b9d227d8d73e28a8c9b88c0f48a082d13ab3fa83c581488f"},
+ {file = "setproctitle-1.3.2-cp39-cp39-win32.whl", hash = "sha256:4d8938249a7cea45ab7e1e48b77685d0f2bab1ebfa9dde23e94ab97968996a7c"},
+ {file = "setproctitle-1.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:a47d97a75fd2d10c37410b180f67a5835cb1d8fdea2648fd7f359d4277f180b9"},
+ {file = "setproctitle-1.3.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:dad42e676c5261eb50fdb16bdf3e2771cf8f99a79ef69ba88729aeb3472d8575"},
+ {file = "setproctitle-1.3.2-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c91b9bc8985d00239f7dc08a49927a7ca1ca8a6af2c3890feec3ed9665b6f91e"},
+ {file = "setproctitle-1.3.2-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8579a43eafd246e285eb3a5b939e7158073d5087aacdd2308f23200eac2458b"},
+ {file = "setproctitle-1.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:2fbd8187948284293f43533c150cd69a0e4192c83c377da837dbcd29f6b83084"},
+ {file = "setproctitle-1.3.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:faec934cfe5fd6ac1151c02e67156c3f526e82f96b24d550b5d51efa4a5527c6"},
+ {file = "setproctitle-1.3.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1aafc91cbdacc9e5fe712c52077369168e6b6c346f3a9d51bf600b53eae56bb"},
+ {file = "setproctitle-1.3.2-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b617f12c9be61e8f4b2857be4a4319754756845dbbbd9c3718f468bbb1e17bcb"},
+ {file = "setproctitle-1.3.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b2c9cb2705fc84cb8798f1ba74194f4c080aaef19d9dae843591c09b97678e98"},
+ {file = "setproctitle-1.3.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a149a5f7f2c5a065d4e63cb0d7a4b6d3b66e6e80f12e3f8827c4f63974cbf122"},
+ {file = "setproctitle-1.3.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e3ac25bfc4a0f29d2409650c7532d5ddfdbf29f16f8a256fc31c47d0dc05172"},
+ {file = "setproctitle-1.3.2-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65d884e22037b23fa25b2baf1a3316602ed5c5971eb3e9d771a38c3a69ce6e13"},
+ {file = "setproctitle-1.3.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7aa0aac1711fadffc1d51e9d00a3bea61f68443d6ac0241a224e4d622489d665"},
+ {file = "setproctitle-1.3.2.tar.gz", hash = "sha256:b9fb97907c830d260fa0658ed58afd48a86b2b88aac521135c352ff7fd3477fd"},
+]
+
+[package.extras]
+test = ["pytest"]
+
+[[package]]
+name = "setuptools"
+version = "65.7.0"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "setuptools-65.7.0-py3-none-any.whl", hash = "sha256:8ab4f1dbf2b4a65f7eec5ad0c620e84c34111a68d3349833494b9088212214dd"},
+ {file = "setuptools-65.7.0.tar.gz", hash = "sha256:4d3c92fac8f1118bb77a22181355e29c239cabfe2b9effdaa665c66b711136d7"},
+]
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+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 = "smmap"
+version = "5.0.0"
+description = "A pure Python implementation of a sliding window memory map manager"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"},
+ {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"},
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.0"
+description = "Sniff out which async library your code is running under"
+category = "main"
+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 = "soupsieve"
+version = "2.3.2.post1"
+description = "A modern CSS selector implementation for Beautiful Soup."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"},
+ {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"},
+]
+
+[[package]]
+name = "stable-baselines3"
+version = "1.7.0"
+description = "Pytorch version of Stable Baselines, implementations of reinforcement learning algorithms."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "stable_baselines3-1.7.0-py3-none-any.whl", hash = "sha256:6cbb55918f15662e958a7088dd5effd230443b545c6f6a6248a3532c962dc1b0"},
+ {file = "stable_baselines3-1.7.0.tar.gz", hash = "sha256:0b6d4b35b5b9060cf94bc09eb631a9a619d9a0d1eda06e690b0bf6336651f0a4"},
+]
+
+[package.dependencies]
+ale-py = {version = "0.7.4", optional = true, markers = "extra == \"extra\""}
+autorom = {version = ">=0.4.2,<0.5.0", extras = ["accept-rom-license"], optional = true, markers = "extra == \"extra\""}
+cloudpickle = "*"
+gym = "0.21"
+importlib-metadata = ">=4.13,<5.0"
+matplotlib = "*"
+numpy = "*"
+opencv-python = {version = "*", optional = true, markers = "extra == \"extra\""}
+pandas = "*"
+pillow = {version = "*", optional = true, markers = "extra == \"extra\""}
+psutil = {version = "*", optional = true, markers = "extra == \"extra\""}
+rich = {version = "*", optional = true, markers = "extra == \"extra\""}
+tensorboard = {version = ">=2.9.1", optional = true, markers = "extra == \"extra\""}
+torch = ">=1.11"
+tqdm = {version = "*", optional = true, markers = "extra == \"extra\""}
+
+[package.extras]
+docs = ["sphinx", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx-copybutton", "sphinx-rtd-theme", "sphinxcontrib.spelling"]
+extra = ["ale-py (==0.7.4)", "autorom[accept-rom-license] (>=0.4.2,<0.5.0)", "opencv-python", "pillow", "psutil", "rich", "tensorboard (>=2.9.1)", "tqdm"]
+tests = ["black", "flake8 (>=3.8)", "flake8-bugbear", "isort (>=5.0)", "mypy", "pytest", "pytest-cov", "pytest-env", "pytest-xdist", "pytype", "scipy (>=1.4.1)"]
+
+[[package]]
+name = "stack-data"
+version = "0.6.2"
+description = "Extract data from python stack frames and tracebacks for informative displays"
+category = "main"
+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 = "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 = "tensorboard"
+version = "2.11.0"
+description = "TensorBoard lets you watch Tensors Flow"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tensorboard-2.11.0-py3-none-any.whl", hash = "sha256:a0e592ee87962e17af3f0dce7faae3fbbd239030159e9e625cce810b7e35c53d"},
+]
+
+[package.dependencies]
+absl-py = ">=0.4"
+google-auth = ">=1.6.3,<3"
+google-auth-oauthlib = ">=0.4.1,<0.5"
+grpcio = ">=1.24.3"
+markdown = ">=2.6.8"
+numpy = ">=1.12.0"
+protobuf = ">=3.9.2,<4"
+requests = ">=2.21.0,<3"
+setuptools = ">=41.0.0"
+tensorboard-data-server = ">=0.6.0,<0.7.0"
+tensorboard-plugin-wit = ">=1.6.0"
+werkzeug = ">=1.0.1"
+wheel = ">=0.26"
+
+[[package]]
+name = "tensorboard-data-server"
+version = "0.6.1"
+description = "Fast data loading for TensorBoard"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "tensorboard_data_server-0.6.1-py3-none-any.whl", hash = "sha256:809fe9887682d35c1f7d1f54f0f40f98bb1f771b14265b453ca051e2ce58fca7"},
+ {file = "tensorboard_data_server-0.6.1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:fa8cef9be4fcae2f2363c88176638baf2da19c5ec90addb49b1cde05c95c88ee"},
+ {file = "tensorboard_data_server-0.6.1-py3-none-manylinux2010_x86_64.whl", hash = "sha256:d8237580755e58eff68d1f3abefb5b1e39ae5c8b127cc40920f9c4fb33f4b98a"},
+]
+
+[[package]]
+name = "tensorboard-plugin-wit"
+version = "1.8.1"
+description = "What-If Tool TensorBoard plugin."
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "tensorboard_plugin_wit-1.8.1-py3-none-any.whl", hash = "sha256:ff26bdd583d155aa951ee3b152b3d0cffae8005dc697f72b44a8e8c2a77a8cbe"},
+]
+
+[[package]]
+name = "terminado"
+version = "0.17.1"
+description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library."
+category = "main"
+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 = "tinycss2"
+version = "1.2.1"
+description = "A tiny CSS parser"
+category = "main"
+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 = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+category = "main"
+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 = "main"
+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 = "toolz"
+version = "0.12.0"
+description = "List processing tools and functional utilities"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "toolz-0.12.0-py3-none-any.whl", hash = "sha256:2059bd4148deb1884bb0eb770a3cde70e7f954cfbbdc2285f1f2de01fd21eb6f"},
+ {file = "toolz-0.12.0.tar.gz", hash = "sha256:88c570861c440ee3f2f6037c4654613228ff40c93a6c25e0eba70d17282c6194"},
+]
+
+[[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 = "torch-tb-profiler"
+version = "0.4.1"
+description = "PyTorch Profiler TensorBoard Plugin"
+category = "main"
+optional = false
+python-versions = ">=3.6.2"
+files = [
+ {file = "torch_tb_profiler-0.4.1-py3-none-any.whl", hash = "sha256:df7428ce5564e8357d0d03c0f246398c448fc8cd91b3075370ca5c25defbc635"},
+ {file = "torch_tb_profiler-0.4.1.tar.gz", hash = "sha256:f2c7fb27d420be443ffde50ada655c19f76a245d21e7772de753196fd0967685"},
+]
+
+[package.dependencies]
+pandas = ">=1.0.0"
+tensorboard = ">=1.15,<2.1.0 || >2.1.0"
+
+[package.extras]
+blob = ["azure-storage-blob"]
+gs = ["google-cloud-storage"]
+s3 = ["boto3"]
+
+[[package]]
+name = "tornado"
+version = "6.2"
+description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
+category = "main"
+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.64.1"
+description = "Fast, Extensible Progress Meter"
+category = "main"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+files = [
+ {file = "tqdm-4.64.1-py2.py3-none-any.whl", hash = "sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1"},
+ {file = "tqdm-4.64.1.tar.gz", hash = "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[package.extras]
+dev = ["py-make (>=0.1.0)", "twine", "wheel"]
+notebook = ["ipywidgets (>=6)"]
+slack = ["slack-sdk"]
+telegram = ["requests"]
+
+[[package]]
+name = "traitlets"
+version = "5.8.1"
+description = "Traitlets Python configuration system"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "traitlets-5.8.1-py3-none-any.whl", hash = "sha256:a1ca5df6414f8b5760f7c5f256e326ee21b581742114545b462b35ffe3f04861"},
+ {file = "traitlets-5.8.1.tar.gz", hash = "sha256:32500888f5ff7bbf3b9267ea31748fa657aaf34d56d85e60f91dda7dc7f5785b"},
+]
+
+[package.extras]
+docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
+test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"]
+
+[[package]]
+name = "typing-extensions"
+version = "4.4.0"
+description = "Backported and Experimental Type Hints for Python 3.7+"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
+ {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
+]
+
+[[package]]
+name = "uri-template"
+version = "1.2.0"
+description = "RFC 6570 URI Template Processor"
+category = "main"
+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.14"
+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.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"},
+ {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"},
+]
+
+[package.extras]
+brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
+secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
+socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+
+[[package]]
+name = "virtualenv"
+version = "20.17.1"
+description = "Virtual Python Environment builder"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "virtualenv-20.17.1-py3-none-any.whl", hash = "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4"},
+ {file = "virtualenv-20.17.1.tar.gz", hash = "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"},
+]
+
+[package.dependencies]
+distlib = ">=0.3.6,<1"
+filelock = ">=3.4.1,<4"
+platformdirs = ">=2.4,<3"
+
+[package.extras]
+docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"]
+testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"]
+
+[[package]]
+name = "wandb"
+version = "0.13.10"
+description = "A CLI and library for interacting with the Weights and Biases API."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "wandb-0.13.10-py3-none-any.whl", hash = "sha256:d4a3397aed2790d5fa8adc5336e3607813dc498a49b1a0a48297f78d89a71b0b"},
+ {file = "wandb-0.13.10.tar.gz", hash = "sha256:a2cde45dd6390ed1923ef3e1eca26487cb1e02e4732515856933c4494a975ad3"},
+]
+
+[package.dependencies]
+appdirs = ">=1.4.3"
+Click = ">=7.0,<8.0.0 || >8.0.0"
+docker-pycreds = ">=0.4.0"
+GitPython = ">=1.0.0"
+pathtools = "*"
+protobuf = {version = ">=3.19.0,<4.21.0 || >4.21.0,<5", markers = "python_version > \"3.9\" or sys_platform != \"linux\""}
+psutil = ">=5.0.0"
+PyYAML = "*"
+requests = ">=2.0.0,<3"
+sentry-sdk = ">=1.0.0"
+setproctitle = "*"
+setuptools = "*"
+
+[package.extras]
+aws = ["boto3"]
+azure = ["azure-storage-blob"]
+gcp = ["google-cloud-storage"]
+grpc = ["grpcio (>=1.27.2)"]
+kubeflow = ["google-cloud-storage", "kubernetes", "minio", "sh"]
+launch = ["boto3", "botocore", "chardet", "google-cloud-storage", "iso8601", "kubernetes", "nbconvert", "nbformat", "typing-extensions"]
+media = ["bokeh", "moviepy", "numpy", "pillow", "plotly", "rdkit-pypi", "soundfile"]
+models = ["cloudpickle"]
+sweeps = ["sweeps (>=0.2.0)"]
+
+[[package]]
+name = "wcwidth"
+version = "0.2.6"
+description = "Measures the displayed width of unicode strings in a terminal"
+category = "main"
+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 = "main"
+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 = "main"
+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.4.2"
+description = "WebSocket client for Python with low level API options"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "websocket-client-1.4.2.tar.gz", hash = "sha256:d6e8f90ca8e2dd4e8027c4561adeb9456b54044312dba655e7cae652ceb9ae59"},
+ {file = "websocket_client-1.4.2-py3-none-any.whl", hash = "sha256:d6b06432f184438d99ac1f456eaf22fe1ade524c3dd16e661142dc54e9cba574"},
+]
+
+[package.extras]
+docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"]
+optional = ["python-socks", "wsaccel"]
+test = ["websockets"]
+
+[[package]]
+name = "werkzeug"
+version = "2.2.2"
+description = "The comprehensive WSGI web application library."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "Werkzeug-2.2.2-py3-none-any.whl", hash = "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"},
+ {file = "Werkzeug-2.2.2.tar.gz", hash = "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.1.1"
+
+[package.extras]
+watchdog = ["watchdog"]
+
+[[package]]
+name = "wheel"
+version = "0.38.4"
+description = "A built-package format for Python"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "wheel-0.38.4-py3-none-any.whl", hash = "sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8"},
+ {file = "wheel-0.38.4.tar.gz", hash = "sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac"},
+]
+
+[package.extras]
+test = ["pytest (>=3.0.0)"]
+
+[[package]]
+name = "widgetsnbextension"
+version = "4.0.5"
+description = "Jupyter interactive widgets for Jupyter Notebook"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "widgetsnbextension-4.0.5-py3-none-any.whl", hash = "sha256:eaaaf434fb9b08bd197b2a14ffe45ddb5ac3897593d43c69287091e5f3147bf7"},
+ {file = "widgetsnbextension-4.0.5.tar.gz", hash = "sha256:003f716d930d385be3fd9de42dd9bf008e30053f73bddde235d14fbeaeff19af"},
+]
+
+[[package]]
+name = "zipp"
+version = "3.11.0"
+description = "Backport of pathlib-compatible object wrapper for zip files"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"},
+ {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"},
+]
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
+testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
+
+[metadata]
+lock-version = "2.0"
+python-versions = "~3.10"
+content-hash = "ab3cf6f3768a3f2d608f049b0ee69c9224dd1530f0bd392a4116f23597aaf34a"
diff --git a/ppo/ppo.py b/ppo/ppo.py
new file mode 100644
index 0000000000000000000000000000000000000000..0daae26ebb77c009cf9c9af51b802fe1efcfdd10
--- /dev/null
+++ b/ppo/ppo.py
@@ -0,0 +1,349 @@
+import numpy as np
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+
+from dataclasses import asdict, dataclass, field
+from time import perf_counter
+from torch.optim import Adam
+from torch.utils.tensorboard.writer import SummaryWriter
+from typing import List, Optional, NamedTuple, TypeVar
+
+from shared.algorithm import Algorithm
+from shared.callbacks.callback import Callback
+from shared.gae import compute_advantage, compute_rtg_and_advantage
+from shared.policy.on_policy import ActorCritic
+from shared.schedule import constant_schedule, linear_schedule, update_learning_rate
+from shared.trajectory import Trajectory, TrajectoryAccumulator
+from wrappers.vectorable_wrapper import VecEnv, VecEnvObs
+
+
+@dataclass
+class PPOTrajectory(Trajectory):
+ logp_a: List[float] = field(default_factory=list)
+
+ def add(
+ self,
+ obs: np.ndarray,
+ act: np.ndarray,
+ next_obs: np.ndarray,
+ rew: float,
+ terminated: bool,
+ v: float,
+ logp_a: float,
+ ):
+ super().add(obs, act, next_obs, rew, terminated, v)
+ self.logp_a.append(logp_a)
+
+
+class PPOTrajectoryAccumulator(TrajectoryAccumulator):
+ def __init__(self, num_envs: int) -> None:
+ super().__init__(num_envs, PPOTrajectory)
+
+ def step(
+ self,
+ obs: VecEnvObs,
+ action: np.ndarray,
+ next_obs: VecEnvObs,
+ reward: np.ndarray,
+ done: np.ndarray,
+ val: np.ndarray,
+ logp_a: np.ndarray,
+ ) -> None:
+ super().step(obs, action, next_obs, reward, done, val, logp_a)
+
+
+class TrainStepStats(NamedTuple):
+ loss: float
+ pi_loss: float
+ v_loss: float
+ entropy_loss: float
+ approx_kl: float
+ clipped_frac: float
+ val_clipped_frac: float
+
+
+@dataclass
+class TrainStats:
+ loss: float
+ pi_loss: float
+ v_loss: float
+ entropy_loss: float
+ approx_kl: float
+ clipped_frac: float
+ val_clipped_frac: float
+ explained_var: float
+
+ def __init__(self, step_stats: List[TrainStepStats], explained_var: float) -> None:
+ self.loss = np.mean([s.loss for s in step_stats]).item()
+ self.pi_loss = np.mean([s.pi_loss for s in step_stats]).item()
+ self.v_loss = np.mean([s.v_loss for s in step_stats]).item()
+ self.entropy_loss = np.mean([s.entropy_loss for s in step_stats]).item()
+ self.approx_kl = np.mean([s.approx_kl for s in step_stats]).item()
+ self.clipped_frac = np.mean([s.clipped_frac for s in step_stats]).item()
+ self.val_clipped_frac = np.mean([s.val_clipped_frac for s in step_stats]).item()
+ self.explained_var = explained_var
+
+ def write_to_tensorboard(self, tb_writer: SummaryWriter, global_step: int) -> None:
+ for name, value in asdict(self).items():
+ tb_writer.add_scalar(f"losses/{name}", value, global_step=global_step)
+
+ def __repr__(self) -> str:
+ return " | ".join(
+ [
+ f"Loss: {round(self.loss, 2)}",
+ f"Pi L: {round(self.pi_loss, 2)}",
+ f"V L: {round(self.v_loss, 2)}",
+ f"E L: {round(self.entropy_loss, 2)}",
+ f"Apx KL Div: {round(self.approx_kl, 2)}",
+ f"Clip Frac: {round(self.clipped_frac, 2)}",
+ f"Val Clip Frac: {round(self.val_clipped_frac, 2)}",
+ ]
+ )
+
+
+PPOSelf = TypeVar("PPOSelf", bound="PPO")
+
+
+class PPO(Algorithm):
+ def __init__(
+ self,
+ policy: ActorCritic,
+ env: VecEnv,
+ device: torch.device,
+ tb_writer: SummaryWriter,
+ learning_rate: float = 3e-4,
+ learning_rate_decay: str = "none",
+ n_steps: int = 2048,
+ batch_size: int = 64,
+ n_epochs: int = 10,
+ gamma: float = 0.99,
+ gae_lambda: float = 0.95,
+ clip_range: float = 0.2,
+ clip_range_decay: str = "none",
+ clip_range_vf: Optional[float] = None,
+ clip_range_vf_decay: str = "none",
+ normalize_advantage: bool = True,
+ ent_coef: float = 0.0,
+ ent_coef_decay: str = "none",
+ vf_coef: float = 0.5,
+ ppo2_vf_coef_halving: bool = False,
+ max_grad_norm: float = 0.5,
+ update_rtg_between_epochs: bool = False,
+ sde_sample_freq: int = -1,
+ ) -> None:
+ super().__init__(policy, env, device, tb_writer)
+ self.policy = policy
+
+ self.gamma = gamma
+ self.gae_lambda = gae_lambda
+ self.optimizer = Adam(self.policy.parameters(), lr=learning_rate, eps=1e-7)
+ self.lr_schedule = (
+ linear_schedule(learning_rate, 0)
+ if learning_rate_decay == "linear"
+ else constant_schedule(learning_rate)
+ )
+ self.max_grad_norm = max_grad_norm
+ self.clip_range_schedule = (
+ linear_schedule(clip_range, 0)
+ if clip_range_decay == "linear"
+ else constant_schedule(clip_range)
+ )
+ self.clip_range_vf_schedule = None
+ if clip_range_vf:
+ self.clip_range_vf_schedule = (
+ linear_schedule(clip_range_vf, 0)
+ if clip_range_vf_decay == "linear"
+ else constant_schedule(clip_range_vf)
+ )
+ self.normalize_advantage = normalize_advantage
+ self.ent_coef_schedule = (
+ linear_schedule(ent_coef, 0)
+ if ent_coef_decay == "linear"
+ else constant_schedule(ent_coef)
+ )
+ self.vf_coef = vf_coef
+ self.ppo2_vf_coef_halving = ppo2_vf_coef_halving
+
+ self.n_steps = n_steps
+ self.batch_size = batch_size
+ self.n_epochs = n_epochs
+ self.sde_sample_freq = sde_sample_freq
+
+ self.update_rtg_between_epochs = update_rtg_between_epochs
+
+ def learn(
+ self: PPOSelf,
+ total_timesteps: int,
+ callback: Optional[Callback] = None,
+ ) -> PPOSelf:
+ obs = self.env.reset()
+ ts_elapsed = 0
+ while ts_elapsed < total_timesteps:
+ start_time = perf_counter()
+ accumulator = self._collect_trajectories(obs)
+ rollout_steps = self.n_steps * self.env.num_envs
+ ts_elapsed += rollout_steps
+ progress = ts_elapsed / total_timesteps
+ train_stats = self.train(accumulator.all_trajectories, progress, ts_elapsed)
+ train_stats.write_to_tensorboard(self.tb_writer, ts_elapsed)
+ end_time = perf_counter()
+ self.tb_writer.add_scalar(
+ "train/steps_per_second",
+ rollout_steps / (end_time - start_time),
+ ts_elapsed,
+ )
+ if callback:
+ callback.on_step(timesteps_elapsed=rollout_steps)
+
+ return self
+
+ def _collect_trajectories(self, obs: VecEnvObs) -> PPOTrajectoryAccumulator:
+ self.policy.eval()
+ accumulator = PPOTrajectoryAccumulator(self.env.num_envs)
+ self.policy.reset_noise()
+ for i in range(self.n_steps):
+ if self.sde_sample_freq > 0 and i > 0 and i % self.sde_sample_freq == 0:
+ self.policy.reset_noise()
+ action, value, logp_a, clamped_action = self.policy.step(obs)
+ next_obs, reward, done, _ = self.env.step(clamped_action)
+ accumulator.step(obs, action, next_obs, reward, done, value, logp_a)
+ obs = next_obs
+ return accumulator
+
+ def train(
+ self, trajectories: List[PPOTrajectory], progress: float, timesteps_elapsed: int
+ ) -> TrainStats:
+ self.policy.train()
+ learning_rate = self.lr_schedule(progress)
+ update_learning_rate(self.optimizer, learning_rate)
+ self.tb_writer.add_scalar(
+ "charts/learning_rate",
+ self.optimizer.param_groups[0]["lr"],
+ timesteps_elapsed,
+ )
+
+ pi_clip = self.clip_range_schedule(progress)
+ self.tb_writer.add_scalar("charts/pi_clip", pi_clip, timesteps_elapsed)
+ if self.clip_range_vf_schedule:
+ v_clip = self.clip_range_vf_schedule(progress)
+ self.tb_writer.add_scalar("charts/v_clip", v_clip, timesteps_elapsed)
+ else:
+ v_clip = None
+ ent_coef = self.ent_coef_schedule(progress)
+ self.tb_writer.add_scalar("charts/ent_coef", ent_coef, timesteps_elapsed)
+
+ obs = torch.as_tensor(
+ np.concatenate([np.array(t.obs) for t in trajectories]), device=self.device
+ )
+ act = torch.as_tensor(
+ np.concatenate([np.array(t.act) for t in trajectories]), device=self.device
+ )
+ rtg, adv = compute_rtg_and_advantage(
+ trajectories, self.policy, self.gamma, self.gae_lambda, self.device
+ )
+ orig_v = torch.as_tensor(
+ np.concatenate([np.array(t.v) for t in trajectories]), device=self.device
+ )
+ orig_logp_a = torch.as_tensor(
+ np.concatenate([np.array(t.logp_a) for t in trajectories]),
+ device=self.device,
+ )
+
+ step_stats = []
+ for _ in range(self.n_epochs):
+ step_stats.clear()
+ if self.update_rtg_between_epochs:
+ rtg, adv = compute_rtg_and_advantage(
+ trajectories, self.policy, self.gamma, self.gae_lambda, self.device
+ )
+ else:
+ adv = compute_advantage(
+ trajectories, self.policy, self.gamma, self.gae_lambda, self.device
+ )
+ idxs = torch.randperm(len(obs))
+ for i in range(0, len(obs), self.batch_size):
+ mb_idxs = idxs[i : i + self.batch_size]
+ mb_adv = adv[mb_idxs]
+ if self.normalize_advantage:
+ mb_adv = (mb_adv - mb_adv.mean(-1)) / (mb_adv.std(-1) + 1e-8)
+ step_stats.append(
+ self._train_step(
+ pi_clip,
+ v_clip,
+ ent_coef,
+ obs[mb_idxs],
+ act[mb_idxs],
+ rtg[mb_idxs],
+ mb_adv,
+ orig_v[mb_idxs],
+ orig_logp_a[mb_idxs],
+ )
+ )
+
+ y_pred, y_true = orig_v.cpu().numpy(), rtg.cpu().numpy()
+ var_y = np.var(y_true).item()
+ explained_var = (
+ np.nan if var_y == 0 else 1 - np.var(y_true - y_pred).item() / var_y
+ )
+
+ return TrainStats(step_stats, explained_var)
+
+ def _train_step(
+ self,
+ pi_clip: float,
+ v_clip: Optional[float],
+ ent_coef: float,
+ obs: torch.Tensor,
+ act: torch.Tensor,
+ rtg: torch.Tensor,
+ adv: torch.Tensor,
+ orig_v: torch.Tensor,
+ orig_logp_a: torch.Tensor,
+ ) -> TrainStepStats:
+ logp_a, entropy, v = self.policy(obs, act)
+ logratio = logp_a - orig_logp_a
+ ratio = torch.exp(logratio)
+ clip_ratio = torch.clamp(ratio, min=1 - pi_clip, max=1 + pi_clip)
+ pi_loss = torch.maximum(-ratio * adv, -clip_ratio * adv).mean()
+
+ v_loss_unclipped = (v - rtg) ** 2
+ if v_clip:
+ v_loss_clipped = (
+ orig_v + torch.clamp(v - orig_v, -v_clip, v_clip) - rtg
+ ) ** 2
+ v_loss = torch.max(v_loss_unclipped, v_loss_clipped).mean()
+ else:
+ v_loss = v_loss_unclipped.mean()
+ if self.ppo2_vf_coef_halving:
+ v_loss *= 0.5
+
+ entropy_loss = -entropy.mean()
+
+ loss = pi_loss + ent_coef * entropy_loss + self.vf_coef * v_loss
+
+ self.optimizer.zero_grad()
+ loss.backward()
+ nn.utils.clip_grad_norm_(self.policy.parameters(), self.max_grad_norm)
+ self.optimizer.step()
+
+ with torch.no_grad():
+ approx_kl = ((ratio - 1) - logratio).mean().cpu().numpy().item()
+ clipped_frac = (
+ ((ratio - 1).abs() > pi_clip).float().mean().cpu().numpy().item()
+ )
+ val_clipped_frac = (
+ (((v - orig_v).abs() > v_clip).float().mean().cpu().numpy().item())
+ if v_clip
+ else 0
+ )
+
+ return TrainStepStats(
+ loss.item(),
+ pi_loss.item(),
+ v_loss.item(),
+ entropy_loss.item(),
+ approx_kl,
+ clipped_frac,
+ val_clipped_frac,
+ )
diff --git a/publish/markdown_format.py b/publish/markdown_format.py
new file mode 100644
index 0000000000000000000000000000000000000000..1e48fd1ef85ce19ad952b04433911b3fe11bf395
--- /dev/null
+++ b/publish/markdown_format.py
@@ -0,0 +1,210 @@
+import os
+import pandas as pd
+import wandb.apis.public
+import yaml
+
+from collections import defaultdict
+from dataclasses import dataclass, asdict
+from typing import Any, Dict, Iterable, List, NamedTuple, Optional, TypeVar
+from urllib.parse import urlparse
+
+from runner.evaluate import Evaluation
+
+EvaluationRowSelf = TypeVar("EvaluationRowSelf", bound="EvaluationRow")
+
+
+@dataclass
+class EvaluationRow:
+ algo: str
+ env: str
+ seed: Optional[int]
+ reward_mean: float
+ reward_std: float
+ eval_episodes: int
+ best: str
+ wandb_url: str
+
+ @staticmethod
+ def data_frame(rows: List[EvaluationRowSelf]) -> pd.DataFrame:
+ results = defaultdict(list)
+ for r in rows:
+ for k, v in asdict(r).items():
+ results[k].append(v)
+ return pd.DataFrame(results)
+
+
+class EvalTableData(NamedTuple):
+ run: wandb.apis.public.Run
+ evaluation: Evaluation
+
+
+def evaluation_table(table_data: Iterable[EvalTableData]) -> str:
+ best_stats = sorted(
+ [d.evaluation.stats for d in table_data], key=lambda r: r.score, reverse=True
+ )[0]
+ table_data = sorted(table_data, key=lambda d: d.evaluation.config.seed() or 0)
+ rows = [
+ EvaluationRow(
+ config.algo,
+ config.env_id,
+ config.seed(),
+ stats.score.mean,
+ stats.score.std,
+ len(stats),
+ "*" if stats == best_stats else "",
+ f"[wandb]({r.url})",
+ )
+ for (r, (_, stats, config)) in table_data
+ ]
+ df = EvaluationRow.data_frame(rows)
+ return df.to_markdown(index=False)
+
+
+def github_project_link(github_url: str) -> str:
+ return f"[{urlparse(github_url).path}]({github_url})"
+
+
+def header_section(algo: str, env: str, github_url: str, wandb_report_url: str) -> str:
+ algo_caps = algo.upper()
+ lines = [
+ f"# **{algo_caps}** Agent playing **{env}**",
+ f"This is a trained model of a **{algo_caps}** agent playing **{env}** using "
+ f"the {github_project_link(github_url)} repo.",
+ f"All models trained at this commit can be found at {wandb_report_url}.",
+ ]
+ return "\n\n".join(lines)
+
+
+def github_tree_link(github_url: str, commit_hash: Optional[str]) -> str:
+ if not commit_hash:
+ return github_project_link(github_url)
+ return f"[{commit_hash[:7]}]({github_url}/tree/{commit_hash})"
+
+
+def results_section(
+ table_data: List[EvalTableData], algo: str, github_url: str, commit_hash: str
+) -> str:
+ # type: ignore
+ lines = [
+ "## Training Results",
+ f"This model was trained from {len(table_data)} trainings of **{algo.upper()}** "
+ + "agents using different initial seeds. "
+ + f"These agents were trained by checking out "
+ + f"{github_tree_link(github_url, commit_hash)}. "
+ + "The best and last models were kept from each training. "
+ + "This submission has loaded the best models from each training, reevaluates "
+ + "them, and selects the best model from these latest evaluations (mean - std).",
+ ]
+ lines.append(evaluation_table(table_data))
+ return "\n\n".join(lines)
+
+
+def prerequisites_section() -> str:
+ return """
+### Prerequisites: Weights & Biases (WandB)
+Training and benchmarking assumes you have a Weights & Biases project to upload runs to.
+By default training goes to a rl-algo-impls project while benchmarks go to
+rl-algo-impls-benchmarks. During training and benchmarking runs, videos of the best
+models and the model weights are uploaded to WandB.
+
+Before doing anything below, you'll need to create a wandb account and run `wandb
+login`.
+"""
+
+
+def usage_section(github_url: str, run_path: str, commit_hash: str) -> str:
+ return f"""
+## Usage
+{urlparse(github_url).path}: {github_url}
+
+Note: While the model state dictionary and hyperaparameters are saved, the latest
+implementation could be sufficiently different to not be able to reproduce similar
+results. You might need to checkout the commit the agent was trained on:
+{github_tree_link(github_url, commit_hash)}.
+```
+# Downloads the model, sets hyperparameters, and runs agent for 3 episodes
+python enjoy.py --wandb-run-path={run_path}
+```
+
+Setup hasn't been completely worked out yet, so you might be best served by using Google
+Colab starting from the
+[colab_enjoy.ipynb](https://github.com/sgoodfriend/rl-algo-impls/blob/main/colab_enjoy.ipynb)
+notebook.
+"""
+
+
+def training_setion(
+ github_url: str, commit_hash: str, algo: str, env: str, seed: Optional[int]
+) -> str:
+ return f"""
+## Training
+If you want the highest chance to reproduce these results, you'll want to checkout the
+commit the agent was trained on: {github_tree_link(github_url, commit_hash)}. While
+training is deterministic, different hardware will give different results.
+
+```
+python train.py --algo {algo} --env {env} {'--seed ' + str(seed) if seed is not None else ''}
+```
+
+Setup hasn't been completely worked out yet, so you might be best served by using Google
+Colab starting from the
+[colab_train.ipynb](https://github.com/sgoodfriend/rl-algo-impls/blob/main/colab_train.ipynb)
+notebook.
+"""
+
+
+def benchmarking_section(report_url: str) -> str:
+ return f"""
+## Benchmarking (with Lambda Labs instance)
+This and other models from {report_url} were generated by running a script on a Lambda
+Labs instance. In a Lambda Labs instance terminal:
+```
+git clone git@github.com:sgoodfriend/rl-algo-impls.git
+cd rl-algo-impls
+bash ./lambda_labs/setup.sh
+wandb login
+bash ./lambda_labs/benchmark.sh
+```
+
+### Alternative: Google Colab Pro+
+As an alternative,
+[colab_benchmark.ipynb](https://github.com/sgoodfriend/rl-algo-impls/tree/main/benchmarks#:~:text=colab_benchmark.ipynb),
+can be used. However, this requires a Google Colab Pro+ subscription and running across
+4 separate instances because otherwise running all jobs will exceed the 24-hour limit.
+"""
+
+
+def hyperparams_section(run_config: Dict[str, Any]) -> str:
+ return f"""
+## Hyperparameters
+This isn't exactly the format of hyperparams in {os.path.join("hyperparams",
+run_config["algo"] + ".yml")}, but instead the Wandb Run Config. However, it's very
+close and has some additional data:
+```
+{yaml.dump(run_config)}
+```
+"""
+
+
+def model_card_text(
+ algo: str,
+ env: str,
+ github_url: str,
+ commit_hash: str,
+ wandb_report_url: str,
+ table_data: List[EvalTableData],
+ best_eval: EvalTableData,
+) -> str:
+ run, (_, _, config) = best_eval
+ run_path = "/".join(run.path)
+ return "\n\n".join(
+ [
+ header_section(algo, env, github_url, wandb_report_url),
+ results_section(table_data, algo, github_url, commit_hash),
+ prerequisites_section(),
+ usage_section(github_url, run_path, commit_hash),
+ training_setion(github_url, commit_hash, algo, env, config.seed()),
+ benchmarking_section(wandb_report_url),
+ hyperparams_section(run.config),
+ ]
+ )
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..d5c87129c896ce8f5ac2e32d63183d7fc206bd4c
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,35 @@
+[tool.poetry]
+name = "rl-algo-impls"
+version = "0.1.0"
+description = "Implementations of reinforcement learning algorithms"
+authors = ["Scott Goodfriend "]
+license = "MIT License"
+readme = "README.md"
+packages = [{include = "rl_algo_impls"}]
+
+[tool.poetry.dependencies]
+python = "~3.10"
+"AutoROM.accept-rom-license" = "^0.4.2"
+stable-baselines3 = {extras = ["extra"], version = "^1.7.0"}
+scipy = "^1.10.0"
+gym = {extras = ["box2d"], version = "^0.21.0"}
+pyglet = "1.5.27"
+PyYAML = "^6.0"
+tensorboard = "^2.11.0"
+pybullet = "^3.2.5"
+wandb = "^0.13.9"
+conda-lock = "^1.3.0"
+torch-tb-profiler = "^0.4.1"
+jupyter = "^1.0.0"
+tabulate = "^0.9.0"
+huggingface-hub = "^0.12.0"
+cryptography = "39.0.1"
+pyvirtualdisplay = "^3.0"
+numexpr = "^2.8.4"
+gym3 = "^0.3.3"
+glfw = "1.12.0"
+ipython = "^8.10.0"
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/replay.meta.json b/replay.meta.json
new file mode 100644
index 0000000000000000000000000000000000000000..b37a03424f5f020e7a2aa9e8777570b227d0cb9e
--- /dev/null
+++ b/replay.meta.json
@@ -0,0 +1 @@
+{"content_type": "video/mp4", "encoder_version": {"backend": "ffmpeg", "version": "b'ffmpeg version 4.2.7-0ubuntu0.1 Copyright (c) 2000-2022 the FFmpeg developers\\nbuilt with gcc 9 (Ubuntu 9.4.0-1ubuntu1~20.04.1)\\nconfiguration: --prefix=/usr --extra-version=0ubuntu0.1 --toolchain=hardened --libdir=/usr/lib/x86_64-linux-gnu --incdir=/usr/include/x86_64-linux-gnu --arch=amd64 --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-avisynth --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-nvenc --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared\\nlibavutil 56. 31.100 / 56. 31.100\\nlibavcodec 58. 54.100 / 58. 54.100\\nlibavformat 58. 29.100 / 58. 29.100\\nlibavdevice 58. 8.100 / 58. 8.100\\nlibavfilter 7. 57.100 / 7. 57.100\\nlibavresample 4. 0. 0 / 4. 0. 0\\nlibswscale 5. 5.100 / 5. 5.100\\nlibswresample 3. 5.100 / 3. 5.100\\nlibpostproc 55. 5.100 / 55. 5.100\\n'", "cmdline": ["ffmpeg", "-nostats", "-loglevel", "error", "-y", "-f", "rawvideo", "-s:v", "160x210", "-pix_fmt", "rgb24", "-framerate", "30", "-i", "-", "-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2", "-vcodec", "libx264", "-pix_fmt", "yuv420p", "-r", "30", "/tmp/tmp5g9x6mzt/a2c-PongNoFrameskip-v4/replay.mp4"]}, "episode": {"r": 21.0, "l": 6634, "t": 3.800911}}
\ No newline at end of file
diff --git a/replay.mp4 b/replay.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..0f0ee6d6c558879ba71a3a96a0c84b07d0d61c0c
Binary files /dev/null and b/replay.mp4 differ
diff --git a/runner/config.py b/runner/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..32045975fdcb7a7d604cac75f43eceb0e8dfedae
--- /dev/null
+++ b/runner/config.py
@@ -0,0 +1,155 @@
+import os
+
+from datetime import datetime
+from dataclasses import dataclass
+from typing import Any, Dict, NamedTuple, Optional, TypedDict, Union
+
+
+@dataclass
+class RunArgs:
+ algo: str
+ env: str
+ seed: Optional[int] = None
+ use_deterministic_algorithms: bool = True
+
+
+class EnvHyperparams(NamedTuple):
+ env_type: str = "gymvec"
+ n_envs: int = 1
+ frame_stack: int = 1
+ make_kwargs: Optional[Dict[str, Any]] = None
+ no_reward_timeout_steps: Optional[int] = None
+ no_reward_fire_steps: Optional[int] = None
+ vec_env_class: str = "sync"
+ normalize: bool = False
+ normalize_kwargs: Optional[Dict[str, Any]] = None
+ rolling_length: int = 100
+ train_record_video: bool = False
+ video_step_interval: Union[int, float] = 1_000_000
+ initial_steps_to_truncate: Optional[int] = None
+ clip_atari_rewards: bool = True
+
+
+class Hyperparams(TypedDict, total=False):
+ device: str
+ n_timesteps: Union[int, float]
+ env_hyperparams: Dict[str, Any]
+ policy_hyperparams: Dict[str, Any]
+ algo_hyperparams: Dict[str, Any]
+ eval_params: Dict[str, Any]
+
+
+@dataclass
+class Config:
+ args: RunArgs
+ hyperparams: Hyperparams
+ root_dir: str
+ run_id: str = datetime.now().isoformat()
+
+ def seed(self, training: bool = True) -> Optional[int]:
+ seed = self.args.seed
+ if training or seed is None:
+ return seed
+ return seed + self.env_hyperparams.get("n_envs", 1)
+
+ @property
+ def device(self) -> str:
+ return self.hyperparams.get("device", "auto")
+
+ @property
+ def n_timesteps(self) -> int:
+ return int(self.hyperparams.get("n_timesteps", 100_000))
+
+ @property
+ def env_hyperparams(self) -> Dict[str, Any]:
+ return self.hyperparams.get("env_hyperparams", {})
+
+ @property
+ def policy_hyperparams(self) -> Dict[str, Any]:
+ return self.hyperparams.get("policy_hyperparams", {})
+
+ @property
+ def algo_hyperparams(self) -> Dict[str, Any]:
+ return self.hyperparams.get("algo_hyperparams", {})
+
+ @property
+ def eval_params(self) -> Dict[str, Any]:
+ return self.hyperparams.get("eval_params", {})
+
+ @property
+ def algo(self) -> str:
+ return self.args.algo
+
+ @property
+ def env_id(self) -> str:
+ return self.hyperparams.get("env_id") or self.args.env
+
+ def model_name(self, include_seed: bool = True) -> str:
+ # Use arg env name instead of environment name
+ parts = [self.algo, self.args.env]
+ if include_seed and self.args.seed is not None:
+ parts.append(f"S{self.args.seed}")
+
+ # Assume that the custom arg name already has the necessary information
+ if not self.hyperparams.get("env_id"):
+ make_kwargs = self.env_hyperparams.get("make_kwargs", {})
+ if make_kwargs:
+ for k, v in make_kwargs.items():
+ if type(v) == bool and v:
+ parts.append(k)
+ elif type(v) == int and v:
+ parts.append(f"{k}{v}")
+ else:
+ parts.append(str(v))
+
+ return "-".join(parts)
+
+ @property
+ def run_name(self) -> str:
+ parts = [self.model_name(), self.run_id]
+ return "-".join(parts)
+
+ @property
+ def saved_models_dir(self) -> str:
+ return os.path.join(self.root_dir, "saved_models")
+
+ @property
+ def downloaded_models_dir(self) -> str:
+ return os.path.join(self.root_dir, "downloaded_models")
+
+ def model_dir_name(
+ self,
+ best: bool = False,
+ extension: str = "",
+ ) -> str:
+ return self.model_name() + ("-best" if best else "") + extension
+
+ def model_dir_path(self, best: bool = False, downloaded: bool = False) -> str:
+ return os.path.join(
+ self.saved_models_dir if not downloaded else self.downloaded_models_dir,
+ self.model_dir_name(best=best),
+ )
+
+ @property
+ def runs_dir(self) -> str:
+ return os.path.join(self.root_dir, "runs")
+
+ @property
+ def tensorboard_summary_path(self) -> str:
+ return os.path.join(self.runs_dir, self.run_name)
+
+ @property
+ def logs_path(self) -> str:
+ return os.path.join(self.runs_dir, f"log.yml")
+
+ @property
+ def videos_dir(self) -> str:
+ return os.path.join(self.root_dir, "videos")
+
+ @property
+ def video_prefix(self) -> str:
+ return os.path.join(self.videos_dir, self.model_name())
+
+ @property
+ def best_videos_dir(self) -> str:
+ return os.path.join(self.videos_dir, f"{self.model_name()}-best")
diff --git a/runner/env.py b/runner/env.py
new file mode 100644
index 0000000000000000000000000000000000000000..65082d31618fb80de07fc6b95ac5e0267144ddd0
--- /dev/null
+++ b/runner/env.py
@@ -0,0 +1,281 @@
+import gym
+import numpy as np
+import os
+
+from gym.vector.async_vector_env import AsyncVectorEnv
+from gym.vector.sync_vector_env import SyncVectorEnv
+from gym.wrappers.resize_observation import ResizeObservation
+from gym.wrappers.gray_scale_observation import GrayScaleObservation
+from gym.wrappers.frame_stack import FrameStack
+from stable_baselines3.common.atari_wrappers import (
+ MaxAndSkipEnv,
+ NoopResetEnv,
+)
+from stable_baselines3.common.vec_env.dummy_vec_env import DummyVecEnv
+from stable_baselines3.common.vec_env.subproc_vec_env import SubprocVecEnv
+from stable_baselines3.common.vec_env.vec_normalize import VecNormalize
+from torch.utils.tensorboard.writer import SummaryWriter
+from typing import Callable, Optional
+
+from runner.config import Config, EnvHyperparams
+from shared.policy.policy import VEC_NORMALIZE_FILENAME
+from wrappers.atari_wrappers import EpisodicLifeEnv, FireOnLifeStarttEnv, ClipRewardEnv
+from wrappers.episode_record_video import EpisodeRecordVideo
+from wrappers.episode_stats_writer import EpisodeStatsWriter
+from wrappers.initial_step_truncate_wrapper import InitialStepTruncateWrapper
+from wrappers.is_vector_env import IsVectorEnv
+from wrappers.noop_env_seed import NoopEnvSeed
+from wrappers.normalize import NormalizeObservation, NormalizeReward
+from wrappers.transpose_image_observation import TransposeImageObservation
+from wrappers.vectorable_wrapper import VecEnv
+from wrappers.video_compat_wrapper import VideoCompatWrapper
+
+
+def make_env(
+ config: Config,
+ hparams: EnvHyperparams,
+ training: bool = True,
+ render: bool = False,
+ normalize_load_path: Optional[str] = None,
+ tb_writer: Optional[SummaryWriter] = None,
+) -> VecEnv:
+ if hparams.env_type == "procgen":
+ return _make_procgen_env(
+ config,
+ hparams,
+ training=training,
+ render=render,
+ normalize_load_path=normalize_load_path,
+ tb_writer=tb_writer,
+ )
+ elif hparams.env_type in {"sb3vec", "gymvec"}:
+ return _make_vec_env(
+ config,
+ hparams,
+ training=training,
+ render=render,
+ normalize_load_path=normalize_load_path,
+ tb_writer=tb_writer,
+ )
+ else:
+ raise ValueError(f"env_type {hparams.env_type} not supported")
+
+
+def make_eval_env(
+ config: Config,
+ hparams: EnvHyperparams,
+ override_n_envs: Optional[int] = None,
+ **kwargs,
+) -> VecEnv:
+ kwargs = kwargs.copy()
+ kwargs["training"] = False
+ if override_n_envs is not None:
+ hparams_kwargs = hparams._asdict()
+ hparams_kwargs["n_envs"] = override_n_envs
+ if override_n_envs == 1:
+ hparams_kwargs["vec_env_class"] = "sync"
+ hparams = EnvHyperparams(**hparams_kwargs)
+ return make_env(config, hparams, **kwargs)
+
+
+def _make_vec_env(
+ config: Config,
+ hparams: EnvHyperparams,
+ training: bool = True,
+ render: bool = False,
+ normalize_load_path: Optional[str] = None,
+ tb_writer: Optional[SummaryWriter] = None,
+) -> VecEnv:
+ (
+ env_type,
+ n_envs,
+ frame_stack,
+ make_kwargs,
+ no_reward_timeout_steps,
+ no_reward_fire_steps,
+ vec_env_class,
+ normalize,
+ normalize_kwargs,
+ rolling_length,
+ train_record_video,
+ video_step_interval,
+ initial_steps_to_truncate,
+ clip_atari_rewards,
+ ) = hparams
+
+ if "BulletEnv" in config.env_id:
+ import pybullet_envs
+
+ spec = gym.spec(config.env_id)
+ seed = config.seed(training=training)
+
+ make_kwargs = make_kwargs.copy() if make_kwargs is not None else {}
+ if "BulletEnv" in config.env_id and render:
+ make_kwargs["render"] = True
+ if "CarRacing" in config.env_id:
+ make_kwargs["verbose"] = 0
+ if "procgen" in config.env_id:
+ if not render:
+ make_kwargs["render_mode"] = "rgb_array"
+
+ def make(idx: int) -> Callable[[], gym.Env]:
+ def _make() -> gym.Env:
+ env = gym.make(config.env_id, **make_kwargs)
+ env = gym.wrappers.RecordEpisodeStatistics(env)
+ env = VideoCompatWrapper(env)
+ if training and train_record_video and idx == 0:
+ env = EpisodeRecordVideo(
+ env,
+ config.video_prefix,
+ step_increment=n_envs,
+ video_step_interval=int(video_step_interval),
+ )
+ if training and initial_steps_to_truncate:
+ env = InitialStepTruncateWrapper(
+ env, idx * initial_steps_to_truncate // n_envs
+ )
+ if "AtariEnv" in spec.entry_point: # type: ignore
+ env = NoopResetEnv(env, noop_max=30)
+ env = MaxAndSkipEnv(env, skip=4)
+ env = EpisodicLifeEnv(env, training=training)
+ action_meanings = env.unwrapped.get_action_meanings()
+ if "FIRE" in action_meanings: # type: ignore
+ env = FireOnLifeStarttEnv(env, action_meanings.index("FIRE"))
+ if clip_atari_rewards:
+ env = ClipRewardEnv(env, training=training)
+ env = ResizeObservation(env, (84, 84))
+ env = GrayScaleObservation(env, keep_dim=False)
+ env = FrameStack(env, frame_stack)
+ elif "CarRacing" in config.env_id:
+ env = ResizeObservation(env, (64, 64))
+ env = GrayScaleObservation(env, keep_dim=False)
+ env = FrameStack(env, frame_stack)
+ elif "procgen" in config.env_id:
+ # env = GrayScaleObservation(env, keep_dim=False)
+ env = NoopEnvSeed(env)
+ env = TransposeImageObservation(env)
+ if frame_stack > 1:
+ env = FrameStack(env, frame_stack)
+
+ if no_reward_timeout_steps:
+ from wrappers.no_reward_timeout import NoRewardTimeout
+
+ env = NoRewardTimeout(
+ env, no_reward_timeout_steps, n_fire_steps=no_reward_fire_steps
+ )
+
+ if seed is not None:
+ env.seed(seed + idx)
+ env.action_space.seed(seed + idx)
+ env.observation_space.seed(seed + idx)
+
+ return env
+
+ return _make
+
+ if env_type == "sb3vec":
+ VecEnvClass = {"sync": DummyVecEnv, "async": SubprocVecEnv}[vec_env_class]
+ elif env_type == "gymvec":
+ VecEnvClass = {"sync": SyncVectorEnv, "async": AsyncVectorEnv}[vec_env_class]
+ else:
+ raise ValueError(f"env_type {env_type} unsupported")
+ envs = VecEnvClass([make(i) for i in range(n_envs)])
+ if training:
+ assert tb_writer
+ envs = EpisodeStatsWriter(
+ envs, tb_writer, training=training, rolling_length=rolling_length
+ )
+ if normalize:
+ normalize_kwargs = normalize_kwargs or {}
+ if env_type == "sb3vec":
+ if normalize_load_path:
+ envs = VecNormalize.load(
+ os.path.join(normalize_load_path, VEC_NORMALIZE_FILENAME),
+ envs, # type: ignore
+ )
+ else:
+ envs = VecNormalize(
+ envs, # type: ignore
+ training=training,
+ **normalize_kwargs,
+ )
+ if not training:
+ envs.norm_reward = False
+ else:
+ if normalize_kwargs.get("norm_obs", True):
+ envs = NormalizeObservation(
+ envs, training=training, clip=normalize_kwargs.get("clip_obs", 10.0)
+ )
+ if training and normalize_kwargs.get("norm_reward", True):
+ envs = NormalizeReward(
+ envs,
+ training=training,
+ clip=normalize_kwargs.get("clip_reward", 10.0),
+ )
+ return envs
+
+
+def _make_procgen_env(
+ config: Config,
+ hparams: EnvHyperparams,
+ training: bool = True,
+ render: bool = False,
+ normalize_load_path: Optional[str] = None,
+ tb_writer: Optional[SummaryWriter] = None,
+) -> VecEnv:
+ from gym3 import ViewerWrapper, ExtractDictObWrapper
+ from procgen.env import ProcgenGym3Env, ToBaselinesVecEnv
+
+ (
+ _,
+ n_envs,
+ frame_stack,
+ make_kwargs,
+ _, # no_reward_timeout_steps
+ _, # no_reward_fire_steps
+ _, # vec_env_class
+ normalize,
+ normalize_kwargs,
+ rolling_length,
+ _, # train_record_video
+ _, # video_step_interval
+ _, # initial_steps_to_truncate
+ _, # clip_atari_rewards
+ ) = hparams
+
+ seed = config.seed(training=training)
+
+ make_kwargs = make_kwargs or {}
+ make_kwargs["render_mode"] = "rgb_array"
+ if seed is not None:
+ make_kwargs["rand_seed"] = seed
+
+ envs = ProcgenGym3Env(n_envs, config.env_id, **make_kwargs)
+ envs = ExtractDictObWrapper(envs, key="rgb")
+ if render:
+ envs = ViewerWrapper(envs, info_key="rgb")
+ envs = ToBaselinesVecEnv(envs)
+ envs = IsVectorEnv(envs)
+ # TODO: Handle Grayscale and/or FrameStack
+ envs = TransposeImageObservation(envs)
+
+ envs = gym.wrappers.RecordEpisodeStatistics(envs)
+
+ if seed is not None:
+ envs.action_space.seed(seed)
+ envs.observation_space.seed(seed)
+
+ if training:
+ assert tb_writer
+ envs = EpisodeStatsWriter(
+ envs, tb_writer, training=training, rolling_length=rolling_length
+ )
+ if normalize and training:
+ normalize_kwargs = normalize_kwargs or {}
+ envs = gym.wrappers.NormalizeReward(envs)
+ clip_obs = normalize_kwargs.get("clip_reward", 10.0)
+ envs = gym.wrappers.TransformReward(
+ envs, lambda r: np.clip(r, -clip_obs, clip_obs)
+ )
+
+ return envs # type: ignore
diff --git a/runner/evaluate.py b/runner/evaluate.py
new file mode 100644
index 0000000000000000000000000000000000000000..85893bacfbfdbcb7a1ba794b040d67fd6bff71b4
--- /dev/null
+++ b/runner/evaluate.py
@@ -0,0 +1,103 @@
+import os
+import shutil
+
+from dataclasses import dataclass
+from typing import NamedTuple, Optional
+
+from runner.env import make_eval_env
+from runner.config import Config, EnvHyperparams, RunArgs
+from runner.running_utils import (
+ load_hyperparams,
+ set_seeds,
+ get_device,
+ make_policy,
+)
+from shared.callbacks.eval_callback import evaluate
+from shared.policy.policy import Policy
+from shared.stats import EpisodesStats
+
+
+@dataclass
+class EvalArgs(RunArgs):
+ render: bool = True
+ best: bool = True
+ n_envs: Optional[int] = 1
+ n_episodes: int = 3
+ deterministic_eval: Optional[bool] = None
+ no_print_returns: bool = False
+ wandb_run_path: Optional[str] = None
+
+
+class Evaluation(NamedTuple):
+ policy: Policy
+ stats: EpisodesStats
+ config: Config
+
+
+def evaluate_model(args: EvalArgs, root_dir: str) -> Evaluation:
+ if args.wandb_run_path:
+ import wandb
+
+ api = wandb.Api()
+ run = api.run(args.wandb_run_path)
+ hyperparams = run.config
+
+ args.algo = hyperparams["algo"]
+ args.env = hyperparams["env"]
+ args.seed = hyperparams.get("seed", None)
+ args.use_deterministic_algorithms = hyperparams.get(
+ "use_deterministic_algorithms", True
+ )
+
+ config = Config(args, hyperparams, root_dir)
+ model_path = config.model_dir_path(best=args.best, downloaded=True)
+
+ model_archive_name = config.model_dir_name(best=args.best, extension=".zip")
+ run.file(model_archive_name).download()
+ if os.path.isdir(model_path):
+ shutil.rmtree(model_path)
+ shutil.unpack_archive(model_archive_name, model_path)
+ os.remove(model_archive_name)
+ else:
+ hyperparams = load_hyperparams(args.algo, args.env, root_dir)
+
+ config = Config(args, hyperparams, root_dir)
+ model_path = config.model_dir_path(best=args.best)
+
+ print(args)
+
+ set_seeds(args.seed, args.use_deterministic_algorithms)
+
+ env = make_eval_env(
+ config,
+ EnvHyperparams(**config.env_hyperparams),
+ override_n_envs=args.n_envs,
+ render=args.render,
+ normalize_load_path=model_path,
+ )
+ device = get_device(config.device, env)
+ policy = make_policy(
+ args.algo,
+ env,
+ device,
+ load_path=model_path,
+ **config.policy_hyperparams,
+ ).eval()
+
+ deterministic = (
+ args.deterministic_eval
+ if args.deterministic_eval is not None
+ else config.eval_params.get("deterministic", True)
+ )
+ return Evaluation(
+ policy,
+ evaluate(
+ env,
+ policy,
+ args.n_episodes,
+ render=args.render,
+ deterministic=deterministic,
+ print_returns=not args.no_print_returns,
+ ),
+ config,
+ )
diff --git a/runner/running_utils.py b/runner/running_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..cfbc25fb6c97606300fa6d9fbcd7e22138789515
--- /dev/null
+++ b/runner/running_utils.py
@@ -0,0 +1,195 @@
+import argparse
+import gym
+import json
+import matplotlib.pyplot as plt
+import numpy as np
+import os
+import random
+import torch
+import torch.backends.cudnn
+import yaml
+
+from gym.spaces import Box, Discrete
+from torch.utils.tensorboard.writer import SummaryWriter
+from typing import Dict, Optional, Type, Union
+
+from runner.config import Hyperparams
+from shared.algorithm import Algorithm
+from shared.callbacks.eval_callback import EvalCallback
+from shared.policy.on_policy import ActorCritic
+from shared.policy.policy import Policy
+
+from a2c.a2c import A2C
+from dqn.dqn import DQN
+from dqn.policy import DQNPolicy
+from ppo.ppo import PPO
+from vpg.vpg import VanillaPolicyGradient
+from vpg.policy import VPGActorCritic
+from wrappers.vectorable_wrapper import VecEnv, single_observation_space
+
+ALGOS: Dict[str, Type[Algorithm]] = {
+ "dqn": DQN,
+ "vpg": VanillaPolicyGradient,
+ "ppo": PPO,
+ "a2c": A2C,
+}
+POLICIES: Dict[str, Type[Policy]] = {
+ "dqn": DQNPolicy,
+ "vpg": VPGActorCritic,
+ "ppo": ActorCritic,
+ "a2c": ActorCritic,
+}
+
+HYPERPARAMS_PATH = "hyperparams"
+
+
+def base_parser(multiple: bool = True) -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--algo",
+ default=["dqn"],
+ type=str,
+ choices=list(ALGOS.keys()),
+ nargs="+" if multiple else 1,
+ help="Abbreviation(s) of algorithm(s)",
+ )
+ parser.add_argument(
+ "--env",
+ default=["CartPole-v1"],
+ type=str,
+ nargs="+" if multiple else 1,
+ help="Name of environment(s) in gym",
+ )
+ parser.add_argument(
+ "--seed",
+ default=[1],
+ type=int,
+ nargs="*" if multiple else "?",
+ help="Seeds to run experiment. Unset will do one run with no set seed",
+ )
+ parser.add_argument(
+ "--use-deterministic-algorithms",
+ default=True,
+ type=bool,
+ help="If seed set, set torch.use_deterministic_algorithms",
+ )
+ return parser
+
+
+def load_hyperparams(algo: str, env_id: str, root_path: str) -> Hyperparams:
+ hyperparams_path = os.path.join(root_path, HYPERPARAMS_PATH, f"{algo}.yml")
+ with open(hyperparams_path, "r") as f:
+ hyperparams_dict = yaml.safe_load(f)
+
+ if env_id in hyperparams_dict:
+ return hyperparams_dict[env_id]
+
+ if "BulletEnv" in env_id:
+ import pybullet_envs
+ spec = gym.spec(env_id)
+ if "AtariEnv" in str(spec.entry_point) and "_atari" in hyperparams_dict:
+ return hyperparams_dict["_atari"]
+ else:
+ raise ValueError(f"{env_id} not specified in {algo} hyperparameters file")
+
+
+def get_device(device: str, env: VecEnv) -> torch.device:
+ # cuda by default
+ if device == "auto":
+ device = "cuda"
+ # Apple MPS is a second choice (sometimes)
+ if device == "cuda" and not torch.cuda.is_available():
+ device = "mps"
+ # If no MPS, fallback to cpu
+ if device == "mps" and not torch.backends.mps.is_available():
+ device = "cpu"
+ # Simple environments like Discreet and 1-D Boxes might also be better
+ # served with the CPU.
+ if device == "mps":
+ obs_space = single_observation_space(env)
+ if isinstance(obs_space, Discrete):
+ device = "cpu"
+ elif isinstance(obs_space, Box) and len(obs_space.shape) == 1:
+ device = "cpu"
+ print(f"Device: {device}")
+ return torch.device(device)
+
+
+def set_seeds(seed: Optional[int], use_deterministic_algorithms: bool) -> None:
+ if seed is None:
+ return
+ random.seed(seed)
+ np.random.seed(seed)
+ torch.manual_seed(seed)
+ torch.backends.cudnn.benchmark = False
+ torch.use_deterministic_algorithms(use_deterministic_algorithms)
+ os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"
+ # Stop warning and it would introduce stochasticity if I was using TF
+ os.environ["TF_ENABLE_ONEDNN_OPTS"] = "0"
+
+
+def make_policy(
+ algo: str,
+ env: VecEnv,
+ device: torch.device,
+ load_path: Optional[str] = None,
+ **kwargs,
+) -> Policy:
+ policy = POLICIES[algo](env, **kwargs).to(device)
+ if load_path:
+ policy.load(load_path)
+ return policy
+
+
+def plot_eval_callback(callback: EvalCallback, tb_writer: SummaryWriter, run_name: str):
+ figure = plt.figure()
+ cumulative_steps = [
+ (idx + 1) * callback.step_freq for idx in range(len(callback.stats))
+ ]
+ plt.plot(
+ cumulative_steps,
+ [s.score.mean for s in callback.stats],
+ "b-",
+ label="mean",
+ )
+ plt.plot(
+ cumulative_steps,
+ [s.score.mean - s.score.std for s in callback.stats],
+ "g--",
+ label="mean-std",
+ )
+ plt.fill_between(
+ cumulative_steps,
+ [s.score.min for s in callback.stats], # type: ignore
+ [s.score.max for s in callback.stats], # type: ignore
+ facecolor="cyan",
+ label="range",
+ )
+ plt.xlabel("Steps")
+ plt.ylabel("Score")
+ plt.legend()
+ plt.title(f"Eval {run_name}")
+ tb_writer.add_figure("eval", figure)
+
+
+Scalar = Union[bool, str, float, int, None]
+
+
+def hparam_dict(
+ hyperparams: Hyperparams, args: Dict[str, Union[Scalar, list]]
+) -> Dict[str, Scalar]:
+ flattened = args.copy()
+ for k, v in flattened.items():
+ if isinstance(v, list):
+ flattened[k] = json.dumps(v)
+ for k, v in hyperparams.items():
+ if isinstance(v, dict):
+ for sk, sv in v.items():
+ key = f"{k}/{sk}"
+ if isinstance(sv, dict) or isinstance(sv, list):
+ flattened[key] = str(sv)
+ else:
+ flattened[key] = sv
+ else:
+ flattened[k] = v # type: ignore
+ return flattened # type: ignore
diff --git a/runner/train.py b/runner/train.py
new file mode 100644
index 0000000000000000000000000000000000000000..ef6f7ac7773498d5c7dd9291cc4821715ec0f6b9
--- /dev/null
+++ b/runner/train.py
@@ -0,0 +1,141 @@
+# Support for PyTorch mps mode (https://pytorch.org/docs/stable/notes/mps.html)
+import os
+
+os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
+
+import dataclasses
+import shutil
+import wandb
+import yaml
+
+from dataclasses import dataclass
+from torch.utils.tensorboard.writer import SummaryWriter
+from typing import Any, Dict, Optional, Sequence
+
+from shared.callbacks.eval_callback import EvalCallback
+from runner.config import Config, EnvHyperparams, RunArgs
+from runner.env import make_env, make_eval_env
+from runner.running_utils import (
+ ALGOS,
+ load_hyperparams,
+ set_seeds,
+ get_device,
+ make_policy,
+ plot_eval_callback,
+ hparam_dict,
+)
+from shared.stats import EpisodesStats
+
+
+@dataclass
+class TrainArgs(RunArgs):
+ wandb_project_name: Optional[str] = None
+ wandb_entity: Optional[str] = None
+ wandb_tags: Sequence[str] = dataclasses.field(default_factory=list)
+
+
+def train(args: TrainArgs):
+ print(args)
+ hyperparams = load_hyperparams(args.algo, args.env, os.getcwd())
+ print(hyperparams)
+ config = Config(args, hyperparams, os.getcwd())
+
+ wandb_enabled = args.wandb_project_name
+ if wandb_enabled:
+ wandb.tensorboard.patch(
+ root_logdir=config.tensorboard_summary_path, pytorch=True
+ )
+ wandb.init(
+ project=args.wandb_project_name,
+ entity=args.wandb_entity,
+ config=hyperparams, # type: ignore
+ name=config.run_name,
+ monitor_gym=True,
+ save_code=True,
+ tags=args.wandb_tags,
+ )
+ wandb.config.update(args)
+
+ tb_writer = SummaryWriter(config.tensorboard_summary_path)
+
+ set_seeds(args.seed, args.use_deterministic_algorithms)
+
+ env = make_env(
+ config, EnvHyperparams(**config.env_hyperparams), tb_writer=tb_writer
+ )
+ device = get_device(config.device, env)
+ policy = make_policy(args.algo, env, device, **config.policy_hyperparams)
+ algo = ALGOS[args.algo](policy, env, device, tb_writer, **config.algo_hyperparams)
+
+ num_parameters = policy.num_parameters()
+ num_trainable_parameters = policy.num_trainable_parameters()
+ if wandb_enabled:
+ wandb.run.summary["num_parameters"] = num_parameters
+ wandb.run.summary["num_trainable_parameters"] = num_trainable_parameters
+ else:
+ print(
+ f"num_parameters = {num_parameters} ; "
+ f"num_trainable_parameters = {num_trainable_parameters}"
+ )
+
+ eval_env = make_eval_env(config, EnvHyperparams(**config.env_hyperparams))
+ record_best_videos = config.eval_params.get("record_best_videos", True)
+ callback = EvalCallback(
+ policy,
+ eval_env,
+ tb_writer,
+ best_model_path=config.model_dir_path(best=True),
+ **config.eval_params,
+ video_env=make_eval_env(
+ config, EnvHyperparams(**config.env_hyperparams), override_n_envs=1
+ )
+ if record_best_videos
+ else None,
+ best_video_dir=config.best_videos_dir,
+ )
+ algo.learn(config.n_timesteps, callback=callback)
+
+ policy.save(config.model_dir_path(best=False))
+
+ eval_stats = callback.evaluate(n_episodes=10, print_returns=True)
+
+ plot_eval_callback(callback, tb_writer, config.run_name)
+
+ log_dict: Dict[str, Any] = {
+ "eval": eval_stats._asdict(),
+ }
+ if callback.best:
+ log_dict["best_eval"] = callback.best._asdict()
+ log_dict.update(hyperparams)
+ log_dict.update(vars(args))
+ with open(config.logs_path, "a") as f:
+ yaml.dump({config.run_name: log_dict}, f)
+
+ best_eval_stats: EpisodesStats = callback.best # type: ignore
+ tb_writer.add_hparams(
+ hparam_dict(hyperparams, vars(args)),
+ {
+ "hparam/best_mean": best_eval_stats.score.mean,
+ "hparam/best_result": best_eval_stats.score.mean
+ - best_eval_stats.score.std,
+ "hparam/last_mean": eval_stats.score.mean,
+ "hparam/last_result": eval_stats.score.mean - eval_stats.score.std,
+ },
+ None,
+ config.run_name,
+ )
+
+ tb_writer.close()
+
+ if wandb_enabled:
+ shutil.make_archive(
+ os.path.join(wandb.run.dir, config.model_dir_name()),
+ "zip",
+ config.model_dir_path(),
+ )
+ shutil.make_archive(
+ os.path.join(wandb.run.dir, config.model_dir_name(best=True)),
+ "zip",
+ config.model_dir_path(best=True),
+ )
+ wandb.finish()
diff --git a/saved_models/a2c-PongNoFrameskip-v4-S3-best/model.pth b/saved_models/a2c-PongNoFrameskip-v4-S3-best/model.pth
new file mode 100644
index 0000000000000000000000000000000000000000..c7fcacff241bff4fab87f6dfc19f28f8152f77bd
--- /dev/null
+++ b/saved_models/a2c-PongNoFrameskip-v4-S3-best/model.pth
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6cf582760ef73a22097e5c9699d683b469f9fbe43983a52cbd2dd415dffe9522
+size 6755173
diff --git a/shared/algorithm.py b/shared/algorithm.py
new file mode 100644
index 0000000000000000000000000000000000000000..2a6fd4c10a4ca5bb6fc67d9bb7cb22b2dd18c9bb
--- /dev/null
+++ b/shared/algorithm.py
@@ -0,0 +1,35 @@
+import gym
+import torch
+
+from abc import ABC, abstractmethod
+from torch.utils.tensorboard.writer import SummaryWriter
+from typing import List, Optional, TypeVar
+
+from shared.callbacks.callback import Callback
+from shared.policy.policy import Policy
+from wrappers.vectorable_wrapper import VecEnv
+
+AlgorithmSelf = TypeVar("AlgorithmSelf", bound="Algorithm")
+
+
+class Algorithm(ABC):
+ @abstractmethod
+ def __init__(
+ self,
+ policy: Policy,
+ env: VecEnv,
+ device: torch.device,
+ tb_writer: SummaryWriter,
+ **kwargs,
+ ) -> None:
+ super().__init__()
+ self.policy = policy
+ self.env = env
+ self.device = device
+ self.tb_writer = tb_writer
+
+ @abstractmethod
+ def learn(
+ self: AlgorithmSelf, total_timesteps: int, callback: Optional[Callback] = None
+ ) -> AlgorithmSelf:
+ ...
diff --git a/shared/callbacks/callback.py b/shared/callbacks/callback.py
new file mode 100644
index 0000000000000000000000000000000000000000..f53784b7070d680817f6b8d9fc1fd13085289b89
--- /dev/null
+++ b/shared/callbacks/callback.py
@@ -0,0 +1,12 @@
+from abc import ABC, abstractmethod
+
+
+class Callback(ABC):
+
+ def __init__(self) -> None:
+ super().__init__()
+ self.timesteps_elapsed = 0
+
+ def on_step(self, timesteps_elapsed: int = 1) -> bool:
+ self.timesteps_elapsed += timesteps_elapsed
+ return True
\ No newline at end of file
diff --git a/shared/callbacks/eval_callback.py b/shared/callbacks/eval_callback.py
new file mode 100644
index 0000000000000000000000000000000000000000..475b8b40d1e12cab5eeceb30b764cdfc3bbe6c8e
--- /dev/null
+++ b/shared/callbacks/eval_callback.py
@@ -0,0 +1,199 @@
+import itertools
+import numpy as np
+import os
+
+from time import perf_counter
+from torch.utils.tensorboard.writer import SummaryWriter
+from typing import List, Optional, Union
+
+from shared.callbacks.callback import Callback
+from shared.policy.policy import Policy
+from shared.stats import Episode, EpisodeAccumulator, EpisodesStats
+from wrappers.vec_episode_recorder import VecEpisodeRecorder
+from wrappers.vectorable_wrapper import VecEnv
+
+
+class EvaluateAccumulator(EpisodeAccumulator):
+ def __init__(
+ self,
+ num_envs: int,
+ goal_episodes: int,
+ print_returns: bool = True,
+ ignore_first_episode: bool = False,
+ ):
+ super().__init__(num_envs)
+ self.completed_episodes_by_env_idx = [[] for _ in range(num_envs)]
+ self.goal_episodes_per_env = int(np.ceil(goal_episodes / num_envs))
+ self.print_returns = print_returns
+ if ignore_first_episode:
+ first_done = set()
+
+ def should_record_done(idx: int) -> bool:
+ has_done_first_episode = idx in first_done
+ first_done.add(idx)
+ return has_done_first_episode
+
+ self.should_record_done = should_record_done
+ else:
+ self.should_record_done = lambda idx: True
+
+ def on_done(self, ep_idx: int, episode: Episode) -> None:
+ if (
+ self.should_record_done(ep_idx)
+ and len(self.completed_episodes_by_env_idx[ep_idx])
+ >= self.goal_episodes_per_env
+ ):
+ return
+ self.completed_episodes_by_env_idx[ep_idx].append(episode)
+ if self.print_returns:
+ print(
+ f"Episode {len(self)} | "
+ f"Score {episode.score} | "
+ f"Length {episode.length}"
+ )
+
+ def __len__(self) -> int:
+ return sum(len(ce) for ce in self.completed_episodes_by_env_idx)
+
+ @property
+ def episodes(self) -> List[Episode]:
+ return list(itertools.chain(*self.completed_episodes_by_env_idx))
+
+ def is_done(self) -> bool:
+ return all(
+ len(ce) == self.goal_episodes_per_env
+ for ce in self.completed_episodes_by_env_idx
+ )
+
+
+def evaluate(
+ env: VecEnv,
+ policy: Policy,
+ n_episodes: int,
+ render: bool = False,
+ deterministic: bool = True,
+ print_returns: bool = True,
+ ignore_first_episode: bool = False,
+) -> EpisodesStats:
+ policy.eval()
+ episodes = EvaluateAccumulator(
+ env.num_envs, n_episodes, print_returns, ignore_first_episode
+ )
+
+ obs = env.reset()
+ while not episodes.is_done():
+ act = policy.act(obs, deterministic=deterministic)
+ obs, rew, done, _ = env.step(act)
+ episodes.step(rew, done)
+ if render:
+ env.render()
+ stats = EpisodesStats(episodes.episodes)
+ if print_returns:
+ print(stats)
+ return stats
+
+
+class EvalCallback(Callback):
+ def __init__(
+ self,
+ policy: Policy,
+ env: VecEnv,
+ tb_writer: SummaryWriter,
+ best_model_path: Optional[str] = None,
+ step_freq: Union[int, float] = 50_000,
+ n_episodes: int = 10,
+ save_best: bool = True,
+ deterministic: bool = True,
+ record_best_videos: bool = True,
+ video_env: Optional[VecEnv] = None,
+ best_video_dir: Optional[str] = None,
+ max_video_length: int = 3600,
+ ignore_first_episode: bool = False,
+ ) -> None:
+ super().__init__()
+ self.policy = policy
+ self.env = env
+ self.tb_writer = tb_writer
+ self.best_model_path = best_model_path
+ self.step_freq = int(step_freq)
+ self.n_episodes = n_episodes
+ self.save_best = save_best
+ self.deterministic = deterministic
+ self.stats: List[EpisodesStats] = []
+ self.best = None
+
+ self.record_best_videos = record_best_videos
+ assert video_env or not record_best_videos
+ self.video_env = video_env
+ assert best_video_dir or not record_best_videos
+ self.best_video_dir = best_video_dir
+ if best_video_dir:
+ os.makedirs(best_video_dir, exist_ok=True)
+ self.max_video_length = max_video_length
+ self.best_video_base_path = None
+
+ self.ignore_first_episode = ignore_first_episode
+
+ def on_step(self, timesteps_elapsed: int = 1) -> bool:
+ super().on_step(timesteps_elapsed)
+ if self.timesteps_elapsed // self.step_freq >= len(self.stats):
+ self.policy.sync_normalization(self.env)
+ self.evaluate()
+ return True
+
+ def evaluate(
+ self, n_episodes: Optional[int] = None, print_returns: Optional[bool] = None
+ ) -> EpisodesStats:
+ start_time = perf_counter()
+ eval_stat = evaluate(
+ self.env,
+ self.policy,
+ n_episodes or self.n_episodes,
+ deterministic=self.deterministic,
+ print_returns=print_returns or False,
+ ignore_first_episode=self.ignore_first_episode,
+ )
+ end_time = perf_counter()
+ self.tb_writer.add_scalar(
+ "eval/steps_per_second",
+ eval_stat.length.sum() / (end_time - start_time),
+ self.timesteps_elapsed,
+ )
+ self.policy.train(True)
+ print(f"Eval Timesteps: {self.timesteps_elapsed} | {eval_stat}")
+
+ self.stats.append(eval_stat)
+
+ if not self.best or eval_stat >= self.best:
+ strictly_better = not self.best or eval_stat > self.best
+ self.best = eval_stat
+ if self.save_best:
+ assert self.best_model_path
+ self.policy.save(self.best_model_path)
+ print("Saved best model")
+ self.best.write_to_tensorboard(
+ self.tb_writer, "best_eval", self.timesteps_elapsed
+ )
+ if strictly_better and self.record_best_videos:
+ assert self.video_env and self.best_video_dir
+ self.policy.sync_normalization(self.video_env)
+ self.best_video_base_path = os.path.join(
+ self.best_video_dir, str(self.timesteps_elapsed)
+ )
+ video_wrapped = VecEpisodeRecorder(
+ self.video_env,
+ self.best_video_base_path,
+ max_video_length=self.max_video_length,
+ )
+ video_stats = evaluate(
+ video_wrapped,
+ self.policy,
+ 1,
+ deterministic=self.deterministic,
+ print_returns=False,
+ )
+ print(f"Saved best video: {video_stats}")
+
+ eval_stat.write_to_tensorboard(self.tb_writer, "eval", self.timesteps_elapsed)
+
+ return eval_stat
diff --git a/shared/gae.py b/shared/gae.py
new file mode 100644
index 0000000000000000000000000000000000000000..05d87467d365696fb5cdb184aa690cce0ef745c5
--- /dev/null
+++ b/shared/gae.py
@@ -0,0 +1,67 @@
+import numpy as np
+import torch
+
+from typing import NamedTuple, Sequence
+
+from shared.policy.on_policy import OnPolicy
+from shared.trajectory import Trajectory
+
+
+class RtgAdvantage(NamedTuple):
+ rewards_to_go: torch.Tensor
+ advantage: torch.Tensor
+
+
+def discounted_cumsum(x: np.ndarray, gamma: float) -> np.ndarray:
+ dc = x.copy()
+ for i in reversed(range(len(x) - 1)):
+ dc[i] += gamma * dc[i + 1]
+ return dc
+
+
+def compute_advantage(
+ trajectories: Sequence[Trajectory],
+ policy: OnPolicy,
+ gamma: float,
+ gae_lambda: float,
+ device: torch.device,
+) -> torch.Tensor:
+ advantage = []
+ for traj in trajectories:
+ last_val = 0
+ if not traj.terminated and traj.next_obs is not None:
+ last_val = policy.value(traj.next_obs)
+ rew = np.append(np.array(traj.rew), last_val)
+ v = np.append(np.array(traj.v), last_val)
+ deltas = rew[:-1] + gamma * v[1:] - v[:-1]
+ advantage.append(discounted_cumsum(deltas, gamma * gae_lambda))
+ return torch.as_tensor(
+ np.concatenate(advantage), dtype=torch.float32, device=device
+ )
+
+
+def compute_rtg_and_advantage(
+ trajectories: Sequence[Trajectory],
+ policy: OnPolicy,
+ gamma: float,
+ gae_lambda: float,
+ device: torch.device,
+) -> RtgAdvantage:
+ rewards_to_go = []
+ advantages = []
+ for traj in trajectories:
+ last_val = 0
+ if not traj.terminated and traj.next_obs is not None:
+ last_val = policy.value(traj.next_obs)
+ rew = np.append(np.array(traj.rew), last_val)
+ v = np.append(np.array(traj.v), last_val)
+ deltas = rew[:-1] + gamma * v[1:] - v[:-1]
+ adv = discounted_cumsum(deltas, gamma * gae_lambda)
+ advantages.append(adv)
+ rewards_to_go.append(v[:-1] + adv)
+ return RtgAdvantage(
+ torch.as_tensor(
+ np.concatenate(rewards_to_go), dtype=torch.float32, device=device
+ ),
+ torch.as_tensor(np.concatenate(advantages), dtype=torch.float32, device=device),
+ )
diff --git a/shared/module/feature_extractor.py b/shared/module/feature_extractor.py
new file mode 100644
index 0000000000000000000000000000000000000000..a3abeba287e9ff9da3408d0b362c6a11e878a060
--- /dev/null
+++ b/shared/module/feature_extractor.py
@@ -0,0 +1,215 @@
+import gym
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+
+from abc import ABC, abstractmethod
+from gym.spaces import Box, Discrete
+from stable_baselines3.common.preprocessing import get_flattened_obs_dim
+from typing import Dict, Optional, Sequence, Type
+
+from shared.module.module import layer_init
+
+
+class CnnFeatureExtractor(nn.Module, ABC):
+ @abstractmethod
+ def __init__(
+ self,
+ in_channels: int,
+ activation: Type[nn.Module] = nn.ReLU,
+ init_layers_orthogonal: Optional[bool] = None,
+ **kwargs,
+ ) -> None:
+ super().__init__()
+
+
+class NatureCnn(CnnFeatureExtractor):
+ """
+ CNN from DQN Nature paper: Mnih, Volodymyr, et al.
+ "Human-level control through deep reinforcement learning."
+ Nature 518.7540 (2015): 529-533.
+ """
+
+ def __init__(
+ self,
+ in_channels: int,
+ activation: Type[nn.Module] = nn.ReLU,
+ init_layers_orthogonal: Optional[bool] = None,
+ **kwargs,
+ ) -> None:
+ if init_layers_orthogonal is None:
+ init_layers_orthogonal = True
+ super().__init__(in_channels, activation, init_layers_orthogonal)
+ self.cnn = nn.Sequential(
+ layer_init(
+ nn.Conv2d(in_channels, 32, kernel_size=8, stride=4),
+ init_layers_orthogonal,
+ ),
+ activation(),
+ layer_init(
+ nn.Conv2d(32, 64, kernel_size=4, stride=2),
+ init_layers_orthogonal,
+ ),
+ activation(),
+ layer_init(
+ nn.Conv2d(64, 64, kernel_size=3, stride=1),
+ init_layers_orthogonal,
+ ),
+ activation(),
+ nn.Flatten(),
+ )
+
+ def forward(self, obs: torch.Tensor) -> torch.Tensor:
+ return self.cnn(obs)
+
+
+class ResidualBlock(nn.Module):
+ def __init__(
+ self,
+ channels: int,
+ activation: Type[nn.Module] = nn.ReLU,
+ init_layers_orthogonal: bool = False,
+ ) -> None:
+ super().__init__()
+ self.residual = nn.Sequential(
+ activation(),
+ layer_init(
+ nn.Conv2d(channels, channels, 3, padding=1), init_layers_orthogonal
+ ),
+ activation(),
+ layer_init(
+ nn.Conv2d(channels, channels, 3, padding=1), init_layers_orthogonal
+ ),
+ )
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ return x + self.residual(x)
+
+
+class ConvSequence(nn.Module):
+ def __init__(
+ self,
+ in_channels: int,
+ out_channels: int,
+ activation: Type[nn.Module] = nn.ReLU,
+ init_layers_orthogonal: bool = False,
+ ) -> None:
+ super().__init__()
+ self.seq = nn.Sequential(
+ layer_init(
+ nn.Conv2d(in_channels, out_channels, 3, padding=1),
+ init_layers_orthogonal,
+ ),
+ nn.MaxPool2d(3, stride=2, padding=1),
+ ResidualBlock(out_channels, activation, init_layers_orthogonal),
+ ResidualBlock(out_channels, activation, init_layers_orthogonal),
+ )
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ return self.seq(x)
+
+
+class ImpalaCnn(CnnFeatureExtractor):
+ """
+ IMPALA-style CNN architecture
+ """
+
+ def __init__(
+ self,
+ in_channels: int,
+ activation: Type[nn.Module] = nn.ReLU,
+ init_layers_orthogonal: Optional[bool] = None,
+ impala_channels: Sequence[int] = (16, 32, 32),
+ **kwargs,
+ ) -> None:
+ if init_layers_orthogonal is None:
+ init_layers_orthogonal = False
+ super().__init__(in_channels, activation, init_layers_orthogonal)
+ sequences = []
+ for out_channels in impala_channels:
+ sequences.append(
+ ConvSequence(
+ in_channels, out_channels, activation, init_layers_orthogonal
+ )
+ )
+ in_channels = out_channels
+ sequences.extend(
+ [
+ activation(),
+ nn.Flatten(),
+ ]
+ )
+ self.seq = nn.Sequential(*sequences)
+
+ def forward(self, obs: torch.Tensor) -> torch.Tensor:
+ return self.seq(obs)
+
+
+CNN_EXTRACTORS_BY_STYLE: Dict[str, Type[CnnFeatureExtractor]] = {
+ "nature": NatureCnn,
+ "impala": ImpalaCnn,
+}
+
+
+class FeatureExtractor(nn.Module):
+ def __init__(
+ self,
+ obs_space: gym.Space,
+ activation: Type[nn.Module],
+ init_layers_orthogonal: bool = False,
+ cnn_feature_dim: int = 512,
+ cnn_style: str = "nature",
+ cnn_layers_init_orthogonal: Optional[bool] = None,
+ impala_channels: Sequence[int] = (16, 32, 32),
+ ) -> None:
+ super().__init__()
+ if isinstance(obs_space, Box):
+ # Conv2D: (channels, height, width)
+ if len(obs_space.shape) == 3:
+ cnn = CNN_EXTRACTORS_BY_STYLE[cnn_style](
+ obs_space.shape[0],
+ activation,
+ init_layers_orthogonal=cnn_layers_init_orthogonal,
+ impala_channels=impala_channels,
+ )
+
+ def preprocess(obs: torch.Tensor) -> torch.Tensor:
+ if len(obs.shape) == 3:
+ obs = obs.unsqueeze(0)
+ return obs.float() / 255.0
+
+ with torch.no_grad():
+ cnn_out = cnn(preprocess(torch.as_tensor(obs_space.sample())))
+ self.preprocess = preprocess
+ self.feature_extractor = nn.Sequential(
+ cnn,
+ layer_init(
+ nn.Linear(cnn_out.shape[1], cnn_feature_dim),
+ init_layers_orthogonal,
+ ),
+ activation(),
+ )
+ self.out_dim = cnn_feature_dim
+ elif len(obs_space.shape) == 1:
+
+ def preprocess(obs: torch.Tensor) -> torch.Tensor:
+ if len(obs.shape) == 1:
+ obs = obs.unsqueeze(0)
+ return obs.float()
+
+ self.preprocess = preprocess
+ self.feature_extractor = nn.Flatten()
+ self.out_dim = get_flattened_obs_dim(obs_space)
+ else:
+ raise ValueError(f"Unsupported observation space: {obs_space}")
+ elif isinstance(obs_space, Discrete):
+ self.preprocess = lambda x: F.one_hot(x, obs_space.n).float()
+ self.feature_extractor = nn.Flatten()
+ self.out_dim = obs_space.n
+ else:
+ raise NotImplementedError
+
+ def forward(self, obs: torch.Tensor) -> torch.Tensor:
+ if self.preprocess:
+ obs = self.preprocess(obs)
+ return self.feature_extractor(obs)
diff --git a/shared/module/module.py b/shared/module/module.py
new file mode 100644
index 0000000000000000000000000000000000000000..c579fb2a3808de47ec8d4c5233eea947b5cf0d28
--- /dev/null
+++ b/shared/module/module.py
@@ -0,0 +1,40 @@
+import numpy as np
+import torch.nn as nn
+
+from typing import Sequence, Type
+
+
+def mlp(
+ layer_sizes: Sequence[int],
+ activation: Type[nn.Module],
+ output_activation: Type[nn.Module] = nn.Identity,
+ init_layers_orthogonal: bool = False,
+ final_layer_gain: float = np.sqrt(2),
+) -> nn.Module:
+ layers = []
+ for i in range(len(layer_sizes) - 2):
+ layers.append(
+ layer_init(
+ nn.Linear(layer_sizes[i], layer_sizes[i + 1]), init_layers_orthogonal
+ )
+ )
+ layers.append(activation())
+ layers.append(
+ layer_init(
+ nn.Linear(layer_sizes[-2], layer_sizes[-1]),
+ init_layers_orthogonal,
+ std=final_layer_gain,
+ )
+ )
+ layers.append(output_activation())
+ return nn.Sequential(*layers)
+
+
+def layer_init(
+ layer: nn.Module, init_layers_orthogonal: bool, std: float = np.sqrt(2)
+) -> nn.Module:
+ if not init_layers_orthogonal:
+ return layer
+ nn.init.orthogonal_(layer.weight, std) # type: ignore
+ nn.init.constant_(layer.bias, 0.0) # type: ignore
+ return layer
diff --git a/shared/policy/actor.py b/shared/policy/actor.py
new file mode 100644
index 0000000000000000000000000000000000000000..202280cb85bae1fded982c65f86520197a20ae51
--- /dev/null
+++ b/shared/policy/actor.py
@@ -0,0 +1,305 @@
+import gym
+import torch
+import torch.nn as nn
+
+from abc import ABC, abstractmethod
+from gym.spaces import Box, Discrete
+from torch.distributions import Categorical, Distribution, Normal
+from typing import NamedTuple, Optional, Sequence, Type, TypeVar, Union
+
+from shared.module.feature_extractor import FeatureExtractor
+from shared.module.module import mlp
+
+
+class PiForward(NamedTuple):
+ pi: Distribution
+ logp_a: Optional[torch.Tensor]
+ entropy: Optional[torch.Tensor]
+
+
+class Actor(nn.Module, ABC):
+ @abstractmethod
+ def forward(self, obs: torch.Tensor, a: Optional[torch.Tensor] = None) -> PiForward:
+ ...
+
+
+class CategoricalActorHead(Actor):
+ def __init__(
+ self,
+ act_dim: int,
+ hidden_sizes: Sequence[int] = (32,),
+ activation: Type[nn.Module] = nn.Tanh,
+ init_layers_orthogonal: bool = True,
+ ) -> None:
+ super().__init__()
+ layer_sizes = tuple(hidden_sizes) + (act_dim,)
+ self._fc = mlp(
+ layer_sizes,
+ activation,
+ init_layers_orthogonal=init_layers_orthogonal,
+ final_layer_gain=0.01,
+ )
+
+ def forward(self, obs: torch.Tensor, a: Optional[torch.Tensor] = None) -> PiForward:
+ logits = self._fc(obs)
+ pi = Categorical(logits=logits)
+ logp_a = None
+ entropy = None
+ if a is not None:
+ logp_a = pi.log_prob(a)
+ entropy = pi.entropy()
+ return PiForward(pi, logp_a, entropy)
+
+
+class GaussianDistribution(Normal):
+ def log_prob(self, a: torch.Tensor) -> torch.Tensor:
+ return super().log_prob(a).sum(axis=-1)
+
+ def sample(self) -> torch.Tensor:
+ return self.rsample()
+
+
+class GaussianActorHead(Actor):
+ def __init__(
+ self,
+ act_dim: int,
+ hidden_sizes: Sequence[int] = (32,),
+ activation: Type[nn.Module] = nn.Tanh,
+ init_layers_orthogonal: bool = True,
+ log_std_init: float = -0.5,
+ ) -> None:
+ super().__init__()
+ layer_sizes = tuple(hidden_sizes) + (act_dim,)
+ self.mu_net = mlp(
+ layer_sizes,
+ activation,
+ init_layers_orthogonal=init_layers_orthogonal,
+ final_layer_gain=0.01,
+ )
+ self.log_std = nn.Parameter(
+ torch.ones(act_dim, dtype=torch.float32) * log_std_init
+ )
+
+ def _distribution(self, obs: torch.Tensor) -> Distribution:
+ mu = self.mu_net(obs)
+ std = torch.exp(self.log_std)
+ return GaussianDistribution(mu, std)
+
+ def forward(self, obs: torch.Tensor, a: Optional[torch.Tensor] = None) -> PiForward:
+ pi = self._distribution(obs)
+ logp_a = None
+ entropy = None
+ if a is not None:
+ logp_a = pi.log_prob(a)
+ entropy = pi.entropy()
+ return PiForward(pi, logp_a, entropy)
+
+
+class TanhBijector:
+ def __init__(self, epsilon: float = 1e-6) -> None:
+ self.epsilon = epsilon
+
+ @staticmethod
+ def forward(x: torch.Tensor) -> torch.Tensor:
+ return torch.tanh(x)
+
+ @staticmethod
+ def inverse(y: torch.Tensor) -> torch.Tensor:
+ eps = torch.finfo(y.dtype).eps
+ clamped_y = y.clamp(min=-1.0 + eps, max=1.0 - eps)
+ return torch.atanh(clamped_y)
+
+ def log_prob_correction(self, x: torch.Tensor) -> torch.Tensor:
+ return torch.log(1.0 - torch.tanh(x) ** 2 + self.epsilon)
+
+
+class StateDependentNoiseDistribution(Normal):
+ def __init__(
+ self,
+ loc,
+ scale,
+ latent_sde: torch.Tensor,
+ exploration_mat: torch.Tensor,
+ exploration_matrices: torch.Tensor,
+ bijector: Optional[TanhBijector] = None,
+ validate_args=None,
+ ):
+ super().__init__(loc, scale, validate_args)
+ self.latent_sde = latent_sde
+ self.exploration_mat = exploration_mat
+ self.exploration_matrices = exploration_matrices
+ self.bijector = bijector
+
+ def log_prob(self, a: torch.Tensor) -> torch.Tensor:
+ gaussian_a = self.bijector.inverse(a) if self.bijector else a
+ log_prob = super().log_prob(gaussian_a).sum(axis=-1)
+ if self.bijector:
+ log_prob -= torch.sum(self.bijector.log_prob_correction(gaussian_a), dim=1)
+ return log_prob
+
+ def sample(self) -> torch.Tensor:
+ noise = self._get_noise()
+ actions = self.mean + noise
+ return self.bijector.forward(actions) if self.bijector else actions
+
+ def _get_noise(self) -> torch.Tensor:
+ if len(self.latent_sde) == 1 or len(self.latent_sde) != len(
+ self.exploration_matrices
+ ):
+ return torch.mm(self.latent_sde, self.exploration_mat)
+ # (batch_size, n_features) -> (batch_size, 1, n_features)
+ latent_sde = self.latent_sde.unsqueeze(dim=1)
+ # (batch_size, 1, n_actions)
+ noise = torch.bmm(latent_sde, self.exploration_matrices)
+ return noise.squeeze(dim=1)
+
+ @property
+ def mode(self) -> torch.Tensor:
+ mean = super().mode
+ return self.bijector.forward(mean) if self.bijector else mean
+
+
+StateDependentNoiseActorHeadSelf = TypeVar(
+ "StateDependentNoiseActorHeadSelf", bound="StateDependentNoiseActorHead"
+)
+
+
+class StateDependentNoiseActorHead(Actor):
+ def __init__(
+ self,
+ act_dim: int,
+ hidden_sizes: Sequence[int] = (32,),
+ activation: Type[nn.Module] = nn.Tanh,
+ init_layers_orthogonal: bool = True,
+ log_std_init: float = -0.5,
+ full_std: bool = True,
+ squash_output: bool = False,
+ learn_std: bool = False,
+ ) -> None:
+ super().__init__()
+ self.act_dim = act_dim
+ layer_sizes = tuple(hidden_sizes) + (self.act_dim,)
+ if len(layer_sizes) == 2:
+ self.latent_net = nn.Identity()
+ elif len(layer_sizes) > 2:
+ self.latent_net = mlp(
+ layer_sizes[:-1],
+ activation,
+ output_activation=activation,
+ init_layers_orthogonal=init_layers_orthogonal,
+ )
+ else:
+ raise ValueError("hidden_sizes must be of at least length 1")
+ self.mu_net = mlp(
+ layer_sizes[-2:],
+ activation,
+ init_layers_orthogonal=init_layers_orthogonal,
+ final_layer_gain=0.01,
+ )
+ self.full_std = full_std
+ std_dim = (hidden_sizes[-1], act_dim if self.full_std else 1)
+ self.log_std = nn.Parameter(
+ torch.ones(std_dim, dtype=torch.float32) * log_std_init
+ )
+ self.bijector = TanhBijector() if squash_output else None
+ self.learn_std = learn_std
+ self.device = None
+
+ self.exploration_mat = None
+ self.exploration_matrices = None
+ self.sample_weights()
+
+ def to(
+ self: StateDependentNoiseActorHeadSelf,
+ device: Optional[torch.device] = None,
+ dtype: Optional[Union[torch.dtype, str]] = None,
+ non_blocking: bool = False,
+ ) -> StateDependentNoiseActorHeadSelf:
+ super().to(device, dtype, non_blocking)
+ self.device = device
+ return self
+
+ def _distribution(self, obs: torch.Tensor) -> Distribution:
+ latent = self.latent_net(obs)
+ mu = self.mu_net(latent)
+ latent_sde = latent if self.learn_std else latent.detach()
+ variance = torch.mm(latent_sde**2, self._get_std() ** 2)
+ assert self.exploration_mat is not None
+ assert self.exploration_matrices is not None
+ return StateDependentNoiseDistribution(
+ mu,
+ torch.sqrt(variance + 1e-6),
+ latent_sde,
+ self.exploration_mat,
+ self.exploration_matrices,
+ self.bijector,
+ )
+
+ def _get_std(self) -> torch.Tensor:
+ std = torch.exp(self.log_std)
+ if self.full_std:
+ return std
+ ones = torch.ones(self.log_std.shape[0], self.act_dim)
+ if self.device:
+ ones = ones.to(self.device)
+ return ones * std
+
+ def forward(self, obs: torch.Tensor, a: Optional[torch.Tensor] = None) -> PiForward:
+ pi = self._distribution(obs)
+ logp_a = None
+ entropy = None
+ if a is not None:
+ logp_a = pi.log_prob(a)
+ entropy = -logp_a
+ return PiForward(pi, logp_a, entropy)
+
+ def sample_weights(self, batch_size: int = 1) -> None:
+ std = self._get_std()
+ weights_dist = Normal(torch.zeros_like(std), std)
+ # Reparametrization trick to pass gradients
+ self.exploration_mat = weights_dist.rsample()
+ self.exploration_matrices = weights_dist.rsample(torch.Size((batch_size,)))
+
+
+def actor_head(
+ action_space: gym.Space,
+ hidden_sizes: Sequence[int],
+ init_layers_orthogonal: bool,
+ activation: Type[nn.Module],
+ log_std_init: float = -0.5,
+ use_sde: bool = False,
+ full_std: bool = True,
+ squash_output: bool = False,
+) -> Actor:
+ assert not use_sde or isinstance(
+ action_space, Box
+ ), "use_sde only valid if Box action_space"
+ assert not squash_output or use_sde, "squash_output only valid if use_sde"
+ if isinstance(action_space, Discrete):
+ return CategoricalActorHead(
+ action_space.n,
+ hidden_sizes=hidden_sizes,
+ activation=activation,
+ init_layers_orthogonal=init_layers_orthogonal,
+ )
+ elif isinstance(action_space, Box):
+ if use_sde:
+ return StateDependentNoiseActorHead(
+ action_space.shape[0],
+ hidden_sizes=hidden_sizes,
+ activation=activation,
+ init_layers_orthogonal=init_layers_orthogonal,
+ log_std_init=log_std_init,
+ full_std=full_std,
+ squash_output=squash_output,
+ )
+ else:
+ return GaussianActorHead(
+ action_space.shape[0],
+ hidden_sizes=hidden_sizes,
+ activation=activation,
+ init_layers_orthogonal=init_layers_orthogonal,
+ log_std_init=log_std_init,
+ )
+ else:
+ raise ValueError(f"Unsupported action space: {action_space}")
diff --git a/shared/policy/critic.py b/shared/policy/critic.py
new file mode 100644
index 0000000000000000000000000000000000000000..70ed0e0778c6809f943aab4da70bec3b88fc5cf2
--- /dev/null
+++ b/shared/policy/critic.py
@@ -0,0 +1,28 @@
+import gym
+import torch
+import torch.nn as nn
+
+from typing import Sequence, Type
+from shared.module.feature_extractor import FeatureExtractor
+from shared.module.module import mlp
+
+
+class CriticHead(nn.Module):
+ def __init__(
+ self,
+ hidden_sizes: Sequence[int] = (32,),
+ activation: Type[nn.Module] = nn.Tanh,
+ init_layers_orthogonal: bool = True,
+ ) -> None:
+ super().__init__()
+ layer_sizes = tuple(hidden_sizes) + (1,)
+ self._fc = mlp(
+ layer_sizes,
+ activation,
+ init_layers_orthogonal=init_layers_orthogonal,
+ final_layer_gain=1.0,
+ )
+
+ def forward(self, obs: torch.Tensor) -> torch.Tensor:
+ v = self._fc(obs)
+ return v.squeeze(-1)
diff --git a/shared/policy/on_policy.py b/shared/policy/on_policy.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c6517232d4cf0672594e354c4d3949c86d7c936
--- /dev/null
+++ b/shared/policy/on_policy.py
@@ -0,0 +1,222 @@
+import gym
+import numpy as np
+import torch
+
+from abc import abstractmethod
+from gym.spaces import Box, Discrete, Space
+from typing import NamedTuple, Optional, Sequence, Tuple, TypeVar
+
+from shared.module.feature_extractor import FeatureExtractor
+from shared.policy.actor import PiForward, StateDependentNoiseActorHead, actor_head
+from shared.policy.critic import CriticHead
+from shared.policy.policy import ACTIVATION, Policy
+from wrappers.vectorable_wrapper import (
+ VecEnv,
+ VecEnvObs,
+ single_observation_space,
+ single_action_space,
+)
+
+
+class Step(NamedTuple):
+ a: np.ndarray
+ v: np.ndarray
+ logp_a: np.ndarray
+ clamped_a: np.ndarray
+
+
+class ACForward(NamedTuple):
+ logp_a: torch.Tensor
+ entropy: torch.Tensor
+ v: torch.Tensor
+
+
+FEAT_EXT_FILE_NAME = "feat_ext.pt"
+V_FEAT_EXT_FILE_NAME = "v_feat_ext.pt"
+PI_FILE_NAME = "pi.pt"
+V_FILE_NAME = "v.pt"
+ActorCriticSelf = TypeVar("ActorCriticSelf", bound="ActorCritic")
+
+
+def clamp_actions(
+ actions: np.ndarray, action_space: gym.Space, squash_output: bool
+) -> np.ndarray:
+ if isinstance(action_space, Box):
+ low, high = action_space.low, action_space.high # type: ignore
+ if squash_output:
+ # Squashed output is already between -1 and 1. Rescale if the actual
+ # output needs to something other than -1 and 1
+ return low + 0.5 * (actions + 1) * (high - low)
+ else:
+ return np.clip(actions, low, high)
+ return actions
+
+
+def default_hidden_sizes(obs_space: Space) -> Sequence[int]:
+ if isinstance(obs_space, Box):
+ if len(obs_space.shape) == 3:
+ # By default feature extractor to output has no hidden layers
+ return []
+ elif len(obs_space.shape) == 1:
+ return [64, 64]
+ else:
+ raise ValueError(f"Unsupported observation space: {obs_space}")
+ elif isinstance(obs_space, Discrete):
+ return [64]
+ else:
+ raise ValueError(f"Unsupported observation space: {obs_space}")
+
+
+class OnPolicy(Policy):
+ @abstractmethod
+ def value(self, obs: VecEnvObs) -> np.ndarray:
+ ...
+
+ @abstractmethod
+ def step(self, obs: VecEnvObs) -> Step:
+ ...
+
+
+class ActorCritic(OnPolicy):
+ def __init__(
+ self,
+ env: VecEnv,
+ pi_hidden_sizes: Optional[Sequence[int]] = None,
+ v_hidden_sizes: Optional[Sequence[int]] = None,
+ init_layers_orthogonal: bool = True,
+ activation_fn: str = "tanh",
+ log_std_init: float = -0.5,
+ use_sde: bool = False,
+ full_std: bool = True,
+ squash_output: bool = False,
+ share_features_extractor: bool = True,
+ cnn_feature_dim: int = 512,
+ cnn_style: str = "nature",
+ cnn_layers_init_orthogonal: Optional[bool] = None,
+ impala_channels: Sequence[int] = (16, 32, 32),
+ **kwargs,
+ ) -> None:
+ super().__init__(env, **kwargs)
+
+ observation_space = single_observation_space(env)
+ action_space = single_action_space(env)
+
+ pi_hidden_sizes = (
+ pi_hidden_sizes
+ if pi_hidden_sizes is not None
+ else default_hidden_sizes(observation_space)
+ )
+ v_hidden_sizes = (
+ v_hidden_sizes
+ if v_hidden_sizes is not None
+ else default_hidden_sizes(observation_space)
+ )
+
+ activation = ACTIVATION[activation_fn]
+ self.action_space = action_space
+ self.squash_output = squash_output
+ self.share_features_extractor = share_features_extractor
+ self._feature_extractor = FeatureExtractor(
+ observation_space,
+ activation,
+ init_layers_orthogonal=init_layers_orthogonal,
+ cnn_feature_dim=cnn_feature_dim,
+ cnn_style=cnn_style,
+ cnn_layers_init_orthogonal=cnn_layers_init_orthogonal,
+ impala_channels=impala_channels,
+ )
+ self._pi = actor_head(
+ self.action_space,
+ (self._feature_extractor.out_dim,) + tuple(pi_hidden_sizes),
+ init_layers_orthogonal,
+ activation,
+ log_std_init=log_std_init,
+ use_sde=use_sde,
+ full_std=full_std,
+ squash_output=squash_output,
+ )
+
+ if not share_features_extractor:
+ self._v_feature_extractor = FeatureExtractor(
+ observation_space,
+ activation,
+ init_layers_orthogonal=init_layers_orthogonal,
+ cnn_feature_dim=cnn_feature_dim,
+ cnn_style=cnn_style,
+ cnn_layers_init_orthogonal=cnn_layers_init_orthogonal,
+ )
+ v_hidden_sizes = (self._v_feature_extractor.out_dim,) + tuple(
+ v_hidden_sizes
+ )
+ else:
+ self._v_feature_extractor = None
+ v_hidden_sizes = (self._feature_extractor.out_dim,) + tuple(v_hidden_sizes)
+ self._v = CriticHead(
+ hidden_sizes=v_hidden_sizes,
+ activation=activation,
+ init_layers_orthogonal=init_layers_orthogonal,
+ )
+
+ def _pi_forward(
+ self, obs: torch.Tensor, action: Optional[torch.Tensor] = None
+ ) -> Tuple[PiForward, torch.Tensor]:
+ p_fe = self._feature_extractor(obs)
+ pi_forward = self._pi(p_fe, action)
+
+ return pi_forward, p_fe
+
+ def _v_forward(self, obs: torch.Tensor, p_fc: torch.Tensor) -> torch.Tensor:
+ v_fe = self._v_feature_extractor(obs) if self._v_feature_extractor else p_fc
+ return self._v(v_fe)
+
+ def forward(self, obs: torch.Tensor, action: torch.Tensor) -> ACForward:
+ (_, logp_a, entropy), p_fc = self._pi_forward(obs, action)
+ v = self._v_forward(obs, p_fc)
+
+ assert logp_a is not None
+ assert entropy is not None
+ return ACForward(logp_a, entropy, v)
+
+ def value(self, obs: VecEnvObs) -> np.ndarray:
+ o = self._as_tensor(obs)
+ with torch.no_grad():
+ fe = (
+ self._v_feature_extractor(o)
+ if self._v_feature_extractor
+ else self._feature_extractor(o)
+ )
+ v = self._v(fe)
+ return v.cpu().numpy()
+
+ def step(self, obs: VecEnvObs) -> Step:
+ o = self._as_tensor(obs)
+ with torch.no_grad():
+ (pi, _, _), p_fc = self._pi_forward(o)
+ a = pi.sample()
+ logp_a = pi.log_prob(a)
+
+ v = self._v_forward(o, p_fc)
+
+ a_np = a.cpu().numpy()
+ clamped_a_np = clamp_actions(a_np, self.action_space, self.squash_output)
+ return Step(a_np, v.cpu().numpy(), logp_a.cpu().numpy(), clamped_a_np)
+
+ def act(self, obs: np.ndarray, deterministic: bool = True) -> np.ndarray:
+ if not deterministic:
+ return self.step(obs).clamped_a
+ else:
+ o = self._as_tensor(obs)
+ with torch.no_grad():
+ (pi, _, _), _ = self._pi_forward(o)
+ a = pi.mode
+ return clamp_actions(a.cpu().numpy(), self.action_space, self.squash_output)
+
+ def load(self, path: str) -> None:
+ super().load(path)
+ self.reset_noise()
+
+ def reset_noise(self, batch_size: Optional[int] = None) -> None:
+ if isinstance(self._pi, StateDependentNoiseActorHead):
+ self._pi.sample_weights(
+ batch_size=batch_size if batch_size else self.env.num_envs
+ )
diff --git a/shared/policy/policy.py b/shared/policy/policy.py
new file mode 100644
index 0000000000000000000000000000000000000000..f0669a5c21094b9e815b7348cdb7b20606f69601
--- /dev/null
+++ b/shared/policy/policy.py
@@ -0,0 +1,114 @@
+import numpy as np
+import os
+import torch
+import torch.nn as nn
+
+from abc import ABC, abstractmethod
+from copy import deepcopy
+from stable_baselines3.common.vec_env import unwrap_vec_normalize
+from stable_baselines3.common.vec_env.vec_normalize import VecNormalize
+from typing import Dict, Optional, Type, TypeVar, Union
+
+from wrappers.normalize import NormalizeObservation, NormalizeReward
+from wrappers.vectorable_wrapper import VecEnv, VecEnvObs, find_wrapper
+
+ACTIVATION: Dict[str, Type[nn.Module]] = {
+ "tanh": nn.Tanh,
+ "relu": nn.ReLU,
+}
+
+VEC_NORMALIZE_FILENAME = "vecnormalize.pkl"
+MODEL_FILENAME = "model.pth"
+NORMALIZE_OBSERVATION_FILENAME = "norm_obs.npz"
+NORMALIZE_REWARD_FILENAME = "norm_reward.npz"
+
+PolicySelf = TypeVar("PolicySelf", bound="Policy")
+
+
+class Policy(nn.Module, ABC):
+ @abstractmethod
+ def __init__(self, env: VecEnv, **kwargs) -> None:
+ super().__init__()
+ self.env = env
+ self.vec_normalize = unwrap_vec_normalize(env)
+ self.norm_observation = find_wrapper(env, NormalizeObservation)
+ self.norm_reward = find_wrapper(env, NormalizeReward)
+ self.device = None
+
+ def to(
+ self: PolicySelf,
+ device: Optional[torch.device] = None,
+ dtype: Optional[Union[torch.dtype, str]] = None,
+ non_blocking: bool = False,
+ ) -> PolicySelf:
+ super().to(device, dtype, non_blocking)
+ self.device = device
+ return self
+
+ @abstractmethod
+ def act(self, obs: VecEnvObs, deterministic: bool = True) -> np.ndarray:
+ ...
+
+ def save(self, path: str) -> None:
+ os.makedirs(path, exist_ok=True)
+
+ if self.vec_normalize:
+ self.vec_normalize.save(os.path.join(path, VEC_NORMALIZE_FILENAME))
+ if self.norm_observation:
+ self.norm_observation.save(
+ os.path.join(path, NORMALIZE_OBSERVATION_FILENAME)
+ )
+ if self.norm_reward:
+ self.norm_reward.save(os.path.join(path, NORMALIZE_REWARD_FILENAME))
+ torch.save(
+ self.state_dict(),
+ os.path.join(path, MODEL_FILENAME),
+ )
+
+ def load(self, path: str) -> None:
+ # VecNormalize load occurs in env.py
+ self.load_state_dict(
+ torch.load(os.path.join(path, MODEL_FILENAME), map_location=self.device)
+ )
+ if self.norm_observation:
+ self.norm_observation.load(
+ os.path.join(path, NORMALIZE_OBSERVATION_FILENAME)
+ )
+ if self.norm_reward:
+ self.norm_reward.load(os.path.join(path, NORMALIZE_REWARD_FILENAME))
+
+ def reset_noise(self) -> None:
+ pass
+
+ def _as_tensor(self, obs: VecEnvObs) -> torch.Tensor:
+ assert isinstance(obs, np.ndarray)
+ o = torch.as_tensor(obs)
+ if self.device is not None:
+ o = o.to(self.device)
+ return o
+
+ def num_trainable_parameters(self) -> int:
+ return sum(p.numel() for p in self.parameters() if p.requires_grad)
+
+ def num_parameters(self) -> int:
+ return sum(p.numel() for p in self.parameters())
+
+ def sync_normalization(self, destination_env) -> None:
+ current = destination_env
+ while current != current.unwrapped:
+ if isinstance(current, VecNormalize):
+ assert self.vec_normalize
+ current.ret_rms = deepcopy(self.vec_normalize.ret_rms)
+ if hasattr(self.vec_normalize, "obs_rms"):
+ current.obs_rms = deepcopy(self.vec_normalize.obs_rms)
+ elif isinstance(current, NormalizeObservation):
+ assert self.norm_observation
+ current.rms = deepcopy(self.norm_observation.rms)
+ elif isinstance(current, NormalizeReward):
+ assert self.norm_reward
+ current.rms = deepcopy(self.norm_reward.rms)
+ current = getattr(current, "venv", getattr(current, "env", current))
+ if not current:
+ raise AttributeError(
+ f"{type(current)} doesn't include env or venv attribute"
+ )
diff --git a/shared/schedule.py b/shared/schedule.py
new file mode 100644
index 0000000000000000000000000000000000000000..1461a782341eff5d89a53f16aebdc268bf9f7f52
--- /dev/null
+++ b/shared/schedule.py
@@ -0,0 +1,31 @@
+from torch.optim import Optimizer
+from typing import Callable
+
+Schedule = Callable[[float], float]
+
+
+def linear_schedule(
+ start_val: float, end_val: float, end_fraction: float = 1.0
+) -> Schedule:
+ def func(progress_fraction: float) -> float:
+ if progress_fraction >= end_fraction:
+ return end_val
+ else:
+ return start_val + (end_val - start_val) * progress_fraction / end_fraction
+
+ return func
+
+
+def constant_schedule(val: float) -> Schedule:
+ return lambda f: val
+
+
+def schedule(name: str, start_val: float) -> Schedule:
+ if name == "linear":
+ return linear_schedule(start_val, 0)
+ return constant_schedule(start_val)
+
+
+def update_learning_rate(optimizer: Optimizer, learning_rate: float) -> None:
+ for param_group in optimizer.param_groups:
+ param_group["lr"] = learning_rate
diff --git a/shared/stats.py b/shared/stats.py
new file mode 100644
index 0000000000000000000000000000000000000000..2315e6bb0de04ee56ca577cb7444f17e93e88fc0
--- /dev/null
+++ b/shared/stats.py
@@ -0,0 +1,160 @@
+import numpy as np
+
+from dataclasses import dataclass
+from torch.utils.tensorboard.writer import SummaryWriter
+from typing import Dict, List, Optional, Sequence, Union, TypeVar
+
+
+@dataclass
+class Episode:
+ score: float = 0
+ length: int = 0
+
+
+StatisticSelf = TypeVar("StatisticSelf", bound="Statistic")
+
+
+@dataclass
+class Statistic:
+ values: np.ndarray
+ round_digits: int = 2
+
+ @property
+ def mean(self) -> float:
+ return np.mean(self.values).item()
+
+ @property
+ def std(self) -> float:
+ return np.std(self.values).item()
+
+ @property
+ def min(self) -> float:
+ return np.min(self.values).item()
+
+ @property
+ def max(self) -> float:
+ return np.max(self.values).item()
+
+ def sum(self) -> float:
+ return np.sum(self.values).item()
+
+ def __len__(self) -> int:
+ return len(self.values)
+
+ def _diff(self: StatisticSelf, o: StatisticSelf) -> float:
+ return (self.mean - self.std) - (o.mean - o.std)
+
+ def __gt__(self: StatisticSelf, o: StatisticSelf) -> bool:
+ return self._diff(o) > 0
+
+ def __ge__(self: StatisticSelf, o: StatisticSelf) -> bool:
+ return self._diff(o) >= 0
+
+ def __repr__(self) -> str:
+ mean = round(self.mean, self.round_digits)
+ std = round(self.std, self.round_digits)
+ if self.round_digits == 0:
+ mean = int(mean)
+ std = int(std)
+ return f"{mean} +/- {std}"
+
+ def to_dict(self) -> Dict[str, float]:
+ return {
+ "mean": self.mean,
+ "std": self.std,
+ "min": self.min,
+ "max": self.max,
+ }
+
+
+EpisodesStatsSelf = TypeVar("EpisodesStatsSelf", bound="EpisodesStats")
+
+
+class EpisodesStats:
+ episodes: Sequence[Episode]
+ simple: bool
+ score: Statistic
+ length: Statistic
+
+ def __init__(self, episodes: Sequence[Episode], simple: bool = False) -> None:
+ self.episodes = episodes
+ self.simple = simple
+ self.score = Statistic(np.array([e.score for e in episodes]))
+ self.length = Statistic(np.array([e.length for e in episodes]), round_digits=0)
+
+ def __gt__(self: EpisodesStatsSelf, o: EpisodesStatsSelf) -> bool:
+ return self.score > o.score
+
+ def __ge__(self: EpisodesStatsSelf, o: EpisodesStatsSelf) -> bool:
+ return self.score >= o.score
+
+ def __repr__(self) -> str:
+ return (
+ f"Score: {self.score} ({round(self.score.mean - self.score.std, 2)}) | "
+ f"Length: {self.length}"
+ )
+
+ def __len__(self) -> int:
+ return len(self.episodes)
+
+ def _asdict(self) -> dict:
+ return {
+ "n_episodes": len(self.episodes),
+ "score": self.score.to_dict(),
+ "length": self.length.to_dict(),
+ }
+
+ def write_to_tensorboard(
+ self, tb_writer: SummaryWriter, main_tag: str, global_step: Optional[int] = None
+ ) -> None:
+ stats = {"mean": self.score.mean}
+ if not self.simple:
+ stats.update(
+ {
+ "min": self.score.min,
+ "max": self.score.max,
+ "result": self.score.mean - self.score.std,
+ "n_episodes": len(self.episodes),
+ "length": self.length.mean,
+ }
+ )
+ for name, value in stats.items():
+ tb_writer.add_scalar(f"{main_tag}/{name}", value, global_step=global_step)
+
+
+class EpisodeAccumulator:
+ def __init__(self, num_envs: int):
+ self._episodes = []
+ self.current_episodes = [Episode() for _ in range(num_envs)]
+
+ @property
+ def episodes(self) -> List[Episode]:
+ return self._episodes
+
+ def step(self, reward: np.ndarray, done: np.ndarray) -> None:
+ for idx, current in enumerate(self.current_episodes):
+ current.score += reward[idx]
+ current.length += 1
+ if done[idx]:
+ self._episodes.append(current)
+ self.current_episodes[idx] = Episode()
+ self.on_done(idx, current)
+
+ def __len__(self) -> int:
+ return len(self.episodes)
+
+ def on_done(self, ep_idx: int, episode: Episode) -> None:
+ pass
+
+ def stats(self) -> EpisodesStats:
+ return EpisodesStats(self.episodes)
+
+
+def log_scalars(
+ tb_writer: SummaryWriter,
+ main_tag: str,
+ tag_scalar_dict: Dict[str, Union[int, float]],
+ global_step: int,
+) -> None:
+ for tag, value in tag_scalar_dict.items():
+ tb_writer.add_scalar(f"{main_tag}/{tag}", value, global_step)
diff --git a/shared/trajectory.py b/shared/trajectory.py
new file mode 100644
index 0000000000000000000000000000000000000000..def6fc8e6876dafe777eb23d9d2e57ba93db5b45
--- /dev/null
+++ b/shared/trajectory.py
@@ -0,0 +1,81 @@
+import numpy as np
+
+from dataclasses import dataclass, field
+from typing import Generic, List, Optional, Type, TypeVar
+
+from wrappers.vectorable_wrapper import VecEnvObs
+
+
+@dataclass
+class Trajectory:
+ obs: List[np.ndarray] = field(default_factory=list)
+ act: List[np.ndarray] = field(default_factory=list)
+ next_obs: Optional[np.ndarray] = None
+ rew: List[float] = field(default_factory=list)
+ terminated: bool = False
+ v: List[float] = field(default_factory=list)
+
+ def add(
+ self,
+ obs: np.ndarray,
+ act: np.ndarray,
+ next_obs: np.ndarray,
+ rew: float,
+ terminated: bool,
+ v: float,
+ ):
+ self.obs.append(obs)
+ self.act.append(act)
+ self.next_obs = next_obs if not terminated else None
+ self.rew.append(rew)
+ self.terminated = terminated
+ self.v.append(v)
+
+ def __len__(self) -> int:
+ return len(self.obs)
+
+
+T = TypeVar("T", bound=Trajectory)
+
+
+class TrajectoryAccumulator(Generic[T]):
+ def __init__(self, num_envs: int, trajectory_class: Type[T] = Trajectory) -> None:
+ self.num_envs = num_envs
+ self.trajectory_class = trajectory_class
+
+ self._trajectories = []
+ self._current_trajectories = [trajectory_class() for _ in range(num_envs)]
+
+ def step(
+ self,
+ obs: VecEnvObs,
+ action: np.ndarray,
+ next_obs: VecEnvObs,
+ reward: np.ndarray,
+ done: np.ndarray,
+ val: np.ndarray,
+ *args,
+ ) -> None:
+ assert isinstance(obs, np.ndarray)
+ assert isinstance(next_obs, np.ndarray)
+ for i, args in enumerate(zip(obs, action, next_obs, reward, done, val, *args)):
+ trajectory = self._current_trajectories[i]
+ # TODO: Eventually take advantage of terminated/truncated differentiation in
+ # later versions of gym.
+ trajectory.add(*args)
+ if done[i]:
+ self._trajectories.append(trajectory)
+ self._current_trajectories[i] = self.trajectory_class()
+ self.on_done(i, trajectory)
+
+ @property
+ def all_trajectories(self) -> List[T]:
+ return self._trajectories + list(
+ filter(lambda t: len(t), self._current_trajectories)
+ )
+
+ def n_timesteps(self) -> int:
+ return sum(len(t) for t in self.all_trajectories)
+
+ def on_done(self, env_idx: int, trajectory: T) -> None:
+ pass
diff --git a/train.py b/train.py
new file mode 100644
index 0000000000000000000000000000000000000000..9df2a92d28e19ff9e2c7bab9742d05d3b4533638
--- /dev/null
+++ b/train.py
@@ -0,0 +1,80 @@
+# Support for PyTorch mps mode (https://pytorch.org/docs/stable/notes/mps.html)
+import os
+
+os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
+
+import itertools
+
+from argparse import Namespace
+from multiprocessing import Pool
+from typing import Any, Dict
+
+from runner.running_utils import base_parser
+from runner.train import train, TrainArgs
+
+
+def args_dict(algo: str, env: str, seed: str, args: Namespace) -> Dict[str, Any]:
+ d = vars(args).copy()
+ d.update(
+ {
+ "algo": algo,
+ "env": env,
+ "seed": seed,
+ }
+ )
+ return d
+
+
+if __name__ == "__main__":
+ parser = base_parser()
+ parser.add_argument(
+ "--wandb-project-name",
+ type=str,
+ default="rl-algo-impls",
+ help="WandB project namme to upload training data to. If none, won't upload.",
+ )
+ parser.add_argument(
+ "--wandb-entity",
+ type=str,
+ default=None,
+ help="WandB team of project. None uses default entity",
+ )
+ parser.add_argument(
+ "--wandb-tags", type=str, nargs="*", help="WandB tags to add to run"
+ )
+ parser.add_argument(
+ "--pool-size", type=int, default=1, help="Simultaneous training jobs to run"
+ )
+ parser.set_defaults(
+ algo="ppo",
+ env="MountainCarContinuous-v0",
+ seed=[1, 2, 3],
+ pool_size=3,
+ )
+ args = parser.parse_args()
+ print(args)
+
+ if args.pool_size == 1:
+ from pyvirtualdisplay.display import Display
+
+ virtual_display = Display(visible=False, size=(1400, 900))
+ virtual_display.start()
+
+ # pool_size isn't a TrainArg so must be removed from args
+ pool_size = min(args.pool_size, len(args.seed))
+ delattr(args, "pool_size")
+
+ algos = args.algo if isinstance(args.algo, list) else [args.algo]
+ envs = args.env if isinstance(args.env, list) else [args.env]
+ seeds = args.seed if isinstance(args.seed, list) else [args.seed]
+ if all(len(arg) == 1 for arg in [algos, envs, seeds]):
+ train(TrainArgs(**args_dict(algos[0], envs[0], seeds[0], args)))
+ else:
+ # Force a new process for each job to get around wandb not allowing more than one
+ # wandb.tensorboard.patch call per process.
+ with Pool(pool_size, maxtasksperchild=1) as p:
+ train_args = [
+ TrainArgs(**args_dict(algo, env, seed, args))
+ for algo, env, seed in itertools.product(algos, envs, seeds)
+ ]
+ p.map(train, train_args)
diff --git a/vpg/policy.py b/vpg/policy.py
new file mode 100644
index 0000000000000000000000000000000000000000..2e2553fbf0ab40f28509bb4d9d99084e2201aa96
--- /dev/null
+++ b/vpg/policy.py
@@ -0,0 +1,128 @@
+import numpy as np
+import torch
+import torch.nn as nn
+
+from typing import Optional, Sequence
+
+from shared.module.feature_extractor import FeatureExtractor
+from shared.policy.actor import (
+ PiForward,
+ Actor,
+ StateDependentNoiseActorHead,
+ actor_head,
+)
+from shared.policy.critic import CriticHead
+from shared.policy.on_policy import (
+ Step,
+ ACForward,
+ OnPolicy,
+ clamp_actions,
+ default_hidden_sizes,
+)
+from shared.policy.policy import ACTIVATION
+from wrappers.vectorable_wrapper import VecEnv, VecEnvObs, single_observation_space, single_action_space
+
+PI_FILE_NAME = "pi.pt"
+V_FILE_NAME = "v.pt"
+
+
+class VPGActor(Actor):
+ def __init__(self, feature_extractor: FeatureExtractor, head: Actor) -> None:
+ super().__init__()
+ self.feature_extractor = feature_extractor
+ self.head = head
+
+ def forward(self, obs: torch.Tensor, a: Optional[torch.Tensor] = None) -> PiForward:
+ fe = self.feature_extractor(obs)
+ return self.head(fe, a)
+
+
+class VPGActorCritic(OnPolicy):
+ def __init__(
+ self,
+ env: VecEnv,
+ hidden_sizes: Optional[Sequence[int]] = None,
+ init_layers_orthogonal: bool = True,
+ activation_fn: str = "tanh",
+ log_std_init: float = -0.5,
+ use_sde: bool = False,
+ full_std: bool = True,
+ squash_output: bool = False,
+ **kwargs,
+ ) -> None:
+ super().__init__(env, **kwargs)
+ activation = ACTIVATION[activation_fn]
+ obs_space = single_observation_space(env)
+ self.action_space = single_action_space(env)
+ self.use_sde = use_sde
+ self.squash_output = squash_output
+
+ hidden_sizes = (
+ hidden_sizes
+ if hidden_sizes is not None
+ else default_hidden_sizes(obs_space)
+ )
+
+ pi_feature_extractor = FeatureExtractor(
+ obs_space, activation, init_layers_orthogonal=init_layers_orthogonal
+ )
+ pi_head = actor_head(
+ self.action_space,
+ (pi_feature_extractor.out_dim,) + tuple(hidden_sizes),
+ init_layers_orthogonal,
+ activation,
+ log_std_init=log_std_init,
+ use_sde=use_sde,
+ full_std=full_std,
+ squash_output=squash_output,
+ )
+ self.pi = VPGActor(pi_feature_extractor, pi_head)
+
+ v_feature_extractor = FeatureExtractor(
+ obs_space, activation, init_layers_orthogonal=init_layers_orthogonal
+ )
+ v_head = CriticHead(
+ (v_feature_extractor.out_dim,) + tuple(hidden_sizes),
+ activation=activation,
+ init_layers_orthogonal=init_layers_orthogonal,
+ )
+ self.v = nn.Sequential(v_feature_extractor, v_head)
+
+ def value(self, obs: VecEnvObs) -> np.ndarray:
+ o = self._as_tensor(obs)
+ with torch.no_grad():
+ v = self.v(o)
+ return v.cpu().numpy()
+
+ def step(self, obs: VecEnvObs) -> Step:
+ o = self._as_tensor(obs)
+ with torch.no_grad():
+ pi, _, _ = self.pi(o)
+ a = pi.sample()
+ logp_a = pi.log_prob(a)
+
+ v = self.v(o)
+
+ a_np = a.cpu().numpy()
+ clamped_a_np = clamp_actions(a_np, self.action_space, self.squash_output)
+ return Step(a_np, v.cpu().numpy(), logp_a.cpu().numpy(), clamped_a_np)
+
+ def act(self, obs: np.ndarray, deterministic: bool = True) -> np.ndarray:
+ if not deterministic:
+ return self.step(obs).clamped_a
+ else:
+ o = self._as_tensor(obs)
+ with torch.no_grad():
+ pi, _, _ = self.pi(o)
+ a = pi.mode
+ return clamp_actions(a.cpu().numpy(), self.action_space, self.squash_output)
+
+ def load(self, path: str) -> None:
+ super().load(path)
+ self.reset_noise()
+
+ def reset_noise(self, batch_size: Optional[int] = None) -> None:
+ if isinstance(self.pi.head, StateDependentNoiseActorHead):
+ self.pi.head.sample_weights(
+ batch_size=batch_size if batch_size else self.env.num_envs
+ )
diff --git a/vpg/vpg.py b/vpg/vpg.py
new file mode 100644
index 0000000000000000000000000000000000000000..6f47ebe532f37270eab10e7c388bc78754d01bc3
--- /dev/null
+++ b/vpg/vpg.py
@@ -0,0 +1,168 @@
+import numpy as np
+import torch
+import torch.nn as nn
+
+from collections import defaultdict
+from dataclasses import dataclass, asdict
+from torch.optim import Adam
+from torch.utils.tensorboard.writer import SummaryWriter
+from typing import Optional, Sequence, TypeVar
+
+from shared.algorithm import Algorithm
+from shared.callbacks.callback import Callback
+from shared.gae import compute_rtg_and_advantage, compute_advantage
+from shared.trajectory import Trajectory, TrajectoryAccumulator
+from vpg.policy import VPGActorCritic
+from wrappers.vectorable_wrapper import VecEnv
+
+
+@dataclass
+class TrainEpochStats:
+ pi_loss: float
+ entropy_loss: float
+ v_loss: float
+ envs_with_done: int = 0
+ episodes_done: int = 0
+
+ def write_to_tensorboard(self, tb_writer: SummaryWriter, global_step: int) -> None:
+ for name, value in asdict(self).items():
+ tb_writer.add_scalar(f"losses/{name}", value, global_step=global_step)
+
+
+class VPGTrajectoryAccumulator(TrajectoryAccumulator):
+ def __init__(self, num_envs: int) -> None:
+ super().__init__(num_envs, trajectory_class=Trajectory)
+ self.completed_per_env: defaultdict[int, int] = defaultdict(int)
+
+ def on_done(self, env_idx: int, trajectory: Trajectory) -> None:
+ self.completed_per_env[env_idx] += 1
+
+
+VanillaPolicyGradientSelf = TypeVar(
+ "VanillaPolicyGradientSelf", bound="VanillaPolicyGradient"
+)
+
+
+class VanillaPolicyGradient(Algorithm):
+ def __init__(
+ self,
+ policy: VPGActorCritic,
+ env: VecEnv,
+ device: torch.device,
+ tb_writer: SummaryWriter,
+ gamma: float = 0.99,
+ pi_lr: float = 3e-4,
+ val_lr: float = 1e-3,
+ train_v_iters: int = 80,
+ gae_lambda: float = 0.97,
+ max_grad_norm: float = 10.0,
+ n_steps: int = 4_000,
+ sde_sample_freq: int = -1,
+ update_rtg_between_v_iters: bool = False,
+ ent_coef: float = 0.0,
+ ) -> None:
+ super().__init__(policy, env, device, tb_writer)
+ self.policy = policy
+
+ self.gamma = gamma
+ self.gae_lambda = gae_lambda
+ self.pi_optim = Adam(self.policy.pi.parameters(), lr=pi_lr)
+ self.val_optim = Adam(self.policy.v.parameters(), lr=val_lr)
+ self.max_grad_norm = max_grad_norm
+
+ self.n_steps = n_steps
+ self.train_v_iters = train_v_iters
+ self.sde_sample_freq = sde_sample_freq
+ self.update_rtg_between_v_iters = update_rtg_between_v_iters
+
+ self.ent_coef = ent_coef
+
+ def learn(
+ self: VanillaPolicyGradientSelf,
+ total_timesteps: int,
+ callback: Optional[Callback] = None,
+ ) -> VanillaPolicyGradientSelf:
+ timesteps_elapsed = 0
+ epoch_cnt = 0
+ while timesteps_elapsed < total_timesteps:
+ epoch_cnt += 1
+ accumulator = self._collect_trajectories()
+ epoch_stats = self.train(accumulator.all_trajectories)
+ epoch_stats.envs_with_done = len(accumulator.completed_per_env)
+ epoch_stats.episodes_done = sum(accumulator.completed_per_env.values())
+ epoch_steps = accumulator.n_timesteps()
+ timesteps_elapsed += epoch_steps
+ epoch_stats.write_to_tensorboard(
+ self.tb_writer, global_step=timesteps_elapsed
+ )
+ print(
+ " | ".join(
+ [
+ f"Epoch: {epoch_cnt}",
+ f"Pi Loss: {round(epoch_stats.pi_loss, 2)}",
+ f"Epoch Loss: {round(epoch_stats.entropy_loss, 2)}",
+ f"V Loss: {round(epoch_stats.v_loss, 2)}",
+ f"Total Steps: {timesteps_elapsed}",
+ ]
+ )
+ )
+ if callback:
+ callback.on_step(timesteps_elapsed=epoch_steps)
+ return self
+
+ def train(self, trajectories: Sequence[Trajectory]) -> TrainEpochStats:
+ self.policy.train()
+ obs = torch.as_tensor(
+ np.concatenate([np.array(t.obs) for t in trajectories]), device=self.device
+ )
+ act = torch.as_tensor(
+ np.concatenate([np.array(t.act) for t in trajectories]), device=self.device
+ )
+ rtg, adv = compute_rtg_and_advantage(
+ trajectories, self.policy, self.gamma, self.gae_lambda, self.device
+ )
+
+ _, logp, entropy = self.policy.pi(obs, act)
+ pi_loss = -(logp * adv).mean()
+ entropy_loss = entropy.mean()
+
+ actor_loss = pi_loss - self.ent_coef * entropy_loss
+
+ self.pi_optim.zero_grad()
+ actor_loss.backward()
+ nn.utils.clip_grad_norm_(self.policy.pi.parameters(), self.max_grad_norm)
+ self.pi_optim.step()
+
+ v_loss = 0
+ for _ in range(self.train_v_iters):
+ if self.update_rtg_between_v_iters:
+ rtg = compute_advantage(
+ trajectories, self.policy, self.gamma, self.gae_lambda, self.device
+ )
+ v = self.policy.v(obs)
+ v_loss = ((v - rtg) ** 2).mean()
+
+ self.val_optim.zero_grad()
+ v_loss.backward()
+ nn.utils.clip_grad_norm_(self.policy.v.parameters(), self.max_grad_norm)
+ self.val_optim.step()
+
+ return TrainEpochStats(
+ pi_loss.item(),
+ entropy_loss.item(),
+ v_loss.item(), # type: ignore
+ )
+
+ def _collect_trajectories(self) -> VPGTrajectoryAccumulator:
+ self.policy.eval()
+ obs = self.env.reset()
+ accumulator = VPGTrajectoryAccumulator(self.env.num_envs)
+ self.policy.reset_noise()
+ for i in range(self.n_steps):
+ if self.sde_sample_freq > 0 and i > 0 and i % self.sde_sample_freq == 0:
+ self.policy.reset_noise()
+ action, value, _, clamped_action = self.policy.step(obs)
+ next_obs, reward, done, _ = self.env.step(clamped_action)
+ accumulator.step(obs, action, next_obs, reward, done, value)
+ obs = next_obs
+ return accumulator
diff --git a/wrappers/atari_wrappers.py b/wrappers/atari_wrappers.py
new file mode 100644
index 0000000000000000000000000000000000000000..6b866eb9c0a80d7428de3468b8bb33d1a3c4d01a
--- /dev/null
+++ b/wrappers/atari_wrappers.py
@@ -0,0 +1,84 @@
+import gym
+import numpy as np
+
+from typing import Any, Dict, Tuple, Union
+
+from wrappers.vectorable_wrapper import VecotarableWrapper
+
+ObsType = Union[np.ndarray, dict]
+ActType = Union[int, float, np.ndarray, dict]
+
+
+class EpisodicLifeEnv(VecotarableWrapper):
+ def __init__(self, env: gym.Env, training: bool = True, noop_act: int = 0) -> None:
+ super().__init__(env)
+ self.training = training
+ self.noop_act = noop_act
+ self.life_done_continue = False
+ self.lives = 0
+
+ def step(self, action: ActType) -> Tuple[ObsType, float, bool, Dict[str, Any]]:
+ obs, rew, done, info = self.env.step(action)
+ new_lives = self.env.unwrapped.ale.lives()
+ self.life_done_continue = new_lives != self.lives and not done
+ # Only if training should life-end be marked as done
+ if self.training and 0 < new_lives < self.lives:
+ done = True
+ self.lives = new_lives
+ return obs, rew, done, info
+
+ def reset(self, **kwargs) -> ObsType:
+ # If life_done_continue (but not game over), then a reset should just allow the
+ # game to progress to the next life.
+ if self.training and self.life_done_continue:
+ obs, _, _, _ = self.env.step(self.noop_act)
+ else:
+ obs = self.env.reset(**kwargs)
+ self.lives = self.env.unwrapped.ale.lives()
+ return obs
+
+
+class FireOnLifeStarttEnv(VecotarableWrapper):
+ def __init__(self, env: gym.Env, fire_act: int = 1) -> None:
+ super().__init__(env)
+ self.fire_act = fire_act
+ action_meanings = env.unwrapped.get_action_meanings()
+ assert action_meanings[fire_act] == "FIRE"
+ assert len(action_meanings) >= 3
+ self.lives = 0
+ self.fire_on_next_step = True
+
+ def step(self, action: ActType) -> Tuple[ObsType, float, bool, Dict[str, Any]]:
+ if self.fire_on_next_step:
+ action = self.fire_act
+ self.fire_on_next_step = False
+ obs, rew, done, info = self.env.step(action)
+ new_lives = self.env.unwrapped.ale.lives()
+ if 0 < new_lives < self.lives and not done:
+ self.fire_on_next_step = True
+ self.lives = new_lives
+ return obs, rew, done, info
+
+ def reset(self, **kwargs) -> ObsType:
+ self.env.reset(**kwargs)
+ obs, _, done, _ = self.env.step(self.fire_act)
+ if done:
+ self.env.reset(**kwargs)
+ obs, _, done, _ = self.env.step(2)
+ if done:
+ self.env.reset(**kwargs)
+ self.fire_on_next_step = False
+ return obs
+
+
+class ClipRewardEnv(VecotarableWrapper):
+ def __init__(self, env: gym.Env, training: bool = True) -> None:
+ super().__init__(env)
+ self.training = training
+
+ def step(self, action: ActType) -> Tuple[ObsType, float, bool, Dict[str, Any]]:
+ obs, rew, done, info = self.env.step(action)
+ if self.training:
+ info["unclipped_reward"] = rew
+ rew = np.sign(rew)
+ return obs, rew, done, info
diff --git a/wrappers/episode_record_video.py b/wrappers/episode_record_video.py
new file mode 100644
index 0000000000000000000000000000000000000000..13aa32813b9318a549de799980a851a168821254
--- /dev/null
+++ b/wrappers/episode_record_video.py
@@ -0,0 +1,75 @@
+import gym
+import numpy as np
+
+from gym.wrappers.monitoring.video_recorder import VideoRecorder
+from typing import Tuple, Union
+
+from wrappers.vectorable_wrapper import VecotarableWrapper
+
+ObsType = Union[np.ndarray, dict]
+ActType = Union[int, float, np.ndarray, dict]
+
+
+class EpisodeRecordVideo(VecotarableWrapper):
+ def __init__(
+ self,
+ env: gym.Env,
+ video_path_prefix: str,
+ step_increment: int = 1,
+ video_step_interval: int = 1_000_000,
+ max_video_length: int = 3600,
+ ) -> None:
+ super().__init__(env)
+ self.video_path_prefix = video_path_prefix
+ self.step_increment = step_increment
+ self.video_step_interval = video_step_interval
+ self.max_video_length = max_video_length
+ self.total_steps = 0
+ self.next_record_video_step = 0
+ self.video_recorder = None
+ self.recorded_frames = 0
+
+ def step(self, action: ActType) -> Tuple[ObsType, float, bool, dict]:
+ obs, rew, done, info = self.env.step(action)
+ self.total_steps += self.step_increment
+ # Using first env to record episodes
+ if self.video_recorder:
+ self.video_recorder.capture_frame()
+ self.recorded_frames += 1
+ if info.get("episode"):
+ episode_info = {
+ k: v.item() if hasattr(v, "item") else v
+ for k, v in info["episode"].items()
+ }
+ self.video_recorder.metadata["episode"] = episode_info
+ if self.recorded_frames > self.max_video_length:
+ self._close_video_recorder()
+ return obs, rew, done, info
+
+ def reset(self, **kwargs) -> ObsType:
+ obs = self.env.reset(**kwargs)
+ if self.video_recorder:
+ self._close_video_recorder()
+ elif self.total_steps >= self.next_record_video_step:
+ self._start_video_recorder()
+ return obs
+
+ def _start_video_recorder(self) -> None:
+ self._close_video_recorder()
+
+ video_path = f"{self.video_path_prefix}-{self.next_record_video_step}"
+ self.video_recorder = VideoRecorder(
+ self.env,
+ base_path=video_path,
+ metadata={"step": self.total_steps},
+ )
+
+ self.video_recorder.capture_frame()
+ self.recorded_frames = 1
+ self.next_record_video_step += self.video_step_interval
+
+ def _close_video_recorder(self) -> None:
+ if self.video_recorder:
+ self.video_recorder.close()
+ self.video_recorder = None
+ self.recorded_frames = 0
diff --git a/wrappers/episode_stats_writer.py b/wrappers/episode_stats_writer.py
new file mode 100644
index 0000000000000000000000000000000000000000..05bcaf8a697097c987d36e11035da65b37b93a40
--- /dev/null
+++ b/wrappers/episode_stats_writer.py
@@ -0,0 +1,66 @@
+import numpy as np
+
+from collections import deque
+from torch.utils.tensorboard.writer import SummaryWriter
+from typing import Any, Dict, List
+
+from shared.stats import Episode, EpisodesStats
+from wrappers.vectorable_wrapper import VecotarableWrapper, VecEnvStepReturn, VecEnvObs
+
+
+class EpisodeStatsWriter(VecotarableWrapper):
+ def __init__(
+ self,
+ env,
+ tb_writer: SummaryWriter,
+ training: bool = True,
+ rolling_length=100,
+ ):
+ super().__init__(env)
+ self.training = training
+ self.tb_writer = tb_writer
+ self.rolling_length = rolling_length
+ self.episodes = deque(maxlen=rolling_length)
+ self.total_steps = 0
+ self.episode_cnt = 0
+ self.last_episode_cnt_print = 0
+
+ def step(self, actions: np.ndarray) -> VecEnvStepReturn:
+ obs, rews, dones, infos = self.env.step(actions)
+ self._record_stats(infos)
+ return obs, rews, dones, infos
+
+ # Support for stable_baselines3.common.vec_env.VecEnvWrapper
+ def step_wait(self) -> VecEnvStepReturn:
+ obs, rews, dones, infos = self.env.step_wait()
+ self._record_stats(infos)
+ return obs, rews, dones, infos
+
+ def _record_stats(self, infos: List[Dict[str, Any]]) -> None:
+ self.total_steps += getattr(self.env, "num_envs", 1)
+ step_episodes = []
+ for info in infos:
+ ep_info = info.get("episode")
+ if ep_info:
+ episode = Episode(ep_info["r"], ep_info["l"])
+ step_episodes.append(episode)
+ self.episodes.append(episode)
+ if step_episodes:
+ tag = "train" if self.training else "eval"
+ step_stats = EpisodesStats(step_episodes, simple=True)
+ step_stats.write_to_tensorboard(self.tb_writer, tag, self.total_steps)
+ rolling_stats = EpisodesStats(self.episodes)
+ rolling_stats.write_to_tensorboard(
+ self.tb_writer, f"{tag}_rolling", self.total_steps
+ )
+ self.episode_cnt += len(step_episodes)
+ if self.episode_cnt >= self.last_episode_cnt_print + self.rolling_length:
+ print(
+ f"Episode: {self.episode_cnt} | "
+ f"Steps: {self.total_steps} | "
+ f"{rolling_stats}"
+ )
+ self.last_episode_cnt_print += self.rolling_length
+
+ def reset(self) -> VecEnvObs:
+ return self.env.reset()
diff --git a/wrappers/initial_step_truncate_wrapper.py b/wrappers/initial_step_truncate_wrapper.py
new file mode 100644
index 0000000000000000000000000000000000000000..ffb82afdeb7b2da98ee3386284016917b8be588b
--- /dev/null
+++ b/wrappers/initial_step_truncate_wrapper.py
@@ -0,0 +1,27 @@
+import gym
+import numpy as np
+
+from typing import Any, Dict, Tuple, Union
+
+from wrappers.vectorable_wrapper import VecotarableWrapper
+
+ObsType = Union[np.ndarray, dict]
+ActType = Union[int, float, np.ndarray, dict]
+
+
+class InitialStepTruncateWrapper(VecotarableWrapper):
+ def __init__(self, env: gym.Env, initial_steps_to_truncate: int) -> None:
+ super().__init__(env)
+ self.initial_steps_to_truncate = initial_steps_to_truncate
+ self.initialized = initial_steps_to_truncate == 0
+ self.steps = 0
+
+ def step(self, action: ActType) -> Tuple[ObsType, float, bool, Dict[str, Any]]:
+ obs, rew, done, info = self.env.step(action)
+ if not self.initialized:
+ self.steps += 1
+ if self.steps >= self.initial_steps_to_truncate:
+ print(f"Truncation at {self.steps} steps")
+ done = True
+ self.initialized = True
+ return obs, rew, done, info
diff --git a/wrappers/is_vector_env.py b/wrappers/is_vector_env.py
new file mode 100644
index 0000000000000000000000000000000000000000..668890fee7b02686f270b5c27ee2100f4331b827
--- /dev/null
+++ b/wrappers/is_vector_env.py
@@ -0,0 +1,13 @@
+from typing import Any
+
+from wrappers.vectorable_wrapper import VecotarableWrapper
+
+
+class IsVectorEnv(VecotarableWrapper):
+ """
+ Override to set properties to match gym.vector.VectorEnv
+ """
+
+ def __init__(self, env: Any) -> None:
+ super().__init__(env)
+ self.is_vector_env = True
diff --git a/wrappers/no_reward_timeout.py b/wrappers/no_reward_timeout.py
new file mode 100644
index 0000000000000000000000000000000000000000..fbefe131a83a7b58dfa8fa404095aba2ec5513c1
--- /dev/null
+++ b/wrappers/no_reward_timeout.py
@@ -0,0 +1,65 @@
+import gym
+import numpy as np
+
+from typing import Optional, Tuple, Union
+
+from wrappers.vectorable_wrapper import VecotarableWrapper
+
+ObsType = Union[np.ndarray, dict]
+ActType = Union[int, float, np.ndarray, dict]
+
+
+class NoRewardTimeout(VecotarableWrapper):
+ def __init__(
+ self, env: gym.Env, n_timeout_steps: int, n_fire_steps: Optional[int] = None
+ ) -> None:
+ super().__init__(env)
+ self.n_timeout_steps = n_timeout_steps
+ self.n_fire_steps = n_fire_steps
+
+ self.fire_act = None
+ if n_fire_steps is not None:
+ action_meanings = env.unwrapped.get_action_meanings()
+ assert "FIRE" in action_meanings
+ self.fire_act = action_meanings.index("FIRE")
+
+ self.steps_since_reward = 0
+
+ self.episode_score = 0
+ self.episode_step_idx = 0
+
+ def step(self, action: ActType) -> Tuple[ObsType, float, bool, dict]:
+ if self.steps_since_reward == self.n_fire_steps:
+ assert self.fire_act is not None
+ self.print_intervention("Force fire action")
+ action = self.fire_act
+ obs, rew, done, info = self.env.step(action)
+
+ self.episode_score += rew
+ self.episode_step_idx += 1
+
+ if rew != 0 or done:
+ self.steps_since_reward = 0
+ else:
+ self.steps_since_reward += 1
+ if self.steps_since_reward >= self.n_timeout_steps:
+ self.print_intervention("Early terminate")
+ done = True
+
+ return obs, rew, done, info
+
+ def reset(self, **kwargs) -> ObsType:
+ self._reset_state()
+ return self.env.reset(**kwargs)
+
+ def _reset_state(self) -> None:
+ self.steps_since_reward = 0
+ self.episode_score = 0
+ self.episode_step_idx = 0
+
+ def print_intervention(self, tag: str) -> None:
+ print(
+ f"{self.__class__.__name__}: {tag} | "
+ f"Score: {self.episode_score} | "
+ f"Length: {self.episode_step_idx}"
+ )
diff --git a/wrappers/noop_env_seed.py b/wrappers/noop_env_seed.py
new file mode 100644
index 0000000000000000000000000000000000000000..027b56d35be750d0d765e6af94ce4fd3ead0544f
--- /dev/null
+++ b/wrappers/noop_env_seed.py
@@ -0,0 +1,11 @@
+from typing import List, Optional
+
+from wrappers.vectorable_wrapper import VecotarableWrapper
+
+class NoopEnvSeed(VecotarableWrapper):
+ """
+ Wrapper to stop a seed call going to the underlying environment.
+ """
+
+ def seed(self, seed: Optional[int] = None) -> Optional[List[int]]:
+ return None
diff --git a/wrappers/normalize.py b/wrappers/normalize.py
new file mode 100644
index 0000000000000000000000000000000000000000..550762b7ab729f41ae0cb8c84f949d3ddf4d56dd
--- /dev/null
+++ b/wrappers/normalize.py
@@ -0,0 +1,140 @@
+import gym
+import numpy as np
+
+from numpy.typing import NDArray
+from typing import Tuple
+
+from wrappers.vectorable_wrapper import (
+ VecotarableWrapper,
+ single_observation_space,
+)
+
+
+class RunningMeanStd:
+ def __init__(self, episilon: float = 1e-4, shape: Tuple[int, ...]=()) -> None:
+ self.mean = np.zeros(shape, np.float64)
+ self.var = np.ones(shape, np.float64)
+ self.count = episilon
+
+ def update(self, x: NDArray) -> None:
+ batch_mean = np.mean(x, axis=0)
+ batch_var = np.var(x, axis=0)
+ batch_count = x.shape[0]
+
+ delta = batch_mean - self.mean
+ total_count = self.count + batch_count
+
+ self.mean += delta * batch_count / total_count
+
+ m_a = self.var * self.count
+ m_b = batch_var * batch_count
+ M2 = m_a + m_b + np.square(delta) * self.count * batch_count / total_count
+ self.var = M2 / total_count
+ self.count = total_count
+
+
+class NormalizeObservation(VecotarableWrapper):
+ def __init__(
+ self,
+ env: gym.Env,
+ training: bool = True,
+ epsilon: float = 1e-8,
+ clip: float = 10.0,
+ ) -> None:
+ super().__init__(env)
+ self.rms = RunningMeanStd(shape=single_observation_space(env).shape)
+ self.training = training
+ self.epsilon = epsilon
+ self.clip = clip
+
+ def step(self, action):
+ obs, reward, done, info = self.env.step(action)
+ return self.normalize(obs), reward, done, info
+
+ def reset(self, **kwargs):
+ obs = self.env.reset(**kwargs)
+ return self.normalize(obs)
+
+ def normalize(self, obs: NDArray) -> NDArray:
+ obs_array = np.array([obs]) if not self.is_vector_env else obs
+ if self.training:
+ self.rms.update(obs_array)
+ normalized = np.clip(
+ (obs_array - self.rms.mean) / np.sqrt(self.rms.var + self.epsilon),
+ -self.clip,
+ self.clip,
+ )
+ return normalized[0] if not self.is_vector_env else normalized
+
+ def save(self, path: str) -> None:
+ np.savez_compressed(
+ path,
+ mean=self.rms.mean,
+ var=self.rms.var,
+ count=self.rms.count,
+ )
+
+ def load(self, path: str) -> None:
+ data = np.load(path)
+ self.rms.mean = data["mean"]
+ self.rms.var = data["var"]
+ self.rms.count = data["count"]
+
+
+class NormalizeReward(VecotarableWrapper):
+ def __init__(
+ self,
+ env: gym.Env,
+ training: bool = True,
+ gamma: float = 0.99,
+ epsilon: float = 1e-8,
+ clip: float = 10.0,
+ ) -> None:
+ super().__init__(env)
+ self.rms = RunningMeanStd(shape=())
+ self.training = training
+ self.gamma = gamma
+ self.epsilon = epsilon
+ self.clip = clip
+
+ self.returns = np.zeros(self.num_envs)
+
+ def step(self, action):
+ obs, reward, done, info = self.env.step(action)
+
+ if not self.is_vector_env:
+ reward = np.array([reward])
+ reward = self.normalize(reward)
+ if not self.is_vector_env:
+ reward = reward[0]
+
+ dones = done if self.is_vector_env else np.array([done])
+ self.returns[dones] = 0
+
+ return obs, reward, done, info
+
+ def reset(self, **kwargs):
+ self.returns = np.zeros(self.num_envs)
+ return self.env.reset(**kwargs)
+
+ def normalize(self, rewards):
+ if self.training:
+ self.returns = self.returns * self.gamma + rewards
+ self.rms.update(self.returns)
+ return np.clip(
+ rewards / np.sqrt(self.rms.var + self.epsilon), -self.clip, self.clip
+ )
+
+ def save(self, path: str) -> None:
+ np.savez_compressed(
+ path,
+ mean=self.rms.mean,
+ var=self.rms.var,
+ count=self.rms.count,
+ )
+
+ def load(self, path: str) -> None:
+ data = np.load(path)
+ self.rms.mean = data["mean"]
+ self.rms.var = data["var"]
+ self.rms.count = data["count"]
diff --git a/wrappers/transpose_image_observation.py b/wrappers/transpose_image_observation.py
new file mode 100644
index 0000000000000000000000000000000000000000..7076c9146fa28cd266a2466b58ac6a6b6555d59b
--- /dev/null
+++ b/wrappers/transpose_image_observation.py
@@ -0,0 +1,34 @@
+import gym
+import numpy as np
+
+from gym import ObservationWrapper
+from gym.spaces import Box
+
+
+class TransposeImageObservation(ObservationWrapper):
+ def __init__(self, env: gym.Env) -> None:
+ super().__init__(env)
+
+ assert isinstance(env.observation_space, Box)
+
+ obs_space = env.observation_space
+ axes = tuple(i for i in range(len(obs_space.shape)))
+ self._transpose_axes = axes[:-3] + (axes[-1],) + axes[-3:-1]
+
+ self.observation_space = Box(
+ low=np.transpose(obs_space.low, axes=self._transpose_axes),
+ high=np.transpose(obs_space.high, axes=self._transpose_axes),
+ shape=[obs_space.shape[idx] for idx in self._transpose_axes],
+ dtype=obs_space.dtype,
+ )
+
+ def observation(self, obs: np.ndarray) -> np.ndarray:
+ full_shape = obs.shape
+ obs_shape = self.observation_space.shape
+ addl_dims = len(full_shape) - len(obs_shape)
+ if addl_dims > 0:
+ transpose_axes = list(range(addl_dims))
+ transpose_axes.extend(a + addl_dims for a in self._transpose_axes)
+ else:
+ transpose_axes = self._transpose_axes
+ return np.transpose(obs, axes=transpose_axes)
diff --git a/wrappers/vec_episode_recorder.py b/wrappers/vec_episode_recorder.py
new file mode 100644
index 0000000000000000000000000000000000000000..c9e92c1d02f8b9e74d6fa561c77f0374c34b48a1
--- /dev/null
+++ b/wrappers/vec_episode_recorder.py
@@ -0,0 +1,80 @@
+import numpy as np
+
+from gym.vector.sync_vector_env import SyncVectorEnv
+from gym.wrappers.monitoring.video_recorder import VideoRecorder
+from stable_baselines3.common.vec_env.base_vec_env import tile_images
+from typing import Optional
+
+from wrappers.vectorable_wrapper import (
+ VecotarableWrapper,
+ VecEnvObs,
+ VecEnvStepReturn,
+)
+
+
+class VecEpisodeRecorder(VecotarableWrapper):
+ def __init__(self, env, base_path: str, max_video_length: int = 3600):
+ super().__init__(env)
+ self.base_path = base_path
+ self.max_video_length = max_video_length
+ self.video_recorder = None
+ self.recorded_frames = 0
+
+ def step(self, actions: np.ndarray) -> VecEnvStepReturn:
+ obs, rew, dones, infos = self.env.step(actions)
+ # Using first env to record episodes
+ if self.video_recorder:
+ self.video_recorder.capture_frame()
+ self.recorded_frames += 1
+ if dones[0] and infos[0].get("episode"):
+ episode_info = {
+ k: v.item() if hasattr(v, "item") else v
+ for k, v in infos[0]["episode"].items()
+ }
+ self.video_recorder.metadata["episode"] = episode_info
+ if dones[0] or self.recorded_frames > self.max_video_length:
+ self._close_video_recorder()
+ return obs, rew, dones, infos
+
+ def reset(self) -> VecEnvObs:
+ obs = self.env.reset()
+ self._start_video_recorder()
+ return obs
+
+ def _start_video_recorder(self) -> None:
+ self._close_video_recorder()
+
+ self.video_recorder = VideoRecorder(
+ SyncVectorEnvRenderCompat(self.env),
+ base_path=self.base_path,
+ )
+
+ self.video_recorder.capture_frame()
+ self.recorded_frames = 1
+
+ def _close_video_recorder(self) -> None:
+ if self.video_recorder:
+ self.video_recorder.close()
+ self.video_recorder = None
+
+
+class SyncVectorEnvRenderCompat(VecotarableWrapper):
+ def __init__(self, env) -> None:
+ super().__init__(env)
+
+ def render(self, mode: str = "human") -> Optional[np.ndarray]:
+ base_env = self.env.unwrapped
+ if isinstance(base_env, SyncVectorEnv):
+ imgs = [env.render(mode="rgb_array") for env in base_env.envs]
+ bigimg = tile_images(imgs)
+ if mode == "humnan":
+ import cv2
+
+ cv2.imshow("vecenv", bigimg[:, :, ::-1])
+ cv2.waitKey(1)
+ elif mode == "rgb_array":
+ return bigimg
+ else:
+ raise NotImplemented(f"Render mode {mode} is not supported")
+ else:
+ return self.env.render(mode=mode)
diff --git a/wrappers/vectorable_wrapper.py b/wrappers/vectorable_wrapper.py
new file mode 100644
index 0000000000000000000000000000000000000000..03df8d1400ab84242353f5dc1288a4394158b941
--- /dev/null
+++ b/wrappers/vectorable_wrapper.py
@@ -0,0 +1,46 @@
+import numpy as np
+from gym import Env, Space, Wrapper
+
+from stable_baselines3.common.vec_env import VecEnv as SB3VecEnv
+from typing import Dict, List, Optional, Type, TypeVar, Tuple, Union
+
+VecEnvObs = Union[np.ndarray, Dict[str, np.ndarray], Tuple[np.ndarray, ...]]
+VecEnvStepReturn = Tuple[VecEnvObs, np.ndarray, np.ndarray, List[Dict]]
+
+
+class VecotarableWrapper(Wrapper):
+ def __init__(self, env: Env) -> None:
+ super().__init__(env)
+ self.num_envs = getattr(env, "num_envs", 1)
+ self.is_vector_env = getattr(env, "is_vector_env", False)
+ self.single_observation_space = single_observation_space(env)
+ self.single_action_space = single_action_space(env)
+
+ def step(self, action) -> VecEnvStepReturn:
+ return self.env.step(action)
+
+ def reset(self) -> VecEnvObs:
+ return self.env.reset()
+
+
+VecEnv = Union[VecotarableWrapper, SB3VecEnv]
+
+
+def single_observation_space(env: Union[VecEnv, Env]) -> Space:
+ return getattr(env, "single_observation_space", env.observation_space)
+
+
+def single_action_space(env: Union[VecEnv, Env]) -> Space:
+ return getattr(env, "single_action_space", env.action_space)
+
+
+W = TypeVar("W", bound=Wrapper)
+
+
+def find_wrapper(env: VecEnv, wrapper_class: Type[W]) -> Optional[W]:
+ current = env
+ while current and current != current.unwrapped:
+ if isinstance(current, wrapper_class):
+ return current
+ current = getattr(current, "env")
+ return None
diff --git a/wrappers/video_compat_wrapper.py b/wrappers/video_compat_wrapper.py
new file mode 100644
index 0000000000000000000000000000000000000000..d2e91c7beeee8f5e04bf1c125214aee1e495dcb5
--- /dev/null
+++ b/wrappers/video_compat_wrapper.py
@@ -0,0 +1,15 @@
+import gym
+import numpy as np
+
+from wrappers.vectorable_wrapper import VecotarableWrapper
+
+
+class VideoCompatWrapper(VecotarableWrapper):
+ def __init__(self, env: gym.Env) -> None:
+ super().__init__(env)
+
+ def render(self, mode="human", **kwargs):
+ r = super().render(mode=mode, **kwargs)
+ if mode == "rgb_array" and isinstance(r, np.ndarray) and r.dtype != np.uint8:
+ r = r.astype(np.uint8)
+ return r