A full engineering log of integrating the LeKiwi 3-wheeled omni-directional robot into Isaac Lab โ covering USD preparation, physics debugging, and friction modeling.
The official NavDP framework ships with support for the Clearpath Dingo, a simple differential-drive robot. While a solid baseline, differential-drive kinematics are limited in real-world applications โ the robot cannot strafe, must rotate to change direction, and cannot easily execute complex maneuvers in tight spaces.
LeKiwi is a compact, 3-wheeled omni-directional robot from the LeRobot project. It features an integrated 6-DOF robotic arm and three omni-wheels arranged in a triangular configuration, allowing holonomic motion (translation in any direction + rotation simultaneously). This makes it an ideal candidate for next-generation embodied navigation research.
The full IsaacLab ArticulationCfg is defined in configs/robots/lekiwi_config.py. Key parameters:
| Parameter | Value | Notes |
|---|---|---|
| Wheel Radius | 0.0325 m | 5% reduced for collision shape (interpenetration prevention) |
| Wheel Base | 0.18 m | Front-wheel-to-front-wheel spacing |
| Wheel Joints | ST3215_Servo_Motor_v1_Revolute_64, _62 | Front two wheels are actuated |
| Wheel Damping | 5.0 | Conservative (vs Dingo's 1.0) for stability |
| Effort Limit | 20.0 Nm | Matched to Dingo for fair comparison |
| Arm Joints | 6ร STS3215_03a.* | 6-DOF arm, stiffness=40, damping=10 |
| Camera Pos | [0, 0, 0.2] | Front camera on base plate |
| Self-Collisions | Disabled | Required to prevent NaN explosions |
# configs/robots/lekiwi_config.py (key excerpt)
LEKIWI_CFG = ArticulationCfg(
prim_path = "{ENV_REGEX_NS}/Robot",
spawn=sim_utils.UsdFileCfg(
usd_path="assets/robots/lekiwi/lekiwi_final.usd",
activate_contact_sensors=True,
),
actuators={
"wheels": ImplicitActuatorCfg(
joint_names_expr=["ST3215_Servo_Motor_v1_Revolute_64",
"ST3215_Servo_Motor_v1_1_Revolute_62",
"ST3215_Servo_Motor_v1_2_Revolute_60"],
effort_limit=20.0,
velocity_limit=100.0,
stiffness=0.0, # Pure velocity control
damping=5.0, # Conservative for stability
),
"arm": ImplicitActuatorCfg(
joint_names_expr=["STS3215_03a.*"],
effort_limit=100.0, stiffness=40.0, damping=10.0,
),
},
)
Raw USD files exported from CAD tools are rarely simulation-ready. LeKiwi required a three-stage pipeline before it could be used in Isaac Lab:
fix_lekiwi_usd.py: deactivates root_joint, applies ArticulationRootAPIadd_wheel_collisions.py: injects Cylinder (drive wheels) + Sphere (caster) collision geometrylekiwi_config.py, physics-validated with check_wheel_physics.pyIntegrating a new robot into Isaac Lab is rarely plug-and-play. Below is the complete debugging chronicle, with each chapter representing a distinct failure mode discovered and resolved during the integration process.
The Issue: The robot spawned in Isaac Sim but was completely static โ it refused to fall due to gravity, respond to forces, or move at all. It was essentially a static prop glued to the origin.
The Diagnosis: The USD file exported from the CAD pipeline contained a fixed root_joint connecting the robot's base to the world frame. This is standard in URDF exports intended for static analysis (e.g., ROS visualization) but completely fatal for dynamic mobile robot simulation. The physics engine treated the robot as a world-fixed object.
The Fix: fix_lekiwi_usd.py opens the USD file, programmatically calls prim.SetActive(False) on the root_joint prim, and applies UsdPhysics.ArticulationRootAPI to the actual chassis body (/LeKiwi/base_plate_layer1_v5). The result is saved as lekiwi_floating.usd โ a true floating-base articulation.
Lesson: Always inspect the articulation tree with inspect_joints.py before attempting to simulate a new USD robot. Look for any FixedJoint connecting the robot base to the world.
The Issue: With the articulation root fixed, the robot could now fall. But the wheels fell straight through the floor. When motors were commanded at 20 rad/s, logs confirmed the joints were spinning at the requested velocity โ yet the robot didn't translate by a single millimeter.
The Diagnosis: A deep USD inspection via inspect_usd.py revealed that the visual meshes had zero collision geometry. The physics engine's broad-phase collision detection found no physics shapes, so the wheels were "ghost" objects โ they could rotate, but had no surface to push against the ground.
The Fix: add_wheel_collisions.py procedurally injects collision primitives:
/wheel_collision) under the wheel's prim pathUsdPhysics.CollisionAPI + PhysxSchema.PhysxCollisionAPIcontactOffset = 0.002, restOffset = 0.001 to prevent interpenetrationLesson: Run find_collisions.py on any new robot USD. If the count of UsdPhysics.CollisionAPI prims is zero, the robot is a ghost.
The Issue: As soon as the collision shapes were added, the simulation exploded. The console was flooded with NaN (Not a Number) errors. The robot's world position would become [NaN, NaN, NaN] within milliseconds of spawning, crashing the entire simulation loop.
The Diagnosis: Two compounding failure modes:
torch.inverse() call on a nearly-singular matrix.The Fix (multi-pronged):
PhysxArticulationAPI.CreateEnabledSelfCollisionsAttr().Set(False) โ disabled self-collisions at the articulation levelWHEEL_RADIUS * 0.95) to prevent ground interpenetration at spawntorch.inverse with torch.linalg.pinv (pseudo-inverse) for numerical robustnesstry/except blocks to catch NaN before it propagatesLesson: Always disable self-collisions when procedurally adding collision shapes. Any shape that overlaps with existing geometry at spawn will cause immediate NaN. Prefer torch.linalg.pinv over torch.inverse in robotics code.
The Issue: The robot was finally stable โ no NaN, correct physics, correct spawn height. But it refused to move correctly. The front wheels would spin and the robot would inch forward 2โ3 cm before stopping, as if dragging an invisible anchor. Turning in place was completely impossible.
The Diagnosis: LeKiwi is kinematically an omni-directional robot. The third wheel at the rear is designed as a caster โ it should roll freely in any direction to allow lateral motion. However, we had assigned all three wheels Cylinder collision shapes. A cylinder naturally resists motion perpendicular to its rolling axis. With the rear caster acting as a fixed-axis cylinder, any attempt to turn or strafe was blocked by an enormous lateral friction force.
The Fix: Changed the rear caster's collision shape from Cylinder to Sphere. A sphere contacts the ground at a single point and has no preferred rolling axis, offering near-zero resistance to lateral motion. The collision script uses is_caster = (i == 2) to select the appropriate shape per wheel index.
# add_wheel_collisions.py
for i, wheel_path in enumerate(WHEEL_BODIES):
is_caster = (i == 2) # 3rd wheel is the passive caster
add_cylinder_collision(stage, wheel_path, WHEEL_RADIUS, WHEEL_WIDTH, is_caster=is_caster)
# is_caster=True โ Sphere; is_caster=False โ Cylinder
Lesson: Understand the kinematic role of each wheel before assigning collision geometry. Casters require shapes that allow omnidirectional contact (sphere, disk with no axle friction), not shapes that enforce a preferred rolling direction.
Current State: The robot spawns correctly, sits at the correct ground height (z โ 0.033m = wheel radius), does not explode, and the drive wheels physically rotate. However, the robot's base velocity remains zero โ it is effectively doing a burnout on frictionless ice.
Evidence:
[-6.64, 14.05, 0.033] โ correct height, zero translationAction: [14.0, 16.0][0, 0, 0]Hypothesis: The procedurally-injected collision cylinders are using Isaac Sim's default material, which appears to have near-zero friction coefficients. In PhysX, a collision shape without an explicit PhysicsMaterial binding sometimes defaults to frictionless behavior โ the equivalent of polished ice. The wheels are spinning, but generating no traction force.
Next Steps:
PhysicsMaterial with staticFriction=0.8, dynamicFriction=0.6 and bind it to the drive wheel collision shapesUsdShade.MaterialBindingAPI to programmatically apply the material in the collision injection scriptPhysxSchema.PhysxMaterialAPI can be applied directly to the collision primresearch_tools/add_wheel_collisions.py.LeKiwi uses four cameras for different aspects of the evaluation pipeline:
| Camera | Variable | Resolution | Purpose |
|---|---|---|---|
| Front Camera | LEKIWI_CameraCfg | 640ร360 | Primary RGB+Depth observation input to the policy |
| Image Goal Camera | LEKIWI_ImageGoal_CameraCfg | 640ร360 | Captures goal image at target location (RGB only) |
| Metric Camera | LEKIWI_MetricCameraCfg | 160ร90 | Low-res overhead camera for BEV metric tracking |
All cameras use a Pinhole model with focal length 1.4, clipping range (0.01, 100.0) m. Camera positions are defined relative to the base link prim path.
The evaluation scripts dynamically select robot configuration based on the --robot argument.
The config objects are imported via configs/robots/__init__.py which re-exports both
DINGO_CFG and LEKIWI_CFG along with all associated constants.
# Any evaluation script
python eval_pointgoal_wheeled.py \
--robot lekiwi \ # Switch from dingo to lekiwi
--port 8888 \
--scene_dir ./assets/scenes/cluttered_easy \
--num_episodes 5
baselines/logoplanner/lekiwi_logoplanner_host.py provides a real-world client that connects a physical LeKiwi robot
(with Intel RealSense D435) to the LoGoPlanner HTTP server. This enables true sim-to-real transfer using the same navigation policy.