diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 328839e6b..1ad2ea953 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,14 +9,14 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.10', '3.11', '3.12'] + python-version: ["3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v3 - run: | - docker build -f docker/Dockerfile \ - --build-arg PYTHON_VERSION=${{ matrix.python-version }} \ - --tag metaworld-docker . + docker build -f docker/Dockerfile \ + --build-arg PYTHON_VERSION=${{ matrix.python-version }} \ + --tag metaworld-docker . - name: Run tests - run: docker run metaworld-docker pytest tests/* + run: docker run metaworld-docker pytest -n auto tests/* # - name: Run doctests # run: docker run metaworld-docker pytest --doctest-modules metaworld/ diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000..717dfc97c --- /dev/null +++ b/TODO.md @@ -0,0 +1,11 @@ +## DONEs with reason +* Renamed `max_path_length` to `max_episode_steps` in several places to align with Gymnasium's terminology. +* * Removed `Meta-World/goal_hidden` and `Meta-World/goal_observable` as one can just use `Meta-World/MT1` with the appropriate `goal_observable` kwarg. + +## TODOs + +* Scripts +* tests +* Docs +* Evaluation +* Test and document meta-batch size there might be the same bug as in master with the division diff --git a/docs/introduction/basic_usage.md b/docs/introduction/basic_usage.md index 6aa32d034..a524cf5b1 100644 --- a/docs/introduction/basic_usage.md +++ b/docs/introduction/basic_usage.md @@ -135,17 +135,17 @@ envs = gym.make_vec('Meta-World/custom-ml-envs', vector_strategy='sync', envs_li ## Arguments The gym.make command supports multiple arguments: -| Argument | Usage | Values | -|----------|-------|--------| -| seed | The number to seed the random number generator with | None or int | -| max_episode_steps | The maximum number of steps per episode | None or int | -| use_one_hot | Whether the one hot wrapper should be use to add the task ID to the observation | True or False | -| num_tasks | The number of parametric variations to sample (default:50) | int | -| terminate_on_success | Whether to terminate the episode during training when the success signal is seen | True or False| -| vector_strategy | What kind of vector strategy the environments should be wrapped in | 'sync' or 'async' | -| task_select | How parametric variations should be selected | "random" or "pseudorandom" | -| reward_function_version | Use the original reward functions from Meta-World or the updated ones | "v1" or "v2" | -| reward_normalization_method | Apply a reward normalization wrapper | None or 'gymnasium' or 'exponential' | -| render_mode | The render mode of each environment | None or 'human' or 'rgb_array' or 'depth_array' | -| camera_name | The Mujoco name of the camera that should be used to render | 'corner' or 'topview' or 'behindGripper' or 'gripperPOV' or 'corner2' or 'corner3' or 'corner4' | -| camera_id | The Mujoco ID of the camera that should be used to render | int | +| Argument | Usage | Values | +| --------------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| seed | The number to seed the random number generator with | None or int | +| max_episode_steps | The maximum number of steps per episode | None or int | +| use_one_hot | Whether the one hot wrapper should be use to add the task ID to the observation | True or False | +| num_tasks | The number of parametric variations to sample (default:50) | int | +| terminate_on_success | Whether to terminate the episode during training when the success signal is seen | True or False | +| vector_strategy | What kind of vector strategy the environments should be wrapped in | 'sync' or 'async' | +| task_sampler | How parametric variations should be selected | "random" or "pseudorandom" | +| reward_function_version | Use the original reward functions from Meta-World or the updated ones | "v1" or "v2" | +| reward_normalization_method | Apply a reward normalization wrapper | None or 'gymnasium' or 'exponential' | +| render_mode | The render mode of each environment | None or 'human' or 'rgb_array' or 'depth_array' | +| camera_name | The Mujoco name of the camera that should be used to render | 'corner' or 'topview' or 'behindGripper' or 'gripperPOV' or 'corner2' or 'corner3' or 'corner4' | +| camera_id | The Mujoco ID of the camera that should be used to render | int | diff --git a/metaworld/__init__.py b/metaworld/__init__.py index a2c1e14c3..266cdd1fc 100644 --- a/metaworld/__init__.py +++ b/metaworld/__init__.py @@ -2,27 +2,32 @@ from __future__ import annotations -import abc -import pickle -from collections import OrderedDict from functools import partial -from typing import Any, Literal, Union +from typing import Any, Literal import gymnasium as gym # type: ignore -import numpy as np -import numpy.typing as npt # noqa: D104 from gymnasium.envs.registration import register -import metaworld.env_dict as _env_dict +from metaworld.benchmark import ( + get_mt1_v3_benchmark, + get_mtX_v3_benchmark, + get_mlCustom_v3_benchmark, + get_ml1_v3_benchmark, + get_mlX_v3_benchmark, + get_mtCustom_v3_benchmark, + TaskSet, + Task, +) from metaworld.env_dict import ( - ALL_V3_ENVIRONMENTS, - ALL_V3_ENVIRONMENTS_GOAL_HIDDEN, - ALL_V3_ENVIRONMENTS_GOAL_OBSERVABLE, + ENV_CLASS_MAP, + MT_BENCHMARKS_TRAIN_ENV_NAMES, + ML_BENCHMARKS, + MLXv3Benchmarks, + MTXv3Benchmarks, ) from metaworld.sawyer_xyz_env import SawyerXYZEnv # type: ignore -from metaworld.types import Task # type: ignore from metaworld.wrappers import ( AutoTerminateOnSuccessWrapper, CheckpointWrapper, @@ -34,406 +39,49 @@ ) -class MetaWorldEnv(abc.ABC): - """Environment that requires a task before use. - - Takes no arguments to its constructor, and raises an exception if used - before `set_task` is called. - """ - - @abc.abstractmethod - def set_task(self, task: Task) -> None: - """Sets the task. - Args: - task: The task to set. - Raises: - ValueError: If `task.env_name` is different from the current task. - """ - raise NotImplementedError - - -class Benchmark(abc.ABC): - """A Benchmark. - - When used to evaluate an algorithm, only a single instance should be used. - """ - - _train_classes: _env_dict.EnvDict - _test_classes: _env_dict.EnvDict - _train_tasks: list[Task] - _test_tasks: list[Task] - - @abc.abstractmethod - def __init__(self): - pass - - @property - def train_classes(self) -> _env_dict.EnvDict: - """Returns all of the environment classes used for training.""" - return self._train_classes - - @property - def test_classes(self) -> _env_dict.EnvDict: - """Returns all of the environment classes used for testing.""" - return self._test_classes - - @property - def train_tasks(self) -> list[Task]: - """Returns all of the training tasks for this benchmark.""" - return self._train_tasks - - @property - def test_tasks(self) -> list[Task]: - """Returns all of the test tasks for this benchmark.""" - return self._test_tasks - - -_ML_OVERRIDE = dict(partially_observable=True) -"""The overrides for the Meta-Learning benchmarks. Disables the inclusion of the goal position in the observation.""" - -_MT_OVERRIDE = dict(partially_observable=False) -"""The overrides for the Multi-Task benchmarks. Enables the inclusion of the goal position in the observation.""" - -_N_GOALS = 50 -"""The number of goals to generate for each environment.""" - - -def _encode_task(env_name, data) -> Task: - """Instantiates a new `Task` object after pickling the data. - - Args: - env_name: The name of the environment. - data: The task data (will be pickled). - - Returns: - A `Task` object. - """ - return Task(env_name=env_name, data=pickle.dumps(data)) - - -def _make_tasks( - classes: _env_dict.EnvDict, - args_kwargs: _env_dict.EnvArgsKwargsDict, - kwargs_override: dict, - seed: int | None = None, -) -> list[Task]: - """Initialises goals for a given set of environments. - - Args: - classes: The environment classes as an `EnvDict`. - args_kwargs: The environment arguments and keyword arguments. - kwargs_override: Any kwarg overrides. - seed: The random seed to use. - - Returns: - A flat list of `Task` objects, `_N_GOALS` for each environment in `classes`. - """ - # Cache existing random state - if seed is not None: - st0 = np.random.get_state() - np.random.seed(seed) - - tasks = [] - for env_name, args in args_kwargs.items(): - kwargs = args["kwargs"].copy() - assert isinstance(kwargs, dict) - assert len(args["args"]) == 0 - - # Init env - env = classes[env_name]() - env._freeze_rand_vec = False - env._set_task_called = True - rand_vecs: list[npt.NDArray[Any]] = [] - - # Set task - del kwargs["task_id"] - env._set_task_inner(**kwargs) - - for _ in range(_N_GOALS): # Generate random goals - env.reset() - assert env._last_rand_vec is not None - rand_vecs.append(env._last_rand_vec) - unique_task_rand_vecs = np.unique(np.array(rand_vecs), axis=0) - assert ( - unique_task_rand_vecs.shape[0] == _N_GOALS - ), f"Only generated {unique_task_rand_vecs.shape[0]} unique goals, not {_N_GOALS}" - env.close() - - # Create a task for each random goal - for rand_vec in rand_vecs: - kwargs = args["kwargs"].copy() - assert isinstance(kwargs, dict) - del kwargs["task_id"] - - kwargs.update(dict(rand_vec=rand_vec, env_cls=classes[env_name])) - kwargs.update(kwargs_override) - - tasks.append(_encode_task(env_name, kwargs)) - - del env - - # Restore random state - if seed is not None: - np.random.set_state(st0) - - return tasks - - -# MT Benchmarks - - -class MT1(Benchmark): - """ - The MT1 benchmark. - A goal-conditioned RL environment for a single Metaworld task. - """ - - ENV_NAMES = list(_env_dict.ALL_V3_ENVIRONMENTS.keys()) - - def __init__(self, env_name, seed=None): - super().__init__() - if env_name not in _env_dict.ALL_V3_ENVIRONMENTS: - raise ValueError(f"{env_name} is not a V3 environment") - cls = _env_dict.ALL_V3_ENVIRONMENTS[env_name] - self._train_classes = OrderedDict([(env_name, cls)]) - self._test_classes = OrderedDict([(env_name, cls)]) - args_kwargs = _env_dict.ML1_args_kwargs[env_name] - - self._train_tasks = _make_tasks( - self._train_classes, {env_name: args_kwargs}, _MT_OVERRIDE, seed=seed - ) - - self._test_tasks = [] - - -class MT10(Benchmark): - """ - The MT10 benchmark. - Contains 10 tasks in its train set. - Has an empty test set. - """ - - def __init__(self, seed=None): - super().__init__() - self._train_classes = _env_dict.MT10_V3 - self._test_classes = OrderedDict() - train_kwargs = _env_dict.MT10_V3_ARGS_KWARGS - self._train_tasks = _make_tasks( - self._train_classes, train_kwargs, _MT_OVERRIDE, seed=seed - ) - - self._test_tasks = [] - self._test_classes = [] - - -class MT25(Benchmark): - """ - The MT25 benchmark. - Contains 25 tasks in its train set. - Has an empty test set. - """ - - def __init__(self, seed=None): - super().__init__() - self._train_classes = _env_dict.MT25_V3 - train_kwargs = _env_dict.MT25_V3_ARGS_KWARGS - self._train_tasks = _make_tasks( - self._train_classes, train_kwargs, _MT_OVERRIDE, seed=seed - ) - - self._test_tasks = [] - self._test_classes = [] - - -class MT50(Benchmark): - """ - The MT50 benchmark. - Contains all (50) tasks in its train set. - Has an empty test set. - """ - - def __init__(self, seed=None): - super().__init__() - self._train_classes = _env_dict.MT50_V3 - self._test_classes = OrderedDict() - train_kwargs = _env_dict.MT50_V3_ARGS_KWARGS - self._train_tasks = _make_tasks( - self._train_classes, train_kwargs, _MT_OVERRIDE, seed=seed - ) - - self._test_tasks = [] - self._test_classes = [] - - -# ML Benchmarks - - -class ML1(Benchmark): - """ - The ML1 benchmark. - A meta-RL environment for a single Metaworld task. - The train and test set contain different goal positions. - The goal position is not part of the observation. - """ - - ENV_NAMES = list(_env_dict.ALL_V3_ENVIRONMENTS.keys()) - - def __init__(self, env_name, seed=None): - super().__init__() - if env_name not in _env_dict.ALL_V3_ENVIRONMENTS: - raise ValueError(f"{env_name} is not a V3 environment") - - cls = _env_dict.ALL_V3_ENVIRONMENTS[env_name] - self._train_classes = OrderedDict([(env_name, cls)]) - self._test_classes = self._train_classes - args_kwargs = _env_dict.ML1_args_kwargs[env_name] - - self._train_tasks = _make_tasks( - self._train_classes, {env_name: args_kwargs}, _ML_OVERRIDE, seed=seed - ) - self._test_tasks = _make_tasks( - self._test_classes, - {env_name: args_kwargs}, - _ML_OVERRIDE, - seed=(seed + 1 if seed is not None else seed), - ) - - -class ML10(Benchmark): - """ - The ML10 benchmark. - Contains 10 tasks in its train set and 5 tasks in its test set. - The goal position is not part of the observation. - """ - - def __init__(self, seed=None): - super().__init__() - self._train_classes = _env_dict.ML10_V3["train"] - self._test_classes = _env_dict.ML10_V3["test"] - train_kwargs = _env_dict.ML10_ARGS_KWARGS["train"] - - test_kwargs = _env_dict.ML10_ARGS_KWARGS["test"] - self._train_tasks = _make_tasks( - self._train_classes, train_kwargs, _ML_OVERRIDE, seed=seed - ) - - self._test_tasks = _make_tasks( - self._test_classes, test_kwargs, _ML_OVERRIDE, seed=seed - ) - - -class ML25(Benchmark): - """ - The ML10 benchmark. - Contains 25 tasks in its train set and 5 tasks in its test set. - The goal position is not part of the observation. - """ - - def __init__(self, seed=None): - super().__init__() - self._train_classes = _env_dict.ML25_V3["train"] - self._test_classes = _env_dict.ML25_V3["test"] - train_kwargs = _env_dict.ML25_ARGS_KWARGS["train"] - - test_kwargs = _env_dict.ML25_ARGS_KWARGS["test"] - self._train_tasks = _make_tasks( - self._train_classes, train_kwargs, _ML_OVERRIDE, seed=seed - ) - - self._test_tasks = _make_tasks( - self._test_classes, test_kwargs, _ML_OVERRIDE, seed=seed - ) - - -class ML45(Benchmark): - """ - The ML45 benchmark. - Contains 45 tasks in its train set and 5 tasks in its test set (50 in total). - The goal position is not part of the observation. - """ - - def __init__(self, seed=None): - super().__init__() - self._train_classes = _env_dict.ML45_V3["train"] - self._test_classes = _env_dict.ML45_V3["test"] - train_kwargs = _env_dict.ML45_ARGS_KWARGS["train"] - test_kwargs = _env_dict.ML45_ARGS_KWARGS["test"] - - self._train_tasks = _make_tasks( - self._train_classes, train_kwargs, _ML_OVERRIDE, seed=seed - ) - self._test_tasks = _make_tasks( - self._test_classes, test_kwargs, _ML_OVERRIDE, seed=seed - ) - - -class CustomML(Benchmark): - """ - A custom meta RL benchmark. - Provide the desired train and test env names during initialisation. - """ - - def __init__(self, train_envs: list[str], test_envs: list[str], seed=None): - if len(set(train_envs).intersection(set(test_envs))) != 0: - raise ValueError("The test tasks cannot contain any of the train tasks.") - - self._train_classes = _env_dict._get_env_dict(train_envs) - train_kwargs = _env_dict._get_args_kwargs( - ALL_V3_ENVIRONMENTS, self._train_classes - ) - - self._test_classes = _env_dict._get_env_dict(test_envs) - test_kwargs = _env_dict._get_args_kwargs( - ALL_V3_ENVIRONMENTS, self._test_classes - ) - - self._train_tasks = _make_tasks( - self._train_classes, train_kwargs, _ML_OVERRIDE, seed=seed - ) - self._test_tasks = _make_tasks( - self._test_classes, test_kwargs, _ML_OVERRIDE, seed=seed - ) - - -def _init_each_env( - env_cls: type[SawyerXYZEnv], +def _init_env_with_wrappers( + env_name: str, tasks: list[Task], - seed: int | None = None, - max_episode_steps: int | None = None, - terminate_on_success: bool = False, + max_episode_steps: int = 500, + terminate_on_success: bool = True, use_one_hot: bool = False, env_id: int | None = None, - num_tasks: int | None = None, + num_env_ids: int | None = None, recurrent_info_in_obs: bool = False, normalize_reward_in_recurrent_info: bool = True, - task_select: Literal["random", "pseudorandom"] = "random", - reward_function_version: Literal["v1", "v2"] = "v2", - reward_normalization_method: Literal["gymnasium", "exponential"] | None = None, + task_sampler: Literal["random", "pseudorandom"] = "random", + sample_tasks_on_reset: bool = True, + reward_normalization_method: Literal["gymnasium", + "exponential"] | None = None, normalize_observations: bool = False, reward_alpha: float = 0.001, - render_mode: Literal["human", "rgb_array", "depth_array"] | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - width: int = 480, - height: int = 480, + num_envs: int | None = None, + **extra_env_kwargs: dict[str, Any], ) -> gym.Env: + # The num_envs arg is ignored and consumed here. It is generated by make_vec and not needed downstream. + + env_cls = ENV_CLASS_MAP[env_name] + + # All tasks have the same env kwargs, so we can just use the first one + kw_args = extra_env_kwargs.copy() + env: gym.Env = env_cls( - reward_function_version=reward_function_version, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - width=width, - height=height, + max_episode_steps=max_episode_steps, + **kw_args, ) - if seed is not None: - env.seed(seed) # type: ignore - env = gym.wrappers.TimeLimit(env, max_episode_steps or env.max_path_length) # type: ignore + # Ensure we have a consistent starting rng state for the wrappers + env.reset(seed=tasks[0].env_seed) + + env = gym.wrappers.TimeLimit( + env, max_episode_steps) # type: ignore env = AutoTerminateOnSuccessWrapper(env) env.toggle_terminate_on_success(terminate_on_success) if use_one_hot: - assert env_id is not None, "Need to pass env_id through constructor" - assert num_tasks is not None, "Need to pass num_tasks through constructor" - env = OneHotWrapper(env, env_id, num_tasks) + if env_id is None or num_env_ids is None: + raise ValueError( + "env_id and num_env_ids must be provided when using one-hot encoding." + ) + env = OneHotWrapper(env, env_id, num_env_ids) if recurrent_info_in_obs: env = RNNBasedMetaRLWrapper( env, normalize_reward=normalize_reward_in_recurrent_info @@ -446,379 +94,291 @@ def _init_each_env( env = gym.wrappers.NormalizeObservation(env) env = gym.wrappers.RecordEpisodeStatistics(env) - if task_select != "random": - env = PseudoRandomTaskSelectWrapper(env, tasks) + if task_sampler == "pseudorandom": + env = PseudoRandomTaskSelectWrapper(env, tasks, sample_tasks_on_reset) + elif task_sampler == "random": + env = RandomTaskSelectWrapper(env, tasks, sample_tasks_on_reset) else: - env = RandomTaskSelectWrapper(env, tasks) + raise ValueError( + f"Invalid task_sampler option: {task_sampler}. Must be 'random' or 'pseudorandom'." + ) env = CheckpointWrapper(env, f"{env_cls}_{env_id}") - if seed is not None: - env.action_space.seed(seed) return env -def make_mt_envs( - name: str, - seed: int | None = None, - num_tasks: int | None = None, - vector_strategy: Literal["sync", "async"] = "sync", - autoreset_mode: gym.vector.AutoresetMode | str = gym.vector.AutoresetMode.SAME_STEP, - **kwargs, -) -> gym.Env | gym.vector.VectorEnv: - benchmark: Benchmark - if name in ALL_V3_ENVIRONMENTS.keys(): - benchmark = MT1(name, seed=seed) - tasks = [task for task in benchmark.train_tasks] - return _init_each_env( # type: ignore[misc] - env_cls=benchmark.train_classes[name], - tasks=tasks, - seed=seed, - num_tasks=num_tasks or 1, - **kwargs, - ) - elif name == "MT10" or name == "MT25" or name == "MT50": - benchmark = globals()[name](seed=seed) - vectorizer: type[gym.vector.VectorEnv] = getattr( - gym.vector, f"{vector_strategy.capitalize()}VectorEnv" - ) - if name == "MT10": - default_num_tasks = 10 - elif name == "MT25": - default_num_tasks = 25 - else: - default_num_tasks = 50 - - return vectorizer( # type: ignore - [ - partial( - _init_each_env, - env_cls=env_cls, - tasks=[ - task for task in benchmark.train_tasks if task.env_name == name - ], - seed=seed, - env_id=env_id, - num_tasks=num_tasks or default_num_tasks, - **kwargs, - ) - for env_id, (name, env_cls) in enumerate( - benchmark.train_classes.items() - ) - ], # type: ignore - autoreset_mode=autoreset_mode, - ) +def _vectorizer_from_strategy( + vector_strategy: Literal["sync", + "async"] | type[gym.vector.VectorEnv] | None = None, +) -> type[gym.vector.VectorEnv]: + vectorizer: type[gym.vector.VectorEnv] + if vector_strategy == "sync" or vector_strategy is None: + vectorizer = gym.vector.SyncVectorEnv + elif vector_strategy == "async": + vectorizer = gym.vector.AsyncVectorEnv else: - raise ValueError( - "Invalid MT env name. Must either be a valid Metaworld task name (e.g. 'reach-v3'), 'MT10' or 'MT50'." - ) + vectorizer = vector_strategy + return vectorizer -def _make_ml_envs_inner( - benchmark: Benchmark, - meta_batch_size: int, - seed: int | None = None, - total_tasks_per_cls: int | None = None, - split: Literal["train", "test"] = "train", - vector_strategy: Literal["sync", "async"] = "sync", +def _vectorize_task_set( + task_set: TaskSet, + meta_batch_size: int | None = None, + vector_strategy: Literal["sync", + "async"] | type[gym.vector.VectorEnv] | None = None, autoreset_mode: gym.vector.AutoresetMode | str = gym.vector.AutoresetMode.SAME_STEP, **kwargs, -): - all_classes = ( - benchmark.train_classes if split == "train" else benchmark.test_classes - ) - all_tasks = benchmark.train_tasks if split == "train" else benchmark.test_tasks +) -> gym.vector.VectorEnv: + + num_env_ids = len(task_set.env_names) + + if meta_batch_size is None: + meta_batch_size = num_env_ids + assert ( - meta_batch_size % len(all_classes) == 0 - ), "meta_batch_size must be divisible by envs_per_task" - tasks_per_env = meta_batch_size // len(all_classes) - - env_tuples = [] - for env_name, env_cls in all_classes.items(): - tasks = [task for task in all_tasks if task.env_name == env_name] - if total_tasks_per_cls is not None: - tasks = tasks[:total_tasks_per_cls] - subenv_tasks = [tasks[i::tasks_per_env] for i in range(0, tasks_per_env)] + meta_batch_size % len(task_set.env_names) == 0 + ), "meta_batch_size must be divisible by the environment count" + tasks_per_env = meta_batch_size // len(task_set.env_names) + + tasks_per_parallel_env = [] + for env_name in task_set.env_names: + # Filter tasks for this env name + tasks = task_set.tasks_dict[env_name] + # Split tasks into `tasks_per_env` sublists + subenv_tasks = [tasks[i::tasks_per_env] + for i in range(0, tasks_per_env)] for tasks_for_subenv in subenv_tasks: assert ( len(tasks_for_subenv) == len(tasks) // tasks_per_env ), f"Invalid division of subtasks, expected {len(tasks) // tasks_per_env} got {len(tasks_for_subenv)}" - env_tuples.append((env_cls, tasks_for_subenv)) + tasks_per_parallel_env.append((env_name, tasks_for_subenv)) - vectorizer: type[gym.vector.SyncVectorEnv | gym.vector.AsyncVectorEnv] = getattr( - gym.vector, f"{vector_strategy.capitalize()}VectorEnv" - ) + env_name_to_id = {name: i for i, name in enumerate(task_set.env_names)} + + merged_kwargs = {} + merged_kwargs.update(task_set.env_kwargs_overrides) + merged_kwargs.update(kwargs) + + vectorizer = _vectorizer_from_strategy(vector_strategy) return vectorizer( [ partial( - _init_each_env, - env_cls=env_cls, + _init_env_with_wrappers, + env_name=env_name, + env_id=env_name_to_id[env_name], + num_env_ids=len(task_set.env_names), tasks=tasks, - seed=seed, - **kwargs, + **merged_kwargs, ) - for env_cls, tasks in env_tuples + for env_name, tasks in tasks_per_parallel_env ], autoreset_mode=autoreset_mode, + # The vectorized env consists of different envs which all have different + # observation spaces. Therefore, we set the observation_mode to 'different'. + observation_mode='different', + ) + + +def _use_one_hot_guard( + use_one_hot: bool, +): + if use_one_hot: + raise ValueError( + "The one-hot wrapper can only be used with the MTX/MTCustom benchmarks except MT1." + ) + + +def _mt1_entry_point( + env_name: str, + seed: int | None = None, + num_tasks_per_env: int | None = None, + use_one_hot: bool = False, + **kwargs, +): + _use_one_hot_guard(use_one_hot) + + mt1_benchmark = get_mt1_v3_benchmark( + env_name, + seed, + num_tasks_per_env, + ) + task_set = mt1_benchmark.generate_train_task_set() + tasks = task_set.tasks_dict[env_name] + + merged_kwargs = {} + merged_kwargs.update(task_set.env_kwargs_overrides) + merged_kwargs.update(kwargs) + return _init_env_with_wrappers( + env_name=env_name, + tasks=tasks, + **merged_kwargs, + ) + + +def _mtX_vector_entry_point( + mt_bench: MTXv3Benchmarks, + seed: int | None = None, + num_tasks_per_env: int | None = None, + **kwargs, +) -> gym.Env | gym.vector.VectorEnv: + mtX_benchmark = get_mtX_v3_benchmark( + mt_bench, + seed, + num_tasks_per_env, ) + task_set = mtX_benchmark.generate_train_task_set() -def make_ml_envs( - name: str, + return _vectorize_task_set( + task_set, + **kwargs, + ) + + +def _mtCustom_vector_entry_point( + train_env_names: list[str], + seed: int | None = None, + num_tasks_per_env: int | None = None, + **kwargs, +) -> gym.Env | gym.vector.VectorEnv: + mtX_benchmark = get_mtCustom_v3_benchmark( + train_env_names, + seed, + num_tasks_per_env, + ) + + task_set = mtX_benchmark.generate_train_task_set() + + return _vectorize_task_set( + task_set, + **kwargs, + ) + + +def _ml1_vector_entry_point( + env_name: str, seed: int | None = None, - meta_batch_size: int = 20, - total_tasks_per_cls: int | None = None, split: Literal["train", "test"] = "train", - vector_strategy: Literal["sync", "async"] = "sync", - autoreset_mode: gym.vector.AutoresetMode | str = gym.vector.AutoresetMode.SAME_STEP, + num_tasks_per_env: int | None = None, + use_one_hot: bool = False, **kwargs, -) -> gym.vector.VectorEnv: - benchmark: Benchmark - if name in ALL_V3_ENVIRONMENTS.keys(): - benchmark = ML1(name, seed=seed) - elif name == "ML10" or name == "ML45" or name == "ML25": - benchmark = globals()[name](seed=seed) - else: - raise ValueError( - "Invalid ML env name. Must either be a valid Metaworld task name (e.g. 'reach-v3'), 'ML10', 'ML25', or 'ML45'." - ) - return _make_ml_envs_inner( - benchmark, - meta_batch_size=meta_batch_size, - seed=seed, - total_tasks_per_cls=total_tasks_per_cls, - split=split, - vector_strategy=vector_strategy, - autoreset_mode=autoreset_mode, +): + _use_one_hot_guard(use_one_hot) + + ml1_benchmark = get_ml1_v3_benchmark( + env_name, + seed, + num_tasks_per_env, + ) + + task_set = ml1_benchmark.generate_task_set(split=split) + + return _vectorize_task_set( + task_set, **kwargs, ) -make_ml_envs_train = partial( - make_ml_envs, - terminate_on_success=False, - task_select="pseudorandom", - split="train", -) -make_ml_envs_test = partial( - make_ml_envs, terminate_on_success=True, task_select="pseudorandom", split="test" -) +def _mlX_vector_entry_point( + ml_bench: MLXv3Benchmarks, + seed: int | None = None, + split: Literal["train", "test"] = "train", + num_tasks_per_env: int | None = None, + use_one_hot: bool = False, + **kwargs, +): + _use_one_hot_guard(use_one_hot) + mlX_benchmark = get_mlX_v3_benchmark( + ml_bench, + seed, + num_tasks_per_env, + ) -def register_mw_envs() -> None: - def _mt_bench_vector_entry_point( - mt_bench: str, - vector_strategy: Literal["sync", "async"], - autoreset_mode: gym.vector.AutoresetMode - | str = gym.vector.AutoresetMode.SAME_STEP, - seed=None, - use_one_hot=False, - num_envs=None, - **lamb_kwargs, - ): - if "num_goals" in lamb_kwargs: - global _N_GOALS - _N_GOALS = lamb_kwargs["num_goals"] - del lamb_kwargs["num_goals"] - return make_mt_envs( # type: ignore - mt_bench, - seed=seed, - use_one_hot=use_one_hot, - vector_strategy=vector_strategy, # type: ignore - autoreset_mode=autoreset_mode, - **lamb_kwargs, - ) + task_set = mlX_benchmark.generate_task_set(split=split) - def _ml_bench_vector_entry_point( - ml_bench: str, - split: Literal["train", "test"], - vector_strategy: Literal["sync", "async"], - autoreset_mode: gym.vector.AutoresetMode - | str = gym.vector.AutoresetMode.SAME_STEP, - total_tasks_per_cls: int | None = None, - seed: int | None = None, - meta_batch_size: int = 20, - num_envs=None, - **lamb_kwargs, - ): - env_generator = make_ml_envs_train if split == "train" else make_ml_envs_test - return env_generator( - ml_bench, - seed=seed, - meta_batch_size=meta_batch_size, - total_tasks_per_cls=total_tasks_per_cls, - vector_strategy=vector_strategy, - split=split, - autoreset_mode=autoreset_mode, - **lamb_kwargs, - ) + return _vectorize_task_set( + task_set, + **kwargs, + ) + + +def _mlCustom_vector_entry_point( + train_env_names: list[str], + test_env_names: list[str], + seed: int | None = None, + split: Literal["train", "test"] = "train", + num_tasks_per_env: int | None = None, + use_one_hot: bool = False, + **kwargs, +): + _use_one_hot_guard(use_one_hot) + + mlX_benchmark = get_mlCustom_v3_benchmark( + train_env_names, + test_env_names, + seed, + num_tasks_per_env, + ) + + task_set = mlX_benchmark.generate_task_set(split=split) + + return _vectorize_task_set( + task_set, + **kwargs, + ) + + +def _register_mw_envs() -> None: + + # --- MT Envs --- register( - id="Meta-World/MT1", - entry_point=lambda env_name, use_one_hot=False, vector_strategy="sync", autoreset_mode=gym.vector.AutoresetMode.SAME_STEP, seed=None, num_envs=None, **kwargs: _mt_bench_vector_entry_point( - env_name, - vector_strategy, - autoreset_mode, - seed, - use_one_hot, - num_envs, - **kwargs, - ), - kwargs={}, + id="Meta-World/MT1-v3", + entry_point=_mt1_entry_point, ) - for split in ["train", "test"]: + for mt_bench in MT_BENCHMARKS_TRAIN_ENV_NAMES.keys(): register( - id=f"Meta-World/ML1-{split}", - vector_entry_point=lambda env_name, vector_strategy="sync", autoreset_mode=gym.vector.AutoresetMode.SAME_STEP, total_tasks_per_cls=None, meta_batch_size=20, seed=None, num_envs=None, **kwargs: _ml_bench_vector_entry_point( - env_name, - split, # type: ignore[arg-type] - vector_strategy, - autoreset_mode, - total_tasks_per_cls, - seed, - meta_batch_size, - num_envs, - **kwargs, + id=f"Meta-World/{mt_bench}", + vector_entry_point=partial( + _mtX_vector_entry_point, + mt_bench=mt_bench, ), - kwargs={}, ) register( - id="Meta-World/goal_hidden", - entry_point=lambda env_name, seed: ALL_V3_ENVIRONMENTS_GOAL_HIDDEN[ - env_name + "-goal-hidden" if "-goal-hidden" not in env_name else env_name - ]( # type: ignore - seed=seed, - ), - kwargs={}, + id="Meta-World/custom-mt-envs", + vector_entry_point=_mtCustom_vector_entry_point, ) - register( - id="Meta-World/goal_observable", - entry_point=lambda env_name, seed: ALL_V3_ENVIRONMENTS_GOAL_OBSERVABLE[ - env_name + "-goal-observable" - if "-goal-observable" not in env_name - else env_name - ]( # type: ignore - seed=seed - ), - kwargs={}, - ) + # --- ML Envs --- - for mt_bench in ["MT10", "MT25", "MT50"]: + for split in ["train", "test"]: register( - id=f"Meta-World/{mt_bench}", - vector_entry_point=lambda _mt_bench=mt_bench, vector_strategy="sync", autoreset_mode=gym.vector.AutoresetMode.SAME_STEP, seed=None, use_one_hot=False, num_envs=None, **kwargs: _mt_bench_vector_entry_point( - _mt_bench, # positional arguments - vector_strategy, - autoreset_mode, - seed, - use_one_hot, - num_envs, - **kwargs, + id=f"Meta-World/ML1-v3-{split}", + vector_entry_point=partial( + _ml1_vector_entry_point, + split=split ), - kwargs={}, ) - for ml_bench in ["ML10", "ML25", "ML45"]: + for ml_bench in ML_BENCHMARKS.keys(): for split in ["train", "test"]: register( id=f"Meta-World/{ml_bench}-{split}", - vector_entry_point=lambda _ml_bench=ml_bench, _split=split, vector_strategy="sync", autoreset_mode=gym.vector.AutoresetMode.SAME_STEP, total_tasks_per_cls=None, seed=None, meta_batch_size=20, num_envs=None, **kwargs: _ml_bench_vector_entry_point( - _ml_bench, - _split, - vector_strategy, - autoreset_mode, - total_tasks_per_cls, - seed, - meta_batch_size, - num_envs, - **kwargs, + vector_entry_point=partial( + _mlX_vector_entry_point, + ml_bench=ml_bench, + split=split ), - kwargs={}, ) - def _custom_mt_vector_entry_point( - vector_strategy: str, - envs_list: list[str], - seed=None, - autoreset_mode: gym.vector.AutoresetMode - | str = gym.vector.AutoresetMode.SAME_STEP, - use_one_hot: bool = False, - num_envs=None, - **lamb_kwargs, - ): - vectorizer: type[gym.vector.VectorEnv] = getattr( - gym.vector, f"{vector_strategy.capitalize()}VectorEnv" - ) - return vectorizer( # type: ignore - [ - partial( # type: ignore - make_mt_envs, - env_name, - num_tasks=len(envs_list), - env_id=idx, - seed=None if not seed else seed + idx, - use_one_hot=use_one_hot, - **lamb_kwargs, - ) - for idx, env_name in enumerate(envs_list) - ], - autoreset_mode=autoreset_mode, - ) - - register( - id="Meta-World/custom-mt-envs", - vector_entry_point=lambda vector_strategy, envs_list, autoreset_mode=gym.vector.AutoresetMode.SAME_STEP, seed=None, use_one_hot=False, num_envs=None, **kwargs: _custom_mt_vector_entry_point( - vector_strategy, - envs_list, - seed, - autoreset_mode, - use_one_hot, - num_envs, - **kwargs, - ), - kwargs={}, - ) - - def _custom_ml_vector_entry_point( - vector_strategy: str, - train_envs: list[str], - test_envs: list[str], - autoreset_mode: gym.vector.AutoresetMode - | str = gym.vector.AutoresetMode.SAME_STEP, - total_tasks_per_cls: int | None = None, - meta_batch_size: int = 20, - seed=None, - num_envs=None, - **lamb_kwargs, - ): - return _make_ml_envs_inner( # type: ignore - CustomML(train_envs, test_envs, seed=seed), - meta_batch_size=meta_batch_size, - vector_strategy=vector_strategy, # type: ignore - autoreset_mode=autoreset_mode, - total_tasks_per_cls=total_tasks_per_cls, - seed=seed, - **lamb_kwargs, - ) - register( id="Meta-World/custom-ml-envs", - vector_entry_point=lambda vector_strategy, train_envs, test_envs, autoreset_mode=gym.vector.AutoresetMode.SAME_STEP, total_tasks_per_cls=None, meta_batch_size=20, seed=None, num_envs=None, **kwargs: _custom_ml_vector_entry_point( - vector_strategy, - train_envs, - test_envs, - autoreset_mode, - total_tasks_per_cls, - meta_batch_size, - seed, - num_envs, - **kwargs, - ), - kwargs={}, + vector_entry_point=_mlCustom_vector_entry_point, ) -register_mw_envs() +_register_mw_envs() __all__: list[str] = [] diff --git a/metaworld/benchmark.py b/metaworld/benchmark.py new file mode 100644 index 000000000..4e2ba9e82 --- /dev/null +++ b/metaworld/benchmark.py @@ -0,0 +1,335 @@ +from __future__ import annotations + +from typing import Any, Sequence +from typing_extensions import Literal +from dataclasses import dataclass + +import copy + +import numpy as np + +from metaworld.utils.numpy import randint +from metaworld.env_dict import MT_BENCHMARKS_TRAIN_ENV_NAMES, ML_BENCHMARKS, MLXv3Benchmarks, MTXv3Benchmarks + +_ML_ENV_KWARGS_OVERRIDE = dict(goal_observable=False) +"""The overrides for the Meta-Learning benchmarks. Disables the inclusion of the goal position in the observation.""" + +_MT_ENV_KWARGS_OVERRIDE = dict(goal_observable=True) +"""The overrides for the Multi-Task benchmarks. Enables the inclusion of the goal position in the observation.""" + +_DEFAULT_NUM_TASKS_PER_ENV = 50 +"""The number of seeds to generate for each environment.""" + + +@dataclass(frozen=True) +class Task: + """ + All data necessary to fully describe a single environment. + """ + + env_name: str + env_seed: int + + +@dataclass(frozen=True) +class TaskSet: + """ + A collection of tasks. + """ + + tasks_dict: dict[str, list[Task]] + """ + Mapping from environment name to the list of tasks. + """ + env_names: list[str] + """ + List of all environment names in the task set. + """ + env_kwargs_overrides: dict[str, Any] + """ + Shared kwargs overrides for each environment in the task set. + Every environment in the task gets the same overrides. + """ + + +def _generate_task_set( + env_names: Sequence[str], + benchmark_seed: int | None, + num_tasks_per_env: int | None = _DEFAULT_NUM_TASKS_PER_ENV, + env_kwargs_overrides: dict[str, dict] | None = None, +) -> TaskSet: + """Generates seeds for a given set of environments. + + Args: + env_names: The environment names as a sequence of strings. + benchmark_seed: The random seed to use for the benchmark. + num_tasks_per_env: The number of seeds to generate per environment. + env_kwargs_overrides: The environment kwargs overrides to use for all environments. + + Returns: + A TaskSet containing all of the generated tasks. + """ + + if env_kwargs_overrides is None: + env_kwargs_overrides = {} + + env_kwargs_overrides = copy.deepcopy(env_kwargs_overrides) + + tasks_dict: dict[str, list[Task]] = { + env_name: [] for env_name in env_names + } + + if num_tasks_per_env == 1 and len(env_names) == 1: + # Directly use the benchmark seed. + # This only happens if the user requests a single seed for a single env. + for env_name in env_names: + tasks_dict[env_name].append(Task(env_name, benchmark_seed)) + else: + seed_rng = np.random.default_rng( + benchmark_seed) if benchmark_seed is not None else np.random.default_rng() + + for env_name in env_names: + seeds = np.atleast_1d(randint(seed_rng, size=num_tasks_per_env)) + + for seed in seeds: + tasks_dict[env_name].append(Task(env_name, seed)) + + return TaskSet( + tasks_dict=tasks_dict, + env_names=list(env_names), + env_kwargs_overrides=env_kwargs_overrides, + ) + + +class Benchmark: + def __init__(self, + name: str, + test_env_names: list[str], + train_env_names: list[str], + seed: int | None = None, + test_train_same_seed: bool = False, + env_kwargs_overrides: dict[str, Any] | None = None, + num_tasks_per_env: int | None = None): + self.name = name + self.test_env_names = test_env_names + self.train_env_names = train_env_names + if seed is None: + self.seed = randint(np.random.default_rng()) + self.seed = seed + self.test_train_same_seed = test_train_same_seed + self.env_kwargs_overrides = env_kwargs_overrides + self.num_tasks_per_env = num_tasks_per_env + + def generate_task_set(self, split: Literal["train", "test"]) -> TaskSet: + if split == "train": + actual_seed = self.seed + env_names = self.train_env_names + elif split == "test": + actual_seed = self.seed + if not self.test_train_same_seed: + actual_seed += 1 + env_names = self.test_env_names + else: + raise ValueError(f"Invalid split: {split}") + + task_set = _generate_task_set( + env_names, + actual_seed, + self.num_tasks_per_env, + self.env_kwargs_overrides, + ) + return task_set + + def generate_train_task_set(self) -> TaskSet: + return self.generate_task_set(split="train") + + def generate_test_task_set(self) -> TaskSet: + return self.generate_task_set(split="test") + + +def get_mt1_v3_benchmark(env_name: str, seed: int | None = None, num_tasks_per_env: int | None = None) -> Benchmark: + """Returns the MT1 benchmark for a given environment name. + + MT1 is a goal-conditioned RL environment for a single Metaworld task. + A fixed set of seeds is generated. + The training and testing environments are exactly the same. + The only purpose of MT1 is to evaluate skill acquisition on a single task. + + Args: + env_name: The name of the environment. + seed: The random seed to use for the benchmark. + Returns: + The MT1 Benchmark. + """ + + env_names = [env_name] + return Benchmark( + name="MT1-v3", + train_env_names=env_names, + test_env_names=env_names, + seed=seed, + test_train_same_seed=True, + env_kwargs_overrides=_MT_ENV_KWARGS_OVERRIDE, + num_tasks_per_env=num_tasks_per_env, + ) + + +def get_mtX_v3_benchmark(mt_bench: MTXv3Benchmarks, + seed: int | None = None, + num_tasks_per_env: int | None = None) -> Benchmark: + """Returns the MTX benchmark for a given MT benchmark name. + + The MTX benchmarks are multi-task RL environments for multiple Metaworld tasks. + A fixed set of seeds is generated. + The training and testing environments are exactly the same. + The only purpose of MTX is to evaluate skill acquisition across multiple tasks. + + Take a look in `env_dict.py` for the list of tasks in each benchmark. + + Args: + mt_bench: The name of the MT benchmark. + seed: The random seed to use for the benchmark. + Returns: + The MTX Benchmark. + """ + + if mt_bench not in MT_BENCHMARKS_TRAIN_ENV_NAMES: + if mt_bench == "MT1-v3": + raise ValueError( + "Use `get_mt1_v3_benchmark` to get the MT1-v3 benchmark.") + raise ValueError(f"Invalid MT benchmark name: {mt_bench}") + + env_names = MT_BENCHMARKS_TRAIN_ENV_NAMES[mt_bench] + return Benchmark( + name=mt_bench, + train_env_names=env_names, + test_env_names=env_names, + seed=seed, + test_train_same_seed=True, + env_kwargs_overrides=_MT_ENV_KWARGS_OVERRIDE, + num_tasks_per_env=num_tasks_per_env, + ) + + +def get_mtCustom_v3_benchmark( + env_names: list[str], + seed: int | None = None, + num_tasks_per_env: int | None = None, +) -> Benchmark: + """Returns a custom MT benchmark for a given list of environment names. + + The custom MT benchmark is a multi-task RL environment for multiple Metaworld tasks. + A fixed set of seeds is generated. + The training and testing environments are exactly the same. + The only purpose of the custom MT benchmark is to evaluate skill acquisition across multiple tasks. + + Args: + env_names: The list of environment names. + seed: The random seed to use for the benchmark. + Returns: + The custom MT Benchmark. + """ + + return Benchmark( + name="custom-mt-envs", + train_env_names=env_names, + test_env_names=env_names, + seed=seed, + test_train_same_seed=True, + env_kwargs_overrides=_MT_ENV_KWARGS_OVERRIDE, + num_tasks_per_env=num_tasks_per_env, + ) + + +def get_ml1_v3_benchmark(env_name: str, seed: int | None = None, num_tasks_per_env: int | None = None) -> Benchmark: + """Returns the ML1 benchmark for a given environment name. + + The ML1 benchmark is a goal-conditioned RL environment for a single Metaworld task. + The train and test environments contain different sets of seeds. + The only purpose of ML1 is to evaluate meta-learning on a single task. + The goal position is zeroed out in the observation. + + Args: + env_name: The name of the environment. + seed: The random seed to use for the benchmark. + Returns: + The ML1 Benchmark. + """ + + env_names = [env_name] + return Benchmark( + name="ML1-v3", + train_env_names=env_names, + test_env_names=env_names, + seed=seed, + test_train_same_seed=False, + env_kwargs_overrides=_ML_ENV_KWARGS_OVERRIDE, + num_tasks_per_env=num_tasks_per_env, + ) + + +def get_mlX_v3_benchmark(ml_bench: MLXv3Benchmarks, + seed: int | None = None, + num_tasks_per_env: int | None = None) -> Benchmark: + """Returns the MLX benchmark for a given ML benchmark name. + + The MLX benchmarks are multi-task RL environments for multiple Metaworld tasks. + The train and test environments contain different sets of seeds and different tasks. + The goal position is zeroed out in the observation. + + Take a look in `env_dict.py` for the list of tasks in each benchmark. + + Args: + ml_bench: The name of the ML benchmark. + seed: The random seed to use for the benchmark. + Returns: + The MLX Benchmark. + """ + + if ml_bench not in ML_BENCHMARKS: + if ml_bench == "ML1-v3": + raise ValueError( + "Use `get_ml1_v3_benchmark` to get the ML1 benchmark.") + raise ValueError(f"Invalid ML benchmark name: {ml_bench}") + + ml_envs = ML_BENCHMARKS[ml_bench] + return Benchmark( + name=ml_bench, + train_env_names=ml_envs['train'], + test_env_names=ml_envs['test'], + seed=seed, + test_train_same_seed=False, + env_kwargs_overrides=_ML_ENV_KWARGS_OVERRIDE, + num_tasks_per_env=num_tasks_per_env, + ) + + +def get_mlCustom_v3_benchmark( + train_env_names: list[str], + test_env_names: list[str], + seed: int | None = None, + num_tasks_per_env: int | None = None, +) -> Benchmark: + """Returns a custom ML benchmark for given lists of training and testing environment names. + + The custom ML benchmark is a multi-task RL environment for multiple Metaworld tasks. + The train and test environments contain different sets of seeds and different tasks. + The goal position is zeroed out in the observation. + + Args: + train_envs: The list of training environment names. + test_envs: The list of testing environment names. + seed: The random seed to use for the benchmark. + Returns: + The custom ML Benchmark. + """ + + return Benchmark( + name="custom-ml-envs", + train_env_names=train_env_names, + test_env_names=test_env_names, + seed=seed, + test_train_same_seed=False, + env_kwargs_overrides=_ML_ENV_KWARGS_OVERRIDE, + num_tasks_per_env=num_tasks_per_env, + ) diff --git a/metaworld/env_dict.py b/metaworld/env_dict.py index 06a517766..b2b78345a 100644 --- a/metaworld/env_dict.py +++ b/metaworld/env_dict.py @@ -1,465 +1,252 @@ -"""Dictionaries mapping environment name strings to environment classes, -and organising them into various collections and splits for the benchmarks.""" +""" +Dictionaries mapping environment name strings to environment classes, +and organising them into various collections and splits for the benchmarks. +""" from __future__ import annotations -import re from collections import OrderedDict -from typing import Dict, List, Literal -from typing import OrderedDict as Typing_OrderedDict -from typing import Sequence, Union - -import numpy as np -from typing_extensions import TypeAlias +from typing import Literal, OrderedDict +import gymnasium as gym from metaworld import envs from metaworld.sawyer_xyz_env import SawyerXYZEnv -# Utils - -EnvDict: TypeAlias = "Typing_OrderedDict[str, type[SawyerXYZEnv]]" -TrainTestEnvDict: TypeAlias = "Typing_OrderedDict[Literal['train', 'test'], EnvDict]" -EnvArgsKwargsDict: TypeAlias = ( - "Dict[str, Dict[Literal['args', 'kwargs'], Union[List, Dict]]]" -) - -ENV_CLS_MAP = { - "assembly-v3": envs.SawyerNutAssemblyEnvV3, - "basketball-v3": envs.SawyerBasketballEnvV3, - "bin-picking-v3": envs.SawyerBinPickingEnvV3, - "box-close-v3": envs.SawyerBoxCloseEnvV3, - "button-press-topdown-v3": envs.SawyerButtonPressTopdownEnvV3, - "button-press-topdown-wall-v3": envs.SawyerButtonPressTopdownWallEnvV3, - "button-press-v3": envs.SawyerButtonPressEnvV3, - "button-press-wall-v3": envs.SawyerButtonPressWallEnvV3, - "coffee-button-v3": envs.SawyerCoffeeButtonEnvV3, - "coffee-pull-v3": envs.SawyerCoffeePullEnvV3, - "coffee-push-v3": envs.SawyerCoffeePushEnvV3, - "dial-turn-v3": envs.SawyerDialTurnEnvV3, - "disassemble-v3": envs.SawyerNutDisassembleEnvV3, - "door-close-v3": envs.SawyerDoorCloseEnvV3, - "door-lock-v3": envs.SawyerDoorLockEnvV3, - "door-open-v3": envs.SawyerDoorEnvV3, - "door-unlock-v3": envs.SawyerDoorUnlockEnvV3, - "hand-insert-v3": envs.SawyerHandInsertEnvV3, - "drawer-close-v3": envs.SawyerDrawerCloseEnvV3, - "drawer-open-v3": envs.SawyerDrawerOpenEnvV3, - "faucet-open-v3": envs.SawyerFaucetOpenEnvV3, - "faucet-close-v3": envs.SawyerFaucetCloseEnvV3, - "hammer-v3": envs.SawyerHammerEnvV3, - "handle-press-side-v3": envs.SawyerHandlePressSideEnvV3, - "handle-press-v3": envs.SawyerHandlePressEnvV3, - "handle-pull-side-v3": envs.SawyerHandlePullSideEnvV3, - "handle-pull-v3": envs.SawyerHandlePullEnvV3, - "lever-pull-v3": envs.SawyerLeverPullEnvV3, - "peg-insert-side-v3": envs.SawyerPegInsertionSideEnvV3, - "pick-place-wall-v3": envs.SawyerPickPlaceWallEnvV3, - "pick-out-of-hole-v3": envs.SawyerPickOutOfHoleEnvV3, - "reach-v3": envs.SawyerReachEnvV3, - "push-back-v3": envs.SawyerPushBackEnvV3, - "push-v3": envs.SawyerPushEnvV3, - "pick-place-v3": envs.SawyerPickPlaceEnvV3, - "plate-slide-v3": envs.SawyerPlateSlideEnvV3, - "plate-slide-side-v3": envs.SawyerPlateSlideSideEnvV3, - "plate-slide-back-v3": envs.SawyerPlateSlideBackEnvV3, - "plate-slide-back-side-v3": envs.SawyerPlateSlideBackSideEnvV3, - "peg-unplug-side-v3": envs.SawyerPegUnplugSideEnvV3, - "soccer-v3": envs.SawyerSoccerEnvV3, - "stick-push-v3": envs.SawyerStickPushEnvV3, - "stick-pull-v3": envs.SawyerStickPullEnvV3, - "push-wall-v3": envs.SawyerPushWallEnvV3, - "reach-wall-v3": envs.SawyerReachWallEnvV3, - "shelf-place-v3": envs.SawyerShelfPlaceEnvV3, - "sweep-into-v3": envs.SawyerSweepIntoGoalEnvV3, - "sweep-v3": envs.SawyerSweepEnvV3, - "window-open-v3": envs.SawyerWindowOpenEnvV3, - "window-close-v3": envs.SawyerWindowCloseEnvV3, -} - - -def _get_env_dict(env_names: Sequence[str]) -> EnvDict: - """Returns an `OrderedDict` containing `(env_name, env_cls)` tuples for the given env_names. - - Args: - env_names: The environment names - - Returns: - The appropriate `OrderedDict. - """ - return OrderedDict([(env_name, ENV_CLS_MAP[env_name]) for env_name in env_names]) - - -def _get_train_test_env_dict( - train_env_names: Sequence[str], test_env_names: Sequence[str] -) -> TrainTestEnvDict: - """Returns an `OrderedDict` containing two sub-keys ("train" and "test" at positions 0 and 1), - each containing the appropriate `OrderedDict` for the train and test classes of the benchmark. - - Args: - train_env_names: The train environment names. - test_env_names: The test environment names - - Returns: - The appropriate `OrderedDict`. - """ - return OrderedDict( - ( - ("train", _get_env_dict(train_env_names)), - ("test", _get_env_dict(test_env_names)), - ) - ) - - -def _get_args_kwargs(all_envs: EnvDict, env_subset: EnvDict) -> EnvArgsKwargsDict: - """Returns containing a `dict` of "args" and "kwargs" for each environment in a given list of environments. - Specifically, sets an empty "args" array and a "kwargs" dictionary with a "task_id" key for each env. - - Args: - all_envs: The full list of envs - env_subset: The subset of envs to get args and kwargs for - - Returns: - The args and kwargs dictionary. - """ - return { - key: dict(args=[], kwargs={"task_id": list(all_envs.keys()).index(key)}) - for key, _ in env_subset.items() - } - - -def _create_hidden_goal_envs(all_envs: EnvDict) -> EnvDict: - """Create versions of the environments with the goal hidden. - - Args: - all_envs: The full list of envs in the benchmark. - - Returns: - An `EnvDict` where the classes have been modified to hide the goal. - """ - hidden_goal_envs = {} - for env_name, env_cls in all_envs.items(): - d = {} - - def initialize(env, seed=None): - if seed is not None: - st0 = np.random.get_state() - np.random.seed(seed) - super(type(env), env).__init__() - env._partially_observable = True - del env.sawyer_observation_space - env._freeze_rand_vec = False - env._set_task_called = True - env.reset() - env._freeze_rand_vec = True - if seed is not None: - env.seed(seed=seed) - np.random.set_state(st0) - - d["__init__"] = initialize - hg_env_name = re.sub( - r"(^|[-])\s*([a-zA-Z])", lambda p: p.group(0).upper(), env_name - ) - hg_env_name = hg_env_name.replace("-", "") - hg_env_key = f"{env_name}-goal-hidden" - hg_env_name = f"{hg_env_name}GoalHidden" - HiddenGoalEnvCls = type(hg_env_name, (env_cls,), d) - hidden_goal_envs[hg_env_key] = HiddenGoalEnvCls - - return OrderedDict(hidden_goal_envs) - - -def _create_observable_goal_envs(all_envs: EnvDict) -> EnvDict: - """Create versions of the environments with the goal observable. - - Args: - all_envs: The full list of envs in the benchmark. - - Returns: - An `EnvDict` where the classes have been modified to make the goal observable. - """ - observable_goal_envs = {} - for env_name, env_cls in all_envs.items(): - d = {} - - def initialize(env, seed=None, render_mode=None): - if seed is not None: - st0 = np.random.get_state() - np.random.seed(seed) - super(type(env), env).__init__() - - env._partially_observable = False - env._freeze_rand_vec = False - del env.sawyer_observation_space - env._set_task_called = True - env.render_mode = render_mode - env.reset() - env._freeze_rand_vec = True - if seed is not None: - env.seed(seed) - np.random.set_state(st0) - - d["__init__"] = initialize - og_env_name = re.sub( - r"(^|[-])\s*([a-zA-Z])", lambda p: p.group(0).upper(), env_name - ) - og_env_name = og_env_name.replace("-", "") - - og_env_key = f"{env_name}-goal-observable" - og_env_name = f"{og_env_name}GoalObservable" - ObservableGoalEnvCls = type(og_env_name, (env_cls,), d) - observable_goal_envs[og_env_key] = ObservableGoalEnvCls - - return OrderedDict(observable_goal_envs) - - -# V3 DICTS - -ALL_V3_ENVIRONMENTS = _get_env_dict( - [ - "assembly-v3", - "basketball-v3", - "bin-picking-v3", - "box-close-v3", - "button-press-topdown-v3", - "button-press-topdown-wall-v3", - "button-press-v3", - "button-press-wall-v3", - "coffee-button-v3", - "coffee-pull-v3", - "coffee-push-v3", - "dial-turn-v3", - "disassemble-v3", - "door-close-v3", - "door-lock-v3", - "door-open-v3", - "door-unlock-v3", - "hand-insert-v3", - "drawer-close-v3", - "drawer-open-v3", - "faucet-open-v3", - "faucet-close-v3", - "hammer-v3", - "handle-press-side-v3", - "handle-press-v3", - "handle-pull-side-v3", - "handle-pull-v3", - "lever-pull-v3", - "pick-place-wall-v3", - "pick-out-of-hole-v3", - "pick-place-v3", - "plate-slide-v3", - "plate-slide-side-v3", - "plate-slide-back-v3", - "plate-slide-back-side-v3", - "peg-insert-side-v3", - "peg-unplug-side-v3", - "soccer-v3", - "stick-push-v3", - "stick-pull-v3", - "push-v3", - "push-wall-v3", - "push-back-v3", - "reach-v3", - "reach-wall-v3", - "shelf-place-v3", - "sweep-into-v3", - "sweep-v3", - "window-open-v3", - "window-close-v3", +ENV_CLASS_MAP: OrderedDict[str, type[SawyerXYZEnv]] = OrderedDict([ + (getattr(env_class, 'ENV_NAME'), env_class) + for env_class in [ + envs.SawyerNutAssemblyEnvV3, + envs.SawyerBasketballEnvV3, + envs.SawyerBinPickingEnvV3, + envs.SawyerBoxCloseEnvV3, + envs.SawyerButtonPressTopdownEnvV3, + envs.SawyerButtonPressTopdownWallEnvV3, + envs.SawyerButtonPressEnvV3, + envs.SawyerButtonPressWallEnvV3, + envs.SawyerCoffeeButtonEnvV3, + envs.SawyerCoffeePullEnvV3, + envs.SawyerCoffeePushEnvV3, + envs.SawyerDialTurnEnvV3, + envs.SawyerNutDisassembleEnvV3, + envs.SawyerDoorCloseEnvV3, + envs.SawyerDoorLockEnvV3, + envs.SawyerDoorEnvV3, + envs.SawyerDoorUnlockEnvV3, + envs.SawyerHandInsertEnvV3, + envs.SawyerDrawerCloseEnvV3, + envs.SawyerDrawerOpenEnvV3, + envs.SawyerFaucetOpenEnvV3, + envs.SawyerFaucetCloseEnvV3, + envs.SawyerHammerEnvV3, + envs.SawyerHandlePressSideEnvV3, + envs.SawyerHandlePressEnvV3, + envs.SawyerHandlePullSideEnvV3, + envs.SawyerHandlePullEnvV3, + envs.SawyerLeverPullEnvV3, + envs.SawyerPegInsertionSideEnvV3, + envs.SawyerPickPlaceWallEnvV3, + envs.SawyerPickOutOfHoleEnvV3, + envs.SawyerReachEnvV3, + envs.SawyerPushBackEnvV3, + envs.SawyerPushEnvV3, + envs.SawyerPickPlaceEnvV3, + envs.SawyerPlateSlideEnvV3, + envs.SawyerPlateSlideSideEnvV3, + envs.SawyerPlateSlideBackEnvV3, + envs.SawyerPlateSlideBackSideEnvV3, + envs.SawyerPegUnplugSideEnvV3, + envs.SawyerSoccerEnvV3, + envs.SawyerStickPushEnvV3, + envs.SawyerStickPullEnvV3, + envs.SawyerPushWallEnvV3, + envs.SawyerReachWallEnvV3, + envs.SawyerShelfPlaceEnvV3, + envs.SawyerSweepIntoGoalEnvV3, + envs.SawyerSweepEnvV3, + envs.SawyerWindowOpenEnvV3, + envs.SawyerWindowCloseEnvV3, ] -) - +]) +'''Mapping from environment name to environment class for all environments.''' -ALL_V3_ENVIRONMENTS_GOAL_HIDDEN = _create_hidden_goal_envs(ALL_V3_ENVIRONMENTS) -ALL_V3_ENVIRONMENTS_GOAL_OBSERVABLE = _create_observable_goal_envs(ALL_V3_ENVIRONMENTS) +ENV_NAMES = list(ENV_CLASS_MAP.keys()) +'''List of all environment names.''' # MT Dicts -MT10_V3 = _get_env_dict( - [ - "reach-v3", - "push-v3", - "pick-place-v3", - "door-open-v3", - "drawer-open-v3", - "drawer-close-v3", - "button-press-topdown-v3", - "peg-insert-side-v3", - "window-open-v3", - "window-close-v3", - ] -) -MT10_V3_ARGS_KWARGS = _get_args_kwargs(ALL_V3_ENVIRONMENTS, MT10_V3) - -MT25_V3 = _get_env_dict( - [ - "reach-v3", - "push-v3", - "pick-place-v3", - "door-open-v3", - "drawer-open-v3", - "drawer-close-v3", - "button-press-topdown-v3", - "peg-insert-side-v3", - "window-open-v3", - "window-close-v3", - "coffee-pull-v3", - "pick-out-of-hole-v3", - "disassemble-v3", - "pick-place-wall-v3", - "basketball-v3", - "stick-pull-v3", - "button-press-wall-v3", - "faucet-open-v3", - "door-lock-v3", - "lever-pull-v3", - "sweep-into-v3", - "faucet-close-v3", - "coffee-button-v3", - "button-press-topdown-wall-v3", - "dial-turn-v3", - ] -) -MT25_V3_ARGS_KWARGS = _get_args_kwargs(ALL_V3_ENVIRONMENTS, MT25_V3) - +MT10_V3_ENV_NAMES = [ + 'reach-v3', + 'push-v3', + 'pick-place-v3', + 'door-open-v3', + 'drawer-open-v3', + 'drawer-close-v3', + 'button-press-topdown-v3', + 'peg-insert-side-v3', + 'window-open-v3', + 'window-close-v3', +] + +MT25_V3_ENV_NAMES = [ + 'reach-v3', + 'push-v3', + 'pick-place-v3', + 'door-open-v3', + 'drawer-open-v3', + 'drawer-close-v3', + 'button-press-topdown-v3', + 'peg-insert-side-v3', + 'window-open-v3', + 'window-close-v3', + 'coffee-pull-v3', + 'pick-out-of-hole-v3', + 'disassemble-v3', + 'pick-place-wall-v3', + 'basketball-v3', + 'stick-pull-v3', + 'button-press-wall-v3', + 'faucet-open-v3', + 'door-lock-v3', + 'lever-pull-v3', + 'sweep-into-v3', + 'faucet-close-v3', + 'coffee-button-v3', + 'button-press-topdown-wall-v3', + 'dial-turn-v3', +] + +MT50_V3_ENV_NAMES = ENV_NAMES + +MT_BENCHMARKS_TRAIN_ENV_NAMES = { + 'MT10-v3': MT10_V3_ENV_NAMES, + 'MT25-v3': MT25_V3_ENV_NAMES, + 'MT50-v3': MT50_V3_ENV_NAMES, +} -MT50_V3 = ALL_V3_ENVIRONMENTS -MT50_V3_ARGS_KWARGS = _get_args_kwargs(ALL_V3_ENVIRONMENTS, MT50_V3) +MTXv3Benchmarks = Literal['MT10-v3', 'MT25-v3', 'MT50-v3'] # ML Dicts -ML1_V3 = _get_train_test_env_dict( - list(ALL_V3_ENVIRONMENTS.keys()), list(ALL_V3_ENVIRONMENTS.keys()) -) -ML1_args_kwargs = _get_args_kwargs(ALL_V3_ENVIRONMENTS, ML1_V3["train"]) - -ML10_V3 = _get_train_test_env_dict( - train_env_names=[ - "reach-v3", - "push-v3", - "pick-place-v3", - "door-open-v3", - "drawer-close-v3", - "button-press-topdown-v3", - "peg-insert-side-v3", - "window-open-v3", - "sweep-v3", - "basketball-v3", +ML10_V3_SPLITS = { + 'train': [ + 'reach-v3', + 'push-v3', + 'pick-place-v3', + 'door-open-v3', + 'drawer-close-v3', + 'button-press-topdown-v3', + 'peg-insert-side-v3', + 'window-open-v3', + 'sweep-v3', + 'basketball-v3', ], - test_env_names=[ - "drawer-open-v3", - "door-close-v3", - "shelf-place-v3", - "sweep-into-v3", - "lever-pull-v3", + 'test': [ + 'drawer-open-v3', + 'door-close-v3', + 'shelf-place-v3', + 'sweep-into-v3', + 'lever-pull-v3', ], -) -ML10_ARGS_KWARGS = { - "train": _get_args_kwargs(ALL_V3_ENVIRONMENTS, ML10_V3["train"]), - "test": _get_args_kwargs(ALL_V3_ENVIRONMENTS, ML10_V3["test"]), } - -ML25_V3 = _get_train_test_env_dict( - train_env_names=[ - "reach-v3", - "push-v3", - "pick-place-v3", - "door-open-v3", - "drawer-open-v3", - "drawer-close-v3", - "button-press-topdown-v3", - "peg-insert-side-v3", - "window-open-v3", - "window-close-v3", - "coffee-pull-v3", - "pick-out-of-hole-v3", - "disassemble-v3", - "pick-place-wall-v3", - "basketball-v3", - "stick-pull-v3", - "button-press-wall-v3", - "faucet-open-v3", - "door-lock-v3", - "lever-pull-v3", - "sweep-into-v3", - "faucet-close-v3", - "coffee-button-v3", - "button-press-topdown-wall-v3", - "dial-turn-v3", +ML25_V3_SPLITS = { + 'train': [ + 'reach-v3', + 'push-v3', + 'pick-place-v3', + 'door-open-v3', + 'drawer-open-v3', + 'drawer-close-v3', + 'button-press-topdown-v3', + 'peg-insert-side-v3', + 'window-open-v3', + 'window-close-v3', + 'coffee-pull-v3', + 'pick-out-of-hole-v3', + 'disassemble-v3', + 'pick-place-wall-v3', + 'basketball-v3', + 'stick-pull-v3', + 'button-press-wall-v3', + 'faucet-open-v3', + 'door-lock-v3', + 'lever-pull-v3', + 'sweep-into-v3', + 'faucet-close-v3', + 'coffee-button-v3', + 'button-press-topdown-wall-v3', + 'dial-turn-v3', ], - test_env_names=[ - "basketball-v3", - "door-close-v3", - "shelf-place-v3", - "sweep-v3", - "button-press-v3", + 'test': [ + 'basketball-v3', + 'door-close-v3', + 'shelf-place-v3', + 'sweep-v3', + 'button-press-v3', ], -) - -ML25_ARGS_KWARGS = { - "train": _get_args_kwargs(ALL_V3_ENVIRONMENTS, ML25_V3["train"]), - "test": _get_args_kwargs(ALL_V3_ENVIRONMENTS, ML25_V3["test"]), } - -ML45_V3 = _get_train_test_env_dict( - train_env_names=[ - "assembly-v3", - "basketball-v3", - "button-press-topdown-v3", - "button-press-topdown-wall-v3", - "button-press-v3", - "button-press-wall-v3", - "coffee-button-v3", - "coffee-pull-v3", - "coffee-push-v3", - "dial-turn-v3", - "disassemble-v3", - "door-close-v3", - "door-open-v3", - "drawer-close-v3", - "drawer-open-v3", - "faucet-open-v3", - "faucet-close-v3", - "hammer-v3", - "handle-press-side-v3", - "handle-press-v3", - "handle-pull-side-v3", - "handle-pull-v3", - "lever-pull-v3", - "pick-place-wall-v3", - "pick-out-of-hole-v3", - "push-back-v3", - "pick-place-v3", - "plate-slide-v3", - "plate-slide-side-v3", - "plate-slide-back-v3", - "plate-slide-back-side-v3", - "peg-insert-side-v3", - "peg-unplug-side-v3", - "soccer-v3", - "stick-push-v3", - "stick-pull-v3", - "push-wall-v3", - "push-v3", - "reach-wall-v3", - "reach-v3", - "shelf-place-v3", - "sweep-into-v3", - "sweep-v3", - "window-open-v3", - "window-close-v3", +ML45_V3_SPLITS = { + 'train': [ + 'assembly-v3', + 'basketball-v3', + 'button-press-topdown-v3', + 'button-press-topdown-wall-v3', + 'button-press-v3', + 'button-press-wall-v3', + 'coffee-button-v3', + 'coffee-pull-v3', + 'coffee-push-v3', + 'dial-turn-v3', + 'disassemble-v3', + 'door-close-v3', + 'door-open-v3', + 'drawer-close-v3', + 'drawer-open-v3', + 'faucet-open-v3', + 'faucet-close-v3', + 'hammer-v3', + 'handle-press-side-v3', + 'handle-press-v3', + 'handle-pull-side-v3', + 'handle-pull-v3', + 'lever-pull-v3', + 'pick-place-wall-v3', + 'pick-out-of-hole-v3', + 'push-back-v3', + 'pick-place-v3', + 'plate-slide-v3', + 'plate-slide-side-v3', + 'plate-slide-back-v3', + 'plate-slide-back-side-v3', + 'peg-insert-side-v3', + 'peg-unplug-side-v3', + 'soccer-v3', + 'stick-push-v3', + 'stick-pull-v3', + 'push-wall-v3', + 'push-v3', + 'reach-wall-v3', + 'reach-v3', + 'shelf-place-v3', + 'sweep-into-v3', + 'sweep-v3', + 'window-open-v3', + 'window-close-v3', ], - test_env_names=[ - "bin-picking-v3", - "box-close-v3", - "hand-insert-v3", - "door-lock-v3", - "door-unlock-v3", + 'test': [ + 'bin-picking-v3', + 'box-close-v3', + 'hand-insert-v3', + 'door-lock-v3', + 'door-unlock-v3', ], -) -ML45_ARGS_KWARGS = { - "train": _get_args_kwargs(ALL_V3_ENVIRONMENTS, ML45_V3["train"]), - "test": _get_args_kwargs(ALL_V3_ENVIRONMENTS, ML45_V3["test"]), } + +ML_BENCHMARKS = { + 'ML10-v3': ML10_V3_SPLITS, + 'ML25-v3': ML25_V3_SPLITS, + 'ML45-v3': ML45_V3_SPLITS, +} + +MLXv3Benchmarks = Literal['ML10-v3', 'ML25-v3', 'ML45-v3'] diff --git a/metaworld/envs/__init__.py b/metaworld/envs/__init__.py index 848d96077..a956b8e9b 100644 --- a/metaworld/envs/__init__.py +++ b/metaworld/envs/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from metaworld.envs.sawyer_assembly_peg_v3 import SawyerNutAssemblyEnvV3 from metaworld.envs.sawyer_basketball_v3 import SawyerBasketballEnvV3 from metaworld.envs.sawyer_bin_picking_v3 import SawyerBinPickingEnvV3 diff --git a/metaworld/envs/sawyer_assembly_peg_v3.py b/metaworld/envs/sawyer_assembly_peg_v3.py index d1741fa7f..df8da7b8e 100644 --- a/metaworld/envs/sawyer_assembly_peg_v3.py +++ b/metaworld/envs/sawyer_assembly_peg_v3.py @@ -8,22 +8,19 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict, ObservationDict from metaworld.utils.reward_utils import tolerance class SawyerNutAssemblyEnvV3(SawyerXYZEnv): + ENV_NAME: str = "assembly-v3" + WRENCH_HANDLE_LENGTH: float = 0.02 def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1, 0.5) @@ -32,18 +29,6 @@ def __init__( goal_low = (-0.1, 0.75, 0.1) goal_high = (0.1, 0.85, 0.1) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_angle": 0.3, "obj_init_pos": np.array([0, 0.6, 0.02], dtype=np.float32), @@ -60,13 +45,19 @@ def __init__( np.hstack((obj_high, goal_high)), dtype=np.float64, ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_assembly_peg.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -126,7 +117,8 @@ def reset_model(self) -> npt.NDArray[np.float64]: if self.reward_function_version == "v1": self.obj_height = self.data.site_xpos[ - mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_SITE, "RoundNut-8") + mujoco.mj_name2id( + self.model, mujoco.mjtObj.mjOBJ_SITE, "RoundNut-8") ][2] self.heightTarget = self.obj_height + 0.1 self.pickCompleted = False diff --git a/metaworld/envs/sawyer_basketball_v3.py b/metaworld/envs/sawyer_basketball_v3.py index 63215615f..c98a36b52 100644 --- a/metaworld/envs/sawyer_basketball_v3.py +++ b/metaworld/envs/sawyer_basketball_v3.py @@ -7,23 +7,20 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerBasketballEnvV3(SawyerXYZEnv): + ENV_NAME: str = "basketball-v3" + PAD_SUCCESS_MARGIN: float = 0.06 TARGET_RADIUS: float = 0.08 def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1, 0.5) @@ -32,17 +29,6 @@ def __init__( goal_low = (-0.1, 0.85, 0.0) goal_high = (0.1, 0.9 + 1e-7, 0.0) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_angle": 0.3, "obj_init_pos": np.array([0, 0.6, 0.03], dtype=np.float32), @@ -64,11 +50,16 @@ def __init__( dtype=np.float64, ) + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) + @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_basketball.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -115,7 +106,8 @@ def reset_model(self) -> npt.NDArray[np.float64]: goal_pos = self._get_state_rand_vec() basket_pos = goal_pos[3:] assert self.obj_init_pos is not None - self.obj_init_pos = np.concatenate([goal_pos[:2], [self.obj_init_pos[-1]]]) + self.obj_init_pos = np.concatenate( + [goal_pos[:2], [self.obj_init_pos[-1]]]) self.model.body("basket_goal").pos = basket_pos self._target_pos = self.data.site("goal").xpos self._set_obj_xyz(self.obj_init_pos) @@ -259,7 +251,8 @@ def compute_reward( cond = self.pickCompleted and (reachDist < 0.1) and not objDropped if cond: placeRew = 1000 * (self.maxPlacingDist - placingDist) + c1 * ( - np.exp(-(placingDist**2) / c2) + np.exp(-(placingDist**2) / c3) + np.exp(-(placingDist**2) / c2) + + np.exp(-(placingDist**2) / c3) ) placeRew = max(placeRew, 0) placeRew, placingDist = [placeRew, placingDist] diff --git a/metaworld/envs/sawyer_bin_picking_v3.py b/metaworld/envs/sawyer_bin_picking_v3.py index 184bdbd90..3754c09ff 100644 --- a/metaworld/envs/sawyer_bin_picking_v3.py +++ b/metaworld/envs/sawyer_bin_picking_v3.py @@ -7,7 +7,7 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils @@ -26,14 +26,11 @@ class SawyerBinPickingEnvV3(SawyerXYZEnv): - (11/23/20) Updated reward function to new pick-place style """ + ENV_NAME: str = "bin-picking-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.07) hand_high = (0.5, 1, 0.5) @@ -43,16 +40,6 @@ def __init__( goal_low = np.array([0.1199, 0.699, -0.001]) goal_high = np.array([0.1201, 0.701, +0.001]) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version self.init_config: InitConfigDict = { "obj_init_angle": 0.3, "obj_init_pos": np.array([-0.12, 0.7, 0.02]), @@ -68,8 +55,8 @@ def __init__( self.liftThresh = 0.1 self.hand_and_obj_space = Box( - np.hstack((self.hand_low, obj_low)), - np.hstack((self.hand_high, obj_high)), + np.hstack((hand_low, obj_low)), + np.hstack((hand_high, obj_high)), dtype=np.float64, ) @@ -86,11 +73,16 @@ def __init__( dtype=np.float64, ) + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) + @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_bin_picking.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -321,7 +313,8 @@ def compute_reward( abs(objPos[0] - placingGoal[0]) < 0.05 and abs(objPos[1] - placingGoal[1]) < 0.05 ): - placeRew, placingDist = [-200 * action[-1] + placeRew, placingDist] + placeRew, placingDist = [-200 * + action[-1] + placeRew, placingDist] else: placeRew, placingDist = [placeRew, placingDist] else: diff --git a/metaworld/envs/sawyer_box_close_v3.py b/metaworld/envs/sawyer_box_close_v3.py index 126630eb0..59ce45ebb 100644 --- a/metaworld/envs/sawyer_box_close_v3.py +++ b/metaworld/envs/sawyer_box_close_v3.py @@ -8,20 +8,17 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerBoxCloseEnvV3(SawyerXYZEnv): + ENV_NAME: str = "box-close-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1, 0.5) @@ -30,17 +27,6 @@ def __init__( goal_low = (-0.1, 0.7, 0.133) goal_high = (0.1, 0.8, 0.133) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_angle": 0.3, "obj_init_pos": np.array([0, 0.55, 0.02], dtype=np.float32), @@ -53,7 +39,8 @@ def __init__( self._target_to_obj_init = None - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) self._random_reset_space = Box( np.hstack((obj_low, goal_low)), np.hstack((obj_high, goal_high)), @@ -63,11 +50,16 @@ def __init__( self.init_obj_quat = None self.liftThresh = 0.12 + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) + @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_box.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -113,7 +105,8 @@ def reset_model(self) -> npt.NDArray[np.float64]: goal_pos = self._get_state_rand_vec() while np.linalg.norm(goal_pos[:2] - goal_pos[-3:-1]) < 0.25: goal_pos = self._get_state_rand_vec() - self.obj_init_pos = np.concatenate([goal_pos[:2], [self.obj_init_pos[-1]]]) + self.obj_init_pos = np.concatenate( + [goal_pos[:2], [self.obj_init_pos[-1]]]) self._target_pos = goal_pos[-3:] self.model.body("boxbody").pos = np.concatenate( @@ -212,11 +205,14 @@ def compute_reward( if self.reward_function_version == "v2": reward_grab = SawyerBoxCloseEnvV3._reward_grab_effort(actions) reward_quat = SawyerBoxCloseEnvV3._reward_quat(obs) - reward_steps = SawyerBoxCloseEnvV3._reward_pos(obs, self._target_pos) + reward_steps = SawyerBoxCloseEnvV3._reward_pos( + obs, self._target_pos) reward = sum( ( - 2.0 * reward_utils.hamacher_product(reward_grab, reward_steps[0]), + 2.0 * + reward_utils.hamacher_product( + reward_grab, reward_steps[0]), 8.0 * reward_steps[1], ) ) @@ -291,7 +287,8 @@ def compute_reward( cond = self.pickCompleted and (reachDist < 0.1) and not objDropped if cond: placeRew = 1000 * (self.maxPlacingDist - placingDist) + c1 * ( - np.exp(-(placingDist**2) / c2) + np.exp(-(placingDist**2) / c3) + np.exp(-(placingDist**2) / c2) + + np.exp(-(placingDist**2) / c3) ) placeRew = max(placeRew, 0) placeRew, placingDist = [placeRew, placingDist] diff --git a/metaworld/envs/sawyer_button_press_topdown_v3.py b/metaworld/envs/sawyer_button_press_topdown_v3.py index 20dc1d340..7eac7a999 100644 --- a/metaworld/envs/sawyer_button_press_topdown_v3.py +++ b/metaworld/envs/sawyer_button_press_topdown_v3.py @@ -8,36 +8,23 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerButtonPressTopdownEnvV3(SawyerXYZEnv): + ENV_NAME: str = "button-press-topdown-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1, 0.5) obj_low = (-0.1, 0.8, 0.115) obj_high = (0.1, 0.9, 0.115) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version self.init_config: InitConfigDict = { "obj_init_pos": np.array([0, 0.8, 0.115], dtype=np.float32), "hand_init_pos": np.array([0, 0.4, 0.2], dtype=np.float32), @@ -46,19 +33,25 @@ def __init__( self.obj_init_pos = self.init_config["obj_init_pos"] self.hand_init_pos = self.init_config["hand_init_pos"] - goal_low = self.hand_low - goal_high = self.hand_high + goal_low = hand_low + goal_high = hand_high self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_button_press_topdown.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: diff --git a/metaworld/envs/sawyer_button_press_topdown_wall_v3.py b/metaworld/envs/sawyer_button_press_topdown_wall_v3.py index b9ae80aff..a62fee2dd 100644 --- a/metaworld/envs/sawyer_button_press_topdown_wall_v3.py +++ b/metaworld/envs/sawyer_button_press_topdown_wall_v3.py @@ -8,37 +8,23 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerButtonPressTopdownWallEnvV3(SawyerXYZEnv): + ENV_NAME: str = "button-press-topdown-wall-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1, 0.5) obj_low = (-0.1, 0.8, 0.115) obj_high = (0.1, 0.9, 0.115) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([0, 0.8, 0.115], dtype=np.float32), "hand_init_pos": np.array([0, 0.4, 0.2], dtype=np.float32), @@ -47,19 +33,25 @@ def __init__( self.obj_init_pos = self.init_config["obj_init_pos"] self.hand_init_pos = self.init_config["hand_init_pos"] - goal_low = self.hand_low - goal_high = self.hand_high + goal_low = hand_low + goal_high = hand_high self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_button_press_topdown_wall.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: diff --git a/metaworld/envs/sawyer_button_press_v3.py b/metaworld/envs/sawyer_button_press_v3.py index b63eb757f..ceefb7801 100644 --- a/metaworld/envs/sawyer_button_press_v3.py +++ b/metaworld/envs/sawyer_button_press_v3.py @@ -7,37 +7,23 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerButtonPressEnvV3(SawyerXYZEnv): + ENV_NAME: str = "button-press-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1, 0.5) obj_low = (-0.1, 0.85, 0.115) obj_high = (0.1, 0.9, 0.115) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([0.0, 0.9, 0.115], dtype=np.float32), "hand_init_pos": np.array([0, 0.4, 0.2], dtype=np.float32), @@ -45,19 +31,25 @@ def __init__( self.goal = np.array([0, 0.78, 0.12]) self.obj_init_pos = self.init_config["obj_init_pos"] self.hand_init_pos = self.init_config["hand_init_pos"] - goal_low = self.hand_low - goal_high = self.hand_high + goal_low = hand_low + goal_high = hand_high self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_button_press.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: diff --git a/metaworld/envs/sawyer_button_press_wall_v3.py b/metaworld/envs/sawyer_button_press_wall_v3.py index 50c3d696a..b249dfe5a 100644 --- a/metaworld/envs/sawyer_button_press_wall_v3.py +++ b/metaworld/envs/sawyer_button_press_wall_v3.py @@ -7,37 +7,23 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerButtonPressWallEnvV3(SawyerXYZEnv): + ENV_NAME: str = "button-press-wall-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1, 0.5) obj_low = (-0.05, 0.85, 0.1149) obj_high = (0.05, 0.9, 0.1151) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([0.0, 0.9, 0.115], dtype=np.float32), "hand_init_pos": np.array([0, 0.4, 0.2], dtype=np.float32), @@ -46,20 +32,26 @@ def __init__( self.obj_init_pos = self.init_config["obj_init_pos"] self.hand_init_pos = self.init_config["hand_init_pos"] - goal_low = self.hand_low - goal_high = self.hand_high + goal_low = hand_low + goal_high = hand_high self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_button_press_wall.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -158,7 +150,8 @@ def compute_reward( reward = 0.0 if tcp_to_obj > 0.07: tcp_status = (1 - obs[3]) / 2.0 - reward = 2 * reward_utils.hamacher_product(tcp_status, near_button) + reward = 2 * \ + reward_utils.hamacher_product(tcp_status, near_button) else: reward = 2 reward += 2 * (1 + obs[3]) diff --git a/metaworld/envs/sawyer_coffee_button_v3.py b/metaworld/envs/sawyer_coffee_button_v3.py index 3f1b34a10..6d354d3d1 100644 --- a/metaworld/envs/sawyer_coffee_button_v3.py +++ b/metaworld/envs/sawyer_coffee_button_v3.py @@ -7,20 +7,17 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerCoffeeButtonEnvV3(SawyerXYZEnv): + ENV_NAME: str = "coffee-button-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: self.max_dist = 0.03 @@ -33,17 +30,6 @@ def __init__( goal_low = obj_low + np.array([-0.001, -0.22 + self.max_dist, 0.299]) goal_high = obj_high + np.array([+0.001, -0.22 + self.max_dist, 0.301]) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([0, 0.9, 0.28]), "obj_init_angle": 0.3, @@ -57,13 +43,19 @@ def __init__( self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_coffee.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: diff --git a/metaworld/envs/sawyer_coffee_pull_v3.py b/metaworld/envs/sawyer_coffee_pull_v3.py index c4fbdfbd8..b1d2eb7f5 100644 --- a/metaworld/envs/sawyer_coffee_pull_v3.py +++ b/metaworld/envs/sawyer_coffee_pull_v3.py @@ -8,20 +8,17 @@ from scipy.spatial.transform import Rotation from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerCoffeePullEnvV3(SawyerXYZEnv): + ENV_NAME: str = "coffee-pull-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1, 0.5) @@ -30,17 +27,6 @@ def __init__( goal_low = (-0.1, 0.55, -0.001) goal_high = (0.1, 0.65, +0.001) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([0, 0.75, 0.0]), "obj_init_angle": 0.3, @@ -56,13 +42,19 @@ def __init__( np.hstack((obj_high, goal_high)), dtype=np.float64, ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_coffee.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -119,7 +111,8 @@ def reset_model(self) -> npt.NDArray[np.float64]: pos_mug_init, pos_mug_goal = np.split(self._get_state_rand_vec(), 2) while np.linalg.norm(pos_mug_init[:2] - pos_mug_goal[:2]) < 0.15: - pos_mug_init, pos_mug_goal = np.split(self._get_state_rand_vec(), 2) + pos_mug_init, pos_mug_goal = np.split( + self._get_state_rand_vec(), 2) self._set_obj_xyz(pos_mug_init) self.obj_init_pos = pos_mug_init @@ -183,7 +176,8 @@ def compute_reward( reward, tcp_to_obj, tcp_opened, - float(np.linalg.norm(obj - target)), # recompute to avoid `scale` above + # recompute to avoid `scale` above + float(np.linalg.norm(obj - target)), object_grasped, in_place, ) diff --git a/metaworld/envs/sawyer_coffee_push_v3.py b/metaworld/envs/sawyer_coffee_push_v3.py index b5072f2b9..96a3cc375 100644 --- a/metaworld/envs/sawyer_coffee_push_v3.py +++ b/metaworld/envs/sawyer_coffee_push_v3.py @@ -8,20 +8,17 @@ from scipy.spatial.transform import Rotation from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerCoffeePushEnvV3(SawyerXYZEnv): + ENV_NAME: str = "coffee-push-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1, 0.5) @@ -30,17 +27,6 @@ def __init__( goal_low = (-0.05, 0.7, -0.001) goal_high = (0.05, 0.75, +0.001) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_angle": 0.3, "obj_init_pos": np.array([0.0, 0.6, 0.0]), @@ -56,13 +42,19 @@ def __init__( np.hstack((obj_high, goal_high)), dtype=np.float64, ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_coffee.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -119,7 +111,8 @@ def reset_model(self) -> npt.NDArray[np.float64]: pos_mug_init, pos_mug_goal = np.split(self._get_state_rand_vec(), 2) while np.linalg.norm(pos_mug_init[:2] - pos_mug_goal[:2]) < 0.15: - pos_mug_init, pos_mug_goal = np.split(self._get_state_rand_vec(), 2) + pos_mug_init, pos_mug_goal = np.split( + self._get_state_rand_vec(), 2) self._set_obj_xyz(pos_mug_init) self.obj_init_pos = pos_mug_init @@ -184,7 +177,8 @@ def compute_reward( reward, tcp_to_obj, tcp_opened, - float(np.linalg.norm(obj - target)), # recompute to avoid `scale` above + # recompute to avoid `scale` above + float(np.linalg.norm(obj - target)), object_grasped, in_place, ) diff --git a/metaworld/envs/sawyer_dial_turn_v3.py b/metaworld/envs/sawyer_dial_turn_v3.py index 25fa138c0..3669db4fe 100644 --- a/metaworld/envs/sawyer_dial_turn_v3.py +++ b/metaworld/envs/sawyer_dial_turn_v3.py @@ -7,22 +7,19 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerDialTurnEnvV3(SawyerXYZEnv): + ENV_NAME: str = "dial-turn-v3" + TARGET_RADIUS: float = 0.07 def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1, 0.5) @@ -31,17 +28,6 @@ def __init__( goal_low = (-0.1, 0.73, 0.0299) goal_high = (0.1, 0.83, 0.0301) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([0, 0.7, 0.0]), "hand_init_pos": np.array([0, 0.6, 0.2], dtype=np.float32), @@ -53,13 +39,19 @@ def __init__( self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_dial.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -89,7 +81,8 @@ def _get_pos_objects(self) -> npt.NDArray[Any]: dial_angle_rad = self.data.joint("knob_Joint_1").qpos offset = np.array( - [np.sin(dial_angle_rad).item(), -np.cos(dial_angle_rad).item(), 0.0] + [np.sin(dial_angle_rad).item(), - + np.cos(dial_angle_rad).item(), 0.0] ) dial_radius = 0.05 @@ -111,7 +104,8 @@ def reset_model(self) -> npt.NDArray[np.float64]: final_pos = goal_pos.copy() + np.array([0, 0.03, 0.03]) self._target_pos = final_pos self.model.body("dial").pos = self.obj_init_pos - self.dial_push_position = self._get_pos_objects() + np.array([0.05, 0.02, 0.09]) + self.dial_push_position = self._get_pos_objects() + \ + np.array([0.05, 0.02, 0.09]) self.model.site("goal").pos = self._target_pos assert self._target_pos is not None and self.obj_init_pos is not None @@ -127,7 +121,8 @@ def compute_reward( ), "`reset_model()` must be called before `compute_reward()`." if self.reward_function_version == "v2": obj = self._get_pos_objects() - dial_push_position = self._get_pos_objects() + np.array([0.05, 0.02, 0.09]) + dial_push_position = self._get_pos_objects() + \ + np.array([0.05, 0.02, 0.09]) tcp = self.tcp_center target = self._target_pos.copy() diff --git a/metaworld/envs/sawyer_disassemble_peg_v3.py b/metaworld/envs/sawyer_disassemble_peg_v3.py index 3e78ca51d..bb8d94724 100644 --- a/metaworld/envs/sawyer_disassemble_peg_v3.py +++ b/metaworld/envs/sawyer_disassemble_peg_v3.py @@ -8,22 +8,19 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerNutDisassembleEnvV3(SawyerXYZEnv): + ENV_NAME: str = "disassemble-v3" + WRENCH_HANDLE_LENGTH: float = 0.02 def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1, 0.5) @@ -32,17 +29,6 @@ def __init__( goal_low = (-0.1, 0.6, 0.1699) goal_high = (0.1, 0.75, 0.1701) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_angle": 0.3, "obj_init_pos": np.array([0, 0.7, 0.025]), @@ -64,11 +50,16 @@ def __init__( dtype=np.float64, ) + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) + @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_assembly_peg.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: diff --git a/metaworld/envs/sawyer_door_close_v3.py b/metaworld/envs/sawyer_door_close_v3.py index 0d48eebb6..4c9877db7 100644 --- a/metaworld/envs/sawyer_door_close_v3.py +++ b/metaworld/envs/sawyer_door_close_v3.py @@ -8,20 +8,17 @@ from scipy.spatial.transform import Rotation from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerDoorCloseEnvV3(SawyerXYZEnv): + ENV_NAME: str = "door-close-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: goal_low = (0.2, 0.65, 0.1499) goal_high = (0.3, 0.75, 0.1501) @@ -30,17 +27,6 @@ def __init__( obj_low = (0.0, 0.85, 0.15) obj_high = (0.1, 0.95, 0.15) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_angle": 0.3, "obj_init_pos": np.array([0.1, 0.95, 0.15], dtype=np.float32), @@ -51,17 +37,24 @@ def __init__( self.obj_init_angle = self.init_config["obj_init_angle"] self.hand_init_pos = self.init_config["hand_init_pos"] - self.door_qpos_adr = self.model.joint("doorjoint").qposadr.item() - self.door_qvel_adr = self.model.joint("doorjoint").dofadr.item() - - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) + + self.door_qpos_adr = self.model.joint("doorjoint").qposadr.item() + self.door_qvel_adr = self.model.joint("doorjoint").dofadr.item() + @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_door_pull.xml") def _get_pos_objects(self) -> npt.NDArray[Any]: @@ -101,7 +94,6 @@ def reset_model(self) -> npt.NDArray[np.float64]: return self._get_obs() - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: diff --git a/metaworld/envs/sawyer_door_lock_v3.py b/metaworld/envs/sawyer_door_lock_v3.py index fb670f419..4e96160bb 100644 --- a/metaworld/envs/sawyer_door_lock_v3.py +++ b/metaworld/envs/sawyer_door_lock_v3.py @@ -8,37 +8,23 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerDoorLockEnvV3(SawyerXYZEnv): + ENV_NAME: str = "door-lock-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, -0.15) hand_high = (0.5, 1, 0.5) obj_low = (-0.1, 0.8, 0.15) obj_high = (0.1, 0.85, 0.15) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([0, 0.85, 0.15], dtype=np.float32), "hand_init_pos": np.array([0, 0.6, 0.2], dtype=np.float32), @@ -47,21 +33,27 @@ def __init__( self.obj_init_pos = self.init_config["obj_init_pos"] self.hand_init_pos = self.init_config["hand_init_pos"] - goal_low = self.hand_low - goal_high = self.hand_high + goal_low = hand_low + goal_high = hand_high self._lock_length = 0.1 self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_door_lock.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -133,7 +125,8 @@ def compute_reward( scale = np.array([0.25, 1.0, 0.5]) tcp_to_obj = float(np.linalg.norm((obj - tcp) * scale)) - tcp_to_obj_init = float(np.linalg.norm((obj - self.init_left_pad) * scale)) + tcp_to_obj_init = float(np.linalg.norm( + (obj - self.init_left_pad) * scale)) obj_to_target = abs(self._target_pos[2] - obj[2]) diff --git a/metaworld/envs/sawyer_door_unlock_v3.py b/metaworld/envs/sawyer_door_unlock_v3.py index f1472ba33..0757900ca 100644 --- a/metaworld/envs/sawyer_door_unlock_v3.py +++ b/metaworld/envs/sawyer_door_unlock_v3.py @@ -7,20 +7,17 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerDoorUnlockEnvV3(SawyerXYZEnv): + ENV_NAME: str = "door-unlock-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, -0.15) hand_high = (0.5, 1, 0.5) @@ -29,17 +26,6 @@ def __init__( goal_low = (0.0, 0.64, 0.2100) goal_high = (0.2, 0.7, 0.2111) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([0, 0.85, 0.15]), "hand_init_pos": np.array([0, 0.6, 0.2], dtype=np.float32), @@ -53,13 +39,19 @@ def __init__( self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_door_lock.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -139,7 +131,8 @@ def compute_reward( scale = np.array([0.25, 1.0, 0.5]) shoulder_to_lock = (gripper + offset - lock) * scale - shoulder_to_lock_init = (self.init_tcp + offset - self.obj_init_pos) * scale + shoulder_to_lock_init = ( + self.init_tcp + offset - self.obj_init_pos) * scale # This `ready_to_push` reward should be a *hint* for the agent, not an # end in itself. Make sure to devalue it compared to the value of diff --git a/metaworld/envs/sawyer_door_v3.py b/metaworld/envs/sawyer_door_v3.py index a9e91c5b8..e4f868085 100644 --- a/metaworld/envs/sawyer_door_v3.py +++ b/metaworld/envs/sawyer_door_v3.py @@ -8,20 +8,17 @@ from scipy.spatial.transform import Rotation from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerDoorEnvV3(SawyerXYZEnv): + ENV_NAME: str = "door-open-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1, 0.5) @@ -30,17 +27,6 @@ def __init__( goal_low = (-0.3, 0.4, 0.1499) goal_high = (-0.2, 0.5, 0.1501) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_angle": 0.3, "obj_init_pos": np.array([0.1, 0.95, 0.15]), @@ -52,19 +38,25 @@ def __init__( self.obj_init_angle = self.init_config["obj_init_angle"] self.hand_init_pos = self.init_config["hand_init_pos"] - self.door_qpos_adr = self.model.joint("doorjoint").qposadr.item() - self.door_qvel_adr = self.model.joint("doorjoint").dofadr.item() - self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) + + self.door_qpos_adr = self.model.joint("doorjoint").qposadr.item() + self.door_qvel_adr = self.model.joint("doorjoint").dofadr.item() @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_door_pull.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -191,7 +183,9 @@ def compute_reward( reward = sum( ( - 2.0 * reward_utils.hamacher_product(reward_steps[0], reward_grab), + 2.0 * + reward_utils.hamacher_product( + reward_steps[0], reward_grab), 8.0 * reward_steps[1], ) ) diff --git a/metaworld/envs/sawyer_drawer_close_v3.py b/metaworld/envs/sawyer_drawer_close_v3.py index 5e7603a08..e04db8fbd 100644 --- a/metaworld/envs/sawyer_drawer_close_v3.py +++ b/metaworld/envs/sawyer_drawer_close_v3.py @@ -7,39 +7,25 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerDrawerCloseEnvV3(SawyerXYZEnv): + ENV_NAME: str = "drawer-close-v3" + _TARGET_RADIUS: float = 0.04 def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1, 0.5) obj_low = (-0.1, 0.9, 0.0) obj_high = (0.1, 0.9, 0.0) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_angle": 0.3, "obj_init_pos": np.array([0.0, 0.9, 0.0], dtype=np.float32), @@ -49,22 +35,28 @@ def __init__( self.obj_init_angle = self.init_config["obj_init_angle"] self.hand_init_pos = self.init_config["hand_init_pos"] - goal_low = self.hand_low - goal_high = self.hand_high + goal_low = hand_low + goal_high = hand_high self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) self.maxDist = 0.15 self.target_reward = 1000 * self.maxDist + 1000 * 2 + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) + @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_drawer.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: diff --git a/metaworld/envs/sawyer_drawer_open_v3.py b/metaworld/envs/sawyer_drawer_open_v3.py index d1b8b001d..63b6ddb2f 100644 --- a/metaworld/envs/sawyer_drawer_open_v3.py +++ b/metaworld/envs/sawyer_drawer_open_v3.py @@ -7,37 +7,23 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerDrawerOpenEnvV3(SawyerXYZEnv): + ENV_NAME: str = "drawer-open-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1, 0.5) obj_low = (-0.1, 0.9, 0.0) obj_high = (0.1, 0.9, 0.0) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_angle": 0.3, "obj_init_pos": np.array([0.0, 0.9, 0.0], dtype=np.float32), @@ -47,22 +33,28 @@ def __init__( self.obj_init_angle = self.init_config["obj_init_angle"] self.hand_init_pos = self.init_config["hand_init_pos"] - goal_low = self.hand_low - goal_high = self.hand_high + goal_low = hand_low + goal_high = hand_high self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) self.maxDist = 0.2 self.target_reward = 1000 * self.maxDist + 1000 * 2 + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) + @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_drawer.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -128,7 +120,8 @@ def compute_reward( handle_error, bounds=(0, 0.02), margin=self.maxDist, sigmoid="long_tail" ) - handle_pos_init = self._target_pos + np.array([0.0, self.maxDist, 0.0]) + handle_pos_init = self._target_pos + \ + np.array([0.0, self.maxDist, 0.0]) # Emphasize XY error so that gripper is able to drop down and cage # handle without running into it. By doing this, we are assuming # that the reward in the Z direction is small enough that the agent diff --git a/metaworld/envs/sawyer_faucet_close_v3.py b/metaworld/envs/sawyer_faucet_close_v3.py index b43709d47..70996dfc9 100644 --- a/metaworld/envs/sawyer_faucet_close_v3.py +++ b/metaworld/envs/sawyer_faucet_close_v3.py @@ -8,20 +8,17 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerFaucetCloseEnvV3(SawyerXYZEnv): + ENV_NAME: str = "faucet-close-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, -0.15) hand_high = (0.5, 1, 0.5) @@ -30,17 +27,6 @@ def __init__( self._handle_length = 0.175 self._target_radius: float = 0.07 - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([0, 0.8, 0.0]), "hand_init_pos": np.array([0.0, 0.4, 0.2]), @@ -48,19 +34,25 @@ def __init__( self.hand_init_pos = self.init_config["hand_init_pos"] self.obj_init_pos = self.init_config["obj_init_pos"] - goal_low = self.hand_low - goal_high = self.hand_high + goal_low = hand_low + goal_high = hand_high self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_faucet.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: diff --git a/metaworld/envs/sawyer_faucet_open_v3.py b/metaworld/envs/sawyer_faucet_open_v3.py index d0fa32fb4..07d61b44c 100644 --- a/metaworld/envs/sawyer_faucet_open_v3.py +++ b/metaworld/envs/sawyer_faucet_open_v3.py @@ -7,20 +7,17 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerFaucetOpenEnvV3(SawyerXYZEnv): + ENV_NAME: str = "faucet-open-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, -0.15) hand_high = (0.5, 1, 0.5) @@ -29,17 +26,6 @@ def __init__( self._handle_length = 0.175 self._target_radius: float = 0.07 - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([0, 0.8, 0.0]), "hand_init_pos": np.array([0.0, 0.4, 0.2]), @@ -47,19 +33,25 @@ def __init__( self.obj_init_pos = self.init_config["obj_init_pos"] self.hand_init_pos = self.init_config["hand_init_pos"] - goal_low = self.hand_low - goal_high = self.hand_high + goal_low = hand_low + goal_high = hand_high self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_faucet.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: diff --git a/metaworld/envs/sawyer_hammer_v3.py b/metaworld/envs/sawyer_hammer_v3.py index be0827181..a5cfda915 100644 --- a/metaworld/envs/sawyer_hammer_v3.py +++ b/metaworld/envs/sawyer_hammer_v3.py @@ -7,22 +7,19 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import HammerInitConfigDict from metaworld.utils import reward_utils class SawyerHammerEnvV3(SawyerXYZEnv): + ENV_NAME: str = "hammer-v3" + HAMMER_HANDLE_LENGTH = 0.14 def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1, 0.5) @@ -31,17 +28,6 @@ def __init__( goal_low = (0.2399, 0.7399, 0.109) goal_high = (0.2401, 0.7401, 0.111) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: HammerInitConfigDict = { "hammer_init_pos": np.array([0, 0.5, 0.0]), "hand_init_pos": np.array([0, 0.4, 0.2]), @@ -55,13 +41,19 @@ def __init__( self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_hammer.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -90,7 +82,8 @@ def _get_id_main_object(self) -> int: def _get_pos_objects(self) -> npt.NDArray[Any]: return np.hstack( - (self.get_body_com("hammer").copy(), self.get_body_com("nail_link").copy()) + (self.get_body_com("hammer").copy(), + self.get_body_com("nail_link").copy()) ) def _get_quat_objects(self) -> npt.NDArray[Any]: diff --git a/metaworld/envs/sawyer_hand_insert_v3.py b/metaworld/envs/sawyer_hand_insert_v3.py index b9236d6dc..228a0399e 100644 --- a/metaworld/envs/sawyer_hand_insert_v3.py +++ b/metaworld/envs/sawyer_hand_insert_v3.py @@ -7,22 +7,19 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerHandInsertEnvV3(SawyerXYZEnv): + ENV_NAME: str = "hand-insert-v3" + TARGET_RADIUS: float = 0.05 def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, -0.15) hand_high = (0.5, 1, 0.5) @@ -31,17 +28,6 @@ def __init__( goal_low = (-0.04, 0.8, -0.0201) goal_high = (0.04, 0.88, -0.0199) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([0, 0.6, 0.05]), "obj_init_angle": 0.3, @@ -57,13 +43,19 @@ def __init__( np.hstack((obj_high, goal_high)), dtype=np.float64, ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_table_with_hole.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -115,14 +107,16 @@ def reset_model(self) -> npt.NDArray[np.float64]: while np.linalg.norm(goal_pos[:2] - goal_pos[-3:-1]) < 0.15: goal_pos = self._get_state_rand_vec() assert self.obj_init_pos is not None - self.obj_init_pos = np.concatenate([goal_pos[:2], [self.obj_init_pos[-1]]]) + self.obj_init_pos = np.concatenate( + [goal_pos[:2], [self.obj_init_pos[-1]]]) self._target_pos = goal_pos[-3:] self._set_obj_xyz(self.obj_init_pos) self.model.site("goal").pos = self._target_pos assert self._target_pos is not None and self.hand_init_pos is not None - self.maxReachDist = np.abs(self.hand_init_pos[-1] - self._target_pos[-1]) + self.maxReachDist = np.abs( + self.hand_init_pos[-1] - self._target_pos[-1]) return self._get_obs() @@ -192,7 +186,8 @@ def compute_reward( if reachDist < 0.05: reachNearRew = 1000 * (self.maxReachDist - reachDist_z) + c1 * ( - np.exp(-(reachDist_z**2) / c2) + np.exp(-(reachDist_z**2) / c3) + np.exp(-(reachDist_z**2) / c2) + + np.exp(-(reachDist_z**2) / c3) ) else: reachNearRew = 0.0 diff --git a/metaworld/envs/sawyer_handle_press_side_v3.py b/metaworld/envs/sawyer_handle_press_side_v3.py index 9a5498c9c..3698cafbe 100644 --- a/metaworld/envs/sawyer_handle_press_side_v3.py +++ b/metaworld/envs/sawyer_handle_press_side_v3.py @@ -7,7 +7,7 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils @@ -25,34 +25,19 @@ class SawyerHandlePressSideEnvV3(SawyerXYZEnv): - (8/05/20) Updated to new XML - (6/30/20) Increased goal's Z coordinate by 0.01 in XML """ + ENV_NAME: str = "handle-press-side-v3" TARGET_RADIUS: float = 0.02 def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1.0, 0.5) obj_low = (-0.35, 0.65, -0.001) obj_high = (-0.25, 0.75, 0.001) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([-0.3, 0.7, 0.0]), "hand_init_pos": np.array( @@ -63,19 +48,25 @@ def __init__( self.obj_init_pos = self.init_config["obj_init_pos"] self.hand_init_pos = self.init_config["hand_init_pos"] - goal_low = self.hand_low - goal_high = self.hand_high + goal_low = hand_low + goal_high = hand_high self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_handle_press_sideways.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -158,7 +149,8 @@ def compute_reward( handle_radius = 0.02 tcp_to_obj = float(np.linalg.norm(obj - tcp)) - tcp_to_obj_init = np.linalg.norm(self._handle_init_pos - self.init_tcp) + tcp_to_obj_init = np.linalg.norm( + self._handle_init_pos - self.init_tcp) reach = reward_utils.tolerance( tcp_to_obj, bounds=(0, handle_radius), diff --git a/metaworld/envs/sawyer_handle_press_v3.py b/metaworld/envs/sawyer_handle_press_v3.py index 201959628..7b552311c 100644 --- a/metaworld/envs/sawyer_handle_press_v3.py +++ b/metaworld/envs/sawyer_handle_press_v3.py @@ -7,22 +7,19 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerHandlePressEnvV3(SawyerXYZEnv): + ENV_NAME: str = "handle-press-v3" + TARGET_RADIUS: float = 0.02 def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1.0, 0.5) @@ -31,17 +28,6 @@ def __init__( goal_low = (-0.1, 0.55, 0.04) goal_high = (0.1, 0.70, 0.08) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([0, 0.9, 0.0]), "hand_init_pos": np.array( @@ -55,13 +41,19 @@ def __init__( self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_handle_press.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -144,7 +136,8 @@ def compute_reward( handle_radius = 0.02 tcp_to_obj = float(np.linalg.norm(obj - tcp)) - tcp_to_obj_init = np.linalg.norm(self._handle_init_pos - self.init_tcp) + tcp_to_obj_init = np.linalg.norm( + self._handle_init_pos - self.init_tcp) reach = reward_utils.tolerance( tcp_to_obj, bounds=(0, handle_radius), diff --git a/metaworld/envs/sawyer_handle_pull_side_v3.py b/metaworld/envs/sawyer_handle_pull_side_v3.py index 89126270b..32654d52c 100644 --- a/metaworld/envs/sawyer_handle_pull_side_v3.py +++ b/metaworld/envs/sawyer_handle_pull_side_v3.py @@ -7,37 +7,23 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerHandlePullSideEnvV3(SawyerXYZEnv): + ENV_NAME: str = "handle-pull-side-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1.0, 0.5) obj_low = (-0.35, 0.65, 0.0) obj_high = (-0.25, 0.75, 0.0) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([-0.3, 0.7, 0.0]), "hand_init_pos": np.array( @@ -48,19 +34,25 @@ def __init__( self.obj_init_pos = self.init_config["obj_init_pos"] self.hand_init_pos = self.init_config["hand_init_pos"] - goal_low = self.hand_low - goal_high = self.hand_high + goal_low = hand_low + goal_high = hand_high self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_handle_press_sideways.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: diff --git a/metaworld/envs/sawyer_handle_pull_v3.py b/metaworld/envs/sawyer_handle_pull_v3.py index d4a9c2868..fe770bb30 100644 --- a/metaworld/envs/sawyer_handle_pull_v3.py +++ b/metaworld/envs/sawyer_handle_pull_v3.py @@ -7,20 +7,17 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerHandlePullEnvV3(SawyerXYZEnv): + ENV_NAME: str = "handle-pull-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1.0, 0.5) @@ -29,17 +26,6 @@ def __init__( goal_low = (-0.1, 0.55, 0.04) goal_high = (0.1, 0.70, 0.18) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([0, 0.9, 0.0]), "hand_init_pos": np.array( @@ -53,13 +39,19 @@ def __init__( self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_handle_press.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: diff --git a/metaworld/envs/sawyer_lever_pull_v3.py b/metaworld/envs/sawyer_lever_pull_v3.py index 31a9a5630..b91f5a765 100644 --- a/metaworld/envs/sawyer_lever_pull_v3.py +++ b/metaworld/envs/sawyer_lever_pull_v3.py @@ -9,7 +9,7 @@ from scipy.spatial.transform import Rotation from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils @@ -27,34 +27,19 @@ class SawyerLeverPullEnvV3(SawyerXYZEnv): - (6/23/20) In `reset_model`, changed `final_pos[2] -= .17` to `+= .17` This ensures that the target point is above the table. """ + ENV_NAME: str = "lever-pull-v3" LEVER_RADIUS = 0.2 def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, -0.15) hand_high = (0.5, 1, 0.5) obj_low = (-0.1, 0.7, 0.0) obj_high = (0.1, 0.8, 0.0) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([0, 0.7, 0.0]), "hand_init_pos": np.array([0, 0.4, 0.2], dtype=np.float32), @@ -64,19 +49,25 @@ def __init__( self.hand_init_pos = self.init_config["hand_init_pos"] self._lever_pos_init = None - goal_low = self.hand_low - goal_high = self.hand_high + goal_low = hand_low + goal_high = hand_high self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_lever_pull.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -179,7 +170,8 @@ def compute_reward( target = self._target_pos obj_to_target = float(np.linalg.norm(lever - target)) - in_place_margin = float(np.linalg.norm(self._lever_pos_init - target)) + in_place_margin = float(np.linalg.norm( + self._lever_pos_init - target)) in_place = reward_utils.tolerance( obj_to_target, @@ -189,7 +181,8 @@ def compute_reward( ) # reward = 2.0 * ready_to_lift + 8.0 * lever_engagement - reward = 10.0 * reward_utils.hamacher_product(ready_to_lift, in_place) + reward = 10.0 * \ + reward_utils.hamacher_product(ready_to_lift, in_place) return ( reward, float(np.linalg.norm(shoulder_to_lever)), diff --git a/metaworld/envs/sawyer_peg_insertion_side_v3.py b/metaworld/envs/sawyer_peg_insertion_side_v3.py index 36a751dc9..edb6553a0 100644 --- a/metaworld/envs/sawyer_peg_insertion_side_v3.py +++ b/metaworld/envs/sawyer_peg_insertion_side_v3.py @@ -8,13 +8,12 @@ from scipy.spatial.transform import Rotation from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerPegInsertionSideEnvV3(SawyerXYZEnv): - TARGET_RADIUS: float = 0.07 """ Motivation for V3: V1 was difficult to solve because the observation didn't say where @@ -31,15 +30,13 @@ class SawyerPegInsertionSideEnvV3(SawyerXYZEnv): - (6/16/20) Used existing goal_low and goal_high values to constrain the hole's position, as opposed to hand_low and hand_high """ + ENV_NAME: str = "peg-insert-side-v3" + + TARGET_RADIUS: float = 0.07 def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_init_pos = (0, 0.6, 0.2) @@ -50,17 +47,6 @@ def __init__( goal_low = (-0.35, 0.4, -0.001) goal_high = (-0.25, 0.7, 0.001) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([0, 0.6, 0.02]), "hand_init_pos": np.array([0, 0.6, 0.2]), @@ -86,11 +72,16 @@ def __init__( self.liftThresh = 0.11 + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) + @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_peg_insertion_side.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -186,11 +177,15 @@ def compute_reward( sigmoid="long_tail", ) ip_orig = in_place - brc_col_box_1 = self._get_site_pos("bottom_right_corner_collision_box_1") - tlc_col_box_1 = self._get_site_pos("top_left_corner_collision_box_1") - - brc_col_box_2 = self._get_site_pos("bottom_right_corner_collision_box_2") - tlc_col_box_2 = self._get_site_pos("top_left_corner_collision_box_2") + brc_col_box_1 = self._get_site_pos( + "bottom_right_corner_collision_box_1") + tlc_col_box_1 = self._get_site_pos( + "top_left_corner_collision_box_1") + + brc_col_box_2 = self._get_site_pos( + "bottom_right_corner_collision_box_2") + tlc_col_box_2 = self._get_site_pos( + "top_left_corner_collision_box_2") collision_box_bottom_1 = reward_utils.rect_prism_tolerance( curr=obj_head, one=tlc_col_box_1, zero=brc_col_box_1 ) @@ -306,7 +301,8 @@ def compute_reward( and (reachDist > 0.02) ) - cond = self.pickCompleted and (reachDist < 0.1) and not (objDropped) + cond = self.pickCompleted and ( + reachDist < 0.1) and not (objDropped) if cond: if placingDistHead <= 0.05: diff --git a/metaworld/envs/sawyer_peg_unplug_side_v3.py b/metaworld/envs/sawyer_peg_unplug_side_v3.py index dbf80c2f4..ffc27234e 100644 --- a/metaworld/envs/sawyer_peg_unplug_side_v3.py +++ b/metaworld/envs/sawyer_peg_unplug_side_v3.py @@ -7,20 +7,17 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerPegUnplugSideEnvV3(SawyerXYZEnv): + ENV_NAME: str = "peg-unplug-side-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1, 0.5) @@ -29,17 +26,6 @@ def __init__( goal_low = obj_low + np.array([0.194, 0.0, 0.131]) goal_high = obj_high + np.array([0.194, 0.0, 0.131]) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([-0.225, 0.6, 0.05]), "hand_init_pos": np.array((0, 0.6, 0.2)), @@ -51,13 +37,19 @@ def __init__( self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_peg_unplug_side.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -114,7 +106,8 @@ def reset_model(self) -> npt.NDArray[np.float64]: self.model.site("goal").pos = self._target_pos assert self._target_pos is not None and self.obj_init_pos is not None - self.maxPlacingDist = np.linalg.norm(self._target_pos - self.obj_init_pos) + self.maxPlacingDist = np.linalg.norm( + self._target_pos - self.obj_init_pos) return self._get_obs() @@ -152,7 +145,8 @@ def compute_reward( margin=in_place_margin, sigmoid="long_tail", ) - grasp_success = tcp_opened > 0.5 and (obj[0] - self.obj_init_pos[0] > 0.015) + grasp_success = tcp_opened > 0.5 and ( + obj[0] - self.obj_init_pos[0] > 0.015) reward = 2 * object_grasped @@ -207,7 +201,8 @@ def compute_reward( c3 = 0.001 if self.reachCompleted: placeRew = 1000 * (self.maxPlacingDist - placingDist) + c1 * ( - np.exp(-(placingDist**2) / c2) + np.exp(-(placingDist**2) / c3) + np.exp(-(placingDist**2) / c2) + + np.exp(-(placingDist**2) / c3) ) placeRew = max(placeRew, 0) placeRew, placingDist = [placeRew, placingDist] diff --git a/metaworld/envs/sawyer_pick_out_of_hole_v3.py b/metaworld/envs/sawyer_pick_out_of_hole_v3.py index 342dd5846..66617236a 100644 --- a/metaworld/envs/sawyer_pick_out_of_hole_v3.py +++ b/metaworld/envs/sawyer_pick_out_of_hole_v3.py @@ -7,22 +7,19 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerPickOutOfHoleEnvV3(SawyerXYZEnv): + ENV_NAME: str = "pick-out-of-hole-v3" + _TARGET_RADIUS: float = 0.02 def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, -0.05) hand_high = (0.5, 1, 0.5) @@ -31,17 +28,6 @@ def __init__( goal_low = (-0.1, 0.5, 0.15) goal_high = (0.1, 0.6, 0.3) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([0, 0.6, 0.0]), "obj_init_angle": 0.3, @@ -57,13 +43,19 @@ def __init__( np.hstack((obj_high, goal_high)), dtype=np.float64, ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_pick_out_of_hole.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -258,7 +250,8 @@ def compute_reward( cond = self.pickCompleted and (reachDist < 0.1) and not objDropped if cond: placeRew = 1000 * (self.maxPlacingDist - placingDist) + c1 * ( - np.exp(-(placingDist**2) / c2) + np.exp(-(placingDist**2) / c3) + np.exp(-(placingDist**2) / c2) + + np.exp(-(placingDist**2) / c3) ) placeRew = max(placeRew, 0) else: diff --git a/metaworld/envs/sawyer_pick_place_v3.py b/metaworld/envs/sawyer_pick_place_v3.py index 8cc9ba825..6bf1aff56 100644 --- a/metaworld/envs/sawyer_pick_place_v3.py +++ b/metaworld/envs/sawyer_pick_place_v3.py @@ -8,7 +8,7 @@ from scipy.spatial.transform import Rotation from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils @@ -27,15 +27,11 @@ class SawyerPickPlaceEnvV3(SawyerXYZEnv): i.e. (self._target_pos - pos_hand) - (6/15/20) Separated reach-push-pick-place into 3 separate envs. """ + ENV_NAME: str = "pick-place-v3" def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: goal_low = (-0.1, 0.8, 0.05) goal_high = (0.1, 0.9, 0.3) @@ -44,17 +40,6 @@ def __init__( obj_low = (-0.1, 0.6, 0.02) obj_high = (0.1, 0.7, 0.02) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_angle": 0.3, "obj_init_pos": np.array([0, 0.6, 0.02]), @@ -72,16 +57,19 @@ def __init__( np.hstack((obj_high, goal_high)), dtype=np.float64, ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) - self.num_resets = 0 - self.obj_init_pos = None + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_pick_place_v3.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -141,7 +129,8 @@ def fix_extreme_obj_pos(self, orig_init_pos: npt.NDArray[Any]) -> npt.NDArray[An def reset_model(self) -> npt.NDArray[np.float64]: self._reset_hand() self._target_pos = self.goal.copy() - self.obj_init_pos = self.fix_extreme_obj_pos(self.init_config["obj_init_pos"]) + self.obj_init_pos = self.fix_extreme_obj_pos( + self.init_config["obj_init_pos"]) self.obj_init_angle = self.init_config["obj_init_angle"] goal_pos = self._get_state_rand_vec() @@ -222,14 +211,17 @@ def _gripper_caging_reward( # compute the tcp_obj distance in the x_z plane tcp_xz = tcp + np.array([0.0, -tcp[1], 0.0]) obj_position_x_z = np.copy(obj_pos) + np.array([0.0, -obj_pos[1], 0.0]) - tcp_obj_norm_x_z = float(np.linalg.norm(tcp_xz - obj_position_x_z, ord=2)) + tcp_obj_norm_x_z = float(np.linalg.norm( + tcp_xz - obj_position_x_z, ord=2)) # used for computing the tcp to object object margin in the x_z plane assert self.obj_init_pos is not None - init_obj_x_z = self.obj_init_pos + np.array([0.0, -self.obj_init_pos[1], 0.0]) + init_obj_x_z = self.obj_init_pos + \ + np.array([0.0, -self.obj_init_pos[1], 0.0]) init_tcp_x_z = self.init_tcp + np.array([0.0, -self.init_tcp[1], 0.0]) tcp_obj_x_z_margin = ( - np.linalg.norm(init_obj_x_z - init_tcp_x_z, ord=2) - x_z_success_margin + np.linalg.norm(init_obj_x_z - init_tcp_x_z, + ord=2) - x_z_success_margin ) x_z_caging = reward_utils.tolerance( @@ -350,10 +342,12 @@ def compute_reward( and (reachDist > 0.02) ) - cond = self.pickCompleted and (reachDist < 0.1) and not (objDropped) + cond = self.pickCompleted and ( + reachDist < 0.1) and not (objDropped) if cond: placeRew = 1000 * (self.maxPlacingDist - placingDist) + c1 * ( - np.exp(-(placingDist**2) / c2) + np.exp(-(placingDist**2) / c3) + np.exp(-(placingDist**2) / c2) + + np.exp(-(placingDist**2) / c3) ) placeRew = max(placeRew, 0) else: diff --git a/metaworld/envs/sawyer_pick_place_wall_v3.py b/metaworld/envs/sawyer_pick_place_wall_v3.py index 037e8079e..db8d06f56 100644 --- a/metaworld/envs/sawyer_pick_place_wall_v3.py +++ b/metaworld/envs/sawyer_pick_place_wall_v3.py @@ -8,7 +8,7 @@ from scipy.spatial.transform import Rotation from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils @@ -28,15 +28,11 @@ class SawyerPickPlaceWallEnvV3(SawyerXYZEnv): - (6/24/20) Separated pick-place-wall into from reach-push-pick-place-wall. """ + ENV_NAME: str = "pick-place-wall-v3" def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: goal_low = (-0.05, 0.85, 0.05) goal_high = (0.05, 0.9, 0.3) @@ -45,17 +41,6 @@ def __init__( obj_low = (-0.05, 0.6, 0.015) obj_high = (0.05, 0.65, 0.015) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_angle": 0.3, "obj_init_pos": np.array([0, 0.6, 0.02]), @@ -73,15 +58,19 @@ def __init__( np.hstack((obj_high, goal_high)), dtype=np.float64, ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) - self.num_resets = 0 + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_pick_place_wall_v3.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -126,7 +115,8 @@ def _get_quat_objects(self) -> npt.NDArray[Any]: def adjust_initObjPos(self, orig_init_pos): # This is to account for meshes for the geom and object are not aligned # If this is not done, the object could be initialized in an extreme position - diff = self.get_body_com("obj")[:2] - self.data.geom("objGeom").xpos[:2] + diff = self.get_body_com("obj")[:2] - \ + self.data.geom("objGeom").xpos[:2] adjustedPos = orig_init_pos[:2] + diff # The convention we follow is that body_com[2] is always 0, and geom_pos[2] is the object height @@ -135,7 +125,8 @@ def adjust_initObjPos(self, orig_init_pos): def reset_model(self) -> npt.NDArray[np.float64]: self._reset_hand() self._target_pos = self.goal.copy() - self.obj_init_pos = self.adjust_initObjPos(self.init_config["obj_init_pos"]) + self.obj_init_pos = self.adjust_initObjPos( + self.init_config["obj_init_pos"]) self.obj_init_angle = self.init_config["obj_init_angle"] goal_pos = self._get_state_rand_vec() @@ -153,7 +144,8 @@ def reset_model(self) -> npt.NDArray[np.float64]: self.objHeight = self.data.geom("objGeom").xpos[2] self.heightTarget = self.objHeight + self.liftThresh - self.maxReachDist = np.linalg.norm(self.init_tcp - np.array(self._target_pos)) + self.maxReachDist = np.linalg.norm( + self.init_tcp - np.array(self._target_pos)) self.maxPushDist = np.linalg.norm( self.obj_init_pos[:2] - np.array(self._target_pos)[:2] ) @@ -191,13 +183,16 @@ def compute_reward( tcp_to_obj = float(np.linalg.norm(obj - tcp)) in_place_scaling = np.array([1.0, 1.0, 3.0]) - obj_to_midpoint = float(np.linalg.norm((obj - midpoint) * in_place_scaling)) + obj_to_midpoint = float(np.linalg.norm( + (obj - midpoint) * in_place_scaling)) obj_to_midpoint_init = float( - np.linalg.norm((self.obj_init_pos - midpoint) * in_place_scaling) + np.linalg.norm((self.obj_init_pos - midpoint) + * in_place_scaling) ) obj_to_target = float(np.linalg.norm(obj - target)) - obj_to_target_init = float(np.linalg.norm(self.obj_init_pos - target)) + obj_to_target_init = float( + np.linalg.norm(self.obj_init_pos - target)) in_place_part1 = reward_utils.tolerance( obj_to_midpoint, @@ -307,7 +302,8 @@ def pickCompletionCriteria(): cond = self.pickCompleted and (reachDist < 0.1) and not objDropped if cond: placeRew = 1000 * (self.maxPlacingDist - placingDist) + c1 * ( - np.exp(-(placingDist**2) / c2) + np.exp(-(placingDist**2) / c3) + np.exp(-(placingDist**2) / c2) + + np.exp(-(placingDist**2) / c3) ) placeRew = max(placeRew, 0) placeRew, placingDist = [placeRew, placingDist] diff --git a/metaworld/envs/sawyer_plate_slide_back_side_v3.py b/metaworld/envs/sawyer_plate_slide_back_side_v3.py index 7bd731db8..59ab8874a 100644 --- a/metaworld/envs/sawyer_plate_slide_back_side_v3.py +++ b/metaworld/envs/sawyer_plate_slide_back_side_v3.py @@ -8,7 +8,7 @@ from scipy.spatial.transform import Rotation from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils @@ -28,15 +28,11 @@ class SawyerPlateSlideBackSideEnvV3(SawyerXYZEnv): (for consistency with other environments) - (6/22/20) Cabinet now sits on ground, instead of .02 units above it """ + ENV_NAME: str = "plate-slide-back-side-v3" def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: goal_low = (-0.05, 0.6, 0.015) goal_high = (0.15, 0.6, 0.015) @@ -45,17 +41,6 @@ def __init__( obj_low = (-0.25, 0.6, 0.0) obj_high = (-0.25, 0.6, 0.0) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_angle": 0.3, "obj_init_pos": np.array([-0.25, 0.6, 0.02], dtype=np.float32), @@ -71,13 +56,19 @@ def __init__( np.hstack((obj_high, goal_high)), dtype=np.float64, ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_plate_slide_sideway.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: diff --git a/metaworld/envs/sawyer_plate_slide_back_v3.py b/metaworld/envs/sawyer_plate_slide_back_v3.py index 77204fc41..bf4f480d6 100644 --- a/metaworld/envs/sawyer_plate_slide_back_v3.py +++ b/metaworld/envs/sawyer_plate_slide_back_v3.py @@ -8,20 +8,17 @@ from scipy.spatial.transform import Rotation from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerPlateSlideBackEnvV3(SawyerXYZEnv): + ENV_NAME: str = "plate-slide-back-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: goal_low = (-0.1, 0.6, 0.015) goal_high = (0.1, 0.6, 0.015) @@ -30,17 +27,6 @@ def __init__( obj_low = (0.0, 0.85, 0.0) obj_high = (0.0, 0.85, 0.0) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_angle": 0.3, "obj_init_pos": np.array([0.0, 0.85, 0.0], dtype=np.float32), @@ -56,13 +42,19 @@ def __init__( np.hstack((obj_high, goal_high)), dtype=np.float64, ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_plate_slide.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: diff --git a/metaworld/envs/sawyer_plate_slide_side_v3.py b/metaworld/envs/sawyer_plate_slide_side_v3.py index 1894ac284..abb51248c 100644 --- a/metaworld/envs/sawyer_plate_slide_side_v3.py +++ b/metaworld/envs/sawyer_plate_slide_side_v3.py @@ -8,20 +8,17 @@ from scipy.spatial.transform import Rotation from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerPlateSlideSideEnvV3(SawyerXYZEnv): + ENV_NAME: str = "plate-slide-side-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: goal_low = (-0.3, 0.54, 0.0) goal_high = (-0.25, 0.66, 0.0) @@ -30,17 +27,6 @@ def __init__( obj_low = (0.0, 0.6, 0.0) obj_high = (0.0, 0.6, 0.0) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_angle": 0.3, "obj_init_pos": np.array([0.0, 0.6, 0.0], dtype=np.float32), @@ -56,13 +42,19 @@ def __init__( np.hstack((obj_high, goal_high)), dtype=np.float64, ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_plate_slide_sideway.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -116,7 +108,8 @@ def reset_model(self) -> npt.NDArray[np.float64]: self.model.site("goal").pos = self._target_pos - self.maxDist = np.linalg.norm(self.obj_init_pos[:-1] - self._target_pos[:-1]) + self.maxDist = np.linalg.norm( + self.obj_init_pos[:-1] - self._target_pos[:-1]) return self._get_obs() diff --git a/metaworld/envs/sawyer_plate_slide_v3.py b/metaworld/envs/sawyer_plate_slide_v3.py index 6d76f2e50..3cb43d043 100644 --- a/metaworld/envs/sawyer_plate_slide_v3.py +++ b/metaworld/envs/sawyer_plate_slide_v3.py @@ -8,22 +8,19 @@ from scipy.spatial.transform import Rotation from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerPlateSlideEnvV3(SawyerXYZEnv): + ENV_NAME: str = "plate-slide-v3" + OBJ_RADIUS: float = 0.04 def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: goal_low = (-0.1, 0.85, 0.0) goal_high = (0.1, 0.9, 0.0) @@ -32,17 +29,6 @@ def __init__( obj_low = (0.0, 0.6, 0.0) obj_high = (0.0, 0.6, 0.0) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_angle": 0.3, "obj_init_pos": np.array([0.0, 0.6, 0.0], dtype=np.float32), @@ -58,13 +44,19 @@ def __init__( np.hstack((obj_high, goal_high)), dtype=np.float64, ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_plate_slide.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -120,7 +112,8 @@ def reset_model(self) -> npt.NDArray[np.float64]: self.model.site("goal").pos = self._target_pos - self.maxDist = np.linalg.norm(self.obj_init_pos[:-1] - self._target_pos[:-1]) + self.maxDist = np.linalg.norm( + self.obj_init_pos[:-1] - self._target_pos[:-1]) return self._get_obs() diff --git a/metaworld/envs/sawyer_push_back_v3.py b/metaworld/envs/sawyer_push_back_v3.py index 888619923..3bb73bb54 100644 --- a/metaworld/envs/sawyer_push_back_v3.py +++ b/metaworld/envs/sawyer_push_back_v3.py @@ -8,23 +8,20 @@ from scipy.spatial.transform import Rotation from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerPushBackEnvV3(SawyerXYZEnv): + ENV_NAME: str = "push-back-v3" + OBJ_RADIUS: float = 0.007 TARGET_RADIUS: float = 0.05 def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: goal_low = (-0.1, 0.6, 0.0199) goal_high = (0.1, 0.7, 0.0201) @@ -33,17 +30,6 @@ def __init__( obj_low = (-0.1, 0.8, 0.02) obj_high = (0.1, 0.85, 0.02) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([0, 0.8, 0.02]), "obj_init_angle": 0.3, @@ -59,13 +45,19 @@ def __init__( np.hstack((obj_high, goal_high)), dtype=np.float64, ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_push_back_v3.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -109,29 +101,34 @@ def _get_quat_objects(self) -> npt.NDArray[Any]: def adjust_initObjPos(self, orig_init_pos: npt.NDArray[Any]) -> npt.NDArray[Any]: # This is to account for meshes for the geom and object are not aligned # If this is not done, the object could be initialized in an extreme position - diff = self.get_body_com("obj")[:2] - self.data.geom("objGeom").xpos[:2] + diff = self.get_body_com("obj")[:2] - \ + self.data.geom("objGeom").xpos[:2] adjustedPos = orig_init_pos[:2] + diff # The convention we follow is that body_com[2] is always 0, and geom_pos[2] is the object height return np.array( - [adjustedPos[0], adjustedPos[1], self.data.geom("objGeom").xpos[-1]] + [adjustedPos[0], adjustedPos[1], + self.data.geom("objGeom").xpos[-1]] ) def reset_model(self) -> npt.NDArray[np.float64]: self._reset_hand() self._target_pos = self.goal.copy() - self.obj_init_pos = self.adjust_initObjPos(self.init_config["obj_init_pos"]) + self.obj_init_pos = self.adjust_initObjPos( + self.init_config["obj_init_pos"]) self.obj_init_angle = self.init_config["obj_init_angle"] assert self.obj_init_pos is not None goal_pos = self._get_state_rand_vec() - self._target_pos = np.concatenate([goal_pos[-3:-1], [self.obj_init_pos[-1]]]) + self._target_pos = np.concatenate( + [goal_pos[-3:-1], [self.obj_init_pos[-1]]]) while np.linalg.norm(goal_pos[:2] - self._target_pos[:2]) < 0.15: goal_pos = self._get_state_rand_vec() self._target_pos = np.concatenate( [goal_pos[-3:-1], [self.obj_init_pos[-1]]] ) - self.obj_init_pos = np.concatenate([goal_pos[:2], [self.obj_init_pos[-1]]]) + self.obj_init_pos = np.concatenate( + [goal_pos[:2], [self.obj_init_pos[-1]]]) self._set_obj_xyz(self.obj_init_pos) self.model.site("goal").pos = self._target_pos @@ -141,7 +138,8 @@ def reset_model(self) -> npt.NDArray[np.float64]: self.objHeight = self.data.geom("objGeom").xpos[2] self.heightTarget = self.objHeight + self.liftThresh - self.maxReachDist = np.linalg.norm(self.init_tcp - np.array(self._target_pos)) + self.maxReachDist = np.linalg.norm( + self.init_tcp - np.array(self._target_pos)) self.maxPushDist = np.linalg.norm( self.obj_init_pos[:2] - np.array(self._target_pos)[:2] ) @@ -215,7 +213,8 @@ def _gripper_caging_reward( assert left_caging >= 0 and left_caging <= 1 y_caging = reward_utils.hamacher_product(right_caging, left_caging) - y_gripping = reward_utils.hamacher_product(right_gripping, left_gripping) + y_gripping = reward_utils.hamacher_product( + right_gripping, left_gripping) assert y_caging >= 0 and y_caging <= 1 @@ -223,11 +222,13 @@ def _gripper_caging_reward( obj_position_x_z = np.copy(obj_pos) + np.array([0.0, -obj_pos[1], 0.0]) tcp_obj_norm_x_z = np.linalg.norm(tcp_xz - obj_position_x_z, ord=2) assert self.obj_init_pos is not None - init_obj_x_z = self.obj_init_pos + np.array([0.0, -self.obj_init_pos[1], 0.0]) + init_obj_x_z = self.obj_init_pos + \ + np.array([0.0, -self.obj_init_pos[1], 0.0]) init_tcp_x_z = self.init_tcp + np.array([0.0, -self.init_tcp[1], 0.0]) tcp_obj_x_z_margin = ( - np.linalg.norm(init_obj_x_z - init_tcp_x_z, ord=2) - x_z_success_margin + np.linalg.norm(init_obj_x_z - init_tcp_x_z, + ord=2) - x_z_success_margin ) x_z_caging = reward_utils.tolerance( float(tcp_obj_norm_x_z), @@ -272,7 +273,8 @@ def compute_reward( margin=target_to_obj_init, sigmoid="long_tail", ) - object_grasped = self._gripper_caging_reward(action, obj, self.OBJ_RADIUS) + object_grasped = self._gripper_caging_reward( + action, obj, self.OBJ_RADIUS) reward = reward_utils.hamacher_product(object_grasped, in_place) diff --git a/metaworld/envs/sawyer_push_v3.py b/metaworld/envs/sawyer_push_v3.py index 2dd838dfe..ce5e9527c 100644 --- a/metaworld/envs/sawyer_push_v3.py +++ b/metaworld/envs/sawyer_push_v3.py @@ -8,7 +8,7 @@ from scipy.spatial.transform import Rotation from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils @@ -27,17 +27,13 @@ class SawyerPushEnvV3(SawyerXYZEnv): i.e. (self._target_pos - pos_hand) - (6/15/20) Separated reach-push-pick-place into 3 separate envs. """ + ENV_NAME: str = "push-v3" TARGET_RADIUS: float = 0.05 def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1, 0.5) @@ -46,17 +42,6 @@ def __init__( goal_low = (-0.1, 0.8, 0.01) goal_high = (0.1, 0.9, 0.02) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_angle": 0.3, "obj_init_pos": np.array([0.0, 0.6, 0.02]), @@ -74,14 +59,19 @@ def __init__( np.hstack((obj_high, goal_high)), dtype=np.float64, ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) - self.num_resets = 0 + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_push_v3.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -145,8 +135,10 @@ def reset_model(self) -> npt.NDArray[np.float64]: while np.linalg.norm(goal_pos[:2] - self._target_pos[:2]) < 0.15: goal_pos = self._get_state_rand_vec() self._target_pos = goal_pos[3:] - self._target_pos = np.concatenate([goal_pos[-3:-1], [self.obj_init_pos[-1]]]) - self.obj_init_pos = np.concatenate([goal_pos[:2], [self.obj_init_pos[-1]]]) + self._target_pos = np.concatenate( + [goal_pos[-3:-1], [self.obj_init_pos[-1]]]) + self.obj_init_pos = np.concatenate( + [goal_pos[:2], [self.obj_init_pos[-1]]]) self._set_obj_xyz(self.obj_init_pos) self.model.site("goal").pos = self._target_pos diff --git a/metaworld/envs/sawyer_push_wall_v3.py b/metaworld/envs/sawyer_push_wall_v3.py index 246681557..8e1db8429 100644 --- a/metaworld/envs/sawyer_push_wall_v3.py +++ b/metaworld/envs/sawyer_push_wall_v3.py @@ -10,7 +10,7 @@ from scipy.spatial.transform import Rotation from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils @@ -30,17 +30,13 @@ class SawyerPushWallEnvV3(SawyerXYZEnv): i.e. (self._target_pos - pos_hand) - (6/15/20) Separated reach-push-pick-place into 3 separate envs. """ + ENV_NAME: str = "push-wall-v3" OBJ_RADIUS: float = 0.02 def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1, 0.5) @@ -49,17 +45,6 @@ def __init__( goal_low = (-0.05, 0.85, 0.01) goal_high = (0.05, 0.9, 0.02) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_angle": 0.3, "obj_init_pos": np.array([0, 0.6, 0.02]), @@ -77,15 +62,19 @@ def __init__( np.hstack((obj_high, goal_high)), dtype=np.float64, ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) - self.num_resets = 0 + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_push_wall_v3.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -126,16 +115,19 @@ def _get_quat_objects(self) -> npt.NDArray[Any]: return Rotation.from_matrix(geom_xmat).as_quat() def adjust_initObjPos(self, orig_init_pos: npt.NDArray[Any]) -> npt.NDArray[Any]: - diff = self.get_body_com("obj")[:2] - self.data.geom("objGeom").xpos[:2] + diff = self.get_body_com("obj")[:2] - \ + self.data.geom("objGeom").xpos[:2] adjustedPos = orig_init_pos[:2] + diff return np.array( - [adjustedPos[0], adjustedPos[1], self.data.geom("objGeom").xpos[-1]] + [adjustedPos[0], adjustedPos[1], + self.data.geom("objGeom").xpos[-1]] ) def reset_model(self) -> npt.NDArray[np.float64]: self._reset_hand() self._target_pos = self.goal.copy() - self.obj_init_pos = self.adjust_initObjPos(self.init_config["obj_init_pos"]) + self.obj_init_pos = self.adjust_initObjPos( + self.init_config["obj_init_pos"]) self.obj_init_angle = self.init_config["obj_init_angle"] goal_pos = self._get_state_rand_vec() @@ -143,8 +135,10 @@ def reset_model(self) -> npt.NDArray[np.float64]: while np.linalg.norm(goal_pos[:2] - self._target_pos[:2]) < 0.15: goal_pos = self._get_state_rand_vec() self._target_pos = goal_pos[3:] - self._target_pos = np.concatenate([goal_pos[-3:-1], [self.obj_init_pos[-1]]]) - self.obj_init_pos = np.concatenate([goal_pos[:2], [self.obj_init_pos[-1]]]) + self._target_pos = np.concatenate( + [goal_pos[-3:-1], [self.obj_init_pos[-1]]]) + self.obj_init_pos = np.concatenate( + [goal_pos[:2], [self.obj_init_pos[-1]]]) self.model.site("goal").pos = self._target_pos @@ -152,7 +146,8 @@ def reset_model(self) -> npt.NDArray[np.float64]: self.objHeight = self.data.geom("objGeom").xpos[2] self.heightTarget = self.objHeight + self.liftThresh - self.maxReachDist = np.linalg.norm(self.init_tcp - np.array(self._target_pos)) + self.maxReachDist = np.linalg.norm( + self.init_tcp - np.array(self._target_pos)) self.maxPushDist = np.linalg.norm( self.obj_init_pos[:2] - np.array(self._target_pos)[:2] ) @@ -185,13 +180,16 @@ def compute_reward( tcp_to_obj = float(np.linalg.norm(obj - tcp)) in_place_scaling = np.array([3.0, 1.0, 1.0]) - obj_to_midpoint = float(np.linalg.norm((obj - midpoint) * in_place_scaling)) + obj_to_midpoint = float(np.linalg.norm( + (obj - midpoint) * in_place_scaling)) obj_to_midpoint_init = float( - np.linalg.norm((self.obj_init_pos - midpoint) * in_place_scaling) + np.linalg.norm((self.obj_init_pos - midpoint) + * in_place_scaling) ) obj_to_target = float(np.linalg.norm(obj - target)) - obj_to_target_init = float(np.linalg.norm(self.obj_init_pos - target)) + obj_to_target_init = float( + np.linalg.norm(self.obj_init_pos - target)) in_place_part1 = reward_utils.tolerance( obj_to_midpoint, diff --git a/metaworld/envs/sawyer_reach_v3.py b/metaworld/envs/sawyer_reach_v3.py index 2e145e35f..e522b0cad 100644 --- a/metaworld/envs/sawyer_reach_v3.py +++ b/metaworld/envs/sawyer_reach_v3.py @@ -8,7 +8,7 @@ from scipy.spatial.transform import Rotation from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils @@ -27,15 +27,11 @@ class SawyerReachEnvV3(SawyerXYZEnv): i.e. (self._target_pos - pos_hand) - (6/15/20) Separated reach-push-pick-place into 3 separate envs. """ + ENV_NAME: str = "reach-v3" def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: goal_low = (-0.1, 0.8, 0.05) goal_high = (0.1, 0.9, 0.3) @@ -44,17 +40,6 @@ def __init__( obj_low = (-0.1, 0.6, 0.02) obj_high = (0.1, 0.7, 0.02) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_angle": 0.3, "obj_init_pos": np.array([0.0, 0.6, 0.02]), @@ -72,13 +57,19 @@ def __init__( np.hstack((obj_high, goal_high)), dtype=np.float64, ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_reach_v3.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -119,7 +110,8 @@ def fix_extreme_obj_pos(self, orig_init_pos: npt.NDArray[Any]) -> npt.NDArray[An def reset_model(self) -> npt.NDArray[np.float64]: self._reset_hand() self._target_pos = self.goal.copy() - self.obj_init_pos = self.fix_extreme_obj_pos(self.init_config["obj_init_pos"]) + self.obj_init_pos = self.fix_extreme_obj_pos( + self.init_config["obj_init_pos"]) self.obj_init_angle = self.init_config["obj_init_angle"] goal_pos = self._get_state_rand_vec() @@ -133,7 +125,8 @@ def reset_model(self) -> npt.NDArray[np.float64]: self.model.site("goal").pos = self._target_pos - self.maxReachDist = np.linalg.norm(self.init_tcp - np.array(self._target_pos)) + self.maxReachDist = np.linalg.norm( + self.init_tcp - np.array(self._target_pos)) return self._get_obs() @@ -151,7 +144,8 @@ def compute_reward( tcp_to_target = float(np.linalg.norm(tcp - target)) # obj_to_target = float(np.linalg.norm(obj - target)) - in_place_margin = float(np.linalg.norm(self.hand_init_pos - target)) + in_place_margin = float( + np.linalg.norm(self.hand_init_pos - target)) in_place = reward_utils.tolerance( tcp_to_target, bounds=(0, _TARGET_RADIUS), diff --git a/metaworld/envs/sawyer_reach_wall_v3.py b/metaworld/envs/sawyer_reach_wall_v3.py index 01b26644d..b9e01303c 100644 --- a/metaworld/envs/sawyer_reach_wall_v3.py +++ b/metaworld/envs/sawyer_reach_wall_v3.py @@ -8,7 +8,7 @@ from scipy.spatial.transform import Rotation from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils @@ -27,15 +27,11 @@ class SawyerReachWallEnvV3(SawyerXYZEnv): points from the end effector to the goal coordinate. i.e. (self._target_pos - pos_hand) """ + ENV_NAME: str = "reach-wall-v3" def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: goal_low = (-0.05, 0.85, 0.05) goal_high = (0.05, 0.9, 0.3) @@ -44,17 +40,6 @@ def __init__( obj_low = (-0.05, 0.6, 0.015) obj_high = (0.05, 0.65, 0.015) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_angle": 0.3, "obj_init_pos": np.array([0, 0.6, 0.02]), @@ -72,15 +57,19 @@ def __init__( np.hstack((obj_high, goal_high)), dtype=np.float64, ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) - self.num_resets = 0 + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_reach_wall_v3.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -126,7 +115,8 @@ def reset_model(self) -> npt.NDArray[np.float64]: self.objHeight = self.data.geom("objGeom").xpos[2] self.heightTarget = self.objHeight + self.liftThresh - self.maxReachDist = np.linalg.norm(self.init_tcp - np.array(self._target_pos)) + self.maxReachDist = np.linalg.norm( + self.init_tcp - np.array(self._target_pos)) self.maxPushDist = np.linalg.norm( self.obj_init_pos[:2] - np.array(self._target_pos)[:2] ) @@ -156,7 +146,8 @@ def compute_reward( tcp_to_target = float(np.linalg.norm(tcp - target)) # obj_to_target = float(np.linalg.norm(obj - target)) - in_place_margin = float(np.linalg.norm(self.hand_init_pos - target)) + in_place_margin = float( + np.linalg.norm(self.hand_init_pos - target)) in_place = reward_utils.tolerance( tcp_to_target, bounds=(0, _TARGET_RADIUS), diff --git a/metaworld/envs/sawyer_shelf_place_v3.py b/metaworld/envs/sawyer_shelf_place_v3.py index 761a2ce79..c3829e301 100644 --- a/metaworld/envs/sawyer_shelf_place_v3.py +++ b/metaworld/envs/sawyer_shelf_place_v3.py @@ -9,20 +9,17 @@ from scipy.spatial.transform import Rotation from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerShelfPlaceEnvV3(SawyerXYZEnv): + ENV_NAME: str = "shelf-place-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: goal_low = (-0.1, 0.8, 0.299) goal_high = (0.1, 0.9, 0.301) @@ -31,17 +28,6 @@ def __init__( obj_low = (-0.1, 0.5, 0.019) obj_high = (0.1, 0.6, 0.021) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([0, 0.6, 0.02]), "obj_init_angle": 0.3, @@ -52,20 +38,24 @@ def __init__( self.obj_init_angle = self.init_config["obj_init_angle"] self.hand_init_pos = self.init_config["hand_init_pos"] - self.num_resets = 0 - self._random_reset_space = Box( np.hstack((obj_low, goal_low)), np.hstack((obj_high, goal_high)), dtype=np.float64, ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_shelf_placing.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -109,7 +99,8 @@ def _get_quat_objects(self) -> npt.NDArray[Any]: def adjust_initObjPos(self, orig_init_pos: npt.NDArray[Any]) -> npt.NDArray[Any]: # This is to account for meshes for the geom and object are not aligned # If this is not done, the object could be initialized in an extreme position - diff = self.get_body_com("obj")[:2] - self.data.geom("objGeom").xpos[:2] + diff = self.get_body_com("obj")[:2] - \ + self.data.geom("objGeom").xpos[:2] adjustedPos = orig_init_pos[:2] + diff # The convention we follow is that body_com[2] is always 0, and geom_pos[2] is the object height @@ -117,7 +108,8 @@ def adjust_initObjPos(self, orig_init_pos: npt.NDArray[Any]) -> npt.NDArray[Any] def reset_model(self) -> npt.NDArray[np.float64]: self._reset_hand() - self.obj_init_pos = self.adjust_initObjPos(self.init_config["obj_init_pos"]) + self.obj_init_pos = self.adjust_initObjPos( + self.init_config["obj_init_pos"]) self.obj_init_angle = self.init_config["obj_init_angle"] goal_pos = self._get_state_rand_vec() @@ -130,7 +122,8 @@ def reset_model(self) -> npt.NDArray[np.float64]: self.model.body("shelf").pos = base_shelf_pos[-3:] mujoco.mj_forward(self.model, self.data) - self._target_pos = self.model.site("goal").pos + self.model.body("shelf").pos + self._target_pos = self.model.site( + "goal").pos + self.model.body("shelf").pos assert self.obj_init_pos is not None self._set_obj_xyz(self.obj_init_pos) @@ -194,7 +187,8 @@ def compute_reward( y_scaling = (obj[1] - (target[1] - 3 * _TARGET_RADIUS)) / ( 3 * _TARGET_RADIUS ) - bound_loss = reward_utils.hamacher_product(y_scaling, z_scaling) + bound_loss = reward_utils.hamacher_product( + y_scaling, z_scaling) in_place = np.clip(in_place - bound_loss, 0.0, 1.0) if ( @@ -274,7 +268,8 @@ def compute_reward( cond = self.pickCompleted and (reachDist < 0.1) and not objDropped if cond: placeRew = 1000 * (self.maxPlacingDist - placingDist) + c1 * ( - np.exp(-(placingDist**2) / c2) + np.exp(-(placingDist**2) / c3) + np.exp(-(placingDist**2) / c2) + + np.exp(-(placingDist**2) / c3) ) placeRew = max(placeRew, 0) placeRew, placingDist = [placeRew, placingDist] diff --git a/metaworld/envs/sawyer_soccer_v3.py b/metaworld/envs/sawyer_soccer_v3.py index 0117cf5af..b50f6ab3c 100644 --- a/metaworld/envs/sawyer_soccer_v3.py +++ b/metaworld/envs/sawyer_soccer_v3.py @@ -8,23 +8,20 @@ from scipy.spatial.transform import Rotation from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerSoccerEnvV3(SawyerXYZEnv): + ENV_NAME: str = "soccer-v3" + OBJ_RADIUS: float = 0.013 TARGET_RADIUS: float = 0.07 def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: goal_low = (-0.1, 0.8, 0.0) goal_high = (0.1, 0.9, 0.0) @@ -33,17 +30,6 @@ def __init__( obj_low = (-0.1, 0.6, 0.03) obj_high = (0.1, 0.7, 0.03) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([0, 0.6, 0.03]), "obj_init_angle": 0.3, @@ -59,13 +45,19 @@ def __init__( np.hstack((obj_high, goal_high)), dtype=np.float64, ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_soccer.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -117,7 +109,8 @@ def reset_model(self) -> npt.NDArray[np.float64]: goal_pos = self._get_state_rand_vec() self._target_pos = goal_pos[3:] assert self.obj_init_pos is not None - self.obj_init_pos = np.concatenate([goal_pos[:2], [self.obj_init_pos[-1]]]) + self.obj_init_pos = np.concatenate( + [goal_pos[:2], [self.obj_init_pos[-1]]]) self.model.body("goal_whole").pos = self._target_pos self._set_obj_xyz(self.obj_init_pos) self.maxPushDist = np.linalg.norm( @@ -186,7 +179,8 @@ def _gripper_caging_reward( assert left_caging >= 0 and left_caging <= 1 y_caging = reward_utils.hamacher_product(right_caging, left_caging) - y_gripping = reward_utils.hamacher_product(right_gripping, left_gripping) + y_gripping = reward_utils.hamacher_product( + right_gripping, left_gripping) assert y_caging >= 0 and y_caging <= 1 @@ -194,11 +188,13 @@ def _gripper_caging_reward( obj_position_x_z = np.copy(obj_pos) + np.array([0.0, -obj_pos[1], 0.0]) tcp_obj_norm_x_z = np.linalg.norm(tcp_xz - obj_position_x_z, ord=2) assert self.obj_init_pos is not None - init_obj_x_z = self.obj_init_pos + np.array([0.0, -self.obj_init_pos[1], 0.0]) + init_obj_x_z = self.obj_init_pos + \ + np.array([0.0, -self.obj_init_pos[1], 0.0]) init_tcp_x_z = self.init_tcp + np.array([0.0, -self.init_tcp[1], 0.0]) tcp_obj_x_z_margin = ( - np.linalg.norm(init_obj_x_z - init_tcp_x_z, ord=2) - x_z_success_margin + np.linalg.norm(init_obj_x_z - init_tcp_x_z, + ord=2) - x_z_success_margin ) x_z_caging = reward_utils.tolerance( float(tcp_obj_norm_x_z), @@ -233,7 +229,8 @@ def compute_reward( tcp_opened: float = obs[3] x_scaling = np.array([3.0, 1.0, 1.0]) tcp_to_obj = float(np.linalg.norm(obj - self.tcp_center)) - target_to_obj = float(np.linalg.norm((obj - self._target_pos) * x_scaling)) + target_to_obj = float(np.linalg.norm( + (obj - self._target_pos) * x_scaling)) target_to_obj_init = float( np.linalg.norm((obj - self.obj_init_pos) * x_scaling) ) @@ -248,10 +245,12 @@ def compute_reward( goal_line = self._target_pos[1] - 0.1 if obj[1] > goal_line and abs(obj[0] - self._target_pos[0]) > 0.10: in_place = np.clip( - in_place - 2 * ((obj[1] - goal_line) / (1 - goal_line)), 0.0, 1.0 + in_place - 2 * ((obj[1] - goal_line) / + (1 - goal_line)), 0.0, 1.0 ) - object_grasped = self._gripper_caging_reward(action, obj, self.OBJ_RADIUS) + object_grasped = self._gripper_caging_reward( + action, obj, self.OBJ_RADIUS) reward = (3 * object_grasped) + (6.5 * in_place) diff --git a/metaworld/envs/sawyer_stick_pull_v3.py b/metaworld/envs/sawyer_stick_pull_v3.py index 087a01e49..f8d349003 100644 --- a/metaworld/envs/sawyer_stick_pull_v3.py +++ b/metaworld/envs/sawyer_stick_pull_v3.py @@ -8,20 +8,17 @@ from scipy.spatial.transform import Rotation from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import ObservationDict, StickInitConfigDict from metaworld.utils import reward_utils class SawyerStickPullEnvV3(SawyerXYZEnv): + ENV_NAME: str = "stick-pull-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.35, 0.05) hand_high = (0.5, 1, 0.5) @@ -30,17 +27,6 @@ def __init__( goal_low = (0.35, 0.45, 0.0199) goal_high = (0.45, 0.55, 0.0201) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: StickInitConfigDict = { "stick_init_pos": np.array([0, 0.6, 0.02]), "hand_init_pos": np.array([0, 0.6, 0.2]), @@ -52,19 +38,26 @@ def __init__( # Fix object init position. self.obj_init_pos = np.array([0.2, 0.69, 0.0]) self.obj_init_qpos = np.array([0.0, 0.09]) - self.obj_space = Box(np.array(obj_low), np.array(obj_high), dtype=np.float64) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.obj_space = Box(np.array(obj_low), np.array( + obj_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) self._random_reset_space = Box( np.hstack((obj_low, goal_low)), np.hstack((obj_high, goal_high)), dtype=np.float64, ) + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) + @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_stick_obj.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -157,8 +150,10 @@ def reset_model(self) -> npt.NDArray[np.float64]: goal_pos = self._get_state_rand_vec() while np.linalg.norm(goal_pos[:2] - goal_pos[-3:-1]) < 0.1: goal_pos = self._get_state_rand_vec() - self.stick_init_pos = np.concatenate([goal_pos[:2], [self.stick_init_pos[-1]]]) - self._target_pos = np.concatenate([goal_pos[-3:-1], [self.stick_init_pos[-1]]]) + self.stick_init_pos = np.concatenate( + [goal_pos[:2], [self.stick_init_pos[-1]]]) + self._target_pos = np.concatenate( + [goal_pos[-3:-1], [self.stick_init_pos[-1]]]) self._set_stick_xyz(self.stick_init_pos) self._set_obj_xyz(self.obj_init_qpos) @@ -171,7 +166,8 @@ def reset_model(self) -> npt.NDArray[np.float64]: self.heightTarget = self.stickHeight + self.liftThresh assert self.obj_init_pos is not None and self.stick_init_pos is not None - self.maxPullDist = np.linalg.norm(self.obj_init_pos[:2] - self._target_pos[:-1]) + self.maxPullDist = np.linalg.norm( + self.obj_init_pos[:2] - self._target_pos[:-1]) self.maxPlaceDist = ( np.linalg.norm( np.array( @@ -211,9 +207,11 @@ def compute_reward( handle_to_target = float(np.linalg.norm(handle - target)) yz_scaling = np.array([1.0, 1.0, 2.0]) - stick_to_container = float(np.linalg.norm((stick - container) * yz_scaling)) + stick_to_container = float(np.linalg.norm( + (stick - container) * yz_scaling)) stick_in_place_margin = float( - np.linalg.norm((self.stick_init_pos - container_init_pos) * yz_scaling) + np.linalg.norm( + (self.stick_init_pos - container_init_pos) * yz_scaling) ) stick_in_place = reward_utils.tolerance( stick_to_container, @@ -340,7 +338,8 @@ def compute_reward( if placeDist < 0.05: c4 = 2000 pullRew += 1000 * (self.maxPullDist - pullDist) + c4 * ( - np.exp(-(pullDist**2) / c2) + np.exp(-(pullDist**2) / c3) + np.exp(-(pullDist**2) / c2) + + np.exp(-(pullDist**2) / c3) ) pullRew = max(pullRew, 0) diff --git a/metaworld/envs/sawyer_stick_push_v3.py b/metaworld/envs/sawyer_stick_push_v3.py index 75f583033..f8ebd3fc8 100644 --- a/metaworld/envs/sawyer_stick_push_v3.py +++ b/metaworld/envs/sawyer_stick_push_v3.py @@ -8,20 +8,17 @@ from scipy.spatial.transform import Rotation from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import ObservationDict, StickInitConfigDict from metaworld.utils import reward_utils class SawyerStickPushEnvV3(SawyerXYZEnv): + ENV_NAME: str = "stick-push-v3" + def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1, 0.5) @@ -30,17 +27,6 @@ def __init__( goal_low = (0.399, 0.55, 0.1319) goal_high = (0.401, 0.6, 0.1321) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: StickInitConfigDict = { "stick_init_pos": np.array([-0.1, 0.6, 0.02]), "hand_init_pos": np.array([0, 0.6, 0.2]), @@ -52,19 +38,26 @@ def __init__( # For now, fix the object initial position. self.obj_init_pos = np.array([0.2, 0.6, 0.0]) self.obj_init_qpos = np.array([0.0, 0.0]) - self.obj_space = Box(np.array(obj_low), np.array(obj_high), dtype=np.float64) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.obj_space = Box(np.array(obj_low), np.array( + obj_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) self._random_reset_space = Box( np.hstack((obj_low, goal_low)), np.hstack((obj_high, goal_high)), dtype=np.float64, ) + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) + @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_stick_obj.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -152,7 +145,8 @@ def reset_model(self) -> npt.NDArray[np.float64]: goal_pos = self._get_state_rand_vec() while np.linalg.norm(goal_pos[:2] - goal_pos[-3:-1]) < 0.1: goal_pos = self._get_state_rand_vec() - self.stick_init_pos = np.concatenate([goal_pos[:2], [self.stick_init_pos[-1]]]) + self.stick_init_pos = np.concatenate( + [goal_pos[:2], [self.stick_init_pos[-1]]]) self._target_pos = np.concatenate( [goal_pos[-3:-1], [self._get_site_pos("insertion")[-1]]] ) @@ -178,7 +172,8 @@ def reset_model(self) -> npt.NDArray[np.float64]: ) + self.heightTarget ) - self.maxPushDist = np.linalg.norm(self.obj_init_pos[:2] - self._target_pos[:2]) + self.maxPushDist = np.linalg.norm( + self.obj_init_pos[:2] - self._target_pos[:2]) return self._get_obs() @@ -213,7 +208,8 @@ def _gripper_caging_reward( medium_density(bool): flag for medium-density. Cannot be used with high-density. """ if high_density and medium_density: - raise ValueError("Can only be either high_density or medium_density") + raise ValueError( + "Can only be either high_density or medium_density") # MARK: Left-right gripper information for caging reward---------------- left_pad = self.get_body_com("leftpad") right_pad = self.get_body_com("rightpad") @@ -230,7 +226,8 @@ def _gripper_caging_reward( reward_utils.tolerance( pad_to_obj_lr[i], # "x" in the description above bounds=(obj_radius, pad_success_thresh), - margin=caging_lr_margin[i], # "margin" in the description above + # "margin" in the description above + margin=caging_lr_margin[i], sigmoid="long_tail", ) for i in range(2) @@ -241,7 +238,8 @@ def _gripper_caging_reward( tcp = self.tcp_center xz = [0, 2] - caging_xz_margin = np.linalg.norm(self.stick_init_pos[xz] - self.init_tcp[xz]) + caging_xz_margin = np.linalg.norm( + self.stick_init_pos[xz] - self.init_tcp[xz]) caging_xz_margin -= xz_thresh caging_xz = reward_utils.tolerance( float( @@ -254,7 +252,8 @@ def _gripper_caging_reward( # MARK: Closed-extent gripper information for caging reward------------- gripper_closed = ( - min(max(0, action[-1]), desired_gripper_effort) / desired_gripper_effort + min(max(0, action[-1]), desired_gripper_effort) / + desired_gripper_effort ) # MARK: Combine components---------------------------------------------- @@ -267,7 +266,8 @@ def _gripper_caging_reward( if medium_density: tcp = self.tcp_center tcp_to_obj = np.linalg.norm(obj_pos - tcp) - tcp_to_obj_init = np.linalg.norm(self.stick_init_pos - self.init_tcp) + tcp_to_obj_init = np.linalg.norm( + self.stick_init_pos - self.init_tcp) reach_margin = abs(tcp_to_obj_init - object_reach_radius) reach = reward_utils.tolerance( float(tcp_to_obj), @@ -397,7 +397,8 @@ def compute_reward( c5 = 0.001 c6 = 0.0001 pushRew += 1000 * (self.maxPushDist - pushDist) + c4 * ( - np.exp(-(pushDist**2) / c5) + np.exp(-(pushDist**2) / c6) + np.exp(-(pushDist**2) / c5) + + np.exp(-(pushDist**2) / c6) ) pushRew = max(pushRew, 0) diff --git a/metaworld/envs/sawyer_sweep_into_goal_v3.py b/metaworld/envs/sawyer_sweep_into_goal_v3.py index 018d96827..9cecbe2be 100644 --- a/metaworld/envs/sawyer_sweep_into_goal_v3.py +++ b/metaworld/envs/sawyer_sweep_into_goal_v3.py @@ -8,22 +8,19 @@ from scipy.spatial.transform import Rotation from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerSweepIntoGoalEnvV3(SawyerXYZEnv): + ENV_NAME: str = "sweep-into-v3" + OBJ_RADIUS: float = 0.02 def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1, 0.5) @@ -32,17 +29,6 @@ def __init__( goal_low = (-0.001, 0.8399, 0.0199) goal_high = (+0.001, 0.8401, 0.0201) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([0.0, 0.6, 0.02]), "obj_init_angle": 0.3, @@ -58,13 +44,19 @@ def __init__( np.hstack((obj_high, goal_high)), dtype=np.float64, ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_table_with_hole.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -109,7 +101,8 @@ def reset_model(self) -> npt.NDArray[np.float64]: while np.linalg.norm(goal_pos[:2] - self._target_pos[:2]) < 0.15: goal_pos = self._get_state_rand_vec() assert self.obj_init_pos is not None - self.obj_init_pos = np.concatenate([goal_pos[:2], [self.obj_init_pos[-1]]]) + self.obj_init_pos = np.concatenate( + [goal_pos[:2], [self.obj_init_pos[-1]]]) self._set_obj_xyz(self.obj_init_pos) self.model.site("goal").pos = self._target_pos @@ -178,7 +171,8 @@ def _gripper_caging_reward( assert left_caging >= 0 and left_caging <= 1 y_caging = reward_utils.hamacher_product(right_caging, left_caging) - y_gripping = reward_utils.hamacher_product(right_gripping, left_gripping) + y_gripping = reward_utils.hamacher_product( + right_gripping, left_gripping) assert y_caging >= 0 and y_caging <= 1 @@ -186,11 +180,13 @@ def _gripper_caging_reward( obj_position_x_z = np.copy(obj_pos) + np.array([0.0, -obj_pos[1], 0.0]) tcp_obj_norm_x_z = np.linalg.norm(tcp_xz - obj_position_x_z, ord=2) assert self.obj_init_pos is not None - init_obj_x_z = self.obj_init_pos + np.array([0.0, -self.obj_init_pos[1], 0.0]) + init_obj_x_z = self.obj_init_pos + \ + np.array([0.0, -self.obj_init_pos[1], 0.0]) init_tcp_x_z = self.init_tcp + np.array([0.0, -self.init_tcp[1], 0.0]) tcp_obj_x_z_margin = ( - np.linalg.norm(init_obj_x_z - init_tcp_x_z, ord=2) - x_z_success_margin + np.linalg.norm(init_obj_x_z - init_tcp_x_z, + ord=2) - x_z_success_margin ) x_z_caging = reward_utils.tolerance( float(tcp_obj_norm_x_z), @@ -225,7 +221,8 @@ def compute_reward( tcp = self.tcp_center obj = obs[4:7] tcp_opened = obs[3] - target = np.array([self._target_pos[0], self._target_pos[1], obj[2]]) + target = np.array( + [self._target_pos[0], self._target_pos[1], obj[2]]) obj_to_target = float(np.linalg.norm(obj - target)) tcp_to_obj = float(np.linalg.norm(obj - tcp)) @@ -238,7 +235,8 @@ def compute_reward( sigmoid="long_tail", ) - object_grasped = self._gripper_caging_reward(action, obj, self.OBJ_RADIUS) + object_grasped = self._gripper_caging_reward( + action, obj, self.OBJ_RADIUS) in_place_and_object_grasped = reward_utils.hamacher_product( object_grasped, in_place ) @@ -278,7 +276,8 @@ def compute_reward( self.reachCompleted = reachDist < 0.05 assert objPos is not None and self.obj_init_pos is not None if ( - objPos[-1] < self.obj_init_pos[-1] - 0.05 and 0.4 < objPos[1] < 1.0 + objPos[-1] < self.obj_init_pos[-1] - + 0.05 and 0.4 < objPos[1] < 1.0 ): # ignore: type reachRew = 0.0 # type: ignore reachDist = 0.0 # type: ignore diff --git a/metaworld/envs/sawyer_sweep_v3.py b/metaworld/envs/sawyer_sweep_v3.py index 50ef1ca28..2063e8539 100644 --- a/metaworld/envs/sawyer_sweep_v3.py +++ b/metaworld/envs/sawyer_sweep_v3.py @@ -7,22 +7,19 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils class SawyerSweepEnvV3(SawyerXYZEnv): + ENV_NAME: str = "sweep-v3" + OBJ_RADIUS: float = 0.02 def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: init_puck_z = 0.1 hand_low = (-0.5, 0.40, 0.05) @@ -32,17 +29,6 @@ def __init__( goal_low = (0.49, 0.6, 0.00) goal_high = (0.51, 0.7, 0.02) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_pos": np.array([0.0, 0.6, 0.02]), # type: ignore "obj_init_angle": 0.3, @@ -56,15 +42,22 @@ def __init__( self.init_puck_z = init_puck_z self._random_reset_space = Box( - np.array(obj_low), np.array(obj_high), dtype=np.float64 # type: ignore + # type: ignore + np.array(obj_low), np.array(obj_high), dtype=np.float64 + ) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) # type: ignore + + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) # type: ignore @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_sweep_v3.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: @@ -103,7 +96,8 @@ def reset_model(self) -> npt.NDArray[np.float64]: self.objHeight = self._get_pos_objects()[2] obj_pos = self._get_state_rand_vec() - self.obj_init_pos = np.concatenate([obj_pos[:2], [self.obj_init_pos[-1]]]) # type: ignore + self.obj_init_pos = np.concatenate( + [obj_pos[:2], [self.obj_init_pos[-1]]]) # type: ignore self._target_pos[1] = obj_pos.copy()[1] self._set_obj_xyz(self.obj_init_pos) @@ -113,7 +107,8 @@ def reset_model(self) -> npt.NDArray[np.float64]: self.objHeight = self.data.geom("objGeom").xpos[2] self.heightTarget = self.objHeight + self.liftThresh - self.maxReachDist = np.linalg.norm(self.init_tcp - np.array(self._target_pos)) + self.maxReachDist = np.linalg.norm( + self.init_tcp - np.array(self._target_pos)) self.maxPushDist = np.linalg.norm( self.obj_init_pos[:2] - np.array(self._target_pos)[:2] ) @@ -187,7 +182,8 @@ def _gripper_caging_reward( assert left_caging >= 0 and left_caging <= 1 y_caging = reward_utils.hamacher_product(right_caging, left_caging) - y_gripping = reward_utils.hamacher_product(right_gripping, left_gripping) + y_gripping = reward_utils.hamacher_product( + right_gripping, left_gripping) assert y_caging >= 0 and y_caging <= 1 @@ -195,11 +191,13 @@ def _gripper_caging_reward( obj_position_x_z = np.copy(obj_pos) + np.array([0.0, -obj_pos[1], 0.0]) tcp_obj_norm_x_z = np.linalg.norm(tcp_xz - obj_position_x_z, ord=2) assert self.obj_init_pos is not None - init_obj_x_z = self.obj_init_pos + np.array([0.0, -self.obj_init_pos[1], 0.0]) + init_obj_x_z = self.obj_init_pos + \ + np.array([0.0, -self.obj_init_pos[1], 0.0]) init_tcp_x_z = self.init_tcp + np.array([0.0, -self.init_tcp[1], 0.0]) tcp_obj_x_z_margin = ( - np.linalg.norm(init_obj_x_z - init_tcp_x_z, ord=2) - x_z_success_margin + np.linalg.norm(init_obj_x_z - init_tcp_x_z, + ord=2) - x_z_success_margin ) x_z_caging = reward_utils.tolerance( float(tcp_obj_norm_x_z), @@ -247,7 +245,8 @@ def compute_reward( sigmoid="long_tail", ) - object_grasped = self._gripper_caging_reward(action, obj, self.OBJ_RADIUS) + object_grasped = self._gripper_caging_reward( + action, obj, self.OBJ_RADIUS) in_place_and_object_grasped = reward_utils.hamacher_product( object_grasped, in_place ) @@ -294,7 +293,8 @@ def compute_reward( c3 = 0.001 if self.reachCompleted: pushRew = 1000 * (self.maxPushDist - pushDistxy) + c1 * ( - np.exp(-(pushDistxy**2) / c2) + np.exp(-(pushDistxy**2) / c3) + np.exp(-(pushDistxy**2) / c2) + + np.exp(-(pushDistxy**2) / c3) ) pushRew = max(pushRew, 0) pushRew = pushRew diff --git a/metaworld/envs/sawyer_window_close_v3.py b/metaworld/envs/sawyer_window_close_v3.py index d4c34ad3b..b7445c282 100644 --- a/metaworld/envs/sawyer_window_close_v3.py +++ b/metaworld/envs/sawyer_window_close_v3.py @@ -7,7 +7,7 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils @@ -17,24 +17,20 @@ class SawyerWindowCloseEnvV3(SawyerXYZEnv): Motivation for V3: V1 was rarely solvable due to limited path length. The window usually - only got ~25% closed before hitting max_path_length + only got ~25% closed before hitting the 500 step limit. Changelog from V1 to V3: - (8/11/20) Updated to Byron's XML - (7/7/20) Added 3 element handle position to the observation (for consistency with other environments) - - (6/15/20) Increased max_path_length from 150 to 200 + - (6/15/20) Increased the step limit from 150 to 200 """ + ENV_NAME: str = "window-close-v3" TARGET_RADIUS: float = 0.05 def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: liftThresh = 0.02 hand_low = (-0.5, 0.40, 0.05) @@ -42,17 +38,6 @@ def __init__( obj_low = (0.0, 0.75, 0.2) obj_high = (0.0, 0.9, 0.2) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_angle": 0.3, "obj_init_pos": np.array([0.1, 0.785, 0.16], dtype=np.float32), @@ -62,24 +47,30 @@ def __init__( self.obj_init_angle = self.init_config["obj_init_angle"] self.hand_init_pos = self.init_config["hand_init_pos"] - goal_low = self.hand_low - goal_high = self.hand_high + goal_low = hand_low + goal_high = hand_high self.liftThresh = liftThresh self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) self.maxPullDist = 0.2 self.target_reward = 1000 * self.maxPullDist + 1000 * 2 + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) + @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_window_horizontal.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: diff --git a/metaworld/envs/sawyer_window_open_v3.py b/metaworld/envs/sawyer_window_open_v3.py index 6b02e0719..d1d2d12c0 100644 --- a/metaworld/envs/sawyer_window_open_v3.py +++ b/metaworld/envs/sawyer_window_open_v3.py @@ -7,7 +7,7 @@ from gymnasium.spaces import Box from metaworld.asset_path_utils import full_V3_path_for -from metaworld.sawyer_xyz_env import RenderMode, SawyerXYZEnv +from metaworld.sawyer_xyz_env import SawyerXYZEnv from metaworld.types import InitConfigDict from metaworld.utils import reward_utils @@ -21,36 +21,21 @@ class SawyerWindowOpenEnvV3(SawyerXYZEnv): - (8/11/20) Updated to Byron's XML - (7/7/20) Added 3 element handle position to the observation (for consistency with other environments) - - (6/15/20) Increased max_path_length from 150 to 200 + - (6/15/20) Increased step limit from 150 to 200 """ + ENV_NAME: str = "window-open-v3" TARGET_RADIUS: float = 0.05 def __init__( self, - render_mode: RenderMode | None = None, - camera_name: str | None = None, - camera_id: int | None = None, - reward_function_version: str = "v2", - height: int = 480, - width: int = 480, + **kwargs, ) -> None: hand_low = (-0.5, 0.40, 0.05) hand_high = (0.5, 1, 0.5) obj_low = (-0.1, 0.7, 0.16) obj_high = (0.1, 0.9, 0.16) - super().__init__( - hand_low=hand_low, - hand_high=hand_high, - render_mode=render_mode, - camera_name=camera_name, - camera_id=camera_id, - height=height, - width=width, - ) - self.reward_function_version = reward_function_version - self.init_config: InitConfigDict = { "obj_init_angle": 0.3, "obj_init_pos": np.array([-0.1, 0.785, 0.16], dtype=np.float32), @@ -60,22 +45,28 @@ def __init__( self.obj_init_angle = self.init_config["obj_init_angle"] self.hand_init_pos = self.init_config["hand_init_pos"] - goal_low = self.hand_low - goal_high = self.hand_high + goal_low = hand_low + goal_high = hand_high self._random_reset_space = Box( np.array(obj_low), np.array(obj_high), dtype=np.float64 ) - self.goal_space = Box(np.array(goal_low), np.array(goal_high), dtype=np.float64) + self.goal_space = Box(np.array(goal_low), np.array( + goal_high), dtype=np.float64) self.maxPullDist = 0.2 self.target_reward = 1000 * self.maxPullDist + 1000 * 2 + super().__init__( + hand_low=hand_low, + hand_high=hand_high, + **kwargs, + ) + @property - def model_name(self) -> str: + def model_path(self) -> str: return full_V3_path_for("sawyer_xyz/sawyer_window_horizontal.xml") - @SawyerXYZEnv._Decorators.assert_task_is_set def evaluate_state( self, obs: npt.NDArray[np.float64], action: npt.NDArray[np.float32] ) -> tuple[float, dict[str, Any]]: diff --git a/metaworld/evaluation.py b/metaworld/evaluation.py index b4328f2a5..28d76a7d7 100644 --- a/metaworld/evaluation.py +++ b/metaworld/evaluation.py @@ -6,7 +6,7 @@ import numpy as np import numpy.typing as npt -from metaworld.env_dict import ALL_V3_ENVIRONMENTS +from metaworld.env_dict import ENV_CLASS_MAP class Agent(Protocol): @@ -35,34 +35,28 @@ def adapt(self) -> None: ... -def _get_task_names( - envs: gym.vector.SyncVectorEnv | gym.vector.AsyncVectorEnv, -) -> list[str]: - metaworld_cls_to_task_name = {v.__name__: k for k, v in ALL_V3_ENVIRONMENTS.items()} - return [ - metaworld_cls_to_task_name[task_name] - for task_name in envs.get_attr("task_name") - ] - - def evaluation( agent: Agent, eval_envs: gym.vector.SyncVectorEnv | gym.vector.AsyncVectorEnv, num_episodes: int = 50, ) -> tuple[float, float, dict[str, float], dict[str, list[float]]]: - terminate_on_success = np.all(eval_envs.get_attr("terminate_on_success")).item() + terminate_on_success = np.all( + eval_envs.get_attr("terminate_on_success")).item() eval_envs.call("toggle_terminate_on_success", True) obs: npt.NDArray[np.float64] obs, _ = eval_envs.reset() agent.reset(np.ones(eval_envs.num_envs, dtype=np.bool_)) - task_names = _get_task_names(eval_envs) - successes = {task_name: 0 for task_name in set(task_names)} + env_names = eval_envs.get_attr('ENV_NAME') + successes = {env_name: 0 for env_name in set(env_names)} episodic_returns: dict[str, list[float]] = { - task_name: [] for task_name in set(task_names) + env_name: [] for env_name in set(env_names) } + pbar_envs_done = tqdm(total=len(set(task_names)) * + num_episodes, desc="Evaluation") + def eval_done(returns): return all(len(r) >= num_episodes for _, r in returns.items()) @@ -75,20 +69,24 @@ def eval_done(returns): for i, env_ended in enumerate(dones): if env_ended: - episodic_returns[task_names[i]].append( + env = env_names[i] + current_count = len(episodic_returns[env]) + if current_count < num_episodes: + pbar_envs_done.update(1) + episodic_returns[env].append( float(infos["final_info"]["episode"]["r"][i]) ) - if len(episodic_returns[task_names[i]]) <= num_episodes: - successes[task_names[i]] += int(infos["final_info"]["success"][i]) + if len(episodic_returns[env]) <= num_episodes: + successes[env] += int(infos["final_info"]["success"][i]) episodic_returns = { - task_name: returns[:num_episodes] - for task_name, returns in episodic_returns.items() + env_name: returns[:num_episodes] + for env_name, returns in episodic_returns.items() } success_rate_per_task = { - task_name: task_successes / num_episodes - for task_name, task_successes in successes.items() + env_name: task_successes / num_episodes + for env_name, task_successes in successes.items() } mean_success_rate = np.mean(list(success_rate_per_task.values())) mean_returns = np.mean(list(episodic_returns.values())) @@ -113,11 +111,11 @@ def metalearning_evaluation( ) -> tuple[float, float, dict[str, float]]: eval_envs.call("toggle_sample_tasks_on_reset", False) eval_envs.call("toggle_terminate_on_success", False) - task_names = _get_task_names(eval_envs) + env_names = eval_envs.get_attr('ENV_NAME') total_mean_success_rate = 0.0 total_mean_return = 0.0 - success_rate_per_task = np.zeros((num_evals, len(set(task_names)))) + success_rate_per_task = np.zeros((num_evals, len(set(env_names)))) for i in range(num_evals): obs: npt.NDArray[np.float64] @@ -155,17 +153,18 @@ def metalearning_evaluation( ) total_mean_success_rate += mean_success_rate total_mean_return += mean_return - success_rate_per_task[i] = np.array(list(_success_rate_per_task.values())) + success_rate_per_task[i] = np.array( + list(_success_rate_per_task.values())) success_rates = (success_rate_per_task).mean(axis=0) - task_success_rates = { - task_name: success_rates[i] for i, task_name in enumerate(set(task_names)) + env_success_rates = { + env_name: success_rates[i] for i, env_name in enumerate(set(env_names)) } return ( total_mean_success_rate / num_evals, total_mean_return / num_evals, - task_success_rates, + env_success_rates, ) diff --git a/metaworld/policies/__init__.py b/metaworld/policies/__init__.py index 37a2a1c6c..1ecb245f7 100644 --- a/metaworld/policies/__init__.py +++ b/metaworld/policies/__init__.py @@ -75,109 +75,109 @@ ENV_POLICY_MAP = dict( { - "assembly-v3": SawyerAssemblyV3Policy, - "basketball-v3": SawyerBasketballV3Policy, - "bin-picking-v3": SawyerBinPickingV3Policy, - "box-close-v3": SawyerBoxCloseV3Policy, - "button-press-topdown-v3": SawyerButtonPressTopdownV3Policy, - "button-press-topdown-wall-v3": SawyerButtonPressTopdownWallV3Policy, - "button-press-v3": SawyerButtonPressV3Policy, - "button-press-wall-v3": SawyerButtonPressWallV3Policy, - "coffee-button-v3": SawyerCoffeeButtonV3Policy, - "coffee-pull-v3": SawyerCoffeePullV3Policy, - "coffee-push-v3": SawyerCoffeePushV3Policy, - "dial-turn-v3": SawyerDialTurnV3Policy, - "disassemble-v3": SawyerDisassembleV3Policy, - "door-close-v3": SawyerDoorCloseV3Policy, - "door-lock-v3": SawyerDoorLockV3Policy, - "door-open-v3": SawyerDoorOpenV3Policy, - "door-unlock-v3": SawyerDoorUnlockV3Policy, - "drawer-close-v3": SawyerDrawerCloseV3Policy, - "drawer-open-v3": SawyerDrawerOpenV3Policy, - "faucet-close-v3": SawyerFaucetCloseV3Policy, - "faucet-open-v3": SawyerFaucetOpenV3Policy, - "hammer-v3": SawyerHammerV3Policy, - "hand-insert-v3": SawyerHandInsertV3Policy, - "handle-press-side-v3": SawyerHandlePressSideV3Policy, - "handle-press-v3": SawyerHandlePressV3Policy, - "handle-pull-v3": SawyerHandlePullV3Policy, - "handle-pull-side-v3": SawyerHandlePullSideV3Policy, - "peg-insert-side-v3": SawyerPegInsertionSideV3Policy, - "lever-pull-v3": SawyerLeverPullV3Policy, - "peg-unplug-side-v3": SawyerPegUnplugSideV3Policy, - "pick-out-of-hole-v3": SawyerPickOutOfHoleV3Policy, - "pick-place-v3": SawyerPickPlaceV3Policy, - "pick-place-wall-v3": SawyerPickPlaceWallV3Policy, - "plate-slide-back-side-v3": SawyerPlateSlideBackSideV3Policy, - "plate-slide-back-v3": SawyerPlateSlideBackV3Policy, - "plate-slide-side-v3": SawyerPlateSlideSideV3Policy, - "plate-slide-v3": SawyerPlateSlideV3Policy, - "reach-v3": SawyerReachV3Policy, - "reach-wall-v3": SawyerReachWallV3Policy, - "push-back-v3": SawyerPushBackV3Policy, - "push-v3": SawyerPushV3Policy, - "push-wall-v3": SawyerPushWallV3Policy, - "shelf-place-v3": SawyerShelfPlaceV3Policy, - "soccer-v3": SawyerSoccerV3Policy, - "stick-pull-v3": SawyerStickPullV3Policy, - "stick-push-v3": SawyerStickPushV3Policy, - "sweep-into-v3": SawyerSweepIntoV3Policy, - "sweep-v3": SawyerSweepV3Policy, - "window-close-v3": SawyerWindowCloseV3Policy, - "window-open-v3": SawyerWindowOpenV3Policy, + 'assembly-v3': SawyerAssemblyV3Policy, + 'basketball-v3': SawyerBasketballV3Policy, + 'bin-picking-v3': SawyerBinPickingV3Policy, + 'box-close-v3': SawyerBoxCloseV3Policy, + 'button-press-topdown-v3': SawyerButtonPressTopdownV3Policy, + 'button-press-topdown-wall-v3': SawyerButtonPressTopdownWallV3Policy, + 'button-press-v3': SawyerButtonPressV3Policy, + 'button-press-wall-v3': SawyerButtonPressWallV3Policy, + 'coffee-button-v3': SawyerCoffeeButtonV3Policy, + 'coffee-pull-v3': SawyerCoffeePullV3Policy, + 'coffee-push-v3': SawyerCoffeePushV3Policy, + 'dial-turn-v3': SawyerDialTurnV3Policy, + 'disassemble-v3': SawyerDisassembleV3Policy, + 'door-close-v3': SawyerDoorCloseV3Policy, + 'door-lock-v3': SawyerDoorLockV3Policy, + 'door-open-v3': SawyerDoorOpenV3Policy, + 'door-unlock-v3': SawyerDoorUnlockV3Policy, + 'drawer-close-v3': SawyerDrawerCloseV3Policy, + 'drawer-open-v3': SawyerDrawerOpenV3Policy, + 'faucet-close-v3': SawyerFaucetCloseV3Policy, + 'faucet-open-v3': SawyerFaucetOpenV3Policy, + 'hammer-v3': SawyerHammerV3Policy, + 'hand-insert-v3': SawyerHandInsertV3Policy, + 'handle-press-side-v3': SawyerHandlePressSideV3Policy, + 'handle-press-v3': SawyerHandlePressV3Policy, + 'handle-pull-v3': SawyerHandlePullV3Policy, + 'handle-pull-side-v3': SawyerHandlePullSideV3Policy, + 'peg-insert-side-v3': SawyerPegInsertionSideV3Policy, + 'lever-pull-v3': SawyerLeverPullV3Policy, + 'peg-unplug-side-v3': SawyerPegUnplugSideV3Policy, + 'pick-out-of-hole-v3': SawyerPickOutOfHoleV3Policy, + 'pick-place-v3': SawyerPickPlaceV3Policy, + 'pick-place-wall-v3': SawyerPickPlaceWallV3Policy, + 'plate-slide-back-side-v3': SawyerPlateSlideBackSideV3Policy, + 'plate-slide-back-v3': SawyerPlateSlideBackV3Policy, + 'plate-slide-side-v3': SawyerPlateSlideSideV3Policy, + 'plate-slide-v3': SawyerPlateSlideV3Policy, + 'reach-v3': SawyerReachV3Policy, + 'reach-wall-v3': SawyerReachWallV3Policy, + 'push-back-v3': SawyerPushBackV3Policy, + 'push-v3': SawyerPushV3Policy, + 'push-wall-v3': SawyerPushWallV3Policy, + 'shelf-place-v3': SawyerShelfPlaceV3Policy, + 'soccer-v3': SawyerSoccerV3Policy, + 'stick-pull-v3': SawyerStickPullV3Policy, + 'stick-push-v3': SawyerStickPushV3Policy, + 'sweep-into-v3': SawyerSweepIntoV3Policy, + 'sweep-v3': SawyerSweepV3Policy, + 'window-close-v3': SawyerWindowCloseV3Policy, + 'window-open-v3': SawyerWindowOpenV3Policy, } ) __all__ = [ - "SawyerAssemblyV3Policy", - "SawyerBasketballV3Policy", - "SawyerBinPickingV3Policy", - "SawyerBoxCloseV3Policy", - "SawyerButtonPressTopdownV3Policy", - "SawyerButtonPressTopdownWallV3Policy", - "SawyerButtonPressV3Policy", - "SawyerButtonPressWallV3Policy", - "SawyerCoffeeButtonV3Policy", - "SawyerCoffeePullV3Policy", - "SawyerCoffeePushV3Policy", - "SawyerDialTurnV3Policy", - "SawyerDisassembleV3Policy", - "SawyerDoorCloseV3Policy", - "SawyerDoorLockV3Policy", - "SawyerDoorOpenV3Policy", - "SawyerDoorUnlockV3Policy", - "SawyerDrawerCloseV3Policy", - "SawyerDrawerOpenV3Policy", - "SawyerFaucetCloseV3Policy", - "SawyerFaucetOpenV3Policy", - "SawyerHammerV3Policy", - "SawyerHandInsertV3Policy", - "SawyerHandlePressSideV3Policy", - "SawyerHandlePressV3Policy", - "SawyerHandlePullSideV3Policy", - "SawyerHandlePullV3Policy", - "SawyerLeverPullV3Policy", - "SawyerPegInsertionSideV3Policy", - "SawyerPegUnplugSideV3Policy", - "SawyerPickOutOfHoleV3Policy", - "SawyerPickPlaceV3Policy", - "SawyerPickPlaceWallV3Policy", - "SawyerPlateSlideBackSideV3Policy", - "SawyerPlateSlideBackV3Policy", - "SawyerPlateSlideSideV3Policy", - "SawyerPlateSlideV3Policy", - "SawyerPushBackV3Policy", - "SawyerPushV3Policy", - "SawyerPushWallV3Policy", - "SawyerReachV3Policy", - "SawyerReachWallV3Policy", - "SawyerShelfPlaceV3Policy", - "SawyerSoccerV3Policy", - "SawyerStickPullV3Policy", - "SawyerStickPushV3Policy", - "SawyerSweepIntoV3Policy", - "SawyerSweepV3Policy", - "SawyerWindowOpenV3Policy", - "SawyerWindowCloseV3Policy", - "ENV_POLICY_MAP", + 'SawyerAssemblyV3Policy', + 'SawyerBasketballV3Policy', + 'SawyerBinPickingV3Policy', + 'SawyerBoxCloseV3Policy', + 'SawyerButtonPressTopdownV3Policy', + 'SawyerButtonPressTopdownWallV3Policy', + 'SawyerButtonPressV3Policy', + 'SawyerButtonPressWallV3Policy', + 'SawyerCoffeeButtonV3Policy', + 'SawyerCoffeePullV3Policy', + 'SawyerCoffeePushV3Policy', + 'SawyerDialTurnV3Policy', + 'SawyerDisassembleV3Policy', + 'SawyerDoorCloseV3Policy', + 'SawyerDoorLockV3Policy', + 'SawyerDoorOpenV3Policy', + 'SawyerDoorUnlockV3Policy', + 'SawyerDrawerCloseV3Policy', + 'SawyerDrawerOpenV3Policy', + 'SawyerFaucetCloseV3Policy', + 'SawyerFaucetOpenV3Policy', + 'SawyerHammerV3Policy', + 'SawyerHandInsertV3Policy', + 'SawyerHandlePressSideV3Policy', + 'SawyerHandlePressV3Policy', + 'SawyerHandlePullSideV3Policy', + 'SawyerHandlePullV3Policy', + 'SawyerLeverPullV3Policy', + 'SawyerPegInsertionSideV3Policy', + 'SawyerPegUnplugSideV3Policy', + 'SawyerPickOutOfHoleV3Policy', + 'SawyerPickPlaceV3Policy', + 'SawyerPickPlaceWallV3Policy', + 'SawyerPlateSlideBackSideV3Policy', + 'SawyerPlateSlideBackV3Policy', + 'SawyerPlateSlideSideV3Policy', + 'SawyerPlateSlideV3Policy', + 'SawyerPushBackV3Policy', + 'SawyerPushV3Policy', + 'SawyerPushWallV3Policy', + 'SawyerReachV3Policy', + 'SawyerReachWallV3Policy', + 'SawyerShelfPlaceV3Policy', + 'SawyerSoccerV3Policy', + 'SawyerStickPullV3Policy', + 'SawyerStickPushV3Policy', + 'SawyerSweepIntoV3Policy', + 'SawyerSweepV3Policy', + 'SawyerWindowOpenV3Policy', + 'SawyerWindowCloseV3Policy', + 'ENV_POLICY_MAP', ] diff --git a/metaworld/policies/sawyer_peg_insertion_side_v3_policy.py b/metaworld/policies/sawyer_peg_insertion_side_v3_policy.py index 3763a4576..902639755 100644 --- a/metaworld/policies/sawyer_peg_insertion_side_v3_policy.py +++ b/metaworld/policies/sawyer_peg_insertion_side_v3_policy.py @@ -44,14 +44,21 @@ def _desired_pos(o_d: dict[str, npt.NDArray[np.float64]]) -> npt.NDArray[Any]: # Z is constant at .16 pos_hole = np.array([-0.35, o_d["goal_pos"][1], 0.16]) + gripper_dist = o_d["gripper_distance_apart"] + + # gets rid of a tiny bump + hole_z_offset = np.array([0.0, 0.0, 0.01]) + if np.linalg.norm(pos_curr[:2] - pos_peg[:2]) > 0.04: + # move to pickup position above peg return pos_peg + np.array([0.0, 0.0, 0.3]) - elif abs(pos_curr[2] - pos_peg[2]) > 0.025: + elif abs(pos_curr[2] - pos_peg[2]) > 0.025 and gripper_dist > 0.5: + # move down to peg only if not holding return pos_peg elif np.linalg.norm(pos_peg[1:] - pos_hole[1:]) > 0.03: - return pos_hole + np.array([0.4, 0.0, 0.0]) + return pos_hole + np.array([0.4, 0.0, 0.0]) + hole_z_offset else: - return pos_hole + return pos_hole + hole_z_offset @staticmethod def _grab_effort(o_d: dict[str, npt.NDArray[np.float64]]) -> float: diff --git a/metaworld/sawyer_xyz_env.py b/metaworld/sawyer_xyz_env.py index fd3840b9c..13533085a 100644 --- a/metaworld/sawyer_xyz_env.py +++ b/metaworld/sawyer_xyz_env.py @@ -3,26 +3,26 @@ from __future__ import annotations import copy -import pickle from functools import cached_property -from typing import Any, Callable, Literal, SupportsFloat +from typing import Any, Literal, SupportsFloat +from abc import ABC, abstractmethod import mujoco import numpy as np import numpy.typing as npt from gymnasium.envs.mujoco import MujocoEnv as mjenv_gym -from gymnasium.spaces import Box, Discrete, Space -from gymnasium.utils import seeding +from gymnasium.spaces import Box, Space from gymnasium.utils.ezpickle import EzPickle from typing_extensions import TypeAlias -from metaworld.types import XYZ, EnvironmentStateDict, ObservationDict, Task +from metaworld.types import XYZ, EnvironmentStateDict, ObservationDict from metaworld.utils import reward_utils +from metaworld.utils.numpy import randint RenderMode: TypeAlias = "Literal['human', 'rgb_array', 'depth_array']" -class SawyerMocapBase(mjenv_gym): +class SawyerMocapBase(mjenv_gym, ABC): """Provides some commonly-shared functions for Sawyer Mujoco envs that use mocap for XYZ control.""" mocap_low = np.array([-0.2, 0.5, 0.06]) @@ -40,9 +40,15 @@ class SawyerMocapBase(mjenv_gym): def sawyer_observation_space(self) -> Space: raise NotImplementedError + @property + @abstractmethod + def ENV_NAME(self) -> str: + """The name of the environment.""" + pass + def __init__( self, - model_name: str, + model_path: str, frame_skip: int = 5, render_mode: RenderMode | None = None, camera_name: str | None = None, @@ -52,7 +58,7 @@ def __init__( ) -> None: mjenv_gym.__init__( self, - model_name, + model_path=model_path, frame_skip=frame_skip, observation_space=self.sawyer_observation_space, render_mode=render_mode, @@ -81,7 +87,7 @@ def tcp_center(self) -> npt.NDArray[Any]: return tcp_center @property - def model_name(self) -> str: + def model_path(self) -> str: raise NotImplementedError def get_env_state(self) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]: @@ -113,7 +119,7 @@ def __getstate__(self) -> EnvironmentStateDict: A dictionary containing the env state from the `__dict__` method, the model name (path) and the mocap state `(qpos, qvel)`. """ state = self.__dict__.copy() - return {"state": state, "mjb": self.model_name, "mocap": self.get_env_state()} + return {"state": state, "mjb": self.model_path, "mocap": self.get_env_state()} def __setstate__(self, state: EnvironmentStateDict) -> None: """Sets the state of the environment from a dict exported through `__getstate__()`. @@ -150,76 +156,66 @@ class SawyerXYZEnv(SawyerMocapBase, EzPickle): ) """Bounds for hand position.""" - max_path_length: int = 500 - """The maximum path length for the environment (the task horizon).""" + max_episode_steps: int = 500 + """The maximum steps for the environment (the task horizon) before it truncates.""" TARGET_RADIUS: float = 0.05 """Upper bound for distance from the target when checking for task completion.""" - class _Decorators: - @classmethod - def assert_task_is_set(cls, func: Callable) -> Callable: - """Asserts that the task has been set in the environment before proceeding with the function call. - To be used as a decorator for SawyerXYZEnv methods.""" - - def inner(*args, **kwargs) -> Any: - env = args[0] - if not env._set_task_called: - raise RuntimeError( - "You must call env.set_task before using env." + func.__name__ - ) - return func(*args, **kwargs) - - return inner - def __init__( self, frame_skip: int = 5, + render_mode: RenderMode | None = None, + camera_name: str | None = None, + camera_id: int | None = None, + width: int = 480, + height: int = 480, + reward_function_version: str | None = None, + goal_observable: bool = True, + max_episode_steps: int = 500, hand_low: XYZ = (-0.2, 0.55, 0.05), hand_high: XYZ = (0.2, 0.75, 0.3), mocap_low: XYZ | None = None, mocap_high: XYZ | None = None, action_scale: float = 1.0 / 100, action_rot_scale: float = 1.0, - render_mode: RenderMode | None = None, - camera_id: int | None = None, - camera_name: str | None = None, - reward_function_version: str | None = None, - width: int = 480, - height: int = 480, ) -> None: - self.action_scale = action_scale - self.action_rot_scale = action_rot_scale + self.hand_low = np.array(hand_low) self.hand_high = np.array(hand_high) + if mocap_low is None: mocap_low = hand_low if mocap_high is None: mocap_high = hand_high self.mocap_low = np.hstack(mocap_low) self.mocap_high = np.hstack(mocap_high) - self.curr_path_length: int = 0 - self.seeded_rand_vec: bool = False - self._freeze_rand_vec: bool = True - self._last_rand_vec: npt.NDArray[Any] | None = None - self.num_resets: int = 0 - self.current_seed: int | None = None - self.obj_init_pos: npt.NDArray[Any] | None = None - self.width = width - self.height = height + self.action_scale = action_scale + self.action_rot_scale = action_rot_scale + + if reward_function_version is None: + reward_function_version = "v2" + if reward_function_version not in ["v1", "v2"]: + raise ValueError( + f"reward_function_version must be either None, 'v1' or 'v2', got {reward_function_version} instead." + ) + self.reward_function_version = reward_function_version + + self._goal_observable = goal_observable - # TODO Probably needs to be removed - self.discrete_goal_space: Box | None = None - self.discrete_goals: list = [] - self.active_discrete_goal: int | None = None + self.current_step: int = 0 + self.max_episode_steps = max_episode_steps - self._partially_observable: bool = True + self.current_seed = int(randint(np.random.default_rng())) - self.task_name = self.__class__.__name__ + self.obj_init_pos: npt.NDArray[Any] | None # OVERRIDE ME + + self.width = width + self.height = height super().__init__( - self.model_name, + model_path=self.model_path, frame_skip=frame_skip, render_mode=render_mode, camera_name=camera_name, @@ -232,22 +228,21 @@ def __init__( self.model, self.data ) # *** DO NOT REMOVE: EZPICKLE WON'T WORK *** # - self._did_see_sim_exception: bool = False - self.init_left_pad: npt.NDArray[Any] = self.get_body_com("leftpad") - self.init_right_pad: npt.NDArray[Any] = self.get_body_com("rightpad") + self.init_left_pad: npt.NDArray[Any] = self.get_body_com( + "leftpad").copy() + self.init_right_pad: npt.NDArray[Any] = self.get_body_com( + "rightpad").copy() self.action_space = Box( # type: ignore np.array([-1, -1, -1, -1]), np.array([+1, +1, +1, +1]), dtype=np.float32, ) - self._obs_obj_max_len: int = 14 - self._set_task_called: bool = False - self.hand_init_pos: npt.NDArray[Any] | None = None # OVERRIDE ME - self._target_pos: npt.NDArray[Any] | None = None # OVERRIDE ME - self._random_reset_space: Box | None = None # OVERRIDE ME - self.goal_space: Box | None = None # OVERRIDE ME - self._last_stable_obs: npt.NDArray[np.float64] | None = None + + self.hand_init_pos: npt.NDArray[Any] | None # OVERRIDE ME + self._target_pos: npt.NDArray[Any] | None # OVERRIDE ME + self._random_reset_space: Box | None # OVERRIDE ME + self.goal_space: Box | None # OVERRIDE ME # Note: It is unlikely that the positions and orientations stored # in this initiation of _prev_obs are correct. That being said, it @@ -258,11 +253,8 @@ def __init__( self.init_qvel = np.copy(self.data.qvel) self._prev_obs = self._get_curr_obs_combined_no_goal() - self.task_name = self.__class__.__name__ - EzPickle.__init__( self, - self.model_name, frame_skip, hand_low, hand_high, @@ -270,52 +262,26 @@ def __init__( mocap_high, action_scale, action_rot_scale, + reward_function_version, + goal_observable, + max_episode_steps, ) - def seed(self, seed: int) -> list[int]: + def _seed(self, seed: int): """Seeds the environment. Args: seed: The seed to use. - Returns: - The seed used inside a 1 element list. """ assert seed is not None - self.np_random, seed = seeding.np_random(seed) + # Reset the gymnasium rng + super().reset(seed=seed) + + # Reset other spaces self.action_space.seed(seed) self.observation_space.seed(seed) - assert self.goal_space self.goal_space.seed(seed) - return [seed] - - @staticmethod - def _set_task_inner() -> None: - """Helper method to set additional task data. To be overridden by subclasses as appropriate.""" - # Doesn't absorb "extra" kwargs, to ensure nothing's missed. - pass - - def set_task(self, task: Task) -> None: - """Sets the environment's task. - - Args: - task: The task to set. - """ - self._set_task_called = True - data = pickle.loads(task.data) - assert isinstance(self, data["env_cls"]) - del data["env_cls"] - self._freeze_rand_vec = True - self._last_rand_vec = data["rand_vec"] - del data["rand_vec"] - new_observability = data["partially_observable"] - if new_observability != self._partially_observable: - # Force recomputation of the observation space - # See https://docs.python.org/3/library/functools.html#functools.cached_property - del self.sawyer_observation_space - self._partially_observable = new_observability - del data["partially_observable"] - self._set_task_inner(**data) def set_xyz_action(self, action: npt.NDArray[Any]) -> None: """Adjusts the position of the mocap body from the given action. @@ -335,19 +301,6 @@ def set_xyz_action(self, action: npt.NDArray[Any]) -> None: self.data.mocap_pos = new_mocap_pos self.data.mocap_quat = np.array([1, 0, 1, 0]) - def discretize_goal_space(self, goals: list) -> None: - """Discretizes the goal space into a Discrete space. - Current disabled and callign it will stop execution. - - Args: - goals: List of goals to discretize - """ - assert False, "Discretization is not supported at the moment." - assert len(goals) >= 1 - self.discrete_goals = goals - # update the goal_space to a Discrete space - self.discrete_goal_space = Discrete(len(self.discrete_goals)) - def _set_obj_xyz(self, pos: npt.NDArray[Any]) -> None: """Sets the position of the object. @@ -494,10 +447,13 @@ def _get_curr_obs_combined_no_goal(self) -> npt.NDArray[np.float64]: # clipping removes the effects of this random extra distance # that is produced by mujoco - gripper_distance_apart = np.linalg.norm(finger_right.xpos - finger_left.xpos) - gripper_distance_apart = np.clip(gripper_distance_apart / 0.1, 0.0, 1.0) + gripper_distance_apart = np.linalg.norm( + finger_right.xpos - finger_left.xpos) + gripper_distance_apart = np.clip( + gripper_distance_apart / 0.1, 0.0, 1.0) - obs_obj_padded = np.zeros(self._obs_obj_max_len) + obs_obj_max_len: int = 14 + obs_obj_padded = np.zeros(obs_obj_max_len) obj_pos = self._get_pos_objects() assert len(obj_pos) % 3 == 0 obj_pos_split = np.split(obj_pos, len(obj_pos) // 3) @@ -506,7 +462,8 @@ def _get_curr_obs_combined_no_goal(self) -> npt.NDArray[np.float64]: assert len(obj_quat) % 4 == 0 obj_quat_split = np.split(obj_quat, len(obj_quat) // 4) obs_obj_padded[: len(obj_pos) + len(obj_quat)] = np.hstack( - [np.hstack((pos, quat)) for pos, quat in zip(obj_pos_split, obj_quat_split)] + [np.hstack((pos, quat)) + for pos, quat in zip(obj_pos_split, obj_quat_split)] ) return np.hstack((pos_hand, gripper_distance_apart, obs_obj_padded)) @@ -518,7 +475,7 @@ def _get_obs(self) -> npt.NDArray[np.float64]: """ # do frame stacking pos_goal = self._get_pos_goal() - if self._partially_observable: + if not self._goal_observable: pos_goal = np.zeros_like(pos_goal) curr_obs = self._get_curr_obs_combined_no_goal() # do frame stacking @@ -539,7 +496,7 @@ def sawyer_observation_space(self) -> Box: obs_obj_max_len = 14 obj_low = np.full(obs_obj_max_len, -np.inf, dtype=np.float64) obj_high = np.full(obs_obj_max_len, +np.inf, dtype=np.float64) - if self._partially_observable: + if not self._goal_observable: goal_low = np.zeros(3) goal_high = np.zeros(3) else: @@ -576,7 +533,6 @@ def sawyer_observation_space(self) -> Box: dtype=np.float64, ) - @_Decorators.assert_task_is_set def step( self, action: npt.NDArray[np.float32] ) -> tuple[npt.NDArray[np.float64], SupportsFloat, bool, bool, dict[str, Any]]: @@ -590,51 +546,36 @@ def step( """ assert len(action) == 4, f"Actions should be size 4, got {len(action)}" self.set_xyz_action(action[:3]) - if self.curr_path_length >= self.max_path_length: - raise ValueError("You must reset the env manually once truncate==True") + if self.current_step >= self.max_episode_steps: + raise ValueError( + "You must reset the env manually once truncate==True") self.do_simulation([action[-1], -action[-1]], n_frames=self.frame_skip) - self.curr_path_length += 1 + self.current_step += 1 # Running the simulator can sometimes mess up site positions, so # re-position them here to make sure they're accurate for site in self._target_site_config: self._set_pos_site(*site) - if self._did_see_sim_exception: - assert self._last_stable_obs is not None - return ( - self._last_stable_obs, # observation just before going unstable - 0.0, # reward (penalize for causing instability) - False, - False, # termination flag always False - { # info - "success": False, - "near_object": 0.0, - "grasp_success": False, - "grasp_reward": 0.0, - "in_place_reward": 0.0, - "obj_to_target": 0.0, - "unscaled_reward": 0.0, - }, - ) mujoco.mj_forward(self.model, self.data) - self._last_stable_obs = self._get_obs() + obs = self._get_obs() - self._last_stable_obs = np.clip( - self._last_stable_obs, + obs = np.clip( + obs, a_max=self.sawyer_observation_space.high, a_min=self.sawyer_observation_space.low, dtype=np.float64, ) - assert isinstance(self._last_stable_obs, np.ndarray) - reward, info = self.evaluate_state(self._last_stable_obs, action) + assert isinstance(obs, np.ndarray) + reward, info = self.evaluate_state(obs, action) + # step will never return a terminate==True if there is a success # but we can return truncate=True if the current path length == max path length truncate = False - if self.curr_path_length == self.max_path_length: + if self.current_step == self.max_episode_steps: truncate = True return ( - np.array(self._last_stable_obs, dtype=np.float64), + np.array(obs, dtype=np.float64), reward, False, truncate, @@ -667,18 +608,30 @@ def reset( """Resets the environment. Args: - seed: The seed to use. Ignored, use `seed()` instead. - options: Additional options to pass to the environment. Ignored. + seed: The seed to use. + options: Additional options to pass to the environment. Returns: The `(obs, info)` tuple. """ - self.curr_path_length = 0 - self.reset_model() - obs, info = super().reset() + self.current_step = 0 + + if seed is not None: + self.current_seed = int(seed) + + self._seed(self.current_seed) + + obs, info = super().reset(seed=self.current_seed, options=options) self._prev_obs = obs[:18].copy() obs[18:36] = self._prev_obs obs = obs.astype(np.float64) + + info.update({ + "seed": self.current_seed, + "env_name": self.ENV_NAME, + "goal_observable": self._goal_observable, + }) + return obs, info def _reset_hand(self, steps: int = 50) -> None: @@ -695,28 +648,12 @@ def _reset_hand(self, steps: int = 50) -> None: self.init_tcp = self.tcp_center def _get_state_rand_vec(self) -> npt.NDArray[np.float64]: - """Gets or generates a random vector for the hand position at reset.""" - if self._freeze_rand_vec: - assert self._last_rand_vec is not None - return self._last_rand_vec - elif self.seeded_rand_vec: - assert self._random_reset_space is not None - rand_vec = self.np_random.uniform( - self._random_reset_space.low, - self._random_reset_space.high, - size=self._random_reset_space.low.size, - ) - self._last_rand_vec = rand_vec - return rand_vec - else: - assert self._random_reset_space is not None - rand_vec: npt.NDArray[np.float64] = np.random.uniform( # type: ignore - self._random_reset_space.low, - self._random_reset_space.high, - size=self._random_reset_space.low.size, - ).astype(np.float64) - self._last_rand_vec = rand_vec - return rand_vec + """Generates a new random vector for the hand position at reset.""" + return self.np_random.uniform( + self._random_reset_space.low, + self._random_reset_space.high, + size=self._random_reset_space.low.size, + ) def _gripper_caging_reward( self, @@ -756,7 +693,8 @@ def _gripper_caging_reward( ), "`obj_init_pos` must be initialized before calling this function." if high_density and medium_density: - raise ValueError("Can only be either high_density or medium_density") + raise ValueError( + "Can only be either high_density or medium_density") # MARK: Left-right gripper information for caging reward---------------- left_pad = self.get_body_com("leftpad") right_pad = self.get_body_com("rightpad") @@ -803,7 +741,8 @@ def _gripper_caging_reward( reward_utils.tolerance( pad_to_obj_lr[i], # "x" in the description above bounds=(obj_radius, pad_success_thresh), - margin=caging_lr_margin[i], # "margin" in the description above + # "margin" in the description above + margin=caging_lr_margin[i], sigmoid="long_tail", ) for i in range(2) @@ -818,10 +757,12 @@ def _gripper_caging_reward( # constant (something in the 0.3 to 0.5 range) and x shrinks as the # gripper moves towards the object. After picking up the object, the # reward is maximized and changes very little - caging_xz_margin = np.linalg.norm(self.obj_init_pos[xz] - self.init_tcp[xz]) + caging_xz_margin = np.linalg.norm( + self.obj_init_pos[xz] - self.init_tcp[xz]) caging_xz_margin -= xz_thresh caging_xz = reward_utils.tolerance( - np.linalg.norm(tcp[xz] - obj_pos[xz]), # "x" in the description above + # "x" in the description above + np.linalg.norm(tcp[xz] - obj_pos[xz]), bounds=(0, xz_thresh), margin=caging_xz_margin, # "margin" in the description above sigmoid="long_tail", @@ -829,7 +770,8 @@ def _gripper_caging_reward( # MARK: Closed-extent gripper information for caging reward------------- gripper_closed = ( - min(max(0, action[-1]), desired_gripper_effort) / desired_gripper_effort + min(max(0, action[-1]), desired_gripper_effort) / + desired_gripper_effort ) # MARK: Combine components---------------------------------------------- diff --git a/metaworld/types.py b/metaworld/types.py index 638d36690..4ef9405aa 100644 --- a/metaworld/types.py +++ b/metaworld/types.py @@ -1,22 +1,12 @@ from __future__ import annotations -from typing import Any, NamedTuple, Tuple +from typing import Any, Tuple import numpy as np import numpy.typing as npt from typing_extensions import NotRequired, TypeAlias, TypedDict -class Task(NamedTuple): - """All data necessary to describe a single MDP. - - Should be passed into a `MetaWorldEnv`'s `set_task` method. - """ - - env_name: str - data: bytes # Contains env parameters like random_init and *a* goal - - XYZ: TypeAlias = "Tuple[float, float, float]" """A 3D coordinate.""" diff --git a/metaworld/utils/numpy.py b/metaworld/utils/numpy.py new file mode 100644 index 000000000..af6753e34 --- /dev/null +++ b/metaworld/utils/numpy.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +import numpy as np + + +def randint(rng: np.random.Generator, size=None) -> int | np.ndarray: + """Returns a random integer from [0, 2**32 - 1] using the provided RNG.""" + return rng.integers(0, 2**32, size=size, dtype=np.uint32) diff --git a/metaworld/wrappers.py b/metaworld/wrappers.py index f7e0ab350..c7f77bafa 100644 --- a/metaworld/wrappers.py +++ b/metaworld/wrappers.py @@ -1,6 +1,7 @@ from __future__ import annotations import base64 +from dataclasses import asdict import gymnasium as gym import numpy as np @@ -8,24 +9,26 @@ from numpy.typing import NDArray from metaworld.sawyer_xyz_env import SawyerXYZEnv -from metaworld.types import Task +from metaworld.benchmark import Task +from metaworld.utils.numpy import randint class OneHotWrapper(gym.ObservationWrapper, gym.utils.RecordConstructorArgs): - def __init__(self, env: Env, task_idx: int, num_tasks: int): + def __init__(self, env: Env, env_id: int, num_env_ids: int): gym.utils.RecordConstructorArgs.__init__(self) gym.ObservationWrapper.__init__(self, env) assert isinstance(env.observation_space, gym.spaces.Box) env_lb = env.observation_space.low env_ub = env.observation_space.high - one_hot_ub = np.ones(num_tasks) - one_hot_lb = np.zeros(num_tasks) + one_hot_ub = np.ones(num_env_ids, dtype=np.float32) + one_hot_lb = np.zeros(num_env_ids, dtype=np.float32) - self.one_hot = np.zeros(num_tasks) - self.one_hot[task_idx] = 1.0 + self.one_hot = np.zeros(num_env_ids) + self.one_hot[env_id] = 1.0 self._observation_space = gym.spaces.Box( - np.concatenate([env_lb, one_hot_lb]), np.concatenate([env_ub, one_hot_ub]) + np.concatenate([env_lb, one_hot_lb], dtype=np.float32), np.concatenate( + [env_ub, one_hot_ub], dtype=np.float32) ) def observation(self, obs: NDArray) -> NDArray: @@ -43,7 +46,8 @@ def _deserialize_task(task_dict: dict[str, str]) -> Task: assert "env_name" in task_dict and "data" in task_dict return Task( - env_name=task_dict["env_name"], data=base64.b64decode(task_dict["data"]) + env_name=task_dict["env_name"], data=base64.b64decode( + task_dict["data"]) ) @@ -89,77 +93,87 @@ def reset(self, *, seed: int | None = None, options: dict | None = None): class RandomTaskSelectWrapper(gym.Wrapper): - """A Gymnasium Wrapper to automatically set / reset the environment to a random - task.""" + """ + A Gymnasium Wrapper to automatically sample a new random task from the provided list of tasks. + It might yield collisions (i.e., the same task might be sampled multiple times in a row or multiple times + before all tasks have been sampled). + """ tasks: list[Task] - sample_tasks_on_reset: bool = True + sample_tasks_on_reset: bool + forked_rng: np.random.Generator def _set_random_task(self): - task_idx = self.np_random.choice(len(self.tasks)) - self.unwrapped.set_task(self.tasks[task_idx]) + task_idx = self.forked_rng.choice(len(self.tasks)) + self.unwrapped.reset(seed=self.tasks[task_idx].env_seed) def __init__( self, env: Env, tasks: list[Task], - sample_tasks_on_reset: bool = True, + sample_tasks_on_reset: bool, ): super().__init__(env) self.unwrapped: SawyerXYZEnv self.tasks = tasks self.sample_tasks_on_reset = sample_tasks_on_reset + # Fork off a new RNG so that task sampling is independent from env RNG + # The env RNG gets seeded on env reset! + self.forked_rng = np.random.default_rng(randint(self.np_random) + 42) + def toggle_sample_tasks_on_reset(self, on: bool): self.sample_tasks_on_reset = on def reset(self, *, seed: int | None = None, options: dict | None = None): + if seed is not None: + raise NotImplementedError( + "Seeding is not supported when using RandomTaskSelectWrapper." + ) if self.sample_tasks_on_reset: self._set_random_task() - return self.env.reset(seed=seed, options=options) + return self.env.reset(seed=None, options=options) - def sample_tasks(self, *, seed: int | None = None, options: dict | None = None): + def sample_tasks(self): self._set_random_task() - return self.env.reset(seed=seed, options=options) + return self.env.reset(seed=None) def get_checkpoint(self) -> dict: return { - "tasks": [_serialize_task(task) for task in self.tasks], - "rng_state": self.np_random.bit_generator.state, + "tasks": [asdict(task) for task in self.tasks], "sample_tasks_on_reset": self.sample_tasks_on_reset, - "env_rng_state": get_env_rng_checkpoint(self.unwrapped), + "forked_rng": self.forked_rng.bit_generator.state, } def load_checkpoint(self, ckpt: dict): assert "tasks" in ckpt - assert "rng_state" in ckpt assert "sample_tasks_on_reset" in ckpt - assert "env_rng_state" in ckpt + assert "forked_rng" in ckpt - self.tasks = [_deserialize_task(task) for task in ckpt["tasks"]] - self.np_random.__setstate__(ckpt["rng_state"]) + self.tasks = [Task(**task) for task in ckpt["tasks"]] self.sample_tasks_on_reset = ckpt["sample_tasks_on_reset"] - set_env_rng(self.unwrapped, ckpt["env_rng_state"]) + self.forked_rng.bit_generator.state = ckpt["forked_rng"] class PseudoRandomTaskSelectWrapper(gym.Wrapper): - """A Gymnasium Wrapper to automatically reset the environment to a *pseudo*random task when explicitly called. + """ + A Gymnasium Wrapper to automatically reset the environment to a *pseudo*random task. Pseudorandom implies no collisions therefore the next task in the list will be used cyclically. However, the tasks will be shuffled every time the last task of the previous shuffle is reached. - - Doesn't sample new tasks on reset by default. """ tasks: list[Task] current_task_idx: int - sample_tasks_on_reset: bool = False + sample_tasks_on_reset: bool + forked_rng: np.random.Generator def _set_pseudo_random_task(self): self.current_task_idx = (self.current_task_idx + 1) % len(self.tasks) if self.current_task_idx == 0: - self.np_random.shuffle(self.tasks) # pyright: ignore [reportArgumentType] - self.unwrapped.set_task(self.tasks[self.current_task_idx]) + # pyright: ignore [reportArgumentType] + self.forked_rng.shuffle(self.tasks) + self.unwrapped.reset(seed=self.tasks[self.current_task_idx].env_seed) def toggle_sample_tasks_on_reset(self, on: bool): self.sample_tasks_on_reset = on @@ -168,40 +182,49 @@ def __init__( self, env: Env, tasks: list[Task], - sample_tasks_on_reset: bool = False, + sample_tasks_on_reset: bool, ): super().__init__(env) self.sample_tasks_on_reset = sample_tasks_on_reset self.tasks = tasks self.current_task_idx = -1 + # Fork off a new RNG so that task sampling is independent from env RNG + # The env RNG gets seeded on env reset! + self.forked_rng = np.random.default_rng(randint(self.np_random) + 42) + self.forked_rng.shuffle(self.tasks) + def reset(self, *, seed: int | None = None, options: dict | None = None): + if seed is not None: + raise NotImplementedError( + "Seeding is not supported when using PseudoRandomTaskSelectWrapper." + ) if self.sample_tasks_on_reset: self._set_pseudo_random_task() - return self.env.reset(seed=seed, options=options) + return self.env.reset(seed=None, options=options) - def sample_tasks(self, *, seed: int | None = None, options: dict | None = None): + def sample_tasks(self): self._set_pseudo_random_task() - return self.env.reset(seed=seed, options=options) + return self.env.reset(seed=None) def get_checkpoint(self) -> dict: return { - "tasks": [_serialize_task(task) for task in self.tasks], - "current_task_idx": self.current_task_idx, + "tasks": [asdict(task) for task in self.tasks], "sample_tasks_on_reset": self.sample_tasks_on_reset, - "env_rng_state": get_env_rng_checkpoint(self.unwrapped), + "current_task_idx": self.current_task_idx, + "forked_rng": self.forked_rng.bit_generator.state, } def load_checkpoint(self, ckpt: dict): assert "tasks" in ckpt - assert "current_task_idx" in ckpt assert "sample_tasks_on_reset" in ckpt - assert "env_rng_state" in ckpt + assert "current_task_idx" in ckpt + assert "forked_rng" in ckpt - self.tasks = [_deserialize_task(task) for task in ckpt["tasks"]] - self.current_task_idx = ckpt["current_task_idx"] + self.tasks = [Task(**task) for task in ckpt["tasks"]] self.sample_tasks_on_reset = ckpt["sample_tasks_on_reset"] - set_env_rng(self.unwrapped, ckpt["env_rng_state"]) + self.current_task_idx = ckpt["current_task_idx"] + self.forked_rng.bit_generator.state = ckpt["forked_rng"] class AutoTerminateOnSuccessWrapper(gym.Wrapper): @@ -273,11 +296,16 @@ def update_mean_var_count_from_moments( class CheckpointWrapper(gym.Wrapper): + """ + A Gymnasium Wrapper to enable checkpointing of environments within a larger multi-environment setup. + Checkpointing is only supported between episodes (i.e., after reset()). + """ env_id: str def __init__(self, env: gym.Env, env_id: str): super().__init__(env) - assert hasattr(self.env, "get_checkpoint") and callable(self.env.get_checkpoint) + assert hasattr(self.env, "get_checkpoint") and callable( + self.env.get_checkpoint) assert hasattr(self.env, "load_checkpoint") and callable( self.env.load_checkpoint ) @@ -299,24 +327,3 @@ def load_checkpoint(self, ckpts: list[tuple[str, dict]]) -> None: [env_id for env_id, _ in ckpts], ) self.env.load_checkpoint(my_ckpt) - - -def get_env_rng_checkpoint(env: SawyerXYZEnv) -> dict[str, dict]: - return { # pyright: ignore [reportReturnType] - "np_random_state": env.np_random.bit_generator.state, - "action_space_rng_state": env.action_space.np_random.bit_generator.state, - "obs_space_rng_state": env.observation_space.np_random.bit_generator.state, - "goal_space_rng_state": env.goal_space.np_random.bit_generator.state, # type: ignore - } - - -def set_env_rng(env: SawyerXYZEnv, state: dict[str, dict]) -> None: - assert "np_random_state" in state - assert "action_space_rng_state" in state - assert "obs_space_rng_state" in state - assert "goal_space_rng_state" in state - - env.np_random.bit_generator.state = state["np_random_state"] - env.action_space.np_random.bit_generator.state = state["action_space_rng_state"] - env.observation_space.np_random.bit_generator.state = state["obs_space_rng_state"] - env.goal_space.np_random.bit_generator.state = state["goal_space_rng_state"] # type: ignore diff --git a/pyproject.toml b/pyproject.toml index 7ecf513aa..ee9056dfc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ authors = [{ name = "Farama Foundation", email = "contact@farama.org" }] license = { text = "MIT License" } keywords = ["Reinforcement Learning", "game", "RL", "AI", "gymnasium"] classifiers = [ - "Development Status :: 4 - Beta", # change to `5 - Production/Stable` when ready + "Development Status :: 4 - Beta", # change to `5 - Production/Stable` when ready "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", @@ -28,13 +28,19 @@ dependencies = [ "mujoco>=3.0.0", "numpy>=1.18", "scipy>=1.4.1", - "imageio" + "imageio", ] -[project.optional-dependencies] -# Update dependencies in `all` if any are added or removed -testing = ["ipdb", "memory_profiler", "pyquaternion==0.9.5", "pytest>=4.4.0"] +[dependency-groups] dev = ["black", "isort", "mypy"] +coverage = ["pytest-cov"] +testing = [ + "ipdb", + "memory_profiler", + "pyquaternion==0.9.5", + "pytest>=4.4.0", + "pytest-xdist", +] [project.urls] Homepage = "https://farama.org" @@ -67,3 +73,7 @@ exclude = ["docs"] [[tool.mypy.overrides]] module = ["setuptools", "glfw", "mujoco", "memory_profiler", "scipy.*"] ignore_missing_imports = true + +[tool.uv] +# By default, install all dependency groups +default-groups = "all" diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..8ab80e5d0 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = -n auto diff --git a/scripts/env_runtime.py b/scripts/env_runtime.py index c5a76d1c6..b9a3e3bfd 100644 --- a/scripts/env_runtime.py +++ b/scripts/env_runtime.py @@ -27,7 +27,7 @@ class RandomTaskSelectWrapper(gymnasium.Wrapper): def _set_random_task(self): task_idx = self.np_random.choice(len(self.tasks)) - self.unwrapped.set_task(self.tasks[task_idx]) + self.unwrapped.reset(seed=self.tasks[task_idx].env_seed) def __init__( self, @@ -64,7 +64,8 @@ def _make_env_internal( tasks = [ task for task in benchmark.train_tasks if task.env_name == env_cls_name ] - env = gymnasium.wrappers.TimeLimit(env, env.max_path_length) # type: ignore + env = gymnasium.wrappers.TimeLimit( + env, env.max_episode_steps) # type: ignore env = gymnasium.wrappers.RecordEpisodeStatistics(env) # type: ignore env = RandomTaskSelectWrapper(env, tasks) # type: ignore return env @@ -92,7 +93,7 @@ def main() -> None: steps += 1 current = time.time() print( - f"Progress: {(current - start) / BENCH_SECONDS * 100 : .2f}%, SPS: {int(steps / (current - start))}", + f"Progress: {(current - start) / BENCH_SECONDS * 100: .2f}%, SPS: {int(steps / (current - start))}", end="\r", ) if current - start > BENCH_SECONDS: diff --git a/scripts/keyboard_control.py b/scripts/keyboard_control.py index c943561b7..c9ad933fb 100644 --- a/scripts/keyboard_control.py +++ b/scripts/keyboard_control.py @@ -36,11 +36,8 @@ env = SawyerPickPlaceEnvV3() -env._partially_observable = False -env._freeze_rand_vec = False -env._set_task_called = True +env._goal_observable = False env.reset() -env._freeze_rand_vec = True lock_action = False random_action = False obs = env.reset() diff --git a/scripts/plot_rewards_returns.ipynb b/scripts/plot_rewards_returns.ipynb index 0546dc8a6..1ee33fab6 100644 --- a/scripts/plot_rewards_returns.ipynb +++ b/scripts/plot_rewards_returns.ipynb @@ -1,164 +1,171 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "import os\n", + "import functools\n", + "\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "import seaborn as sns\n", + "\n", + "import gymnasium as gym\n", + "\n", + "from tests.metaworld.envs.mujoco.sawyer_xyz.utils import trajectory_summary\n", + "from tests.metaworld.envs.mujoco.sawyer_xyz.test_scripted_policies import ALL_ENVS, test_cases_latest_nonoise\n", + "\n", + "sns.set()\n", + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "def sample_trajectories_from(env, policy, act_noise_pct, iters=100):\n", + " sampled_rewards = []\n", + " sampled_returns = []\n", + " sampled_first_successes = []\n", + "\n", + " for _ in range(iters):\n", + " s = trajectory_summary(\n", + " env, policy, act_noise_pct, end_on_success=False)\n", + " sampled_rewards.append(s[1])\n", + " sampled_returns.append(s[2])\n", + " sampled_first_successes.append(s[3])\n", + "\n", + " sampled_rewards = np.vstack(sampled_rewards)\n", + " sampled_returns = np.vstack(sampled_returns)\n", + " sampled_first_successes = np.array(sampled_first_successes)\n", + "\n", + " return sampled_rewards, sampled_returns, sampled_first_successes\n", + "\n", + "\n", + "def plot(rewards, returns, first_successes, tag):\n", + " first_success = min(int(first_successes.mean()), rewards.shape[1])\n", + " first_success_rew = rewards.mean(axis=0)[first_success]\n", + " first_success_ret = returns.mean(axis=0)[first_success]\n", + "\n", + " fig, ax = plt.subplots(1, 2, figsize=(6.75, 4))\n", + "\n", + " reward_df = pd.DataFrame(rewards).melt()\n", + " ax[0] = sns.lineplot(x='variable', y='value',\n", + " data=reward_df, ax=ax[0], ci=95, lw=.5)\n", + " ax[0].set_xlabel('Time Steps')\n", + " ax[0].set_ylabel('Reward')\n", + " ax[0].set_title('Rewards')\n", + " ax[0].vlines(first_success, ymin=0, ymax=first_success_rew,\n", + " linestyle='--', color='green')\n", + " ax[0].hlines(first_success_rew, xmin=0, xmax=first_success,\n", + " linestyle='--', color='green')\n", + "# ax[0].set_yscale('symlog')\n", + "\n", + " return_df = pd.DataFrame(returns).melt()\n", + " ax[1] = sns.lineplot(x='variable', y='value',\n", + " data=return_df, ax=ax[1], ci=95, lw=.5)\n", + " ax[1].set_xlabel('Time Steps')\n", + " ax[1].set_ylabel('Return')\n", + " ax[1].set_title('Returns')\n", + " ax[1].vlines(first_success, ymin=0, ymax=first_success_ret,\n", + " linestyle='--', color='green')\n", + " ax[1].hlines(first_success_ret, xmin=0, xmax=first_success,\n", + " linestyle='--', color='green')\n", + "# ax[1].set_yscale('symlog')\n", + "\n", + " plt.subplots_adjust(top=.85)\n", + " fig.suptitle(f'{tag} (n={rewards.shape[0]})')\n", + " fig.tight_layout(rect=[0, 0.03, 1, 0.95])\n", + "\n", + " plt.show()\n", + " if not os.path.exists('figures'):\n", + " os.mkdir('figures')\n", + " fig.savefig(f'figures/{tag}_rewards_returns.jpg')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "%reload_ext autoreload\n", + "config = [\n", + " # env name, action noise, path length\n", + " ['pick-place-V3', np.zeros(4), 200],\n", + "]\n", + "\n", + "for env, noise, max_episode_steps in config:\n", + " tag = env + '-noise-' + np.array2string(noise, precision=2, separator=',', suppress_small=True)\n", + "\n", + " policy = functools.reduce(lambda a,b : a if a[0] == env else b, test_cases_latest_nonoise)[1]\n", + " env = ALL_ENVS[env](goal_observable=True, max_episode_steps=max_episode_steps)\n", + "\n", + " sampled_rewards, sampled_returns, sampled_first_successes = sample_trajectories_from(env, policy, noise)\n", + " plot(sampled_rewards, sampled_returns, sampled_first_successes, tag)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "i = 12\n", + "first_success = sampled_first_successes[i]\n", + "first_success_reward = sampled_rewards[i][first_success]\n", + "fig, ax = plt.subplots(1, 1, figsize=(6.75, 4))\n", + "ax.plot(np.arange(len(sampled_rewards[0])), sampled_rewards[i])\n", + "\n", + "ax.vlines(first_success, ymin=0, ymax=first_success_reward,\n", + " linestyle='--', color='green')\n", + "ax.hlines(first_success_reward, xmin=0, xmax=first_success,\n", + " linestyle='--', color='green')" + ] } - }, - "outputs": [], - "source": [ - "import os\n", - "import functools\n", - "\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "import pandas as pd\n", - "import seaborn as sns\n", - "\n", - "from tests.metaworld.envs.mujoco.sawyer_xyz.utils import trajectory_summary\n", - "from tests.metaworld.envs.mujoco.sawyer_xyz.test_scripted_policies import ALL_ENVS, test_cases_latest_nonoise\n", - "\n", - "sns.set()\n", - "%load_ext autoreload\n", - "%autoreload 2" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def sample_trajectories_from(env, policy, act_noise_pct, iters=100):\n", - " sampled_rewards = []\n", - " sampled_returns = []\n", - " sampled_first_successes = []\n", - "\n", - " for _ in range(iters):\n", - " s = trajectory_summary(env, policy, act_noise_pct, end_on_success=False)\n", - " sampled_rewards.append(s[1])\n", - " sampled_returns.append(s[2])\n", - " sampled_first_successes.append(s[3])\n", - "\n", - " sampled_rewards = np.vstack(sampled_rewards)\n", - " sampled_returns = np.vstack(sampled_returns)\n", - " sampled_first_successes = np.array(sampled_first_successes)\n", - "\n", - " return sampled_rewards, sampled_returns, sampled_first_successes\n", - "\n", - "\n", - "def plot(rewards, returns, first_successes, tag):\n", - " first_success = min(int(first_successes.mean()), rewards.shape[1])\n", - " first_success_rew = rewards.mean(axis=0)[first_success]\n", - " first_success_ret = returns.mean(axis=0)[first_success]\n", - " \n", - " fig, ax = plt.subplots(1, 2, figsize=(6.75, 4))\n", - "\n", - " reward_df = pd.DataFrame(rewards).melt()\n", - " ax[0] = sns.lineplot(x='variable', y='value', data=reward_df, ax=ax[0], ci=95, lw=.5)\n", - " ax[0].set_xlabel('Time Steps')\n", - " ax[0].set_ylabel('Reward')\n", - " ax[0].set_title('Rewards')\n", - " ax[0].vlines(first_success, ymin=0, ymax=first_success_rew, linestyle='--', color='green')\n", - " ax[0].hlines(first_success_rew, xmin=0, xmax=first_success, linestyle='--', color='green')\n", - "# ax[0].set_yscale('symlog')\n", - "\n", - " return_df = pd.DataFrame(returns).melt()\n", - " ax[1] = sns.lineplot(x='variable', y='value', data=return_df, ax=ax[1], ci=95, lw=.5)\n", - " ax[1].set_xlabel('Time Steps')\n", - " ax[1].set_ylabel('Return')\n", - " ax[1].set_title('Returns')\n", - " ax[1].vlines(first_success, ymin=0, ymax=first_success_ret, linestyle='--', color='green')\n", - " ax[1].hlines(first_success_ret, xmin=0, xmax=first_success, linestyle='--', color='green')\n", - "# ax[1].set_yscale('symlog')\n", - "\n", - " plt.subplots_adjust(top=.85)\n", - " fig.suptitle(f'{tag} (n={rewards.shape[0]})')\n", - " fig.tight_layout(rect=[0, 0.03, 1, 0.95])\n", - "\n", - " plt.show()\n", - " if not os.path.exists('figures'):\n", - " os.mkdir('figures')\n", - " fig.savefig(f'figures/{tag}_rewards_returns.jpg')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "%reload_ext autoreload\n", - "config = [\n", - " # env name, action noise, path length\n", - " ['pick-place-V3', np.zeros(4), 200],\n", - "]\n", - "\n", - "for env, noise, path_length in config:\n", - " tag = env + '-noise-' + np.array2string(noise, precision=2, separator=',', suppress_small=True)\n", - "\n", - " policy = functools.reduce(lambda a,b : a if a[0] == env else b, test_cases_latest_nonoise)[1]\n", - " env = ALL_ENVS[env]()\n", - " env.max_path_length = path_length\n", - " env._partially_observable = False\n", - " env._freeze_rand_vec = False\n", - " env._set_task_called = True\n", - "\n", - " sampled_rewards, sampled_returns, sampled_first_successes = sample_trajectories_from(env, policy, noise)\n", - " plot(sampled_rewards, sampled_returns, sampled_first_successes, tag)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.9" } - }, - "outputs": [], - "source": [ - "i = 12\n", - "first_success = sampled_first_successes[i]\n", - "first_success_reward = sampled_rewards[i][first_success]\n", - "fig, ax = plt.subplots(1, 1, figsize=(6.75, 4))\n", - "ax.plot(np.arange(len(sampled_rewards[0])), sampled_rewards[i])\n", - "\n", - "ax.vlines(first_success, ymin=0, ymax=first_success_reward, linestyle='--', color='green')\n", - "ax.hlines(first_success_reward, xmin=0, xmax=first_success, linestyle='--', color='green')" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.9" - } - }, - "nbformat": 4, - "nbformat_minor": 1 + "nbformat": 4, + "nbformat_minor": 1 } diff --git a/scripts/plot_rewards_returns_noise_3D.ipynb b/scripts/plot_rewards_returns_noise_3D.ipynb index f3fdb7c8f..6240dd4e2 100644 --- a/scripts/plot_rewards_returns_noise_3D.ipynb +++ b/scripts/plot_rewards_returns_noise_3D.ipynb @@ -41,7 +41,8 @@ " sampled_first_successes = []\n", "\n", " for _ in range(iters):\n", - " s = trajectory_summary(env, policy, act_noise_pct, end_on_success=False)\n", + " s = trajectory_summary(\n", + " env, policy, act_noise_pct, end_on_success=False)\n", " sampled_rewards.append(s[1])\n", " sampled_returns.append(s[2])\n", " sampled_first_successes.append(s[3])\n", @@ -57,7 +58,7 @@ " x = np.linspace(0, 1, rewards.shape[0])\n", " y = np.arange(rewards.shape[1])\n", " X, Y = np.meshgrid(x, y)\n", - " \n", + "\n", " fig = plt.figure(figsize=(12, 5))\n", "\n", " Z = rewards.T\n", @@ -66,9 +67,10 @@ " rcount, ccount, _ = colors.shape\n", "\n", " ax0 = fig.add_subplot(121, projection='3d')\n", - " surf = ax0.plot_surface(X, Y, Z, rcount=rcount, ccount=ccount, facecolors=colors, shade=False)\n", + " surf = ax0.plot_surface(X, Y, Z, rcount=rcount,\n", + " ccount=ccount, facecolors=colors, shade=False)\n", "\n", - " surf.set_facecolor((0,0,0,0))\n", + " surf.set_facecolor((0, 0, 0, 0))\n", " ax0.set_xlabel(f'Noise Percent in Action Dim {dim}')\n", " ax0.set_ylabel('Time Steps')\n", " ax0.set_zlabel('Rewards')\n", @@ -80,9 +82,10 @@ " rcount, ccount, _ = colors.shape\n", "\n", " ax1 = fig.add_subplot(122, projection='3d')\n", - " surf = ax1.plot_surface(X, Y, Z, rcount=rcount, ccount=ccount, facecolors=colors, shade=False)\n", + " surf = ax1.plot_surface(X, Y, Z, rcount=rcount,\n", + " ccount=ccount, facecolors=colors, shade=False)\n", "\n", - " surf.set_facecolor((0,0,0,0))\n", + " surf.set_facecolor((0, 0, 0, 0))\n", " ax1.set_xlabel(f'Noise Percent in Action Dim {dim}')\n", " ax1.set_ylabel('Time Steps')\n", " ax1.set_zlabel('Returns')\n", @@ -111,25 +114,24 @@ "outputs": [], "source": [ "config = [\n", - "# ['button-press-topdown-v1', 3],\n", + " # ['button-press-topdown-v1', 3],\n", " ['pick-place-V3', 3],\n", - "# ['reach-V3', 3],\n", - "# ['window-open-V3', 3],\n", - "# ['sweep-v1', 3],\n", - "# ['sweep-into-v1', 3],\n", - "# ['shelf-place-V3', 3],\n", - "# ['push-V3', 3],\n", - "# ['peg-insert-side-V3', 3],\n", - "# ['lever-pull-V3', 3],\n", + " # ['reach-V3', 3],\n", + " # ['window-open-V3', 3],\n", + " # ['sweep-v1', 3],\n", + " # ['sweep-into-v1', 3],\n", + " # ['shelf-place-V3', 3],\n", + " # ['push-V3', 3],\n", + " # ['peg-insert-side-V3', 3],\n", + " # ['lever-pull-V3', 3],\n", "]\n", "\n", "for env, axis in config:\n", " tag = env + '-vary-axis-' + str(axis)\n", - " policy = functools.reduce(lambda a,b : a if a[0] == env else b, test_cases_latest_nonoise)[1]\n", + " policy = functools.reduce(\n", + " lambda a, b: a if a[0] == env else b, test_cases_latest_nonoise)[1]\n", " env = ALL_ENVS[env]()\n", - " env._partially_observable = False\n", - " env._freeze_rand_vec = False\n", - " env._set_task_called = True\n", + " env._goal_observable = False\n", "\n", " sampled_rewards, sampled_returns = [], []\n", " noise = np.full(4, .75)\n", diff --git a/scripts/policy_testing.py b/scripts/policy_testing.py index 79f35f5f6..f357ba946 100644 --- a/scripts/policy_testing.py +++ b/scripts/policy_testing.py @@ -17,11 +17,7 @@ ml1 = metaworld.MT50(seed=seed) env = ml1.train_classes[env_name]() task = [t for t in ml1.train_tasks if t.env_name == env_name][0] -env.set_task(task) -env.seed(seed) -env.action_space.seed(seed) -env.observation_space.seed(seed) -obs, _ = env.reset() +obs, _ = env.reset(seed=seed) p = policy() count = 0 diff --git a/scripts/profile_memory_usage.py b/scripts/profile_memory_usage.py index 2658fa04c..670c76223 100755 --- a/scripts/profile_memory_usage.py +++ b/scripts/profile_memory_usage.py @@ -3,13 +3,13 @@ import memory_profiler from metaworld.envs.env_dict import ALL_V3_ENVIRONMENTS -from tests.helpers import step_env +from tests.helpers import check_multiple_env_steps def build_and_step(env_cls): env = env_cls() env.reset() - step_env(env, max_path_length=1000, iterations=10) + check_multiple_env_steps(env, max_episode_steps=1000, iterations=10) return env @@ -40,7 +40,7 @@ def profile_hard_mode_shared(): profile = profile_hard_mode_indepedent() print("--------- Independent memory footprints ---------") for cls, u in profile.items(): - print(f"{cls.__name__ : <40} {u : >5.1f} MB") + print(f"{cls.__name__: <40} {u: >5.1f} MB") max_independent = max(profile.values()) mean_independent = sum(profile.values()) / len(profile) min_independent = min(profile.values()) @@ -48,7 +48,7 @@ def profile_hard_mode_shared(): print("| min | mean | max |") print("|----------|----------|----------|") print( - f"| {min_independent : .1f} MB | {mean_independent : .1f} MB | {max_independent : .1f} MB |" + f"| {min_independent: .1f} MB | {mean_independent: .1f} MB | {max_independent: .1f} MB |" ) print("\n") @@ -56,5 +56,5 @@ def profile_hard_mode_shared(): max_usage = profile_hard_mode_shared() mean_shared = max_usage / len(ALL_V3_ENVIRONMENTS) print( - f"Mean memory footprint (n = {len(ALL_V3_ENVIRONMENTS)}): {mean_shared : .1f} MB" + f"Mean memory footprint (n = {len(ALL_V3_ENVIRONMENTS)}): {mean_shared: .1f} MB" ) diff --git a/scripts/scripted_policy_movies.ipynb b/scripts/scripted_policy_movies.ipynb index a10950627..bd6e277c8 100644 --- a/scripts/scripted_policy_movies.ipynb +++ b/scripts/scripted_policy_movies.ipynb @@ -1,174 +1,178 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "import os\n", - "import functools\n", - "\n", - "import cV3\n", - "import numpy as np\n", - "\n", - "from tests.metaworld.envs.mujoco.sawyer_xyz.test_scripted_policies import ALL_ENVS, test_cases_latest_nonoise\n" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "import os\n", + "import functools\n", + "\n", + "import cV3\n", + "import numpy as np\n", + "\n", + "from tests.metaworld.envs.mujoco.sawyer_xyz.test_scripted_policies import ALL_ENVS, test_cases_latest_nonoise" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "def trajectory_generator(env, policy, act_noise_pct, res=(640, 480), camera='corner'):\n", + " action_space_ptp = env.action_space.high - env.action_space.low\n", + "\n", + " env.reset()\n", + " env.reset_model()\n", + " o = env.reset()\n", + "\n", + " for _ in range(env.max_episode_steps):\n", + " a = policy.get_action(o)\n", + " a = np.random.normal(a, act_noise_pct * action_space_ptp)\n", + "\n", + " o, r, done, info = env.step(a)\n", + " # Camera is one of ['corner', 'topview', 'behindGripper', 'gripperPOV']\n", + " yield r, done, info, env.sim.render(*res, mode='offscreen', camera_name=camera)[:, :, ::-1]\n", + "\n", + "\n", + "def writer_for(tag, fps, res):\n", + " if not os.path.exists('movies'):\n", + " os.mkdir('movies')\n", + " return cV3.VideoWriter(\n", + " f'movies/{tag}.avi',\n", + " cV3.VideoWriter_fourcc('M', 'J', 'P', 'G'),\n", + " fps,\n", + " res\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "resolution = (1920, 1080)\n", + "# one of ['corner', 'topview', 'behindGripper', 'gripperPOV']\n", + "camera = 'behindGripper'\n", + "flip = True # if True, flips output image 180 degrees\n", + "\n", + "config = [\n", + " # env, action noise pct, cycles, quit on success\n", + " ('assembly-V3', np.zeros(4), 3, True),\n", + " ('basketball-V3', np.zeros(4), 3, True),\n", + " ('bin-picking-V3', np.zeros(4), 3, True),\n", + " ('box-close-V3', np.zeros(4), 3, True),\n", + " ('button-press-topdown-V3', np.zeros(4), 3, True),\n", + " ('button-press-topdown-wall-V3', np.zeros(4), 3, True),\n", + " ('button-press-V3', np.zeros(4), 3, True),\n", + " ('button-press-wall-V3', np.zeros(4), 3, True),\n", + " ('coffee-button-V3', np.zeros(4), 3, True),\n", + " ('coffee-pull-V3', np.zeros(4), 3, True),\n", + " ('coffee-push-V3', np.zeros(4), 3, True),\n", + " ('dial-turn-V3', np.zeros(4), 3, True),\n", + " ('disassemble-V3', np.zeros(4), 3, True),\n", + " ('door-close-V3', np.zeros(4), 3, True),\n", + " ('door-lock-V3', np.zeros(4), 3, True),\n", + " ('door-open-V3', np.zeros(4), 3, True),\n", + " ('door-unlock-V3', np.zeros(4), 3, True),\n", + " ('hand-insert-V3', np.zeros(4), 3, True),\n", + " ('drawer-close-V3', np.zeros(4), 3, True),\n", + " ('drawer-open-V3', np.zeros(4), 3, True),\n", + " ('faucet-open-V3', np.zeros(4), 3, True),\n", + " ('faucet-close-V3', np.zeros(4), 3, True),\n", + " ('hammer-V3', np.zeros(4), 3, True),\n", + " ('handle-press-side-V3', np.zeros(4), 3, True),\n", + " ('handle-press-V3', np.zeros(4), 3, True),\n", + " ('handle-pull-side-V3', np.zeros(4), 3, True),\n", + " ('handle-pull-V3', np.zeros(4), 3, True),\n", + " ('lever-pull-V3', np.zeros(4), 3, True),\n", + " ('peg-insert-side-V3', np.zeros(4), 3, True),\n", + " ('pick-place-wall-V3', np.zeros(4), 3, True),\n", + " ('pick-out-of-hole-V3', np.zeros(4), 3, True),\n", + " ('reach-V3', np.zeros(4), 3, True),\n", + " ('push-back-V3', np.zeros(4), 3, True),\n", + " ('push-V3', np.zeros(4), 3, True),\n", + " ('pick-place-V3', np.zeros(4), 3, True),\n", + " ('plate-slide-V3', np.zeros(4), 3, True),\n", + " ('plate-slide-side-V3', np.zeros(4), 3, True),\n", + " ('plate-slide-back-V3', np.zeros(4), 3, True),\n", + " ('plate-slide-back-side-V3', np.zeros(4), 3, True),\n", + " ('peg-insert-side-V3', np.zeros(4), 3, True),\n", + " ('peg-unplug-side-V3', np.zeros(4), 3, True),\n", + " ('soccer-V3', np.zeros(4), 3, True),\n", + " ('stick-push-V3', np.zeros(4), 3, True),\n", + " ('stick-pull-V3', np.zeros(4), 3, True),\n", + " ('push-wall-V3', np.zeros(4), 3, True),\n", + " ('push-V3', np.zeros(4), 3, True),\n", + " ('reach-wall-V3', np.zeros(4), 3, True),\n", + " ('reach-V3', np.zeros(4), 3, True),\n", + " ('shelf-place-V3', np.zeros(4), 3, True),\n", + " ('sweep-into-V3', np.zeros(4), 3, True),\n", + " ('sweep-V3', np.zeros(4), 3, True),\n", + " ('window-open-V3', np.zeros(4), 3, True),\n", + " ('window-close-V3', np.zeros(4), 3, True),\n", + "]\n", + "\n", + "for env, noise, cycles, quit_on_success in config:\n", + " tag = env + '-noise-' + \\\n", + " np.array2string(noise, precision=2, separator=',', suppress_small=True)\n", + "\n", + " policy = functools.reduce(\n", + " lambda a, b: a if a[0] == env else b, test_cases_latest_nonoise)[1]\n", + " env = ALL_ENVS[env]()\n", + " env._goal_observable = False\n", + "\n", + " writer = writer_for(\n", + " tag, env.metadata['video.frames_per_second'], resolution)\n", + " for _ in range(cycles):\n", + " for r, done, info, img in trajectory_generator(env, policy, noise, resolution, camera):\n", + " if flip:\n", + " img = cV3.rotate(img, cV3.ROTATE_180)\n", + " writer.write(img)\n", + " if quit_on_success and info['success']:\n", + " break\n", + "\n", + " writer.release()" + ] } - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "def trajectory_generator(env, policy, act_noise_pct, res=(640, 480), camera='corner'):\n", - " action_space_ptp = env.action_space.high - env.action_space.low\n", - "\n", - " env.reset()\n", - " env.reset_model()\n", - " o = env.reset()\n", - "\n", - " for _ in range(env.max_path_length):\n", - " a = policy.get_action(o)\n", - " a = np.random.normal(a, act_noise_pct * action_space_ptp)\n", - "\n", - " o, r, done, info = env.step(a)\n", - " # Camera is one of ['corner', 'topview', 'behindGripper', 'gripperPOV']\n", - " yield r, done, info, env.sim.render(*res, mode='offscreen', camera_name=camera)[:,:,::-1]\n", - "\n", - "def writer_for(tag, fps, res):\n", - " if not os.path.exists('movies'):\n", - " os.mkdir('movies')\n", - " return cV3.VideoWriter(\n", - " f'movies/{tag}.avi',\n", - " cV3.VideoWriter_fourcc('M','J','P','G'),\n", - " fps,\n", - " res\n", - " )" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "resolution = (1920, 1080)\n", - "camera = 'behindGripper' # one of ['corner', 'topview', 'behindGripper', 'gripperPOV']\n", - "flip=True # if True, flips output image 180 degrees\n", - "\n", - "config = [\n", - " # env, action noise pct, cycles, quit on success\n", - " ('assembly-V3', np.zeros(4), 3, True),\n", - " ('basketball-V3', np.zeros(4), 3, True),\n", - " ('bin-picking-V3', np.zeros(4), 3, True),\n", - " ('box-close-V3', np.zeros(4), 3, True),\n", - " ('button-press-topdown-V3', np.zeros(4), 3, True),\n", - " ('button-press-topdown-wall-V3', np.zeros(4), 3, True),\n", - " ('button-press-V3', np.zeros(4), 3, True),\n", - " ('button-press-wall-V3', np.zeros(4), 3, True),\n", - " ('coffee-button-V3', np.zeros(4), 3, True),\n", - " ('coffee-pull-V3', np.zeros(4), 3, True),\n", - " ('coffee-push-V3', np.zeros(4), 3, True),\n", - " ('dial-turn-V3', np.zeros(4), 3, True),\n", - " ('disassemble-V3', np.zeros(4), 3, True),\n", - " ('door-close-V3', np.zeros(4), 3, True),\n", - " ('door-lock-V3', np.zeros(4), 3, True),\n", - " ('door-open-V3', np.zeros(4), 3, True),\n", - " ('door-unlock-V3', np.zeros(4), 3, True),\n", - " ('hand-insert-V3', np.zeros(4), 3, True),\n", - " ('drawer-close-V3', np.zeros(4), 3, True),\n", - " ('drawer-open-V3', np.zeros(4), 3, True),\n", - " ('faucet-open-V3', np.zeros(4), 3, True),\n", - " ('faucet-close-V3', np.zeros(4), 3, True),\n", - " ('hammer-V3', np.zeros(4), 3, True),\n", - " ('handle-press-side-V3', np.zeros(4), 3, True),\n", - " ('handle-press-V3', np.zeros(4), 3, True),\n", - " ('handle-pull-side-V3', np.zeros(4), 3, True),\n", - " ('handle-pull-V3', np.zeros(4), 3, True),\n", - " ('lever-pull-V3', np.zeros(4), 3, True),\n", - " ('peg-insert-side-V3', np.zeros(4), 3, True),\n", - " ('pick-place-wall-V3', np.zeros(4), 3, True),\n", - " ('pick-out-of-hole-V3', np.zeros(4), 3, True),\n", - " ('reach-V3', np.zeros(4), 3, True),\n", - " ('push-back-V3', np.zeros(4), 3, True),\n", - " ('push-V3', np.zeros(4), 3, True),\n", - " ('pick-place-V3', np.zeros(4), 3, True),\n", - " ('plate-slide-V3', np.zeros(4), 3, True),\n", - " ('plate-slide-side-V3', np.zeros(4), 3, True),\n", - " ('plate-slide-back-V3', np.zeros(4), 3, True),\n", - " ('plate-slide-back-side-V3', np.zeros(4), 3, True),\n", - " ('peg-insert-side-V3', np.zeros(4), 3, True),\n", - " ('peg-unplug-side-V3', np.zeros(4), 3, True),\n", - " ('soccer-V3', np.zeros(4), 3, True),\n", - " ('stick-push-V3', np.zeros(4), 3, True),\n", - " ('stick-pull-V3', np.zeros(4), 3, True),\n", - " ('push-wall-V3', np.zeros(4), 3, True),\n", - " ('push-V3', np.zeros(4), 3, True),\n", - " ('reach-wall-V3', np.zeros(4), 3, True),\n", - " ('reach-V3', np.zeros(4), 3, True),\n", - " ('shelf-place-V3', np.zeros(4), 3, True),\n", - " ('sweep-into-V3', np.zeros(4), 3, True),\n", - " ('sweep-V3', np.zeros(4), 3, True),\n", - " ('window-open-V3', np.zeros(4), 3, True),\n", - " ('window-close-V3', np.zeros(4), 3, True),\n", - "]\n", - "\n", - "for env, noise, cycles, quit_on_success in config:\n", - " tag = env + '-noise-' + np.array2string(noise, precision=2, separator=',', suppress_small=True)\n", - "\n", - " policy = functools.reduce(lambda a,b : a if a[0] == env else b, test_cases_latest_nonoise)[1]\n", - " env = ALL_ENVS[env]()\n", - " env._partially_observable = False\n", - " env._freeze_rand_vec = False\n", - " env._set_task_called = True\n", - "\n", - " writer = writer_for(tag, env.metadata['video.frames_per_second'], resolution)\n", - " for _ in range(cycles):\n", - " for r, done, info, img in trajectory_generator(env, policy, noise, resolution, camera):\n", - " if flip: img = cV3.rotate(img, cV3.ROTATE_180)\n", - " writer.write(img)\n", - " if quit_on_success and info['success']:\n", - " break\n", - "\n", - " writer.release()" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" } - } - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 0 + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/tests/gym/__init__.py b/tests/gym/__init__.py new file mode 100644 index 000000000..01caf906d --- /dev/null +++ b/tests/gym/__init__.py @@ -0,0 +1,2 @@ +import multiprocessing as mp +mp.set_start_method('spawn', force=True) diff --git a/tests/gym/helpers.py b/tests/gym/helpers.py new file mode 100644 index 000000000..1e9ed51f1 --- /dev/null +++ b/tests/gym/helpers.py @@ -0,0 +1,186 @@ +from abc import ABC, abstractmethod +from typing import Protocol + +import numpy as np + +from metaworld.policies import ENV_POLICY_MAP + +import gymnasium as gym + + +class MetaworldAgent(ABC, Protocol): + @abstractmethod + def get_action(self, obs: np.ndarray, info: dict, task_name: str, action_space) -> np.ndarray: + pass + + @abstractmethod + def reset(self): + pass + + +class RandomMetaworldAgent(MetaworldAgent): + def __init__(self, seed: int = None): + if seed is None: + self.seed = 42 + self.seed = seed + self.reset() + + def get_action(self, obs: np.ndarray, info: dict, task_name: str, action_space) -> np.ndarray: + low = action_space.low + high = action_space.high + return self.rng.uniform(low, high) + + def reset(self): + self.rng = np.random.default_rng(self.seed) + + +class ExpertPolicyMetaworldAgent(MetaworldAgent): + def get_action(self, obs, info, task_name: str, action_space): + if task_name is None: + raise ValueError( + "Task name must be provided for ExpertPolicyMetaworldAgent.") + if self.policy_task_name != task_name: + self.policy_task_name = task_name + policy_cls = ENV_POLICY_MAP[task_name] + self.policy = policy_cls() + return self.policy.get_action(obs) + + def reset(self): + self.policy_task_name = None + self.policy = None + + +def run_agent_episode_in_env(env: gym.Env, + agent: MetaworldAgent, + max_episode_steps: int, + record_keys: set[str] | None = None,) -> dict: + + if record_keys is None: + record_keys = set() + + # If we run N steps, we record N+1 observations (initial + N results). + buffer_size = max_episode_steps + 1 + + rec_obs = 'observations' in record_keys + rec_rewards = 'rewards' in record_keys + rec_terminates = 'terminates' in record_keys + rec_truncates = 'truncates' in record_keys + rec_agent_actions = 'agent_actions' in record_keys + + agent.reset() + obs, reset_info = env.reset() + info = reset_info + + # Preallocate arrays + # Observations + if rec_obs: + observations = np.zeros( + (buffer_size, *obs.shape), dtype=obs.dtype) + + # Actions (Note: buffer_size is enough, though actions will be 1 less than obs) + if rec_agent_actions: + agent_actions = np.zeros( + (buffer_size, *env.action_space.shape), dtype=env.action_space.dtype) + + # Scalars (floats initialized to NaN, bools to False) + if rec_rewards: + rewards = np.full(buffer_size, np.nan, dtype=np.float64) + if rec_terminates: + terminates = np.zeros(buffer_size, dtype=bool) + if rec_truncates: + truncates = np.zeros(buffer_size, dtype=bool) + + agent_first_success_step = None + any_terminated = False + any_truncated = False + + done = False + agent_step = 0 + while True: + # Record Pre-Step Data + if rec_obs: + observations[agent_step] = obs + + if done: + break + + agent_action = agent.get_action( + obs, info, env.unwrapped.ENV_NAME, env.action_space) + + if rec_agent_actions: + agent_actions[agent_step] = agent_action + + obs, reward, terminate, truncate, info = env.step( + agent_action) + step_is_success = info.get('success', 0.0) >= 1.0 + + if terminate: + any_terminated = True + + if truncate: + any_truncated = True + + if step_is_success and agent_first_success_step is None: + agent_first_success_step = agent_step + + done = terminate or truncate + + # Record Post-Step Data + if rec_rewards: + rewards[agent_step] = reward + if rec_terminates: + terminates[agent_step] = terminate + if rec_truncates: + truncates[agent_step] = truncate + + agent_step += 1 + + # Determine slice indices + obs_slice = agent_step + 1 + trans_slice = agent_step + + ret = {} + if rec_obs: + ret['observations'] = observations[:obs_slice] + if rec_rewards: + ret['rewards'] = rewards[:trans_slice] + if rec_terminates: + ret['terminates'] = terminates[:trans_slice] + if rec_truncates: + ret['truncates'] = truncates[:trans_slice] + + if rec_agent_actions: + ret['agent_actions'] = agent_actions[:trans_slice] + + ret['env_name'] = env.unwrapped.ENV_NAME + ret['env_seed'] = reset_info['seed'] + ret['agent_first_success_step'] = agent_first_success_step + ret['total_episode_steps'] = agent_step + ret['any_terminated'] = any_terminated + ret['any_truncated'] = any_truncated + + return ret + + +def run_agent_episode(env_name, + seed, + agent: MetaworldAgent, + max_episode_steps: int, + record_keys: set[str] | None = None, + reward_function_version: str = 'v2', + ) -> dict: + env = gym.make('Meta-World/MT1', + env_name=env_name, + seed=seed, + reward_function_version=reward_function_version, + max_episode_steps=max_episode_steps, + num_tasks_per_env=1, + ) + episode_results = run_agent_episode_in_env( + env=env, + agent=agent, + max_episode_steps=max_episode_steps, + record_keys=record_keys, + ) + env.close() + return episode_results diff --git a/tests/gym/test_envs.py b/tests/gym/test_envs.py new file mode 100644 index 000000000..ce6b1151e --- /dev/null +++ b/tests/gym/test_envs.py @@ -0,0 +1,33 @@ +import numpy as np +import pytest + +from metaworld.env_dict import ENV_NAMES +from tests.gym.helpers import RandomMetaworldAgent, run_agent_episode + + +@pytest.mark.parametrize("env_name", ENV_NAMES) +def test_env_seeds_produce_unique_observations(env_name): + agent = RandomMetaworldAgent(seed=42) + + # The initial observation is sufficient to verify different seeds produce different results + # since it contains randomized object positions and the goal position. + max_episode_steps = 1 + + record_keys = set(['observations']) + seeds = [42, 43, 44, 45, 46] + observations = [] + + for seed in seeds: + ep_results = run_agent_episode( + env_name=env_name, + seed=seed, + agent=agent, + max_episode_steps=max_episode_steps, + record_keys=record_keys, + ) + observations.append(ep_results['observations'][0]) + + # Verify that all observations are unique + unique_observations = {tuple(obs) for obs in observations} + assert len(unique_observations) == len(seeds), \ + f"Not all observations are unique for env {env_name} with different seeds" diff --git a/tests/gym/test_goal_observability.py b/tests/gym/test_goal_observability.py new file mode 100644 index 000000000..c3d224826 --- /dev/null +++ b/tests/gym/test_goal_observability.py @@ -0,0 +1,160 @@ +import pytest +import numpy as np +import gymnasium as gym + +from metaworld.env_dict import ( + ENV_NAMES, + ENV_CLASS_MAP, + MT_BENCHMARKS_TRAIN_ENV_NAMES, + ML_BENCHMARKS +) + +# --- Helper Functions --- + + +def _assert_goal_observability(obs, env: gym.Env, goal_observable: bool): + """Checks a single observation/env pair.""" + env_name = env.unwrapped.ENV_NAME + zero_pos = np.zeros(3) + goal_pos = obs[-3:] + + if goal_observable: + assert not np.array_equal(goal_pos, zero_pos), \ + f"Goal position appears to be hidden in env {env_name} when it should be observable" + else: + assert np.array_equal(goal_pos, zero_pos), \ + f"Goal position appears to be observable in env {env_name} when it should be hidden" + + +def _verify_goal_observability(env_instance, expected_observable: bool): + """ + Handles env.reset, checking vector vs scalar envs, and closing. + """ + try: + obs, _ = env_instance.reset() + + # Check if it is a VectorEnv (has attribute 'envs') + if hasattr(env_instance, 'envs'): + # Iterate through vector environments + for single_obs, single_env in zip(obs, env_instance.envs): + _assert_goal_observability( + single_obs, single_env, expected_observable) + else: + # Standard single environment + _assert_goal_observability(obs, env_instance, expected_observable) + + finally: + env_instance.close() + + +# --- Individual Environment Tests --- + +@pytest.mark.parametrize("goal_observable", [True, False]) +@pytest.mark.parametrize("env_name", ENV_NAMES) +def test_v3_env_explicit_goal_observability(env_name, goal_observable): + """Test explicit goal observability flags on individual V3 environments.""" + env = ENV_CLASS_MAP[env_name](goal_observable=goal_observable) + _verify_goal_observability(env, goal_observable) + + +@pytest.mark.parametrize("env_name", ENV_NAMES) +def test_v3_env_default_goal_observability(env_name): + """Test default goal observability behavior (should be observable) on V3 environments.""" + env = ENV_CLASS_MAP[env_name]() + _verify_goal_observability(env, expected_observable=True) + + +# --- MT (Multi-Task) Tests (Default: Observable) --- + +@pytest.mark.parametrize("override_setting, expected", [ + (None, True), # Default behavior + (False, False), # User override + (True, True) # Explicit True +]) +def test_mt1_goal_observability(override_setting, expected): + """Test MT1 specific loading.""" + kwargs = {"env_name": "reach-v3"} + if override_setting is not None: + kwargs["goal_observable"] = override_setting + + env = gym.make("Meta-World/MT1", **kwargs) + _verify_goal_observability(env, expected) + + +@pytest.mark.parametrize("benchmark_name", MT_BENCHMARKS_TRAIN_ENV_NAMES.keys()) +@pytest.mark.parametrize("override_setting, expected", [ + (None, True), # Default behavior for MT is Visible + (False, False), # Override to Hidden +]) +def test_mt_benchmarks_goal_observability(benchmark_name, override_setting, expected): + """Test standard MT benchmarks (MT10, MT50, etc).""" + kwargs = {} + if override_setting is not None: + kwargs["goal_observable"] = override_setting + + envs = gym.make_vec(f"Meta-World/{benchmark_name}", **kwargs) + _verify_goal_observability(envs, expected) + + +@pytest.mark.parametrize("override_setting, expected", [ + (None, True), # Default + (False, False) # Override +]) +def test_mt_custom_goal_observability(override_setting, expected): + """Test Custom MT environment construction.""" + kwargs = {"train_env_names": ["reach-v3"]} + if override_setting is not None: + kwargs["goal_observable"] = override_setting + + envs = gym.make_vec("Meta-World/custom-mt-envs", **kwargs) + _verify_goal_observability(envs, expected) + + +# --- ML (Meta-Learning) Tests (Default: Hidden) --- + +@pytest.mark.parametrize("override_setting, expected", [ + (None, False), # Default behavior for ML is Hidden + (True, True), # User override + (False, False) # Explicit False +]) +def test_ml1_goal_observability(override_setting, expected): + """Test ML1 specific loading.""" + kwargs = {"env_name": "reach-v3"} + if override_setting is not None: + kwargs["goal_observable"] = override_setting + + envs = gym.make_vec("Meta-World/ML1-train", **kwargs) + _verify_goal_observability(envs, expected) + + +@pytest.mark.parametrize("benchmark_name", ML_BENCHMARKS.keys()) +@pytest.mark.parametrize("override_setting, expected", [ + (None, False), # Default behavior for ML is Hidden + (True, True), # Override to Visible +]) +def test_ml_benchmarks_goal_observability(benchmark_name, override_setting, expected): + """Test standard ML benchmarks (ML10, ML45, etc).""" + kwargs = {} + if override_setting is not None: + kwargs["goal_observable"] = override_setting + + envs = gym.make_vec(f"Meta-World/{benchmark_name}-train", **kwargs) + _verify_goal_observability(envs, expected) + + +@pytest.mark.parametrize("override_setting, expected", [ + (None, False), # Default + (True, True) # Override +]) +def test_ml_custom_goal_observability(override_setting, expected): + """Test Custom ML environment construction.""" + kwargs = { + "train_env_names": ["reach-v3"], + "test_env_names": ["pick-place-v3"], + "split": "train" + } + if override_setting is not None: + kwargs["goal_observable"] = override_setting + + envs = gym.make_vec("Meta-World/custom-ml-envs", **kwargs) + _verify_goal_observability(envs, expected) diff --git a/tests/gym/test_memory_usage.py b/tests/gym/test_memory_usage.py new file mode 100644 index 000000000..93a9da7c5 --- /dev/null +++ b/tests/gym/test_memory_usage.py @@ -0,0 +1,48 @@ +import memory_profiler +import pytest +import gymnasium as gym +from concurrent.futures import ProcessPoolExecutor + +from metaworld.env_dict import ENV_CLASS_MAP + +from tests.gym.helpers import run_agent_episode_in_env, RandomMetaworldAgent + + +def _build_env_and_run_eps(env_name): + seed = 42 + agent = RandomMetaworldAgent(seed=seed) + max_episode_steps = 150 + env = gym.make("Meta-World/MT1", + env_name=env_name, + seed=seed, + num_tasks_per_env=1, + max_episode_steps=max_episode_steps,) + + episodes = 10 + for _ in range(episodes): + run_agent_episode_in_env( + env=env, + agent=agent, + max_episode_steps=max_episode_steps, + ) + env.close() + + +def _profile_env_memory(env_name): + target = (_build_env_and_run_eps, [env_name], {}) + memory_usage = memory_profiler.memory_usage(target) + return memory_usage + + +@pytest.mark.parametrize("env_name", ENV_CLASS_MAP.keys()) +def test_env_memory_profiler(env_name): + # Create a separate process to be able to accurately measure memory usage + with ProcessPoolExecutor(max_workers=1) as executor: + future = executor.submit(_profile_env_memory, env_name) + memory_usage = future.result() + + # Max memory usage per env in MB + env_max_memory_usage_threshold = 300 + print(f"Memory usage for env {env_name}: {memory_usage}") + env_max_memory_usage = max(memory_usage) + assert env_max_memory_usage < env_max_memory_usage_threshold, f"Env {env_name} exceeded max memory usage of {env_max_memory_usage_threshold}: {env_max_memory_usage}MB" diff --git a/tests/gym/test_mt1_single_seed.py b/tests/gym/test_mt1_single_seed.py new file mode 100644 index 000000000..f1e8a0be4 --- /dev/null +++ b/tests/gym/test_mt1_single_seed.py @@ -0,0 +1,66 @@ +import numpy as np +import pytest + +import metaworld +from metaworld.env_dict import ENV_NAMES + +import gymnasium as gym + +from tests.gym.helpers import RandomMetaworldAgent, run_agent_episode, run_agent_episode_in_env + + +@pytest.mark.parametrize("env_name", ENV_NAMES) +def test_env_mt1_single_seed(env_name): + agent = RandomMetaworldAgent(seed=42) + + max_episode_steps = 200 + record_keys = set(['observations']) + seed = 42 + + env = gym.make("Meta-World/MT1", + env_name=env_name, + seed=seed, + num_tasks_per_env=1, + max_episode_steps=max_episode_steps,) + + first_ep = run_agent_episode_in_env( + env=env, + agent=agent, + max_episode_steps=max_episode_steps, + record_keys=record_keys, + ) + # Verify that for the num_tasks_per_env=1 and MT1 env the seed is the same as the one passed + assert first_ep['env_seed'] == seed, f"Env seed {first_ep['env_seed']} does not match passed seed {seed}" + + # Reset the env and run everything again + second_ep = run_agent_episode_in_env( + env=env, + agent=agent, + max_episode_steps=max_episode_steps, + record_keys=record_keys, + ) + # Verify that the seed is still the same + assert second_ep['env_seed'] == seed, f"Env seed {second_ep['env_seed']} does not match passed seed {seed}" + + env.close() + + # Verify that the observations are the same across both runs + obs_first = first_ep['observations'] + obs_second = second_ep['observations'] + assert np.array_equal(np.array(obs_first), np.array(obs_second)), \ + f"Observations do not match for env {env_name} with single seed" + + # --- Do another run with a new env to verify that it also matches --- + third_ep = run_agent_episode( + env_name=env_name, + agent=agent, + max_episode_steps=max_episode_steps, + record_keys=record_keys, + seed=seed, + ) + # Verify that the seed is still the same + assert third_ep['env_seed'] == seed, f"Env seed {third_ep['env_seed']} does not match passed seed {seed}" + + obs_third = third_ep['observations'] + assert np.array_equal(np.array(obs_first), np.array(obs_third)), \ + f"Observations do not match for env {env_name} with single seed" diff --git a/tests/gym/test_one_hot_wrapper.py b/tests/gym/test_one_hot_wrapper.py new file mode 100644 index 000000000..65c173893 --- /dev/null +++ b/tests/gym/test_one_hot_wrapper.py @@ -0,0 +1,25 @@ +import numpy as np +import pytest + +import metaworld +import gymnasium as gym + +from metaworld.env_dict import ENV_NAMES, MT_BENCHMARKS_TRAIN_ENV_NAMES + + +@pytest.mark.parametrize("mtx_benchmark_name", MT_BENCHMARKS_TRAIN_ENV_NAMES.keys()) +def test_env_one_hot_wrapper(mtx_benchmark_name): + """Test that the one-hot wrapper correctly encodes task information.""" + envs = gym.make_vec(f"Meta-World/{mtx_benchmark_name}", use_one_hot=True) + obs, info = envs.reset() + # 39:: one-hot part for MT10 + one_hots = [] + for i in range(envs.num_envs): + one_hot_obs = obs[i][39:] + # Check that only one index is 1 and the rest are 0s + assert np.sum(one_hot_obs) == 1.0 + # Find the index of the 1 + one_index = np.argmax(one_hot_obs) + one_hots.append(one_index) + # Ensure that we have all tasks represented in the one-hot encodings + assert set(one_hots) == set(range(envs.num_envs)) diff --git a/tests/gym/test_policies.py b/tests/gym/test_policies.py new file mode 100644 index 000000000..c45d035fd --- /dev/null +++ b/tests/gym/test_policies.py @@ -0,0 +1,48 @@ +import pytest + +import metaworld +from metaworld.env_dict import ENV_NAMES + +import gymnasium as gym + +from tests.gym.helpers import ExpertPolicyMetaworldAgent, run_agent_episode_in_env + + +@pytest.mark.parametrize("env_name", ENV_NAMES) +# TODO: Add 'v1' back when all the reward functions are fixed to correctly report success. +@pytest.mark.parametrize("reward_function_version", ['v2']) +def test_policies(env_name, reward_function_version): + agent = ExpertPolicyMetaworldAgent() + + max_episode_steps = 500 + num_episodes = 30 + + env = gym.make("Meta-World/MT1", + env_name=env_name, + seed=42, + num_tasks_per_env=num_episodes, + max_episode_steps=max_episode_steps, + reward_function_version=reward_function_version, + task_sampler="pseudorandom", + ) + + ep_seeds = [] + successes = 0 + for _ in range(num_episodes): + ep_results = run_agent_episode_in_env( + env=env, + agent=agent, + max_episode_steps=max_episode_steps, + ) + ep_seeds.append(ep_results['env_seed']) + successes += int(ep_results['agent_first_success_step'] is not None) + + env.close() + + success_rate = successes / num_episodes + good_policy_success_rate = 0.8 + assert success_rate >= good_policy_success_rate, f"Success rate {success_rate} for env {env_name} below {good_policy_success_rate}" + + # Verify that all seeds are unique + assert len(set( + ep_seeds)) == num_episodes, f"Not all episode seeds are unique for env {env_name}" diff --git a/tests/metaworld/envs/mujoco/sawyer_xyz/test_obs_space_hand.py b/tests/gym/test_sawyer_xyz_obs_space_hand.py similarity index 67% rename from tests/metaworld/envs/mujoco/sawyer_xyz/test_obs_space_hand.py rename to tests/gym/test_sawyer_xyz_obs_space_hand.py index 5510ac927..6e546da31 100644 --- a/tests/metaworld/envs/mujoco/sawyer_xyz/test_obs_space_hand.py +++ b/tests/gym/test_sawyer_xyz_obs_space_hand.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from metaworld.env_dict import ALL_V3_ENVIRONMENTS +from metaworld.env_dict import ENV_CLASS_MAP from metaworld.policies.action import Action from metaworld.policies.policy import Policy, move from metaworld.sawyer_xyz_env import SawyerXYZEnv @@ -20,7 +20,8 @@ def get_action(self, obs): action = Action({"delta_pos": np.arange(3), "grab_effort": 3}) - action["delta_pos"] = move(o_d["hand_pos"], to_xyz=self._target, p=25.0) + action["delta_pos"] = move( + o_d["hand_pos"], to_xyz=self._target, p=25.0) action["grab_effort"] = 0.0 return action.array @@ -43,22 +44,17 @@ def sample_spherical(num_points, radius=1.0): @pytest.mark.parametrize("target", sample_spherical(100, 10.0)) def test_reaching_limit(target): - env = ALL_V3_ENVIRONMENTS["reach-v3"]() - env._partially_observable = False - env._freeze_rand_vec = False - env._set_task_called = True + env = ENV_CLASS_MAP["reach-v3"](goal_observable=True) policy = SawyerRandomReachPolicy(target) - env.reset() - env.reset_model() - o_prev, info = env.reset() + init_obs, info = env.reset() - for _ in range(env.max_path_length): - a = policy.get_action(o_prev) - o = env.step(a)[0] - if np.linalg.norm(o[:3] - o_prev[:3]) < 0.001: + for _ in range(env.max_episode_steps): + a = policy.get_action(init_obs) + obs = env.step(a)[0] + if np.linalg.norm(obs[:3] - init_obs[:3]) < 0.001: break - o_prev = o + init_obs = obs - assert SawyerXYZEnv._HAND_SPACE.contains(o[:3]) + assert SawyerXYZEnv._HAND_SPACE.contains(obs[:3]) diff --git a/tests/gym/test_thread_safety.py b/tests/gym/test_thread_safety.py new file mode 100644 index 000000000..173053bea --- /dev/null +++ b/tests/gym/test_thread_safety.py @@ -0,0 +1,68 @@ +import os +import concurrent.futures + +import numpy as np +import numpy.testing as npt + +import metaworld + +from tests.gym.helpers import RandomMetaworldAgent, run_agent_episode + + +def _run_episode(seed: int, env_name: str) -> dict: + agent = RandomMetaworldAgent(seed=seed) + record_keys = ["observations"] + ep_results = run_agent_episode( + env_name=env_name, + seed=seed, + agent=agent, + max_episode_steps=200, + record_keys=record_keys, + ) + + return ep_results + + +def test_env_determinism_across_threads(): + """ + Test that running multiple episodes in parallel threads with the same seeds + produces identical observations, ensuring thread safety and determinism. + """ + + max_workers = os.cpu_count() or 1 + + num_parallel_eps = 55 + env_name = "reach-v3" + + num_batches = 3 + + seeds = np.arange(num_parallel_eps) + all_seeds = np.tile(seeds, num_batches) + np.random.shuffle(all_seeds) + + all_futures = [] + batches = {} + + with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: + for seed in all_seeds: + fut = executor.submit(_run_episode, seed, env_name) + batches.setdefault(seed, []).append(fut) + all_futures.append(fut) + + for seed, batch_futures in batches.items(): + results = [f.result() for f in batch_futures] + + observations = [res["observations"] for res in results] + + try: + for i in range(1, len(observations)): + npt.assert_array_equal( + observations[0], + observations[i], + err_msg=f"Mismatch in seed {seed} between runs 0 and {i}" + ) + except AssertionError as e: + # Cancel all futures + for f in all_futures: + f.cancel() + raise e diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py deleted file mode 100644 index 5d0f1ff84..000000000 --- a/tests/integration/helpers.py +++ /dev/null @@ -1,33 +0,0 @@ -import numpy as np - - -def step_env(env, max_path_length=100, iterations=1, render=True): - """Step env helper.""" - for _ in range(iterations): - obs = env.reset()[0] - for _ in range(max_path_length): - next_obs, _, terminated, truncated, info = env.step( - env.action_space.sample() - ) - if env._partially_observable: - assert (next_obs[-3:] == np.zeros(3)).all() - else: - assert (next_obs[-3:] == env._get_pos_goal()).all() - assert (next_obs[:3] == env.get_endeff_pos()).all() - internal_obs = env._get_pos_objects() - internal_quat = env._get_quat_objects() - assert (next_obs[4:7] == internal_obs[:3]).all() - assert (next_obs[7:11] == internal_quat[:4]).all() - if internal_obs.shape == (6,): - assert internal_quat.shape == (8,) - assert (next_obs[11:14] == internal_obs[3:]).all() - assert (next_obs[14:18] == internal_quat[4:]).all() - else: - assert (next_obs[11:14] == np.zeros(3)).all() - assert (next_obs[14:18] == np.zeros(4)).all() - assert (obs[:18] == next_obs[18:-3]).all() - obs = next_obs - if render: - env.render() - if terminated or truncated: - break diff --git a/tests/integration/test_memory_usage.py b/tests/integration/test_memory_usage.py deleted file mode 100644 index 3f74e9cb9..000000000 --- a/tests/integration/test_memory_usage.py +++ /dev/null @@ -1,56 +0,0 @@ -import memory_profiler -import pytest - -from metaworld.env_dict import ALL_V3_ENVIRONMENTS -from tests.helpers import step_env - - -def build_and_step(env_cls): - env = env_cls() - step_env(env, max_path_length=150, iterations=10, render=False) - env.close() - - -def build_and_step_all(classes): - envs = [] - for env_cls in classes: - env = build_and_step(env_cls) - envs += [env] - - -@pytest.fixture(scope="module") -def mt50_usage(): - profile = {} - for env_cls in ALL_V3_ENVIRONMENTS.values(): - target = (build_and_step, [env_cls], {}) - memory_usage = memory_profiler.memory_usage(target) - profile[env_cls] = max(memory_usage) - - return profile - - -@pytest.mark.skip -@pytest.mark.parametrize("env_cls", ALL_V3_ENVIRONMENTS.values()) -def test_max_memory_usage(env_cls, mt50_usage): - # No env should use more than 250MB - # - # Note: this is quite a bit higher than the average usage cap, because - # loading a single environment incurs a fixed memory overhead which can't - # be shared among environment in the same process - assert mt50_usage[env_cls] < 250 - - -@pytest.mark.skip -def test_avg_memory_usage(): - # average usage no greater than 60MB/env - target = (build_and_step_all, [ALL_V3_ENVIRONMENTS.values()], {}) - usage = memory_profiler.memory_usage(target) - average = max(usage) / len(ALL_V3_ENVIRONMENTS) - assert average < 60 - - -@pytest.mark.skip -def test_from_task_memory_usage(): - target = (ALL_V3_ENVIRONMENTS["reach-v1"], (), {}) - usage = memory_profiler.memory_usage(target) - assert max(usage) < 250 diff --git a/tests/integration/test_new_api.py b/tests/integration/test_new_api.py deleted file mode 100644 index b58ecf644..000000000 --- a/tests/integration/test_new_api.py +++ /dev/null @@ -1,323 +0,0 @@ -import pickle - -import numpy as np -import pytest - -import metaworld -from metaworld import ML1, ML10, ML45, MT10, MT50 -from tests.helpers import step_env - -STEPS = 3 - - -@pytest.mark.parametrize("env_name", ML1.ENV_NAMES) -def test_all_ml1(env_name): - ml1 = ML1(env_name) - train_env_instances = { - env_name: env_cls() for (env_name, env_cls) in ml1.train_classes.items() - } - train_env_rand_vecs = check_tasks_unique(ml1.train_tasks, ml1._train_classes.keys()) - for task in ml1.train_tasks: - env = train_env_instances[task.env_name] - env.set_task(task) - env.reset() - old_obj_init = env.obj_init_pos - old_target_pos = env._target_pos - step_env(env, max_path_length=STEPS, render=False) - assert np.all(np.allclose(old_obj_init, env.obj_init_pos)) - assert np.all(np.allclose(old_target_pos, env._target_pos)) - for env in train_env_instances.values(): - env.close() - del train_env_instances - - test_env_instances = { - env_name: env_cls() for (env_name, env_cls) in ml1.test_classes.items() - } - test_env_rand_vecs = check_tasks_unique(ml1.test_tasks, ml1._test_classes.keys()) - for task in ml1.test_tasks: - env = test_env_instances[task.env_name] - env.set_task(task) - env.reset() - old_obj_init = env.obj_init_pos - old_target_pos = env._target_pos - step_env(env, max_path_length=STEPS, render=False) - assert np.all(np.allclose(old_obj_init, env.obj_init_pos)) - assert np.all(np.allclose(old_target_pos, env._target_pos)) - for env in test_env_instances.values(): - env.close() - train_test_rand_vecs = set() - for rand_vecs in train_env_rand_vecs.values(): - for rand_vec in rand_vecs: - train_test_rand_vecs.add(tuple(rand_vec)) - for rand_vecs in test_env_rand_vecs.values(): - for rand_vec in rand_vecs: - train_test_rand_vecs.add(tuple(rand_vec)) - assert ( - len(train_test_rand_vecs) - == (len(ml1.test_classes.keys()) + len(ml1.train_classes.keys())) - * metaworld._N_GOALS - ) - del test_env_instances - - -def test_all_ml10(): - ml10 = ML10() - train_env_instances = { - env_name: env_cls() for (env_name, env_cls) in ml10.train_classes.items() - } - train_env_rand_vecs = check_tasks_unique( - ml10.train_tasks, ml10._train_classes.keys() - ) - for task in ml10.train_tasks: - env = train_env_instances[task.env_name] - env.set_task(task) - env.reset() - old_obj_init = env.obj_init_pos - old_target_pos = env._target_pos - step_env(env, max_path_length=STEPS, render=False) - assert np.all(np.allclose(old_obj_init, env.obj_init_pos)) - assert np.all(np.allclose(old_target_pos, env._target_pos)) - step_env(env, max_path_length=STEPS, render=False) - for env in train_env_instances.values(): - env.close() - del train_env_instances - - test_env_instances = { - env_name: env_cls() for (env_name, env_cls) in ml10.test_classes.items() - } - test_env_rand_vecs = check_tasks_unique(ml10.test_tasks, ml10._test_classes.keys()) - for task in ml10.test_tasks: - env = test_env_instances[task.env_name] - env.set_task(task) - env.reset() - old_obj_init = env.obj_init_pos - old_target_pos = env._target_pos - step_env(env, max_path_length=STEPS, render=False) - assert np.all(np.allclose(old_obj_init, env.obj_init_pos)) - assert np.all(np.allclose(old_target_pos, env._target_pos)) - step_env(env, max_path_length=STEPS, render=False) - for env in test_env_instances.values(): - env.close() - train_test_rand_vecs = set() - for rand_vecs in train_env_rand_vecs.values(): - for rand_vec in rand_vecs: - train_test_rand_vecs.add(tuple(rand_vec)) - for rand_vecs in test_env_rand_vecs.values(): - for rand_vec in rand_vecs: - train_test_rand_vecs.add(tuple(rand_vec)) - assert ( - len(train_test_rand_vecs) - == (len(ml10.test_classes.keys()) + len(ml10.train_classes.keys())) - * metaworld._N_GOALS - ) - del test_env_instances - - -def test_all_ml45(): - ml45 = ML45() - train_env_instances = { - env_name: env_cls() for (env_name, env_cls) in ml45.train_classes.items() - } - train_env_rand_vecs = check_tasks_unique( - ml45.train_tasks, ml45._train_classes.keys() - ) - for task in ml45.train_tasks: - env = train_env_instances[task.env_name] - env.set_task(task) - obs, info = env.reset() - old_obj_init = env.obj_init_pos - old_target_pos = env._target_pos - step_env(env, max_path_length=STEPS, render=False) - assert np.all(np.allclose(old_obj_init, env.obj_init_pos)) - assert np.all(np.allclose(old_target_pos, env._target_pos)) - for env in train_env_instances.values(): - env.close() - - del train_env_instances - - test_env_instances = { - env_name: env_cls() for (env_name, env_cls) in ml45.test_classes.items() - } - test_env_rand_vecs = check_tasks_unique(ml45.test_tasks, ml45._test_classes.keys()) - for task in ml45.test_tasks: - env = test_env_instances[task.env_name] - env.set_task(task) - obs, info = env.reset() - assert np.all(obs[-3:] == np.array([0, 0, 0])) - assert env.observation_space.shape == (39,) - old_obj_init = env.obj_init_pos - old_target_pos = env._target_pos - step_env(env, max_path_length=STEPS, render=False) - assert np.all(np.allclose(old_obj_init, env.obj_init_pos)) - assert np.all(np.allclose(old_target_pos, env._target_pos)) - for env in test_env_instances.values(): - env.close() - train_test_rand_vecs = set() - for rand_vecs in train_env_rand_vecs.values(): - for rand_vec in rand_vecs: - train_test_rand_vecs.add(tuple(rand_vec)) - for rand_vecs in test_env_rand_vecs.values(): - for rand_vec in rand_vecs: - train_test_rand_vecs.add(tuple(rand_vec)) - assert ( - len(train_test_rand_vecs) - == (len(ml45.test_classes.keys()) + len(ml45.train_classes.keys())) - * metaworld._N_GOALS - ) - del test_env_instances - - -def test_all_mt10(): - mt10 = MT10() - train_env_instances = { - env_name: env_cls() for (env_name, env_cls) in mt10.train_classes.items() - } - train_env_rand_vecs = check_tasks_unique( - mt10.train_tasks, mt10._train_classes.keys() - ) - for task in mt10.train_tasks: - env = train_env_instances[task.env_name] - env.set_task(task) - env.reset() - old_obj_init = env.obj_init_pos - old_target_pos = env._target_pos - step_env(env, max_path_length=STEPS, render=False) - assert np.all(np.allclose(old_obj_init, env.obj_init_pos)) - assert np.all(np.allclose(old_target_pos, env._target_pos)) - for env in train_env_instances.values(): - env.close() - del train_env_instances - - assert len(mt10.test_classes) == 0 - assert len(mt10.test_tasks) == 0 - train_test_rand_vecs = set() - for rand_vecs in train_env_rand_vecs.values(): - for rand_vec in rand_vecs: - train_test_rand_vecs.add(tuple(rand_vec)) - assert len(train_test_rand_vecs) == 10 * 50 - - -def test_all_mt50(): - mt50 = MT50() - train_env_instances = { - env_name: env_cls() for (env_name, env_cls) in mt50.train_classes.items() - } - train_env_rand_vecs = check_tasks_unique( - mt50.train_tasks, mt50._train_classes.keys() - ) - for task in mt50.train_tasks: - env = train_env_instances[task.env_name] - env.set_task(task) - obs, info = env.reset() - assert np.any(obs[-3:] != np.array([0, 0, 0])) - assert env.observation_space.shape == (39,) - old_obj_init = env.obj_init_pos - old_target_pos = env._target_pos - step_env(env, max_path_length=STEPS, render=False) - assert np.all(np.allclose(old_obj_init, env.obj_init_pos)) - assert np.all(np.allclose(old_target_pos, env._target_pos)) - # only needs to be done for 50 environments once - check_target_poss_unique(train_env_instances, train_env_rand_vecs) - for env in train_env_instances.values(): - env.close() - del train_env_instances - - assert len(mt50.test_classes) == 0 - assert len(mt50.test_tasks) == 0 - train_test_rand_vecs = set() - for rand_vecs in train_env_rand_vecs.values(): - for rand_vec in rand_vecs: - train_test_rand_vecs.add(tuple(rand_vec)) - assert len(train_test_rand_vecs) == 50 * 50 - - -def check_tasks_unique(tasks, env_names): - """Verify that all the rand_vecs that are sampled are unique.""" - env_to_rand_vecs = {} - for env_name in env_names: - env_to_rand_vecs[env_name] = np.array( - [ - pickle.loads(task.data)["rand_vec"] - for task in tasks - if (task.env_name == env_name) - ] - ) - unique_task_rand_vecs = np.unique(np.array(env_to_rand_vecs[env_name]), axis=0) - assert unique_task_rand_vecs.shape[0] == metaworld._N_GOALS - return env_to_rand_vecs - - -def check_target_poss_unique(env_instances, env_rand_vecs): - """Verify that all the state_goals are unique for the different rand_vecs that are sampled. - - Note: The following envs randomize object initial position but not state_goal. - ['hammer-v3', 'sweep-into-v3', 'bin-picking-v3', 'basketball-v3'] - - """ - for env_name, rand_vecs in env_rand_vecs.items(): - if env_name in { - "hammer-v3", - "sweep-into-v3", - "bin-picking-v3", - "basketball-v3", - }: - continue - env = env_instances[env_name] - state_goals = [] - for rand_vec in rand_vecs: - env._last_rand_vec = rand_vec - env.reset() - state_goals.append(env._target_pos) - state_goals = np.array(state_goals) - unique_target_poss = np.unique(state_goals, axis=0) - assert ( - unique_target_poss.shape[0] == metaworld._N_GOALS == len(rand_vecs) - ), env_name - - -def test_identical_environments(): - def helper(env, env_2): - for i in range(len(env.train_tasks)): - rand_vec_1 = pickle.loads(env.train_tasks[i].data)["rand_vec"] - rand_vec_2 = pickle.loads(env_2.train_tasks[i].data)["rand_vec"] - np.testing.assert_equal(rand_vec_1, rand_vec_2) - - def helper_neq(env, env_2): - for i in range(len(env.train_tasks)): - rand_vec_1 = pickle.loads(env.train_tasks[i].data)["rand_vec"] - rand_vec_2 = pickle.loads(env_2.train_tasks[i].data)["rand_vec"] - assert not (rand_vec_1 == rand_vec_2).all() - - # testing MT1 - mt1_1 = metaworld.MT1("sweep-into-v3", seed=10) - mt1_2 = metaworld.MT1("sweep-into-v3", seed=10) - helper(mt1_1, mt1_2) - - # testing ML1 - ml1_1 = metaworld.ML1("sweep-into-v3", seed=10) - ml1_2 = metaworld.ML1("sweep-into-v3", seed=10) - helper(ml1_1, ml1_2) - - # testing MT10 - mt10_1 = metaworld.MT10(seed=10) - mt10_2 = metaworld.MT10(seed=10) - helper(mt10_1, mt10_2) - - # testing ML10 - ml10_1 = metaworld.ML10(seed=10) - ml10_2 = metaworld.ML10(seed=10) - helper(ml10_1, ml10_2) - - # testing ML45 - ml45_1 = metaworld.ML45(seed=10) - ml45_2 = metaworld.ML45(seed=10) - helper(ml45_1, ml45_2) - - # testing MT50 - mt50_1 = metaworld.MT50(seed=10) - mt50_2 = metaworld.MT50(seed=10) - helper(mt50_1, mt50_2) - - # test that 2 benchmarks with different seeds have different goals - mt50_3 = metaworld.MT50(seed=50) - helper_neq(mt50_1, mt50_3) diff --git a/tests/integration/test_single_goal_envs.py b/tests/integration/test_single_goal_envs.py deleted file mode 100644 index 409c44673..000000000 --- a/tests/integration/test_single_goal_envs.py +++ /dev/null @@ -1,147 +0,0 @@ -import numpy as np - -from metaworld.env_dict import ( - ALL_V3_ENVIRONMENTS_GOAL_HIDDEN, - ALL_V3_ENVIRONMENTS_GOAL_OBSERVABLE, -) -from tests.helpers import step_env - - -def test_hidden_goal_envs(): - for env_key, env_cls in ALL_V3_ENVIRONMENTS_GOAL_HIDDEN.items(): - assert "goal-hidden" in env_key - assert "GoalHidden" in env_cls.__name__ - state_before = np.random.get_state() - env = env_cls(seed=5) - enV3 = env_cls(seed=5) - step_env(env, max_path_length=3, iterations=3, render=False) - - first_target = env._target_pos - env.reset() - second_target = env._target_pos - - assert (first_target == second_target).all() - env.reset() - enV3.reset() - assert (env._target_pos == enV3._target_pos).all() - state_after = np.random.get_state() - for idx, (state_before_idx, state_after_idx) in enumerate( - zip(state_before, state_after) - ): - if idx == 1: - assert (state_before_idx == state_after_idx).all() - else: - assert state_before_idx == state_after_idx - - -def test_observable_goal_envs(): - for env_key, env_cls in ALL_V3_ENVIRONMENTS_GOAL_OBSERVABLE.items(): - assert "goal-observable" in env_key - assert "GoalObservable" in env_cls.__name__ - state_before = np.random.get_state() - env = env_cls(seed=10) - enV3 = env_cls(seed=10) - step_env(env, max_path_length=3, iterations=3, render=False) - - first_target = env._target_pos - env.reset() - second_target = env._target_pos - - assert (first_target == second_target).all() - env.reset() - enV3.reset() - assert (env._target_pos == enV3._target_pos).all() - state_after = np.random.get_state() - for idx, (state_before_idx, state_after_idx) in enumerate( - zip(state_before, state_after) - ): - if idx == 1: - assert (state_before_idx == state_after_idx).all() - else: - assert state_before_idx == state_after_idx - - -def test_seeding_observable(): - door_open_goal_observable_cls = ALL_V3_ENVIRONMENTS_GOAL_OBSERVABLE[ - "door-open-v3-goal-observable" - ] - - env1 = door_open_goal_observable_cls(seed=5) - enV3 = door_open_goal_observable_cls(seed=5) - - env1.reset() # Reset environment - enV3.reset() - a1 = env1.action_space.sample() # Sample an action - a2 = enV3.action_space.sample() - next_obs1, _, _, _, _ = env1.step( - a1 - ) # Step the environoment with the sampled random action - next_obs2, _, _, _, _ = enV3.step(a2) - assert ( - next_obs1[-3:] == next_obs2[-3:] - ).all() # 2 envs initialized with the same seed will have the same goal - assert not ( - next_obs2[-3:] == np.zeros(3) - ).all() # The env's are goal observable, meaning the goal is not zero'd out - - env3 = door_open_goal_observable_cls( - seed=10 - ) # Construct an environment with a different seed - env1.reset() # Reset environment - env3.reset() - a1 = env1.action_space.sample() # Sample an action - a3 = env3.action_space.sample() - next_obs1, _, _, _, _ = env1.step( - a1 - ) # Step the environoment with the sampled random action - next_obs3, _, _, _, _ = env3.step(a3) - - assert not ( - next_obs1[-3:] == next_obs3[-3:] - ).all() # 2 envs initialized with different seeds will have different goals - assert not ( - next_obs1[-3:] == np.zeros(3) - ).all() # The env's are goal observable, meaning the goal is not zero'd out - - -def test_seeding_hidden(): - door_open_goal_hidden_cls = ALL_V3_ENVIRONMENTS_GOAL_HIDDEN[ - "door-open-v3-goal-hidden" - ] - - env1 = door_open_goal_hidden_cls(seed=5) - enV3 = door_open_goal_hidden_cls(seed=5) - - env1.reset() # Reset environment - enV3.reset() - a1 = env1.action_space.sample() # Sample an action - a2 = enV3.action_space.sample() - next_obs1, _, _, _, _ = env1.step( - a1 - ) # Step the environoment with the sampled random action - next_obs2, _, _, _, _ = enV3.step(a2) - assert ( - env1._target_pos == enV3._target_pos - ).all() # 2 envs initialized with the same seed will have the same goal - assert (next_obs2[-3:] == np.zeros(3)).all() and ( - next_obs1[-3] == np.zeros(3) - ).all() # The env's are goal observable, meaning the goal is zero'd out - - env3 = door_open_goal_hidden_cls( - seed=10 - ) # Construct an environment with a different seed - env1.reset() # Reset environment - env3.reset() - a1 = env1.action_space.sample() # Sample an action - a3 = env3.action_space.sample() - next_obs1, _, _, _, _ = env1.step( - a1 - ) # Step the environoment with the sampled random action - next_obs3, _, _, _, _ = env3.step(a3) - - assert not ( - env1._target_pos[-3:] == env3._target_pos[-3:] - ).all() # 2 envs initialized with different seeds will have different goals - assert (next_obs1[-3:] == np.zeros(3)).all() and ( - np.zeros(3) == next_obs3[-3:] - ).all() # The env's are goal observable, meaning the goal is zero'd out diff --git a/tests/metaworld/__init__.py b/tests/metaworld/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/metaworld/envs/__init__.py b/tests/metaworld/envs/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/metaworld/envs/mujoco/__init__.py b/tests/metaworld/envs/mujoco/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/metaworld/envs/mujoco/sawyer_xyz/__init__.py b/tests/metaworld/envs/mujoco/sawyer_xyz/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/metaworld/envs/mujoco/sawyer_xyz/helpers.py b/tests/metaworld/envs/mujoco/sawyer_xyz/helpers.py deleted file mode 100644 index 5d0f1ff84..000000000 --- a/tests/metaworld/envs/mujoco/sawyer_xyz/helpers.py +++ /dev/null @@ -1,33 +0,0 @@ -import numpy as np - - -def step_env(env, max_path_length=100, iterations=1, render=True): - """Step env helper.""" - for _ in range(iterations): - obs = env.reset()[0] - for _ in range(max_path_length): - next_obs, _, terminated, truncated, info = env.step( - env.action_space.sample() - ) - if env._partially_observable: - assert (next_obs[-3:] == np.zeros(3)).all() - else: - assert (next_obs[-3:] == env._get_pos_goal()).all() - assert (next_obs[:3] == env.get_endeff_pos()).all() - internal_obs = env._get_pos_objects() - internal_quat = env._get_quat_objects() - assert (next_obs[4:7] == internal_obs[:3]).all() - assert (next_obs[7:11] == internal_quat[:4]).all() - if internal_obs.shape == (6,): - assert internal_quat.shape == (8,) - assert (next_obs[11:14] == internal_obs[3:]).all() - assert (next_obs[14:18] == internal_quat[4:]).all() - else: - assert (next_obs[11:14] == np.zeros(3)).all() - assert (next_obs[14:18] == np.zeros(4)).all() - assert (obs[:18] == next_obs[18:-3]).all() - obs = next_obs - if render: - env.render() - if terminated or truncated: - break diff --git a/tests/metaworld/envs/mujoco/sawyer_xyz/test_sawyer_xyz_env.py b/tests/metaworld/envs/mujoco/sawyer_xyz/test_sawyer_xyz_env.py deleted file mode 100644 index c4610256b..000000000 --- a/tests/metaworld/envs/mujoco/sawyer_xyz/test_sawyer_xyz_env.py +++ /dev/null @@ -1,45 +0,0 @@ -import random - -import numpy as np - -import metaworld - - -def test_reset_returns_same_obj_and_goal(): - benchmark = metaworld.MT50() - env_dict = benchmark.train_classes - tasks = benchmark.train_tasks - initial_obj_poses = {name: [] for name in env_dict.keys()} - goal_poses = {name: [] for name in env_dict.keys()} - - # Execute rollout for each environment in benchmark. - for env_name, env_cls in env_dict.items(): - # Create environment and set task. - env = env_cls() - env_tasks = [t for t in tasks if t.env_name == env_name] - env.set_task(random.choice(env_tasks)) - - # Step through environment for a fixed number of episodes. - for _ in range(2): - # Reset environment and extract initial object position. - obs, info = env.reset() - goal = obs[-3:] - goal_poses[env_name].append(goal) - initial_obj_pos = obs[3:9] - initial_obj_poses[env_name].append(initial_obj_pos) - - # Display initial object positions and find environments with non-unique positions. - violating_envs_obs = [] - for env_name, task_initial_pos in initial_obj_poses.items(): - if len(np.unique(np.array(task_initial_pos), axis=0)) > 1 and not np.allclose( - task_initial_pos[0], task_initial_pos[1], rtol=1e-1, atol=1e-1 - ): - violating_envs_obs.append(env_name) - violating_envs_goals = [] - for env_name, target_pos in goal_poses.items(): - if len(np.unique(np.array(target_pos), axis=0)) > 1 and not np.allclose( - target_pos[0], target_pos[1], rtol=1e-2, atol=1e-3 - ): - violating_envs_goals.append(env_name) - assert not violating_envs_obs - assert not violating_envs_goals diff --git a/tests/metaworld/envs/mujoco/sawyer_xyz/test_scripted_policies.py b/tests/metaworld/envs/mujoco/sawyer_xyz/test_scripted_policies.py deleted file mode 100644 index 96c50cd27..000000000 --- a/tests/metaworld/envs/mujoco/sawyer_xyz/test_scripted_policies.py +++ /dev/null @@ -1,35 +0,0 @@ -import random - -import numpy as np -import pytest - -from metaworld import MT1 -from metaworld.policies import ENV_POLICY_MAP - - -@pytest.mark.parametrize("env_name", MT1.ENV_NAMES) -def test_policy(env_name): - SEED = 42 - random.seed(SEED) - np.random.random(SEED) - - mt1 = MT1(env_name, seed=SEED) - env = mt1.train_classes[env_name]() - env.seed(SEED) - p = ENV_POLICY_MAP[env_name]() - completed = 0 - for task in mt1.train_tasks: - env.set_task(task) - obs, info = env.reset() - done = False - count = 0 - while count < 500 and not done: - count += 1 - a = p.get_action(obs) - next_obs, _, trunc, termn, info = env.step(a) - done = trunc or termn - obs = next_obs - if int(info["success"]) == 1: - completed += 1 - break - assert (float(completed) / 50) >= 0.80 diff --git a/tests/metaworld/envs/mujoco/sawyer_xyz/test_seeded_rand_vec.py b/tests/metaworld/envs/mujoco/sawyer_xyz/test_seeded_rand_vec.py deleted file mode 100644 index 48c679143..000000000 --- a/tests/metaworld/envs/mujoco/sawyer_xyz/test_seeded_rand_vec.py +++ /dev/null @@ -1,27 +0,0 @@ -import random - -import numpy as np -import pytest - -from metaworld.env_dict import ALL_V3_ENVIRONMENTS_GOAL_OBSERVABLE - - -@pytest.mark.parametrize("env_name", sorted(ALL_V3_ENVIRONMENTS_GOAL_OBSERVABLE.keys())) -def test_observations_match(env_name): - seed = random.randrange(1000) - env1 = ALL_V3_ENVIRONMENTS_GOAL_OBSERVABLE[env_name](seed=seed) - env1.seeded_rand_vec = True - enV3 = ALL_V3_ENVIRONMENTS_GOAL_OBSERVABLE[env_name](seed=seed) - enV3.seeded_rand_vec = True - - (obs1, _), (obs2, _) = env1.reset(), enV3.reset() - assert (obs1 == obs2).all() - - for i in range(env1.max_path_length): - a = np.random.uniform(low=-1, high=-1, size=4) - obs1, r1, done1, _, _ = env1.step(a) - obs2, r2, done2, _, _ = enV3.step(a) - assert (obs1 == obs2).all() - assert r1 == r2 - assert not done1 - assert not done2 diff --git a/tests/metaworld/envs/mujoco/sawyer_xyz/utils.py b/tests/metaworld/envs/mujoco/sawyer_xyz/utils.py deleted file mode 100644 index 2aa63e8cd..000000000 --- a/tests/metaworld/envs/mujoco/sawyer_xyz/utils.py +++ /dev/null @@ -1,90 +0,0 @@ -import numpy as np - - -def trajectory_summary( - env, policy, act_noise_pct, iters=500, render=False, end_on_success=True -): - """Tests whether a given policy solves an environment - Args: - env (metaworld.envs.MujocoEnv): Environment to test - policy (metaworld.policies.policies.Policy): Policy that's supposed to - succeed in env - act_noise_pct (np.ndarray): Decimal value(s) indicating std deviation of - the noise as a % of action space - render (bool): Whether to render the env in a GUI - end_on_success (bool): Whether to stop stepping after first success - Returns: - (bool, np.ndarray, np.ndarray, int): Success flag, Rewards, Returns, - Index of first success - """ - success = False - first_success = 0 - rewards = [] - - for t, (r, done, info) in enumerate( - trajectory_generator(env, policy, act_noise_pct, render) - ): - rewards.append(r) - assert set(info.keys()) == { - "success", - "near_object", - "grasp_success", - "grasp_reward", - "in_place_reward", - "obj_to_target", - "unscaled_reward", - } - success |= bool(info["success"]) - if not success: - first_success = t - if (success or done) and end_on_success: - break - - rewards = np.array(rewards) - returns = np.cumsum(rewards) - - return success, rewards, returns, first_success - - -def trajectory_generator(env, policy, act_noise_pct, render=False): - """Tests whether a given policy solves an environment - Args: - env (metaworld.envs.MujocoEnv): Environment to test - policy (metaworld.policies.policies.Policy): Policy that's supposed to - succeed in env - act_noise_pct (np.ndarray): Decimal value(s) indicating std deviation of - the noise as a % of action space - render (bool): Whether to render the env in a GUI - Yields: - (float, bool, dict): Reward, Done flag, Info dictionary - """ - action_space_ptp = env.action_space.high - env.action_space.low - env._partially_observable = True - env.reset() - env.reset_model() - o, info = env.reset() - assert o.shape == env.observation_space.shape - assert env.observation_space.contains(o), obs_space_error_text(env, o) - last_info = None - print(act_noise_pct * action_space_ptp) - for _ in range(env.max_path_length): - a = policy.get_action(o) - a = np.random.normal(a, act_noise_pct * action_space_ptp) - - o, r, terminated, truncated, info = env.step(a) - done = terminated or truncated - assert env.observation_space.contains(o), obs_space_error_text(env, o) - last_info = info - if render: - env.render() - if done: - break - return last_info - - -def obs_space_error_text(env, obs): - return "Obs Out of Bounds\n\tlow: {}, \n\tobs: {}, \n\thigh: {}".format( - env.observation_space.low[[0, 1, 2, -3, -2, -1]], - obs[[0, 1, 2, -3, -2, -1]], - env.observation_space.high[[0, 1, 2, -3, -2, -1]], - ) diff --git a/tests/metaworld/test_gym_make.py b/tests/metaworld/test_gym_make.py deleted file mode 100644 index 00c72a071..000000000 --- a/tests/metaworld/test_gym_make.py +++ /dev/null @@ -1,204 +0,0 @@ -from __future__ import annotations - -import random -from typing import Literal - -import gymnasium as gym -import numpy as np -import pytest - -import metaworld # noqa: F401 -from metaworld import _N_GOALS, SawyerXYZEnv -from metaworld.env_dict import ( - ALL_V3_ENVIRONMENTS, - ALL_V3_ENVIRONMENTS_GOAL_HIDDEN, - ALL_V3_ENVIRONMENTS_GOAL_OBSERVABLE, - ML10_V3, - ML45_V3, - MT10_V3, - MT50_V3, - EnvDict, - TrainTestEnvDict, -) - - -def _get_task_names( - envs: gym.vector.SyncVectorEnv | gym.vector.AsyncVectorEnv, -) -> list[str]: - metaworld_cls_to_task_name = {v.__name__: k for k, v in ALL_V3_ENVIRONMENTS.items()} - return [ - metaworld_cls_to_task_name[task_name] - for task_name in envs.get_attr("task_name") - ] - - -@pytest.mark.parametrize("benchmark,env_dict", (("MT10", MT10_V3), ("MT50", MT50_V3))) -@pytest.mark.parametrize("vector_strategy", ("sync", "async")) -def test_mt_benchmarks(benchmark: str, env_dict: EnvDict, vector_strategy: str): - SEED = 42 - random.seed(SEED) - np.random.seed(SEED) - - max_episode_steps = 10 - - envs = gym.make_vec( - f"Meta-World/{benchmark}", - vector_strategy=vector_strategy, - seed=SEED, - use_one_hot=True, - max_episode_steps=max_episode_steps, - ) - - # Assert vec is correct - expected_vectorisation = getattr( - gym.vector, f"{vector_strategy.capitalize()}VectorEnv" - ) - assert isinstance(envs, expected_vectorisation) - - # Assert envs are correct - task_names = _get_task_names(envs) - assert envs.num_envs == len(env_dict.keys()) - assert set(task_names) == set(env_dict.keys()) - - # Assert every env has N_GOALS goals - envs_tasks = envs.get_attr("tasks") - for env_tasks in envs_tasks: - assert len(env_tasks) == _N_GOALS - - # Test wrappers: one hot obs, task sampling, max path length - obs, _ = envs.reset() - original_vecs = envs.get_attr("_last_rand_vec") - - has_truncated = False - for _ in range(max_episode_steps + 1): - obs, _, _, truncated, _ = envs.step(envs.action_space.sample()) - print(obs) - env_one_hots = obs[:, -envs.num_envs :] - env_ids = np.argmax(env_one_hots, axis=1) - assert set(env_ids) == set(range(envs.num_envs)) - - if any(truncated): - has_truncated = True - - assert has_truncated - - new_vecs = envs.get_attr("_last_rand_vec") - task_has_changed = False - for og_vec, new_vec in zip(original_vecs, new_vecs): - if np.any(og_vec != new_vec): - task_has_changed = True - assert task_has_changed - - partially_observable = all(envs.get_attr("_partially_observable")) - assert not partially_observable - - -@pytest.mark.parametrize("env_name", ALL_V3_ENVIRONMENTS.keys()) -def test_mt1(env_name: str): - metaworld_cls_to_task_name = {v.__name__: k for k, v in ALL_V3_ENVIRONMENTS.items()} - env = gym.make("Meta-World/MT1", env_name=env_name) - assert isinstance(env.unwrapped, SawyerXYZEnv) - assert len(env.get_wrapper_attr("tasks")) == _N_GOALS - assert metaworld_cls_to_task_name[env.unwrapped.task_name] == env_name - - env.reset() - assert not env.unwrapped._partially_observable - - -@pytest.mark.parametrize("env_name", ALL_V3_ENVIRONMENTS_GOAL_HIDDEN.keys()) -def test_goal_hidden(env_name: str): - env = gym.make("Meta-World/goal_hidden", env_name=env_name, seed=None) - assert isinstance(env.unwrapped, SawyerXYZEnv) - - env.reset() - assert env.unwrapped._partially_observable - - -@pytest.mark.parametrize("env_name", ALL_V3_ENVIRONMENTS_GOAL_OBSERVABLE.keys()) -def test_goal_observable(env_name: str): - env = gym.make("Meta-World/goal_observable", env_name=env_name, seed=None) - assert isinstance(env.unwrapped, SawyerXYZEnv) - - env.reset() - assert not env.unwrapped._partially_observable - - -@pytest.mark.parametrize("env_name", ALL_V3_ENVIRONMENTS.keys()) -@pytest.mark.parametrize("split", ("train", "test")) -@pytest.mark.parametrize("vector_strategy", ("sync", "async")) -def test_ml1(env_name, split, vector_strategy): - meta_batch_size = 10 - max_episode_steps = 10 - - envs = gym.make_vec( - f"Meta-World/ML1-{split}", - env_name=env_name, - vector_strategy=vector_strategy, - meta_batch_size=meta_batch_size, - max_episode_steps=max_episode_steps, - ) - assert envs.num_envs == meta_batch_size - task_names = _get_task_names(envs) - assert all([task_name == env_name for task_name in task_names]) - - # Assert vec is correct - expected_vectorisation = getattr( - gym.vector, f"{vector_strategy.capitalize()}VectorEnv" - ) - assert isinstance(envs, expected_vectorisation) - - envs_tasks = envs.get_attr("tasks") - total_tasks = sum([len(env_tasks) for env_tasks in envs_tasks]) - assert total_tasks == _N_GOALS - - partially_observable = all(envs.get_attr("_partially_observable")) - assert partially_observable - - -@pytest.mark.parametrize("benchmark,env_dict", (("ML10", ML10_V3), ("ML45", ML45_V3))) -@pytest.mark.parametrize("split", ("train", "test")) -@pytest.mark.parametrize("vector_strategy", ("sync", "async")) -def test_ml_benchmarks( - benchmark: str, - env_dict: TrainTestEnvDict, - split: Literal["train", "test"], - vector_strategy: str, -): - meta_batch_size = 20 if benchmark != "ML45" else 45 - total_tasks_per_cls = _N_GOALS - if benchmark == "ML45": - total_tasks_per_cls = 45 - elif benchmark == "ML10" and split == "test": - total_tasks_per_cls = 40 - max_episode_steps = 10 - - envs = gym.make_vec( - f"Meta-World/{benchmark}-{split}", - vector_strategy=vector_strategy, - meta_batch_size=meta_batch_size, - max_episode_steps=max_episode_steps, - total_tasks_per_cls=total_tasks_per_cls, - ) - assert envs.num_envs == meta_batch_size - task_names = _get_task_names(envs) # type: ignore - assert set(task_names) == set(env_dict[split].keys()) - - # Assert vec is correct - expected_vectorisation = getattr( - gym.vector, f"{vector_strategy.capitalize()}VectorEnv" - ) - assert isinstance(envs, expected_vectorisation) - - envs_tasks = envs.get_attr("tasks") - tasks_per_env = {} - for task in env_dict[split].keys(): - tasks_per_env[task] = 0 - - for env_tasks, env_name in zip(envs_tasks, task_names): - tasks_per_env[env_name] += len(env_tasks) - - for task in env_dict[split].keys(): - assert tasks_per_env[task] == total_tasks_per_cls - - partially_observable = all(envs.get_attr("_partially_observable")) - assert partially_observable diff --git a/tests/helpers.py b/tests_old/helpers.py similarity index 65% rename from tests/helpers.py rename to tests_old/helpers.py index 06ad4063d..9142d6a62 100644 --- a/tests/helpers.py +++ b/tests_old/helpers.py @@ -1,15 +1,24 @@ import numpy as np -def step_env(env, max_path_length=100, iterations=1, render=True): - """Step env helper.""" - for _ in range(iterations): +def check_multiple_env_steps(env, max_episode_steps=100, episodes=1, render=True): + """ + Resets and steps through the environment multiple times, checking that + the observations are consistent with the internal state of the environment. + + Args: + env: The environment to test. + max_episode_steps: Maximum number of steps per episode. + iterations: Number of episodes to run. To test consistency across resets. + render: Whether to render the environment during stepping. + """ + for _ in range(episodes): obs, info = env.reset() - for _ in range(max_path_length): + for _ in range(max_episode_steps): next_obs, _, terminated, truncated, info = env.step( env.action_space.sample() ) - if env._partially_observable: + if not env._goal_observable: assert (next_obs[-3:] == np.zeros(3)).all() else: assert (next_obs[-3:] == env._get_pos_goal()).all() diff --git a/tests/metaworld/test_evaluation.py b/tests_old/metaworld/test_evaluation.py similarity index 72% rename from tests/metaworld/test_evaluation.py rename to tests_old/metaworld/test_evaluation.py index f67e587fd..2d2afa9e7 100644 --- a/tests/metaworld/test_evaluation.py +++ b/tests_old/metaworld/test_evaluation.py @@ -10,6 +10,7 @@ import metaworld # noqa: F401 from metaworld import evaluation from metaworld.policies import ENV_POLICY_MAP +from metaworld.env_dict import ML_BENCHMARKS class ScriptedPolicyAgent(evaluation.MetaLearningAgent): @@ -19,8 +20,9 @@ def __init__( num_rollouts: int | None = None, max_episode_steps: int | None = None, ): - env_task_names = evaluation._get_task_names(envs) - self.policies = [ENV_POLICY_MAP[task]() for task in env_task_names] # type: ignore + env_names = evaluation.envs_get_env_names(envs) + self.policies = [ENV_POLICY_MAP[env_name]() + for env_name in env_names] # type: ignore self.num_rollouts = num_rollouts self.max_episode_steps = max_episode_steps self.adapt_calls = 0 @@ -39,7 +41,8 @@ def adapt_action( actions: list[npt.NDArray[np.float32]] = [] num_envs = len(self.policies) for env_idx in range(num_envs): - actions.append(self.policies[env_idx].get_action(observations[env_idx])) + actions.append(self.policies[env_idx].get_action( + observations[env_idx])) stacked_actions = np.stack(actions, axis=0, dtype=np.float64) return stacked_actions, { "log_probs": np.ones((num_envs,)), @@ -59,7 +62,8 @@ def eval_action( actions: list[npt.NDArray[np.float32]] = [] num_envs = len(self.policies) for env_idx in range(num_envs): - actions.append(self.policies[env_idx].get_action(observations[env_idx])) + actions.append(self.policies[env_idx].get_action( + observations[env_idx])) stacked_actions = np.stack(actions, axis=0, dtype=np.float64) return stacked_actions @@ -67,21 +71,6 @@ def adapt(self) -> None: self.adapt_calls += 1 -class RemovePartialObservabilityWrapper(gym.vector.VectorWrapper): - def get_attr(self, name): - return self.env.get_attr(name) - - def set_attr(self, name, values): - return self.env.set_attr(name, values) - - def call(self, name, *args, **kwargs): - return self.env.call(name, *args, **kwargs) - - def step(self, actions): - self.env.set_attr("_partially_observable", False) - return super().step(actions) - - def test_evaluation(): SEED = 42 max_episode_steps = 300 # To speed up the test @@ -95,18 +84,33 @@ def test_evaluation(): max_episode_steps=max_episode_steps, vector_strategy="async", ) + num_envs = envs.num_envs agent = ScriptedPolicyAgent(envs) mean_success_rate, mean_returns, success_rate_per_task, _ = evaluation.evaluation( agent, envs, num_episodes=num_episodes ) + envs.close() assert isinstance(mean_returns, float) - assert mean_success_rate >= 0.80 - assert len(success_rate_per_task) == envs.num_envs - assert np.all(np.array(list(success_rate_per_task.values())) >= 0.80) + assert len(success_rate_per_task) == num_envs + worst_accepted_fail_rate = 0.8 + failed_envs_names = [] + failed_envs_rates = [] + for task_name, success_rate in success_rate_per_task.items(): + if success_rate < worst_accepted_fail_rate: + failed_envs_names.append(task_name) + failed_envs_rates.append(success_rate) + if len(failed_envs_names) > 0: + print( + f"The following environments failed the success rate threshold of {worst_accepted_fail_rate*100}%:" + ) + for name, rate in zip(failed_envs_names, failed_envs_rates): + print(f"- {name}: {rate*100}% success rate") + assert False, "Some environments did not meet the success rate threshold." + assert mean_success_rate >= worst_accepted_fail_rate # @pytest.mark.skip -@pytest.mark.parametrize("benchmark", ("ML10", "ML45")) +@pytest.mark.parametrize("benchmark", ML_BENCHMARKS.keys()) def test_metalearning_evaluation(benchmark): SEED = 42 @@ -114,7 +118,8 @@ def test_metalearning_evaluation(benchmark): meta_batch_size = 10 # Number of parallel envs adaptation_steps = 2 # Number of adaptation iterations - adaptation_episodes = 2 # Number of train episodes per task in meta_batch_size per adaptation iteration + # Number of train episodes per task in meta_batch_size per adaptation iteration + adaptation_episodes = 2 num_evals = 50 # Number of different task vectors tested for each task num_episodes = 1 # Number of test episodes per task vector @@ -143,7 +148,8 @@ def test_metalearning_evaluation(benchmark): ) assert isinstance(mean_returns, float) assert mean_success_rate >= 0.80 - assert len(success_rate_per_task) == len(set(evaluation._get_task_names(envs))) + assert len(success_rate_per_task) == len( + set(evaluation.envs_get_env_names(envs))) assert np.all(np.array(list(success_rate_per_task.values())) >= 0.80) assert agent.adapt_calls == num_evals * adaptation_steps assert ( diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..33b165a5c --- /dev/null +++ b/uv.lock @@ -0,0 +1,1407 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version < '3.11'", +] + +[[package]] +name = "absl-py" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/64/c7/8de93764ad66968d19329a7e0c147a2bb3c7054c554d4a119111b8f9440f/absl_py-2.4.0.tar.gz", hash = "sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4", size = 116543, upload-time = "2026-01-28T10:17:05.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl", hash = "sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d", size = 135750, upload-time = "2026-01-28T10:17:04.19Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "black" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/1b/523329e713f965ad0ea2b7a047eeb003007792a0353622ac7a8cb2ee6fef/black-26.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ca699710dece84e3ebf6e92ee15f5b8f72870ef984bf944a57a777a48357c168", size = 1849661, upload-time = "2026-01-18T04:59:12.425Z" }, + { url = "https://files.pythonhosted.org/packages/14/82/94c0640f7285fa71c2f32879f23e609dd2aa39ba2641f395487f24a578e7/black-26.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e8e75dabb6eb83d064b0db46392b25cabb6e784ea624219736e8985a6b3675d", size = 1689065, upload-time = "2026-01-18T04:59:13.993Z" }, + { url = "https://files.pythonhosted.org/packages/f0/78/474373cbd798f9291ed8f7107056e343fd39fef42de4a51c7fd0d360840c/black-26.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb07665d9a907a1a645ee41a0df8a25ffac8ad9c26cdb557b7b88eeeeec934e0", size = 1751502, upload-time = "2026-01-18T04:59:15.971Z" }, + { url = "https://files.pythonhosted.org/packages/29/89/59d0e350123f97bc32c27c4d79563432d7f3530dca2bff64d855c178af8b/black-26.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7ed300200918147c963c87700ccf9966dceaefbbb7277450a8d646fc5646bf24", size = 1400102, upload-time = "2026-01-18T04:59:17.8Z" }, + { url = "https://files.pythonhosted.org/packages/e1/bc/5d866c7ae1c9d67d308f83af5462ca7046760158bbf142502bad8f22b3a1/black-26.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c5b7713daea9bf943f79f8c3b46f361cc5229e0e604dcef6a8bb6d1c37d9df89", size = 1207038, upload-time = "2026-01-18T04:59:19.543Z" }, + { url = "https://files.pythonhosted.org/packages/30/83/f05f22ff13756e1a8ce7891db517dbc06200796a16326258268f4658a745/black-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5", size = 1831956, upload-time = "2026-01-18T04:59:21.38Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/b2c570550e39bedc157715e43927360312d6dd677eed2cc149a802577491/black-26.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68", size = 1672499, upload-time = "2026-01-18T04:59:23.257Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d7/990d6a94dc9e169f61374b1c3d4f4dd3037e93c2cc12b6f3b12bc663aa7b/black-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14", size = 1735431, upload-time = "2026-01-18T04:59:24.729Z" }, + { url = "https://files.pythonhosted.org/packages/36/1c/cbd7bae7dd3cb315dfe6eeca802bb56662cc92b89af272e014d98c1f2286/black-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c", size = 1400468, upload-time = "2026-01-18T04:59:27.381Z" }, + { url = "https://files.pythonhosted.org/packages/59/b1/9fe6132bb2d0d1f7094613320b56297a108ae19ecf3041d9678aec381b37/black-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4", size = 1207332, upload-time = "2026-01-18T04:59:28.711Z" }, + { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" }, + { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" }, + { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" }, + { url = "https://files.pythonhosted.org/packages/49/f9/71c161c4c7aa18bdda3776b66ac2dc07aed62053c7c0ff8bbda8c2624fe2/black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", size = 1406466, upload-time = "2026-01-18T04:59:35.177Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8b/a7b0f974e473b159d0ac1b6bcefffeb6bec465898a516ee5cc989503cbc7/black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", size = 1216393, upload-time = "2026-01-18T04:59:37.18Z" }, + { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" }, + { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" }, + { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" }, + { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/49/349848445b0e53660e258acbcc9b0d014895b6739237920886672240f84b/coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3", size = 826523, upload-time = "2026-01-25T13:00:04.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/2d/63e37369c8e81a643afe54f76073b020f7b97ddbe698c5c944b51b0a2bc5/coverage-7.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4af3b01763909f477ea17c962e2cca8f39b350a4e46e3a30838b2c12e31b81b", size = 218842, upload-time = "2026-01-25T12:57:15.3Z" }, + { url = "https://files.pythonhosted.org/packages/57/06/86ce882a8d58cbcb3030e298788988e618da35420d16a8c66dac34f138d0/coverage-7.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36393bd2841fa0b59498f75466ee9bdec4f770d3254f031f23e8fd8e140ffdd2", size = 219360, upload-time = "2026-01-25T12:57:17.572Z" }, + { url = "https://files.pythonhosted.org/packages/cd/84/70b0eb1ee19ca4ef559c559054c59e5b2ae4ec9af61398670189e5d276e9/coverage-7.13.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9cc7573518b7e2186bd229b1a0fe24a807273798832c27032c4510f47ffdb896", size = 246123, upload-time = "2026-01-25T12:57:19.087Z" }, + { url = "https://files.pythonhosted.org/packages/35/fb/05b9830c2e8275ebc031e0019387cda99113e62bb500ab328bb72578183b/coverage-7.13.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca9566769b69a5e216a4e176d54b9df88f29d750c5b78dbb899e379b4e14b30c", size = 247930, upload-time = "2026-01-25T12:57:20.929Z" }, + { url = "https://files.pythonhosted.org/packages/81/aa/3f37858ca2eed4f09b10ca3c6ddc9041be0a475626cd7fd2712f4a2d526f/coverage-7.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c9bdea644e94fd66d75a6f7e9a97bb822371e1fe7eadae2cacd50fcbc28e4dc", size = 249804, upload-time = "2026-01-25T12:57:22.904Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b3/c904f40c56e60a2d9678a5ee8df3d906d297d15fb8bec5756c3b0a67e2df/coverage-7.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5bd447332ec4f45838c1ad42268ce21ca87c40deb86eabd59888859b66be22a5", size = 246815, upload-time = "2026-01-25T12:57:24.314Z" }, + { url = "https://files.pythonhosted.org/packages/41/91/ddc1c5394ca7fd086342486440bfdd6b9e9bda512bf774599c7c7a0081e0/coverage-7.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7c79ad5c28a16a1277e1187cf83ea8dafdcc689a784228a7d390f19776db7c31", size = 247843, upload-time = "2026-01-25T12:57:26.544Z" }, + { url = "https://files.pythonhosted.org/packages/87/d2/cdff8f4cd33697883c224ea8e003e9c77c0f1a837dc41d95a94dd26aad67/coverage-7.13.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:76e06ccacd1fb6ada5d076ed98a8c6f66e2e6acd3df02819e2ee29fd637b76ad", size = 245850, upload-time = "2026-01-25T12:57:28.507Z" }, + { url = "https://files.pythonhosted.org/packages/f5/42/e837febb7866bf2553ab53dd62ed52f9bb36d60c7e017c55376ad21fbb05/coverage-7.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:49d49e9a5e9f4dc3d3dac95278a020afa6d6bdd41f63608a76fa05a719d5b66f", size = 246116, upload-time = "2026-01-25T12:57:30.16Z" }, + { url = "https://files.pythonhosted.org/packages/09/b1/4a3f935d7df154df02ff4f71af8d61298d713a7ba305d050ae475bfbdde2/coverage-7.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed2bce0e7bfa53f7b0b01c722da289ef6ad4c18ebd52b1f93704c21f116360c8", size = 246720, upload-time = "2026-01-25T12:57:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/538a6fd44c515f1c5197a3f078094cbaf2ce9f945df5b44e29d95c864bff/coverage-7.13.2-cp310-cp310-win32.whl", hash = "sha256:1574983178b35b9af4db4a9f7328a18a14a0a0ce76ffaa1c1bacb4cc82089a7c", size = 221465, upload-time = "2026-01-25T12:57:33.511Z" }, + { url = "https://files.pythonhosted.org/packages/5e/09/4b63a024295f326ec1a40ec8def27799300ce8775b1cbf0d33b1790605c4/coverage-7.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:a360a8baeb038928ceb996f5623a4cd508728f8f13e08d4e96ce161702f3dd99", size = 222397, upload-time = "2026-01-25T12:57:34.927Z" }, + { url = "https://files.pythonhosted.org/packages/6c/01/abca50583a8975bb6e1c59eff67ed8e48bb127c07dad5c28d9e96ccc09ec/coverage-7.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:060ebf6f2c51aff5ba38e1f43a2095e087389b1c69d559fde6049a4b0001320e", size = 218971, upload-time = "2026-01-25T12:57:36.953Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0e/b6489f344d99cd1e5b4d5e1be52dfd3f8a3dc5112aa6c33948da8cabad4e/coverage-7.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1ea8ca9db5e7469cd364552985e15911548ea5b69c48a17291f0cac70484b2e", size = 219473, upload-time = "2026-01-25T12:57:38.934Z" }, + { url = "https://files.pythonhosted.org/packages/17/11/db2f414915a8e4ec53f60b17956c27f21fb68fcf20f8a455ce7c2ccec638/coverage-7.13.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b780090d15fd58f07cf2011943e25a5f0c1c894384b13a216b6c86c8a8a7c508", size = 249896, upload-time = "2026-01-25T12:57:40.365Z" }, + { url = "https://files.pythonhosted.org/packages/80/06/0823fe93913663c017e508e8810c998c8ebd3ec2a5a85d2c3754297bdede/coverage-7.13.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:88a800258d83acb803c38175b4495d293656d5fac48659c953c18e5f539a274b", size = 251810, upload-time = "2026-01-25T12:57:42.045Z" }, + { url = "https://files.pythonhosted.org/packages/61/dc/b151c3cc41b28cdf7f0166c5fa1271cbc305a8ec0124cce4b04f74791a18/coverage-7.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6326e18e9a553e674d948536a04a80d850a5eeefe2aae2e6d7cf05d54046c01b", size = 253920, upload-time = "2026-01-25T12:57:44.026Z" }, + { url = "https://files.pythonhosted.org/packages/2d/35/e83de0556e54a4729a2b94ea816f74ce08732e81945024adee46851c2264/coverage-7.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59562de3f797979e1ff07c587e2ac36ba60ca59d16c211eceaa579c266c5022f", size = 250025, upload-time = "2026-01-25T12:57:45.624Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/af2eb9c3926ce3ea0d58a0d2516fcbdacf7a9fc9559fe63076beaf3f2596/coverage-7.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27ba1ed6f66b0e2d61bfa78874dffd4f8c3a12f8e2b5410e515ab345ba7bc9c3", size = 251612, upload-time = "2026-01-25T12:57:47.713Z" }, + { url = "https://files.pythonhosted.org/packages/26/62/5be2e25f3d6c711d23b71296f8b44c978d4c8b4e5b26871abfc164297502/coverage-7.13.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8be48da4d47cc68754ce643ea50b3234557cbefe47c2f120495e7bd0a2756f2b", size = 249670, upload-time = "2026-01-25T12:57:49.378Z" }, + { url = "https://files.pythonhosted.org/packages/b3/51/400d1b09a8344199f9b6a6fc1868005d766b7ea95e7882e494fa862ca69c/coverage-7.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2a47a4223d3361b91176aedd9d4e05844ca67d7188456227b6bf5e436630c9a1", size = 249395, upload-time = "2026-01-25T12:57:50.86Z" }, + { url = "https://files.pythonhosted.org/packages/e0/36/f02234bc6e5230e2f0a63fd125d0a2093c73ef20fdf681c7af62a140e4e7/coverage-7.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6f141b468740197d6bd38f2b26ade124363228cc3f9858bd9924ab059e00059", size = 250298, upload-time = "2026-01-25T12:57:52.287Z" }, + { url = "https://files.pythonhosted.org/packages/b0/06/713110d3dd3151b93611c9cbfc65c15b4156b44f927fced49ac0b20b32a4/coverage-7.13.2-cp311-cp311-win32.whl", hash = "sha256:89567798404af067604246e01a49ef907d112edf2b75ef814b1364d5ce267031", size = 221485, upload-time = "2026-01-25T12:57:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/16/0c/3ae6255fa1ebcb7dec19c9a59e85ef5f34566d1265c70af5b2fc981da834/coverage-7.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:21dd57941804ae2ac7e921771a5e21bbf9aabec317a041d164853ad0a96ce31e", size = 222421, upload-time = "2026-01-25T12:57:55.433Z" }, + { url = "https://files.pythonhosted.org/packages/b5/37/fabc3179af4d61d89ea47bd04333fec735cd5e8b59baad44fed9fc4170d7/coverage-7.13.2-cp311-cp311-win_arm64.whl", hash = "sha256:10758e0586c134a0bafa28f2d37dd2cdb5e4a90de25c0fc0c77dabbad46eca28", size = 221088, upload-time = "2026-01-25T12:57:57.41Z" }, + { url = "https://files.pythonhosted.org/packages/46/39/e92a35f7800222d3f7b2cbb7bbc3b65672ae8d501cb31801b2d2bd7acdf1/coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d", size = 219142, upload-time = "2026-01-25T12:58:00.448Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/8bf9e9309c4c996e65c52a7c5a112707ecdd9fbaf49e10b5a705a402bbb4/coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3", size = 219503, upload-time = "2026-01-25T12:58:02.451Z" }, + { url = "https://files.pythonhosted.org/packages/87/93/17661e06b7b37580923f3f12406ac91d78aeed293fb6da0b69cc7957582f/coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99", size = 251006, upload-time = "2026-01-25T12:58:04.059Z" }, + { url = "https://files.pythonhosted.org/packages/12/f0/f9e59fb8c310171497f379e25db060abef9fa605e09d63157eebec102676/coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f", size = 253750, upload-time = "2026-01-25T12:58:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b1/1935e31add2232663cf7edd8269548b122a7d100047ff93475dbaaae673e/coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f", size = 254862, upload-time = "2026-01-25T12:58:07.647Z" }, + { url = "https://files.pythonhosted.org/packages/af/59/b5e97071ec13df5f45da2b3391b6cdbec78ba20757bc92580a5b3d5fa53c/coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa", size = 251420, upload-time = "2026-01-25T12:58:09.309Z" }, + { url = "https://files.pythonhosted.org/packages/3f/75/9495932f87469d013dc515fb0ce1aac5fa97766f38f6b1a1deb1ee7b7f3a/coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce", size = 252786, upload-time = "2026-01-25T12:58:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/6a/59/af550721f0eb62f46f7b8cb7e6f1860592189267b1c411a4e3a057caacee/coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94", size = 250928, upload-time = "2026-01-25T12:58:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b1/21b4445709aae500be4ab43bbcfb4e53dc0811c3396dcb11bf9f23fd0226/coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5", size = 250496, upload-time = "2026-01-25T12:58:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/0f5d89dfe0392990e4f3980adbde3eb34885bc1effb2dc369e0bf385e389/coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b", size = 252373, upload-time = "2026-01-25T12:58:15.976Z" }, + { url = "https://files.pythonhosted.org/packages/01/c9/0cf1a6a57a9968cc049a6b896693faa523c638a5314b1fc374eb2b2ac904/coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41", size = 221696, upload-time = "2026-01-25T12:58:17.517Z" }, + { url = "https://files.pythonhosted.org/packages/4d/05/d7540bf983f09d32803911afed135524570f8c47bb394bf6206c1dc3a786/coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e", size = 222504, upload-time = "2026-01-25T12:58:19.115Z" }, + { url = "https://files.pythonhosted.org/packages/15/8b/1a9f037a736ced0a12aacf6330cdaad5008081142a7070bc58b0f7930cbc/coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894", size = 221120, upload-time = "2026-01-25T12:58:21.334Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f0/3d3eac7568ab6096ff23791a526b0048a1ff3f49d0e236b2af6fb6558e88/coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6", size = 219168, upload-time = "2026-01-25T12:58:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a6/f8b5cfeddbab95fdef4dcd682d82e5dcff7a112ced57a959f89537ee9995/coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc", size = 219537, upload-time = "2026-01-25T12:58:24.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/8d8e6e0c516c838229d1e41cadcec91745f4b1031d4db17ce0043a0423b4/coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f", size = 250528, upload-time = "2026-01-25T12:58:26.567Z" }, + { url = "https://files.pythonhosted.org/packages/8e/78/befa6640f74092b86961f957f26504c8fba3d7da57cc2ab7407391870495/coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1", size = 253132, upload-time = "2026-01-25T12:58:28.251Z" }, + { url = "https://files.pythonhosted.org/packages/9d/10/1630db1edd8ce675124a2ee0f7becc603d2bb7b345c2387b4b95c6907094/coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9", size = 254374, upload-time = "2026-01-25T12:58:30.294Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/0d9381647b1e8e6d310ac4140be9c428a0277330991e0c35bdd751e338a4/coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c", size = 250762, upload-time = "2026-01-25T12:58:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5636dfc9a7c871ee8776af83ee33b4c26bc508ad6cee1e89b6419a366582/coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5", size = 252502, upload-time = "2026-01-25T12:58:33.961Z" }, + { url = "https://files.pythonhosted.org/packages/02/2a/7ff2884d79d420cbb2d12fed6fff727b6d0ef27253140d3cdbbd03187ee0/coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4", size = 250463, upload-time = "2026-01-25T12:58:35.529Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/ba51087db645b6c7261570400fc62c89a16278763f36ba618dc8657a187b/coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c", size = 250288, upload-time = "2026-01-25T12:58:37.226Z" }, + { url = "https://files.pythonhosted.org/packages/03/07/44e6f428551c4d9faf63ebcefe49b30e5c89d1be96f6a3abd86a52da9d15/coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31", size = 252063, upload-time = "2026-01-25T12:58:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/c2/67/35b730ad7e1859dd57e834d1bc06080d22d2f87457d53f692fce3f24a5a9/coverage-7.13.2-cp313-cp313-win32.whl", hash = "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8", size = 221716, upload-time = "2026-01-25T12:58:40.484Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/e5fcf5a97c72f45fc14829237a6550bf49d0ab882ac90e04b12a69db76b4/coverage-7.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb", size = 222522, upload-time = "2026-01-25T12:58:43.247Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/25d7b2f946d239dd2d6644ca2cc060d24f97551e2af13b6c24c722ae5f97/coverage-7.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557", size = 221145, upload-time = "2026-01-25T12:58:45Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f7/080376c029c8f76fadfe43911d0daffa0cbdc9f9418a0eead70c56fb7f4b/coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e", size = 219861, upload-time = "2026-01-25T12:58:46.586Z" }, + { url = "https://files.pythonhosted.org/packages/42/11/0b5e315af5ab35f4c4a70e64d3314e4eec25eefc6dec13be3a7d5ffe8ac5/coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7", size = 220207, upload-time = "2026-01-25T12:58:48.277Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0c/0874d0318fb1062117acbef06a09cf8b63f3060c22265adaad24b36306b7/coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3", size = 261504, upload-time = "2026-01-25T12:58:49.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/5e/1cd72c22ecb30751e43a72f40ba50fcef1b7e93e3ea823bd9feda8e51f9a/coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3", size = 263582, upload-time = "2026-01-25T12:58:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/8acf356707c7a42df4d0657020308e23e5a07397e81492640c186268497c/coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421", size = 266008, upload-time = "2026-01-25T12:58:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/41/41/ea1730af99960309423c6ea8d6a4f1fa5564b2d97bd1d29dda4b42611f04/coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5", size = 260762, upload-time = "2026-01-25T12:58:55.372Z" }, + { url = "https://files.pythonhosted.org/packages/22/fa/02884d2080ba71db64fdc127b311db60e01fe6ba797d9c8363725e39f4d5/coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23", size = 263571, upload-time = "2026-01-25T12:58:57.52Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6b/4083aaaeba9b3112f55ac57c2ce7001dc4d8fa3fcc228a39f09cc84ede27/coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c", size = 261200, upload-time = "2026-01-25T12:58:59.255Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d2/aea92fa36d61955e8c416ede9cf9bf142aa196f3aea214bb67f85235a050/coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f", size = 260095, upload-time = "2026-01-25T12:59:01.066Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ae/04ffe96a80f107ea21b22b2367175c621da920063260a1c22f9452fd7866/coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573", size = 262284, upload-time = "2026-01-25T12:59:02.802Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7a/6f354dcd7dfc41297791d6fb4e0d618acb55810bde2c1fd14b3939e05c2b/coverage-7.13.2-cp313-cp313t-win32.whl", hash = "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343", size = 222389, upload-time = "2026-01-25T12:59:04.563Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d5/080ad292a4a3d3daf411574be0a1f56d6dee2c4fdf6b005342be9fac807f/coverage-7.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47", size = 223450, upload-time = "2026-01-25T12:59:06.677Z" }, + { url = "https://files.pythonhosted.org/packages/88/96/df576fbacc522e9fb8d1c4b7a7fc62eb734be56e2cba1d88d2eabe08ea3f/coverage-7.13.2-cp313-cp313t-win_arm64.whl", hash = "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7", size = 221707, upload-time = "2026-01-25T12:59:08.363Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/1da9e51a0775634b04fcc11eb25c002fc58ee4f92ce2e8512f94ac5fc5bf/coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef", size = 219213, upload-time = "2026-01-25T12:59:11.909Z" }, + { url = "https://files.pythonhosted.org/packages/46/35/b3caac3ebbd10230fea5a33012b27d19e999a17c9285c4228b4b2e35b7da/coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f", size = 219549, upload-time = "2026-01-25T12:59:13.638Z" }, + { url = "https://files.pythonhosted.org/packages/76/9c/e1cf7def1bdc72c1907e60703983a588f9558434a2ff94615747bd73c192/coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5", size = 250586, upload-time = "2026-01-25T12:59:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ba/49/f54ec02ed12be66c8d8897270505759e057b0c68564a65c429ccdd1f139e/coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4", size = 253093, upload-time = "2026-01-25T12:59:17.491Z" }, + { url = "https://files.pythonhosted.org/packages/fb/5e/aaf86be3e181d907e23c0f61fccaeb38de8e6f6b47aed92bf57d8fc9c034/coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27", size = 254446, upload-time = "2026-01-25T12:59:19.752Z" }, + { url = "https://files.pythonhosted.org/packages/28/c8/a5fa01460e2d75b0c853b392080d6829d3ca8b5ab31e158fa0501bc7c708/coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548", size = 250615, upload-time = "2026-01-25T12:59:21.928Z" }, + { url = "https://files.pythonhosted.org/packages/86/0b/6d56315a55f7062bb66410732c24879ccb2ec527ab6630246de5fe45a1df/coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660", size = 252452, upload-time = "2026-01-25T12:59:23.592Z" }, + { url = "https://files.pythonhosted.org/packages/30/19/9bc550363ebc6b0ea121977ee44d05ecd1e8bf79018b8444f1028701c563/coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92", size = 250418, upload-time = "2026-01-25T12:59:25.392Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/580530a31ca2f0cc6f07a8f2ab5460785b02bb11bdf815d4c4d37a4c5169/coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82", size = 250231, upload-time = "2026-01-25T12:59:27.888Z" }, + { url = "https://files.pythonhosted.org/packages/e2/42/dd9093f919dc3088cb472893651884bd675e3df3d38a43f9053656dca9a2/coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892", size = 251888, upload-time = "2026-01-25T12:59:29.636Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a6/0af4053e6e819774626e133c3d6f70fae4d44884bfc4b126cb647baee8d3/coverage-7.13.2-cp314-cp314-win32.whl", hash = "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe", size = 221968, upload-time = "2026-01-25T12:59:31.424Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cc/5aff1e1f80d55862442855517bb8ad8ad3a68639441ff6287dde6a58558b/coverage-7.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859", size = 222783, upload-time = "2026-01-25T12:59:33.118Z" }, + { url = "https://files.pythonhosted.org/packages/de/20/09abafb24f84b3292cc658728803416c15b79f9ee5e68d25238a895b07d9/coverage-7.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6", size = 221348, upload-time = "2026-01-25T12:59:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/b6/60/a3820c7232db63be060e4019017cd3426751c2699dab3c62819cdbcea387/coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b", size = 219950, upload-time = "2026-01-25T12:59:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/fd/37/e4ef5975fdeb86b1e56db9a82f41b032e3d93a840ebaf4064f39e770d5c5/coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417", size = 220209, upload-time = "2026-01-25T12:59:38.339Z" }, + { url = "https://files.pythonhosted.org/packages/54/df/d40e091d00c51adca1e251d3b60a8b464112efa3004949e96a74d7c19a64/coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee", size = 261576, upload-time = "2026-01-25T12:59:40.446Z" }, + { url = "https://files.pythonhosted.org/packages/c5/44/5259c4bed54e3392e5c176121af9f71919d96dde853386e7730e705f3520/coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1", size = 263704, upload-time = "2026-01-25T12:59:42.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/ae9f005827abcbe2c70157459ae86053971c9fa14617b63903abbdce26d9/coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d", size = 266109, upload-time = "2026-01-25T12:59:44.073Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c0/8e279c1c0f5b1eaa3ad9b0fb7a5637fc0379ea7d85a781c0fe0bb3cfc2ab/coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6", size = 260686, upload-time = "2026-01-25T12:59:45.804Z" }, + { url = "https://files.pythonhosted.org/packages/b2/47/3a8112627e9d863e7cddd72894171c929e94491a597811725befdcd76bce/coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a", size = 263568, upload-time = "2026-01-25T12:59:47.929Z" }, + { url = "https://files.pythonhosted.org/packages/92/bc/7ea367d84afa3120afc3ce6de294fd2dcd33b51e2e7fbe4bbfd200f2cb8c/coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04", size = 261174, upload-time = "2026-01-25T12:59:49.717Z" }, + { url = "https://files.pythonhosted.org/packages/33/b7/f1092dcecb6637e31cc2db099581ee5c61a17647849bae6b8261a2b78430/coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f", size = 260017, upload-time = "2026-01-25T12:59:51.463Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/f3d07d4b95fbe1a2ef0958c15da614f7e4f557720132de34d2dc3aa7e911/coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f", size = 262337, upload-time = "2026-01-25T12:59:53.407Z" }, + { url = "https://files.pythonhosted.org/packages/e0/db/b0d5b2873a07cb1e06a55d998697c0a5a540dcefbf353774c99eb3874513/coverage-7.13.2-cp314-cp314t-win32.whl", hash = "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3", size = 222749, upload-time = "2026-01-25T12:59:56.316Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2f/838a5394c082ac57d85f57f6aba53093b30d9089781df72412126505716f/coverage-7.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba", size = 223857, upload-time = "2026-01-25T12:59:58.201Z" }, + { url = "https://files.pythonhosted.org/packages/44/d4/b608243e76ead3a4298824b50922b89ef793e50069ce30316a65c1b4d7ef/coverage-7.13.2-cp314-cp314t-win_arm64.whl", hash = "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c", size = 221881, upload-time = "2026-01-25T13:00:00.449Z" }, + { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "etils" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/a0/522bbff0f3cdd37968f90dd7f26c7aa801ed87f5ba335f156de7f2b88a48/etils-1.13.0.tar.gz", hash = "sha256:a5b60c71f95bcd2d43d4e9fb3dc3879120c1f60472bb5ce19f7a860b1d44f607", size = 106368, upload-time = "2025-07-15T10:29:10.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl", hash = "sha256:d9cd4f40fbe77ad6613b7348a18132cc511237b6c076dbb89105c0b520a4c6bb", size = 170603, upload-time = "2025-07-15T10:29:09.076Z" }, +] + +[package.optional-dependencies] +epath = [ + { name = "fsspec" }, + { name = "importlib-resources" }, + { name = "typing-extensions" }, + { name = "zipp" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "farama-notifications" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/2c/8384832b7a6b1fd6ba95bbdcae26e7137bb3eedc955c42fd5cdcc086cfbf/Farama-Notifications-0.0.4.tar.gz", hash = "sha256:13fceff2d14314cf80703c8266462ebf3733c7d165336eee998fc58e545efd18", size = 2131, upload-time = "2023-02-27T18:28:41.047Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/2c/ffc08c54c05cdce6fbed2aeebc46348dbe180c6d2c541c7af7ba0aa5f5f8/Farama_Notifications-0.0.4-py3-none-any.whl", hash = "sha256:14de931035a41961f7c056361dc7f980762a143d05791ef5794a751a2caf05ae", size = 2511, upload-time = "2023-02-27T18:28:39.447Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/7d/5df2650c57d47c57232af5ef4b4fdbff182070421e405e0d62c6cdbfaa87/fsspec-2026.1.0.tar.gz", hash = "sha256:e987cb0496a0d81bba3a9d1cee62922fb395e7d4c3b575e57f547953334fe07b", size = 310496, upload-time = "2026-01-09T15:21:35.562Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" }, +] + +[[package]] +name = "glfw" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/72/642d4f12f61816ac96777f7360d413e3977a7dd08237d196f02da681b186/glfw-2.10.0.tar.gz", hash = "sha256:801e55d8581b34df9aa2cfea43feb06ff617576e2a8cc5dac23ee75b26d10abe", size = 31475, upload-time = "2025-09-12T08:54:38.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/1f/a9ce08b1173b0ab625ee92f0c47a5278b3e76fd367699880d8ee7d56c338/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-macosx_10_6_intel.whl", hash = "sha256:5f365a8c94bcea71ec91327e7c16e7cf739128479a18b8c1241b004b40acc412", size = 105329, upload-time = "2025-09-12T08:54:27.938Z" }, + { url = "https://files.pythonhosted.org/packages/7c/96/5a2220abcbd027eebcf8bedd28207a2de168899e51be13ba01ebdd4147a1/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-macosx_11_0_arm64.whl", hash = "sha256:5328db1a92d07abd988730517ec02aa8390d3e6ef7ce98c8b57ecba2f43a39ba", size = 102179, upload-time = "2025-09-12T08:54:29.163Z" }, + { url = "https://files.pythonhosted.org/packages/9d/41/a5bd1d9e1808f400102bd7d328c4ac17b65fb2fc8014014ec6f23d02f662/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux2014_aarch64.whl", hash = "sha256:312c4c1dd5509613ed6bc1e95a8dbb75a36b6dcc4120f50dc3892b40172e9053", size = 230039, upload-time = "2025-09-12T08:54:30.201Z" }, + { url = "https://files.pythonhosted.org/packages/80/aa/3b503c448609dee6cb4e7138b4109338f0e65b97be107ab85562269d378d/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux2014_x86_64.whl", hash = "sha256:59c53387dc08c62e8bed86bbe3a8d53ab1b27161281ffa0e7f27b64284e2627c", size = 241984, upload-time = "2025-09-12T08:54:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/ac/2d/bfe39a42cad8e80b02bf5f7cae19ba67832c1810bbd3624a8e83153d74a4/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux_2_28_aarch64.whl", hash = "sha256:c6f292fdaf3f9a99e598ede6582d21c523a6f51f8f5e66213849101a6bcdc699", size = 231052, upload-time = "2025-09-12T08:54:32.859Z" }, + { url = "https://files.pythonhosted.org/packages/f7/02/6e639e90f181dc9127046e00d0528f9f7ad12d428972e3a5378b9aefdb0b/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux_2_28_x86_64.whl", hash = "sha256:7916034efa867927892635733a3b6af8cd95ceb10566fd7f1e0d2763c2ee8b12", size = 243525, upload-time = "2025-09-12T08:54:34.006Z" }, + { url = "https://files.pythonhosted.org/packages/84/06/cb588ca65561defe0fc48d1df4c2ac12569b81231ae4f2b52ab37007d0bd/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-win32.whl", hash = "sha256:6c9549da71b93e367b4d71438798daae1da2592039fd14204a80a1a2348ae127", size = 552685, upload-time = "2025-09-12T08:54:35.723Z" }, + { url = "https://files.pythonhosted.org/packages/86/27/00c9c96af18ac0a5eac2ff61cbe306551a2d770d7173f396d0792ee1a59e/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-win_amd64.whl", hash = "sha256:6292d5d6634d668cd23d337e6089491d3945a9aa4ac6e1667b0003520d7caa51", size = 559466, upload-time = "2025-09-12T08:54:37.661Z" }, + { url = "https://files.pythonhosted.org/packages/b3/87/de0b33f6f00687499ca1371f22aa73396341b85bf88f1a284f9da8842493/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-macosx_10_6_intel.whl", hash = "sha256:2aab89d2d9535635ba011fc7303390685169a1aa6731ad580d08d043524b8899", size = 105326, upload-time = "2026-01-28T05:57:56.083Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a6/6ea2f73ad4474896d9e38b3ffbe6ffd5a802c738392269e99e8c6621a461/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-macosx_11_0_arm64.whl", hash = "sha256:23936202a107039b5372f0b88ae1d11080746aa1c78910a45d4a0c4cf408cfaa", size = 102180, upload-time = "2026-01-28T05:57:57.787Z" }, + { url = "https://files.pythonhosted.org/packages/58/19/d81b19e8261b9cb51b81d1402167791fef81088dfe91f0c4e9d136fdc5ca/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-manylinux2014_aarch64.whl", hash = "sha256:7be06d0838f61df67bd54cb6266a6193d54083acb3624ff3c3812a6358406fa4", size = 230038, upload-time = "2026-01-28T05:57:59.105Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fa/b035636cd82198b97b51a93efe9cfc4343d6b15cefbd336a3f2be871d848/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-manylinux2014_x86_64.whl", hash = "sha256:91d36b3582a766512eff8e3b5dcc2d3ffcbf10b7cf448551085a08a10f1b8244", size = 241983, upload-time = "2026-01-28T05:58:00.352Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b4/f7b6cc022dd7c68b6c702d19da5d591f978f89c958b9bd3090615db0c739/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-manylinux_2_28_aarch64.whl", hash = "sha256:27c9e9a2d5e1dc3c9e3996171d844d9df9a5a101e797cb94cce217b7afcf8fd9", size = 231053, upload-time = "2026-01-28T05:58:01.683Z" }, + { url = "https://files.pythonhosted.org/packages/5a/3f/efeb7c6801c46e11bd666a5180f0d615f74f72264212f74f39586c6fda9d/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-manylinux_2_28_x86_64.whl", hash = "sha256:ce6724bb7cb3d0543dcba17206dce909f94176e68220b8eafee72e9f92bcf542", size = 243522, upload-time = "2026-01-28T05:58:03.517Z" }, + { url = "https://files.pythonhosted.org/packages/cf/b9/b04c3aa0aad2870cfe799f32f8b59789c98e1816bbce9e83f4823c5b840b/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-win32.whl", hash = "sha256:fca724a21a372731edb290841edd28a9fb1ee490f833392752844ac807c0086a", size = 552682, upload-time = "2026-01-28T05:58:05.649Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e1/6d6816b296a529ac9b897ad228b1e084eb1f92319e96371880eebdc874a6/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-win_amd64.whl", hash = "sha256:823c0bd7770977d4b10e0ed0aef2f3682276b7c88b8b65cfc540afce5951392f", size = 559464, upload-time = "2026-01-28T05:58:07.261Z" }, +] + +[[package]] +name = "gymnasium" +version = "1.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "farama-notifications" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/59/653a9417d98ed3e29ef9734ba52c3495f6c6823b8d5c0c75369f25111708/gymnasium-1.2.3.tar.gz", hash = "sha256:2b2cb5b5fbbbdf3afb9f38ca952cc48aa6aa3e26561400d940747fda3ad42509", size = 829230, upload-time = "2025-12-18T16:51:10.234Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/d3/ea5f088e3638dbab12e5c20d6559d5b3bdaeaa1f2af74e526e6815836285/gymnasium-1.2.3-py3-none-any.whl", hash = "sha256:e6314bba8f549c7fdcc8677f7cd786b64908af6e79b57ddaa5ce1825bffb5373", size = 952113, upload-time = "2025-12-18T16:51:08.445Z" }, +] + +[[package]] +name = "imageio" +version = "2.37.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/606be632e37bf8d05b253e8626c2291d74c691ddc7bcdf7d6aaf33b32f6a/imageio-2.37.2.tar.gz", hash = "sha256:0212ef2727ac9caa5ca4b2c75ae89454312f440a756fcfc8ef1993e718f50f8a", size = 389600, upload-time = "2025-11-04T14:29:39.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/fe/301e0936b79bcab4cacc7548bf2853fc28dced0a578bab1f7ef53c9aa75b/imageio-2.37.2-py3-none-any.whl", hash = "sha256:ad9adfb20335d718c03de457358ed69f141021a333c40a53e57273d8a5bd0b9b", size = 317646, upload-time = "2025-11-04T14:29:37.948Z" }, +] + +[[package]] +name = "importlib-resources" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "ipdb" +version = "0.13.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "decorator" }, + { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/1b/7e07e7b752017f7693a0f4d41c13e5ca29ce8cbcfdcc1fd6c4ad8c0a27a0/ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726", size = 17042, upload-time = "2023-03-09T15:40:57.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/4c/b075da0092003d9a55cf2ecc1cae9384a1ca4f650d51b00fc59875fe76f6/ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4", size = 12130, upload-time = "2023-03-09T15:40:55.021Z" }, +] + +[[package]] +name = "ipython" +version = "8.38.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.11'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "jedi", marker = "python_full_version < '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, + { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "stack-data", marker = "python_full_version < '3.11'" }, + { name = "traitlets", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/61/1810830e8b93c72dcd3c0f150c80a00c3deb229562d9423807ec92c3a539/ipython-8.38.0.tar.gz", hash = "sha256:9cfea8c903ce0867cc2f23199ed8545eb741f3a69420bfcf3743ad1cec856d39", size = 5513996, upload-time = "2026-01-05T10:59:06.901Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/df/db59624f4c71b39717c423409950ac3f2c8b2ce4b0aac843112c7fb3f721/ipython-8.38.0-py3-none-any.whl", hash = "sha256:750162629d800ac65bb3b543a14e7a74b0e88063eac9b92124d4b2aa3f6d8e86", size = 831813, upload-time = "2026-01-05T10:59:04.239Z" }, +] + +[[package]] +name = "ipython" +version = "9.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version >= '3.11'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, + { name = "jedi", marker = "python_full_version >= '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, + { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "stack-data", marker = "python_full_version >= '3.11'" }, + { name = "traitlets", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/60/2111715ea11f39b1535bed6024b7dec7918b71e5e5d30855a5b503056b50/ipython-9.10.0.tar.gz", hash = "sha256:cd9e656be97618a0676d058134cd44e6dc7012c0e5cb36a9ce96a8c904adaf77", size = 4426526, upload-time = "2026-02-02T10:00:33.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl", hash = "sha256:c6ab68cc23bba8c7e18e9b932797014cc61ea7fd6f19de180ab9ba73e65ee58d", size = 622774, upload-time = "2026-02-02T10:00:31.503Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "isort" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049, upload-time = "2025-10-11T13:30:59.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[package]] +name = "librt" +version = "0.7.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" }, + { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" }, + { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" }, + { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" }, + { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" }, + { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" }, + { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" }, + { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" }, + { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" }, + { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" }, + { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" }, + { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" }, + { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" }, + { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" }, + { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" }, + { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" }, + { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" }, + { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" }, + { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" }, + { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" }, + { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" }, + { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" }, + { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" }, + { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" }, + { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" }, + { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" }, + { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" }, + { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" }, + { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" }, + { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, +] + +[[package]] +name = "memory-profiler" +version = "0.61.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "psutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/88/e1907e1ca3488f2d9507ca8b0ae1add7b1cd5d3ca2bc8e5b329382ea2c7b/memory_profiler-0.61.0.tar.gz", hash = "sha256:4e5b73d7864a1d1292fb76a03e82a3e78ef934d06828a698d9dada76da2067b0", size = 35935, upload-time = "2022-11-15T17:57:28.994Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/26/aaca612a0634ceede20682e692a6c55e35a94c21ba36b807cc40fe910ae1/memory_profiler-0.61.0-py3-none-any.whl", hash = "sha256:400348e61031e3942ad4d4109d18753b2fb08c2f6fb8290671c5513a34182d84", size = 31803, upload-time = "2022-11-15T17:57:27.031Z" }, +] + +[[package]] +name = "metaworld" +version = "3.0.0" +source = { editable = "." } +dependencies = [ + { name = "gymnasium" }, + { name = "imageio" }, + { name = "mujoco" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] + +[package.dev-dependencies] +coverage = [ + { name = "pytest-cov" }, +] +dev = [ + { name = "black" }, + { name = "isort" }, + { name = "mypy" }, +] +testing = [ + { name = "ipdb" }, + { name = "memory-profiler" }, + { name = "pyquaternion" }, + { name = "pytest" }, + { name = "pytest-xdist", extra = ["psutil"] }, +] + +[package.metadata] +requires-dist = [ + { name = "gymnasium", specifier = ">=1.1" }, + { name = "imageio" }, + { name = "mujoco", specifier = ">=3.0.0" }, + { name = "numpy", specifier = ">=1.18" }, + { name = "scipy", specifier = ">=1.4.1" }, +] + +[package.metadata.requires-dev] +coverage = [{ name = "pytest-cov" }] +dev = [ + { name = "black" }, + { name = "isort" }, + { name = "mypy" }, +] +testing = [ + { name = "ipdb" }, + { name = "memory-profiler" }, + { name = "pyquaternion", specifier = "==0.9.5" }, + { name = "pytest", specifier = ">=4.4.0" }, + { name = "pytest-xdist", extras = ["psutil"] }, +] + +[[package]] +name = "mujoco" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "etils", extra = ["epath"] }, + { name = "glfw" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pyopengl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/3b/f688fbe34eb609ffdc9dc0f53f7acd3327588f970752780d05a0762d3511/mujoco-3.4.0.tar.gz", hash = "sha256:5a6dc6b7db41eb0ab8724cd477bd0316ba4b53debfc2d80a2d6f444a116fb8d2", size = 826806, upload-time = "2025-12-05T23:13:46.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/a0/ff8c20b923675ee803580bb8a33a2781e48c007a2845607f15184cf7fc32/mujoco-3.4.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:b7ae8a534ecf6afab3abab3dc0718ea47f89a2e2f096905870cbf5faf23076c3", size = 6905902, upload-time = "2025-12-05T23:12:59.76Z" }, + { url = "https://files.pythonhosted.org/packages/03/dd/2875a57cdb423d98bdcb359f34af5eb8a24e48a903f4bb7110d75e478dac/mujoco-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7960cf47b4ed274955280200a812e6d780f03707d5258b42c3afb249051216ee", size = 6861873, upload-time = "2025-12-05T23:13:02.42Z" }, + { url = "https://files.pythonhosted.org/packages/67/05/736c180caf0b051ec5ba26ab024e609fb18545b212dd0ee96ec84458f184/mujoco-3.4.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0918ff57a92ba00a95538bf6f0c67973044953bb090b9fd811f9ee6cda4ffcb8", size = 6487647, upload-time = "2025-12-05T23:13:05.097Z" }, + { url = "https://files.pythonhosted.org/packages/71/95/ba02262c7a7a786a64b8d77315e7e4d3c77598ff63d8cd605ba2b96ec349/mujoco-3.4.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b2b8a75ea191ae577bfa2385ca6ecd6328f37f6f46bc3cfb41835b2653f716a", size = 6911042, upload-time = "2025-12-05T23:13:07.727Z" }, + { url = "https://files.pythonhosted.org/packages/22/d5/f94edc884c11b63f0d7ba9322ec4ca98bc7cad57c5c034c2e3332287d9ad/mujoco-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ffe79a1476767806318b7dfbbb642e428b873385fb2d2f06e69d461459d01ed1", size = 5399174, upload-time = "2025-12-05T23:13:10.271Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a1/5f92234b0e2f2b8c5b392fd71be3cfb5363bdccded4cac0b5889d07da6cb/mujoco-3.4.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:b456e1d6c3ca7010480d52f7645e49f4b564952063c5a52af1524e226ea72920", size = 6919759, upload-time = "2025-12-05T23:13:12.444Z" }, + { url = "https://files.pythonhosted.org/packages/09/d4/99acbb782cc2c2ab5e5dacdf48b5d2850b641e34a91715d262d14103b764/mujoco-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98d3ab7b02d99ac2866bb111807797549853d6ddf485ddc072cec3c1d33dfad8", size = 6874776, upload-time = "2025-12-05T23:13:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/57/92/1e10be7922508a307017e2a62852555ab4f61148d9791a7bcdf03d902a9a/mujoco-3.4.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00088b3879cff675b0bad2ddaf27f1088d7d35f020903c25088c43360adcc311", size = 6501363, upload-time = "2025-12-05T23:13:17.313Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c1/37c07061be2b410de33624093fc06fc49ec13888252f1a6c30fcd40633bd/mujoco-3.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0b3dce70abdd0ac6475bb8040d7d97a4e3ccf5f6a00cc06086b7a169e246522", size = 6925780, upload-time = "2025-12-05T23:13:19.426Z" }, + { url = "https://files.pythonhosted.org/packages/04/0a/c1055f2329761c87edcdc18a480ab43ba942ed10f156888b4744d69c1deb/mujoco-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:e0ab4a450826ca04db11608325b37adf77dee4cde2a9dd9d43e6fa46c44545a9", size = 5422000, upload-time = "2025-12-05T23:13:21.679Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c3/858d2e6fd3bf986a64bb5f0f157b601c2b9604d2b43bbc0469fe1b44d61b/mujoco-3.4.0-cp312-cp312-macosx_10_16_x86_64.whl", hash = "sha256:96bccc995fc561078b5cac1e53f8ba2ba8619348bc0c6cff15bbf6f9a441d220", size = 6922335, upload-time = "2025-12-05T23:13:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/10dbaef500af18866a41d842f2f95cf113aacf0f1eb91677c4817cff3495/mujoco-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:339800c695166c8041cdef95ea5384fc607ad1b86c19528c785a17ed742c3a5e", size = 6793849, upload-time = "2025-12-05T23:13:25.714Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6b/522696a7413f33596b8d18bca52dee2f7c0ecf23a5f08097f346e5a5b656/mujoco-3.4.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1efdab2b146cdbcef4560606b6cbc74abe80f2b94a8593a5cdc469172085a2b3", size = 6520021, upload-time = "2025-12-05T23:13:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/c6/10/fa6b8762efbb02bd349503a39fb9dbbcf9e12041b0c5b29d484cafc09355/mujoco-3.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e5a36e61495be2a855f61194813e1277ab4b330cc180e50c8e3c7a459dc40b6", size = 6995663, upload-time = "2025-12-05T23:13:30.436Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/5336e8e86e429a7f301dac63cae6ae75ff9e91bfb01502a55f60d7305eca/mujoco-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:10314213c395aedeaf2778596e78dd9ae01d74dc92d4f75c696707781d59826a", size = 5497926, upload-time = "2025-12-05T23:13:32.382Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ba/9fa63c63728d9ac0982b77af650229f14b1aa53e3331dba7ef6829fdda56/mujoco-3.4.0-cp313-cp313-macosx_10_16_x86_64.whl", hash = "sha256:345fb5adb4e9c1c108aab2ff8418280edf61cec5b705c483eae680c4cf350898", size = 6922502, upload-time = "2025-12-05T23:13:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/e8/51/c3e5c3b199b1b74c85f0cb02dc0ef80363bf91ea245ee8a932804768d5e8/mujoco-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87a68d063d06d261e83755093e79371901b6a8171b0b8b88dcb020f966d4e463", size = 6794329, upload-time = "2025-12-05T23:13:37.557Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2f/b2f531ae6e8fbbf095dfbb614b7d1130d3d6791920a9b075861a13f5a97b/mujoco-3.4.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:06cd3366b9548b251c3170c9b073e41a9f4a621b4f7e59ceb5fee8c46f6165ca", size = 6520414, upload-time = "2025-12-05T23:13:39.778Z" }, + { url = "https://files.pythonhosted.org/packages/df/72/0c47350ec39611ff8defe9e8af10c23c9ad0235974f1999a523fcd1c3e68/mujoco-3.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7edaffb223cf1343ed980030c466170eb8f9d624cf69c9a99925cbce371f22db", size = 6996132, upload-time = "2025-12-05T23:13:41.996Z" }, + { url = "https://files.pythonhosted.org/packages/dc/25/bbf8c01758d619c86bcbe58db6a7d61ca423e7c76791f323b8ea2e92c2bd/mujoco-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:c01a842a17c0229dce2fb65a051a8e6ee5f5307c50c825c850e3702dda4344e6", size = 5497055, upload-time = "2025-12-05T23:13:44.795Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" }, + { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" }, + { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" }, + { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" }, + { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" }, + { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" }, + { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" }, + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, + { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, + { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, + { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, + { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, + { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, + { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, + { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, + { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, + { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, + { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, + { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, + { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, + { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" }, + { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" }, + { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" }, + { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "parso" +version = "0.8.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "pillow" +version = "12.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/41/f73d92b6b883a579e79600d391f2e21cb0df767b2714ecbd2952315dfeef/pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd", size = 5304089, upload-time = "2026-01-02T09:10:24.953Z" }, + { url = "https://files.pythonhosted.org/packages/94/55/7aca2891560188656e4a91ed9adba305e914a4496800da6b5c0a15f09edf/pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0", size = 4657815, upload-time = "2026-01-02T09:10:27.063Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d2/b28221abaa7b4c40b7dba948f0f6a708bd7342c4d47ce342f0ea39643974/pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8", size = 6222593, upload-time = "2026-01-02T09:10:29.115Z" }, + { url = "https://files.pythonhosted.org/packages/71/b8/7a61fb234df6a9b0b479f69e66901209d89ff72a435b49933f9122f94cac/pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1", size = 8027579, upload-time = "2026-01-02T09:10:31.182Z" }, + { url = "https://files.pythonhosted.org/packages/ea/51/55c751a57cc524a15a0e3db20e5cde517582359508d62305a627e77fd295/pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda", size = 6335760, upload-time = "2026-01-02T09:10:33.02Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7c/60e3e6f5e5891a1a06b4c910f742ac862377a6fe842f7184df4a274ce7bf/pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7", size = 7027127, upload-time = "2026-01-02T09:10:35.009Z" }, + { url = "https://files.pythonhosted.org/packages/06/37/49d47266ba50b00c27ba63a7c898f1bb41a29627ced8c09e25f19ebec0ff/pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a", size = 6449896, upload-time = "2026-01-02T09:10:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/67fd87d2913902462cd9b79c6211c25bfe95fcf5783d06e1367d6d9a741f/pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef", size = 7151345, upload-time = "2026-01-02T09:10:39.064Z" }, + { url = "https://files.pythonhosted.org/packages/bd/15/f8c7abf82af68b29f50d77c227e7a1f87ce02fdc66ded9bf603bc3b41180/pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09", size = 6325568, upload-time = "2026-01-02T09:10:41.035Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/7d1c0e160b6b5ac2605ef7d8be537e28753c0db5363d035948073f5513d7/pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91", size = 7032367, upload-time = "2026-01-02T09:10:43.09Z" }, + { url = "https://files.pythonhosted.org/packages/f4/03/41c038f0d7a06099254c60f618d0ec7be11e79620fc23b8e85e5b31d9a44/pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea", size = 2452345, upload-time = "2026-01-02T09:10:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", size = 5304057, upload-time = "2026-01-02T09:10:46.627Z" }, + { url = "https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", size = 4657811, upload-time = "2026-01-02T09:10:49.548Z" }, + { url = "https://files.pythonhosted.org/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", size = 6232243, upload-time = "2026-01-02T09:10:51.62Z" }, + { url = "https://files.pythonhosted.org/packages/ef/30/0df458009be6a4caca4ca2c52975e6275c387d4e5c95544e34138b41dc86/pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e", size = 8037872, upload-time = "2026-01-02T09:10:53.446Z" }, + { url = "https://files.pythonhosted.org/packages/e4/86/95845d4eda4f4f9557e25381d70876aa213560243ac1a6d619c46caaedd9/pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84", size = 6345398, upload-time = "2026-01-02T09:10:55.426Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0", size = 7034667, upload-time = "2026-01-02T09:10:57.11Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f6/683b83cb9b1db1fb52b87951b1c0b99bdcfceaa75febf11406c19f82cb5e/pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b", size = 6458743, upload-time = "2026-01-02T09:10:59.331Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/de833d63622538c1d58ce5395e7c6cb7e7dce80decdd8bde4a484e095d9f/pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18", size = 7159342, upload-time = "2026-01-02T09:11:01.82Z" }, + { url = "https://files.pythonhosted.org/packages/8c/40/50d86571c9e5868c42b81fe7da0c76ca26373f3b95a8dd675425f4a92ec1/pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64", size = 6328655, upload-time = "2026-01-02T09:11:04.556Z" }, + { url = "https://files.pythonhosted.org/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75", size = 7031469, upload-time = "2026-01-02T09:11:06.538Z" }, + { url = "https://files.pythonhosted.org/packages/48/36/d5716586d887fb2a810a4a61518a327a1e21c8b7134c89283af272efe84b/pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304", size = 2452515, upload-time = "2026-01-02T09:11:08.226Z" }, + { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, + { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, + { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" }, + { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" }, + { url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" }, + { url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" }, + { url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" }, + { url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" }, + { url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" }, + { url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" }, + { url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" }, + { url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" }, + { url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" }, + { url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" }, + { url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" }, + { url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" }, + { url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" }, + { url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" }, + { url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" }, + { url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" }, + { url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" }, + { url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" }, + { url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" }, + { url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" }, + { url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" }, + { url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" }, + { url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" }, + { url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" }, + { url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", size = 5228605, upload-time = "2026-01-02T09:13:14.084Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", size = 4622245, upload-time = "2026-01-02T09:13:15.964Z" }, + { url = "https://files.pythonhosted.org/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", size = 5247593, upload-time = "2026-01-02T09:13:17.913Z" }, + { url = "https://files.pythonhosted.org/packages/d2/95/3e0742fe358c4664aed4fd05d5f5373dcdad0b27af52aa0972568541e3f4/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd", size = 6989008, upload-time = "2026-01-02T09:13:20.083Z" }, + { url = "https://files.pythonhosted.org/packages/5a/74/fe2ac378e4e202e56d50540d92e1ef4ff34ed687f3c60f6a121bcf99437e/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc", size = 5313824, upload-time = "2026-01-02T09:13:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/f3/77/2a60dee1adee4e2655ac328dd05c02a955c1cd683b9f1b82ec3feb44727c/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a", size = 5963278, upload-time = "2026-01-02T09:13:24.706Z" }, + { url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyopengl" +version = "3.1.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/16/912b7225d56284859cd9a672827f18be43f8012f8b7b932bc4bd959a298e/pyopengl-3.1.10.tar.gz", hash = "sha256:c4a02d6866b54eb119c8e9b3fb04fa835a95ab802dd96607ab4cdb0012df8335", size = 1915580, upload-time = "2025-08-18T02:33:01.76Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/e4/1ba6f44e491c4eece978685230dde56b14d51a0365bc1b774ddaa94d14cd/pyopengl-3.1.10-py3-none-any.whl", hash = "sha256:794a943daced39300879e4e47bd94525280685f42dbb5a998d336cfff151d74f", size = 3194996, upload-time = "2025-08-18T02:32:59.902Z" }, +] + +[[package]] +name = "pyquaternion" +version = "0.9.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/c8/02b30c4a86744d2e15f7f16ab353f7231bd0241117713e5d60f466044994/pyquaternion-0.9.5.tar.gz", hash = "sha256:2d89d19259d62a8fbd25219eee7dacc1f6bb570becb70e1e883f622597c7d81d", size = 15497, upload-time = "2019-02-10T14:03:06.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/e3/339e1135d94c2db689fbf33603cbc8f2861ca15a1dce79963f796b3cc910/pyquaternion-0.9.5-py3-none-any.whl", hash = "sha256:bac5945d08b9a2f4106dc76206e40f353c7240fdf37a370e13b03113c135f59b", size = 14212, upload-time = "2019-02-10T14:03:25.651Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[package.optional-dependencies] +psutil = [ + { name = "psutil" }, +] + +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/3e/9cca699f3486ce6bc12ff46dc2031f1ec8eb9ccc9a320fdaf925f1417426/scipy-1.17.0.tar.gz", hash = "sha256:2591060c8e648d8b96439e111ac41fd8342fdeff1876be2e19dea3fe8930454e", size = 30396830, upload-time = "2026-01-10T21:34:23.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/4b/c89c131aa87cad2b77a54eb0fb94d633a842420fa7e919dc2f922037c3d8/scipy-1.17.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:2abd71643797bd8a106dff97894ff7869eeeb0af0f7a5ce02e4227c6a2e9d6fd", size = 31381316, upload-time = "2026-01-10T21:24:33.42Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5f/a6b38f79a07d74989224d5f11b55267714707582908a5f1ae854cf9a9b84/scipy-1.17.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:ef28d815f4d2686503e5f4f00edc387ae58dfd7a2f42e348bb53359538f01558", size = 27966760, upload-time = "2026-01-10T21:24:38.911Z" }, + { url = "https://files.pythonhosted.org/packages/c1/20/095ad24e031ee8ed3c5975954d816b8e7e2abd731e04f8be573de8740885/scipy-1.17.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:272a9f16d6bb4667e8b50d25d71eddcc2158a214df1b566319298de0939d2ab7", size = 20138701, upload-time = "2026-01-10T21:24:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/89/11/4aad2b3858d0337756f3323f8960755704e530b27eb2a94386c970c32cbe/scipy-1.17.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:7204fddcbec2fe6598f1c5fdf027e9f259106d05202a959a9f1aecf036adc9f6", size = 22480574, upload-time = "2026-01-10T21:24:47.266Z" }, + { url = "https://files.pythonhosted.org/packages/85/bd/f5af70c28c6da2227e510875cadf64879855193a687fb19951f0f44cfd6b/scipy-1.17.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc02c37a5639ee67d8fb646ffded6d793c06c5622d36b35cfa8fe5ececb8f042", size = 32862414, upload-time = "2026-01-10T21:24:52.566Z" }, + { url = "https://files.pythonhosted.org/packages/ef/df/df1457c4df3826e908879fe3d76bc5b6e60aae45f4ee42539512438cfd5d/scipy-1.17.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dac97a27520d66c12a34fd90a4fe65f43766c18c0d6e1c0a80f114d2260080e4", size = 35112380, upload-time = "2026-01-10T21:24:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/5f/bb/88e2c16bd1dd4de19d80d7c5e238387182993c2fb13b4b8111e3927ad422/scipy-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb7446a39b3ae0fe8f416a9a3fdc6fba3f11c634f680f16a239c5187bc487c0", size = 34922676, upload-time = "2026-01-10T21:25:04.287Z" }, + { url = "https://files.pythonhosted.org/packages/02/ba/5120242cc735f71fc002cff0303d536af4405eb265f7c60742851e7ccfe9/scipy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:474da16199f6af66601a01546144922ce402cb17362e07d82f5a6cf8f963e449", size = 37507599, upload-time = "2026-01-10T21:25:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/08629657ac6c0da198487ce8cd3de78e02cfde42b7f34117d56a3fe249dc/scipy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:255c0da161bd7b32a6c898e7891509e8a9289f0b1c6c7d96142ee0d2b114c2ea", size = 36380284, upload-time = "2026-01-10T21:25:15.632Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4a/465f96d42c6f33ad324a40049dfd63269891db9324aa66c4a1c108c6f994/scipy-1.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:85b0ac3ad17fa3be50abd7e69d583d98792d7edc08367e01445a1e2076005379", size = 24370427, upload-time = "2026-01-10T21:25:20.514Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/7241a63e73ba5a516f1930ac8d5b44cbbfabd35ac73a2d08ca206df007c4/scipy-1.17.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:0d5018a57c24cb1dd828bcf51d7b10e65986d549f52ef5adb6b4d1ded3e32a57", size = 31364580, upload-time = "2026-01-10T21:25:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/5057f812d4f6adc91a20a2d6f2ebcdb517fdbc87ae3acc5633c9b97c8ba5/scipy-1.17.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:88c22af9e5d5a4f9e027e26772cc7b5922fab8bcc839edb3ae33de404feebd9e", size = 27969012, upload-time = "2026-01-10T21:25:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/e3/21/f6ec556c1e3b6ec4e088da667d9987bb77cc3ab3026511f427dc8451187d/scipy-1.17.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f3cd947f20fe17013d401b64e857c6b2da83cae567adbb75b9dcba865abc66d8", size = 20140691, upload-time = "2026-01-10T21:25:34.802Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fe/5e5ad04784964ba964a96f16c8d4676aa1b51357199014dce58ab7ec5670/scipy-1.17.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e8c0b331c2c1f531eb51f1b4fc9ba709521a712cce58f1aa627bc007421a5306", size = 22463015, upload-time = "2026-01-10T21:25:39.277Z" }, + { url = "https://files.pythonhosted.org/packages/4a/69/7c347e857224fcaf32a34a05183b9d8a7aca25f8f2d10b8a698b8388561a/scipy-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5194c445d0a1c7a6c1a4a4681b6b7c71baad98ff66d96b949097e7513c9d6742", size = 32724197, upload-time = "2026-01-10T21:25:44.084Z" }, + { url = "https://files.pythonhosted.org/packages/d1/fe/66d73b76d378ba8cc2fe605920c0c75092e3a65ae746e1e767d9d020a75a/scipy-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9eeb9b5f5997f75507814ed9d298ab23f62cf79f5a3ef90031b1ee2506abdb5b", size = 35009148, upload-time = "2026-01-10T21:25:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/07dec27d9dc41c18d8c43c69e9e413431d20c53a0339c388bcf72f353c4b/scipy-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:40052543f7bbe921df4408f46003d6f01c6af109b9e2c8a66dd1cf6cf57f7d5d", size = 34798766, upload-time = "2026-01-10T21:25:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/81/61/0470810c8a093cdacd4ba7504b8a218fd49ca070d79eca23a615f5d9a0b0/scipy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0cf46c8013fec9d3694dc572f0b54100c28405d55d3e2cb15e2895b25057996e", size = 37405953, upload-time = "2026-01-10T21:26:07.75Z" }, + { url = "https://files.pythonhosted.org/packages/92/ce/672ed546f96d5d41ae78c4b9b02006cedd0b3d6f2bf5bb76ea455c320c28/scipy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:0937a0b0d8d593a198cededd4c439a0ea216a3f36653901ea1f3e4be949056f8", size = 36328121, upload-time = "2026-01-10T21:26:16.509Z" }, + { url = "https://files.pythonhosted.org/packages/9d/21/38165845392cae67b61843a52c6455d47d0cc2a40dd495c89f4362944654/scipy-1.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:f603d8a5518c7426414d1d8f82e253e454471de682ce5e39c29adb0df1efb86b", size = 24314368, upload-time = "2026-01-10T21:26:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/0c/51/3468fdfd49387ddefee1636f5cf6d03ce603b75205bf439bbf0e62069bfd/scipy-1.17.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:65ec32f3d32dfc48c72df4291345dae4f048749bc8d5203ee0a3f347f96c5ce6", size = 31344101, upload-time = "2026-01-10T21:26:30.25Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/9406aec58268d437636069419e6977af953d1e246df941d42d3720b7277b/scipy-1.17.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:1f9586a58039d7229ce77b52f8472c972448cded5736eaf102d5658bbac4c269", size = 27950385, upload-time = "2026-01-10T21:26:36.801Z" }, + { url = "https://files.pythonhosted.org/packages/4f/98/e7342709e17afdfd1b26b56ae499ef4939b45a23a00e471dfb5375eea205/scipy-1.17.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9fad7d3578c877d606b1150135c2639e9de9cecd3705caa37b66862977cc3e72", size = 20122115, upload-time = "2026-01-10T21:26:42.107Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0e/9eeeb5357a64fd157cbe0302c213517c541cc16b8486d82de251f3c68ede/scipy-1.17.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:423ca1f6584fc03936972b5f7c06961670dbba9f234e71676a7c7ccf938a0d61", size = 22442402, upload-time = "2026-01-10T21:26:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c9/10/be13397a0e434f98e0c79552b2b584ae5bb1c8b2be95db421533bbca5369/scipy-1.17.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe508b5690e9eaaa9467fc047f833af58f1152ae51a0d0aed67aa5801f4dd7d6", size = 32696338, upload-time = "2026-01-10T21:26:55.521Z" }, + { url = "https://files.pythonhosted.org/packages/63/1e/12fbf2a3bb240161651c94bb5cdd0eae5d4e8cc6eaeceb74ab07b12a753d/scipy-1.17.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6680f2dfd4f6182e7d6db161344537da644d1cf85cf293f015c60a17ecf08752", size = 34977201, upload-time = "2026-01-10T21:27:03.501Z" }, + { url = "https://files.pythonhosted.org/packages/19/5b/1a63923e23ccd20bd32156d7dd708af5bbde410daa993aa2500c847ab2d2/scipy-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eec3842ec9ac9de5917899b277428886042a93db0b227ebbe3a333b64ec7643d", size = 34777384, upload-time = "2026-01-10T21:27:11.423Z" }, + { url = "https://files.pythonhosted.org/packages/39/22/b5da95d74edcf81e540e467202a988c50fef41bd2011f46e05f72ba07df6/scipy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d7425fcafbc09a03731e1bc05581f5fad988e48c6a861f441b7ab729a49a55ea", size = 37379586, upload-time = "2026-01-10T21:27:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b6/8ac583d6da79e7b9e520579f03007cb006f063642afd6b2eeb16b890bf93/scipy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:87b411e42b425b84777718cc41516b8a7e0795abfa8e8e1d573bf0ef014f0812", size = 36287211, upload-time = "2026-01-10T21:28:43.122Z" }, + { url = "https://files.pythonhosted.org/packages/55/fb/7db19e0b3e52f882b420417644ec81dd57eeef1bd1705b6f689d8ff93541/scipy-1.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:357ca001c6e37601066092e7c89cca2f1ce74e2a520ca78d063a6d2201101df2", size = 24312646, upload-time = "2026-01-10T21:28:49.893Z" }, + { url = "https://files.pythonhosted.org/packages/20/b6/7feaa252c21cc7aff335c6c55e1b90ab3e3306da3f048109b8b639b94648/scipy-1.17.0-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:ec0827aa4d36cb79ff1b81de898e948a51ac0b9b1c43e4a372c0508c38c0f9a3", size = 31693194, upload-time = "2026-01-10T21:27:27.454Z" }, + { url = "https://files.pythonhosted.org/packages/76/bb/bbb392005abce039fb7e672cb78ac7d158700e826b0515cab6b5b60c26fb/scipy-1.17.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:819fc26862b4b3c73a60d486dbb919202f3d6d98c87cf20c223511429f2d1a97", size = 28365415, upload-time = "2026-01-10T21:27:34.26Z" }, + { url = "https://files.pythonhosted.org/packages/37/da/9d33196ecc99fba16a409c691ed464a3a283ac454a34a13a3a57c0d66f3a/scipy-1.17.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:363ad4ae2853d88ebcde3ae6ec46ccca903ea9835ee8ba543f12f575e7b07e4e", size = 20537232, upload-time = "2026-01-10T21:27:40.306Z" }, + { url = "https://files.pythonhosted.org/packages/56/9d/f4b184f6ddb28e9a5caea36a6f98e8ecd2a524f9127354087ce780885d83/scipy-1.17.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:979c3a0ff8e5ba254d45d59ebd38cde48fce4f10b5125c680c7a4bfe177aab07", size = 22791051, upload-time = "2026-01-10T21:27:46.539Z" }, + { url = "https://files.pythonhosted.org/packages/9b/9d/025cccdd738a72140efc582b1641d0dd4caf2e86c3fb127568dc80444e6e/scipy-1.17.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:130d12926ae34399d157de777472bf82e9061c60cc081372b3118edacafe1d00", size = 32815098, upload-time = "2026-01-10T21:27:54.389Z" }, + { url = "https://files.pythonhosted.org/packages/48/5f/09b879619f8bca15ce392bfc1894bd9c54377e01d1b3f2f3b595a1b4d945/scipy-1.17.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e886000eb4919eae3a44f035e63f0fd8b651234117e8f6f29bad1cd26e7bc45", size = 35031342, upload-time = "2026-01-10T21:28:03.012Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9a/f0f0a9f0aa079d2f106555b984ff0fbb11a837df280f04f71f056ea9c6e4/scipy-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:13c4096ac6bc31d706018f06a49abe0485f96499deb82066b94d19b02f664209", size = 34893199, upload-time = "2026-01-10T21:28:10.832Z" }, + { url = "https://files.pythonhosted.org/packages/90/b8/4f0f5cf0c5ea4d7548424e6533e6b17d164f34a6e2fb2e43ffebb6697b06/scipy-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cacbaddd91fcffde703934897c5cd2c7cb0371fac195d383f4e1f1c5d3f3bd04", size = 37438061, upload-time = "2026-01-10T21:28:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/f9/cc/2bd59140ed3b2fa2882fb15da0a9cb1b5a6443d67cfd0d98d4cec83a57ec/scipy-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:edce1a1cf66298cccdc48a1bdf8fb10a3bf58e8b58d6c3883dd1530e103f87c0", size = 36328593, upload-time = "2026-01-10T21:28:28.007Z" }, + { url = "https://files.pythonhosted.org/packages/13/1b/c87cc44a0d2c7aaf0f003aef2904c3d097b422a96c7e7c07f5efd9073c1b/scipy-1.17.0-cp313-cp313t-win_arm64.whl", hash = "sha256:30509da9dbec1c2ed8f168b8d8aa853bc6723fede1dbc23c7d43a56f5ab72a67", size = 24625083, upload-time = "2026-01-10T21:28:35.188Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2d/51006cd369b8e7879e1c630999a19d1fbf6f8b5ed3e33374f29dc87e53b3/scipy-1.17.0-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:c17514d11b78be8f7e6331b983a65a7f5ca1fd037b95e27b280921fe5606286a", size = 31346803, upload-time = "2026-01-10T21:28:57.24Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2e/2349458c3ce445f53a6c93d4386b1c4c5c0c540917304c01222ff95ff317/scipy-1.17.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:4e00562e519c09da34c31685f6acc3aa384d4d50604db0f245c14e1b4488bfa2", size = 27967182, upload-time = "2026-01-10T21:29:04.107Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7c/df525fbfa77b878d1cfe625249529514dc02f4fd5f45f0f6295676a76528/scipy-1.17.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f7df7941d71314e60a481e02d5ebcb3f0185b8d799c70d03d8258f6c80f3d467", size = 20139125, upload-time = "2026-01-10T21:29:10.179Z" }, + { url = "https://files.pythonhosted.org/packages/33/11/fcf9d43a7ed1234d31765ec643b0515a85a30b58eddccc5d5a4d12b5f194/scipy-1.17.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:aabf057c632798832f071a8dde013c2e26284043934f53b00489f1773b33527e", size = 22443554, upload-time = "2026-01-10T21:29:15.888Z" }, + { url = "https://files.pythonhosted.org/packages/80/5c/ea5d239cda2dd3d31399424967a24d556cf409fbea7b5b21412b0fd0a44f/scipy-1.17.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a38c3337e00be6fd8a95b4ed66b5d988bac4ec888fd922c2ea9fe5fb1603dd67", size = 32757834, upload-time = "2026-01-10T21:29:23.406Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7e/8c917cc573310e5dc91cbeead76f1b600d3fb17cf0969db02c9cf92e3cfa/scipy-1.17.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00fb5f8ec8398ad90215008d8b6009c9db9fa924fd4c7d6be307c6f945f9cd73", size = 34995775, upload-time = "2026-01-10T21:29:31.915Z" }, + { url = "https://files.pythonhosted.org/packages/c5/43/176c0c3c07b3f7df324e7cdd933d3e2c4898ca202b090bd5ba122f9fe270/scipy-1.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f2a4942b0f5f7c23c7cd641a0ca1955e2ae83dedcff537e3a0259096635e186b", size = 34841240, upload-time = "2026-01-10T21:29:39.995Z" }, + { url = "https://files.pythonhosted.org/packages/44/8c/d1f5f4b491160592e7f084d997de53a8e896a3ac01cd07e59f43ca222744/scipy-1.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf133ced83889583156566d2bdf7a07ff89228fe0c0cb727f777de92092ec6b", size = 37394463, upload-time = "2026-01-10T21:29:48.723Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ec/42a6657f8d2d087e750e9a5dde0b481fd135657f09eaf1cf5688bb23c338/scipy-1.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:3625c631a7acd7cfd929e4e31d2582cf00f42fcf06011f59281271746d77e061", size = 37053015, upload-time = "2026-01-10T21:30:51.418Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/6b89a6afd132787d89a362d443a7bddd511b8f41336a1ae47f9e4f000dc4/scipy-1.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:9244608d27eafe02b20558523ba57f15c689357c85bdcfe920b1828750aa26eb", size = 24951312, upload-time = "2026-01-10T21:30:56.771Z" }, + { url = "https://files.pythonhosted.org/packages/e9/01/f58916b9d9ae0112b86d7c3b10b9e685625ce6e8248df139d0fcb17f7397/scipy-1.17.0-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:2b531f57e09c946f56ad0b4a3b2abee778789097871fc541e267d2eca081cff1", size = 31706502, upload-time = "2026-01-10T21:29:56.326Z" }, + { url = "https://files.pythonhosted.org/packages/59/8e/2912a87f94a7d1f8b38aabc0faf74b82d3b6c9e22be991c49979f0eceed8/scipy-1.17.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:13e861634a2c480bd237deb69333ac79ea1941b94568d4b0efa5db5e263d4fd1", size = 28380854, upload-time = "2026-01-10T21:30:01.554Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1c/874137a52dddab7d5d595c1887089a2125d27d0601fce8c0026a24a92a0b/scipy-1.17.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:eb2651271135154aa24f6481cbae5cc8af1f0dd46e6533fb7b56aa9727b6a232", size = 20552752, upload-time = "2026-01-10T21:30:05.93Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f0/7518d171cb735f6400f4576cf70f756d5b419a07fe1867da34e2c2c9c11b/scipy-1.17.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:c5e8647f60679790c2f5c76be17e2e9247dc6b98ad0d3b065861e082c56e078d", size = 22803972, upload-time = "2026-01-10T21:30:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/7c/74/3498563a2c619e8a3ebb4d75457486c249b19b5b04a30600dfd9af06bea5/scipy-1.17.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fb10d17e649e1446410895639f3385fd2bf4c3c7dfc9bea937bddcbc3d7b9ba", size = 32829770, upload-time = "2026-01-10T21:30:16.359Z" }, + { url = "https://files.pythonhosted.org/packages/48/d1/7b50cedd8c6c9d6f706b4b36fa8544d829c712a75e370f763b318e9638c1/scipy-1.17.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8547e7c57f932e7354a2319fab613981cde910631979f74c9b542bb167a8b9db", size = 35051093, upload-time = "2026-01-10T21:30:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/e2/82/a2d684dfddb87ba1b3ea325df7c3293496ee9accb3a19abe9429bce94755/scipy-1.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33af70d040e8af9d5e7a38b5ed3b772adddd281e3062ff23fec49e49681c38cf", size = 34909905, upload-time = "2026-01-10T21:30:28.704Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5e/e565bd73991d42023eb82bb99e51c5b3d9e2c588ca9d4b3e2cc1d3ca62a6/scipy-1.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb55bb97d00f8b7ab95cb64f873eb0bf54d9446264d9f3609130381233483f", size = 37457743, upload-time = "2026-01-10T21:30:34.819Z" }, + { url = "https://files.pythonhosted.org/packages/58/a8/a66a75c3d8f1fb2b83f66007d6455a06a6f6cf5618c3dc35bc9b69dd096e/scipy-1.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1ff269abf702f6c7e67a4b7aad981d42871a11b9dd83c58d2d2ea624efbd1088", size = 37098574, upload-time = "2026-01-10T21:30:40.782Z" }, + { url = "https://files.pythonhosted.org/packages/56/a5/df8f46ef7da168f1bc52cd86e09a9de5c6f19cc1da04454d51b7d4f43408/scipy-1.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:031121914e295d9791319a1875444d55079885bbae5bdc9c5e0f2ee5f09d34ff", size = 25246266, upload-time = "2026-01-10T21:30:45.923Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/62/a7c072fbfefb2980a00f99ca994279cb9ecf310cb2e6b2a4d2a28fe192b3/wcwidth-0.5.3.tar.gz", hash = "sha256:53123b7af053c74e9fe2e92ac810301f6139e64379031f7124574212fb3b4091", size = 157587, upload-time = "2026-01-31T03:52:10.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/c1/d73f12f8cdb1891334a2ccf7389eed244d3941e74d80dd220badb937f3fb/wcwidth-0.5.3-py3-none-any.whl", hash = "sha256:d584eff31cd4753e1e5ff6c12e1edfdb324c995713f75d26c29807bb84bf649e", size = 92981, upload-time = "2026-01-31T03:52:09.14Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]