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.4"}]
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.
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 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, 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.
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: ..."}}
Where to find what
| link | what it answers |
|---|---|
ExMujoco |
the curated surface, and the example above |
ExMujoco.Model |
one function per mjModel field, named as MuJoCo names them |
ExMujoco.Data |
the same for mjData |
ExMujoco.Api |
MuJoCo's C functions, bound one for one |
ExMujoco.Tensor |
what an array field answers, and how to read it |
docs/coverage.md |
what is bound, what is not, and why |
docs/plan.md |
what is not here yet, and when |
docs/building.md |
platforms, your own MuJoCo, where the shared library goes |
docs/development.md |
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.
Licence
MPL-2.0 for this package. MuJoCo is Apache-2.0 and is linked, not modified; see NOTICE.