Native MuJoCo Solver¶
novaphy.solvers.SolverMuJoCo is NovaPhy's native MuJoCo-style
forward-dynamics and constraint solver. It is implemented inside NovaPhy; it
does not call the MuJoCo Python package at runtime. The CPU implementation is
part of the normal build, while a CUDA implementation is optional.
The solver follows the same caller-owned buffer contract as the other
SolverBase implementations:
Collision detection remains explicit. SolverMuJoCo consumes contacts
produced by CollisionPipeline; it does not run broadphase or narrowphase
inside step().
Complete CPU Example¶
import numpy as np
import novaphy
builder = novaphy.ModelBuilder()
# Register this before adding imported or hand-authored MuJoCo attributes.
novaphy.solvers.SolverMuJoCo.register_custom_attributes(builder)
builder.set_gravity(np.array([0.0, -9.81, 0.0], dtype=np.float32))
builder.add_ground_plane(y=0.0)
body = builder.add_link(
xform=novaphy.Transform.from_translation(
np.array([0.0, 1.0, 0.0], dtype=np.float32)
),
mass=1.0,
com=np.zeros(3, dtype=np.float32),
inertia=np.eye(3, dtype=np.float32) * 0.1,
)
builder.add_shape_box(body, hx=0.1, hy=0.1, hz=0.1)
joint = builder.add_joint_free(parent=-1, child=body)
builder.add_articulation([joint])
model = builder.finalize(device=novaphy.Device.cpu())
config = novaphy.solvers.SolverMuJoCo.Config()
config.solver = novaphy.solvers.SolverMuJoCo.SolverType.NEWTON
config.integrator = novaphy.solvers.SolverMuJoCo.IntegratorType.IMPLICIT_FAST
config.cone = novaphy.solvers.SolverMuJoCo.ConeType.PYRAMIDAL
config.iterations = 50
config.ls_iterations = 4
config.nconmax = 64
solver = novaphy.solvers.SolverMuJoCo(model, config)
state_in = model.state()
state_out = model.state()
control = model.control()
pipeline = novaphy.CollisionPipeline(model)
contacts = pipeline.contacts()
for _ in range(240):
state_in.clear_forces()
pipeline.collide(state_in, contacts)
solver.step(state_in, state_out, control, contacts, 1.0 / 120.0)
state_in, state_out = state_out, state_in
Distinct input and output states make the data flow explicit. In-place stepping is also supported:
Passing None for control uses the model's default control values. Passing
None for contacts means that the step has no contact constraints.
Resetting State¶
solver.reset(state, world_mask=None, flags=None) restores selected joint
arrays from the model defaults and clears solver-local working buffers.
Use StateFlags to select
JOINT_Q, JOINT_QD, or both. Body and particle flags are ignored by this
reduced-coordinate solver; warm-start, force, actuator, and iteration scratch
buffers are always cleared.
flags = int(novaphy.StateFlags.JOINT_Q) | int(novaphy.StateFlags.JOINT_QD)
solver.reset(state, flags=flags)
Registering MuJoCo Attributes¶
Call
SolverMuJoCo.register_custom_attributes(builder) before an importer or
procedural scene writes mujoco:* attributes. Registration defines the
attribute names, types, and frequencies used for imported solver options,
actuators, tendons, equality constraints, and MuJoCo-specific state/control
data.
builder = novaphy.ModelBuilder()
novaphy.solvers.SolverMuJoCo.register_custom_attributes(builder)
builder.add_urdf("robot.urdf", floating=True)
ModelBuilder.add_mjcf() and ModelBuilder.add_mjcf_string() register these
attributes automatically. Explicit registration is still the clear choice
for URDF, USD, or fully procedural models that will use MuJoCo-specific
attributes. Constructing SolverMuJoCo after builder.finalize() cannot add
missing attributes to the immutable model.
Registration is model preparation only. It does not select a backend, create runtime state, generate contacts, or step the simulation.
Configuration¶
Construct the typed configuration through SolverMuJoCo.Config. Every field
initially has the value None. For numerical fields with a corresponding
registered model option, an explicit value takes precedence over the imported
value and then the native default. nconmax and disable_contacts are
solver-side controls: an MJCF <size nconmax="..."> declaration and legacy
imported enable/disable flags do not set them.
solver_cls = novaphy.solvers.SolverMuJoCo
config = solver_cls.Config()
config.solver = solver_cls.SolverType.CG
config.integrator = solver_cls.IntegratorType.RK4
config.cone = solver_cls.ConeType.ELLIPTIC
config.disable_contacts = False
solver = solver_cls(model, config)
The constructor accepts SolverMuJoCo(model, config=None). Solver options are
not accepted as free keyword arguments.
| Field | Meaning |
|---|---|
nconmax |
Maximum selected rigid contacts per world. |
iterations, ls_iterations |
Constraint-solver and line-search iteration limits. |
solver |
SolverType.CG or SolverType.NEWTON. |
integrator |
EULER, RK4, IMPLICIT, or IMPLICIT_FAST. |
cone |
PYRAMIDAL or ELLIPTIC friction cone. |
impratio, tolerance, ls_tolerance |
Numerical solver controls. |
density, viscosity, wind |
MuJoCo-style fluid-force options. |
disable_contacts |
Explicitly disable or enable contact constraints. |
solver.settings exposes the solver's copied configuration. The nested
CtrlSource, CtrlType, and TrnType enums describe native actuator
attributes; imported or hand-authored actuator data is stored in the
registered mujoco:* namespace.
CPU and Optional CUDA¶
The model device selects the implementation. There is no backend field in
SolverMuJoCo.Config.
use_cuda = True
if use_cuda:
if not novaphy.has_mujoco_cuda():
raise RuntimeError(
"Rebuild with NOVAPHY_WITH_CUDA=ON and "
"NOVAPHY_WITH_MUJOCO_CUDA=ON"
)
device = novaphy.Device.cuda(0)
else:
device = novaphy.Device.cpu()
model = builder.finalize(device=device)
solver = novaphy.solvers.SolverMuJoCo(model)
novaphy.has_mujoco_cuda() reports whether the native MuJoCo CUDA backend was
built. A True result still requires a usable CUDA runtime and device.
Attempting to construct SolverMuJoCo for a CUDA model when the backend is
absent raises an error; NovaPhy does not silently change the model to CPU.
State, control, and contacts must use the model's device. Allocate them from
model.state(), model.control(), and a CollisionPipeline(model) instead
of mixing buffers from another model or device.
The selected implementation is visible through solver.backend_info:
The native CUDA path supports external CUDA Graph capture. Python host callbacks require the host-compatible path and disable graph capture for that solver.
Contacts and Capacity¶
Run collision detection against the current input state before each step that should enforce contacts:
config.disable_contacts = True suppresses contact constraints even when a
Contacts object is passed. solver.get_max_contact_count() returns the
effective per-world capacity. Contact candidates beyond that capacity are
dropped by the solver, so set nconmax deliberately for dense scenes.
Use solver.update_contacts(contacts, state) when optional solver-owned
contact report channels need to be refreshed, and
solver.debug_snapshot() for detailed solver diagnostics. The debug snapshot
is an inspection surface, not simulation state.
Supported Joint Surface¶
The current native implementation supports prismatic, revolute, ball, fixed,
free, and D6 joints, including limits, armature, friction, target drives,
feedforward forces, and equality/mimic constraints. Distance and cable joints
are not supported by SolverMuJoCo; query solver.joint_support() when
selecting a solver programmatically.
Demos¶
The repository includes CPU/CUDA-selectable examples:
python python/demos/mujoco/demo_mujoco_cartpole.py --backend cpu --steps 240
python python/demos/mujoco/demo_robot_basic.py --backend cpu --headless 60
python python/demos/mujoco/demo_mujoco_pyramids_numerous.py \
--backend cpu --num-pyramids 2 --num-layers 4 --headless 60
Replace --backend cpu with --backend cuda only when
novaphy.has_mujoco_cuda() is true. See the demo catalog
for the robot, weld, policy, and hydroelastic examples.