Skip to content

MJX: add missing mesh_* fields to mjx.Model#3265

Open
ingyukoh wants to merge 1 commit into
google-deepmind:mainfrom
ingyukoh:mjx-add-mesh-fields
Open

MJX: add missing mesh_* fields to mjx.Model#3265
ingyukoh wants to merge 1 commit into
google-deepmind:mainfrom
ingyukoh:mjx-add-mesh-fields

Conversation

@ingyukoh
Copy link
Copy Markdown

@ingyukoh ingyukoh commented May 9, 2026

Closes #2685.

Summary

Adds the missing mesh_* fields to mjx.Model (in mjx/_src/types.py) so they can be accessed alongside the rest of the mesh metadata after put_model.

Fields added:

  • mesh_facenum (companion to existing mesh_faceadr)
  • mesh_facenormal, mesh_facetexcoord
  • mesh_scale
  • mesh_pathadr
  • mesh_poly* family: mesh_polyadr, mesh_polynum, mesh_polynormal, mesh_polyvertadr, mesh_polyvertnum, mesh_polyvert, mesh_polymapadr, mesh_polymapnum, mesh_polymap

Why

The reporter (#2685) called out that mesh_faceadr is on mjx.Model but mesh_facenum is not -- you need both to slice the faces of a specific mesh. Several other mesh_* fields were similarly absent. Each of these exists on mujoco.MjModel and is just metadata (no compute path), so the only thing preventing access through MJX was the missing declaration on types.Model.

What changed

Only mjx/mujoco/mjx/_src/types.py -- 14 new np.ndarray field declarations on class Model(PyTreeNode), matching the existing mesh_* block convention.

io.py already builds the field set dynamically:

mj_field_names = {f.name for f in types.Model.fields() if f.name != ''_impl''}
fields = {f: getattr(m, f) for f in mj_field_names}

so adding the names to types.py is sufficient -- no io.py changes needed (also confirmed by @crisiumnih in the issue thread).

Verification

Smoke test against put_model on a 2-mesh scene:

mesh_facenum           mjx-shape=(2,)   match=True
mesh_facenormal        mjx-shape=(8, 3) match=True
mesh_facetexcoord      mjx-shape=(8, 3) match=True
mesh_scale             mjx-shape=(2, 3) match=True
mesh_pathadr           mjx-shape=(2,)   match=True
mesh_polynum           mjx-shape=(2,)   match=True
mesh_polyadr           mjx-shape=(2,)   match=True
mesh_polynormal        mjx-shape=(8, 3) match=True
mesh_polyvertadr       mjx-shape=(8,)   match=True
mesh_polyvertnum       mjx-shape=(8,)   match=True
mesh_polyvert          mjx-shape=(24,)  match=True
mesh_polymapadr        mjx-shape=(8,)   match=True
mesh_polymapnum        mjx-shape=(8,)   match=True
mesh_polymap           mjx-shape=(24,)  match=True

All fields propagate from MjModel and round-trip equal.

mjx/mujoco/mjx/_src/io_test.py::ModelIOTest -- 18 passed, 6 skipped, 0 failures.

Test plan

  • put_model populates all 14 new fields with values matching MjModel
  • ModelIOTest passes locally
  • CLA check
  • CI

Adds mesh_facenum, mesh_facenormal, mesh_facetexcoord, mesh_scale,
mesh_pathadr, and the mesh_poly* family (mesh_polyadr, mesh_polynum,
mesh_polynormal, mesh_polyvertadr, mesh_polyvertnum, mesh_polyvert,
mesh_polymapadr, mesh_polymapnum, mesh_polymap) to the mjx.Model
PyTreeNode definition.

These fields exist on mujoco.MjModel but were not previously declared
on mjx.Model, so put_model could not surface them. In particular,
mesh_faceadr was defined while its companion mesh_facenum was not,
making it impossible to slice per-mesh face ranges from mjx.Model
without keeping the original MjModel around.

io.py copies fields from MjModel by iterating types.Model.fields(),
so adding the names to types.py is sufficient -- no io.py changes
needed.

Closes google-deepmind#2685.
@yuvaltassa
Copy link
Copy Markdown
Collaborator

MJX does not use these fields, what is the benefit of adding them?

@ingyukoh
Copy link
Copy Markdown
Author

ingyukoh commented May 20, 2026

You're right that MJX doesn't read these fields directly — the original argument was metadata symmetry, since the report (#2685) pointed out that mesh_faceadr is exposed but mesh_facenum isn't, which makes per-mesh face ranges unsliceable from mjx.Model alone. That's the narrow gap.

The broader mesh_poly* set was included on the same completeness reasoning, but if MJX is intentionally a minimal interface, only mesh_facenum (the explicit asymmetry the reporter named) really fills a hole. The rest can be dropped.

Happy to either reduce this PR to just mesh_facenum, or close it — whichever you prefer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Missing mesh_* fields from mjx.Model

2 participants