Skip to content

Commit 72de425

Browse files
committed
Add MuJoCo model authoring helpers
Expose typed builder-first helpers for MuJoCo-specific entities. Remap heterogeneous actuator targets during builder composition.
1 parent d12010d commit 72de425

13 files changed

Lines changed: 1818 additions & 6 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `newton.solvers.mujoco` helpers for programmatically authoring MuJoCo actuators, contact pairs, tendons, and equality constraints.

docs/api/newton_solvers.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ https://newton-physics.github.io/newton/stable/solvers/index.html.
2121
:hidden:
2222

2323
newton_solvers_experimental
24+
newton_solvers_mujoco
2425
newton_solvers_style3d
2526

2627
.. rubric:: Submodules
2728

2829
- :doc:`newton.solvers.experimental <newton_solvers_experimental>`
30+
- :doc:`newton.solvers.mujoco <newton_solvers_mujoco>`
2931
- :doc:`newton.solvers.style3d <newton_solvers_style3d>`
3032

3133
.. rubric:: Classes

docs/api/newton_solvers_mujoco.rst

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
.. SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
2+
.. SPDX-License-Identifier: CC-BY-4.0
3+
4+
newton.solvers.mujoco
5+
=====================
6+
7+
MuJoCo solver and programmatic model-authoring helpers.
8+
9+
Use :class:`~newton.solvers.SolverMuJoCo` as the solver class. The module-level
10+
helpers create MuJoCo-specific actuators, tendons, contact pairs, and equality
11+
constraints on a :class:`~newton.ModelBuilder` without exposing custom-frequency
12+
storage details.
13+
14+
Example::
15+
16+
from newton.solvers import mujoco
17+
18+
actuator = mujoco.add_actuator_dcmotor(
19+
builder,
20+
target=mujoco.ActuatorTarget.joint(joint),
21+
motorconst=(0.05, 0.05),
22+
resistance=2.0,
23+
)
24+
25+
.. note::
26+
27+
This page documents helper functions exposed through the ``newton.solvers.mujoco`` attribute.
28+
Because ``newton.solvers`` is a module rather than a package, use
29+
``from newton.solvers import mujoco`` instead of ``import newton.solvers.mujoco``.
30+
31+
.. currentmodule:: newton._src.solvers.mujoco
32+
33+
.. rubric:: Classes
34+
35+
.. autoclass:: ActuatorTarget
36+
37+
.. autoclass:: TendonWrapGeom
38+
39+
.. autoclass:: TendonWrapPulley
40+
41+
.. autoclass:: TendonWrapSite
42+
43+
44+
.. rubric:: Functions
45+
46+
.. autofunction:: add_actuator_dcmotor
47+
48+
.. autofunction:: add_actuator_general
49+
50+
.. autofunction:: add_actuator_motor
51+
52+
.. autofunction:: add_actuator_position
53+
54+
.. autofunction:: add_actuator_velocity
55+
56+
.. autofunction:: add_contact_pair
57+
58+
.. autofunction:: add_equality_connect
59+
60+
.. autofunction:: add_equality_joint
61+
62+
.. autofunction:: add_equality_weld
63+
64+
.. autofunction:: add_tendon_fixed
65+
66+
.. autofunction:: add_tendon_spatial

newton/_src/sim/builder.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,15 @@ class CustomAttribute:
896896
urdf_value_transformer: Callable[[str, dict[str, Any] | None], Any] | None = None
897897
"""Transformer function that converts a URDF attribute value string to a valid Warp dtype. If undefined, the generic converter from :func:`newton.utils.parse_warp_value_from_string` is used. Receives an optional context dict with parsing-time information."""
898898

899+
reference_value_transformer: Callable[[Any, dict[str, Any]], Any] | None = None
900+
"""Transformer for entity references copied by :meth:`ModelBuilder.add_builder`.
901+
902+
Use this instead of :attr:`references` when the referenced entity type depends on
903+
other values in the same row. The callback receives the value and a context with
904+
``builder`` (the source builder), ``destination_builder``, ``entity_offsets``,
905+
``custom_frequency_offsets``, ``row_index``, ``world``, and ``label_prefix``.
906+
"""
907+
899908
def __post_init__(self):
900909
"""Initialize default values and validate dtype compatibility."""
901910
# Allow str dtype for string attributes (stored as Python lists, not warp arrays)
@@ -1707,6 +1716,7 @@ def _custom_attribute_specs_match(existing: CustomAttribute, incoming: CustomAtt
17071716
and existing.assignment == incoming.assignment
17081717
and existing.namespace == incoming.namespace
17091718
and existing.references == incoming.references
1719+
and existing.reference_value_transformer is incoming.reference_value_transformer
17101720
)
17111721

17121722
@staticmethod
@@ -4033,8 +4043,13 @@ def get_offset(entity_or_key: str | None) -> int:
40334043
value_offset = 0 if use_current_world else get_offset(attr.references)
40344044
is_equality_target_attr = full_key == "mujoco:equality_constraint_target"
40354045
is_collision_mask_domain_attr = full_key == collision_mask_domain_key and bool(collision_mask_domain_remap)
4046+
has_reference_value_transformer = attr.reference_value_transformer is not None
40364047
needs_remap = (
4037-
value_offset != 0 or use_current_world or is_equality_target_attr or is_collision_mask_domain_attr
4048+
value_offset != 0
4049+
or use_current_world
4050+
or is_equality_target_attr
4051+
or is_collision_mask_domain_attr
4052+
or has_reference_value_transformer
40384053
)
40394054

40404055
if needs_remap:
@@ -4092,7 +4107,22 @@ def transform_enum_value(
40924107
value: Any,
40934108
is_equality_target: bool = is_equality_target_attr,
40944109
is_collision_mask_domain: bool = is_collision_mask_domain_attr,
4110+
reference_value_transformer: Callable[[Any, dict[str, Any]], Any]
4111+
| None = attr.reference_value_transformer,
40954112
) -> Any:
4113+
if reference_value_transformer is not None:
4114+
return reference_value_transformer(
4115+
value,
4116+
{
4117+
"builder": builder,
4118+
"destination_builder": self,
4119+
"entity_offsets": entity_offsets,
4120+
"custom_frequency_offsets": custom_frequency_offsets,
4121+
"row_index": entity_idx,
4122+
"world": world,
4123+
"label_prefix": label_prefix,
4124+
},
4125+
)
40964126
if is_equality_target:
40974127
return transform_equality_target_value(entity_idx, value)
40984128
if is_collision_mask_domain:

newton/_src/solvers/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import TYPE_CHECKING
66

77
if TYPE_CHECKING:
8-
from . import style3d
8+
from . import mujoco, style3d
99
from .featherstone import SolverFeatherstone
1010
from .implicit_mpm import SolverImplicitMPM
1111
from .kamino import SolverKamino
@@ -26,6 +26,7 @@
2626
"SolverStyle3D",
2727
"SolverVBD",
2828
"SolverXPBD",
29+
"mujoco",
2930
"style3d",
3031
]
3132

@@ -43,6 +44,7 @@
4344
"SolverStyle3D": (".style3d.solver_style3d", "SolverStyle3D"),
4445
"SolverVBD": (".vbd", "SolverVBD"),
4546
"SolverXPBD": (".xpbd", "SolverXPBD"),
47+
"mujoco": (".mujoco", None),
4648
"style3d": (".style3d", None),
4749
}
4850

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,59 @@
11
# SPDX-FileCopyrightText: Copyright (c) 2025 The Newton Developers
22
# SPDX-License-Identifier: Apache-2.0
33

4+
"""MuJoCo solver and programmatic model-authoring helpers.
5+
6+
Use :class:`~newton.solvers.SolverMuJoCo` as the solver class. The module-level
7+
helpers create MuJoCo-specific actuators, tendons, contact pairs, and equality
8+
constraints on a :class:`~newton.ModelBuilder` without exposing custom-frequency
9+
storage details.
10+
11+
Example::
12+
13+
from newton.solvers import mujoco
14+
15+
actuator = mujoco.add_actuator_dcmotor(
16+
builder,
17+
target=mujoco.ActuatorTarget.joint(joint),
18+
motorconst=(0.05, 0.05),
19+
resistance=2.0,
20+
)
21+
"""
22+
23+
from .actuators import (
24+
ActuatorTarget,
25+
add_actuator_dcmotor,
26+
add_actuator_general,
27+
add_actuator_motor,
28+
add_actuator_position,
29+
add_actuator_velocity,
30+
)
31+
from .contacts import add_contact_pair
32+
from .equality import add_equality_connect, add_equality_joint, add_equality_weld
433
from .solver_mujoco import SolverMuJoCo
34+
from .tendons import (
35+
TendonWrapGeom,
36+
TendonWrapPulley,
37+
TendonWrapSite,
38+
add_tendon_fixed,
39+
add_tendon_spatial,
40+
)
541

642
__all__ = [
43+
"ActuatorTarget",
744
"SolverMuJoCo",
45+
"TendonWrapGeom",
46+
"TendonWrapPulley",
47+
"TendonWrapSite",
48+
"add_actuator_dcmotor",
49+
"add_actuator_general",
50+
"add_actuator_motor",
51+
"add_actuator_position",
52+
"add_actuator_velocity",
53+
"add_contact_pair",
54+
"add_equality_connect",
55+
"add_equality_joint",
56+
"add_equality_weld",
57+
"add_tendon_fixed",
58+
"add_tendon_spatial",
859
]
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Shared implementation helpers for MuJoCo model authoring."""
5+
6+
from __future__ import annotations
7+
8+
from typing import Any
9+
10+
from ...sim import ModelBuilder
11+
12+
13+
def _ensure_mujoco_attributes(builder: ModelBuilder, sentinel: str) -> None:
14+
"""Register the MuJoCo schema when the requested domain is unavailable."""
15+
if builder.has_custom_attribute(sentinel):
16+
return
17+
18+
from .solver_mujoco import SolverMuJoCo # noqa: PLC0415
19+
20+
SolverMuJoCo.register_custom_attributes(builder)
21+
22+
23+
def _prepare_custom_frequency_row(
24+
builder: ModelBuilder,
25+
frequency: str,
26+
values: dict[str, Any],
27+
custom_attributes: dict[str, Any] | None,
28+
) -> dict[str, Any]:
29+
"""Validate and combine one solver-owned custom-frequency row."""
30+
extras = custom_attributes or {}
31+
overlap = values.keys() & extras.keys()
32+
if overlap:
33+
names = ", ".join(sorted(overlap))
34+
raise ValueError(f"custom_attributes cannot override MuJoCo-managed values: {names}")
35+
36+
row = {**values, **extras}
37+
for key in row:
38+
attribute = builder.custom_attributes.get(key)
39+
if attribute is None:
40+
raise AttributeError(
41+
f"Custom attribute '{key}' is not registered. Register it before adding a {frequency} row."
42+
)
43+
if attribute.frequency != frequency:
44+
raise ValueError(
45+
f"Custom attribute '{key}' uses frequency {attribute.frequency!r}, expected {frequency!r}."
46+
)
47+
return row
48+
49+
50+
def _add_custom_frequency_row(
51+
builder: ModelBuilder,
52+
frequency: str,
53+
values: dict[str, Any],
54+
*,
55+
index_key: str,
56+
custom_attributes: dict[str, Any] | None = None,
57+
) -> int:
58+
"""Append one validated custom-frequency row and return its index."""
59+
row = _prepare_custom_frequency_row(builder, frequency, values, custom_attributes)
60+
return builder.add_custom_values(**row)[index_key]
61+
62+
63+
__all__ = []

0 commit comments

Comments
 (0)