feat(SR): publish canonical analysis package
Canonicalize V5 case identities and preserve the SR evidence chain while replacing ambiguous diagnostics with reproducible tables, phase-matched flow fields, and presentation-ready summaries. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+76
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render two 16:9 presentation pages from frozen canonical SR artifacts."""
|
||||
from __future__ import annotations
|
||||
import argparse, csv, json, re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
BLUE, ORANGE, INK, MUTED, PALE = "#1769AA", "#D65F1E", "#17202A", "#52606D", "#F3F7FA"
|
||||
|
||||
def load_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8"))
|
||||
def rows(path: Path) -> list[dict[str,str]]:
|
||||
with path.open(newline="", encoding="utf-8") as f: return list(csv.DictReader(f))
|
||||
def similarity(path: Path) -> float: return float(rows(path)[-1]["similarity"])
|
||||
def expression(root: Path, family: str, head: str) -> str:
|
||||
p=root/f"article-refit-{family}-topology-a-20260718/formulas/{head}.json"
|
||||
return load_json(p)["deployment_expression"]
|
||||
def pretty(expr: str) -> str:
|
||||
rounded = re.sub(r"(?<![\w.])[-+]?(?:\d+\.\d*|\.\d+)(?:[eE][+-]?\d+)?", lambda match: f"{float(match.group()):.4f}", expr)
|
||||
return rounded.replace(" * ", r"\,").replace("Cd_rear_a", r"C_{D,r}^{a}").replace("Cl_rear_s", r"C_{L,r}^{s}").replace("Cl_F", r"C_{L,F}")
|
||||
def save(fig: plt.Figure, out: Path, stem: str) -> None:
|
||||
out.mkdir(parents=True, exist_ok=True); fig.savefig(out/f"{stem}.png", dpi=200, facecolor="white"); fig.savefig(out/f"{stem}.pdf", facecolor="white"); plt.close(fig)
|
||||
def base(title: str, subtitle: str):
|
||||
fig=plt.figure(figsize=(16,9), facecolor="white"); fig.text(.05,.94,title,fontsize=28,weight="bold",color=INK,va="top"); fig.text(.05,.885,subtitle,fontsize=13,color=MUTED,va="top"); return fig
|
||||
|
||||
def page_method(root: Path, out: Path) -> None:
|
||||
fig=base("From PPO trajectories to an executable symbolic controller", "Causal alignment, symmetry, all-case refitting, then closed-loop CFD—not offline fit alone")
|
||||
steps=[("1","PPO trajectories","states • forces • actions"),("2","Causal dataset","post-action state → next action"),("3","Topology discovery","G-symmetry-aware symbolic search"),("4","Coefficient refit","fixed topology • all family cases"),("5","CFD validation","40 / 200 / 400 steps • deletion")]
|
||||
for i,(n,h,d) in enumerate(steps):
|
||||
x=.05+i*.19; fig.patches.append(plt.Rectangle((x,.67),.16,.13,transform=fig.transFigure,fc=PALE,ec=BLUE,lw=1.5))
|
||||
fig.text(x+.012,.765,n,fontsize=20,weight="bold",color=BLUE);fig.text(x+.042,.765,h,fontsize=14,weight="bold",color=INK);fig.text(x+.012,.705,d,fontsize=10.5,color=MUTED)
|
||||
if i<4: fig.text(x+.17,.725,"→",fontsize=24,color=ORANGE,ha="center")
|
||||
formulas={f:(pretty(expression(root,f,"joint_front")),pretty(expression(root,f,"joint_rear_shared_upper"))) for f in ("karman","illusion")}
|
||||
for x,f,title in ((.05,"karman","Kármán shared family"),(.52,"illusion","Illusion shared numerical family")):
|
||||
fig.patches.append(plt.Rectangle((x,.25),.43,.34,transform=fig.transFigure,fc="white",ec="#CAD5DF",lw=1.2))
|
||||
fig.text(x+.025,.545,title,fontsize=18,weight="bold",color=BLUE if f=="karman" else ORANGE)
|
||||
front,rear=formulas[f]
|
||||
fig.text(x+.025,.485,r"$\alpha_F(x)=\frac{1}{2}[f(x)-f(Gx)]$",fontsize=14,color=INK)
|
||||
fig.text(x+.025,.435,rf"$f(x)={front}$",fontsize=14,color=INK)
|
||||
fig.text(x+.025,.375,rf"$\alpha_U(x)={rear}$",fontsize=14,color=INK)
|
||||
fig.text(x+.025,.325,r"$\alpha_L(x)=-\alpha_U(Gx)$",fontsize=14,color=INK)
|
||||
note=("Persistent rear counter-rotation constant dominates;\nrear lift correction is secondary." if f=="karman" else "Deployable shared family; deletion evidence does not\nestablish term uniqueness.")
|
||||
fig.text(x+.025,.285,note,fontsize=12,color=MUTED,linespacing=1.35)
|
||||
fig.text(.05,.12,"G mirrors upper/lower quantities and applies the signed symmetry map. α is dimensionless cylinder surface speed.",fontsize=11.5,color=MUTED)
|
||||
save(fig,out,"01_sr_method_and_formulas")
|
||||
|
||||
def page_evidence(root: Path, out: Path) -> None:
|
||||
fig=base("Quantitative evidence at a glance", "Legacy DTW similarity; deterministic frozen artifacts; control-step windows stated explicitly")
|
||||
pkg=root/"article2-plotting-package-20260721"; standard=root/"article2-timeseries-csv-20260720"; longsr=root/"article2-long-timeseries-csv-20260720"
|
||||
scenes={"Kármán":["karman_re50","karman_re100","karman_re200","karman_re400"],"Illusion":["illusion_0.75L","illusion_1L","illusion_1.5L"]}
|
||||
vals={}
|
||||
for fam,ss in scenes.items():
|
||||
sr=np.array([similarity(longsr/s/"sr_dtw_convergence.csv") for s in ss]); ppo=np.array([similarity(pkg/"long_ppo"/s/"ppo_dtw_convergence.csv") for s in ss]); vals[fam]=(sr,ppo)
|
||||
gen=rows(root/"article2-generalization-summary-20260720/generalization.csv")
|
||||
deletion=rows(pkg/"tables/term_deletion.csv")
|
||||
cards=[(.05,.61,.42,.20,"400-step training cases",[(f"Kármán SR range",f"{vals['Kármán'][0].min():.3f}–{vals['Kármán'][0].max():.3f}"),("Kármán mean SR / PPO",f"{vals['Kármán'][0].mean():.3f} / {vals['Kármán'][1].mean():.3f}"),("Illusion SR range",f"{vals['Illusion'][0].min():.3f}–{vals['Illusion'][0].max():.3f}"),("Illusion mean SR / PPO",f"{vals['Illusion'][0].mean():.3f} / {vals['Illusion'][1].mean():.3f}")]),
|
||||
(.52,.61,.43,.20,"200-step unseen conditions",[("Kármán: 4 points",f"{min(float(r['legacy_dtw']) for r in gen if r['objective']=='karman'):.3f}–{max(float(r['legacy_dtw']) for r in gen if r['objective']=='karman'):.3f}"),("Illusion: 5 points",f"{min(float(r['legacy_dtw']) for r in gen if r['objective']=='illusion'):.3f}–{max(float(r['legacy_dtw']) for r in gen if r['objective']=='illusion'):.3f}"),("Evidence level","one realization / condition"),("Scope","sampled interpolation + extrapolation")])]
|
||||
for x,y,w,h,title,items in cards:
|
||||
fig.patches.append(plt.Rectangle((x,y),w,h,transform=fig.transFigure,fc=PALE,ec="#CAD5DF"));fig.text(x+.02,y+h-.045,title,fontsize=17,weight="bold",color=INK)
|
||||
for j,(k,v) in enumerate(items): fig.text(x+.02,y+h-.09-.032*j,k,fontsize=11,color=MUTED);fig.text(x+w-.02,y+h-.09-.032*j,v,fontsize=11.5,weight="bold",color=BLUE,ha="right")
|
||||
variants=[("K front term","k_front0"),("K rear lift","k_rear0"),("K rear constant","k_rear1"),("I worst mean deletion","i_front1")]
|
||||
fig.text(.05,.52,"40-step term deletion: mean parent-relative change",fontsize=18,weight="bold",color=INK)
|
||||
for i,(label,var) in enumerate(variants):
|
||||
sub=[r for r in deletion if r["variant"]==var]; delta=np.mean([float(r["delta_vs_parent_40_step"]) for r in sub]);x=.05+i*.225
|
||||
fig.patches.append(plt.Rectangle((x,.35),.20,.12,transform=fig.transFigure,fc="white",ec="#CAD5DF"));fig.text(x+.015,.43,label,fontsize=11.5,color=MUTED);fig.text(x+.015,.375,f"{delta:+.3f}",fontsize=22,weight="bold",color=ORANGE if delta<-.03 else BLUE)
|
||||
fig.text(.05,.235,"Read with care",fontsize=17,weight="bold",color=INK)
|
||||
cautions=["40-step deletion, 200-step generalization, and 400-step training summaries answer different questions.","Legacy DTW is an aligned trajectory-similarity metric—not causal proof or a physical delay estimate.","Unseen conditions are pointwise deterministic runs; no continuous parameter law or uncertainty band is claimed.","Offline action RMSE is intentionally omitted: imitation on PPO-visited states is not article performance."]
|
||||
for i,t in enumerate(cautions): fig.text(.07,.195-.038*i,"• "+t,fontsize=11.5,color=MUTED)
|
||||
save(fig,out,"02_sr_quantitative_evidence")
|
||||
|
||||
def main() -> int:
|
||||
ap=argparse.ArgumentParser();ap.add_argument("--repo-root",type=Path,default=Path(__file__).resolve().parents[3]);ap.add_argument("--output-dir",type=Path,default=None);a=ap.parse_args();root=a.repo_root.resolve()/"src/SR_analysis/results/runs";out=a.output_dir or root/"article2-plotting-package-20260721/presentation";page_method(root,out);page_evidence(root,out);return 0
|
||||
if __name__=="__main__": raise SystemExit(main())
|
||||
Reference in New Issue
Block a user