diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..887a2c1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# SCM syntax highlighting & preventing 3-way merges +pixi.lock merge=binary linguist-language=YAML linguist-generated=true diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 782bc5c..2c65233 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -1,22 +1,27 @@ -name: Testing # Skips RL tests because stable-baselines3 comes with a lot of heavy-weight dependencies - -on: [push] +name: Testing +on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: mamba-org/setup-micromamba@v1 + + - name: Setup Pixi (installs pixi + caches envs) # https://github.com/marketplace/actions/setup-pixi + uses: prefix-dev/setup-pixi@v0.9.0 # pin the action version with: - micromamba-version: '2.0.2-1' # any version from https://github.com/mamba-org/micromamba-releases - environment-name: test-env - init-shell: bash - create-args: python=3.11 - cache-environment: true - - name: Install dependencies and package - run: pip install .[test] - shell: micromamba-shell {0} - - name: Test with pytest - run: pytest tests --cov=crazyflow - shell: micromamba-shell {0} + pixi-version: v0.49.0 # pin the pixi binary version (optional) + cache: true # enable caching of installed envs + # only write new caches on main pushes (TODO: Enable) + # cache-write: ${{ github.event_name == 'push' && github.ref_name == 'main' }} + # ensure the 'test' environment(s) are installed + environments: test + # don't activate env (we'll call pixi run -e test explicitly) + activate-environment: false + # prefer using existing lockfile if present (faster, deterministic) + locked: true + + - name: Verify pixi and run tests + run: | + pixi --version + pixi run -e test pytest \ No newline at end of file diff --git a/.gitignore b/.gitignore index 388792e..1108820 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,7 @@ build **/*.pt tutorials/ppo/wandb dist -benchmark/data \ No newline at end of file +benchmark/data +# pixi environments +.pixi +*.egg-info diff --git a/README.md b/README.md index 21aaedf..4e93c1a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ -------------------------------------------------------------------------------- -Fast, parallelizable simulations of Crazyflies with JAX and MuJoCo. +Fast, parallelizable simulations of Crazyflies with JAX. [![Python Version]][Python Version URL] [![Ruff Check]][Ruff Check URL] [![Documentation Status]][Documentation Status URL] [![Tests]][Tests URL] @@ -32,7 +32,6 @@ The simulation is built as a pipeline of functions that are composed at initiali Multiple physics models are supported: - analytical: A first-principles model based on physical equations - sys_id: A system-identified model trained on real drone data -- mujoco: MuJoCo physics engine for more complex interactions #### Control Modes Different control interfaces are available: @@ -41,9 +40,10 @@ Different control interfaces are available: - thrust: Low-level control of individual motor thrusts #### Integration Methods -For analytical and system-identified physics: +We support multiple integration schemes for additional precision: - euler: Simple first-order integration - rk4: Fourth-order Runge-Kutta integration for higher accuracy +- symplectic\_euler: Symplectic integration for conservation of energy ### Parallelization Crazyflow supports massive parallelization across: @@ -58,6 +58,12 @@ The framework supports domain randomization through the crazyflow/randomize modu ### Functional Design The simulation follows a functional programming paradigm: All state is contained in immutable data structures. Updates create new states rather than modifying existing ones. All functions are pure, enabling JAX's transformations (JIT, grad, vmap) and thus automatic differentiation through the entire simulation, making it suitable for gradient-based optimization and reinforcement learning. +### Contacts and Non-Drone Models +We focus on drones dynamics in free-space flight. Consequently, no models other than drones are available in the simulation and contact dynamics with external objects are not considered. However, we use MuJoCo for contact detection and visualization. Users can load their own objects into the simulation by changing the MuJoCo world spec. Drone collisions with these objects will be detected during collision checks, but they won't have an effect on the dynamics (i.e. drones will pass through objects). Similarly, the objects themselves will be static. + +### Visualization +We use `gymnasium`'s MuJoCo renderer and synchronize the simulation data with MuJoCo to either render an interactive UI or RGB arrays. + ## Examples The repository includes several example scripts demonstrating different capabilities: | Example | Description | diff --git a/benchmark/main.py b/benchmark/main.py index 9fb10c3..1824a01 100644 --- a/benchmark/main.py +++ b/benchmark/main.py @@ -3,10 +3,12 @@ from datetime import datetime from pathlib import Path +import fire import gymnasium import jax import jax.numpy as jnp import numpy as np +from jax.errors import JaxRuntimeError from ml_collections import config_dict import crazyflow # noqa: F401, ensure gymnasium envs are registered @@ -41,7 +43,7 @@ def analyze_timings(times: list[float], n_steps: int, n_worlds: int, freq: float def profile_gym_env_step( - sim_config: config_dict.ConfigDict, n_steps: int, device: str + sim_config: config_dict.ConfigDict, n_steps: int, device: str, print_summary: bool = True ) -> list[float]: """Profile the Crazyflow gym environment step performance.""" times = [] @@ -49,7 +51,7 @@ def profile_gym_env_step( envs = gymnasium.make_vec( "DroneReachPos-v0", - time_horizon_in_seconds=3, + max_episode_time=3, num_envs=sim_config.n_worlds, device=sim_config.device, freq=sim_config.freq, @@ -60,7 +62,7 @@ def profile_gym_env_step( action = np.zeros((sim_config.n_worlds, 4), dtype=np.float32) action[..., 0] = 0.3 # Step through env once to ensure JIT compilation - envs.reset(seed=42) + envs.reset() envs.step(action) jax.block_until_ready(envs.unwrapped.sim.data) # Ensure JIT compiled dynamics @@ -73,12 +75,15 @@ def profile_gym_env_step( times.append(time.perf_counter() - tstart) envs.close() - print("Gym env step performance:") - analyze_timings(times, n_steps, envs.unwrapped.sim.n_worlds, envs.unwrapped.sim.freq) + if print_summary: + print("Gym env step performance:") + analyze_timings(times, n_steps, envs.unwrapped.sim.n_worlds, sim_config.freq) return times -def profile_step(sim_config: config_dict.ConfigDict, n_steps: int, device: str) -> list[float]: +def profile_step( + sim_config: config_dict.ConfigDict, n_steps: int, device: str, print_summary: bool = True +) -> list[float]: """Profile the Crazyflow simulator step performance.""" sim = Sim(**sim_config) times = [] @@ -99,8 +104,9 @@ def profile_step(sim_config: config_dict.ConfigDict, n_steps: int, device: str) jax.block_until_ready(sim.data) times.append(time.perf_counter() - tstart) - print("Sim step performance:") - analyze_timings(times, n_steps, sim.n_worlds, sim.freq) + if print_summary: + print("Sim step performance:") + analyze_timings(times, n_steps, sim.n_worlds, sim.freq) return times @@ -140,9 +146,8 @@ def profile_reset(sim_config: config_dict.ConfigDict, n_steps: int, device: str) analyze_timings(times_masked, n_steps, sim.n_worlds, sim.freq) -def main(): +def main(device: str = "cpu", n_worlds_exp: int = 6): """Main entry point for profiling.""" - device = "cpu" sim_config = config_dict.ConfigDict() sim_config.n_worlds = 1 sim_config.n_drones = 1 @@ -181,93 +186,109 @@ def main(): # Reopen the file in append mode for each result n_steps = 1000 + skip_sim, skip_gym = False, False # Test with increasing number of parallel environments (worlds) - for n_worlds in [1, 10, 100, 1000, 10000, 100000, 1000000]: - print(f"\nTesting with {n_worlds} parallel environments:") + for n_worlds in [10**i for i in range(n_worlds_exp + 1)]: sim_config.n_worlds = n_worlds + print("-" * 80) + if not skip_sim: + # Test with a single step first to see if we should continue + sim_config.freq = 500 # Test sim at 500 hz + single_step_time = profile_step(sim_config, 2, device, print_summary=False)[1] + + # If single step takes too long, skip this and remaining tests + if single_step_time > max_seconds_per_run / n_steps: # threshold for the tests + print( + f" Skipping benchmark for {n_worlds} and higher - projected time " + f"{single_step_time * n_steps:.2f}s (> 1m)" + ) + skip_sim = True + + if not skip_sim: + # Configure simulator + print(f"Running simulator benchmark ({n_worlds} worlds)...") + # Run simulator benchmark using existing function + times_sim = profile_step(sim_config, n_steps, device) + + # Calculate metrics for CSV + total_time = sum(times_sim) + avg_step_time = np.mean(times_sim) + n_frames = n_steps * n_worlds + fps = n_frames / total_time + real_time_factor = (n_steps / sim_config.freq) * n_worlds / total_time + + # Save simulator results + # Reopen CSV writer in append mode + with open(csv_file, "a", newline="") as f: + csv_writer = csv.writer(f) + csv_writer.writerow( + [ + "simulator", + 1, # n_drones + n_worlds, + n_steps, + total_time, + avg_step_time, + fps, + real_time_factor, + sim_config.device, + ] + ) + f.flush() + + if not skip_gym: + print(f"Running gym environment benchmark ({n_worlds} worlds)...") + # Run gym environment benchmark using existing function + sim_config.freq = 50 # Test gym at 50 hz + try: + step_times = profile_gym_env_step(sim_config, 2, device, print_summary=False) + single_step_time = step_times[1] + # If single step takes too long, skip this test only + if single_step_time > max_seconds_per_run / n_steps: # threshold for the tests + print( + f" Skipping benchmark for {n_worlds} - projected time " + f"{single_step_time * n_steps:.2f}s (> 1m)" + ) + skip_gym = True + except JaxRuntimeError: + print(f" Skipping benchmark for {n_worlds} - resource exhausted") + skip_gym = True - # Test with a single step first to see if we should continue - sim_config.freq = 500 # Test sim at 500 hz - test_times = profile_step(sim_config, 1, device) - - single_step_time = test_times[0] - # If single step takes too long, skip this and remaining tests - if single_step_time > max_seconds_per_run / n_steps: # threshold for the tests - print( - f" Skipping benchmark for {n_worlds} and higher - single step took " - f"{single_step_time * 1000:.2f}s (> 1m)" - ) - break - - # Configure simulator - print(f" Running simulator benchmark ({n_worlds} worlds)...") - # Run simulator benchmark using existing function - times_sim = profile_step(sim_config, n_steps, device) - - # Calculate metrics for CSV - total_time = sum(times_sim) - avg_step_time = np.mean(times_sim) - n_frames = n_steps * n_worlds - fps = n_frames / total_time - real_time_factor = (n_steps / sim_config.freq) * n_worlds / total_time - - # Save simulator results - # Reopen CSV writer in append mode - with open(csv_file, "w", newline="") as f: - csv_writer = csv.writer(f) - csv_writer.writerow( - [ - "simulator", - 1, # n_drones - n_worlds, - n_steps, - total_time, - avg_step_time, - fps, - real_time_factor, - sim_config.device, - ] - ) - f.flush() - - print(f" Running gym environment benchmark ({n_worlds} worlds)...") - # Run gym environment benchmark using existing function - sim_config.freq = 50 # Test gym at 50 hz - try: - times_gym = profile_gym_env_step(sim_config, n_steps, device) - except ValueError as e: - if "RESOURCE_EXHAUSTED" in str(e): + if not skip_gym: + try: + times_gym = profile_gym_env_step(sim_config, n_steps, device) + except JaxRuntimeError: print(f" Skipping benchmark for {n_worlds} - resource exhausted") - continue # Only continue, we might still be able to benchmark sim - raise e - - # Calculate metrics for CSV - total_time = sum(times_gym) - avg_step_time = np.mean(times_gym) - n_frames = n_steps * n_worlds - fps = n_frames / total_time - real_time_factor = (n_steps / sim_config.freq) * sim_config.n_worlds / total_time - - # Save gym environment results - with open(csv_file, "a", newline="") as f: - csv_writer = csv.writer(f) - csv_writer.writerow( - [ - "gym_env", - sim_config.n_drones, - sim_config.n_worlds, - n_steps, - total_time, - avg_step_time, - fps, - real_time_factor, - sim_config.device, - ] - ) - f.flush() + skip_gym = True + continue + + # Calculate metrics for CSV + total_time = sum(times_gym) + avg_step_time = np.mean(times_gym) + n_frames = n_steps * n_worlds + fps = n_frames / total_time + real_time_factor = (n_steps / sim_config.freq) * sim_config.n_worlds / total_time + + # Save gym environment results + with open(csv_file, "a", newline="") as f: + csv_writer = csv.writer(f) + csv_writer.writerow( + [ + "gym_env", + sim_config.n_drones, + sim_config.n_worlds, + n_steps, + total_time, + avg_step_time, + fps, + real_time_factor, + sim_config.device, + ] + ) + f.flush() print(f"\nBenchmark results saved to {csv_file}") if __name__ == "__main__": - main() + fire.Fire(main) diff --git a/benchmark/performance.py b/benchmark/performance.py index 8a0c30d..e0a53d5 100644 --- a/benchmark/performance.py +++ b/benchmark/performance.py @@ -13,7 +13,7 @@ from crazyflow.sim import Sim if TYPE_CHECKING: - from crazyflow.gymnasium_envs import CrazyflowEnvReachGoal + from crazyflow.envs import ReachPosEnv def profile_step(sim_config: config_dict.ConfigDict, n_steps: int, device: str): @@ -44,7 +44,7 @@ def profile_step(sim_config: config_dict.ConfigDict, n_steps: int, device: str): def profile_gym_env_step(sim_config: config_dict.ConfigDict, n_steps: int, device: str): device = jax.devices(device)[0] - envs: CrazyflowEnvReachGoal = gymnasium.make_vec( + envs: ReachPosEnv = gymnasium.make_vec( "DroneReachPos-v0", time_horizon_in_seconds=2, num_envs=sim_config.n_worlds, **sim_config ) diff --git a/benchmark/plot.py b/benchmark/plot.py index df15f09..308d0b4 100644 --- a/benchmark/plot.py +++ b/benchmark/plot.py @@ -111,16 +111,25 @@ def format_log_axes(ax: plt.Axes, dfs: dict[str, pd.DataFrame], prefix: str): ax.set_xticklabels([xticklabels[i] for i in valid_indices]) # Get min and max y values for plots - min_y = min([df["fps"].min() for key, df in dfs.items() if key.startswith(prefix)]) - max_y = max([df["fps"].max() for key, df in dfs.items() if key.startswith(prefix)]) + min_y = min([df["fps"].min() for key, df in dfs.items()]) + max_y = max([df["fps"].max() for key, df in dfs.items()]) # Create logarithmic y-ticks - yticks = np.array([1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000]) - mask = (yticks >= min_y * 0.1) & (yticks <= max_y * 10) - valid_indices = np.nonzero(mask)[0] - ax.set_yticks(yticks[valid_indices]) - yticklabels = ["1", "10", "100", "1K", "10K", "100K", "1M", "10M", "100M"] - ax.set_yticklabels([yticklabels[i] for i in valid_indices]) + # Generate yticks based on data range + min_power = int(np.floor(np.log10(min_y))) + max_power = int(np.ceil(np.log10(max_y))) + yticks = np.array([10**i for i in range(min_power, max_power + 1)]) + ax.set_yticks(yticks) + yticklabels = [] + abbrev = {1e9: "B", 1e6: "M", 1e3: "K"} + for i in yticks: + for divisor, suffix in sorted(abbrev.items(), reverse=True): + if i >= divisor: + yticklabels.append(f"{int(i // divisor)}{suffix}") + break + else: + yticklabels.append(f"{int(i)}") + ax.set_yticklabels(yticklabels) # Remove minor ticks for cleaner appearance ax.minorticks_off() diff --git a/crazyflow/__init__.py b/crazyflow/__init__.py index fb0121d..139e868 100644 --- a/crazyflow/__init__.py +++ b/crazyflow/__init__.py @@ -1,6 +1,6 @@ -import crazyflow.gymnasium_envs # noqa: F401, ensure gymnasium envs are registered +import crazyflow.envs # noqa: F401, ensure gymnasium envs are registered from crazyflow.control import Control from crazyflow.sim import Physics, Sim __all__ = ["Sim", "Physics", "Control"] -__version__ = "0.0.1" +__version__ = "0.0.2" diff --git a/crazyflow/envs/__init__.py b/crazyflow/envs/__init__.py new file mode 100644 index 0000000..64e481d --- /dev/null +++ b/crazyflow/envs/__init__.py @@ -0,0 +1,17 @@ +from gymnasium.envs.registration import register + +from crazyflow.envs.figure_8_env import FigureEightEnv +from crazyflow.envs.landing_env import LandingEnv +from crazyflow.envs.norm_actions_wrapper import NormalizeActions +from crazyflow.envs.reach_pos_env import ReachPosEnv +from crazyflow.envs.reach_vel_env import ReachVelEnv + +__all__ = ["ReachPosEnv", "ReachVelEnv", "LandingEnv", "NormalizeActions", "FigureEightEnv"] + +register(id="DroneReachPos-v0", vector_entry_point=ReachPosEnv) + +register(id="DroneReachVel-v0", vector_entry_point=ReachVelEnv) + +register(id="DroneLanding-v0", vector_entry_point=LandingEnv) + +register(id="DroneFigureEightTrajectory-v0", vector_entry_point=FigureEightEnv) diff --git a/crazyflow/envs/drone_env.py b/crazyflow/envs/drone_env.py new file mode 100644 index 0000000..24f28f9 --- /dev/null +++ b/crazyflow/envs/drone_env.py @@ -0,0 +1,218 @@ +import warnings +from functools import partial +from typing import Callable, Literal + +import jax +import jax.numpy as jnp +import numpy as np +from gymnasium import spaces +from gymnasium.vector import AutoresetMode, VectorEnv +from gymnasium.vector.utils import batch_space +from jax import Array +from numpy.typing import NDArray + +from crazyflow.control.control import MAX_THRUST, MIN_THRUST, Control +from crazyflow.sim import Sim +from crazyflow.sim.physics import Physics +from crazyflow.sim.structs import SimData +from crazyflow.utils import leaf_replace + + +def action_space(control_type: Control) -> spaces.Box: + """Select the appropriate action space for a given control type. + + Args: + control_type: The desired control mode. + + Returns: + The action space. + """ + match control_type: + case Control.attitude: + return spaces.Box( + np.array([4 * MIN_THRUST, -np.pi / 2, -np.pi / 2, -np.pi / 2], dtype=np.float32), + np.array([4 * MAX_THRUST, np.pi / 2, np.pi / 2, np.pi / 2], dtype=np.float32), + ) + case Control.thrust: + return spaces.Box(MIN_THRUST, MAX_THRUST, shape=(4,)) + case _: + raise ValueError(f"Invalid control type {control_type}") + + +class DroneEnv(VectorEnv): + """JAX-based Gymnasium core environment for Crazyflow. + + ## Action space + We have three types of actions: + - `attitude`: 4D vector consisting of [collective thrust, roll, pitch, yaw] + - `thrust`: 4D vector consisting of the individual motor thrusts [f1, f2, f3, f4] + - `state`: Currently not implemented + + The default action space is `attitude`. + """ + + metadata = {"autoreset_mode": AutoresetMode.NEXT_STEP} + + def __init__( + self, + *, + num_envs: int = 1, + max_episode_time: float = 10.0, + physics: Literal["sys_id", "analytical"] | Physics = Physics.sys_id, + freq: int = 500, + device: str = "cpu", + reset_randomization: Callable[[SimData, Array], SimData] | None = None, + ): + """Initialize the CrazyflowEnv. + + Args: + num_envs: The number of environments to run in parallel. + max_episode_time: The time horizon after which episodes are truncated (s). + physics: The crazyflow physics simulation model. + freq: The frequency at which the environment is run. + device: The device of the environment and the simulation. + reset_randomization: A function that randomizes the initial state of the simulation. If + None, the default randomization for pos and vel is used. + """ + self.num_envs = num_envs + self.device = jax.devices(device)[0] + self.freq = freq + self.max_episode_time = max_episode_time + assert Physics(physics) in Physics, f"Invalid physics type {physics}" + + # Initialize the simulation + self.sim = Sim(n_worlds=num_envs, n_drones=1, device=device, physics=physics) + assert self.sim.freq >= self.sim.control_freq, "Sim freq must be higher than control freq" + if not self.sim.freq % self.freq == 0: + # We can handle other cases, but it's not recommended + warnings.warn("Simulation frequency should be a multiple of env frequency.") + if self.sim.control == Control.state: + raise NotImplementedError("State control currently not supported") + + # Compile our domain randomization on reset into the sim pipeline + if reset_randomization is None: + reset_randomization = self._reset_randomization + self.sim.reset_pipeline += (reset_randomization,) + self.sim.build_reset_fn() + + self.n_substeps = self.sim.freq // self.freq + self._marked_for_reset = jnp.zeros((self.sim.n_worlds), dtype=jnp.bool_, device=self.device) + + # Define action and observation spaces + self.single_action_space = action_space(self.sim.control) + self.action_space = batch_space(self.single_action_space, self.sim.n_worlds) + self.single_observation_space = spaces.Dict( + { + "pos": spaces.Box(-np.inf, np.inf, shape=(3,)), + "quat": spaces.Box(-np.inf, np.inf, shape=(4,)), + "vel": spaces.Box(-np.inf, np.inf, shape=(3,)), + "ang_vel": spaces.Box(-np.inf, np.inf, shape=(3,)), + } + ) + self.observation_space = batch_space(self.single_observation_space, self.sim.n_worlds) + + def step(self, action: Array) -> tuple[Array, Array, Array, Array, dict]: + self._apply_action(action) + self.sim.step(self.n_substeps) + # Reset all environments which terminated or were truncated in the last step + self._reset(mask=self._marked_for_reset) + terminated, truncated = self.terminated(), self.truncated() + self._marked_for_reset = self._done(terminated, truncated) + return self.obs(), self.reward(), terminated, truncated, {} + + def _apply_action(self, action: Array): + low, high = self.action_space.low, self.action_space.high + action = self._sanitize_action(action, low, high, self.sim.n_worlds, self.sim.device) + match self.sim.control: + case Control.state: + raise NotImplementedError("State control currently not supported") + case Control.attitude: + self.sim.attitude_control(action) + case Control.thrust: + self.sim.thrust_control(action) + case _: + raise ValueError(f"Invalid control type {self.sim.control}") + + @staticmethod + @partial(jax.jit, static_argnames=["n_worlds", "device"]) + def _sanitize_action( + action: Array, low: NDArray, high: NDArray, n_worlds: int, device: str + ) -> Array: + action = jnp.clip(action, low, high) + return jnp.array(action, device=device).reshape((n_worlds, 1, -1)) + + @staticmethod + @jax.jit + def _done(terminated: Array, truncated: Array) -> Array: + return terminated | truncated + + def reset( + self, *, seed: int | None = None, options: dict | None = None + ) -> tuple[dict[str, Array], dict]: + super().reset(seed=seed) + if seed is not None: + self.sim.seed(seed) + self._reset(options=options) + self._marked_for_reset = self._marked_for_reset.at[...].set(False) + return self.obs(), {} + + def render(self): + self.sim.render() + + def close(self): + self.sim.close() + + def _reset(self, mask: Array | None = None, options: dict | None = None) -> None: + self.sim.reset(mask=mask) + + def reward(self) -> Array: + raise NotImplementedError + + def terminated(self) -> Array: + return self._terminated(self.sim.data.states.pos) + + def truncated(self) -> Array: + return self._truncated(self.sim.time[..., 0], self.max_episode_time) + + def _reward(): + raise NotImplementedError + + @staticmethod + @jax.jit + def _terminated(pos: Array) -> Array: + return pos[:, 0, 2] < 0 # Terminate if the drone has crashed into the ground + + @staticmethod + @jax.jit + def _truncated(time: Array, max_episode_time: float) -> Array: + return time >= max_episode_time + + def obs(self) -> dict[str, Array]: + obs = { + "pos": self.sim.data.states.pos, + "quat": self.sim.data.states.quat, + "vel": self.sim.data.states.vel, + "ang_vel": self.sim.data.states.ang_vel, + } + # drop n_drones dimension, as it is always 1 for now + return {k: v[:, 0, :] for k, v in obs.items()} + + @staticmethod + def _reset_randomization(data: SimData, mask: Array) -> SimData: + """Randomize the initial position and velocity of the drones. + + This function will get compiled into the reset function of the simulation. Therefore, it + must take data and mask as input arguments and must return a SimData object. + """ + # Sample initial position + shape = (data.core.n_worlds, data.core.n_drones, 3) + pos_min = jnp.array([-1.0, -1.0, 1.0]) + pos_max = jnp.array([1.0, 1.0, 2.0]) + key, pos_key, vel_key = jax.random.split(data.core.rng_key, 3) + data = data.replace(core=data.core.replace(rng_key=key)) + pos = jax.random.uniform(key=pos_key, shape=shape, minval=pos_min, maxval=pos_max) + # Sample initial velocity + vel = jax.random.uniform(key=vel_key, shape=shape, minval=-1.0, maxval=1.0) + # Setting initial ryp_rate when using physics.sys_id will not have an impact, so we skip it + data = data.replace(states=leaf_replace(data.states, mask, pos=pos, vel=vel)) + return data diff --git a/crazyflow/envs/figure_8_env.py b/crazyflow/envs/figure_8_env.py new file mode 100644 index 0000000..3c5b741 --- /dev/null +++ b/crazyflow/envs/figure_8_env.py @@ -0,0 +1,127 @@ +from typing import Literal + +import jax +import jax.numpy as jnp +import numpy as np +from gymnasium import spaces +from gymnasium.vector.utils import batch_space +from jax import Array + +from crazyflow.envs.drone_env import DroneEnv +from crazyflow.sim.physics import Physics +from crazyflow.sim.structs import SimData +from crazyflow.sim.visualize import draw_line, draw_points +from crazyflow.utils import leaf_replace + + +class FigureEightEnv(DroneEnv): + """Drone environment for following a figure-eight trajectory. + + This environment is used to follow a figure-eight trajectory. The observations contain the + relative position errors to the next `n_samples` points that are distanced by `samples_dt`. The + reward is based on the distance to the next trajectory point. + """ + + def __init__( + self, + n_samples: int = 10, + samples_dt: float = 0.1, + trajectory_time: float = 10.0, + *, + num_envs: int = 1, + max_episode_time: float = 10.0, + physics: Literal["sys_id", "analytical"] | Physics = Physics.sys_id, + freq: int = 500, + device: str = "cpu", + ): + """Initialize the environment and create the figure-eight trajectory. + + Args: + n_samples: Number of next trajectory points to sample for observations. + samples_dt: Time between trajectory sample points in seconds. + trajectory_time: Total time for completing the figure-eight trajectory in seconds. + num_envs: Number of environments to run in parallel. + max_episode_time: Maximum episode time in seconds. + physics: Physics backend to use. + freq: Frequency of the simulation. + device: Device to use for the simulation. + """ + super().__init__( + num_envs=num_envs, + max_episode_time=max_episode_time, + physics=physics, + freq=freq, + device=device, + ) + if trajectory_time < self.max_episode_time: + raise ValueError("Trajectory time must be greater than max episode time") + + # Create the figure eight trajectory + n_steps = int(np.ceil(trajectory_time * self.freq)) + t = np.linspace(0, 2 * np.pi, n_steps) + radius = 1 # Radius for the circles + y = np.zeros_like(t) # x is 0 everywhere + x = radius * np.sin(t) # Scale amplitude for 1-meter diameter + z = radius * np.sin(2 * t) + 1.2 # Scale amplitude for 1-meter diameter + self.trajectory = np.array([x, y, z]).T + + # Define trajectory sampling parameters + self.n_samples = n_samples + self.samples_dt = samples_dt + self.sample_offsets = np.array(np.arange(n_samples) * self.freq * samples_dt, dtype=int) + + # Update observation space + spec = {k: v for k, v in self.single_observation_space.items()} + spec["local_samples"] = spaces.Box(-np.inf, np.inf, shape=(3 * self.n_samples,)) + self.single_observation_space = spaces.Dict(spec) + self.observation_space = batch_space(self.single_observation_space, self.sim.n_worlds) + + def render(self): + idx = (self.steps + self.sample_offsets[None, ...]) % self.trajectory.shape[0] + next_trajectory = self.trajectory[idx, ...] + draw_line(self.sim, next_trajectory[0], start_size=3.0, end_size=3.0) + draw_points(self.sim, next_trajectory[0], rgba=np.array([1.0, 0, 0, 1]), size=0.01) + self.sim.render() + + def obs(self) -> dict[str, Array]: + obs = super().obs() + idx = (self.steps + self.sample_offsets[None, ...]) % self.trajectory.shape[0] + dpos = self.trajectory[idx, ...] - self.sim.data.states.pos + obs["local_samples"] = dpos.reshape(-1, 3 * self.n_samples) + return obs + + def reward(self) -> Array: + return self._reward( + self.terminated(), self.sim.data.states.pos, self.trajectory[self.steps.squeeze()] + ) + + @property + def steps(self) -> Array: + """The current step in the trajectory.""" + return self.sim.data.core.steps // (self.sim.freq // self.freq) - 1 + + @staticmethod + @jax.jit + def _reward(terminated: Array, pos: Array, goal: Array) -> Array: + # distance to next trajectory point + norm_distance = jnp.linalg.norm(pos[:, 0, :] - goal, axis=-1) + reward = jnp.exp(-2.0 * norm_distance) + reward = jnp.where(terminated, -1.0, reward) + return reward + + @staticmethod + def _reset_randomization(data: SimData, mask: Array) -> SimData: + """Randomize the initial position and velocity of the drones. + + This function will get compiled into the reset function of the simulation. Therefore, it + must take data and mask as input arguments and must return a SimData object. + """ + # Sample initial position + shape = (data.core.n_worlds, data.core.n_drones, 3) + pmin, pmax = jnp.array([-0.1, -0.1, 1.1]), jnp.array([0.1, 0.1, 1.3]) + key, pos_key, vel_key = jax.random.split(data.core.rng_key, 3) + data = data.replace(core=data.core.replace(rng_key=key)) + pos = jax.random.uniform(key=pos_key, shape=shape, minval=pmin, maxval=pmax) + vel = jax.random.uniform(key=vel_key, shape=shape, minval=-0.5, maxval=0.5) + data = data.replace(states=leaf_replace(data.states, mask, pos=pos, vel=vel)) + return data diff --git a/crazyflow/envs/landing_env.py b/crazyflow/envs/landing_env.py new file mode 100644 index 0000000..5fdd1fb --- /dev/null +++ b/crazyflow/envs/landing_env.py @@ -0,0 +1,67 @@ +from typing import Literal + +import jax +import jax.numpy as jnp +import mujoco +import numpy as np +from gymnasium import spaces +from gymnasium.vector.utils import batch_space +from jax import Array + +from crazyflow.envs.drone_env import DroneEnv +from crazyflow.sim.physics import Physics +from crazyflow.sim.structs import SimState + + +class LandingEnv(DroneEnv): + """Drone environment for landing at a target position.""" + + def __init__( + self, + num_envs: int = 1, + max_episode_time: float = 10.0, + physics: Literal["sys_id", "analytical"] | Physics = Physics.sys_id, + freq: int = 500, + device: str = "cpu", + ): + super().__init__( + num_envs=num_envs, + max_episode_time=max_episode_time, + physics=physics, + freq=freq, + device=device, + ) + spec = {k: v for k, v in self.single_observation_space.items()} + spec["difference_to_goal"] = spaces.Box(-np.inf, np.inf, shape=(3,)) + self.single_observation_space = spaces.Dict(spec) + self.observation_space = batch_space(self.single_observation_space, self.sim.n_worlds) + self._goal = jnp.zeros((self.sim.n_worlds, 3), device=self.device) + self._goal = self._goal.at[..., 2].set(0.1) # 10cm above ground + + def render(self): + if self.sim.viewer is not None: + self.sim.viewer.viewer.add_marker( + type=mujoco.mjtGeom.mjGEOM_SPHERE, + size=np.array([0.02, 0.02, 0.02]), + pos=np.array(self._goal[0]), + rgba=np.array([1, 0, 0, 0.5]), + ) + self.sim.render() + + def obs(self) -> dict[str, Array]: + obs = super().obs() + # drop n_drones dimension, as it is always 1 for now + obs["difference_to_goal"] = self._goal - self.sim.data.states.pos[:, 0, :] + return obs + + def reward(self) -> Array: + return self._reward(self.terminated(), self.sim.data.states, self._goal) + + @staticmethod + @jax.jit + def _reward(terminated: Array, states: SimState, goal: Array) -> Array: + norm_distance = jnp.linalg.norm(states.pos[:, 0, :] - goal, axis=-1) + speed = jnp.linalg.norm(states.vel[:, 0, :], axis=-1) + reward = jnp.exp(-2.0 * norm_distance) * jnp.exp(-2.0 * speed) + reward = jnp.where(terminated, -1.0, reward) + return reward diff --git a/crazyflow/envs/norm_actions_wrapper.py b/crazyflow/envs/norm_actions_wrapper.py new file mode 100644 index 0000000..1f11b28 --- /dev/null +++ b/crazyflow/envs/norm_actions_wrapper.py @@ -0,0 +1,43 @@ +import jax +import jax.numpy as jnp +import numpy as np +from gymnasium.vector import VectorWrapper +from gymnasium.vector.utils import batch_space +from jax import Array + +from crazyflow.envs.drone_env import DroneEnv + + +class NormalizeActions(VectorWrapper): + """Normalize the actions to be in the range [-1, 1]. + + This wrapper clips the expected actions to [-1,1] and rescales them to the action space expected + in simulation. + """ + + def __init__(self, env: DroneEnv): + super().__init__(env) + # Simulation action space bounds + self.action_sim_low = self.single_action_space.low + self.action_sim_high = self.single_action_space.high + + # Compute scale and mean for rescaling + device = env.device + self._scale = jnp.array((self.action_sim_high - self.action_sim_low) / 2.0, device=device) + self._mean = jnp.array((self.action_sim_high + self.action_sim_low) / 2.0, device=device) + + # Modify the wrapper's action space to [-1, 1] + self.single_action_space.low = -np.ones_like(self.action_sim_low) + self.single_action_space.high = np.ones_like(self.action_sim_high) + self.action_space = batch_space(self.single_action_space, self.num_envs) + + def step(self, actions: Array) -> tuple[dict, Array, Array, Array, dict]: + action = self._scale_actions(actions, self._scale, self._mean) + obs, reward, terminated, truncated, info = self.env.step(action) + return obs, reward, terminated, truncated, info + + @staticmethod + @jax.jit + def _scale_actions(actions: Array, scale: Array, mean: Array) -> Array: + """Rescale and clip actions from [-1, 1] to [action_sim_low, action_sim_high].""" + return jnp.clip(actions, -1.0, 1.0) * scale + mean diff --git a/crazyflow/envs/reach_pos_env.py b/crazyflow/envs/reach_pos_env.py new file mode 100644 index 0000000..2613ebf --- /dev/null +++ b/crazyflow/envs/reach_pos_env.py @@ -0,0 +1,112 @@ +from functools import partial +from typing import Literal + +import jax +import jax.numpy as jnp +import mujoco +import numpy as np +from gymnasium import spaces +from gymnasium.vector.utils import batch_space +from jax import Array + +from crazyflow.envs.drone_env import DroneEnv +from crazyflow.sim.physics import Physics +from crazyflow.sim.structs import SimData +from crazyflow.utils import leaf_replace + + +class ReachPosEnv(DroneEnv): + """Drone environment for reaching a goal position.""" + + def __init__( + self, + pos_min: Array | None = None, + pos_max: Array | None = None, + vel_min: float = -1.0, + vel_max: float = 1.0, + num_envs: int = 1, + max_episode_time: float = 10.0, + physics: Literal["sys_id", "analytical"] | Physics = Physics.sys_id, + freq: int = 500, + device: str = "cpu", + ): + pos_min = jnp.array([-1.0, -1.0, 1.0]) if pos_min is None else pos_min + pos_max = jnp.array([1.0, 1.0, 2.0]) if pos_max is None else pos_max + reset_randomization = partial( + self._reset_randomization, pmin=pos_min, pmax=pos_max, vmin=vel_min, vmax=vel_max + ) + super().__init__( + num_envs=num_envs, + max_episode_time=max_episode_time, + physics=physics, + freq=freq, + device=device, + reset_randomization=reset_randomization, + ) + self.jax_key = jax.device_put( + jax.random.key(int(self.np_random.random() * 2**32)), self.device + ) + spec = {k: v for k, v in self.single_observation_space.items()} + spec["difference_to_goal"] = spaces.Box(-np.inf, np.inf, shape=(3,)) + self.single_observation_space = spaces.Dict(spec) + self.observation_space = batch_space(self.single_observation_space, self.sim.n_worlds) + self._goal = jnp.zeros((self.sim.n_worlds, 3), dtype=jnp.float32, device=self.device) + + def _reset(self, mask: Array | None = None, options: dict | None = None): + if options is None: + options = {} + self.jax_key, subkey = jax.random.split(self.jax_key) + pmin, pmax = options.get("goal_pos_min"), options.get("goal_pos_max") + self._goal = self._sample_goal(subkey, self._goal, pmin, pmax, mask) + self.sim.reset(mask=mask) + + def render(self): + if self.sim.viewer is not None: + self.sim.viewer.viewer.add_marker( + type=mujoco.mjtGeom.mjGEOM_SPHERE, + size=np.array([0.02, 0.02, 0.02]), + pos=np.array(self._goal[0]), + rgba=np.array([1, 0, 0, 0.5]), + ) + self.sim.render() + + def obs(self) -> dict[str, Array]: + obs = super().obs() + # drop n_drones dimension, as it is always 1 for now + obs["difference_to_goal"] = self._goal - self.sim.data.states.pos[:, 0, :] + return obs + + def reward(self) -> Array: + return self._reward(self.terminated(), self.sim.data.states.pos, self._goal) + + @staticmethod + @jax.jit + def _reward(terminated: Array, pos: Array, goal: Array) -> Array: + norm_distance = jnp.linalg.norm(pos[:, 0, :] - goal, axis=-1) + reward = jnp.exp(-2.0 * norm_distance) + reward = jnp.where(terminated, -1.0, reward) + return reward + + @staticmethod + @jax.jit + def _sample_goal( + key: Array, goal: Array, pmin: Array | None, pmax: Array | None, mask: Array | None + ) -> Array: + pmin = jnp.array([-1.0, -1.0, 0.5]) if pmin is None else pmin + pmax = jnp.array([1.0, 1.0, 1.5]) if pmax is None else pmax + new_goal = jax.random.uniform(key, shape=goal.shape, minval=pmin, maxval=pmax) + if mask is not None: + new_goal = jnp.where(mask[..., None], new_goal, goal) + return new_goal + + @staticmethod + def _reset_randomization( + data: SimData, mask: Array, pmin: Array, pmax: Array, vmin: float, vmax: float + ) -> SimData: + shape = (data.core.n_worlds, data.core.n_drones, 3) + key, pos_key, vel_key = jax.random.split(data.core.rng_key, 3) + data = data.replace(core=data.core.replace(rng_key=key)) + pos = jax.random.uniform(key=pos_key, shape=shape, minval=pmin, maxval=pmax) + vel = jax.random.uniform(key=vel_key, shape=shape, minval=vmin, maxval=vmax) + data = data.replace(states=leaf_replace(data.states, mask, pos=pos, vel=vel)) + return data diff --git a/crazyflow/envs/reach_vel_env.py b/crazyflow/envs/reach_vel_env.py new file mode 100644 index 0000000..3727ad2 --- /dev/null +++ b/crazyflow/envs/reach_vel_env.py @@ -0,0 +1,78 @@ +from typing import Literal + +import jax +import jax.numpy as jnp +import numpy as np +from gymnasium import spaces +from gymnasium.vector.utils import batch_space +from jax import Array + +from crazyflow.envs.drone_env import DroneEnv +from crazyflow.sim.physics import Physics + + +class ReachVelEnv(DroneEnv): + """Drone environment for reaching a goal velocity.""" + + def __init__( + self, + *, + num_envs: int = 1, + max_episode_time: float = 10.0, + physics: Literal["sys_id", "analytical"] | Physics = Physics.sys_id, + freq: int = 500, + device: str = "cpu", + ): + super().__init__( + num_envs=num_envs, + max_episode_time=max_episode_time, + physics=physics, + freq=freq, + device=device, + ) + self.jax_key = jax.device_put( + jax.random.key(int(self.np_random.random() * 2**32)), self.device + ) + assert self.sim.n_drones == 1, "Currently only supported for one drone" + spec = {k: v for k, v in self.single_observation_space.items()} + spec["difference_to_target_vel"] = spaces.Box(-np.inf, np.inf, shape=(3,)) + self.single_observation_space = spaces.Dict(spec) + self.observation_space = batch_space(self.single_observation_space, self.sim.n_worlds) + self._goal_vel = jnp.zeros((self.sim.n_worlds, 3), dtype=jnp.float32, device=self.device) + + def obs(self) -> dict[str, Array]: + obs = super().obs() + # drop n_drones dimension, as it is always 1 for now + obs["difference_to_target_vel"] = self._goal_vel - self.sim.data.states.vel[:, 0, :] + return obs + + def reward(self) -> Array: + return self._reward(self.terminated(), self.sim.data.states.vel, self._goal_vel) + + @staticmethod + @jax.jit + def _reward(terminated: Array, vel: Array, target_vel: Array) -> Array: + norm_distance = jnp.linalg.norm(vel[:, 0, :] - target_vel, axis=-1) + reward = jnp.exp(-norm_distance) + reward = jnp.where(terminated, -1.0, reward) + return reward + + def _reset(self, mask: Array | None = None, options: dict | None = None) -> None: + if options is None: + options = {} + self.jax_key, subkey = jax.random.split(self.jax_key) + vmin, vmax = options.get("vel_min"), options.get("vel_max") + self._goal_vel = self._sample_goal_vel(subkey, self._goal_vel, vmin, vmax, mask) + self.sim.reset(mask=mask) + + @staticmethod + @jax.jit + def _sample_goal_vel( + key: Array, goal_vel: Array, vmin: Array | None, vmax: Array | None, mask: Array | None + ) -> Array: + vmin = jnp.array([-1.0, -1.0, -1.0]) if vmin is None else vmin + vmax = jnp.array([1.0, 1.0, 1.0]) if vmax is None else vmax + new_goal_vel = jax.random.uniform(key, shape=goal_vel.shape, minval=vmin, maxval=vmax) + if mask is not None: + new_goal_vel = jnp.where(mask[..., None], new_goal_vel, goal_vel) + return new_goal_vel diff --git a/crazyflow/gymnasium_envs/__init__.py b/crazyflow/gymnasium_envs/__init__.py deleted file mode 100644 index b1c2e2d..0000000 --- a/crazyflow/gymnasium_envs/__init__.py +++ /dev/null @@ -1,44 +0,0 @@ -from gymnasium.envs.registration import register - -from crazyflow.gymnasium_envs.crazyflow import ( - CrazyflowEnvFigureEightTrajectory, - CrazyflowEnvLanding, - CrazyflowEnvReachGoal, - CrazyflowEnvTargetVelocity, - CrazyflowRL, - FigureEightXY, -) - -__all__ = [ - "CrazyflowEnvReachGoal", - "CrazyflowEnvTargetVelocity", - "CrazyflowEnvLanding", - "CrazyflowRL", - "CrazyflowEnvFigureEightTrajectory", - "FigureEightXY", -] - -register( - id="DroneReachPos-v0", - vector_entry_point="crazyflow.gymnasium_envs.crazyflow:CrazyflowEnvReachGoal", -) - -register( - id="DroneReachVel-v0", - vector_entry_point="crazyflow.gymnasium_envs.crazyflow:CrazyflowEnvTargetVelocity", -) - -register( - id="DroneLanding-v0", - vector_entry_point="crazyflow.gymnasium_envs.crazyflow:CrazyflowEnvLanding", -) - -register( - id="DroneFigureEightTrajectory-v0", - vector_entry_point="crazyflow.gymnasium_envs.crazyflow:CrazyflowEnvFigureEightTrajectory", -) - -register( - id="DroneFigureEightXY-v0", - vector_entry_point="crazyflow.gymnasium_envs.crazyflow:FigureEightXY", -) diff --git a/crazyflow/gymnasium_envs/crazyflow.py b/crazyflow/gymnasium_envs/crazyflow.py deleted file mode 100644 index 2139164..0000000 --- a/crazyflow/gymnasium_envs/crazyflow.py +++ /dev/null @@ -1,653 +0,0 @@ -import warnings -from functools import partial -from typing import Literal - -import jax -import jax.numpy as jnp -import mujoco -import numpy as np -from gymnasium import spaces -from gymnasium.envs.mujoco.mujoco_rendering import MujocoRenderer -from gymnasium.vector import VectorEnv, VectorWrapper -from gymnasium.vector.utils import batch_space -from jax import Array - -from crazyflow.control.control import MAX_THRUST, MIN_THRUST, Control -from crazyflow.sim import Sim -from crazyflow.sim.structs import SimState - - -def action_space(control_type: Control) -> spaces.Box: - """Select the appropriate action space for a given control type. - - Args: - control_type: The desired control mode. - - Returns: - The action space. - """ - match control_type: - case Control.attitude: - return spaces.Box( - np.array([4 * MIN_THRUST, -np.pi / 2, -np.pi / 2, -np.pi / 2], dtype=np.float32), - np.array([4 * MAX_THRUST, np.pi / 2, np.pi / 2, np.pi / 2], dtype=np.float32), - ) - case Control.thrust: - return spaces.Box(MIN_THRUST, MAX_THRUST, shape=(4,)) - case _: - raise ValueError(f"Invalid control type {control_type}") - - -class CrazyflowBaseEnv(VectorEnv): - """JAX Gymnasium environment for Crazyflie simulation. - - ## Action space - We have three types of actions: - - `attitude`: 4D vector consisting of [collective thrust, roll, pitch, yaw] - - `thrust`: 4D vector consisting of the individual motor thrusts [f1, f2, f3, f4] - - `state`: Currently not implemented - - The default action space is `attitude`. - """ - - obs_keys = ["pos", "quat", "vel", "ang_vel"] - # TODO: Once we switch to gymnasium >= 1.1.0, we should set the autoreset mode - # metadata = {"autoreset_mode": AutoresetMode.NEXT_STEP} - - def __init__( - self, - *, - num_envs: int = 1, # required for VectorEnv - time_horizon_in_seconds: float = 10.0, - physics: Literal["sys_id", "analytical"] = "sys_id", - freq: int = 500, - device: str = "cpu", - ): - """Initialize the CrazyflowEnv. - - Args: - num_envs: The number of environments to run in parallel. - time_horizon_in_seconds: The time horizon after which episodes are truncated. - physics: The crazyflow physics simulation model. - freq: The frequency at which the environment is run. - device: The device of the environment and the simulation. - """ - self.num_envs = num_envs - self.device = jax.devices(device)[0] - # Set random initial seed for JAX. For seeding, people should use the reset function - self.jax_key = jax.device_put( - jax.random.key(int(self.np_random.random() * 2**32)), self.device - ) - - self.time_horizon_in_seconds = time_horizon_in_seconds - assert physics in ("sys_id", "analytical"), "Invalid physics type" - self.sim = Sim(n_worlds=num_envs, n_drones=1, device=device, physics=physics) - - self.freq = freq - assert self.sim.freq >= self.sim.control_freq, "Sim freq must be higher than control freq" - if not self.sim.freq % self.freq == 0: - # We can handle other cases, but it's not recommended - warnings.warn("Simulation frequency should be a multiple of env frequency.") - if self.sim.control == Control.state: - raise NotImplementedError("State control currently not supported") - - self.n_substeps = self.sim.freq // self.freq - self.prev_done = jnp.zeros((self.sim.n_worlds), dtype=jnp.bool_, device=self.device) - - self.single_action_space = action_space(self.sim.control) - self.action_space = batch_space(self.single_action_space, self.sim.n_worlds) - - self.single_observation_space = spaces.Dict( - { - "pos": spaces.Box(-np.inf, np.inf, shape=(3,)), - "quat": spaces.Box(-np.inf, np.inf, shape=(4,)), - "vel": spaces.Box(-np.inf, np.inf, shape=(3,)), - "ang_vel": spaces.Box(-np.inf, np.inf, shape=(3,)), - } - ) - self.observation_space = batch_space(self.single_observation_space, self.sim.n_worlds) - - def step(self, action: Array) -> tuple[Array, Array, Array, Array, dict]: - action = np.clip(action, self.action_space.low, self.action_space.high) - assert self.action_space.contains(np.array(action)), f"{action!r} ({type(action)}) invalid" - action = self._sanitize_action(action, self.sim.n_worlds, self.sim.n_drones, self.device) - self._apply_action(action) - - self.sim.step(self.n_substeps) - # Reset all environments which terminated or were truncated in the last step - if jnp.any(self.prev_done): - self.reset_masked(mask=self.prev_done) - - terminated = self.terminated - truncated = self.truncated - # We need to calculate the reward before setting `self.prev_done`, because reward depends on - # prev_done. Moving this line to after the write to prev_done results in wrong rewards. - reward = self.reward - self.prev_done = self._done(terminated, truncated) - - terminated = terminated - truncated = truncated - return self._obs(), reward, terminated, truncated, {} - - def _apply_action(self, action: Array): - match self.sim.control: - case Control.state: - raise NotImplementedError("State control currently not supported") - case Control.attitude: - self.sim.attitude_control(action) - case Control.thrust: - self.sim.thrust_control(action) - case _: - raise ValueError(f"Invalid control type {self.sim.control}") - - @staticmethod - @partial(jax.jit, static_argnames=["n_worlds", "n_drones", "device"]) - def _sanitize_action(action: Array, n_worlds: int, n_drones: int, device: str) -> Array: - return jnp.array(action, device=device).reshape((n_worlds, n_drones, -1)) - - @staticmethod - @jax.jit - def _done(terminated: Array, truncated: Array) -> Array: - return jnp.logical_or(terminated, truncated) - - def reset( - self, *, seed: int | None = None, options: dict | None = None - ) -> tuple[dict[str, Array], dict]: - super().reset(seed=seed) - if seed is not None: - self.jax_key = jax.random.key(seed) - - self.reset_masked( - mask=jnp.ones((self.sim.n_worlds), dtype=bool, device=self.device), reset_params=options - ) - self.prev_done = jnp.zeros((self.sim.n_worlds), dtype=bool, device=self.device) - return self._obs(), {} - - def reset_masked(self, mask: Array, reset_params: dict | None = None) -> None: - if reset_params is None: - reset_params = {} - - default_drone_reset_params = { - "pos_min": reset_params.pop("pos_min", jnp.array([-1.0, -1.0, 1.0])), # x,y,z - "pos_max": reset_params.pop("pos_max", jnp.array([1.0, 1.0, 2.0])), # x,y,z - "vel_min": reset_params.pop("vel_min", -1.0), - "vel_max": reset_params.pop("vel_max", 1.0), - } - - # sanity check to see if all keys have been used - if len(reset_params) > 0: - warnings.warn( - f"Unused reset parameters: {reset_params.keys()}. " - "These will be ignored in the reset function. In case this parameter has already been used, please make sure to pop it from the dictionary." - ) - - self.sim.reset(mask=mask) - mask3d = mask[:, None, None] - # NOTE Setting initial ryp_rate when using physics.sys_id will not have an impact - # Sample initial pos - self.jax_key, subkey = jax.random.split(self.jax_key) - init_pos = jax.random.uniform( - key=subkey, - shape=(self.sim.n_worlds, self.sim.n_drones, 3), - minval=default_drone_reset_params["pos_min"], - maxval=default_drone_reset_params["pos_max"], - ) - self.sim.data = self.sim.data.replace( - states=self.sim.data.states.replace( - pos=jnp.where(mask3d, init_pos, self.sim.data.states.pos) - ) - ) - # Sample initial vel - self.jax_key, subkey = jax.random.split(self.jax_key) - init_vel = jax.random.uniform( - key=subkey, - shape=(self.sim.n_worlds, self.sim.n_drones, 3), - minval=default_drone_reset_params["vel_min"], - maxval=default_drone_reset_params["vel_max"], - ) - self.sim.data = self.sim.data.replace( - states=self.sim.data.states.replace( - vel=jnp.where(mask3d, init_vel, self.sim.data.states.vel) - ) - ) - - @property - def reward(self) -> Array: - return self._reward(self.prev_done, self.terminated, self.sim.data.states) - - @property - def terminated(self) -> Array: - return self._terminated(self.prev_done, self.sim.data.states, self.sim.contacts()) - - @property - def truncated(self) -> Array: - return self._truncated(self.prev_done, self.sim.time, self.time_horizon_in_seconds) - - def _reward() -> None: - raise NotImplementedError - - @staticmethod - @jax.jit - def _terminated(dones: Array, states: SimState, contacts: Array) -> Array: - contact = jnp.any(contacts, axis=1) - z_coords = states.pos[..., 2] - # Sanity check if we are below the ground. Should not be triggered due to collision checking - below_ground = jnp.any(z_coords < -0.1, axis=1) - terminated = jnp.logical_or(below_ground, contact) - return jnp.where(dones, False, terminated) - - @staticmethod - @jax.jit - def _truncated(dones: Array, time: Array, time_horizon_in_seconds: float) -> Array: - truncated = (time >= time_horizon_in_seconds).squeeze() - return jnp.where(dones, False, truncated) - - def render(self): - self.sim.render() - - def _obs(self) -> dict[str, Array]: - fields = self.obs_keys - states = [getattr(self.sim.data.states, field) for field in fields] - return { - k: v[:, 0, :] for k, v in zip(fields, states) - } # drop n_drones dimension, as it is always 1 for now - - def close(self): - self.sim.close() - - -class CrazyflowEnvReachGoal(CrazyflowBaseEnv): - """JAX Gymnasium environment for Crazyflie simulation.""" - - def __init__(self, render_goal_marker: bool = False, **kwargs: dict): - super().__init__(**kwargs) - self.render_goal_marker = render_goal_marker - spec = {k: v for k, v in self.single_observation_space.items()} - spec["difference_to_goal"] = spaces.Box(-np.inf, np.inf, shape=(3,)) - self.single_observation_space = spaces.Dict(spec) - self.observation_space = batch_space(self.single_observation_space, self.sim.n_worlds) - self.goal = jnp.zeros((self.sim.n_worlds, 3), dtype=jnp.float32, device=self.device) - - @property - def reward(self) -> Array: - return self._reward(self.prev_done, self.terminated, self.sim.data.states, self.goal) - - @staticmethod - @jax.jit - def _reward(prev_done: Array, terminated: Array, states: SimState, goal: Array) -> Array: - norm_distance = jnp.linalg.norm(states.pos, axis=2) - reward = jnp.exp(-2.0 * norm_distance) - reward = jnp.where(terminated.reshape(-1, 1), -1.0, reward) - reward = jnp.where(prev_done.reshape(-1, 1), 0.0, reward) - return reward - - def reset_masked(self, mask: Array, reset_params: dict | None = None) -> None: - if reset_params is None: - reset_params = {} - - # Generate new goals - self.jax_key, subkey = jax.random.split(self.jax_key) - new_goals = jax.random.uniform( - key=subkey, - shape=(self.sim.n_worlds, 3), - minval=reset_params.pop("goal_pos_min", jnp.array([-1.0, -1.0, 0.5])), # x,y,z - maxval=reset_params.pop("goal_pos_max", jnp.array([1.0, 1.0, 1.5])), # x,y,z - ) - self.goal = self.goal.at[mask].set(new_goals[mask]) - - super().reset_masked(mask, reset_params) - - def step(self, action: Array) -> tuple[Array, Array, Array, Array, dict]: - if self.render_goal_marker: - for i in range(self.sim.n_worlds): - if hasattr(self.sim, "viewer") and self.sim.viewer is not None: - self.sim.viewer.viewer.add_marker( - type=mujoco.mjtGeom.mjGEOM_SPHERE, - size=np.array([0.02, 0.02, 0.02]), - pos=np.array(self.goal[i]), - rgba=np.array([1, 0, 0, 0.5]), - ) - return super().step(action) - - def _obs(self) -> dict[str, Array]: - obs = super()._obs() - obs["difference_to_goal"] = ( - self.goal - self.sim.data.states.pos[:, 0, :] - ) # drop n_drones dimension, as it is always 1 for now - return obs - - -class CrazyflowEnvTargetVelocity(CrazyflowBaseEnv): - """JAX Gymnasium environment for Crazyflie simulation.""" - - def __init__(self, **kwargs: dict): - super().__init__(**kwargs) - assert self.sim.n_drones == 1, "Currently only supported for one drone" - spec = {k: v for k, v in self.single_observation_space.items()} - spec["difference_to_target_vel"] = spaces.Box(-np.inf, np.inf, shape=(3,)) - self.single_observation_space = spaces.Dict(spec) - self.observation_space = batch_space(self.single_observation_space, self.sim.n_worlds) - self.target_vel = jnp.zeros((self.sim.n_worlds, 3), dtype=jnp.float32) - - @property - def reward(self) -> Array: - return self._reward(self.prev_done, self.terminated, self.sim.data.states, self.target_vel) - - @staticmethod - @jax.jit - def _reward(prev_done: Array, terminated: Array, states: SimState, target_vel: Array) -> Array: - norm_distance = jnp.linalg.norm(states.vel - target_vel, axis=2) - reward = jnp.exp(-norm_distance) - reward = jnp.where(terminated.reshape(-1, 1), -1.0, reward) - reward = jnp.where(prev_done.reshape(-1, 1), 0.0, reward) - return reward - - def reset_masked(self, mask: Array, reset_params: dict | None = None) -> None: - if reset_params is None: - reset_params = {} - - # Generate new target_vels - self.jax_key, subkey = jax.random.split(self.jax_key) - new_target_vel = jax.random.uniform( - key=subkey, - shape=(self.sim.n_worlds, 3), - minval=reset_params.pop("target_vel_min", jnp.array([-1.0, -1.0, -1.0])), # x,y,z - maxval=reset_params.pop("target_vel_max", jnp.array([1.0, 1.0, 1.0])), # x,y,z - ) - self.target_vel = self.target_vel.at[mask].set(new_target_vel[mask]) - - super().reset_masked(mask) - - def _obs(self) -> dict[str, Array]: - obs = super()._obs() - obs["difference_to_target_vel"] = ( - self.target_vel - self.sim.data.states.vel[:, 0, :] - ) # drop n_drones dimension, as it is always 1 for now - return obs - - -class CrazyflowEnvLanding(CrazyflowBaseEnv): - """JAX Gymnasium environment for Crazyflie simulation.""" - - def __init__(self, render_landing_marker: bool = False, **kwargs: dict): - super().__init__(**kwargs) - self.render_landing_target = render_landing_marker - spec = {k: v for k, v in self.single_observation_space.items()} - spec["difference_to_goal"] = spaces.Box(-np.inf, np.inf, shape=(3,)) - self.single_observation_space = spaces.Dict(spec) - self.observation_space = batch_space(self.single_observation_space, self.sim.n_worlds) - self.goal = jnp.zeros((self.sim.n_worlds, 3), dtype=jnp.float32, device=self.device) - self.goal = self.goal.at[..., 2].set(0.1) # 10cm above ground - - @property - def reward(self) -> Array: - return self._reward(self.prev_done, self.terminated, self.sim.data.states, self.goal) - - @staticmethod - @jax.jit - def _reward(prev_done: Array, terminated: Array, states: SimState, goal: Array) -> Array: - norm_distance = jnp.linalg.norm(states.pos - goal, axis=2) - speed = jnp.linalg.norm(states.vel, axis=2) - reward = jnp.exp(-2.0 * norm_distance) * jnp.exp(-2.0 * speed) - reward = jnp.where(terminated.reshape(-1, 1), -1.0, reward) - reward = jnp.where(prev_done.reshape(-1, 1), 0.0, reward) - return reward - - def step(self, action: Array) -> tuple[Array, Array, Array, Array, dict]: - if self.render_landing_target: - for i in range(self.sim.n_worlds): - if hasattr(self.sim, "viewer") and self.sim.viewer is not None: - self.sim.viewer.viewer.add_marker( - type=mujoco.mjtGeom.mjGEOM_SPHERE, - size=np.array([0.02, 0.02, 0.02]), - pos=np.array(self.goal[i]), - rgba=np.array([1, 0, 0, 0.5]), - ) - return super().step(action) - - def _obs(self) -> dict[str, Array]: - obs = super()._obs() - obs["difference_to_goal"] = ( - self.goal - self.sim.data.states.pos[:, 0, :] - ) # drop n_drones dimension, as it is always 1 for now - return obs - - -def render_trajectory(viewer: MujocoRenderer | None, pos: Array) -> None: - """Render trajectory.""" - if viewer is None: - return - assert pos.ndim == 2 and pos.shape[1] == 3, f"Expected shape (n_points, 3), got {pos.shape}" - for p in pos: - viewer.viewer.add_marker( - type=mujoco.mjtGeom.mjGEOM_SPHERE, - size=np.array([0.02, 0.02, 0.02]), - pos=p, - rgba=np.array([1, 0, 0, 0.8]), - ) - - -class CrazyflowEnvFigureEightTrajectory(CrazyflowBaseEnv): - """JAX Gymnasium environment for Crazyfly simulation. - - This environment is used to follow a figure-eight trajectory. The observations contain the - relative position errors to the next `n_samples` points that are distanced by `samples_dt`. The - reward is based on the distance to the next trajectory point. - """ - - def __init__( - self, - n_samples: int = 10, - samples_dt: float = 0.1, - trajectory_time: float = 10.0, - render_samples: bool = False, - **kwargs: dict, - ): - """Initializes the environment. - - Args: - n_samples: Number of next trajectory points to sample for observations. - samples_dt: Time between trajectory sample points in seconds. - trajectory_time: Total time for completing the figure-eight trajectory in seconds. - render_samples: Flag to enable/disable rendering of the trajectory sample. - **kwargs: Arguments passed to the Crazyfly simulation. - """ - super().__init__(**kwargs) - if trajectory_time < self.time_horizon_in_seconds: - raise ValueError("Trajectory time must be greater than time horizon in seconds") - - self.render_samples = render_samples - - # Create the figure eight trajectory - n_steps = int(np.ceil(trajectory_time * self.freq)) - t = np.linspace(0, 2 * np.pi, n_steps) - radius = 1 # Radius for the circles - y = np.zeros_like(t) # x is 0 everywhere - x = radius * np.sin(t) # Scale amplitude for 1-meter diameter - z = radius * np.sin(2 * t) + 1.2 # Scale amplitude for 1-meter diameter - self.trajectory = np.array([x, y, z]).T - - self.sample_offsets = np.array(np.arange(n_samples) * self.freq * samples_dt, dtype=int) - - # Define trajectory sampling parameters - self.n_samples = n_samples - self.samples_dt = samples_dt - - spec = {k: v for k, v in self.single_observation_space.items()} - spec["local_samples"] = spaces.Box(-np.inf, np.inf, shape=(3 * self.n_samples,)) - self.single_observation_space = spaces.Dict(spec) - self.observation_space = batch_space(self.single_observation_space, self.sim.n_worlds) - - @property - def reward(self) -> Array: - return self._reward( - self.prev_done, self.terminated, self.sim.data.states, self.trajectory[self.steps] - ).reshape(-1) - - @staticmethod - @jax.jit - def _reward(prev_done: Array, terminated: Array, states: SimState, goal: Array) -> Array: - norm_distance = jnp.linalg.norm( - states.pos - goal, axis=2 - ) # distance to next trajectory point - reward = jnp.exp(-2.0 * norm_distance) - reward = jnp.where(terminated.reshape(-1, 1), -1.0, reward) - reward = jnp.where(prev_done.reshape(-1, 1), 0.0, reward) - return reward - - def reset_masked(self, mask: Array, reset_params: dict | None = None) -> None: - if reset_params is None: - reset_params = {} - - # Different initial conditions than CrazyflowBaseEnv - default_drone_reset_params = { - "pos_min": reset_params.pop("pos_min", jnp.array([-0.1, -0.1, 1.1])), # x,y,z - "pos_max": reset_params.pop("pos_max", jnp.array([0.1, 0.1, 1.3])), # x,y,z - "vel_min": reset_params.pop("vel_min", -0.5), - "vel_max": reset_params.pop("vel_max", 0.5), - } - - super().reset_masked(mask, default_drone_reset_params) - - def _obs(self) -> dict[str, Array]: - obs = super()._obs() - idx = (self.steps + self.sample_offsets[None, ...]) % self.trajectory.shape[0] - next_trajectory = self.trajectory[idx, ...] - if self.render_samples: - render_trajectory(self.sim.viewer, next_trajectory[0]) - dpos = next_trajectory - self.sim.data.states.pos - obs["local_samples"] = dpos.reshape(-1, 3 * self.n_samples) - return obs - - @property - def steps(self) -> Array: - """The parameters tau for the next trajectory. Must be in [0,1].""" - return self.sim.data.core.steps // (self.sim.freq // self.freq) - 1 - - -class FigureEightXY(CrazyflowBaseEnv): - """JAX Gymnasium environment for Crazyflie simulation with a figure eight in the x-y plane. - - This environment has a single, predefined trajectory that the drone should follow. Each episode - lasts exactly 10 seconds at 50Hz. The reward is based on the distance to the current trajectory - point. - """ - - def __init__(self, num_envs: int = 1, device: str = "cpu"): - """Initialize the fixed trajectory environment.""" - super().__init__(num_envs=num_envs, freq=50, device=device) - # Create a fixed trajectory (a simple circle in the x-z plane) - n_steps = int(self.time_horizon_in_seconds * self.freq) - t = np.linspace(0, self.time_horizon_in_seconds, n_steps) - - traj_period = 5.0 - traj_freq = 2.0 * np.pi / traj_period - x = np.sin(traj_freq * t) - y = np.sin(traj_freq * t) * np.cos(traj_freq * t) - z = np.ones_like(t) - - dims = 12 # x dx y dy z dz r p yaw dr dp dyaw - self.trajectory = np.zeros((n_steps, dims)) - self.trajectory[:, 0] = x - self.trajectory[:, 2] = y - self.trajectory[:, 4] = z - - # Flag to enable/disable rendering of the trajectory - self.render_trajectory = True - - @property - def reward(self) -> Array: - """Calculate reward based on distance to current trajectory point.""" - step = self.steps % self.trajectory.shape[0] - return self._reward( - self.prev_done, self.terminated, self.sim.data.states, self.trajectory[step, [0, 2, 4]] - ).reshape(-1) - - @staticmethod - @jax.jit - def _reward(prev_done: Array, terminated: Array, states: SimState, target: Array) -> Array: - """Calculate reward based on distance to current trajectory point and velocity alignment.""" - norm_distance = jnp.linalg.norm(states.pos - target, axis=-1) - reward = jnp.exp(-2.0 * norm_distance) - # Apply penalties for termination and previous done states - reward = jnp.where(terminated.reshape(-1, 1), -1.0, reward) - reward = jnp.where(prev_done.reshape(-1, 1), 0.0, reward) - return reward - - def reset_masked(self, mask: Array) -> None: - """Reset the environment with specific initial conditions. - - The drone starts near the beginning of the trajectory with low velocity. - - Args: - mask: Boolean array indicating which environments to reset - """ - # Set initial position near the start of the trajectory - - reset_params = { - "pos_min": jnp.array([self.trajectory[0, i] - 0.1 for i in (0, 2, 4)]), - "pos_max": jnp.array([self.trajectory[0, i] + 0.1 for i in (0, 2, 4)]), - "vel_min": -0.2, - "vel_max": 0.2, - } - super().reset_masked(mask, reset_params) - - def step(self, action: Array) -> tuple[Array, Array, Array, Array, dict]: - """Step the environment and render trajectory if enabled.""" - if self.render_trajectory and hasattr(self.sim, "viewer") and self.sim.viewer is not None: - # Render the full trajectory - render_trajectory(self.sim.viewer, self.trajectory[::5, [0, 2, 4]]) - - # Highlight current target point - step = self.steps % self.trajectory.shape[0] - current_target = self.trajectory[step, [0, 2, 4]] - self.sim.viewer.viewer.add_marker( - type=mujoco.mjtGeom.mjGEOM_SPHERE, - size=np.array([0.05, 0.05, 0.05]), # Larger marker for current target - pos=np.array(current_target[0]), - rgba=np.array([0, 1, 0, 0.8]), # Green color for current target - ) - - return super().step(action) - - @property - def steps(self) -> Array: - """Get the current step index in the trajectory.""" - return self.sim.data.core.steps // (self.sim.freq // self.freq) - - -class CrazyflowRL(VectorWrapper): - """Wrapper to use the crazyflow JAX environments with common DRL frameworks. - - Currently, this wrapper clips the expected actions to [-1,1] and rescales them to the action - space expected in simulation. - """ - - def __init__(self, env: VectorEnv): - super().__init__(env) - - # Simulation action space bounds - self.action_sim_low = self.single_action_space.low - self.action_sim_high = self.single_action_space.high - - # Compute scale and mean for rescaling - self.action_scale = jnp.array((self.action_sim_high - self.action_sim_low) / 2.0) - self.action_mean = jnp.array((self.action_sim_high + self.action_sim_low) / 2.0) - - # Modify the wrapper's action space to [-1, 1] - self.single_action_space.low = -np.ones_like(self.action_sim_low) - self.single_action_space.high = np.ones_like(self.action_sim_high) - self.action_space = batch_space(self.single_action_space, self.num_envs) - - def step(self, actions: Array) -> tuple[dict, Array, Array, Array, dict]: - actions = np.clip(actions, -1.0, 1.0) - obs, reward, terminated, truncated, info = self.env.step(self.actions(actions)) - return obs, reward, terminated, truncated, info - - def actions(self, actions: Array) -> Array: - """Rescale and clip actions from [-1, 1] to [action_sim_low, action_sim_high].""" - # Rescale actions using the computed scale and mean - rescaled_actions = actions * self.action_scale + self.action_mean - # Ensure actions are within the valid range of the simulation action space - rescaled_actions = np.clip(rescaled_actions, self.action_sim_low, self.action_sim_high) - return rescaled_actions diff --git a/crazyflow/sim/physics.py b/crazyflow/sim/physics.py index b42499c..6957055 100644 --- a/crazyflow/sim/physics.py +++ b/crazyflow/sim/physics.py @@ -22,7 +22,6 @@ class Physics(str, Enum): """Physics mode for the simulation.""" - mujoco = "mujoco" analytical = "analytical" sys_id = "sys_id" default = analytical diff --git a/crazyflow/sim/sim.py b/crazyflow/sim/sim.py index c214f8a..f840f8c 100644 --- a/crazyflow/sim/sim.py +++ b/crazyflow/sim/sim.py @@ -1,8 +1,8 @@ from __future__ import annotations -from functools import partial +from functools import partial, wraps from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any, Callable, ParamSpec, TypeVar import jax import jax.numpy as jnp @@ -13,7 +13,7 @@ from jax import Array, Device from jax.scipy.spatial.transform import Rotation as R -from crazyflow.constants import J_INV, MASS, SIGN_MIX_MATRIX, J +from crazyflow.constants import J_INV, MASS, J from crazyflow.control.control import Control, attitude2rpm, pwm2rpm, state2attitude, thrust2pwm from crazyflow.exception import ConfigError, NotInitializedError from crazyflow.sim.integration import Integrator, euler, rk4, symplectic_euler @@ -22,17 +22,30 @@ collective_force2acceleration, collective_torque2ang_vel_deriv, rpms2collective_wrench, - rpms2motor_forces, - rpms2motor_torques, surrogate_identified_collective_wrench, ) from crazyflow.sim.structs import SimControls, SimCore, SimData, SimParams, SimState, SimStateDeriv -from crazyflow.utils import grid_2d, leaf_replace, patch_viewer, pytree_replace, to_device +from crazyflow.utils import grid_2d, leaf_replace, pytree_replace, to_device if TYPE_CHECKING: from mujoco.mjx import Data, Model from numpy.typing import NDArray +Params = ParamSpec("Params") # Represents arbitrary parameters +Return = TypeVar("Return") # Represents the return type + + +def requires_mujoco_sync(fn: Callable[Params, Return]) -> Callable[Params, Return]: + """Decorator to ensure that the simulation data is synchronized with the MuJoCo mjx data.""" + + @wraps(fn) + def wrapper(sim: Sim, *args: Any, **kwargs: Any) -> SimData: + if not sim.data.core.mjx_synced: + sim.data, sim.mjx_data = sync_sim2mjx(sim.data, sim.mjx_data, sim.mjx_model) + return fn(sim, *args, **kwargs) + + return wrapper + class Sim: default_path = Path(__file__).parents[1] / "models/cf2/scene.xml" @@ -71,10 +84,10 @@ def __init__( # Initialize MuJoCo world and data self._xml_path = xml_path or self.default_path self.spec = self.build_mjx_spec() - self.mj_model, self.mj_data, self.mjx_model, mjx_data = self.build_mjx_model(self.spec) + self.mj_model, self.mj_data, self.mjx_model, self.mjx_data = self.build_mjx_model(self.spec) self.viewer: MujocoRenderer | None = None - self.data = self.init_data(state_freq, attitude_freq, thrust_freq, rng_key, mjx_data) + self.data = self.init_data(state_freq, attitude_freq, thrust_freq, rng_key) self.default_data: SimData self.build_default_data() @@ -90,9 +103,6 @@ def __init__( # We never drop below -0.001 (drones can't pass through the floor). We use -0.001 to # enable checks for negative z sign self.step_pipeline += (clip_floor_pos,) - # MuJoCo needs to sync after every physics step so that the next step control, wrench - # and disturbance functions see the correct state. - self.step_pipeline += (select_sync_fn(self.physics),) self.build_reset_fn() self.build_step_fn() @@ -140,6 +150,7 @@ def thrust_control(self, cmd: Array): controls = to_device(cmd, self.device) self.data = self.data.replace(controls=self.data.controls.replace(thrust=controls)) + @requires_mujoco_sync def render( self, mode: str | None = "human", @@ -149,7 +160,6 @@ def render( height: int = 480, ) -> NDArray | None: if self.viewer is None: - patch_viewer() self.mj_model.vis.global_.offwidth = width self.mj_model.vis.global_.offheight = height self.viewer = MujocoRenderer( @@ -160,9 +170,9 @@ def render( height=height, width=width, ) - self.mj_data.qpos[:] = self.data.mjx_data.qpos[world, :] - self.mj_data.mocap_pos[:] = self.data.mjx_data.mocap_pos[world, :] - self.mj_data.mocap_quat[:] = self.data.mjx_data.mocap_quat[world, :] + self.mj_data.qpos[:] = self.mjx_data.qpos[world, :] + self.mj_data.mocap_pos[:] = self.mjx_data.mocap_pos[world, :] + self.mj_data.mocap_quat[:] = self.mjx_data.mocap_quat[world, :] mujoco.mj_forward(self.mj_model, self.mj_data) return self.viewer.render(mode) @@ -232,20 +242,8 @@ def single_step(data: SimData, _: None) -> tuple[SimData, None]: # always use the same n_steps value for successive calls. @partial(jax.jit, static_argnames="n_steps") def step(data: SimData, n_steps: int = 1) -> SimData: - # Performance optimization: When step is called, jax checks if it can reuse a previously - # compiled version of the function. This check flattens the sim.data PyTree and compares - # the metadata of each leaf with the cached metadata. The more leaves contained in - # sim.data, the more time is spent on the cache lookup even if the function has already - # been compiled. Since mjx_model contains many PyTree nodes and it is not used by - # physics modes other than mujoco with domain randomization, we set it to None and - # capture the current sim.mjx_model in the step function's closure. Changes to the - # params are synced to mjx_model at the start of the step function. - if optimize_mjx_model := (data.mjx_model is None): - data = data.replace(mjx_model=self.mjx_model) - data = self.sync_sim2mjx(data) data, _ = jax.lax.scan(single_step, data, length=n_steps, unroll=1) - if optimize_mjx_model: - data = data.replace(mjx_model=None) + data = data.replace(core=data.core.replace(mjx_synced=False)) # Flag mjx data as stale return data self._step = step @@ -259,7 +257,7 @@ def reset(data: SimData, default_data: SimData, mask: Array | None = None) -> Si data = pytree_replace(data, default_data, mask) # Does not overwrite rng_key for fn in pipeline: data = fn(data, mask) - data = self.sync_sim2mjx(data, self.mjx_model) + data = data.replace(core=data.core.replace(mjx_synced=False)) # Flag mjx data as stale return data self._reset = reset @@ -270,7 +268,6 @@ def build_data(self): self.data.controls.attitude_freq, self.data.controls.thrust_freq, self.data.core.rng_key, - self.data.mjx_data, ) def build_default_data(self): @@ -281,13 +278,10 @@ def build_mjx(self): if self.viewer is not None: self.viewer.close() self.viewer = None - self.mj_model, self.mj_data, self.mjx_model, mjx_data = self.build_mjx_model(self.spec) - self.data = self.data.replace(mjx_data=mjx_data) - self.data = self.sync_sim2mjx(self.data, self.mjx_model) - self.default_data = self.default_data.replace(mjx_data=mjx_data) + self.mj_model, self.mj_data, self.mjx_model, self.mjx_data = self.build_mjx_model(self.spec) def init_data( - self, state_freq: int, attitude_freq: int, thrust_freq: int, rng_key: Array, mjx_data: Data + self, state_freq: int, attitude_freq: int, thrust_freq: int, rng_key: Array ) -> tuple[SimData, SimData]: """Initialize the simulation data.""" drone_ids = [self.mj_model.body(f"drone:{i}").id for i in range(self.n_drones)] @@ -298,14 +292,11 @@ def init_data( controls=SimControls.create(N, D, state_freq, attitude_freq, thrust_freq, self.device), params=SimParams.create(N, D, MASS, J, J_INV, self.device), core=SimCore.create(self.freq, N, D, drone_ids, rng_key, self.device), - mjx_data=mjx_data, - mjx_model=None, ) if D > 1: # If multiple drones, arrange them in a grid grid = grid_2d(D) states = data.states.replace(pos=data.states.pos.at[..., :2].set(grid)) data = data.replace(states=states) - data = self.sync_sim2mjx(data, self.mjx_model) return data @property @@ -343,6 +334,7 @@ def controllable(self) -> Array: raise NotImplementedError(f"Control mode {self.control} not implemented") return controllable(self.data.core.steps, self.data.core.freq, control_steps, control_freq) + @requires_mujoco_sync def contacts(self, body: str | None = None) -> Array: """Get contact information from the simulation. @@ -353,45 +345,11 @@ def contacts(self, body: str | None = None) -> Array: An boolean array of shape (n_worlds,) that is True if any contact is present. """ if body is None: - return self.data.mjx_data.contact.dist < 0 + return self.mjx_data._impl.contact.dist < 0 body_id = self.mj_model.body(body).id geom_start = self.mj_model.body_geomadr[body_id] geom_count = self.mj_model.body_geomnum[body_id] - return contacts(geom_start, geom_count, self.data.mjx_data) - - @staticmethod - @jax.jit - def sync_sim2mjx(data: SimData, mjx_model: Model | None = None) -> SimData: - states = data.states - pos, quat, vel, ang_vel = states.pos, states.quat, states.vel, states.ang_vel - quat = quat[..., [3, 0, 1, 2]] # MuJoCo quat is [w, x, y, z], ours is [x, y, z, w] - qpos = rearrange(jnp.concat([pos, quat], axis=-1), "w d qpos -> w (d qpos)") - qvel = rearrange(jnp.concat([vel, ang_vel], axis=-1), "w d qvel -> w (d qvel)") - mjx_data = data.mjx_data - mjx_model = data.mjx_model if mjx_model is None else mjx_model - assert mjx_model is not None, "MuJoCo model is not initialized" - mjx_data = mjx_data.replace(qpos=qpos, qvel=qvel) - mjx_data = jax.vmap(mjx.kinematics, in_axes=(None, 0))(mjx_model, mjx_data) - mjx_data = jax.vmap(mjx.collision, in_axes=(None, 0))(mjx_model, mjx_data) - data = data.replace(mjx_data=mjx_data) - if data.mjx_model is None: # Only modify model if it is part of data - return data - # Sync model parameters such as mass and inertia for domain randomization - # This is currently not supported. See https://github.com/google-deepmind/mujoco/issues/1607 - # TODO: Implement once mjx supports batching single model fields. - return data - - @staticmethod - @jax.jit - def sync_mjx2sim(data: SimData) -> SimData: - mjx_data = data.mjx_data - qpos = mjx_data.qpos.reshape(data.core.n_worlds, data.core.n_drones, 7) - qvel = mjx_data.qvel.reshape(data.core.n_worlds, data.core.n_drones, 6) - pos, quat = jnp.split(qpos, [3], axis=-1) - vel, ang_vel = jnp.split(qvel, [3], axis=-1) - quat = quat[..., [1, 2, 3, 0]] # MuJoCo quat is [w, x, y, z], ours is [x, y, z, w] - states = data.states.replace(pos=pos, quat=quat, vel=vel, ang_vel=ang_vel) - return data.replace(states=states) + return contacts(geom_start, geom_count, self.mjx_data) @staticmethod def _reset(data: SimData, default_data: SimData, mask: Array | None = None) -> SimData: @@ -422,8 +380,6 @@ def select_wrench_fn(physics: Physics) -> Callable[[SimData], SimData]: return analytical_wrench case Physics.sys_id: return identified_wrench - case Physics.mujoco: - return mujoco_wrench case _: raise NotImplementedError(f"Physics mode {physics} not implemented") @@ -451,37 +407,14 @@ def select_integrate_fn(physics: Physics, integrator: Integrator) -> Callable[[S case _: raise NotImplementedError(f"Integrator {integrator} not implemented") - match physics: - case Physics.sys_id | Physics.analytical: - derivative_fn = select_derivative_fn(physics) - - def integrate(data: SimData) -> SimData: - data = integrate_fn(data, derivative_fn) - data = data.replace(core=data.core.replace(steps=data.core.steps + 1)) - return data - - return integrate - case Physics.mujoco: - - def integrate(data: SimData) -> SimData: - data = mjx_physics_fn(data) - data = data.replace(core=data.core.replace(steps=data.core.steps + 1)) - return data - - return integrate - case _: - raise NotImplementedError(f"Physics mode {physics} not implemented") + derivative_fn = select_derivative_fn(physics) + def integrate(data: SimData) -> SimData: + data = integrate_fn(data, derivative_fn) + data = data.replace(core=data.core.replace(steps=data.core.steps + 1)) + return data -def select_sync_fn(physics: Physics) -> Callable[[SimData], SimData]: - """Select the sync function for the given physics mode.""" - match physics: - case Physics.sys_id | Physics.analytical: - return Sim.sync_sim2mjx - case Physics.mujoco: - return Sim.sync_mjx2sim - case _: - raise NotImplementedError(f"Physics mode {physics} not implemented") + return integrate @jax.jit @@ -511,6 +444,21 @@ def contacts(geom_start: int, geom_count: int, data: Data) -> Array: return (data.contact.dist < 0) & (geom1_valid | geom2_valid) +@jax.jit +def sync_sim2mjx(data: SimData, mjx_data: Data, mjx_model: Model) -> tuple[SimData, Data]: + """Synchronize the simulation data with the MuJoCo model.""" + states = data.states + pos, quat, vel, ang_vel = states.pos, states.quat, states.vel, states.ang_vel + quat = jnp.roll(quat, 1, axis=-1) # MuJoCo quat is [w, x, y, z], ours is [x, y, z, w] + qpos = rearrange(jnp.concat([pos, quat], axis=-1), "w d qpos -> w (d qpos)") + qvel = rearrange(jnp.concat([vel, ang_vel], axis=-1), "w d qvel -> w (d qvel)") + mjx_data = mjx_data.replace(qpos=qpos, qvel=qvel) + mjx_data = jax.vmap(mjx.kinematics, in_axes=(None, 0))(mjx_model, mjx_data) + mjx_data = jax.vmap(mjx.collision, in_axes=(None, 0))(mjx_model, mjx_data) + data = data.replace(core=data.core.replace(mjx_synced=True)) + return data, mjx_data + + def step_state_controller(data: SimData) -> SimData: """Compute the updated controls for the state controller.""" states, controls = data.states, data.controls @@ -585,33 +533,6 @@ def identified_wrench(data: SimData) -> SimData: identified_derivative = analytical_derivative # We can use the same derivative function for both -def mujoco_wrench(data: SimData) -> SimData: - """Compute the wrench from the MuJoCo dynamics model.""" - forces = rpms2motor_forces(data.controls.rpms) - torques = SIGN_MIX_MATRIX[..., 2] * rpms2motor_torques(data.controls.rpms) - # Zero out external forces and torques to avoid summation over multiple steps - states = data.states - force, torque = jnp.zeros_like(states.force), jnp.zeros_like(states.torque) - states = states.replace(motor_forces=forces, motor_torques=torques, force=force, torque=torque) - return data.replace(states=states) - - -batched_mjx_step = jax.vmap(mjx.step, in_axes=(None, 0)) - - -def mjx_physics_fn(data: SimData) -> SimData: - """Step the MuJoCo simulation.""" - force_torques = jnp.concatenate([data.states.motor_forces, data.states.motor_torques], axis=-1) - force_torques = rearrange(force_torques, "w d ft -> w (d ft)") - mjx_data = data.mjx_data.replace(ctrl=force_torques) - # Add disturbances from data.states.force/torque with mjx_data.xfrc_applied - xfrc = jnp.concatenate([data.states.force, data.states.torque], axis=-1) - xfrc_applied = data.mjx_data.xfrc_applied.at[:, data.core.drone_ids, :].set(xfrc) - mjx_data = mjx_data.replace(xfrc_applied=xfrc_applied) - mjx_data = batched_mjx_step(data.mjx_model, mjx_data) - return data.replace(mjx_data=mjx_data) - - def identity(data: SimData, *args: Any, **kwargs: Any) -> SimData: """Identity function for the simulation pipeline. @@ -624,7 +545,9 @@ def clip_floor_pos(data: SimData) -> SimData: """Clip the position of the drone to the floor.""" clip = data.states.pos[..., 2] < -0.001 clip_pos = data.states.pos.at[..., 2].set(jnp.where(clip, -0.001, data.states.pos[..., 2])) - clip_vel = data.states.vel.at[..., 2].set(jnp.where(clip, 0, data.states.vel[..., 2])) + clip_vel = data.states.vel.at[..., :3].set( + jnp.where(clip[..., None], 0, data.states.vel[..., :3]) + ) return data.replace(states=data.states.replace(pos=clip_pos, vel=clip_vel)) diff --git a/crazyflow/sim/structs.py b/crazyflow/sim/structs.py index d19fb5b..dc92f14 100644 --- a/crazyflow/sim/structs.py +++ b/crazyflow/sim/structs.py @@ -1,15 +1,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import jax import jax.numpy as jnp from flax.struct import dataclass, field from jax import Array, Device -if TYPE_CHECKING: - from mujoco.mjx import Data, Model - @dataclass class SimState: @@ -186,6 +181,8 @@ class SimCore: """MuJoCo IDs of the drones in the simulation.""" rng_key: Array # (N, 1) """Random number generator key for the simulation.""" + mjx_synced: Array # (1,) + """Whether the simulation data is synchronized with the MuJoCo model.""" @staticmethod def create( @@ -208,6 +205,7 @@ def create( n_drones=n_drones, drone_ids=jnp.array(drone_ids, dtype=jnp.int32, device=device), rng_key=rng_key, + mjx_synced=jnp.array(False, dtype=jnp.bool_, device=device), ) @@ -223,10 +221,3 @@ class SimData: """Drone parameters.""" core: SimCore """Core parameters of the simulation.""" - mjx_data: Data - """MuJoCo data structure.""" - mjx_model: Model | None - """MuJoCo model structure. - - Can be set to None for performance optimizations. See `Sim.build_step` for more details. - """ diff --git a/crazyflow/sim/visualize.py b/crazyflow/sim/visualize.py new file mode 100644 index 0000000..a218c61 --- /dev/null +++ b/crazyflow/sim/visualize.py @@ -0,0 +1,78 @@ +import mujoco +import numpy as np +from numpy.typing import NDArray +from scipy.spatial.transform import Rotation as R + +from crazyflow.sim import Sim + + +def draw_line( + sim: Sim, + points: NDArray, + rgba: NDArray | None = None, + start_size: float = 3.0, + end_size: float = 3.0, +): + """Draw a line into the simulation. + + Args: + sim: The simulation. + points: An array of [N, 3] points that make up the line. + rgba: The color of the line. + start_size: The size of the start of the line. + end_size: The size of the end of the line. + + Note: + This function has to be called every time before the sim.render() step. + """ + assert points.ndim == 2, f"Expected array of [N, 3] points, got Array of shape {points.shape}" + assert points.shape[-1] == 3, f"Points must be 3D, are {points.shape[-1]}" + if sim.viewer is None: # Do not attempt to add markers if viewer is still None + return + if sim.max_visual_geom < points.shape[0]: + raise RuntimeError("Attempted to draw too many lines. Try to increase Sim.max_visual_geom") + viewer = sim.viewer.viewer + sizes = np.zeros_like(points)[:-1, :] + sizes[:, 2] = np.linalg.norm(points[1:] - points[:-1], axis=-1) + sizes[:, :2] = np.linspace(start_size, end_size, len(sizes))[..., None] + if rgba is None: + rgba = np.array([1.0, 0, 0, 1]) + mats = _rotation_matrix_from_points(points[:-1], points[1:]).as_matrix().reshape(-1, 9) + for i in range(len(points) - 1): + viewer.add_marker( + type=mujoco.mjtGeom.mjGEOM_LINE, size=sizes[i], pos=points[i], mat=mats[i], rgba=rgba + ) + + +def draw_points(sim: Sim, points: NDArray, rgba: NDArray | None = None, size: float = 0.01): + """Draw points into the simulation. + + Args: + sim: The simulation. + points: An array of [N, 3] points to draw. + rgba: The color of the points. + size: The size of the points. + """ + if sim.viewer is None: # Do not attempt to add markers if viewer is still None + return + if sim.max_visual_geom < points.shape[0]: + raise RuntimeError("Attempted to draw too many points. Try to increase Sim.max_visual_geom") + viewer = sim.viewer.viewer + if rgba is None: + rgba = np.array([1.0, 0, 0, 1]) + for i in range(len(points)): + viewer.add_marker( + type=mujoco.mjtGeom.mjGEOM_SPHERE, + size=np.array([size, size, size]), + pos=points[i], + rgba=rgba, + ) + + +def _rotation_matrix_from_points(p1: NDArray, p2: NDArray) -> R: + """Generate rotation matrices that align their z-axis to p2-p1.""" + z_axis = (v := p2 - p1) / np.linalg.norm(v, axis=-1, keepdims=True) + random_vector = np.random.rand(*z_axis.shape) + x_axis = (v := np.cross(random_vector, z_axis)) / np.linalg.norm(v, axis=-1, keepdims=True) + y_axis = np.cross(z_axis, x_axis) + return R.from_matrix(np.stack((x_axis, y_axis, z_axis), axis=-1)) diff --git a/crazyflow/utils.py b/crazyflow/utils.py index e509a5a..88c56bd 100644 --- a/crazyflow/utils.py +++ b/crazyflow/utils.py @@ -6,9 +6,6 @@ import jax import jax.numpy as jnp -import mujoco -import numpy as np -from gymnasium.envs.mujoco.mujoco_rendering import BaseRender from jax import Array @@ -71,40 +68,6 @@ def to_device(data: Array, device: str) -> Array: return jnp.array(data, device=device) -def patch_viewer(): - """Patch the viewer._add_marker_to_scene method to work with mujoco>=3.0. - - The current gymnasium implementation is broken for mujoco>=3.0. The method is our only hook into - the rendering pipeline between `mjv_updateScene`, which deletes all previous markers, and - `mjr_render`, so if we want to add markers to the scene, we need to do it here. The patch - ensures that markers are added correctly to the scene. - - If you want to add more markers than the default limit, you can increase the `max_visual_geom` - parameter in the `Sim` class. - """ - - def _add_marker_to_scene(self: BaseRender, marker: dict): - """Add a marker to the rendering scene. - - Args: - self: The renderer instance. - marker: The marker to add to the scene. - """ - if self.scn.ngeom >= self.scn.maxgeom: - raise RuntimeError(f"Ran out of geoms. maxgeom: {self.scn.maxgeom}") - geom_type = marker.get("type", mujoco.mjtGeom.mjGEOM_SPHERE) - size = marker.get("size", np.array([0.01, 0.01, 0.01])) - pos = marker.get("pos", np.array([0, 0, 0])) - mat = marker.get("mat", np.eye(3).flatten()) - rgba = marker.get("rgba", np.array([1, 1, 1, 1])) - mujoco.mjv_initGeom( - self.scn.geoms[self.scn.ngeom], geom_type, size=size, pos=pos, mat=mat, rgba=rgba - ) - self.scn.ngeom += 1 - - BaseRender._add_marker_to_scene = _add_marker_to_scene - - def enable_cache( cache_path: Path = Path("/tmp/jax_cache"), min_entry_size_bytes: int = -1, diff --git a/docs/img/performance.png b/docs/img/performance.png index 91f11fc..17f7540 100644 Binary files a/docs/img/performance.png and b/docs/img/performance.png differ diff --git a/examples/gymnasium_env_trajectory.py b/examples/figure8.py similarity index 63% rename from examples/gymnasium_env_trajectory.py rename to examples/figure8.py index 50de5a7..59c8c4c 100644 --- a/examples/gymnasium_env_trajectory.py +++ b/examples/figure8.py @@ -1,14 +1,14 @@ import gymnasium +import jax.numpy as jnp import numpy as np from gymnasium.wrappers.vector import JaxToNumpy # , JaxToTorch -from crazyflow.gymnasium_envs import CrazyflowRL # noqa: F401 +from crazyflow.envs import NormalizeActions # noqa: F401 from crazyflow.utils import enable_cache def main(): enable_cache() - SEED = 42 # Create environment that contains a figure eight trajectory. You can parametrize the # observation space, i.e., which part of the trajectory is contained in the observation. Please # refer to the documentation of the environment for more information. @@ -19,23 +19,23 @@ def main(): n_samples=10, samples_dt=0.1, trajectory_time=10.0, - render_samples=True, ) - # RL wrapper to clip the actions to [-1, 1] and rescale them for use with common DRL libraries. - # envs = CrazyflowRL(envs) - - # This wrapper makes it possible to interact with the environment using numpy arrays, if - # desired. JaxToTorch is available as well. + # NormalizeActions wrapper to clip the actions to [-1, 1] and rescale them for use with common + # DRL libraries. + envs = NormalizeActions(envs) envs = JaxToNumpy(envs) # dummy action for going up (in attitude control) action = np.zeros((20, 4), dtype=np.float32) - action[..., 0] = 0.31 + action[..., 0] = -0.2 - obs, info = envs.reset(seed=SEED) + obs, info = envs.reset() # Step through the environment - for _ in range(500): + for _ in range(1_000): + # Prevent alignment warnings. Related issue: https://github.com/jax-ml/jax/issues/29810 + # TODO: Remove once https://github.com/jax-ml/jax/pull/29963 is merged. + action = np.asarray(jnp.asarray(action)) observation, reward, terminated, truncated, info = envs.step(action) envs.render() diff --git a/examples/gymnasium_env.py b/examples/gymnasium_env.py index 3139a6b..dc3d50b 100644 --- a/examples/gymnasium_env.py +++ b/examples/gymnasium_env.py @@ -1,4 +1,5 @@ import gymnasium +import jax.numpy as jnp import numpy as np from gymnasium.wrappers.vector import JaxToNumpy # , JaxToTorch @@ -8,10 +9,10 @@ def main(): enable_cache() - SEED = 42 - envs = gymnasium.make_vec("DroneReachPos-v0", num_envs=20, freq=50, time_horizon_in_seconds=2) + envs = gymnasium.make_vec("DroneReachPos-v0", num_envs=20, freq=50, max_episode_time=2) - # This wrapper makes it possible to interact with the environment using numpy arrays, if desired. JaxToTorch is available as well. + # This wrapper converts numpy actions to jax arrays and jax observations to numpy arrays. To + # convert to torch, use JaxToTorch. envs = JaxToNumpy(envs) # Dummy action for going up (in attitude control) @@ -20,7 +21,6 @@ def main(): # Environments provide reset parameters that can be used to set the initial state of the environment. obs, info = envs.reset( - seed=SEED, options={ "pos_min": np.array([-1.0, 1.0, 1.0]), "pos_max": np.array([-1.0, 1.0, 1.0]), @@ -28,14 +28,16 @@ def main(): "vel_max": 0.0, "goal_pos_min": np.array([-1.0, 1.0, 1.0]), "goal_pos_max": np.array([-1.0, 1.0, 1.0]), - }, + } ) # Step through the environment - for _ in range(100): + for _ in range(1_000): + # Prevent alignment warnings. Related issue: https://github.com/jax-ml/jax/issues/29810 + # TODO: Remove once https://github.com/jax-ml/jax/pull/29963 is merged. + action = np.asarray(jnp.asarray(action)) observation, reward, terminated, truncated, info = envs.step(action) envs.render() - envs.close() diff --git a/pixi.lock b/pixi.lock new file mode 100644 index 0000000..8e36c85 --- /dev/null +++ b/pixi.lock @@ -0,0 +1,2385 @@ +version: 6 +environments: + benchmark: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.44-h4bf12b8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.44-h4852527_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/conda-gcc-specs-15.1.0-hc55bae6_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.0.0-hf484d3e_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc-15.1.0-hd165ed4_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-15.1.0-h4393ad2_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-15.1.0-h1ac4077_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gfortran-15.1.0-h33e79ad_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-15.1.0-h3b9cdf2_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-15.1.0-h6a1bac1_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-15.1.0-h1a088d8_11.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_8.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.1.0-h4c094af_104.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-15.1.0-h97b714f_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-15.1.0-h4c094af_104.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openblas-0.3.30-pthreads_h6ec200e_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.5-hec9711d_102_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.12.9-hbf64f1c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - pypi: https://files.pythonhosted.org/packages/8f/aa/ba0014cc4659328dc818a28827be78e6d97312ab0cb98105a770924dc11e/absl_py-2.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/9e/16bc0b59ca73b47b82380c0ac6ef5084fb0f6e487d24eb102926456708eb/casadi-3.7.1-cp313-none-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/6f/3d/46bb04776c465cea2dd8aa2d4b61ab610b707f798f47838ef7e6105b025c/chex-0.1.90-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/e8/64c37fadfc2816a7701fa8a6ed8d87327c7d54eacfbfb6edab14a2f2be75/cloudpickle-3.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/62/9773de14fe6c45c23649e98b83231fffd7b9892b6cf863251dc2afa73643/einops-0.8.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/2c/ffc08c54c05cdce6fbed2aeebc46348dbe180c6d2c541c7af7ba0aa5f5f8/Farama_Notifications-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6b/b6/82c7e601d6d3c3278c40b7bd35e17e82aa227f050aa9f66cb7b7fce29471/fire-0.7.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/8a/0b/6b46d04a251f8a8b430bfd79b02a071bcd37b5dfa8321a7ad5db47dcd103/flax-0.11.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e9/a2/5a9fc21c354bf8613215ce233ab0d933bd17d5ff4c29693636551adbc7b3/fonttools-4.59.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2f/e0/014d5d9d7a4564cf1c40b5039bc882db69fd881111e03ab3657ac0b218e2/fsspec-2025.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/ba/de3630757c7d7fc2086aaf3994926d6b869d31586e4d0c14f1666af31b93/glfw-2.9.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 + - pypi: https://files.pythonhosted.org/packages/a0/e2/a111dbb8625af467ea4760a1373d6ef27aac3137931219902406ccc05423/gymnasium-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/1e/62a2ec3104394a2975a2629eec89276ede9dbe717092f6966fcf963e1bf0/humanize-4.12.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/bd/b394387b598ed84d8d0fa90611a90bee0adc2021820ad5729f7ced74a8e2/imageio-2.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ad/de/3092df5073cd9c07c01b10612fc541538b74b02184fac90e3beada20f758/jax-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/49/ef/1fe3c10c27199fbaf593ee7c53226e4eae057d505207707cd815a8ee0764/jax_cuda12_pjrt-0.7.0-py3-none-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0d/c2/0f5698d4ececeff3cf0e9802b1f58f137adea83562cfa735591436978a49/jax_cuda12_plugin-0.7.0-cp313-cp313-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c7/83/7ba260095e98a5004af4fdb4315010c445441473cac41afdb973bf212deb/jaxlib-0.7.0-cp313-cp313-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/1b/233e3094b749df16e3e6cd5a44849fd33852e692ad009cf7de00cf58ddf6/matplotlib-3.10.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/8a/18d4ff2c7bd83f30d6924bd4ad97abf418488c3f908dea228d6f0961ad68/ml_collections-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/f3/091ba84e5395d7fe5b30c081a44dec881cd84b408db1763ee50768b2ab63/ml_dtypes-0.5.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/20/22/2ebae7ae43cd8f2debc35c631172ddf14e2a87ffcc04cf43ff9df9fff0d3/msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b5/8c/54e5dd1df6fced73ea0b183ccf05939757bfc6fab9d720e1e051963ae154/mujoco-3.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/59/b1/965d28e5ece3e59772b79dc43144b70f73b188f8f49b4997c91a646d85a4/mujoco_mjx-3.3.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/0f/571b2c7a3833ae419fe69ff7b479a78d313581785203cc70a8db90121b9a/numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/77/3c/aa88abe01f3be3d1f8f787d1d33dc83e76fec05945f9a28fbb41cfb99cd5/nvidia_cublas_cu12-12.9.1.4-py3-none-manylinux_2_27_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c1/2e/b84e32197e33f39907b455b83395a017e697c07a449a2b15fd07fc1c9981/nvidia_cuda_cupti_cu12-12.9.79-py3-none-manylinux_2_25_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/25/48/b54a06168a2190572a312bfe4ce443687773eb61367ced31e064953dd2f7/nvidia_cuda_nvcc_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b8/85/e4af82cc9202023862090bfca4ea827d533329e925c758f0cde964cb54b7/nvidia_cuda_nvrtc_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/bc/46/a92db19b8309581092a3add7e6fceb4c301a3fd233969856a8cbf042cd3c/nvidia_cuda_runtime_cu12-12.9.79-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/de/14/9288024887ba320eb4e51d01cf37aab11d38f774016bcc0dedac0948d0bc/nvidia_cudnn_cu12-9.12.0.46-py3-none-manylinux_2_27_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/95/f4/61e6996dd20481ee834f57a8e9dca28b1869366a135e0d42e2aa8493bdd4/nvidia_cufft_cu12-11.4.1.4-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/33/40/79b0c64d44d6c166c0964ec1d803d067f4a145cca23e23925fd351d0e642/nvidia_cusolver_cu12-11.7.5.82-py3-none-manylinux_2_27_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/12/46/b0fd4b04f86577921feb97d8e2cf028afe04f614d17fb5013de9282c9216/nvidia_cusparse_cu12-12.5.10.65-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c4/cb/2cf5b8e6a669c90ac6410c3a9d86881308492765b6744de5d0ce75089999/nvidia_nccl_cu12-2.27.7-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/46/0c/c75bbfb967457a0b7670b8ad267bfc4fffdf341c074e0a80db06c24ccfd4/nvidia_nvjitlink_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3b/6c/99acb2f9eb85c29fc6f3a7ac4dccfd992e22666dd08a642b303311326a97/nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/33/f86091c706db1a5459f501830241afff2ecab3532725c188ea57be6e54de/optax-0.2.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/21/9632e0c6f826e2a0e19e1bcb9420fac9970696efc77ca1b37a496666f0ad/orbax_checkpoint-0.11.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e9/e2/20a317688435470872885e7fc8f95109ae9683dec7c50be29b56911515a5/pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/92/44/8634af40b0db528b5b37e901c0dc67321354880d251bf8965901d57693a5/PyOpenGL-3.1.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/82/08e4076df538fb56caa1d489588d880ec7c52d8273a606bb54d660528f7c/scipy-1.16.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/e3/874b1cca3d3897b486d3afdccc475eb3a09815bf1015b01cf7fcb52a55f0/simplejson-3.20.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/9a/9dcc01c8f87047b09602ea16379233b8a308d1d83d5432bf8bc89163ca3e/tensorstore-0.1.76-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/4f/bd/de8d508070629b6d84a30d01d57e4a65c69aa7f5abe7560b8fad3b50ea59/termcolor-3.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/2b/36e984399089c026a6499ac8f7401d38487cf0183839a4aa78140d373771/treescope-0.1.10-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/79/9563bd90b2b964e85424b6e41e58c39d812d31f62198140b87c823e19ed9/trimesh-4.7.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl + - pypi: ./ + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.44-h4bf12b8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.44-h4852527_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/conda-gcc-specs-15.1.0-hc55bae6_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.0.0-hf484d3e_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc-15.1.0-hd165ed4_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-15.1.0-h4393ad2_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-15.1.0-h1ac4077_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gfortran-15.1.0-h33e79ad_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-15.1.0-h3b9cdf2_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-15.1.0-h6a1bac1_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-15.1.0-h1a088d8_11.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_8.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.1.0-h4c094af_104.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-15.1.0-h97b714f_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-15.1.0-h4c094af_104.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openblas-0.3.30-pthreads_h6ec200e_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.5-hec9711d_102_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.12.9-hbf64f1c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - pypi: https://files.pythonhosted.org/packages/8f/aa/ba0014cc4659328dc818a28827be78e6d97312ab0cb98105a770924dc11e/absl_py-2.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/9e/16bc0b59ca73b47b82380c0ac6ef5084fb0f6e487d24eb102926456708eb/casadi-3.7.1-cp313-none-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/6f/3d/46bb04776c465cea2dd8aa2d4b61ab610b707f798f47838ef7e6105b025c/chex-0.1.90-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/e8/64c37fadfc2816a7701fa8a6ed8d87327c7d54eacfbfb6edab14a2f2be75/cloudpickle-3.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/62/9773de14fe6c45c23649e98b83231fffd7b9892b6cf863251dc2afa73643/einops-0.8.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/2c/ffc08c54c05cdce6fbed2aeebc46348dbe180c6d2c541c7af7ba0aa5f5f8/Farama_Notifications-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/0b/6b46d04a251f8a8b430bfd79b02a071bcd37b5dfa8321a7ad5db47dcd103/flax-0.11.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2f/e0/014d5d9d7a4564cf1c40b5039bc882db69fd881111e03ab3657ac0b218e2/fsspec-2025.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/ba/de3630757c7d7fc2086aaf3994926d6b869d31586e4d0c14f1666af31b93/glfw-2.9.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 + - pypi: https://files.pythonhosted.org/packages/a0/e2/a111dbb8625af467ea4760a1373d6ef27aac3137931219902406ccc05423/gymnasium-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/1e/62a2ec3104394a2975a2629eec89276ede9dbe717092f6966fcf963e1bf0/humanize-4.12.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/bd/b394387b598ed84d8d0fa90611a90bee0adc2021820ad5729f7ced74a8e2/imageio-2.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ad/de/3092df5073cd9c07c01b10612fc541538b74b02184fac90e3beada20f758/jax-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/83/7ba260095e98a5004af4fdb4315010c445441473cac41afdb973bf212deb/jaxlib-0.7.0-cp313-cp313-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/8a/18d4ff2c7bd83f30d6924bd4ad97abf418488c3f908dea228d6f0961ad68/ml_collections-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/f3/091ba84e5395d7fe5b30c081a44dec881cd84b408db1763ee50768b2ab63/ml_dtypes-0.5.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/20/22/2ebae7ae43cd8f2debc35c631172ddf14e2a87ffcc04cf43ff9df9fff0d3/msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b5/8c/54e5dd1df6fced73ea0b183ccf05939757bfc6fab9d720e1e051963ae154/mujoco-3.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/59/b1/965d28e5ece3e59772b79dc43144b70f73b188f8f49b4997c91a646d85a4/mujoco_mjx-3.3.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/0f/571b2c7a3833ae419fe69ff7b479a78d313581785203cc70a8db90121b9a/numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/33/f86091c706db1a5459f501830241afff2ecab3532725c188ea57be6e54de/optax-0.2.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/21/9632e0c6f826e2a0e19e1bcb9420fac9970696efc77ca1b37a496666f0ad/orbax_checkpoint-0.11.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/92/44/8634af40b0db528b5b37e901c0dc67321354880d251bf8965901d57693a5/PyOpenGL-3.1.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/82/08e4076df538fb56caa1d489588d880ec7c52d8273a606bb54d660528f7c/scipy-1.16.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/e3/874b1cca3d3897b486d3afdccc475eb3a09815bf1015b01cf7fcb52a55f0/simplejson-3.20.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1d/9a/9dcc01c8f87047b09602ea16379233b8a308d1d83d5432bf8bc89163ca3e/tensorstore-0.1.76-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/2b/36e984399089c026a6499ac8f7401d38487cf0183839a4aa78140d373771/treescope-0.1.10-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/79/9563bd90b2b964e85424b6e41e58c39d812d31f62198140b87c823e19ed9/trimesh-4.7.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl + - pypi: ./ + gpu: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.44-h4bf12b8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.44-h4852527_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/conda-gcc-specs-15.1.0-hc55bae6_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.0.0-hf484d3e_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc-15.1.0-hd165ed4_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-15.1.0-h4393ad2_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-15.1.0-h1ac4077_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gfortran-15.1.0-h33e79ad_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-15.1.0-h3b9cdf2_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-15.1.0-h6a1bac1_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-15.1.0-h1a088d8_11.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_8.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.1.0-h4c094af_104.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-15.1.0-h97b714f_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-15.1.0-h4c094af_104.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openblas-0.3.30-pthreads_h6ec200e_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.5-hec9711d_102_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.12.9-hbf64f1c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - pypi: https://files.pythonhosted.org/packages/8f/aa/ba0014cc4659328dc818a28827be78e6d97312ab0cb98105a770924dc11e/absl_py-2.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/9e/16bc0b59ca73b47b82380c0ac6ef5084fb0f6e487d24eb102926456708eb/casadi-3.7.1-cp313-none-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/6f/3d/46bb04776c465cea2dd8aa2d4b61ab610b707f798f47838ef7e6105b025c/chex-0.1.90-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/e8/64c37fadfc2816a7701fa8a6ed8d87327c7d54eacfbfb6edab14a2f2be75/cloudpickle-3.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/62/9773de14fe6c45c23649e98b83231fffd7b9892b6cf863251dc2afa73643/einops-0.8.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/2c/ffc08c54c05cdce6fbed2aeebc46348dbe180c6d2c541c7af7ba0aa5f5f8/Farama_Notifications-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/0b/6b46d04a251f8a8b430bfd79b02a071bcd37b5dfa8321a7ad5db47dcd103/flax-0.11.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2f/e0/014d5d9d7a4564cf1c40b5039bc882db69fd881111e03ab3657ac0b218e2/fsspec-2025.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/ba/de3630757c7d7fc2086aaf3994926d6b869d31586e4d0c14f1666af31b93/glfw-2.9.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 + - pypi: https://files.pythonhosted.org/packages/a0/e2/a111dbb8625af467ea4760a1373d6ef27aac3137931219902406ccc05423/gymnasium-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/1e/62a2ec3104394a2975a2629eec89276ede9dbe717092f6966fcf963e1bf0/humanize-4.12.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/bd/b394387b598ed84d8d0fa90611a90bee0adc2021820ad5729f7ced74a8e2/imageio-2.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ad/de/3092df5073cd9c07c01b10612fc541538b74b02184fac90e3beada20f758/jax-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/49/ef/1fe3c10c27199fbaf593ee7c53226e4eae057d505207707cd815a8ee0764/jax_cuda12_pjrt-0.7.0-py3-none-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0d/c2/0f5698d4ececeff3cf0e9802b1f58f137adea83562cfa735591436978a49/jax_cuda12_plugin-0.7.0-cp313-cp313-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c7/83/7ba260095e98a5004af4fdb4315010c445441473cac41afdb973bf212deb/jaxlib-0.7.0-cp313-cp313-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/8a/18d4ff2c7bd83f30d6924bd4ad97abf418488c3f908dea228d6f0961ad68/ml_collections-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/f3/091ba84e5395d7fe5b30c081a44dec881cd84b408db1763ee50768b2ab63/ml_dtypes-0.5.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/20/22/2ebae7ae43cd8f2debc35c631172ddf14e2a87ffcc04cf43ff9df9fff0d3/msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b5/8c/54e5dd1df6fced73ea0b183ccf05939757bfc6fab9d720e1e051963ae154/mujoco-3.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/59/b1/965d28e5ece3e59772b79dc43144b70f73b188f8f49b4997c91a646d85a4/mujoco_mjx-3.3.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/0f/571b2c7a3833ae419fe69ff7b479a78d313581785203cc70a8db90121b9a/numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/77/3c/aa88abe01f3be3d1f8f787d1d33dc83e76fec05945f9a28fbb41cfb99cd5/nvidia_cublas_cu12-12.9.1.4-py3-none-manylinux_2_27_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c1/2e/b84e32197e33f39907b455b83395a017e697c07a449a2b15fd07fc1c9981/nvidia_cuda_cupti_cu12-12.9.79-py3-none-manylinux_2_25_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/25/48/b54a06168a2190572a312bfe4ce443687773eb61367ced31e064953dd2f7/nvidia_cuda_nvcc_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b8/85/e4af82cc9202023862090bfca4ea827d533329e925c758f0cde964cb54b7/nvidia_cuda_nvrtc_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/bc/46/a92db19b8309581092a3add7e6fceb4c301a3fd233969856a8cbf042cd3c/nvidia_cuda_runtime_cu12-12.9.79-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/de/14/9288024887ba320eb4e51d01cf37aab11d38f774016bcc0dedac0948d0bc/nvidia_cudnn_cu12-9.12.0.46-py3-none-manylinux_2_27_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/95/f4/61e6996dd20481ee834f57a8e9dca28b1869366a135e0d42e2aa8493bdd4/nvidia_cufft_cu12-11.4.1.4-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/33/40/79b0c64d44d6c166c0964ec1d803d067f4a145cca23e23925fd351d0e642/nvidia_cusolver_cu12-11.7.5.82-py3-none-manylinux_2_27_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/12/46/b0fd4b04f86577921feb97d8e2cf028afe04f614d17fb5013de9282c9216/nvidia_cusparse_cu12-12.5.10.65-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c4/cb/2cf5b8e6a669c90ac6410c3a9d86881308492765b6744de5d0ce75089999/nvidia_nccl_cu12-2.27.7-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/46/0c/c75bbfb967457a0b7670b8ad267bfc4fffdf341c074e0a80db06c24ccfd4/nvidia_nvjitlink_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3b/6c/99acb2f9eb85c29fc6f3a7ac4dccfd992e22666dd08a642b303311326a97/nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/33/f86091c706db1a5459f501830241afff2ecab3532725c188ea57be6e54de/optax-0.2.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/21/9632e0c6f826e2a0e19e1bcb9420fac9970696efc77ca1b37a496666f0ad/orbax_checkpoint-0.11.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/92/44/8634af40b0db528b5b37e901c0dc67321354880d251bf8965901d57693a5/PyOpenGL-3.1.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/82/08e4076df538fb56caa1d489588d880ec7c52d8273a606bb54d660528f7c/scipy-1.16.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/e3/874b1cca3d3897b486d3afdccc475eb3a09815bf1015b01cf7fcb52a55f0/simplejson-3.20.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1d/9a/9dcc01c8f87047b09602ea16379233b8a308d1d83d5432bf8bc89163ca3e/tensorstore-0.1.76-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/2b/36e984399089c026a6499ac8f7401d38487cf0183839a4aa78140d373771/treescope-0.1.10-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/79/9563bd90b2b964e85424b6e41e58c39d812d31f62198140b87c823e19ed9/trimesh-4.7.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl + - pypi: ./ + test: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.44-h4bf12b8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.44-h4852527_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/conda-gcc-specs-15.1.0-hc55bae6_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.0.0-hf484d3e_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc-15.1.0-hd165ed4_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-15.1.0-h4393ad2_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-15.1.0-h1ac4077_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gfortran-15.1.0-h33e79ad_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-15.1.0-h3b9cdf2_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-15.1.0-h6a1bac1_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-15.1.0-h1a088d8_11.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_8.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.1.0-h4c094af_104.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-15.1.0-h97b714f_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-15.1.0-h4c094af_104.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openblas-0.3.30-pthreads_h6ec200e_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.5-hec9711d_102_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.12.9-hbf64f1c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - pypi: https://files.pythonhosted.org/packages/8f/aa/ba0014cc4659328dc818a28827be78e6d97312ab0cb98105a770924dc11e/absl_py-2.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/b1/0542e0cab6f49f151a2d7a42400f84f706fc0b64e85dc1f56708b2e9fd37/array_api_compat-1.12.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/9e/16bc0b59ca73b47b82380c0ac6ef5084fb0f6e487d24eb102926456708eb/casadi-3.7.1-cp313-none-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/6f/3d/46bb04776c465cea2dd8aa2d4b61ab610b707f798f47838ef7e6105b025c/chex-0.1.90-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/e8/64c37fadfc2816a7701fa8a6ed8d87327c7d54eacfbfb6edab14a2f2be75/cloudpickle-3.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ea/2f/6ae1db51dc34db499bfe340e89f79a63bd115fc32513a7bacdf17d33cd86/coverage-7.10.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/87/62/9773de14fe6c45c23649e98b83231fffd7b9892b6cf863251dc2afa73643/einops-0.8.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/2c/ffc08c54c05cdce6fbed2aeebc46348dbe180c6d2c541c7af7ba0aa5f5f8/Farama_Notifications-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/0b/6b46d04a251f8a8b430bfd79b02a071bcd37b5dfa8321a7ad5db47dcd103/flax-0.11.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2f/e0/014d5d9d7a4564cf1c40b5039bc882db69fd881111e03ab3657ac0b218e2/fsspec-2025.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/ba/de3630757c7d7fc2086aaf3994926d6b869d31586e4d0c14f1666af31b93/glfw-2.9.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 + - pypi: https://files.pythonhosted.org/packages/a0/e2/a111dbb8625af467ea4760a1373d6ef27aac3137931219902406ccc05423/gymnasium-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/1e/62a2ec3104394a2975a2629eec89276ede9dbe717092f6966fcf963e1bf0/humanize-4.12.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/bd/b394387b598ed84d8d0fa90611a90bee0adc2021820ad5729f7ced74a8e2/imageio-2.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ad/de/3092df5073cd9c07c01b10612fc541538b74b02184fac90e3beada20f758/jax-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/49/ef/1fe3c10c27199fbaf593ee7c53226e4eae057d505207707cd815a8ee0764/jax_cuda12_pjrt-0.7.0-py3-none-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0d/c2/0f5698d4ececeff3cf0e9802b1f58f137adea83562cfa735591436978a49/jax_cuda12_plugin-0.7.0-cp313-cp313-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c7/83/7ba260095e98a5004af4fdb4315010c445441473cac41afdb973bf212deb/jaxlib-0.7.0-cp313-cp313-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/8a/18d4ff2c7bd83f30d6924bd4ad97abf418488c3f908dea228d6f0961ad68/ml_collections-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/f3/091ba84e5395d7fe5b30c081a44dec881cd84b408db1763ee50768b2ab63/ml_dtypes-0.5.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/20/22/2ebae7ae43cd8f2debc35c631172ddf14e2a87ffcc04cf43ff9df9fff0d3/msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b5/8c/54e5dd1df6fced73ea0b183ccf05939757bfc6fab9d720e1e051963ae154/mujoco-3.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/59/b1/965d28e5ece3e59772b79dc43144b70f73b188f8f49b4997c91a646d85a4/mujoco_mjx-3.3.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/0f/571b2c7a3833ae419fe69ff7b479a78d313581785203cc70a8db90121b9a/numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/77/3c/aa88abe01f3be3d1f8f787d1d33dc83e76fec05945f9a28fbb41cfb99cd5/nvidia_cublas_cu12-12.9.1.4-py3-none-manylinux_2_27_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c1/2e/b84e32197e33f39907b455b83395a017e697c07a449a2b15fd07fc1c9981/nvidia_cuda_cupti_cu12-12.9.79-py3-none-manylinux_2_25_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/25/48/b54a06168a2190572a312bfe4ce443687773eb61367ced31e064953dd2f7/nvidia_cuda_nvcc_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b8/85/e4af82cc9202023862090bfca4ea827d533329e925c758f0cde964cb54b7/nvidia_cuda_nvrtc_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/bc/46/a92db19b8309581092a3add7e6fceb4c301a3fd233969856a8cbf042cd3c/nvidia_cuda_runtime_cu12-12.9.79-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/de/14/9288024887ba320eb4e51d01cf37aab11d38f774016bcc0dedac0948d0bc/nvidia_cudnn_cu12-9.12.0.46-py3-none-manylinux_2_27_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/95/f4/61e6996dd20481ee834f57a8e9dca28b1869366a135e0d42e2aa8493bdd4/nvidia_cufft_cu12-11.4.1.4-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/33/40/79b0c64d44d6c166c0964ec1d803d067f4a145cca23e23925fd351d0e642/nvidia_cusolver_cu12-11.7.5.82-py3-none-manylinux_2_27_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/12/46/b0fd4b04f86577921feb97d8e2cf028afe04f614d17fb5013de9282c9216/nvidia_cusparse_cu12-12.5.10.65-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c4/cb/2cf5b8e6a669c90ac6410c3a9d86881308492765b6744de5d0ce75089999/nvidia_nccl_cu12-2.27.7-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/46/0c/c75bbfb967457a0b7670b8ad267bfc4fffdf341c074e0a80db06c24ccfd4/nvidia_nvjitlink_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3b/6c/99acb2f9eb85c29fc6f3a7ac4dccfd992e22666dd08a642b303311326a97/nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/33/f86091c706db1a5459f501830241afff2ecab3532725c188ea57be6e54de/optax-0.2.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/21/9632e0c6f826e2a0e19e1bcb9420fac9970696efc77ca1b37a496666f0ad/orbax_checkpoint-0.11.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/92/44/8634af40b0db528b5b37e901c0dc67321354880d251bf8965901d57693a5/PyOpenGL-3.1.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/82/08e4076df538fb56caa1d489588d880ec7c52d8273a606bb54d660528f7c/scipy-1.16.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/e3/874b1cca3d3897b486d3afdccc475eb3a09815bf1015b01cf7fcb52a55f0/simplejson-3.20.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1d/9a/9dcc01c8f87047b09602ea16379233b8a308d1d83d5432bf8bc89163ca3e/tensorstore-0.1.76-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/2b/36e984399089c026a6499ac8f7401d38487cf0183839a4aa78140d373771/treescope-0.1.10-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/79/9563bd90b2b964e85424b6e41e58c39d812d31f62198140b87c823e19ed9/trimesh-4.7.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl + - pypi: ./ +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 + md5: d7c89558ba9fa0495403155b64376d81 + license: None + purls: [] + size: 2562 + timestamp: 1578324546067 +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + build_number: 16 + sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 + md5: 73aaf86a425cc6e73fcf236a5a46396d + depends: + - _libgcc_mutex 0.1 conda_forge + - libgomp >=7.5.0 + constrains: + - openmp_impl 9999 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 23621 + timestamp: 1650670423406 +- pypi: https://files.pythonhosted.org/packages/8f/aa/ba0014cc4659328dc818a28827be78e6d97312ab0cb98105a770924dc11e/absl_py-2.3.1-py3-none-any.whl + name: absl-py + version: 2.3.1 + sha256: eeecf07f0c2a93ace0772c92e596ace6d3d3996c042b2128459aaae2a76de11d + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl + name: aiofiles + version: 24.1.0 + sha256: b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/e0/b1/0542e0cab6f49f151a2d7a42400f84f706fc0b64e85dc1f56708b2e9fd37/array_api_compat-1.12.0-py3-none-any.whl + name: array-api-compat + version: 1.12.0 + sha256: a0b4795b6944a9507fde54679f9350e2ad2b1e2acf4a2408a098cdc27f890a8b + requires_dist: + - cupy ; extra == 'cupy' + - dask>=2024.9.0 ; extra == 'dask' + - jax ; extra == 'jax' + - numpy>=1.22 ; extra == 'numpy' + - torch ; extra == 'pytorch' + - sparse>=0.15.1 ; extra == 'sparse' + - ndonnx ; extra == 'ndonnx' + - furo ; extra == 'docs' + - linkify-it-py ; extra == 'docs' + - myst-parser ; extra == 'docs' + - sphinx ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - array-api-strict ; extra == 'dev' + - dask[array]>=2024.9.0 ; extra == 'dev' + - jax[cpu] ; extra == 'dev' + - numpy>=1.22 ; extra == 'dev' + - pytest ; extra == 'dev' + - torch ; extra == 'dev' + - sparse>=0.15.1 ; extra == 'dev' + - ndonnx ; extra == 'dev' + requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.44-h4bf12b8_1.conda + sha256: 8556847f91a85c31ef65b05b7e9182a52775616d5d4e550dfb48cdee5fd35687 + md5: e45cfedc8ca5630e02c106ea36d2c5c6 + depends: + - ld_impl_linux-64 2.44 h1423503_1 + - sysroot_linux-64 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 3781716 + timestamp: 1752032761608 +- conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.44-h4852527_1.conda + sha256: fbd94448d099a8c5fe7d9ec8c67171ab6e2f4221f453fe327de9b5aaf507f992 + md5: 38e0be090e3af56e44a9cac46101f6cd + depends: + - binutils_impl_linux-64 2.44 h4bf12b8_1 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 36046 + timestamp: 1752032788780 +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + sha256: 5ced96500d945fb286c9c838e54fa759aa04a7129c59800f0846b4335cee770d + md5: 62ee74e96c5ebb0af99386de58cf9553 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc-ng >=12 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 252783 + timestamp: 1720974456583 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + sha256: 837b795a2bb39b75694ba910c13c15fa4998d4bb2a622c214a6a5174b2ae53d1 + md5: 74784ee3d225fc3dca89edb635b4e5cc + depends: + - __unix + license: ISC + purls: [] + size: 154402 + timestamp: 1754210968730 +- pypi: https://files.pythonhosted.org/packages/96/9e/16bc0b59ca73b47b82380c0ac6ef5084fb0f6e487d24eb102926456708eb/casadi-3.7.1-cp313-none-manylinux2014_x86_64.whl + name: casadi + version: 3.7.1 + sha256: 4373a813b306e2a5d083e2fc583daa1f65ad6e0c47688194e54e4ee589fd133a + requires_dist: + - numpy +- pypi: https://files.pythonhosted.org/packages/6f/3d/46bb04776c465cea2dd8aa2d4b61ab610b707f798f47838ef7e6105b025c/chex-0.1.90-py3-none-any.whl + name: chex + version: 0.1.90 + sha256: fce3de82588f72d4796e545e574a433aa29229cbdcf792555e41bead24b704ae + requires_dist: + - absl-py>=0.9.0 + - typing-extensions>=4.2.0 + - jax>=0.4.27 + - jaxlib>=0.4.27 + - numpy>=1.24.1 + - setuptools ; python_full_version >= '3.12' + - toolz>=0.9.0 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/7e/e8/64c37fadfc2816a7701fa8a6ed8d87327c7d54eacfbfb6edab14a2f2be75/cloudpickle-3.1.1-py3-none-any.whl + name: cloudpickle + version: 3.1.1 + sha256: c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/linux-64/conda-gcc-specs-15.1.0-hc55bae6_4.conda + sha256: 5ba36bd025d26e3d4b1145d904e6b88a33c1e06b2dbb195a9880cfe13a6770c5 + md5: 54e6f76f5aa752dfeb69f049e08eb885 + depends: + - gcc_impl_linux-64 >=15.1.0,<15.1.1.0a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 33222 + timestamp: 1753904065774 +- pypi: https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: contourpy + version: 1.3.3 + sha256: 4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9 + requires_dist: + - numpy>=1.25 + - furo ; extra == 'docs' + - sphinx>=7.2 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - bokeh ; extra == 'bokeh' + - selenium ; extra == 'bokeh' + - contourpy[bokeh,docs] ; extra == 'mypy' + - bokeh ; extra == 'mypy' + - docutils-stubs ; extra == 'mypy' + - mypy==1.17.0 ; extra == 'mypy' + - types-pillow ; extra == 'mypy' + - contourpy[test-no-images] ; extra == 'test' + - matplotlib ; extra == 'test' + - pillow ; extra == 'test' + - pytest ; extra == 'test-no-images' + - pytest-cov ; extra == 'test-no-images' + - pytest-rerunfailures ; extra == 'test-no-images' + - pytest-xdist ; extra == 'test-no-images' + - wurlitzer ; extra == 'test-no-images' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/ea/2f/6ae1db51dc34db499bfe340e89f79a63bd115fc32513a7bacdf17d33cd86/coverage-7.10.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: coverage + version: 7.10.3 + sha256: 913ceddb4289cbba3a310704a424e3fb7aac2bc0c3a23ea473193cb290cf17d4 + requires_dist: + - tomli ; python_full_version <= '3.11' and extra == 'toml' + requires_python: '>=3.9' +- pypi: ./ + name: crazyflow + version: 0.0.2 + sha256: 0d100752e1efd754bd2441a44d19c7c8b8868cbe91d88494bcc2443d05c1fca9 + requires_dist: + - jax>=0.7.0 + - mujoco>=3.3.0 + - mujoco-mjx>=3.3.0 + - gymnasium[mujoco]>=1.2.0 + - imageio + - einops + - flax + - ml-collections + - casadi + - numpy + - pytest>=8.0.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-timeout ; extra == 'test' + - gymnasium[array-api] ; extra == 'test' + - jax[cuda12] ; extra == 'gpu' + - fire ; extra == 'benchmark' + - matplotlib ; extra == 'benchmark' + - pandas ; extra == 'benchmark' + requires_python: '>=3.10' + editable: true +- conda: https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.0.0-hf484d3e_0.tar.bz2 + sha256: 72a84140fccc64e7941aa19affb86d49734c1f2729f13dedeadd588e4a0a68b4 + md5: 5f1c23fd134564eeb5ee730640168c5a + depends: + - gxx_linux-64 + - libgcc-ng >=7.3.0 + - libstdcxx-ng >=7.3.0 + license: BSD + purls: [] + size: 4573 + timestamp: 1549898421693 +- pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + name: cycler + version: 0.12.1 + sha256: 85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 + requires_dist: + - ipython ; extra == 'docs' + - matplotlib ; extra == 'docs' + - numpydoc ; extra == 'docs' + - sphinx ; extra == 'docs' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/87/62/9773de14fe6c45c23649e98b83231fffd7b9892b6cf863251dc2afa73643/einops-0.8.1-py3-none-any.whl + name: einops + version: 0.8.1 + sha256: 919387eb55330f5757c6bea9165c5ff5cfe63a642682ea788a6d472576d81737 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl + name: etils + version: 1.13.0 + sha256: d9cd4f40fbe77ad6613b7348a18132cc511237b6c076dbb89105c0b520a4c6bb + requires_dist: + - etils[array-types] ; extra == 'all' + - etils[eapp] ; extra == 'all' + - etils[ecolab] ; extra == 'all' + - etils[edc] ; extra == 'all' + - etils[enp] ; extra == 'all' + - etils[epath] ; extra == 'all' + - etils[epath-gcs] ; extra == 'all' + - etils[epath-s3] ; extra == 'all' + - etils[epy] ; extra == 'all' + - etils[etqdm] ; extra == 'all' + - etils[etree] ; extra == 'all' + - etils[etree-dm] ; extra == 'all' + - etils[etree-jax] ; extra == 'all' + - etils[etree-tf] ; extra == 'all' + - etils[enp] ; extra == 'array-types' + - pytest ; extra == 'dev' + - pytest-subtests ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - pyink ; extra == 'dev' + - pylint>=2.6.0 ; extra == 'dev' + - chex ; extra == 'dev' + - fiddle ; extra == 'dev' + - torch ; extra == 'dev' + - optree ; extra == 'dev' + - tensorflow-datasets ; extra == 'dev' + - pydantic ; extra == 'dev' + - sphinx-apitree[ext] ; extra == 'docs' + - etils[dev,all] ; extra == 'docs' + - absl-py ; extra == 'eapp' + - simple-parsing ; extra == 'eapp' + - etils[epy] ; extra == 'eapp' + - jupyter ; extra == 'ecolab' + - numpy ; extra == 'ecolab' + - mediapy ; extra == 'ecolab' + - packaging ; extra == 'ecolab' + - protobuf ; extra == 'ecolab' + - etils[enp] ; extra == 'ecolab' + - etils[epy] ; extra == 'ecolab' + - etils[etree] ; extra == 'ecolab' + - etils[epy] ; extra == 'edc' + - numpy ; extra == 'enp' + - einops ; extra == 'enp' + - etils[epy] ; extra == 'enp' + - fsspec ; extra == 'epath' + - importlib-resources ; extra == 'epath' + - typing-extensions ; extra == 'epath' + - zipp ; extra == 'epath' + - etils[epy] ; extra == 'epath' + - gcsfs ; extra == 'epath-gcs' + - etils[epath] ; extra == 'epath-gcs' + - s3fs ; extra == 'epath-s3' + - etils[epath] ; extra == 'epath-s3' + - typing-extensions ; extra == 'epy' + - absl-py ; extra == 'etqdm' + - tqdm ; extra == 'etqdm' + - etils[epy] ; extra == 'etqdm' + - etils[array-types] ; extra == 'etree' + - etils[epy] ; extra == 'etree' + - etils[enp] ; extra == 'etree' + - etils[etqdm] ; extra == 'etree' + - dm-tree ; extra == 'etree-dm' + - etils[etree] ; extra == 'etree-dm' + - jax[cpu] ; extra == 'etree-jax' + - etils[etree] ; extra == 'etree-jax' + - tensorflow ; extra == 'etree-tf' + - etils[etree] ; extra == 'etree-tf' + - etils[ecolab] ; extra == 'lazy-imports' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/05/2c/ffc08c54c05cdce6fbed2aeebc46348dbe180c6d2c541c7af7ba0aa5f5f8/Farama_Notifications-0.0.4-py3-none-any.whl + name: farama-notifications + version: 0.0.4 + sha256: 14de931035a41961f7c056361dc7f980762a143d05791ef5794a751a2caf05ae +- pypi: https://files.pythonhosted.org/packages/6b/b6/82c7e601d6d3c3278c40b7bd35e17e82aa227f050aa9f66cb7b7fce29471/fire-0.7.0.tar.gz + name: fire + version: 0.7.0 + sha256: 961550f07936eaf65ad1dc8360f2b2bf8408fad46abbfa4d2a3794f8d2a95cdf + requires_dist: + - termcolor +- pypi: https://files.pythonhosted.org/packages/8a/0b/6b46d04a251f8a8b430bfd79b02a071bcd37b5dfa8321a7ad5db47dcd103/flax-0.11.1-py3-none-any.whl + name: flax + version: 0.11.1 + sha256: b29a46564193be437c88babb5e479b5c258fc7c54f005bc3051f05fc82e0ab83 + requires_dist: + - numpy>=1.23.2 ; python_full_version >= '3.11' + - numpy>=1.26.0 ; python_full_version >= '3.12' + - jax>=0.6.0 + - msgpack + - optax + - orbax-checkpoint + - tensorstore + - rich>=11.1 + - typing-extensions>=4.2 + - pyyaml>=5.4.1 + - treescope>=0.1.7 + - matplotlib ; extra == 'all' + - clu ; extra == 'testing' + - clu<=0.0.9 ; python_full_version < '3.10' and extra == 'testing' + - einops ; extra == 'testing' + - gymnasium[accept-rom-license,atari] ; extra == 'testing' + - jaxlib ; extra == 'testing' + - jaxtyping ; extra == 'testing' + - jraph>=0.0.6.dev0 ; extra == 'testing' + - ml-collections ; extra == 'testing' + - mypy ; extra == 'testing' + - opencv-python ; extra == 'testing' + - pytest ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-custom-exit-code ; extra == 'testing' + - pytest-xdist ; extra == 'testing' + - pytype ; extra == 'testing' + - sentencepiece ; extra == 'testing' + - tensorflow-text>=2.11.0 ; sys_platform != 'darwin' and extra == 'testing' + - tensorflow-datasets ; extra == 'testing' + - tensorflow>=2.12.0 ; extra == 'testing' + - torch ; extra == 'testing' + - treescope>=0.1.1 ; python_full_version >= '3.10' and extra == 'testing' + - cloudpickle>=3.0.0 ; extra == 'testing' + - ale-py>=0.10.2 ; extra == 'testing' + - sphinx>=4.3.0 ; extra == 'docs' + - sphinx-book-theme ; extra == 'docs' + - pygments>=2.6.1 ; extra == 'docs' + - ipykernel ; extra == 'docs' + - myst-nb ; extra == 'docs' + - nbstripout ; extra == 'docs' + - recommonmark ; extra == 'docs' + - ipython-genutils ; extra == 'docs' + - sphinx-design ; extra == 'docs' + - jupytext==1.13.8 ; extra == 'docs' + - dm-haiku>=0.0.14 ; extra == 'docs' + - docutils==0.16 ; extra == 'docs' + - matplotlib ; extra == 'docs' + - scikit-learn ; extra == 'docs' + - ml-collections ; extra == 'docs' + - einops ; extra == 'docs' + - kagglehub>=0.3.3 ; extra == 'docs' + - ipywidgets>=8.1.5 ; extra == 'docs' + - nanobind>=2.5.0 ; extra == 'dev' + - pre-commit>=3.8.0 ; extra == 'dev' + - scikit-build-core[pyproject]>=0.11.0 ; extra == 'dev' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/e9/a2/5a9fc21c354bf8613215ce233ab0d933bd17d5ff4c29693636551adbc7b3/fonttools-4.59.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl + name: fonttools + version: 4.59.1 + sha256: 8387876a8011caec52d327d5e5bca705d9399ec4b17afb8b431ec50d47c17d23 + requires_dist: + - lxml>=4.0 ; extra == 'lxml' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' + - zopfli>=0.1.4 ; extra == 'woff' + - unicodedata2>=15.1.0 ; python_full_version < '3.13' and extra == 'unicode' + - lz4>=1.7.4.2 ; extra == 'graphite' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' + - pycairo ; extra == 'interpolatable' + - matplotlib ; extra == 'plot' + - sympy ; extra == 'symfont' + - xattr ; sys_platform == 'darwin' and extra == 'type1' + - skia-pathops>=0.5.0 ; extra == 'pathops' + - uharfbuzz>=0.23.0 ; extra == 'repacker' + - lxml>=4.0 ; extra == 'all' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' + - zopfli>=0.1.4 ; extra == 'all' + - unicodedata2>=15.1.0 ; python_full_version < '3.13' and extra == 'all' + - lz4>=1.7.4.2 ; extra == 'all' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' + - pycairo ; extra == 'all' + - matplotlib ; extra == 'all' + - sympy ; extra == 'all' + - xattr ; sys_platform == 'darwin' and extra == 'all' + - skia-pathops>=0.5.0 ; extra == 'all' + - uharfbuzz>=0.23.0 ; extra == 'all' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/2f/e0/014d5d9d7a4564cf1c40b5039bc882db69fd881111e03ab3657ac0b218e2/fsspec-2025.7.0-py3-none-any.whl + name: fsspec + version: 2025.7.0 + sha256: 8b012e39f63c7d5f10474de957f3ab793b47b45ae7d39f2fb735f8bbe25c0e21 + requires_dist: + - adlfs ; extra == 'abfs' + - adlfs ; extra == 'adl' + - pyarrow>=1 ; extra == 'arrow' + - dask ; extra == 'dask' + - distributed ; extra == 'dask' + - pre-commit ; extra == 'dev' + - ruff>=0.5 ; extra == 'dev' + - numpydoc ; extra == 'doc' + - sphinx ; extra == 'doc' + - sphinx-design ; extra == 'doc' + - sphinx-rtd-theme ; extra == 'doc' + - yarl ; extra == 'doc' + - dropbox ; extra == 'dropbox' + - dropboxdrivefs ; extra == 'dropbox' + - requests ; extra == 'dropbox' + - adlfs ; extra == 'full' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'full' + - dask ; extra == 'full' + - distributed ; extra == 'full' + - dropbox ; extra == 'full' + - dropboxdrivefs ; extra == 'full' + - fusepy ; extra == 'full' + - gcsfs ; extra == 'full' + - libarchive-c ; extra == 'full' + - ocifs ; extra == 'full' + - panel ; extra == 'full' + - paramiko ; extra == 'full' + - pyarrow>=1 ; extra == 'full' + - pygit2 ; extra == 'full' + - requests ; extra == 'full' + - s3fs ; extra == 'full' + - smbprotocol ; extra == 'full' + - tqdm ; extra == 'full' + - fusepy ; extra == 'fuse' + - gcsfs ; extra == 'gcs' + - pygit2 ; extra == 'git' + - requests ; extra == 'github' + - gcsfs ; extra == 'gs' + - panel ; extra == 'gui' + - pyarrow>=1 ; extra == 'hdfs' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'http' + - libarchive-c ; extra == 'libarchive' + - ocifs ; extra == 'oci' + - s3fs ; extra == 's3' + - paramiko ; extra == 'sftp' + - smbprotocol ; extra == 'smb' + - paramiko ; extra == 'ssh' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'test' + - numpy ; extra == 'test' + - pytest ; extra == 'test' + - pytest-asyncio!=0.22.0 ; extra == 'test' + - pytest-benchmark ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-mock ; extra == 'test' + - pytest-recording ; extra == 'test' + - pytest-rerunfailures ; extra == 'test' + - requests ; extra == 'test' + - aiobotocore>=2.5.4,<3.0.0 ; extra == 'test-downstream' + - dask[dataframe,test] ; extra == 'test-downstream' + - moto[server]>4,<5 ; extra == 'test-downstream' + - pytest-timeout ; extra == 'test-downstream' + - xarray ; extra == 'test-downstream' + - adlfs ; extra == 'test-full' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'test-full' + - cloudpickle ; extra == 'test-full' + - dask ; extra == 'test-full' + - distributed ; extra == 'test-full' + - dropbox ; extra == 'test-full' + - dropboxdrivefs ; extra == 'test-full' + - fastparquet ; extra == 'test-full' + - fusepy ; extra == 'test-full' + - gcsfs ; extra == 'test-full' + - jinja2 ; extra == 'test-full' + - kerchunk ; extra == 'test-full' + - libarchive-c ; extra == 'test-full' + - lz4 ; extra == 'test-full' + - notebook ; extra == 'test-full' + - numpy ; extra == 'test-full' + - ocifs ; extra == 'test-full' + - pandas ; extra == 'test-full' + - panel ; extra == 'test-full' + - paramiko ; extra == 'test-full' + - pyarrow ; extra == 'test-full' + - pyarrow>=1 ; extra == 'test-full' + - pyftpdlib ; extra == 'test-full' + - pygit2 ; extra == 'test-full' + - pytest ; extra == 'test-full' + - pytest-asyncio!=0.22.0 ; extra == 'test-full' + - pytest-benchmark ; extra == 'test-full' + - pytest-cov ; extra == 'test-full' + - pytest-mock ; extra == 'test-full' + - pytest-recording ; extra == 'test-full' + - pytest-rerunfailures ; extra == 'test-full' + - python-snappy ; extra == 'test-full' + - requests ; extra == 'test-full' + - smbprotocol ; extra == 'test-full' + - tqdm ; extra == 'test-full' + - urllib3 ; extra == 'test-full' + - zarr ; extra == 'test-full' + - zstandard ; python_full_version < '3.14' and extra == 'test-full' + - tqdm ; extra == 'tqdm' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/linux-64/gcc-15.1.0-hd165ed4_4.conda + sha256: f1982bdefaea2e15b15b58b9590fb04a957de85d1e0e78ec00803df6e1414f0b + md5: 4980d50b2fa5818ed67acabc4aef8856 + depends: + - conda-gcc-specs + - gcc_impl_linux-64 15.1.0.* + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 30935 + timestamp: 1753904205082 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-15.1.0-h4393ad2_4.conda + sha256: 59e2af2cb3d0592d0b61ee81eaba60a24321676c053630f092957909c70d6940 + md5: bd50f28da1e011caf83ebfe967dbcc94 + depends: + - binutils_impl_linux-64 >=2.40 + - libgcc >=15.1.0 + - libgcc-devel_linux-64 15.1.0 h4c094af_104 + - libgomp >=15.1.0 + - libsanitizer 15.1.0 h97b714f_4 + - libstdcxx >=15.1.0 + - sysroot_linux-64 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 77268492 + timestamp: 1753903968595 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-15.1.0-h1ac4077_11.conda + sha256: ec6192aee2425dd95fe854c9636dd6c5ef6bd63b6a24404c7a0d15147a451745 + md5: 8b168842f96f48c156c38078efa6222a + depends: + - binutils_linux-64 + - gcc_impl_linux-64 15.1.0.* + - sysroot_linux-64 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 32348 + timestamp: 1748905891752 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gfortran-15.1.0-h33e79ad_4.conda + sha256: 365ea11f669ac222688bcd4e64dbb31572d1a494f09d6573f6492a7cb456b165 + md5: 6a5f5090d8eb31faebf1e37652e8d1d3 + depends: + - gcc 15.1.0.* + - gcc_impl_linux-64 15.1.0.* + - gfortran_impl_linux-64 15.1.0.* + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 30403 + timestamp: 1753904215832 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gfortran_impl_linux-64-15.1.0-h3b9cdf2_4.conda + sha256: c47f605b20da92d9a6bf12fc7e794799dcfe1fbc39b1405b45c2e788410c5df6 + md5: 82f37031ba4df0e97a222646ddcfd673 + depends: + - gcc_impl_linux-64 >=15.1.0 + - libgcc >=15.1.0 + - libgfortran5 >=15.1.0 + - libstdcxx >=15.1.0 + - sysroot_linux-64 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 18342034 + timestamp: 1753904128995 +- pypi: https://files.pythonhosted.org/packages/7f/ba/de3630757c7d7fc2086aaf3994926d6b869d31586e4d0c14f1666af31b93/glfw-2.9.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 + name: glfw + version: 2.9.0 + sha256: 4e11271e49eb9bc53431ade022e284d5a59abeace81fe3b178db1bf3ccc0c449 + requires_dist: + - glfw-preview ; extra == 'preview' +- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-15.1.0-h6a1bac1_4.conda + sha256: 6ea00d692ae4b4097ce98ddafa777812dc3e3252e85d944800833275ae3aac68 + md5: f880f89a51a8f93ecdc3b82c4627dc99 + depends: + - gcc_impl_linux-64 15.1.0 h4393ad2_4 + - libstdcxx-devel_linux-64 15.1.0 h4c094af_104 + - sysroot_linux-64 + - tzdata + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 16256662 + timestamp: 1753904166626 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-15.1.0-h1a088d8_11.conda + sha256: 6c32eaef53fed1d67331c6a94b5c09669bd6c6e34da1e03f0498ddc854ef7897 + md5: bed2ffede053aa08bf4ada31878ac426 + depends: + - binutils_linux-64 + - gcc_linux-64 15.1.0 h1ac4077_11 + - gxx_impl_linux-64 15.1.0.* + - sysroot_linux-64 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 30688 + timestamp: 1748905910436 +- pypi: https://files.pythonhosted.org/packages/a0/e2/a111dbb8625af467ea4760a1373d6ef27aac3137931219902406ccc05423/gymnasium-1.2.0-py3-none-any.whl + name: gymnasium + version: 1.2.0 + sha256: fc4a1e4121a9464c29b4d7dc6ade3fbeaa36dea448682f5f71a6d2c17489ea76 + requires_dist: + - numpy>=1.21.0 + - cloudpickle>=1.2.0 + - typing-extensions>=4.3.0 + - farama-notifications>=0.0.1 + - ale-py>=0.9 ; extra == 'atari' + - box2d-py==2.3.5 ; extra == 'box2d' + - pygame>=2.1.3 ; extra == 'box2d' + - swig==4.* ; extra == 'box2d' + - pygame>=2.1.3 ; extra == 'classic-control' + - pygame>=2.1.3 ; extra == 'classic-control' + - mujoco>=2.1.5 ; extra == 'mujoco' + - imageio>=2.14.1 ; extra == 'mujoco' + - packaging>=23.0 ; extra == 'mujoco' + - pygame>=2.1.3 ; extra == 'toy-text' + - pygame>=2.1.3 ; extra == 'toy-text' + - jax>=0.4.16 ; extra == 'jax' + - jaxlib>=0.4.16 ; extra == 'jax' + - flax>=0.5.0 ; extra == 'jax' + - array-api-compat>=1.11.0 ; extra == 'jax' + - numpy>=2.1 ; extra == 'jax' + - torch>=1.13.0 ; extra == 'torch' + - array-api-compat>=1.11.0 ; extra == 'torch' + - numpy>=2.1 ; extra == 'torch' + - array-api-compat>=1.11.0 ; extra == 'array-api' + - numpy>=2.1 ; extra == 'array-api' + - moviepy>=1.0.0 ; extra == 'other' + - matplotlib>=3.0 ; extra == 'other' + - opencv-python>=3.0 ; extra == 'other' + - seaborn>=0.13 ; extra == 'other' + - ale-py>=0.9 ; extra == 'all' + - box2d-py==2.3.5 ; extra == 'all' + - pygame>=2.1.3 ; extra == 'all' + - swig==4.* ; extra == 'all' + - pygame>=2.1.3 ; extra == 'all' + - mujoco>=2.1.5 ; extra == 'all' + - imageio>=2.14.1 ; extra == 'all' + - packaging>=23.0 ; extra == 'all' + - pygame>=2.1.3 ; extra == 'all' + - jax>=0.4.16 ; extra == 'all' + - jaxlib>=0.4.16 ; extra == 'all' + - flax>=0.5.0 ; extra == 'all' + - array-api-compat>=1.11.0 ; extra == 'all' + - numpy>=2.1 ; extra == 'all' + - torch>=1.13.0 ; extra == 'all' + - array-api-compat>=1.11.0 ; extra == 'all' + - numpy>=2.1 ; extra == 'all' + - array-api-compat>=1.11.0 ; extra == 'all' + - numpy>=2.1 ; extra == 'all' + - opencv-python>=3.0 ; extra == 'all' + - matplotlib>=3.0 ; extra == 'all' + - moviepy>=1.0.0 ; extra == 'all' + - pytest>=7.1.3 ; extra == 'testing' + - scipy>=1.7.3 ; extra == 'testing' + - dill>=0.3.7 ; extra == 'testing' + - array-api-extra>=0.7.0 ; extra == 'testing' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a0/1e/62a2ec3104394a2975a2629eec89276ede9dbe717092f6966fcf963e1bf0/humanize-4.12.3-py3-none-any.whl + name: humanize + version: 4.12.3 + sha256: 2cbf6370af06568fa6d2da77c86edb7886f3160ecd19ee1ffef07979efc597f6 + requires_dist: + - freezegun ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/cb/bd/b394387b598ed84d8d0fa90611a90bee0adc2021820ad5729f7ced74a8e2/imageio-2.37.0-py3-none-any.whl + name: imageio + version: 2.37.0 + sha256: 11efa15b87bc7871b61590326b2d635439acc321cf7f8ce996f812543ce10eed + requires_dist: + - numpy + - pillow>=8.3.2 + - astropy ; extra == 'all-plugins' + - av ; extra == 'all-plugins' + - imageio-ffmpeg ; extra == 'all-plugins' + - numpy>2 ; extra == 'all-plugins' + - pillow-heif ; extra == 'all-plugins' + - psutil ; extra == 'all-plugins' + - rawpy ; extra == 'all-plugins' + - tifffile ; extra == 'all-plugins' + - av ; extra == 'all-plugins-pypy' + - imageio-ffmpeg ; extra == 'all-plugins-pypy' + - pillow-heif ; extra == 'all-plugins-pypy' + - psutil ; extra == 'all-plugins-pypy' + - tifffile ; extra == 'all-plugins-pypy' + - wheel ; extra == 'build' + - pytest ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - fsspec[github] ; extra == 'dev' + - black ; extra == 'dev' + - flake8 ; extra == 'dev' + - sphinx<6 ; extra == 'docs' + - numpydoc ; extra == 'docs' + - pydata-sphinx-theme ; extra == 'docs' + - imageio-ffmpeg ; extra == 'ffmpeg' + - psutil ; extra == 'ffmpeg' + - astropy ; extra == 'fits' + - astropy ; extra == 'full' + - av ; extra == 'full' + - black ; extra == 'full' + - flake8 ; extra == 'full' + - fsspec[github] ; extra == 'full' + - gdal ; extra == 'full' + - imageio-ffmpeg ; extra == 'full' + - itk ; extra == 'full' + - numpy>2 ; extra == 'full' + - numpydoc ; extra == 'full' + - pillow-heif ; extra == 'full' + - psutil ; extra == 'full' + - pydata-sphinx-theme ; extra == 'full' + - pytest ; extra == 'full' + - pytest-cov ; extra == 'full' + - rawpy ; extra == 'full' + - sphinx<6 ; extra == 'full' + - tifffile ; extra == 'full' + - wheel ; extra == 'full' + - gdal ; extra == 'gdal' + - itk ; extra == 'itk' + - black ; extra == 'linting' + - flake8 ; extra == 'linting' + - pillow-heif ; extra == 'pillow-heif' + - av ; extra == 'pyav' + - rawpy ; extra == 'rawpy' + - numpy>2 ; extra == 'rawpy' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - fsspec[github] ; extra == 'test' + - tifffile ; extra == 'tifffile' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl + name: importlib-resources + version: 6.5.2 + sha256: 789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec + requires_dist: + - zipp>=3.1.0 ; python_full_version < '3.10' + - pytest>=6,!=8.1.* ; extra == 'test' + - zipp>=3.17 ; extra == 'test' + - jaraco-test>=5.4 ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pytest-checkdocs>=2.4 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=2.2 ; extra == 'enabler' + - pytest-mypy ; extra == 'type' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl + name: iniconfig + version: 2.1.0 + sha256: 9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/ad/de/3092df5073cd9c07c01b10612fc541538b74b02184fac90e3beada20f758/jax-0.7.0-py3-none-any.whl + name: jax + version: 0.7.0 + sha256: 62833036cbaf4641d66ae94c61c0446890a91b2c0d153946583a0ebe04877a76 + requires_dist: + - jaxlib<=0.7.0,>=0.7.0 + - ml-dtypes>=0.5.0 + - numpy>=1.26 + - opt-einsum + - scipy>=1.12 + - jaxlib==0.7.0 ; extra == 'minimum-jaxlib' + - jaxlib==0.6.2 ; extra == 'ci' + - jaxlib<=0.7.0,>=0.7.0 ; extra == 'tpu' + - libtpu==0.0.19.* ; extra == 'tpu' + - requests ; extra == 'tpu' + - jaxlib<=0.7.0,>=0.7.0 ; extra == 'cuda' + - jax-cuda12-plugin[with-cuda]<=0.7.0,>=0.7.0 ; extra == 'cuda' + - jaxlib<=0.7.0,>=0.7.0 ; extra == 'cuda12' + - jax-cuda12-plugin[with-cuda]<=0.7.0,>=0.7.0 ; extra == 'cuda12' + - jaxlib<=0.7.0,>=0.7.0 ; extra == 'cuda13' + - jax-cuda13-plugin[with-cuda]<=0.7.0,>=0.7.0 ; extra == 'cuda13' + - jaxlib<=0.7.0,>=0.7.0 ; extra == 'cuda12-local' + - jax-cuda12-plugin<=0.7.0,>=0.7.0 ; extra == 'cuda12-local' + - jaxlib<=0.7.0,>=0.7.0 ; extra == 'cuda13-local' + - jax-cuda13-plugin<=0.7.0,>=0.7.0 ; extra == 'cuda13-local' + - jaxlib<=0.7.0,>=0.7.0 ; extra == 'rocm' + - jax-rocm60-plugin<=0.7.0,>=0.7.0 ; extra == 'rocm' + - kubernetes ; extra == 'k8s' + - xprof ; extra == 'xprof' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/49/ef/1fe3c10c27199fbaf593ee7c53226e4eae057d505207707cd815a8ee0764/jax_cuda12_pjrt-0.7.0-py3-none-manylinux2014_x86_64.whl + name: jax-cuda12-pjrt + version: 0.7.0 + sha256: 742cc260d697771cb883084398748a007d233286aa3fe5fc152a74c073d59a24 +- pypi: https://files.pythonhosted.org/packages/0d/c2/0f5698d4ececeff3cf0e9802b1f58f137adea83562cfa735591436978a49/jax_cuda12_plugin-0.7.0-cp313-cp313-manylinux2014_x86_64.whl + name: jax-cuda12-plugin + version: 0.7.0 + sha256: bfde0db714b2ad5fe9e86c1f6d2e60c7d4d980d015029858208828abcd8274cf + requires_dist: + - jax-cuda12-pjrt==0.7.0 + - nvidia-cublas-cu12>=12.1.3.1 ; extra == 'with-cuda' + - nvidia-cuda-cupti-cu12>=12.1.105 ; extra == 'with-cuda' + - nvidia-cuda-nvcc-cu12>=12.6.85 ; extra == 'with-cuda' + - nvidia-cuda-runtime-cu12>=12.1.105 ; extra == 'with-cuda' + - nvidia-cudnn-cu12>=9.8,<10.0 ; extra == 'with-cuda' + - nvidia-cufft-cu12>=11.0.2.54 ; extra == 'with-cuda' + - nvidia-cusolver-cu12>=11.4.5.107 ; extra == 'with-cuda' + - nvidia-cusparse-cu12>=12.1.0.106 ; extra == 'with-cuda' + - nvidia-nccl-cu12>=2.18.1 ; extra == 'with-cuda' + - nvidia-nvjitlink-cu12>=12.1.105 ; extra == 'with-cuda' + - nvidia-cuda-nvrtc-cu12>=12.1.55 ; extra == 'with-cuda' + - nvidia-nvshmem-cu12>=3.2.5 ; extra == 'with-cuda' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/c7/83/7ba260095e98a5004af4fdb4315010c445441473cac41afdb973bf212deb/jaxlib-0.7.0-cp313-cp313-manylinux2014_x86_64.whl + name: jaxlib + version: 0.7.0 + sha256: df31664a53c13a9263bca0e8c39e0380a0ccae0b1c125376df63a480d9cb2087 + requires_dist: + - scipy>=1.12 + - numpy>=1.26 + - ml-dtypes>=0.5.0 + requires_python: '>=3.11' +- conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_8.conda + sha256: 305c22a251db227679343fd73bfde121e555d466af86e537847f4c8b9436be0d + md5: ff007ab0f0fdc53d245972bba8a6d40c + constrains: + - sysroot_linux-64 ==2.28 + license: LGPL-2.0-or-later AND LGPL-2.0-or-later WITH exceptions AND GPL-2.0-or-later + license_family: GPL + purls: [] + size: 1272697 + timestamp: 1752669126073 +- pypi: https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: kiwisolver + version: 1.4.9 + sha256: b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098 + requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + sha256: 1a620f27d79217c1295049ba214c2f80372062fd251b569e9873d4a953d27554 + md5: 0be7c6e070c19105f966d3758448d018 + depends: + - __glibc >=2.17,<3.0.a0 + constrains: + - binutils_impl_linux-64 2.44 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 676044 + timestamp: 1752032747103 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda + sha256: da2080da8f0288b95dd86765c801c6e166c4619b910b11f9a8446fb852438dc2 + md5: 4211416ecba1866fab0c6470986c22d6 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - expat 2.7.1.* + license: MIT + license_family: MIT + purls: [] + size: 74811 + timestamp: 1752719572741 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda + sha256: 764432d32db45466e87f10621db5b74363a9f847d2b8b1f9743746cd160f06ab + md5: ede4673863426c0883c0063d853bbd85 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 57433 + timestamp: 1743434498161 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda + sha256: 144e35c1c2840f2dc202f6915fc41879c19eddbb8fa524e3ca4aa0d14018b26f + md5: f406dcbb2e7bef90d793e50e79a2882b + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + constrains: + - libgcc-ng ==15.1.0=*_4 + - libgomp 15.1.0 h767d61c_4 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 824153 + timestamp: 1753903866511 +- conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.1.0-h4c094af_104.conda + sha256: 90a2a128bed4926ab90db5e19989d68a0bff412e993c1324fe004ef02337e3e9 + md5: 05eec361e8eca1ad47bad0f8b97a9d67 + depends: + - __unix + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 2726287 + timestamp: 1753903789289 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda + sha256: 76ceac93ed98f208363d6e9c75011b0ff7b97b20f003f06461a619557e726637 + md5: 28771437ffcd9f3417c66012dc49a3be + depends: + - libgcc 15.1.0 h767d61c_4 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 29249 + timestamp: 1753903872571 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_4.conda + sha256: 2fe41683928eb3c57066a60ec441e605a69ce703fc933d6d5167debfeba8a144 + md5: 53e876bc2d2648319e94c33c57b9ec74 + depends: + - libgfortran5 15.1.0 hcea5267_4 + constrains: + - libgfortran-ng ==15.1.0=*_4 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 29246 + timestamp: 1753903898593 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_4.conda + sha256: 3070e5e2681f7f2fb7af0a81b92213f9ab430838900da8b4f9b8cf998ddbdd84 + md5: 8a4ab7ff06e4db0be22485332666da0f + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=15.1.0 + constrains: + - libgfortran 15.1.0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 1564595 + timestamp: 1753903882088 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda + sha256: e0487a8fec78802ac04da0ac1139c3510992bc58a58cde66619dde3b363c2933 + md5: 3baf8976c96134738bba224e9ef6b1e5 + depends: + - __glibc >=2.17,<3.0.a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 447289 + timestamp: 1753903801049 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + sha256: f2591c0069447bbe28d4d696b7fcb0c5bd0b4ac582769b89addbcf26fb3430d8 + md5: 1a580f7796c7bf6393fddb8bbbde58dc + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - xz 5.8.1.* + license: 0BSD + purls: [] + size: 112894 + timestamp: 1749230047870 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + sha256: 3aa92d4074d4063f2a162cd8ecb45dccac93e543e565c01a787e16a43501f7ee + md5: c7e925f37e3b40d893459e625f6a53f1 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 91183 + timestamp: 1748393666725 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_1.conda + sha256: 3f3fc30fe340bc7f8f46fea6a896da52663b4d95caed1f144e8ea114b4bb6b61 + md5: 7e2ba4ca7e6ffebb7f7fc2da2744df61 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libgfortran + - libgfortran5 >=14.3.0 + constrains: + - openblas >=0.3.30,<0.3.31.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 5918161 + timestamp: 1753405234435 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-15.1.0-h97b714f_4.conda + sha256: 1a65aeb39ffc7c1ed41c4aebd05263016d70b6f8da21a5185d0c3dba459a0931 + md5: 9577e03ec70b7986ab78a3f057af0df8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=15.1.0 + - libstdcxx >=15.1.0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 5139090 + timestamp: 1753903908812 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda + sha256: 6d9c32fc369af5a84875725f7ddfbfc2ace795c28f246dc70055a79f9b2003da + md5: 0b367fad34931cb79e0d6b7e5c06bb1c + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + license: blessing + purls: [] + size: 932581 + timestamp: 1753948484112 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_4.conda + sha256: b5b239e5fca53ff90669af1686c86282c970dd8204ebf477cf679872eb6d48ac + md5: 3c376af8888c386b9d3d1c2701e2f3ab + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc 15.1.0 h767d61c_4 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 3903453 + timestamp: 1753903894186 +- conda: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-15.1.0-h4c094af_104.conda + sha256: 30f9b61a69fafcb242a164238bf9317a743794912da41747b6b31594a0abbb47 + md5: 608049d7d920f3c559197d4c5445d243 + depends: + - __unix + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 14390986 + timestamp: 1753903815323 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_4.conda + sha256: 81c841c1cf4c0d06414aaa38a249f9fdd390554943065c3a0b18a9fb7e8cc495 + md5: 2d34729cbc1da0ec988e57b13b712067 + depends: + - libstdcxx 15.1.0 h8f9b012_4 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 29317 + timestamp: 1753903924491 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + sha256: 787eb542f055a2b3de553614b25f09eefb0a0931b0c87dbcce6efdfd92f04f18 + md5: 40b61aab5c7ba9ff276c41cfffe6b80b + depends: + - libgcc-ng >=12 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 33601 + timestamp: 1680112270483 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4 + md5: edb0dca6bc32e4f4789199455a1dbeb8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 60963 + timestamp: 1727963148474 +- pypi: https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl + name: markdown-it-py + version: 4.0.0 + sha256: 87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 + requires_dist: + - mdurl~=0.1 + - psutil ; extra == 'benchmarking' + - pytest ; extra == 'benchmarking' + - pytest-benchmark ; extra == 'benchmarking' + - commonmark~=0.9 ; extra == 'compare' + - markdown~=3.4 ; extra == 'compare' + - mistletoe~=1.0 ; extra == 'compare' + - mistune~=3.0 ; extra == 'compare' + - panflute~=2.3 ; extra == 'compare' + - markdown-it-pyrs ; extra == 'compare' + - linkify-it-py>=1,<3 ; extra == 'linkify' + - mdit-py-plugins>=0.5.0 ; extra == 'plugins' + - gprof2dot ; extra == 'profiling' + - mdit-py-plugins>=0.5.0 ; extra == 'rtd' + - myst-parser ; extra == 'rtd' + - pyyaml ; extra == 'rtd' + - sphinx ; extra == 'rtd' + - sphinx-copybutton ; extra == 'rtd' + - sphinx-design ; extra == 'rtd' + - sphinx-book-theme~=1.0 ; extra == 'rtd' + - jupyter-sphinx ; extra == 'rtd' + - ipykernel ; extra == 'rtd' + - coverage ; extra == 'testing' + - pytest ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-regressions ; extra == 'testing' + - requests ; extra == 'testing' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/52/1b/233e3094b749df16e3e6cd5a44849fd33852e692ad009cf7de00cf58ddf6/matplotlib-3.10.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: matplotlib + version: 3.10.5 + sha256: d52fd5b684d541b5a51fb276b2b97b010c75bee9aa392f96b4a07aeb491e33c7 + requires_dist: + - contourpy>=1.0.1 + - cycler>=0.10 + - fonttools>=4.22.0 + - kiwisolver>=1.3.1 + - numpy>=1.23 + - packaging>=20.0 + - pillow>=8 + - pyparsing>=2.3.1 + - python-dateutil>=2.7 + - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' + - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' + - setuptools-scm>=7 ; extra == 'dev' + - setuptools>=64 ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + name: mdurl + version: 0.1.2 + sha256: 84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/ab/8a/18d4ff2c7bd83f30d6924bd4ad97abf418488c3f908dea228d6f0961ad68/ml_collections-1.1.0-py3-none-any.whl + name: ml-collections + version: 1.1.0 + sha256: 23b6fa4772aac1ae745a96044b925a5746145a70734f087eaca6626e92c05cbc + requires_dist: + - absl-py + - pyyaml + - pytest ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - pylint>=2.6.0 ; extra == 'dev' + - pyink ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/14/f3/091ba84e5395d7fe5b30c081a44dec881cd84b408db1763ee50768b2ab63/ml_dtypes-0.5.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: ml-dtypes + version: 0.5.3 + sha256: 6936283b56d74fbec431ca57ce58a90a908fdbd14d4e2d22eea6d72bb208a7b7 + requires_dist: + - numpy>=1.21 + - numpy>=1.21.2 ; python_full_version >= '3.10' + - numpy>=1.23.3 ; python_full_version >= '3.11' + - numpy>=1.26.0 ; python_full_version >= '3.12' + - numpy>=2.1.0 ; python_full_version >= '3.13' + - absl-py ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - pylint>=2.6.0 ; extra == 'dev' + - pyink ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/20/22/2ebae7ae43cd8f2debc35c631172ddf14e2a87ffcc04cf43ff9df9fff0d3/msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: msgpack + version: 1.1.1 + sha256: 9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/b5/8c/54e5dd1df6fced73ea0b183ccf05939757bfc6fab9d720e1e051963ae154/mujoco-3.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: mujoco + version: 3.3.5 + sha256: 70ef62e02169e74d7dd38e8eacc5275d9d1f53eb0713fd410fe1d71410ecfd93 + requires_dist: + - absl-py + - etils[epath] + - glfw + - numpy + - pyopengl + - usd-core ; extra == 'usd' + - pillow ; extra == 'usd' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/59/b1/965d28e5ece3e59772b79dc43144b70f73b188f8f49b4997c91a646d85a4/mujoco_mjx-3.3.5-py3-none-any.whl + name: mujoco-mjx + version: 3.3.5 + sha256: 207e7144f26dd5a9c4657e5aa055f5e103d074bf2aaac9c8aba26e50426d2522 + requires_dist: + - absl-py + - etils[epath] + - jax + - jaxlib + - mujoco>=3.3.5.dev0 + - scipy + - trimesh + - warp-lang==1.8.1 ; extra == 'warp' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 + md5: 47e340acb35de30501a76c7c799c41d7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: X11 AND BSD-3-Clause + purls: [] + size: 891641 + timestamp: 1738195959188 +- pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl + name: nest-asyncio + version: 1.6.0 + sha256: 87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c + requires_python: '>=3.5' +- pypi: https://files.pythonhosted.org/packages/1d/0f/571b2c7a3833ae419fe69ff7b479a78d313581785203cc70a8db90121b9a/numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: numpy + version: 2.3.2 + sha256: 938065908d1d869c7d75d8ec45f735a034771c6ea07088867f713d1cd3bbbe4f + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/77/3c/aa88abe01f3be3d1f8f787d1d33dc83e76fec05945f9a28fbb41cfb99cd5/nvidia_cublas_cu12-12.9.1.4-py3-none-manylinux_2_27_x86_64.whl + name: nvidia-cublas-cu12 + version: 12.9.1.4 + sha256: 453611eb21a7c1f2c2156ed9f3a45b691deda0440ec550860290dc901af5b4c2 + requires_python: '>=3' +- pypi: https://files.pythonhosted.org/packages/c1/2e/b84e32197e33f39907b455b83395a017e697c07a449a2b15fd07fc1c9981/nvidia_cuda_cupti_cu12-12.9.79-py3-none-manylinux_2_25_x86_64.whl + name: nvidia-cuda-cupti-cu12 + version: 12.9.79 + sha256: 096bcf334f13e1984ba36685ad4c1d6347db214de03dbb6eebb237b41d9d934f + requires_python: '>=3' +- pypi: https://files.pythonhosted.org/packages/25/48/b54a06168a2190572a312bfe4ce443687773eb61367ced31e064953dd2f7/nvidia_cuda_nvcc_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl + name: nvidia-cuda-nvcc-cu12 + version: 12.9.86 + sha256: 5d6a0d32fdc7ea39917c20065614ae93add6f577d840233237ff08e9a38f58f0 + requires_python: '>=3' +- pypi: https://files.pythonhosted.org/packages/b8/85/e4af82cc9202023862090bfca4ea827d533329e925c758f0cde964cb54b7/nvidia_cuda_nvrtc_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl + name: nvidia-cuda-nvrtc-cu12 + version: 12.9.86 + sha256: 210cf05005a447e29214e9ce50851e83fc5f4358df8b453155d5e1918094dcb4 + requires_python: '>=3' +- pypi: https://files.pythonhosted.org/packages/bc/46/a92db19b8309581092a3add7e6fceb4c301a3fd233969856a8cbf042cd3c/nvidia_cuda_runtime_cu12-12.9.79-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: nvidia-cuda-runtime-cu12 + version: 12.9.79 + sha256: 25bba2dfb01d48a9b59ca474a1ac43c6ebf7011f1b0b8cc44f54eb6ac48a96c3 + requires_python: '>=3' +- pypi: https://files.pythonhosted.org/packages/de/14/9288024887ba320eb4e51d01cf37aab11d38f774016bcc0dedac0948d0bc/nvidia_cudnn_cu12-9.12.0.46-py3-none-manylinux_2_27_x86_64.whl + name: nvidia-cudnn-cu12 + version: 9.12.0.46 + sha256: 73471a185656232b383693294431882edb14584ee47f41c0abd81556b92ef2ac + requires_dist: + - nvidia-cublas-cu12 + requires_python: '>=3' +- pypi: https://files.pythonhosted.org/packages/95/f4/61e6996dd20481ee834f57a8e9dca28b1869366a135e0d42e2aa8493bdd4/nvidia_cufft_cu12-11.4.1.4-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: nvidia-cufft-cu12 + version: 11.4.1.4 + sha256: c67884f2a7d276b4b80eb56a79322a95df592ae5e765cf1243693365ccab4e28 + requires_dist: + - nvidia-nvjitlink-cu12 + requires_python: '>=3' +- pypi: https://files.pythonhosted.org/packages/33/40/79b0c64d44d6c166c0964ec1d803d067f4a145cca23e23925fd351d0e642/nvidia_cusolver_cu12-11.7.5.82-py3-none-manylinux_2_27_x86_64.whl + name: nvidia-cusolver-cu12 + version: 11.7.5.82 + sha256: 15da72d1340d29b5b3cf3fd100e3cd53421dde36002eda6ed93811af63c40d88 + requires_dist: + - nvidia-cublas-cu12 + - nvidia-nvjitlink-cu12 + - nvidia-cusparse-cu12 + requires_python: '>=3' +- pypi: https://files.pythonhosted.org/packages/12/46/b0fd4b04f86577921feb97d8e2cf028afe04f614d17fb5013de9282c9216/nvidia_cusparse_cu12-12.5.10.65-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: nvidia-cusparse-cu12 + version: 12.5.10.65 + sha256: 73060ce019ac064a057267c585bf1fd5a353734151f87472ff02b2c5c9984e78 + requires_dist: + - nvidia-nvjitlink-cu12 + requires_python: '>=3' +- pypi: https://files.pythonhosted.org/packages/c4/cb/2cf5b8e6a669c90ac6410c3a9d86881308492765b6744de5d0ce75089999/nvidia_nccl_cu12-2.27.7-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: nvidia-nccl-cu12 + version: 2.27.7 + sha256: de5ba5562f08029a19cb1cd659404b18411ed0d6c90ac5f52f30bf99ad5809aa + requires_python: '>=3' +- pypi: https://files.pythonhosted.org/packages/46/0c/c75bbfb967457a0b7670b8ad267bfc4fffdf341c074e0a80db06c24ccfd4/nvidia_nvjitlink_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl + name: nvidia-nvjitlink-cu12 + version: 12.9.86 + sha256: e3f1171dbdc83c5932a45f0f4c99180a70de9bd2718c1ab77d14104f6d7147f9 + requires_python: '>=3' +- pypi: https://files.pythonhosted.org/packages/3b/6c/99acb2f9eb85c29fc6f3a7ac4dccfd992e22666dd08a642b303311326a97/nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: nvidia-nvshmem-cu12 + version: 3.3.20 + sha256: d00f26d3f9b2e3c3065be895e3059d6479ea5c638a3f38c9fec49b1b9dd7c1e5 + requires_python: '>=3' +- conda: https://conda.anaconda.org/conda-forge/linux-64/openblas-0.3.30-pthreads_h6ec200e_1.conda + sha256: 7286d26ba0b3d60bede5c16e4bfd09a71df1752edd29dd1fdb96a2fa1f79ced6 + md5: 611fcf119d77a78439794c43f7667664 + depends: + - libopenblas 0.3.30 pthreads_h94d23a6_1 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 6053748 + timestamp: 1753405247724 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda + sha256: c9f54d4e8212f313be7b02eb962d0cb13a8dae015683a403d3accd4add3e520e + md5: ffffb341206dd0dab0c36053c048d621 + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3128847 + timestamp: 1754465526100 +- pypi: https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl + name: opt-einsum + version: 3.4.0 + sha256: 69bb92469f86a1565195ece4ac0323943e83477171b91d24c35afe028a90d7cd + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/b9/33/f86091c706db1a5459f501830241afff2ecab3532725c188ea57be6e54de/optax-0.2.5-py3-none-any.whl + name: optax + version: 0.2.5 + sha256: 966deae936207f268ac8f564d8ed228d645ac1aaddefbbf194096d2299b24ba8 + requires_dist: + - absl-py>=0.7.1 + - chex>=0.1.87 + - jax>=0.4.27 + - jaxlib>=0.4.27 + - numpy>=1.18.0 + - sphinx>=6.0.0 ; extra == 'docs' + - sphinx-book-theme>=1.0.1 ; extra == 'docs' + - sphinxcontrib-katex ; extra == 'docs' + - sphinx-autodoc-typehints ; extra == 'docs' + - ipython>=8.8.0 ; extra == 'docs' + - myst-nb>=1.0.0 ; extra == 'docs' + - matplotlib>=3.5.0 ; extra == 'docs' + - sphinx-gallery>=0.14.0 ; extra == 'docs' + - sphinx-collections>=0.0.1 ; extra == 'docs' + - flax ; extra == 'docs' + - sphinx-contributors ; extra == 'docs' + - flax>=0.5.3 ; extra == 'test' + - scipy>=1.7.1 ; extra == 'test' + - scikit-learn ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/14/21/9632e0c6f826e2a0e19e1bcb9420fac9970696efc77ca1b37a496666f0ad/orbax_checkpoint-0.11.21-py3-none-any.whl + name: orbax-checkpoint + version: 0.11.21 + sha256: 930454e23a86ee9155c20fc36a699366c65ecd508e21d3d84aec9b5173d461c2 + requires_dist: + - absl-py + - etils[epath,epy] + - typing-extensions + - msgpack + - jax>=0.5.0 + - numpy + - pyyaml + - tensorstore>=0.1.71 + - nest-asyncio + - aiofiles + - protobuf + - humanize + - simplejson>=3.16.0 + - flax ; extra == 'docs' + - google-cloud-logging ; extra == 'docs' + - grain ; extra == 'docs' + - aiofiles ; extra == 'docs' + - tensorflow-datasets ; extra == 'docs' + - opencv-python ; extra == 'docs' + - safetensors ; extra == 'docs' + - google-cloud-logging ; extra == 'testing' + - mock ; extra == 'testing' + - flax ; extra == 'testing' + - pytest ; extra == 'testing' + - pytest-xdist ; extra == 'testing' + - chex ; extra == 'testing' + - aiofiles ; extra == 'testing' + - safetensors ; extra == 'testing' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + name: packaging + version: '25.0' + sha256: 29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/e9/e2/20a317688435470872885e7fc8f95109ae9683dec7c50be29b56911515a5/pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: pandas + version: 2.3.1 + sha256: 2ba6aff74075311fc88504b1db890187a3cd0f887a5b10f5525f8e2ef55bfdb9 + requires_dist: + - numpy>=1.22.4 ; python_full_version < '3.11' + - numpy>=1.23.2 ; python_full_version == '3.11.*' + - numpy>=1.26.0 ; python_full_version >= '3.12' + - python-dateutil>=2.8.2 + - pytz>=2020.1 + - tzdata>=2022.7 + - hypothesis>=6.46.1 ; extra == 'test' + - pytest>=7.3.2 ; extra == 'test' + - pytest-xdist>=2.2.0 ; extra == 'test' + - pyarrow>=10.0.1 ; extra == 'pyarrow' + - bottleneck>=1.3.6 ; extra == 'performance' + - numba>=0.56.4 ; extra == 'performance' + - numexpr>=2.8.4 ; extra == 'performance' + - scipy>=1.10.0 ; extra == 'computation' + - xarray>=2022.12.0 ; extra == 'computation' + - fsspec>=2022.11.0 ; extra == 'fss' + - s3fs>=2022.11.0 ; extra == 'aws' + - gcsfs>=2022.11.0 ; extra == 'gcp' + - pandas-gbq>=0.19.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.0 ; extra == 'excel' + - python-calamine>=0.1.7 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.0.5 ; extra == 'excel' + - pyarrow>=10.0.1 ; extra == 'parquet' + - pyarrow>=10.0.1 ; extra == 'feather' + - tables>=3.8.0 ; extra == 'hdf5' + - pyreadstat>=1.2.0 ; extra == 'spss' + - sqlalchemy>=2.0.0 ; extra == 'postgresql' + - psycopg2>=2.9.6 ; extra == 'postgresql' + - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.0 ; extra == 'mysql' + - pymysql>=1.0.2 ; extra == 'mysql' + - sqlalchemy>=2.0.0 ; extra == 'sql-other' + - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' + - beautifulsoup4>=4.11.2 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'xml' + - matplotlib>=3.6.3 ; extra == 'plot' + - jinja2>=3.1.2 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.3.0 ; extra == 'clipboard' + - zstandard>=0.19.0 ; extra == 'compression' + - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' + - adbc-driver-postgresql>=0.8.0 ; extra == 'all' + - adbc-driver-sqlite>=0.8.0 ; extra == 'all' + - beautifulsoup4>=4.11.2 ; extra == 'all' + - bottleneck>=1.3.6 ; extra == 'all' + - dataframe-api-compat>=0.1.7 ; extra == 'all' + - fastparquet>=2022.12.0 ; extra == 'all' + - fsspec>=2022.11.0 ; extra == 'all' + - gcsfs>=2022.11.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.46.1 ; extra == 'all' + - jinja2>=3.1.2 ; extra == 'all' + - lxml>=4.9.2 ; extra == 'all' + - matplotlib>=3.6.3 ; extra == 'all' + - numba>=0.56.4 ; extra == 'all' + - numexpr>=2.8.4 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.0 ; extra == 'all' + - pandas-gbq>=0.19.0 ; extra == 'all' + - psycopg2>=2.9.6 ; extra == 'all' + - pyarrow>=10.0.1 ; extra == 'all' + - pymysql>=1.0.2 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.0 ; extra == 'all' + - pytest>=7.3.2 ; extra == 'all' + - pytest-xdist>=2.2.0 ; extra == 'all' + - python-calamine>=0.1.7 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.3.0 ; extra == 'all' + - scipy>=1.10.0 ; extra == 'all' + - s3fs>=2022.11.0 ; extra == 'all' + - sqlalchemy>=2.0.0 ; extra == 'all' + - tables>=3.8.0 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2022.12.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.0.5 ; extra == 'all' + - zstandard>=0.19.0 ; extra == 'all' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: pillow + version: 11.3.0 + sha256: 13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8 + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - typing-extensions ; python_full_version < '3.10' and extra == 'typing' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + name: pluggy + version: 1.6.0 + sha256: e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + requires_dist: + - pre-commit ; extra == 'dev' + - tox ; extra == 'dev' + - pytest ; extra == 'testing' + - pytest-benchmark ; extra == 'testing' + - coverage ; extra == 'testing' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl + name: protobuf + version: 6.31.1 + sha256: 4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + name: pygments + version: 2.19.2 + sha256: 86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b + requires_dist: + - colorama>=0.4.6 ; extra == 'windows-terminal' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/92/44/8634af40b0db528b5b37e901c0dc67321354880d251bf8965901d57693a5/PyOpenGL-3.1.9-py3-none-any.whl + name: pyopengl + version: 3.1.9 + sha256: 15995fd3b0deb991376805da36137a4ae5aba6ddbb5e29ac1f35462d130a3f77 +- pypi: https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl + name: pyparsing + version: 3.2.3 + sha256: a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf + requires_dist: + - railroad-diagrams ; extra == 'diagrams' + - jinja2 ; extra == 'diagrams' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl + name: pytest + version: 8.4.1 + sha256: 539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7 + requires_dist: + - colorama>=0.4 ; sys_platform == 'win32' + - exceptiongroup>=1 ; python_full_version < '3.11' + - iniconfig>=1 + - packaging>=20 + - pluggy>=1.5,<2 + - pygments>=2.7.2 + - tomli>=1 ; python_full_version < '3.11' + - argcomplete ; extra == 'dev' + - attrs>=19.2 ; extra == 'dev' + - hypothesis>=3.56 ; extra == 'dev' + - mock ; extra == 'dev' + - requests ; extra == 'dev' + - setuptools ; extra == 'dev' + - xmlschema ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl + name: pytest-cov + version: 6.2.1 + sha256: f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5 + requires_dist: + - pytest>=6.2.5 + - coverage[toml]>=7.5 + - pluggy>=1.2 + - fields ; extra == 'testing' + - hunter ; extra == 'testing' + - process-tests ; extra == 'testing' + - pytest-xdist ; extra == 'testing' + - virtualenv ; extra == 'testing' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl + name: pytest-timeout + version: 2.4.0 + sha256: c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2 + requires_dist: + - pytest>=7.0.0 + requires_python: '>=3.7' +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.5-hec9711d_102_cp313.conda + build_number: 102 + sha256: c2cdcc98ea3cbf78240624e4077e164dc9d5588eefb044b4097c3df54d24d504 + md5: 89e07d92cf50743886f41638d58c4328 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.7.0,<3.0a0 + - libffi >=3.4.6,<3.5.0a0 + - libgcc >=13 + - liblzma >=5.8.1,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.50.1,<4.0a0 + - libuuid >=2.38.1,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.0,<4.0a0 + - python_abi 3.13.* *_cp313 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + license: Python-2.0 + purls: [] + size: 33273132 + timestamp: 1750064035176 + python_site_packages_path: lib/python3.13/site-packages +- pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + name: python-dateutil + version: 2.9.0.post0 + sha256: a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 + requires_dist: + - six>=1.5 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + build_number: 8 + sha256: 210bffe7b121e651419cb196a2a63687b087497595c9be9d20ebe97dd06060a7 + md5: 94305520c52a4aa3f6c2b1ff6008d9f8 + constrains: + - python 3.13.* *_cp313 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 7002 + timestamp: 1752805902938 +- pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl + name: pytz + version: '2025.2' + sha256: 5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00 +- pypi: https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: pyyaml + version: 6.0.2 + sha256: 70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + sha256: 2d6d0c026902561ed77cd646b5021aef2d4db22e57a5b0178dfc669231e06d2c + md5: 283b96675859b20a825f8fa30f311446 + depends: + - libgcc >=13 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 282480 + timestamp: 1740379431762 +- pypi: https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl + name: rich + version: 14.1.0 + sha256: 536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f + requires_dist: + - ipywidgets>=7.5.1,<9 ; extra == 'jupyter' + - markdown-it-py>=2.2.0 + - pygments>=2.13.0,<3.0.0 + requires_python: '>=3.8.0' +- conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.12.9-hbf64f1c_1.conda + noarch: python + sha256: 97523e0f15d0c24f8059734383cb6c5b65b3e3a6a065a845a3af474a038d9862 + md5: b3614b1bb279195baf9dd2ef52d8f273 + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - __glibc >=2.17 + license: MIT + purls: + - pkg:pypi/ruff?source=hash-mapping + size: 10542465 + timestamp: 1755273908360 +- pypi: https://files.pythonhosted.org/packages/e4/82/08e4076df538fb56caa1d489588d880ec7c52d8273a606bb54d660528f7c/scipy-1.16.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: scipy + version: 1.16.1 + sha256: fedc2cbd1baed37474b1924c331b97bdff611d762c196fac1a9b71e67b813b1b + requires_dist: + - numpy>=1.25.2,<2.6 + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-xdist ; extra == 'test' + - asv ; extra == 'test' + - mpmath ; extra == 'test' + - gmpy2 ; extra == 'test' + - threadpoolctl ; extra == 'test' + - scikit-umfpack ; extra == 'test' + - pooch ; extra == 'test' + - hypothesis>=6.30 ; extra == 'test' + - array-api-strict>=2.3.1 ; extra == 'test' + - cython ; extra == 'test' + - meson ; extra == 'test' + - ninja ; sys_platform != 'emscripten' and extra == 'test' + - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - sphinx-design>=0.4.0 ; extra == 'doc' + - matplotlib>=3.5 ; extra == 'doc' + - numpydoc ; extra == 'doc' + - jupytext ; extra == 'doc' + - myst-nb>=1.2.0 ; extra == 'doc' + - pooch ; extra == 'doc' + - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' + - jupyterlite-pyodide-kernel ; extra == 'doc' + - linkify-it-py ; extra == 'doc' + - mypy==1.10.0 ; extra == 'dev' + - typing-extensions ; extra == 'dev' + - types-psutil ; extra == 'dev' + - pycodestyle ; extra == 'dev' + - ruff>=0.0.292 ; extra == 'dev' + - cython-lint>=0.12.2 ; extra == 'dev' + - rich-click ; extra == 'dev' + - doit>=0.36.0 ; extra == 'dev' + - pydevtool ; extra == 'dev' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + name: setuptools + version: 80.9.0 + sha256: 062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 + requires_dist: + - pytest>=6,!=8.1.* ; extra == 'test' + - virtualenv>=13.0.0 ; extra == 'test' + - wheel>=0.44.0 ; extra == 'test' + - pip>=19.1 ; extra == 'test' + - packaging>=24.2 ; extra == 'test' + - jaraco-envs>=2.2 ; extra == 'test' + - pytest-xdist>=3 ; extra == 'test' + - jaraco-path>=3.7.2 ; extra == 'test' + - build[virtualenv]>=1.0.3 ; extra == 'test' + - filelock>=3.4.0 ; extra == 'test' + - ini2toml[lite]>=0.14 ; extra == 'test' + - tomli-w>=1.0.0 ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-perf ; sys_platform != 'cygwin' and extra == 'test' + - jaraco-develop>=7.21 ; python_full_version >= '3.9' and sys_platform != 'cygwin' and extra == 'test' + - pytest-home>=0.5 ; extra == 'test' + - pytest-subprocess ; extra == 'test' + - pyproject-hooks!=1.1 ; extra == 'test' + - jaraco-test>=5.5 ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pygments-github-lexers==0.0.5 ; extra == 'doc' + - sphinx-favicon ; extra == 'doc' + - sphinx-inline-tabs ; extra == 'doc' + - sphinx-reredirects ; extra == 'doc' + - sphinxcontrib-towncrier ; extra == 'doc' + - sphinx-notfound-page>=1,<2 ; extra == 'doc' + - pyproject-hooks!=1.1 ; extra == 'doc' + - towncrier<24.7 ; extra == 'doc' + - packaging>=24.2 ; extra == 'core' + - more-itertools>=8.8 ; extra == 'core' + - jaraco-text>=3.7 ; extra == 'core' + - importlib-metadata>=6 ; python_full_version < '3.10' and extra == 'core' + - tomli>=2.0.1 ; python_full_version < '3.11' and extra == 'core' + - wheel>=0.43.0 ; extra == 'core' + - platformdirs>=4.2.2 ; extra == 'core' + - jaraco-functools>=4 ; extra == 'core' + - more-itertools ; extra == 'core' + - pytest-checkdocs>=2.4 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - ruff>=0.8.0 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=2.2 ; extra == 'enabler' + - pytest-mypy ; extra == 'type' + - mypy==1.14.* ; extra == 'type' + - importlib-metadata>=7.0.2 ; python_full_version < '3.10' and extra == 'type' + - jaraco-develop>=7.21 ; sys_platform != 'cygwin' and extra == 'type' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/19/e3/874b1cca3d3897b486d3afdccc475eb3a09815bf1015b01cf7fcb52a55f0/simplejson-3.20.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: simplejson + version: 3.20.1 + sha256: ab980fcc446ab87ea0879edad41a5c28f2d86020014eb035cf5161e8de4474c6 + requires_python: '>=2.5,!=3.0.*,!=3.1.*,!=3.2.*' +- pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + name: six + version: 1.17.0 + sha256: 4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' +- conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.conda + sha256: 0053c17ffbd9f8af1a7f864995d70121c292e317804120be4667f37c92805426 + md5: 1bad93f0aa428d618875ef3a588a889e + depends: + - __glibc >=2.28 + - kernel-headers_linux-64 4.18.0 he073ed8_8 + - tzdata + license: LGPL-2.0-or-later AND LGPL-2.0-or-later WITH exceptions AND GPL-2.0-or-later + license_family: GPL + purls: [] + size: 24210909 + timestamp: 1752669140965 +- pypi: https://files.pythonhosted.org/packages/1d/9a/9dcc01c8f87047b09602ea16379233b8a308d1d83d5432bf8bc89163ca3e/tensorstore-0.1.76-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: tensorstore + version: 0.1.76 + sha256: 20782f833bfa3c59dd3787f657388054c54ee0ab48dad181b360e3e5e81e4c4b + requires_dist: + - numpy>=1.22.0 + - ml-dtypes>=0.5.0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/4f/bd/de8d508070629b6d84a30d01d57e4a65c69aa7f5abe7560b8fad3b50ea59/termcolor-3.1.0-py3-none-any.whl + name: termcolor + version: 3.1.0 + sha256: 591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa + requires_dist: + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda + sha256: a84ff687119e6d8752346d1d408d5cf360dee0badd487a472aa8ddedfdc219e1 + md5: a0116df4f4ed05c303811a837d5b39d8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libzlib >=1.3.1,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3285204 + timestamp: 1748387766691 +- pypi: https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl + name: toolz + version: 1.0.0 + sha256: 292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/43/2b/36e984399089c026a6499ac8f7401d38487cf0183839a4aa78140d373771/treescope-0.1.10-py3-none-any.whl + name: treescope + version: 0.1.10 + sha256: dde52f5314f4c29d22157a6fe4d3bd103f9cae02791c9e672eefa32c9aa1da51 + requires_dist: + - numpy>=1.25.2 + - pylint>=2.6.0 ; extra == 'dev' + - pyink>=24.3.0 ; extra == 'dev' + - ipython ; extra == 'dev' + - jupyter ; extra == 'dev' + - pytest>=8.2.2 ; extra == 'dev' + - pytype ; extra == 'dev' + - ipython ; extra == 'docs' + - sphinx>=6.0.0,<7.3.0 ; extra == 'docs' + - sphinx-book-theme>=1.0.1 ; extra == 'docs' + - sphinxcontrib-katex ; extra == 'docs' + - ipython>=8.8.0 ; extra == 'docs' + - jax[cpu]>=0.4.23 ; extra == 'docs' + - myst-nb>=1.0.0 ; extra == 'docs' + - myst-parser>=3.0.1 ; extra == 'docs' + - matplotlib>=3.5.0 ; extra == 'docs' + - packaging==24.1 ; extra == 'docs' + - palettable==3.3.3 ; extra == 'docs' + - pandas==2.2.2 ; extra == 'docs' + - plotly==5.22.0 ; extra == 'docs' + - penzai~=0.2.4 ; extra == 'docs' + - sphinx-contributors ; extra == 'docs' + - sphinx-hoverxref ; extra == 'docs' + - torch==2.3.1 ; extra == 'docs' + - ipython ; extra == 'notebook' + - palettable ; extra == 'notebook' + - jax>=0.4.23 ; extra == 'notebook' + - absl-py>=1.4.0 ; extra == 'test' + - jax>=0.4.23 ; extra == 'test' + - pytest>=8.2.2 ; extra == 'test' + - torch>=2.0.0 ; extra == 'test' + - pydantic>=2.0.0 ; extra == 'test' + - omegaconf>=2.0.0 ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/3e/79/9563bd90b2b964e85424b6e41e58c39d812d31f62198140b87c823e19ed9/trimesh-4.7.3-py3-none-any.whl + name: trimesh + version: 4.7.3 + sha256: 4091a4ecf08dc141ac554f552e19168fce872ab51fa4512bf73a06bbe2360640 + requires_dist: + - numpy>=1.20 + - colorlog ; extra == 'easy' + - manifold3d>=2.3.0 ; extra == 'easy' + - charset-normalizer ; extra == 'easy' + - lxml ; extra == 'easy' + - jsonschema ; extra == 'easy' + - networkx ; extra == 'easy' + - svg-path ; extra == 'easy' + - pycollada<=0.9.0 ; python_full_version < '3.9' and extra == 'easy' + - pycollada ; python_full_version >= '3.9' and extra == 'easy' + - shapely ; extra == 'easy' + - xxhash ; extra == 'easy' + - rtree ; extra == 'easy' + - httpx ; extra == 'easy' + - scipy ; extra == 'easy' + - embreex ; platform_machine == 'x86_64' and extra == 'easy' + - pillow ; extra == 'easy' + - vhacdx ; python_full_version >= '3.9' and extra == 'easy' + - mapbox-earcut>=1.0.2 ; python_full_version >= '3.9' and extra == 'easy' + - sympy ; extra == 'recommend' + - meshio ; extra == 'recommend' + - pyglet<2 ; extra == 'recommend' + - psutil ; extra == 'recommend' + - scikit-image ; extra == 'recommend' + - fast-simplification ; extra == 'recommend' + - python-fcl ; extra == 'recommend' + - cascadio ; extra == 'recommend' + - pytest-cov ; extra == 'test' + - pytest ; extra == 'test' + - pyinstrument ; extra == 'test' + - ruff ; extra == 'test' + - coveralls ; extra == 'test-more' + - pyright ; extra == 'test-more' + - ezdxf ; extra == 'test-more' + - xatlas ; extra == 'test-more' + - pytest-beartype ; python_full_version >= '3.10' and extra == 'test-more' + - matplotlib ; extra == 'test-more' + - pymeshlab ; extra == 'test-more' + - triangle ; extra == 'test-more' + - ipython ; extra == 'test-more' + - marimo ; extra == 'test-more' + - openctm ; extra == 'deprecated' + - trimesh[deprecated,easy,recommend,test,test-more] ; extra == 'all' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl + name: typing-extensions + version: 4.14.1 + sha256: d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl + name: tzdata + version: '2025.2' + sha256: 1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 + requires_python: '>=2' +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + sha256: 5aaa366385d716557e365f0a4e9c3fca43ba196872abbbe3d56bb610d131e192 + md5: 4222072737ccff51314b5ece9c7d6f5a + license: LicenseRef-Public-Domain + purls: [] + size: 122968 + timestamp: 1742727099393 +- pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl + name: zipp + version: 3.23.0 + sha256: 071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e + requires_dist: + - pytest>=6,!=8.1.* ; extra == 'test' + - jaraco-itertools ; extra == 'test' + - jaraco-functools ; extra == 'test' + - more-itertools ; extra == 'test' + - big-o ; extra == 'test' + - pytest-ignore-flaky ; extra == 'test' + - jaraco-test ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pytest-checkdocs>=2.4 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=2.2 ; extra == 'enabler' + - pytest-mypy ; extra == 'type' + requires_python: '>=3.9' diff --git a/pyproject.toml b/pyproject.toml index 36f762b..65a5d1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,23 +18,24 @@ classifiers = [ ] dependencies = [ - "jax>=0.5.0,!=0.5.3,!=0.6.0", # 0.5.3 and 0.6.0 have a bug that causes resets to not work properly. This is related to the fusing of SimData.mjx_model into the _reset function. + "jax>=0.7.0", # 0.5.3 and 0.6.0 have a bug that causes resets to not work properly. "mujoco>=3.3.0", "mujoco-mjx>=3.3.0", - "gymnasium", + "gymnasium[mujoco]>=1.2.0", "imageio", "einops", "flax", "ml_collections", "casadi", "numpy", + # "drone-models @ git+https://github.com/utiasDSL/drone-models.git@08ffb18", ] requires-python = ">=3.10" [project.optional-dependencies] -test = ["pytest>=8.0.0", "pytest-cov", "pytest-timeout"] +test = ["pytest>=8.0.0", "pytest-cov", "pytest-timeout", "gymnasium[array-api]"] gpu = ["jax[cuda12]"] -benchmark = ["fire"] +benchmark = ["fire", "matplotlib", "pandas"] [project.urls] Homepage = "https://github.com/utiasDSL/crazyflow" @@ -80,7 +81,7 @@ target-version = "py312" [tool.ruff.lint] select = ["E4", "E7", "E9", "F", "I", "D", "TCH", "ANN"] -ignore = ["ANN401", "ANN101"] +ignore = ["ANN401"] fixable = ["ALL"] unfixable = [] @@ -106,3 +107,26 @@ skip-magic-trailing-comma = true line-ending = "auto" docstring-code-format = true docstring-code-line-length = "dynamic" + +[tool.pixi.workspace] +channels = ["conda-forge"] +platforms = ["linux-64"] + +[tool.pixi.pypi-dependencies] +crazyflow = { path = "./", editable = true } + +[tool.pixi.environments] +default = { solve-group = "default" } +benchmark = { features = ["benchmark", "gpu"], solve-group = "default" } +gpu = { features = ["gpu"], solve-group = "default" } +test = { features = ["test", "gpu"], solve-group = "default" } + +[tool.pixi.tasks] + +[tool.pixi.dependencies] +# TODO: Remove compilers once scipy 1.17 is released and drone-models have been updated. +gcc = ">=15.1.0,<15.2" +cxx-compiler = ">=1.0.0,<2" +gfortran = ">=15.1.0,<15.2" +openblas = ">=0.3.30,<0.4" +ruff = "*" \ No newline at end of file diff --git a/tests/integration/test_disturbance.py b/tests/integration/test_disturbance.py index 822cd6f..7840597 100644 --- a/tests/integration/test_disturbance.py +++ b/tests/integration/test_disturbance.py @@ -20,9 +20,10 @@ def test_disturbance(physics: Physics): sim = Sim(n_worlds=2, n_drones=3, control="state", physics=physics) control = np.zeros((sim.n_worlds, sim.n_drones, 13)) control[..., :3] = 1.0 + n_steps = 10 pos, pos_disturbed = [], [] - for _ in range(sim.control_freq): + for _ in range(n_steps): sim.state_control(control) sim.step(sim.freq // sim.control_freq) pos.append(sim.data.states.pos[0, 0]) @@ -30,7 +31,7 @@ def test_disturbance(physics: Physics): sim.reset() sim.step_pipeline = sim.step_pipeline[:2] + (disturbance_fn,) + sim.step_pipeline[2:] sim.build_step_fn() - for _ in range(sim.control_freq): + for _ in range(n_steps): sim.state_control(control) sim.step(sim.freq // sim.control_freq) pos_disturbed.append(sim.data.states.pos[0, 0]) diff --git a/tests/integration/test_gymnasium_envs.py b/tests/integration/test_envs.py similarity index 59% rename from tests/integration/test_gymnasium_envs.py rename to tests/integration/test_envs.py index c451c58..505ff58 100644 --- a/tests/integration/test_gymnasium_envs.py +++ b/tests/integration/test_envs.py @@ -10,20 +10,25 @@ def test_gymnasium_reset(): """Test reset behavior of the DroneReachPos-v0 environment.""" SEED = 42 - envs = gymnasium.make_vec("DroneReachPos-v0", num_envs=1, freq=50, time_horizon_in_seconds=2) + envs = gymnasium.make_vec( + "DroneReachPos-v0", + num_envs=1, + freq=50, + max_episode_time=2, + pos_min=np.array([-1.0, 1.0, 1.0]), + pos_max=np.array([-1.0, 1.0, 1.0]), + vel_min=0.0, + vel_max=0.0, + ) envs = JaxToNumpy(envs) obs, _ = envs.reset( seed=SEED, options={ - "pos_min": np.array([-1.0, 1.0, 1.0]), - "pos_max": np.array([-1.0, 1.0, 1.0]), - "vel_min": 0.0, - "vel_max": 0.0, "goal_pos_min": np.array([-1.0, 1.0, 1.0]), "goal_pos_max": np.array([-1.0, 1.0, 1.0]), }, ) assert np.all(obs["pos"] == np.array([[-1.0, 1.0, 1.0]])) - assert np.all(obs["difference_to_goal"] == np.array([[.0, .0, .0]])) - assert np.all(obs["vel"] == np.array([[0.0, 0.0, 0.0]])) \ No newline at end of file + assert np.all(obs["difference_to_goal"] == np.array([[0.0, 0.0, 0.0]])) + assert np.all(obs["vel"] == np.array([[0.0, 0.0, 0.0]])) diff --git a/tests/integration/test_randomize.py b/tests/integration/test_randomize.py index c8cb20f..36a7f4c 100644 --- a/tests/integration/test_randomize.py +++ b/tests/integration/test_randomize.py @@ -8,12 +8,8 @@ @pytest.mark.integration -@pytest.mark.parametrize("physics", [Physics.analytical, Physics.mujoco]) -def test_randomize_mass(physics: Physics): - if physics == Physics.mujoco: # TODO: Add mujoco when implemented - pytest.skip("MuJoCo randomization not implemented yet") - - sim = Sim(n_worlds=2, n_drones=4, control=Control.state, physics=physics) +def test_randomize_mass(): + sim = Sim(n_worlds=2, n_drones=4, control=Control.state, physics=Physics.analytical) add_on_mass = np.random.uniform(-0.005, 0.005, size=(sim.n_worlds, sim.n_drones)) masses = np.ones((sim.n_worlds, sim.n_drones)) * 0.025 @@ -35,12 +31,8 @@ def test_randomize_mass(physics: Physics): @pytest.mark.integration -@pytest.mark.parametrize("physics", [Physics.analytical, Physics.mujoco]) -def test_randomize_inertia(physics: Physics): - if physics == Physics.mujoco: # TODO: Add mujoco when implemented - pytest.skip("MuJoCo randomization not implemented yet") - - sim = Sim(n_worlds=2, n_drones=4, control=Control.state, physics=physics) +def test_randomize_inertia(): + sim = Sim(n_worlds=2, n_drones=4, control=Control.state, physics=Physics.analytical) add_on_j = np.random.uniform(-1.5e-5, 1.5e-5, size=(sim.n_worlds, sim.n_drones, 3, 3)) randomized_j = J + add_on_j diff --git a/tests/unit/test_sim.py b/tests/unit/test_sim.py index 88762d2..1ee94a1 100644 --- a/tests/unit/test_sim.py +++ b/tests/unit/test_sim.py @@ -1,3 +1,5 @@ +import os + import jax import jax.numpy as jnp import numpy as np @@ -7,6 +9,7 @@ from crazyflow.control import Control from crazyflow.exception import ConfigError from crazyflow.sim import Physics, Sim +from crazyflow.sim.sim import sync_sim2mjx def available_backends() -> list[str]: @@ -27,6 +30,11 @@ def skip_unavailable_device(device: str): pytest.skip(f"{device} device not available") +def skip_headless(): + if os.environ.get("DISPLAY") is None: + pytest.skip("DISPLAY is not set, skipping test in headless environment") + + def array_meta_assert( x: Array, shape: tuple[int, ...] | None = None, @@ -269,6 +277,7 @@ def test_render_human(device: str): @pytest.mark.parametrize("device", ["gpu", "cpu"]) def test_render_rgb_array(device: str): skip_unavailable_device(device) + skip_headless() sim = Sim(n_worlds=2, device=device) img = sim.render(mode="rgb_array", width=1024, height=1024) assert isinstance(img, np.ndarray), "Image must be a numpy array" @@ -285,20 +294,19 @@ def test_device(device: str): sim = Sim(n_worlds=2, physics=Physics.sys_id, device=device) sim.step() assert sim.data.states.pos.device == jax.devices(device)[0] - assert sim.data.mjx_data.qpos.device == jax.devices(device)[0] @pytest.mark.unit @pytest.mark.parametrize("device", ["gpu", "cpu"]) @pytest.mark.parametrize("n_worlds", [1, 2]) @pytest.mark.parametrize("n_drones", [1, 3]) -def test_shape_consistency(device: str, n_drones: int, n_worlds: int): +def test_sync_shape_consistency(device: str, n_drones: int, n_worlds: int): skip_unavailable_device(device) sim = Sim(n_worlds=n_worlds, n_drones=n_drones, physics=Physics.sys_id, device=device) - qpos_shape, qvel_shape = sim.data.mjx_data.qpos.shape, sim.data.mjx_data.qvel.shape - sim.step() - assert sim.data.mjx_data.qpos.shape == qpos_shape, "step() should not change qpos shape" - assert sim.data.mjx_data.qvel.shape == qvel_shape, "step() should not change qvel shape" + qpos_shape, qvel_shape = sim.mjx_data.qpos.shape, sim.mjx_data.qvel.shape + _, mjx_data = sync_sim2mjx(sim.data, sim.mjx_data, sim.mjx_model) + assert mjx_data.qpos.shape == qpos_shape, "sync_sim2mjx() should not change qpos shape" + assert mjx_data.qvel.shape == qvel_shape, "sync_sim2mjx() should not change qvel shape" @pytest.mark.unit @@ -357,7 +365,7 @@ def test_seed_reset(): @pytest.mark.unit @pytest.mark.parametrize("physics", [Physics.analytical, Physics.sys_id]) def test_floor_penetration(physics: Physics): - """Test that drones cannot penetrate the floor (z < 0). + """Test that drones cannot penetrate the floor (z < 0.01). We don't test for mujoco, as mujoco uses collisions by default and will let the drone bounce on the floor. @@ -367,8 +375,6 @@ def test_floor_penetration(physics: Physics): # Command to fall: zero thrust and attitude that points downward attitude_cmd = np.zeros((1, 1, 4)) # [roll, pitch, yaw, thrust] attitude_cmd[..., 0] = 0.0 # Zero thrust to fall - attitude_cmd[..., 1] = 0.5 # Roll to destabilize - attitude_cmd[..., 3] = 0.5 # Pitch to destabilize sim.attitude_control(attitude_cmd) # Run simulation for short duration to let drone fall for _ in range(5): # 0.1 seconds at 500Hz @@ -380,3 +386,24 @@ def test_floor_penetration(physics: Physics): final_z_pos = sim.data.states.pos[..., 2] assert jnp.all(final_z_pos == -0.001), f"Drone should be on floor but z={final_z_pos}" sim.close() + + +@pytest.mark.unit +@pytest.mark.parametrize("physics", [Physics.sys_id, Physics.analytical]) +def test_contacts(physics: Physics): + sim = Sim(physics=physics, control=Control.attitude, freq=500, device="cpu") + sim.reset() + sim.step(10) # Make sure the drone is on the ground + contacts = sim.contacts() + assert jnp.all(contacts), "Drone should be in contact with the floor" + sim.close() + + +@pytest.mark.unit +@pytest.mark.parametrize("physics", [Physics.sys_id, Physics.analytical]) +def test_recompilation(physics: Physics): + sim = Sim(physics=physics, control=Control.attitude, freq=500, device="cpu") + # Make sure we don't recompile the step function after the first call + sim.step(1) + sim.step(1) + assert sim._step._cache_size() == 1, "Step function should not be recompiled"