Packages

MuJoCo physics from Elixir: models, data, the state and every field of them, through a compile-time generated Fine NIF. PyTorch-free, Python-free.

Current section

Files

Jump to
ex_mujoco README.md
Raw

README.md

# ExMujoco

MuJoCo physics from Elixir, through a NIF generated from MuJoCo's own headers.

```elixir
{:ok, model} = ExMujoco.load_xml("model/scene.xml")
data = ExMujoco.data(model)

for _ <- 1..1000, do: ExMujoco.step(data)

ExMujoco.Data.qpos(data) |> ExMujoco.Tensor.to_list()
ExMujoco.Model.geom_size(model) |> ExMujoco.Tensor.to_list()
```

No Python, no `pythonx`, no wheel: a C++ NIF built with [Fine](https://github.com/elixir-nx/fine),
linked against the MuJoCo library and compiled for the machine it runs on.

## Why

[Monty](https://github.com/thousandbrainsproject/tbp.monty) — the Thousand Brains Project's
sensorimotor framework — reaches MuJoCo through the Python bindings, and in this house that
means a `pythonx` interpreter inside the BEAM. That works, and it costs: JSON between the
layers, a thread that owns the GL context, and a second runtime to ship. The physics is a C
library. This package calls it.

The plan is phased — physics first, then scene building, then rendering — and
[`docs/plan.md`](docs/plan.md) says what each phase is for and what it is waiting on.

## Install

```elixir
def deps do
  [{:ex_mujoco, "~> 0.1.8"}]
end
```

Building it wants a C++17 compiler and `make` — the same thing Fine wants, and the toolchain
an Elixir install already has (Xcode command line tools on macOS, `build-essential` on Linux,
MSVC on Windows). MuJoCo itself is fetched for you: the official release artifact for the
platform, checked against the checksum published beside it, in about ten seconds.

```console
$ mix compile
ex_mujoco: Fetching MuJoCo 4.0.1 for darwin-universal
ex_mujoco: MuJoCo 4.0.1 is in _build/mujoco/4.0.1-darwin-arm64
```

Three ways to point it at a MuJoCo, in the order they are tried:

| how                     | command                                         | when                                                |
| ----------------------- | ----------------------------------------------- | --------------------------------------------------- |
| your own install        | `MUJOCO_DIR=/opt/mujoco mix compile`            | Conan, vcpkg, a build of your own, a system package |
| from source, at any ref | `EX_MUJOCO_SOURCE_REF=main mix compile`         | the latest MuJoCo, or a commit that fixes something |
| from a checkout         | `EX_MUJOCO_SOURCE_DIR=~/src/mujoco mix compile` | you are working on MuJoCo                           |
| the pinned release      | `mix compile`                                   | everything else                                     |

A release pins one MuJoCo: `mujoco.version` names it, the official artifact's published
checksum is checked before it is unpacked, and the release workflow refuses to build a tag
against anything else. The three overrides above are the development paths — `main` included —
and `mix ex_mujoco.gen --check` tells you when they have moved past the committed bindings.

```elixir
iex> ExMujoco.mujoco_version()   # the pin, as MuJoCo spells it
"4.0.1"
iex> ExMujoco.version()          # what the linked library answers
4000001
iex> ExMujoco.header_version()   # what the bindings were generated from
4000001
```

All three agree, or the NIF does not load — two versions of one physics is not a difference of
behaviour, it is a difference of answer. What the build itself fetched (version, artifact name,
artifact checksum, platform, timestamp) is in `_build/mujoco/<version>-<platform>-<arch>/.provider`.

## What you get

`ExMujoco` is the curated surface — a model, its data, stepping, state:

|                                           |                                          |
| ----------------------------------------- | ---------------------------------------- |
| `ExMujoco.load_xml/1`, `load_string/1`    | compile MJCF, from a file or from memory |
| `ExMujoco.data/1`                         | an `mjData` for a model                  |
| `ExMujoco.forward/1`, `step/1`, `reset/1` | the three things you do to data          |
| `ExMujoco.state/1,2`, `put_state/3`       | save and restore the integration state   |
| `ExMujoco.contact_force/2`                | one contact's six numbers                |

Underneath, generated from the headers:

- **`ExMujoco.Model`** — every field of `mjModel`, one function each: `nq/1`, `geom_size/1`,
  `body_mass/1`, `jnt_axis/1` … A scalar answers a number; an array answers an
  `ExMujoco.Tensor` carrying the element type and the shape.
- **`ExMujoco.Data`** — the same for `mjData`: `qpos/1`, `qvel/1`, `xpos/1`, `efc_force/1` …
- **`ExMujoco.Api`** — MuJoCo's own functions, bound one for one, under MuJoCo's names:
  `mj_name2id/3`, `mj_step1/2`, `mj_kinematics/2` …
- **`ExMujoco.Types`** — every enum, as numbers, named as MuJoCo names them:
  `ExMujoco.Types.Geom.box()`, `ExMujoco.Types.Obj.body()`.

Reading a field copies it, so the value you hold does not change when the next step runs.

```elixir
iex> ExMujoco.Model.geom_size(model)
#ExMujoco.Tensor<{2, 3} :f64 6 values>

iex> ExMujoco.Api.mj_name2id(model, ExMujoco.Types.Obj.geom(), "floor")
0
```

## What is not here yet

Both are in [`docs/plan.md`](docs/plan.md) with the reason they are their own phase:

- **`mjSpec`** — building a model in memory (`mjs_addBody` and the rest). Compiling XML, from
  a path or from a string, covers everything that can be expressed in MJCF.
- **Rendering** — `mjrContext` and offscreen frames. A GL context is per platform (EGL,
  OSMesa, CGL, WGL) and belongs to a thread; that is a different job from stepping physics.

`docs/coverage.md` lists every MuJoCo function this package does _not_ bind, and why. Nothing
is dropped in silence.

## Errors

MuJoCo reports a fatal error by not returning — inside a NIF that would abort the VM. The NIF
intercepts it (`c_src/mj_call.hpp`), and it arrives as `ExMujoco.Error` with MuJoCo's own
message:

```elixir
iex> ExMujoco.load_string("<mujoco></mujoco>")
{:error, %ExMujoco.Error{message: "mj_parseXMLString: ..."}}
```

## Where to find what

| link                                                                   | what it answers                                              |
| ---------------------------------------------------------------------- | ------------------------------------------------------------ |
| [`ExMujoco`](https://hexdocs.pm/ex_mujoco/ExMujoco.html)               | the curated surface, and the example above                   |
| [`ExMujoco.Model`](https://hexdocs.pm/ex_mujoco/ExMujoco.Model.html)   | one function per `mjModel` field, named as MuJoCo names them |
| [`ExMujoco.Data`](https://hexdocs.pm/ex_mujoco/ExMujoco.Data.html)     | the same for `mjData`                                        |
| [`ExMujoco.Api`](https://hexdocs.pm/ex_mujoco/ExMujoco.Api.html)       | MuJoCo's C functions, bound one for one                      |
| [`ExMujoco.Tensor`](https://hexdocs.pm/ex_mujoco/ExMujoco.Tensor.html) | what an array field answers, and how to read it              |
| [`docs/coverage.md`](https://hexdocs.pm/ex_mujoco/coverage.html)       | what is bound, what is not, and why                          |
| [`docs/plan.md`](https://hexdocs.pm/ex_mujoco/plan.html)               | what is not here yet, and when                               |
| [`docs/building.md`](https://hexdocs.pm/ex_mujoco/building.html)       | platforms, your own MuJoCo, where the shared library goes    |
| [`docs/development.md`](https://hexdocs.pm/ex_mujoco/development.html) | working on the package: bindings, tests, releasing           |

That table is the shape of the documentation, on purpose: **this page is for using the
package** — install, the pin, usage, and where each other question is answered — and everything
about building it, planning it or working on it is a guide beside it. The API reference is
grouped the same way, so `Physics` is the curated surface, `The C API` is MuJoCo's own
functions, `Types` is the enums (eighty-odd tiny modules, in one group rather than loose in the
list), and the generator's modules sit at the end under `Generating the bindings` where a caller
never has to scroll past them.

The field pages are filed the way MuJoCo's header is: `ExMujoco.Model.geom_pos/1` is under
`geoms`, `ExMujoco.Data.qpos/1` under `state`, and `ExMujoco.Data.xpos/1` under
`computed by mj_fwdPosition/mj_kinematics` — the heading MuJoCo writes above the block, so the
index is MuJoCo's index. `ExMujoco.Api` does the same, and each function there carries MuJoCo's
own sentence about what it does; each `ExMujoco.Types.*` value carries MuJoCo's sentence about
what it means.

## Licence

MPL-2.0 for this package. MuJoCo is Apache-2.0 and is linked, not modified; see `NOTICE`.