File size: 5,426 Bytes
7ea69d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""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()