"""Harmonic Pancake RevC 30:1 (PLA flexspline, aligned) vs RevB (PETG flexspline).

Run from anywhere:  python revc_analysis.py
Reads the RevC CSV one folder up, the RevB CSVs from ../../260830_Harmonic_Pancake_RevB_30_to_1/Test Data,
and writes rc*.png + numbers_revc.txt next to this file.

Same gear as RevB (identical gearmaker link): pancake, cycloid, m=0.55, z_flex=60, z_circ=62 -> 30:1.
Angles are at the OUTPUT in degrees (motor travel = input turns * 12, arm = MT6701 turns * 30).
Reuses the loaders/detectors in analysis.py and the stop/stiffness code in RevB's key_metrics.py.
"""
import os
import sys
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D

import analysis as A
from analysis import (HERE, OUT, DPI, ARM_M, RATIO, SURF, INK, INK2, GRID, TILE,
                      C_CH1, C_CH2, C_IN, C_SKIP, C_ALT)

# RevB data: REVB_DATA if set, else the lab folder layout, else the same folder as the
# RevC CSV (the layout of the website download, where key_metrics.py sits next to this file).
REVB = os.environ.get("REVB_DATA") or os.path.join(
    A.DATA, "..", "..", "260830_Harmonic_Pancake_RevB_30_to_1", "Test Data")
if not os.path.isdir(REVB):
    REVB = A.DATA
sys.path.append(os.path.join(REVB, "analysis"))
import key_metrics as K  # noqa: E402  (RevB stop/stiffness extraction)

C_C, C_B = C_IN, C_ALT  # RevC green, RevB purple
FILES = {
    "baseline": (A.DATA, "leftbeef-20261011-122428-207003.csv"),  # NEMA17 direct drive
    "revc": (A.DATA, next((f for f in ("pla_flex_spline_And_Alligned.csv", "hdp30-revc-20261018.csv")  # lab / website name
                           if os.path.exists(os.path.join(A.DATA, f))), "hdp30-revc-20261018.csv")),
    "run1": (REVB, "rightbeef-20261011-180808-209421.csv"),       # RevB healthy -> skipping
    "run2": (REVB, "rightbeef-20261011-205830-100749.csv"),       # RevB damaged
}


def load(key):
    d = pd.read_csv(os.path.join(*FILES[key]))
    d = d.interpolate().bfill()  # RevC file has 3 blank output_deg rows; unwrap needs them filled
    d["in_turns"] = np.unwrap(np.radians(d.input_deg)) / (2 * np.pi)
    d["mt_turns"] = np.unwrap(np.radians(d.output_deg)) / (2 * np.pi)
    d["T_nm"] = (d.ch1_n - d.ch2_n) * ARM_M
    return d


def hysteresis(k, d, c, s):
    """Per held stop: load -> hold -> unload loop (same method as RevB key_metrics KM5)."""
    t, inp, mt, T = d.time_s.values, d.in_turns.values, d.mt_turns.values, d.T_nm.values
    mono = lambda lvl, q, w: np.interp(lvl, np.maximum.accumulate(q), w)
    out = []
    for _, r in c.iterrows():
        if r.cmd == 0 or (k == "run1" and r.t1 > 41.3):
            continue
        if ((s.t >= r.t0 - 0.1) & (s.t <= r.t1 + 0.3)).any():
            continue
        sg = 1 if r.ch == "ch1" else -1
        a, b = np.searchsorted(t, r.t0), np.searchsorted(t, r.t1)
        tq, tw = sg * T, sg * (inp * 12 - mt * 30)
        i0 = a
        while i0 > 0 and tq[i0] > 0.03:
            i0 -= 1
        ipk = a + np.argmax(tq[a:a + 40])
        j = b
        while j > 0 and abs(inp[j] - inp[j - 1]) > 1e-3:
            j -= 1
        i1 = b
        while tq[i1] > 0.03:
            i1 += 1
        hold = r.F_hold * ARM_M
        ref = mono(0.06, tq[i0:ipk + 1], tw[i0:ipk + 1])
        L = lambda lvl: mono(lvl, tq[i0:ipk + 1], tw[i0:ipk + 1]) - ref
        U = lambda lvl: mono(lvl, tq[j:i1 + 1][::-1], tw[j:i1 + 1][::-1]) - ref
        path = np.arange(i0, i1 + 1)
        x, y = tw[path] - ref, tq[path]
        loop = np.trapezoid(y, np.radians(x))
        w_in = np.trapezoid(tq[i0:j + 1], np.radians(tw[i0:j + 1]))
        i2 = min(np.searchsorted(t, t[ipk] + 2.0), j)  # RevB holds are ~1.9 s
        out.append(dict(run=k, ch=r.ch, hold=hold, x=x, y=y, n_load=ipk - i0, n_hold=j - i0,
                        set=U(0.06) - L(0.06), w50=U(0.5 * hold) - L(0.5 * hold),
                        loss_mJ=loop * 1e3, loss_pct=100 * loop / w_in,
                        relax2s_pct=100 * (tq[ipk] - tq[i2]) / tq[ipk],
                        relax_pct=100 * (tq[ipk] - tq[j]) / tq[ipk], hold_s=t[j] - t[ipk],
                        ht=t[ipk:j] - t[ipk], hq=tq[ipk:j] / tq[ipk]))
    return out


def departure(st):
    """Motor travel (output deg) after the cell reads 0 until the arm has moved 0.05 deg."""
    m, o = st["dep_motor"], st["dep_out"]
    after = np.where((m >= 0) & (o > 0.05))[0]
    return m[after[0]] if len(after) else np.nan


def free_te(d, c):
    """Transmission error in free travel: MT6701 vs input, linear scale removed, per stroke."""
    t = d.time_s.values
    v = np.gradient(d.in_turns.values, t)
    free = (np.abs(v) > 1.0) & (d.current_cmd_a.values > 0)
    for _, r in c.iterrows():
        free &= ~((t > r.t0 - 0.3) & (t < r.t1 + 0.5))
    k = np.polyfit(d.in_turns[free], d.mt_turns[free], 1)[0]
    idx = np.where(free)[0]
    segs = [g for g in np.split(idx, np.where(np.diff(idx) > 1)[0] + 1) if len(g) >= 200]
    res = []
    for g in segs:
        x = d.in_turns.values[g]
        r = (d.mt_turns.values[g] - k * x) * 30
        res.append(dict(x=x - x.min(), r=r - r.mean(), pp=np.ptp(r), rms=r.std(), v=np.median(v[g]) * 60))
    return k, res


def main():
    D = {k: load(k) for k in FILES}
    C = {k: A.contacts(D[k]) for k in FILES}
    S = {k: A.skips(D[k]) for k in ("revc", "run1", "run2")}
    cb = C["baseline"]
    kt = np.sum(cb.F_hold * ARM_M * cb.iq) / np.sum(cb.iq ** 2)
    ideal = kt * RATIO
    for k in ("revc", "run1", "run2"):
        c = C[k]
        c["T_hold"], c["T_peak"] = c.F_hold * ARM_M, c.F_peak * ARM_M
        c["eff"] = 100 * c.T_hold / (ideal * c.iq.replace(0, np.nan))

    ST = {k: K.stops(k, D[k], C[k], S[k]) for k in ("revc", "run1")}
    HY = {k: hysteresis(k, D[k], C[k], S[k]) for k in ("revc", "run1")}
    TE = {k: free_te(D[k], C[k]) for k in ("revc", "run2")}

    d, c, s = D["revc"], C["revc"], S["revc"]
    cr = c[c.cmd > 0]
    b1 = C["run1"][(C["run1"].cmd > 0) & (C["run1"].t1 < 41.3)]

    lines = []
    P = lines.append
    P(f"motor Kt {kt:.3f} N*m/A (direct drive), ideal 30:1 output {ideal:.2f} N*m/A")
    P(f"RevC: {len(d)} rows over {d.time_s.iloc[-1]:.1f} s, median dt {np.median(np.diff(d.time_s)) * 1e3:.1f} ms")
    P("RevC current program: " + ", ".join(
        f"{d.current_cmd_a.values[i + 1]:.2f} A @ {d.time_s.values[i + 1]:.1f} s"
        for i in np.where(np.diff(d.current_cmd_a.values) != 0)[0]))
    P(f"\nRevC contacts:\n{c.round(3).to_string()}")
    P(f"\nRevC tooth skips detected: {len(s)}")
    for ch in ("ch1", "ch2"):
        P(f"  {ch} stop input position over time: {np.round(c[c.ch == ch].in_pos.values, 3)}  (no drift = no skips)")
    P(f"RevC MT6701 scale: {TE['revc'][0]:.4f} MT turns / input turn = {TE['revc'][0] * 30:.2f} per output turn")
    P(f"\nStatic efficiency RevC: " + ", ".join(f"{r.ch} {r.eff:.0f}% @ {r.iq:.2f} A / {r.T_hold:.2f} N*m" for _, r in cr.iterrows()))
    P(f"Static efficiency RevB healthy: " + ", ".join(f"{r.ch} {r.eff:.0f}% @ {r.iq:.2f} A / {r.T_hold:.2f} N*m" for _, r in b1.iterrows()))

    P("\nStiffness (clean unloading branch, 10-90 % of hold):")
    for k in ("revc", "run1"):
        for st in ST[k]:
            P(f"  {k:5s} {st['ch']} hold {st['hold']:.2f} N*m: {st['k_u'] * 57.3:5.1f} N*m/rad, "
              f"unwind {st['u_tw'].max():.2f} deg, departure dead band {departure(st):.2f} deg "
              f"(1 sample = {st['sample_deg']:.2f} deg)")

    P("\nHysteresis / creep per stop:")
    for k in ("revc", "run1"):
        for h in HY[k]:
            P(f"  {k:5s} {h['ch']} hold {h['hold']:.2f}: set {h['set']:+.3f} deg, w50 {h['w50']:+.3f} deg, "
              f"loss {h['loss_pct']:+.1f}% ({h['loss_mJ']:+.2f} mJ), relax {h['relax2s_pct']:.1f}% in 2 s, "
              f"{h['relax_pct']:.1f}% over {h['hold_s']:.1f} s")

    P("\nFree travel:")
    for k in ("revc", "run2"):
        z = TE[k][1]
        P(f"  {k}: TE per stroke p-p {np.round([e['pp'] for e in z], 2)} deg, rms {np.round([e['rms'] for e in z], 3)}")
    for k in ("revc", "run2"):
        dd = D[k]
        t = dd.time_s.values
        v = np.gradient(dd.in_turns.values, t) * 60
        m = (np.abs(np.abs(v) - 150) < 10) & (dd.current_cmd_a.values > 0)
        for _, r in C[k].iterrows():
            m &= ~((t > r.t0 - 0.3) & (t < r.t1 + 0.5))
        P(f"  {k}: no-load drag at 150 RPM |iq| = {dd.iq_a.values[m].__abs__().mean():.3f} A")
    P(f"  RevC id max |{d.id_a.abs().max():.3f}| A; iq exceeded cmd by >30 mA in "
      f"{int(((d.iq_a.abs() - d.current_cmd_a) > 0.03).sum())} rows")

    report = "\n".join(lines)
    print(report)
    open(os.path.join(HERE, "numbers_revc.txt"), "w").write(report)

    # ---- RC1: overview --------------------------------------------------------
    fig, ax = plt.subplots(2, 1, figsize=(13, 7.5), sharex=True, gridspec_kw=dict(height_ratios=[3, 2]))
    ax[0].plot(d.time_s, d.ch1_n * ARM_M, color=C_CH1, lw=1.2, label="CH1 side")
    ax[0].plot(d.time_s, -d.ch2_n * ARM_M, color=C_CH2, lw=1.2, label="CH2 side")
    ax[0].axhline(1.48, color=C_B, ls="--", lw=1.2)
    ax[0].text(0.5, 1.53, "RevB first skip 1.48 N·m", color=C_B, fontsize=11)
    ax[0].axhline(-1.48, color=C_B, ls="--", lw=1.2)
    for _, r in cr.iterrows():
        sg = 1 if r.ch == "ch1" else -1
        ax[0].text((r.t0 + r.t1) / 2, sg * (r.T_hold + 0.12), f"{r.T_hold:.2f} N·m @ {r.iq:.2f} A",
                   color=INK, ha="center", fontsize=11)
    ax[0].set_ylim(-1.85, 1.85)
    ax[0].set_ylabel("output torque  [N·m]")
    ax[0].set_title("RevC (PLA flex, aligned): three holds up to 1.49 N·m, zero tooth skips")
    ax[0].legend(loc="upper right", ncol=2)
    ax[1].plot(d.time_s, d.in_turns, color=C_IN, lw=1.5)
    for ch, col in (("ch1", C_CH1), ("ch2", C_CH2)):
        for p in c[c.ch == ch].in_pos:
            ax[1].axhline(p, color=col, lw=0.8, ls=":")
    ax[1].text(19, 5.3, "CH1 stop: 4.527 → 4.531 turns", color=C_CH1, fontsize=11)
    ax[1].text(19, -9.9, "CH2 stop: −10.775 turns", color=C_CH2, fontsize=11)
    ax[1].text(54.3, -6, "0.70 A step\nended before\nreaching CH1", color=INK2, fontsize=10)
    ax[1].set_ylabel("motor position  [turns]")
    ax[1].set_xlabel("time  [s]")
    fig.tight_layout()
    fig.savefig(os.path.join(OUT, "rc1_overview.png"), dpi=DPI)

    # ---- RC2: torque capacity vs RevB -----------------------------------------
    s1 = S["run1"]
    ladder = [
        ("RevB: held, no skip", b1.T_peak.max(), C_B),
        ("RevB: FIRST skip", s1.T_peak.iloc[0], C_SKIP),
        ("RevB: ratcheting after (median)", s1.T_peak.iloc[1:].median(), C_SKIP),
        ("RevC: held 4 s, no skip (CH2)", cr.T_hold.max(), C_C),
        ("RevC: impact peak, no skip (CH2)", cr.T_peak.max(), C_C),
    ]
    fig, ax = plt.subplots(figsize=(12, 5.5))
    for i, (lab, v, col) in enumerate(ladder):
        ax.barh(i, v, color=col, height=0.6)
        ax.text(v + 0.02, i, f"{v:.2f} N·m", va="center", color=INK, fontsize=13, fontweight="bold")
    # Heard on the rig after this recording, so not in the CSV: cracking from ~15 N, wall broke at ~20 N.
    for x, lab in ((1.5, "cracking\nheard ~1.5"), (2.0, "flex wall\nbroke ~2.0")):
        ax.axvline(x, color=C_SKIP, ls="--", lw=1.4)
        ax.text(x + 0.03, 5.1, lab, color=C_SKIP, fontsize=11, va="bottom")
    ax.text(2.06, 2.0, "RevC, heard on the rig\nafter this recording,\nnot in the logged data",
            color=INK2, fontsize=10.5, va="center")
    ax.set_yticks(range(len(ladder)))
    ax.set_yticklabels([l[0] for l in ladder], fontsize=12, color=INK)
    ax.set_xlim(0, 2.6)
    ax.set_ylim(5.2, -0.6)
    ax.set_xlabel("output torque  [N·m]   (= load-cell N × 0.1 m)")
    ax.grid(axis="y", visible=False)
    ax.set_title("TORQUE: RevC held past RevB's skip point; PLA cracks from ~1.5")
    fig.tight_layout()
    fig.savefig(os.path.join(OUT, "rc2_torque_vs_revb.png"), dpi=DPI)

    # ---- RC3: efficiency ------------------------------------------------------
    fig, ax = plt.subplots(figsize=(11, 6.5))
    for cc, col, lab in ((b1, C_B, "RevB healthy (PETG)"), (cr, C_C, "RevC (PLA, aligned)")):
        for _, r in cc.iterrows():
            ax.scatter(r.T_hold, r.eff, s=140, marker="o" if r.ch == "ch1" else "s", color=col,
                       edgecolors=SURF, linewidths=2, zorder=4)
            ax.annotate(f"{r.iq:.2f} A", (r.T_hold, r.eff), textcoords="offset points", xytext=(10, -4),
                        color=INK2, fontsize=10)
    ax.set_xlabel("held output torque  [N·m]")
    ax.set_ylabel("static efficiency  [%]\n= held torque / (30 × motor torque)")
    ax.set_xlim(0, 1.7)
    ax.set_ylim(0, 75)
    ax.legend(handles=[Line2D([], [], marker="o", ls="", color=C_B, ms=10, label="RevB healthy (PETG)"),
                       Line2D([], [], marker="o", ls="", color=C_C, ms=10, label="RevC (PLA, aligned)"),
                       Line2D([], [], marker="o", ls="", color=INK2, ms=9, label="CH1 direction"),
                       Line2D([], [], marker="s", ls="", color=INK2, ms=9, label="CH2 direction")],
              loc="lower right", ncol=2)
    ax.set_title(f"EFFICIENCY: RevC {cr.eff.min():.0f}-{cr.eff.max():.0f} % stalled (RevB {b1.eff.min():.0f}-{b1.eff.max():.0f} %)")
    fig.tight_layout()
    fig.savefig(os.path.join(OUT, "rc3_efficiency.png"), dpi=DPI)

    # ---- RC4: stiffness -------------------------------------------------------
    fig, ax = plt.subplots(figsize=(11, 7))
    for k, col in (("run1", C_B), ("revc", C_C)):
        for st in ST[k]:
            ax.plot(st["u_tw"], st["u_tq"], color=col, lw=1.8, marker="o" if st["ch"] == "ch1" else "s",
                    ms=4.5, alpha=0.9)
    kc = [st["k_u"] * 57.3 for st in ST["revc"]]
    kb = [st["k_u"] * 57.3 for st in ST["run1"]]
    ax.text(0.15, 1.38, f"RevC: {min(kc):.0f}-{max(kc):.0f} N·m/rad\n(1.56° at 1.49 N·m)", color=C_C,
            fontsize=13, fontweight="bold")
    ax.text(1.75, 0.45, f"RevB healthy: {min(kb):.0f}-{max(kb):.0f} N·m/rad\n(2.0-2.6° at 1.25-1.35 N·m)",
            color=C_B, fontsize=13, fontweight="bold")
    ax.set_xlim(0, 3)
    ax.set_ylim(0, 1.6)
    ax.set_xlabel("gearbox twist  [deg at the output]   (motor/30 minus measured arm motion)")
    ax.set_ylabel("output torque  [N·m]")
    ax.legend(handles=[Line2D([], [], color=C_B, lw=3, label="RevB healthy (PETG)"),
                       Line2D([], [], color=C_C, lw=3, label="RevC (PLA, aligned)"),
                       Line2D([], [], color=INK2, marker="o", ls="", label="CH1 direction"),
                       Line2D([], [], color=INK2, marker="s", ls="", label="CH2 direction")],
              loc="lower right")
    ax.set_title("TORSIONAL STIFFNESS: same below 0.5 N·m, then RevC stiffens: half the twist at 1.25 N·m")
    fig.tight_layout()
    fig.savefig(os.path.join(OUT, "rc4_stiffness.png"), dpi=DPI)

    # ---- RC5: creep during the hold -------------------------------------------
    fig, ax = plt.subplots(figsize=(11, 6))
    for k, col in (("run1", C_B), ("revc", C_C)):
        for h in HY[k]:
            ax.plot(h["ht"], 100 * h["hq"], color=col, lw=1.8, alpha=0.9,
                    ls="-" if h["ch"] == "ch1" else "--")
    rc2 = np.median([h["relax2s_pct"] for h in HY["revc"]])
    rb2 = np.median([h["relax2s_pct"] for h in HY["run1"]])
    ax.axvline(2, color=GRID, lw=1)
    ax.text(2.1, 101, f"after 2 s:  RevB −{rb2:.0f} %   RevC −{rc2:.0f} %", color=INK, fontsize=12)
    ax.set_xlabel("time since peak, motor stalled at the current limit  [s]")
    ax.set_ylabel("torque on stop  [% of peak]")
    ax.set_ylim(80, 103)
    ax.legend(handles=[Line2D([], [], color=C_B, lw=3, label="RevB healthy (PETG)"),
                       Line2D([], [], color=C_C, lw=3, label="RevC (PLA, aligned)"),
                       Line2D([], [], color=INK2, lw=2, label="CH1 direction"),
                       Line2D([], [], color=INK2, lw=2, ls="--", label="CH2 direction")],
              loc="lower left", ncol=2)
    ax.set_title("CREEP: the PLA flexspline loses about half as much torque while held")
    fig.tight_layout()
    fig.savefig(os.path.join(OUT, "rc5_creep.png"), dpi=DPI)

    # ---- RC6: hysteresis loops ------------------------------------------------
    fig, axs = plt.subplots(1, 2, figsize=(15, 7.5), sharey=True)
    for ax, k, title in ((axs[0], "run1", "RevB healthy (PETG)"), (axs[1], "revc", "RevC (PLA, aligned)")):
        for h in HY[k]:
            ax.plot(h["x"], h["y"], color=C_CH1 if h["ch"] == "ch1" else C_CH2, lw=1.8, marker="o", ms=3)
        H = pd.DataFrame([{kk: v for kk, v in h.items() if kk not in ("x", "y", "ht", "hq")} for h in HY[k]])
        ax.set_title(f"{title}\n" + " · ".join(
            f"{r.ch.upper()} {r.hold:.2f} N·m: {max(r.loss_pct, 0):.0f}% lost" for _, r in H.iterrows()), fontsize=12)
        ax.set_xlabel("gearbox twist from start of loading  [deg at the output]")
        ax.set_xlim(-0.2, 3.6)
    axs[0].set_ylabel("output torque on stop  [N·m]")
    axs[1].legend(handles=[Line2D([], [], color=C_CH1, lw=3, label="CH1 direction"),
                           Line2D([], [], color=C_CH2, lw=3, label="CH2 direction")], loc="upper right")
    fig.suptitle("HYSTERESIS: RevC CH1 loops close within resolution; the 1.5 N·m CH2 loop is wider",
                 x=0.01, ha="left", fontsize=15, fontweight="bold", color=INK)
    fig.tight_layout()
    fig.savefig(os.path.join(OUT, "rc6_hysteresis.png"), dpi=DPI)

    # ---- RC7: transmission error in free travel --------------------------------
    fig, ax = plt.subplots(figsize=(12, 5.5))
    for k, col, lab in (("run2", C_B, "RevB (run 2, 150 RPM)"), ("revc", C_C, "RevC (150 RPM)")):
        z = TE[k][1]
        for n, e in enumerate(z):
            ax.plot(e["x"], e["r"], color=col, lw=1.3, alpha=0.75, label=lab if n == 0 else None)
    pc = np.median([e["pp"] for e in TE["revc"][1]])
    pb = np.median([e["pp"] for e in TE["run2"][1]])
    ax.set_xlabel("input turns into the stroke")
    ax.set_ylabel("arm position error  [deg at the output]")
    ax.legend(loc="upper right")
    ax.set_title(f"TRANSMISSION ERROR (free travel): about the same, {pb:.1f}° vs {pc:.1f}° p-p (magnet ring included)")
    fig.tight_layout()
    fig.savefig(os.path.join(OUT, "rc7_transmission_error.png"), dpi=DPI)

    # ---- RC0: summary card -----------------------------------------------------
    hc = [h for h in HY["revc"]]
    tiles = [
        ("TORQUE", f"{cr.T_hold.max():.2f} N·m", f"held 4 s, no skip\n(impact peak {cr.T_peak.max():.2f})",
         "RevB: first skip at 1.48", C_CH2),
        ("EFFICIENCY", f"{cr.eff.min():.0f}-{cr.eff.max():.0f} %", "static, stalled\n(0.40-0.55 A)",
         f"RevB: {b1.eff.min():.0f}-{b1.eff.max():.0f} %", C_IN),
        ("STIFFNESS", f"{min(kc):.0f}-{max(kc):.0f}", "N·m/rad\n(stiffer at higher load)",
         f"RevB: {min(kb):.0f}-{max(kb):.0f} N·m/rad", C_CH1),
        ("CREEP", f"−{rc2:.0f} % / 2 s", "torque lost while held\nat constant motor position",
         f"RevB: −{rb2:.0f} % in 1.9 s", C_ALT),
        ("TRANS. ERROR", f"{pc:.1f}° p-p", "free travel, includes\nmagnet-ring error", f"RevB (damaged): {pb:.1f}° p-p", C_CH2),
    ]
    fig, axs = plt.subplots(1, 5, figsize=(20, 4.6))
    for ax, (name, big, sub, note, col) in zip(axs, tiles):
        ax.axis("off")
        ax.add_patch(plt.Rectangle((0, 0), 1, 1, transform=ax.transAxes, facecolor=TILE, edgecolor=GRID, lw=1))
        ax.add_patch(plt.Rectangle((0, 0.96), 1, 0.04, transform=ax.transAxes, facecolor=col, lw=0))
        ax.text(0.07, 0.82, name, transform=ax.transAxes, color=INK2, fontsize=13, fontweight="bold")
        ax.text(0.07, 0.55, big, transform=ax.transAxes, color=INK, fontsize=28, fontweight="bold")
        ax.text(0.07, 0.30, sub, transform=ax.transAxes, color=INK2, fontsize=11.5, va="center")
        ax.text(0.07, 0.08, note, transform=ax.transAxes, color=INK2, fontsize=11.5)
    fig.suptitle("Harmonic Pancake RevC 30:1 (PLA flexspline, aligned) vs RevB (PETG)",
                 x=0.01, ha="left", fontsize=15, fontweight="bold", color=INK)
    fig.tight_layout()
    fig.savefig(os.path.join(OUT, "rc0_summary.png"), dpi=DPI)
    print("\nfigures written to", OUT)


if __name__ == "__main__":
    main()
