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/choreography.py from Histochemichael/microduck-electric-slide-policy: direct link, hf CLI and curl.
- Browser
- Download file 6.8 kB
-
https://huggingface.co/Histochemichael/microduck-electric-slide-policy/resolve/main/code/choreography.py
- Command line
-
hf download hf://Histochemichael/microduck-electric-slide-policy/code/choreography.py
-
curl -L -o choreography.py https://huggingface.co/Histochemichael/microduck-electric-slide-policy/resolve/main/code/choreography.py
6.8 kB
| """Music-timed, 50 Hz Electric Slide choreography for a Microduck flock. | |
| The motion is deliberately kinematic for the first visual prototype: root poses | |
| and servo poses are sampled directly in MuJoCo. That makes the 25-duck show | |
| repeatable while keeping a clean seam for replacing the sampler with an ONNX | |
| policy later. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| import math | |
| BPM = 111.0 | |
| BEAT_SECONDS = 60.0 / BPM | |
| COUNTS_PER_WALL = 18 | |
| WALLS = 4 | |
| SHOW_BEATS = COUNTS_PER_WALL * WALLS | |
| SHOW_SECONDS = SHOW_BEATS * BEAT_SECONDS | |
| CONTROL_HZ = 50 | |
| # Official STAND keyframe, in official actuator/joint order. | |
| STAND_POSE = ( | |
| 0.0, | |
| -0.08726646259971647, | |
| -0.457924, | |
| -0.004940, | |
| 0.452984, | |
| 0.3490658503988659, | |
| 0.3490658503988659, | |
| 0.0, | |
| 0.0, | |
| 0.0, | |
| 0.08726646259971647, | |
| 0.457924, | |
| 0.004940, | |
| -0.452984, | |
| ) | |
| # Per-count root translation in the duck's local frame (forward, left) and yaw. | |
| # Right is negative local y. Four 18-count walls trace a closed square. | |
| ROOT_STEPS = ( | |
| (0.00, -0.10, 0.0), # grapevine right: side | |
| (0.00, -0.04, 0.0), # cross behind | |
| (0.00, -0.10, 0.0), # side | |
| (0.00, 0.00, 0.0), # touch | |
| (0.00, 0.10, 0.0), # grapevine left: side | |
| (0.00, 0.04, 0.0), # cross behind | |
| (0.00, 0.10, 0.0), # side | |
| (0.00, 0.00, 0.0), # touch | |
| (-0.08, 0.00, 0.0), # walk back | |
| (-0.08, 0.00, 0.0), | |
| (-0.08, 0.00, 0.0), | |
| (0.00, 0.00, 0.0), # touch | |
| (0.08, 0.00, 0.0), # rock forward | |
| (0.00, 0.00, 0.0), # toe touch + scoop | |
| (-0.08, 0.00, 0.0), # rock back | |
| (0.00, 0.00, 0.0), # toe touch + lean | |
| (0.05, 0.00, 0.0), # step into turn | |
| (0.00, 0.00, math.pi / 2.0), # scuff + quarter turn left | |
| ) | |
| class DuckPose: | |
| position: tuple[float, float, float] | |
| quaternion: tuple[float, float, float, float] | |
| joints: tuple[float, ...] | |
| def formation_positions(grid: int = 5, spacing: float = 0.52) -> list[tuple[float, float]]: | |
| """Return a centered, row-major square formation.""" | |
| center = (grid - 1) / 2.0 | |
| return [ | |
| ((col - center) * spacing, (center - row) * spacing) | |
| for row in range(grid) | |
| for col in range(grid) | |
| ] | |
| def _smoothstep(value: float) -> float: | |
| value = max(0.0, min(1.0, value)) | |
| return value * value * (3.0 - 2.0 * value) | |
| def _rotate(x: float, y: float, yaw: float) -> tuple[float, float]: | |
| cosine, sine = math.cos(yaw), math.sin(yaw) | |
| return cosine * x - sine * y, sine * x + cosine * y | |
| def root_motion(beat: float) -> tuple[float, float, float]: | |
| """Integrate the 18-count pattern through the supplied beat (0..72).""" | |
| beat = max(0.0, min(float(SHOW_BEATS), beat)) | |
| whole = int(math.floor(beat)) | |
| fraction = _smoothstep(beat - whole) | |
| x = y = yaw = 0.0 | |
| for index in range(whole): | |
| dx, dy, dyaw = ROOT_STEPS[index % COUNTS_PER_WALL] | |
| world_dx, world_dy = _rotate(dx, dy, yaw) | |
| x += world_dx | |
| y += world_dy | |
| yaw += dyaw | |
| if whole < SHOW_BEATS: | |
| dx, dy, dyaw = ROOT_STEPS[whole % COUNTS_PER_WALL] | |
| world_dx, world_dy = _rotate(dx, dy, yaw) | |
| x += world_dx * fraction | |
| y += world_dy * fraction | |
| yaw += dyaw * fraction | |
| return x, y, yaw | |
| def _quaternion(yaw: float, pitch: float, roll: float) -> tuple[float, float, float, float]: | |
| """MuJoCo wxyz quaternion from ZYX yaw, pitch, and roll.""" | |
| cy, sy = math.cos(yaw / 2), math.sin(yaw / 2) | |
| cp, sp = math.cos(pitch / 2), math.sin(pitch / 2) | |
| cr, sr = math.cos(roll / 2), math.sin(roll / 2) | |
| return ( | |
| cr * cp * cy + sr * sp * sy, | |
| sr * cp * cy - cr * sp * sy, | |
| cr * sp * cy + sr * cp * sy, | |
| cr * cp * sy - sr * sp * cy, | |
| ) | |
| def sample_pose(time_seconds: float, duck_index: int, base_xy: tuple[float, float]) -> DuckPose: | |
| """Sample one duck at show time, including subtle flock-specific attitude.""" | |
| beat = (time_seconds / BEAT_SECONDS) % SHOW_BEATS | |
| count = int(beat) % COUNTS_PER_WALL | |
| phase = beat - math.floor(beat) | |
| step_pulse = math.sin(math.pi * phase) | |
| groove = math.sin(2.0 * math.pi * (beat / 2.0 + (duck_index % 5) * 0.035)) | |
| row = duck_index // 5 | |
| col = duck_index % 5 | |
| accent = (row - 2) * 0.018 + (col - 2) * 0.012 | |
| travel_x, travel_y, yaw = root_motion(beat) | |
| z = 0.12 + 0.006 * abs(groove) | |
| pitch = 0.025 * groove | |
| roll = -0.035 * groove + accent | |
| pose = list(STAND_POSE) | |
| # Continuous knee compression gives the loose, grounded look in the clip. | |
| compression = 0.035 + 0.055 * abs(groove) | |
| pose[3] += compression | |
| pose[13] -= compression | |
| pose[2] += compression * 0.45 | |
| pose[12] -= compression * 0.45 | |
| # Alternating weight shift and foot pickup for traveling counts. | |
| if count in (0, 2, 4, 6, 8, 10): | |
| lead_left = count in (4, 6, 10) | |
| lift = 0.16 * step_pulse | |
| if lead_left: | |
| pose[2] += lift | |
| pose[3] += lift * 0.70 | |
| pose[4] -= lift * 0.45 | |
| roll += 0.055 * step_pulse | |
| else: | |
| pose[11] -= lift | |
| pose[12] -= lift * 0.70 | |
| pose[13] += lift * 0.45 | |
| roll -= 0.055 * step_pulse | |
| # Touches are visible: a toe point, a beak nod, and a tiny hold. | |
| if count in (3, 7, 11): | |
| pose[6] -= 0.13 * step_pulse | |
| pose[7] += 0.14 * groove | |
| z -= 0.008 * step_pulse | |
| # The reference clip's character section: low scoop forward, lean back, | |
| # then let the head lead the new wall. | |
| if count in (12, 13): | |
| scoop = step_pulse if count == 13 else _smoothstep(phase) | |
| z -= 0.022 * scoop | |
| pitch += 0.11 * scoop | |
| pose[5] += 0.24 * scoop | |
| pose[6] -= 0.22 * scoop | |
| pose[7] += (0.18 + accent) * scoop | |
| pose[8] -= 0.14 * groove | |
| elif count in (14, 15): | |
| lean = step_pulse if count == 15 else _smoothstep(phase) | |
| pitch -= 0.09 * lean | |
| pose[5] -= 0.18 * lean | |
| pose[6] += 0.15 * lean | |
| pose[8] += 0.18 * groove | |
| elif count in (16, 17): | |
| turn_lead = step_pulse if count == 16 else math.sin(math.pi * phase) | |
| pose[7] += 0.38 * turn_lead | |
| pose[8] += 0.13 * groove | |
| z += 0.012 * step_pulse | |
| # Keep the flock coordinated while the head-roll accent ripples by row. | |
| pose[8] += 0.075 * math.sin(2 * math.pi * (beat / 4.0 + row * 0.08)) | |
| return DuckPose( | |
| position=(base_xy[0] + travel_x, base_xy[1] + travel_y, z), | |
| quaternion=_quaternion(yaw, pitch, roll), | |
| joints=tuple(pose), | |
| ) | |
| def sample_flock(time_seconds: float, grid: int = 5, spacing: float = 0.52) -> list[DuckPose]: | |
| positions = formation_positions(grid, spacing) | |
| return [sample_pose(time_seconds, index, xy) for index, xy in enumerate(positions)] | |