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
Download code/training/evaluate_phase_policy.py from Histochemichael/microduck-electric-slide-policy: direct link, hf CLI and curl.
- Browser
- Download file 5.35 kB
-
https://huggingface.co/Histochemichael/microduck-electric-slide-policy/resolve/main/code/training/evaluate_phase_policy.py
- Command line
-
hf download hf://Histochemichael/microduck-electric-slide-policy/code/training/evaluate_phase_policy.py
-
curl -L -o evaluate_phase_policy.py https://huggingface.co/Histochemichael/microduck-electric-slide-policy/resolve/main/code/training/evaluate_phase_policy.py
5.35 kB
| """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() | |