Skip to content

Magnetic door (mujoco.org)

Basic contacts

A magnet strip snaps an ajar fridge door shut; a light tug cannot open it, a hard one can, and it swings back and latches.

0.00 s
The steel preset (the default), recordedon an NVIDIA RTX PRO 6000 Blackwell.

Run it

Terminal window
cd examples
# the default preset (steel), then the others
uv run shelf play mjorg_magnetic_door
# change solver settings live
uv run shelf tune mjorg_magnetic_door

Source: examples/shelf/01_basic_contacts/mjorg_magnetic_door/ · ported from google-deepmind/mujoco model/adhesion/fridge_door.xml (MuJoCo 4.0 tour on mujoco.org)

Presets

PresetSolverStatusChecked

Solvers with no row are untested on this example. The full settings are inpresets.toml.

Code

examples/shelf/01_basic_contacts/mjorg_magnetic_door/example.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
###########################################################################
# Example mujoco.org magnetic door
#
# A hinged fridge door with a magnet strip that faces a strip on the cabinet frame. The
# ajar door snaps shut, holds against a light tug on its handle, opens on a hard one, and
# its hinge spring swings it back until the magnets latch it again.
#
# Replicates the "Magnetic Adhesion" scene of the MuJoCo 4.0 tour on mujoco.org from its
# MJCF (scene.xml, google-deepmind/mujoco model/adhesion/fridge_door.xml), with Steel's
# contact adhesion: the geoms' `adhesion` imports as `steel:adhesion`. See README.md for
# the gaps.
#
# Command: python -m shelf run mjorg_magnetic_door
#
###########################################################################
import math
from pathlib import Path
import numpy as np
import warp as wp
import newton
import newton.examples
from common.steel_scenes import SceneExample
from newton.solvers import SolverSteel
AJAR_ANGLE = 0.006 # [rad] the XML's `ajar` keyframe
HANDLE_LEVER = 0.46 # [m] the handle's distance from the hinge axis
# (start [s], duration [s], pull on the handle [N]); the light tug stays below the latch's pull-off
TUGS = ((1.0, 0.3, 4.0), (2.0, 0.15, 25.0))
LATCHED_ANGLE = 0.002 # [rad] about 1 mm at the magnet strip
OPENED_ANGLE = math.radians(20.0)
@wp.kernel(enable_backward=False)
def tug_handle(dt: float, dof: int, time: wp.array[wp.float32], joint_f: wp.array[wp.float32]):
"""Write the handle tug at the current time as an opening hinge torque, then advance the time."""
t = time[0]
torque = float(0.0)
if t >= wp.static(TUGS[0][0]) and t < wp.static(TUGS[0][0] + TUGS[0][1]):
torque = wp.static(TUGS[0][2] * HANDLE_LEVER)
if t >= wp.static(TUGS[1][0]) and t < wp.static(TUGS[1][0] + TUGS[1][1]):
torque = wp.static(TUGS[1][2] * HANDLE_LEVER)
joint_f[dof] = torque
time[0] = t + dt
class Example(SceneExample):
file = __file__
# MuJoCo's free camera for the scene: azimuth -120, elevation -20 around the statistic centre
camera = {"eye": (0.96, 1.67, 1.10), "target": (0.0, 0.0, 0.4)}
def build(self):
builder = newton.ModelBuilder()
SolverSteel.register_custom_attributes(builder)
self.preset.apply_model(builder)
# the hinge pin is the scene's one visual-only geom; show the colliders next to it
builder.add_mjcf(str(Path(__file__).with_name("scene.xml")), force_show_colliders=True)
hinge = builder.joint_label.index("Magnetic fridge door/worldbody/door/hinge")
self.hinge_q = builder.joint_q_start[hinge]
self.hinge_dof = builder.joint_qd_start[hinge]
builder.joint_q[self.hinge_q] = AJAR_ANGLE
self.time = wp.zeros(1, dtype=wp.float32)
self.angles = []
return builder, {}
def before_substep(self, substep):
wp.launch(tug_handle, dim=1, inputs=[self.sim_dt, self.hinge_dof], outputs=[self.time, self.control.joint_f])
def test_post_step(self):
self.angles.append((self.sim_time, float(self.state_0.joint_q.numpy()[self.hinge_q])))
def check(self):
"""The door snaps shut, holds the light tug, opens on the hard tug and latches again."""
self.solver.check_constraint_capacity() # dropped contact rows invalidate the run
t, q = (np.asarray(v) for v in zip(*self.angles))
light, hard = TUGS[0][0], TUGS[1][0]
assert t[-1] >= hard + 4.5, f"run at least {hard + 4.5:g} s, not {t[-1]:.2f} s"
closed = q[(t >= 0.5) & (t < light)]
assert closed.max() <= LATCHED_ANGLE, f"the ajar door did not snap shut (max {closed.max():.4f} rad)"
held = q[(t >= light) & (t < hard)]
assert held.max() <= LATCHED_ANGLE, f"the light tug opened the door to {held.max():.4f} rad"
opened = q[t >= hard].max()
assert opened >= OPENED_ANGLE, f"the hard tug opened the door only to {math.degrees(opened):.1f} deg"
end = q[t >= t[-1] - 0.5]
assert end.max() <= LATCHED_ANGLE, f"the door is not latched again at the end (max {end.max():.4f} rad)"
if __name__ == "__main__":
parser = Example.create_parser()
viewer, args = newton.examples.init(parser)
newton.examples.run(Example(viewer, args), args)

README

Steel replica of “Magnetic Adhesion” from the MuJoCo 4.0 tour on mujoco.org. A fridge door hangs on a sprung hinge, and a magnet strip on its edge faces a strip on the cabinet frame. The ajar door snaps shut and stays latched until it is pulled harder than the magnets hold.

  • Source: google-deepmind/mujoco model/adhesion/fridge_door.xml, Apache-2.0 (text in LICENSE). scene.xml is that file as it is upstream. Newton’s MJCF importer reads the strips’ adhesion (1.5 N per contact point) into steel:adhesion and their gap (3 mm) into the shape gap, and Steel pulls on each contact of the pair with the sum, 3 N, while it is within the gap. Both Steel and MuJoCo 4.0 snap the door shut from 0.0135 rad and leave it open at 0.015 rad, a band of about 6 mm at the strips.
  • Script: the door starts at the XML’s ajar keyframe. The tour’s mouse drags become two tugs on the handle, applied as hinge torque at 0.46 m: 4 N for 0.3 s at 1 s and 25 N for 0.15 s at 2 s.
  • Check, over 8 s: the door is shut (within 0.002 rad) from 0.5 s, stays shut through the light tug, opens past 20 degrees on the hard tug, and is latched again for the last 0.5 s; no contact rows were dropped. In Steel it shuts within the first frame, opens to its 135 degree stop and latches again at 6.4 s. MuJoCo 4.0 on the same file and script passes the same phases and latches again at 4.8 s.

Known gaps:

  • The preset’s 5 mm rigid gap keeps other shapes from making contacts with the magnets. With Newton’s default 0.1 m the magnets also pull on the cabinet walls and shelf, and the hard tug cannot open the door.
  • Pull-off at the handle is 12.5-14 N in Steel and 11.5-12.5 N in MuJoCo 4.0. Both hold the magnet pair with four contact points.
  • Inside the gap MuJoCo turns friction off; Steel bounds it by the solved impulse instead.
  • The hinge limit is hard in Steel (the door stops at 135 degrees) and soft in MuJoCo (138 degrees), and the door takes about 1.5 s longer to swing back in Steel.
  • Rendering uses the shelf viewer, not MuJoCo’s Filament renderer.