The robot description: one source of truth
Every simulator, every training run and every check needs the same robot: the same joints, limits, masses and motors. Marvin Bot writes that robot down once, in a handful of YAML files, and generates everything else from them, so the models cannot quietly drift apart.
Think of it like a recipe kept in one master cookbook. If three cooks each copied the recipe by hand and one of them changed “200 g sugar” to “250 g”, you would get three slightly different cakes and never know why. Before anything can be simulated or trained, the software needs a written-down robot. This page is about keeping that one written-down robot correct, and why that matters before a single motor turns.
The idea in one picture
Section titled “The idea in one picture”A robot description is a file that tells software what the robot is made of: rigid bodies called links, connected by joints, each link with a mass, a centre of mass and an inertia tensor, plus shapes for drawing (visual) and for touching things (collision). (An inertia tensor is just a small table of numbers saying how hard the link is to spin about each direction; more below.) Different tools want different file formats. ROS (Robot Operating System, a widely used open-source robot software toolkit) and most robotics tools read URDF; MuJoCo reads MJCF; Isaac Sim and Isaac Lab load USD.
If you write these by hand you get three robots that are almost, but not quite, the same. A knee limit changed in one file and not the others is enough to make a policy (the trained neural-network controller of page 16) trained in one simulator fail in another. Marvin Bot avoids this by treating all of them as build products: files that a program writes from the YAML, never edited by hand.
How to read this picture: the five boxes on the left are the hand-edited source files; each arrow is a program (named in code) that turns one file into the next. Only the YAML files are ever edited by a person.
Two explorers below make the important details concrete: the shoulder’s coupled joint limit, which no simulator file format can express, and a one-joint version of the generator, where you can break things and see which check would catch you.
Playground A: the coupled shoulder limit
Section titled “Playground A: the coupled shoulder limit”Marvin Bot’s shoulder roll may go below 0° (bringing the hands to the midline, for clapping or holding a box) only while the arm is swung forward; with the arm hanging, negative roll would push the upper arm into the torso. description/robot.yaml writes the lowest allowed roll as a piecewise-linear function of shoulder pitch (a graph made of straight-line pieces joined at corners). Drag the dot, or use the sliders.
In plain words: shoulder pitch swings the arm forwards and up (0° = hanging, 90° = pointing straight ahead); shoulder roll lifts the arm out sideways (positive) or brings it in across the chest (negative). Start with the Clap preset.
From first principles
Section titled “From first principles”Links, joints and the kinematic tree
Section titled “Links, joints and the kinematic tree”A humanoid is a tree of rigid links. The root is the pelvis; each joint connects a parent link to a child link at a fixed offset (the joint’s origin) and lets the child rotate about one axis. Knowing every joint angle and walking down the tree, multiplying the transforms, gives every link’s position: this is forward kinematics.[9] Marvin Bot has 27 powered joints: 6 per leg, 6 per arm, waist yaw, neck yaw and neck pitch.
Inertia: what the dynamics need
Section titled “Inertia: what the dynamics need”To simulate motion, not just draw it, each link needs three things: its mass m (kg), the position of its centre of mass, and its 3 × 3 inertia tensor I (kg·m²), which says how hard it is to spin the link about each axis. For a solid box of mass m and sides a, b, c, the inertia about the axis along a is m(b² + c²)/12. When several parts share a link, their inertias are added after shifting each to the common centre of mass with the parallel-axis theorem (I + m d², d the shift). tools/build_urdf.py does exactly this, in its combine() function, for the CAD parts, the estimated boxes and the motor housings on each link.
Collision and visual geometry
Section titled “Collision and visual geometry”A link can carry two kinds of shape. Visual geometry is only drawn; it can be a detailed mesh. Collision geometry is what the physics engine tests for contact, every step, for every robot. It should be simple (boxes, capsules, convex hulls) because contact detection is a large share of simulation cost and detailed meshes make contacts noisy. Marvin Bot’s collision model is 21 boxes (13 in skeleton.yaml, the left-side ones mirrored): feet, shins, thighs, pelvis, torso, head, arms and hands; its visuals are the CAD meshes.
URDF: the common denominator
Section titled “URDF: the common denominator”URDF (Unified Robot Description Format) is the XML format of ROS.[1][2] A <link> holds <inertial> (origin, mass, the six independent inertia values ixx ixy ixz iyy iyz izz), any number of <visual> and <collision> elements. A <joint> names its type (revolute for a hinge with limits), its parent and child links, an <origin>, an <axis> and a <limit> with lower and upper in radians, effort (maximum torque, N·m) and velocity (maximum speed, rad/s); for a revolute joint the limit element is required.[1] URDF has no place for motor gains, rotor inertia, contact softness or a limit that depends on another joint.
MJCF: MuJoCo’s own format
Section titled “MJCF: MuJoCo’s own format”MJCF is MuJoCo’s XML format.[3] Bodies nest inside each other (the tree is the XML nesting), a hinge <joint> lives inside the child body, and actuators are separate elements: <motor> applies a commanded torque, <position> is a built-in PD servo (a spring-and-damper pull towards a target angle, see page 15) with gains kp and kv. MJCF adds what URDF lacks:
armature: extra inertia added to a joint, used for the spinning motor rotor that the gearbox makes feel heavier (page 15);frictionloss: dry joint friction, andactuatorfrcrange: the most torque a motor may apply;contype/conaffinity: per-shape on/off switches that decide which shapes may collide;solref,solimp: how soft or hard contacts feel.
MuJoCo also loads URDF directly; angles in MJCF are degrees by default unless <compiler angle="radian"/> is given, while URDF is always radians.[3][4] MuJoCo has no joint velocity limit: speed has to be limited in the motor model.
USD: Isaac Sim’s scene format
Section titled “USD: Isaac Sim’s scene format”USD (Universal Scene Description, from Pixar, now OpenUSD) is a general 3-D scene format; NVIDIA’s physics schemas add rigid bodies, joints and articulations to it.[6] Isaac Lab ships a URDF converter that runs Isaac Sim’s URDF importer and writes USD.[7] Since Isaac Lab 3.0 most workflows run without Isaac Sim, but the importer still needs it.[8]
The sign convention
Section titled “The sign convention”“Knee 15°” means nothing until you say which way is positive. robot.yaml uses anatomical names on the left side: pitch + = flexion (thigh or arm forward, knee bent, toes up), roll + = abduction (limb away from the midline), yaw + = towards the midline (toes or hand turning in). The physical axis is then chosen in skeleton.yaml so that the right-hand rule gives that direction. (Right-hand rule: point your right thumb along the axis; your curled fingers show which way a positive angle turns.) The knee’s axis is +y (rotating the shin about +y swings the foot backwards), the hip pitch’s is −y (the thigh swings forward), a left roll axis is +x and a left yaw axis is −z.
The mirror rule
Section titled “The mirror rule”The right side is generated from the left by negating every y coordinate. For an axis that is a little subtler: a mirror flips the handedness of rotations, so pitch axes (along y) stay as they are, while roll (x) and yaw (z) axes are negated (mirror_axis() in build_urdf.py). The result: “left knee 30°” and “right knee 30°” both bend the knee, and “hip roll +10°” moves either leg outward.
Why care? Training doubles its data by mirroring: every left-leg step it sees can also be used as a right-leg step. Because the axes are mirrored, training can mirror a pose very simply: swap the left and right joint values unchanged, and negate only the centre-line yaw joints, waist yaw and neck yaw, which have no partner (FLIP_SIGN in sim/isaaclab/marvin_bot/tasks/symmetry.py). A limb axis mirrored wrongly breaks that rule silently: nothing crashes, but training is quietly fed mirrored poses that are wrong.
sim/mujoco/check_symmetry.py checks this with 20 random poses: it mirrors each pose by the rule, computes every link position with MuJoCo, and requires each link to land on its partner’s mirror image within 1 µm.
The coupled limit, and why simulators ignore it
Section titled “The coupled limit, and why simulators ignore it”Every format above stores a joint’s range as two numbers, independent of the other joints. Shoulder roll’s real limit depends on shoulder pitch, so the formats cannot hold it. What each piece of software does:
- Simulators (MuJoCo, Isaac Lab on Newton or PhysX) use −20° as a plain lower limit and rely on the collision boxes: an arm pushed into the chest meets the torso box and stops.
- The CAD check (
cad.covers --check) tests every point of the bound and the midpoints between them for part clearance. - The controller on the robot must clamp commands to the bound before they reach the motors, because collision boxes are only approximate and the real shell is not a box.
The lowest roll at pitch p is linear between the points (20°, 0°), (45°, −10°), (75°, −10°), (90°, −20°), (135°, −20°), (150°, 0°) and 0° outside them. For example at p = 60°: −10°; at p = 30°: 0 + (30 − 20)/(45 − 20) × (−10) = −4°.
Playground B: one joint from YAML to URDF and MJCF
Section titled “Playground B: one joint from YAML to URDF and MJCF”Pick a joint, then edit its entries in the mini description on the left. The panel on the right regenerates the URDF joint (as tools/build_urdf.py writes it, both sides) and the MuJoCo version, and lists the checks the repo’s tools would run on it. The highlighted fields are editable.
What the less obvious fields mean: model is which motor drives the joint (page 04); bus is which communication cable it hangs on (page 10); axis is the direction the joint turns about; kp, kd are the stiffness and damping of the joint’s controller, armature the motor’s spinning inertia and friction its dry friction (all on page 15). Start with A broken one: the Checks list goes red, and each line tells you which repo tool would catch the mistake. Green = passes, amber = suspicious, red = the build would fail.
How Marvin Bot does it
Section titled “How Marvin Bot does it”Trade-offs and alternatives
Section titled “Trade-offs and alternatives”| Approach | For | Against |
|---|---|---|
| Hand-written URDF (common in open-source robots) | No tooling; every ROS tool reads it | Masses, limits and motors copied by hand; nothing stops the URDF disagreeing with the CAD or the BOM |
| URDF exported from a CAD package | Inertia straight from the CAD | Needs the CAD tool; motor data and limits are added by hand afterwards; regenerating loses hand edits |
| Hand-written MJCF as master | MJCF holds more (armature, contact, actuators) | Isaac Lab and ROS want URDF or USD; converting MJCF the other way loses information |
| YAML source + generated URDF (Marvin Bot) | One place to change; generator checks lengths and masses; CI fails if anything is stale; BOM and CAN load are checked against the same file | A custom generator to maintain; simulator-specific details (gains, armature, contact) live in code next to each simulator |
| Xacro macros (ROS) | Standard, mirrors left/right with parameters | Still a URDF dialect; no link to the motor data, BOM or checks |
The coupled limit is a smaller trade-off of the same kind. Writing it as a plain −30° limit was tried and rejected: at −30° the shoulder yaw motor meets the chest shell at every forward angle (docs/decisions.md). Enforcing the bound in the simulator itself (for example with a custom constraint) is possible in MuJoCo but would have to be repeated for each backend; the repo instead relies on the collision boxes in simulation and leaves the exact clamp to the controller.
Decisions you can make here
Section titled “Decisions you can make here”- Where a new constraint lives: in the description (and so in every model) or in one simulator’s code. Rule of thumb: geometry, mass and limits in
description/; gains, contact and solver settings beside each simulator. The trade: putting it in the description means every model gets it automatically, but the generator has to learn a new field; putting it in one simulator is quicker but the others will not know about it. - Whether a joint needs a coupled limit, and who enforces it on hardware (the controller’s command clamp) and in training (collisions, or a penalty). The trade: a plain limit of 0° (no roll inwards at all) is simple and safe everywhere but takes away the clapping and box-holding poses; the coupled limit keeps them but someone must write and test the clamp.
- Whether a new joint’s sign follows the anatomical convention, and whether it belongs in
FLIP_SIGN. Breaking the convention costs nothing today but makes every future mirror, retarget and log harder to read.
Built from your settings above. Paste it into an issue, a design discussion or an AI assistant session.
Further reading
Section titled “Further reading”- ROS wiki: URDF XML specification: short pages per element; read joint and link before editing any URDF.
- MuJoCo documentation: Modeling: how MJCF is organised, defaults classes, and how MuJoCo reads URDF.
- Isaac Lab: importing a new asset: the URDF and MJCF converters and their options.
- Lynch and Park, Modern Robotics, chapters 4 and 8: forward kinematics and rigid-body inertia, with free videos.
Sources
Section titled “Sources”- ROS wiki, urdf/XML/joint — used for: joint types, origin, axis, limit attributes (lower, upper, effort, velocity) and units.
- ROS wiki, urdf/XML/link — used for: inertial, visual and collision elements.
- MuJoCo documentation, XML Reference — used for: joint armature, frictionloss, actuatorfrcrange, geom contype/conaffinity, position and motor actuators, compiler angle.
- MuJoCo documentation, Modeling — used for: MuJoCo loading URDF and the URDF extensions.
- RobStride, Product_Information — used for: peak torque and no-load speed behind the effort and velocity limits (via
description/actuators.yaml). - OpenUSD, Introduction to USD — used for: what USD is.
- Isaac Lab documentation, Importing a New Asset — used for: the URDF converter.
- Isaac Lab v3.0.0 release notes (2026-10-20) — used for: Isaac Sim optional in Isaac Lab 3.0, physics backends.
- K. M. Lynch and F. C. Park, Modern Robotics, Cambridge University Press 2017 — used for: forward kinematics, rigid-body inertia and the parallel-axis theorem.