Skip to content

microduck

microduck runs Pollen Robotics' ONNX locomotion skills for the 14-DOF Microduck biped, configured from the graph's own metadata.

By the end of this page you can make the Microduck walk, stand or run any shipped ONNX skill, hot-swap skills mid-rollout, and know what the observation carries.

pip install 'strands-robots[microduck]'    # onnxruntime + huggingface_hub; CPU is enough

What it is

The Microduck, Pollen Robotics' open 14-DOF biped, ships its skills as ONNX actors (velstand, alpha_walking, alpha_stand, alpha_sitstand, roulade, ball_kick_*, roller*, alpha_ground_pick) with the input normaliser fused into the graph. MicroduckPolicy adapts one export to the Policy contract: the ONNX metadata carries joint_names, default_joint_pos, action_scale and command_names, so pointing the policy at a different file reconfigures it. The observation is fed raw, never re-normalised. The raw action feeds the next tick's last_action block, matching Pollen's reference deployment. requires_images is False.

The weight file is the skill: provider microduck runs whichever export onnx_path names. A bare name not in the working directory is fetched from pollen-robotics/microduck-policies, whose manifest makes velstand.onnx the default walk since v5: zero command stands.

from strands_robots.policies import create_policy

walk = create_policy("microduck", onnx_path="velstand.onnx", command=[0.15, 0.0, 0.0])

Constructor keywords

keyword type default
onnx_path str \| Path \| None None
session MicroduckSession \| None None
providers list[str] \| None None
command NDArray[np.float32] \| list[float] \| None None
joint_names list[str] \| None None
default_pose NDArray[np.float32] \| list[float] \| None None
action_scale float \| None None
command_names list[str] \| None None
no **kwargs: an unknown keyword is a TypeError

All keyword-only, no **kwargs. providers defaults to ["CPUExecutionProvider"]; the actor is a small MLP. command width comes from the ONNX command_names; the default is all zeros, stand in place. action_scale must be a positive finite number: 0 would hold the default pose and discard the network.

Observation

build_observation in strands_robots/policies/microduck/observation.py assembles [command, base_ang_vel(3), projected_gravity(3), joint_pos, joint_vel, last_action], 48 non-command floats for 14 joints. The sim backends and the driver both emit these blocks; a missing block or a non-finite value is refused with the key named.

Per-call keywords

keyword shape meaning
command width of command_names replace the whole command vector
target_velocity [vx, vy, omega] or [vx, vy] write the twist block; the two-component form leaves omega as it was, because this policy's command persists across ticks

Skills as a bundle

MicroduckPolicyBundle in strands_robots.policies.microduck.composite holds several MicroduckPolicy instances warm and exposes one as active. bundle.switch("stand") swaps to that skill mid-rollout. switch_on_velocity=<threshold> with move_key and idle_key auto-selects between two skills by the magnitude of the commanded twist each tick; both keys must name held skills when the gate is on.

from strands_robots.policies.microduck import MicroduckPolicy
from strands_robots.policies.microduck.composite import MicroduckPolicyBundle

bundle = MicroduckPolicyBundle(
    {"walk": MicroduckPolicy(onnx_path="alpha_walking.onnx"), "stand": MicroduckPolicy(onnx_path="alpha_stand.onnx")},
    active="stand",
    switch_on_velocity=0.05,
    move_key="walk",
    idle_key="stand",
)
sim.run_policy(robot_name="microduck", policy_object=bundle, policy_kwargs={"target_velocity": [0.15, 0.0]}, duration=10.0)

Skill scenes

A weight and its training scene are one pair. Robot("microduck") resolves flat ground with no props; four skills need a scene shipped beside it. A skill on the wrong scene is not an error: a roller policy without wheels stands, a ball kick swings at nothing, both report success.

skill scene the scene adds
velstand, alpha_walking, alpha_stand, alpha_sitstand, roulade, alpha_ground_pick scene.xml (the declared asset) nothing
roller, roller_crouch scene_rollers.xml four passive ankle wheels
ball_kick_left, ball_kick_right scene_ball.xml a 70 mm ball in front of the duck

Reach a variant by path: find scene_rollers.xml under microduck/ on strands_robots.assets.get_search_paths() and pass Robot("microduck", urdf_path=str(scene)). scene_rollers.xml inserts two wheel joints after each ankle, so a flat qpos[7:21] read gets wheels where neck_pitch and head_pitch sit on the default scene; the actuator order is the same on all three and MicroduckPolicy reads by joint name.

The ball scene places the ball, not the kick geometry

scene_ball.xml declares the ball 0.3 m straight ahead; training placed it 0.09 m ahead and 0.042 m to the side of the kicking foot, so from the shipped position ball_kick_left reports success and misses. Teleport the ball before the rollout: set_joint_positions takes the free joint's seven-value qpos at that offset in the trunk's yaw frame; set_joint_velocities zeroes its six qvel. The file names the joint ball_free; add_robot(name=...) prefixes it; resolve the name.

The stance every weight was trained in

Actions decode as default_pose + raw_action * action_scale, so the stance is the origin of the network's output. It ships as MICRODUCK_DEFAULT_POSE and as the STAND keyframe of scene.xml and scene_rollers.xml: Robot("microduck", urdf_path=str(scene), keyframe="STAND") seats it and every reset() restores it; on scene_rollers.xml add position=[0, 0, 0.0207]. Without it the robot starts 0.458 rad off at the widest joint. scene_ball.xml has no keyframe; seat the stance yourself.

Run it

from strands_robots.simulation import create_simulation

sim = create_simulation("mujoco")
sim.create_world()
sim.add_robot("microduck")
result = sim.run_policy(
    robot_name="microduck",
    policy_provider="microduck",
    policy_config={"onnx_path": "velstand.onnx"},
    policy_kwargs={"target_velocity": [0.15, 0.0, 0.0]},
    duration=10.0,
    control_frequency=50.0,
)
print(result["status"])

Limits

  • Microduck only. The joint names come from the graph's metadata and must match the robot's.
  • One skill per policy; the bundle is how several coexist.
  • An export without the fused normaliser produces wrong actions silently. Use Pollen's exports.
Edit page