Skip to content

Contacts rj45 plug

Mesh and SDF contacts

An RJ45 plug moved into its socket with the translation gizmo.

0.00 s
The steel_vbd preset, recordedon an NVIDIA RTX PRO 6000 Blackwell.

Run it

Terminal window
cd examples
# the default preset (vbd), then the others
uv run shelf play contacts_rj45_plug
uv run shelf play contacts_rj45_plug --solver steel_vbd
# change solver settings live
uv run shelf tune contacts_rj45_plug

Source: examples/shelf/03_mesh_sdf_contacts/contacts_rj45_plug/ · ported from newton/examples/contacts/example_contacts_rj45_plug.py @ skild-ai/newton 7565a26b1

Presets

PresetSolverStatusChecked
vbddefaultvbdworks2026-10-28, newton 7565a26b1, NVIDIA RTX A6000
steelsteelunsupportedthe cable is a rod of ROD joints, which SolverSteel rejects at construction (NotImplementedError); steel_vbd runs the plug and latch on Steel and the cable on VBD

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

Code

examples/shelf/03_mesh_sdf_contacts/contacts_rj45_plug/example.py
# SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
# SPDX-License-Identifier: Apache-2.0
# Modified for Steel Shelf: preset-driven execution and shared runner integration.
###########################################################################
# Example SDF RJ45 Plug-Socket Insertion
#
# Use the translation gizmo to move the plug toward the socket.
# Click an axis arrow to slide along one axis, or a plane square
# for two-axis motion. The latch deflects on entry and locks
# the plug once fully inserted. ``--insert`` scripts the gizmo instead:
# push the plug in, then pull it back against the latch.
#
# Command: uv run -m shelf run contacts_rj45_plug
#
###########################################################################
import dataclasses
import numpy as np
import warp as wp
from pxr import Usd, UsdGeom
import newton
import newton.examples
import newton.usd
from newton.math import quat_between_vectors_robust
from newton.solvers.experimental.coupled import SolverCoupled, SolverCoupledProxy
from common import presets
CONTACT_KE = 1.0e5
CONTACT_KD = 0.0
SHAPE_CFG = newton.ModelBuilder.ShapeConfig(
mu=0.0,
ke=CONTACT_KE,
kd=CONTACT_KD,
gap=0.002,
density=1.0e6,
mu_torsional=0.0,
mu_rolling=0.0,
)
MESH_SDF_MAX_RESOLUTION = 128
MESH_SDF_NARROW_BAND_RANGE = (-2.0 * SHAPE_CFG.gap, 2.0 * SHAPE_CFG.gap)
PLUG_Y_OFFSET = -0.025
CABLE_RADIUS = 0.00325
CABLE_KINEMATIC_COUNT = 4 # first N rod bodies are inside the plug and follow it
SOCKET_OPACITY = 0.35
# Contact parameters for cable and ground plane (tuned for VBD).
CABLE_MU = 2.0
# Latch revolute-joint tuning.
LATCH_LIMIT_LOWER = -0.2 # max inward deflection [rad]
LATCH_LIMIT_UPPER = 0.3 # max outward deflection [rad]
LATCH_SPRING_KE = 0.15 # angular return-spring stiffness [N*m/rad]
LATCH_SPRING_KD = 0.03 # angular return-spring damping [N*m*s/rad]
LATCH_LIMIT_KD = 1.0e-4 # angular limit damping [N*m*s/rad]
# --insert: (time [s], gizmo y offset from the start [m]) keyframes; -PLUG_Y_OFFSET is fully inserted, and the
# push aims past it so the gizmo spring seats the plug and snaps the latch.
INSERT_PUSH = -PLUG_Y_OFFSET + 0.015
INSERT_KEYS = ((0.3, 0.0), (1.8, INSERT_PUSH), (2.6, INSERT_PUSH), (3.4, 0.0))
INSERT_LATCHED_TOL = 0.002 # [m] the plug may back out this far against the latch
@wp.kernel
def _apply_gizmo_force(
body_q: wp.array[wp.transform],
body_qd: wp.array[wp.spatial_vector],
body_f: wp.array[wp.spatial_vector],
body_mass: wp.array[float],
pick_target: wp.array[wp.vec3],
stiffness: float,
damping: float,
pick_body: wp.array[int],
plug_idx: int,
latch_idx: int,
gravity: wp.vec3,
):
"""Apply forces based on interaction mode.
``pick_body[0]`` encodes the mode:
* ``>= 0`` -- viewer is picking that body index (damping only on others)
* ``< 0`` -- spring toward ``pick_target[0]``
Also cancels gravity for the plug and latch so only the cable sags.
"""
# Cancel gravity for plug and latch unconditionally.
anti_g0 = -gravity * body_mass[plug_idx]
anti_g1 = -gravity * body_mass[latch_idx]
wp.atomic_add(body_f, plug_idx, wp.spatial_vector(anti_g0, wp.vec3(0.0)))
wp.atomic_add(body_f, latch_idx, wp.spatial_vector(anti_g1, wp.vec3(0.0)))
target = pick_target[0]
picked_body = pick_body[0]
if picked_body >= 0:
if picked_body != plug_idx:
vel0 = wp.spatial_top(body_qd[plug_idx])
mass0 = body_mass[plug_idx]
f0 = -(10.0 + mass0) * damping * vel0
wp.atomic_add(body_f, plug_idx, wp.spatial_vector(f0, wp.vec3(0.0)))
if picked_body != latch_idx:
vel1 = wp.spatial_top(body_qd[latch_idx])
mass1 = body_mass[latch_idx]
f1 = -(10.0 + mass1) * damping * vel1
wp.atomic_add(body_f, latch_idx, wp.spatial_vector(f1, wp.vec3(0.0)))
return
pos0 = wp.transform_get_translation(body_q[plug_idx])
vel0 = wp.spatial_top(body_qd[plug_idx])
mass0 = body_mass[plug_idx]
mult0 = 10.0 + mass0
f0 = mult0 * (stiffness * (target - pos0) - damping * vel0)
wp.atomic_add(body_f, plug_idx, wp.spatial_vector(f0, wp.vec3(0.0)))
vel1 = wp.spatial_top(body_qd[latch_idx])
mass1 = body_mass[latch_idx]
spring_accel = (target - pos0) * (mult0 * stiffness / mass0)
f1 = spring_accel * mass1 - vel1 * ((10.0 + mass1) * damping)
wp.atomic_add(body_f, latch_idx, wp.spatial_vector(f1, wp.vec3(0.0)))
@wp.kernel
def _sync_cable_anchors(
body_q: wp.array[wp.transform],
body_qd: wp.array[wp.spatial_vector],
plug_idx: int,
anchor_indices: wp.array[int],
anchor_offsets: wp.array[wp.vec3],
anchor_rotations: wp.array[wp.quat],
):
"""Copy the plug transform into kinematic cable bodies."""
tid = wp.tid()
plug_tf = body_q[plug_idx]
plug_pos = wp.transform_get_translation(plug_tf)
plug_rot = wp.transform_get_rotation(plug_tf)
idx = anchor_indices[tid]
anchor_world = plug_pos + wp.quat_rotate(plug_rot, anchor_offsets[tid])
cable_rot = wp.normalize(wp.mul(plug_rot, anchor_rotations[tid]))
body_q[idx] = wp.transform(anchor_world, cable_rot)
body_qd[idx] = wp.spatial_vector(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
@wp.kernel
def _align_cable_orientations(
body_q: wp.array[wp.transform],
cable_body_idx: wp.array[int],
cable_next_idx: wp.array[int],
cable_next_start_offsets: wp.array[wp.vec3],
):
"""Swing-correct each dynamic cable capsule to its deformed segment direction.
Keeps the body origin (capsule midpoint/COM) fixed and only updates
the rotation so +Z points toward the next capsule's start endpoint.
"""
tid = wp.tid()
bi = cable_body_idx[tid]
bi_next = cable_next_idx[tid]
tf = body_q[bi]
pos = wp.transform_get_translation(tf)
rot = wp.transform_get_rotation(tf)
next_tf = body_q[bi_next]
next_pos = wp.transform_get_translation(next_tf)
next_rot = wp.transform_get_rotation(next_tf)
seg = next_pos + wp.quat_rotate(next_rot, cable_next_start_offsets[tid]) - pos
seg_len = wp.length(seg)
if seg_len < 1.0e-10:
return
d = seg / seg_len
z_current = wp.quat_rotate(rot, wp.vec3(0.0, 0.0, 1.0))
q_swing = quat_between_vectors_robust(z_current, d)
rot_new = wp.normalize(wp.mul(q_swing, rot))
body_q[bi] = wp.transform(pos, rot_new)
def _load_mesh(stage, prim_path: str) -> tuple[newton.Mesh, wp.vec3]:
"""Load a mesh from USD, center at prim origin, and build SDF.
Returns:
Tuple of (mesh, prim_pos) where prim_pos is the prim world-space
translation [m], usable as the body/shape position.
"""
prim = stage.GetPrimAtPath(prim_path)
usd_mesh = newton.usd.get_mesh(prim, load_normals=True)
tf = newton.usd.get_transform(prim, local=False)
prim_pos = wp.transform_get_translation(tf)
vertices = np.array(usd_mesh.vertices, dtype=np.float32)
indices = np.array(usd_mesh.indices, dtype=np.int32)
normals = np.array(usd_mesh.normals, dtype=np.float32) if usd_mesh.normals is not None else None
mesh = newton.Mesh(vertices, indices, normals=normals)
mesh.build_sdf(
max_resolution=MESH_SDF_MAX_RESOLUTION,
narrow_band_range=MESH_SDF_NARROW_BAND_RANGE,
margin=SHAPE_CFG.gap,
)
return mesh, prim_pos
def _load_cable_centerline(stage) -> tuple[wp.vec3, ...]:
"""Load cable centerline from the ``/World/CableCurve`` BasisCurves prim.
Returns world-space positions with :data:`PLUG_Y_OFFSET` applied.
"""
prim = stage.GetPrimAtPath("/World/CableCurve")
all_points = UsdGeom.BasisCurves(prim).GetPointsAttr().Get()
tf = newton.usd.get_transform(prim, local=False)
prim_pos = wp.transform_get_translation(tf)
return tuple(
wp.vec3(
float(p[0]) + float(prim_pos[0]),
float(p[1]) + float(prim_pos[1]) + PLUG_Y_OFFSET,
float(p[2]) + float(prim_pos[2]),
)
for p in all_points
)
class Example:
def __init__(self, viewer, args=None):
self.preset = presets.load(__file__)
self.fps = self.preset.fps
self.frame_dt = 1.0 / self.fps
self.sim_time = 0.0
self.sim_substeps = self.preset.substeps
self.sim_dt = self.frame_dt / self.sim_substeps
self.viewer = viewer
self.insert = bool(getattr(args, "insert", False))
self.pick_stiffness = 50.0
self.pick_damping = 10.0
usd_path = newton.examples.get_asset("rj45_plug.usd")
stage = Usd.Stage.Open(usd_path)
socket_mesh, sc = _load_mesh(stage, "/World/Socket")
plug_mesh, pc = _load_mesh(stage, "/World/Plug")
latch_mesh, lc = _load_mesh(stage, "/World/Latch")
builder = newton.ModelBuilder(gravity=(0.0, 0.0, -9.81))
builder.rigid_gap = 0.005
builder.add_ground_plane()
# Socket (static body)
socket_shape = builder.add_shape_mesh(
-1,
mesh=socket_mesh,
xform=wp.transform(sc, wp.quat_identity()),
cfg=SHAPE_CFG,
opacity=SOCKET_OPACITY,
label="socket",
)
# Plug (dynamic body, offset along -Y insertion axis)
plug_pos = wp.vec3(pc[0], pc[1] + PLUG_Y_OFFSET, pc[2])
self._plug_body = builder.add_link(
xform=wp.transform(plug_pos, wp.quat_identity()),
label="plug",
)
plug_shape = builder.add_shape_mesh(
self._plug_body,
mesh=plug_mesh,
cfg=SHAPE_CFG,
)
# Latch (dynamic body, same Y offset as plug)
latch_pos = wp.vec3(lc[0], lc[1] + PLUG_Y_OFFSET, lc[2])
self._latch_body = builder.add_link(
xform=wp.transform(latch_pos, wp.quat_identity()),
label="latch",
)
latch_shape = builder.add_shape_mesh(
self._latch_body,
mesh=latch_mesh,
cfg=SHAPE_CFG,
)
connector_shapes = (socket_shape, plug_shape, latch_shape)
# D6 joint: world -> plug (free translation, locked rotation)
JointDof = newton.ModelBuilder.JointDofConfig
d6_joint = builder.add_joint_d6(
parent=-1,
child=self._plug_body,
linear_axes=(
JointDof(axis=(1.0, 0.0, 0.0)),
JointDof(axis=(0.0, 1.0, 0.0)),
JointDof(axis=(0.0, 0.0, 1.0)),
),
angular_axes=None,
parent_xform=wp.transform(plug_pos, wp.quat_identity()),
child_xform=wp.transform_identity(),
)
# Revolute joint: plug -> latch (hinge along -X axis)
rev_joint = builder.add_joint_revolute(
parent=self._plug_body,
child=self._latch_body,
axis=(-1.0, 0.0, 0.0),
parent_xform=wp.transform(lc - pc, wp.quat_identity()),
child_xform=wp.transform_identity(),
target_ke=LATCH_SPRING_KE,
target_kd=LATCH_SPRING_KD,
limit_lower=LATCH_LIMIT_LOWER,
limit_upper=LATCH_LIMIT_UPPER,
limit_kd=LATCH_LIMIT_KD,
collision_filter_parent=True,
)
builder.add_articulation([d6_joint, rev_joint])
self._connector_joints = [d6_joint, rev_joint]
cable_points = _load_cable_centerline(stage)
rod = newton.Rod(cable_points, radius=CABLE_RADIUS)
cable_quats = [wp.quat(*(float(value) for value in frame)) for frame in rod.quaternions]
bend_stiffness = 1.0e1
rod_bodies, rod_joints = builder.add_rod(
rod=rod,
cfg=dataclasses.replace(
builder.default_shape_cfg,
ke=CONTACT_KE,
kd=CONTACT_KD,
mu=CABLE_MU,
),
bend_stiffness=bend_stiffness,
bend_damping=1.0e0,
label="cable",
)
# Collision-filter cable segments that overlap the plug at rest.
for body_idx in rod_bodies[:CABLE_KINEMATIC_COUNT]:
for cable_shape in builder.body_shapes[body_idx]:
for conn_shape in connector_shapes:
builder.add_shape_collision_filter_pair(cable_shape, conn_shape)
# Lock the kinematic prefix and the far cable end.
for idx in (*rod_bodies[:CABLE_KINEMATIC_COUNT], rod_bodies[-1]):
builder.body_mass[idx] = 0.0
builder.body_inv_mass[idx] = 0.0
builder.body_inertia[idx] = wp.mat33(0.0)
builder.body_inv_inertia[idx] = wp.mat33(0.0)
anchor_body_ids = tuple(rod_bodies[:CABLE_KINEMATIC_COUNT])
anchor_offsets = tuple(
0.5 * (cable_points[i] + cable_points[i + 1]) - plug_pos for i in range(CABLE_KINEMATIC_COUNT)
)
anchor_rots = tuple(cable_quats[i] for i in range(CABLE_KINEMATIC_COUNT))
self._cable_bodies, self._cable_joints = list(rod_bodies), list(rod_joints)
builder.color()
self.model = builder.finalize()
self._cable_anchor_indices = wp.array(anchor_body_ids, dtype=int, device=self.model.device)
self._cable_anchor_offsets = wp.array(anchor_offsets, dtype=wp.vec3, device=self.model.device)
self._cable_anchor_rotations = wp.array(anchor_rots, dtype=wp.quat, device=self.model.device)
# Endpoint alignment: include the last kinematic body so it aims toward
# the first dynamic body's start endpoint after deformation. The
# kinematic prefix is reset by _sync_cable_anchors before each solve;
# dynamic body rotations carry into the next collision pass.
align_start = max(CABLE_KINEMATIC_COUNT - 1, 0)
align_bodies = tuple(rod_bodies[align_start:-1])
align_next = tuple(rod_bodies[align_start + 1 :])
align_next_start_offsets = tuple(
wp.vec3(0.0, 0.0, -0.5 * float(wp.length(cable_points[i + 2] - cable_points[i + 1])))
for i in range(align_start, len(rod_bodies) - 1)
)
self._cable_align_indices = wp.array(align_bodies, dtype=int, device=self.model.device)
self._cable_align_next = wp.array(align_next, dtype=int, device=self.model.device)
self._cable_align_next_start_offsets = wp.array(
align_next_start_offsets, dtype=wp.vec3, device=self.model.device
)
self._cable_align_count = len(align_bodies)
self.viewer.set_model(self.model)
self.viewer.picking_enabled = True
self.viewer.set_camera(
pos=wp.vec3(0.125, plug_pos[1] - 0.025, 0.03),
pitch=-10.0,
yaw=180.0,
)
self.viewer.camera_speed = 0.2
self.state_0 = self.model.state()
self.state_1 = self.model.state()
self.control = self.model.control()
self.collision_pipeline = newton.CollisionPipeline(self.model)
self.contacts = self.collision_pipeline.contacts()
self._initial_body_q = self.state_0.body_q.numpy().copy()
self.build_solver()
self._rest_pos = plug_pos
self._latch_deflection_min = 0.0
self.gizmo_tf = wp.transform(plug_pos, wp.quat_identity())
self._pick_body = wp.array([-1], dtype=int, device=self.model.device)
self._pick_target = wp.zeros(1, dtype=wp.vec3, device=self.model.device)
self._gravity = wp.vec3(*self.model.gravity.numpy()[-1])
self.capture()
def build_solver(self):
"""Build the solver and everything bound to its instance; the tuner calls this again to swap it."""
if not self.preset.entries:
self.solver = self.preset.make_solver(self.model)
return
# steel_vbd: Steel owns the plug and latch, VBD the cable rod; the connector proxies into VBD for
# cable contact.
self.model.rigid_contact_max = self.contacts.rigid_contact_max # Steel sizes its contact scratch from it
coupling = self.preset.solver
connector_bodies = [self._plug_body, self._latch_body]
self.solver = SolverCoupledProxy(
model=self.model,
entries=[
SolverCoupled.Entry(
name="steel",
solver=lambda v: self.preset.entry("steel").make_solver(v),
bodies=connector_bodies,
joints=self._connector_joints,
),
SolverCoupled.Entry(
name="vbd",
solver=lambda v: self.preset.entry("vbd").make_solver(v),
bodies=self._cable_bodies,
joints=self._cable_joints,
),
],
coupling=SolverCoupledProxy.Config(
proxies=[
SolverCoupledProxy.Proxy(
source="steel",
destination="vbd",
bodies=connector_bodies,
mass_scale=float(coupling.need("mass_scale")),
mode=coupling.need("coupling_mode"),
)
],
iterations=int(coupling.need("proxy_iterations")),
),
)
def capture(self):
with wp.ScopedCapture() as capture:
self.simulate()
self.graph = capture.graph
def simulate(self):
for substep in range(self.sim_substeps):
self.state_0.clear_forces()
wp.launch(
kernel=_apply_gizmo_force,
dim=1,
inputs=(
self.state_0.body_q,
self.state_0.body_qd,
self.state_0.body_f,
self.model.body_mass,
self._pick_target,
self.pick_stiffness,
self.pick_damping,
self._pick_body,
self._plug_body,
self._latch_body,
self._gravity,
),
device=self.model.device,
)
self.viewer.apply_forces(self.state_0)
# Teleport kinematic cable bodies to follow the plug before the
# solver runs, so joint constraints at the boundary see the
# correct anchor positions and rest-relative rotations.
wp.launch(
kernel=_sync_cable_anchors,
dim=CABLE_KINEMATIC_COUNT,
inputs=(
self.state_0.body_q,
self.state_0.body_qd,
self._plug_body,
self._cable_anchor_indices,
self._cable_anchor_offsets,
self._cable_anchor_rotations,
),
device=self.model.device,
)
self.collision_pipeline.collide(self.state_0, self.contacts)
self.solver.step(self.state_0, self.state_1, self.control, self.contacts, self.sim_dt)
if self.sim_substeps % 2 == 1 and substep == self.sim_substeps - 1:
# an odd count under graph capture: copy the last result into state_0 instead of swapping,
# so every replay of the captured graph starts from the newest state
self.state_0.assign(self.state_1)
else:
self.state_0, self.state_1 = self.state_1, self.state_0
# Snap each capsule's +Z to the next capsule's start endpoint so
# collision/render geometry follows the deformed centerline.
wp.launch(
kernel=_align_cable_orientations,
dim=self._cable_align_count,
inputs=(
self.state_0.body_q,
self._cable_align_indices,
self._cable_align_next,
self._cable_align_next_start_offsets,
),
device=self.model.device,
)
def step(self):
if self.insert:
dy = float(np.interp(self.sim_time, *zip(*INSERT_KEYS)))
self.gizmo_tf = wp.transform(
wp.vec3(self._rest_pos[0], self._rest_pos[1] + dy, self._rest_pos[2]), wp.quat_identity()
)
gp = wp.transform_get_translation(self.gizmo_tf)
picking = getattr(self.viewer, "picking", None)
picked_body = picking.pick_body if picking is not None else -1
self._pick_body.assign([picked_body])
self._pick_target.assign([gp])
if self.graph:
wp.capture_launch(self.graph)
else:
self.simulate()
self.sim_time += self.frame_dt
if isinstance(self.solver, newton.solvers.SolverVBD):
counts = self.solver.body_body_contact_counts.numpy()
buf = self.solver.body_body_contact_buffer_pre_alloc
overflow = np.where(counts > buf)[0]
if len(overflow):
for i in overflow:
label = self.model.body_label[i] if hasattr(self.model, "body_label") else i
print(f"[contact overflow] body {label} (idx={i}): {counts[i]} contacts (buffer={buf})")
if self.insert:
# Hinge angle about -X from the body poses (VBD leaves joint_q as built); the plug cannot rotate.
body_q = self.state_0.body_q.numpy()
plug_qx, plug_qw = body_q[self._plug_body][3], body_q[self._plug_body][6]
latch_qx, latch_qw = body_q[self._latch_body][3], body_q[self._latch_body][6]
latch_angle = -2.0 * float(
np.arctan2(latch_qx * plug_qw - latch_qw * plug_qx, latch_qw * plug_qw + latch_qx * plug_qx)
)
self._latch_deflection_min = min(self._latch_deflection_min, latch_angle)
return
# Snap gizmo to the plug when the user isn't dragging it.
gizmo_active = bool(getattr(self.viewer, "gizmo_is_using", False))
if not gizmo_active:
plug_tf = self.state_0.body_q.numpy()[self._plug_body]
if picked_body >= 0:
snap = wp.vec3(*plug_tf[:3])
else:
snap = wp.vec3(self._rest_pos[0], plug_tf[1], self._rest_pos[2])
self.gizmo_tf[:] = wp.transform(snap, wp.quat_identity())
def render(self):
self.viewer.begin_frame(self.sim_time)
self.viewer.log_gizmo("plug", self.gizmo_tf, rotate=())
self.viewer.log_state(self.state_0)
self.viewer.log_contacts(self.contacts, self.state_0)
self.viewer.end_frame()
def test_final(self):
body_q = self.state_0.body_q.numpy()
initial_q = self._initial_body_q
for i in range(len(body_q)):
assert np.all(np.isfinite(body_q[i])), f"Body {i} has non-finite transform"
drift = np.linalg.norm(body_q[i] - initial_q[i])
assert drift < 1.0, f"Body {i} drifted {drift:.4f} from initial transform"
if self.insert and self.sim_time >= INSERT_KEYS[-1][0]:
backed_out = -PLUG_Y_OFFSET - (body_q[self._plug_body][1] - self._rest_pos[1])
assert backed_out < INSERT_LATCHED_TOL, f"plug backed out {backed_out * 1e3:.1f} mm: the latch did not hold"
assert self._latch_deflection_min < -0.05, (
f"latch never deflected (min {self._latch_deflection_min:.3f} rad)"
)
@staticmethod
def create_parser():
parser = newton.examples.create_parser()
parser.add_argument(
"--insert", action="store_true", help="script the gizmo: insert the plug, then pull it back"
)
return parser
if __name__ == "__main__":
viewer, args = newton.examples.init(Example.create_parser())
newton.examples.run(Example(viewer, args), args)