Skip to content

Output contract ​

A target turns the resolved program and its facts into the files a robot stack runs. This page fixes what every target's output guarantees, and then the files of the first target, ros-gz-nav2, of ros-mujoco-nav2, and of mjcf, the robot alone for MuJoCo.

Rules for every target ​

One directory, runnable by path. --output DIR receives an ordinary directory. The user builds nothing and installs nothing to run it; the entry point is a launch file inside it, or for mjcf the model MuJoCo opens. A file of the same name is replaced, never written through, so a link in its place is replaced and its target left alone; other files in the directory are left alone. The compiler refuses an output directory that is, or contains, a source file or a library, and a directory inside it that is a symbolic link (environment.output_linked).

One authoritative file per fact. A fact appears in the one file whose consumer needs it, and every other file that needs the same fact is derived from the same analysis value at compile time, never re-derived at run time. The table in each target's section says which file owns which fact.

No absolute paths. A file on disk never contains the output directory's path, so the directory can be moved or mounted elsewhere. Where a consumer demands an absolute path at run time (a simulator plugin that opens a parameter file by name), the file holds the token ${OUTPUT_DIR} and the launch file substitutes the real path when it starts the process.

Deterministic. The same program, libraries and compiler version produce byte-identical output: no timestamps, no host names, no random ids.

Execution input and inspection output are separate. The runtime reads only the files under robot/, world/, nav/, task/, launch/ and runtime/, and of the mjcf target's output only scene.xml, drive.json and, for a legged base that walks by a policy, the policy beside them that drive.json names (policy.onnx). compile.json is for people and tools: the facts, the validation results, the approximations the target made, and the sources and parts the output came from. Deleting it changes nothing at run time.

Approximations are declared. Where the target stands in for something it cannot simulate as such (a mecanum wheel as a sphere with anisotropic friction, a lidar with a default sample count), it says so in compile.json under approximations, with what was approximated, how, and why. Nothing is approximated silently, and nothing the target cannot do is emitted as if it could: an unsupported construct is a target.* diagnostic that stops emission.

The task stays goal-agnostic in the tree and goal-bound in the mission. The behavior tree names what to do; the mission file holds the poses the tree's goals resolve to. A target's own executor reads both.

ros-gz-nav2 ​

ROS 2 Jazzy, Gazebo Harmonic, ros2_control, and Nav2. The output is:

text
<output>/
├── robot/
│   ├── robot.urdf            links, joints, inertials, visuals, collisions,
│   │                         ros2_control interfaces, Gazebo sensor and friction blocks
│   ├── controllers.yaml      controller_manager, the drive controller, joint states;
│   │                         not for a base on tracks, which has no joint to control
│   ├── meshes/<part-id>/     the visual meshes, copied from the libraries, each with the
│   │                         files that lie beside it there
│   └── hulls/                <part-id>_hull.stl: each declared hull, as a binary STL,
│                             when a part declares one
├── world/
│   ├── world.sdf             physics, lighting, ground plane, scene entities
│   ├── meshes/<object-id>/   the meshes of the library objects the world places, copied
│   │                         from the libraries with the files beside them; the world
│   │                         names each by its path from here
│   └── bridge.yaml           ros_gz_bridge topics: clock, sensors, a tracked base's motion
├── nav/
│   ├── nav2.base.yaml        plugin choices and constants
│   ├── nav2.robot.yaml       what the robot decides: footprint, inflation, kinematics, AMCL start
│   ├── map.pgm               the occupancy grid drawn from the world's obstacles
│   └── map.yaml              its resolution and origin, for the map server
├── task/
│   ├── behavior_tree.xml     the task as a BehaviorTree.CPP tree over the vocabulary's actions
│   ├── mission.json          every goal and entity by name, the spawn, the time budget
│   └── evaluation.json       the checks, and the planar shapes of the parts and entities
├── launch/
│   └── bringup.launch.py     the entry point: simulator, robot, controllers, navigation, task
├── runtime/                  the task executor and its adapters, copied here; needs only ROS 2
└── compile.json              inspection: facts, validation, approximations, provenance;
                              the library and id of each part, of each placed object and
                              of each environment a scenario takes

Nav2 reads parameter files in order with later files winning, so the launch file passes nav2.base.yaml then nav2.robot.yaml. Everything the robot decides is in the second file and nowhere else.

Status. Every file above is emitted. The launch brings up the simulator, the robot, the drive controller, Nav2 and the task runtime, in that order, and shuts everything down when the task ends (exit_on_task_end:=false keeps it running; navigation:=false brings up only the simulator, the robot and its drive controller, to drive it by hand on /cmd_vel). It also ends the run, and says what stopped, when a process the base is driven through ends before the launch does: the runtime's drive node, or the bridge, through which the controllers get the clock a command's lifetime is measured on. Nothing after these ends a command, so a base whose drive had stopped would run on at its last speeds; a run ended this way writes no result. Gazebo itself takes about five seconds to end. The task runtime writes run/task_result.json under the output directory (run_dir:=DIR moves it); run/ is never written by the compiler, so a rerun of the compiler leaves it alone. Navigation localizes on the map drawn from the program's world, nav/map.yaml, which the map server serves and the global costmap's static layer reads: a robot with a scan is localized against it by AMCL from the spawn pose; one without has the world frame map meet odom at the spawn pose through a static transform.

Frames and topics ​

  • base_footprint is the root link: the ground projection of the robot, the frame the spawn pose places, and the controller's base frame. base_link is the chassis part frame; every other link is named after its component and its joint <component>_joint, and hangs from the link of the part it is mated onto: a wheel from its motor, a motor from the chassis. A wheel joint carries the limits its motor's part states, its stall torque as the effort and its no-load speed as the velocity. A wheel that a motor turns through a driveline of the chassis hangs from the chassis; its joint carries the motor's speed over the driveline's reduction and an equal share of its torque times the reduction, or no limit when the driveline states none. A track's unit is a fixed link whose one collision is the box of its belt: as long as the belt's run on the ground and as wide as the belt, standing on the run, as tall as a wheel that travels as far per turn as the belt. No joint and no further link is made for a belt.
  • The base takes geometry_msgs/msg/TwistStamped on /cmd_vel, whatever the controller; it publishes odometry on /odom and odom -> base_footprint on /tf. A stale command (older than 0.5 s) stops the base. Mecanum, two-wheel differential, skid-steer, tricycle, Ackermann and omni bases are bound to the matching ros2_control controllers; the part on a steered mount, a motor or a free wheel, hangs from a kingpin joint named <component>_steer_joint, and wheels that only roll are observed but not commanded. Every wheel the program drives is commanded: a binding that left one out is refused. An omni base is bound to velocity_controllers/JointGroupVelocityController, and runtime/run_omni_drive.py turns /cmd_vel into the wheel speeds from each wheel's position and rolling direction (robot/omni_drive.json) and publishes the odometry from the measured wheel speeds; ros2_controllers' omni controller models only wheels evenly spaced on a circle, and the catalog's four-wheel omni bases sit on rectangles. An Ackermann base is bound to the same velocity group for its driven wheels, two or four, and to position_controllers/JointGroupPositionController, spawned as steering_controller, for its kingpins; runtime/run_steering_drive.py turns /cmd_vel into each driven wheel's speed along its own circle about the turning centre and each kingpin's angle (robot/steering_drive.json), and publishes the odometry from the rear wheels' measured speeds. ros2_controllers' Ackermann controller takes exactly two traction joints. A tracked base has no joint, so the description holds no <ros2_control> block, robot/controllers.yaml is not written and the launch starts no controller manager and spawns no controller. runtime/run_track_drive.py holds /cmd_vel to what the two belts' motors reach, each belt running at the speed of its side, a half separation from the chassis' origin (robot/track_drive.json); it publishes the motion the belts then carry the base in on /tracks/cmd_vel (geometry_msgs/msg/Twist) and the odometry that motion stands for. The bridge hands that topic to Gazebo's TrackedVehicle system, which tells the TrackController on each belt's link its speed and the point the base turns about; a belt stops when nothing has reached it for 0.5 s. A chassis whose origin does not lie midway between its two tracks, within 2 mm, is refused.
  • A base that steers plans with Smac Hybrid-A* at its minimum turning radius and follows with MPPI's Ackermann model; it is not held to a final heading, since it cannot turn in place to fix one. The radius is the tightest circle the middle of the unsteered axle follows with every kingpin within its limit: for each kingpin, its distance ahead of that axle over the tangent of its limit, plus its distance from the centre line.
  • Sensors publish under their role name: the first laser on /scan, the first spinning lidar on /lidar3d/scan (its middle ring) and /lidar3d/points, the first IMU on /imu, the first camera on /camera/image_raw and /camera/camera_info, the first depth camera on /depth/image_raw and /depth/points, the first RGBD camera on /rgbd/image_raw, /rgbd/depth/image_raw, /rgbd/points and /rgbd/camera_info, the first ultrasonic range finder on /ultrasonic/range (its cone of beams on /ultrasonic/points), the first range finder that measures with light (Range) on /range (its cone on /range/points), the first bumper on /bumper (ros_gz_interfaces/msg/Contacts); a second sensor of a kind is suffixed with its component name. Frames are the component names.
  • The costmaps' footprint is the box of the robot's collision shapes: the analysis footprint, widened to each wheel's sphere (below), which reaches further across the axle than the wheel's width.
  • The costmaps mark what every point cloud sees, points 0.05 to 2 m above the ground, and with the localizer read one scan. It is the first laser's; without a laser it is the first spinning lidar's middle ring; a robot with neither but with a depth or RGBD camera gets /scan from runtime/run_depth_scan.py, which projects the cloud onto the ground plane in base_footprint (points 0.05 to 2 m high, within 5 m, 360 beams, beams outside the camera's view infinite). Range finders feed the costmaps' range layer: runtime/run_range_sensor.py reports each cone's nearest return as a sensor_msgs/msg/Range, ULTRASOUND or INFRARED, and a reading at the maximum range clears the cone. An ultrasonic sensor's returns less than 0.05 m above the ground are dropped first (every simulated beam returns, a pulse glancing off the floor does not); a Range sensor keeps them, since light comes back from the floor. A Range sensor whose limits are equal says only whether something is within that distance: it reports -inf or +inf, and the range layer marks or clears its cone. A robot with no range sensor is warned that it navigates blind.
  • The simulator publishes the model pose on /model/<robot>/pose as tf2_msgs/msg/TFMessage. Tests and the evaluator judge a run by this, never by odometry.

Who owns which fact ​

FactAuthoritative fileAlso derived into
Link and joint geometry, inertialsrobot/robot.urdf—
Wheel radius, track width, wheelbaserobot/controllers.yaml (the controller's kinematics); for a runtime drive its layout, robot/omni_drive.json, robot/steering_drive.json or robot/track_drive.jsonnav2.robot.yaml velocity limits
Footprintnav/nav2.robot.yaml (both costmaps, once each)—
Wheel joint names and command interfacesrobot/robot.urdf (<ros2_control>)controllers.yaml names the same joints
Sensor topics and framesrobot/robot.urdf (<gazebo> sensor blocks)world/bridge.yaml, nav2.robot.yaml observation sources
Spawn posetask/mission.jsonthe launch file's spawn arguments; AMCL's initial pose in nav2.robot.yaml
Obstacle positions, sizes and, for a library object, the shapes it collides byworld/world.sdfnav/map.pgm rasterized from the same values; task/evaluation.json
Goal and entity positionstask/mission.jsonnav/map.pgm's extent; task/evaluation.json's entity regions
Gravity, scene entitiesworld/world.sdf—
Time budgettask/mission.json—

What the launch file substitutes at run time ​

  • ${OUTPUT_DIR} in robot/robot.urdf (the path of controllers.yaml for gz_ros2_control, and the mesh paths). world/world.sdf holds nothing to substitute: it names an object's mesh by its path from the world file, meshes/<object-id>/<file>, which Gazebo resolves against that file.
  • The path of nav/map.yaml, passed to the map server.
  • The spawn pose, read from task/mission.json, for the simulator and, on a robot without a laser, for the map -> odom transform.

Nothing else is computed at launch; the launch file is a fixed program over the files above. Nav2's own default behavior tree runs Navigate; the program's tree is not handed to bt_navigator.

The task ​

task/behavior_tree.xml is the task in BehaviorTree.CPP's XML layout with the language's node names as tags and its parameter names as attributes, so it reads like the program and opens in Groot. A goal is written by name (<Navigate goal="dock"/>) and resolved through task/mission.json, which holds every goal and every entity, since Navigate and GoalReached take either. A task whose body has several nodes runs them in order, so the body is wrapped in a Sequence.

runtime/run_task.py runs the tree. It needs ROS 2 and, when the tree uses Navigate, Spin, Advance or BackUp, the task waits for a Nav2 costmap or the runtime starts Nav2's servers, the nav2_msgs package; nothing from the compiler. Each leaf is bound as follows.

NodeBound to
NavigateNav2 navigate_to_pose, goal in the map frame
SpinNav2 spin
Advance, BackUpNav2 drive_on_heading, backup; 0.15 m/s unless speed_mps is given
Strafe/cmd_vel with odometry feedback (Nav2 has no lateral behavior)
Waitsimulation time
Stopzero velocity on /cmd_vel for half a second
GoalReachedmap -> base_footprint within the goal tolerance of nav2.base.yaml
DistanceTraveledodometry path length since the task started
TimeExpiredsimulation time since the task started

The launch starts Nav2's lifecycle manager without autostart. The task runtime asks it to start the servers, 3 s of wall time or more after the manager first answered it (run_task.py --start-lifecycle, --start-after), and waits until the manager reports them active. The manager answers once it has made its clients of the servers, and the wait gives the servers more time to discover those clients. rmw_fastrtps gives a server's reply up when the manager's client has not matched it within the time the reply may block, and Nav2 Jazzy's manager waits for a change_state reply without a limit. The 3 s are a measured remedy, not a guarantee. After a start that failed the manager reports its servers not active, so the task does not start. When the runtime receives the manager's answer that it could not start its servers, the run ends with status not_started and says so, without waiting for the limit below.

The task starts once the drive's odometry and the robot's pose in the map have arrived, Nav2's lifecycle manager reports its servers active, and the task has received from the local and the global costmap their map and the robot's footprint, which Nav2 checks a goal against (so that Nav2's servers most likely have them too); one that cannot start within 120 s of simulation time ends with status not_started and says what it waited for. The tree ticks at ten hertz. The mission's timeout_s ends the run with status timeout. run/task_result.json records the status (success, failure, timeout, error, not_started), the start and end times, one event per leaf with its parameters and outcome, the final pose the robot believes it has, and the odometry distance. It is the robot's own account. A halted Nav2 action is canceled at once, and before the run is judged the runtime waits up to 5 s for the goal to stop; a goal that has not stopped by then is named in unstopped_actions.

The evaluation ​

While the task runs, the runtime records the robot's pose as the simulator publishes it, the joint states, and every bumper's contact reports. When the task ends it writes run/evaluation.json with three verdicts that rest on different evidence:

  • task: the status the task runtime reported.
  • physical: from the recorded ground truth alone. Whether the robot ended near the last goal it navigated to (within twice the controller's goal tolerance, since the controller stops on its belief and the ground truth differs from it by the localization error), and its closest approach to any Obstacle over the run; touching one (within 5 mm) fails.
  • checks: each check of the .eval file, pass, fail or unknown, with the value it was judged on. Check_vel, Check_acc, Check_jerk and Check_effort read the joint states of the named wheel and judge every sample by magnitude; Check_distance and Check_clearance use the recorded poses and the planar shapes of task/evaluation.json; Check_contact on a Bumper reads the bumper's own reports (the seconds it was pressed and the scene entities that pressed it), on any other part the poses and shapes; Check_time uses the task's duration. The vocabulary states what each bound means.

The shapes are the collision geometry the robot is simulated with, seen from above: the chassis and sensor collision boxes, each wheel and each ground contact a part declares as a circle of its radius (both are spheres in the simulator), a part without a collision box as a point, and each scene entity as the regions its shapes cover seen from above at its full pose, so that a cylinder lying on its side covers its length: one region for a primitive, and for a library object one for each shape it collides by, a robot being as near to it as to the nearest of them. An object that declares no collision has no region and is not listed. The map is drawn from the same regions. A part's ground contacts count as that part: a check on the chassis also measures the skid under it. Nothing is read from odometry or from Nav2. A check on a part without a moving joint is refused at compile time; a check that has no samples to judge is unknown, never pass.

Approximations this target makes ​

Fact in the programWhat the target emitsWhy
A mecanum wheelA sphere collision of the wheel's radius with mu = 1, mu2 = 0 and fdir1 along the axis of the roller under the hub, expressed in base_footprint: (1, -1, 0) for the front-left and rear-right mounts, (1, 1, 0) for the other two. It grips along that axis and slides freely across it, as the roller turnsGazebo's DART engine has no roller model; the sphere gives one contact point, and the friction direction gives the roller its axis
A driven standard wheel on a two-wheel baseA sphere collision of the wheel's radius with isotropic friction mu = mu2 = 3One contact point keeps the controller's odometry honest; a two-wheel base turns by wheel speed difference and needs lateral grip
An omni wheelA sphere collision of the wheel's radius with mu = 1 along its rolling direction (the declared axle turned a quarter turn about up, expressed in base_footprint) and mu2 = 0 along its axleThe rollers along the rim let the wheel slide along its axle; the sphere gives one contact point
A driven standard wheel on a skid-steer baseA sphere collision with mu = 1 across its rolling direction (fdir1 along its axle, 0 1 0 in base_footprint) and mu2 = 3 along it, and Gazebo's WheelSlip system with a sideways slip compliance of 2, multiplied by (wheelbase / track)² for a base longer than it is wide, at an equal share of the robot's weightA skid-steer base turns by dragging its wheels sideways. With rigid sideways grip it swung about its more heavily loaded axle; a sideways force that grows with the slip, scaled alike for every wheel, lets it turn about its middle while every wheel carries at least half the average load. WheelSlip takes the first friction direction to be the axle. The compliance is the previous compiler's (icra2027-submission)
A passive wheel (caster)A sphere collision with mu = mu2 = 0.01A support must slide in every direction
The ground plane and each SupportSurfaceA collision with mu = mu2 = 3, the highest coefficient the target's contact models stateGazebo's DART engine gives a contact the smaller of its two surfaces' coefficients, so a floor at the default 1 would hold every wheel to 1 and the AgileX LIMO PRO, a four-wheel skid-steer base 200 mm long and 173 mm wide, did not turn in place
A trackA box on its run on the ground, as long as the run, as wide as the belt and as tall as a wheel that travels as far per turn as the belt, whose surface moves to carry the base and grips with mu = mu2 = 1; the rest of the unit collides with nothingThe library states the belt's run, width and travel, not its shape around its wheels nor its grip
A track's driveA belt held to the speed its motor reaches unloaded; where the speed it can reach is not known (no Motor turns it, or its driveline states no reduction), a belt whose speed nothing boundsA moving contact surface takes a speed, not a force: the belt's pull is bounded by the floor's grip, not by its motor's stall torque
A tracked baseA base whose belts carry it about the centre its command turns it about, without scrubbing sideways as they turn it; its odometry is made from the speeds its belts are givenHow far a belt slips sideways in a turn follows from its pressure along its run and its grip, which the library does not state; neither simulator gives a belt a turning hub to measure
A tracked base, in GazeboA base that turns in place short of its command when its tracks' runs are long for the distance between them: 0.83 of it with runs of 0.15 m that lie 0.16 m apart, 1.02 with runs of 0.10 mGazebo's TrackController moves every contact of a belt at the belt's one speed about the centre of rotation, so a contact further from that centre than the middle of its belt drags
A ground contact a part declares (contacts)A sphere collision of its radius on a fixed, massless link of its own, <link>_contact_<n>, with mu = mu2 = 0.01; the footprint reaches around itLike a caster, a support must slide in every direction; the statics rule counts it, so the simulator has it
A steered wheelA 10 g knuckle link between the chassis and the wheel, turning about the declared kingpin within its limit, with an effort limit of 50 N m and a speed limit of 3 rad/sThe library declares the kingpin's axis and limit but no knuckle body or steering actuator
An Ackermann baseEach driven wheel held to the speed of its own path about the turning centre, and each kingpin to its own angle, by the runtime's steering driveThe library states which wheels a motor turns and how far a kingpin turns, not the differential and the steering linkage that join them
A wheel one motor turns together with others through a drivelineA wheel joint limited to an equal share of the motor's stall torque times the driveline's reductionThe chassis part states the driveline's reduction, not how its differential shares the torque between the wheels
A wheel on a driveline that states no reductionA wheel joint without effort or velocity limitsWhat the motor can do at the wheel is not known
A laserA gpu_lidar with 360 beams over the full circle, 0.3 to 12 m, at 10 HzThe library does not yet state sensor specifications
A spinning lidarA gpu_lidar with 16 rings of 360 beams over 0.52 rad, 0.3 to 20 m, at 10 HzAs above
An IMUAn ideal imu sensor at 100 HzAs above
A cameraA 320 by 240 camera, 1.089 rad field of view, at 10 HzAs above
A depth cameraA 160 by 120 depth_camera, 1.089 rad field of view, 0.1 to 5 m, at 10 HzAs above; at 320 by 240 the clouds overran ROS transport and navigation stopped on stale transforms
An RGBD cameraA 160 by 120 rgbd_camera, 1.089 rad field of view, 0.1 to 5 m, at 10 HzAs above
An ultrasonic range finderA gpu_lidar of 9 by 9 beams filling a 0.52 rad cone, 0.02 to 4 m, at 20 Hz, whose nearest return is the reading; the cone, limits and rate its part states, when it doesThe simulator has no range finder
A range finder that measures with light (Range)A gpu_lidar of 9 by 9 beams filling the cone its part states, from its minimum (from 0.01 m for a fixed-distance sensor or a minimum of zero) to its maximum, at its rate, whose nearest return is the readingThe simulator has no range finder
An ultrasonic range finder's readingMade at run time from the cone's returns more than 0.05 m above the groundA pulse glancing off the floor does not echo; every simulated beam returns
A bumperAn ideal contact sensor over its collision box, read at 50 Hz; the bumper keeps its own link so that the sensor can name the collisionThe library does not state the sensor's specification
A spinning lidar as the only planar range sensorIts middle ring as the scan the costmaps and the localizer readNav2 and AMCL read laser scans
A depth or RGBD camera as the only range sensorIts cloud projected onto a planar scan for the costmaps and the localizerAs above
A part without a collision boxIts visual mesh as collision geometryNothing else is known

The friction values are the ones the previous compiler drove its experiments with. Each approximation is listed in compile.json for the component that triggered it.

Not emitted ​

A program that uses any of these stops with a target.unsupported diagnostic naming the construct, and nothing is written: an omni base whose rolling directions do not span the plane, a steering base whose chassis declares no kingpin, a scene entity declared movable (is_static = false: an entity states no mass to move it with), a joint check on a part without a moving joint (a track among them: its belt is a moving surface), a part with links of its own, which of the targets only mjcf writes, a moving joint (revolute, prismatic or continuous) on a connector a mating uses, other than a wheel's own: on the mating part's own connector unless the part is a wheel, or on the connector it mates onto, either of which the robot description would hold fixed, a track whose part states no travel per turn or whose run does not lie along the robot, a tracked base whose tracks do not lie equally far to the two sides of its chassis' origin, a component named like a link the target adds: base_footprint, base_link, a declared ground contact's <link>_contact_<n> or a steered wheel's knuckle <wheel>_steer, and a scene entity named like another model of the world: the robot, or ground_plane when the scenario has a floor.

The ros-mujoco-nav2 target ​

The second target keeps everything above that is ROS: the robot description (robot/robot.urdf, with a <mujoco> compiler block MuJoCo's URDF reader takes and none of Gazebo's blocks), the controllers, the navigation parameters, the map, the task files and the runtime, and swaps the simulator. Its world/ holds mujoco.json instead of an SDF and a bridge: the ground, the world's light (the Gazebo world's, below), the scene entities (each a body <name>::link, so that no entity name can meet a link's in MuJoCo's single namespace, with its shapes: the primitive a program states, or the shapes a library object collides by, each placed in the entity's frame; and for a library object drawn in glTF its visual, the mesh's file in the output and where it sits in the entity's frame, the mesh lying under world/meshes/<object-id>/ as in the Gazebo target), the actuators on the wheel and kingpin joints, a tracked base's belts and the topic their motion arrives on, the wheels' contact friction, the sensors with the topics they publish on, and the spawn pose. Each body weighs what the library's inertial states: the compiler block stops MuJoCo from inferring a mass from a link's collision shapes, so a link without an inertial (the root, a ground contact's) weighs only MuJoCo's 1 g bound. runtime/run_mujoco_sim.py loads the description and that scene, steps the physics paced to the wall clock, publishes /clock, and speaks to ros2_control through its topic-based hardware (/mujoco/joint_states, /mujoco/joint_commands). It publishes the robot's pose on /model/<robot>/pose and every sensor on the same topics as the Gazebo target, computed from the physics state: lasers and range finders cast rays, which return from what collides (the robot never sees itself, and the floor returns only to a Range sensor, which measures with light), cameras render offscreen (OSMesa by default, MUJOCO_GL=egl with a GPU) what is shown: the sky, the floor and each entity, by its shape or by its object's mesh, a mark that only shows among them, and never the robot's own collision shapes. A camera draws from the near end of its range to the far end and shows the sky beyond, as a Gazebo camera does, also in the colour picture of a camera that measures depth. The far end of a camera that measures depth is a distance from it, 5 m, and that of a plain camera a distance ahead of it, 10 m, as in Gazebo; an IMU reads the body's motion, a bumper reads the contacts. The launch takes the same arguments; headless has no effect.

Both targets light a world alike, since a program states no light: an ambient light of 0.4, a sun of 1 that shines along (-0.5, 0.1, -0.9) and casts shadows, a sky of (0.7, 0.75, 0.85), a floor of 0.8 grey and, for a box, a sphere or a cylinder a program states, a grey of 0.5. world/world.sdf states them as they are. Gazebo encodes its picture, each value raised to 1 / 2.2, and MuJoCo draws the numbers it is given, so world/mujoco.json holds the colours encoded (background, ground, shape, and gamma for the node to encode a mesh's material colours by) and the sun at half its strength, at which the robot's colour camera read one world alike in the two test images: the sky (217, 223, 235) and (214, 221, 234) of 255, the lit floor 188 to 209 and 196, a picture's red (76, 5, 8) and (79, 14, 15), a stated box's side away from the sun 71 and 74. The node draws the sun's shadows in four times the depth MuJoCo would, since the lit floor showed dark specks short of the horizon otherwise.

The node runs on MuJoCo 3.11 or later and says so on an older one. A tracked base is simulated through the surface velocity of a geom, which MuJoCo has had since 3.11: the node takes the motion on /tracks/cmd_vel and moves the surface of each belt's box as that motion requires at every point of it, along the run at the speed of its side and across it as the turn swings the point round, so that what touches the floor stands still on it. No controller manager runs for such a base.

The launch ends the run, and says what stopped, when the runtime's drive node or the controller manager ends before it does: a group controller holds the last command its drive gave it, and the MuJoCo node the last one the controller manager gave it.

A geom's friction in MuJoCo is the same in every direction, and the node writes no contact pairs, so a sphere cannot stand in for a mecanum or omni wheel, which slides freely across its rolling direction on its rollers: a base on such wheels is refused with target.unsupported and compiles for ros-gz-nav2 instead (the mjcf target writes a mecanum wheel as its rollers). A part that declares neither a collision box nor a hull is refused too, unless it is a wheel, which collides as a sphere of its radius, or a track, which collides as its belt's box: the Gazebo target collides such a part through its visual mesh, which the library ships as Collada, and MuJoCo reads meshes only as STL, OBJ or MSH. A library object that only shows, by a mesh that is not glTF, is refused as well: the node reads glTF and nothing else of a world's objects, and nothing of this one would be in the scene. The world's gravity and ground plane are the program's, as in Gazebo, and so is the refusal of an entity declared movable. A library environment is in neither target's files as such: its entities are the scenario's own by the time a target reads the world. Its own stand-ins, declared in compile.json:

ConstructEmittedReason
A wheelA sphere contact of the wheel's radius with isotropic friction 1, 0.01 for a wheel that does not rollMuJoCo's contact friction is isotropic; a rolling wheel grips, a caster slides
A ground contact a part declaresA sphere contact of its radius on the Gazebo target's fixed link, with isotropic friction 0.01Like a caster, a support slides in every direction
The driveA velocity actuator on each driven wheel, of gain 0.5 N m s/rad, or its share of the base's weight at its rim over 20 ms where that is more, its force clamped to the motor's stall torque and its command to the motor's no-load speed; each driven wheel's joint carrying 1e-4 kg m² of its motor's turning inertia; and a position actuator of gains 50 N m/rad and 2 N m s/rad on each kingpinA part states its motor's ratings, not the driver's loop gain or the motor's inertia behind its gearbox. At 0.5 the heaviest bases barely moved and did not turn; without the inertia a base on wheels of a few grams turned in place faster than it was told and, driven ahead at once, spun round
An ultrasonic range finderIts reading from the cone's nearest return, the floor never returningA pulse glancing off the floor does not echo; the rays skip it
A skid-steer baseA base that turns slower than commanded, the demo skid-steer base in place at about a third of the rate, its wheel odometry, taken from its wheels' turning, reporting the commanded turnMuJoCo's isotropic friction resists a wheel dragged across its rolling direction as much as it grips along it
A library objectIts mesh for the cameras, in its materials' base colours and the PNG pictures laid on them, without their emission and their other maps; the shapes it collides by for contact and for the lasers' and range finders' rays, not drawn where the mesh is. Of an object whose mesh is not glTF, those shapes, shown in the mesh's placeMuJoCo decodes STL, OBJ and MSH, not the glTF a library's objects are drawn in, so the node reads glTF itself and hands MuJoCo the triangles and pictures; its rays are cast at what collides
A mark that only shows, a Marker that does not collideIts shape or its object's mesh for the cameras; nothing of it for the lasers and range findersThe node casts its rays at what collides, where Gazebo's lasers and range finders see what is shown

The stand-ins of a driveline, of an Ackermann base and of a track and a tracked base are the Gazebo target's, since both targets share the robot description and the drive; the tracked base's shortfall in a turn is Gazebo's alone.

The test image is robotdsl/ros-mujoco-nav2:jazzy, built from docker/ros-mujoco-nav2/jazzy/Dockerfile.

The mjcf target ​

The third target writes the robot alone, for MuJoCo without ROS: the model MuJoCo opens by itself, its viewer or MuJoCo compiled to WebAssembly in a browser page, and what drives it. It is the robot to drive by hand.

FileWhat it holds
scene.xmlThe robot on its floor as one MJCF, which names no file
drive.jsonHow a base motion becomes each actuator's command or each belt's surface motion, or how a legged base's walking controller is called, which body each component and each of its parts' links is, and each servo
policy.onnxA legged base's policy, when it walks by one, copied as its part states it

scene.xml is the robot ros-mujoco-nav2 gives MuJoCo, as MuJoCo's URDF reader makes the robot description into a model under that target's compiler block, written out: a body for each link where its joint puts it, a hinge for each continuous or revolute joint, bounded in force by its limit's effort, each body's inertial as the robot description gives it, the steering knuckles it adds included, within MuJoCo's bounds on mass and inertia, and each collision as a geom, a declared hull as a mesh written in the model with its faces wound outward, as its STL has them. Around it is what that target's node adds at run time: a free joint at the world's spawn pose, the robot's geoms in group 1, which meet the world and not each other, the friction of each wheel, belt and ground contact, the world's gravity and its floor, the step of 1 ms, the implicit-fast integrator, a velocity actuator on each joint the drive turns and a position actuator on each kingpin it steers, named <joint>_actuator, and the node's lighting: its headlight, its sun, its sky and the depth of the sun's shadows. Of the world there is the floor and the gravity and nothing else; a sensor is its body, mass and collision, without its sensing; nothing of ROS, Nav2 or the task.

The tests build the model both ways, by this target and by that target's own node from its output, and compare in MuJoCo 3.15 every field of their bodies, joints, degrees of freedom, geoms, actuators, light and sky, their options and their drawing settings. Set aside are the world's entities, the cameras the node adds for the robot's sensors and the nearest distance it has them draw at, the light's offset from the world's centre of mass, which the entities weigh in, and the offsets that place an element in its model's arrays: of these, each body's parent, each joint's and each geom's body and each actuator's joint are compared by name, and the others, which MuJoCo derives from them, are left out. A mesh is compared by its triangles and their normals in its body's frame, since MuJoCo keeps a mesh in a frame it chooses by its shape. Each hull part's mesh is a file of its own, named by its part, so the two agree for any number of hull parts.

A mecanum wheel, which ros-mujoco-nav2 refuses, is written as its rollers: its body is the hub, with no shape of its own, and round it are its rollers, each turning freely on a hinge about its own axis that nothing drives. The roller under the hub lies at 45 degrees to the axle, on whichever of the two such lines the mount's roller axis as ros-gz-nav2 states it is nearer (seen from above, (1, -1) at the front-left and rear-right mounts, (1, 1) at the other two), and the others follow it round. A roller is a barrel, as a mecanum wheel's are: five spheres along its axis, each reaching the wheel's radius from the axle, so that the rollers keep to the wheel's radius but for the dips between their spheres. At its middle a roller's radius is a quarter of the wheel's radius or of the rollers' span, whichever is less; the rollers span 95% of the wheel's width along the axle; and there are as many as make their spans overlap round the rim by a tenth, twelve at the least. A wheel is refused whose rollers' end spheres would be less than a fifth of their middle radius (a wheel wider than about two thirds of its diameter, 0.666 of it), or that would need more than 32 rollers (one narrower than about a ninth of it, 0.108). Each roller weighs 1 g with a moment of 1e-6 kg m² about each axis, the least the compiler block takes, out of the wheel's mass; a wheel whose hub would be left less than 1 g is refused. The hub keeps the rest of the mass and the part's inertia, so the wheel turns about its axle a little heavier than the part states. The rollers meet the floor with a rolling wheel's friction, and not the robot; each roller's body and hinge are named <wheel's link>/roller_<n>, a name no component can have.

A part's own links, which ros-mujoco-nav2 refuses, are written below the part's body, whose inertial is the part's base alone, each after the link it hangs from: a body named <component>/<link>, a name no component can have, where its joint puts it; for a revolute joint, a hinge of that name about the joint's axis within its range (a limit MuJoCo holds as a soft constraint), with the damping, armature and frictionloss its servo states, and a position actuator, <component>/<link>_actuator, of the servo's kp, its force bounded by the servo's effort and its command by the joint's range, every number of the link written exactly (rounded, a narrow range closed to a point, which MuJoCo reads as no range, and a turn or an armature to nine digits was not the model a walking controller was trained on); a fixed joint, no joint; the link's inertial, a diagonal tensor as it is and any other as its principal moments about its principal axes (diaginertia and quat), so that MuJoCo does not decompose the tensor again (its decomposition of a thin plate's tensor turned off the axes broke the triangle inequality by a rounding, and it refused the model), a link lighter than 1 g, or a principal moment the decomposition finds less than 1e-6 kg m², written raised to those bounds, to which MuJoCo raises every body, and then the largest moment written as the sum of the other two where the decomposition's rounding put it above, since MuJoCo checks the numbers written; and its shapes, a box by its half sizes, a sphere by its radius, a cylinder by its radius and half its height and a capsule by its radius and half the distance between its ends' centres, in group 1 with the robot's other shapes, meeting the world and not the robot. MuJoCo starts a model at its joints' zero. When some links turn on servos, the keyframe hold has the free joint at the spawn pose, each servo's hinge and command at the servo's hold, written exactly, and every other joint and command at zero, so a program that resets to it (mj_resetDataKeyframe) starts the robot as its servos hold it when nothing commands them; a robot whose links are all fixed has no servo and no keyframe. A legged base is written to walk by the walking controller its part states, or by the walker trained for its body and its legs as they are placed; without either, to stand, the model a walker is trained on. No shape of the robot meets anything, the chassis's and every part's in group 1 with contype and conaffinity 0, and each foot meets the floor by a contact pair of its own, the foot's shape named after its part, the chassis or the leg it is a foot of, and the foot with a slash, of three dimensions, with the foot's friction sliding and MuJoCo's own for turning and rolling (0.005, 0.0001), as in the models walking policies are trained on. The model's free body is the chassis itself, as in those models, with no footprint body below it that MuJoCo's compiler would weigh at its least mass, and its inertial is written exactly, as its links' are. The keyframe stands the robot on its feet at the spawn pose. Under a policy, the model steps as the policy was trained, its <option> the controller's simulator (its step, written exactly, so that the period is a whole number of steps as the library states it, its integrator, the solver's iterations and line-search iterations, and Euler's implicit damping, off as a <flag>); the policy's IMU is a site named imu on its body, read by a velocimeter (imu_velocimeter) and a gyro (imu_gyro); and policy.onnx beside the model is the policy. Under a gait, and standing, the model steps as the target's other models do, has no IMU, and nothing goes beside it. A library's numbers are 0 or between 1e-100 and 1e30 in size, which keeps every number this target writes for a link finite and one MuJoCo's parser reads. Whether a robot whose quantities lie far beyond any real part's, a centre of mass 1e30 m away, steps in MuJoCo is not checked yet.

drive.json (robotdsl.mjcf_drive) names, by bodies, the body whose frame is each component's own (base_link for the chassis), and says how to drive the base:

  • "kinematics": "differential" (Diff_two_caster, Diff_drive), as ros-mujoco-nav2's diff_drive_controller drives it: each driven wheel turns at (v - w·y) / radius, its y half the wheel separation on the left and minus it on the right.

  • "kinematics": "steering" (Ackermann, Tricycle): the runtime's steering drive (runtime/mobiledsl_runtime/steering_kinematics.py in the ROS targets' output), from axle_x, min_turning_radius, the driven wheels (x, y, offset, radius) and the kingpins (x, y, limit, sign): each driven wheel at the speed of its own path about the turning centre and each kingpin at its own angle, the turn held to what the kingpins can steer. That is how ros-mujoco-nav2 drives a car. It drives a tricycle by tricycle_controller, which takes the front wheel on its kingpin's axis; here a tricycle is driven as a car is, a stand-in declared below.

  • "kinematics": "holonomic" (Mecanum_four_wheel), as ros-gz-nav2's mecanum_drive_controller drives it: each driven wheel turns at its coupling row times (v, v_left, w), the base's speed ahead and to the left (m/s) and its turn to the left (rad/s). The rows are the controller's, for wheels of radius r whose mounts lie a lever arm k (half the wheelbase plus half the track) from the middle along x and y: (1, -1, -k) / r front left, (1, 1, k) / r front right, (1, 1, -k) / r rear left and (1, -1, k) / r rear right.

  • "kinematics": "policy" (Legged): the walking controller its part states, or the walker its library trained for its body and legs. policy.joints are the servos in the order the policy reads and moves them: the chassis's, in the order of its links, or a walker's, in its order, each named by the program's component and link. policy.file, the policy beside the model, is called every policy.period seconds of simulated time, the model having started at the keyframe, with the terms of policy.observation one after another: the IMU's linear velocity in its own frame (the velocimeter policy.imu.velocimeter), its angular velocity (the gyro policy.imu.gyro), straight down in its frame (the transpose of the site policy.imu.site's turn applied to (0, 0, -1)); the command, ahead, to the left and turning left, within policy.command's ranges; each of policy.joints' angle from its servo's hold and its speed, those of policy.hidden read as 0; the action it gave last, zeros at first; and the gait's phase (policy.phase), the cosine of each foot's phase and then their sines, each phase starting at its offset and turned by 2π times the frequency times the period at each call, kept between -π and π, and read as still_at while the command's length is under still_below, turning on meanwhile. Each servo's target is its hold plus policy.action_scale times the action's term for its joint, in the order of policy.joints. A page steps the model in two halves, mj_step1, the targets, mj_step2, as MuJoCo's control callback would.

  • "kinematics": "stand" (Legged): a legged base without a walking controller, its part's own or a walker its library trained for its body and legs. The model starts at the keyframe, and its servos hold their holds; nothing drives it, and drive.json gives no command.

  • "kinematics": "gait" (Legged): the gait its part states, worked out from the model itself, as the tests' tests/gait_walker.py does. At the keyframe, after mj_forward, with p the chassis body's place (xpos), R its turn (xmat), g the size of gravity and h gait.standing, the chassis's height above the floor its feet stand on:

    • each foot of gait.feet (geom, joints from its part's frame down, friction, offset) has its geom's place f; its place in the chassis frame, home = R^T (f - p); its level, l = f_z - p_z + h, its geom's height above the floor; it is on the floor, planted at (f_x, f_y, l); and its leg's angles are its hinges' qpos. The offsets take the feet in turn about their middle as they stand, every other one half a period later;
    • Raibert's gain is k = sqrt(c / g), c the height of the robot's centre of mass (the chassis's subtree_com) above the floor, c_z - p_z + h;
    • the body is led from where it is: its place (p_x, p_y) and its yaw, atan2(R_10, R_00);
    • the gait has not started, the command it follows is zero, and the robot has settled (1).

    Then, with n = max(1, round(gait.control / timestep)), a half rounded to the even whole number as Python's round does, and the steps counted from 0 at the keyframe, at step 0 and at every n-th step after, between mj_step1 and mj_step2, at the step's time t, the servos' targets are worked out as below for the command the gait follows, (ahead, aside, turn) in the chassis frame. With c the command it is told, each part held within its range of gait.command, and s = (gait.slew.ahead, gait.slew.aside, gait.slew.turn) dt, dt as in 2, the command it follows, f, becomes f + (c - f held within -s and s), part by part, the sum worked out as written even when c is within s of f, when it is c but for its rounding. Between, the servos keep the targets they were given.

    1. The robot is settled at a working when its centre of mass moves along the floor no faster than gait.settle.speed, |(v_x, v_y)|, and the chassis turns no faster than gait.settle.turn, |w_z|, with v and w as in 2. It has settled when it is settled at this working and at the m - 1 before, m = max(1, round(gait.period / dt)), dt as in 2, rounded as n is; at the keyframe it counts as settled at all of them. The gait goes on while the command it follows is not zero, or while the robot has not settled. It starts at t when it goes on and has not started.
    2. With p, R and y the chassis's place, turn and yaw now, v the velocity of the robot's centre of mass (subtree_linvel of the chassis, after mj_subtreeVel), w the chassis's turn rate (R times the free joint's last three qvel), v* = Rz(y) (ahead, aside, 0) and dt = n times the timestep: the body's lead place moves by v* dt and is held within gait.lead.distance of p, along x and along y each; its lead yaw turns by turn dt and is held within gait.lead.yaw of y, the difference wrapped to [-pi, pi). The body is to stand at b = (the lead place, h), turned by H = Rz(the lead yaw).
    3. A foot's phase is 0 before the gait starts, else ((t - start) / gait.period + its offset) mod 1. When the gait goes on no more, has started and every phase is under gait.duty, it stops, and every phase is 0.
    4. A foot is on the floor while its phase is under gait.duty. A foot that comes down is planted at its geom's place, at its level; a foot that comes up is lifted from its geom's place.
    5. A foot on the floor is to be at H^T (planted - b) in the chassis frame. A foot in the air, s = (phase - duty) / (1 - duty) of its swing, lands, with a = (1 - s)(1 - duty) period + duty period / 2 and r = Rz(y) home, at L = (p_x + r_x, p_y + r_y, l) + v* a + (Rz(turn a) r - r)
      • k (v - v*), the last two taken along x and y alone. It is at lifted + (L - lifted)(3 s^2 - 2 s^3) along x and y and at l + gait.step_height sin(pi s) along z, so at R^T (that - p) in the chassis frame.
    6. Each leg's angles, from its last ones, by damped least squares on the model's own kinematics (a second data: every joint as now but the free joint, whose seven qpos are 0, 0, 0, 1, 0, 0, 0, the chassis at the origin and not turned, and the leg's angles set; mj_kinematics, mj_comPos): at most 8 rounds, until the foot geom is within 2e-4 m of its place; each round adds J^T (J J^T + 1e-4 I)^-1 e, with J the geom's Jacobian in the leg's hinges (mj_jacGeom) and e what it misses by, and holds each angle within its hinge's range.
    7. The balance, with m the robot's mass, f = 2 pi gait.balance.frequency and d = 2 gait.balance.damping f: the force F = m (d (v* - v)_x, d (v* - v)_y, g + f^2 (h - p_z) - d v_z); with E = H R^T, the turn's error e = (E_21 - E_12, E_02 - E_20, E_10 - E_01) / 2; and the moment M = I (f^2 e + d ((0, 0, turn) - w)), I the robot's inertia about its centre of mass c, in the world: over the chassis and every body below it, the sum of Q diag(i) Q^T + u (|o|^2 I - o o^T), with Q its ximat, i its body_inertia, u its body_mass and o its xipos less c. The feet on the floor share F and M: their forces, three each, minimise (A x - (F, M))^T W (A x - (F, M)) + 1e-3 x^T x, the rows of A summing the forces and their moments about c (the foot geom's place less c, crossed with its force), and W = diag(1, 1, 1, 10, 10, 10). Each foot's force is then held upward or zero (its z at least 0), and when its part along the floor, of length q, is more than the foot's friction mu times its z, that part is scaled by mu z / max(q, 1e-9).
    8. Each servo of a foot's leg is told its angle, less, for a foot on the floor, (J^T x) / kp, with x the foot's force, J its geom's Jacobian in the leg's hinges on the model as it is, and kp the servo's gain, its actuator's first actuator_gainprm.
  • "kinematics": "tracks" (Tracked), as ros-mujoco-nav2's node drives it. No joint turns: the surfaces of the belts' boxes move.

    • Once, with the model as it loads and mj_kinematics run on it, take every geom of each of the belts' bodies (body): body_geomnum of them from body_geomadr. Keep each one's centre in the frame of the root body, c = R_rootᵀ (x_geom - x_root), and the rotation from that frame into its own, R_geomᵀ R_root. Tell MuJoCo that surfaces move (flg_surfacevel), which it skips otherwise, before stepping.
    • For a base motion v (m/s) and w (rad/s), take one factor s: the least max_speed / |v - w*c_y| over the geoms of the belts that state a max_speed (m/s), where that is less than 1, and otherwise 1. A null max_speed bounds nothing.
    • With v' = sv and w' = sw, each geom's geom_surfacevel is the linear velocity (-v' + w'*c_y, -w'*c_x, 0) in its components 0 to 2 and the angular velocity (0, 0, -w') in 3 to 5, each rotated into the geom's frame.

    That is the runtime's surface_motion (runtime/mobiledsl_runtime/track_kinematics.py in the ROS targets' output), as the node applies it to each command; the node also drops a command that is not finite, and stops the belts when commands stop arriving.

Each driven wheel and kingpin names its actuator and joint; a wheel's max_speed is its motor's no-load speed at the wheel's joint, through whatever reduction the drive has between them, in rad/s, which bounds the joint's velocity command; null where the part states none.

A robot whose parts have links of their own names each link's body, by component and link (links); when some of them turn on servos, it names each servo, in the order of their actuators in the model (servos: its joint, its actuator, the angle it holds when nothing commands it, hold, and its range), and the keyframe that has them there (keyframe).

It refuses, as ros-mujoco-nav2 does, a base on omni wheels, a moving joint on a connector a mating uses other than a wheel's own, and a robot with a part that declares neither a collision box nor a hull, other than a wheel, which collides as a sphere of its radius, and a track, which collides as its belt's box: such a part would collide through its visual mesh. Of its own it refuses a world without a floor, a mecanum wheel too light for its rollers, whose hub would be left less than 1 g, and one too wide or too narrow for them, and a legged base walking by a gait in a world whose gravity is not straight down. Its stand-ins, in compile.json, are that target's for the wheels, the tracks, the ground contacts and the drive, but for the odometry a tracked base's drive makes there, which this target makes none of; with these of its own:

ConstructEmittedReason
A sensorIts body, mass and collision, without its sensingThe model is the robot to drive by hand, which reads no sensor
The worldIts floor and its gravity, not its entitiesThe model is the robot to drive by hand
A skid-steer baseA base that turns slower than its wheels' speeds command: told to turn at 0.5 rad/s while driving at 0.2 m/s, the skid-steer bases measured turned at a seventh to seven tenths of itMuJoCo's contact friction is isotropic, so a wheel dragged across its rolling direction is resisted as much as it grips along it
A tricycleIts front wheel at the speed of its own path about the turning centre on the rear axle and its kingpin at its own angle, as the steering drive drives a car's wheelsros-mujoco-nav2 drives it by tricycle_controller, which takes the wheel on the kingpin's axis; the steering drive takes where the wheel and the kingpin are
A mecanum wheelIts rollers round its hub, as above, said instead of a sphere contact: how many, their shape, their hand, their span, their friction and their massA geom's friction is the same in every direction (the model writes no contact pairs), so a wheel that grips only along its rollers rolls on rollers; the part states neither their number nor their shape
A part with links of its own whose links have shapesIts links' shapes with MuJoCo's default friction, 1 slidingThe part states no friction for its links' shapes
A legged base walking by a policyIts feet alone meeting the floor, each with its own friction, and no other shape of the robot meeting the floor or anotherIts walking controller was trained on a model whose feet alone collide, and walks as it was trained
A legged base walking by a gaitThe sameIts gait steps on its feet and holds the rest of it off the floor
A legged base without a walking controllerThe sameIt stands on its feet, as the model a walking controller is trained on, which no walking controller walks yet
A legged base walking by a policy, carrying other partsIts walking controller carrying themThe controller was trained on the chassis alone, so their mass is one it was not trained with
A link lighter than 1 g, or with a principal moment the decomposition finds less than 1e-6 kg m²The link as heavy as 1 g, and each such principal moment 1e-6 kg m², as the model writes themMuJoCo's compiler raises a body to the least mass and moment the model allows, the bounds the ros-mujoco-nav2 target sets