diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9ec21f3..5aec5cd 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -37,7 +37,7 @@ jobs: locked: true - name: Build documentation - run: pixi run -e docs mkdocs build + run: pixi run -e docs docs-build - name: Setup Pages uses: actions/configure-pages@v3 diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 43abeee..11c8941 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -8,24 +8,40 @@ jobs: - uses: actions/checkout@v4 - name: Install submodules - run: | - git submodule update --init --recursive + run: git submodule update --init --recursive - - name: Setup Pixi (installs pixi + caches envs) # https://github.com/marketplace/actions/setup-pixi - uses: prefix-dev/setup-pixi@v0.9.3 # pin the action version + - name: Setup Pixi + uses: prefix-dev/setup-pixi@v0.9.3 with: - pixi-version: v0.67.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 'tests' environment(s) are installed + pixi-version: v0.67.0 + cache: true + cache-write: ${{ github.event_name == 'push' && github.ref_name == 'main' }} environments: tests - # 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 tests tests \ No newline at end of file + pixi run -e tests tests + + test-docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install submodules + run: git submodule update --init --recursive + + - name: Setup Pixi + uses: prefix-dev/setup-pixi@v0.9.3 + with: + pixi-version: v0.67.0 + cache: true + cache-write: ${{ github.event_name == 'push' && github.ref_name == 'main' }} + environments: tests + activate-environment: false + locked: true + + - name: Run doc tests + run: pixi run -e tests test-docs \ No newline at end of file diff --git a/.gitignore b/.gitignore index d3e7cca..20a9481 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,7 @@ benchmark/data .pixi *.egg-info -# MkDocs build output +# ProperDocs build output site/ # Temporary files .DS_Store diff --git a/README.md b/README.md index 00fa266..cfe8e33 100644 --- a/README.md +++ b/README.md @@ -1,158 +1,94 @@ ![Crazyflow Logo](https://github.com/learnsyslab/crazyflow/raw/main/docs/img/logo.png) - -------------------------------------------------------------------------------- +
-Fast, parallelizable simulations of drones with JAX. - -[![Python Version]][Python Version URL] [![Ruff Check]][Ruff Check URL] [![Documentation Status]][Documentation Status URL] [![Tests]][Tests URL] - -[Python Version]: https://img.shields.io/badge/python-3.11+-blue.svg -[Python Version URL]: https://www.python.org - -[Ruff Check]: https://github.com/learnsyslab/crazyflow/actions/workflows/ruff.yml/badge.svg?style=flat-square -[Ruff Check URL]: https://github.com/learnsyslab/crazyflow/actions/workflows/ruff.yml + **Fast, parallelizable simulations of Quadrotor drones with JAX.** -[Documentation Status]: https://github.com/learnsyslab/crazyflow/actions/workflows/docs.yml/badge.svg -[Documentation Status URL]: https://learnsyslab.github.io/crazyflow + [![Python](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org) + [![Tests](https://github.com/learnsyslab/crazyflow/actions/workflows/testing.yml/badge.svg)](https://github.com/learnsyslab/crazyflow/actions/workflows/testing.yml) + [![Ruff](https://github.com/learnsyslab/crazyflow/actions/workflows/ruff.yml/badge.svg)](https://github.com/learnsyslab/crazyflow/actions/workflows/ruff.yml) + [![Docs](https://github.com/learnsyslab/crazyflow/actions/workflows/docs.yml/badge.svg)](https://learnsyslab.github.io/crazyflow) -[Tests]: https://github.com/learnsyslab/crazyflow/actions/workflows/testing.yml/badge.svg -[Tests URL]: https://github.com/learnsyslab/crazyflow/actions/workflows/testing.yml +
-## Quick Start -For a more detailed guide, check out our [documentation](https://learnsyslab.github.io/crazyflow/). +Crazyflow is a research simulator for quadrotors. It runs batched, differentiable simulations on CPU and GPU via JAX, with analytical and abstracted models for the Crazyflie 2.x family. -### Normal installation -The regular way to use Crazyflow is to install it from PyPI with your favourite package manager, e.g., with pip: -``` bash -pip install crazyflow -``` - -### Developer installation -If you plan to develop with and around Crazyflow, you can use the existing [pixi](https://pixi.sh/) environment. -``` bash -git clone --recurse-submodules git@github.com:learnsyslab/crazyflow.git -cd crazyflow -pixi shell -``` +```python +import numpy as np +from crazyflow.sim import Sim +from crazyflow.control import Control -This will install Crazyflow, drone-models, and drone-controllers in editable mode for easy development. +sim = Sim(n_worlds=4096, n_drones=1, control=Control.state) +cmd = np.zeros((4096, 1, 13)) +cmd[..., 2] = 0.5 # hover at 0.5 m across all worlds -In case you want to use another package manager or install the simulator with it's subpackages into another project, simply install all packages manually in your environment: -``` bash -pip install -e . # Installing Crazyflow -pip install -e ./submodules/drone-models -pip install -e ./submodules/drone-controllers +for _ in range(100): + sim.state_control(cmd) + sim.step(sim.freq // sim.control_freq) + sim.render() ``` +## Documentation -## Architecture - -Crazyflow is a high-performance simulation framework for Crazyflie drones that leverages JAX for efficient parallelization and automatic differentiation. The architecture is designed around a flexible pipeline that can be configured at initialization time, enabling users to swap out physics backends, control methods, and integration schemes. - -### Core Components - -#### Simulation Pipeline -The simulation is built as a pipeline of functions that are composed at initialization time based on the configuration. This approach avoids runtime branching and allows JAX to optimize the entire pipeline as a single computation. Users can insert their own pure functions into the pipeline to modify the simulation behavior while maintaining compatibility with JAX's optimizations. - -#### Physics Backends -Multiple physics models are supported: -- first_principles: A first-principles model based on physical equations -- so_rpy: A system-identified model trained on real drone data -- so_rpy_rotor: An enhanced system-identified model that includes thrust dynamics -- so_rpy_rotor_drag: A system-identified model that includes thrust dynamics and drag effects - -#### Control Modes -Different control interfaces are available: -- state: High-level control of position, velocity, and yaw -- attitude: Mid-level control of collective thrust and orientation -- thrust: Low-level control of individual motor thrusts - -#### Integration Methods -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 +[learnsyslab.github.io/crazyflow](https://learnsyslab.github.io/crazyflow) — installation, user guide, examples, and API reference. -### Parallelization -Crazyflow supports massive parallelization across: -- Worlds: Independent simulation environments that can run in parallel -- Drones: Multiple drones within each world -- Devices: Computations can be executed on CPU or GPU -This parallelization is achieved through JAX's vectorization capabilities, allowing thousands of simulations to run simultaneously with minimal overhead. +## Features -### Domain Randomization -The framework supports domain randomization through the crazyflow/randomize module, allowing parameters like mass to be varied across simulations to improve sim-to-real transfer. +- **n\_worlds x n\_drones** — batched over independent environments and multi-drone swarms simultaneously +- **GPU-accelerated** — up to 914 M steps/s on an RTX 4090 (first-principles physics, 262 K worlds) +- **Differentiable** — `jax.grad` works through the full dynamics and control pipeline +- **First-principles models** — physics model using first-principles equations and parameters identified from real-world measurements +- **Abstracted models** — three physics models fitted from real Crazyflie flight data +- **Modular pipelines** — step and reset are tuples of plain JAX functions; insert anything, anywhere +- **MuJoCo integration** — onscreen and offscreen rendering, raycasting, and contact detection via MJX -### 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. +## Installation -### 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. +```bash +pip install crazyflow # CPU +pip install "crazyflow[gpu]" # GPU (Linux x86-64, CUDA 12) +``` -### Visualization -We use `gymnasium`'s MuJoCo renderer and synchronize the simulation data with MuJoCo to either render an interactive UI or RGB arrays. +Developer install with editable submodules ([pixi](https://pixi.sh/) required): -## Examples -The repository includes several example scripts demonstrating different capabilities: -| Example | Description | -| ----------------------------------------- | ----------------------------------------------------------- | -| [`hover.py`](examples/hover.py) | Basic hovering using state control | -| [`thrust.py`](examples/thrust.py) | Direct motor control using thrust commands | -| [`render.py`](examples/render.py) | Visualization of multiple drones with motion traces | -| [`contacts.py`](examples/contacts.py) | Collision detection between drones | -| [`gradient.py`](examples/gradient.py) | Computing gradients through the simulation for optimization | -| [`change_pos.py`](examples/change_pos.py) | Manipulating drone positions programmatically | +```bash +git clone --recurse-submodules git@github.com:learnsyslab/crazyflow.git +cd crazyflow +pixi shell +``` ## Performance -These benchmarks give you a rough idea of the performance you should expect from the simulator. Gym benchmarks use gym environments with a step frequency of 50Hz while simulating at 500Hz. The simulation benchmarks use 500Hz. - -![Crazyflow Performance](docs/img/performance.png) - -The chart above shows the performance of Crazyflow on different hardware. The simulator can achieve close to 100 million steps per second on a GPU with 1 million parallel environments. The gym environment, which includes additional overhead for the Gymnasium interface and is not fully optimized, still achieves decent performance with over 98,000 steps per second on GPU with 10,000 parallel environments. - -Performance benchmarks were run on: -- CPU: Intel Core i9-13900KF -- GPU: NVIDIA RTX 4090 -To reproduce the benchmark results, rerun the `benchmark/main.py` script. +First-principles physics, one drone. CPU: AMD Ryzen 9 7950X. GPU: NVIDIA RTX 4090. -## Known Issues -- `"RuntimeError: MUJOCO_PATH environment variable is not set"` upon installing this package. This error can be resolved by using `venv` instead of `conda`. Somtimes the `mujoco` install can [fail with `conda`](https://github.com/google-deepmind/mujoco/issues/1004). -- If using `zsh` don't forget to escape brackets when installing additional dependencies: `pip install .\[gpu\]`. +| n\_worlds | CPU steps/s | GPU steps/s | +|---|---|---| +| 64 | 3.3 M | 1.2 M | +| 1 024 | 9.2 M | 18.7 M | +| 16 384 | 11.9 M | 257 M | +| 65 536 | 15.6 M | 678 M | +| 262 144 | 12.6 M | 914 M | -### Using the project with VSCode devcontainers +Full benchmarks including multi-drone scaling are in the [documentation](https://learnsyslab.github.io/crazyflow). -**Running on CPU**: by default the containers run on CPU. You don't need to take any action. +## Related packages -**Running on GPU**: The devcontsainers can easily run using your computer's NVIDIA GPU on Linux and Windows. This makes sense if you want to accelerate simulation by running thousands of simulation in parallel. In order to work you need to install the [CUDA Toolkit](https://developer.nvidia.com/cuda-downloads?target_os=Linux&target_arch=x86_64&Distribution=WSL-Ubuntu&target_version=2.0&target_type=deb_local), [NVIDIA Container runtime](https://developer.nvidia.com/container-runtime) for your computer. Finally, enable GPU access to the devcontainers by setting the commented out `"--gpus=all"` and `"--runtime=nvidia"` flags in `devcontainer.json`. +Crazyflow is built on two companion packages that can also be used independently: +| Package | Description | +|---|---| +| [drone-models](https://github.com/learnsyslab/drone-models) | Drone dynamics models (first-principles and fitted) compatible with NumPy, JAX, and PyTorch. Used by Crazyflow as the physics backend. | +| [drone-controllers](https://github.com/learnsyslab/drone-controllers) | Reference controller implementations including the Mellinger geometric controller. Used by Crazyflow to provide the state and attitude control modes. | -**Linux** -1. Make sure to be in a X11 session ([link](https://askubuntu.com/questions/1410256/how-do-i-use-the-x-window-manager-instead-of-wayland-on-ubuntu-22-04)), otherwise rendering of the drone will fail. -2. Install [Docker](https://docs.docker.com/engine/install/) (, and make sure Docker Daemon is running) -3. Install [VSCode](https://code.visualstudio.com/), with [devcontainer extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers), and [remote dev pack](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker). -4. Clone this project's code. Rename `/.devcontainer/devcontainer.linux.json` to `/.devcontainer/devcontainer.json`. -5. Open this project in VSCode. VSCode should automatically detect the devcontainer and prompt you to `Reopen in container`. If not, see [here](https://code.visualstudio.com/docs/devcontainers/containers#_quick-start-open-an-existing-folder-in-a-container) to open manually. Note: Opening the container for the first time might take a while (up to 15 min), as the container is pulled from the web and build. +Both are installed automatically as dependencies. For development, they are included as submodules in `submodules/` and installed in editable mode by the pixi environment. -**Windows** (requires Windows 10 or later) - -For windows, we require WSL2 to run the devcontainer. (So its actually Linux with extra steps.) Full instructions can be found [in the official docs](https://code.visualstudio.com/blogs/2020/07/01/containers-wsl#_getting-started). Here are the important steps: -1. Install [Docker](https://docs.docker.com/desktop/setup/install/windows-install/), and WSL2, and Ubuntu 22.04 LTS (, and make sure Docker Daemon is running) -2. Docker will recognize that you have WSL installed and prompt you via Windows Notifications to enable WSL integration -> confirm this with `Enable WSL integration`. If not, open `Docker Desktop`, navigate to the settings, and manually enable WSL integration. (There are TWO setting options for this. Make sure to enable BOTH!) -3. Install [VSCode](https://code.visualstudio.com/), with the [WSL extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl), [devcontainer extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers), and [remote dev pack](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker). -4. Clone the source code for the exercises in the WSL2 file system to `/home` (`~`), or wherever you like. (Performance when working on the WSL file system is much better compared to Windows file system). You can access the WSL filesystem either by starting a WSL/Ubuntu console, or via the Windows File Explorer at `\\wsl.localhost\Ubuntu\home` (replace `Ubuntu` with your distro, if necessary). -7. Rename `/.devcontainer/devcontainer.windows.json` to `/.devcontainer/devcontainer.json`. -8. Open this project in VSCode. The easiest way to do so is by starting a WSL/Ubuntu shell, navigating via `cd` to the source code, then type `code .` to open VSCode. VSCode should automatically detect the devcontainer and prompt you to `Reopen in container`. If not, see [here](https://code.visualstudio.com/docs/devcontainers/containers#_quick-start-open-an-existing-folder-in-a-container) to open manually. Note: Opening the container for the first time might take a while (up to 15 min), as the container is pulled from the web and build. - - -**MacOS** - -Unfortunately, we did not get the devcontainer to work with MacOS yet, even after following [those](https://gist.github.com/sorny/969fe55d85c9b0035b0109a31cbcb088) steps. We expect that the issue is related to Mujoco rendering from inside the Docker container and display forwarding with X11. There is also an [unresolved Issue](https://github.com/google-deepmind/mujoco/issues/1047) on GitHub. If you manage to make it work, please let us know. - -Until then, MacOS users are required to install this project using an python environment manager such as [conda](https://docs.anaconda.com/anaconda/install/) or [mamba](https://mamba.readthedocs.io/en/latest/). If you use conda, these are the required commands: ```conda create --name crazyflow -c conda-forge python=3.11```, ```conda activate crazyflow```, ```conda install pip```, ```pip install -e .```. - -____________ - -Known Issues: - - if building docker container fails at `RUN apt-get update`, make sure your host systems time is set correct: https://askubuntu.com/questions/1511514/docker-build-fails-at-run-apt-update-error-failed-to-solve-process-bin-sh +## Citation +```bibtex +@misc{schuck2025crazyflow, + author = {Schuck, Martin and Rath, Marcel P. and Hua, Yufei and Goudar, Abhishek and Zhou, SiQi and Schoellig, Angela P.}, + title = {Crazyflow: An Accurate, GPU-Accelerated Differentiable Drone Simulator in JAX}, + year = {2026}, + note = {Preprint} +} +``` diff --git a/docs/api/index.md b/docs/api/index.md index 22d55f3..63b6068 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -1,5 +1,22 @@ -# Python API +# API Reference -This section is generated with mkdocstrings (Python handler). It documents the public API of the crazyflow package. +This section is auto-generated from the Crazyflow source code using [mkdocstrings](https://mkdocstrings.github.io/). -::: crazyflow \ No newline at end of file +## Module overview + +| Module | Description | +|---|---| +| `crazyflow.sim` | Core `Sim` class and physics pipeline | +| `crazyflow.sim.data` | `SimData`, `SimState`, `SimControls`, `SimParams`, `SimCore` pytrees | +| `crazyflow.sim.functional` | Pure functional control API for use inside `jax.jit` | +| `crazyflow.sim.physics` | `Physics` enum and physics model implementations | +| `crazyflow.sim.integration` | `Integrator` enum, Euler, RK4, and symplectic Euler | +| `crazyflow.sim.sensors` | Raycasting and sensor extraction utilities | +| `crazyflow.sim.symbolic` | CasADi symbolic model API | +| `crazyflow.control` | `Control` enum | +| `crazyflow.control.mellinger` | Mellinger controller data and parameters | +| `crazyflow.envs` | Gymnasium vectorized environments | +| `crazyflow.randomize` | Domain randomization helpers | +| `crazyflow.utils` | Grid utilities and pytree helpers | + +Navigate the full generated reference using the sidebar. diff --git a/docs/cite.md b/docs/cite.md new file mode 100644 index 0000000..e57b129 --- /dev/null +++ b/docs/cite.md @@ -0,0 +1,16 @@ +# Cite + +If you use Crazyflow in academic work, please cite the accompanying paper: + +BibTeX: + +```bibtex +@misc{schuck2025crazyflow, + author = {Schuck, Martin and Rath, Marcel P. and Hua, Yufei and Goudar, Abhishek and Zhou, SiQi and Schoellig, Angela P.}, + title = {Crazyflow: An Accurate, GPU-Accelerated Differentiable Drone Simulator in JAX}, + year = {2026}, + note = {Preprint} +} +``` + +The paper covers motivation, architecture, benchmarks against related simulators, and sim-to-real validation results. A DOI and journal/conference venue will be added here once the work is published. diff --git a/docs/examples.md b/docs/examples.md deleted file mode 100644 index 3aebb1a..0000000 --- a/docs/examples.md +++ /dev/null @@ -1,92 +0,0 @@ -# Examples - -Selected example scripts. Check out more in `examples/` - ---- - -### State control - -A minimal demo showing how to use the Sim API at a very high level: create a Sim, send simple position commands and step the simulation. Useful to get started with high-level position/setpoint control. - -```bash -python examples/change_pos.py -``` - ---- - -### Cameras & RGBD - -Shows how to obtain RGB and depth frames from the renderer. Demonstrates offscreen capture via sim.render(), retrieving image arrays for perception or visual‑in‑the‑loop controllers, and saving sequences (GIFs). - -```bash -python examples/cameras.py -``` - -

-RGB Image -                -Depth Image -

-![cameras](img/examples/cameras.gif) - ---- - -### Contacts & collision model - -Demonstrates how to query contact information from the simulation and how to switch collision geometry types (e.g., sphere → box) for higher‑fidelity contact modeling. Useful for debugging collisions or when more accurate contact checks are required. - -```bash -python examples/contacts.py -``` - -![contact_sphere](img/examples/contact_sphere.png){width=200 height=200} -![contact_box](img/examples/contact_box.png){width=200 height=200} - ---- - -### LED deck & materials - -Illustrates how to activate and control the LED deck and other drone material colors at runtime for visualization and debugging. - -```bash -python examples/led_deck.py -``` - -![led_deck](img/examples/led_decks.png) - ---- - -### Randomization - -Shows how to add reset‑time randomization: initial position/quaternion randomization, mass/inertia perturbations and other properties that should vary between episodes. The example demonstrates how to register reset randomizers and how to persist different initial conditions across runs. - -Run: -```bash -python examples/randomize.py -``` - ---- - -### Disturbance - -Demonstrates step‑time disturbances inserted into the step pipeline (external forces/torques, actuator noise, etc.). The example (examples/disturbance.py) shows how to insert a disturbance function into sim.step_pipeline, compare disturbed vs undisturbed runs and optionally plot the resulting trajectories. - -Run: -```bash -python examples/disturbance.py -``` - ---- - -### Figure‑8 / RL environment - -A scripted figure‑8 environment intended for evaluation or as a training target. The example shows how to create vectorized envs, apply the NormalizeActions wrapper and step/render the environment. It does not include any agent implementations — integrate the environment with your preferred RL training code (Stable Baselines3, RLlib, custom JAX trainers, etc.). - -Run: -```bash -python examples/figure8.py -``` - ---- - -For API details and configuration options referenced by these examples, see the [API Reference](api/index.md) and [Usage](usage.md). \ No newline at end of file diff --git a/docs/examples/index.md b/docs/examples/index.md new file mode 100644 index 0000000..daad37a --- /dev/null +++ b/docs/examples/index.md @@ -0,0 +1,136 @@ +# Examples + +These examples build on each other — each one introduces one new concept on top of the previous. Start from the top if you're new, or jump to whichever section covers what you need. + +--- + +## Hover + +A single drone commanded to hold a fixed height using state control. This is the minimal end-to-end loop: create a `Sim`, reset it, apply a state command, and step forward. + +```{ .python notest } +--8<-- "examples/hover.py" +``` + +```bash +python examples/hover.py +``` + +--- + +## Attitude control + +Commanding roll, pitch, yaw, and collective thrust directly. This level bypasses the Mellinger position loop and is typical for RL agents that output attitude targets. + +```{ .python notest } +--8<-- "examples/attitude.py" +``` + +--- + +## Gradient descent through dynamics + +Because the simulator is built entirely from JAX operations, `jax.grad` can differentiate through it. Starting the drone above the target height keeps it away from the floor, so the floor-clipping stage never fires and gradients flow freely through the entire trajectory. + +```{ .python notest } +--8<-- "examples/gradient.py" +``` + +--- + +## Domain randomization + +Varying physical parameters per world at reset. Each world gets a slightly different mass, so identical commands produce diverging trajectories. + +```{ .python notest } +--8<-- "examples/randomize.py" +``` + +--- + +## Disturbance injection + +Inserting a random external force and torque into the step pipeline. The disturbance fires on every physics tick, so the drone fights wind-like perturbations. + +```{ .python notest } +--8<-- "examples/disturbance.py" +``` + +--- + +## Cameras and RGBD + +Offscreen rendering returns RGB and depth images on every frame. The FPV camera (`fpv_cam`) is attached to the drone and moves with it. + +
+ RGB and depth camera outputs from a Crazyflow drone simulation +
+ +```{ .python notest } +--8<-- "examples/cameras.py" +``` + +```bash +python examples/cameras.py +``` + +--- + +## LED deck and materials + +`change_material` updates the RGBA colour and emission of any named material on any subset of drones at runtime. + +
+ Crazyflow drones with runtime-controlled LED deck materials +
+ +```{ .python notest } +--8<-- "examples/led_deck.py" +``` + +```bash +python examples/led_deck.py +``` + +--- + +## Contact queries + +The default collision geometry is a sphere around the drone frame. `use_box_collision` replaces it with a tighter oriented box, useful for narrow-gap flight and accurate contact debugging. + +
+
+ Contact query visualization using the default sphere collision geometry +
+
+ Contact query visualization using the oriented box collision geometry +
+
+ +```{ .python notest } +--8<-- "examples/contacts.py" +``` + +--- + +## Raycasting and depth sensing + +`render_depth` fires rays from a camera and returns per-pixel distances. This is faster than full RGB rendering and useful for obstacle sensing or depth-based controllers. + +```{ .python notest } +--8<-- "examples/raycasting.py" +``` + +```bash +python examples/raycasting.py +``` + +--- + +## Gymnasium environment + +Evaluating a random policy in the figure-8 environment. The env wraps `Sim` behind the standard Gymnasium `VectorEnv` interface. + +```{ .python notest } +--8<-- "examples/figure8.py" +``` diff --git a/docs/features.md b/docs/features.md deleted file mode 100644 index ef3d4ff..0000000 --- a/docs/features.md +++ /dev/null @@ -1,34 +0,0 @@ -# Features & Architecture - -Crazyflow is a research‑first simulator for small quadrotors. Its design favours clarity, composability and performance: a compact Sim API composes a physics model, a numerical integrator and a controller layer. Around that core sits an extensible "step pipeline" where randomization, disturbances, logging or custom hooks can be inserted without changing the simulator internals. - -### Architecture -The goals are reproducibility, throughput and research flexibility. First‑principles (analytic, identified) and simplified, data‑driven models are supported alongside a symbolic model API for controller development and analysis. The codebase is implemented to take advantage of JAX and MuJoCo: batched GPU execution, JIT compilation and automatic differentiation are available, and analytical gradients of the simulation are exposed where useful. The step pipeline makes it simple to compose randomization, disturbances and custom hooks without touching the core simulator. - -### Models - -Crazyflow supports two complementary model classes: - -- First‑principles models — physics‑based analytical models with identified parameters intended for high‑fidelity simulation and sim‑to‑real work. -- Simplified, data‑driven models — lightweight models fitted from flight data to capture off‑nominal effects and to speed up learning/control experiments. These data‑driven models can be obtained from just a few minutes of flight data; the repository provides fitting scripts and a minimal identification pipeline (works quickly if a stable controller is available). - -All models (first‑principles and data‑driven) are also exposed as symbolic model objects so they can be reused directly in model‑based controllers, MPC formulations, or analysis tools. - -### Controllers -The repository includes reference controller implementations and integration points for common research workflows: - -- Geometric controllers (Mellinger style) for standard tracking. -- Interfaces for MPC / MPCC workflows (note: MPC example controllers and advanced control code are available in the repo for our [drone racing course](https://github.com/learnsyslab/lsy_drone_racing/tree/main/lsy_drone_racing/control)). -- Reinforcement learning: we provide environments suitable for training and deploying RL agents and include example setups for PPO and SHAC agents. - -### Performance & evaluation -Assuming free‑space flight (avoiding generic contact solving when possible) lets Crazyflow prioritise speed and simplicity. The combination of JAX, MuJoCo and a modular pipeline enables high‑throughput experiments and large‑scale benchmarking. - -### Extensibility -Researchers can add new dynamics, controllers or pipeline stages without modifying the simulator core. Swap integrators, change physics models, or inject disturbances via small, well‑scoped functions. - -### Publication -See our upcoming paper for full motivation, benchmarks and evaluation: -Schuck, M. & Rath, M. (2025). Crazyflow: Fast, parallelizable simulations of Crazyflies with JAX and MuJoCo. (preprint — link TBD) - -For API details and configuration options, see the API reference: [API Reference](api/index.md) \ No newline at end of file diff --git a/docs/gen_ref_pages.py b/docs/gen_ref_pages.py new file mode 100644 index 0000000..58de12f --- /dev/null +++ b/docs/gen_ref_pages.py @@ -0,0 +1,68 @@ +"""Generate the code reference pages and navigation. + +This script is executed by the mkdocs-gen-files plugin during ``mkdocs build`` or +``mkdocs serve``. It is not meant to be run directly or imported outside of that context. +Install the docs environment (``pixi shell -e docs``) to use it. +""" + +from pathlib import Path + +try: + import mkdocs_gen_files +except ImportError: + pass # not running in a docs environment — nothing to generate +else: + SKIP_PARTS = {"_typing", "__main__", "__pycache__"} + + for path in sorted(Path("crazyflow").rglob("*.py")): + module_path = path.relative_to(".").with_suffix("") + doc_path = path.relative_to(".").with_suffix(".md") + full_doc_path = Path("api", doc_path) + + parts = tuple(module_path.parts) + + if any(part in SKIP_PARTS for part in parts): + continue + + if parts[-1] == "__init__": + parts = parts[:-1] + doc_path = doc_path.with_name("index.md") + full_doc_path = full_doc_path.with_name("index.md") + elif parts[-1] == "__main__": + continue + + with mkdocs_gen_files.open(full_doc_path, "w") as fd: + ident = ".".join(parts) + fd.write(f"::: {ident}\n") + + mkdocs_gen_files.set_edit_path(full_doc_path, path) + + summary = """\ +* [Overview](index.md) +* [crazyflow](crazyflow/index.md) +* Sim + * [sim](crazyflow/sim/index.md) + * [sim.data](crazyflow/sim/data.md) + * [sim.functional](crazyflow/sim/functional.md) + * [sim.physics](crazyflow/sim/physics.md) + * [sim.integration](crazyflow/sim/integration.md) + * [sim.sensors](crazyflow/sim/sensors.md) + * [sim.symbolic](crazyflow/sim/symbolic.md) + * [sim.visualize](crazyflow/sim/visualize.md) +* Control + * [control](crazyflow/control/index.md) + * [control.mellinger](crazyflow/control/mellinger.md) +* Environments + * [envs](crazyflow/envs/index.md) + * [envs.drone_env](crazyflow/envs/drone_env.md) + * [envs.figure_8_env](crazyflow/envs/figure_8_env.md) + * [envs.landing_env](crazyflow/envs/landing_env.md) + * [envs.reach_pos_env](crazyflow/envs/reach_pos_env.md) + * [envs.reach_vel_env](crazyflow/envs/reach_vel_env.md) + * [envs.norm_actions_wrapper](crazyflow/envs/norm_actions_wrapper.md) +* [randomize](crazyflow/randomize/index.md) +* [utils](crazyflow/utils.md) +""" + + with mkdocs_gen_files.open("api/SUMMARY.md", "w") as nav_file: + nav_file.write(summary) diff --git a/docs/get-started/index.md b/docs/get-started/index.md new file mode 100644 index 0000000..b40ff59 --- /dev/null +++ b/docs/get-started/index.md @@ -0,0 +1,6 @@ +# Get Started + +Install Crazyflow and run your first simulation in minutes. + +- [Installation](installation.md) — pip, pixi, GPU, and from-source options +- [Quick Start](quick-start.md) — step-by-step walkthrough of the object-oriented API diff --git a/docs/get-started/installation.md b/docs/get-started/installation.md new file mode 100644 index 0000000..ffac14e --- /dev/null +++ b/docs/get-started/installation.md @@ -0,0 +1,59 @@ +# Installation + +Select your installation method from the tabs below, then read the notes under each section for what it includes. + +=== "pip" + + ```bash + pip install crazyflow + ``` + +=== "pip + GPU" + + ```bash + pip install "crazyflow[gpu]" + ``` + +=== "pixi" + + ```bash + git clone --recurse-submodules git@github.com:learnsyslab/crazyflow.git + cd crazyflow + pixi shell + ``` + +=== "pixi + tests" + + ```bash + git clone --recurse-submodules git@github.com:learnsyslab/crazyflow.git + cd crazyflow + pixi shell -e tests + ``` + +--- + +## GPU support + +JAX defaults to CPU-only execution. The `gpu` extra swaps in `jax[cuda12]`, enabling GPU execution. Setting `device="gpu"` in the `Sim` constructor then routes all computation through CUDA. + +!!! note + GPU support is only available on Linux x86-64. + +## Developer install + +[Pixi](https://pixi.sh/) creates a fully reproducible environment. This variant installs `crazyflow`, `drone_models`, and `drone_controllers` in editable mode from the `submodules/` folder. Any source change takes effect immediately without reinstalling. Recommended for contributors and researchers who modify the simulator. + +## Testing + +Adds `pytest` and `pytest-markdown-docs` for running the test suite and doc snippet tests. + +```bash +pixi run tests # unit and integration tests +pixi run test-docs # doc code snippet tests +``` + +## Verify the installation + +```bash +python -c "from crazyflow.sim import Sim; sim = Sim(); sim.reset(); print('OK')" +``` diff --git a/docs/get-started/quick-start.md b/docs/get-started/quick-start.md new file mode 100644 index 0000000..26e9de5 --- /dev/null +++ b/docs/get-started/quick-start.md @@ -0,0 +1,140 @@ +# Quick Start + +This page walks through a complete minimal workflow: create a simulator, send a position command, step it forward, and read back the drone's state. + +## Create a simulator + +`Sim` is the top-level object. All configuration is provided at construction time: physics model, control mode, simulation frequency, number of parallel worlds, and number of drones per world. + +```python +from crazyflow.sim import Sim + +sim = Sim(n_worlds=1, n_drones=1, freq=500) +sim.reset() +``` + +`reset()` initialises all worlds to the default state: the drone is at the origin, upright, with zero velocity. + +## State and command + +The default control mode is `Control.state`. A state command is a 13-element vector that sets the desired position, velocity, acceleration, yaw, and body angular rates. + +| Index | Variable | Units | +|---|---|---| +| 0–2 | Position \(x, y, z\) | m | +| 3–5 | Velocity \(\dot{x}, \dot{y}, \dot{z}\) | m/s | +| 6–8 | Acceleration \(\ddot{x}, \ddot{y}, \ddot{z}\) | m/s² | +| 9 | Yaw | rad | +| 10 | Roll rate | rad/s | +| 11 | Pitch rate | rad/s | +| 12 | Yaw rate | rad/s | + +The command array has shape `(n_worlds, n_drones, 13)`. + +```python +import numpy as np +from crazyflow.sim import Sim +from crazyflow.control import Control + +sim = Sim(n_worlds=1, n_drones=1, freq=500, control=Control.state) +sim.reset() + +cmd = np.zeros((1, 1, 13), dtype=np.float32) +cmd[0, 0, 2] = 0.5 # target height: 0.5 m +``` + +## Step the simulation + +`state_control` stages the command. `step` advances the simulation by the given number of physics steps. Calling `sim.step(sim.freq // sim.control_freq)` advances exactly one control cycle. + +```python +import numpy as np +from crazyflow.sim import Sim +from crazyflow.control import Control + +sim = Sim(n_worlds=1, n_drones=1, freq=500, control=Control.state) +sim.reset() + +cmd = np.zeros((1, 1, 13), dtype=np.float32) +cmd[0, 0, 2] = 0.5 + +for _ in range(10): + sim.state_control(cmd) + sim.step(sim.freq // sim.control_freq) +``` + +## Read back state + +All simulation state lives in `sim.data.states`. Arrays are indexed as `[world, drone, :]`. + +```python +import numpy as np +from crazyflow.sim import Sim +from crazyflow.control import Control + +sim = Sim(n_worlds=1, n_drones=1, freq=500, control=Control.state) +sim.reset() + +cmd = np.zeros((1, 1, 13), dtype=np.float32) +cmd[0, 0, 2] = 0.5 + +for _ in range(10): + sim.state_control(cmd) + sim.step(sim.freq // sim.control_freq) + +pos = sim.data.states.pos[0, 0] # (3,) — position in metres +quat = sim.data.states.quat[0, 0] # (4,) — quaternion xyzw +vel = sim.data.states.vel[0, 0] # (3,) — linear velocity m/s +ang_vel = sim.data.states.ang_vel[0, 0] # (3,) — angular velocity rad/s +``` + +## Simulate multiple worlds + +Increase `n_worlds` to run independent simulations in a single batched call. All state arrays gain a leading world dimension. + +```python +import numpy as np +from crazyflow.sim import Sim +from crazyflow.control import Control + +sim = Sim(n_worlds=4, n_drones=1, freq=500, control=Control.state) +sim.reset() + +cmd = np.zeros((4, 1, 13), dtype=np.float32) +cmd[:, 0, 2] = np.array([0.2, 0.4, 0.6, 0.8]) # different target heights per world + +for _ in range(10): + sim.state_control(cmd) + sim.step(sim.freq // sim.control_freq) + +pos = sim.data.states.pos[:, 0, :] # (4, 3) — position of drone 0 in each world +``` + +## Simulate multiple drones + +Increase `n_drones` to place multiple drones inside a single world. Each drone has its own independent state; all receive commands from the same `(n_worlds, n_drones, 13)` array. + +```python +import numpy as np +from crazyflow.sim import Sim +from crazyflow.control import Control + +sim = Sim(n_worlds=1, n_drones=4, freq=500, control=Control.state) +sim.reset() + +cmd = np.zeros((1, 4, 13), dtype=np.float32) +cmd[0, :, 2] = np.array([0.2, 0.4, 0.6, 0.8]) # different height per drone + +for _ in range(10): + sim.state_control(cmd) + sim.step(sim.freq // sim.control_freq) + +pos = sim.data.states.pos[0, :, :] # (4, 3) — all 4 drones in world 0 +``` + +## Next steps + +- [Object-Oriented API](../user-guide/oo-api.md) — all control modes, rendering, and reset +- [Functional API](../user-guide/functional-api.md) — purely functional interface for use inside JAX transformations +- [Physics Models](../user-guide/physics-models.md) — choosing between first-principles and fitted models +- [Examples](../examples/index.md) — runnable scripts diff --git a/docs/index.md b/docs/index.md index 83531f9..956f9e4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,35 +1,242 @@ # Crazyflow
- Crazyflow Logo + Crazyflow Logo
-## Overview +**Fast, parallelizable simulations of Crazyflie drones with JAX.** -Crazyflow is a high-performance research simulator for Crazyflie‑style small quadrotors. -Built on JAX and MuJoCo, it supports batched GPU execution, differentiable dynamics, and accurate, identified models — designed for reproducible experiments at scale. +Crazyflow is a research simulator for Crazyflie-style quadrotors that runs millions of independent environments in parallel on CPU or GPU. It is built on JAX, exposes a differentiable dynamics pipeline, and ships identified models for the Crazyflie 2.x family. -Audience: researchers working on control, learning, system identification, sim2real, multi-agent RL and swarm control for quadrotors. +--- -## Highlights +## Showcase -- Modular simulation stack (physics, integrator, controller) -- GPU-ready, batched execution for massive parallelism -- Differentiable / autodiff-enabled dynamics -- Support for analytical (identified) and data-driven models -- Onboard-controller support and symbolic model matching -- Extensible step pipeline (randomization, disturbances, custom hooks) -- MuJoCo-based visualization and offscreen rendering + + + +--- + +## Supported drones + + + + +All models come from the [drone-models](https://learnsyslab.github.io/drone-models/) library. Available configurations: `cf2x_L250`, `cf2x_P250`, `cf2x_T350`, `cf21B_500`, and any model returned by `drone_models.available_drones()`. + +--- + +## Performance + + + +Throughput for one drone across parallel worlds, first-principles physics. CPU: AMD Ryzen 9 7950X. GPU: NVIDIA RTX 4090. + +```vegalite +{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "width": "container", + "height": 300, + "config": {"view": {"stroke": "transparent"}}, + "data": { + "values": [ + {"nw":1,"dev":"CPU","sps":403294},{"nw":4,"dev":"CPU","sps":884432}, + {"nw":16,"dev":"CPU","sps":1202200},{"nw":64,"dev":"CPU","sps":3309800}, + {"nw":256,"dev":"CPU","sps":6656300},{"nw":1024,"dev":"CPU","sps":9214400}, + {"nw":4096,"dev":"CPU","sps":8865400},{"nw":16384,"dev":"CPU","sps":11898000}, + {"nw":65536,"dev":"CPU","sps":15609000},{"nw":262144,"dev":"CPU","sps":12569000}, + {"nw":1048576,"dev":"CPU","sps":8554100}, + {"nw":1,"dev":"GPU","sps":21494},{"nw":4,"dev":"GPU","sps":70557}, + {"nw":16,"dev":"GPU","sps":253727},{"nw":64,"dev":"GPU","sps":1168000}, + {"nw":256,"dev":"GPU","sps":4095700},{"nw":1024,"dev":"GPU","sps":18697000}, + {"nw":4096,"dev":"GPU","sps":65107000},{"nw":16384,"dev":"GPU","sps":257190000}, + {"nw":65536,"dev":"GPU","sps":678220000},{"nw":262144,"dev":"GPU","sps":913980000}, + {"nw":1048576,"dev":"GPU","sps":699520000} + ] + }, + "mark": {"type": "line", "point": {"filled": true, "size": 40}}, + "encoding": { + "x": { + "field": "nw", "type": "quantitative", + "scale": {"type": "log", "base": 2}, + "axis": { + "title": "n_worlds", + "tickCount": 6, + "gridOpacity": 0.3, + "labelExpr": "'2^' + round(log(datum.value)/log(2))" + } + }, + "y": { + "field": "sps", "type": "quantitative", + "scale": {"type": "log"}, + "axis": {"title": "Steps / second", "tickCount": 5, "gridOpacity": 0.3, "format": ".2s"} + }, + "color": { + "field": "dev", "type": "nominal", + "scale": {"domain": ["CPU","GPU"], "range": ["#2196F3","#4CAF50"]}, + "legend": {"title": null} + } + } +} +``` + +GPU throughput across `n_worlds` and `n_drones` (RTX 4090). Empty cells exceed available GPU memory. Color encodes steps per second on a log scale. + +```vegalite +{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "width": "container", + "height": 300, + "config": {"view": {"stroke": "transparent"}}, + "data": { + "values": [ + {"nw":"2^0","nd":"2^0","sps":21494},{"nw":"2^2","nd":"2^0","sps":70557},{"nw":"2^4","nd":"2^0","sps":253727},{"nw":"2^6","nd":"2^0","sps":1168000},{"nw":"2^8","nd":"2^0","sps":4095700},{"nw":"2^10","nd":"2^0","sps":18697000},{"nw":"2^12","nd":"2^0","sps":65107000},{"nw":"2^14","nd":"2^0","sps":257190000},{"nw":"2^16","nd":"2^0","sps":678220000},{"nw":"2^18","nd":"2^0","sps":913980000},{"nw":"2^20","nd":"2^0","sps":699520000}, + {"nw":"2^0","nd":"2^2","sps":18279},{"nw":"2^2","nd":"2^2","sps":72522},{"nw":"2^4","nd":"2^2","sps":290679},{"nw":"2^6","nd":"2^2","sps":1161400},{"nw":"2^8","nd":"2^2","sps":4609300},{"nw":"2^10","nd":"2^2","sps":16042000},{"nw":"2^12","nd":"2^2","sps":51070000},{"nw":"2^14","nd":"2^2","sps":155390000},{"nw":"2^16","nd":"2^2","sps":184350000},{"nw":"2^18","nd":"2^2","sps":140930000},{"nw":"2^20","nd":"2^2","sps":97411000}, + {"nw":"2^0","nd":"2^4","sps":16060},{"nw":"2^2","nd":"2^4","sps":72711},{"nw":"2^4","nd":"2^4","sps":290997},{"nw":"2^6","nd":"2^4","sps":1161900},{"nw":"2^8","nd":"2^4","sps":4016500},{"nw":"2^10","nd":"2^4","sps":12750000},{"nw":"2^12","nd":"2^4","sps":39799000},{"nw":"2^14","nd":"2^4","sps":46391000},{"nw":"2^16","nd":"2^4","sps":35329000},{"nw":"2^18","nd":"2^4","sps":24479000}, + {"nw":"2^0","nd":"2^6","sps":18249},{"nw":"2^2","nd":"2^6","sps":72484},{"nw":"2^4","nd":"2^6","sps":290029},{"nw":"2^6","nd":"2^6","sps":1009200},{"nw":"2^8","nd":"2^6","sps":3205100},{"nw":"2^10","nd":"2^6","sps":9851600},{"nw":"2^12","nd":"2^6","sps":11530000},{"nw":"2^14","nd":"2^6","sps":8824300},{"nw":"2^16","nd":"2^6","sps":6112800}, + {"nw":"2^0","nd":"2^8","sps":18193},{"nw":"2^2","nd":"2^8","sps":72629},{"nw":"2^4","nd":"2^8","sps":253004},{"nw":"2^6","nd":"2^8","sps":798453},{"nw":"2^8","nd":"2^8","sps":2472200},{"nw":"2^10","nd":"2^8","sps":2874800},{"nw":"2^12","nd":"2^8","sps":2204800},{"nw":"2^14","nd":"2^8","sps":1536800}, + {"nw":"2^0","nd":"2^10","sps":18268},{"nw":"2^2","nd":"2^10","sps":63107},{"nw":"2^4","nd":"2^10","sps":200804},{"nw":"2^6","nd":"2^10","sps":609339},{"nw":"2^8","nd":"2^10","sps":718658},{"nw":"2^10","nd":"2^10","sps":553150},{"nw":"2^12","nd":"2^10","sps":381016}, + {"nw":"2^0","nd":"2^12","sps":15873},{"nw":"2^2","nd":"2^12","sps":49898},{"nw":"2^4","nd":"2^12","sps":154616},{"nw":"2^6","nd":"2^12","sps":180297},{"nw":"2^8","nd":"2^12","sps":137266},{"nw":"2^10","nd":"2^12","sps":95417} + ] + }, + "mark": "rect", + "encoding": { + "x": { + "field": "nw", "type": "ordinal", + "sort": ["2^0","2^2","2^4","2^6","2^8","2^10","2^12","2^14","2^16","2^18","2^20"], + "axis": {"title": "n_worlds", "labelAngle": 0, "grid": false} + }, + "y": { + "field": "nd", "type": "ordinal", + "sort": ["2^12","2^10","2^8","2^6","2^4","2^2","2^0"], + "axis": {"title": "n_drones", "grid": false} + }, + "color": { + "field": "sps", "type": "quantitative", + "scale": {"type": "log", "range": ["#00ffff", "#7f00ff", "#ff00ff"]}, + "legend": {"title": "Steps / s", "format": ".2s", "gradientLength": 280} + } + } +} +``` + +*Numbers are illustrative placeholders and will be replaced with measured benchmarks before release.* + +--- + +## Why Crazyflow + +Most simulators offer either vectorized environments for RL training or multi-drone swarm simulation — rarely both, and rarely with accurate onboard flight dynamics for every agent. Crazyflow is built around both simultaneously. The entire simulator is structured around an `n_worlds × n_drones` batch dimension: `n_worlds` gives you massively parallel independent environments, and `n_drones` gives you full swarm simulation inside each one, each drone running its own accurate, identified flight model and control stack. Scaling to millions of parallel instances requires no code changes. + +Simulating the full Crazyflie firmware stack with GPU acceleration and differentiability is not possible with existing tools, so Crazyflow reimplements the entire dynamics and control stack in JAX. This gives accelerated, fully batchable simulation that runs on CPU and GPU without modification. Differentiability comes as a direct consequence: `jax.grad` works through physics, control, and integration without any manual gradient derivations, enabling gradient-based policy optimization, system identification, and sensitivity analysis out of the box. + +To make research possible rather than just evaluation, the simulator is designed to be fully open to modification. The step and reset pipelines are plain tuples of JAX functions. There are no fixed hooks or plugin interfaces — you splice in your own dynamics, disturbances, randomization, or reward shaping at any point, and the JIT compiler fuses everything into a single kernel. + +For perception and collision, Crazyflow integrates MuJoCo and MJX. GUI rendering uses the MuJoCo viewer directly. Depth sensing, raycasting, and contact detection run through MJX, which keeps them batchable over worlds and compatible with JAX transformations. + +## Quick install + +```bash +pip install crazyflow +``` + +See [Installation](get-started/installation.md) for GPU, developer, and from-source options. + +## Minimal example + +```python +import numpy as np +from crazyflow.sim import Sim +from crazyflow.control import Control + +sim = Sim(n_worlds=1, n_drones=1, control=Control.state) +sim.reset() + +# State command: [x, y, z, vx, vy, vz, ax, ay, az, yaw, roll_rate, pitch_rate, yaw_rate] +cmd = np.zeros((1, 1, 13), dtype=np.float32) +cmd[0, 0, 2] = 0.5 # hover at 0.5 m + +sim.state_control(cmd) +sim.step(sim.freq // sim.control_freq) -1. Installation — follow the install instructions: [Installation Guide](installation.md) -2. Run an example — see runnable demos and thumbnails: [Examples](examples.md) -3. API & reference — full Python API generated with mkdocstrings: [API Reference](api/index.md) +pos = sim.data.states.pos[0, 0] # shape (3,) — position of world 0, drone 0 +``` -## Want to learn more? +## Where to go next -- Read about features and architecture: [Features & Architecture](features.md) -- Try the simple quickstart: [Getting Started / Usage](usage.md) \ No newline at end of file +- [Quick Start](get-started/quick-start.md) — step-by-step walkthrough of the object-oriented API +- [Functional API](user-guide/functional-api.md) — JIT compilation, autodiff, and `jax.lax.scan` rollouts +- [Examples](examples/index.md) — runnable scripts covering hover, gradients, batched simulation, and more +- [API Reference](api/index.md) — full Python API diff --git a/docs/installation.md b/docs/installation.md deleted file mode 100644 index 49ffe84..0000000 --- a/docs/installation.md +++ /dev/null @@ -1,95 +0,0 @@ -# Installation - -## Requirements -The simulator is extensively tested on Ubuntu 24.04. However, other platforms should also work out of the box with the PyPI install. Further, you need an environment with: - -- Python >= 3.11, < 3.14 -- MuJoCo (follow the mujoco installation instructions for your platform) -- (Optional) pixi >= 0.6.1 (required for the provided developer environment) - -## Quick install (PyPI) -Recommended for users who only want to use Crazyflow: - -```bash -pip install crazyflow -``` - -## Developer install (recommended for contributors) -Use [pixi](https://pixi.sh/) to create a reproducible development environment that also installs submodules in editable mode. This requires some 64 bit linux distribution to work. - -1. Clone the repo (with submodules) -```bash -git clone --recurse-submodules git@github.com:learnsyslab/crazyflow.git -cd crazyflow -``` - -2. Enter the [pixi](https://pixi.sh/) shell (pixi >= 0.6.1 required) -```bash -pixi shell -``` - -Inside the pixi shell you will have crazyflow, drone-models and drone-controllers installed in editable mode. - -## Install from source (manual / without pixi) -If you prefer not to use pixi, install the packages manually in editable mode: - -```bash -pip install -e . # Install crazyflow -pip install -e ./submodules/drone-models -pip install -e ./submodules/drone-controllers -``` - -## Optional extras (GPU / benchmarking) -- GPU (JAX with CUDA): three ways to enable the gpu extras - - Start the [pixi](https://pixi.sh/) gpu environment: - ```bash - pixi shell -e gpu - ``` - - Install local editable package with gpu extras: - ```bash - pip install -e ".[gpu]" - ``` - - Install from PyPI with gpu extras: - ```bash - pip install "crazyflow[gpu]" - ``` - -- Benchmarking / plotting: - - Use the dedicated pixi benchmarking environment: - ```bash - pixi shell -e benchmark - ``` - - Or install the benchmark extras locally: - ```bash - pip install -e ".[benchmark]" - ``` - - Or install from PyPI with extras: - ```bash - pip install "crazyflow[benchmark]" - ``` - -## Verify installation -A quick smoke test — run one of the examples (offscreen or with rendering enabled depending on your platform): -```bash -python examples/cameras.py -``` - -Run the test suite (recommended via [pixi](https://pixi.sh/)): -```bash -pixi run tests -``` -Or activate the test environment and run pytest directly: -```bash -pixi shell -e tests -pytest -v tests -``` - -See the [Examples](examples.md) page for more runnable scripts. - -## Building and serving the docs locally -Recommended: use [pixi](https://pixi.sh/) to run the configured docs tasks: - -```bash -pixi run docs-build # build the static site -pixi run docs-serve # serve with live reload -``` \ No newline at end of file diff --git a/docs/javascripts/mathjax.js b/docs/javascripts/mathjax.js new file mode 100644 index 0000000..ee567d6 --- /dev/null +++ b/docs/javascripts/mathjax.js @@ -0,0 +1,12 @@ +window.MathJax = { + tex: { + inlineMath: [["\\(", "\\)"]], + displayMath: [["\\[", "\\]"]], + processEscapes: true, + processEnvironments: true, + }, + options: { + ignoreHtmlClass: ".*|", + processHtmlClass: "arithmatex", + }, +}; diff --git a/docs/javascripts/showcase.js b/docs/javascripts/showcase.js new file mode 100644 index 0000000..91cab3f --- /dev/null +++ b/docs/javascripts/showcase.js @@ -0,0 +1,125 @@ +function initializeCarousel() { + const carouselContainer = document.querySelector(".carousel-container"); + + if (!carouselContainer || carouselContainer.dataset.carouselReady === "true") { + return; + } + + carouselContainer.dataset.carouselReady = "true"; + + const carousel = carouselContainer.querySelector(".carousel"); + const slides = Array.from(carouselContainer.querySelectorAll(".carousel-slide")); + const prevBtn = carouselContainer.querySelector(".carousel-btn-prev"); + const nextBtn = carouselContainer.querySelector(".carousel-btn-next"); + const indicatorsContainer = carouselContainer.querySelector(".carousel-indicators"); + + if (!carousel || slides.length === 0 || !indicatorsContainer) { + return; + } + + let currentSlide = 0; + + slides.forEach((_, index) => { + const indicator = document.createElement("button"); + indicator.type = "button"; + indicator.classList.add("carousel-indicator"); + indicator.setAttribute("aria-label", `Go to slide ${index + 1}`); + indicator.addEventListener("click", () => goToSlide(index)); + indicatorsContainer.appendChild(indicator); + }); + + const indicators = Array.from(indicatorsContainer.querySelectorAll(".carousel-indicator")); + + function goToSlide(index) { + slides.forEach((slide) => { + slide.classList.remove("active", "prev", "next"); + }); + + currentSlide = (index + slides.length) % slides.length; + + const prevIndex = (currentSlide - 1 + slides.length) % slides.length; + const nextIndex = (currentSlide + 1) % slides.length; + + slides[currentSlide].classList.add("active"); + slides[prevIndex].classList.add("prev"); + slides[nextIndex].classList.add("next"); + + indicators.forEach((indicator, indicatorIndex) => { + indicator.classList.toggle("active", indicatorIndex === currentSlide); + indicator.setAttribute("aria-current", indicatorIndex === currentSlide ? "true" : "false"); + }); + } + + function nextSlide() { + goToSlide(currentSlide + 1); + } + + function prevSlide() { + goToSlide(currentSlide - 1); + } + + if (prevBtn) { + prevBtn.addEventListener("click", prevSlide); + } + + if (nextBtn) { + nextBtn.addEventListener("click", nextSlide); + } + + document.addEventListener("keydown", (event) => { + if (!document.querySelector(".carousel-container")) { + return; + } + + if (event.key === "ArrowLeft") { + prevSlide(); + } else if (event.key === "ArrowRight") { + nextSlide(); + } + }); + + let touchStartX = 0; + let touchEndX = 0; + + carousel.addEventListener( + "touchstart", + (event) => { + touchStartX = event.changedTouches[0].screenX; + }, + false + ); + + carousel.addEventListener( + "touchend", + (event) => { + touchEndX = event.changedTouches[0].screenX; + handleSwipe(); + }, + false + ); + + function handleSwipe() { + const swipeThreshold = 50; + const diff = touchStartX - touchEndX; + + if (Math.abs(diff) <= swipeThreshold) { + return; + } + + if (diff > 0) { + nextSlide(); + } else { + prevSlide(); + } + } + + goToSlide(0); +} + +if (typeof document$ !== "undefined") { + document$.subscribe(initializeCarousel); +} else if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initializeCarousel); +} else { + initializeCarousel(); +} diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index b4ce1fc..e951678 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -25,4 +25,248 @@ .md-header__button.md-logo svg { height: 2rem; width: auto; -} \ No newline at end of file +} + +/* Carousel styles, adapted from the CRISP docs homepage. */ +.carousel-container { + position: relative; + max-width: 1100px; + margin: 1rem auto; + padding: 0 1rem; +} + +.carousel { + position: relative; + width: 100%; + height: 400px; + overflow: visible; + display: flex; + align-items: center; + justify-content: center; +} + +.carousel-slide { + position: absolute; + width: 85%; + max-width: 700px; + aspect-ratio: 16 / 9; + opacity: 0; + transform: translateX(0) scale(0.8); + transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1); + pointer-events: none; + z-index: 1; + overflow: hidden; + border-radius: 12px; + background: #000; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); +} + +.carousel-slide.active { + opacity: 1; + transform: translateX(0) scale(1); + z-index: 3; + pointer-events: auto; +} + +.carousel-slide.prev { + opacity: 0.4; + transform: translateX(-20%) scale(0.85); + z-index: 2; +} + +.carousel-slide.next { + opacity: 0.4; + transform: translateX(20%) scale(0.85); + z-index: 2; +} + +.carousel-slide iframe { + width: 100%; + height: 100%; + display: block; + border: 0; + background: #000; +} + +.carousel-caption { + position: absolute; + right: 0; + bottom: 0; + left: 0; + padding: 1rem 1.5rem; + background: linear-gradient(to top, rgba(0, 0, 0, 0.85), rgba(0, 0, 0, 0.5), transparent); + color: #fff; + font-size: 0.65rem; + font-weight: 500; + line-height: 1.5; + text-align: center; +} + +.carousel-btn { + position: absolute; + top: 50%; + z-index: 10; + padding: 1rem 1.2rem; + border: 0; + border-radius: 8px; + background-color: rgba(0, 0, 0, 0.5); + color: #fff; + font-family: monospace; + font-size: 1.5rem; + line-height: 1; + cursor: pointer; + user-select: none; + transform: translateY(-50%); + transition: all 0.3s ease; +} + +.carousel-btn:hover, +.carousel-btn:focus-visible { + background-color: rgba(0, 0, 0, 0.8); + outline: 2px solid var(--md-accent-fg-color); + outline-offset: 2px; + transform: translateY(-50%) scale(1.1); +} + +.carousel-btn:active { + transform: translateY(-50%) scale(0.95); +} + +.carousel-btn-prev { + left: 1rem; +} + +.carousel-btn-next { + right: 1rem; +} + +.carousel-indicators { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.carousel-indicator { + width: 12px; + height: 12px; + padding: 0; + border: 0; + border-radius: 50%; + background-color: var(--md-default-fg-color--light); + opacity: 0.3; + cursor: pointer; + transition: all 0.3s ease; +} + +.carousel-indicator:hover, +.carousel-indicator:focus-visible { + opacity: 0.6; + outline: 2px solid var(--md-accent-fg-color); + outline-offset: 2px; + transform: scale(1.2); +} + +.carousel-indicator.active { + background-color: var(--md-primary-fg-color); + opacity: 1; + transform: scale(1.3); +} + +.carousel-controls-info { + max-width: 900px; + margin: 1rem auto; + text-align: center; + font-size: 0.6rem; + opacity: 0.7; +} + +@media (max-width: 768px) { + .carousel { + height: 350px; + } + + .carousel-slide { + width: 85%; + } + + .carousel-slide.prev { + opacity: 0.3; + transform: translateX(-50%) scale(0.8); + } + + .carousel-slide.next { + opacity: 0.3; + transform: translateX(50%) scale(0.8); + } + + .carousel-btn { + padding: 0.8rem 1rem; + font-size: 1.2rem; + } + + .carousel-caption { + padding: 0.8rem 1rem; + font-size: 0.5rem; + } + + .carousel-btn-prev { + left: 0.5rem; + } + + .carousel-btn-next { + right: 0.5rem; + } +} + +/* Drone model grid */ +.drone-grid table { + margin: 0 auto; +} + +.drone-grid td { + vertical-align: middle; + text-align: center; +} + +/* Example page media */ +.example-media, +.example-media-grid { + margin: 1rem 0 1.25rem; +} + +.example-media { + text-align: center; +} + +.example-media img, +.example-media-grid img { + display: block; + width: 100%; + height: auto; + border-radius: 8px; +} + +.example-media--compact img { + max-width: 720px; + margin: 0 auto; +} + +.example-media-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; + align-items: start; + justify-items: center; +} + +.example-media-grid figure { + margin: 0; +} + +.example-media-grid--contacts img { + width: auto; + max-width: 100%; + max-height: 560px; + object-fit: contain; +} diff --git a/docs/usage.md b/docs/usage.md deleted file mode 100644 index 805fa72..0000000 --- a/docs/usage.md +++ /dev/null @@ -1,55 +0,0 @@ -# Quickstart - -Minimal example (create sim, reset, control step, render) - -```python -from crazyflow.sim import Sim -import numpy as np - -# Minimal sim -sim = Sim() -sim.reset() - -# simple state command (n_worlds=1, n_drones=1, 13 state cmd) -cmd = np.zeros((1, 1, 13), dtype=np.float32) -cmd[0, 0, 2] = 0.5 # 0.5m hovering setpoint -sim.state_control(cmd) -sim.step(1) # step one control cycle -sim.render() # opens a MuJoCo window (or render to images) -sim.close() -``` - -A slightly more explicit example where you adjust sim settings (control, integrator, physics): - -```python -from crazyflow.sim import Sim -from crazyflow.control import Control -from crazyflow.sim.integration import Integrator -from crazyflow.sim.physics import Physics -import numpy as np - -sim = Sim( - n_drones=4, - n_worlds=2, - control=Control.state, - integrator=Integrator.rk4, - physics=Physics.first_principles, - drone_model="cf2x_T350", -) -sim.reset() - -duration = 5.0 -fps = 60 - -# send a simple state command (shape: n_worlds, n_drones, 13) -cmd = np.zeros((4, 2, 13), dtype=np.float32) -cmd[..., 2] = sim.data.states.pos[..., :3] + 0.5 # 0.5m hovering setpoint -for i in range(int(duration * sim.control_freq)): - sim.state_control(cmd) - sim.step(sim.freq // sim.control_freq) - if ((i * fps) % sim.control_freq) < fps: - sim.render() -sim.close() -``` - -For the full API and configuration options (Sim args, control helpers, render options), see the API reference: [API reference](api/index.md) \ No newline at end of file diff --git a/docs/user-guide/control-modes.md b/docs/user-guide/control-modes.md new file mode 100644 index 0000000..57e82ea --- /dev/null +++ b/docs/user-guide/control-modes.md @@ -0,0 +1,168 @@ +# Control Modes + +Crazyflow provides four levels of control abstraction, from high-level position setpoints down to direct motor commands. Each level is a separate control mode selected at construction time. + +## Control hierarchy + +Commands flow down a hierarchy. A state command is converted to an attitude command by the Mellinger controller; an attitude command is converted to force/torque by the geometric controller; force/torque is converted to rotor velocities by the mixer. + +``` +State (13D) + └─ Mellinger controller + └─ Attitude (4D: roll, pitch, yaw, thrust) + └─ Geometric controller + └─ Force/torque (4D: Fc, Tx, Ty, Tz) + └─ Mixer + └─ Rotor velocities (4D: ω₁…ω₄) +``` + +When you select `Control.state`, the full chain runs on every control tick. When you select `Control.attitude`, only the lower two stages run. + +## State control + +```python +from crazyflow.sim import Sim +from crazyflow.control import Control + +sim = Sim(control=Control.state, state_freq=100, attitude_freq=500) +sim.reset() +``` + +Command shape: `(n_worlds, n_drones, 13)` + +| Index | Variable | Units | +|---|---|---| +| 0–2 | Target position \(x, y, z\) | m | +| 3–5 | Target velocity \(\dot{x}, \dot{y}, \dot{z}\) | m/s | +| 6–8 | Target acceleration \(\ddot{x}, \ddot{y}, \ddot{z}\) | m/s² | +| 9 | Yaw | rad | +| 10 | Roll rate | rad/s | +| 11 | Pitch rate | rad/s | +| 12 | Yaw rate | rad/s | + +Set unused elements to zero. A common hover command sets only the z position: + +```python +import numpy as np +from crazyflow.sim import Sim +from crazyflow.control import Control + +sim = Sim(control=Control.state) +sim.reset() + +cmd = np.zeros((1, 1, 13), dtype=np.float32) +cmd[0, 0, 2] = 1.0 # hover at 1 m + +sim.state_control(cmd) +sim.step(sim.freq // sim.control_freq) +``` + +## Attitude control + +```python +from crazyflow.sim import Sim, Physics +from crazyflow.control import Control + +sim = Sim(control=Control.attitude, physics=Physics.so_rpy, attitude_freq=500) +sim.reset() +``` + +Command shape: `(n_worlds, n_drones, 4)` + +| Index | Variable | Units | +|---|---|---| +| 0 | Roll setpoint | rad | +| 1 | Pitch setpoint | rad | +| 2 | Yaw setpoint | rad | +| 3 | Collective thrust | N | + +For a hover command, set thrust to `mass × g`: + +```python +import numpy as np +from crazyflow.sim import Sim, Physics +from crazyflow.control import Control + +sim = Sim(control=Control.attitude, physics=Physics.so_rpy) +sim.reset() + +mass = float(sim.data.params.mass[0, 0, 0]) +cmd = np.zeros((1, 1, 4), dtype=np.float32) +cmd[0, 0, 3] = mass * 9.81 + +sim.attitude_control(cmd) +sim.step(sim.freq // sim.control_freq) +``` + +## Force-torque control + +Direct force and torque input. Requires `Physics.first_principles`. + +Command shape: `(n_worlds, n_drones, 4)` + +| Index | Variable | Units | +|---|---|---| +| 0 | Collective force \(F_c\) | N | +| 1 | Body-frame torque \(\tau_x\) | Nm | +| 2 | Body-frame torque \(\tau_y\) | Nm | +| 3 | Body-frame torque \(\tau_z\) | Nm | + +```python +import numpy as np +from crazyflow.sim import Sim, Physics +from crazyflow.control import Control + +sim = Sim(control=Control.force_torque, physics=Physics.first_principles) +sim.reset() + +mass = float(sim.data.params.mass[0, 0, 0]) +cmd = np.zeros((1, 1, 4), dtype=np.float32) +cmd[0, 0, 0] = mass * 9.81 + +sim.force_torque_control(cmd) +sim.step(1) +``` + +## Rotor velocity control + +Direct motor commands. Requires `Physics.first_principles`. + +Command shape: `(n_worlds, n_drones, 4)` + +| Index | Motor | Units | +|---|---|---| +| 0–3 | Motors 0–3 angular velocity | RPM | + +The hover RPM for `cf2x_L250` is approximately 15 000 RPM, but the exact value depends on drone mass. + +```python +import numpy as np +from crazyflow.sim import Sim, Physics +from crazyflow.control import Control + +sim = Sim(control=Control.rotor_vel, physics=Physics.first_principles) +sim.reset() + +cmd = np.full((1, 1, 4), 15_000.0, dtype=np.float32) + +sim.rotor_vel_control(cmd) +sim.step(1) +``` + +## Control frequency + +Each control mode has its own update rate. The physics tick (`freq`) is always the fastest. + +| Mode | Rate argument | Default | +|---|---|---| +| `state` | `state_freq` | 100 Hz | +| `attitude` | `attitude_freq` | 500 Hz | +| `force_torque` | `force_torque_freq` | 500 Hz | +| `rotor_vel` | — | every physics step | + +The simulator applies a new command only when the control tick fires. Between ticks, the previous command is held. The number of physics steps per control tick is `freq // control_freq`. + +## Next steps + +- [Functional API](functional-api.md) — running control inside JIT with `F.controllable` +- [Physics Models](physics-models.md) — compatibility between physics and control modes diff --git a/docs/user-guide/functional-api.md b/docs/user-guide/functional-api.md new file mode 100644 index 0000000..dc43dd1 --- /dev/null +++ b/docs/user-guide/functional-api.md @@ -0,0 +1,192 @@ +# Functional API + +The object-oriented API is convenient for scripting, but it relies on Python-level mutations to `sim.data`. JAX's tracing mechanism used inside `jax.jit`, `jax.grad`, and `jax.lax.scan`, cannot see through Python-level side effects. This means that as soon as you try to compile a loop or differentiate through a trajectory using the OO methods, JAX either raises an error or silently produces incorrect results because it cannot build a computation graph from Python mutations. + +The functional API addresses this by expressing the same operations as pure functions that take `SimData` and return updated `SimData`. There is no hidden state, so JAX can trace, compile, and differentiate through arbitrary compositions of these functions. + +## What does not work inside JAX transformations + +The object-oriented `Sim` methods mutate `sim.data` in place through Python calls. JAX cannot trace through Python-level state mutations, so these methods cannot be used inside `jax.jit`, `jax.grad`, or `jax.lax.scan`: + +```{ .python notest } +import jax +import jax.numpy as jnp +from crazyflow.sim import Sim +from crazyflow.control import Control + +sim = Sim(control=Control.attitude) +sim.reset() + +@jax.jit +def broken(cmd): + sim.attitude_control(cmd) # mutates sim.data — JAX traces the ops but leaks the tracer + sim.step(1) + return sim.data.states.pos # sim.data now holds a leaked tracer; accessing it outside JIT raises UnexpectedTracerError +``` + +## What does work + +The purely functional counterpart passes `SimData` explicitly and returns updated `SimData`. Every operation is a plain JAX function with no Python-level mutation, so the full simulation pipeline is traceable by any JAX transformation: + +```python +import jax +import jax.numpy as jnp +import crazyflow.sim.functional as F +from crazyflow.sim import Sim +from crazyflow.control import Control + +sim = Sim(n_worlds=1, n_drones=1, control=Control.attitude, freq=500) +sim.reset() + +data, default_data = sim.data, sim.default_data +step, reset = sim.build_step_fn(), sim.build_reset_fn() + +cmd = jnp.zeros((1, 1, 4), dtype=jnp.float32) +cmd = cmd.at[..., 3].set(float(data.params.mass[0, 0, 0]) * 9.81) + +@jax.jit +def run(data, cmd): + data = F.attitude_control(data, cmd) + data = step(data, 10) + return data + +data = run(data, cmd) +assert data.states.pos.shape == (1, 1, 3) +``` + +`F.attitude_control`, `step`, and `reset` are all pure functions that take `SimData` and return `SimData`. They can be composed freely and passed to `jax.grad`, `jax.vmap`, or `jax.lax.scan`. + +## `build_step_fn` and `build_reset_fn` + +These two methods return compiled, purely functional step and reset functions. They can also be used to recreate those functions after the [pipelines](pipelines.md) have been modified. + +- **`sim.build_step_fn()`** returns a `jax.jit`-compiled function with signature `(SimData, n_steps: int) -> SimData`. +- **`sim.build_reset_fn()`** returns a `jax.jit`-compiled function with signature `(data: SimData, default_data: SimData, mask: Array | None) -> SimData`. + +The typical setup is: + +```python +from crazyflow.sim import Sim +from crazyflow.control import Control + +sim = Sim(n_worlds=4, n_drones=1, control=Control.attitude) +sim.reset() + +data, default_data = sim.data, sim.default_data +step, reset = sim.build_step_fn(), sim.build_reset_fn() +``` + +From this point, `data` is a plain JAX pytree and `step` and `reset` are compiled functions with no reference to the Python `Sim` object. + +## Purely functional controller functions + +`crazyflow.sim.functional` mirrors all four `Sim` control methods as pure functions: + +```python +import crazyflow.sim.functional as F +``` + +| Function | Description | +|---|---| +| `F.state_control(data, controls)` | Stage a state command | +| `F.attitude_control(data, controls)` | Stage an attitude command | +| `F.force_torque_control(data, controls)` | Stage a force/torque command | +| `F.rotor_vel_control(data, controls)` | Stage rotor velocity commands | +| `F.controllable(data)` | Boolean mask — which worlds may update their controller this step | + +These are exactly the same operations as the OO methods, but `SimData` is passed explicitly and returned rather than mutating `sim.data` in place. All functions return a new `SimData`; the original is never modified. + +## Running simulation inside a compiled function + +Any code that calls `step` or the functional control functions can be wrapped in `@jax.jit`. The first call compiles; subsequent calls with the same array shapes are instant. + +```python +import jax +import jax.numpy as jnp +import crazyflow.sim.functional as F +from crazyflow.sim import Sim +from crazyflow.control import Control + +sim = Sim(n_worlds=1, n_drones=1, control=Control.attitude, freq=500) +sim.reset() + +data, default_data = sim.data, sim.default_data +step, reset = sim.build_step_fn(), sim.build_reset_fn() + +cmd = jnp.zeros((1, 1, 4), dtype=jnp.float32) +cmd = cmd.at[..., 3].set(float(data.params.mass[0, 0, 0]) * 9.81) + +@jax.jit +def simulate_episode(data, default_data, cmd): + data = reset(data, default_data) + data = F.attitude_control(data, cmd) + data = step(data, 10) + return data + +data = simulate_episode(data, default_data, cmd) +pos = data.states.pos[0, 0] +``` + +Calling `step(data, n_steps)` is more efficient than a Python loop over `step(data, 1)` because the entire sequence compiles into a single XLA program. + +## Differentiating through the dynamics + +Because every operation is a pure JAX function, `jax.grad` can differentiate through the entire dynamics pipeline. Here we start the drone 2 m above the floor and compute the gradient of a height-tracking loss with respect to the attitude command. + +```python +import jax +import jax.numpy as jnp +from crazyflow.sim import Sim +from crazyflow.control import Control + +sim = Sim(control=Control.attitude, attitude_freq=50) +sim.reset() + +# Place the drone 2 m above the floor, above the 1 m target +data = sim.data.replace( + states=sim.data.states.replace( + pos=sim.data.states.pos.at[..., 2].set(2.0) + ) +) +step = sim.build_step_fn() + +def loss(cmd, data): + data = data.replace( + controls=data.controls.replace( + attitude=data.controls.attitude.replace(staged_cmd=cmd) + ) + ) + data = step(data, 10) + return (data.states.pos[0, 0, 2] - 1.0) ** 2 # squared error to 1 m + +grad_fn = jax.jit(jax.grad(loss)) + +cmd = jnp.zeros((1, 1, 4), dtype=jnp.float32) +cmd = cmd.at[..., 3].set(float(data.params.mass[0, 0, 0]) * 9.81) + +grad = grad_fn(cmd, data) +# Drone is above the target: reducing thrust lowers it toward 1 m. +# The gradient is positive — descent (less thrust) is the correct direction. +assert float(grad[0, 0, 3]) > 0.0 +``` + +## Working with SimData directly + +`SimData` supports `.replace()` at every level of nesting. This is the standard way to set initial conditions or inject custom state before a rollout: + +```python +import jax.numpy as jnp +from crazyflow.sim import Sim + +sim = Sim(n_worlds=4, n_drones=1) +sim.reset() + +# Set all 4 worlds to different starting heights +new_pos = sim.data.states.pos.at[:, 0, 2].set(jnp.array([0.2, 0.4, 0.6, 0.8])) +sim.data = sim.data.replace(states=sim.data.states.replace(pos=new_pos)) +``` + +## Next steps + +- [Examples](../examples/index.md) — complete runnable scripts for gradient descent, batched simulation, and RL +- [Pipelines](pipelines.md) — customising the pipeline that `build_step_fn` compiles diff --git a/docs/user-guide/gymnasium-envs.md b/docs/user-guide/gymnasium-envs.md new file mode 100644 index 0000000..9ee0a4f --- /dev/null +++ b/docs/user-guide/gymnasium-envs.md @@ -0,0 +1,82 @@ +# Gymnasium Environments + +Crazyflow ships a set of [Gymnasium](https://gymnasium.farama.org/) vectorized environments built on top of `Sim`. They follow the standard `VectorEnv` interface and are suitable for training RL agents with frameworks such as Stable Baselines3, CleanRL, or custom JAX trainers. + +## Available environments + +| Class | Task | Observation | Action | +|---|---|---|---| +| `DroneEnv` | Base class (no reward) | pos, quat, vel, ang_vel | attitude or force/torque | +| `ReachPosEnv` | Reach a target position | pos, quat, vel, ang_vel, target | attitude | +| `ReachVelEnv` | Match a target velocity | vel, ang_vel, target_vel | attitude | +| `LandingEnv` | Land safely | pos, quat, vel, ang_vel | attitude | +| `Figure8Env` | Follow a figure-8 trajectory | pos, quat, vel, ang_vel, phase | attitude | + +All environments run `num_envs` parallel instances backed by a single `Sim` with `n_worlds=num_envs`. + +## Basic usage + +```{ .python notest } +from crazyflow.envs import Figure8Env + +env = Figure8Env(num_envs=16, device="cpu") +obs, info = env.reset() + +for _ in range(500): + action = env.action_space.sample() # random policy for illustration + obs, reward, terminated, truncated, info = env.step(action) + +env.close() +``` + +## Constructor arguments + +All environments accept these common arguments: + +| Argument | Default | Description | +|---|---|---| +| `num_envs` | 1 | Number of parallel environments | +| `max_episode_time` | 10.0 | Episode length before truncation, seconds | +| `physics` | `Physics.so_rpy` | Physics model | +| `drone_model` | `"cf2x_L250"` | Drone configuration | +| `freq` | 500 | Physics frequency, Hz | +| `device` | `"cpu"` | `"cpu"` or `"gpu"` | +| `reset_randomization` | `None` | Optional `SimData → SimData` function applied at reset | + +## Action normalization + +`NormalizeActionsWrapper` rescales the action space to `[-1, 1]`, which simplifies policy learning: + +```{ .python notest } +from crazyflow.envs import Figure8Env +from crazyflow.envs.norm_actions_wrapper import NormalizeActionsWrapper + +env = NormalizeActionsWrapper(Figure8Env(num_envs=32)) +obs, info = env.reset() +action = env.action_space.sample() # in [-1, 1]^4 +obs, reward, terminated, truncated, info = env.step(action) +env.close() +``` + +## Reset randomization + +Pass a `reset_randomization` callable to vary initial conditions between episodes. The function receives `SimData` and a JAX random key and must return updated `SimData`: + +```{ .python notest } +import jax +import jax.numpy as jnp +from crazyflow.envs import ReachPosEnv +from crazyflow.sim.data import SimData + +def randomize(data: SimData, key: jax.Array) -> SimData: + key, subkey = jax.random.split(key) + noise = jax.random.normal(subkey, data.states.pos.shape) * 0.05 + return data.replace(states=data.states.replace(pos=data.states.pos + noise)) + +env = ReachPosEnv(num_envs=64, reset_randomization=randomize) +``` + +## Next steps + +- [Examples](../examples/index.md) — figure-8 and RL training examples +- [Functional API](functional-api.md) — building fully jittable training loops with `jax.lax.scan` diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md new file mode 100644 index 0000000..bd7ed22 --- /dev/null +++ b/docs/user-guide/index.md @@ -0,0 +1,13 @@ +# User Guide + +In-depth documentation for every part of the simulator. + +- [Simulator Overview](sim-overview.md) — `SimData` layout, worlds, drones, and the data convention +- [Object-Oriented API](oo-api.md) — `Sim` class, control methods, rendering, and reset +- [Functional API](functional-api.md) — purely functional interface for JAX transformations +- [Physics Models](physics-models.md) — first-principles vs. fitted models, when to use each +- [Control Modes](control-modes.md) — state, attitude, force/torque, and rotor velocity control +- [Pipelines](pipelines.md) — composable step and reset pipelines, randomization, and disturbances +- [Visualization](visualization.md) — rendering modes, cameras, raycasting, and materials +- [MuJoCo Integration](mujoco.md) — MJCF scene construction, adding objects, and sync internals +- [Gymnasium Environments](gymnasium-envs.md) — vectorized environments for RL training diff --git a/docs/user-guide/mujoco.md b/docs/user-guide/mujoco.md new file mode 100644 index 0000000..8cbdc22 --- /dev/null +++ b/docs/user-guide/mujoco.md @@ -0,0 +1,144 @@ +# MuJoCo Integration + +Crazyflow focuses on drone physics and controllers. However, we still want to provide rendering and collision checking, and to do that we leverage [MuJoCo](https://mujoco.org/) and its JAX port [MJX](https://mujoco.readthedocs.io/en/stable/mjx.html). We keep an MJX representation of the scene in sync with Crazyflow's physics state and invoke MJX functions where needed: collision queries, forward kinematics, and sensor rendering. GUI rendering uses the CPU-side MuJoCo renderer directly. + +## MuJoCo and MJX objects + +Crazyflow maintains two parallel representations at all times: + +| Object | Type | Purpose | +|---|---|---| +| `sim.mj_model` | `mujoco.MjModel` | Reference model, used for GUI rendering | +| `sim.mj_data` | `mujoco.MjData` | Scratch MuJoCo data buffer, only used to initialise MJX | +| `sim.mjx_model` | `mjx.Model` | JAX pytree of the model (static, shared across worlds) | +| `sim.mjx_data` | `mjx.Data` | JAX pytree of the scene state, batched over `n_worlds` | + +`mjx_data` does not hold the physics state. It holds the scene geometry state (body transforms, contact distances, camera positions), derived from `sim.data` through an explicit sync step whenever rendering or collision queries are needed. + +## MJCF and scene construction + +The scene is built programmatically from MJCF (MuJoCo's XML format) at `Sim` construction time using the `MjSpec` API. The process is: + +1. Load the base scene from `crazyflow/scene.xml` (floor, lighting, and sky). +2. Load the drone MJCF from the `drone-models` package. +3. Mark the drone body as mocap. Mocap bodies are kinematically driven by external position and quaternion updates rather than joints, which avoids the O(nv²) cost of computing constraint matrices and saves memory. +4. Attach one copy per drone to a frame in the world body. +5. Compile the spec into `mj_model`, then convert to `mjx_model` and `mjx_data` via `mjx.put_model` and `mjx.put_data`. Vmap `mjx_data` across `n_worlds`. + +The spec is accessible as `sim.spec` before compilation, and `sim.mj_model` / `sim.mjx_model` after. + +## Adding objects to the scene + +Custom geometry (gates, obstacles, walls, or any MJCF body) can be added by editing `sim.spec` and calling `sim.build_mjx()`. The new geometry is available for collision and rendering but has no effect on the drone dynamics, which are computed independently in JAX. + +```{ .python notest } +import mujoco +from crazyflow.sim import Sim + +sim = Sim(n_worlds=1, n_drones=1) + +# Define a box body as an inline XML string (or load from a file) +box_xml = """ + + + + + + + +""" +obstacle_spec = mujoco.MjSpec.from_string(box_xml) + +# Attach one or more instances to a new frame in the scene +frame = sim.spec.worldbody.add_frame() +for i, pos in enumerate([[1.0, 0.0, 0.5], [2.0, 0.0, 0.5]]): + body = obstacle_spec.body("obstacle") + attached = frame.attach_body(body, "", f":{i}") + attached.pos = pos + +# Recompile — closes the viewer if open, rebuilds mj_model and mjx_model/mjx_data +sim.build_mjx() +sim.reset() +``` + +Loading from a file works identically: + +```{ .python notest } +import mujoco +gate_spec = mujoco.MjSpec.from_file("assets/gate.xml") +``` + +For a real-world example, see the drone racing environment in [lsy_drone_racing](https://github.com/learnsyslab/lsy_drone_racing), which loads gate and obstacle specs from MJCF files and attaches them at the configured track positions. + +### Setting body positions at runtime + +If you mark an attached body as mocap (`attached.mocap = True`), its position can be updated at runtime by writing directly into `sim.mjx_data.mocap_pos` without rebuilding the model. This is how the drone positions themselves are driven. + +## Synchronization + +The JAX physics pipeline writes to `sim.data` but never touches `sim.mjx_data`. `mjx_data` is only needed for collision queries and rendering, which require current body transforms. To avoid computing those on every physics step, Crazyflow tracks a `mjx_synced` flag in `sim.data.core`. + +After `sim.step()` or `sim.reset()`, `mjx_synced` is set to `False`. The `sim.render()` and `sim.contacts()` methods check the flag; if stale, they call `sync_sim2mjx()` once and set it back to `True`. + +`sync_sim2mjx` does three things: + +1. Write drone positions and quaternions into `mjx_data.mocap_pos` / `mjx_data.mocap_quat`. +2. `jax.vmap(mjx.kinematics)` to propagate body transforms through the kinematic tree. +3. `jax.vmap(mjx.camlight)` and `jax.vmap(mjx.collision)` for rendering and contact detection respectively. + +These run only once per render or contact call, regardless of how many physics steps were taken since the last sync. + +```{ .python notest } +for i in range(10): + sim.step(5) # JAX physics only, mjx_synced = False + if i % 5 == 0: + sim.render() # syncs once: kinematics + camlight + collision +``` + +## Advanced: the sync flag and avoiding redundant MJX calls + +`sync_sim2mjx` runs kinematics, collision detection, and camera transforms in one shot. The `mjx_synced` flag ensures this happens at most once between physics steps: once the flag is set, any further calls to `sim.render()` or `sim.contacts()` within the same tick skip the sync entirely and operate on the already-computed MJX state. The flag is only cleared when `sim.data` actually changes, so if the physics state has not advanced, the expensive MJX operations are not repeated. + +This means the order of calls matters. Grouping all rendering and contact queries together after a step lets them share a single sync: + +```{ .python notest } +sim.step(5) +contacts = sim.contacts() # sync runs here +sim.render() # flag already set, no second sync +``` + +Interleaving a step between them forces two syncs: + +```{ .python notest } +contacts = sim.contacts() # sync runs here +sim.step(5) # flag cleared +sim.render() # sync runs again +``` + +## Advanced: fusing mjx_data into a contact check function + +Passing `sim.mjx_data` as an argument to a `@jax.jit`-compiled function is expensive. JAX must flatten the entire pytree at the JIT boundary on every call, and `mjx_data` contains many leaves. For contact checking that runs in a tight loop, this overhead matters. + +The solution is to **close over** `mjx_data` rather than pass it as an argument. With `mjx_data` captured in the function closure, JAX treats it as a constant and only flattens it once at compile time. At call time, only the small dynamic state needs to be canonicalized. + +The drone racing environment in [lsy_drone_racing](https://github.com/learnsyslab/lsy_drone_racing) uses this pattern to build a contact check function: + +```{ .python notest } +from crazyflow.sim.sim import sync_sim2mjx + +_mjx_data = sim.mjx_data # captured in closure + +def check_contacts(sim_data: SimData, obstacle_mocap_pos: Array) -> Array: + # Update obstacle positions and sync inside JIT + mjx_data = _mjx_data.replace(mocap_pos=obstacle_mocap_pos) + _, mjx_data = sync_sim2mjx(sim_data, mjx_data, sim.mjx_model) + return mjx_data._impl.contact.dist < 0 +``` + +`_mjx_data` is fused into the closure and compiled as a constant. Only `sim_data` and the obstacle positions cross the JIT boundary at runtime — a much smaller pytree than passing the full `mjx_data`. + +## Next steps + +- [Pipelines](pipelines.md) — inserting custom stages into the step and reset pipelines +- [Examples](../examples/index.md#cameras-and-rgbd) — FPV camera and RGBD rendering +- [Examples](../examples/index.md#contact-queries) — contact detection with box collision geometry diff --git a/docs/user-guide/oo-api.md b/docs/user-guide/oo-api.md new file mode 100644 index 0000000..76253e4 --- /dev/null +++ b/docs/user-guide/oo-api.md @@ -0,0 +1,240 @@ +# Object-Oriented API + +The `Sim` class is the main entry point. It provides a Python-level control loop that is easy to script and debug. + +!!! note + The OO API is not compatible with JAX transformations. If you need to run simulation inside `jax.jit`, `jax.grad`, or `jax.lax.scan`, use the [Functional API](functional-api.md) instead. + +## Creating a Sim + +All configuration is fixed at construction time. + +```python +from crazyflow.sim import Sim, Physics +from crazyflow.sim.integration import Integrator +from crazyflow.control import Control + +sim = Sim( + n_worlds=1, + n_drones=1, + drone_model="cf2x_L250", # Crazyflie 2.x with L250 props + physics=Physics.first_principles, + control=Control.state, + integrator=Integrator.rk4, + freq=500, # physics update rate, Hz + state_freq=100, # state controller rate, Hz + attitude_freq=500, # attitude controller rate, Hz + device="cpu", +) +sim.reset() +``` + +Key constructor arguments: + +| Argument | Default | Description | +|---|---|---| +| `n_worlds` | 1 | Number of independent parallel environments | +| `n_drones` | 1 | Drones per world | +| `drone_model` | `"cf2x_L250"` | Drone configuration (see `drone_models.available_drones`) | +| `physics` | `Physics.default` | Physics model | +| `control` | `Control.default` | Control mode | +| `integrator` | `Integrator.default` | Numerical integrator | +| `freq` | 500 | Physics frequency, Hz | +| `device` | `"cpu"` | `"cpu"` or `"gpu"` | + +## Control methods + +All control methods take an array of shape `(n_worlds, n_drones, command_dim)` and stage it for the next `step` call. + +### State control + +The highest-level interface. A 13-element command sets desired position, velocity, acceleration, yaw, and angular rates. An internal Mellinger controller converts this to attitude commands. + +```python +import numpy as np +from crazyflow.sim import Sim +from crazyflow.control import Control + +sim = Sim(n_worlds=1, n_drones=1, control=Control.state) +sim.reset() + +# [x, y, z, vx, vy, vz, ax, ay, az, yaw, roll_rate, pitch_rate, yaw_rate] +cmd = np.zeros((1, 1, 13), dtype=np.float32) +cmd[0, 0, 2] = 0.5 # hover at 0.5 m + +sim.state_control(cmd) +sim.step(sim.freq // sim.control_freq) +``` + +### Attitude control + +Commands roll, pitch, yaw setpoints (rad) and a collective thrust (N). This level bypasses the position/velocity loop and is suitable for attitude tracking or RL agents that output attitude targets. + +```python +import numpy as np +from crazyflow.sim import Sim, Physics +from crazyflow.control import Control + +sim = Sim(n_worlds=1, n_drones=1, control=Control.attitude, physics=Physics.so_rpy) +sim.reset() + +# [roll, pitch, yaw, collective_thrust_N] +cmd = np.zeros((1, 1, 4), dtype=np.float32) +cmd[0, 0, 3] = float(sim.data.params.mass[0, 0, 0]) * 9.81 # hover thrust + +sim.attitude_control(cmd) +sim.step(sim.freq // sim.control_freq) +``` + +### Force-torque control + +Direct force and torque input, useful for testing dynamics or custom controllers. Requires `Physics.first_principles`. + +```python +import numpy as np +from crazyflow.sim import Sim, Physics +from crazyflow.control import Control + +sim = Sim(n_worlds=1, n_drones=1, control=Control.force_torque, physics=Physics.first_principles) +sim.reset() + +# [collective_force_N, torque_x_Nm, torque_y_Nm, torque_z_Nm] +cmd = np.zeros((1, 1, 4), dtype=np.float32) +cmd[0, 0, 0] = float(sim.data.params.mass[0, 0, 0]) * 9.81 # hover force + +sim.force_torque_control(cmd) +sim.step(1) +``` + +### Rotor velocity control + +The lowest level: directly command each motor's RPM. Requires `Physics.first_principles`. + +```python +import numpy as np +from crazyflow.sim import Sim, Physics +from crazyflow.control import Control + +sim = Sim(n_worlds=1, n_drones=1, control=Control.rotor_vel, physics=Physics.first_principles) +sim.reset() + +# [rpm_motor_0, rpm_motor_1, rpm_motor_2, rpm_motor_3] +cmd = np.full((1, 1, 4), 15_000.0, dtype=np.float32) + +sim.rotor_vel_control(cmd) +sim.step(1) +``` + +## Stepping and resetting + +`sim.step(n_steps)` advances the simulation by `n_steps` physics ticks. On each tick, the full step pipeline runs, including the control stack. Controllers fire at their configured rate (e.g. the state controller at `state_freq`, the attitude controller at `attitude_freq`), not on every physics tick. Between controller ticks, the previously staged command is held. + +Passing more steps to a single `step(n_steps)` call is more efficient than multiple `step(1)` calls: XLA compiles the full loop into a single kernel. If you have staged a control command and do not need to set a new one, you can advance the simulation by any number of steps and the controllers will continue firing at the correct rate. + +!!! note + Changing `n_steps` between calls triggers recompilation. Keep it consistent inside a training or evaluation loop. + +`sim.reset()` reinitialises all worlds to their default state. Pass a boolean mask of shape `(n_worlds,)` to reset only selected worlds: `True` resets that world, `False` leaves it unchanged. This is useful in RL training loops where episodes end at different times. + +```python +import numpy as np +from crazyflow.sim import Sim +from crazyflow.control import Control + +sim = Sim(n_worlds=4, n_drones=1, control=Control.state) +sim.reset() # reset all worlds + +# Stage a command and advance 50 physics steps (controllers fire at their rate) +cmd = np.zeros((4, 1, 13), dtype=np.float32) +cmd[..., 2] = 0.5 +sim.state_control(cmd) +sim.step(50) + +# Reset only worlds 0 and 2, leaving 1 and 3 running +import jax.numpy as jnp +mask = jnp.array([True, False, True, False]) +sim.reset(mask=mask) +``` + +## Reading state + +Access any state field through `sim.data.states`: + +```python +import numpy as np +from crazyflow.sim import Sim +from crazyflow.control import Control + +sim = Sim(n_worlds=2, n_drones=3, control=Control.state) +sim.reset() + +cmd = np.zeros((2, 3, 13), dtype=np.float32) +for _ in range(10): + sim.state_control(cmd) + sim.step(sim.freq // sim.control_freq) + +# All drones in all worlds +pos = sim.data.states.pos # (2, 3, 3) +quat = sim.data.states.quat # (2, 3, 4) +vel = sim.data.states.vel # (2, 3, 3) + +# Drone 1 in world 0 +pos_w0_d1 = sim.data.states.pos[0, 1] # (3,) +``` + +## Rendering + +`sim.render()` opens an interactive MuJoCo viewer or returns an image array for offscreen rendering. + +```{ .python notest } +sim.render() # interactive window, world 0 +sim.render(mode="rgb_array") # returns (H, W, 3) uint8 +sim.render(mode="depth_array") # returns (H, W) float32 +sim.render(world=1, camera="front") # different world or named camera +sim.close() # close the viewer +``` + +## Domain randomization + +Physical parameters can be randomized per-world using the `randomize` helpers: + +```python +import jax +import numpy as np +from crazyflow.sim import Sim +from crazyflow.randomize import randomize_inertia, randomize_mass + +sim = Sim(n_worlds=4, n_drones=1) +sim.reset() + +nominal_mass = sim.data.params.mass +noise = jax.random.normal(jax.random.key(0), nominal_mass.shape) * 2e-3 +randomize_mass(sim, nominal_mass + noise) + +nominal_J = sim.data.params.J +J_noise = jax.random.normal(jax.random.key(1), nominal_J.shape) * 1e-6 +randomize_inertia(sim, nominal_J + J_noise) +``` + +To randomize only a subset of worlds, pass a boolean mask: + +```python +import jax +import numpy as np +from crazyflow.sim import Sim +from crazyflow.randomize import randomize_mass + +sim = Sim(n_worlds=4, n_drones=1) +sim.reset() + +import jax.numpy as jnp +mask = jnp.array([True, True, False, False]) # only worlds 0 and 1 +nominal_mass = sim.data.params.mass +noise = jax.random.normal(jax.random.key(0), nominal_mass.shape) * 2e-3 +randomize_mass(sim, nominal_mass + noise, mask=mask) +``` + +## Next steps + +- [Functional API](functional-api.md) — run simulation inside `jax.jit` and `jax.grad` +- [Pipelines](pipelines.md) — insert custom stages for disturbances and logging diff --git a/docs/user-guide/physics-models.md b/docs/user-guide/physics-models.md new file mode 100644 index 0000000..754a902 --- /dev/null +++ b/docs/user-guide/physics-models.md @@ -0,0 +1,88 @@ +# Physics Models + +Crazyflow supports four physics models, selectable via the `Physics` enum. All models share the same state representation and control interface, so you can swap them at construction time without changing any other code. + +```python +from crazyflow.sim import Sim, Physics + +sim = Sim(physics=Physics.first_principles) +``` + +## Available models + +| Model | Enum value | Command input | Description | +|---|---|---|---| +| First principles | `Physics.first_principles` | Rotor RPM | Full analytical model with identified parameters | +| SO(3) + RPY | `Physics.so_rpy` | Roll/pitch/yaw + thrust | Simplified fitted model | +| SO(3) + RPY + rotor | `Physics.so_rpy_rotor` | Roll/pitch/yaw + thrust | Adds first-order rotor dynamics | +| SO(3) + RPY + rotor + drag | `Physics.so_rpy_rotor_drag` | Roll/pitch/yaw + thrust | Adds translational and rotational drag | + +`Physics.default` resolves to `Physics.first_principles`. + +## First-principles model + +The first-principles model derives forces and torques analytically from motor speeds using identified physical parameters: mass, arm length, propeller constants, and the full inertia tensor. It operates at the rotor-velocity level and is the most accurate model for sim-to-real transfer. + +```python +from crazyflow.sim import Sim, Physics +from crazyflow.control import Control + +# Force-torque and rotor_vel control modes require first_principles +sim = Sim( + physics=Physics.first_principles, + control=Control.rotor_vel, +) +sim.reset() +``` + +Parameters accessible through `sim.data.params`: + +| Parameter | Description | +|---|---| +| `mass` | Drone mass, kg | +| `J` | Inertia matrix, kg·m² | +| `L` | Motor arm length, m | +| `rpm2thrust` | Thrust coefficient, N/(RPM²) | +| `rpm2torque` | Torque coefficient, Nm/(RPM²) | +| `mixing_matrix` | Maps rotor RPMs² to [thrust, tx, ty, tz] | +| `rotor_dyn_coef` | First-order rotor time constant | + +## Fitted models (so_rpy family) + +The `so_rpy` models are identified from flight data using a small number of flight minutes. They take higher-level commands (roll/pitch/yaw setpoints + collective thrust in Newtons) and are faster to simulate because they skip the rotor-velocity level. + +These models are a good choice when: + +- You are training RL agents and want speed over fidelity +- Your controller outputs attitude targets (as most Crazyflie firmware does) +- You do not need rotor-level detail + +```python +from crazyflow.sim import Sim, Physics +from crazyflow.control import Control + +sim = Sim( + physics=Physics.so_rpy_rotor_drag, # most accurate of the fitted family + control=Control.attitude, +) +sim.reset() +``` + +The `so_rpy_rotor_drag` variant includes translational drag, which captures the velocity-dependent deceleration effect visible in aggressive flights. It is the recommended fitted model for sim-to-real experiments. + +## Control mode compatibility + +| Physics model | `Control.state` | `Control.attitude` | `Control.force_torque` | `Control.rotor_vel` | +|---|---|---|---|---| +| `first_principles` | ✓ | ✓ | ✓ | ✓ | +| `so_rpy` | ✓ | ✓ | ✗ | ✗ | +| `so_rpy_rotor` | ✓ | ✓ | ✗ | ✗ | +| `so_rpy_rotor_drag` | ✓ | ✓ | ✗ | ✗ | + +!!! warning + Using `Control.force_torque` or `Control.rotor_vel` with a fitted model raises `ConfigError` at construction time. + +## Next steps + +- [Control Modes](control-modes.md) — command shapes and the control hierarchy +- [Object-Oriented API](oo-api.md) — full constructor arguments diff --git a/docs/user-guide/pipelines.md b/docs/user-guide/pipelines.md new file mode 100644 index 0000000..ac8ed50 --- /dev/null +++ b/docs/user-guide/pipelines.md @@ -0,0 +1,96 @@ +# Pipelines + +Crazyflow has two pipelines, one for stepping and one for resetting, each a tuple of pure JAX functions that transform `SimData`. Both are constructed at `Sim` initialisation and compiled into a single `jax.jit`-cached function by `build_step_fn()` / `build_reset_fn()`. You can modify either pipeline by editing the tuple and calling the corresponding build function. + +## The step pipeline + +`sim.step_pipeline` contains four stages by default: + +1. **Control functions** — convert the staged command through the control hierarchy (state → attitude → force/torque → rotor velocities, depending on the selected mode) +2. **Integrator** — advance the ODE one physics step (Euler, RK4, or symplectic Euler) +3. **Step counter** — increment `data.core.steps` +4. **Floor clip** — prevent drones from passing through the floor + +```python +from crazyflow.sim import Sim + +sim = Sim() +print(sim.step_pipeline) +# (, , , ) +``` + +## The reset pipeline + +`sim.reset_pipeline` is empty by default. When `sim.reset()` is called, it first restores `SimData` to the default state, then runs every function in the reset pipeline in order. The reset function signature is `(data: SimData, default_data: SimData, mask: Array | None) -> SimData`. + +Populate `sim.reset_pipeline` to add episode-level randomization without modifying the default state. + +## Modifying the step pipeline + +Insert or remove stages by slicing and concatenating the tuple. + +!!! warning + Always call `sim.build_step_fn()` after modifying `sim.step_pipeline`. Without it, `sim.step()` still runs the previously compiled kernel and silently ignores your changes. + +To see how to modify the step pipeline with a stochastic disturbance, see the [Disturbance injection example](../examples/index.md#disturbance-injection). + +## Modifying the reset pipeline + +Add a function to the reset pipeline to vary initial conditions between episodes. The function receives the freshly-restored `data`, the `default_data` it was restored from, and an optional `mask` of worlds that were reset. + +```{ .python notest } +import jax +from crazyflow.sim import Sim +from crazyflow.sim.data import SimData +from jax import Array + +def randomize_initial_pos(data: SimData, default_data: SimData, mask: Array | None) -> SimData: + key, subkey = jax.random.split(data.core.rng_key) + noise = jax.random.normal(subkey, data.states.pos.shape) * 0.1 # ±10 cm + return data.replace( + states=data.states.replace(pos=default_data.states.pos + noise), + core=data.core.replace(rng_key=key), + ) + +sim = Sim(n_worlds=16) +sim.reset_pipeline = (randomize_initial_pos,) +sim.build_reset_fn() # recompile +sim.reset() +# Each of the 16 worlds now starts at a slightly different position +``` + +Multiple stages can be chained; the output of each function is passed as input to the next: + +```{ .python notest } +sim.reset_pipeline = (randomize_initial_pos, randomize_mass_fn, log_reset_fn) +sim.build_reset_fn() +``` + +## Removing a stage + +Remove any stage by excluding it from the tuple. A common case is removing the floor clip when computing gradients through a trajectory that starts high above the ground: + +```{ .python notest } +from crazyflow.sim import Sim + +sim = Sim() +sim.step_pipeline = sim.step_pipeline[:-1] # drop clip_floor_pos +sim.build_step_fn() +``` + +## Writing a custom stage + +A step pipeline function must have the signature `(SimData) -> SimData`. A reset pipeline function must have the signature `(SimData, SimData, Array | None) -> SimData`. Both must be pure JAX functions with no Python-level side effects, so they can be traced and compiled. + +```{ .python notest } +from crazyflow.sim.data import SimData + +def my_step_stage(data: SimData) -> SimData: + # JAX operations only — return updated data + return data.replace(...) +``` + +## Next steps + +- [Functional API](functional-api.md) — how `build_step_fn` fits into a compiled training loop +- [Examples](../examples/index.md) — disturbance injection and domain randomization scripts diff --git a/docs/user-guide/sim-overview.md b/docs/user-guide/sim-overview.md new file mode 100644 index 0000000..e47885a --- /dev/null +++ b/docs/user-guide/sim-overview.md @@ -0,0 +1,94 @@ +# Simulator Overview + +## Worlds and drones + +Crazyflow organises simulation state into a two-dimensional batch: **worlds × drones**. + +- **`n_worlds`** — number of independent simulation environments. Each world has its own physics state and evolves independently. Use this to run domain randomisation, parallel RL rollouts, or MPPI sampling. +- **`n_drones`** — number of drones per world. All drones in a world share the same physics tick but have independent states. + +Every state array has shape `(n_worlds, n_drones, feature_dim)`. To read the position of drone 0 in world 2: + +```python +from crazyflow.sim import Sim + +sim = Sim(n_worlds=4, n_drones=2) +sim.reset() + +pos = sim.data.states.pos[2, 0] # world 2, drone 0 → shape (3,) +``` + +## SimData + +All simulation state is stored in `sim.data`, a `SimData` pytree. The main sub-trees are: + +| Field | Type | Description | +|---|---|---| +| `states` | `SimState` | Current kinematic state of every drone | +| `states_deriv` | `SimStateDeriv` | Time derivatives computed by the physics model | +| `controls` | `SimControls` | Staged commands and controller state | +| `params` | `SimParams` | Physical parameters (mass, inertia, motor constants, …) | +| `core` | `SimCore` | Metadata: step count, frequency, RNG key, device | + +### SimState fields + +| Field | Shape | Units | +|---|---|---| +| `pos` | `(N, M, 3)` | Position in world frame, metres | +| `quat` | `(N, M, 4)` | Orientation quaternion, scalar-last `xyzw` | +| `vel` | `(N, M, 3)` | Linear velocity, m/s | +| `ang_vel` | `(N, M, 3)` | Angular velocity in body frame, rad/s | +| `force` | `(N, M, 3)` | External force applied to the drone body, N | +| `torque` | `(N, M, 3)` | External torque applied to the drone body, Nm | +| `rotor_vel` | `(N, M, 4)` | Motor angular velocities, RPM | + +where `N = n_worlds` and `M = n_drones`. + +## Immutability and `data.replace` + +`SimData` is a JAX pytree. All fields are immutable — operations return a new `SimData` rather than modifying in place. This is what makes the simulation compatible with `jax.jit`, `jax.grad`, and `jax.vmap`. + +To modify a field, use `.replace()`: + +```python +from crazyflow.sim import Sim + +sim = Sim(n_worlds=1, n_drones=1) +sim.reset() + +import jax.numpy as jnp +new_pos = sim.data.states.pos.at[..., 2].add(1.0) +sim.data = sim.data.replace(states=sim.data.states.replace(pos=new_pos)) +``` + +## Simulation frequency and the control stack + +`freq` sets the physics update rate in Hz. The control stack is executed as part of each physics step, but controllers fire at their own sub-frequency rather than every tick. For example, with `freq=500` and `state_freq=100`, the state (Mellinger) controller runs every 5 physics steps, and the attitude controller runs at `attitude_freq`. + +This means you can advance multiple physics steps in a single `sim.step(n_steps)` call and the control stack will execute at the correct rate automatically, with no manual sub-stepping required. This is also what makes fusing many steps into a single compiled call efficient. + +```python +import numpy as np +from crazyflow.sim import Sim +from crazyflow.control import Control + +sim = Sim(freq=500, control=Control.state) +sim.reset() +cmd = np.zeros((1, 1, 13), dtype=np.float32) +sim.state_control(cmd) +sim.step(sim.freq // sim.control_freq) # 500 // 100 = 5 physics steps, controller fires once +``` + +## The step and reset pipelines + +Each call to `sim.step()` runs `sim.step_pipeline`, a tuple of pure JAX functions that transforms `SimData`. By default it contains the control conversion functions, the numerical integrator, a step counter, and a floor clip. Similarly, `sim.reset_pipeline` is applied during `sim.reset()` and is empty by default. + +Both pipelines can be extended with custom functions for disturbances, domain randomization, or logging without modifying the core simulator. + +See [Pipelines](pipelines.md) for full details. + +## Next steps + +- [Object-Oriented API](oo-api.md) — all `Sim` methods in detail +- [Functional API](functional-api.md) — working with `SimData` directly inside JAX transformations +- [Pipelines](pipelines.md) — extending the step and reset pipelines diff --git a/docs/user-guide/visualization.md b/docs/user-guide/visualization.md new file mode 100644 index 0000000..cdcfe90 --- /dev/null +++ b/docs/user-guide/visualization.md @@ -0,0 +1,94 @@ +# Visualization + +Crazyflow supports onscreen interactive rendering and offscreen RGB/depth capture through MuJoCo's renderer. Rendering is fully decoupled from the physics step: call `sim.render()` at any frequency independently of how fast the simulation runs. + + + + + + + + + +## Render modes + +`sim.render()` accepts a `mode` argument that controls what it returns: + +| Mode | Return value | Description | +|---|---|---| +| `"human"` (default) | `None` | Opens an interactive MuJoCo viewer window | +| `"rgb_array"` | `(H, W, 3) uint8` | Offscreen RGB frame | +| `"depth_array"` | `(H, W) float32` | Offscreen depth frame in metres | +| `"rgbd_tuple"` | `(rgb, depth)` | Both channels as a tuple | + +```{ .python notest } +sim.render() # interactive window +rgb = sim.render(mode="rgb_array") # numpy array (H, W, 3) +depth = sim.render(mode="depth_array") # numpy array (H, W) +rgb, depth = sim.render(mode="rgbd_tuple", camera="fpv_cam:0", width=320, height=240) +sim.close() # close the viewer +``` + +## Cameras + +Pass a camera name or integer ID to select which camera to render from. The default (`camera=-1`) uses the free camera. Each drone ships with a first-person view camera named `fpv_cam:`: + +```{ .python notest } +sim.render(camera="fpv_cam:0") # first-person view from drone 0 +sim.render(camera=0) # camera by integer ID +``` + +## Raycasting and depth sensing + +For obstacle sensing or perception-based controllers, `render_depth` fires a ray from each camera pixel and returns per-pixel distances — faster than full RGB rendering because it skips lighting and colour computation: + +```{ .python notest } +import jax.numpy as jnp +from crazyflow.sim.sensors import build_render_depth_fn, render_depth + +# One-shot depth render — returns (n_worlds, H, W) +dist = render_depth(sim, camera=0, resolution=(100, 100), include_drone=False) +dist = dist.at[dist > 1.5].set(jnp.nan) + +# Compiled variant for repeated calls +render_fn = build_render_depth_fn( + sim.mjx_model, + camera=0, + resolution=(200, 200), + geomgroup=(1, 1, 0, 1, 1, 1, 1, 1), # exclude drone visual mesh +) +dist = render_fn(sim) +``` + +## Changing materials at runtime + +`change_material` updates the RGBA colour and emission intensity of any named material on any subset of drones without rebuilding the model: + +```{ .python notest } +import numpy as np +from crazyflow.sim.visualize import change_material + +drone_ids = np.arange(sim.n_drones) +change_material(sim, mat_name="led_top", drone_ids=drone_ids, rgba=np.array([1, 0.3, 0, 1]), emission=0.8) +sim.render() +``` + +## Rendering world 0 vs other worlds + +`sim.render()` always renders a single world at a time. Pass `world=` to choose which one: + +```{ .python notest } +sim.render(world=0) # default +sim.render(world=3) # render world 3 +``` + +## Sync and performance + +Rendering triggers an implicit synchronization between the JAX physics state (`sim.data`) and the MuJoCo render buffers (`sim.mjx_data`). This sync computes full forward kinematics, camera transforms, and collision geometry — it is the most expensive operation per render call. See [MuJoCo Integration](mujoco.md#synchronization) for details on how to avoid redundant syncs. + +## Next steps + +- [MuJoCo Integration](mujoco.md) — how the scene is built, how to add objects, and sync internals +- [Examples](../examples/index.md#cameras-and-rgbd) — FPV camera and RGBD capture +- [Examples](../examples/index.md#raycasting-and-depth-sensing) — compiled depth renderer +- [Examples](../examples/index.md#led-deck-and-materials) — per-drone colour control diff --git a/pixi.lock b/pixi.lock index c828f33..e51e358 100644 --- a/pixi.lock +++ b/pixi.lock @@ -102,7 +102,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/5f/78/a3d9ceda0793f4fb43daa292af7b801932611a1aed442636ddfc93d58c7a/jax_cuda12_pjrt-0.10.0-py3-none-manylinux_2_27_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e6/87/67ec012db59ce55aabbf34b9184e420e9ec7e3d57e04d5cb8e91016a434d/jax_cuda12_plugin-0.10.0-cp312-cp312-manylinux_2_27_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b5/20/9b07fc8b327b222b6f72a4978eb4f2ebe856ee71237d63c4d808ec3945e0/jaxlib-0.10.0-cp312-cp312-manylinux_2_27_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.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/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -222,7 +222,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/70/aa/dfac6d72cc35bc07e7587115b6946e333ef4ccb2e6cd26ecf639438c5d26/jax-0.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/95/305854c2ef2b645f7df1666be66b1167c392cc39384d09aca2e9499b71bf/jaxlib-0.10.0-cp313-cp313-manylinux_2_27_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.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/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -314,7 +314,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/70/aa/dfac6d72cc35bc07e7587115b6946e333ef4ccb2e6cd26ecf639438c5d26/jax-0.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d4/b6/b66b0abb9df8f9f8f19a5244b849cb07fc7389a4a5e1fb7794f7cefd7f26/jaxlib-0.10.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.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/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl @@ -438,7 +438,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/f7/95/305854c2ef2b645f7df1666be66b1167c392cc39384d09aca2e9499b71bf/jaxlib-0.10.0-cp313-cp313-manylinux_2_27_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.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/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -557,7 +557,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/70/aa/dfac6d72cc35bc07e7587115b6946e333ef4ccb2e6cd26ecf639438c5d26/jax-0.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d4/b6/b66b0abb9df8f9f8f19a5244b849cb07fc7389a4a5e1fb7794f7cefd7f26/jaxlib-0.10.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.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/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl @@ -659,6 +659,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-12.0.0-py313h80991f8_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.5.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/properdocs-1.6.7-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pymdown-extensions-10.19.1-pyhd8ed1ab_0.conda @@ -702,9 +703,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/70/aa/dfac6d72cc35bc07e7587115b6946e333ef4ccb2e6cd26ecf639438c5d26/jax-0.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/95/305854c2ef2b645f7df1666be66b1167c392cc39384d09aca2e9499b71bf/jaxlib-0.10.0-cp313-cp313-manylinux_2_27_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.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/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/6f/4015dbb4c26bf1fc4b5b637188fc47ec2f1781baccc2e13b0c48887ae9b0/mkdocs_charts_plugin-0.0.13-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ee/1b/3075eb67fe66e19db059f0a25744c4e56978a309603a20e1d3353d545b5e/mkdocs_gen_files-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/2c/bcf1ae903975ad6f169abb05c1eb0f94395478364deb89270cf034081b29/mkdocs_literate_nav-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b0/4d/a330cab5e055d45e924cec69da54a3d8ed37643964f8d1fa1a772b496273/mkdocs_section_index-0.3.12-py3-none-any.whl @@ -718,7 +720,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/69/6a93d8600c339d7687a05857c7907bd4dd8cf88691a5ea106d7a50af90a1/optax-0.2.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3c/1b/6a69800c82bffaee8d10bd6f063da1ec9d745b20826daba22a87acff778d/orbax_checkpoint-0.11.39-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/4d/fc923f5c85318ee8cc903566dc4e0ebe41b2dfc1d2ecf5546db232397ed6/properdocs-1.6.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/de/e4/1ba6f44e491c4eece978685230dde56b14d51a0365bc1b774ddaa94d14cd/pyopengl-3.1.10-py3-none-any.whl @@ -793,6 +794,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pillow-12.0.0-py313ha86496b_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh145f28c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.5.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/properdocs-1.6.7-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-hd74edd7_1002.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pymdown-extensions-10.19.1-pyhd8ed1ab_0.conda @@ -836,9 +838,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/70/aa/dfac6d72cc35bc07e7587115b6946e333ef4ccb2e6cd26ecf639438c5d26/jax-0.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d4/b6/b66b0abb9df8f9f8f19a5244b849cb07fc7389a4a5e1fb7794f7cefd7f26/jaxlib-0.10.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.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/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/6f/4015dbb4c26bf1fc4b5b637188fc47ec2f1781baccc2e13b0c48887ae9b0/mkdocs_charts_plugin-0.0.13-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ee/1b/3075eb67fe66e19db059f0a25744c4e56978a309603a20e1d3353d545b5e/mkdocs_gen_files-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/2c/bcf1ae903975ad6f169abb05c1eb0f94395478364deb89270cf034081b29/mkdocs_literate_nav-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b0/4d/a330cab5e055d45e924cec69da54a3d8ed37643964f8d1fa1a772b496273/mkdocs_section_index-0.3.12-py3-none-any.whl @@ -852,7 +855,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/69/6a93d8600c339d7687a05857c7907bd4dd8cf88691a5ea106d7a50af90a1/optax-0.2.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3c/1b/6a69800c82bffaee8d10bd6f063da1ec9d745b20826daba22a87acff778d/orbax_checkpoint-0.11.39-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/4d/fc923f5c85318ee8cc903566dc4e0ebe41b2dfc1d2ecf5546db232397ed6/properdocs-1.6.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/11/3325d41e6ee15bf1125654301211247b042563bcc898784351252549a8ad/protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/de/e4/1ba6f44e491c4eece978685230dde56b14d51a0365bc1b774ddaa94d14cd/pyopengl-3.1.10-py3-none-any.whl @@ -949,7 +951,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/5f/78/a3d9ceda0793f4fb43daa292af7b801932611a1aed442636ddfc93d58c7a/jax_cuda12_pjrt-0.10.0-py3-none-manylinux_2_27_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e6/87/67ec012db59ce55aabbf34b9184e420e9ec7e3d57e04d5cb8e91016a434d/jax_cuda12_plugin-0.10.0-cp312-cp312-manylinux_2_27_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b5/20/9b07fc8b327b222b6f72a4978eb4f2ebe856ee71237d63c4d808ec3945e0/jaxlib-0.10.0-cp312-cp312-manylinux_2_27_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.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/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -1162,7 +1164,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/5f/78/a3d9ceda0793f4fb43daa292af7b801932611a1aed442636ddfc93d58c7a/jax_cuda12_pjrt-0.10.0-py3-none-manylinux_2_27_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e6/87/67ec012db59ce55aabbf34b9184e420e9ec7e3d57e04d5cb8e91016a434d/jax_cuda12_plugin-0.10.0-cp312-cp312-manylinux_2_27_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b5/20/9b07fc8b327b222b6f72a4978eb4f2ebe856ee71237d63c4d808ec3945e0/jaxlib-0.10.0-cp312-cp312-manylinux_2_27_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.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/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -1188,6 +1190,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/de/e4/1ba6f44e491c4eece978685230dde56b14d51a0365bc1b774ddaa94d14cd/pyopengl-3.1.10-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/b9/c3df11997d29e69b3f8edae1e903bf44eaf4774ccf4c5b6ddcebde88931c/pytest_markdown_docs-0.9.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -1301,7 +1304,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/f7/95/305854c2ef2b645f7df1666be66b1167c392cc39384d09aca2e9499b71bf/jaxlib-0.10.0-cp313-cp313-manylinux_2_27_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.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/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -1420,7 +1423,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/70/aa/dfac6d72cc35bc07e7587115b6946e333ef4ccb2e6cd26ecf639438c5d26/jax-0.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d4/b6/b66b0abb9df8f9f8f19a5244b849cb07fc7389a4a5e1fb7794f7cefd7f26/jaxlib-0.10.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.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/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl @@ -1623,7 +1626,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/70/aa/dfac6d72cc35bc07e7587115b6946e333ef4ccb2e6cd26ecf639438c5d26/jax-0.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/95/305854c2ef2b645f7df1666be66b1167c392cc39384d09aca2e9499b71bf/jaxlib-0.10.0-cp313-cp313-manylinux_2_27_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.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/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -1636,6 +1639,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/de/e4/1ba6f44e491c4eece978685230dde56b14d51a0365bc1b774ddaa94d14cd/pyopengl-3.1.10-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/b9/c3df11997d29e69b3f8edae1e903bf44eaf4774ccf4c5b6ddcebde88931c/pytest_markdown_docs-0.9.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/38/2e/21a3ede87f0bf82d6c7bcb90480d50a6490eb974c6ab20881188e440957c/simplejson-4.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl @@ -1741,7 +1745,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/70/aa/dfac6d72cc35bc07e7587115b6946e333ef4ccb2e6cd26ecf639438c5d26/jax-0.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d4/b6/b66b0abb9df8f9f8f19a5244b849cb07fc7389a4a5e1fb7794f7cefd7f26/jaxlib-0.10.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.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/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl @@ -1754,6 +1758,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/ec/11/3325d41e6ee15bf1125654301211247b042563bcc898784351252549a8ad/protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/de/e4/1ba6f44e491c4eece978685230dde56b14d51a0365bc1b774ddaa94d14cd/pyopengl-3.1.10-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/b9/c3df11997d29e69b3f8edae1e903bf44eaf4774ccf4c5b6ddcebde88931c/pytest_markdown_docs-0.9.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/9a/e5/54cb7c50ad5fdc1e0a86b7df4b135c2cbd5c4623605aa94466659098e8da/simplejson-4.1.1-cp313-cp313-macosx_11_0_arm64.whl @@ -2199,7 +2204,7 @@ packages: - pypi: ./ name: crazyflow version: 0.2.0 - sha256: bc58f630a735b18f54c3b051c1029148fbb49823f9a4360bbc10c22c8ac910cb + sha256: 08222eddec396a707d3e8124e0cd59fca97b6f672a78ef47f2824637c11b16b5 requires_dist: - numpy>=2.0.0 - scipy>=1.17.0 @@ -2219,7 +2224,6 @@ packages: - matplotlib ; extra == 'benchmark' - pandas ; extra == 'benchmark' requires_python: '>=3.11,<3.14' - editable: true - pypi: https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl name: cryptography version: 48.0.0 @@ -2297,7 +2301,6 @@ packages: - scipy>=1.17.0 - array-api-compat - array-api-extra - editable: true - pypi: ./submodules/drone-models name: drone-models version: 0.1.0 @@ -2310,7 +2313,6 @@ packages: - array-api-extra - matplotlib ; extra == 'sysid' - jax>=0.7 ; extra == 'sysid' - editable: true - pypi: https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl name: einops version: 0.8.2 @@ -4342,40 +4344,37 @@ packages: - pkg:pypi/markdown?source=hash-mapping size: 85401 timestamp: 1762856570927 -- pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl name: markdown-it-py - version: 4.2.0 - sha256: 9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a + version: 3.0.0 + sha256: 355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 requires_dist: - mdurl~=0.1 - psutil ; extra == 'benchmarking' - pytest ; extra == 'benchmarking' - pytest-benchmark ; extra == 'benchmarking' + - pre-commit~=3.0 ; extra == 'code-style' - commonmark~=0.9 ; extra == 'compare' - markdown~=3.4 ; extra == 'compare' - mistletoe~=1.0 ; extra == 'compare' - - mistune~=3.0 ; extra == 'compare' + - mistune~=2.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' + - mdit-py-plugins ; extra == 'plugins' - gprof2dot ; extra == 'profiling' - - mdit-py-plugins>=0.5.0 ; extra == 'rtd' + - mdit-py-plugins ; 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' + - sphinx-book-theme ; extra == 'rtd' - jupyter-sphinx ; extra == 'rtd' - - ipykernel ; extra == 'rtd' - coverage ; extra == 'testing' - pytest ; extra == 'testing' - pytest-cov ; extra == 'testing' - pytest-regressions ; extra == 'testing' - - pytest-timeout ; extra == 'testing' - - requests ; extra == 'testing' - requires_python: '>=3.10' + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py313h3dea7bd_0.conda sha256: a530a411bdaaf0b1e4de8869dfaca46cb07407bc7dc0702a9e231b0e5ce7ca85 md5: c14389156310b8ed3520d84f854be1ee @@ -4590,6 +4589,14 @@ packages: - markupsafe>=2.0.1 - mkdocs>=1.1 requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/31/6f/4015dbb4c26bf1fc4b5b637188fc47ec2f1781baccc2e13b0c48887ae9b0/mkdocs_charts_plugin-0.0.13-py3-none-any.whl + name: mkdocs-charts-plugin + version: 0.0.13 + sha256: cef515bb3b4acb3dfea45787d7db3a6f39bf649dbb369a73416c6a1386cac258 + requires_dist: + - mkdocs>=1.1 + - pymdown-extensions>=9.2 + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/ee/1b/3075eb67fe66e19db059f0a25744c4e56978a309603a20e1d3353d545b5e/mkdocs_gen_files-0.6.1-py3-none-any.whl name: mkdocs-gen-files version: 0.6.1 @@ -5436,26 +5443,29 @@ packages: - pkg:pypi/pluggy?source=compressed-mapping size: 25877 timestamp: 1764896838868 -- pypi: https://files.pythonhosted.org/packages/bd/4d/fc923f5c85318ee8cc903566dc4e0ebe41b2dfc1d2ecf5546db232397ed6/properdocs-1.6.7-py3-none-any.whl - name: properdocs - version: 1.6.7 - sha256: 6fa0cfa2e01bf338f684892c8a506cf70ea88ae7f3479c933b6fa20168101cbd - requires_dist: - - click>=7.0 - - colorama>=0.4 ; sys_platform == 'win32' - - ghp-import>=1.0 - - importlib-metadata>=4.4 ; python_full_version < '3.10' - - jinja2>=2.11.1 - - markdown>=3.3.6 - - markupsafe>=2.0.1 - - packaging>=20.5 - - pathspec>=0.11.1 - - platformdirs>=2.2.0 - - pyyaml-env-tag>=0.1 - - pyyaml>=5.1 - - watchdog>=2.0 - - babel>=2.9.0 ; extra == 'i18n' - requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/noarch/properdocs-1.6.7-pyhcf101f3_0.conda + sha256: 083e3685d91bf5e4820bf5566699f18931a5384b5022610816c38cb907d5ad55 + md5: 1d38b41f5243ca90e0231bfdbdd1d817 + depends: + - python >=3.10 + - click >=7.0 + - ghp-import >=1.0 + - jinja2 >=2.11.1 + - markdown >=3.3.6 + - markupsafe >=2.0.1 + - packaging >=20.5 + - pathspec >=0.11.1 + - platformdirs >=2.2.0 + - pyyaml >=5.1 + - pyyaml-env-tag >=0.1 + - watchdog >=2.0 + - python + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/properdocs?source=hash-mapping + size: 163754 + timestamp: 1774874357088 - pypi: https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl name: protobuf version: 7.34.1 @@ -5708,6 +5718,14 @@ packages: - pkg:pypi/pytest?source=hash-mapping size: 299581 timestamp: 1765062031645 +- pypi: https://files.pythonhosted.org/packages/0c/b9/c3df11997d29e69b3f8edae1e903bf44eaf4774ccf4c5b6ddcebde88931c/pytest_markdown_docs-0.9.2-py3-none-any.whl + name: pytest-markdown-docs + version: 0.9.2 + sha256: 9c05a5bee48214cb36583d4a5131d3a23b327a8d82c92ae860106053fd7d1f9e + requires_dist: + - markdown-it-py>=2.2.0,<4.0 + - pytest>=7.0.0 + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-timeout-2.4.0-pyhd8ed1ab_0.conda sha256: 25afa7d9387f2aa151b45eb6adf05f9e9e3f58c8de2bc09be7e85c114118eeb9 md5: 52a50ca8ea1b3496fbd3261bea8c5722 diff --git a/mkdocs.yml b/properdocs.yml similarity index 51% rename from mkdocs.yml rename to properdocs.yml index 168110b..35f86c7 100644 --- a/mkdocs.yml +++ b/properdocs.yml @@ -1,5 +1,5 @@ -site_name: Crazyflow Documentation -site_description: Fast, parallelizable simulations of Crazyflies with JAX and MuJoCo +site_name: Crazyflow +site_description: Fast, parallelizable simulations of Crazyflie drones with JAX and MuJoCo site_url: https://learnsyslab.github.io/crazyflow/ repo_name: learnsyslab/crazyflow repo_url: https://github.com/learnsyslab/crazyflow @@ -11,14 +11,18 @@ theme: logo: img/logo_white.svg favicon: img/logo.svg features: - - navigation.tabs + - content.code.copy + - content.code.select + - content.code.annotate + - navigation.footer + - navigation.indexes - navigation.sections - - navigation.expand + - navigation.tabs - navigation.top + - navigation.tracking - search.highlight - - search.share - - content.code.copy - - content.code.annotate + - search.suggest + - toc.follow palette: - media: "(prefers-color-scheme: light)" scheme: default @@ -37,26 +41,45 @@ theme: nav: - Home: index.md - - Installation: installation.md - - Usage: usage.md - - "Features & Architecture": features.md - - Examples: examples.md - - API: - - Overview: api/index.md + - Get Started: + - Installation: get-started/installation.md + - Quick Start: get-started/quick-start.md + - User Guide: + - user-guide/index.md + - Simulator Overview: user-guide/sim-overview.md + - Object-Oriented API: user-guide/oo-api.md + - Functional API: user-guide/functional-api.md + - Physics Models: user-guide/physics-models.md + - Control Modes: user-guide/control-modes.md + - Pipelines: user-guide/pipelines.md + - Visualization: user-guide/visualization.md + - MuJoCo Integration: user-guide/mujoco.md + - Gymnasium Environments: user-guide/gymnasium-envs.md + - Examples: examples/index.md + - API Reference: api/ + - Cite: cite.md plugins: - search - autorefs + - charts + - gen-files: + scripts: + - docs/gen_ref_pages.py + - literate-nav: + nav_file: SUMMARY.md + - section-index - mkdocstrings: handlers: python: - paths: ["../crazyflow"] + paths: ["."] options: docstring_style: google show_source: true show_root_heading: true show_category_heading: true merge_init_into_class: true + show_submodules: false markdown_extensions: - admonition @@ -66,6 +89,9 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format + - name: vegalite + class: vegalite + format: !!python/name:mkdocs_charts_plugin.fences.fence_vegalite - pymdownx.highlight: anchor_linenums: true line_spans: __span @@ -74,11 +100,23 @@ markdown_extensions: - pymdownx.snippets - pymdownx.tabbed: alternate_style: true + - pymdownx.arithmatex: + generic: true - attr_list - md_in_html - tables - footnotes - def_list + - toc: + permalink: true + +extra_javascript: + - javascripts/mathjax.js + - javascripts/showcase.js + - https://unpkg.com/mathjax@3/es5/tex-mml-chtml.js + - https://cdn.jsdelivr.net/npm/vega@5 + - https://cdn.jsdelivr.net/npm/vega-lite@5 + - https://cdn.jsdelivr.net/npm/vega-embed@6 extra_css: - stylesheets/extra.css @@ -87,3 +125,7 @@ extra: social: - icon: fontawesome/brands/github link: https://github.com/learnsyslab/crazyflow + +watch: + - crazyflow/ + - examples/ diff --git a/pyproject.toml b/pyproject.toml index 8345521..8a54bdf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -151,12 +151,16 @@ pytest-timeout = ">=2.1.0" array-api-strict = ">=2.4.1" matplotlib = "*" +[tool.pixi.feature.tests.pypi-dependencies] +pytest-markdown-docs = "*" + [tool.pixi.feature.tests.tasks] tests = { cmd = "pytest -v tests", description = "Run tests" } +test-docs = { cmd = "pytest -v --markdown-docs --markdown-docs-syntax=superfences crazyflow/ docs/ --ignore=docs/gen_ref_pages.py", description = "Run doctests in docs and docstrings" } [tool.pixi.feature.docs.dependencies] python = ">=3.10" -mkdocs = ">=1.5.0" +properdocs = ">=1.5.0" mkdocs-material = ">=9.0.0" [tool.pixi.feature.docs.pypi-dependencies] @@ -165,10 +169,11 @@ mkdocstrings = { extras = ["python"], version = ">=0.26.0" } mkdocs-gen-files = ">=0.5.0" mkdocs-literate-nav = ">=0.6.0" mkdocs-section-index = ">=0.3.0" +mkdocs-charts-plugin = ">=0.0.10" [tool.pixi.feature.docs.tasks] -docs-build = { cmd = "mkdocs build", description = "Build docs" } -docs-serve = { cmd = "mkdocs serve --livereload", description = "Serve docs locally" } +docs-build = { cmd = "properdocs build", description = "Build docs" } +docs-serve = { cmd = "properdocs serve --livereload", description = "Serve docs locally" } [tool.pixi.feature.dist.pypi-dependencies] build = "*" @@ -177,4 +182,4 @@ twine = "*" [tool.pixi.feature.dist.tasks] build = { cmd = "python -m build && twine check dist/*", description = "Build and test the package" } upload_test = { cmd = "twine upload -r testpypi dist/*", description = "Test upload of the built package" } -upload = { cmd = "twine upload dist/*", description = "Upload of the built package" } \ No newline at end of file +upload = { cmd = "twine upload dist/*", description = "Upload of the built package" }