Instructions to use Histochemichael/microduck-electric-slide-policy with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Microduck
How to use Histochemichael/microduck-electric-slide-policy with Microduck:
# Replace SLOT with the slot specified in the model card (walk, stand, sitstand, ground_pick, kick_left, kick_right, roulade). sudo robotctl policy load SLOT Histochemichael/microduck-electric-slide-policy
- Notebooks
- Google Colab
- Kaggle
File size: 5,347 Bytes
32502a8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | """Closed-loop MuJoCo evaluation for the trained phase-imitation policy."""
from __future__ import annotations
import argparse
import json
import math
from pathlib import Path
import sys
import mujoco
import numpy as np
import onnxruntime as ort
DANCE_DIR = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(DANCE_DIR))
from choreography import SHOW_SECONDS # noqa: E402
from generate_scene import build_scene # noqa: E402
from physics_show import ( # noqa: E402
CONTROL_HZ,
DECIMATION,
DEFAULT_POLICY,
PHYSICS_HZ,
ContactDiagnostics,
MultiDuckPolicy,
)
def phase_command(time_seconds: float, duration: float) -> np.ndarray:
phase = (time_seconds % duration) / duration
result = np.zeros(13, dtype=np.float32)
result[0] = math.cos(2.0 * math.pi * phase)
result[1] = math.sin(2.0 * math.pi * phase)
return result
def run(args: argparse.Namespace) -> dict:
output = args.output.resolve()
output.parent.mkdir(parents=True, exist_ok=True)
scene = output.parent / "phase_policy_eval_scene.xml"
build_scene(scene, grid=1, spacing=1.5)
model = mujoco.MjModel.from_xml_path(str(scene))
model.opt.timestep = 1.0 / PHYSICS_HZ
data = mujoco.MjData(model)
indexing = MultiDuckPolicy(model, data, args.teacher_policy.resolve(), grid=1)
indexing.reset()
session = ort.InferenceSession(str(args.policy.resolve()))
input_name = session.get_inputs()[0].name
output_name = session.get_outputs()[0].name
metadata = session.get_modelmeta().custom_metadata_map
action_scale = float(metadata["action_scale"])
default_pose = np.array(
[float(value) for value in metadata["default_joint_pos"].split(",")],
dtype=np.float32,
)
if session.get_inputs()[0].shape[-1] != 61 or session.get_outputs()[0].shape[-1] != 14:
raise ValueError("Phase policy must use the 61D -> 14D deployment contract")
indexing.default_pose = default_pose
indexing.data.ctrl[:] = default_pose
indexing.last_actions.fill(0.0)
mujoco.mj_forward(model, data)
for _ in range(int(round(args.warmup * CONTROL_HZ))):
command = phase_command(0.0, args.duration)
observation = indexing.observation(0, command)
action = session.run([output_name], {input_name: observation[None, :]})[0][0]
indexing.last_actions[0] = action
data.ctrl[indexing.actuator_ids[0]] = default_pose + action * action_scale
for _ in range(DECIMATION):
mujoco.mj_step(model, data)
initial_xy = data.qpos[indexing.free_qpos[0]:indexing.free_qpos[0] + 2].copy()
diagnostics = ContactDiagnostics(indexing, initial_xy[None, :])
reference = np.load(args.reference.resolve(), allow_pickle=True)
steps = int(round(args.duration * CONTROL_HZ))
rollout_joint = np.empty((steps, 14), dtype=np.float32)
rollout_action = np.empty((steps, 14), dtype=np.float32)
observations = np.empty((steps, 61), dtype=np.float32)
for index in range(steps):
command = phase_command(index / CONTROL_HZ, args.duration)
observation = indexing.observation(0, command)
action = session.run([output_name], {input_name: observation[None, :]})[0][0]
indexing.last_actions[0] = action
data.ctrl[indexing.actuator_ids[0]] = default_pose + action * action_scale
for _ in range(DECIMATION):
mujoco.mj_step(model, data)
diagnostics.update()
observations[index] = observation
rollout_action[index] = action
rollout_joint[index] = data.qpos[indexing.joint_qpos[0]]
reference_joint = reference["joint_pos"][:steps]
joint_rmse = np.sqrt(np.mean((rollout_joint - reference_joint) ** 2, axis=0))
report = diagnostics.report(args.duration)
report["closed_loop_joint_rmse_rad"] = float(np.sqrt(np.mean((rollout_joint - reference_joint) ** 2)))
report["closed_loop_joint_rmse_per_joint_rad"] = joint_rmse.round(7).tolist()
report["policy_path"] = str(args.policy)
report["reference_path"] = str(args.reference)
report["passed"] = (
not report["fallen_ducks"]
and report["minimum_foot_liftoffs"] >= 2
and report["maximum_airborne_fraction"] <= 0.20
and np.isfinite(rollout_joint).all()
)
np.savez_compressed(
output,
time_seconds=np.arange(steps, dtype=np.float32) / CONTROL_HZ,
joint_pos=rollout_joint,
action=rollout_action,
observation_61d=observations,
reference_joint_pos=reference_joint,
)
output.with_suffix(".json").write_text(json.dumps(report, indent=2), encoding="utf-8")
print(json.dumps(report, indent=2))
if not report["passed"]:
raise SystemExit("Closed-loop phase-policy validation failed")
return report
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--policy", type=Path, required=True)
parser.add_argument("--reference", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--teacher-policy", type=Path, default=DEFAULT_POLICY)
parser.add_argument("--duration", type=float, default=SHOW_SECONDS)
parser.add_argument("--warmup", type=float, default=1.5)
run(parser.parse_args())
if __name__ == "__main__":
main()
|