ExMujoco
MuJoCo physics from Elixir, through a NIF generated from MuJoCo's own headers.
{: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,
linked against the MuJoCo library and compiled for the machine it runs on.
Why
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 says what each phase is for and what it is waiting on.
Install
def deps do
[{:ex_mujoco, "~> 0.1.1"}]
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.
$ 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.
The source build runs CMake and takes a few minutes; the release fetch takes seconds.
docs/building.md has the detail, including what happens to the shared
library afterwards and why.
Windows needs nothing set up: the library MuJoCo ships is loaded from priv/ by a second,
small library, because Windows will not look beside a library for the libraries it imports.
The one prerequisite is MuJoCo's own — the Visual C++ runtime, which ships with Visual Studio.
CI builds the tarball Hex would ship, unpacks it, and installs it in an empty directory on Linux and Windows — compiling MuJoCo from scratch there — because a package that only builds in its own checkout is a package nobody can use.
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 ofmjModel, one function each:nq/1,geom_size/1,body_mass/1,jnt_axis/1… A scalar answers a number; an array answers anExMujoco.Tensorcarrying the element type and the shape.ExMujoco.Data— the same formjData: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, with names that read like Monty's Python: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.
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 with the reason they are their own phase:
mjSpec— building a model in memory (mjs_addBodyand the rest). Compiling XML, from a path or from a string, covers everything that can be expressed in MJCF.- Rendering —
mjrContextand 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:
iex> ExMujoco.load_string("<mujoco></mujoco>")
{:error, %ExMujoco.Error{message: "mj_parseXMLString: ..."}}
How the bindings are made
MuJoCo generates its own Python bindings by reading its headers. This package does the same:
mix ex_mujoco.gen parses MUJOCO_DIR/include/mujoco/*.h — functions, struct members with the
extents from mjxmacro.h, and enums — and writes the C++ that Fine registers and the Elixir
that calls it. The output is committed, so a package build needs neither the generator nor a
network, and mix ex_mujoco.gen --check (which CI runs) fails when the two disagree.
c_src/manifest.exs is the one hand-edited decision: which C functions are bound 1:1, and
which are bound by hand because each call needs an answer no signature gives.
Releasing
VERSION is the one place the version lives; mix.exs reads it. To publish:
$ ./scripts/tag_and_push_version.sh --dry-run # what it would do
$ ./scripts/tag_and_push_version.sh # v0.1.1, pushed
It refuses to tag a commit whose CI is not green, and a commit CI never saw —
a docs-only push, since the workflow ignores markdown — is compared against the
last run that did succeed. The tag is what .github/workflows/release.yml
publishes from: it builds the package, publishes it to Hex and creates the
GitHub release.
Licence
MPL-2.0 for this package. MuJoCo is Apache-2.0 and is linked, not modified; see NOTICE.