Spaces:
Running
Running
| """Render comparison plots for dance_compare.py CSV output. | |
| For one dance, generate a figure showing the same channels across | |
| both methods (python_native vs marionette_style), with N runs of | |
| each overlaid. Channels: head Y translation, head pitch (derived | |
| from rotation matrix), antenna_left, antenna_right. | |
| """ | |
| import csv | |
| import glob | |
| import sys | |
| from pathlib import Path | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| DANCE = sys.argv[1] if len(sys.argv) > 1 else "head_tilt_roll" | |
| CSV_DIR = Path("/Users/remi/Downloads/dance-compare") | |
| OUT_PNG = Path(f"/Users/remi/Downloads/dance-compare-{DANCE}.png") | |
| def load(path): | |
| rows = [] | |
| with open(path) as f: | |
| for r in csv.DictReader(f): | |
| rows.append({k: float(v) if v else None for k, v in r.items()}) | |
| return rows | |
| def head_rpy(rows, prefix): | |
| """Extract (time, roll_deg, pitch_deg, yaw_deg) sequences from the | |
| rotation submatrix of the stored 4x4 head transform. | |
| Flat row-major indices: | |
| [0 1 2 3 ] [R00 R01 R02 X ] | |
| [4 5 6 7 ] [R10 R11 R12 Y ] | |
| [8 9 10 11] [R20 R21 R22 Z ] | |
| [12 13 14 15] [0 0 0 1 ] | |
| Standard ZYX Euler decomposition: | |
| roll = atan2( R21, R22 ) around X | |
| pitch = atan2(-R20, sqrt(R21² + R22²)) around Y | |
| yaw = atan2( R10, R00 ) around Z | |
| """ | |
| t = np.array([r["t_s"] for r in rows]) | |
| R00 = np.array([r[f"{prefix}h0"] for r in rows]) | |
| R10 = np.array([r[f"{prefix}h4"] for r in rows]) | |
| R20 = np.array([r[f"{prefix}h8"] for r in rows]) | |
| R21 = np.array([r[f"{prefix}h9"] for r in rows]) | |
| R22 = np.array([r[f"{prefix}h10"] for r in rows]) | |
| roll = np.degrees(np.arctan2(R21, R22)) | |
| pitch = np.degrees(np.arctan2(-R20, np.sqrt(R21 * R21 + R22 * R22))) | |
| yaw = np.degrees(np.arctan2(R10, R00)) | |
| return t, roll, pitch, yaw | |
| def antennas(rows, prefix): | |
| t = np.array([r["t_s"] for r in rows]) | |
| l = np.array([r[f"{prefix}ant_l_deg"] for r in rows]) | |
| r = np.array([r[f"{prefix}ant_r_deg"] for r in rows]) | |
| return t, l, r | |
| def plot_method_column(axes, csvs, method_label, color): | |
| """Plot all runs of one method into axes[0..4]. Channels: | |
| roll, pitch, yaw, left antenna, right antenna.""" | |
| cmd_drawn = False | |
| for path in csvs: | |
| rows = load(path) | |
| if not rows: continue | |
| if not cmd_drawn: | |
| t_c, roll_c, pitch_c, yaw_c = head_rpy(rows, "cmd_") | |
| t_ca, l_c, r_c = antennas(rows, "cmd_") | |
| axes[0].plot(t_c, roll_c, color="#444", linestyle="--", linewidth=1.0, label="commanded") | |
| axes[1].plot(t_c, pitch_c, color="#444", linestyle="--", linewidth=1.0, label="commanded") | |
| axes[2].plot(t_c, yaw_c, color="#444", linestyle="--", linewidth=1.0, label="commanded") | |
| axes[3].plot(t_ca, l_c, color="#444", linestyle="--", linewidth=1.0, label="commanded") | |
| axes[4].plot(t_ca, r_c, color="#444", linestyle="--", linewidth=1.0, label="commanded") | |
| cmd_drawn = True | |
| t_a, roll_a, pitch_a, yaw_a = head_rpy(rows, "act_") | |
| t_aa, l_a, r_a = antennas(rows, "act_") | |
| axes[0].plot(t_a, roll_a, color=color, alpha=0.55, linewidth=0.9) | |
| axes[1].plot(t_a, pitch_a, color=color, alpha=0.55, linewidth=0.9) | |
| axes[2].plot(t_a, yaw_a, color=color, alpha=0.55, linewidth=0.9) | |
| axes[3].plot(t_aa, l_a, color=color, alpha=0.55, linewidth=0.9) | |
| axes[4].plot(t_aa, r_a, color=color, alpha=0.55, linewidth=0.9) | |
| def main(): | |
| python_csvs = sorted(glob.glob(str(CSV_DIR / f"{DANCE}-python_native-run*.csv"))) | |
| mariontte_csvs = sorted(glob.glob(str(CSV_DIR / f"{DANCE}-marionette_style-run*.csv"))) | |
| print(f"python_native runs: {len(python_csvs)}") | |
| print(f"marionette_style runs: {len(mariontte_csvs)}") | |
| fig, axes = plt.subplots(5, 2, figsize=(13, 12), sharex=True) | |
| fig.suptitle(f"Dance playback comparison — {DANCE}\n" | |
| f"Same motion, two streaming strategies, {len(python_csvs)} runs each", | |
| fontsize=13, fontweight="bold") | |
| plot_method_column(axes[:, 0], python_csvs, "python_native", "#1f77b4") | |
| plot_method_column(axes[:, 1], mariontte_csvs, "marionette_style", "#2ca02c") | |
| titles = ["Head roll (deg)", "Head pitch (deg)", "Head yaw (deg)", "Left antenna (deg)", "Right antenna (deg)"] | |
| for row, t in enumerate(titles): | |
| axes[row, 0].set_ylabel(t, fontsize=10) | |
| for col in (0, 1): | |
| axes[row, col].grid(True, alpha=0.3) | |
| axes[row, col].tick_params(labelsize=8) | |
| axes[0, 0].set_title("Python native (3 calls/tick @ 100 Hz, no lead comp)", fontsize=10) | |
| axes[0, 1].set_title("Marionette style (combined set_target @ 50 Hz, lead 90/205 ms)", fontsize=10) | |
| for col in (0, 1): | |
| axes[-1, col].set_xlabel("t (s)", fontsize=10) | |
| axes[0, col].legend(fontsize=8, loc="upper right") | |
| # Share y-axis per channel for fair visual comparison | |
| for row in range(5): | |
| ymin = min(axes[row, 0].get_ylim()[0], axes[row, 1].get_ylim()[0]) | |
| ymax = max(axes[row, 0].get_ylim()[1], axes[row, 1].get_ylim()[1]) | |
| axes[row, 0].set_ylim(ymin, ymax) | |
| axes[row, 1].set_ylim(ymin, ymax) | |
| fig.tight_layout(rect=(0, 0, 1, 0.95)) | |
| fig.savefig(OUT_PNG, dpi=140) | |
| print(f"Wrote {OUT_PNG}") | |
| if __name__ == "__main__": | |
| main() | |