import json
import sys
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
MARKERS = {"ground-lab": "o", "flight-qualified": "s", "deployed": "^"}
DARK = {
"rc": {
"figure.facecolor": "#0c0b08",
"savefig.facecolor": "#0c0b08",
"axes.facecolor": "#0c0b08",
"axes.edgecolor": "#5a5040",
"axes.labelcolor": "#f1ece2",
"axes.titlecolor": "#e0bd84",
"text.color": "#f1ece2",
"xtick.color": "#c9c2b4",
"ytick.color": "#c9c2b4",
"grid.color": "#342c21",
"legend.facecolor": "#141109",
"legend.edgecolor": "#3a352b",
},
"colours": {"ground-lab": "#4aa3c4", "flight-qualified": "#46b67e", "deployed": "#dd6a48"},
"line": "#f1ece2", "accent": "#e0bd84", "label": "#f1ece2", }
LIGHT = {
"rc": {
"figure.facecolor": "white",
"savefig.facecolor": "white",
"axes.facecolor": "white",
"axes.edgecolor": "black",
"axes.labelcolor": "black",
"axes.titlecolor": "black",
"text.color": "black",
"xtick.color": "black",
"ytick.color": "black",
"grid.color": "#b0b0b0",
},
"colours": {"ground-lab": "#1f6f8b", "flight-qualified": "#4a7a3a", "deployed": "#d2674a"},
"line": "k",
"accent": "#9a7a33",
"label": None, }
def fhr(t):
if t is None or not np.isfinite(t):
return "> swept range"
return f"{t/3600:.1f} h" if t >= 3600 else f"{t:.0f} s"
def render(d, out, theme, dpi=None):
hold = np.array(d["holdovers_s"])
thr = d["threshold_ns"]
with plt.rc_context(theme["rc"]):
fig, ax = plt.subplots(figsize=(7.6, 5.0))
for c in d["curves"]:
col = theme["colours"].get(c["trl"], "#888")
mk = MARKERS.get(c["trl"], "o")
mean = np.array([p["timing_p95_ns"]["mean"] for p in c["points"]])
lo = np.array([p["timing_p95_ns"]["ci95_low"] for p in c["points"]])
hi = np.array([p["timing_p95_ns"]["ci95_high"] for p in c["points"]])
ax.fill_between(hold, lo, hi, color=col, alpha=0.20)
label = f"{c['id']} [{c['trl']}] — 1 µs at {fhr(c['time_to_threshold_s'])}"
ax.plot(hold, mean, color=col, lw=2, marker=mk, ms=4, label=label)
ax.axhline(thr, color=theme["line"], ls="--", lw=1.2)
ax.text(hold[0], thr * 1.3, f"{thr:.0f} ns ({thr/1000:.0f} µs) budget", fontsize=8, va="bottom")
ax.set_xscale("log")
ax.set_yscale("log")
ax.set_xlabel("holdover duration (s)")
ax.set_ylabel("p95 timing error (ns)")
ax.set_title(
"Clock-holdover technology-readiness ladder\n"
"(GNSS-disciplined, then free-running; 95% bootstrap CI)"
)
ax.grid(True, which="both", alpha=0.25)
ax.legend(fontsize=8, loc="upper left", framealpha=0.9, labelcolor=theme["label"])
eng = d.get("engine_version", "?")
fig.text(0.99, 0.01, f"Kshana v{eng} · seed 42 · 32 MC runs/node",
ha="right", va="bottom", fontsize=7, color=theme["accent"])
fig.tight_layout()
fig.savefig(out, dpi=dpi, facecolor=fig.get_facecolor())
plt.close(fig)
def main():
path = sys.argv[1] if len(sys.argv) > 1 else "paper/crossover/clock.json"
d = json.load(open(path))
base = path.rsplit(".", 1)[0]
render(d, base + ".png", DARK, dpi=150) render(d, base + ".pdf", LIGHT) print("wrote", base + ".png", "(dark) and", base + ".pdf", "(light)")
if __name__ == "__main__":
main()