Skip to content

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:

solver.step(state_in, state_out, control, contacts, dt)

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:

pipeline.collide(state, contacts)
solver.step(state, state, control, contacts, dt)

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:

info = solver.backend_info
print(info.kind, info.device, info.supports_graph_capture)

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:

pipeline.collide(state_in, contacts)
solver.step(state_in, state_out, control, contacts, dt)

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.

See Also