From 50ea95abcea1add1f39cfcfda1dfc448b8bf8846 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 30 Oct 2026 19:50:04 -0700 Subject: [PATCH 01/19] sim2: Go2Sim walks the Go2 policy through world_state and joint-command actions, SimL1 reproduces the head L1's rolling voxel map, unitree-go2-sim2-l1 blueprint --- dimos/robot/all_blueprints.py | 3 + .../blueprints/basic/unitree_go2_sim2_l1.py | 63 +++++ dimos/robot/unitree/go2/sim2.py | 163 +++++++++++++ dimos/sim2/l1.py | 132 ++++++++++ dimos/sim2/logistics.xml | 24 ++ dimos/sim2/mujoco_backend.py | 21 +- dimos/sim2/spec.py | 12 + dimos/sim2/test_mujoco_simulation.py | 23 +- dimos/sim2/test_sim_time.py | 225 ++++++++++++++++++ dimos/sim2/world.py | 14 ++ stubs/mujoco/__init__.pyi | 4 + 11 files changed, 676 insertions(+), 8 deletions(-) create mode 100644 dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py create mode 100644 dimos/robot/unitree/go2/sim2.py create mode 100644 dimos/sim2/l1.py create mode 100644 dimos/sim2/logistics.xml create mode 100644 dimos/sim2/test_sim_time.py diff --git a/dimos/robot/all_blueprints.py b/dimos/robot/all_blueprints.py index 12cf3eba35a..d51691a89ac 100644 --- a/dimos/robot/all_blueprints.py +++ b/dimos/robot/all_blueprints.py @@ -182,6 +182,7 @@ "unitree-go2-rpp-benchmark": "dimos.robot.unitree.go2.blueprints.basic.unitree_go2_rpp_benchmark:unitree_go2_rpp_benchmark", "unitree-go2-rpp-controller": "dimos.robot.unitree.go2.blueprints.basic.unitree_go2_rpp_controller:unitree_go2_rpp_controller", "unitree-go2-security": "dimos.robot.unitree.go2.blueprints.agentic.unitree_go2_security:unitree_go2_security", + "unitree-go2-sim2-l1": "dimos.robot.unitree.go2.blueprints.basic.unitree_go2_sim2_l1:unitree_go2_sim2_l1", "unitree-go2-spatial": "dimos.robot.unitree.go2.blueprints.smart.unitree_go2_spatial:unitree_go2_spatial", "unitree-go2-temporal-memory": "dimos.robot.unitree.go2.blueprints.agentic.unitree_go2_temporal_memory:unitree_go2_temporal_memory", "unitree-go2-typesafe": "dimos.robot.unitree.go2.blueprints.agentic.unitree_go2_typesafe:unitree_go2_typesafe", @@ -266,6 +267,7 @@ "go2-mid360-recorder": "dimos.robot.unitree.go2.go2_mid360_recorder.Go2Mid360Recorder", "go2-mid360-static-tf": "dimos.robot.unitree.go2.go2_mid360_static_transforms.Go2Mid360StaticTf", "go2-teleop-module": "dimos.teleop.webxr.extensions.Go2TeleopModule", + "go2-urdf-root": "dimos.robot.unitree.go2.blueprints.basic.unitree_go2_sim2_l1.Go2UrdfRoot", "google-maps-skill-container": "dimos.agents.skills.google_maps_skill_container.GoogleMapsSkillContainer", "gps-nav-skill-container": "dimos.agents.skills.gps_nav_skill.GpsNavSkillContainer", "grasp-gen-x-module": "dimos.manipulation.grasping.grasp_gen_x.module.GraspGenXModule", @@ -354,6 +356,7 @@ "sim-camera": "dimos.sim2.camera.SimCamera", "sim-go2-world": "dimos.simulation.go2_sim.world.SimGo2World", "sim-imu": "dimos.sim2.imu.SimImu", + "sim-l1": "dimos.sim2.l1.SimL1", "simple-phone-teleop": "dimos.teleop.phone.phone_extensions.SimplePhoneTeleop", "simulation": "dimos.sim2.module.Simulation", "spatial-memory": "dimos.perception.experimental.spatial_perception.SpatialMemory", diff --git a/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py b/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py new file mode 100644 index 00000000000..b5936d7bb45 --- /dev/null +++ b/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py @@ -0,0 +1,63 @@ +# Copyright 2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""unitree-go2-basic in sim2: the walking Go2, its head L1 and front camera, its URDF posed for viewers.""" + +from dimos.core.coordination.blueprints import autoconnect +from dimos.core.global_config import global_config +from dimos.msgs.geometry_msgs.Transform import Transform +from dimos.protocol.tf.static_tf_publisher import StaticTfPublisher +from dimos.robot.assets.model import RobotModel +from dimos.robot.assets.source import RobotDescriptionSource +from dimos.robot.unitree.go2.blueprints.basic.unitree_go2_basic import rerun_config +from dimos.robot.unitree.go2.go2_mid360_static_transforms import CAMERA_XYZ, OPTICAL_RPY +from dimos.robot.unitree.go2.sim2 import Go2Sim +from dimos.robot.urdf_manager import UrdfManager +from dimos.sim2.camera import SimCamera +from dimos.sim2.l1 import SimL1 +from dimos.sim2.module import Simulation +from dimos.sim2.spec import Mount +from dimos.visualization.vis_module import vis_module + +_GO2_DESCRIPTION = ( + RobotDescriptionSource( + url="https://github.com/unitreerobotics/unitree_ros", + ref="38a9b00ecbff0b70e85a7627eb9c65db9a2341f7", + ) + / "robots" + / "go2_description" +) + + +class Go2UrdfRoot(StaticTfPublisher): + """base_link -> base, the root link of Unitree's go2_description URDF.""" + + def transforms(self) -> list[Transform]: + return [Transform(frame_id="base_link", child_frame_id="base")] + + +unitree_go2_sim2_l1 = autoconnect( + Simulation.blueprint(), + vis_module(viewer_backend=global_config.viewer, rerun_config=rerun_config), + Go2Sim.blueprint(), + Go2UrdfRoot.blueprint(), + UrdfManager.blueprint( + model=RobotModel.from_file( + _GO2_DESCRIPTION / "urdf" / "go2_description.urdf", + package_paths={"go2_description": _GO2_DESCRIPTION}, + ) + ).remappings([(UrdfManager, "coordinator_joint_state", "joint_state")]), + SimL1.blueprint(), + SimCamera.blueprint(robot_id="go2", mount=Mount("base", CAMERA_XYZ, OPTICAL_RPY)), +).global_config(n_workers=6, robot_model="unitree_go2") diff --git a/dimos/robot/unitree/go2/sim2.py b/dimos/robot/unitree/go2/sim2.py new file mode 100644 index 00000000000..6ba060cffe4 --- /dev/null +++ b/dimos/robot/unitree/go2/sim2.py @@ -0,0 +1,163 @@ +# Copyright 2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A simulated Go2 with GO2Connection's cmd_vel/odom/tf: the walking policy, observing world_change and sending joint commands.""" + +import asyncio +from collections.abc import AsyncIterator +import math +from pathlib import Path +import time +from typing import Any + +from dimos_lcm.world_msgs import WorldAction, WorldChange +import numpy as np + +from dimos.core.module import Module, ModuleConfig +from dimos.core.shared_config import shared_config +from dimos.core.stream import In, Out +from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped +from dimos.msgs.geometry_msgs.Twist import Twist +from dimos.msgs.sensor_msgs.JointState import JointState +from dimos.msgs.tf2_msgs.TFMessage import TFMessage +from dimos.robot.unitree.go2.connection import GO2Connection +from dimos.robot.unitree.go2.constants import CMD_VEL_TIMEOUT +from dimos.sim2.actions import JointCommand, world_action +from dimos.sim2.module import Simulation +from dimos.sim2.spec import Servo, SimulatedRobot +from dimos.sim2.view import rotation_matrix +from dimos.sim2.world_change import WorldMirror +from dimos.simulation.go2_legged.policy import Proprioception, load_policy +from dimos.simulation.go2_legged.robot import COMMAND_SLEW, CONTROL_DT, projected_gravity, yaw_of +from dimos.utils.data import get_data + + +class Go2SimConfig(ModuleConfig): + robot_id: str = "go2" + xyz: tuple[float, float, float] = (0.0, 0.0, 0.35) + policy: Path | None = None + odom_frame_id: str = "world" + # Yaw rate commanded per radian of heading error while no turn is commanded. + heading_gain: float = 2.0 + + +class Go2Sim(Module): + config: Go2SimConfig + cmd_vel: In[Twist] + world_change: In[WorldChange] + actions: Out[WorldAction] + odom: Out[PoseStamped] + tf: Out[TFMessage] + # Named as the Go2's URDF names them, for UrdfManager. + joint_state: Out[JointState] + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self._policy = load_policy(self.config.policy) + self._desired = np.zeros(3) + self._desired_expires = 0.0 + self._command = np.zeros(3) + self._heading: float | None = None + + @shared_config(Simulation) + def simulated(self) -> dict[str, Any]: + config, policy = self.config, self._policy + servos = zip(policy.joint_names, policy.kp, policy.kd, policy.default_pose, strict=True) + return { + "simulated": SimulatedRobot( + config.robot_id, + get_data("go2_sim") / "unitree_go2" / "go2.xml", + "base", + xyz=config.xyz, + control_period=CONTROL_DT, + servos=tuple( + Servo(joint, float(kp), float(kd), float(q)) for joint, kp, kd, q in servos + ), + ) + } + + async def main(self) -> AsyncIterator[None]: + self._world = WorldMirror() + loop = asyncio.get_running_loop() + + # The world holds every CONTROL_DT of sim time until the policy has acted, as in training. + def on_frame(world: WorldMirror) -> None: + if world.has_event(self.config.robot_id, "events/act"): + loop.call_soon_threadsafe(self._act) + + self._world.on_frame(on_frame) + self._world.subscribe(self.world_change) + yield + + async def handle_cmd_vel(self, twist: Twist) -> None: + self._desired = np.array([twist.linear.x, twist.linear.y, twist.angular.z]) + self._desired_expires = time.monotonic() + CMD_VEL_TIMEOUT + + def _act(self) -> None: + policy, world = self._policy, self._world + with world.lock: + base = world.entity(self.config.robot_id) + names, rows = world.joints(self.config.robot_id) + stamp = world.stamp() + if base is None or base.pose is None or base.velocity is None: + return + x, y, z, w = base.pose[3:] + quat = np.array([w, x, y, z]) + desired = self._desired if time.monotonic() < self._desired_expires else np.zeros(3) + self._command += np.clip(desired - self._command, -COMMAND_SLEW, COMMAND_SLEW) + # The bundled policy drifts in yaw even standing still; with no turn commanded, hold heading. + if desired[2] == 0: + self._heading = yaw_of(quat) if self._heading is None else self._heading + turn = self.config.heading_gain * math.remainder(self._heading - yaw_of(quat), math.tau) + else: + self._heading, turn = None, 0.0 + joints = {name: row for name, row in zip(names, rows, strict=True)} + observation = Proprioception( + # The world's angular velocity is world-aligned; the policy's gyro reads in the base's frame. + angular_velocity=rotation_matrix(quat).T @ base.velocity[3:], + gravity=projected_gravity(quat), + joint_position=np.array([joints[name][0] for name in policy.joint_names]), + joint_velocity=np.array([joints[name][1] for name in policy.joint_names]), + ) + targets = policy.act(observation, self._command + np.array([0.0, 0.0, turn])) + self.actions.publish( + world_action( + world, + JointCommand( + self.config.robot_id, + tuple(policy.joint_names), + position=tuple(map(float, targets)), + ), + ) + ) + self.joint_state.publish( + JointState( + ts=stamp, + name=list(policy.joint_names), + position=observation.joint_position.tolist(), + velocity=observation.joint_velocity.tolist(), + ) + ) + self._publish_pose(base.pose[:3], quat, stamp) + + def _publish_pose(self, position: np.ndarray, quat_wxyz: np.ndarray, stamp: float) -> None: + w, x, y, z = quat_wxyz + odom = PoseStamped( + ts=stamp, + frame_id=self.config.odom_frame_id, + position=position.tolist(), + orientation=[x, y, z, w], + ) + self.tf.publish(TFMessage(*GO2Connection._odom_to_tf(odom))) + self.odom.publish(odom) diff --git a/dimos/sim2/l1.py b/dimos/sim2/l1.py new file mode 100644 index 00000000000..7d58b5470f5 --- /dev/null +++ b/dimos/sim2/l1.py @@ -0,0 +1,132 @@ +# Copyright 2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""The Go2's head L1 as GO2Connection's `lidar` sees it: the robot's rolling occupancy map, not a scan. + +Measured on real recordings: 5 cm world-anchored cells, the whole map resent every 0.13 s, +cropped to a world-axis-aligned 128x128-cell square around the robot. Cells persist while +inside the square; the real map's flicker comes from sensor noise this sim does not have. +""" + +import asyncio +from collections.abc import AsyncIterator +import math +from typing import Any + +from dimos_lcm.world_msgs import WorldChange +import numpy as np +from numpy.typing import NDArray + +from dimos.core.module import Module, ModuleConfig +from dimos.core.shared_config import shared_config +from dimos.core.stream import In, Out +from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2 +from dimos.robot.unitree.go2.go2_mid360_static_transforms import L1_MOUNT, L1_XYZ +from dimos.sim2.module import Simulation, SimulationSpec +from dimos.sim2.spec import Mount, SimulatedSensor +from dimos.sim2.view import WorldView, open_view, rotation_matrix +from dimos.sim2.world_change import WorldMirror +from dimos.simulation.sensors.mid360.lidar import Raycaster + +# Cell keys pack three offset 21-bit indices into one int64. +_KEY_BITS = 21 +_KEY_OFFSET = 1 << (_KEY_BITS - 1) + + +class SimL1Config(ModuleConfig): + name: str = "l1" + robot_id: str = "go2" + mount: Mount = Mount("base", L1_XYZ, tuple(math.radians(a) for a in L1_MOUNT)) # type: ignore[arg-type] + frame_id: str | None = "world" + period: float = 0.13 + points_per_second: float = 21_600.0 + max_range: float = 30.0 + cell: float = 0.05 + half_width_cells: int = 64 + z_range: tuple[float, float] = (-0.6, 1.3) + geom_groups: tuple[int, ...] = (0,) + seed: int = 0 + + +class SimL1(Module): + config: SimL1Config + lidar: Out[PointCloud2] + world_change: In[WorldChange] + _simulation: SimulationSpec + + @shared_config(Simulation) + def simulated(self) -> dict[str, Any]: + config = self.config + return {"simulated": SimulatedSensor(config.name, "lidar", config.mount, config.robot_id)} + + async def main(self) -> AsyncIterator[None]: + self._world = WorldMirror() + self._world.subscribe(self.world_change) + view = open_view(self._simulation.describe()) + self._mapping = asyncio.create_task( + self._map(view, view.raycaster(self.config.geom_groups)) + ) + yield + + async def _map(self, view: WorldView, raycaster: Raycaster) -> None: + config = self.config + rng = np.random.default_rng(config.seed) + rays = int(config.points_per_second * config.period) + site = f"{config.robot_id}/{config.name}" + center_body = f"{config.robot_id}/{config.mount.body}" + cells = np.zeros(0, dtype=np.int64) + # Every period of sim time, so the map keeps its rate per sim second at any speed. + async for _ in self._world.ticks(1 / config.period): + view.update(self._world) + origin, orientation = view.pose(site) + directions = _hemisphere(rng, rays) @ rotation_matrix(orientation).T + distance, _ = raycaster.cast(origin, directions, config.max_range) + hit = distance > 0 + cells = np.union1d(cells, self._keys(origin + directions[hit] * distance[hit, None])) + cells = self._crop(cells, view.pose(center_body)[0]) + self.lidar.publish( + PointCloud2.from_numpy( + (self._indices(cells) + 0.5) * config.cell, self.frame_id, self._world.stamp() + ) + ) + + def _keys(self, points: NDArray[np.float64]) -> NDArray[np.int64]: + index = np.floor(points / self.config.cell).astype(np.int64) + _KEY_OFFSET + return np.unique((index[:, 0] << 2 * _KEY_BITS) | (index[:, 1] << _KEY_BITS) | index[:, 2]) + + def _indices(self, keys: NDArray[np.int64]) -> NDArray[np.int64]: + mask = (1 << _KEY_BITS) - 1 + return ( + np.stack([keys >> 2 * _KEY_BITS, (keys >> _KEY_BITS) & mask, keys & mask], axis=1) + - _KEY_OFFSET + ) + + def _crop(self, keys: NDArray[np.int64], center: NDArray[np.float64]) -> NDArray[np.int64]: + config = self.config + index = self._indices(keys) + low = np.floor(center[:2] / config.cell).astype(np.int64) - config.half_width_cells + inside = np.all( + (index[:, :2] >= low) & (index[:, :2] < low + 2 * config.half_width_cells), axis=1 + ) + z = (index[:, 2] + 0.5) * config.cell + inside &= (z >= config.z_range[0]) & (z <= config.z_range[1]) + return keys[inside] + + +def _hemisphere(rng: np.random.Generator, count: int) -> NDArray[np.float64]: + """Uniform directions over the sensor's +z hemisphere: 360 degrees around, 90 up.""" + z = rng.uniform(0.0, 1.0, count) + azimuth = rng.uniform(0.0, 2 * math.pi, count) + ring = np.sqrt(1 - z**2) + return np.stack([ring * np.cos(azimuth), ring * np.sin(azimuth), z], axis=1) diff --git a/dimos/sim2/logistics.xml b/dimos/sim2/logistics.xml new file mode 100644 index 00000000000..b02f6875467 --- /dev/null +++ b/dimos/sim2/logistics.xml @@ -0,0 +1,24 @@ + + diff --git a/dimos/sim2/mujoco_backend.py b/dimos/sim2/mujoco_backend.py index 3acb71648ac..bcba5786266 100644 --- a/dimos/sim2/mujoco_backend.py +++ b/dimos/sim2/mujoco_backend.py @@ -102,11 +102,10 @@ class MujocoBackend: def __init__( self, config: "SimulationConfig", simulated: Mapping[str, Simulated], directory: Path ) -> None: - if config.scene is None: - raise ValueError("the mujoco backend needs a scene; pass scene= or --scene-package") self._config = config + self._scene = config.scene or Path(__file__).with_name("logistics.xml") self._robots = {d.robot_id: d for d in simulated.values() if isinstance(d, SimulatedRobot)} - self.model = compile_world(config.scene, simulated.values()) + self.model = compile_world(self._scene, simulated.values()) # Robots whose controllers the world steps in lockstep with, and the actions each sent since its last tick. self._controllers = [r for r in self._robots.values() if r.control_period is not None] self._next_ticks = [0.0] * len(self._controllers) @@ -127,16 +126,16 @@ def __init__( self._sent_paused = False self._refusals: list[tuple[str, str]] = [] self._wrench_until: dict[int, float] = {} - self._files = _files_named_by(config.scene, None) + self._files = _files_named_by(self._scene, None) for robot in self._robots.values(): self._files.update(_files_named_by(robot.model, None)) self._reset() def describe(self) -> dict[str, Any]: - return {"scene": str(self._config.scene)} + return {"scene": str(self._scene)} def assets(self) -> Mapping[str, Path | bytes]: - return {"world.xml": (self._config.scene or Path()).read_bytes(), **self._files} + return {"world.xml": self._scene.read_bytes(), **self._files} def scene_data(self) -> str: model = self.model @@ -183,7 +182,7 @@ def scene_data(self) -> str: return json.dumps( { "format": "dimos.scene_data.v1", - "id": f"{self._config.scene.stem}-{id(self)}", + "id": f"{self._scene.stem}-{id(self)}", "frame": "world", "bodies": bodies, "meshes": meshes, @@ -261,8 +260,16 @@ def _reset(self) -> None: self._next_ticks = [0.0] * len(self._controllers) self._wrench_until.clear() mujoco.mj_resetData(self.model, self.data) + self._hold_initial_pose() mujoco.mj_forward(self.model, self.data) + def _hold_initial_pose(self) -> None: + for robot in self._robots.values(): + for servo in robot.servos: + joint = self.model.joint(f"{robot.robot_id}/{servo.joint}") + self.data.qpos[joint.qposadr[0]] = servo.initial + self.data.ctrl[self.model.actuator_trnid[:, 0] == joint.id] = servo.initial + def _apply_pending(self) -> None: with self._lock: pending, self._pending = self._pending, [] diff --git a/dimos/sim2/spec.py b/dimos/sim2/spec.py index 5e78db9526f..febab9cd7ad 100644 --- a/dimos/sim2/spec.py +++ b/dimos/sim2/spec.py @@ -31,6 +31,16 @@ class Mount: rpy: Vector = (0.0, 0.0, 0.0) +@dataclass(frozen=True) +class Servo: + """A joint whose actuator becomes a PD position servo, starting at and holding initial.""" + + joint: str + kp: float + kd: float + initial: float = 0.0 + + @dataclass(frozen=True) class SimulatedRobot: robot_id: str @@ -42,6 +52,8 @@ class SimulatedRobot: # Its controller acts every this many sim seconds: the world sends it an "events/act" then, and waits for its actions. control_period: float | None = None + servos: tuple[Servo, ...] = () + @dataclass(frozen=True) class SimulatedSensor: diff --git a/dimos/sim2/test_mujoco_simulation.py b/dimos/sim2/test_mujoco_simulation.py index f7e7dd31a23..639db49001e 100644 --- a/dimos/sim2/test_mujoco_simulation.py +++ b/dimos/sim2/test_mujoco_simulation.py @@ -31,7 +31,7 @@ from dimos.sim2.actions import JointCommand, SetPose from dimos.sim2.module import Simulation, SimulationConfig from dimos.sim2.mujoco_backend import MujocoBackend -from dimos.sim2.spec import Mount, SimulatedRobot, SimulatedSensor +from dimos.sim2.spec import Mount, Servo, SimulatedRobot, SimulatedSensor from dimos.sim2.world import compile_world from dimos.sim2.world_change import OBJECT, WorldMirror @@ -203,6 +203,27 @@ def test_scene_data_describes_the_world_without_its_robots(scene, robot_model, t ) +def test_a_motor_command_drives_a_servo_with_its_own_gains(scene, robot_model, tmp_path): + robot = SimulatedRobot("dog", robot_model, "base", servos=(Servo("neck", 0.01, 0.001),)) + backend = MujocoBackend(SimulationConfig(scene=scene), {"dog": robot}, tmp_path) + published = _Collect() + + async def run_briefly() -> None: + running = asyncio.create_task(backend.run(published)) + await asyncio.sleep(0.05) + backend.apply([JointCommand("dog", ("neck",), position=(0.5,), kp=(0.05,), kd=(0.005,))]) + await asyncio.sleep(1.0) + running.cancel() + with contextlib.suppress(asyncio.CancelledError): + await running + + asyncio.run(run_briefly()) + actuator = backend.model.actuator("dog/neck").id + assert backend.model.actuator_gainprm[actuator, 0] == 0.05 + assert tuple(backend.model.actuator_biasprm[actuator, 1:3]) == (-0.05, -0.005) + assert published.world.joints("dog")[1][0, 0] == pytest.approx(0.5, abs=0.02) + + def test_world_holds_at_each_control_tick_for_its_controller(scene, robot_model, tmp_path): robot = SimulatedRobot("dog", robot_model, "base", control_period=0.02) backend = MujocoBackend(SimulationConfig(scene=scene), {"dog": robot}, tmp_path) diff --git a/dimos/sim2/test_sim_time.py b/dimos/sim2/test_sim_time.py new file mode 100644 index 00000000000..4da99f441f0 --- /dev/null +++ b/dimos/sim2/test_sim_time.py @@ -0,0 +1,225 @@ +# Copyright 2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Sensors keep their rates per sim second whether the world runs slower or faster than real.""" + +import asyncio +from collections.abc import AsyncIterator +from pathlib import Path +import time +from typing import Any +import uuid + +from dimos_lcm.world_msgs import WorldAction, WorldChange +import numpy as np +import pytest + +from dimos.core.coordination.blueprint_config.parser import BlueprintConfigParser +from dimos.core.coordination.blueprints import autoconnect +from dimos.core.coordination.module_coordinator import ModuleCoordinator +from dimos.core.core import rpc +from dimos.core.module import Module, ModuleConfig +from dimos.core.shared_config import shared_config +from dimos.core.stream import In, Out +from dimos.msgs.sensor_msgs.Image import Image +from dimos.msgs.sensor_msgs.Imu import Imu +from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2 +from dimos.sim2.actions import JointCommand, world_action +from dimos.sim2.camera import SimCamera +from dimos.sim2.imu import SimImu +from dimos.sim2.l1 import SimL1 +from dimos.sim2.livox import LivoxSim +from dimos.sim2.module import Simulation +from dimos.sim2.spec import Mount, SimulatedRobot +from dimos.sim2.world_change import WorldMirror + +L1_PERIOD = 0.13 +RATES = { + "color_image": 10.0, + "imu": 50.0, + "lidar": 10.0, + "livox_imu": 50.0, + "l1_lidar": 1 / L1_PERIOD, +} + + +@pytest.fixture +def scene(tmp_path: Path) -> Path: + path = tmp_path / "scene.xml" + path.write_text(""" + + + + + + +""") + return path + + +@pytest.fixture +def robot_model(tmp_path: Path) -> Path: + path = tmp_path / "robot.xml" + path.write_text(""" + + + + + + + + + + +""") + return path + + +class _DogConfig(ModuleConfig): + model: Path + # With a control period, the world waits this long (wall seconds) for each of its commands. + control_period: float | None = None + think: float = 0.0 + + +class _Dog(Module): + """A robot that, in lockstep, is slow to answer: the world runs slower than real waiting for it.""" + + config: _DogConfig + world_change: In[WorldChange] + actions: Out[WorldAction] + + @shared_config(Simulation) + def simulated(self) -> dict[str, Any]: + config = self.config + robot = SimulatedRobot("dog", config.model, "base", control_period=config.control_period) + return {"simulated": robot} + + async def main(self) -> AsyncIterator[None]: + world, loop = WorldMirror(), asyncio.get_running_loop() + + def act() -> None: + self.actions.publish(world_action(world, JointCommand("dog", ("neck",), effort=(0.0,)))) + + def on_frame(seen: WorldMirror) -> None: + if seen.has_event("dog", "events/act"): + loop.call_soon_threadsafe(loop.call_later, self.config.think, act) + + world.on_frame(on_frame) + world.subscribe(self.world_change) + yield + + +class _Readings(Module): + color_image: In[Image] + imu: In[Imu] + lidar: In[PointCloud2] + livox_imu: In[Imu] + l1_lidar: In[PointCloud2] + + async def main(self) -> AsyncIterator[None]: + self._stamps: dict[str, list[float]] = {name: [] for name in RATES} + self._accelerations: list[float] = [] + self._started = time.monotonic() + for name in RATES: + getattr(self, name).subscribe( + lambda message, n=name: self._stamps[n].append(message.ts) + ) + self.imu.subscribe( + lambda m: self._accelerations.append( + float(np.linalg.norm(m.linear_acceleration.to_numpy())) + ) + ) + yield + + @rpc + def readings(self) -> dict[str, Any]: + return { + "stamps": self._stamps, + "accelerations": self._accelerations, + "wall": time.monotonic() - self._started, + } + + +def _run( + scene: Path, robot_model: Path, seconds: float, speed: float = 1.0, **dog: Any +) -> dict[str, Any]: + blueprint = autoconnect( + Simulation.blueprint(scene=scene, speed=speed), + _Dog.blueprint(model=robot_model, **dog), + SimCamera.blueprint( + robot_id="dog", mount=Mount("head"), width=64, height=48, rate_hz=RATES["color_image"] + ), + SimImu.blueprint(robot_id="dog", mount=Mount("base"), rate_hz=RATES["imu"]), + LivoxSim.blueprint( + name="livox", + robot_id="dog", + mount=Mount("head"), + rate_hz=RATES["lidar"], + imu_rate_hz=RATES["livox_imu"], + ).remappings([(LivoxSim, "imu", "livox_imu")]), + SimL1.blueprint(robot_id="dog", mount=Mount("base"), period=L1_PERIOD).remappings( + [(SimL1, "lidar", "l1_lidar")] + ), + _Readings.blueprint(), + # A worker each, as a robot's blueprint gives its sensors: one sharing a busy loop falls behind at 4x. + ).global_config(n_workers=8) + # Its own zenoh domain: another world running on this machine would otherwise share its topics. + overrides = {"g": {"viewer": "none", "zenoh_domain": f"sim2-test-{uuid.uuid4().hex[:8]}"}} + parsed = BlueprintConfigParser(blueprint).parse(environ={}, overrides=overrides) + coordinator = ModuleCoordinator.build(blueprint, parsed) + try: + time.sleep(seconds) + return coordinator.get_instance(_Readings).readings() + finally: + coordinator.stop() + + +# Sim seconds at the start in which a sensor may stall once, warming up (its first point cloud imports open3d). +WARM_UP = 1.0 + + +def _per_sim_second(stamps: list[float], start: float) -> float: + # Stamps are the world's epoch plus its sim time, so their spacing is sim time. + steady = [stamp for stamp in stamps if stamp >= start + WARM_UP] + assert len(steady) > 5, f"only {len(steady)} after warming up" + return (len(steady) - 1) / (steady[-1] - steady[0]) + + +@pytest.mark.parametrize( + ("speed", "dog"), + [ + (0.5, {}), + (4.0, {}), + # Lockstep with a robot that takes 50 ms (wall) to answer each 20 ms (sim) tick. + (1.0, {"control_period": 0.02, "think": 0.05}), + ], + ids=["half_speed", "four_times_speed", "slower_than_real_lockstep"], +) +def test_sensors_keep_their_rates_per_sim_second(scene, robot_model, speed, dog): + readings = _run(scene, robot_model, seconds=8.0, speed=speed, **dog) + stamps = readings["stamps"] + counts = {name: len(stamps[name]) for name in RATES} + assert all(count > 5 for count in counts.values()), counts + start = min(stamps[name][0] for name in RATES) + sim_seconds = stamps["imu"][-1] - stamps["imu"][0] + for name, rate in RATES.items(): + measured = _per_sim_second(stamps[name], start) + assert measured == pytest.approx(rate, rel=0.1), (name, measured) + if dog: + # The world waited on the robot: well slower than real. + assert sim_seconds < 0.7 * readings["wall"] + # At rest on the floor (after it settles), the IMU feels the floor push up against gravity. + settled = readings["accelerations"][len(readings["accelerations"]) // 2 :] + assert np.mean(settled) == pytest.approx(9.81, abs=0.3) diff --git a/dimos/sim2/world.py b/dimos/sim2/world.py index a756b65ef98..eb59634a03e 100644 --- a/dimos/sim2/world.py +++ b/dimos/sim2/world.py @@ -54,6 +54,20 @@ def compile_world(scene: Path, simulated: Iterable[Simulated]) -> mujoco.MjModel root.mocap = True for key in list(spec.keys): spec.delete(key) + actuators = {actuator.target: actuator for actuator in spec.actuators} + for servo in robot.servos: + actuator = actuators[servo.joint] + # A motor's control range, if it has one, is its torque limit; a position actuator keeps its own (or its joint's). + motor = ( + actuator.biastype == mujoco.mjtBias.mjBIAS_NONE + and actuator.ctrlrange[0] < actuator.ctrlrange[1] + ) + torque_limit = actuator.ctrlrange.copy() + actuator.set_to_position(kp=servo.kp, kv=servo.kd) + actuator.ctrllimited = mujoco.mjtLimited.mjLIMITED_FALSE + if motor: + actuator.forcelimited = mujoco.mjtLimited.mjLIMITED_TRUE + actuator.forcerange = torque_limit for sensor in sensors: if sensor.robot_id == robot.robot_id: _mount(spec, sensor, robot.robot_id) diff --git a/stubs/mujoco/__init__.pyi b/stubs/mujoco/__init__.pyi index 35c4e5b6a59..6ebdd59e6ac 100644 --- a/stubs/mujoco/__init__.pyi +++ b/stubs/mujoco/__init__.pyi @@ -165,6 +165,10 @@ class mjtLightType: mjLIGHT_DIRECTIONAL: int mjLIGHT_SPOT: int +class mjtLimited: + mjLIMITED_FALSE: int + mjLIMITED_TRUE: int + class mjtSensor: mjSENS_GYRO: int mjSENS_ACCELEROMETER: int From 4e3ec260c80454e14a540ecd44ad514906301f00 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 2 Nov 2026 10:56:03 -0800 Subject: [PATCH 02/19] sim2 Go2: GO2_SIM_MODEL is made at import, as a plain path: blueprint config validation takes no lazy path object --- dimos/robot/unitree/go2/sim2.py | 8 ++++---- dimos/sim2/utils/mjcf_to_urdf.py | 14 -------------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/dimos/robot/unitree/go2/sim2.py b/dimos/robot/unitree/go2/sim2.py index d99c1d9b232..daea431fe1a 100644 --- a/dimos/robot/unitree/go2/sim2.py +++ b/dimos/robot/unitree/go2/sim2.py @@ -41,17 +41,17 @@ from dimos.sim2.actions import JointCommand, world_action from dimos.sim2.module import Simulation from dimos.sim2.spec import Servo, SimulatedRobot -from dimos.sim2.utils.mjcf_to_urdf import MjcfUrdf +from dimos.sim2.utils.mjcf_to_urdf import mjcf_to_urdf from dimos.sim2.view import rotation_matrix from dimos.sim2.world_change import WorldMirror from dimos.simulation.go2_legged.policy import Proprioception, load_policy from dimos.simulation.go2_legged.robot import COMMAND_SLEW, CONTROL_DT, projected_gravity, yaw_of -from dimos.utils.data import LfsPath +from dimos.utils.data import get_data GO2_ROBOT_ID = "go2" -GO2_MJCF = LfsPath("go2_sim/unitree_go2/go2.xml") +GO2_MJCF = get_data("go2_sim") / "unitree_go2" / "go2.xml" # The MJCF's base body is the URDF's base_link, the frame odometry moves. -GO2_SIM_MODEL = RobotModel.from_file(MjcfUrdf(GO2_MJCF, GO2_ROBOT_ID, "base_link")) +GO2_SIM_MODEL = RobotModel.from_file(mjcf_to_urdf(GO2_MJCF, GO2_ROBOT_ID, "base_link")) class Go2SimConfig(ModuleConfig): diff --git a/dimos/sim2/utils/mjcf_to_urdf.py b/dimos/sim2/utils/mjcf_to_urdf.py index 4cc64bed306..0d8f45467f6 100644 --- a/dimos/sim2/utils/mjcf_to_urdf.py +++ b/dimos/sim2/utils/mjcf_to_urdf.py @@ -21,9 +21,7 @@ from __future__ import annotations -from dataclasses import dataclass import hashlib -import os from pathlib import Path import tempfile import xml.etree.ElementTree as ET @@ -61,18 +59,6 @@ def mjcf_to_urdf( return urdf -@dataclass(frozen=True) -class MjcfUrdf(os.PathLike[str]): - """mjcf_to_urdf's URDF, made only when first opened: a RobotModel source that costs nothing to declare.""" - - mjcf: os.PathLike[str] | str - name: str | None = None - root_link: str | None = None - - def __fspath__(self) -> str: - return str(mjcf_to_urdf(Path(os.fspath(self.mjcf)), self.name, self.root_link)) - - def _urdf(model: mujoco.MjModel, name: str, root_link: str | None, directory: Path) -> str: roots = [b for b in range(1, model.nbody) if model.body_parentid[b] == 0] if len(roots) != 1: From b584d81295315a5133e65e77f96d00bec9bccf82 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 2 Nov 2026 10:59:01 -0800 Subject: [PATCH 03/19] unitree-go2-sim2-l1: the L1 frame's rpy is a 3-tuple --- .../unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py b/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py index 20d2c9877b4..b05d287a343 100644 --- a/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py +++ b/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py @@ -42,7 +42,10 @@ Go2Sim.blueprint(), UrdfManager.blueprint( model=GO2_SIM_MODEL.with_fixed_frame( - L1_FRAME, "base_link", xyz=L1_XYZ, rpy=tuple(math.radians(a) for a in L1_MOUNT) + L1_FRAME, + "base_link", + xyz=L1_XYZ, + rpy=(math.radians(L1_MOUNT[0]), math.radians(L1_MOUNT[1]), math.radians(L1_MOUNT[2])), ) .with_fixed_frame(CAMERA_FRAME, "base_link", xyz=CAMERA_XYZ) .with_fixed_frame(CAMERA_OPTICAL_FRAME, CAMERA_FRAME, rpy=OPTICAL_RPY), From 72cb6e0e0ab5ffaa272c502f85cde06b19fb9fef Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 2 Nov 2026 11:13:13 -0800 Subject: [PATCH 04/19] all_blueprints: no go2-urdf-root (the sim Go2's URDF is rooted at base_link) --- dimos/robot/all_blueprints.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dimos/robot/all_blueprints.py b/dimos/robot/all_blueprints.py index d51691a89ac..01669bed0b4 100644 --- a/dimos/robot/all_blueprints.py +++ b/dimos/robot/all_blueprints.py @@ -267,7 +267,6 @@ "go2-mid360-recorder": "dimos.robot.unitree.go2.go2_mid360_recorder.Go2Mid360Recorder", "go2-mid360-static-tf": "dimos.robot.unitree.go2.go2_mid360_static_transforms.Go2Mid360StaticTf", "go2-teleop-module": "dimos.teleop.webxr.extensions.Go2TeleopModule", - "go2-urdf-root": "dimos.robot.unitree.go2.blueprints.basic.unitree_go2_sim2_l1.Go2UrdfRoot", "google-maps-skill-container": "dimos.agents.skills.google_maps_skill_container.GoogleMapsSkillContainer", "gps-nav-skill-container": "dimos.agents.skills.gps_nav_skill.GpsNavSkillContainer", "grasp-gen-x-module": "dimos.manipulation.grasping.grasp_gen_x.module.GraspGenXModule", From 7beb41694ca4a654a2ddfda55c62bf133def516b Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 2 Nov 2026 17:53:32 -0800 Subject: [PATCH 05/19] UrdfManager takes its model from model= or the one module that sends it with @shared_config(UrdfManager); Go2Sim sends its URDF --- dimos/robot/test_urdf_manager.py | 20 +++++++++++ .../blueprints/basic/unitree_go2_sim2_l1.py | 6 +--- dimos/robot/unitree/go2/sim2.py | 10 +++++- dimos/robot/urdf_manager.py | 34 +++++++++++++++---- dimos/robot/urdf_manager_point_clearing.py | 10 ++++-- 5 files changed, 65 insertions(+), 15 deletions(-) diff --git a/dimos/robot/test_urdf_manager.py b/dimos/robot/test_urdf_manager.py index 3ab5d4e59dc..d94a9eaa152 100644 --- a/dimos/robot/test_urdf_manager.py +++ b/dimos/robot/test_urdf_manager.py @@ -435,3 +435,23 @@ def test_joint_transforms_match_roboplan_forward_kinematics_on_the_r1pro(): ), atol=1e-9, ) + + +def test_the_model_can_come_from_the_one_module_that_shares_it(tmp_path): + urdf = tmp_path / "robot.urdf" + urdf.write_text(_URDF) + model = RobotModel.from_file(urdf) + module = UrdfManagerPointClearing(padding_m=0.01) + try: + module.config_receive((("robot", {"model": model}),)) + assert module._base_link == "base" + with pytest.raises(ValueError, match="takes one model"): + module.config_receive((("robot", {"model": model}), ("other", {"model": model}))) + finally: + module.dispose() + given = UrdfManager(model=model) + try: + with pytest.raises(ValueError, match="takes one model"): + given.config_receive((("robot", {"model": model}),)) + finally: + given.dispose() diff --git a/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py b/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py index d34a9f1d5c4..d22b9eb3507 100644 --- a/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py +++ b/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py @@ -22,17 +22,13 @@ from dimos.sim2.camera import SimCamera from dimos.sim2.l1 import SimL1 from dimos.sim2.module import Simulation -from dimos.sim2.utils.mjcf_to_urdf import mjcf_to_urdf from dimos.visualization.vis_module import vis_module unitree_go2_sim2_l1 = autoconnect( Simulation.blueprint(), vis_module(viewer_backend=global_config.viewer, rerun_config=rerun_config), Go2Sim.blueprint(), - # The MJCF's base body is the URDF's base_link, the frame odometry moves. - UrdfManager.blueprint(model=mjcf_to_urdf(Go2Sim.mjcf, "go2", "base_link")).remappings( - [(UrdfManager, "coordinator_joint_state", "joint_state")] - ), + UrdfManager.blueprint().remappings([(UrdfManager, "coordinator_joint_state", "joint_state")]), SimL1.blueprint(frame_id=Go2Sim.l1_mount_frame), SimCamera.blueprint(frame_id=Go2Sim.camera_mount_frame), ).global_config(n_workers=6, robot_model="unitree_go2") diff --git a/dimos/robot/unitree/go2/sim2.py b/dimos/robot/unitree/go2/sim2.py index 6645eba9e01..557c9adb77d 100644 --- a/dimos/robot/unitree/go2/sim2.py +++ b/dimos/robot/unitree/go2/sim2.py @@ -15,7 +15,7 @@ """A simulated Go2 with GO2Connection's cmd_vel/odom/tf: the walking policy, observing world_observations and sending joint commands. Go2Sim.mjcf is the Go2's MJCF with the rig's sensor frames as sites: a sensor names one (Go2Sim.lidar_mount_frame, ...) -as its frame_id, and UrdfManager.blueprint(model=mjcf_to_urdf(Go2Sim.mjcf)) poses the same frames on tf. +as its frame_id, and Go2Sim hands a UrdfManager the URDF of it, which poses the same frames on tf. """ import asyncio @@ -36,6 +36,7 @@ from dimos.msgs.geometry_msgs.Twist import Twist from dimos.msgs.sensor_msgs.JointState import JointState from dimos.msgs.tf2_msgs.TFMessage import TFMessage +from dimos.robot.assets.model import RobotModel from dimos.robot.unitree.go2.constants import CMD_VEL_TIMEOUT from dimos.robot.unitree.go2.go2_mid360_static_transforms import ( CAMERA_XYZ, @@ -45,10 +46,12 @@ MID360_XYZ, OPTICAL_RPY, ) +from dimos.robot.urdf_manager import UrdfManager from dimos.sim2.actions import JointCommand, world_action from dimos.sim2.module import Simulation from dimos.sim2.spec import Servo, SimulatedRobot from dimos.sim2.utils.mjcf_frames import mjcf_with_frames +from dimos.sim2.utils.mjcf_to_urdf import mjcf_to_urdf from dimos.sim2.view import rotation_matrix from dimos.sim2.world_observations import ACT, WorldMirror from dimos.simulation.go2_legged.policy import Proprioception, load_policy @@ -118,6 +121,11 @@ def simulated(self) -> dict[str, Any]: ) } + @shared_config(UrdfManager) + def urdf(self) -> dict[str, RobotModel]: + # The MJCF's base body is the URDF's base_link, the frame odometry moves. + return {"model": mjcf_to_urdf(self.mjcf, self.config.robot_id, "base_link")} + async def main(self) -> AsyncIterator[None]: self._world = WorldMirror() loop = asyncio.get_running_loop() diff --git a/dimos/robot/urdf_manager.py b/dimos/robot/urdf_manager.py index ee566aa7156..16f44ba01cc 100644 --- a/dimos/robot/urdf_manager.py +++ b/dimos/robot/urdf_manager.py @@ -26,7 +26,8 @@ from pydantic import Field from dimos.core.core import rpc -from dimos.core.module import Module, ModuleConfig +from dimos.core.module import ModuleConfig +from dimos.core.shared_config import ConfigSenders, SharedConfigModule from dimos.core.stream import IO, In, Out from dimos.msgs.files.File import File from dimos.msgs.geometry_msgs.Transform import Transform @@ -44,7 +45,8 @@ class UrdfManagerConfig(ModuleConfig): - model: RobotModel + # Or None, for the one module that sends it: @shared_config(UrdfManager) returning {"model": RobotModel}. + model: RobotModel | None = None # Every single-DOF and fixed URDF joint as a parent -> child tf edge; off where another module owns those frames. publish_joint_tf: bool = True # An edge goes out when it moves, at most this often... @@ -54,7 +56,7 @@ class UrdfManagerConfig(ModuleConfig): unchanged_tf_hz: float = Field(default=4.0, gt=0.0) -class UrdfManager(Module): +class UrdfManager(SharedConfigModule): """Poses the URDF from the joint state onto tf, and serves the URDF and the files it names to renderers.""" config: UrdfManagerConfig @@ -70,12 +72,32 @@ class UrdfManager(Module): def __init__(self, **kwargs: object) -> None: super().__init__(**kwargs) self._lock = RLock() - self._description = self.config.model.load() - self._file_paths = _files_named_by(self._description.xml) - self._joints = {joint.name: joint for joint in self._description.joints} self._next_joint_tf_t = 0.0 # Per edge: the last transform sent (translation + rotation) and when. self._sent_edges: dict[tuple[str, str], tuple[tuple[float, ...], float]] = {} + if self.config.model is not None: + self._load(self.config.model) + + def config_receive(self, config_senders: ConfigSenders) -> None: + models = [(name, config.get("model")) for name, config in config_senders] + if self.config.model is not None: + models.insert(0, ("model=", self.config.model)) + if len(models) != 1: + raise ValueError( + f"UrdfManager takes one model, from model= or one @shared_config(UrdfManager); got {[n for n, _ in models]}" + ) + name, model = models[0] + if not isinstance(model, RobotModel): + raise TypeError( + f"{name}'s @shared_config(UrdfManager) must return {{'model': RobotModel}}" + ) + if model is not self.config.model: + self._load(model) + + def _load(self, model: RobotModel) -> None: + self._description = model.load() + self._file_paths = _files_named_by(self._description.xml) + self._joints = {joint.name: joint for joint in self._description.joints} @rpc def start(self) -> None: diff --git a/dimos/robot/urdf_manager_point_clearing.py b/dimos/robot/urdf_manager_point_clearing.py index 5234255006d..b60ddcce15a 100644 --- a/dimos/robot/urdf_manager_point_clearing.py +++ b/dimos/robot/urdf_manager_point_clearing.py @@ -29,6 +29,7 @@ from dimos.msgs.geometry_msgs.Transform import Transform from dimos.msgs.sensor_msgs.JointState import JointState from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2 +from dimos.robot.assets.model import RobotModel from dimos.robot.roboplan_body_filter import RoboPlanBodyFilter from dimos.robot.urdf_manager import MULTI_DOF_JOINT_TYPES, UrdfManager, UrdfManagerConfig from dimos.types.timestamped import TimestampedBufferCollection @@ -58,13 +59,16 @@ class UrdfManagerPointClearing(UrdfManager): def __init__(self, **kwargs: object) -> None: super().__init__(**kwargs) + self._states = TimestampedBufferCollection[JointState](self.config.state_history_s) + self._clouds_in = self._clouds_dropped = self._points_in = self._points_removed = 0 + self._next_stats_t = time.monotonic() + self.config.filter_stats_interval_s + + def _load(self, model: RobotModel) -> None: + super()._load(model) self._body_filter: RobotBodyFilter = RoboPlanBodyFilter( self._description, self.config.padding_m ) self._base_link = self._description.root_link - self._states = TimestampedBufferCollection[JointState](self.config.state_history_s) - self._clouds_in = self._clouds_dropped = self._points_in = self._points_removed = 0 - self._next_stats_t = time.monotonic() + self.config.filter_stats_interval_s async def handle_pointcloud(self, cloud: PointCloud2) -> None: """Filter the latest capture without starving TF transport callbacks.""" From e69a731743a2569a2bb7c38714e0b93c8e148620 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 2 Nov 2026 18:32:50 -0800 Subject: [PATCH 06/19] test_sim_time: a run waits (up to a minute more) until every sensor has steady stamps, so a loaded machine's slow first point cloud does not fail it --- dimos/sim2/test_sim_time.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/dimos/sim2/test_sim_time.py b/dimos/sim2/test_sim_time.py index ef52e03bc4c..9629195d0f2 100644 --- a/dimos/sim2/test_sim_time.py +++ b/dimos/sim2/test_sim_time.py @@ -180,12 +180,32 @@ def _run( parsed = BlueprintConfigParser(blueprint).parse(environ={}, overrides=overrides) coordinator = ModuleCoordinator.build(blueprint, parsed) try: + readings = coordinator.get_instance(_Readings) + # At least `seconds` of wall time, then until every sensor has steady stamps: on a loaded machine + # a sensor's first point cloud (it imports open3d) can take most of that. + deadline = time.monotonic() + seconds + MAX_EXTRA_WALL time.sleep(seconds) - return coordinator.get_instance(_Readings).readings() + while time.monotonic() < deadline and not _steady(readings.readings()["stamps"]): + time.sleep(1.0) + return readings.readings() finally: coordinator.stop() +# Wall seconds a run may go on past its own, waiting for every sensor to be steady. +MAX_EXTRA_WALL = 60.0 +STEADY_STAMPS = 8 + + +def _steady(stamps: dict[str, list[float]]) -> bool: + if not all(stamps[name] for name in RATES): + return False + start = min(stamps[name][0] for name in RATES) + return all( + sum(stamp >= start + WARM_UP for stamp in stamps[name]) >= STEADY_STAMPS for name in RATES + ) + + # Sim seconds at the start in which a sensor may stall once, warming up (its first point cloud imports open3d). WARM_UP = 1.0 From babb5befea3837a173f21c2f33d45a04a401e194 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Wed, 4 Nov 2026 13:50:54 -0800 Subject: [PATCH 07/19] sim2_go2_l1: Raycaster from main's shared lidar module --- dimos/sim2/l1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dimos/sim2/l1.py b/dimos/sim2/l1.py index 46ea2c1109b..7fd2b0e50b8 100644 --- a/dimos/sim2/l1.py +++ b/dimos/sim2/l1.py @@ -36,7 +36,7 @@ from dimos.sim2.spec import SimulatedRobot, SimulatedSensor from dimos.sim2.view import WorldView, open_view, rotation_matrix, sensor_frame from dimos.sim2.world_observations import WorldMirror -from dimos.simulation.sensors.mid360.lidar import Raycaster +from dimos.simulation.sensors.lidar import Raycaster # Cell keys pack three offset 21-bit indices into one int64. _KEY_BITS = 21 From e54f4b1c81a6565f0057837b75ea5f513cb6085b Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Sat, 7 Nov 2026 23:21:00 -0800 Subject: [PATCH 08/19] sim2: the manager is UsdManager now The USD rename sits below sim2 in the stack, so every import of dimos.robot.urdf_manager names a module that is no longer there. --- .../unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py | 4 ++-- dimos/robot/unitree/go2/sim2.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py b/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py index d22b9eb3507..e54eec2df4f 100644 --- a/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py +++ b/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py @@ -18,7 +18,7 @@ from dimos.core.global_config import global_config from dimos.robot.unitree.go2.blueprints.basic.unitree_go2_basic import rerun_config from dimos.robot.unitree.go2.sim2 import Go2Sim -from dimos.robot.urdf_manager import UrdfManager +from dimos.robot.usd_manager import UsdManager from dimos.sim2.camera import SimCamera from dimos.sim2.l1 import SimL1 from dimos.sim2.module import Simulation @@ -28,7 +28,7 @@ Simulation.blueprint(), vis_module(viewer_backend=global_config.viewer, rerun_config=rerun_config), Go2Sim.blueprint(), - UrdfManager.blueprint().remappings([(UrdfManager, "coordinator_joint_state", "joint_state")]), + UsdManager.blueprint().remappings([(UsdManager, "coordinator_joint_state", "joint_state")]), SimL1.blueprint(frame_id=Go2Sim.l1_mount_frame), SimCamera.blueprint(frame_id=Go2Sim.camera_mount_frame), ).global_config(n_workers=6, robot_model="unitree_go2") diff --git a/dimos/robot/unitree/go2/sim2.py b/dimos/robot/unitree/go2/sim2.py index 557c9adb77d..544fa7fc225 100644 --- a/dimos/robot/unitree/go2/sim2.py +++ b/dimos/robot/unitree/go2/sim2.py @@ -15,7 +15,7 @@ """A simulated Go2 with GO2Connection's cmd_vel/odom/tf: the walking policy, observing world_observations and sending joint commands. Go2Sim.mjcf is the Go2's MJCF with the rig's sensor frames as sites: a sensor names one (Go2Sim.lidar_mount_frame, ...) -as its frame_id, and Go2Sim hands a UrdfManager the URDF of it, which poses the same frames on tf. +as its frame_id, and Go2Sim hands a UsdManager the URDF of it, which poses the same frames on tf. """ import asyncio @@ -46,7 +46,7 @@ MID360_XYZ, OPTICAL_RPY, ) -from dimos.robot.urdf_manager import UrdfManager +from dimos.robot.usd_manager import UsdManager from dimos.sim2.actions import JointCommand, world_action from dimos.sim2.module import Simulation from dimos.sim2.spec import Servo, SimulatedRobot @@ -93,7 +93,7 @@ class Go2Sim(Module): actions: Out[WorldAction] odom: Out[PoseStamped] tf: Out[TFMessage] - # Named as the Go2's URDF names them, for UrdfManager. + # Named as the Go2's URDF names them, for UsdManager. joint_state: Out[JointState] def __init__(self, **kwargs: Any) -> None: @@ -121,7 +121,7 @@ def simulated(self) -> dict[str, Any]: ) } - @shared_config(UrdfManager) + @shared_config(UsdManager) def urdf(self) -> dict[str, RobotModel]: # The MJCF's base body is the URDF's base_link, the frame odometry moves. return {"model": mjcf_to_urdf(self.mjcf, self.config.robot_id, "base_link")} From db8dda4a4b0b18012ebb69badcb1e96fa7237321 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Sat, 7 Nov 2026 23:40:01 -0800 Subject: [PATCH 09/19] usd_manager: keep importing Module, which pydantic still resolves against Carrying UsdManager onto SharedConfigModule took the Module import with it, but the name is still what a forward reference in a config resolves to: without it every config that builds on this one refuses to finish defining itself. --- dimos/robot/usd_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dimos/robot/usd_manager.py b/dimos/robot/usd_manager.py index ae9ded8efbb..deef7e29ae5 100644 --- a/dimos/robot/usd_manager.py +++ b/dimos/robot/usd_manager.py @@ -27,7 +27,7 @@ from pydantic import Field, field_validator from dimos.core.core import rpc -from dimos.core.module import ModuleConfig +from dimos.core.module import Module, ModuleConfig from dimos.core.rpc_client import RPCClient from dimos.core.shared_config import ConfigSenders, SharedConfigModule from dimos.core.stream import IO, In, Out From 56ca2558eefdc1bf26b3c158ef418e1890bdd713 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 9 Nov 2026 08:37:17 -0800 Subject: [PATCH 10/19] sim2 l1: describe itself in USD, and vendor the Go2 as USD Two things this branch owns: the L1's own description, and the robot it is bolted to. The L1 answers `get_usd` with the frame it scans from and no case. Its unit sits under a cover that is part of the Go2's own shell and nobody has measured what is inside it, so a layer claiming dimensions would be invented geometry in the physics and on the screen. The Go2 is now a committed `.usda` with its meshes vendored beside it in LFS, converted once offline, the same shape as the M20 and office_555. The MJCF it came from lives under `data/`, which is gitignored, so the robot's description was previously only on a machine that had already downloaded it. 33 of 33 meshes re-pointed, none baked: 0.12 MB MuJoCo 4.0.0: bodies 18 joints 13 actuators 12 geoms 56 meshes 33, steps 100x dimos UsdModel: root base, 12 joints, 26 frames, 32 assets, every hash OK The rig's four frames -- the L1, the front camera, the Mid-360 on that camera, and the camera's optical frame -- are not baked in. They are what this robot is carrying rather than what it is, and whoever carries them adds them in USD, where both ends see them as frames. --- dimos/robot/unitree/go2/assets/go2.usda | 2420 +++++++++++++++++ .../unitree/go2/assets/meshes/base_0.obj | 3 + .../unitree/go2/assets/meshes/base_0.usdc | 3 + .../unitree/go2/assets/meshes/base_1.obj | 3 + .../unitree/go2/assets/meshes/base_1.usdc | 3 + .../unitree/go2/assets/meshes/base_2.obj | 3 + .../unitree/go2/assets/meshes/base_2.usdc | 3 + .../unitree/go2/assets/meshes/base_3.obj | 3 + .../unitree/go2/assets/meshes/base_3.usdc | 3 + .../unitree/go2/assets/meshes/base_4.obj | 3 + .../unitree/go2/assets/meshes/base_4.usdc | 3 + .../unitree/go2/assets/meshes/calf_0.obj | 3 + .../unitree/go2/assets/meshes/calf_0.usdc | 3 + .../unitree/go2/assets/meshes/calf_1.obj | 3 + .../unitree/go2/assets/meshes/calf_1.usdc | 3 + .../go2/assets/meshes/calf_mirror_0.obj | 3 + .../go2/assets/meshes/calf_mirror_0.usdc | 3 + .../go2/assets/meshes/calf_mirror_1.obj | 3 + .../go2/assets/meshes/calf_mirror_1.usdc | 3 + .../robot/unitree/go2/assets/meshes/foot.obj | 3 + .../robot/unitree/go2/assets/meshes/foot.usdc | 3 + .../robot/unitree/go2/assets/meshes/hip_0.obj | 3 + .../unitree/go2/assets/meshes/hip_0.usdc | 3 + .../robot/unitree/go2/assets/meshes/hip_1.obj | 3 + .../unitree/go2/assets/meshes/hip_1.usdc | 3 + .../unitree/go2/assets/meshes/thigh_0.obj | 3 + .../unitree/go2/assets/meshes/thigh_0.usdc | 3 + .../unitree/go2/assets/meshes/thigh_1.obj | 3 + .../unitree/go2/assets/meshes/thigh_1.usdc | 3 + .../go2/assets/meshes/thigh_mirror_0.obj | 3 + .../go2/assets/meshes/thigh_mirror_0.usdc | 3 + .../go2/assets/meshes/thigh_mirror_1.obj | 3 + .../go2/assets/meshes/thigh_mirror_1.usdc | 3 + dimos/sim2/l1.py | 11 + 34 files changed, 2527 insertions(+) create mode 100644 dimos/robot/unitree/go2/assets/go2.usda create mode 100644 dimos/robot/unitree/go2/assets/meshes/base_0.obj create mode 100644 dimos/robot/unitree/go2/assets/meshes/base_0.usdc create mode 100644 dimos/robot/unitree/go2/assets/meshes/base_1.obj create mode 100644 dimos/robot/unitree/go2/assets/meshes/base_1.usdc create mode 100644 dimos/robot/unitree/go2/assets/meshes/base_2.obj create mode 100644 dimos/robot/unitree/go2/assets/meshes/base_2.usdc create mode 100644 dimos/robot/unitree/go2/assets/meshes/base_3.obj create mode 100644 dimos/robot/unitree/go2/assets/meshes/base_3.usdc create mode 100644 dimos/robot/unitree/go2/assets/meshes/base_4.obj create mode 100644 dimos/robot/unitree/go2/assets/meshes/base_4.usdc create mode 100644 dimos/robot/unitree/go2/assets/meshes/calf_0.obj create mode 100644 dimos/robot/unitree/go2/assets/meshes/calf_0.usdc create mode 100644 dimos/robot/unitree/go2/assets/meshes/calf_1.obj create mode 100644 dimos/robot/unitree/go2/assets/meshes/calf_1.usdc create mode 100644 dimos/robot/unitree/go2/assets/meshes/calf_mirror_0.obj create mode 100644 dimos/robot/unitree/go2/assets/meshes/calf_mirror_0.usdc create mode 100644 dimos/robot/unitree/go2/assets/meshes/calf_mirror_1.obj create mode 100644 dimos/robot/unitree/go2/assets/meshes/calf_mirror_1.usdc create mode 100644 dimos/robot/unitree/go2/assets/meshes/foot.obj create mode 100644 dimos/robot/unitree/go2/assets/meshes/foot.usdc create mode 100644 dimos/robot/unitree/go2/assets/meshes/hip_0.obj create mode 100644 dimos/robot/unitree/go2/assets/meshes/hip_0.usdc create mode 100644 dimos/robot/unitree/go2/assets/meshes/hip_1.obj create mode 100644 dimos/robot/unitree/go2/assets/meshes/hip_1.usdc create mode 100644 dimos/robot/unitree/go2/assets/meshes/thigh_0.obj create mode 100644 dimos/robot/unitree/go2/assets/meshes/thigh_0.usdc create mode 100644 dimos/robot/unitree/go2/assets/meshes/thigh_1.obj create mode 100644 dimos/robot/unitree/go2/assets/meshes/thigh_1.usdc create mode 100644 dimos/robot/unitree/go2/assets/meshes/thigh_mirror_0.obj create mode 100644 dimos/robot/unitree/go2/assets/meshes/thigh_mirror_0.usdc create mode 100644 dimos/robot/unitree/go2/assets/meshes/thigh_mirror_1.obj create mode 100644 dimos/robot/unitree/go2/assets/meshes/thigh_mirror_1.usdc diff --git a/dimos/robot/unitree/go2/assets/go2.usda b/dimos/robot/unitree/go2/assets/go2.usda new file mode 100644 index 00000000000..a798ca1f6de --- /dev/null +++ b/dimos/robot/unitree/go2/assets/go2.usda @@ -0,0 +1,2420 @@ +#usda 1.0 +( + customLayerData = { + string creator = "MuJoCo USD Converter v0.9.0" + } + defaultPrim = "go2" + doc = """Generated from Composed Stage of root layer /tmp/claude-1000/-home-dimos/ff9ad774-151d-43c4-9774-fa96d8ad90f5/scratchpad/go2usd/converted/go2.usda +""" + kilogramsPerUnit = 1 + metersPerUnit = 1 + upAxis = "Z" +) + +over "Flattened_Prototype_1" +{ + def Shader "PreviewSurface" + { + uniform token info:id = "UsdPreviewSurface" + color3f inputs:diffuseColor.connect = + float inputs:metallic.connect = + float inputs:opacity.connect = + float inputs:roughness.connect = + token outputs:surface + } + + def Shader "OpenPBR" + { + uniform token info:id = "ND_open_pbr_surface_surfaceshader" + color3f inputs:base_color.connect = + float inputs:base_metalness.connect = + float inputs:geometry_opacity.connect = + float inputs:specular_roughness.connect = + token outputs:out + } +} + +over "Flattened_Prototype_2" +{ + def Shader "PreviewSurface" + { + uniform token info:id = "UsdPreviewSurface" + color3f inputs:diffuseColor.connect = + float inputs:metallic.connect = + float inputs:opacity.connect = + float inputs:roughness.connect = + token outputs:surface + } + + def Shader "OpenPBR" + { + uniform token info:id = "ND_open_pbr_surface_surfaceshader" + color3f inputs:base_color.connect = + float inputs:base_metalness.connect = + float inputs:geometry_opacity.connect = + float inputs:specular_roughness.connect = + token outputs:out + } +} + +over "Flattened_Prototype_3" +{ + def Shader "PreviewSurface" + { + uniform token info:id = "UsdPreviewSurface" + color3f inputs:diffuseColor.connect = + float inputs:metallic.connect = + float inputs:opacity.connect = + float inputs:roughness.connect = + token outputs:surface + } + + def Shader "OpenPBR" + { + uniform token info:id = "ND_open_pbr_surface_surfaceshader" + color3f inputs:base_color.connect = + float inputs:base_metalness.connect = + float inputs:geometry_opacity.connect = + float inputs:specular_roughness.connect = + token outputs:out + } +} + +over "Flattened_Prototype_4" +{ + def Shader "PreviewSurface" + { + uniform token info:id = "UsdPreviewSurface" + color3f inputs:diffuseColor.connect = + float inputs:metallic.connect = + float inputs:opacity.connect = + float inputs:roughness.connect = + token outputs:surface + } + + def Shader "OpenPBR" + { + uniform token info:id = "ND_open_pbr_surface_surfaceshader" + color3f inputs:base_color.connect = + float inputs:base_metalness.connect = + float inputs:geometry_opacity.connect = + float inputs:specular_roughness.connect = + token outputs:out + } +} + +def Xform "go2" ( + apiSchemas = ["GeomModelAPI"] + assetInfo = { + string name = "go2" + } + kind = "component" +) +{ + float3[] extentsHint = [(-0.24186657, -0.162013, -0.0039150002), (0.331958, 0.162013, 0.533834), (3.4028235e38, 3.4028235e38, 3.4028235e38), (-3.4028235e38, -3.4028235e38, -3.4028235e38), (3.4028235e38, 3.4028235e38, 3.4028235e38), (-3.4028235e38, -3.4028235e38, -3.4028235e38), (-0.2394, -0.164, -0.0029999998), (0.34, 0.164, 0.502)] + + def Scope "Geometry" + { + def Xform "base" ( + apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsMassAPI", "PhysicsArticulationRootAPI", "NewtonArticulationRootAPI"] + ) + { + uniform bool newton:jointsAddMobility = 1 + point3f physics:centerOfMass = (0.021112, 0, -0.005366) + float3 physics:diagonalInertia = (0.107027, 0.0980771, 0.0244531) + float physics:mass = 6.921 + quatf physics:principalAxes = (-0.0005434711, 0.71343505, -0.0017376901, 0.70071906) + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, 0.445) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + + def Mesh "base_0" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/base_0.obj@ + string version = "sha256:8cdc53719002a5ee737d867904e534a6d6fa930b616bdcc1b3131e577d4e46c0" + } + prepend references = @meshes/base_0.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (-1.66533e-15, 1.51788e-18, 3.40006e-16) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Mesh "base_1" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/base_1.obj@ + string version = "sha256:39d2f4724ff64dd5a0640c308fd98902a2946660a8d033ec8dbf368fd5af0dd6" + } + prepend references = @meshes/base_1.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (-1.66533e-15, 1.21973e-18, 3.40006e-16) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Mesh "base_2" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/base_2.obj@ + string version = "sha256:29cae463f95d678e4ede863c36c9b19b0d356b8340412add10670d70bdcc3095" + } + prepend references = @meshes/base_2.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Mesh "base_3" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/base_3.obj@ + string version = "sha256:f51ba43bf300e68957a0c1aa83eaf3e4926f4ac7e64a0d6c33ae5d21fcbcf5f6" + } + prepend references = @meshes/base_3.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (-4.92661e-16, -6.59195e-17, -2.70617e-16) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Mesh "base_4" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/base_4.obj@ + string version = "sha256:49ddec0129546e5dd2075d5beacf7f207baa2860aeff7626818e64d2efbf3821" + } + prepend references = @meshes/base_4.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (-7.49401e-16, 5.85469e-18, 7.69784e-18) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cube "Box" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + float3[] extent = [(-1, -1, -1), (1, 1, 1)] + rel material:binding:physics = + uniform int mjc:condim = 1 + uniform int mjc:group = 3 + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double size = 2 + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (0.1881, 0.04675, 0.057) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cylinder "Cylinder" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + uniform token axis = "Z" + float3[] extent = [(-0.05, -0.05, -0.045), (0.05, 0.05, 0.045)] + double height = 0.09 + rel material:binding:physics = + uniform int mjc:condim = 1 + uniform int mjc:group = 3 + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double radius = 0.05 + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0.285, 0, 0.01) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Sphere "Sphere" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + float3[] extent = [(-0.047, -0.047, -0.047), (0.047, 0.047, 0.047)] + rel material:binding:physics = + uniform int mjc:condim = 1 + uniform int mjc:group = 3 + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double radius = 0.047 + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0.293, 0, -0.06) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Sphere "imu" ( + apiSchemas = ["MjcSiteAPI", "NewtonSiteAPI"] + ) + { + float3[] extent = [(-0.005, -0.005, -0.005), (0.005, 0.005, 0.005)] + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double radius = 0.005 + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (-0.02557, 0, 0.04232) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Xform "FL_hip" ( + apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsMassAPI"] + ) + { + point3f physics:centerOfMass = (-0.0054, 0.00194, -0.000105) + float3 physics:diagonalInertia = (0.00088403, 0.000596003, 0.000479967) + float physics:mass = 0.678 + quatf physics:principalAxes = (0.49701393, 0.49924493, 0.50546193, 0.49823695) + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0.1934, 0.0465, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + + def Mesh "hip_0" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/hip_0.obj@ + string version = "sha256:cc29932ab4d251ce3f72389ec4c1f757e44eee138b87e772b648c2bc76b506cd" + } + prepend references = @meshes/hip_0.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Mesh "hip_1" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/hip_1.obj@ + string version = "sha256:3fd5bd9b2ef6abe622c0739d3c67eaa1679b19b47132951972c4892a779ea7cf" + } + prepend references = @meshes/hip_1.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cylinder "Cylinder" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + uniform token axis = "Z" + float3[] extent = [(-0.046, -0.046, -0.02), (0.046, 0.046, 0.02)] + double height = 0.04 + rel material:binding:physics = + uniform int mjc:condim = 1 + uniform int mjc:group = 3 + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double radius = 0.046 + quatf xformOp:orient = (0.7071068, 0.7071067, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0.08, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Xform "FL_thigh" ( + apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsMassAPI"] + ) + { + point3f physics:centerOfMass = (-0.00374, -0.0223, -0.0327) + float3 physics:diagonalInertia = (0.00594973, 0.00584149, 0.000878787) + float physics:mass = 1.152 + quatf physics:principalAxes = (0.82953286, 0.08476349, -0.020063197, 0.5516229) + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0.0955, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + + def Mesh "thigh_0" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/thigh_0.obj@ + string version = "sha256:bb3edfe6a7b04f4f4dff1ce7b05d426ddce941e276cc6372fb542055893c71ec" + } + prepend references = @meshes/thigh_0.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Mesh "thigh_1" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/thigh_1.obj@ + string version = "sha256:3a631822f9f7d2114b1b79dcdff5abf95103630abb7d91d1f3b8b18117132ed2" + } + prepend references = @meshes/thigh_1.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cube "Box" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + float3[] extent = [(-1, -1, -1), (1, 1, 1)] + rel material:binding:physics = + uniform int mjc:condim = 1 + uniform int mjc:group = 3 + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double size = 2 + quatf xformOp:orient = (0.7071068, 0, 0.7071067, 0) + float3 xformOp:scale = (0.1065, 0.01225, 0.017) + double3 xformOp:translate = (0, 0, -0.1065) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Xform "FL_calf" ( + apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsMassAPI"] + ) + { + point3f physics:centerOfMass = (0.00629595, -0.000622121, -0.141417) + float3 physics:diagonalInertia = (0.0014901, 0.00146356, 0.0000531397) + float physics:mass = 0.241352 + quatf physics:principalAxes = (0.7106718, 0.0015409894, -0.0045008687, 0.7035078) + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, -0.213) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + + def Mesh "calf_0" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/calf_0.obj@ + string version = "sha256:4a818cd563c5b7e2a271504442d05fd777d38cbaa028d4a10ad29c9f87bb355e" + } + prepend references = @meshes/calf_0.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (-1.99493e-17, 2.30393e-19, 3.33067e-16) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Mesh "calf_1" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/calf_1.obj@ + string version = "sha256:51950c3a153ae3208592df86ea11c9d6756f8f645ebca5caf6910c61aeb03817" + } + prepend references = @meshes/calf_1.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (4.94396e-17, -1.78893e-18, 1.94289e-15) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cylinder "Cylinder" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + uniform token axis = "Z" + float3[] extent = [(-0.012, -0.012, -0.06), (0.012, 0.012, 0.06)] + double height = 0.12 + rel material:binding:physics = + uniform int mjc:condim = 1 + uniform int mjc:group = 3 + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double radius = 0.012 + quatf xformOp:orient = (0.99449265, 0, -0.10480634, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0.008, 0, -0.06) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cylinder "Cylinder_1" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + displayName = "Cylinder" + uiHints = { + string displayName = "Cylinder" + } + ) + { + uniform token axis = "Z" + float3[] extent = [(-0.011, -0.011, -0.0325), (0.011, 0.011, 0.0325)] + double height = 0.065 + rel material:binding:physics = + uniform int mjc:condim = 1 + uniform int mjc:group = 3 + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double radius = 0.011 + quatf xformOp:orient = (0.99968755, 0, 0.02499595, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0.02, 0, -0.148) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Mesh "foot" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/foot.obj@ + string version = "sha256:df9e78a7c0110d02557439010f2a5f11f0ec4fd907b32704145dbd6aa7affc0b" + } + prepend references = @meshes/foot.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, -0.213) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Sphere "FL" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + float3[] extent = [(-0.022, -0.022, -0.022), (0.022, 0.022, 0.022)] + rel material:binding:physics = + uniform int mjc:condim = 6 + uniform int mjc:group = 3 + uniform int mjc:priority = 1 + uniform double[] mjc:solimp = [0.015, 1, 0.022, 0.5, 2] + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double radius = 0.022 + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (-0.002, 0, -0.213) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def PhysicsRevoluteJoint "FL_calf_joint" ( + apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + ) + { + float newton:armature = 0.01 + float newton:damping = 0.034906585 + float newton:friction = 0.2 + uniform token physics:axis = "Y" + rel physics:body0 = + rel physics:body1 = + point3f physics:localPos0 = (0, 0, -0.213) + point3f physics:localPos1 = (0, 0, 0) + quatf physics:localRot0 = (1, 0, 0, 0) + quatf physics:localRot1 = (1, 0, 0, 0) + float physics:lowerLimit = -155.99922 + float physics:upperLimit = -48.00011 + } + } + + def PhysicsRevoluteJoint "FL_thigh_joint" ( + apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + ) + { + float newton:armature = 0.01 + float newton:damping = 0.034906585 + float newton:friction = 0.2 + uniform token physics:axis = "Y" + rel physics:body0 = + rel physics:body1 = + point3f physics:localPos0 = (0, 0.0955, 0) + point3f physics:localPos1 = (0, 0, 0) + quatf physics:localRot0 = (1, 0, 0, 0) + quatf physics:localRot1 = (1, 0, 0, 0) + float physics:lowerLimit = -90.00021 + float physics:upperLimit = 200.00238 + } + } + + def PhysicsRevoluteJoint "FL_hip_joint" ( + apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + ) + { + float newton:armature = 0.01 + float newton:damping = 0.034906585 + float newton:friction = 0.2 + uniform token physics:axis = "X" + rel physics:body0 = + rel physics:body1 = + point3f physics:localPos0 = (0.1934, 0.0465, 0) + point3f physics:localPos1 = (0, 0, 0) + quatf physics:localRot0 = (1, 0, 0, 0) + quatf physics:localRot1 = (1, 0, 0, 0) + float physics:lowerLimit = -60.00014 + float physics:upperLimit = 60.00014 + } + } + + def Xform "FR_hip" ( + apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsMassAPI"] + ) + { + point3f physics:centerOfMass = (-0.0054, -0.00194, -0.000105) + float3 physics:diagonalInertia = (0.00088403, 0.000596003, 0.000479967) + float physics:mass = 0.678 + quatf physics:principalAxes = (0.49823695, 0.50546193, 0.49924493, 0.49701393) + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0.1934, -0.0465, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + + def Mesh "hip_0" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/hip_0.obj@ + string version = "sha256:cc29932ab4d251ce3f72389ec4c1f757e44eee138b87e772b648c2bc76b506cd" + } + prepend references = @meshes/hip_0.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (0.0000463268, 1, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Mesh "hip_1" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/hip_1.obj@ + string version = "sha256:3fd5bd9b2ef6abe622c0739d3c67eaa1679b19b47132951972c4892a779ea7cf" + } + prepend references = @meshes/hip_1.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (0.0000463268, 1, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cylinder "Cylinder" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + uniform token axis = "Z" + float3[] extent = [(-0.046, -0.046, -0.02), (0.046, 0.046, 0.02)] + double height = 0.04 + rel material:binding:physics = + uniform int mjc:condim = 1 + uniform int mjc:group = 3 + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double radius = 0.046 + quatf xformOp:orient = (0.7071068, 0.7071067, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, -0.08, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Xform "FR_thigh" ( + apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsMassAPI"] + ) + { + point3f physics:centerOfMass = (-0.00374, 0.0223, -0.0327) + float3 physics:diagonalInertia = (0.00594973, 0.00584149, 0.000878787) + float physics:mass = 1.152 + quatf physics:principalAxes = (0.5516229, -0.020063197, 0.08476349, 0.82953286) + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, -0.0955, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + + def Mesh "thigh_mirror_0" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/thigh_mirror_0.obj@ + string version = "sha256:07abaedce761f1dd48647384ac14ba1959276bc78d5a2a62051e8f1c6093933b" + } + prepend references = @meshes/thigh_mirror_0.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Mesh "thigh_mirror_1" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/thigh_mirror_1.obj@ + string version = "sha256:db984a3e983eabbc9b38183427cca9ab0bb3bdb049f9eab030e06edda36579d6" + } + prepend references = @meshes/thigh_mirror_1.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cube "Box" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + float3[] extent = [(-1, -1, -1), (1, 1, 1)] + rel material:binding:physics = + uniform int mjc:condim = 1 + uniform int mjc:group = 3 + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double size = 2 + quatf xformOp:orient = (0.7071068, 0, 0.7071067, 0) + float3 xformOp:scale = (0.1065, 0.01225, 0.017) + double3 xformOp:translate = (0, 0, -0.1065) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Xform "FR_calf" ( + apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsMassAPI"] + ) + { + point3f physics:centerOfMass = (0.00629595, 0.000622121, -0.141417) + float3 physics:diagonalInertia = (0.0014901, 0.00146356, 0.0000531397) + float physics:mass = 0.241352 + quatf physics:principalAxes = (0.70350766, -0.004500868, 0.0015409893, 0.71067166) + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, -0.213) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + + def Mesh "calf_mirror_0" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/calf_mirror_0.obj@ + string version = "sha256:6dc0406207a1c08a5953fd5962aca1a270fd06593d30d100205e05c1b32d4af2" + } + prepend references = @meshes/calf_mirror_0.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (-1.64799e-17, -1.89735e-19, 2.77556e-16) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Mesh "calf_mirror_1" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/calf_mirror_1.obj@ + string version = "sha256:f84f5af90b914633165beb3a5493ee499169144c5797dcf04c6eb5842f74b31a" + } + prepend references = @meshes/calf_mirror_1.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (4.94396e-17, 1.78893e-18, 1.94289e-15) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cylinder "Cylinder" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + uniform token axis = "Z" + float3[] extent = [(-0.013, -0.013, -0.06), (0.013, 0.013, 0.06)] + double height = 0.12 + rel material:binding:physics = + uniform int mjc:condim = 1 + uniform int mjc:group = 3 + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double radius = 0.013 + quatf xformOp:orient = (0.9950042, 0, -0.0998333, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0.01, 0, -0.06) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cylinder "Cylinder_1" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + displayName = "Cylinder" + uiHints = { + string displayName = "Cylinder" + } + ) + { + uniform token axis = "Z" + float3[] extent = [(-0.011, -0.011, -0.0325), (0.011, 0.011, 0.0325)] + double height = 0.065 + rel material:binding:physics = + uniform int mjc:condim = 1 + uniform int mjc:group = 3 + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double radius = 0.011 + quatf xformOp:orient = (0.99968755, 0, 0.02499595, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0.02, 0, -0.148) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Mesh "foot" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/foot.obj@ + string version = "sha256:df9e78a7c0110d02557439010f2a5f11f0ec4fd907b32704145dbd6aa7affc0b" + } + prepend references = @meshes/foot.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, -0.213) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Sphere "FR" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + float3[] extent = [(-0.022, -0.022, -0.022), (0.022, 0.022, 0.022)] + rel material:binding:physics = + uniform int mjc:condim = 6 + uniform int mjc:group = 3 + uniform int mjc:priority = 1 + uniform double[] mjc:solimp = [0.015, 1, 0.022, 0.5, 2] + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double radius = 0.022 + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (-0.002, 0, -0.213) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def PhysicsRevoluteJoint "FR_calf_joint" ( + apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + ) + { + float newton:armature = 0.01 + float newton:damping = 0.034906585 + float newton:friction = 0.2 + uniform token physics:axis = "Y" + rel physics:body0 = + rel physics:body1 = + point3f physics:localPos0 = (0, 0, -0.213) + point3f physics:localPos1 = (0, 0, 0) + quatf physics:localRot0 = (1, 0, 0, 0) + quatf physics:localRot1 = (1, 0, 0, 0) + float physics:lowerLimit = -155.99922 + float physics:upperLimit = -48.00011 + } + } + + def PhysicsRevoluteJoint "FR_thigh_joint" ( + apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + ) + { + float newton:armature = 0.01 + float newton:damping = 0.034906585 + float newton:friction = 0.2 + uniform token physics:axis = "Y" + rel physics:body0 = + rel physics:body1 = + point3f physics:localPos0 = (0, -0.0955, 0) + point3f physics:localPos1 = (0, 0, 0) + quatf physics:localRot0 = (1, 0, 0, 0) + quatf physics:localRot1 = (1, 0, 0, 0) + float physics:lowerLimit = -90.00021 + float physics:upperLimit = 200.00238 + } + } + + def PhysicsRevoluteJoint "FR_hip_joint" ( + apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + ) + { + float newton:armature = 0.01 + float newton:damping = 0.034906585 + float newton:friction = 0.2 + uniform token physics:axis = "X" + rel physics:body0 = + rel physics:body1 = + point3f physics:localPos0 = (0.1934, -0.0465, 0) + point3f physics:localPos1 = (0, 0, 0) + quatf physics:localRot0 = (1, 0, 0, 0) + quatf physics:localRot1 = (1, 0, 0, 0) + float physics:lowerLimit = -60.00014 + float physics:upperLimit = 60.00014 + } + } + + def Xform "RL_hip" ( + apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsMassAPI"] + ) + { + point3f physics:centerOfMass = (0.0054, 0.00194, -0.000105) + float3 physics:diagonalInertia = (0.00088403, 0.000596003, 0.000479967) + float physics:mass = 0.678 + quatf physics:principalAxes = (0.5054619, 0.4982369, 0.49701387, 0.49924487) + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (-0.1934, 0.0465, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + + def Mesh "hip_0" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/hip_0.obj@ + string version = "sha256:cc29932ab4d251ce3f72389ec4c1f757e44eee138b87e772b648c2bc76b506cd" + } + prepend references = @meshes/hip_0.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (0.0000463268, 0, 1, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Mesh "hip_1" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/hip_1.obj@ + string version = "sha256:3fd5bd9b2ef6abe622c0739d3c67eaa1679b19b47132951972c4892a779ea7cf" + } + prepend references = @meshes/hip_1.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (0.0000463268, 0, 1, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cylinder "Cylinder" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + uniform token axis = "Z" + float3[] extent = [(-0.046, -0.046, -0.02), (0.046, 0.046, 0.02)] + double height = 0.04 + rel material:binding:physics = + uniform int mjc:condim = 1 + uniform int mjc:group = 3 + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double radius = 0.046 + quatf xformOp:orient = (0.7071068, 0.7071067, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0.08, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Xform "RL_thigh" ( + apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsMassAPI"] + ) + { + point3f physics:centerOfMass = (-0.00374, -0.0223, -0.0327) + float3 physics:diagonalInertia = (0.00594973, 0.00584149, 0.000878787) + float physics:mass = 1.152 + quatf physics:principalAxes = (0.82953286, 0.08476349, -0.020063197, 0.5516229) + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0.0955, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + + def Mesh "thigh_0" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/thigh_0.obj@ + string version = "sha256:bb3edfe6a7b04f4f4dff1ce7b05d426ddce941e276cc6372fb542055893c71ec" + } + prepend references = @meshes/thigh_0.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Mesh "thigh_1" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/thigh_1.obj@ + string version = "sha256:3a631822f9f7d2114b1b79dcdff5abf95103630abb7d91d1f3b8b18117132ed2" + } + prepend references = @meshes/thigh_1.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cube "Box" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + float3[] extent = [(-1, -1, -1), (1, 1, 1)] + rel material:binding:physics = + uniform int mjc:condim = 1 + uniform int mjc:group = 3 + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double size = 2 + quatf xformOp:orient = (0.7071068, 0, 0.7071067, 0) + float3 xformOp:scale = (0.1065, 0.01225, 0.017) + double3 xformOp:translate = (0, 0, -0.1065) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Xform "RL_calf" ( + apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsMassAPI"] + ) + { + point3f physics:centerOfMass = (0.00629595, -0.000622121, -0.141417) + float3 physics:diagonalInertia = (0.0014901, 0.00146356, 0.0000531397) + float physics:mass = 0.241352 + quatf physics:principalAxes = (0.7106718, 0.0015409894, -0.0045008687, 0.7035078) + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, -0.213) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + + def Mesh "calf_0" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/calf_0.obj@ + string version = "sha256:4a818cd563c5b7e2a271504442d05fd777d38cbaa028d4a10ad29c9f87bb355e" + } + prepend references = @meshes/calf_0.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (-1.99493e-17, 2.30393e-19, 3.33067e-16) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Mesh "calf_1" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/calf_1.obj@ + string version = "sha256:51950c3a153ae3208592df86ea11c9d6756f8f645ebca5caf6910c61aeb03817" + } + prepend references = @meshes/calf_1.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (4.94396e-17, -1.78893e-18, 1.94289e-15) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cylinder "Cylinder" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + uniform token axis = "Z" + float3[] extent = [(-0.013, -0.013, -0.06), (0.013, 0.013, 0.06)] + double height = 0.12 + rel material:binding:physics = + uniform int mjc:condim = 1 + uniform int mjc:group = 3 + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double radius = 0.013 + quatf xformOp:orient = (0.9950042, 0, -0.0998333, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0.01, 0, -0.06) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cylinder "Cylinder_1" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + displayName = "Cylinder" + uiHints = { + string displayName = "Cylinder" + } + ) + { + uniform token axis = "Z" + float3[] extent = [(-0.011, -0.011, -0.0325), (0.011, 0.011, 0.0325)] + double height = 0.065 + rel material:binding:physics = + uniform int mjc:condim = 1 + uniform int mjc:group = 3 + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double radius = 0.011 + quatf xformOp:orient = (0.99968755, 0, 0.02499595, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0.02, 0, -0.148) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Mesh "foot" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/foot.obj@ + string version = "sha256:df9e78a7c0110d02557439010f2a5f11f0ec4fd907b32704145dbd6aa7affc0b" + } + prepend references = @meshes/foot.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, -0.213) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Sphere "RL" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + float3[] extent = [(-0.022, -0.022, -0.022), (0.022, 0.022, 0.022)] + rel material:binding:physics = + uniform int mjc:condim = 6 + uniform int mjc:group = 3 + uniform int mjc:priority = 1 + uniform double[] mjc:solimp = [0.015, 1, 0.022, 0.5, 2] + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double radius = 0.022 + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (-0.002, 0, -0.213) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def PhysicsRevoluteJoint "RL_calf_joint" ( + apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + ) + { + float newton:armature = 0.01 + float newton:damping = 0.034906585 + float newton:friction = 0.2 + uniform token physics:axis = "Y" + rel physics:body0 = + rel physics:body1 = + point3f physics:localPos0 = (0, 0, -0.213) + point3f physics:localPos1 = (0, 0, 0) + quatf physics:localRot0 = (1, 0, 0, 0) + quatf physics:localRot1 = (1, 0, 0, 0) + float physics:lowerLimit = -155.99922 + float physics:upperLimit = -48.00011 + } + } + + def PhysicsRevoluteJoint "RL_thigh_joint" ( + apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + ) + { + float newton:armature = 0.01 + float newton:damping = 0.034906585 + float newton:friction = 0.2 + uniform token physics:axis = "Y" + rel physics:body0 = + rel physics:body1 = + point3f physics:localPos0 = (0, 0.0955, 0) + point3f physics:localPos1 = (0, 0, 0) + quatf physics:localRot0 = (1, 0, 0, 0) + quatf physics:localRot1 = (1, 0, 0, 0) + float physics:lowerLimit = -30.00007 + float physics:upperLimit = 260.00253 + } + } + + def PhysicsRevoluteJoint "RL_hip_joint" ( + apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + ) + { + float newton:armature = 0.01 + float newton:damping = 0.034906585 + float newton:friction = 0.2 + uniform token physics:axis = "X" + rel physics:body0 = + rel physics:body1 = + point3f physics:localPos0 = (-0.1934, 0.0465, 0) + point3f physics:localPos1 = (0, 0, 0) + quatf physics:localRot0 = (1, 0, 0, 0) + quatf physics:localRot1 = (1, 0, 0, 0) + float physics:lowerLimit = -60.00014 + float physics:upperLimit = 60.00014 + } + } + + def Xform "RR_hip" ( + apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsMassAPI"] + ) + { + point3f physics:centerOfMass = (0.0054, -0.00194, -0.000105) + float3 physics:diagonalInertia = (0.00088403, 0.000596003, 0.000479967) + float physics:mass = 0.678 + quatf physics:principalAxes = (0.49924493, 0.49701393, 0.49823695, 0.50546193) + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (-0.1934, -0.0465, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + + def Mesh "hip_0" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/hip_0.obj@ + string version = "sha256:cc29932ab4d251ce3f72389ec4c1f757e44eee138b87e772b648c2bc76b506cd" + } + prepend references = @meshes/hip_0.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (-2.1461701e-9, -0.0000463268, -0.0000463268, 1) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Mesh "hip_1" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/hip_1.obj@ + string version = "sha256:3fd5bd9b2ef6abe622c0739d3c67eaa1679b19b47132951972c4892a779ea7cf" + } + prepend references = @meshes/hip_1.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (-2.1461701e-9, -0.0000463268, -0.0000463268, 1) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cylinder "Cylinder" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + uniform token axis = "Z" + float3[] extent = [(-0.046, -0.046, -0.02), (0.046, 0.046, 0.02)] + double height = 0.04 + rel material:binding:physics = + uniform int mjc:condim = 1 + uniform int mjc:group = 3 + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double radius = 0.046 + quatf xformOp:orient = (0.7071068, 0.7071067, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, -0.08, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Xform "RR_thigh" ( + apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsMassAPI"] + ) + { + point3f physics:centerOfMass = (-0.00374, 0.0223, -0.0327) + float3 physics:diagonalInertia = (0.00594973, 0.00584149, 0.000878787) + float physics:mass = 1.152 + quatf physics:principalAxes = (0.5516229, -0.020063197, 0.08476349, 0.82953286) + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, -0.0955, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + + def Mesh "thigh_mirror_0" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/thigh_mirror_0.obj@ + string version = "sha256:07abaedce761f1dd48647384ac14ba1959276bc78d5a2a62051e8f1c6093933b" + } + prepend references = @meshes/thigh_mirror_0.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Mesh "thigh_mirror_1" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/thigh_mirror_1.obj@ + string version = "sha256:db984a3e983eabbc9b38183427cca9ab0bb3bdb049f9eab030e06edda36579d6" + } + prepend references = @meshes/thigh_mirror_1.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cube "Box" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + float3[] extent = [(-1, -1, -1), (1, 1, 1)] + rel material:binding:physics = + uniform int mjc:condim = 1 + uniform int mjc:group = 3 + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double size = 2 + quatf xformOp:orient = (0.7071068, 0, 0.7071067, 0) + float3 xformOp:scale = (0.1065, 0.01225, 0.017) + double3 xformOp:translate = (0, 0, -0.1065) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Xform "RR_calf" ( + apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsMassAPI"] + ) + { + point3f physics:centerOfMass = (0.00629595, 0.000622121, -0.141417) + float3 physics:diagonalInertia = (0.0014901, 0.00146356, 0.0000531397) + float physics:mass = 0.241352 + quatf physics:principalAxes = (0.70350766, -0.004500868, 0.0015409893, 0.71067166) + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, -0.213) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + + def Mesh "calf_mirror_0" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/calf_mirror_0.obj@ + string version = "sha256:6dc0406207a1c08a5953fd5962aca1a270fd06593d30d100205e05c1b32d4af2" + } + prepend references = @meshes/calf_mirror_0.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (-1.64799e-17, -1.89735e-19, 2.77556e-16) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Mesh "calf_mirror_1" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/calf_mirror_1.obj@ + string version = "sha256:f84f5af90b914633165beb3a5493ee499169144c5797dcf04c6eb5842f74b31a" + } + prepend references = @meshes/calf_mirror_1.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (4.94396e-17, 1.78893e-18, 1.94289e-15) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cylinder "Cylinder" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + uniform token axis = "Z" + float3[] extent = [(-0.013, -0.013, -0.06), (0.013, 0.013, 0.06)] + double height = 0.12 + rel material:binding:physics = + uniform int mjc:condim = 1 + uniform int mjc:group = 3 + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double radius = 0.013 + quatf xformOp:orient = (0.9950042, 0, -0.0998333, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0.01, 0, -0.06) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cylinder "Cylinder_1" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + displayName = "Cylinder" + uiHints = { + string displayName = "Cylinder" + } + ) + { + uniform token axis = "Z" + float3[] extent = [(-0.011, -0.011, -0.0325), (0.011, 0.011, 0.0325)] + double height = 0.065 + rel material:binding:physics = + uniform int mjc:condim = 1 + uniform int mjc:group = 3 + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double radius = 0.011 + quatf xformOp:orient = (0.99968755, 0, 0.02499595, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0.02, 0, -0.148) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Mesh "foot" ( + apiSchemas = ["MaterialBindingAPI", "MjcImageableAPI"] + assetInfo = { + asset identifier = @meshes/foot.obj@ + string version = "sha256:df9e78a7c0110d02557439010f2a5f11f0ec4fd907b32704145dbd6aa7affc0b" + } + prepend references = @meshes/foot.usdc@ + ) + { + rel material:binding = + uniform int mjc:group = 2 + uniform token orientation = "rightHanded" + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + uniform token subdivisionScheme = "none" + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, -0.213) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Sphere "RR" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + float3[] extent = [(-0.022, -0.022, -0.022), (0.022, 0.022, 0.022)] + rel material:binding:physics = + uniform int mjc:condim = 6 + uniform int mjc:group = 3 + uniform int mjc:priority = 1 + uniform double[] mjc:solimp = [0.015, 1, 0.022, 0.5, 2] + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float newton:contactMargin = 0.001 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + uniform token purpose = "guide" + double radius = 0.022 + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (-0.002, 0, -0.213) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def PhysicsRevoluteJoint "RR_calf_joint" ( + apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + ) + { + float newton:armature = 0.01 + float newton:damping = 0.034906585 + float newton:friction = 0.2 + uniform token physics:axis = "Y" + rel physics:body0 = + rel physics:body1 = + point3f physics:localPos0 = (0, 0, -0.213) + point3f physics:localPos1 = (0, 0, 0) + quatf physics:localRot0 = (1, 0, 0, 0) + quatf physics:localRot1 = (1, 0, 0, 0) + float physics:lowerLimit = -155.99922 + float physics:upperLimit = -48.00011 + } + } + + def PhysicsRevoluteJoint "RR_thigh_joint" ( + apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + ) + { + float newton:armature = 0.01 + float newton:damping = 0.034906585 + float newton:friction = 0.2 + uniform token physics:axis = "Y" + rel physics:body0 = + rel physics:body1 = + point3f physics:localPos0 = (0, -0.0955, 0) + point3f physics:localPos1 = (0, 0, 0) + quatf physics:localRot0 = (1, 0, 0, 0) + quatf physics:localRot1 = (1, 0, 0, 0) + float physics:lowerLimit = -30.00007 + float physics:upperLimit = 260.00253 + } + } + + def PhysicsRevoluteJoint "RR_hip_joint" ( + apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + ) + { + float newton:armature = 0.01 + float newton:damping = 0.034906585 + float newton:friction = 0.2 + uniform token physics:axis = "X" + rel physics:body0 = + rel physics:body1 = + point3f physics:localPos0 = (-0.1934, -0.0465, 0) + point3f physics:localPos1 = (0, 0, 0) + quatf physics:localRot0 = (1, 0, 0, 0) + quatf physics:localRot1 = (1, 0, 0, 0) + float physics:lowerLimit = -60.00014 + float physics:upperLimit = 60.00014 + } + } + } + } + + def Scope "Materials" + { + def Material "black" ( + apiSchemas = ["MaterialXConfigAPI"] + instanceable = true + add references = + ) + { + string config:mtlx:version = "1.39" + color3f inputs:color = (0, 0, 0) ( + limits = { + dictionary hard = { + float3 maximum = (1, 1, 1) + float3 minimum = (0, 0, 0) + } + } + sdrMetadata = { + string uimax = "1, 1, 1" + string uimin = "0, 0, 0" + } + ) + float inputs:metallic = 0 ( + limits = { + dictionary hard = { + float maximum = 1 + float minimum = 0 + } + } + sdrMetadata = { + string uimax = "1" + string uimin = "0" + } + ) + float inputs:opacity = 1 ( + limits = { + dictionary hard = { + float maximum = 1 + float minimum = 0 + } + } + sdrMetadata = { + string uimax = "1" + string uimin = "0" + } + ) + float inputs:roughness = 0.5 ( + limits = { + dictionary hard = { + float maximum = 1 + float minimum = 0 + } + } + sdrMetadata = { + string uimax = "1" + string uimin = "0" + } + ) + token outputs:mtlx:surface.connect = + token outputs:surface.connect = + } + + def Material "white" ( + apiSchemas = ["MaterialXConfigAPI"] + instanceable = true + add references = + ) + { + string config:mtlx:version = "1.39" + color3f inputs:color = (0.99999994, 1, 0.9999999) ( + limits = { + dictionary hard = { + float3 maximum = (1, 1, 1) + float3 minimum = (0, 0, 0) + } + } + sdrMetadata = { + string uimax = "1, 1, 1" + string uimin = "0, 0, 0" + } + ) + float inputs:metallic = 0 ( + limits = { + dictionary hard = { + float maximum = 1 + float minimum = 0 + } + } + sdrMetadata = { + string uimax = "1" + string uimin = "0" + } + ) + float inputs:opacity = 1 ( + limits = { + dictionary hard = { + float maximum = 1 + float minimum = 0 + } + } + sdrMetadata = { + string uimax = "1" + string uimin = "0" + } + ) + float inputs:roughness = 0.5 ( + limits = { + dictionary hard = { + float maximum = 1 + float minimum = 0 + } + } + sdrMetadata = { + string uimax = "1" + string uimin = "0" + } + ) + token outputs:mtlx:surface.connect = + token outputs:surface.connect = + } + + def Material "gray" ( + apiSchemas = ["MaterialXConfigAPI"] + instanceable = true + add references = + ) + { + string config:mtlx:version = "1.39" + color3f inputs:color = (0.40874615, 0.43727827, 0.5611312) ( + limits = { + dictionary hard = { + float3 maximum = (1, 1, 1) + float3 minimum = (0, 0, 0) + } + } + sdrMetadata = { + string uimax = "1, 1, 1" + string uimin = "0, 0, 0" + } + ) + float inputs:metallic = 0 ( + limits = { + dictionary hard = { + float maximum = 1 + float minimum = 0 + } + } + sdrMetadata = { + string uimax = "1" + string uimin = "0" + } + ) + float inputs:opacity = 1 ( + limits = { + dictionary hard = { + float maximum = 1 + float minimum = 0 + } + } + sdrMetadata = { + string uimax = "1" + string uimin = "0" + } + ) + float inputs:roughness = 0.5 ( + limits = { + dictionary hard = { + float maximum = 1 + float minimum = 0 + } + } + sdrMetadata = { + string uimax = "1" + string uimin = "0" + } + ) + token outputs:mtlx:surface.connect = + token outputs:surface.connect = + } + + def Material "metal" ( + apiSchemas = ["MaterialXConfigAPI"] + instanceable = true + add references = + ) + { + string config:mtlx:version = "1.39" + color3f inputs:color = (0.7874123, 0.8900055, 0.8900054) ( + limits = { + dictionary hard = { + float3 maximum = (1, 1, 1) + float3 minimum = (0, 0, 0) + } + } + sdrMetadata = { + string uimax = "1, 1, 1" + string uimin = "0, 0, 0" + } + ) + float inputs:metallic = 0 ( + limits = { + dictionary hard = { + float maximum = 1 + float minimum = 0 + } + } + sdrMetadata = { + string uimax = "1" + string uimin = "0" + } + ) + float inputs:opacity = 1 ( + limits = { + dictionary hard = { + float maximum = 1 + float minimum = 0 + } + } + sdrMetadata = { + string uimax = "1" + string uimin = "0" + } + ) + float inputs:roughness = 0.5 ( + limits = { + dictionary hard = { + float maximum = 1 + float minimum = 0 + } + } + sdrMetadata = { + string uimax = "1" + string uimin = "0" + } + ) + token outputs:mtlx:surface.connect = + token outputs:surface.connect = + } + } + + def Scope "Physics" + { + def Material "PhysicsMaterial" ( + apiSchemas = ["PhysicsMaterialAPI", "NewtonMaterialAPI"] + ) + { + float newton:contactDamping = 100 + float newton:contactStiffness = 2500 + float newton:rollingFriction = 0.0001 + float newton:torsionalFriction = 0.005 + float physics:dynamicFriction = 0.6 + float physics:staticFriction = 0.6 + } + + def Material "PhysicsMaterial_1" ( + apiSchemas = ["PhysicsMaterialAPI", "NewtonMaterialAPI"] + ) + { + float newton:contactDamping = 100 + float newton:contactStiffness = 2500 + float newton:rollingFriction = 0.01 + float newton:torsionalFriction = 0.02 + float physics:dynamicFriction = 0.8 + float physics:staticFriction = 0.8 + } + + def MjcActuator "FL_hip" + { + uniform double mjc:ctrlRange:max = 23.7 + uniform double mjc:ctrlRange:min = -23.7 + rel mjc:target = + } + + def MjcActuator "FL_thigh" + { + uniform double mjc:ctrlRange:max = 23.7 + uniform double mjc:ctrlRange:min = -23.7 + rel mjc:target = + } + + def MjcActuator "FL_calf" + { + uniform double mjc:ctrlRange:max = 45.43 + uniform double mjc:ctrlRange:min = -45.43 + rel mjc:target = + } + + def MjcActuator "FR_hip" + { + uniform double mjc:ctrlRange:max = 23.7 + uniform double mjc:ctrlRange:min = -23.7 + rel mjc:target = + } + + def MjcActuator "FR_thigh" + { + uniform double mjc:ctrlRange:max = 23.7 + uniform double mjc:ctrlRange:min = -23.7 + rel mjc:target = + } + + def MjcActuator "FR_calf" + { + uniform double mjc:ctrlRange:max = 45.43 + uniform double mjc:ctrlRange:min = -45.43 + rel mjc:target = + } + + def MjcActuator "RL_hip" + { + uniform double mjc:ctrlRange:max = 23.7 + uniform double mjc:ctrlRange:min = -23.7 + rel mjc:target = + } + + def MjcActuator "RL_thigh" + { + uniform double mjc:ctrlRange:max = 23.7 + uniform double mjc:ctrlRange:min = -23.7 + rel mjc:target = + } + + def MjcActuator "RL_calf" + { + uniform double mjc:ctrlRange:max = 45.43 + uniform double mjc:ctrlRange:min = -45.43 + rel mjc:target = + } + + def MjcActuator "RR_hip" + { + uniform double mjc:ctrlRange:max = 23.7 + uniform double mjc:ctrlRange:min = -23.7 + rel mjc:target = + } + + def MjcActuator "RR_thigh" + { + uniform double mjc:ctrlRange:max = 23.7 + uniform double mjc:ctrlRange:min = -23.7 + rel mjc:target = + } + + def MjcActuator "RR_calf" + { + uniform double mjc:ctrlRange:max = 45.43 + uniform double mjc:ctrlRange:min = -45.43 + rel mjc:target = + } + } +} + +def PhysicsScene "PhysicsScene" ( + apiSchemas = ["NewtonSceneAPI", "MjcSceneAPI"] +) +{ + uniform token mjc:compiler:angle = "radian" + uniform token mjc:option:cone = "elliptic" + uniform double mjc:option:impratio = 100 + uniform int newton:maxSolverIterations = 100 + uniform int newton:timeStepsPerSecond = 500 + vector3f physics:gravityDirection = (0, 0, -1) + float physics:gravityMagnitude = 9.81 +} diff --git a/dimos/robot/unitree/go2/assets/meshes/base_0.obj b/dimos/robot/unitree/go2/assets/meshes/base_0.obj new file mode 100644 index 00000000000..5fe9df91055 --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/base_0.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cdc53719002a5ee737d867904e534a6d6fa930b616bdcc1b3131e577d4e46c0 +size 1330465 diff --git a/dimos/robot/unitree/go2/assets/meshes/base_0.usdc b/dimos/robot/unitree/go2/assets/meshes/base_0.usdc new file mode 100644 index 00000000000..0038b6ce998 --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/base_0.usdc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bdf0954da8b72a7c1912509d4138358e2ce66cea8489f9eb679970802f08659e +size 145178 diff --git a/dimos/robot/unitree/go2/assets/meshes/base_1.obj b/dimos/robot/unitree/go2/assets/meshes/base_1.obj new file mode 100644 index 00000000000..794801d4d61 --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/base_1.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39d2f4724ff64dd5a0640c308fd98902a2946660a8d033ec8dbf368fd5af0dd6 +size 811791 diff --git a/dimos/robot/unitree/go2/assets/meshes/base_1.usdc b/dimos/robot/unitree/go2/assets/meshes/base_1.usdc new file mode 100644 index 00000000000..0e24ca084ae --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/base_1.usdc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60db2dd6c8e9da1bc1eaa9f9e265fa05539495f9dde6c228f08a50bc8243af41 +size 93854 diff --git a/dimos/robot/unitree/go2/assets/meshes/base_2.obj b/dimos/robot/unitree/go2/assets/meshes/base_2.obj new file mode 100644 index 00000000000..4f9b38d8d01 --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/base_2.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29cae463f95d678e4ede863c36c9b19b0d356b8340412add10670d70bdcc3095 +size 294090 diff --git a/dimos/robot/unitree/go2/assets/meshes/base_2.usdc b/dimos/robot/unitree/go2/assets/meshes/base_2.usdc new file mode 100644 index 00000000000..3c3c8de8e7e --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/base_2.usdc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4a2fe5304c694d10166999f88b278101a4b8f887c316af480bab06a812489ca +size 34306 diff --git a/dimos/robot/unitree/go2/assets/meshes/base_3.obj b/dimos/robot/unitree/go2/assets/meshes/base_3.obj new file mode 100644 index 00000000000..a15aa64b2c3 --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/base_3.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f51ba43bf300e68957a0c1aa83eaf3e4926f4ac7e64a0d6c33ae5d21fcbcf5f6 +size 379093 diff --git a/dimos/robot/unitree/go2/assets/meshes/base_3.usdc b/dimos/robot/unitree/go2/assets/meshes/base_3.usdc new file mode 100644 index 00000000000..0e95d6ed8a0 --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/base_3.usdc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe4aeeaf2a78031bdae691a82ef56a102f9139167093917de62bbfbab4b52104 +size 43946 diff --git a/dimos/robot/unitree/go2/assets/meshes/base_4.obj b/dimos/robot/unitree/go2/assets/meshes/base_4.obj new file mode 100644 index 00000000000..b1f8c0da332 --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/base_4.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49ddec0129546e5dd2075d5beacf7f207baa2860aeff7626818e64d2efbf3821 +size 7776234 diff --git a/dimos/robot/unitree/go2/assets/meshes/base_4.usdc b/dimos/robot/unitree/go2/assets/meshes/base_4.usdc new file mode 100644 index 00000000000..06b2d72069c --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/base_4.usdc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa1cefc73622ef12333bdd9c6149cc3494906ef5930c524d1a246714f84e3d08 +size 801602 diff --git a/dimos/robot/unitree/go2/assets/meshes/calf_0.obj b/dimos/robot/unitree/go2/assets/meshes/calf_0.obj new file mode 100644 index 00000000000..3f35628886d --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/calf_0.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a818cd563c5b7e2a271504442d05fd777d38cbaa028d4a10ad29c9f87bb355e +size 877174 diff --git a/dimos/robot/unitree/go2/assets/meshes/calf_0.usdc b/dimos/robot/unitree/go2/assets/meshes/calf_0.usdc new file mode 100644 index 00000000000..624aecfcb88 --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/calf_0.usdc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a027596b564101bbc90dc6813e203631c6ed3e65ba82f912a00121717cc862ac +size 96538 diff --git a/dimos/robot/unitree/go2/assets/meshes/calf_1.obj b/dimos/robot/unitree/go2/assets/meshes/calf_1.obj new file mode 100644 index 00000000000..053c95f2f19 --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/calf_1.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51950c3a153ae3208592df86ea11c9d6756f8f645ebca5caf6910c61aeb03817 +size 326865 diff --git a/dimos/robot/unitree/go2/assets/meshes/calf_1.usdc b/dimos/robot/unitree/go2/assets/meshes/calf_1.usdc new file mode 100644 index 00000000000..d98525955e8 --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/calf_1.usdc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d7cddd2273cbbdf4e76f9bc183eab355cc4ebd867dfbaa13c5f99e60dccd16d +size 37930 diff --git a/dimos/robot/unitree/go2/assets/meshes/calf_mirror_0.obj b/dimos/robot/unitree/go2/assets/meshes/calf_mirror_0.obj new file mode 100644 index 00000000000..ba259c36a94 --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/calf_mirror_0.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6dc0406207a1c08a5953fd5962aca1a270fd06593d30d100205e05c1b32d4af2 +size 876538 diff --git a/dimos/robot/unitree/go2/assets/meshes/calf_mirror_0.usdc b/dimos/robot/unitree/go2/assets/meshes/calf_mirror_0.usdc new file mode 100644 index 00000000000..4c122f0da8b --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/calf_mirror_0.usdc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b27304f37607e8c922d203a78de9b9f49b57df6a5ccda56774a793691a71993 +size 96430 diff --git a/dimos/robot/unitree/go2/assets/meshes/calf_mirror_1.obj b/dimos/robot/unitree/go2/assets/meshes/calf_mirror_1.obj new file mode 100644 index 00000000000..6a8527eea70 --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/calf_mirror_1.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f84f5af90b914633165beb3a5493ee499169144c5797dcf04c6eb5842f74b31a +size 327261 diff --git a/dimos/robot/unitree/go2/assets/meshes/calf_mirror_1.usdc b/dimos/robot/unitree/go2/assets/meshes/calf_mirror_1.usdc new file mode 100644 index 00000000000..3a8e488dd2a --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/calf_mirror_1.usdc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2cc51d09f4eec723243156a6e45c94f91d72372e4e33ffd660415d3f59dd7da +size 38006 diff --git a/dimos/robot/unitree/go2/assets/meshes/foot.obj b/dimos/robot/unitree/go2/assets/meshes/foot.obj new file mode 100644 index 00000000000..51a8bf807c4 --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/foot.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df9e78a7c0110d02557439010f2a5f11f0ec4fd907b32704145dbd6aa7affc0b +size 1144729 diff --git a/dimos/robot/unitree/go2/assets/meshes/foot.usdc b/dimos/robot/unitree/go2/assets/meshes/foot.usdc new file mode 100644 index 00000000000..bb06b449079 --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/foot.usdc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34b59f98171216944299a4e7f383b6cc4097d4614fc5cacfadba7d8cc261b682 +size 181362 diff --git a/dimos/robot/unitree/go2/assets/meshes/hip_0.obj b/dimos/robot/unitree/go2/assets/meshes/hip_0.obj new file mode 100644 index 00000000000..cc11c34b045 --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/hip_0.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc29932ab4d251ce3f72389ec4c1f757e44eee138b87e772b648c2bc76b506cd +size 2863864 diff --git a/dimos/robot/unitree/go2/assets/meshes/hip_0.usdc b/dimos/robot/unitree/go2/assets/meshes/hip_0.usdc new file mode 100644 index 00000000000..b2b0d36e91b --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/hip_0.usdc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f601f6be95ade019bce65636481db4941f1b417cf805b2c25fb439896a2b4e85 +size 307938 diff --git a/dimos/robot/unitree/go2/assets/meshes/hip_1.obj b/dimos/robot/unitree/go2/assets/meshes/hip_1.obj new file mode 100644 index 00000000000..ce4ab76ed4e --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/hip_1.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3fd5bd9b2ef6abe622c0739d3c67eaa1679b19b47132951972c4892a779ea7cf +size 2767918 diff --git a/dimos/robot/unitree/go2/assets/meshes/hip_1.usdc b/dimos/robot/unitree/go2/assets/meshes/hip_1.usdc new file mode 100644 index 00000000000..f99b04b2f77 --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/hip_1.usdc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3b479d7d8540be8a8315c6fd010c36bf81dd771f5f5d42994ab3f17feddefab +size 295838 diff --git a/dimos/robot/unitree/go2/assets/meshes/thigh_0.obj b/dimos/robot/unitree/go2/assets/meshes/thigh_0.obj new file mode 100644 index 00000000000..89d373e7251 --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/thigh_0.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb3edfe6a7b04f4f4dff1ce7b05d426ddce941e276cc6372fb542055893c71ec +size 2841868 diff --git a/dimos/robot/unitree/go2/assets/meshes/thigh_0.usdc b/dimos/robot/unitree/go2/assets/meshes/thigh_0.usdc new file mode 100644 index 00000000000..a1ea59bda2e --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/thigh_0.usdc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa500d407e3024ec41bd28d8c08415312842aac4f9a2cefc4a3388f38cfab678 +size 307550 diff --git a/dimos/robot/unitree/go2/assets/meshes/thigh_1.obj b/dimos/robot/unitree/go2/assets/meshes/thigh_1.obj new file mode 100644 index 00000000000..7a231d1de8a --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/thigh_1.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a631822f9f7d2114b1b79dcdff5abf95103630abb7d91d1f3b8b18117132ed2 +size 1491320 diff --git a/dimos/robot/unitree/go2/assets/meshes/thigh_1.usdc b/dimos/robot/unitree/go2/assets/meshes/thigh_1.usdc new file mode 100644 index 00000000000..edda64f1fe0 --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/thigh_1.usdc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9db5134864dc5a19686ed60eaa792a006b6f7dcf6843a05d8a487accf7ab1a0d +size 161766 diff --git a/dimos/robot/unitree/go2/assets/meshes/thigh_mirror_0.obj b/dimos/robot/unitree/go2/assets/meshes/thigh_mirror_0.obj new file mode 100644 index 00000000000..12995f29c75 --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/thigh_mirror_0.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07abaedce761f1dd48647384ac14ba1959276bc78d5a2a62051e8f1c6093933b +size 2817674 diff --git a/dimos/robot/unitree/go2/assets/meshes/thigh_mirror_0.usdc b/dimos/robot/unitree/go2/assets/meshes/thigh_mirror_0.usdc new file mode 100644 index 00000000000..6593b52c396 --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/thigh_mirror_0.usdc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e66c1dbc8c609b816a392653eb67e62efc9f75fb14992e6ac24726a4470ec05 +size 306238 diff --git a/dimos/robot/unitree/go2/assets/meshes/thigh_mirror_1.obj b/dimos/robot/unitree/go2/assets/meshes/thigh_mirror_1.obj new file mode 100644 index 00000000000..3417868ae49 --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/thigh_mirror_1.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db984a3e983eabbc9b38183427cca9ab0bb3bdb049f9eab030e06edda36579d6 +size 1482261 diff --git a/dimos/robot/unitree/go2/assets/meshes/thigh_mirror_1.usdc b/dimos/robot/unitree/go2/assets/meshes/thigh_mirror_1.usdc new file mode 100644 index 00000000000..13e2b02826d --- /dev/null +++ b/dimos/robot/unitree/go2/assets/meshes/thigh_mirror_1.usdc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e16aff230853e6c399516f7adaf1c1ab1523dbe1ac7c0b8483f246cdbd98d7a8 +size 161402 diff --git a/dimos/sim2/l1.py b/dimos/sim2/l1.py index 7fd2b0e50b8..97182077033 100644 --- a/dimos/sim2/l1.py +++ b/dimos/sim2/l1.py @@ -28,11 +28,13 @@ import numpy as np from numpy.typing import NDArray +from dimos.core.core import rpc from dimos.core.module import Module, ModuleConfig from dimos.core.shared_config import shared_config from dimos.core.stream import In, Out from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2 from dimos.sim2.module import Simulation, SimulationSpec +from dimos.sim2.sensor_usd import frame_usd from dimos.sim2.spec import SimulatedRobot, SimulatedSensor from dimos.sim2.view import WorldView, open_view, rotation_matrix, sensor_frame from dimos.sim2.world_observations import WorldMirror @@ -68,6 +70,15 @@ class SimL1(Module): def simulated(self) -> dict[str, Any]: return {"simulated": SimulatedSensor("lidar", self.frame_id)} + @rpc + def get_usd(self) -> list[str]: + """``[asset_root, usd]``: the frame this L1 scans from. + + A frame rather than a case: the Go2's utlidar sits under a cover that is part of the + robot's own shell, and nobody has measured the unit inside it. + """ + return frame_usd(self.frame_id, name="sim_l1") + async def main(self) -> AsyncIterator[None]: self._world = WorldMirror() self._world.subscribe(self.world_observations) From 31077d278fdada2506c479f3e8dc63260e2d877d Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 9 Nov 2026 11:18:11 -0800 Subject: [PATCH 11/19] the Go2 stands in a room, and its layer says what holds it up The whole shape, on the one blueprint that was simplest to turn over. Two managers, nested: the inner one answers for the robot -- the Go2's layer with each sensor grafted onto the frame it is bolted to -- and the outer one for the world, the room with the robot grafted onto a spot on its floor. What the outer one composes is the world, and it is what the physics runs and what every sensor renders from, so a scan comes back off the furniture that is drawn. Nobody describes themselves to anything. The blueprint says which robots are in the world, the robot's gains and the pose it holds are in its layer (baked from the policy blob by `scripts/usd_drives.py`, with a test that notices if the blob changes), and where it stands is the room's mount frame and nowhere else. `logistics.xml` becomes `assets/logistics.usda`, because a backend here is handed USD and nothing else. Its lighting is written in by hand: the converter drops lights outright, and a layer that says nothing about how it is lit gets whatever the viewer guesses. Two things found on the way. The vendored meshes were committed as pointer text with no LFS filter on the path, so a clone got pointers and the sim refused the robot's first body with "at least 4 vertices required"; `.gitattributes` now covers them. And the integration test -- which runs the whole thing, four sensors at three speeds, through a composed world -- needs gains the toy can take: its head's inertia about the neck is 1e-4, and a kd much past 0.01 is stiffer than a 5 ms step can integrate. --- .gitattributes | 5 + dimos/robot/all_blueprints.py | 1 - dimos/robot/unitree/go2/assets/go2.usda | 144 ++++++- .../blueprints/basic/unitree_go2_sim2_l1.py | 79 +++- dimos/robot/unitree/go2/sim2.py | 69 ++-- dimos/robot/unitree/go2/test_usd.py | 44 ++ dimos/robot/usd_manager.py | 44 +- dimos/robot/usd_manager_point_clearing.py | 11 +- dimos/sim2/assets/logistics.py | 28 ++ dimos/sim2/assets/logistics.usda | 388 ++++++++++++++++++ dimos/sim2/l1.py | 82 ++-- dimos/sim2/logistics.xml | 24 -- dimos/sim2/test_sim_time.py | 122 +++--- pyproject.toml | 6 + scripts/mjcf_to_usd.py | 314 ++++++++++++++ scripts/usd_drives.py | 63 +++ 16 files changed, 1211 insertions(+), 213 deletions(-) create mode 100644 dimos/robot/unitree/go2/test_usd.py create mode 100644 dimos/sim2/assets/logistics.py create mode 100644 dimos/sim2/assets/logistics.usda delete mode 100644 dimos/sim2/logistics.xml create mode 100755 scripts/mjcf_to_usd.py create mode 100644 scripts/usd_drives.py diff --git a/.gitattributes b/.gitattributes index a5a27eed6bf..4e651ca8b40 100644 --- a/.gitattributes +++ b/.gitattributes @@ -32,6 +32,11 @@ docs/assets/favicon.png -filter -diff -merge misc/DimSim/public/sims/*.json filter=lfs diff=lfs merge=lfs -text misc/DimSim/public/embodiment/*.glb filter=lfs diff=lfs merge=lfs -text misc/DimSim/scenes/**/*.glb filter=lfs diff=lfs merge=lfs -text +# A vendored robot's meshes. They were committed as pointer text with no filter on the path, so +# a clone got the pointers and nothing turned them back into meshes -- the sim then refused the +# robot's first body with "at least 4 vertices required". The layer beside them stays text and +# out of LFS on purpose (see pyproject): a scene travels as a string, so it has to be readable. +dimos/robot/*/*/assets/meshes/** filter=lfs diff=lfs merge=lfs -text binary misc/DimSim/scenes/**/*.gltf filter=lfs diff=lfs merge=lfs -text # Machine-generated, and reviewed as artifacts rather than as source: a lock file # is cargo's output, and motion's planner crate is what an autoresearch lab diff --git a/dimos/robot/all_blueprints.py b/dimos/robot/all_blueprints.py index 894187a3b0e..357a2585b15 100644 --- a/dimos/robot/all_blueprints.py +++ b/dimos/robot/all_blueprints.py @@ -372,7 +372,6 @@ "sim-l1": "dimos.sim2.l1.SimL1", "sim-m20-world": "dimos.simulation.m20_sim.world.SimM20World", "simple-phone-teleop": "dimos.teleop.phone.phone_extensions.SimplePhoneTeleop", - "simulated-world": "dimos.sim2.sim.SimulatedWorld", "spatial-memory": "dimos.perception.experimental.spatial_perception.SpatialMemory", "speak-skill": "dimos.agents.skills.speak_skill.SpeakSkill", "spot-high-level": "dimos.experimental.robot.bosdyn.spot.effectors.high_level.SpotHighLevel", diff --git a/dimos/robot/unitree/go2/assets/go2.usda b/dimos/robot/unitree/go2/assets/go2.usda index a798ca1f6de..dc083a7ceab 100644 --- a/dimos/robot/unitree/go2/assets/go2.usda +++ b/dimos/robot/unitree/go2/assets/go2.usda @@ -728,9 +728,12 @@ def Xform "go2" ( } def PhysicsRevoluteJoint "FL_calf_joint" ( - apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + apiSchemas = ["MjcJointAPI", "NewtonJointAPI", "PhysicsDriveAPI:angular"] ) { + float drive:angular:physics:damping = 0.5 + float drive:angular:physics:stiffness = 20 + float drive:angular:physics:targetPosition = -1.5 float newton:armature = 0.01 float newton:damping = 0.034906585 float newton:friction = 0.2 @@ -747,9 +750,12 @@ def Xform "go2" ( } def PhysicsRevoluteJoint "FL_thigh_joint" ( - apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + apiSchemas = ["MjcJointAPI", "NewtonJointAPI", "PhysicsDriveAPI:angular"] ) { + float drive:angular:physics:damping = 0.5 + float drive:angular:physics:stiffness = 20 + float drive:angular:physics:targetPosition = 0.8 float newton:armature = 0.01 float newton:damping = 0.034906585 float newton:friction = 0.2 @@ -766,9 +772,12 @@ def Xform "go2" ( } def PhysicsRevoluteJoint "FL_hip_joint" ( - apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + apiSchemas = ["MjcJointAPI", "NewtonJointAPI", "PhysicsDriveAPI:angular"] ) { + float drive:angular:physics:damping = 0.5 + float drive:angular:physics:stiffness = 20 + float drive:angular:physics:targetPosition = 0 float newton:armature = 0.01 float newton:damping = 0.034906585 float newton:friction = 0.2 @@ -1151,9 +1160,12 @@ def Xform "go2" ( } def PhysicsRevoluteJoint "FR_calf_joint" ( - apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + apiSchemas = ["MjcJointAPI", "NewtonJointAPI", "PhysicsDriveAPI:angular"] ) { + float drive:angular:physics:damping = 0.5 + float drive:angular:physics:stiffness = 20 + float drive:angular:physics:targetPosition = -1.5 float newton:armature = 0.01 float newton:damping = 0.034906585 float newton:friction = 0.2 @@ -1170,9 +1182,12 @@ def Xform "go2" ( } def PhysicsRevoluteJoint "FR_thigh_joint" ( - apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + apiSchemas = ["MjcJointAPI", "NewtonJointAPI", "PhysicsDriveAPI:angular"] ) { + float drive:angular:physics:damping = 0.5 + float drive:angular:physics:stiffness = 20 + float drive:angular:physics:targetPosition = 0.8 float newton:armature = 0.01 float newton:damping = 0.034906585 float newton:friction = 0.2 @@ -1189,9 +1204,12 @@ def Xform "go2" ( } def PhysicsRevoluteJoint "FR_hip_joint" ( - apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + apiSchemas = ["MjcJointAPI", "NewtonJointAPI", "PhysicsDriveAPI:angular"] ) { + float drive:angular:physics:damping = 0.5 + float drive:angular:physics:stiffness = 20 + float drive:angular:physics:targetPosition = 0 float newton:armature = 0.01 float newton:damping = 0.034906585 float newton:friction = 0.2 @@ -1574,9 +1592,12 @@ def Xform "go2" ( } def PhysicsRevoluteJoint "RL_calf_joint" ( - apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + apiSchemas = ["MjcJointAPI", "NewtonJointAPI", "PhysicsDriveAPI:angular"] ) { + float drive:angular:physics:damping = 0.5 + float drive:angular:physics:stiffness = 20 + float drive:angular:physics:targetPosition = -1.5 float newton:armature = 0.01 float newton:damping = 0.034906585 float newton:friction = 0.2 @@ -1593,9 +1614,12 @@ def Xform "go2" ( } def PhysicsRevoluteJoint "RL_thigh_joint" ( - apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + apiSchemas = ["MjcJointAPI", "NewtonJointAPI", "PhysicsDriveAPI:angular"] ) { + float drive:angular:physics:damping = 0.5 + float drive:angular:physics:stiffness = 20 + float drive:angular:physics:targetPosition = 0.8 float newton:armature = 0.01 float newton:damping = 0.034906585 float newton:friction = 0.2 @@ -1612,9 +1636,12 @@ def Xform "go2" ( } def PhysicsRevoluteJoint "RL_hip_joint" ( - apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + apiSchemas = ["MjcJointAPI", "NewtonJointAPI", "PhysicsDriveAPI:angular"] ) { + float drive:angular:physics:damping = 0.5 + float drive:angular:physics:stiffness = 20 + float drive:angular:physics:targetPosition = 0 float newton:armature = 0.01 float newton:damping = 0.034906585 float newton:friction = 0.2 @@ -1997,9 +2024,12 @@ def Xform "go2" ( } def PhysicsRevoluteJoint "RR_calf_joint" ( - apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + apiSchemas = ["MjcJointAPI", "NewtonJointAPI", "PhysicsDriveAPI:angular"] ) { + float drive:angular:physics:damping = 0.5 + float drive:angular:physics:stiffness = 20 + float drive:angular:physics:targetPosition = -1.5 float newton:armature = 0.01 float newton:damping = 0.034906585 float newton:friction = 0.2 @@ -2016,9 +2046,12 @@ def Xform "go2" ( } def PhysicsRevoluteJoint "RR_thigh_joint" ( - apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + apiSchemas = ["MjcJointAPI", "NewtonJointAPI", "PhysicsDriveAPI:angular"] ) { + float drive:angular:physics:damping = 0.5 + float drive:angular:physics:stiffness = 20 + float drive:angular:physics:targetPosition = 0.8 float newton:armature = 0.01 float newton:damping = 0.034906585 float newton:friction = 0.2 @@ -2035,9 +2068,12 @@ def Xform "go2" ( } def PhysicsRevoluteJoint "RR_hip_joint" ( - apiSchemas = ["MjcJointAPI", "NewtonJointAPI"] + apiSchemas = ["MjcJointAPI", "NewtonJointAPI", "PhysicsDriveAPI:angular"] ) { + float drive:angular:physics:damping = 0.5 + float drive:angular:physics:stiffness = 20 + float drive:angular:physics:targetPosition = 0 float newton:armature = 0.01 float newton:damping = 0.034906585 float newton:friction = 0.2 @@ -2322,85 +2358,169 @@ def Xform "go2" ( def MjcActuator "FL_hip" { + uniform double[] mjc:biasPrm = [0, -20, -0.5, 0, 0, 0, 0, 0, 0, 0] + uniform token mjc:biasType = "affine" + uniform token mjc:ctrlLimited = "false" uniform double mjc:ctrlRange:max = 23.7 uniform double mjc:ctrlRange:min = -23.7 + uniform token mjc:forceLimited = "true" + uniform double mjc:forceRange:max = 23.7 + uniform double mjc:forceRange:min = -23.7 + uniform double[] mjc:gainPrm = [20, 0, 0, 0, 0, 0, 0, 0, 0, 0] rel mjc:target = } def MjcActuator "FL_thigh" { + uniform double[] mjc:biasPrm = [0, -20, -0.5, 0, 0, 0, 0, 0, 0, 0] + uniform token mjc:biasType = "affine" + uniform token mjc:ctrlLimited = "false" uniform double mjc:ctrlRange:max = 23.7 uniform double mjc:ctrlRange:min = -23.7 + uniform token mjc:forceLimited = "true" + uniform double mjc:forceRange:max = 23.7 + uniform double mjc:forceRange:min = -23.7 + uniform double[] mjc:gainPrm = [20, 0, 0, 0, 0, 0, 0, 0, 0, 0] rel mjc:target = } def MjcActuator "FL_calf" { + uniform double[] mjc:biasPrm = [0, -20, -0.5, 0, 0, 0, 0, 0, 0, 0] + uniform token mjc:biasType = "affine" + uniform token mjc:ctrlLimited = "false" uniform double mjc:ctrlRange:max = 45.43 uniform double mjc:ctrlRange:min = -45.43 + uniform token mjc:forceLimited = "true" + uniform double mjc:forceRange:max = 45.43 + uniform double mjc:forceRange:min = -45.43 + uniform double[] mjc:gainPrm = [20, 0, 0, 0, 0, 0, 0, 0, 0, 0] rel mjc:target = } def MjcActuator "FR_hip" { + uniform double[] mjc:biasPrm = [0, -20, -0.5, 0, 0, 0, 0, 0, 0, 0] + uniform token mjc:biasType = "affine" + uniform token mjc:ctrlLimited = "false" uniform double mjc:ctrlRange:max = 23.7 uniform double mjc:ctrlRange:min = -23.7 + uniform token mjc:forceLimited = "true" + uniform double mjc:forceRange:max = 23.7 + uniform double mjc:forceRange:min = -23.7 + uniform double[] mjc:gainPrm = [20, 0, 0, 0, 0, 0, 0, 0, 0, 0] rel mjc:target = } def MjcActuator "FR_thigh" { + uniform double[] mjc:biasPrm = [0, -20, -0.5, 0, 0, 0, 0, 0, 0, 0] + uniform token mjc:biasType = "affine" + uniform token mjc:ctrlLimited = "false" uniform double mjc:ctrlRange:max = 23.7 uniform double mjc:ctrlRange:min = -23.7 + uniform token mjc:forceLimited = "true" + uniform double mjc:forceRange:max = 23.7 + uniform double mjc:forceRange:min = -23.7 + uniform double[] mjc:gainPrm = [20, 0, 0, 0, 0, 0, 0, 0, 0, 0] rel mjc:target = } def MjcActuator "FR_calf" { + uniform double[] mjc:biasPrm = [0, -20, -0.5, 0, 0, 0, 0, 0, 0, 0] + uniform token mjc:biasType = "affine" + uniform token mjc:ctrlLimited = "false" uniform double mjc:ctrlRange:max = 45.43 uniform double mjc:ctrlRange:min = -45.43 + uniform token mjc:forceLimited = "true" + uniform double mjc:forceRange:max = 45.43 + uniform double mjc:forceRange:min = -45.43 + uniform double[] mjc:gainPrm = [20, 0, 0, 0, 0, 0, 0, 0, 0, 0] rel mjc:target = } def MjcActuator "RL_hip" { + uniform double[] mjc:biasPrm = [0, -20, -0.5, 0, 0, 0, 0, 0, 0, 0] + uniform token mjc:biasType = "affine" + uniform token mjc:ctrlLimited = "false" uniform double mjc:ctrlRange:max = 23.7 uniform double mjc:ctrlRange:min = -23.7 + uniform token mjc:forceLimited = "true" + uniform double mjc:forceRange:max = 23.7 + uniform double mjc:forceRange:min = -23.7 + uniform double[] mjc:gainPrm = [20, 0, 0, 0, 0, 0, 0, 0, 0, 0] rel mjc:target = } def MjcActuator "RL_thigh" { + uniform double[] mjc:biasPrm = [0, -20, -0.5, 0, 0, 0, 0, 0, 0, 0] + uniform token mjc:biasType = "affine" + uniform token mjc:ctrlLimited = "false" uniform double mjc:ctrlRange:max = 23.7 uniform double mjc:ctrlRange:min = -23.7 + uniform token mjc:forceLimited = "true" + uniform double mjc:forceRange:max = 23.7 + uniform double mjc:forceRange:min = -23.7 + uniform double[] mjc:gainPrm = [20, 0, 0, 0, 0, 0, 0, 0, 0, 0] rel mjc:target = } def MjcActuator "RL_calf" { + uniform double[] mjc:biasPrm = [0, -20, -0.5, 0, 0, 0, 0, 0, 0, 0] + uniform token mjc:biasType = "affine" + uniform token mjc:ctrlLimited = "false" uniform double mjc:ctrlRange:max = 45.43 uniform double mjc:ctrlRange:min = -45.43 + uniform token mjc:forceLimited = "true" + uniform double mjc:forceRange:max = 45.43 + uniform double mjc:forceRange:min = -45.43 + uniform double[] mjc:gainPrm = [20, 0, 0, 0, 0, 0, 0, 0, 0, 0] rel mjc:target = } def MjcActuator "RR_hip" { + uniform double[] mjc:biasPrm = [0, -20, -0.5, 0, 0, 0, 0, 0, 0, 0] + uniform token mjc:biasType = "affine" + uniform token mjc:ctrlLimited = "false" uniform double mjc:ctrlRange:max = 23.7 uniform double mjc:ctrlRange:min = -23.7 + uniform token mjc:forceLimited = "true" + uniform double mjc:forceRange:max = 23.7 + uniform double mjc:forceRange:min = -23.7 + uniform double[] mjc:gainPrm = [20, 0, 0, 0, 0, 0, 0, 0, 0, 0] rel mjc:target = } def MjcActuator "RR_thigh" { + uniform double[] mjc:biasPrm = [0, -20, -0.5, 0, 0, 0, 0, 0, 0, 0] + uniform token mjc:biasType = "affine" + uniform token mjc:ctrlLimited = "false" uniform double mjc:ctrlRange:max = 23.7 uniform double mjc:ctrlRange:min = -23.7 + uniform token mjc:forceLimited = "true" + uniform double mjc:forceRange:max = 23.7 + uniform double mjc:forceRange:min = -23.7 + uniform double[] mjc:gainPrm = [20, 0, 0, 0, 0, 0, 0, 0, 0, 0] rel mjc:target = } def MjcActuator "RR_calf" { + uniform double[] mjc:biasPrm = [0, -20, -0.5, 0, 0, 0, 0, 0, 0, 0] + uniform token mjc:biasType = "affine" + uniform token mjc:ctrlLimited = "false" uniform double mjc:ctrlRange:max = 45.43 uniform double mjc:ctrlRange:min = -45.43 + uniform token mjc:forceLimited = "true" + uniform double mjc:forceRange:max = 45.43 + uniform double mjc:forceRange:min = -45.43 + uniform double[] mjc:gainPrm = [20, 0, 0, 0, 0, 0, 0, 0, 0, 0] rel mjc:target = } } diff --git a/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py b/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py index e54eec2df4f..c42bc95635b 100644 --- a/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py +++ b/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py @@ -12,23 +12,86 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""unitree-go2-basic in sim2: the walking Go2, its head L1 and front camera, its URDF posed for viewers.""" +"""unitree-go2-basic in sim2: the walking Go2 in the logistics room, its head L1 and front camera. + +Two managers, nested, which is the shape every one of these has. The inner one answers for the +*robot*: the Go2's own layer with each sensor grafted onto the frame it is bolted to. The outer +one answers for the *world*: the room, with the robot grafted onto a spot on its floor. What +that outer one composes is the world -- `get_scene` hands out one layer holding the room, the +robot and the sensors -- and it is what the physics runs and what every sensor renders from, so +a scan lands on the furniture that is drawn. + +Nobody describes themselves to the simulation. The blueprint says which robots are in the world, +because the blueprint is where that is already written down, and the robot's gains and the pose +it holds are in its layer. +""" from dimos.core.coordination.blueprints import autoconnect from dimos.core.global_config import global_config +from dimos.robot.assets.model import UsdModel from dimos.robot.unitree.go2.blueprints.basic.unitree_go2_basic import rerun_config from dimos.robot.unitree.go2.sim2 import Go2Sim from dimos.robot.usd_manager import UsdManager +from dimos.sim2.assets.logistics import LOGISTICS from dimos.sim2.camera import SimCamera from dimos.sim2.l1 import SimL1 -from dimos.sim2.module import Simulation +from dimos.sim2.sim import sim_module +from dimos.sim2.spec import SimulatedRobot +from dimos.simulation.go2_legged.robot import CONTROL_DT from dimos.visualization.vis_module import vis_module +# What the world calls this robot: the entity id its actions are addressed to, and the prefix on +# every frame of it. +ROBOT = "go2" +# Where it stands, said once. The scene's mount frame is the one statement of it: the simulation +# gets this world from the manager and finds the robot already in it, so a second xyz on the +# description could only disagree with the room. +SPOT = (0.0, 0.0, 0.35) +# The namespace the scene's manager lives in, and so the name the things that ask it for the +# scene use. A manager outside every namespace would be a second one answering on the same +# unnamespaced topics as the robot's. +SCENE = "scene" +SCENE_MANAGER = f"{SCENE}/{UsdManager.name}" + +# The room, with a spot on its floor for the robot. A mounts dict is keyed by frame, so a frame +# is what a visitor hangs off; the room arrives with frames for its own furniture and none for +# anything standing in it. +_room = UsdModel.from_file(LOGISTICS).with_frames({ROBOT: ("logistics", SPOT, (0.0, 0.0, 0.0))}) + +# The sensors as their own blueprints, because the manager mounts them by the instance each one +# deploys: a frame_id alone would name a module the manager cannot address. Each asks the scene's +# manager for the world it is in and builds its own copy of it. +_lidar = SimL1.blueprint(frame_id=Go2Sim.l1_mount_frame, scene_manager=SCENE_MANAGER) +_camera = SimCamera.blueprint(frame_id=Go2Sim.camera_mount_frame, scene_manager=SCENE_MANAGER) + +# One manager holding the whole robot: the Go2's own layer, with each sensor grafted onto the +# frame it is bolted to. Without the mounts it answers for the robot alone, so the lidar and the +# camera are nowhere in the stage a viewer draws. +_robot = UsdManager.blueprint( + model=Go2Sim.usd, mounts={Go2Sim.l1_mount_frame: _lidar, Go2Sim.camera_mount_frame: _camera} +).remappings([(UsdManager, "coordinator_joint_state", "joint_state")]) + +_scene = UsdManager.blueprint( + model=_room, + mounts={ROBOT: _robot}, + # It is a scene, not a robot: its frames are the world's and take no prefix. + frame_id_prefix="", + # The room's root link is "logistics" and tf has never heard of it: the robot publishes + # world -> go2/base, so "world" is what everything else is placed against. + tf_renames={"logistics": "world"}, +).namespace(SCENE, expose={"render_file_request", "file_info", "tf"}) + unitree_go2_sim2_l1 = autoconnect( - Simulation.blueprint(), + sim_module( + scene_manager=SCENE_MANAGER, + # The one thing a layer cannot say: what the world calls this robot, and how often its + # controller acts. Its gains and the pose it holds are in the layer. + robots=[SimulatedRobot(ROBOT, Go2Sim.usd, control_period=CONTROL_DT)], + ), vis_module(viewer_backend=global_config.viewer, rerun_config=rerun_config), - Go2Sim.blueprint(), - UsdManager.blueprint().remappings([(UsdManager, "coordinator_joint_state", "joint_state")]), - SimL1.blueprint(frame_id=Go2Sim.l1_mount_frame), - SimCamera.blueprint(frame_id=Go2Sim.camera_mount_frame), -).global_config(n_workers=6, robot_model="unitree_go2") + Go2Sim.blueprint(robot_id=ROBOT), + _scene, + _robot, + _lidar, + _camera, +).global_config(n_workers=8, robot_model="unitree_go2") diff --git a/dimos/robot/unitree/go2/sim2.py b/dimos/robot/unitree/go2/sim2.py index 544fa7fc225..5a9915d4c6d 100644 --- a/dimos/robot/unitree/go2/sim2.py +++ b/dimos/robot/unitree/go2/sim2.py @@ -14,8 +14,11 @@ """A simulated Go2 with GO2Connection's cmd_vel/odom/tf: the walking policy, observing world_observations and sending joint commands. -Go2Sim.mjcf is the Go2's MJCF with the rig's sensor frames as sites: a sensor names one (Go2Sim.lidar_mount_frame, ...) -as its frame_id, and Go2Sim hands a UsdManager the URDF of it, which poses the same frames on tf. +Go2Sim.usd is the committed Go2 layer with the rig's sensor frames added: a sensor names one +(Go2Sim.lidar_mount_frame, ...) as its frame_id, and the blueprint hands the same layer to a +UsdManager, which poses those frames on tf, and to `sim_module`, which runs them. The layer +carries the policy's gains and its home pose as drives (see `go2_drives`), so what holds this +robot up is stated once, where the robot is described. """ import asyncio @@ -29,14 +32,13 @@ import numpy as np from dimos.core.module import Module, ModuleConfig -from dimos.core.shared_config import shared_config from dimos.core.stream import In, Out from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped from dimos.msgs.geometry_msgs.Transform import Transform from dimos.msgs.geometry_msgs.Twist import Twist from dimos.msgs.sensor_msgs.JointState import JointState from dimos.msgs.tf2_msgs.TFMessage import TFMessage -from dimos.robot.assets.model import RobotModel +from dimos.robot.assets.model import UsdModel from dimos.robot.unitree.go2.constants import CMD_VEL_TIMEOUT from dimos.robot.unitree.go2.go2_mid360_static_transforms import ( CAMERA_XYZ, @@ -46,17 +48,29 @@ MID360_XYZ, OPTICAL_RPY, ) -from dimos.robot.usd_manager import UsdManager from dimos.sim2.actions import JointCommand, world_action -from dimos.sim2.module import Simulation -from dimos.sim2.spec import Servo, SimulatedRobot -from dimos.sim2.utils.mjcf_frames import mjcf_with_frames -from dimos.sim2.utils.mjcf_to_urdf import mjcf_to_urdf -from dimos.sim2.view import rotation_matrix +from dimos.sim2.mujoco.sensor_world import rotation_matrix from dimos.sim2.world_observations import ACT, WorldMirror from dimos.simulation.go2_legged.policy import Proprioception, load_policy -from dimos.simulation.go2_legged.robot import COMMAND_SLEW, CONTROL_DT, projected_gravity, yaw_of -from dimos.utils.data import get_data +from dimos.simulation.go2_legged.robot import COMMAND_SLEW, projected_gravity, yaw_of + + +def go2_drives() -> dict[str, tuple[float, float, float]]: + """The gains and home pose the walking policy runs on, as `usd_with_drives` wants them. + + Read out of the policy blob, which carries its own kp, kd and default pose: the gains that + hold this robot up are the controller's, not the vendor's. Called by + `scripts/usd_drives.py` to write them into the committed layer, and by the test that keeps + the layer honest if the blob ever changes -- never at runtime, because by then the layer is + what says it. + """ + policy = load_policy(None) + return { + joint: (float(kp), float(kd), float(q)) + for joint, kp, kd, q in zip( + policy.joint_names, policy.kp, policy.kd, policy.default_pose, strict=True + ) + } class Go2SimConfig(ModuleConfig): @@ -69,9 +83,10 @@ class Go2SimConfig(ModuleConfig): class Go2Sim(Module): - # The real rig's frames (go2_mid360_static_transforms), on the MJCF's base body. - mjcf = mjcf_with_frames( - get_data("go2_sim") / "unitree_go2" / "go2.xml", + # The committed layer, converted once from the vendor MJCF (see assets/go2.usda), carrying + # the real rig's frames (go2_mid360_static_transforms) on its base. The mounts are added here + # rather than baked in because a mount is whose robot carries it, not what the robot is. + usd = UsdModel.from_file(Path(__file__).parent / "assets" / "go2.usda").with_frames( { "utlidar_lidar": ( "base", @@ -81,7 +96,7 @@ class Go2Sim(Module): "front_camera": ("base", CAMERA_XYZ, (0.0, 0.0, 0.0)), "mid360_link": ("front_camera", MID360_XYZ, (0.0, MID360_PITCH_DOWN, 0.0)), "camera_optical": ("front_camera", (0.0, 0.0, 0.0), OPTICAL_RPY), - }, + } ) l1_mount_frame = "utlidar_lidar" lidar_mount_frame = "mid360_link" @@ -104,28 +119,6 @@ def __init__(self, **kwargs: Any) -> None: self._command = np.zeros(3) self._heading: float | None = None - @shared_config(Simulation) - def simulated(self) -> dict[str, Any]: - config, policy = self.config, self._policy - servos = zip(policy.joint_names, policy.kp, policy.kd, policy.default_pose, strict=True) - return { - "simulated": SimulatedRobot( - config.robot_id, - self.mjcf, - "base", - xyz=config.xyz, - control_period=CONTROL_DT, - servos=tuple( - Servo(joint, float(kp), float(kd), float(q)) for joint, kp, kd, q in servos - ), - ) - } - - @shared_config(UsdManager) - def urdf(self) -> dict[str, RobotModel]: - # The MJCF's base body is the URDF's base_link, the frame odometry moves. - return {"model": mjcf_to_urdf(self.mjcf, self.config.robot_id, "base_link")} - async def main(self) -> AsyncIterator[None]: self._world = WorldMirror() loop = asyncio.get_running_loop() diff --git a/dimos/robot/unitree/go2/test_usd.py b/dimos/robot/unitree/go2/test_usd.py new file mode 100644 index 00000000000..6aa1ac5f926 --- /dev/null +++ b/dimos/robot/unitree/go2/test_usd.py @@ -0,0 +1,44 @@ +# Copyright 2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""The committed Go2 layer says what the Go2 is, so this is what holds it to that.""" + +import pytest + +pytest.importorskip("pxr") + + +def test_the_layer_carries_the_gains_the_policy_runs_on() -> None: + """The gains are in the layer, written there once by `scripts/usd_drives.py`. + + Which means they can go stale: the policy blob is where they came from, and nothing stops + somebody replacing it. This is what notices. Run the script again if it fails -- the layer is + the source of truth for whatever runs the robot, and it has to agree with the controller the + robot actually ships with. + """ + from dimos.robot.assets.usd import usd_drives + from dimos.robot.unitree.go2.sim2 import Go2Sim, go2_drives + + assert usd_drives(Go2Sim.usd.usd) == { + joint: pytest.approx(gains) for joint, gains in go2_drives().items() + } + + +def test_every_sensor_mount_the_rig_has_is_a_frame_of_the_layer() -> None: + """A sensor names one of these as its frame_id, and a manager grafts it onto that frame.""" + from dimos.robot.unitree.go2.sim2 import Go2Sim + + frames = Go2Sim.usd.frame_names() + + assert {Go2Sim.l1_mount_frame, Go2Sim.lidar_mount_frame, Go2Sim.camera_mount_frame} <= frames diff --git a/dimos/robot/usd_manager.py b/dimos/robot/usd_manager.py index 48ed6c88251..f8375406243 100644 --- a/dimos/robot/usd_manager.py +++ b/dimos/robot/usd_manager.py @@ -30,7 +30,6 @@ from dimos.core.core import rpc from dimos.core.module import Module, ModuleConfig from dimos.core.rpc_client import RPCClient -from dimos.core.shared_config import ConfigSenders, SharedConfigModule from dimos.core.stream import IO, In, Out from dimos.msgs.files.File import File from dimos.msgs.geometry_msgs.Transform import Transform @@ -248,7 +247,7 @@ def _normalize_mounts(cls, value: object) -> object: return {frame: Mount.of(v) for frame, v in value.items()} -class UsdManager(SharedConfigModule): +class UsdManager(Module): """Poses the URDF from the joint state onto tf, and serves the robot as USD plus the files it names.""" config: UsdManagerConfig @@ -271,7 +270,12 @@ def __init__(self, **kwargs: object) -> None: self._ready = asyncio.Event() super().__init__(**kwargs) self._lock = RLock() - self._load(self.config.model) + # Converted here and once: everything below works on the USD, whatever it came from. + self._description = _as_usd(self.config.model or _world_model()) + self._file_paths = self._description.referenced_files() + self._joints = {joint.name: joint for joint in self._description.joints} + # tf_renames reach this robot's own frames too, so every name it emits goes through here. + self._frames = self._renamed_own_frames() self._next_joint_tf_t = 0.0 self._warned_about_joint_names = False # Per edge: the last transform sent (translation + rotation) and when. @@ -353,40 +357,6 @@ def _check_mounts(self) -> None: f"subtree" ) - def config_receive(self, config_senders: ConfigSenders) -> None: - models = [(name, config.get("model")) for name, config in config_senders] - if self.config.model is not None: - models.insert(0, ("model=", self.config.model)) - if len(models) > 1: - raise ValueError( - f"UsdManager takes one model, from model= or one @shared_config(UsdManager); " - f"got {[n for n, _ in models]}" - ) - if not models: - return - name, model = models[0] - if not isinstance(model, RobotModel | UsdModel): - raise TypeError( - f"{name}'s @shared_config(UsdManager) must return " - f"{{'model': RobotModel}} or {{'model': UsdModel}}" - ) - if model is not self.config.model: - self._load(model) - - def _load(self, model: RobotModel | UsdModel | None) -> None: - """Take this model as the one this manager answers for. - - A model given as `model=` is loaded at construction; one that arrives over - @shared_config replaces it when it comes. None is a scene with no robot of its own -- - a single link called "world" for mounts to hang from. - """ - # Converted here and once: everything below works on the USD, whatever it came from. - self._description = _as_usd(model or _world_model()) - self._file_paths = self._description.referenced_files() - self._joints = {joint.name: joint for joint in self._description.joints} - # tf_renames reach this robot's own frames too, so every name it emits goes through here. - self._frames = self._renamed_own_frames() - @rpc def start(self) -> None: self.tfbuffer # noqa: B018 - Initialize TF before binding input handlers. diff --git a/dimos/robot/usd_manager_point_clearing.py b/dimos/robot/usd_manager_point_clearing.py index 205cfaf07e3..5e5b0744968 100644 --- a/dimos/robot/usd_manager_point_clearing.py +++ b/dimos/robot/usd_manager_point_clearing.py @@ -29,7 +29,7 @@ from dimos.msgs.geometry_msgs.Transform import Transform from dimos.msgs.sensor_msgs.JointState import JointState from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2 -from dimos.robot.assets.model import LoadedRobotModel, RobotModel +from dimos.robot.assets.model import LoadedRobotModel from dimos.robot.assets.usd_to_urdf import usd_to_urdf from dimos.robot.roboplan_body_filter import RoboPlanBodyFilter from dimos.robot.usd_manager import MULTI_DOF_JOINT_TYPES, UsdManager, UsdManagerConfig @@ -60,16 +60,13 @@ class UsdManagerPointClearing(UsdManager): def __init__(self, **kwargs: object) -> None: super().__init__(**kwargs) - self._states = TimestampedBufferCollection[JointState](self.config.state_history_s) - self._clouds_in = self._clouds_dropped = self._points_in = self._points_removed = 0 - self._next_stats_t = time.monotonic() + self.config.filter_stats_interval_s - - def _load(self, model: RobotModel) -> None: - super()._load(model) self._body_filter: RobotBodyFilter = RoboPlanBodyFilter( self._filter_description(), self.config.padding_m ) self._base_link = self._description.root_link + self._states = TimestampedBufferCollection[JointState](self.config.state_history_s) + self._clouds_in = self._clouds_dropped = self._points_in = self._points_removed = 0 + self._next_stats_t = time.monotonic() + self.config.filter_stats_interval_s def _filter_description(self) -> LoadedRobotModel: """The URDF RoboPlan builds its collision scene from. diff --git a/dimos/sim2/assets/logistics.py b/dimos/sim2/assets/logistics.py new file mode 100644 index 00000000000..7e42c843bca --- /dev/null +++ b/dimos/sim2/assets/logistics.py @@ -0,0 +1,28 @@ +# Copyright 2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""The room a sim2 blueprint runs in when nobody has a real scene to hand: walls, shelves, a table. + +USD, like every other world here, converted once from the MJCF it used to be written as and +committed beside this file. Its lighting is written in by hand, because the converter drops +lights outright ("lights are not supported") and a layer that says nothing about how it is lit +gets whatever the viewer guesses. + + MuJoCo: 1 plane, 8 boxes, no joints and nothing that moves + dimos: root link "logistics", which is the frame a visitor hangs off +""" + +from pathlib import Path + +LOGISTICS = Path(__file__).parent / "logistics.usda" diff --git a/dimos/sim2/assets/logistics.usda b/dimos/sim2/assets/logistics.usda new file mode 100644 index 00000000000..1290bdf8d16 --- /dev/null +++ b/dimos/sim2/assets/logistics.usda @@ -0,0 +1,388 @@ +#usda 1.0 +( + customLayerData = { + string creator = "MuJoCo USD Converter v0.9.0" + } + defaultPrim = "logistics" + doc = """Generated from Composed Stage of root layer /tmp/claude-1000/-home-dimos/ff9ad774-151d-43c4-9774-fa96d8ad90f5/scratchpad/logistics/converted/logistics.usda +""" + kilogramsPerUnit = 1 + metersPerUnit = 1 + upAxis = "Z" +) + +over "Flattened_Prototype_1" +{ + def Shader "PreviewSurface" + { + uniform token info:id = "UsdPreviewSurface" + color3f inputs:diffuseColor.connect = + float inputs:metallic.connect = + float inputs:opacity.connect = + float inputs:roughness.connect = + token outputs:surface + } + + def Shader "OpenPBR" + { + uniform token info:id = "ND_open_pbr_surface_surfaceshader" + color3f inputs:base_color.connect = + float inputs:base_metalness.connect = + float inputs:geometry_opacity.connect = + float inputs:specular_roughness.connect = + token outputs:out + } +} + +def Xform "logistics" ( + apiSchemas = ["GeomModelAPI"] + assetInfo = { + string name = "logistics" + } + kind = "component" +) +{ + float3[] extentsHint = [(-10, -10, -1.3113022e-8), (10, 10, 3)] + + def Scope "Geometry" + { + def Plane "floor" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + uniform token axis = "Z" + float3[] extent = [(-10, -10, -0), (10, 10, 0)] + double length = 20 + rel material:binding = + rel material:binding:physics = + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.5, 0.5)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + double width = 20 + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cube "north_wall" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + float3[] extent = [(-1, -1, -1), (1, 1, 1)] + rel material:binding:physics = + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.78, 0.8, 0.8)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + double size = 2 + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (6, 0.1, 1.5) + double3 xformOp:translate = (0, 5, 1.5) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cube "west_wall" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + float3[] extent = [(-1, -1, -1), (1, 1, 1)] + rel material:binding:physics = + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.78, 0.8, 0.8)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + double size = 2 + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (0.1, 5, 1.5) + double3 xformOp:translate = (-5, 0, 1.5) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cube "shelf_left" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + float3[] extent = [(-1, -1, -1), (1, 1, 1)] + rel material:binding:physics = + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.16, 0.36, 0.35)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + double size = 2 + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (0.45, 1.2, 0.8) + double3 xformOp:translate = (3, 2, 0.8) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cube "shelf_right" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + float3[] extent = [(-1, -1, -1), (1, 1, 1)] + rel material:binding:physics = + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.16, 0.36, 0.35)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + double size = 2 + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (0.45, 1.2, 0.8) + double3 xformOp:translate = (3, -2, 0.8) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cube "crate" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + float3[] extent = [(-1, -1, -1), (1, 1, 1)] + rel material:binding:physics = + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.74, 0.28, 0.18)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + double size = 2 + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (0.4, 0.4, 0.3) + double3 xformOp:translate = (-2, 2, 0.3) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cube "table" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + float3[] extent = [(-1, -1, -1), (1, 1, 1)] + rel material:binding:physics = + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.5, 0.52, 0.55)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + double size = 2 + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 0.5, 0.06) + double3 xformOp:translate = (0, -3, 0.7) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cube "Box" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + ) + { + float3[] extent = [(-1, -1, -1), (1, 1, 1)] + rel material:binding:physics = + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.22, 0.25, 0.25)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + double size = 2 + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (0.06, 0.4, 0.33) + double3 xformOp:translate = (-0.8, -3, 0.33) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + + def Cube "Box_1" ( + apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI", "MjcCollisionAPI", "PhysicsMassAPI", "MaterialBindingAPI"] + displayName = "Box" + uiHints = { + string displayName = "Box" + } + ) + { + float3[] extent = [(-1, -1, -1), (1, 1, 1)] + rel material:binding:physics = + uniform double[] mjc:solref = [0.02, 1] + float newton:contactGap = 0 + float physics:density = 1000 + color3f[] primvars:displayColor = [(0.22, 0.25, 0.25)] ( + interpolation = "constant" + ) + int[] primvars:displayColor:indices = None + float[] primvars:displayOpacity = [1] ( + interpolation = "constant" + ) + int[] primvars:displayOpacity:indices = None + double size = 2 + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (0.06, 0.4, 0.33) + double3 xformOp:translate = (0.8, -3, 0.33) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } + } + + def Scope "Materials" + { + def Material "floor" ( + apiSchemas = ["MaterialXConfigAPI"] + instanceable = true + add references = + ) + { + string config:mtlx:version = "1.39" + color3f inputs:color = (0.635, 0.67, 0.67) ( + limits = { + dictionary hard = { + float3 maximum = (1, 1, 1) + float3 minimum = (0, 0, 0) + } + } + sdrMetadata = { + string uimax = "1, 1, 1" + string uimin = "0, 0, 0" + } + ) + float inputs:metallic = 0 ( + limits = { + dictionary hard = { + float maximum = 1 + float minimum = 0 + } + } + sdrMetadata = { + string uimax = "1" + string uimin = "0" + } + ) + float inputs:opacity = 1 ( + limits = { + dictionary hard = { + float maximum = 1 + float minimum = 0 + } + } + sdrMetadata = { + string uimax = "1" + string uimin = "0" + } + ) + float inputs:roughness = 0.5 ( + limits = { + dictionary hard = { + float maximum = 1 + float minimum = 0 + } + } + sdrMetadata = { + string uimax = "1" + string uimin = "0" + } + ) + token outputs:mtlx:surface.connect = + token outputs:surface.connect = + } + } + + def Scope "Physics" + { + def Material "PhysicsMaterial" ( + apiSchemas = ["PhysicsMaterialAPI", "NewtonMaterialAPI"] + ) + { + float newton:contactDamping = 100 + float newton:contactStiffness = 2500 + float newton:rollingFriction = 0.0001 + float newton:torsionalFriction = 0.005 + float physics:dynamicFriction = 1 + float physics:staticFriction = 1 + } + } +} + +def PhysicsScene "PhysicsScene" ( + apiSchemas = ["NewtonSceneAPI", "MjcSceneAPI"] +) +{ + uniform token mjc:option:cone = "elliptic" + uniform double mjc:option:impratio = 10 + uniform int newton:maxSolverIterations = 100 + uniform int newton:timeStepsPerSecond = 200 + vector3f physics:gravityDirection = (0, 0, -1) + float physics:gravityMagnitude = 9.81 +} + +# The MJCF said `` and a headlight ambient of 0.35; +# the converter drops lights entirely ("lights are not supported"), so they are written here. A +# layer that says nothing about its lighting gets whatever the viewer guesses, which for the +# office scene was a room lit from nowhere with flat black through the windows. +def DistantLight "sun" ( + prepend apiSchemas = ["ShadowAPI"] +) +{ + # Straight down, as the MJCF's dir="0 0 -1" asked for. + color3f inputs:color = (1, 1, 1) + float inputs:intensity = 2.0 + bool inputs:shadow:enable = 1 + quatf xformOp:orient = (0.70710678, -0.70710678, 0, 0) + uniform token[] xformOpOrder = ["xformOp:orient"] +} + +def DomeLight "ambient" ( + prepend apiSchemas = ["ShadowAPI"] +) +{ + # The headlight's ambient term: there is no bounce in the renderer, so this stands in for + # what would reach a surface after scattering off the walls. + color3f inputs:color = (1, 1, 1) + float inputs:intensity = 1.2 + bool inputs:shadow:enable = 0 +} diff --git a/dimos/sim2/l1.py b/dimos/sim2/l1.py index 97182077033..20c44e3f4f6 100644 --- a/dimos/sim2/l1.py +++ b/dimos/sim2/l1.py @@ -22,21 +22,20 @@ import asyncio from collections.abc import AsyncIterator import math -from typing import Any from dimos_lcm.world_msgs import WorldObservations import numpy as np from numpy.typing import NDArray from dimos.core.core import rpc -from dimos.core.module import Module, ModuleConfig -from dimos.core.shared_config import shared_config +from dimos.core.module import Module from dimos.core.stream import In, Out from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2 -from dimos.sim2.module import Simulation, SimulationSpec +from dimos.sim2.mujoco.sensor_world import WorldView, open_view, robot_ids, rotation_matrix +from dimos.sim2.mujoco.world import SensorMount from dimos.sim2.sensor_usd import frame_usd -from dimos.sim2.spec import SimulatedRobot, SimulatedSensor -from dimos.sim2.view import WorldView, open_view, rotation_matrix, sensor_frame +from dimos.sim2.sim import SceneConfig, scene_from +from dimos.sim2.usd_layer import usd_layer from dimos.sim2.world_observations import WorldMirror from dimos.simulation.sensors.lidar import Raycaster @@ -45,9 +44,9 @@ _KEY_OFFSET = 1 << (_KEY_BITS - 1) -class SimL1Config(ModuleConfig): - # The L1's frame, as the robot's URDF names it: where it sits. - frame_id: str | None = "utlidar_lidar" +class SimL1Config(SceneConfig): + # The L1's frame, as the robot's description names it: where it sits. + frame_id: str = "utlidar_lidar" # The rolling map is published in this frame. map_frame_id: str = "world" period: float = 0.13 @@ -64,11 +63,6 @@ class SimL1(Module): config: SimL1Config lidar: Out[PointCloud2] world_observations: In[WorldObservations] - _simulation: SimulationSpec - - @shared_config(Simulation) - def simulated(self) -> dict[str, Any]: - return {"simulated": SimulatedSensor("lidar", self.frame_id)} @rpc def get_usd(self) -> list[str]: @@ -82,37 +76,52 @@ def get_usd(self) -> list[str]: async def main(self) -> AsyncIterator[None]: self._world = WorldMirror() self._world.subscribe(self.world_observations) - description = self._simulation.describe() - site = sensor_frame(description, self.frame_id) - # The map is centred on the robot the L1 is on. - robot_id = site.split("/")[0] - root = next( - d.root_body - for d in description["simulated"] - if isinstance(d, SimulatedRobot) and d.robot_id == robot_id - ) - view = open_view(description) - self._mapping = asyncio.create_task( - self._map(view, view.raycaster(self.config.geom_groups), site, f"{robot_id}/{root}") - ) + self._mapping = asyncio.create_task(self._map()) yield + self._mapping.cancel() - async def _map( - self, view: WorldView, raycaster: Raycaster, site: str, center_body: str - ) -> None: - config = self.config + def _open(self) -> WorldView: + """This L1's own world: the composed scene, with a site at its own frame. + + Opened on the first frame rather than at start, because the robots in the world are what + the world says they are -- the observations announce them -- and the view has to name its + bodies the way the observations do. + """ + scene = scene_from(self.config.scene_manager, self.rpc, self.config.scene) + if scene is None: + raise ValueError( + f"{self.frame_id}: nothing says what the scene is, so there is nothing to map." + f" Give this L1 the scene_manager that composes the world." + ) + return open_view( + usd_layer(scene), + robot_ids(self._world), + SensorMount("lidar", self.frame_id, self.frame_id), + ) + + async def _map(self) -> None: + config, site = self.config, self.frame_id + view: WorldView | None = None + raycaster: Raycaster | None = None + rider: str | None = None rng = np.random.default_rng(config.seed) rays = int(config.points_per_second * config.period) cells = np.zeros(0, dtype=np.int64) # Every period of sim time, so the map keeps its rate per sim second at any speed. async for _ in self._world.ticks(1 / config.period): + if view is None: + view = self._open() + raycaster = view.raycaster(config.geom_groups) + # The map is centred on the robot the L1 rides, which its own frame names. + rider = view.owner(site) + assert raycaster is not None view.update(self._world) origin, orientation = view.pose(site) directions = _hemisphere(rng, rays) @ rotation_matrix(orientation).T distance, _ = raycaster.cast(origin, directions, config.max_range) hit = distance > 0 cells = np.union1d(cells, self._keys(origin + directions[hit] * distance[hit, None])) - cells = self._crop(cells, view.pose(center_body)[0]) + cells = self._crop(cells, self._center(rider, origin)) self.lidar.publish( PointCloud2.from_numpy( (self._indices(cells) + 0.5) * config.cell, @@ -121,6 +130,15 @@ async def _map( ) ) + def _center(self, rider: str | None, origin: NDArray[np.float64]) -> NDArray[np.float64]: + """Where the square is cropped around: the robot carrying this L1, or the L1 itself. + + Read off the observations rather than out of the view, because that is where the robot's + pose comes from in the first place and the view is only a copy of it. + """ + entity = self._world.entity(rider) if rider is not None else None + return origin if entity is None or entity.pose is None else entity.pose[:3] + def _keys(self, points: NDArray[np.float64]) -> NDArray[np.int64]: index = np.floor(points / self.config.cell).astype(np.int64) + _KEY_OFFSET return np.unique((index[:, 0] << 2 * _KEY_BITS) | (index[:, 1] << _KEY_BITS) | index[:, 2]) diff --git a/dimos/sim2/logistics.xml b/dimos/sim2/logistics.xml deleted file mode 100644 index b02f6875467..00000000000 --- a/dimos/sim2/logistics.xml +++ /dev/null @@ -1,24 +0,0 @@ - - diff --git a/dimos/sim2/test_sim_time.py b/dimos/sim2/test_sim_time.py index 9629195d0f2..2df56e08521 100644 --- a/dimos/sim2/test_sim_time.py +++ b/dimos/sim2/test_sim_time.py @@ -16,7 +16,6 @@ import asyncio from collections.abc import AsyncIterator -from pathlib import Path import time from typing import Any import uuid @@ -30,19 +29,22 @@ from dimos.core.coordination.module_coordinator import ModuleCoordinator from dimos.core.core import rpc from dimos.core.module import Module, ModuleConfig -from dimos.core.shared_config import shared_config from dimos.core.stream import In, Out from dimos.msgs.sensor_msgs.Image import Image from dimos.msgs.sensor_msgs.Imu import Imu from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2 +from dimos.robot.assets.model import UsdModel +from dimos.robot.assets.usd import graft_usd, prefix_usd_frames, usd_with_drives +from dimos.robot.usd_manager import UsdManager from dimos.sim2.actions import JointCommand, world_action +from dimos.sim2.assets.logistics import LOGISTICS +from dimos.sim2.assets.toy import toy_robot from dimos.sim2.camera import SimCamera from dimos.sim2.imu import SimImu from dimos.sim2.l1 import SimL1 from dimos.sim2.livox import LivoxSim -from dimos.sim2.module import Simulation +from dimos.sim2.sim import sim_module from dimos.sim2.spec import SimulatedRobot -from dimos.sim2.utils.mjcf_frames import mjcf_with_frames from dimos.sim2.world_observations import ACT, WorldMirror L1_PERIOD = 0.13 @@ -55,40 +57,42 @@ } +# Where the scene's manager lives, and so the name everything that needs the world asks. +SCENE_MANAGER = f"scene/{UsdManager.name}" +# Every sensor sits at a frame of the dog's model. +FRAMES = {"eye": "head", "imu_link": "base", "livox": "head", "l1": "base"} + + @pytest.fixture -def scene(tmp_path: Path) -> Path: - path = tmp_path / "scene.xml" - path.write_text(""" - - - - - - -""") - return path +def robot_model() -> UsdModel: + """The dog: a box on a free joint with a hinged head, a driven neck, and a frame per sensor.""" + model = toy_robot().with_frames( + {frame: (body, (0.0, 0.0, 0.0), (0.0, 0.0, 0.0)) for frame, body in FRAMES.items()} + ) + # Its gains, where a robot's gains live: in its layer. Small ones, because the head is a + # 0.1 kg sphere at the joint and its inertia about the neck is 1e-4: kd beyond about 0.01 + # is stiffer than a 5 ms step can integrate, and the whole robot leaves the room. + return UsdModel.from_layer(usd_with_drives(model.usd, {"neck": (0.01, 0.001, 0.0)})) @pytest.fixture -def robot_model(tmp_path: Path) -> Path: - path = tmp_path / "robot.xml" - path.write_text(""" - - - - - - - - - - -""") - return path +def composed(robot_model: UsdModel) -> UsdModel: + """The world as a manager composes it: the room, with the dog grafted onto a spot on its floor. + + What the simulation runs and what every sensor renders from, which is the point -- one layer, + one world, and the furniture a scan comes back off is the furniture that is drawn. + """ + # The toy's layer puts its base at (9, 9, 9), so the spot it stands on takes that back out: + # a mount frame places a robot by its own layer's origin, not by where its root happens to be. + room = UsdModel.from_file(LOGISTICS).with_frames( + {"dog": ("logistics", (-9.0, -9.0, -8.7), (0.0, 0.0, 0.0))} + ) + return UsdModel.from_layer( + graft_usd(room.usd, {"dog": prefix_usd_frames(robot_model.usd, "dog")}) + ) class _DogConfig(ModuleConfig): - model: Path # With a control period, the world waits this long (wall seconds) for each of its commands. control_period: float | None = None think: float = 0.0 @@ -101,12 +105,6 @@ class _Dog(Module): world_observations: In[WorldObservations] actions: Out[WorldAction] - @shared_config(Simulation) - def simulated(self) -> dict[str, Any]: - config = self.config - robot = SimulatedRobot("dog", config.model, "base", control_period=config.control_period) - return {"simulated": robot} - async def main(self) -> AsyncIterator[None]: world, loop = WorldMirror(), asyncio.get_running_loop() @@ -154,27 +152,43 @@ def readings(self) -> dict[str, Any]: def _run( - scene: Path, robot_model: Path, seconds: float, speed: float = 1.0, **dog: Any + composed: UsdModel, + robot_model: UsdModel, + seconds: float, + speed: float = 1.0, + **dog: Any, ) -> dict[str, Any]: - # Every sensor at a frame of the dog's model. - frames = {"eye": "head", "imu_link": "base", "livox": "head", "l1": "base"} - model = mjcf_with_frames( - robot_model, - {frame: (body, (0.0, 0.0, 0.0), (0.0, 0.0, 0.0)) for frame, body in frames.items()}, - out_dir=robot_model.parent, - ) blueprint = autoconnect( - Simulation.blueprint(scene=scene, speed=speed), - _Dog.blueprint(model=model, **dog), - SimCamera.blueprint(frame_id="eye", width=64, height=48, rate_hz=RATES["color_image"]), - SimImu.blueprint(frame_id="imu_link", rate_hz=RATES["imu"]), + sim_module( + scene_manager=SCENE_MANAGER, + speed=speed, + robots=[SimulatedRobot("dog", robot_model, control_period=dog.get("control_period"))], + ), + _Dog.blueprint(**dog), + # The one manager, answering for the whole world. Everything else asks it. + UsdManager.blueprint( + model=composed, frame_id_prefix="", tf_renames={"logistics": "world"} + ).namespace("scene", expose={"render_file_request", "file_info", "tf"}), + SimCamera.blueprint( + frame_id="eye", + width=64, + height=48, + rate_hz=RATES["color_image"], + scene_manager=SCENE_MANAGER, + ), + SimImu.blueprint(frame_id="imu_link", rate_hz=RATES["imu"], scene_manager=SCENE_MANAGER), LivoxSim.blueprint( - frame_id="livox", rate_hz=RATES["lidar"], imu_rate_hz=RATES["livox_imu"] + frame_id="livox", + rate_hz=RATES["lidar"], + imu_rate_hz=RATES["livox_imu"], + scene_manager=SCENE_MANAGER, ).remappings([(LivoxSim, "imu", "livox_imu")]), - SimL1.blueprint(frame_id="l1", period=L1_PERIOD).remappings([(SimL1, "lidar", "l1_lidar")]), + SimL1.blueprint(frame_id="l1", period=L1_PERIOD, scene_manager=SCENE_MANAGER).remappings( + [(SimL1, "lidar", "l1_lidar")] + ), _Readings.blueprint(), # A worker each, as a robot's blueprint gives its sensors: one sharing a busy loop falls behind at 4x. - ).global_config(n_workers=8) + ).global_config(n_workers=9) # Its own zenoh domain: another world running on this machine would otherwise share its topics. overrides = {"g": {"viewer": "none", "zenoh_domain": f"sim2-test-{uuid.uuid4().hex[:8]}"}} parsed = BlueprintConfigParser(blueprint).parse(environ={}, overrides=overrides) @@ -227,8 +241,8 @@ def _per_sim_second(stamps: list[float], start: float) -> float: ], ids=["half_speed", "four_times_speed", "slower_than_real_lockstep"], ) -def test_sensors_keep_their_rates_per_sim_second(scene, robot_model, speed, dog): - readings = _run(scene, robot_model, seconds=8.0, speed=speed, **dog) +def test_sensors_keep_their_rates_per_sim_second(composed, robot_model, speed, dog): + readings = _run(composed, robot_model, seconds=8.0, speed=speed, **dog) stamps = readings["stamps"] counts = {name: len(stamps[name]) for name in RATES} assert all(count > 5 for count in counts.values()), counts diff --git a/pyproject.toml b/pyproject.toml index d2afc2b7b3a..8271c9efe49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -870,6 +870,12 @@ ignore = [ "dimos/manipulation/manipulation_module.py", "dimos/manipulation/test_roboplan.py", "misc/DimSim/scenes/apartment/objects/manifest.json", + # A USD layer is the thing a robot or a scene travels as, and it is text on purpose: it stays + # out of LFS so a change to a scene shows up in a diff rather than as an opaque pointer. A + # clone that has not run `git lfs pull` still has to be able to read one, because a scene is + # handed over as a string and there is nothing to fetch it from. + "dimos/robot/*/*/assets/*.usda", + "dimos/simulation/scenes/*/*.usda", "misc/DimSim/src/engine.js", # Generated golden vectors (web/shared/fixtures/gen.ts); grows per panel kind. "web/shared/fixtures/manifests.json", diff --git a/scripts/mjcf_to_usd.py b/scripts/mjcf_to_usd.py new file mode 100755 index 00000000000..6d6e696457d --- /dev/null +++ b/scripts/mjcf_to_usd.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 +# Copyright 2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A vendor MJCF as the USD dimos serves. Run once by hand; the result is committed. + +Why two steps. Google DeepMind's converter (``mujoco-usd`` on PyPI) is the only thing that reads +an MJCF into USD with its physics intact -- it writes MjcActuator, MjcJointAPI and MjcCollisionAPI +prims, so MuJoCo can still simulate from the layer and nothing is smuggled through customData. +But it **bakes** every mesh into the layer: the M20 comes out 4.8 MB of binary crate, 19 MB as +text, with no reference arcs at all. dimos hands a scene over as one string and serves each mesh +separately by content hash, so baked geometry is both too big and the wrong shape. + +So the converter runs first, in its own virtualenv, and this re-points the baked meshes at the +files they came from: + + python3 -m venv /tmp/mjcfusd + /tmp/mjcfusd/bin/pip install mujoco-usd-converter + # its own warning: usd-exchange ships OpenUSD and will not share with usd-core + /tmp/mjcfusd/bin/pip uninstall -y usd-core + /tmp/mjcfusd/bin/pip install --force-reinstall usd-exchange + /tmp/mjcfusd/bin/python -c " + import mujoco_usd_converter + mujoco_usd_converter.Converter().convert('.xml', '/tmp/converted')" + + # then, in dimos's own venv: + python scripts/mjcf_to_usd.py --mjcf .xml --converted /tmp/converted/.usda \\ + --meshes /meshes --out dimos/robot///assets/.usda + +Each mesh then ends up named twice, on purpose, because two kinds of consumer want two +different things from it: + +* a **reference arc** to a ``.usdc`` sidecar this writes beside the original. A reference + composes a USD layer and only a USD layer, so this is what makes the result valid USD -- + usdview, Isaac and MuJoCo 4 all compose it and get real triangles. +* ``assetInfo["identifier"]``, naming the **original mesh file**, which is what dimos serves + over its own bus and what dim-controller fetches and draws. It never composes the stage. + +Dropping either one costs a consumer its geometry, so both are authored. + +**Never install mujoco-usd into dimos's virtualenv.** It pulls usd-exchange, which bundles its +own OpenUSD and declares usd-core incompatible, and mujoco-core, which shadows mujoco. Removing +them afterwards takes usd-core's modules with it. +""" + +from __future__ import annotations + +import argparse +import os +from pathlib import Path +import re +import sys +from typing import Any +import xml.etree.ElementTree as ET + +from dimos.robot.assets.usd import hash_of_file +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + +# The geometry a referenced mesh file supplies for itself. +_BAKED = frozenset({"points", "faceVertexIndices", "faceVertexCounts", "normals", "extent"}) +# The two primvars that are the mesh's appearance rather than its geometry. They are one value +# per prim, not one per vertex, so keeping them costs nothing -- and dropping them is what left +# every piece of office_555's furniture at MuJoCo's default grey: the MJCF gives those geoms an +# rgba rather than a material, the exporter writes it here, and the sidecar does not carry it. +_KEPT_PRIMVARS = frozenset({"primvars:displayColor", "primvars:displayOpacity"}) +# What a sidecar is written as: binary, because nothing hands these around as text -- they are +# fetched as bytes, one file per mesh, exactly like the STL beside them. +_SIDECAR_SUFFIX = ".usdc" + + +def write_sidecar(source: Path, pxr: Any) -> Path: + """``source`` as a one-prim USD layer beside it, so a reference to it is valid USD. + + Returns the layer's path. Rewritten every run rather than cached: it is derived from the + mesh, and a stale sidecar is a robot whose geometry silently does not match its meshes. + """ + import trimesh + + out = source.with_suffix(_SIDECAR_SUFFIX) + loaded = trimesh.load_mesh(str(source), process=False) + if isinstance(loaded, trimesh.Scene): + loaded = loaded.to_geometry() + if len(loaded.faces) == 0: + # Refusing beats writing an empty sidecar. A git-lfs **pointer** parses as a mesh with + # no faces, so running this against a worktree that has not been materialised produced + # 427 identical 737-byte layers and the scene then died much later, inside MuJoCo, as + # "at least 4 vertices required" on a mesh whose file looked present. + raise ValueError( + f"{source} has no faces. If it is {source.stat().st_size} bytes it is a git-lfs " + f"pointer: run `git lfs checkout` on its directory first." + ) + + stage = pxr.Usd.Stage.CreateInMemory(out.name) + mesh = pxr.UsdGeom.Mesh.Define(stage, "/mesh") + mesh.CreatePointsAttr([pxr.Gf.Vec3f(*point) for point in loaded.vertices.tolist()]) + mesh.CreateFaceVertexIndicesAttr([int(i) for i in loaded.faces.reshape(-1).tolist()]) + mesh.CreateFaceVertexCountsAttr([3] * len(loaded.faces)) + # Without an extent a consumer has to load every point to frame the robot. A degenerate + # mesh has no bounds to state, and saying nothing is better than saying something wrong. + bounds = loaded.bounds + if bounds is not None: + mesh.CreateExtentAttr( + [pxr.Gf.Vec3f(*bounds[0].tolist()), pxr.Gf.Vec3f(*bounds[1].tolist())] + ) + stage.SetDefaultPrim(mesh.GetPrim()) + stage.GetRootLayer().Export(str(out)) + return out + + +def mesh_files(mjcf: Path) -> dict[str, str]: + """Every name a baked prim might carry -> the mesh file behind it. + + Three spellings, because the converter names a mesh prim after whichever the MJCF gave it: + + * the **mesh asset's** own name, which is what a robot MJCF uses -- its geoms are unnamed + and the prim inherits ``; + * the **file's stem**, for a `` with no name at all. MuJoCo's own rule is + that such a mesh is called after its file, and the Go2 declares all sixteen of its meshes + that way (``, referenced as `mesh="base_0"`). Reading only the + explicit spelling found one named mesh in the whole robot; + * the **geom's** name, which is what a scene MJCF uses -- office_555 names every geom + (`drawer_cabinet_5_g29`) and has them reference meshes called `asset_0`, `asset_1`, ... + + Indexing all three means one lookup covers every model, and a robot and a room go through + the same script. Without the geom spelling the office scene re-points nothing: 3547 meshes + stay baked and the layer comes out 35 MB. + """ + root = ET.fromstring(mjcf.read_text()) + files = {} + for mesh in root.iter("mesh"): + file = mesh.get("file") + name = mesh.get("name") or (Path(file).stem if file else None) + if name and file: + files[name] = file + for geom in root.iter("geom"): + name, mesh_name = geom.get("name"), geom.get("mesh") + if name and mesh_name and mesh_name in files: + files[name] = files[mesh_name] + return files + + +def _mesh_of(prim_name: str, files: dict[str, str]) -> str | None: + """The mesh file a prim's name refers to, allowing for the converter's de-duplication. + + Two geoms on one body that use the same mesh become two prims, and the converter names the + second ``_1``. Both are the same file. Without this every such prim stays baked, which + on the G1 is 23 of 58. + """ + if prim_name in files: + return files[prim_name] + stripped = re.sub(r"_\d+$", "", prim_name) + return files.get(stripped) if stripped != prim_name else None + + +def rereference( + stage: Any, files: dict[str, str], meshes: Path, out_dir: Path, pxr: Any +) -> tuple[int, int]: + """Swap each baked mesh for a reference to its file, with the file's hash beside it. + + Returns how many were re-pointed and how many were left alone. A mesh the MJCF does not + name is left baked rather than dropped: it came from somewhere this cannot resolve, and a + hole in the robot is worse than a large layer. + + References are written **relative to the layer**, because the layer is committed and the + absolute path it was converted at means nothing on anyone else's machine. + """ + done = skipped = 0 + for prim in list(stage.Traverse()): + if not prim.IsA(pxr.UsdGeom.Mesh): + continue + file = _mesh_of(prim.GetName(), files) + if file is None: + logger.warning(f"{prim.GetPath()} has no mesh of that name in the MJCF; left baked") + skipped += 1 + continue + # --meshes is authoritative, so a `file=` that carries a directory (because the MJCF + # set meshdir, or names the vendor checkout by relative path) is matched by its + # basename rather than pasted onto the directory a second time. + path = next( + ( + candidate + for candidate in ((meshes / file).resolve(), (meshes / Path(file).name).resolve()) + if candidate.is_file() + ), + None, + ) + if path is None: + logger.warning(f"no {file} under {meshes}; {prim.GetPath()} left baked") + skipped += 1 + continue + # Everything per-vertex goes: the reference supplies it, and it is what made the layer + # large. primvars included -- the converter bakes normals there, which alone is most of + # the size. Transform ops, material bindings and the mesh's own colour stay, since the + # file carries none of them. + for property in list(prim.GetPropertyNames()): + if property.startswith(("xformOp", "material:")) or property in _KEPT_PRIMVARS: + continue + if property.startswith("primvars:") or property in _BAKED: + prim.RemoveProperty(property) + sidecar = write_sidecar(path, pxr) + relative = os.path.relpath(path, out_dir) + prim.GetReferences().ClearReferences() + # The sidecar, not the mesh file: a reference composes a USD layer and nothing else. + prim.GetReferences().AddReference(os.path.relpath(sidecar, out_dir)) + digest = hash_of_file(str(path)) + info = prim.GetAssetInfo() or {} + info["identifier"] = pxr.Sdf.AssetPath(relative) + if digest: + info["version"] = digest + prim.SetAssetInfo(info) + done += 1 + return done, skipped + + +def name_joints(stage: Any, pxr: Any) -> int: + """Give every unnamed joint prim the name of the body it moves. Returns how many. + + The converter leaves a joint it was not given a name for as the bare schema name, so a scene + of loose furniture comes out with **every** weld called `PhysicsFixedJoint` -- office_555 has + 448 of them. That is legal USD, since each is a child of a different body, but it makes the + layer useless as a frame source: `usd_frame_names` returns one name for 448 edges, a URDF + converted from it has 448 joints of the same name and will not load, and grafting by frame + name is ambiguous. The robots do not hit this because an MJCF joint is always named. + + `_joint` matches what the robot layers already carry (`left_ankle_roll_joint`). + """ + renamed = 0 + for prim in list(stage.Traverse()): + if not prim.IsA(pxr.UsdPhysics.Joint) or prim.GetName() != prim.GetTypeName(): + continue + targets = prim.GetRelationship("physics:body1").GetTargets() + if not targets: + logger.warning(f"{prim.GetPath()} is unnamed and moves nothing; left as it is") + continue + child = str(targets[0]).rsplit("/", 1)[-1] + edit = pxr.Sdf.NamespaceEdit.Rename(prim.GetPath(), f"{child}_joint") + batch = pxr.Sdf.BatchNamespaceEdit() + batch.Add(edit) + if stage.GetRootLayer().Apply(batch): + renamed += 1 + else: + logger.warning(f"could not rename {prim.GetPath()} to {child}_joint") + return renamed + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--mjcf", required=True, type=Path, help="the vendor MJCF, for its mesh names" + ) + parser.add_argument("--converted", required=True, type=Path, help="the converter's stage") + parser.add_argument( + "--meshes", required=True, type=Path, help="the directory holding the mesh files" + ) + parser.add_argument("--out", required=True, type=Path, help="the .usda to write") + args = parser.parse_args() + + from pxr import Gf, Sdf, Usd, UsdGeom, UsdPhysics + + pxr = argparse.Namespace(Gf=Gf, Sdf=Sdf, Usd=Usd, UsdGeom=UsdGeom, UsdPhysics=UsdPhysics) + + for path in (args.mjcf, args.converted): + if not path.is_file(): + print(f"ERROR: no file at {path}", file=sys.stderr) + return 1 + + files = mesh_files(args.mjcf) + + # Flattened, because the converter writes a root that payloads several binary layers and a + # scene has to leave here as one text layer. + stage = Usd.Stage.Open(str(args.converted)) + flat = Usd.Stage.Open(stage.Flatten()) + # A model of boxes and spheres names no meshes and has none to re-point, which is fine. A + # model whose stage is full of them and whose MJCF names none is the --mjcf being the wrong + # file, and re-pointing nothing would quietly write a layer of baked geometry. + baked = any(prim.IsA(UsdGeom.Mesh) for prim in flat.Traverse()) + if baked and not files: + print( + f"ERROR: {args.converted} has meshes and {args.mjcf} names none; " + f"is --mjcf the file it was converted from?", + file=sys.stderr, + ) + return 1 + args.out.parent.mkdir(parents=True, exist_ok=True) + named = name_joints(flat, pxr) + done, skipped = rereference(flat, files, args.meshes, args.out.parent.resolve(), pxr) + + # The ROOT LAYER, not the stage: Stage.Export() flattens composition, which resolves the + # reference arcs we just authored back into baked geometry -- the opposite of the point. + flat.GetRootLayer().Export(str(args.out)) + size = args.out.stat().st_size + print(f"{done} mesh(es) re-pointed with a .usdc sidecar, {skipped} left baked") + print(f"{named} unnamed joint(s) named after the body they move") + print(f"wrote {args.out} ({size / 1e6:.2f} MB)") + if skipped: + print("note: a baked mesh keeps the layer large; see the warnings above for why") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/usd_drives.py b/scripts/usd_drives.py new file mode 100644 index 00000000000..262c0821fa1 --- /dev/null +++ b/scripts/usd_drives.py @@ -0,0 +1,63 @@ +# Copyright 2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Write a robot's gains and home pose into its committed layer. Run once by hand. + +A robot's gains are a fact about the robot, so they belong in the layer that describes it rather +than in whatever happens to be running it. For a robot whose controller ships as a blob -- the +Go2's walking policy carries its own kp, kd and default pose -- that means reading them out of +the blob once and committing the result:: + + python scripts/usd_drives.py \\ + --layer dimos/robot/unitree/go2/assets/go2.usda \\ + --from dimos.robot.unitree.go2.sim2:go2_drives + +``--from`` names a callable returning ``{joint: (kp, kd, target)}``. It lives with the robot, +because which gains a robot runs on is the robot's business and not this script's; all this does +is the USD authoring, and the test beside the robot is what keeps the layer honest if the blob +it came from ever changes. +""" + +from __future__ import annotations + +import argparse +from importlib import import_module +from pathlib import Path + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--layer", type=Path, required=True, help="the .usda to rewrite in place") + parser.add_argument( + "--from", + dest="source", + required=True, + help="module:callable returning {joint: (kp, kd, target)}", + ) + args = parser.parse_args() + + from dimos.robot.assets.usd import usd_drives, usd_with_drives + + module, _, name = args.source.partition(":") + drives = getattr(import_module(module), name)() + layer = usd_with_drives(args.layer.read_text(), drives) + args.layer.write_text(layer) + written = usd_drives(layer) + print(f"wrote {len(written)} drive(s) into {args.layer}") + for joint, (kp, kd, target) in sorted(written.items()): + print(f" {joint}: kp={kp:g} kd={kd:g} target={target:g}") + + +if __name__ == "__main__": + main() From 0c6a63c5bbff16d0dd2affbfc489d933ce94c05f Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 9 Nov 2026 12:08:02 -0800 Subject: [PATCH 12/19] the Go2's config stops carrying an xyz nothing reads Where it stands is the room's mount frame and has been since the scene composed it; a second place to say so could only disagree with the first. --- dimos/robot/unitree/go2/sim2.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dimos/robot/unitree/go2/sim2.py b/dimos/robot/unitree/go2/sim2.py index 5a9915d4c6d..7abc9bad6b2 100644 --- a/dimos/robot/unitree/go2/sim2.py +++ b/dimos/robot/unitree/go2/sim2.py @@ -75,7 +75,6 @@ def go2_drives() -> dict[str, tuple[float, float, float]]: class Go2SimConfig(ModuleConfig): robot_id: str = "go2" - xyz: tuple[float, float, float] = (0.0, 0.0, 0.35) policy: Path | None = None odom_frame_id: str = "world" # Yaw rate commanded per radian of heading error while no turn is commanded. From 4889030f8f7db0bc1c8c76c5bbf92195733f4313 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 9 Nov 2026 13:24:45 -0800 Subject: [PATCH 13/19] sim2_go2_l1: trim comment fluff --- dimos/memory2/db_tf.py | 14 +-- .../blueprints/basic/unitree_go2_sim2_l1.py | 32 ++---- dimos/robot/unitree/go2/sim2.py | 27 +++-- dimos/robot/unitree/go2/test_usd.py | 9 +- dimos/sim2/assets/logistics.py | 9 +- dimos/sim2/l1.py | 20 ++-- dimos/sim2/test_sim_time.py | 10 +- scripts/mjcf_to_usd.py | 101 ++++++++---------- scripts/usd_drives.py | 5 +- 9 files changed, 92 insertions(+), 135 deletions(-) diff --git a/dimos/memory2/db_tf.py b/dimos/memory2/db_tf.py index eb7ef743828..4ff516d63e8 100644 --- a/dimos/memory2/db_tf.py +++ b/dimos/memory2/db_tf.py @@ -15,15 +15,11 @@ """Compatibility stub for the removed ``dimos.memory2.db_tf`` module. Some older recordings register a ``tf_graph`` stream whose payload type is -``dimos.memory2.db_tf.TfGraph``. That module no longer exists, so any tool that -resolves every stream's payload type up front (e.g. ``dimos map summary`` / -``dimos map replay`` via ``_stream_payload_types``) fails to import the whole -recording with ``ModuleNotFoundError: No module named 'dimos.memory2.db_tf'``. - -The ``tf_graph`` stream is irrelevant for mapping — those tools only iterate the -PointCloud2 and odometry streams — so this is a do-nothing placeholder that just -lets the payload-type lookup succeed. It is not a functional TF graph; iterating -or decoding a ``tf_graph`` stream through it is not supported. +``dimos.memory2.db_tf.TfGraph``. That module is gone, so any tool resolving every stream's +payload type up front (``dimos map summary``, ``dimos map replay``) fails to import the whole +recording with ``ModuleNotFoundError``. The ``tf_graph`` stream is irrelevant for mapping, so +this is a do-nothing placeholder. Iterating or decoding a ``tf_graph`` stream through it is not +supported. """ from __future__ import annotations diff --git a/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py b/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py index c42bc95635b..18a8e985cef 100644 --- a/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py +++ b/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py @@ -12,18 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""unitree-go2-basic in sim2: the walking Go2 in the logistics room, its head L1 and front camera. +"""unitree-go2-basic in sim2: the walking Go2 in the logistics room, its head L1 and front +camera. Two managers, nested, which is the shape every one of these has. The inner one answers for the *robot*: the Go2's own layer with each sensor grafted onto the frame it is bolted to. The outer one answers for the *world*: the room, with the robot grafted onto a spot on its floor. What -that outer one composes is the world -- `get_scene` hands out one layer holding the room, the -robot and the sensors -- and it is what the physics runs and what every sensor renders from, so -a scan lands on the furniture that is drawn. - -Nobody describes themselves to the simulation. The blueprint says which robots are in the world, -because the blueprint is where that is already written down, and the robot's gains and the pose -it holds are in its layer. +that outer one composes is what the physics runs and what every sensor renders from. """ from dimos.core.coordination.blueprints import autoconnect @@ -43,30 +38,25 @@ # What the world calls this robot: the entity id its actions are addressed to, and the prefix on # every frame of it. ROBOT = "go2" -# Where it stands, said once. The scene's mount frame is the one statement of it: the simulation -# gets this world from the manager and finds the robot already in it, so a second xyz on the -# description could only disagree with the room. +# Where it stands, said once. The scene's mount frame is the one statement of it: a second xyz +# on the description could only disagree with the room. SPOT = (0.0, 0.0, 0.35) -# The namespace the scene's manager lives in, and so the name the things that ask it for the -# scene use. A manager outside every namespace would be a second one answering on the same -# unnamespaced topics as the robot's. +# The namespace the scene's manager lives in, and so the name everything that asks it uses. A +# manager outside every namespace would answer on the same unnamespaced topics as the robot's. SCENE = "scene" SCENE_MANAGER = f"{SCENE}/{UsdManager.name}" -# The room, with a spot on its floor for the robot. A mounts dict is keyed by frame, so a frame -# is what a visitor hangs off; the room arrives with frames for its own furniture and none for -# anything standing in it. +# The room, with a spot on its floor for the robot. A mounts dict is keyed by frame, and the +# room arrives with frames for its own furniture and none for anything standing in it. _room = UsdModel.from_file(LOGISTICS).with_frames({ROBOT: ("logistics", SPOT, (0.0, 0.0, 0.0))}) # The sensors as their own blueprints, because the manager mounts them by the instance each one -# deploys: a frame_id alone would name a module the manager cannot address. Each asks the scene's -# manager for the world it is in and builds its own copy of it. +# deploys: a frame_id alone would name a module the manager cannot address. _lidar = SimL1.blueprint(frame_id=Go2Sim.l1_mount_frame, scene_manager=SCENE_MANAGER) _camera = SimCamera.blueprint(frame_id=Go2Sim.camera_mount_frame, scene_manager=SCENE_MANAGER) # One manager holding the whole robot: the Go2's own layer, with each sensor grafted onto the -# frame it is bolted to. Without the mounts it answers for the robot alone, so the lidar and the -# camera are nowhere in the stage a viewer draws. +# frame it is bolted to. Without the mounts the lidar and camera are nowhere in the stage. _robot = UsdManager.blueprint( model=Go2Sim.usd, mounts={Go2Sim.l1_mount_frame: _lidar, Go2Sim.camera_mount_frame: _camera} ).remappings([(UsdManager, "coordinator_joint_state", "joint_state")]) diff --git a/dimos/robot/unitree/go2/sim2.py b/dimos/robot/unitree/go2/sim2.py index 7abc9bad6b2..1816aa7ede8 100644 --- a/dimos/robot/unitree/go2/sim2.py +++ b/dimos/robot/unitree/go2/sim2.py @@ -12,13 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""A simulated Go2 with GO2Connection's cmd_vel/odom/tf: the walking policy, observing world_observations and sending joint commands. +"""A simulated Go2 with GO2Connection's cmd_vel/odom/tf: the walking policy, observing +world_observations and sending joint commands. -Go2Sim.usd is the committed Go2 layer with the rig's sensor frames added: a sensor names one -(Go2Sim.lidar_mount_frame, ...) as its frame_id, and the blueprint hands the same layer to a -UsdManager, which poses those frames on tf, and to `sim_module`, which runs them. The layer -carries the policy's gains and its home pose as drives (see `go2_drives`), so what holds this -robot up is stated once, where the robot is described. +Go2Sim.usd is the committed Go2 layer with the rig's sensor frames added: a sensor names one as +its frame_id, and the blueprint hands the same layer to a UsdManager, which poses those frames +on tf, and to `sim_module`, which runs them. The layer carries the policy's gains and its home +pose as drives (see `go2_drives`). """ import asyncio @@ -58,11 +58,10 @@ def go2_drives() -> dict[str, tuple[float, float, float]]: """The gains and home pose the walking policy runs on, as `usd_with_drives` wants them. - Read out of the policy blob, which carries its own kp, kd and default pose: the gains that - hold this robot up are the controller's, not the vendor's. Called by - `scripts/usd_drives.py` to write them into the committed layer, and by the test that keeps - the layer honest if the blob ever changes -- never at runtime, because by then the layer is - what says it. + Read out of the policy blob, which carries its own kp, kd and default pose: the gains that hold + this robot up are the controller's, not the vendor's. Called by `scripts/usd_drives.py` to + write them into the committed layer, and by the test that keeps the layer honest -- never at + runtime, because by then the layer is what says it. """ policy = load_policy(None) return { @@ -82,9 +81,9 @@ class Go2SimConfig(ModuleConfig): class Go2Sim(Module): - # The committed layer, converted once from the vendor MJCF (see assets/go2.usda), carrying - # the real rig's frames (go2_mid360_static_transforms) on its base. The mounts are added here - # rather than baked in because a mount is whose robot carries it, not what the robot is. + # The committed layer, converted once from the vendor MJCF (see assets/go2.usda), carrying the + # real rig's frames on its base. The mounts are added here rather than baked in because a mount + # is whose robot carries it, not what the robot is. usd = UsdModel.from_file(Path(__file__).parent / "assets" / "go2.usda").with_frames( { "utlidar_lidar": ( diff --git a/dimos/robot/unitree/go2/test_usd.py b/dimos/robot/unitree/go2/test_usd.py index 6aa1ac5f926..639578f7b5f 100644 --- a/dimos/robot/unitree/go2/test_usd.py +++ b/dimos/robot/unitree/go2/test_usd.py @@ -20,12 +20,9 @@ def test_the_layer_carries_the_gains_the_policy_runs_on() -> None: - """The gains are in the layer, written there once by `scripts/usd_drives.py`. - - Which means they can go stale: the policy blob is where they came from, and nothing stops - somebody replacing it. This is what notices. Run the script again if it fails -- the layer is - the source of truth for whatever runs the robot, and it has to agree with the controller the - robot actually ships with. + """The gains are in the layer, written there once by `scripts/usd_drives.py`, so they can go + stale if somebody replaces the policy blob. This is what notices; run the script again if it + fails. """ from dimos.robot.assets.usd import usd_drives from dimos.robot.unitree.go2.sim2 import Go2Sim, go2_drives diff --git a/dimos/sim2/assets/logistics.py b/dimos/sim2/assets/logistics.py index 7e42c843bca..38918af1e00 100644 --- a/dimos/sim2/assets/logistics.py +++ b/dimos/sim2/assets/logistics.py @@ -12,12 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""The room a sim2 blueprint runs in when nobody has a real scene to hand: walls, shelves, a table. +"""The room a sim2 blueprint runs in when nobody has a real scene to hand: walls, shelves, a +table. -USD, like every other world here, converted once from the MJCF it used to be written as and -committed beside this file. Its lighting is written in by hand, because the converter drops -lights outright ("lights are not supported") and a layer that says nothing about how it is lit -gets whatever the viewer guesses. +USD, converted once from the MJCF it used to be and committed beside this file. Its lighting is +written in by hand, because the converter drops lights outright ("lights are not supported"). MuJoCo: 1 plane, 8 boxes, no joints and nothing that moves dimos: root link "logistics", which is the frame a visitor hangs off diff --git a/dimos/sim2/l1.py b/dimos/sim2/l1.py index 20c44e3f4f6..f1faa6ec1f2 100644 --- a/dimos/sim2/l1.py +++ b/dimos/sim2/l1.py @@ -66,10 +66,8 @@ class SimL1(Module): @rpc def get_usd(self) -> list[str]: - """``[asset_root, usd]``: the frame this L1 scans from. - - A frame rather than a case: the Go2's utlidar sits under a cover that is part of the - robot's own shell, and nobody has measured the unit inside it. + """``[asset_root, usd]``: the frame this L1 scans from. A frame rather than a case: the Go2's + utlidar sits under a cover that is part of the robot's own shell. """ return frame_usd(self.frame_id, name="sim_l1") @@ -81,11 +79,9 @@ async def main(self) -> AsyncIterator[None]: self._mapping.cancel() def _open(self) -> WorldView: - """This L1's own world: the composed scene, with a site at its own frame. - - Opened on the first frame rather than at start, because the robots in the world are what - the world says they are -- the observations announce them -- and the view has to name its - bodies the way the observations do. + """This L1's own world: the composed scene, with a site at its own frame. Opened on the first + frame rather than at start, because the robots in the world are the ones the observations + announce, and the view has to name its bodies the way they do. """ scene = scene_from(self.config.scene_manager, self.rpc, self.config.scene) if scene is None: @@ -131,10 +127,8 @@ async def _map(self) -> None: ) def _center(self, rider: str | None, origin: NDArray[np.float64]) -> NDArray[np.float64]: - """Where the square is cropped around: the robot carrying this L1, or the L1 itself. - - Read off the observations rather than out of the view, because that is where the robot's - pose comes from in the first place and the view is only a copy of it. + """Where the square is cropped around: the robot carrying this L1, or the L1 itself. Read off + the observations rather than out of the view, because that is where the pose comes from. """ entity = self._world.entity(rider) if rider is not None else None return origin if entity is None or entity.pose is None else entity.pose[:3] diff --git a/dimos/sim2/test_sim_time.py b/dimos/sim2/test_sim_time.py index 2df56e08521..dda8cdb4c51 100644 --- a/dimos/sim2/test_sim_time.py +++ b/dimos/sim2/test_sim_time.py @@ -77,10 +77,8 @@ def robot_model() -> UsdModel: @pytest.fixture def composed(robot_model: UsdModel) -> UsdModel: - """The world as a manager composes it: the room, with the dog grafted onto a spot on its floor. - - What the simulation runs and what every sensor renders from, which is the point -- one layer, - one world, and the furniture a scan comes back off is the furniture that is drawn. + """The world as a manager composes it: the room, with the dog grafted onto a spot on its + floor. One layer, one world, so the furniture a scan comes back off is the one that is drawn. """ # The toy's layer puts its base at (9, 9, 9), so the spot it stands on takes that back out: # a mount frame places a robot by its own layer's origin, not by where its root happens to be. @@ -195,8 +193,8 @@ def _run( coordinator = ModuleCoordinator.build(blueprint, parsed) try: readings = coordinator.get_instance(_Readings) - # At least `seconds` of wall time, then until every sensor has steady stamps: on a loaded machine - # a sensor's first point cloud (it imports open3d) can take most of that. + # At least `seconds` of wall time, then until every sensor has steady stamps: a sensor's first + # point cloud (it imports open3d) can take most of that on a loaded machine. deadline = time.monotonic() + seconds + MAX_EXTRA_WALL time.sleep(seconds) while time.monotonic() < deadline and not _steady(readings.readings()["stamps"]): diff --git a/scripts/mjcf_to_usd.py b/scripts/mjcf_to_usd.py index 6d6e696457d..8cab475d1a9 100755 --- a/scripts/mjcf_to_usd.py +++ b/scripts/mjcf_to_usd.py @@ -16,11 +16,10 @@ """A vendor MJCF as the USD dimos serves. Run once by hand; the result is committed. Why two steps. Google DeepMind's converter (``mujoco-usd`` on PyPI) is the only thing that reads -an MJCF into USD with its physics intact -- it writes MjcActuator, MjcJointAPI and MjcCollisionAPI -prims, so MuJoCo can still simulate from the layer and nothing is smuggled through customData. -But it **bakes** every mesh into the layer: the M20 comes out 4.8 MB of binary crate, 19 MB as -text, with no reference arcs at all. dimos hands a scene over as one string and serves each mesh -separately by content hash, so baked geometry is both too big and the wrong shape. +an MJCF into USD with its physics intact -- MjcActuator, MjcJointAPI and MjcCollisionAPI prims, +so MuJoCo can still simulate from the layer. But it **bakes** every mesh into the layer (the M20 +comes out 19 MB as text, with no reference arcs), and dimos serves each mesh separately by +content hash. So the converter runs first, in its own virtualenv, and this re-points the baked meshes at the files they came from: @@ -42,10 +41,9 @@ different things from it: * a **reference arc** to a ``.usdc`` sidecar this writes beside the original. A reference - composes a USD layer and only a USD layer, so this is what makes the result valid USD -- - usdview, Isaac and MuJoCo 4 all compose it and get real triangles. + composes a USD layer and only a USD layer, so this is what makes the result valid USD. * ``assetInfo["identifier"]``, naming the **original mesh file**, which is what dimos serves - over its own bus and what dim-controller fetches and draws. It never composes the stage. + over its own bus. It never composes the stage. Dropping either one costs a consumer its geometry, so both are authored. @@ -71,10 +69,10 @@ # The geometry a referenced mesh file supplies for itself. _BAKED = frozenset({"points", "faceVertexIndices", "faceVertexCounts", "normals", "extent"}) -# The two primvars that are the mesh's appearance rather than its geometry. They are one value -# per prim, not one per vertex, so keeping them costs nothing -- and dropping them is what left -# every piece of office_555's furniture at MuJoCo's default grey: the MJCF gives those geoms an -# rgba rather than a material, the exporter writes it here, and the sidecar does not carry it. +# The two primvars that are the mesh's appearance rather than its geometry. One value per prim, +# not per vertex, so keeping them costs nothing -- and dropping them left every piece of +# office_555's furniture at MuJoCo's default grey, because the MJCF gives those geoms an rgba +# the exporter writes here and the sidecar does not carry. _KEPT_PRIMVARS = frozenset({"primvars:displayColor", "primvars:displayOpacity"}) # What a sidecar is written as: binary, because nothing hands these around as text -- they are # fetched as bytes, one file per mesh, exactly like the STL beside them. @@ -84,8 +82,8 @@ def write_sidecar(source: Path, pxr: Any) -> Path: """``source`` as a one-prim USD layer beside it, so a reference to it is valid USD. - Returns the layer's path. Rewritten every run rather than cached: it is derived from the - mesh, and a stale sidecar is a robot whose geometry silently does not match its meshes. + Rewritten every run rather than cached: a stale sidecar is a robot whose geometry silently + does not match its meshes. """ import trimesh @@ -94,10 +92,9 @@ def write_sidecar(source: Path, pxr: Any) -> Path: if isinstance(loaded, trimesh.Scene): loaded = loaded.to_geometry() if len(loaded.faces) == 0: - # Refusing beats writing an empty sidecar. A git-lfs **pointer** parses as a mesh with - # no faces, so running this against a worktree that has not been materialised produced - # 427 identical 737-byte layers and the scene then died much later, inside MuJoCo, as - # "at least 4 vertices required" on a mesh whose file looked present. + # Refusing beats writing an empty sidecar. A git-lfs **pointer** parses as a mesh with no + # faces, so running this against an unmaterialised worktree produced 427 identical 737-byte + # layers and the scene died much later inside MuJoCo as "at least 4 vertices required". raise ValueError( f"{source} has no faces. If it is {source.stat().st_size} bytes it is a git-lfs " f"pointer: run `git lfs checkout` on its directory first." @@ -125,18 +122,14 @@ def mesh_files(mjcf: Path) -> dict[str, str]: Three spellings, because the converter names a mesh prim after whichever the MJCF gave it: - * the **mesh asset's** own name, which is what a robot MJCF uses -- its geoms are unnamed - and the prim inherits ``; - * the **file's stem**, for a `` with no name at all. MuJoCo's own rule is - that such a mesh is called after its file, and the Go2 declares all sixteen of its meshes - that way (``, referenced as `mesh="base_0"`). Reading only the - explicit spelling found one named mesh in the whole robot; - * the **geom's** name, which is what a scene MJCF uses -- office_555 names every geom - (`drawer_cabinet_5_g29`) and has them reference meshes called `asset_0`, `asset_1`, ... - - Indexing all three means one lookup covers every model, and a robot and a room go through - the same script. Without the geom spelling the office scene re-points nothing: 3547 meshes - stay baked and the layer comes out 35 MB. + * the **mesh asset's** own name, which is what a robot MJCF uses; + * the **file's stem**, for a `` with no name -- MuJoCo's rule, and how the Go2 + declares all sixteen of its meshes. Reading only the explicit spelling found one; + * the **geom's** name, which is what a scene MJCF uses (office_555 names every geom and has + them reference meshes called `asset_0`, `asset_1`, ...). + + Without the geom spelling the office scene re-points nothing: 3547 meshes stay baked and the + layer comes out 35 MB. """ root = ET.fromstring(mjcf.read_text()) files = {} @@ -153,11 +146,9 @@ def mesh_files(mjcf: Path) -> dict[str, str]: def _mesh_of(prim_name: str, files: dict[str, str]) -> str | None: - """The mesh file a prim's name refers to, allowing for the converter's de-duplication. - - Two geoms on one body that use the same mesh become two prims, and the converter names the - second ``_1``. Both are the same file. Without this every such prim stays baked, which - on the G1 is 23 of 58. + """The mesh file a prim's name refers to, allowing for the converter's de-duplication: two + geoms on one body using the same mesh become two prims, the second named ``_1``. Without + this every such prim stays baked, which on the G1 is 23 of 58. """ if prim_name in files: return files[prim_name] @@ -168,14 +159,12 @@ def _mesh_of(prim_name: str, files: dict[str, str]) -> str | None: def rereference( stage: Any, files: dict[str, str], meshes: Path, out_dir: Path, pxr: Any ) -> tuple[int, int]: - """Swap each baked mesh for a reference to its file, with the file's hash beside it. - - Returns how many were re-pointed and how many were left alone. A mesh the MJCF does not - name is left baked rather than dropped: it came from somewhere this cannot resolve, and a - hole in the robot is worse than a large layer. + """Swap each baked mesh for a reference to its file, with the file's hash beside it. Returns + how many were re-pointed and how many were left alone. - References are written **relative to the layer**, because the layer is committed and the - absolute path it was converted at means nothing on anyone else's machine. + A mesh the MJCF does not name is left baked rather than dropped: a hole in the robot is worse + than a large layer. References are written **relative to the layer**, because the layer is + committed and the path it was converted at means nothing on anyone else's machine. """ done = skipped = 0 for prim in list(stage.Traverse()): @@ -186,9 +175,8 @@ def rereference( logger.warning(f"{prim.GetPath()} has no mesh of that name in the MJCF; left baked") skipped += 1 continue - # --meshes is authoritative, so a `file=` that carries a directory (because the MJCF - # set meshdir, or names the vendor checkout by relative path) is matched by its - # basename rather than pasted onto the directory a second time. + # --meshes is authoritative, so a `file=` carrying a directory is matched by its basename + # rather than pasted onto the directory a second time. path = next( ( candidate @@ -201,10 +189,9 @@ def rereference( logger.warning(f"no {file} under {meshes}; {prim.GetPath()} left baked") skipped += 1 continue - # Everything per-vertex goes: the reference supplies it, and it is what made the layer - # large. primvars included -- the converter bakes normals there, which alone is most of - # the size. Transform ops, material bindings and the mesh's own colour stay, since the - # file carries none of them. + # Everything per-vertex goes: the reference supplies it, and it is what made the layer large. + # primvars included -- the converter bakes normals there. Transform ops, material bindings and + # the mesh's own colour stay, since the file carries none of them. for property in list(prim.GetPropertyNames()): if property.startswith(("xformOp", "material:")) or property in _KEPT_PRIMVARS: continue @@ -228,12 +215,10 @@ def rereference( def name_joints(stage: Any, pxr: Any) -> int: """Give every unnamed joint prim the name of the body it moves. Returns how many. - The converter leaves a joint it was not given a name for as the bare schema name, so a scene - of loose furniture comes out with **every** weld called `PhysicsFixedJoint` -- office_555 has - 448 of them. That is legal USD, since each is a child of a different body, but it makes the - layer useless as a frame source: `usd_frame_names` returns one name for 448 edges, a URDF - converted from it has 448 joints of the same name and will not load, and grafting by frame - name is ambiguous. The robots do not hit this because an MJCF joint is always named. + The converter leaves a joint it was not given a name for as the bare schema name, so a scene of + loose furniture comes out with **every** weld called `PhysicsFixedJoint` -- office_555 has 448 + of them. Legal USD, but useless as a frame source: `usd_frame_names` returns one name for 448 + edges, and a URDF converted from it has 448 joints of the same name and will not load. `_joint` matches what the robot layers already carry (`left_ankle_roll_joint`). """ @@ -283,9 +268,9 @@ def main() -> int: # scene has to leave here as one text layer. stage = Usd.Stage.Open(str(args.converted)) flat = Usd.Stage.Open(stage.Flatten()) - # A model of boxes and spheres names no meshes and has none to re-point, which is fine. A - # model whose stage is full of them and whose MJCF names none is the --mjcf being the wrong - # file, and re-pointing nothing would quietly write a layer of baked geometry. + # A model of boxes and spheres names no meshes, which is fine. A stage full of them whose MJCF + # names none is the --mjcf being the wrong file, and re-pointing nothing would quietly write a + # layer of baked geometry. baked = any(prim.IsA(UsdGeom.Mesh) for prim in flat.Traverse()) if baked and not files: print( diff --git a/scripts/usd_drives.py b/scripts/usd_drives.py index 262c0821fa1..6a6ea37072f 100644 --- a/scripts/usd_drives.py +++ b/scripts/usd_drives.py @@ -24,9 +24,8 @@ --from dimos.robot.unitree.go2.sim2:go2_drives ``--from`` names a callable returning ``{joint: (kp, kd, target)}``. It lives with the robot, -because which gains a robot runs on is the robot's business and not this script's; all this does -is the USD authoring, and the test beside the robot is what keeps the layer honest if the blob -it came from ever changes. +because which gains a robot runs on is the robot's business; all this does is the USD +authoring, and the test beside the robot keeps the layer honest if the blob ever changes. """ from __future__ import annotations From 4d8f8aaa7af475d013f7bcb479312bb84d18e540 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 9 Nov 2026 14:16:06 -0800 Subject: [PATCH 14/19] sim2_go2_l1: merge up, delete comments and docstrings --- dimos/memory2/db_tf.py | 11 ---- .../blueprints/basic/unitree_go2_sim2_l1.py | 25 -------- dimos/robot/unitree/go2/sim2.py | 23 ------- dimos/robot/unitree/go2/test_usd.py | 6 -- dimos/sim2/assets/logistics.py | 9 --- dimos/sim2/l1.py | 22 ------- dimos/sim2/test_sim_time.py | 26 -------- scripts/mjcf_to_usd.py | 63 ------------------- scripts/usd_drives.py | 15 ----- 9 files changed, 200 deletions(-) diff --git a/dimos/memory2/db_tf.py b/dimos/memory2/db_tf.py index 4ff516d63e8..b590dee85d6 100644 --- a/dimos/memory2/db_tf.py +++ b/dimos/memory2/db_tf.py @@ -12,15 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Compatibility stub for the removed ``dimos.memory2.db_tf`` module. - -Some older recordings register a ``tf_graph`` stream whose payload type is -``dimos.memory2.db_tf.TfGraph``. That module is gone, so any tool resolving every stream's -payload type up front (``dimos map summary``, ``dimos map replay``) fails to import the whole -recording with ``ModuleNotFoundError``. The ``tf_graph`` stream is irrelevant for mapping, so -this is a do-nothing placeholder. Iterating or decoding a ``tf_graph`` stream through it is not -supported. -""" from __future__ import annotations @@ -28,7 +19,5 @@ class TfGraph: - """Inert stand-in for the old ``db_tf.TfGraph`` payload type.""" - def __init__(self, *_args: Any, **_kwargs: Any) -> None: pass diff --git a/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py b/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py index 18a8e985cef..697e0c9eb9a 100644 --- a/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py +++ b/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_sim2_l1.py @@ -12,14 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""unitree-go2-basic in sim2: the walking Go2 in the logistics room, its head L1 and front -camera. - -Two managers, nested, which is the shape every one of these has. The inner one answers for the -*robot*: the Go2's own layer with each sensor grafted onto the frame it is bolted to. The outer -one answers for the *world*: the room, with the robot grafted onto a spot on its floor. What -that outer one composes is what the physics runs and what every sensor renders from. -""" from dimos.core.coordination.blueprints import autoconnect from dimos.core.global_config import global_config @@ -35,28 +27,16 @@ from dimos.simulation.go2_legged.robot import CONTROL_DT from dimos.visualization.vis_module import vis_module -# What the world calls this robot: the entity id its actions are addressed to, and the prefix on -# every frame of it. ROBOT = "go2" -# Where it stands, said once. The scene's mount frame is the one statement of it: a second xyz -# on the description could only disagree with the room. SPOT = (0.0, 0.0, 0.35) -# The namespace the scene's manager lives in, and so the name everything that asks it uses. A -# manager outside every namespace would answer on the same unnamespaced topics as the robot's. SCENE = "scene" SCENE_MANAGER = f"{SCENE}/{UsdManager.name}" -# The room, with a spot on its floor for the robot. A mounts dict is keyed by frame, and the -# room arrives with frames for its own furniture and none for anything standing in it. _room = UsdModel.from_file(LOGISTICS).with_frames({ROBOT: ("logistics", SPOT, (0.0, 0.0, 0.0))}) -# The sensors as their own blueprints, because the manager mounts them by the instance each one -# deploys: a frame_id alone would name a module the manager cannot address. _lidar = SimL1.blueprint(frame_id=Go2Sim.l1_mount_frame, scene_manager=SCENE_MANAGER) _camera = SimCamera.blueprint(frame_id=Go2Sim.camera_mount_frame, scene_manager=SCENE_MANAGER) -# One manager holding the whole robot: the Go2's own layer, with each sensor grafted onto the -# frame it is bolted to. Without the mounts the lidar and camera are nowhere in the stage. _robot = UsdManager.blueprint( model=Go2Sim.usd, mounts={Go2Sim.l1_mount_frame: _lidar, Go2Sim.camera_mount_frame: _camera} ).remappings([(UsdManager, "coordinator_joint_state", "joint_state")]) @@ -64,18 +44,13 @@ _scene = UsdManager.blueprint( model=_room, mounts={ROBOT: _robot}, - # It is a scene, not a robot: its frames are the world's and take no prefix. frame_id_prefix="", - # The room's root link is "logistics" and tf has never heard of it: the robot publishes - # world -> go2/base, so "world" is what everything else is placed against. tf_renames={"logistics": "world"}, ).namespace(SCENE, expose={"render_file_request", "file_info", "tf"}) unitree_go2_sim2_l1 = autoconnect( sim_module( scene_manager=SCENE_MANAGER, - # The one thing a layer cannot say: what the world calls this robot, and how often its - # controller acts. Its gains and the pose it holds are in the layer. robots=[SimulatedRobot(ROBOT, Go2Sim.usd, control_period=CONTROL_DT)], ), vis_module(viewer_backend=global_config.viewer, rerun_config=rerun_config), diff --git a/dimos/robot/unitree/go2/sim2.py b/dimos/robot/unitree/go2/sim2.py index 1816aa7ede8..1fe3193fd5c 100644 --- a/dimos/robot/unitree/go2/sim2.py +++ b/dimos/robot/unitree/go2/sim2.py @@ -12,14 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""A simulated Go2 with GO2Connection's cmd_vel/odom/tf: the walking policy, observing -world_observations and sending joint commands. - -Go2Sim.usd is the committed Go2 layer with the rig's sensor frames added: a sensor names one as -its frame_id, and the blueprint hands the same layer to a UsdManager, which poses those frames -on tf, and to `sim_module`, which runs them. The layer carries the policy's gains and its home -pose as drives (see `go2_drives`). -""" import asyncio from collections.abc import AsyncIterator @@ -56,13 +48,6 @@ def go2_drives() -> dict[str, tuple[float, float, float]]: - """The gains and home pose the walking policy runs on, as `usd_with_drives` wants them. - - Read out of the policy blob, which carries its own kp, kd and default pose: the gains that hold - this robot up are the controller's, not the vendor's. Called by `scripts/usd_drives.py` to - write them into the committed layer, and by the test that keeps the layer honest -- never at - runtime, because by then the layer is what says it. - """ policy = load_policy(None) return { joint: (float(kp), float(kd), float(q)) @@ -76,14 +61,10 @@ class Go2SimConfig(ModuleConfig): robot_id: str = "go2" policy: Path | None = None odom_frame_id: str = "world" - # Yaw rate commanded per radian of heading error while no turn is commanded. heading_gain: float = 2.0 class Go2Sim(Module): - # The committed layer, converted once from the vendor MJCF (see assets/go2.usda), carrying the - # real rig's frames on its base. The mounts are added here rather than baked in because a mount - # is whose robot carries it, not what the robot is. usd = UsdModel.from_file(Path(__file__).parent / "assets" / "go2.usda").with_frames( { "utlidar_lidar": ( @@ -106,7 +87,6 @@ class Go2Sim(Module): actions: Out[WorldAction] odom: Out[PoseStamped] tf: Out[TFMessage] - # Named as the Go2's URDF names them, for UsdManager. joint_state: Out[JointState] def __init__(self, **kwargs: Any) -> None: @@ -121,7 +101,6 @@ async def main(self) -> AsyncIterator[None]: self._world = WorldMirror() loop = asyncio.get_running_loop() - # The world holds every CONTROL_DT of sim time until the policy has acted, as in training. def on_frame(world: WorldMirror) -> None: if world.got(self.config.robot_id, ACT): loop.call_soon_threadsafe(self._act) @@ -146,7 +125,6 @@ def _act(self) -> None: quat = np.array([w, x, y, z]) desired = self._desired if time.monotonic() < self._desired_expires else np.zeros(3) self._command += np.clip(desired - self._command, -COMMAND_SLEW, COMMAND_SLEW) - # The bundled policy drifts in yaw even standing still; with no turn commanded, hold heading. if desired[2] == 0: self._heading = yaw_of(quat) if self._heading is None else self._heading turn = self.config.heading_gain * math.remainder(self._heading - yaw_of(quat), math.tau) @@ -154,7 +132,6 @@ def _act(self) -> None: self._heading, turn = None, 0.0 joints = {name: row for name, row in zip(names, rows, strict=True)} observation = Proprioception( - # The world's angular velocity is world-aligned; the policy's gyro reads in the base's frame. angular_velocity=rotation_matrix(quat).T @ base.velocity[3:], gravity=projected_gravity(quat), joint_position=np.array([joints[name][0] for name in policy.joint_names]), diff --git a/dimos/robot/unitree/go2/test_usd.py b/dimos/robot/unitree/go2/test_usd.py index 639578f7b5f..c68ee396e6a 100644 --- a/dimos/robot/unitree/go2/test_usd.py +++ b/dimos/robot/unitree/go2/test_usd.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""The committed Go2 layer says what the Go2 is, so this is what holds it to that.""" import pytest @@ -20,10 +19,6 @@ def test_the_layer_carries_the_gains_the_policy_runs_on() -> None: - """The gains are in the layer, written there once by `scripts/usd_drives.py`, so they can go - stale if somebody replaces the policy blob. This is what notices; run the script again if it - fails. - """ from dimos.robot.assets.usd import usd_drives from dimos.robot.unitree.go2.sim2 import Go2Sim, go2_drives @@ -33,7 +28,6 @@ def test_the_layer_carries_the_gains_the_policy_runs_on() -> None: def test_every_sensor_mount_the_rig_has_is_a_frame_of_the_layer() -> None: - """A sensor names one of these as its frame_id, and a manager grafts it onto that frame.""" from dimos.robot.unitree.go2.sim2 import Go2Sim frames = Go2Sim.usd.frame_names() diff --git a/dimos/sim2/assets/logistics.py b/dimos/sim2/assets/logistics.py index 38918af1e00..30d0e8d83c5 100644 --- a/dimos/sim2/assets/logistics.py +++ b/dimos/sim2/assets/logistics.py @@ -12,15 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""The room a sim2 blueprint runs in when nobody has a real scene to hand: walls, shelves, a -table. - -USD, converted once from the MJCF it used to be and committed beside this file. Its lighting is -written in by hand, because the converter drops lights outright ("lights are not supported"). - - MuJoCo: 1 plane, 8 boxes, no joints and nothing that moves - dimos: root link "logistics", which is the frame a visitor hangs off -""" from pathlib import Path diff --git a/dimos/sim2/l1.py b/dimos/sim2/l1.py index f1faa6ec1f2..860bc81170b 100644 --- a/dimos/sim2/l1.py +++ b/dimos/sim2/l1.py @@ -12,12 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""The Go2's head L1 as GO2Connection's `lidar` sees it: the robot's rolling occupancy map, not a scan. - -Measured on real recordings: 5 cm world-anchored cells, the whole map resent every 0.13 s, -cropped to a world-axis-aligned 128x128-cell square around the robot. Cells persist while -inside the square; the real map's flicker comes from sensor noise this sim does not have. -""" import asyncio from collections.abc import AsyncIterator @@ -39,15 +33,12 @@ from dimos.sim2.world_observations import WorldMirror from dimos.simulation.sensors.lidar import Raycaster -# Cell keys pack three offset 21-bit indices into one int64. _KEY_BITS = 21 _KEY_OFFSET = 1 << (_KEY_BITS - 1) class SimL1Config(SceneConfig): - # The L1's frame, as the robot's description names it: where it sits. frame_id: str = "utlidar_lidar" - # The rolling map is published in this frame. map_frame_id: str = "world" period: float = 0.13 points_per_second: float = 21_600.0 @@ -66,9 +57,6 @@ class SimL1(Module): @rpc def get_usd(self) -> list[str]: - """``[asset_root, usd]``: the frame this L1 scans from. A frame rather than a case: the Go2's - utlidar sits under a cover that is part of the robot's own shell. - """ return frame_usd(self.frame_id, name="sim_l1") async def main(self) -> AsyncIterator[None]: @@ -79,10 +67,6 @@ async def main(self) -> AsyncIterator[None]: self._mapping.cancel() def _open(self) -> WorldView: - """This L1's own world: the composed scene, with a site at its own frame. Opened on the first - frame rather than at start, because the robots in the world are the ones the observations - announce, and the view has to name its bodies the way they do. - """ scene = scene_from(self.config.scene_manager, self.rpc, self.config.scene) if scene is None: raise ValueError( @@ -103,12 +87,10 @@ async def _map(self) -> None: rng = np.random.default_rng(config.seed) rays = int(config.points_per_second * config.period) cells = np.zeros(0, dtype=np.int64) - # Every period of sim time, so the map keeps its rate per sim second at any speed. async for _ in self._world.ticks(1 / config.period): if view is None: view = self._open() raycaster = view.raycaster(config.geom_groups) - # The map is centred on the robot the L1 rides, which its own frame names. rider = view.owner(site) assert raycaster is not None view.update(self._world) @@ -127,9 +109,6 @@ async def _map(self) -> None: ) def _center(self, rider: str | None, origin: NDArray[np.float64]) -> NDArray[np.float64]: - """Where the square is cropped around: the robot carrying this L1, or the L1 itself. Read off - the observations rather than out of the view, because that is where the pose comes from. - """ entity = self._world.entity(rider) if rider is not None else None return origin if entity is None or entity.pose is None else entity.pose[:3] @@ -157,7 +136,6 @@ def _crop(self, keys: NDArray[np.int64], center: NDArray[np.float64]) -> NDArray def _hemisphere(rng: np.random.Generator, count: int) -> NDArray[np.float64]: - """Uniform directions over the sensor's +z hemisphere: 360 degrees around, 90 up.""" z = rng.uniform(0.0, 1.0, count) azimuth = rng.uniform(0.0, 2 * math.pi, count) ring = np.sqrt(1 - z**2) diff --git a/dimos/sim2/test_sim_time.py b/dimos/sim2/test_sim_time.py index dda8cdb4c51..d379f8bd382 100644 --- a/dimos/sim2/test_sim_time.py +++ b/dimos/sim2/test_sim_time.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Sensors keep their rates per sim second whether the world runs slower or faster than real.""" import asyncio from collections.abc import AsyncIterator @@ -57,31 +56,20 @@ } -# Where the scene's manager lives, and so the name everything that needs the world asks. SCENE_MANAGER = f"scene/{UsdManager.name}" -# Every sensor sits at a frame of the dog's model. FRAMES = {"eye": "head", "imu_link": "base", "livox": "head", "l1": "base"} @pytest.fixture def robot_model() -> UsdModel: - """The dog: a box on a free joint with a hinged head, a driven neck, and a frame per sensor.""" model = toy_robot().with_frames( {frame: (body, (0.0, 0.0, 0.0), (0.0, 0.0, 0.0)) for frame, body in FRAMES.items()} ) - # Its gains, where a robot's gains live: in its layer. Small ones, because the head is a - # 0.1 kg sphere at the joint and its inertia about the neck is 1e-4: kd beyond about 0.01 - # is stiffer than a 5 ms step can integrate, and the whole robot leaves the room. return UsdModel.from_layer(usd_with_drives(model.usd, {"neck": (0.01, 0.001, 0.0)})) @pytest.fixture def composed(robot_model: UsdModel) -> UsdModel: - """The world as a manager composes it: the room, with the dog grafted onto a spot on its - floor. One layer, one world, so the furniture a scan comes back off is the one that is drawn. - """ - # The toy's layer puts its base at (9, 9, 9), so the spot it stands on takes that back out: - # a mount frame places a robot by its own layer's origin, not by where its root happens to be. room = UsdModel.from_file(LOGISTICS).with_frames( {"dog": ("logistics", (-9.0, -9.0, -8.7), (0.0, 0.0, 0.0))} ) @@ -91,14 +79,11 @@ def composed(robot_model: UsdModel) -> UsdModel: class _DogConfig(ModuleConfig): - # With a control period, the world waits this long (wall seconds) for each of its commands. control_period: float | None = None think: float = 0.0 class _Dog(Module): - """A robot that, in lockstep, is slow to answer: the world runs slower than real waiting for it.""" - config: _DogConfig world_observations: In[WorldObservations] actions: Out[WorldAction] @@ -163,7 +148,6 @@ def _run( robots=[SimulatedRobot("dog", robot_model, control_period=dog.get("control_period"))], ), _Dog.blueprint(**dog), - # The one manager, answering for the whole world. Everything else asks it. UsdManager.blueprint( model=composed, frame_id_prefix="", tf_renames={"logistics": "world"} ).namespace("scene", expose={"render_file_request", "file_info", "tf"}), @@ -185,16 +169,12 @@ def _run( [(SimL1, "lidar", "l1_lidar")] ), _Readings.blueprint(), - # A worker each, as a robot's blueprint gives its sensors: one sharing a busy loop falls behind at 4x. ).global_config(n_workers=9) - # Its own zenoh domain: another world running on this machine would otherwise share its topics. overrides = {"g": {"viewer": "none", "zenoh_domain": f"sim2-test-{uuid.uuid4().hex[:8]}"}} parsed = BlueprintConfigParser(blueprint).parse(environ={}, overrides=overrides) coordinator = ModuleCoordinator.build(blueprint, parsed) try: readings = coordinator.get_instance(_Readings) - # At least `seconds` of wall time, then until every sensor has steady stamps: a sensor's first - # point cloud (it imports open3d) can take most of that on a loaded machine. deadline = time.monotonic() + seconds + MAX_EXTRA_WALL time.sleep(seconds) while time.monotonic() < deadline and not _steady(readings.readings()["stamps"]): @@ -204,7 +184,6 @@ def _run( coordinator.stop() -# Wall seconds a run may go on past its own, waiting for every sensor to be steady. MAX_EXTRA_WALL = 60.0 STEADY_STAMPS = 8 @@ -218,12 +197,10 @@ def _steady(stamps: dict[str, list[float]]) -> bool: ) -# Sim seconds at the start in which a sensor may stall once, warming up (its first point cloud imports open3d). WARM_UP = 1.0 def _per_sim_second(stamps: list[float], start: float) -> float: - # Stamps are the wall time the sim started at plus its sim time, so their spacing is sim time. steady = [stamp for stamp in stamps if stamp >= start + WARM_UP] assert len(steady) > 5, f"only {len(steady)} after warming up" return (len(steady) - 1) / (steady[-1] - steady[0]) @@ -234,7 +211,6 @@ def _per_sim_second(stamps: list[float], start: float) -> float: [ (0.5, {}), (4.0, {}), - # Lockstep with a robot that takes 50 ms (wall) to answer each 20 ms (sim) tick. (1.0, {"control_period": 0.02, "think": 0.05}), ], ids=["half_speed", "four_times_speed", "slower_than_real_lockstep"], @@ -250,8 +226,6 @@ def test_sensors_keep_their_rates_per_sim_second(composed, robot_model, speed, d measured = _per_sim_second(stamps[name], start) assert measured == pytest.approx(rate, rel=0.1), (name, measured) if dog: - # The world waited on the robot: well slower than real. assert sim_seconds < 0.7 * readings["wall"] - # At rest on the floor (after it settles), the IMU feels the floor push up against gravity. settled = readings["accelerations"][len(readings["accelerations"]) // 2 :] assert np.mean(settled) == pytest.approx(9.81, abs=0.3) diff --git a/scripts/mjcf_to_usd.py b/scripts/mjcf_to_usd.py index 8cab475d1a9..9067cc033b4 100755 --- a/scripts/mjcf_to_usd.py +++ b/scripts/mjcf_to_usd.py @@ -67,24 +67,12 @@ logger = setup_logger() -# The geometry a referenced mesh file supplies for itself. _BAKED = frozenset({"points", "faceVertexIndices", "faceVertexCounts", "normals", "extent"}) -# The two primvars that are the mesh's appearance rather than its geometry. One value per prim, -# not per vertex, so keeping them costs nothing -- and dropping them left every piece of -# office_555's furniture at MuJoCo's default grey, because the MJCF gives those geoms an rgba -# the exporter writes here and the sidecar does not carry. _KEPT_PRIMVARS = frozenset({"primvars:displayColor", "primvars:displayOpacity"}) -# What a sidecar is written as: binary, because nothing hands these around as text -- they are -# fetched as bytes, one file per mesh, exactly like the STL beside them. _SIDECAR_SUFFIX = ".usdc" def write_sidecar(source: Path, pxr: Any) -> Path: - """``source`` as a one-prim USD layer beside it, so a reference to it is valid USD. - - Rewritten every run rather than cached: a stale sidecar is a robot whose geometry silently - does not match its meshes. - """ import trimesh out = source.with_suffix(_SIDECAR_SUFFIX) @@ -92,9 +80,6 @@ def write_sidecar(source: Path, pxr: Any) -> Path: if isinstance(loaded, trimesh.Scene): loaded = loaded.to_geometry() if len(loaded.faces) == 0: - # Refusing beats writing an empty sidecar. A git-lfs **pointer** parses as a mesh with no - # faces, so running this against an unmaterialised worktree produced 427 identical 737-byte - # layers and the scene died much later inside MuJoCo as "at least 4 vertices required". raise ValueError( f"{source} has no faces. If it is {source.stat().st_size} bytes it is a git-lfs " f"pointer: run `git lfs checkout` on its directory first." @@ -105,8 +90,6 @@ def write_sidecar(source: Path, pxr: Any) -> Path: mesh.CreatePointsAttr([pxr.Gf.Vec3f(*point) for point in loaded.vertices.tolist()]) mesh.CreateFaceVertexIndicesAttr([int(i) for i in loaded.faces.reshape(-1).tolist()]) mesh.CreateFaceVertexCountsAttr([3] * len(loaded.faces)) - # Without an extent a consumer has to load every point to frame the robot. A degenerate - # mesh has no bounds to state, and saying nothing is better than saying something wrong. bounds = loaded.bounds if bounds is not None: mesh.CreateExtentAttr( @@ -118,19 +101,6 @@ def write_sidecar(source: Path, pxr: Any) -> Path: def mesh_files(mjcf: Path) -> dict[str, str]: - """Every name a baked prim might carry -> the mesh file behind it. - - Three spellings, because the converter names a mesh prim after whichever the MJCF gave it: - - * the **mesh asset's** own name, which is what a robot MJCF uses; - * the **file's stem**, for a `` with no name -- MuJoCo's rule, and how the Go2 - declares all sixteen of its meshes. Reading only the explicit spelling found one; - * the **geom's** name, which is what a scene MJCF uses (office_555 names every geom and has - them reference meshes called `asset_0`, `asset_1`, ...). - - Without the geom spelling the office scene re-points nothing: 3547 meshes stay baked and the - layer comes out 35 MB. - """ root = ET.fromstring(mjcf.read_text()) files = {} for mesh in root.iter("mesh"): @@ -146,10 +116,6 @@ def mesh_files(mjcf: Path) -> dict[str, str]: def _mesh_of(prim_name: str, files: dict[str, str]) -> str | None: - """The mesh file a prim's name refers to, allowing for the converter's de-duplication: two - geoms on one body using the same mesh become two prims, the second named ``_1``. Without - this every such prim stays baked, which on the G1 is 23 of 58. - """ if prim_name in files: return files[prim_name] stripped = re.sub(r"_\d+$", "", prim_name) @@ -159,13 +125,6 @@ def _mesh_of(prim_name: str, files: dict[str, str]) -> str | None: def rereference( stage: Any, files: dict[str, str], meshes: Path, out_dir: Path, pxr: Any ) -> tuple[int, int]: - """Swap each baked mesh for a reference to its file, with the file's hash beside it. Returns - how many were re-pointed and how many were left alone. - - A mesh the MJCF does not name is left baked rather than dropped: a hole in the robot is worse - than a large layer. References are written **relative to the layer**, because the layer is - committed and the path it was converted at means nothing on anyone else's machine. - """ done = skipped = 0 for prim in list(stage.Traverse()): if not prim.IsA(pxr.UsdGeom.Mesh): @@ -175,8 +134,6 @@ def rereference( logger.warning(f"{prim.GetPath()} has no mesh of that name in the MJCF; left baked") skipped += 1 continue - # --meshes is authoritative, so a `file=` carrying a directory is matched by its basename - # rather than pasted onto the directory a second time. path = next( ( candidate @@ -189,9 +146,6 @@ def rereference( logger.warning(f"no {file} under {meshes}; {prim.GetPath()} left baked") skipped += 1 continue - # Everything per-vertex goes: the reference supplies it, and it is what made the layer large. - # primvars included -- the converter bakes normals there. Transform ops, material bindings and - # the mesh's own colour stay, since the file carries none of them. for property in list(prim.GetPropertyNames()): if property.startswith(("xformOp", "material:")) or property in _KEPT_PRIMVARS: continue @@ -200,7 +154,6 @@ def rereference( sidecar = write_sidecar(path, pxr) relative = os.path.relpath(path, out_dir) prim.GetReferences().ClearReferences() - # The sidecar, not the mesh file: a reference composes a USD layer and nothing else. prim.GetReferences().AddReference(os.path.relpath(sidecar, out_dir)) digest = hash_of_file(str(path)) info = prim.GetAssetInfo() or {} @@ -213,15 +166,6 @@ def rereference( def name_joints(stage: Any, pxr: Any) -> int: - """Give every unnamed joint prim the name of the body it moves. Returns how many. - - The converter leaves a joint it was not given a name for as the bare schema name, so a scene of - loose furniture comes out with **every** weld called `PhysicsFixedJoint` -- office_555 has 448 - of them. Legal USD, but useless as a frame source: `usd_frame_names` returns one name for 448 - edges, and a URDF converted from it has 448 joints of the same name and will not load. - - `_joint` matches what the robot layers already carry (`left_ankle_roll_joint`). - """ renamed = 0 for prim in list(stage.Traverse()): if not prim.IsA(pxr.UsdPhysics.Joint) or prim.GetName() != prim.GetTypeName(): @@ -264,13 +208,8 @@ def main() -> int: files = mesh_files(args.mjcf) - # Flattened, because the converter writes a root that payloads several binary layers and a - # scene has to leave here as one text layer. stage = Usd.Stage.Open(str(args.converted)) flat = Usd.Stage.Open(stage.Flatten()) - # A model of boxes and spheres names no meshes, which is fine. A stage full of them whose MJCF - # names none is the --mjcf being the wrong file, and re-pointing nothing would quietly write a - # layer of baked geometry. baked = any(prim.IsA(UsdGeom.Mesh) for prim in flat.Traverse()) if baked and not files: print( @@ -283,8 +222,6 @@ def main() -> int: named = name_joints(flat, pxr) done, skipped = rereference(flat, files, args.meshes, args.out.parent.resolve(), pxr) - # The ROOT LAYER, not the stage: Stage.Export() flattens composition, which resolves the - # reference arcs we just authored back into baked geometry -- the opposite of the point. flat.GetRootLayer().Export(str(args.out)) size = args.out.stat().st_size print(f"{done} mesh(es) re-pointed with a .usdc sidecar, {skipped} left baked") diff --git a/scripts/usd_drives.py b/scripts/usd_drives.py index 6a6ea37072f..530e64cedfe 100644 --- a/scripts/usd_drives.py +++ b/scripts/usd_drives.py @@ -12,21 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Write a robot's gains and home pose into its committed layer. Run once by hand. - -A robot's gains are a fact about the robot, so they belong in the layer that describes it rather -than in whatever happens to be running it. For a robot whose controller ships as a blob -- the -Go2's walking policy carries its own kp, kd and default pose -- that means reading them out of -the blob once and committing the result:: - - python scripts/usd_drives.py \\ - --layer dimos/robot/unitree/go2/assets/go2.usda \\ - --from dimos.robot.unitree.go2.sim2:go2_drives - -``--from`` names a callable returning ``{joint: (kp, kd, target)}``. It lives with the robot, -because which gains a robot runs on is the robot's business; all this does is the USD -authoring, and the test beside the robot keeps the layer honest if the blob ever changes. -""" from __future__ import annotations From 214993374349aaee650a0e1a40206afefb8dc742 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 9 Nov 2026 14:34:02 -0800 Subject: [PATCH 15/19] sim2_go2_l1: the L1 can see the room, and cmd_vel expires on the world's clock SimL1 defaulted to geom group 0 alone. That is the default the Livox's own config records as the one that made the beam pass through every desk, chair and robot -- and a converted scene puts collision geometry in group 3: office_555 has no group-0 geoms at all. So the L1 published an empty map every 0.13 s and nothing said why. It takes (0, 3), as the Livox does. handle_cmd_vel set its deadline from time.monotonic and _act compared against it, inside a world where every other rate paces on world.stamp(). At speed=4 a command expired after a quarter of CMD_VEL_TIMEOUT of sim time. Both ends use the world's clock now, and `time` is no longer imported here. --- dimos/robot/unitree/go2/sim2.py | 7 ++++--- dimos/sim2/l1.py | 6 +++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/dimos/robot/unitree/go2/sim2.py b/dimos/robot/unitree/go2/sim2.py index 1fe3193fd5c..c1ad1598a74 100644 --- a/dimos/robot/unitree/go2/sim2.py +++ b/dimos/robot/unitree/go2/sim2.py @@ -17,7 +17,6 @@ from collections.abc import AsyncIterator import math from pathlib import Path -import time from typing import Any from dimos_lcm.world_msgs import WorldAction, WorldObservations @@ -111,7 +110,9 @@ def on_frame(world: WorldMirror) -> None: async def handle_cmd_vel(self, twist: Twist) -> None: self._desired = np.array([twist.linear.x, twist.linear.y, twist.angular.z]) - self._desired_expires = time.monotonic() + CMD_VEL_TIMEOUT + # Sim time, like every other rate here: on wall time a command expires in a quarter of + # its intended interval at speed=4, and the robot forgets what it was told. + self._desired_expires = self._world.stamp() + CMD_VEL_TIMEOUT def _act(self) -> None: policy, world = self._policy, self._world @@ -123,7 +124,7 @@ def _act(self) -> None: return x, y, z, w = base.pose[3:] quat = np.array([w, x, y, z]) - desired = self._desired if time.monotonic() < self._desired_expires else np.zeros(3) + desired = self._desired if stamp < self._desired_expires else np.zeros(3) self._command += np.clip(desired - self._command, -COMMAND_SLEW, COMMAND_SLEW) if desired[2] == 0: self._heading = yaw_of(quat) if self._heading is None else self._heading diff --git a/dimos/sim2/l1.py b/dimos/sim2/l1.py index 860bc81170b..c3a88825090 100644 --- a/dimos/sim2/l1.py +++ b/dimos/sim2/l1.py @@ -46,7 +46,11 @@ class SimL1Config(SceneConfig): cell: float = 0.05 half_width_cells: int = 64 z_range: tuple[float, float] = (-0.6, 1.3) - geom_groups: tuple[int, ...] = (0,) + # 0 and 3 as the Livox has it: group 0 alone is the old default, and it made the beam pass + # through every desk, chair and robot. A converted scene puts collision geometry in 3 -- + # office_555 has no group-0 geoms at all, so an L1 looking only at 0 maps nothing, forever, + # without an error. + geom_groups: tuple[int, ...] = (0, 3) seed: int = 0 From b268f300b9e5ad5771be765d859f7661662123fe Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 9 Nov 2026 14:57:48 -0800 Subject: [PATCH 16/19] sim2_go2_l1: merge up, delete comments and docstrings --- dimos/robot/unitree/go2/sim2.py | 2 -- dimos/sim2/l1.py | 4 ---- 2 files changed, 6 deletions(-) diff --git a/dimos/robot/unitree/go2/sim2.py b/dimos/robot/unitree/go2/sim2.py index c1ad1598a74..e83bfea82f4 100644 --- a/dimos/robot/unitree/go2/sim2.py +++ b/dimos/robot/unitree/go2/sim2.py @@ -110,8 +110,6 @@ def on_frame(world: WorldMirror) -> None: async def handle_cmd_vel(self, twist: Twist) -> None: self._desired = np.array([twist.linear.x, twist.linear.y, twist.angular.z]) - # Sim time, like every other rate here: on wall time a command expires in a quarter of - # its intended interval at speed=4, and the robot forgets what it was told. self._desired_expires = self._world.stamp() + CMD_VEL_TIMEOUT def _act(self) -> None: diff --git a/dimos/sim2/l1.py b/dimos/sim2/l1.py index c3a88825090..01bc00b78b8 100644 --- a/dimos/sim2/l1.py +++ b/dimos/sim2/l1.py @@ -46,10 +46,6 @@ class SimL1Config(SceneConfig): cell: float = 0.05 half_width_cells: int = 64 z_range: tuple[float, float] = (-0.6, 1.3) - # 0 and 3 as the Livox has it: group 0 alone is the old default, and it made the beam pass - # through every desk, chair and robot. A converted scene puts collision geometry in 3 -- - # office_555 has no group-0 geoms at all, so an L1 looking only at 0 maps nothing, forever, - # without an error. geom_groups: tuple[int, ...] = (0, 3) seed: int = 0 From abed68d247b15be30867d0848b35d992815ee6f2 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 9 Nov 2026 18:32:33 -0800 Subject: [PATCH 17/19] gateway: annotate the sim2 Go2 with its L1 --- experimental/gateway/annotations.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/experimental/gateway/annotations.yaml b/experimental/gateway/annotations.yaml index 82286561ba6..52ec9f88bd9 100644 --- a/experimental/gateway/annotations.yaml +++ b/experimental/gateway/annotations.yaml @@ -40,6 +40,10 @@ robots: title: Go2 basic description: 'Connects to a Go2 and streams its camera, lidar and pose. The simplest way to get a Go2 going: drive it from Controller.' starter: 1 + unitree-go2-sim2-l1: + title: Go2 in sim2 with its L1 lidar + description: 'A simulated Go2 in sim2, scanning with its own L1: camera, lidar and pose, no robot needed.' + recommended_config: [] unitree-go2: title: Go2 with map + navigation description: Builds a 3D map and a costmap as it walks; click a spot to send it there, or let it explore on its own. From cb127553ccd618d38b4c897a14a75a7017cfa579 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Tue, 10 Nov 2026 15:28:28 -0800 Subject: [PATCH 18/19] sim2 timing tests: skip where there is no usd-core --- dimos/sim2/test_sim_time.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dimos/sim2/test_sim_time.py b/dimos/sim2/test_sim_time.py index d379f8bd382..ae84eadf6d2 100644 --- a/dimos/sim2/test_sim_time.py +++ b/dimos/sim2/test_sim_time.py @@ -46,6 +46,12 @@ from dimos.sim2.spec import SimulatedRobot from dimos.sim2.world_observations import ACT, WorldMirror +# Every scene here is read out of a USD layer, and a runner without usd-core cannot read one: +# linux-aarch64 has no wheel for it (pyproject's marker says so), so without this the tests +# fail there on dimos's own "Reading or writing USD needs usd-core (pxr)". Their neighbours +# in this directory skip the same way. +pytest.importorskip("pxr") + L1_PERIOD = 0.13 RATES = { "color_image": 10.0, From 8fefaebb5c0fe63944d9a969067c476a5c2ef1e8 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Tue, 10 Nov 2026 19:03:56 -0800 Subject: [PATCH 19/19] a test world and a real one must not answer to the same names Measured, with three-g1-office-555 running on this machine: the live world logged `the world stopped: KeyError(... is neither a file this description names nor an asset the last scene published)` twice while this file's tests ran, each time right after a second `starting the world` in its own log. Two names collide. `SCENE_MANAGER` here was `scene/usdmanager`, which is what that blueprint's `SCENE_NAMESPACE = "scene"` makes of its own scene manager, and an unnamespaced `sim_module()` is `mujocosim` on both sides. The per-run `zenoh_domain` this already took does not help: a domain prefixes key expressions, so it keeps the *streams* apart, while an RPC goes out on the instance name. So the office's sensors asked this test's manager for a scene -- whose assets are published under different names -- and this test's teardown stopped the office's world. Only those two needed it. Every sensor in that blueprint is inside a robot's `.namespace(name)`, so it is `main/livoxsim` and never `livoxsim`; renaming the sensors here as well was tried and reverted, since it changed nothing. No behaviour change: `test_sim_time` has the same three failures with and without this, on a quiet box and on a busy one. They are `color_image: 0`, which reproduces with this file untouched on this branch and is a separate matter. --- dimos/sim2/test_sim_time.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/dimos/sim2/test_sim_time.py b/dimos/sim2/test_sim_time.py index ae84eadf6d2..ac7643cb14b 100644 --- a/dimos/sim2/test_sim_time.py +++ b/dimos/sim2/test_sim_time.py @@ -62,7 +62,20 @@ } -SCENE_MANAGER = f"scene/{UsdManager.name}" +# The scene manager and the world are named for this run alone. A zenoh domain is not enough: it +# prefixes key expressions, so it keeps the streams apart, while an RPC goes out on the *instance +# name*. `scene/usdmanager` and an unnamespaced `sim_module()` are exactly what three-g1-office-555 +# calls its own, so with that sim up the two worlds answered each other: this test's sensors asked +# the office's manager for a scene, the office's world asked this one, and since the two publish +# different asset names the live world died on a get_asset KeyError. Its teardown then stopped the +# office's world module too, which is the same name. The sensors need no such treatment -- every +# one of them is inside a robot's namespace over there. +def _names() -> tuple[str, str]: + """A unique scene namespace for this run, and the name its manager answers RPCs on.""" + scene = f"scene{uuid.uuid4().hex[:8]}" + return scene, f"{scene}/{UsdManager.name}" + + FRAMES = {"eye": "head", "imu_link": "base", "livox": "head", "l1": "base"} @@ -147,31 +160,34 @@ def _run( speed: float = 1.0, **dog: Any, ) -> dict[str, Any]: + run = uuid.uuid4().hex[:8] + scene, scene_manager = _names() blueprint = autoconnect( sim_module( - scene_manager=SCENE_MANAGER, + scene_manager=scene_manager, speed=speed, robots=[SimulatedRobot("dog", robot_model, control_period=dog.get("control_period"))], + instance_name=f"sim{run}", ), _Dog.blueprint(**dog), UsdManager.blueprint( model=composed, frame_id_prefix="", tf_renames={"logistics": "world"} - ).namespace("scene", expose={"render_file_request", "file_info", "tf"}), + ).namespace(scene, expose={"render_file_request", "file_info", "tf"}), SimCamera.blueprint( frame_id="eye", width=64, height=48, rate_hz=RATES["color_image"], - scene_manager=SCENE_MANAGER, + scene_manager=scene_manager, ), - SimImu.blueprint(frame_id="imu_link", rate_hz=RATES["imu"], scene_manager=SCENE_MANAGER), + SimImu.blueprint(frame_id="imu_link", rate_hz=RATES["imu"], scene_manager=scene_manager), LivoxSim.blueprint( frame_id="livox", rate_hz=RATES["lidar"], imu_rate_hz=RATES["livox_imu"], - scene_manager=SCENE_MANAGER, + scene_manager=scene_manager, ).remappings([(LivoxSim, "imu", "livox_imu")]), - SimL1.blueprint(frame_id="l1", period=L1_PERIOD, scene_manager=SCENE_MANAGER).remappings( + SimL1.blueprint(frame_id="l1", period=L1_PERIOD, scene_manager=scene_manager).remappings( [(SimL1, "lidar", "l1_lidar")] ), _Readings.blueprint(),