diff --git a/configs/config_lbm_karman_2000x600_re50.json b/configs/config_lbm_karman_2000x600_re50.json deleted file mode 100644 index 9eb9176..0000000 --- a/configs/config_lbm_karman_2000x600_re50.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "_doc": "Karman Cloak Re50: uniform inlet, free-slip walls, 2000x600 grid. Pinball centered.", - "grid": { - "lattice_model": "D2Q9", - "nx": 2000, - "ny": 600, - "nz": 1 - }, - "physics": { - "data_type": "FP32", - "viscosity": 0.008, - "velocity": 0.01, - "rho": 1.0 - }, - "method": { - "collision": "MRT", - "streaming": "double_buffer", - "store_precision": "FP32", - "ddf_shifting": false, - "les": { - "enabled": false, - "cs": 0.16, - "closed_form": true - }, - "trt": { - "magic_param": 0.1875 - }, - "inlet": { - "profile": "uniform", - "scheme": "regularized", - "trt_neq_damp": 0.5, - "regularized_neq_damp": 0.5 - }, - "outlet": { - "mode": "neq_extrap", - "backflow_clamp": true, - "blend_alpha": 0.7, - "srt_neq_damp": 0.5 - }, - "y_wall_bc": "free_slip", - "omega_guard": { - "min": 0.01, - "max": 1.99 - } - }, - "cuda": { - "threads_per_block": 256, - "compute_capability": "auto" - } -} diff --git a/scripts/plot_sr_results.py b/scripts/plot_sr_results.py deleted file mode 100644 index f042ee0..0000000 --- a/scripts/plot_sr_results.py +++ /dev/null @@ -1,254 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate SR analysis result charts for reporting. -Output: docs/figures/SR_analysis/ (PNG files) -""" -import json, os, sys, glob -import numpy as np -import matplotlib -matplotlib.use("Agg") -import matplotlib.pyplot as plt -import matplotlib.ticker as ticker - -_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) -_OUT = os.path.join(_REPO, "docs", "figures", "SR_analysis") -os.makedirs(_OUT, exist_ok=True) - -# --------------------------------------------------------------------------- -# Data -# --------------------------------------------------------------------------- - -# Illusion three-scenario comparison (new SR route: phase-state + error-state + absolute) -illusion_results = { - "0.75L": {"old_v23": 0.908, "new_phase": 0.974, "ppo": 0.972, "model": "d1a3o14_250525_imit_075L_2U_400S", "S": 400}, - "1L": {"old_v23": 0.962, "new_phase": 0.958, "ppo": 0.973, "model": "d1a3o14_250525_imit_1L_2U_600S", "S": 600}, - "1.5L": {"old_v23": 0.926, "new_phase": "N/A", "ppo": 0.945, "model": "d1a3o14_250525_imit_15L_2U", "S": 800}, -} - -# Karman ablation -karman_ablation = { - "old v23\n(a_lag+da)": 0.901, - "static->deriv\n(8dim)": 0.745, - "full-lag->deriv\n(16dim)": 0.619, - "phase->deriv\n(6dim)": 0.656, - "phase->abs\n(6dim)": 0.699, - "phase+mu->abs\n(9dim)": 0.700, - "expanded->abs\n(10dim)": 0.580, -} - -# Karman generalization -karman_gen = { - "Re25": 0.567, "Re50": 0.582, "Re70": 0.577, - "Re100": 0.901, "Re150": 0.595, "Re200": 0.793, - "Re300": 0.541, "Re400": 0.664 -} - -# Ablation one-step R2 -ablation_r2 = { - "static\n0": 0.321, - "phase\n6": 0.837, - "phase+abs\n6": 0.965, - "full-lag\n16": 0.939, - "expanded\n10": 0.980, - "phase+mu\n9": 0.979, -} - -# --------------------------------------------------------------------------- -# Color scheme -# --------------------------------------------------------------------------- -C_OLD = "#d62728" # red -C_NEW_PHASE = "#2ca02c" # green -C_PPO = "#1f77b4" # blue -C_DERIV = "#ff7f0e" # orange -C_ABS = "#2ca02c" # green -C_GEN = "#9467bd" # purple -C_BG = "#f0f0f0" - -# --------------------------------------------------------------------------- -# Fig 1: Illusion comparison bar chart -# --------------------------------------------------------------------------- -fig, ax = plt.subplots(figsize=(10, 5)) -labels = list(illusion_results.keys()) -x = np.arange(len(labels)) -w = 0.25 - -old_vals = [illusion_results[k]["old_v23"] for k in labels] -new_vals = [illusion_results[k]["new_phase"] for k in labels] -ppo_vals = [illusion_results[k]["ppo"] for k in labels] - -# Convert N/A to NaN for plotting -new_vals_plot = [v if isinstance(v, (int, float)) else 0 for v in new_vals] - -ax.bar(x - w, old_vals, w, label="Old v23 (动作历史)", color=C_OLD, alpha=0.8) -ax.bar(x, new_vals_plot, w, label="New phase-state (无动作历史)", color=C_NEW_PHASE, alpha=0.8) -ax.bar(x + w, ppo_vals, w, label="PPO 基线", color=C_PPO, alpha=0.6) - -# 1.5L label -ax.text(x[2], 0.05, "N/A\n(bang-bang)", ha="center", va="bottom", fontsize=9, color="gray") - -ax.set_xticks(x) -ax.set_xticklabels(labels) -ax.set_ylabel("闭环相似度 (DTW)") -ax.set_title("Figure 1: Illusion 三场景 — 新路线 vs 旧版 vs PPO 基线", fontsize=12) -ax.legend(fontsize=9) -ax.set_ylim(0, 1.05) -ax.grid(axis="y", alpha=0.3) -fig.tight_layout() -fig.savefig(os.path.join(_OUT, "fig1_illusion_comparison.png"), dpi=150) -print("Saved fig1_illusion_comparison.png") -plt.close(fig) - -# --------------------------------------------------------------------------- -# Fig 2: Karman ablation bar chart -# --------------------------------------------------------------------------- -fig, ax = plt.subplots(figsize=(12, 4.5)) -keys = list(karman_ablation.keys()) -vals = list(karman_ablation.values()) -colors = [] -for k in keys: - if "old" in k.lower(): colors.append(C_OLD) - elif "abs" in k: colors.append(C_ABS) - elif "deriv" in k: colors.append(C_DERIV) - else: colors.append("#7f7f7f") - -bars = ax.bar(range(len(keys)), vals, color=colors, alpha=0.85) -ax.axhline(y=0.901, color=C_OLD, linestyle="--", alpha=0.5, label="Old v23 baseline (0.901)") -ax.set_xticks(range(len(keys))) -ax.set_xticklabels(keys, fontsize=8, rotation=20, ha="right") -ax.set_ylabel("闭环相似度") -ax.set_title("Figure 2: Karman re100 消融实验 — 输入/输出形式对比", fontsize=12) -ax.legend(fontsize=9) -ax.set_ylim(0, 1.0) -ax.grid(axis="y", alpha=0.3) - -# Add red dashed line at 0.699 highlighting best new route -ax.axhline(y=0.699, color=C_ABS, linestyle=":", alpha=0.5) -ax.text(5.5, 0.705, "phase+abs 最佳: 0.699", fontsize=8, color=C_ABS) - -fig.tight_layout() -fig.savefig(os.path.join(_OUT, "fig2_karman_ablation.png"), dpi=150) -print("Saved fig2_karman_ablation.png") -plt.close(fig) - -# --------------------------------------------------------------------------- -# Fig 3: Karman generalization across Re -# --------------------------------------------------------------------------- -fig, ax = plt.subplots(figsize=(10, 4.5)) -re_vals = sorted(karman_gen.keys(), key=lambda s: int(s.replace("Re",""))) -sims = [karman_gen[k] for k in re_vals] - -ax.plot(range(len(re_vals)), sims, "o-", color=C_GEN, linewidth=2, markersize=8) -# Mark training Re -train_re = [0, 3, 5, 7] # indices of Re50/100/200/400 -for i in train_re: - ax.plot(i, sims[i], "o", color=C_OLD, markersize=12, markeredgecolor="black", markeredgewidth=1.5) -ax.set_xticks(range(len(re_vals))) -ax.set_xticklabels(re_vals) -ax.set_ylabel("闭环相似度") -ax.set_title("Figure 3: Karman 跨 Re 泛化 (旧 v23 模型)", fontsize=12) -ax.set_xlabel("Reynolds Number (2D reference)") -ax.axhline(y=0.5, color="gray", linestyle=":", alpha=0.5) -ax.grid(axis="y", alpha=0.3) -ax.set_ylim(0, 1.0) - -# Annotations -ax.annotate("训练 Re", xy=(1.8, 0.92), fontsize=9, color=C_OLD) -ax.annotate("泛化 Re\n(未见过的)", xy=(4.5, 0.55), fontsize=9, color=C_GEN) - -fig.tight_layout() -fig.savefig(os.path.join(_OUT, "fig3_karman_generalization.png"), dpi=150) -print("Saved fig3_karman_generalization.png") -plt.close(fig) - -# --------------------------------------------------------------------------- -# Fig 4: One-step R2 vs closed-loop scatter (diagnostic) -# --------------------------------------------------------------------------- -fig, ax = plt.subplots(figsize=(7, 5.5)) - -points = [ - ("old v23", 0.996, 0.901, C_OLD), - ("static→deriv", 0.321, 0.745, C_DERIV), - ("full-lag→deriv", 0.939, 0.619, "#7f7f7f"), - ("phase→deriv", 0.837, 0.656, C_DERIV), - ("phase→abs", 0.965, 0.699, C_ABS), - ("expanded→abs", 0.980, 0.580, "#7f7f7f"), - ("phase+mu→abs", 0.979, 0.700, C_ABS), -] -for name, r2, sim, color in points: - ax.scatter(r2, sim, s=100, color=color, zorder=5) - ax.annotate(name, (r2, sim), textcoords="offset points", xytext=(5, 5), fontsize=8) - -ax.set_xlabel("One-step R²") -ax.set_ylabel("CFD 闭环相似度") -ax.set_title("Figure 4: Karman re100 — One-step R² 与闭环不一致性", fontsize=12) -ax.grid(alpha=0.3) - -# Upper-left region = good closed-loop, bad one-step (static-deriv) -# Upper-right region = good both (old v23) -# Lower-right region = good one-step, bad closed-loop (most new methods) -ax.annotate("稳健欠拟合", xy=(0.15, 0.85), fontsize=9, color="gray", fontstyle="italic") -ax.annotate("分布偏移\n(训练好, 闭环差)", xy=(0.75, 0.45), fontsize=9, color="gray", fontstyle="italic") - -fig.tight_layout() -fig.savefig(os.path.join(_OUT, "fig4_r2_vs_closedloop.png"), dpi=150) -print("Saved fig4_r2_vs_closedloop.png") -plt.close(fig) - -# --------------------------------------------------------------------------- -# Fig 5: Phase-state feature coefficients (Illusion 1L) -# --------------------------------------------------------------------------- -# Load the SINDy results for illusion 1L phase-state -try: - with open(os.path.join(_REPO, "src/SR_analysis/sindy/illusion/sindy_results_deriv.json")) as f: - sr = json.load(f) - per = sr["per_scene"]["illusion_1L"] - fn_f = per["feature_names_front"] - coef_f = per["front"]["best_coef"][:len(fn_f)] - - fig, ax = plt.subplots(figsize=(8, 4.5)) - # Sort by |coef| - pairs = sorted(zip(fn_f, coef_f), key=lambda p: -abs(p[1])) - names = [p[0] for p in pairs] - vals = [p[1] for p in pairs] - colors_bar = [C_NEW_PHASE if v > 0 else C_OLD for v in vals] - ax.barh(range(len(names)), vals, color=colors_bar, alpha=0.8) - ax.set_yticks(range(len(names))) - ax.set_yticklabels(names) - ax.axvline(x=0, color="black", linewidth=0.5) - ax.set_xlabel("系数值") - ax.set_title("Figure 5: Illusion 1L Front — Phase-state 特征系数", fontsize=12) - ax.grid(axis="x", alpha=0.3) - fig.tight_layout() - fig.savefig(os.path.join(_OUT, "fig5_illusion_coefficients.png"), dpi=150) - print("Saved fig5_illusion_coefficients.png") - plt.close(fig) -except Exception as e: - print(f"fig5 skipped: {e}") - -# --------------------------------------------------------------------------- -# Fig 6: Summary timeline / roadmap -# --------------------------------------------------------------------------- -fig, ax = plt.subplots(figsize=(10, 3.5)) -phases = [ - ("Phase 0\nBug Audit", "2026-06-12\n12 bugs\nfixed", 0.8), - ("Phase 1\nTarget fix", "2026-06-13\nIllusion target\ninfo added", 0.85), - ("Phase 2\nAblation", "2026-06-14\nPhase-state\nvalidated", 0.90), - ("Phase 2b\nOutput mode", "2026-06-15\nPhase+abs\nIllusion 0.97", 0.95), - ("Phase 3\nIllusion SR", "Next\nPySR on\n0.75L/1L", 0.7), -] -y_pos = 1 -for i, (label, desc, conf) in enumerate(phases): - color = plt.cm.RdYlGn(conf) - ax.barh(y_pos, 1, left=i, height=0.5, color=color, alpha=0.8) - ax.text(i + 0.5, y_pos, label, ha="center", va="center", fontsize=8, fontweight="bold") - ax.text(i + 0.5, y_pos - 0.3, desc, ha="center", va="top", fontsize=6, color="gray") -ax.set_xlim(0, len(phases)) -ax.set_ylim(0, 2) -ax.axis("off") -ax.set_title("Figure 6: 研究进展路线图", fontsize=12) -fig.tight_layout() -fig.savefig(os.path.join(_OUT, "fig6_roadmap.png"), dpi=150) -print("Saved fig6_roadmap.png") -plt.close(fig) - -print(f"\nAll figures saved to {_OUT}/") diff --git a/src/SR_analysis/PIPELINE.md b/src/SR_analysis/PIPELINE.md index e5117a4..71bdb69 100644 --- a/src/SR_analysis/PIPELINE.md +++ b/src/SR_analysis/PIPELINE.md @@ -1,42 +1,24 @@ -# SR Analysis Pipeline +# SR_analysis Pipeline -> Symbolic regression pipeline for extracting interpretable DRL control laws (obs → act) from the fluidic pinball. -> Four independent stages: inference → fitting → validation → analysis. +> Symbolic regression pipeline for extracting interpretable DRL control laws (obs -> act) +> from the fluidic pinball. Four independent stages: inference -> fitting -> validation -> analysis. ## Pipeline Architecture ``` -[PPO 推理] [PySR 拟合] [CFD 验证] [分析/画图] -stage_1_infer.py stage_2_fit.py stage_3_validate.py stage_4_analyze.py - ↓ ↓ ↓ ↓ -controlled.npz formulas/*.json validations/*.json data/figures/*.png -``` - -## Quick Start - -```bash -# 1. Generate PPO data -conda run -n pycuda_3_10 python stage_1_infer.py --scene karman_re100 --device 2 - -# 2. Fit PySR formula -conda run -n sr_env python stage_2_fit.py --scene karman_re100 --mode per-scene - -# 3. Validate in CFD -conda run -n pycuda_3_10 python stage_3_validate.py \ - --scene karman_re100 --device 2 --mode pysr \ - --formula-front results/formulas/karman_re100_front.json \ - --formula-top results/formulas/karman_re100_top.json - -# 4. Analyze results -conda run -n pycuda_3_10 python stage_4_analyze.py --scene karman_re100 --mode ppo-viz +[PPO Inference] [PySR Fitting] [CFD Validation] [Analysis/Figures] +stage_1_infer.py -> stage_2_fit.py -> stage_3_validate.py -> stage_4_analyze.py + | | | | +controlled.npz formulas/*.json validations/*.json figures/*.png+pdf +target.npz FIGURE_INDEX.md ``` ## Environments -| Env | Used For | -|-----|----------| -| `pycuda_3_10` | Stage 1 (CFD inference), Stage 3 (CFD validation), Stage 4 (analysis) | -| `sr_env` | Stage 2 (PySR symbolic regression) | +| Env | Used For | Key Packages | +|-----|----------|-------------| +| `pycuda_3_10` | Stage 1 (CFD inference), Stage 3 (CFD validation), Stage 4 (analysis) | pycuda, numpy, matplotlib, torch, stable-baselines3 | +| `sr_env` | Stage 2 (PySR symbolic regression) | pysr, numpy, sympy | GPU: device 2 recommended (device 0 may conflict with PyTorch). @@ -45,100 +27,184 @@ GPU: device 2 recommended (device 0 may conflict with PyTorch). ### Reynolds Number - Code Re uses reference length 2D = 40: `Re = U0 * 40 / nu` - Physical Re_D uses D = 20: `Re_D = Re / 2` -- Default: Re_code=100 → Re_D=50, nu=0.004 +- Default: Re_code=100 -> Re_D=50, nu=0.004 ### Action - `controlled.npz` stores actions as **normalized [-1, +1]** (not physical omega) -- Physical omega: `omega = (action * scale + bias) * U0`, then divided by radius for angular velocity -- Fitting target: **non-dimensional alpha = omega / U0** (not omega) +- Physical omega: `omega = (action * scale + bias) * U0` +- Fitting target: **non-dimensional alpha = (omega * radius) / (surface_vel * U0)** -### Action Bias vs FIFO Bias -- **DRL action decoder bias**: `action * scale + bias` → physical omega. Karman: [0,-4,4], Illusion: [0,-2,2], Vortex: [0,-4,4] -- **FIFO initialization bias** (environment warmup): different values! Illusion FIFO uses [0, -U0, U0] (1U scale), not [0, -2U0, 2U0] +### Action Decoder Bias (scene-specific) +| Scene | Scale | Bias | +|-------|:-----:|------| +| Karman | 8 | [0, -4, 4] | +| Illusion | 8 | [0, -2, 2] | +| Vortex | 4 | [0, -4, 4] | + +### G-Mirror Symmetry +- Correct: `[aF, aT, aB] -> [-aF, -aB, -aT]` +- v23 structure: Front no-bias, rear shared-head (alpha_B = -Top composed with G) ### Inlet - Parabolic velocity profile (not uniform). Top/bottom walls are no-slip bounce-back. - U0 = 0.01 at centerline (lattice units) -### G-mirror -- Correct: `[aF, aT, aB] → [-aF, -aB, -aT]` (not the old buggy version) -- v23 structure: Front no-bias (α_F = 0 when features = 0), rear shared-head (α_B = -Top∘G) - -### Norm -- Each scene computes its own force/sensor normalization during environment initialization -- Must use the **same norm values** during inference and during validation -- Norm values are saved in `norm.json` in each data directory - -### Sample Interval & Steps -| Scene | SI | Validation Steps | -|-------|:--:|:----------------:| +### Sample Interval & Validation Steps +| Scene | SI | Validation Steps (rule: >= NX/U0/SI) | +|-------|:--:|:-------------------------------------:| | Karman | 800 | 160-200 | | Illusion 0.75L | 400 | 320 | | Illusion 1L | 600 | 214 | | Illusion 1.5L | 800 | 160 | | Vortex | 800 | 150 (transient) | -Rule: steps ≥ NX/U0/SI = 1280/0.01/SI ≈ 128000/SI +--- -## Results Index +## Stage 1: PPO Inference Data Generation -All results indexed in [`scene_registry.json`](scene_registry.json). Canonical formulas in `results/formulas/`, CFD validations in `results/validations/`. +Generates `controlled.npz` (sensors, forces, actions) and `target.npz` for all scenes. -### Canonical Formulas +```bash +# Karman cloak (all trained Reynolds numbers) +for re in 50 100 200 400; do + conda run -n pycuda_3_10 python stage_1_infer.py --scene karman_re${re} --device 2 +done -| Formula File | Scene | Formula | -|-------------|-------|---------| -| `results/formulas/karman_joint_front.json` | Karman cross-Re (joint) | `daF_dt - 14.952*mu*Cl_tot` | -| `results/formulas/karman_joint_top.json` | Karman cross-Re (joint) | `3.414` (constant) | -| `results/formulas/illusion_joint_front.json` | Illusion joint (0.75L+1L) | `Cd_tot - (Cd_err + 5.428) - 0.00978*(du_a_dt + u_a)` | -| `results/formulas/illusion_joint_top.json` | Illusion joint (0.75L+1L) | `(Cd_err - (Cd_rear - Cl_err))*0.535 + 2.782` | +# Illusion (trained diameters) +for d in 0.75 1.0; do + conda run -n pycuda_3_10 python stage_1_infer.py --scene illusion_${d}L --device 2 +done -### Key CFD Results +# Illusion target-only (generalization diameters -- no PPO model, just target recording) +for d in 0.5 0.6 0.8 1.2 1.5 2.0; do + conda run -n pycuda_3_10 python stage_1_infer.py --scene illusion_${d}L --target-only --device 2 +done -| Scene | Formula | Similarity | -|-------|---------|:----------:| -| Karman cross-Re avg | Joint | 0.847 | -| Illusion 0.75L | Joint | 0.978 | -| Illusion 1L | Joint | 0.970 | -| Vortex lamb | Karman joint | 0.949 (exceeds PPO 0.942) | -| Illusion 0.6L | Joint (generalization) | 0.939 | -| Illusion 0.8L | Joint (generalization) | 0.908 | -| Illusion 1.2L | Joint (generalization) | 0.849 | -| Illusion 2L | Joint (generalization) | 0.676 | +# Vortex +for v in lamb taylor; do + conda run -n pycuda_3_10 python stage_1_infer.py --scene vortex_${v} --device 2 +done +``` -### Feature Sets +**Output per scene** (in `data/{scene_id}/{scene_name}/`): +- `controlled.npz`: actions [N,3], sensors [N,6], forces [N,6] +- `target.npz`: target sensor signals [FIFO_LEN, 6] +- `norm.json`: normalization factors +- `result.json`: similarity + reward summary +- `target_harmonics.json` (Illusion only): FFT harmonics for force reconstruction +--- + +## Stage 2: PySR Symbolic Regression Fitting + +Runs PySR on `controlled.npz` data to discover interpretable control laws. + +```bash +# Illusion joint (0.75L + 1.0L) -- primary contribution +conda run -n sr_env python stage_2_fit.py \ + --scenes illusion_0.75L,illusion_1L --mode joint --deep + +# Karman cross-Re joint (re50-400) +conda run -n sr_env python stage_2_fit.py \ + --scenes karman_re50,karman_re100,karman_re200,karman_re400 --mode joint --deep + +# Per-scene individual fitting (optional, for comparison) +conda run -n sr_env python stage_2_fit.py --scene karman_re100 --mode per-scene --deep +conda run -n sr_env python stage_2_fit.py --scene illusion_0.75L --mode per-scene --deep +``` + +**Feature sets**: | Name | Features | Dim | Used For | |------|----------|:---:|----------| -| PHASE_STATE_KEYS | u_a, du_a_dt, Cl_tot, dCl_tot_dt, Cd_tot, Cd_rear | 6 | Karman per-Re | -| ILLUSION_PHASE_KEYS | phase-state + Cd_err, Cl_err, dCd_err_dt, dCl_err_dt | 10 | Illusion | -| PHYS_DADT | physics + daF_dt, daB_dt, daT_dt + mu | 17 | Karman joint/deep | +| ILLUSION_PHASE | u_a, du_a/dt, Cl_tot, dCl_tot/dt, Cd_tot, Cd_rear, Cd_err, Cl_err, dCd_err/dt, dCl_err/dt | 10 | Illusion | +| PHYS_DADT+mu | Static + daF/dt, daB/dt, daT/dt + mu terms | 17 | Karman joint | -## Key Documentation +**Constraints (v23)**: +- Front no-bias: alpha_F = 0 when features = 0 +- Rear shared-head: alpha_B = -Top composed with G-mirror -| File | Content | -|------|---------| -| `PIPELINE.md` | This file — overview, environment, conventions | -| `sindy_sr_knowledge.md` | Bug history, confirmed facts, known limitations | -| `sindy_sr_notes.md` | Task list, current status | -| `STAGE_1_INFER.md` | Stage 1: PPO data generation | -| `STAGE_2_FIT.md` | Stage 2: PySR fitting | -| `STAGE_3_VALIDATE.md` | Stage 3: CFD validation | -| `STAGE_4_ANALYZE.md` | Stage 4: Analysis & visualization | -| `docs/SR_analysis_report.md` | Full report (465+ lines) | -| `docs/illusion_joint_formula_analysis.md` | Illusion joint formula deep dive | +**Output**: `results/formulas/{scene}_{channel}.json` -## Stage 0 Audit (2026-06-28) +--- -All imports verified in both conda environments: +## Stage 3: CFD Closed-Loop Validation -| Script | pycuda_3_10 | sr_env | -|--------|:-----------:|:------:| -| `stage_1_infer.py` (infer_karman / infer_illusion / infer_vortex) | OK | — | -| `stage_2_fit.py` (PySR) | — | OK | -| `stage_3_validate.py` (closed-loop) | OK | — | -| `stage_4_analyze.py` (analysis) | OK | — | -| `core/features.py` (feature_builder) | OK | OK | -| `core/cfd.py` (cfd_interface) | OK | — | +Validates PySR formulas or PPO baselines in closed-loop CFD. This is the final arbiter. -No broken imports. All dependencies available. +```bash +# Karman cross-Re (trained + generalization) +for re in 50 100 200 400 25 70 150 300; do + conda run -n pycuda_3_10 python stage_3_validate.py \ + --scene karman_re${re} --device 2 --mode pysr \ + --formula-front results/formulas/karman_joint_front.json \ + --formula-top results/formulas/karman_joint_top.json +done + +# Illusion trained (joint formula) +for d in 0.75 1.0; do + conda run -n pycuda_3_10 python stage_3_validate.py \ + --scene illusion_${d}L --device 2 --mode pysr \ + --formula-front results/formulas/illusion_joint_front.json \ + --formula-top results/formulas/illusion_joint_top.json +done + +# Illusion generalization (joint formula on unseen diameters) +for d in 0.5 0.6 0.8 1.2 2.0; do + conda run -n pycuda_3_10 python stage_3_validate.py \ + --scene illusion_${d}L --device 2 --mode pysr \ + --formula-front results/formulas/illusion_joint_front.json \ + --formula-top results/formulas/illusion_joint_top.json +done + +# PPO baselines +for d in 0.75 1.0 1.5; do + conda run -n pycuda_3_10 python stage_3_validate.py \ + --scene illusion_${d}L --device 2 --mode ppo +done + +# Vortex generalization +for v in lamb taylor; do + conda run -n pycuda_3_10 python stage_3_validate.py \ + --scene vortex_${v} --device 2 --mode pysr \ + --formula-front results/formulas/karman_joint_front.json \ + --formula-top results/formulas/karman_joint_top.json +done +``` + +**Output**: `results/validations/{scene_name}.json` with similarity scores. + +--- + +## Stage 4: Publication Figures + +Generates all figures and the figure index. No CLI arguments needed. + +```bash +conda run -n pycuda_3_10 python stage_4_analyze.py +``` + +**Output** (in `results/figures/`): +- `fig_illusion_degradation.png/pdf` — Main result: cross-diameter generalization +- `fig_karman_cross_re.png/pdf` — Karman cross-Re validation bars +- `fig_formula_comparison.png/pdf` — Formula structure diagram +- `fig_vortex_generalization.png/pdf` — Vortex cross-scene transfer +- `fig_action_comparison.png/pdf` — PPO action timeseries +- `fig_master_table.png/pdf` — Complete results table +- `FIGURE_INDEX.md` — Figure catalog with paper-ready captions + +--- + +## Canonical Formulas + +| Formula File | Scene | Formula | CFD Similarity | +|-------------|-------|---------|:---:| +| `results/formulas/karman_joint_front.json` | Karman cross-Re | `daF_dt - 14.952*mu*Cl_tot` | avg 0.847 | +| `results/formulas/karman_joint_top.json` | Karman cross-Re | `3.414` (constant) | — | +| `results/formulas/illusion_joint_front.json` | Illusion joint | `Cd_tot - (Cd_err + 5.428) - (-0.00978)*(du_a_dt + u_a)` | 0.978/0.970 | +| `results/formulas/illusion_joint_top.json` | Illusion joint | `(Cd_err - (Cd_rear - Cl_err))*0.535 + 2.782` | — | + +## Known Limitations + +- **Karman rear formula is constant** (alpha_T = 3.414): rear control information not fully utilized by the joint formula. +- **Illusion 1.5L is non-fittable**: PPO policy uses high-frequency modulation (5.6x shedding frequency) that the current feature set cannot capture. +- **daB_dt in Karman front formula** must be removed for deployment: it's a training distribution artifact (PPO trajectories have correlated front/rear actions, but rear is constant at deployment). diff --git a/src/SR_analysis/README.md b/src/SR_analysis/README.md index 3361218..45dc028 100644 --- a/src/SR_analysis/README.md +++ b/src/SR_analysis/README.md @@ -1,84 +1,81 @@ -# SR_analysis: Symbolic Regression Analysis Pipeline +# SR_analysis: Symbolic Regression for DRL Flow Control -Extracts interpretable control laws (`obs -> act`) from DRL-trained policies for the -fluidic pinball. Uses **PySR symbolic regression** on dimensionless physical features with -G-equivariant structural constraints (v23: front no-bias, rear shared-head). +Extracts interpretable control laws (obs -> act) from DRL-trained PPO policies +for the fluidic pinball using PySR symbolic regression. Validates all formulas +in CFD closed-loop and produces publication-quality figures. + +## Pipeline + +``` +stage_1_infer.py -> stage_2_fit.py -> stage_3_validate.py -> stage_4_analyze.py + (PPO data) (PySR formula) (CFD closed-loop) (paper figures) +``` ## Quick Start ```bash -# 1. Generate PPO data +# Generate PPO inference data conda run -n pycuda_3_10 python stage_1_infer.py --scene karman_re100 --device 2 -# 2. Fit formula -conda run -n sr_env python stage_2_fit.py --scene karman_re100 --mode per-scene +# Fit symbolic formula +conda run -n sr_env python stage_2_fit.py --scenes illusion_0.75L,illusion_1L --mode joint --deep -# 3. Validate in CFD -conda run -n pycuda_3_10 python stage_3_validate.py \\ - --scene karman_re100 --device 2 --mode pysr \\ - --formula-front results/formulas/karman_joint_front.json \\ - --formula-top results/formulas/karman_joint_top.json +# Validate in CFD +conda run -n pycuda_3_10 python stage_3_validate.py \ + --scene illusion_1L --device 2 --mode pysr \ + --formula-front results/formulas/illusion_joint_front.json \ + --formula-top results/formulas/illusion_joint_top.json -# 4. Analyze -conda run -n pycuda_3_10 python stage_4_analyze.py --scene karman_re100 --mode ppo-viz -``` - -## Pipeline Architecture - -``` -stage_1_infer.py → stage_2_fit.py → stage_3_validate.py → stage_4_analyze.py - (PPO数据) (PySR拟合) (CFD闭环验证) (分析/画图) +# Generate figures +conda run -n pycuda_3_10 python stage_4_analyze.py ``` ## Directory Structure ``` SR_analysis/ - PIPELINE.md # 总览文档 (入口) - README.md # 本文件 - sindy_sr_knowledge.md # 知识库 (bugs, 事实, 结果) - sindy_sr_notes.md # 任务清单 - scene_registry.json # 所有场景的规范结果索引 - configs.py # 场景注册表 - core/ # 共享工具库 - features.py # 特征构建 (无量纲化+phase-state) - fitting.py # STLSQ拟合+特征矩阵 - cfd.py # LegacyCelerisLab接口 - g_operator.py # G-mirror变换 - data/ # 运行时生成的.npz数据 + stage_1_infer.py # PPO inference (all scenes) + stage_2_fit.py # PySR symbolic regression fitting + stage_3_validate.py # CFD closed-loop validation + stage_4_analyze.py # Publication figure generation + configs.py # All scene definitions (19 scenes) + scene_registry.json # Canonical results registry + utils/ # Shared library (features, CFD, fitting) + data/ # Runtime .npz data per scene results/ - formulas/ # 规范公式JSON - validations/ # CFD闭环验证结果 - archive/ # 归档的中间文件 - stage_1_infer.py # Stage 1: 统一PPO推理入口 - STAGE_1_INFER.md - stage_2_fit.py # Stage 2: 统一PySR拟合入口 - STAGE_2_FIT.md - stage_3_validate.py # Stage 3: 统一CFD闭环验证入口 - STAGE_3_VALIDATE.md - stage_4_analyze.py # Stage 4: 统一分析/画图入口 - STAGE_4_ANALYZE.md + formulas/ # Canonical formula JSONs + validations/ # CFD validation outputs + figures/ # Publication-quality PNG/PDF + FIGURE_INDEX.md # Figure catalog with captions + README.md # Formula + validation index + docs/ + SR_analysis_report.md # Full analysis report + illusion_joint_formula_analysis.md + PIPELINE.md # Detailed reproduction guide + literature_note.md # Paper writing + literature positioning + old/ # Archived historical files ``` ---- +## Key Results -## Key Design Decisions +| Scene | Formula | CFD Similarity | +|-------|---------|:---:| +| Karman cross-Re (joint) | alpha_F = daF_dt - 14.95*mu*Cl_tot | avg 0.847 | +| Illusion joint (0.75L+1L) | alpha_F = Cd_tot - Cd_err - 5.43 + 0.01*(du_a_dt+u_a) | 0.978/0.970 | +| Vortex lamb (Karman formula) | Karman joint, zero retraining | 0.949 (exceeds PPO) | -1. **Feature levels**: Static (8-dim) -> Phase-state (6-dim) -> Illusion-phase (10-dim) -2. **Output target**: Non-dimensional alpha, not physical omega -3. **v23 structure**: Front no-bias, rear shared-head (Bottom = -Top(Gx)) -4. **Final judge**: CFD closed-loop similarity, not one-step R2 +## Environments -## Key Documentation +- `pycuda_3_10`: CFD + DRL model loading + visualization (stages 1, 3, 4) +- `sr_env`: PySR symbolic regression (stage 2) +- GPU: device 2 recommended + +## Documentation | File | Content | |------|---------| -| `PIPELINE.md` | **Primary entry** — pipeline overview, environment, conventions | -| `sindy_sr_knowledge.md` | Bug history, confirmed facts, known limitations | -| `sindy_sr_notes.md` | Task list, current status | -| `docs/SR_analysis_report.md` | Full report (465+ lines) | -| `docs/illusion_joint_formula_analysis.md` | Illusion joint formula deep dive | - -## Core Files (≤20) - -`stage_1_infer.py`, `stage_2_fit.py`, `stage_3_validate.py`, `stage_4_analyze.py`, `configs.py`, `scene_registry.json`, `core/features.py`, `core/fitting.py`, `core/cfd.py`, `core/g_operator.py` + 8 docs. +| `PIPELINE.md` | Full reproduction guide with all commands | +| `docs/SR_analysis_report.md` | Complete methodology + results + discussion | +| `results/figures/FIGURE_INDEX.md` | All figures with paper-ready captions | +| `literature_note.md` | Literature positioning + writing guidance | +| `results/README.md` | Formula + validation file index | diff --git a/src/SR_analysis/configs.py b/src/SR_analysis/configs.py index 2d1aac2..71fbea3 100644 --- a/src/SR_analysis/configs.py +++ b/src/SR_analysis/configs.py @@ -16,7 +16,7 @@ from typing import Any, Dict, List, Optional, Tuple # -- Root paths (resolved when configs.py is imported) ----------------------- _PROJ = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) MODEL_DIR = os.path.join(_PROJ, "..", "models") -LEGACY_CFG_DIR = os.path.join(os.path.dirname(__file__), "configs", "legacy") +LEGACY_CFG_DIR = os.path.join(_PROJ, "..", "configs", "legacy_configs") # -- Physics constants ------------------------------------------------------- U0 = 0.01 # default inlet velocity (lattice) diff --git a/src/SR_analysis/core/cfd.py b/src/SR_analysis/core/cfd.py deleted file mode 100644 index a0b0f5e..0000000 --- a/src/SR_analysis/core/cfd.py +++ /dev/null @@ -1,403 +0,0 @@ -"""CFD interface for LegacyCelerisLab (pycuda_3_10 env). - -All functions use the LegacyCelerisLab (old) CFD API via: - from LegacyCelerisLab import FlowField - -Must be run inside: conda run -n pycuda_3_10 - -NOTE: This module should be imported directly, not through SR_analysis.utils -because it requires pycuda. Other utils (sindy_fitter, feature_builder, g_operator) -do NOT require pycuda and can be imported from the __init__. -""" -from __future__ import annotations - -import json -import os -import sys -from collections import deque -from typing import Any, Dict, List, Optional, Tuple - -import numpy as np - -# -- Import legacy CFD ------------------------------------------------------- -# LegacyCelerisLab lives at the repo root; SR_analysis is at repo_root/src/SR_analysis. -_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) -if _REPO not in sys.path: - sys.path.insert(0, _REPO) -_SRC = os.path.join(_REPO, "src") -if _SRC not in sys.path: - sys.path.insert(0, _SRC) - -from LegacyCelerisLab import FlowField # noqa: E402 -from LegacyCelerisLab import utils as legacy_utils # noqa: E402 - -# --------------------------------------------------------------------------- -# Action-smoothing constant (legacy run() internal) -# --------------------------------------------------------------------------- -ACTION_SMOOTH_WEIGHT = 0.1 # used by FlowField.run() internally - - -def nu_from_re(re_code: float, u0: float = 0.01, d_ref: float = 40.0) -> float: - """Return kinematic viscosity for a given code Reynolds number. - - ``re_code`` uses reference length *2*D* = 40.0 (matching model file naming). - """ - return u0 * d_ref / re_code - - -def load_legacy_configs(config_dir: str) -> Tuple[Any, Any]: - """Load and return legacy (cuda_config, field_config) from *config_dir*.""" - cuda_cfg = legacy_utils.load_cuda_config( - os.path.join(config_dir, "config_cuda.json") - ) - field_cfg = legacy_utils.load_flow_field_config( - os.path.join(config_dir, "config_flowfield.json") - ) - return cuda_cfg, field_cfg - - -# --------------------------------------------------------------------------- -# Environment helpers -- Karman cloak (disturbance cylinder + pinball) -# --------------------------------------------------------------------------- - -def build_karman_cloak_env( - flow_field: FlowField, - *, - u0: float, - l0: float, - sample_interval: int, - fifo_len: int, - data_type: type, -) -> Tuple[np.ndarray, dict]: - """Phase 0-1: add dist-cylinder & 3 sensors, stabilize, record target. - - Steps (mirrors env_karman_cloak_standard.__init__): - 1. add dist_cylinder (id=0) - 2. add 3 sensors (id=1,2,3) - 3. stabilize run(4*NX/U0, zero-action[4]) - 4. record FIFO_LEN x run(SAMPLE_INTERVAL, zero[4]), collect obs[2:8] - - Returns - ------- - target_states : ndarray (FIFO_LEN, 6) -- sensor0/1/2 ux,uy - info : dict with n_objects, NX, NY - """ - # dist cylinder - center = (10.0 * l0, (flow_field.FIELD_SHAPE[1] - 1) / 2, 0.0) - flow_field.add_cylinder(center, l0) - - # sensors - for y_off in [2.0, 0.0, -2.0]: - sc = (40.0 * l0, (flow_field.FIELD_SHAPE[1] - 1) / 2 + y_off * l0, 0.0) - flow_field.add_sensor(sc, l0 / 4.0) - - n_obj = flow_field.obs.size // 2 - - # stabilize - stabilize_steps = int(4 * flow_field.FIELD_SHAPE[0] / u0) - print(f" stabilising ({stabilize_steps} steps)...") - flow_field.run(stabilize_steps, np.zeros(n_obj, dtype=data_type)) - - # record target (only sensor signals = obs[2:8]) - target_states = np.empty((0, 6), dtype=data_type) - for _ in range(fifo_len): - flow_field.run(sample_interval, np.zeros(n_obj, dtype=data_type)) - new_state = flow_field.obs.copy()[2:8] - target_states = np.vstack((target_states, new_state)) - - print(f" target recorded: {target_states.shape}") - return target_states, {"n_objects": n_obj, "NX": flow_field.FIELD_SHAPE[0], - "NY": flow_field.FIELD_SHAPE[1]} - - -def add_pinball( - flow_field: FlowField, - *, - l0: float, - u0: float, - sample_interval: int, - fifo_len: int, - data_type: type, - action_bias: Optional[Tuple[float, float, float]] = None, - pinball_front_x: float = 30.0, - pinball_rear_x: float = 31.3, - obs_slice_start: int = 2, - obs_slice_end: int = 14, - n_objects_total: Optional[int] = None, -) -> dict: - """Add pinball cylinders, stabilize, compute norm, bias rollout. - - Steps: - 1. add front, bottom, top cylinders - 2. stabilize run(4*NX/U0, zero-action) - 3. get_ddf() + save_ddf() (checkpoint) - 4. FIFO_LEN x run(SAMPLE_INTERVAL, zero) -> compute norm - 5. apply_ddf() (restore pre-bias state) - 6. FIFO_LEN x run(SAMPLE_INTERVAL, bias-action) -> save_states - 7. apply_ddf() - - Parameters - ---------- - pinball_front_x, pinball_rear_x : pinball geometry (L0 units). - Default 30.0/31.3 for Karman; 19.0/20.3 for Illusion. - obs_slice_start, obs_slice_end : slice of obs for norm. - Default [2:14] for Karman (7 objects); [0:12] for Illusion (6 objects). - n_objects_total : if provided, used for bias array length. - Default: inferred from flow_field after adding cylinders. - - Returns dict with norm values. - """ - if action_bias is None: - action_bias = (0.0, -4.0, 4.0) - - u0_float = float(u0) - - # add 3 pinball cylinders - ny = flow_field.FIELD_SHAPE[1] - centers = [ - (pinball_front_x * l0, (ny - 1) / 2, 0.0), - (pinball_rear_x * l0, (ny - 1) / 2 + 0.75 * l0, 0.0), - (pinball_rear_x * l0, (ny - 1) / 2 - 0.75 * l0, 0.0), - ] - for c in centers: - flow_field.add_cylinder(c, l0 / 2.0) - - n_obj = flow_field.obs.size // 2 if n_objects_total is None else n_objects_total - print(f" bodies after pinball: {n_obj}") - - # stabilize - stabilize_steps = int(4 * flow_field.FIELD_SHAPE[0] / u0_float) - print(f" stabilising pinball ({stabilize_steps} steps)...") - flow_field.run(stabilize_steps, np.zeros(n_obj, dtype=data_type)) - - # checkpoint DDF - flow_field.get_ddf() - flow_field.save_ddf() - - # --- norm phase (zero-action) --- - fifo = deque(maxlen=fifo_len) - for _ in range(fifo_len): - flow_field.run(sample_interval, np.zeros(n_obj, dtype=data_type)) - fifo.append(flow_field.obs.copy()[obs_slice_start:obs_slice_end]) - - temp_states = np.array(fifo, dtype=data_type) - # forces are at indices [6:12] relative to the slice end - force_start = obs_slice_end - obs_slice_start - 6 - force_end = force_start + 6 - force_norm_fact = 6.0 * float(np.max(np.abs(temp_states[:, force_start:force_end]))) - sens_deviation = np.mean(temp_states[:, 0:6], axis=0).astype(data_type) - sens_norm_fact = np.zeros(6, dtype=data_type) - for i in range(6): - sens_norm_fact[i] = 5.0 * float(np.max(np.abs(temp_states[:, i] - sens_deviation[i]))) - - print(f" norm: force_norm_fact={force_norm_fact:.6f}") - print(f" norm: sens_deviation={sens_deviation}") - print(f" norm: sens_norm_fact={sens_norm_fact}") - - # --- bias-action rollout --- - flow_field.apply_ddf() - bias = np.zeros(n_obj, dtype=data_type) - bias[n_obj - 3] = float(action_bias[0] * u0_float) - bias[n_obj - 2] = float(action_bias[1] * u0_float) - bias[n_obj - 1] = float(action_bias[2] * u0_float) - print(f" bias action: {bias}") - - fifo.clear() - for _ in range(fifo_len): - flow_field.run(sample_interval, bias) - fifo.append(flow_field.obs.copy()[obs_slice_start:obs_slice_end]) - - save_states = np.array(list(fifo), dtype=data_type) - # CRITICAL: save DDF again AFTER bias FIFO, so restore_ddf() goes - # to the post-bias state (consistent with saved FIFO). - # Without this, reset() restores to a bare stabilized state with - # no bias history, invalidating the FIFO. - flow_field.get_ddf() - flow_field.save_ddf() - flow_field.apply_ddf() - - return { - "force_norm_fact": force_norm_fact, - "sens_deviation": sens_deviation.tolist(), - "sens_norm_fact": sens_norm_fact.tolist(), - "action_bias": list(action_bias), - "save_states": save_states, - } - - -def build_observation( - obs_slice: np.ndarray, - norm: dict, -) -> np.ndarray: - """Assemble normalised DRL observation (12-dim) from a single obs slice. - - ``obs_slice`` is 12-element: sensor[0:6] + force[6:12]. - - Returns clipped 12-dim array in [-1, 1]. - """ - forces = obs_slice[6:12] / norm["force_norm_fact"] - sens = (obs_slice[0:6] - norm["sens_deviation"]) / norm["sens_norm_fact"] - obs = np.clip(np.hstack([forces, sens]), -1.0, 1.0).astype(np.float32) - return obs - - -def action_to_physical( - action_norm: np.ndarray, - *, - scale: float = 8.0, - bias: Tuple[float, float, float] = (0.0, -4.0, 4.0), - u0: float = 0.01, -) -> np.ndarray: - """Convert normalized action [-1,1] to physical omega (lattice units). - - physical_omega[i] = (action_norm[i] * scale + bias[i]) * u0 - """ - action_norm = np.asarray(action_norm, dtype=np.float64).reshape(-1, 3) - bias_arr = np.array(bias, dtype=np.float64) - return (action_norm * scale + bias_arr) * u0 - - -def scale_action( - action_norm: np.ndarray, - *, - scale: float = 8.0, - bias: Tuple[float, float, float] = (0.0, -4.0, 4.0), - u0: float = 0.01, - n_total_bodies: int = 7, -) -> np.ndarray: - """Convert normalised action ([-1,1]^3) to legacy CFD action array. - - Returns array of length *n_total_bodies* with cylinders' omegas at the - last 3 slots. - """ - a = np.zeros(n_total_bodies, dtype=np.float32) - omega = (np.array(action_norm, dtype=np.float32) * scale + np.array(bias, dtype=np.float32)) * u0 - a[n_total_bodies - 3:] = omega - return a - - -# --------------------------------------------------------------------------- -# Vorticity & field export -# --------------------------------------------------------------------------- - -def vorticity_from_ddf(flow_field: FlowField, u0: float) -> np.ndarray: - """Compute z-vorticity from current DDF on host.""" - flow_field.get_ddf() - ddf = flow_field.ddf.copy().reshape((9, flow_field.FIELD_SHAPE[1], - flow_field.FIELD_SHAPE[0])).transpose(2, 1, 0) - ux = (ddf[:, :, 1] + ddf[:, :, 5] + ddf[:, :, 8] - - ddf[:, :, 3] - ddf[:, :, 6] - ddf[:, :, 7]) / u0 - uy = (ddf[:, :, 2] + ddf[:, :, 5] + ddf[:, :, 6] - - ddf[:, :, 4] - ddf[:, :, 7] - ddf[:, :, 8]) / u0 - omega = np.gradient(uy, axis=0) - np.gradient(ux, axis=1) - return omega.astype(np.float64) - - -def save_vorticity_png(path: str, omega: np.ndarray, title: str = ""): - """Save vorticity field as a PNG with symmetric colour bar.""" - import matplotlib - matplotlib.use("Agg") - import matplotlib.pyplot as plt - - abs_o = np.abs(omega[np.isfinite(omega)]) - vmax = float(np.percentile(abs_o, 99.5)) if abs_o.size > 0 else 1.0 - if vmax <= 0: - vmax = 1.0 - - ny, nx = omega.shape - fig, ax = plt.subplots(figsize=(min(18, max(8, nx / 60)), min(10, max(3, ny / 40)))) - im = ax.imshow(omega, origin="lower", aspect="equal", cmap="RdBu_r", - vmin=-vmax, vmax=vmax, extent=(0, nx - 1, 0, ny - 1)) - ax.set_xlabel("x (lattice)") - ax.set_ylabel("y (lattice)") - if title: - ax.set_title(title) - fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04, label=r"$\omega_z$") - fig.tight_layout() - fig.savefig(path, dpi=150, bbox_inches="tight") - plt.close(fig) - - -# --------------------------------------------------------------------------- -# DTW similarity -# --------------------------------------------------------------------------- - -def calc_lag(target: np.ndarray, state: np.ndarray) -> int: - """Find lag that maximises cross-correlation between two 1-D signals.""" - t = target - np.mean(target) - s = state - np.mean(state) - corr = np.correlate(t, s, mode="full") - lags = np.arange(-len(target) + 1, len(target)) - return int(lags[np.argmax(corr)]) - - -def calc_dtw_sim(target: np.ndarray, state: np.ndarray) -> float: - """DTW-based similarity: 1 - (DTW distance / len(target)).""" - n, m = len(target), len(state) - dtw = np.full((n + 1, m + 1), np.inf) - dtw[0, 0] = 0.0 - for i in range(1, n + 1): - for j in range(1, m + 1): - cost = abs(float(target[i - 1]) - float(state[j - 1])) - dtw[i, j] = cost + min(dtw[i - 1, j], dtw[i, j - 1], dtw[i - 1, j - 1]) - return float(1.0 - dtw[n, m] / n) - - -def compute_similarity( - target_states: np.ndarray, - state_series: np.ndarray, - conv_len: int, -) -> float: - """Compute lag-compensated DTW similarity over *conv_len* window.""" - ref = target_states[conv_len:2 * conv_len, 1] - cur = state_series[-conv_len:, 1] - lag = calc_lag(ref, cur) - - sim_sum = 0.0 - for i in range(6): - target_seq = np.roll(target_states[:, i], -lag)[conv_len:2 * conv_len] - state_seq = state_series[-conv_len:, i] - sim_sum += calc_dtw_sim(target_seq, state_seq) / 6.0 - return float(sim_sum) - - -# --------------------------------------------------------------------------- -# Dummy env for loading SB3 models -# --------------------------------------------------------------------------- - -def create_dummy_env(s_dim: int = 12, a_dim: int = 3): - """Return a gym.Env with correct observation/action spaces for model loading.""" - import gymnasium as gym - from gymnasium import spaces - - class DummyEnv(gym.Env): - def __init__(self): - super().__init__() - self.observation_space = spaces.Box(low=-1, high=1, shape=(s_dim,), dtype=np.float32) - self.action_space = spaces.Box(low=-1, high=1, shape=(a_dim,), dtype=np.float32) - - def reset(self, seed=None): - return np.zeros(s_dim, dtype=np.float32), {} - - def step(self, action): - return np.zeros(s_dim, dtype=np.float32), 0.0, False, False, {} - - def render(self): - pass - - return DummyEnv() - - -def load_ppo_model(model_path: str, device: str = "cuda:0", s_dim: int = 12, a_dim: int = 3): - """Load a PPO model with Sin activation.""" - import torch - from torch.nn import Module - from stable_baselines3 import PPO - - class Sin(Module): - def forward(self, x): - return torch.sin(x) - - dummy_env = create_dummy_env(s_dim, a_dim) - model = PPO.load(model_path, env=dummy_env, device=device) - return model diff --git a/src/SR_analysis/core/features.py b/src/SR_analysis/core/features.py deleted file mode 100644 index 8713f50..0000000 --- a/src/SR_analysis/core/features.py +++ /dev/null @@ -1,386 +0,0 @@ -"""Unified feature builder for all cloak scenes. - -Produces dimensionless features with consistent G-equivariant structure. -All scenes (Karman, steady, vortex, illusion) use this same builder. - -Copy of analysis_cloak/common/feature_builder.py -- kept as canonical source. -""" -from __future__ import annotations - -from typing import Dict, List, Optional, Tuple - -import numpy as np - - -# -- Physical constants ------------------------------------------------------ -U0 = 0.01 # inlet velocity (lattice units) -D_CYL = 20.0 # cylinder diameter (lattice) - - -# -- Dimensionless conversion ------------------------------------------------ - -def compute_dimensionless( - sensors: np.ndarray, # (T, 6) raw lattice [s0_ux,s0_uy, s1_ux,s1_uy, s2_ux,s2_uy] - forces: np.ndarray, # (T, 6) raw lattice [f0_fx,f0_fy, f1_fx,f1_fy, f2_fx,f2_fy] - u0: float = U0, - d: float = D_CYL, - rho: float = 1.0, -) -> Dict[str, np.ndarray]: - """Convert raw lattice CFD data to dimensionless quantities. - - Sensor order: [s0_ux,s0_uy, s1_ux,s1_uy, s2_ux,s2_uy] - where s0=top(y=+2L0), s1=mid(y=0), s2=bottom(y=-2L0) - Force order: [front_fx,front_fy, bottom_fx,bottom_fy, top_fx,top_fy] - - Returns: - u_hat_B, u_hat_C, u_hat_T: nondim streamwise velocity (bottom/centre/top) - v_hat_B, v_hat_C, v_hat_T: nondim crosswise velocity - Cd_F, Cd_T, Cd_B: drag coefficient per cylinder - Cl_F, Cl_T, Cl_B: lift coefficient per cylinder - """ - s = np.asarray(sensors, dtype=np.float64) - f = np.asarray(forces, dtype=np.float64) - - # Sensor positions: s0=top, s1=centre, s2=bottom - # Convention: B=bottom=s2, C=centre=s1, T=top=s0 - return { - "u_hat_T": s[:, 0] / u0, - "v_hat_T": s[:, 1] / u0, - "u_hat_C": s[:, 2] / u0, - "v_hat_C": s[:, 3] / u0, - "u_hat_B": s[:, 4] / u0, - "v_hat_B": s[:, 5] / u0, - "Cd_F": 2.0 * f[:, 0] / (rho * u0**2 * d), - "Cl_F": 2.0 * f[:, 1] / (rho * u0**2 * d), - "Cd_B": 2.0 * f[:, 2] / (rho * u0**2 * d), - "Cl_B": 2.0 * f[:, 3] / (rho * u0**2 * d), - "Cd_T": 2.0 * f[:, 4] / (rho * u0**2 * d), - "Cl_T": 2.0 * f[:, 5] / (rho * u0**2 * d), - } - - -# -- G operator (corrected) -------------------------------------------------- - -def apply_G_alpha(alpha: np.ndarray) -> np.ndarray: - """Apply mirror G to action: [aF, aT, aB] -> [-aF, -aB, -aT].""" - return np.array([-alpha[0], -alpha[2], -alpha[1]], dtype=alpha.dtype) - - -def apply_G_x(dim: Dict[str, np.ndarray], - a_prev: np.ndarray, - a_prev2: np.ndarray) -> Tuple[Dict, np.ndarray, np.ndarray]: - """Apply G to dimensionless state. - - Returns (G_dim, G_a_prev, G_a_prev2) with corrected sign rules. - """ - G_dim = { - "u_hat_B": dim["u_hat_T"], "u_hat_C": dim["u_hat_C"], "u_hat_T": dim["u_hat_B"], - "v_hat_B": -dim["v_hat_T"], "v_hat_C": -dim["v_hat_C"], "v_hat_T": -dim["v_hat_B"], - "Cd_F": dim["Cd_F"], "Cd_T": dim["Cd_B"], "Cd_B": dim["Cd_T"], - "Cl_F": -dim["Cl_F"], "Cl_T": -dim["Cl_B"], "Cl_B": -dim["Cl_T"], - } - G_a_prev = np.column_stack([-a_prev[:, 0], -a_prev[:, 2], -a_prev[:, 1]]) - G_a_prev2 = np.column_stack([-a_prev2[:, 0], -a_prev2[:, 2], -a_prev2[:, 1]]) - return G_dim, G_a_prev, G_a_prev2 - - -# -- Feature key definitions ------------------------------------------------- - -# Original feature set (includes sin_ua/cos_ua) -CORE_FEAT_KEYS = [ - "u_m", "u_a", "u_c", - "v_a", - "Cd_tot", "Cd_rear", - "Cl_tot", "Cl_diff", - "sin_ua", "cos_ua", - "aF_lag1", "aB_lag1", "aT_lag1", - "daF", "daB", "daT", -] - -# V2 core features: no sin_ua/cos_ua, no mu (for single-scene fitting) -CORE_FEAT_KEYS_V2 = [ - "u_m", "u_a", "u_c", - "v_a", - "Cd_tot", "Cd_rear", - "Cl_tot", "Cl_diff", - "aF_lag1", "aB_lag1", "aT_lag1", - "daF", "daB", "daT", -] - -# Time-explicit features: da/dt_c (for cross-scene coefficient comparison) -TIME_FEAT_KEYS = [ - "daF_dt", "daB_dt", "daT_dt", -] - -MU_FEAT_KEYS = ["mu", "mu_u_a", "mu_v_a", "mu_Cd_tot", "mu_Cl_diff"] - -# Illusion target force features (for scenes where we know the target Cd/Cl) -ILLUSION_TARGET_KEYS = ["target_Cd", "target_Cl"] -ILLUSION_FEAT_KEYS_V2 = CORE_FEAT_KEYS_V2 + ILLUSION_TARGET_KEYS -ILLUSION_ALL_FEAT_KEYS_V2 = ILLUSION_FEAT_KEYS_V2 + MU_FEAT_KEYS # with mu for joint - -# Physics-only feature keys: NO action history terms (no aF_lag1, no daF, etc.) -# These are the clean inputs for learning d(alpha)/dt as a function of physics state. -PHYSICS_FEAT_KEYS = [ - "u_m", "u_a", "u_c", "v_a", - "Cd_tot", "Cd_rear", "Cl_tot", "Cl_diff", -] - -# Illusion error-state: physics features + force error (not raw target forces) -ILLUSION_ERR_KEYS = PHYSICS_FEAT_KEYS + ["Cd_err", "Cl_err"] - -# Observation lag (1-step delayed) and derivative (time-normalized) features -# These add temporal/phasing information to the otherwise static physics features. -LAG_FEAT_KEYS = [ - "u_m_lag1", "u_a_lag1", "u_c_lag1", "v_a_lag1", - "Cd_tot_lag1", "Cd_rear_lag1", "Cl_tot_lag1", "Cl_diff_lag1", -] -DERIV_FEAT_KEYS = [ - "du_m_dt", "du_a_dt", "du_c_dt", "dv_a_dt", - "dCd_tot_dt", "dCd_rear_dt", "dCl_tot_dt", "dCl_diff_dt", -] -# Action lag (for ablation level 4 — comparing against old approach) -ACTION_LAG_KEYS = ["aF_lag1", "aB_lag1", "aT_lag1"] - -# Augmented levels for ablation study: -# Level 0 = x_n (PHYSICS_FEAT_KEYS, no memory at all) -# Level 1 = x_n + x_{n-1} (current + 1-step lag) -# Level 2 = x_n + dx/dt (current + derivative) -# Level 3 = x_n + x_{n-1} + dx/dt (full temporal context) -# Level 4 = Level 3 + a_{n-1} (add action history for comparison) -AUG_LEVEL_1_KEYS = PHYSICS_FEAT_KEYS + LAG_FEAT_KEYS -AUG_LEVEL_2_KEYS = PHYSICS_FEAT_KEYS + DERIV_FEAT_KEYS -AUG_LEVEL_3_KEYS = PHYSICS_FEAT_KEYS + LAG_FEAT_KEYS + DERIV_FEAT_KEYS -AUG_LEVEL_4_KEYS = AUG_LEVEL_3_KEYS + ACTION_LAG_KEYS - -# Illusion equivalents with error-state -ILLUSION_LAG_KEYS = [ - "Cd_err_lag1", "Cl_err_lag1", -] -ILLUSION_DERIV_KEYS = [ - "dCd_err_dt", "dCl_err_dt", -] -ILLUSION_AUG_LEVEL_1_KEYS = ILLUSION_ERR_KEYS + LAG_FEAT_KEYS + ILLUSION_LAG_KEYS -ILLUSION_AUG_LEVEL_2_KEYS = ILLUSION_ERR_KEYS + DERIV_FEAT_KEYS + ILLUSION_DERIV_KEYS -ILLUSION_AUG_LEVEL_3_KEYS = ILLUSION_AUG_LEVEL_1_KEYS + DERIV_FEAT_KEYS + ILLUSION_DERIV_KEYS -ILLUSION_AUG_LEVEL_4_KEYS = ILLUSION_AUG_LEVEL_3_KEYS + ACTION_LAG_KEYS - -# Phase-state features: low-dimensional dynamic state z = [phase_obs, phase_deriv, static_force] -# This replaces the full x_n + x_{n-1} with a compact representation. -# The idea: u_a + du_a/dt encodes oscillation phase, Cl_tot + dCl_tot/dt encodes lift dynamics, -# and Cd_tot/Cd_rear provide static force feedback. -PHASE_STATE_KEYS = [ - "u_a", "du_a_dt", # oscillation phase (cross-stream asymmetry + rate) - "Cl_tot", "dCl_tot_dt", # lift dynamics - "Cd_tot", "Cd_rear", # static drag feedback -] - -# Karman expanded: phase-state + supplementary static quantities -# For Karman, 6-dim phase-state may not capture enough information. -# Adding u_m (mean streamwise), u_c (center sensor), v_a (cross asymmetry), Cl_diff (lift distribution) -KARMAN_EXPANDED_KEYS = PHASE_STATE_KEYS + [ - "u_m", "u_c", "v_a", "Cl_diff", -] - -# Illusion phase-state: error-based + dynamics -ILLUSION_PHASE_KEYS = PHASE_STATE_KEYS + [ - "Cd_err", "Cl_err", "dCd_err_dt", "dCl_err_dt", -] - -ALL_FEAT_KEYS = CORE_FEAT_KEYS + MU_FEAT_KEYS -# V2 all features (for cross-Re joint fitting with mu) -ALL_FEAT_KEYS_V2 = CORE_FEAT_KEYS_V2 + MU_FEAT_KEYS - - -# -- Feature computation ----------------------------------------------------- - -def compute_features( - dim: Dict[str, np.ndarray], - actions_prev: np.ndarray, # (T, 3) physical omega(t-1) or nondim alpha(t-1) - actions_prev2: np.ndarray, # (T, 3) physical omega(t-2) - mu: float, - alpha_mode: bool = False, # if True, actions_prev are already nondim alpha - include_mu: bool = True, - include_cos_sin: bool = True, # if True, include sin_ua/cos_ua features - dt_c: float = 1.0, # control interval in T0 units (for time normalization) - u0: float = U0, # inlet velocity for omega->alpha conversion - target_forces: Optional[np.ndarray] = None, # (T, 2) raw lattice target forces [fx,fy] - rho: float = 1.0, # fluid density for Cd/Cl conversion - sensors_raw: Optional[np.ndarray] = None, # (T, 6) raw lattice sensors, for obs dynamics - forces_raw: Optional[np.ndarray] = None, # (T, 6) raw lattice forces, for obs dynamics -) -> Dict[str, np.ndarray]: - """Compute unified feature dictionary from dimensionless primitives. - - Args: - dim: from compute_dimensionless() - actions_prev: lagged actions (physical omega or nondim alpha) - actions_prev2: twice-lagged actions - mu: 1/Re_D - alpha_mode: if True, actions are already nondim; else convert - include_mu: include mu modulation terms - include_cos_sin: include sin_ua/cos_ua phase encoding - dt_c: control interval (in T0 = D/U0 units), for time-normalized deltas - u0: inlet velocity (lattice), used only when alpha_mode=False - - Returns dict with all features as (T,) or (T,3) arrays. - """ - T = actions_prev.shape[0] - u_B, u_C, u_T = dim["u_hat_B"], dim["u_hat_C"], dim["u_hat_T"] - v_B, v_C, v_T = dim["v_hat_B"], dim["v_hat_C"], dim["v_hat_T"] - Cd_F, Cd_T, Cd_B = dim["Cd_F"], dim["Cd_T"], dim["Cd_B"] - Cl_F, Cl_T, Cl_B = dim["Cl_F"], dim["Cl_T"], dim["Cl_B"] - - # If actions are in physical omega, convert to nondim alpha - if alpha_mode: - a = actions_prev.astype(np.float64) - a2 = actions_prev2.astype(np.float64) - else: - a = actions_prev.astype(np.float64) / u0 - a2 = actions_prev2.astype(np.float64) / u0 - - sym = {} - - # Sensor combinations (nondim) - sym["u_m"] = (u_B + u_C + u_T) / 3.0 - sym["u_a"] = (u_T - u_B) / 2.0 - sym["u_c"] = u_C.copy() - sym["v_a"] = (v_T - v_B) / 2.0 - - # Force combinations (dimensionless Cd/Cl) - sym["Cd_tot"] = Cd_F + Cd_T + Cd_B - sym["Cd_rear"] = Cd_T + Cd_B - sym["Cl_tot"] = Cl_F + Cl_T + Cl_B - sym["Cl_diff"] = Cl_T - Cl_B - - # Phase (optional, may obscure linear structure) - if include_cos_sin: - sym["sin_ua"] = np.sin(np.pi * sym["u_a"]) - sym["cos_ua"] = np.cos(np.pi * sym["u_a"]) - - # Memory (nondim alpha) -- discrete version - sym["aF_lag1"] = a[:, 0] - sym["aB_lag1"] = a[:, 1] - sym["aT_lag1"] = a[:, 2] - sym["daF"] = a[:, 0] - a2[:, 0] - sym["daB"] = a[:, 1] - a2[:, 1] - sym["daT"] = a[:, 2] - a2[:, 2] - - # Time-normalized deltas (for cross-scene coefficient comparison) - sym["daF_dt"] = sym["daF"] / dt_c - sym["daB_dt"] = sym["daB"] / dt_c - sym["daT_dt"] = sym["daT"] / dt_c - - # Target forces (for illusion scenes) — convert lattice forces to Cd/Cl - if target_forces is not None: - tf = np.asarray(target_forces, dtype=np.float64) - if tf.ndim == 1: - tf = tf.reshape(1, -1) - sym["target_Cd"] = 2.0 * tf[:, 0] / (rho * u0**2 * D_CYL) - sym["target_Cl"] = 2.0 * tf[:, 1] / (rho * u0**2 * D_CYL) - # Error-state: deviation between actual and target force - sym["Cd_err"] = sym["Cd_tot"] - sym["target_Cd"] - sym["Cl_err"] = sym["Cl_tot"] - sym["target_Cl"] - - # Mu modulation - if include_mu: - sym["mu"] = np.full(T, mu, dtype=np.float64) - sym["mu_u_a"] = sym["u_a"] * mu - sym["mu_v_a"] = sym["v_a"] * mu - sym["mu_Cd_tot"] = sym["Cd_tot"] * mu - sym["mu_Cl_diff"] = sym["Cl_diff"] * mu - sym["mu_Cl_tot"] = sym["Cl_tot"] * mu # additional for phase-state - - # Observation dynamics: 1-step lag + time-normalized derivative - # These add temporal/phase info that static features lack. - if sensors_raw is not None and forces_raw is not None: - sr = np.asarray(sensors_raw, dtype=np.float64) - fr = np.asarray(forces_raw, dtype=np.float64) - # Lag-1 observations (shift by 1, pad first with current) - s_lag1 = np.zeros_like(sr) - f_lag1 = np.zeros_like(fr) - s_lag1[1:] = sr[:-1] - f_lag1[1:] = fr[:-1] - # Compute dim for lag-1 - dim_lag1 = compute_dimensionless(s_lag1, f_lag1, u0=u0, d=D_CYL, rho=rho) - - # Compute combined features for lag-1 - uB_1, uC_1, uT_1 = dim_lag1["u_hat_B"], dim_lag1["u_hat_C"], dim_lag1["u_hat_T"] - vB_1, vC_1, vT_1 = dim_lag1["v_hat_B"], dim_lag1["v_hat_C"], dim_lag1["v_hat_T"] - CdF_1, CdT_1, CdB_1 = dim_lag1["Cd_F"], dim_lag1["Cd_T"], dim_lag1["Cd_B"] - ClF_1, ClT_1, ClB_1 = dim_lag1["Cl_F"], dim_lag1["Cl_T"], dim_lag1["Cl_B"] - - u_m_1 = (uB_1 + uC_1 + uT_1) / 3.0 - u_a_1 = (uT_1 - uB_1) / 2.0 - u_c_1 = uC_1.copy() - v_a_1 = (vT_1 - vB_1) / 2.0 - Cd_tot_1 = CdF_1 + CdT_1 + CdB_1 - Cd_rear_1 = CdT_1 + CdB_1 - Cl_tot_1 = ClF_1 + ClT_1 + ClB_1 - Cl_diff_1 = ClT_1 - ClB_1 - - # Trim to T (in case raw arrays are longer — e.g. validator passes 2 rows) - def _trim(x): - return x[-T:] if x.shape[0] > T else x - - # Store lag-1 features (trimmed to T) - sym["u_m_lag1"] = _trim(u_m_1) - sym["u_a_lag1"] = _trim(u_a_1) - sym["u_c_lag1"] = _trim(u_c_1) - sym["v_a_lag1"] = _trim(v_a_1) - sym["Cd_tot_lag1"] = _trim(Cd_tot_1) - sym["Cd_rear_lag1"] = _trim(Cd_rear_1) - sym["Cl_tot_lag1"] = _trim(Cl_tot_1) - sym["Cl_diff_lag1"] = _trim(Cl_diff_1) - - # Time-normalized derivatives: (current - lag1) / dt_c - eps = 1e-12 - sym["du_m_dt"] = _trim((sym["u_m"] - u_m_1) / (dt_c + eps)) - sym["du_a_dt"] = _trim((sym["u_a"] - u_a_1) / (dt_c + eps)) - sym["du_c_dt"] = _trim((sym["u_c"] - u_c_1) / (dt_c + eps)) - sym["dv_a_dt"] = _trim((sym["v_a"] - v_a_1) / (dt_c + eps)) - sym["dCd_tot_dt"] = _trim((sym["Cd_tot"] - Cd_tot_1) / (dt_c + eps)) - sym["dCd_rear_dt"] = _trim((sym["Cd_rear"] - Cd_rear_1) / (dt_c + eps)) - sym["dCl_tot_dt"] = _trim((sym["Cl_tot"] - Cl_tot_1) / (dt_c + eps)) - sym["dCl_diff_dt"] = _trim((sym["Cl_diff"] - Cl_diff_1) / (dt_c + eps)) - - # Illusion error-state dynamics - if target_forces is not None: - tf = np.asarray(target_forces, dtype=np.float64) - tf_lag1 = np.zeros_like(tf) - tf_lag1[1:] = tf[:-1] - tCd_1 = 2.0 * tf_lag1[:, 0] / (rho * u0**2 * D_CYL) - tCl_1 = 2.0 * tf_lag1[:, 1] / (rho * u0**2 * D_CYL) - sym["Cd_err_lag1"] = _trim(Cd_tot_1 - tCd_1) - sym["Cl_err_lag1"] = _trim(Cl_tot_1 - tCl_1) - sym["dCd_err_dt"] = _trim((sym["Cd_err"] - sym["Cd_err_lag1"]) / (dt_c + eps)) - sym["dCl_err_dt"] = _trim((sym["Cl_err"] - sym["Cl_err_lag1"]) / (dt_c + eps)) - - return sym - - -def build_feature_matrix( - sym: Dict[str, np.ndarray], - feat_keys: List[str], - add_bias: bool = True, -) -> np.ndarray: - """Build feature matrix (T, N) from symbol dict.""" - cols = [] - if add_bias: - cols.append(np.ones(sym[feat_keys[0]].shape[0], dtype=np.float64)) - for k in feat_keys: - if k in sym: - cols.append(np.asarray(sym[k], dtype=np.float64)) - else: - # Missing key -> zero - T = sym.get("u_m", np.ones(1)).shape[0] - cols.append(np.zeros(T, dtype=np.float64)) - return np.column_stack(cols) - - -def get_feature_names(feat_keys: List[str], add_bias: bool = True) -> List[str]: - """Get feature names matching build_feature_matrix output.""" - names = [] - if add_bias: - names.append("bias") - names.extend(feat_keys) - return names diff --git a/src/SR_analysis/core/fitting.py b/src/SR_analysis/core/fitting.py deleted file mode 100644 index db5cc50..0000000 --- a/src/SR_analysis/core/fitting.py +++ /dev/null @@ -1,503 +0,0 @@ -"""SINDy fitting utilities: STLSQ threshold grid, feature matrix building. - -All features are built using the unified feature_builder module. -""" -from __future__ import annotations - -from typing import Dict, List, Optional, Tuple - -import numpy as np - -from .feature_builder import ( - compute_dimensionless, compute_features, build_feature_matrix, - get_feature_names, ALL_FEAT_KEYS, U0, -) - -# Default thresholds used across all scenes -DEFAULT_THRESHOLDS = [0.0, 0.001, 0.002, 0.005, 0.01, 0.015, 0.02, 0.03, 0.05, 0.1] - - -def fit_channel( - Theta: np.ndarray, - y: np.ndarray, - thresholds: Optional[List[float]] = None, - alpha: float = 1e-4, - max_iter: int = 25, -) -> Tuple[List[dict], dict]: - """Fit a single channel (one cylinder) with STLSQ threshold grid. - - Returns - ------- - rows : list of dict per threshold - best : dict with best threshold entry (highest R2) - """ - import pysindy as ps - - if thresholds is None: - thresholds = DEFAULT_THRESHOLDS - - # Normalise features for thresholding stability - std = np.std(Theta, axis=0) - std = np.where(std < 1e-8, 1.0, std) - Theta_s = Theta / std - - best = None - rows = [] - for th in thresholds: - opt = ps.STLSQ(threshold=th, alpha=alpha, max_iter=max_iter) - opt.fit(Theta_s, y) - coef = np.asarray(opt.coef_, dtype=np.float64).flatten() / std - y_pred = Theta @ coef - ssr = float(np.sum((y - y_pred) ** 2)) - sst = float(np.sum((y - np.mean(y)) ** 2) + 1e-12) - r2 = 1.0 - ssr / sst - mae = float(np.mean(np.abs(y - y_pred))) - nz = int(np.sum(np.abs(coef) > 1e-8)) - entry = {"threshold": float(th), "nz": nz, "r2": r2, "mae": mae, "coef": coef} - rows.append(entry) - if best is None or r2 > best["r2"]: - best = entry - return rows, best - - -def fit_sindy( - Theta: np.ndarray, - y: np.ndarray, - thresholds: Optional[List[float]] = None, -) -> List[dict]: - """Run SINDy with threshold grid, return results list. - - Each result dict has keys: threshold, nz, r2, mae, coef. - """ - if thresholds is None: - thresholds = DEFAULT_THRESHOLDS - - std = np.std(Theta, axis=0) - std = np.where(std < 1e-8, 1.0, std) - Theta_s = Theta / std - - results = [] - for th in thresholds: - import pysindy as ps - opt = ps.STLSQ(threshold=th, alpha=1e-4, max_iter=25) - opt.fit(Theta_s, y) - coef = np.asarray(opt.coef_, dtype=np.float64).flatten() / std - - y_pred = Theta @ coef - ssr = float(np.sum((y - y_pred) ** 2)) - sst = float(np.sum((y - np.mean(y)) ** 2) + 1e-12) - r2 = 1.0 - ssr / sst - mae = float(np.mean(np.abs(y - y_pred))) - nz = int(np.sum(np.abs(coef) > 1e-8)) - - results.append({ - "threshold": float(th), "nz": nz, "r2": r2, - "mae": mae, "coef": [float(c) for c in coef], - }) - - return results - - -def print_control_law(feature_names: List[str], coef: np.ndarray, channel_label: str = "ch"): - """Pretty-print a sparse control law.""" - terms = [] - for i, c in enumerate(coef): - if abs(c) > 1e-8: - terms.append(f"{c:.6f} * {feature_names[i]}") - print(f" {channel_label}: {' + '.join(terms)}") - nz = sum(1 for c in coef if abs(c) > 1e-8) - print(f" non-zero terms: {nz}") - - -def get_active_support( - coef: np.ndarray, - feat_names: List[str], - relative_threshold: float = 0.02, -) -> Dict[str, float]: - """Extract active features from coefficient vector. - - Features with |coef| / max(|coef|) >= relative_threshold are considered active. - """ - max_c = np.max(np.abs(coef)) - if max_c < 1e-12: - return {} - active = {} - for name, c in zip(feat_names, coef): - if abs(c) / max_c >= relative_threshold: - active[name] = float(c) - return active - - -def get_feature_matrix_from_data( - sensors: np.ndarray, # (T, 6) - forces: np.ndarray, # (T, 6) - actions_phys: np.ndarray, # (T, 3) physical omega - mu: float, - u0: float = U0, - alpha_mode: bool = False, - include_mu: bool = True, - n_warmup: int = 2, -) -> Tuple[np.ndarray, np.ndarray, np.ndarray, List[str], List[str]]: - """Build feature matrices from raw CFD data. - - Constructs dimensionless features via feature_builder, creates front (no bias) - and rear (with bias) feature matrices, and returns them aligned with Y. - - Parameters - ---------- - sensors, forces, actions_phys : raw data arrays. - mu : 1/Re_D. - u0 : inlet velocity (lattice units). - alpha_mode : if True, actions_phys are already nondim alpha. - include_mu : include mu modulation features. - n_warmup : number of warmup steps to discard (default 2 for lag/da). - - Returns - ------- - Theta_front : (T-warmup, N_front) feature matrix, NO bias column - Theta_rear : (T-warmup, N_rear) feature matrix, WITH bias column - Y : (T-warmup, 3) target action matrix - feat_names_front : list of feature names for front - feat_names_rear : list of feature names for rear - """ - T = sensors.shape[0] - a_prev = np.zeros((T, 3), dtype=np.float64) - a_prev2 = np.zeros((T, 3), dtype=np.float64) - a_prev[1:] = actions_phys[:-1] - a_prev2[2:] = actions_phys[:-2] - - dim = compute_dimensionless(sensors, forces, u0=u0, d=20.0) - sym = compute_features(dim, a_prev, a_prev2, mu, - alpha_mode=alpha_mode, include_mu=include_mu, u0=u0) - - Theta_f = build_feature_matrix(sym, ALL_FEAT_KEYS, add_bias=False) - Theta_r = build_feature_matrix(sym, ALL_FEAT_KEYS, add_bias=True) - - feat_names_front = get_feature_names(ALL_FEAT_KEYS, add_bias=False) - feat_names_rear = get_feature_names(ALL_FEAT_KEYS, add_bias=True) - - return (Theta_f[n_warmup:], Theta_r[n_warmup:], - actions_phys[n_warmup:], - feat_names_front, feat_names_rear) - - -# --------------------------------------------------------------------------- -# Weighted STLSQ with Huber-like robust regression -# --------------------------------------------------------------------------- - -def _huber_weights(residuals: np.ndarray, c: float = 1.345) -> np.ndarray: - """Compute Huber-like weights from residuals. - - Args: - residuals: (T,) residual array. - c: tuning constant (default 1.345 gives 95% efficiency for Normal errors). - - Returns: - weights: (T,) weight array in [0, 1]. - """ - s = np.median(np.abs(residuals)) * 1.4826 # robust scale estimate (MAD) - if s < 1e-12: - s = 1.0 - r = np.abs(residuals) / s - w = np.where(r <= c, 1.0, c / r) - return np.asarray(w, dtype=np.float64) - - -def fit_sindy_weighted( - Theta: np.ndarray, - y: np.ndarray, - thresholds: Optional[List[float]] = None, - alpha: float = 1e-4, - max_iter: int = 25, - sample_weights: Optional[np.ndarray] = None, - n_robust_passes: int = 2, -) -> List[dict]: - """Run SINDy with threshold grid and optional robust weighting. - - Two-stage robust fitting: - 1. First pass: OLS, compute residuals, compute Huber weights - 2. Second pass: weighted STLSQ with Huber weights - - Args: - Theta: (T, N) feature matrix. - y: (T,) target. - thresholds: list of threshold values. - alpha: ridge regularization. - max_iter: max STLSQ iterations. - sample_weights: optional (T,) pre-defined sample weights. - n_robust_passes: number of robust re-weighting passes (1 = skip). - - Returns: - results: list of dict per threshold with keys: - threshold, nz, r2, mae, coef, weights_used - """ - import pysindy as ps - - if thresholds is None: - thresholds = DEFAULT_THRESHOLDS - - # Normalise features for thresholding stability - std = np.std(Theta, axis=0) - std = np.where(std < 1e-8, 1.0, std) - Theta_s = Theta / std - - # Initialize weights - if sample_weights is not None: - w = np.asarray(sample_weights, dtype=np.float64).flatten() - w = w / np.mean(w) # normalize to mean=1 - else: - w = np.ones(Theta.shape[0], dtype=np.float64) - - # Robust re-weighting passes - for _ in range(n_robust_passes - 1): - # OLS on weighted data - Theta_w = Theta_s * np.sqrt(w)[:, None] - y_w = y * np.sqrt(w) - coef_ols, _, _, _ = np.linalg.lstsq(Theta_w, y_w, rcond=None) - coef_ols = coef_ols.flatten() / std - - resid = y - Theta @ coef_ols - w_new = _huber_weights(resid) - if sample_weights is not None: - w = w * w_new - else: - w = w_new - w = w / np.mean(w) - - results = [] - for th in thresholds: - if np.max(w) > 1e-8: - # Weighted STLSQ: apply weights via sample_weight - opt = ps.STLSQ(threshold=th, alpha=alpha, max_iter=max_iter) - opt.fit(Theta_s, y, sample_weight=w) - coef = np.asarray(opt.coef_, dtype=np.float64).flatten() / std - else: - coef = np.zeros(Theta.shape[1], dtype=np.float64) - - y_pred = Theta @ coef - # Weighted R2 - ssr = float(np.sum(w * (y - y_pred) ** 2)) - sst = float(np.sum(w * (y - np.average(y, weights=w)) ** 2) + 1e-12) - r2 = 1.0 - ssr / sst - mae = float(np.mean(np.abs(y - y_pred))) - nz = int(np.sum(np.abs(coef) > 1e-8)) - - results.append({ - "threshold": float(th), - "nz": nz, - "r2": r2, - "mae": mae, - "coef": [float(c) for c in coef], - "weights_min": float(np.min(w)), - "weights_max": float(np.max(w)), - }) - - return results - - -# --------------------------------------------------------------------------- -# V2 feature matrix builder (configurable feature sets, time normalization) -# --------------------------------------------------------------------------- - -def get_feature_matrix_v2( - sensors: np.ndarray, # (T, 6) - forces: np.ndarray, # (T, 6) - actions_phys: np.ndarray, # (T, 3) physical omega - mu: float, - u0: float = U0, - alpha_mode: bool = False, - include_mu: bool = False, # default False for single-scene - include_cos_sin: bool = False, # default False (avoid masking linear structure) - use_time_norm: bool = False, # if True, use time-normalized deltas (da/dt_c) - feat_keys: Optional[List[str]] = None, # custom feat keys (default CORE_FEAT_KEYS_V2) - dt_c: float = 1.0, # control interval in T0 units - n_warmup: int = 2, - target_forces: Optional[np.ndarray] = None, # (T, 2) raw lattice target forces -) -> Tuple[np.ndarray, np.ndarray, np.ndarray, List[str], List[str]]: - """Build feature matrices with configurable feature sets. - - Uses CORE_FEAT_KEYS_V2 by default (no sin_ua/cos_ua, no mu). - For Illusion scenes with target forces, provide target_forces and - feat_keys=ILLUSION_FEAT_KEYS_V2 (or pass None to auto-detect). - For cross-Re joint fitting, set include_mu=True. - For time-normalized deltas, set use_time_norm=True and provide dt_c. - - Returns - ------- - Theta_front : (T-warmup, N_front) NO bias column - Theta_rear : (T-warmup, N_rear) WITH bias column - Y : (T-warmup, 3) target actions (physical omega) - feat_names_front, feat_names_rear - """ - from .feature_builder import ( - CORE_FEAT_KEYS_V2, TIME_FEAT_KEYS, MU_FEAT_KEYS, ALL_FEAT_KEYS_V2, - ILLUSION_FEAT_KEYS_V2, ILLUSION_ALL_FEAT_KEYS_V2, - ) - - if feat_keys is None: - if target_forces is not None: - # Illusion scenes: include target force features - feat_keys = ILLUSION_FEAT_KEYS_V2 - elif use_time_norm: - # Replace discrete da with time-normalized da/dt - feat_keys = [k for k in CORE_FEAT_KEYS_V2 if not k.startswith("da")] - feat_keys += TIME_FEAT_KEYS - else: - feat_keys = CORE_FEAT_KEYS_V2 - - if include_mu: - # Add mu features if not already in feat_keys - for mk in MU_FEAT_KEYS: - if mk not in feat_keys: - feat_keys = feat_keys + [mk] - - T = sensors.shape[0] - a_prev = np.zeros((T, 3), dtype=np.float64) - a_prev2 = np.zeros((T, 3), dtype=np.float64) - a_prev[1:] = actions_phys[:-1] - a_prev2[2:] = actions_phys[:-2] - - dim = compute_dimensionless(sensors, forces, u0=u0, d=20.0) - sym = compute_features( - dim, a_prev, a_prev2, mu, - alpha_mode=alpha_mode, - include_mu=include_mu, - include_cos_sin=include_cos_sin, - dt_c=dt_c, - u0=u0, - target_forces=target_forces, - ) - - Theta_f = build_feature_matrix(sym, feat_keys, add_bias=False) - Theta_r = build_feature_matrix(sym, feat_keys, add_bias=True) - - feat_names_front = get_feature_names(feat_keys, add_bias=False) - feat_names_rear = get_feature_names(feat_keys, add_bias=True) - - return (Theta_f[n_warmup:], Theta_r[n_warmup:], - actions_phys[n_warmup:], - feat_names_front, feat_names_rear) - - -# --------------------------------------------------------------------------- -# Derivative-mode feature builders: fit d(alpha)/dt = g(physics_state) -# No action history in input features; Y is time-normalized action derivative. -# --------------------------------------------------------------------------- - -def compute_action_deriv( - actions_phys: np.ndarray, # (T, 3) physical omega - dt_c: float, # control interval in T0 units - u0: float = U0, - center_diff: bool = False, # if True, use (alpha(t) - alpha(t-2))/(2*dt_c) -) -> np.ndarray: - """Compute time-normalized action derivative d(alpha)/dt. - - forward_diff: d(alpha)/dt ~ (alpha(t) - alpha(t-1)) / dt_c - center_diff: d(alpha)/dt ~ (alpha(t) - alpha(t-2)) / (2*dt_c) - - Returns (T, 3) array, with first row(s) zero-padded. - """ - alpha = np.asarray(actions_phys, dtype=np.float64) / u0 # non-dim - T = alpha.shape[0] - deriv = np.zeros_like(alpha) - if center_diff and T >= 3: - deriv[2:] = (alpha[2:] - alpha[:-2]) / (2.0 * dt_c) - elif T >= 2: - deriv[1:] = (alpha[1:] - alpha[:-1]) / dt_c - return deriv - - -def get_feature_matrix_deriv( - sensors: np.ndarray, # (T, 6) - forces: np.ndarray, # (T, 6) - actions_phys: np.ndarray, # (T, 3) physical omega - mu: float, - u0: float = U0, - dt_c: float = 1.0, # control interval in T0 units - feat_keys: Optional[List[str]] = None, # default PHYSICS_FEAT_KEYS - include_mu: bool = False, # add mu modulation features (for cross-Re joint) - target_forces: Optional[np.ndarray] = None, # (T, 2) for Illusion - n_warmup: int = 2, - center_diff: bool = False, - augment_level: int = 0, # 0=static, 1=+lags, 2=+derivs, 3=both, 4=+action_lag - output_mode: str = "deriv", # "deriv": predict d(alpha)/dt; "absolute": predict alpha directly -) -> Tuple[np.ndarray, np.ndarray, np.ndarray, List[str], List[str]]: - """Build feature matrices for phase-state SINDy. - - Input features: physics state with optional temporal context. - - Parameters - ---------- - output_mode : str - "deriv": Y = d(alpha)/dt (time-normalized). Closed-loop needs integration. - "absolute": Y = alpha (non-dimensional action). No integration needed. - - augment_level : int - 0: PHYSICS_FEAT_KEYS only (static, no memory) - 1: + lag-1 obs features (adds temporal context) - 2: + obs derivative features (adds rate information) - 3: + lag-1 + derivative (full temporal context) - 4: + action lag (aF_lag1 etc., for ablation comparison against old approach) - """ - from .feature_builder import ( - PHYSICS_FEAT_KEYS, ILLUSION_ERR_KEYS, MU_FEAT_KEYS, - AUG_LEVEL_1_KEYS, AUG_LEVEL_2_KEYS, - AUG_LEVEL_3_KEYS, AUG_LEVEL_4_KEYS, - ILLUSION_AUG_LEVEL_1_KEYS, ILLUSION_AUG_LEVEL_2_KEYS, - ILLUSION_AUG_LEVEL_3_KEYS, ILLUSION_AUG_LEVEL_4_KEYS, - ) - - # Select feature key set based on augment_level - _AUG_MAP = {0: PHYSICS_FEAT_KEYS, 1: AUG_LEVEL_1_KEYS, - 2: AUG_LEVEL_2_KEYS, 3: AUG_LEVEL_3_KEYS, 4: AUG_LEVEL_4_KEYS} - _AUG_ILLUSION_MAP = {0: ILLUSION_ERR_KEYS, 1: ILLUSION_AUG_LEVEL_1_KEYS, - 2: ILLUSION_AUG_LEVEL_2_KEYS, 3: ILLUSION_AUG_LEVEL_3_KEYS, - 4: ILLUSION_AUG_LEVEL_4_KEYS} - - if feat_keys is None: - if target_forces is not None: - feat_keys = _AUG_ILLUSION_MAP.get(augment_level, ILLUSION_AUG_LEVEL_3_KEYS) - else: - feat_keys = _AUG_MAP.get(augment_level, AUG_LEVEL_3_KEYS) - - if include_mu: - for mk in MU_FEAT_KEYS: - if mk not in feat_keys: - feat_keys = feat_keys + [mk] - - if include_mu: - for mk in MU_FEAT_KEYS: - if mk not in feat_keys: - feat_keys = feat_keys + [mk] - - T = sensors.shape[0] - # a_prev/a_prev2 only used for computing the DERIVATIVE target Y, not in Theta - a_prev = np.zeros((T, 3), dtype=np.float64) - a_prev2 = np.zeros((T, 3), dtype=np.float64) - a_prev[1:] = actions_phys[:-1] - a_prev2[2:] = actions_phys[:-2] - - dim = compute_dimensionless(sensors, forces, u0=u0, d=20.0) - sym = compute_features( - dim, a_prev, a_prev2, mu, - alpha_mode=False, include_mu=include_mu, - include_cos_sin=False, dt_c=dt_c, u0=u0, - target_forces=target_forces, - sensors_raw=sensors, forces_raw=forces, - ) - - Theta_f = build_feature_matrix(sym, feat_keys, add_bias=False) - Theta_r = build_feature_matrix(sym, feat_keys, add_bias=True) - - feat_names_front = get_feature_names(feat_keys, add_bias=False) - feat_names_rear = get_feature_names(feat_keys, add_bias=True) - - # Y: depends on output_mode - if output_mode == "absolute": - Y = np.asarray(actions_phys, dtype=np.float64) / u0 # non-dim alpha (absolute action) - else: - Y = compute_action_deriv(actions_phys, dt_c, u0=u0, center_diff=center_diff) - - return (Theta_f[n_warmup:], Theta_r[n_warmup:], - Y[n_warmup:], - feat_names_front, feat_names_rear) diff --git a/src/SR_analysis/core/g_operator.py b/src/SR_analysis/core/g_operator.py deleted file mode 100644 index 0a2f956..0000000 --- a/src/SR_analysis/core/g_operator.py +++ /dev/null @@ -1,191 +0,0 @@ -"""G-operator and equivariance tools. - -Provides G-operator transformations, dimensionless conversion, -and equivariance diagnostics for PPO control laws. -""" -from __future__ import annotations - -from typing import Any, Dict, Optional, Tuple - -import numpy as np - -from .feature_builder import compute_dimensionless as _compute_dimless - - -def apply_G_alpha(alpha: np.ndarray) -> np.ndarray: - """Apply mirror G to action: [aF, aT, aB] -> [-aF, -aB, -aT].""" - return np.array([-alpha[0], -alpha[2], -alpha[1]], dtype=alpha.dtype) - - -def apply_G_raw(obs_slice: np.ndarray, - a_prev: np.ndarray, - a_prev2: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: - """Apply G to raw obs slice [sensor(6)+force(6)] and action arrays. - - Parameters - ---------- - obs_slice : (12,) raw obs [s0_ux,uy, s1_ux,uy, s2_ux,uy, f0_fx,fy, f1_fx,fy, f2_fx,fy] - a_prev : (3,) physical omega at t-1 - a_prev2 : (3,) physical omega at t-2 - - Returns - ------- - G_obs : (12,) transformed obs slice - G_a_prev : (3,) transformed a_prev - G_a_prev2 : (3,) transformed a_prev2 - """ - G_obs = np.zeros(12, dtype=np.float64) - # sensors: swap top(0,1) <-> bottom(4,5), negate v - G_obs[0] = obs_slice[4] - G_obs[1] = -obs_slice[5] - G_obs[2] = obs_slice[2] - G_obs[3] = -obs_slice[3] - G_obs[4] = obs_slice[0] - G_obs[5] = -obs_slice[1] - # forces: swap bottom(2,3) <-> top(4,5), negate fy - G_obs[6] = obs_slice[6] - G_obs[7] = -obs_slice[7] - G_obs[8] = obs_slice[10] - G_obs[9] = -obs_slice[11] - G_obs[10] = obs_slice[8] - G_obs[11] = -obs_slice[9] - - G_a_prev = np.array([-a_prev[0], -a_prev[2], -a_prev[1]], dtype=np.float64) - G_a_prev2 = np.array([-a_prev2[0], -a_prev2[2], -a_prev2[1]], dtype=np.float64) - return G_obs, G_a_prev, G_a_prev2 - - -def check_equivariance( - model: Any, - obs_slice_series: np.ndarray, # (T, 12) raw obs - actions_phys: np.ndarray, # (T, 3) physical omega - norm: dict, - action_scale: float = 8.0, - action_bias: Tuple[float, float, float] = (0.0, -4.0, 4.0), - u0: float = 0.01, -) -> Dict[str, float]: - """Check G-equivariance of a PPO model over a time series. - - Returns dict with front/rear equivariance errors. - """ - from .cfd_interface import build_observation, action_to_physical - - T = min(obs_slice_series.shape[0], actions_phys.shape[0]) - ef, eb, et = [], [], [] - - for t in range(2, T): - # Get current obs - osl = obs_slice_series[t] - a_prev = actions_phys[t - 1] if t > 0 else actions_phys[0] - a_prev2 = actions_phys[t - 2] if t > 1 else actions_phys[0] - - # Predict action for current state - obs = build_observation(osl, norm) - act, _ = model.predict(obs, deterministic=True) - act = act.astype(np.float32).flatten() - alpha = action_to_physical(act.reshape(1, 3), - scale=action_scale, bias=action_bias, u0=u0).flatten() - - # Apply G to state - G_obs, _, _ = apply_G_raw(osl, a_prev, a_prev2) - obs_G = build_observation(G_obs, norm) - act_G, _ = model.predict(obs_G, deterministic=True) - act_G = act_G.astype(np.float32).flatten() - alpha_G = action_to_physical(act_G.reshape(1, 3), - scale=action_scale, bias=action_bias, u0=u0).flatten() - - # Expected: G(alpha) = [-aF, -aB, -aT] - expected = apply_G_alpha(alpha) - - ef.append(abs(float(alpha_G[0]) - float(expected[0]))) - eb.append(abs(float(alpha_G[1]) - float(expected[1]))) - et.append(abs(float(alpha_G[2]) - float(expected[2]))) - - ef_arr = np.array(ef) - eb_arr = np.array(eb) - et_arr = np.array(et) - alpha_range = float(np.max(np.abs(actions_phys[2:]))) - - return { - "front_mean_abs_error": float(np.mean(ef_arr)), - "front_rel_error": float(np.mean(ef_arr) / (alpha_range + 1e-12)), - "rear_bottom_rel_error": float(np.mean(eb_arr) / (alpha_range + 1e-12)), - "rear_top_rel_error": float(np.mean(et_arr) / (alpha_range + 1e-12)), - "alpha_range": alpha_range, - } - - -def diagnose_one_re(model, ff, target_states, norm, config, n_steps=150) -> dict: - """Run PPO inference and check equivariance. - - Parameters - ---------- - model : loaded PPO model - ff : FlowField instance (must be at saved checkpoint state) - target_states : (FIFO_LEN, 6) target sensor signals - norm : norm dict - config : scene config dict with action_scale, action_bias, u0, etc. - - Returns - ------- - dict with equivariance metrics. - """ - from collections import deque - from .cfd_interface import (build_observation, scale_action, - action_to_physical, compute_similarity) - - action_scale = config.get("action_scale", 8.0) - action_bias = config.get("action_bias", (0.0, -4.0, 4.0)) - u0 = config.get("u0", 0.01) - sample_interval = config.get("sample_interval", 800) - fifo_len = config.get("fifo_len", 150) - n_obj_total = config.get("n_objects_total", 7) - - ff.restore_ddf() - ff.apply_ddf() - - # Bias FIFO init - fifo = deque(maxlen=fifo_len) - bias_arr = scale_action(np.zeros(3, dtype=np.float32), - scale=action_scale, bias=action_bias, - u0=u0, n_total_bodies=n_obj_total) - for _ in range(fifo_len): - ff.run(sample_interval, bias_arr) - fifo.append(ff.obs.copy()[2:14]) - - # Inference - obs_array = [] - action_array = [] - obs = np.zeros(12, dtype=np.float32) - - for _ in range(n_steps): - act, _ = model.predict(obs, deterministic=True) - act = act.astype(np.float32).flatten() - action_array.append(act.copy()) - - action_arr = scale_action(act, scale=action_scale, bias=action_bias, - u0=u0, n_total_bodies=n_obj_total) - ff.context.push() - ff.run(sample_interval, action_arr) - ff.context.pop() - - obs_slice = ff.obs.copy()[2:14] - fifo.append(obs_slice) - obs_array.append(obs_slice) - obs = build_observation(obs_slice, norm) - - obs_series = np.array(obs_array, dtype=np.float64) - actions_phys = action_to_physical(np.array(action_array), - scale=action_scale, bias=action_bias, u0=u0) - states_arr = np.array(list(fifo), dtype=np.float32) - sim = compute_similarity(target_states, states_arr[:, 0:6], - config.get("conv_len", 30)) - - # Equivariance check - eq = check_equivariance(model, obs_series, actions_phys, norm, - action_scale, action_bias, u0) - - return { - "similarity": sim, - "equivariance": eq, - } diff --git a/src/SR_analysis/literature_note.md b/src/SR_analysis/literature_note.md new file mode 100644 index 0000000..333e96f --- /dev/null +++ b/src/SR_analysis/literature_note.md @@ -0,0 +1,97 @@ +# SR 文献定位与写作草稿 + +当前 SR 部分最稳的文献定位,不是宣称“首次把符号回归用于流动控制”,而是把本工作放在三条已有传统的交叉处:一是流动控制中的显式机器学习控制律,主要来自 genetic programming / linear genetic programming;二是流体中的稀疏动力学与传感器低维建模,主要来自 SINDy 与 sparse ROM;三是 DRL 流动控制的黑箱策略学习。[Gau14, Li16, Loi16, Loi17, Rab18b, Par20, Cas22, Zol25] 当前工作的真正新意更接近于:**对已训练 DRL 策略做后验白箱提取,并在闭环 CFD 中验证其跨场景骨架与适用边界**,而不是单纯“搜索一个显式控制律”。[Cas22, Zol25] + +## 一组最该引用的核心文献 + +| 角色 | 文献 | 期刊 | 当前用途 | +|---|---|---|---| +| 最接近的方法学邻居 | [Zol25] | Nature Communications | 解释 SINDy-RL 如何把稀疏模型、奖励与策略放进同一可解释 RL 框架;用于说明“可解释 RL + 流动控制”已有高水平先例,但其重点更偏 model-based RL,而不是对已训练 PPO 做后验蒸馏 | +| 显式闭环控制律先例 | [Gau14] | Journal of Fluid Mechanics | 证明流动控制中可以直接得到符号化、可解释的闭环控制律;适合作为“白箱控制律并非空白”的经典前史 | +| MIMO 显式控制律先例 | [Li16] | Experiments in Fluids | 说明多传感器、多执行器条件下,linear genetic programming 可以自动做传感器选择并产生显式反馈律 | +| DRL 与可解释控制律的直接对照 | [Cas22] | Physics of Fluids | 最适合用来强调:DRL 更鲁棒,但 LGPC 更紧凑、更可解释,是本工作立题最直接的动机 | +| 稀疏流体建模方法学基础 | [Loi16] | Journal of Fluid Mechanics | 说明 SINDy 类方法在流体中首先成功的是受物理约束的低维动力学识别,而非直接控制律提取 | +| 传感器到低维状态的关键桥梁 | [Loi17] | Journal of Fluid Mechanics | 说明传感器特征、稀疏动态状态和 coherent structures 可以被连起来;是后续接 CCD/OID 的方法论支撑 | +| DRL 流动控制应用背景 | [Rab18b] | Journal of Fluid Mechanics | 作为 DRL 做 wake/cylinder flow control 的奠基背景,突出其高性能但黑箱 | +| DRL 鲁棒性与少传感器背景 | [Par20] | Journal of Fluid Mechanics | 用来说明黑箱 DRL 在 sensor sparsity、robustness、Re variation 上已有成熟结果,但策略本身仍不透明 | + +## 论文里可采用的文献分组写法 + +### 1. 显式机器学习控制律的前史 + +这一组文献说明,流动控制领域早已有“让机器直接给出显式控制律”的传统,但主要路线不是 DRL 后验蒸馏,而是 genetic programming / machine learning control。最经典的例子是 backward-facing step 分离控制 [Gau14],它通过 genetic programming 直接搜索闭环控制律,并从结果中识别出与回流泡低频摆动相关的新控制机制。Ahmed body 拖曳控制的 linear genetic programming 工作 [Li16] 则进一步表明,在 MIMO 场景中,显式控制律还能自动做传感器筛选。更近的 cylinder wake 对比研究 [Cas22] 直接指出:DRL 在噪声和初值扰动下更稳,而 LGPC 更容易得到紧凑、可解释的控制律。[Gau14, Li16, Cas22] + +这一组文献在写法上最适合承担两件事: + +- 证明“显式白箱控制律”在流动控制里已有扎实先例,不是凭空提出的新口号。[Gau14, Li16] +- 同时说明这些工作大多是**直接搜索控制律**,而不是对一个已成功的 DRL 策略做后验结构提取,因此与当前工作的技术问题并不相同。[Cas22] + +### 2. 稀疏建模与低维状态识别的主线 + +SINDy 在流体中的核心成功先例主要不是控制律,而是低维动力学和 reduced-order models。受约束 sparse Galerkin regression [Loi16] 说明,在物理约束下做稀疏动力学识别可以得到比纯黑箱回归更可信的 ROM。Loiseau 等人的 sensor-based sparse ROM 工作 [Loi17] 更接近当前任务,因为它明确从传感器出发,构建动态特征状态,并把稀疏动力学和 coherent structures 联系起来。这一脉络非常适合用来支撑本文中的一个关键论点:**动作历史之所以常被模型滥用,往往是因为状态表示还不充分;真正的目标不是记住动作,而是找到足够的低维状态。** [Loi16, Loi17] + +这一组文献在论文中应作为方法论背景,而不是写成“他们已经做过本文想做的事”。更稳的表述是: + +- 这些工作证明了稀疏建模在流体低维表示中的价值。[Loi16, Loi17] +- 但它们主要识别的是植物模型或状态动力学,而不是 `obs -> act` 的显式反馈律。[Loi17] + +### 3. 黑箱 DRL 流动控制的应用背景 + +Rabault 等人的 cylinder wake DRL 工作 [Rab18b] 是最经典的“DRL 能自己发现有效流动控制策略”的证明之一,但作者也明确指出策略本身难以直接解释。Paris 等人的后续工作 [Par20] 则把焦点放到鲁棒性、传感器布置和 practical implementation 上,说明黑箱 DRL 已经可以在 Reynolds 数变化和传感器噪声下保持性能。它们的意义在于提供本文的应用背景:**黑箱策略已经有效,但解释与压缩仍是缺口。** [Rab18b, Par20] + +### 4. 最接近本文的“可解释 RL”文献 + +SINDy-RL [Zol25] 是当前最重要的方法学邻居。它将稀疏动力学、符号奖励与策略表示纳入同一 RL 框架,并包含流动控制例子。最需要写清的一点是:这篇工作虽然也涉及 symbolic policy,但其核心更偏 **model-based RL + symbolic surrogate learning**,而不是对一个已经训练好的 DRL 控制器做后验蒸馏与闭环验证。这个区别很重要,因为本文的贡献恰恰落在后者:从已成功的 PPO 控制策略中提炼低维、可执行、可比较的显式控制律。[Zol25] + +## 当前工作与现有文献最稳的关系定位 + +当前工作最稳的 literature positioning 可以写成下面三点: + +- **不是**首次在流动控制中寻找显式控制律,因为 genetic programming / LGPC 的机器学习控制工作早已做到这一点。[Gau14, Li16, Cas22] +- **不是**首次把 SINDy 与 RL 联系起来,因为 SINDy-RL 已经给出了高水平先例。[Zol25] +- **更可能是**首次或少见地把“已训练 DRL 策略的后验白箱提取”作为流动控制核心问题,并把结果放到闭环 CFD、跨场景 shared backbone 与 regime boundary 的框架里系统比较。[Cas22, Zol25] + +这个口径比“首次使用 SR”更稳,也更容易经得起审稿。 + +## 对当前 SR 章节最值得参考的写法 + +### 可直接借用的写作角度 + +| 角度 | 适合参考的文献 | 可借的写法 | +|---|---|---| +| 黑箱 vs 白箱张力 | [Cas22, Rab18b, Par20] | 先承认 DRL 性能与鲁棒性,再指出策略不可解释,顺势引出 white-box distillation 的必要性 | +| 显式控制律的先例 | [Gau14, Li16] | 把 symbolic / GP 控制律写成流动控制中的既有传统,而非本文特有发明 | +| 稀疏状态而非动作记忆 | [Loi17] | 强调应优先找足够的低维状态与特征,而不是让动作历史充当隐藏状态代理 | +| 物理约束与模型可信度 | [Loi16] | 论证为何闭环可运行、符合结构约束、具有物理一致性的公式比单纯拟合优度更重要 | +| 可解释 RL 邻近工作 | [Zol25] | 对比“联合学习 symbolic model/policy”与“后验蒸馏已训练 PPO policy”的区别 | + +### 当前 SR 章节最稳的主叙事 + +比较稳的叙事不是“找到一个统一总公式”,而是: + +1. 黑箱 DRL 在流动控制中已能取得高性能,但其策略结构通常不透明。[Rab18b, Par20] +2. 流动控制中已有显式机器学习控制律的传统,尤其来自 genetic programming / LGPC,但其与 DRL 后验蒸馏并不等同。[Gau14, Li16, Cas22] +3. 稀疏建模和传感器低维状态识别为白箱策略提取提供了方法学基础。[Loi16, Loi17] +4. 本文的工作重点是在已训练 DRL 策略之上,提取可闭环执行、可比较、可分析 shared-backbone 与 regime split 的显式控制律。[Zol25] + +## 哪些表述要避免 + +- 不要写“本文首次将符号回归用于流动控制”。这会直接被 [Gau14]、[Li16]、[Cas22] 否定。 +- 不要写“本文首次将 SINDy 用于 RL 可解释化”。这会被 [Zol25] 否定。 +- 不要把 [Loi16]、[Loi17] 写成直接的控制律提取先例,它们主要是低维动力学识别,不是 `obs -> act` policy distillation。 +- 不要把所有 symbolic / sparse literature 混成同一类。最好明确区分“控制律搜索”“动力学识别”“黑箱 DRL 背景”三条线。[Gau14, Loi16, Rab18b] + +## 当前最建议进正文的引用组合 + +如果正文篇幅有限,SR 章节最核心的一组文献可压缩为: + +- **显式控制律先例**:[Gau14, Li16, Cas22] +- **稀疏方法学基础**:[Loi16, Loi17] +- **DRL 背景**:[Rab18b, Par20] +- **最接近的可解释 RL 邻居**:[Zol25] + +这一组已经足够支撑 SR 部分的立题、方法定位与新意边界。 + +## 当前文稿草稿可直接使用的一段话 + +Machine-learning-based flow control already has two partially separate traditions. One seeks explicit feedback laws directly, most notably through genetic programming or linear genetic programming, and has shown that compact symbolic controllers can be discovered for separation control, drag reduction, and wake manipulation [Gau14, Li16, Cas22]. The other uses deep reinforcement learning to discover effective but largely opaque policies in canonical flow-control benchmarks [Rab18b, Par20]. In parallel, sparse-regression methods such as SINDy have provided a strong basis for identifying reduced-order fluid dynamics and sensor-based low-dimensional state representations, but these works generally target plant modeling rather than direct extraction of closed-loop feedback laws [Loi16, Loi17]. The closest recent bridge is SINDy-RL, which combines sparse model discovery and reinforcement learning within a model-based framework [Zol25]. The present work is best positioned as post hoc white-box extraction of low-dimensional control laws from successful DRL policies, with CFD closed-loop validation and cross-scene comparison as the central tests. diff --git a/src/SR_analysis/results/archive/raw/illusion_0.5L_pysr.json b/src/SR_analysis/old/archive/raw/illusion_0.5L_pysr.json similarity index 100% rename from src/SR_analysis/results/archive/raw/illusion_0.5L_pysr.json rename to src/SR_analysis/old/archive/raw/illusion_0.5L_pysr.json diff --git a/src/SR_analysis/results/archive/raw/illusion_0.6L_pysr.json b/src/SR_analysis/old/archive/raw/illusion_0.6L_pysr.json similarity index 100% rename from src/SR_analysis/results/archive/raw/illusion_0.6L_pysr.json rename to src/SR_analysis/old/archive/raw/illusion_0.6L_pysr.json diff --git a/src/SR_analysis/results/archive/raw/illusion_0.75L_pysr.json b/src/SR_analysis/old/archive/raw/illusion_0.75L_pysr.json similarity index 100% rename from src/SR_analysis/results/archive/raw/illusion_0.75L_pysr.json rename to src/SR_analysis/old/archive/raw/illusion_0.75L_pysr.json diff --git a/src/SR_analysis/results/archive/raw/illusion_0.75L_v23.json b/src/SR_analysis/old/archive/raw/illusion_0.75L_v23.json similarity index 100% rename from src/SR_analysis/results/archive/raw/illusion_0.75L_v23.json rename to src/SR_analysis/old/archive/raw/illusion_0.75L_v23.json diff --git a/src/SR_analysis/results/archive/raw/illusion_0.8L_pysr.json b/src/SR_analysis/old/archive/raw/illusion_0.8L_pysr.json similarity index 100% rename from src/SR_analysis/results/archive/raw/illusion_0.8L_pysr.json rename to src/SR_analysis/old/archive/raw/illusion_0.8L_pysr.json diff --git a/src/SR_analysis/results/archive/raw/illusion_1.2L_pysr.json b/src/SR_analysis/old/archive/raw/illusion_1.2L_pysr.json similarity index 100% rename from src/SR_analysis/results/archive/raw/illusion_1.2L_pysr.json rename to src/SR_analysis/old/archive/raw/illusion_1.2L_pysr.json diff --git a/src/SR_analysis/results/archive/raw/illusion_1.5L_v23.json b/src/SR_analysis/old/archive/raw/illusion_1.5L_v23.json similarity index 100% rename from src/SR_analysis/results/archive/raw/illusion_1.5L_v23.json rename to src/SR_analysis/old/archive/raw/illusion_1.5L_v23.json diff --git a/src/SR_analysis/results/archive/raw/illusion_1L_pysr.json b/src/SR_analysis/old/archive/raw/illusion_1L_pysr.json similarity index 100% rename from src/SR_analysis/results/archive/raw/illusion_1L_pysr.json rename to src/SR_analysis/old/archive/raw/illusion_1L_pysr.json diff --git a/src/SR_analysis/results/archive/raw/illusion_1L_target_pysr.json b/src/SR_analysis/old/archive/raw/illusion_1L_target_pysr.json similarity index 100% rename from src/SR_analysis/results/archive/raw/illusion_1L_target_pysr.json rename to src/SR_analysis/old/archive/raw/illusion_1L_target_pysr.json diff --git a/src/SR_analysis/results/archive/raw/illusion_1L_v23.json b/src/SR_analysis/old/archive/raw/illusion_1L_v23.json similarity index 100% rename from src/SR_analysis/results/archive/raw/illusion_1L_v23.json rename to src/SR_analysis/old/archive/raw/illusion_1L_v23.json diff --git a/src/SR_analysis/results/archive/raw/illusion_2L_pysr.json b/src/SR_analysis/old/archive/raw/illusion_2L_pysr.json similarity index 100% rename from src/SR_analysis/results/archive/raw/illusion_2L_pysr.json rename to src/SR_analysis/old/archive/raw/illusion_2L_pysr.json diff --git a/src/SR_analysis/results/archive/raw/karman_joint_deep_front.json b/src/SR_analysis/old/archive/raw/karman_joint_deep_front.json similarity index 100% rename from src/SR_analysis/results/archive/raw/karman_joint_deep_front.json rename to src/SR_analysis/old/archive/raw/karman_joint_deep_front.json diff --git a/src/SR_analysis/results/archive/raw/karman_joint_deep_top.json b/src/SR_analysis/old/archive/raw/karman_joint_deep_top.json similarity index 100% rename from src/SR_analysis/results/archive/raw/karman_joint_deep_top.json rename to src/SR_analysis/old/archive/raw/karman_joint_deep_top.json diff --git a/src/SR_analysis/results/archive/raw/karman_re100_abs.json b/src/SR_analysis/old/archive/raw/karman_re100_abs.json similarity index 100% rename from src/SR_analysis/results/archive/raw/karman_re100_abs.json rename to src/SR_analysis/old/archive/raw/karman_re100_abs.json diff --git a/src/SR_analysis/results/archive/raw/karman_re100_deriv.json b/src/SR_analysis/old/archive/raw/karman_re100_deriv.json similarity index 100% rename from src/SR_analysis/results/archive/raw/karman_re100_deriv.json rename to src/SR_analysis/old/archive/raw/karman_re100_deriv.json diff --git a/src/SR_analysis/results/archive/raw/karman_re100_mu_pysr.json b/src/SR_analysis/old/archive/raw/karman_re100_mu_pysr.json similarity index 100% rename from src/SR_analysis/results/archive/raw/karman_re100_mu_pysr.json rename to src/SR_analysis/old/archive/raw/karman_re100_mu_pysr.json diff --git a/src/SR_analysis/results/archive/raw/karman_re100_phase_pysr.json b/src/SR_analysis/old/archive/raw/karman_re100_phase_pysr.json similarity index 100% rename from src/SR_analysis/results/archive/raw/karman_re100_phase_pysr.json rename to src/SR_analysis/old/archive/raw/karman_re100_phase_pysr.json diff --git a/src/SR_analysis/results/archive/raw/karman_re100_pysr.json b/src/SR_analysis/old/archive/raw/karman_re100_pysr.json similarity index 100% rename from src/SR_analysis/results/archive/raw/karman_re100_pysr.json rename to src/SR_analysis/old/archive/raw/karman_re100_pysr.json diff --git a/src/SR_analysis/results/archive/raw/karman_re100_v23_th0.003.json b/src/SR_analysis/old/archive/raw/karman_re100_v23_th0.003.json similarity index 100% rename from src/SR_analysis/results/archive/raw/karman_re100_v23_th0.003.json rename to src/SR_analysis/old/archive/raw/karman_re100_v23_th0.003.json diff --git a/src/SR_analysis/results/archive/raw/karman_re150_v23.json b/src/SR_analysis/old/archive/raw/karman_re150_v23.json similarity index 100% rename from src/SR_analysis/results/archive/raw/karman_re150_v23.json rename to src/SR_analysis/old/archive/raw/karman_re150_v23.json diff --git a/src/SR_analysis/results/archive/raw/karman_re200_pysr.json b/src/SR_analysis/old/archive/raw/karman_re200_pysr.json similarity index 100% rename from src/SR_analysis/results/archive/raw/karman_re200_pysr.json rename to src/SR_analysis/old/archive/raw/karman_re200_pysr.json diff --git a/src/SR_analysis/results/archive/raw/karman_re200_v23.json b/src/SR_analysis/old/archive/raw/karman_re200_v23.json similarity index 100% rename from src/SR_analysis/results/archive/raw/karman_re200_v23.json rename to src/SR_analysis/old/archive/raw/karman_re200_v23.json diff --git a/src/SR_analysis/results/archive/raw/karman_re25_v23.json b/src/SR_analysis/old/archive/raw/karman_re25_v23.json similarity index 100% rename from src/SR_analysis/results/archive/raw/karman_re25_v23.json rename to src/SR_analysis/old/archive/raw/karman_re25_v23.json diff --git a/src/SR_analysis/results/archive/raw/karman_re300_v23.json b/src/SR_analysis/old/archive/raw/karman_re300_v23.json similarity index 100% rename from src/SR_analysis/results/archive/raw/karman_re300_v23.json rename to src/SR_analysis/old/archive/raw/karman_re300_v23.json diff --git a/src/SR_analysis/results/archive/raw/karman_re400_pysr.json b/src/SR_analysis/old/archive/raw/karman_re400_pysr.json similarity index 100% rename from src/SR_analysis/results/archive/raw/karman_re400_pysr.json rename to src/SR_analysis/old/archive/raw/karman_re400_pysr.json diff --git a/src/SR_analysis/results/archive/raw/karman_re400_pysr_SI200.json b/src/SR_analysis/old/archive/raw/karman_re400_pysr_SI200.json similarity index 100% rename from src/SR_analysis/results/archive/raw/karman_re400_pysr_SI200.json rename to src/SR_analysis/old/archive/raw/karman_re400_pysr_SI200.json diff --git a/src/SR_analysis/results/archive/raw/karman_re400_pysr_SI400.json b/src/SR_analysis/old/archive/raw/karman_re400_pysr_SI400.json similarity index 100% rename from src/SR_analysis/results/archive/raw/karman_re400_pysr_SI400.json rename to src/SR_analysis/old/archive/raw/karman_re400_pysr_SI400.json diff --git a/src/SR_analysis/results/archive/raw/karman_re400_v23.json b/src/SR_analysis/old/archive/raw/karman_re400_v23.json similarity index 100% rename from src/SR_analysis/results/archive/raw/karman_re400_v23.json rename to src/SR_analysis/old/archive/raw/karman_re400_v23.json diff --git a/src/SR_analysis/results/archive/raw/karman_re50_pysr.json b/src/SR_analysis/old/archive/raw/karman_re50_pysr.json similarity index 100% rename from src/SR_analysis/results/archive/raw/karman_re50_pysr.json rename to src/SR_analysis/old/archive/raw/karman_re50_pysr.json diff --git a/src/SR_analysis/results/archive/raw/karman_re50_v23.json b/src/SR_analysis/old/archive/raw/karman_re50_v23.json similarity index 100% rename from src/SR_analysis/results/archive/raw/karman_re50_v23.json rename to src/SR_analysis/old/archive/raw/karman_re50_v23.json diff --git a/src/SR_analysis/results/archive/raw/karman_re70_deriv.json b/src/SR_analysis/old/archive/raw/karman_re70_deriv.json similarity index 100% rename from src/SR_analysis/results/archive/raw/karman_re70_deriv.json rename to src/SR_analysis/old/archive/raw/karman_re70_deriv.json diff --git a/src/SR_analysis/results/archive/raw/karman_re70_v23.json b/src/SR_analysis/old/archive/raw/karman_re70_v23.json similarity index 100% rename from src/SR_analysis/results/archive/raw/karman_re70_v23.json rename to src/SR_analysis/old/archive/raw/karman_re70_v23.json diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_0.75L_front.json b/src/SR_analysis/old/archive/raw/pysr_illusion_0.75L_front.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_illusion_0.75L_front.json rename to src/SR_analysis/old/archive/raw/pysr_illusion_0.75L_front.json diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_0.75L_front_target.json b/src/SR_analysis/old/archive/raw/pysr_illusion_0.75L_front_target.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_illusion_0.75L_front_target.json rename to src/SR_analysis/old/archive/raw/pysr_illusion_0.75L_front_target.json diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_0.75L_top.json b/src/SR_analysis/old/archive/raw/pysr_illusion_0.75L_top.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_illusion_0.75L_top.json rename to src/SR_analysis/old/archive/raw/pysr_illusion_0.75L_top.json diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_0.75L_top_target.json b/src/SR_analysis/old/archive/raw/pysr_illusion_0.75L_top_target.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_illusion_0.75L_top_target.json rename to src/SR_analysis/old/archive/raw/pysr_illusion_0.75L_top_target.json diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_1.5L_front.json b/src/SR_analysis/old/archive/raw/pysr_illusion_1.5L_front.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_illusion_1.5L_front.json rename to src/SR_analysis/old/archive/raw/pysr_illusion_1.5L_front.json diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_1.5L_top.json b/src/SR_analysis/old/archive/raw/pysr_illusion_1.5L_top.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_illusion_1.5L_top.json rename to src/SR_analysis/old/archive/raw/pysr_illusion_1.5L_top.json diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_1L_front.json b/src/SR_analysis/old/archive/raw/pysr_illusion_1L_front.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_illusion_1L_front.json rename to src/SR_analysis/old/archive/raw/pysr_illusion_1L_front.json diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_1L_front_target.json b/src/SR_analysis/old/archive/raw/pysr_illusion_1L_front_target.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_illusion_1L_front_target.json rename to src/SR_analysis/old/archive/raw/pysr_illusion_1L_front_target.json diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_1L_top.json b/src/SR_analysis/old/archive/raw/pysr_illusion_1L_top.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_illusion_1L_top.json rename to src/SR_analysis/old/archive/raw/pysr_illusion_1L_top.json diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_1L_top_target.json b/src/SR_analysis/old/archive/raw/pysr_illusion_1L_top_target.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_illusion_1L_top_target.json rename to src/SR_analysis/old/archive/raw/pysr_illusion_1L_top_target.json diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_joint_front.json b/src/SR_analysis/old/archive/raw/pysr_illusion_joint_front.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_illusion_joint_front.json rename to src/SR_analysis/old/archive/raw/pysr_illusion_joint_front.json diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_joint_marker_front.json b/src/SR_analysis/old/archive/raw/pysr_illusion_joint_marker_front.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_illusion_joint_marker_front.json rename to src/SR_analysis/old/archive/raw/pysr_illusion_joint_marker_front.json diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_joint_top.json b/src/SR_analysis/old/archive/raw/pysr_illusion_joint_top.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_illusion_joint_top.json rename to src/SR_analysis/old/archive/raw/pysr_illusion_joint_top.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re100_front.json b/src/SR_analysis/old/archive/raw/pysr_karman_re100_front.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re100_front.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re100_front.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re100_front_deep.json b/src/SR_analysis/old/archive/raw/pysr_karman_re100_front_deep.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re100_front_deep.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re100_front_deep.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re100_mu_front.json b/src/SR_analysis/old/archive/raw/pysr_karman_re100_mu_front.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re100_mu_front.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re100_mu_front.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re100_mu_top.json b/src/SR_analysis/old/archive/raw/pysr_karman_re100_mu_top.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re100_mu_top.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re100_mu_top.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re100_top.json b/src/SR_analysis/old/archive/raw/pysr_karman_re100_top.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re100_top.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re100_top.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re100_top_deep.json b/src/SR_analysis/old/archive/raw/pysr_karman_re100_top_deep.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re100_top_deep.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re100_top_deep.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re200_front.json b/src/SR_analysis/old/archive/raw/pysr_karman_re200_front.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re200_front.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re200_front.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re200_front_deep.json b/src/SR_analysis/old/archive/raw/pysr_karman_re200_front_deep.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re200_front_deep.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re200_front_deep.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re200_front_mu.json b/src/SR_analysis/old/archive/raw/pysr_karman_re200_front_mu.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re200_front_mu.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re200_front_mu.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re200_top.json b/src/SR_analysis/old/archive/raw/pysr_karman_re200_top.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re200_top.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re200_top.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re200_top_deep.json b/src/SR_analysis/old/archive/raw/pysr_karman_re200_top_deep.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re200_top_deep.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re200_top_deep.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re200_top_mu.json b/src/SR_analysis/old/archive/raw/pysr_karman_re200_top_mu.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re200_top_mu.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re200_top_mu.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re400_front.json b/src/SR_analysis/old/archive/raw/pysr_karman_re400_front.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re400_front.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re400_front.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re400_front_deep.json b/src/SR_analysis/old/archive/raw/pysr_karman_re400_front_deep.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re400_front_deep.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re400_front_deep.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re400_front_mu.json b/src/SR_analysis/old/archive/raw/pysr_karman_re400_front_mu.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re400_front_mu.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re400_front_mu.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re400_top.json b/src/SR_analysis/old/archive/raw/pysr_karman_re400_top.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re400_top.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re400_top.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re400_top_deep.json b/src/SR_analysis/old/archive/raw/pysr_karman_re400_top_deep.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re400_top_deep.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re400_top_deep.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re400_top_mu.json b/src/SR_analysis/old/archive/raw/pysr_karman_re400_top_mu.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re400_top_mu.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re400_top_mu.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re50_front.json b/src/SR_analysis/old/archive/raw/pysr_karman_re50_front.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re50_front.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re50_front.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re50_front_deep.json b/src/SR_analysis/old/archive/raw/pysr_karman_re50_front_deep.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re50_front_deep.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re50_front_deep.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re50_front_mu.json b/src/SR_analysis/old/archive/raw/pysr_karman_re50_front_mu.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re50_front_mu.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re50_front_mu.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re50_top.json b/src/SR_analysis/old/archive/raw/pysr_karman_re50_top.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re50_top.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re50_top.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re50_top_deep.json b/src/SR_analysis/old/archive/raw/pysr_karman_re50_top_deep.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re50_top_deep.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re50_top_deep.json diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re50_top_mu.json b/src/SR_analysis/old/archive/raw/pysr_karman_re50_top_mu.json similarity index 100% rename from src/SR_analysis/results/archive/raw/pysr_karman_re50_top_mu.json rename to src/SR_analysis/old/archive/raw/pysr_karman_re50_top_mu.json diff --git a/src/SR_analysis/results/archive/raw/vortex_lamb_pysr.json b/src/SR_analysis/old/archive/raw/vortex_lamb_pysr.json similarity index 100% rename from src/SR_analysis/results/archive/raw/vortex_lamb_pysr.json rename to src/SR_analysis/old/archive/raw/vortex_lamb_pysr.json diff --git a/src/SR_analysis/results/archive/raw/vortex_taylor_pysr.json b/src/SR_analysis/old/archive/raw/vortex_taylor_pysr.json similarity index 100% rename from src/SR_analysis/results/archive/raw/vortex_taylor_pysr.json rename to src/SR_analysis/old/archive/raw/vortex_taylor_pysr.json diff --git a/src/SR_analysis/validate/batch_illusion_generalization.sh b/src/SR_analysis/old/batch_illusion_generalization.sh similarity index 100% rename from src/SR_analysis/validate/batch_illusion_generalization.sh rename to src/SR_analysis/old/batch_illusion_generalization.sh diff --git a/src/SR_analysis/compare/illusion_shared_core.json b/src/SR_analysis/old/compare/illusion_shared_core.json similarity index 100% rename from src/SR_analysis/compare/illusion_shared_core.json rename to src/SR_analysis/old/compare/illusion_shared_core.json diff --git a/src/SR_analysis/compare/karman_shared_core.json b/src/SR_analysis/old/compare/karman_shared_core.json similarity index 100% rename from src/SR_analysis/compare/karman_shared_core.json rename to src/SR_analysis/old/compare/karman_shared_core.json diff --git a/src/SR_analysis/validate/eval_rollout.py b/src/SR_analysis/old/eval_rollout.py similarity index 100% rename from src/SR_analysis/validate/eval_rollout.py rename to src/SR_analysis/old/eval_rollout.py diff --git a/src/SR_analysis/validate/launch_pysr_validation.py b/src/SR_analysis/old/launch_pysr_validation.py similarity index 100% rename from src/SR_analysis/validate/launch_pysr_validation.py rename to src/SR_analysis/old/launch_pysr_validation.py diff --git a/src/SR_analysis/configs/legacy/config_cuda.json b/src/SR_analysis/old/legacy/config_cuda.json similarity index 100% rename from src/SR_analysis/configs/legacy/config_cuda.json rename to src/SR_analysis/old/legacy/config_cuda.json diff --git a/src/SR_analysis/configs/legacy/config_flowfield.json b/src/SR_analysis/old/legacy/config_flowfield.json similarity index 100% rename from src/SR_analysis/configs/legacy/config_flowfield.json rename to src/SR_analysis/old/legacy/config_flowfield.json diff --git a/src/SR_analysis/old_data/pareto_karman_re100.json b/src/SR_analysis/old/old_data/pareto_karman_re100.json similarity index 100% rename from src/SR_analysis/old_data/pareto_karman_re100.json rename to src/SR_analysis/old/old_data/pareto_karman_re100.json diff --git a/src/SR_analysis/old_data/pareto_vortex_lamb.json b/src/SR_analysis/old/old_data/pareto_vortex_lamb.json similarity index 100% rename from src/SR_analysis/old_data/pareto_vortex_lamb.json rename to src/SR_analysis/old/old_data/pareto_vortex_lamb.json diff --git a/src/SR_analysis/old_data/pareto_vortex_taylor.json b/src/SR_analysis/old/old_data/pareto_vortex_taylor.json similarity index 100% rename from src/SR_analysis/old_data/pareto_vortex_taylor.json rename to src/SR_analysis/old/old_data/pareto_vortex_taylor.json diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0000_pct00.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_0.75L_PPO_step0000_pct00.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0000_pct00.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_0.75L_PPO_step0000_pct00.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0060_pct12.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_0.75L_PPO_step0060_pct12.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0060_pct12.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_0.75L_PPO_step0060_pct12.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0120_pct25.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_0.75L_PPO_step0120_pct25.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0120_pct25.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_0.75L_PPO_step0120_pct25.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0180_pct37.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_0.75L_PPO_step0180_pct37.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0180_pct37.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_0.75L_PPO_step0180_pct37.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0240_pct50.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_0.75L_PPO_step0240_pct50.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0240_pct50.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_0.75L_PPO_step0240_pct50.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0300_pct62.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_0.75L_PPO_step0300_pct62.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0300_pct62.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_0.75L_PPO_step0300_pct62.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0360_pct75.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_0.75L_PPO_step0360_pct75.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0360_pct75.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_0.75L_PPO_step0360_pct75.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0420_pct87.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_0.75L_PPO_step0420_pct87.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0420_pct87.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_0.75L_PPO_step0420_pct87.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0479_pct99.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_0.75L_PPO_step0479_pct99.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0479_pct99.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_0.75L_PPO_step0479_pct99.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0000_pct00.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_1.5L_PPO_step0000_pct00.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0000_pct00.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_1.5L_PPO_step0000_pct00.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0037_pct12.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_1.5L_PPO_step0037_pct12.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0037_pct12.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_1.5L_PPO_step0037_pct12.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0074_pct24.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_1.5L_PPO_step0074_pct24.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0074_pct24.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_1.5L_PPO_step0074_pct24.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0111_pct37.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_1.5L_PPO_step0111_pct37.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0111_pct37.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_1.5L_PPO_step0111_pct37.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0148_pct49.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_1.5L_PPO_step0148_pct49.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0148_pct49.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_1.5L_PPO_step0148_pct49.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0185_pct61.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_1.5L_PPO_step0185_pct61.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0185_pct61.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_1.5L_PPO_step0185_pct61.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0222_pct74.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_1.5L_PPO_step0222_pct74.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0222_pct74.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_1.5L_PPO_step0222_pct74.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0259_pct86.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_1.5L_PPO_step0259_pct86.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0259_pct86.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_1.5L_PPO_step0259_pct86.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0296_pct98.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_1.5L_PPO_step0296_pct98.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0296_pct98.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_1.5L_PPO_step0296_pct98.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0299_pct99.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_1.5L_PPO_step0299_pct99.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0299_pct99.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_1.5L_PPO_step0299_pct99.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0000_pct00.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_1L_PPO_step0000_pct00.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0000_pct00.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_1L_PPO_step0000_pct00.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0040_pct12.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_1L_PPO_step0040_pct12.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0040_pct12.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_1L_PPO_step0040_pct12.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0080_pct25.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_1L_PPO_step0080_pct25.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0080_pct25.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_1L_PPO_step0080_pct25.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0120_pct37.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_1L_PPO_step0120_pct37.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0120_pct37.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_1L_PPO_step0120_pct37.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0160_pct50.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_1L_PPO_step0160_pct50.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0160_pct50.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_1L_PPO_step0160_pct50.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0200_pct62.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_1L_PPO_step0200_pct62.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0200_pct62.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_1L_PPO_step0200_pct62.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0240_pct75.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_1L_PPO_step0240_pct75.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0240_pct75.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_1L_PPO_step0240_pct75.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0280_pct87.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_1L_PPO_step0280_pct87.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0280_pct87.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_1L_PPO_step0280_pct87.png diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0319_pct99.png b/src/SR_analysis/old/old_data/ppo_step_images/illusion_1L_PPO_step0319_pct99.png similarity index 100% rename from src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0319_pct99.png rename to src/SR_analysis/old/old_data/ppo_step_images/illusion_1L_PPO_step0319_pct99.png diff --git a/src/SR_analysis/old_data/pysr_all_results.json b/src/SR_analysis/old/old_data/pysr_all_results.json similarity index 100% rename from src/SR_analysis/old_data/pysr_all_results.json rename to src/SR_analysis/old/old_data/pysr_all_results.json diff --git a/src/SR_analysis/old_data/sindy_karman_v1.json b/src/SR_analysis/old/old_data/sindy_karman_v1.json similarity index 100% rename from src/SR_analysis/old_data/sindy_karman_v1.json rename to src/SR_analysis/old/old_data/sindy_karman_v1.json diff --git a/src/SR_analysis/old_data/sindy_karman_with_mu.json b/src/SR_analysis/old/old_data/sindy_karman_with_mu.json similarity index 100% rename from src/SR_analysis/old_data/sindy_karman_with_mu.json rename to src/SR_analysis/old/old_data/sindy_karman_with_mu.json diff --git a/src/SR_analysis/old_data/sindy_karman_with_sin.json b/src/SR_analysis/old/old_data/sindy_karman_with_sin.json similarity index 100% rename from src/SR_analysis/old_data/sindy_karman_with_sin.json rename to src/SR_analysis/old/old_data/sindy_karman_with_sin.json diff --git a/src/SR_analysis/old_data/sindy_vortex_v1.json b/src/SR_analysis/old/old_data/sindy_vortex_v1.json similarity index 100% rename from src/SR_analysis/old_data/sindy_vortex_v1.json rename to src/SR_analysis/old/old_data/sindy_vortex_v1.json diff --git a/src/SR_analysis/validate/predict_pysr.py b/src/SR_analysis/old/predict_pysr.py similarity index 100% rename from src/SR_analysis/validate/predict_pysr.py rename to src/SR_analysis/old/predict_pysr.py diff --git a/src/SR_analysis/validate/run_closed_loop.py b/src/SR_analysis/old/run_closed_loop.py similarity index 100% rename from src/SR_analysis/validate/run_closed_loop.py rename to src/SR_analysis/old/run_closed_loop.py diff --git a/src/SR_analysis/validate/run_closed_loop_illusion.py b/src/SR_analysis/old/run_closed_loop_illusion.py similarity index 100% rename from src/SR_analysis/validate/run_closed_loop_illusion.py rename to src/SR_analysis/old/run_closed_loop_illusion.py diff --git a/src/SR_analysis/validate/run_closed_loop_re400_si.py b/src/SR_analysis/old/run_closed_loop_re400_si.py similarity index 100% rename from src/SR_analysis/validate/run_closed_loop_re400_si.py rename to src/SR_analysis/old/run_closed_loop_re400_si.py diff --git a/src/SR_analysis/validate/run_closed_loop_vortex.py b/src/SR_analysis/old/run_closed_loop_vortex.py similarity index 100% rename from src/SR_analysis/validate/run_closed_loop_vortex.py rename to src/SR_analysis/old/run_closed_loop_vortex.py diff --git a/src/SR_analysis/scripts/analyze_illusion_degradation.py b/src/SR_analysis/old/scripts/analyze_illusion_degradation.py similarity index 100% rename from src/SR_analysis/scripts/analyze_illusion_degradation.py rename to src/SR_analysis/old/scripts/analyze_illusion_degradation.py diff --git a/src/SR_analysis/scripts/diagnose_illusion.py b/src/SR_analysis/old/scripts/diagnose_illusion.py similarity index 100% rename from src/SR_analysis/scripts/diagnose_illusion.py rename to src/SR_analysis/old/scripts/diagnose_illusion.py diff --git a/src/SR_analysis/scripts/gen_illusion_target.py b/src/SR_analysis/old/scripts/gen_illusion_target.py similarity index 100% rename from src/SR_analysis/scripts/gen_illusion_target.py rename to src/SR_analysis/old/scripts/gen_illusion_target.py diff --git a/src/SR_analysis/scripts/infer_illusion.py b/src/SR_analysis/old/scripts/infer_illusion.py similarity index 100% rename from src/SR_analysis/scripts/infer_illusion.py rename to src/SR_analysis/old/scripts/infer_illusion.py diff --git a/src/SR_analysis/scripts/infer_karman.py b/src/SR_analysis/old/scripts/infer_karman.py similarity index 100% rename from src/SR_analysis/scripts/infer_karman.py rename to src/SR_analysis/old/scripts/infer_karman.py diff --git a/src/SR_analysis/scripts/infer_vortex.py b/src/SR_analysis/old/scripts/infer_vortex.py similarity index 100% rename from src/SR_analysis/scripts/infer_vortex.py rename to src/SR_analysis/old/scripts/infer_vortex.py diff --git a/src/SR_analysis/scripts/test_cylinder_order.py b/src/SR_analysis/old/scripts/test_cylinder_order.py similarity index 100% rename from src/SR_analysis/scripts/test_cylinder_order.py rename to src/SR_analysis/old/scripts/test_cylinder_order.py diff --git a/src/SR_analysis/scripts/visualize_ppo_illusion.py b/src/SR_analysis/old/scripts/visualize_ppo_illusion.py similarity index 100% rename from src/SR_analysis/scripts/visualize_ppo_illusion.py rename to src/SR_analysis/old/scripts/visualize_ppo_illusion.py diff --git a/src/SR_analysis/sindy/run_pysr.py b/src/SR_analysis/old/sindy/run_pysr.py similarity index 100% rename from src/SR_analysis/sindy/run_pysr.py rename to src/SR_analysis/old/sindy/run_pysr.py diff --git a/src/SR_analysis/sindy/run_pysr_deep.py b/src/SR_analysis/old/sindy/run_pysr_deep.py similarity index 100% rename from src/SR_analysis/sindy/run_pysr_deep.py rename to src/SR_analysis/old/sindy/run_pysr_deep.py diff --git a/src/SR_analysis/sindy/run_pysr_deep_illusion.py b/src/SR_analysis/old/sindy/run_pysr_deep_illusion.py similarity index 100% rename from src/SR_analysis/sindy/run_pysr_deep_illusion.py rename to src/SR_analysis/old/sindy/run_pysr_deep_illusion.py diff --git a/src/SR_analysis/sindy_sr_knowledge.md b/src/SR_analysis/old/sindy_sr_knowledge.md similarity index 100% rename from src/SR_analysis/sindy_sr_knowledge.md rename to src/SR_analysis/old/sindy_sr_knowledge.md diff --git a/src/SR_analysis/sindy_sr_notes.md b/src/SR_analysis/old/sindy_sr_notes.md similarity index 100% rename from src/SR_analysis/sindy_sr_notes.md rename to src/SR_analysis/old/sindy_sr_notes.md diff --git a/src/SR_analysis/validate/results/archive/pysr_deep_indiv_top.json b/src/SR_analysis/old/validate_results_archive/pysr_deep_indiv_top.json similarity index 100% rename from src/SR_analysis/validate/results/archive/pysr_deep_indiv_top.json rename to src/SR_analysis/old/validate_results_archive/pysr_deep_indiv_top.json diff --git a/src/SR_analysis/validate/results/archive/pysr_deep_individual_deep.json b/src/SR_analysis/old/validate_results_archive/pysr_deep_individual_deep.json similarity index 100% rename from src/SR_analysis/validate/results/archive/pysr_deep_individual_deep.json rename to src/SR_analysis/old/validate_results_archive/pysr_deep_individual_deep.json diff --git a/src/SR_analysis/validate/results/archive/pysr_deep_joint_deep.json b/src/SR_analysis/old/validate_results_archive/pysr_deep_joint_deep.json similarity index 100% rename from src/SR_analysis/validate/results/archive/pysr_deep_joint_deep.json rename to src/SR_analysis/old/validate_results_archive/pysr_deep_joint_deep.json diff --git a/src/SR_analysis/validate/results/archive/pysr_deep_joint_top.json b/src/SR_analysis/old/validate_results_archive/pysr_deep_joint_top.json similarity index 100% rename from src/SR_analysis/validate/results/archive/pysr_deep_joint_top.json rename to src/SR_analysis/old/validate_results_archive/pysr_deep_joint_top.json diff --git a/src/SR_analysis/validate/results/archive/pysr_illusion_deep_individual_deep.json b/src/SR_analysis/old/validate_results_archive/pysr_illusion_deep_individual_deep.json similarity index 100% rename from src/SR_analysis/validate/results/archive/pysr_illusion_deep_individual_deep.json rename to src/SR_analysis/old/validate_results_archive/pysr_illusion_deep_individual_deep.json diff --git a/src/SR_analysis/validate/results/archive/pysr_illusion_front.json b/src/SR_analysis/old/validate_results_archive/pysr_illusion_front.json similarity index 100% rename from src/SR_analysis/validate/results/archive/pysr_illusion_front.json rename to src/SR_analysis/old/validate_results_archive/pysr_illusion_front.json diff --git a/src/SR_analysis/validate/results/archive/pysr_illusion_joint_deep.json b/src/SR_analysis/old/validate_results_archive/pysr_illusion_joint_deep.json similarity index 100% rename from src/SR_analysis/validate/results/archive/pysr_illusion_joint_deep.json rename to src/SR_analysis/old/validate_results_archive/pysr_illusion_joint_deep.json diff --git a/src/SR_analysis/validate/results/archive/pysr_illusion_joint_marker_deep.json b/src/SR_analysis/old/validate_results_archive/pysr_illusion_joint_marker_deep.json similarity index 100% rename from src/SR_analysis/validate/results/archive/pysr_illusion_joint_marker_deep.json rename to src/SR_analysis/old/validate_results_archive/pysr_illusion_joint_marker_deep.json diff --git a/src/SR_analysis/validate/results/archive/pysr_illusion_top.json b/src/SR_analysis/old/validate_results_archive/pysr_illusion_top.json similarity index 100% rename from src/SR_analysis/validate/results/archive/pysr_illusion_top.json rename to src/SR_analysis/old/validate_results_archive/pysr_illusion_top.json diff --git a/src/SR_analysis/validate/results/archive/pysr_karman_front.json b/src/SR_analysis/old/validate_results_archive/pysr_karman_front.json similarity index 100% rename from src/SR_analysis/validate/results/archive/pysr_karman_front.json rename to src/SR_analysis/old/validate_results_archive/pysr_karman_front.json diff --git a/src/SR_analysis/validate/results/archive/pysr_karman_joint.json b/src/SR_analysis/old/validate_results_archive/pysr_karman_joint.json similarity index 100% rename from src/SR_analysis/validate/results/archive/pysr_karman_joint.json rename to src/SR_analysis/old/validate_results_archive/pysr_karman_joint.json diff --git a/src/SR_analysis/validate/results/archive/pysr_karman_joint_front.json b/src/SR_analysis/old/validate_results_archive/pysr_karman_joint_front.json similarity index 100% rename from src/SR_analysis/validate/results/archive/pysr_karman_joint_front.json rename to src/SR_analysis/old/validate_results_archive/pysr_karman_joint_front.json diff --git a/src/SR_analysis/validate/results/archive/pysr_karman_joint_mu.json b/src/SR_analysis/old/validate_results_archive/pysr_karman_joint_mu.json similarity index 100% rename from src/SR_analysis/validate/results/archive/pysr_karman_joint_mu.json rename to src/SR_analysis/old/validate_results_archive/pysr_karman_joint_mu.json diff --git a/src/SR_analysis/validate/results/archive/pysr_karman_joint_phasemu.json b/src/SR_analysis/old/validate_results_archive/pysr_karman_joint_phasemu.json similarity index 100% rename from src/SR_analysis/validate/results/archive/pysr_karman_joint_phasemu.json rename to src/SR_analysis/old/validate_results_archive/pysr_karman_joint_phasemu.json diff --git a/src/SR_analysis/validate/results/archive/pysr_karman_joint_phys_dadt.json b/src/SR_analysis/old/validate_results_archive/pysr_karman_joint_phys_dadt.json similarity index 100% rename from src/SR_analysis/validate/results/archive/pysr_karman_joint_phys_dadt.json rename to src/SR_analysis/old/validate_results_archive/pysr_karman_joint_phys_dadt.json diff --git a/src/SR_analysis/validate/results/archive/pysr_karman_joint_top.json b/src/SR_analysis/old/validate_results_archive/pysr_karman_joint_top.json similarity index 100% rename from src/SR_analysis/validate/results/archive/pysr_karman_joint_top.json rename to src/SR_analysis/old/validate_results_archive/pysr_karman_joint_top.json diff --git a/src/SR_analysis/validate/results/archive/pysr_karman_joint_v2_full.json b/src/SR_analysis/old/validate_results_archive/pysr_karman_joint_v2_full.json similarity index 100% rename from src/SR_analysis/validate/results/archive/pysr_karman_joint_v2_full.json rename to src/SR_analysis/old/validate_results_archive/pysr_karman_joint_v2_full.json diff --git a/src/SR_analysis/validate/results/archive/pysr_karman_joint_whitelist.json b/src/SR_analysis/old/validate_results_archive/pysr_karman_joint_whitelist.json similarity index 100% rename from src/SR_analysis/validate/results/archive/pysr_karman_joint_whitelist.json rename to src/SR_analysis/old/validate_results_archive/pysr_karman_joint_whitelist.json diff --git a/src/SR_analysis/validate/results/archive/pysr_karman_top.json b/src/SR_analysis/old/validate_results_archive/pysr_karman_top.json similarity index 100% rename from src/SR_analysis/validate/results/archive/pysr_karman_top.json rename to src/SR_analysis/old/validate_results_archive/pysr_karman_top.json diff --git a/src/SR_analysis/validate/results/illusion_0.75L_PPO_vorticity.png b/src/SR_analysis/old/validate_vorticity_pngs/illusion_0.75L_PPO_vorticity.png similarity index 100% rename from src/SR_analysis/validate/results/illusion_0.75L_PPO_vorticity.png rename to src/SR_analysis/old/validate_vorticity_pngs/illusion_0.75L_PPO_vorticity.png diff --git a/src/SR_analysis/validate/results/illusion_0.75L_target.png b/src/SR_analysis/old/validate_vorticity_pngs/illusion_0.75L_target.png similarity index 100% rename from src/SR_analysis/validate/results/illusion_0.75L_target.png rename to src/SR_analysis/old/validate_vorticity_pngs/illusion_0.75L_target.png diff --git a/src/SR_analysis/validate/results/illusion_0.75L_target_vorticity.png b/src/SR_analysis/old/validate_vorticity_pngs/illusion_0.75L_target_vorticity.png similarity index 100% rename from src/SR_analysis/validate/results/illusion_0.75L_target_vorticity.png rename to src/SR_analysis/old/validate_vorticity_pngs/illusion_0.75L_target_vorticity.png diff --git a/src/SR_analysis/validate/results/illusion_0.75L_uncontrolled.png b/src/SR_analysis/old/validate_vorticity_pngs/illusion_0.75L_uncontrolled.png similarity index 100% rename from src/SR_analysis/validate/results/illusion_0.75L_uncontrolled.png rename to src/SR_analysis/old/validate_vorticity_pngs/illusion_0.75L_uncontrolled.png diff --git a/src/SR_analysis/validate/results/illusion_0.75L_uncontrolled_vorticity.png b/src/SR_analysis/old/validate_vorticity_pngs/illusion_0.75L_uncontrolled_vorticity.png similarity index 100% rename from src/SR_analysis/validate/results/illusion_0.75L_uncontrolled_vorticity.png rename to src/SR_analysis/old/validate_vorticity_pngs/illusion_0.75L_uncontrolled_vorticity.png diff --git a/src/SR_analysis/validate/results/illusion_0.75L_vorticity.png b/src/SR_analysis/old/validate_vorticity_pngs/illusion_0.75L_vorticity.png similarity index 100% rename from src/SR_analysis/validate/results/illusion_0.75L_vorticity.png rename to src/SR_analysis/old/validate_vorticity_pngs/illusion_0.75L_vorticity.png diff --git a/src/SR_analysis/validate/results/illusion_1.5L_PPO_vorticity.png b/src/SR_analysis/old/validate_vorticity_pngs/illusion_1.5L_PPO_vorticity.png similarity index 100% rename from src/SR_analysis/validate/results/illusion_1.5L_PPO_vorticity.png rename to src/SR_analysis/old/validate_vorticity_pngs/illusion_1.5L_PPO_vorticity.png diff --git a/src/SR_analysis/validate/results/illusion_1.5L_target.png b/src/SR_analysis/old/validate_vorticity_pngs/illusion_1.5L_target.png similarity index 100% rename from src/SR_analysis/validate/results/illusion_1.5L_target.png rename to src/SR_analysis/old/validate_vorticity_pngs/illusion_1.5L_target.png diff --git a/src/SR_analysis/validate/results/illusion_1.5L_target_vorticity.png b/src/SR_analysis/old/validate_vorticity_pngs/illusion_1.5L_target_vorticity.png similarity index 100% rename from src/SR_analysis/validate/results/illusion_1.5L_target_vorticity.png rename to src/SR_analysis/old/validate_vorticity_pngs/illusion_1.5L_target_vorticity.png diff --git a/src/SR_analysis/validate/results/illusion_1.5L_uncontrolled.png b/src/SR_analysis/old/validate_vorticity_pngs/illusion_1.5L_uncontrolled.png similarity index 100% rename from src/SR_analysis/validate/results/illusion_1.5L_uncontrolled.png rename to src/SR_analysis/old/validate_vorticity_pngs/illusion_1.5L_uncontrolled.png diff --git a/src/SR_analysis/validate/results/illusion_1.5L_uncontrolled_vorticity.png b/src/SR_analysis/old/validate_vorticity_pngs/illusion_1.5L_uncontrolled_vorticity.png similarity index 100% rename from src/SR_analysis/validate/results/illusion_1.5L_uncontrolled_vorticity.png rename to src/SR_analysis/old/validate_vorticity_pngs/illusion_1.5L_uncontrolled_vorticity.png diff --git a/src/SR_analysis/validate/results/illusion_1.5L_vorticity.png b/src/SR_analysis/old/validate_vorticity_pngs/illusion_1.5L_vorticity.png similarity index 100% rename from src/SR_analysis/validate/results/illusion_1.5L_vorticity.png rename to src/SR_analysis/old/validate_vorticity_pngs/illusion_1.5L_vorticity.png diff --git a/src/SR_analysis/validate/results/illusion_1L_PPO_vorticity.png b/src/SR_analysis/old/validate_vorticity_pngs/illusion_1L_PPO_vorticity.png similarity index 100% rename from src/SR_analysis/validate/results/illusion_1L_PPO_vorticity.png rename to src/SR_analysis/old/validate_vorticity_pngs/illusion_1L_PPO_vorticity.png diff --git a/src/SR_analysis/validate/results/illusion_1L_target.png b/src/SR_analysis/old/validate_vorticity_pngs/illusion_1L_target.png similarity index 100% rename from src/SR_analysis/validate/results/illusion_1L_target.png rename to src/SR_analysis/old/validate_vorticity_pngs/illusion_1L_target.png diff --git a/src/SR_analysis/validate/results/illusion_1L_target_vorticity.png b/src/SR_analysis/old/validate_vorticity_pngs/illusion_1L_target_vorticity.png similarity index 100% rename from src/SR_analysis/validate/results/illusion_1L_target_vorticity.png rename to src/SR_analysis/old/validate_vorticity_pngs/illusion_1L_target_vorticity.png diff --git a/src/SR_analysis/validate/results/illusion_1L_uncontrolled.png b/src/SR_analysis/old/validate_vorticity_pngs/illusion_1L_uncontrolled.png similarity index 100% rename from src/SR_analysis/validate/results/illusion_1L_uncontrolled.png rename to src/SR_analysis/old/validate_vorticity_pngs/illusion_1L_uncontrolled.png diff --git a/src/SR_analysis/validate/results/illusion_1L_uncontrolled_vorticity.png b/src/SR_analysis/old/validate_vorticity_pngs/illusion_1L_uncontrolled_vorticity.png similarity index 100% rename from src/SR_analysis/validate/results/illusion_1L_uncontrolled_vorticity.png rename to src/SR_analysis/old/validate_vorticity_pngs/illusion_1L_uncontrolled_vorticity.png diff --git a/src/SR_analysis/validate/results/illusion_1L_vorticity.png b/src/SR_analysis/old/validate_vorticity_pngs/illusion_1L_vorticity.png similarity index 100% rename from src/SR_analysis/validate/results/illusion_1L_vorticity.png rename to src/SR_analysis/old/validate_vorticity_pngs/illusion_1L_vorticity.png diff --git a/src/SR_analysis/results/README.md b/src/SR_analysis/results/README.md new file mode 100644 index 0000000..6911419 --- /dev/null +++ b/src/SR_analysis/results/README.md @@ -0,0 +1,80 @@ +# Results Reference Table + +> Canonical result files for PySR symbolic regression and CFD closed-loop validation. +> See `docs/SR_analysis_report.md` and `docs/illusion_joint_formula_analysis.md` for analysis. + +--- + +## Category A: Canonical PySR Formulas + +| File | Scene | Formula | R2 | +|------|-------|---------|:--:| +| `formulas/karman_joint_front.json` | Karman cross-Re | `daF_dt - 14.952*mu*Cl_tot` | 1.000 | +| `formulas/karman_joint_top.json` | Karman cross-Re | `3.414` (constant) | 1.000 | +| `formulas/illusion_joint_front.json` | Illusion joint | `Cd_tot - (Cd_err + 5.428) - 0.00978*(du_a_dt + u_a)` | 0.907 | +| `formulas/illusion_joint_top.json` | Illusion joint | `(Cd_err - (Cd_rear - Cl_err))*0.535 + 2.782` | 0.828 | +| `formulas/karman_re50_front.json` | Karman Re=50 | Independent per-Re | 1.000 | +| `formulas/karman_re100_front.json` | Karman Re=100 | Independent per-Re | 1.000 | +| `formulas/karman_re200_front.json` | Karman Re=200 | Independent per-Re | 1.000 | +| `formulas/karman_re400_front.json` | Karman Re=400 | Independent per-Re | 1.000 | +| `formulas/illusion_0.75L_front.json` | Illusion 0.75L | `-0.169*(Cl_tot + dCl_tot_dt) - 1.240` | 0.692 | +| `formulas/illusion_1L_front.json` | Illusion 1L | `(du_a_dt + u_a + 26.5)*0.0123` | 0.929 | +| `formulas/illusion_1.5L_front.json` | Illusion 1.5L | Non-SR (see report) | — | +| `formulas/illusion_joint_marker_front.json` | Illusion joint + marker | `diam*((du_a_dt+u_a)*0.01 + 6.32) - 6.00` | 0.942 | + +--- + +## Category B: CFD Closed-Loop Validation + +| File | Scene | Mode | Similarity | +|------|-------|------|:---:| +| `validations/karman_re50.json` | Karman Re=50 | Joint formula | 0.847 | +| `validations/karman_re100.json` | Karman Re=100 | Joint formula | 0.888 | +| `validations/karman_re200.json` | Karman Re=200 | Joint formula | 0.845 | +| `validations/karman_re400.json` | Karman Re=400 | Joint formula | 0.806 | +| `validations/karman_re50_ppo.json` | Karman Re=50 | PPO baseline | 0.961 | +| `validations/karman_re100_ppo.json` | Karman Re=100 | PPO baseline | 0.954 | +| `validations/karman_re200_ppo.json` | Karman Re=200 | PPO baseline | 0.884 | +| `validations/karman_re400_ppo.json` | Karman Re=400 | PPO baseline | 0.795 | +| `validations/illusion_0.75L.json` | Illusion 0.75L | Joint formula | 0.978 | +| `validations/illusion_1L.json` | Illusion 1L | Joint formula | 0.970 | +| `validations/illusion_0.5L.json` | Illusion 0.5L | Joint formula (gen) | 0.854 | +| `validations/illusion_0.6L.json` | Illusion 0.6L | Joint formula (gen) | 0.939 | +| `validations/illusion_0.8L.json` | Illusion 0.8L | Joint formula (gen) | 0.908 | +| `validations/illusion_1.2L.json` | Illusion 1.2L | Joint formula (gen) | 0.849 | +| `validations/illusion_2L.json` | Illusion 2L | Joint formula (gen) | 0.676 | +| `validations/vortex_lamb.json` | Vortex lamb | Karman formula | 0.949 | +| `validations/vortex_taylor.json` | Vortex taylor | Karman formula | 0.905 | + +--- + +## Category C: Source Data + +| Path | Content | +|------|---------| +| `data/karman/karman_re{50,100,200,400}/controlled.npz` | PPO actions, sensors, forces | +| `data/illusion/illusion_{0.5L..2L}/controlled.npz` | PPO data (trained diameters only) | +| `data/illusion/illusion_{0.5L..2L}/target.npz` | Target sensor signals | +| `data/illusion/illusion_{0.5L..2L}/target_harmonics.json` | FFT harmonics | +| `data/vortex/vortex_{lamb,taylor}/controlled.npz` | Vortex PPO data | + +--- + +## How to Read These Files + +**Formula JSONs** (`results/formulas/*.json`): +- `feature_names`: list of feature names used in the formula +- `best_sympy`: the symbolic expression (parseable by sympy) +- `best_score`: one-step R-squared on training/validation data + +**Validation JSONs** (`results/validations/*.json`): +- `similarity`: CFD closed-loop DTW similarity (the final metric) +- `mode`: "pysr", "ppo", or "uncontrolled" +- `n_steps`: number of control steps used in validation + +**NPZ data** (`data/*/controlled.npz`): +- `actions`: shape [N, 3], normalized [-1, 1] +- `sensors`: shape [N, 6], raw sensor velocities [s0_ux, s0_uy, s1_ux, s1_uy, s2_ux, s2_uy] +- `forces`: shape [N, 6], raw forces [front_fx, front_fy, bottom_fx, bottom_fy, top_fx, top_fy] + +The single source of truth for all numerical results is `scene_registry.json` at the root of SR_analysis/. diff --git a/src/SR_analysis/results/figures/FIGURE_INDEX.md b/src/SR_analysis/results/figures/FIGURE_INDEX.md new file mode 100644 index 0000000..2cb54df --- /dev/null +++ b/src/SR_analysis/results/figures/FIGURE_INDEX.md @@ -0,0 +1,102 @@ +# SR Analysis Figure Index + +All figures are in `results/figures/` as both PNG and PDF (300 DPI, publication quality). + +--- + +## Figure 1: Illusion Cross-Diameter Generalization +**File**: `fig_illusion_degradation.png`, `fig_illusion_degradation.pdf` + +**Caption**: CFD closed-loop validation of the joint symbolic regression formula +across target cylinder diameters. The formula was trained on 0.75L and 1.0L data +jointly. Performance degrades monotonically as diameter deviates from the training +range. At 1.5L, the PPO policy employs high-frequency periodic modulation (5.6x +the shedding frequency), which symbolic regression cannot capture with the current +feature set. The shaded regions indicate PPO-level performance (similarity > 0.9) +and degradation regime. + +**Paper section**: Results — Symbolic Regression — Illusion Cross-Diameter Generalization. + +--- + +## Figure 2: Karman Cross-Re Formula Validation +**File**: `fig_karman_cross_re.png`, `fig_karman_cross_re.pdf` + +**Caption**: Comparison of PPO policy and joint PySR formula performance across +Reynolds numbers for the Karman cloak task. The joint formula +(alpha_F = daF_dt - 14.95 * mu * Cl_tot) was trained on Re 50-400 data and +generalizes to unseen intermediate Reynolds numbers (25, 70, 150, 300). The dashed +line shows the average PPO performance across trained Reynolds numbers. + +**Paper section**: Results — Symbolic Regression — Karman Cross-Re Joint Formula. + +--- + +## Figure 3: Formula Structure Comparison +**File**: `fig_formula_comparison.png`, `fig_formula_comparison.pdf` + +**Caption**: Structural comparison of the discovered symbolic control laws for the +Karman cloak and illusion tasks. The Karman formula is dominated by force-feedback +terms (action derivative and viscous-scaled lift coefficient), while the illusion +formula is dominated by error-tracking terms (drag coefficient mismatch between +pinball and target). This structural difference reflects the fundamentally different +control objectives: wake preservation vs. wake sculpting. + +**Paper section**: Results — Symbolic Regression — Structural Comparison. + +--- + +## Figure 4: Vortex Cross-Scene Generalization +**File**: `fig_vortex_generalization.png`, `fig_vortex_generalization.pdf` + +**Caption**: The Karman joint formula, trained only on Karman vortex street data, +generalizes to two transient vortex cloaking tasks without retraining. For the +Lamb dipole, the symbolic formula exceeds the PPO policy's performance. This +demonstrates that the discovered feedback structure (force-feedback with viscous +scaling) captures a universal cloaking mechanism rather than a scene-specific +strategy. + +**Paper section**: Results — Symbolic Regression — Cross-Scene Generalization. + +--- + +## Figure 5: PPO Control Action Timeseries +**File**: `fig_action_comparison.png`, `fig_action_comparison.pdf` + +**Caption**: Representative PPO control actions for the illusion 1.0L scene, +showing the front cylinder (dominated by drag-error tracking) and rear cylinders +(oscillating in anti-phase). The actions are non-dimensional alpha = omega/U0, +where omega is the cylinder rotational angular velocity. The periodic structure +reflects the vortex shedding cycle of the target cylinder. + +**Paper section**: Results — Symbolic Regression — Control Action Analysis. + +--- + +## Figure 6: Master Results Table +**File**: `fig_master_table.png`, `fig_master_table.pdf` + +**Caption**: Complete results table for all illusion scenes. Green cells indicate +near-PPO performance (similarity > 95%), yellow indicates moderate degradation +(80-95%), and red indicates significant degradation (< 80%). The 1.5L scene +(gray) is fundamentally non-fittable by symbolic regression due to its +high-frequency action modulation. "% of PPO" shows the formula's performance +as a percentage of the PPO baseline. + +**Paper section**: Results — Symbolic Regression — Summary Table. + +--- + +## Data Sources + +All numerical values in these figures are derived from: +- `scene_registry.json` — canonical result registry +- `results/validations/*.json` — CFD closed-loop validation outputs +- `data/*/controlled.npz` — PPO inference data (actions, sensors, forces) + +## Reproduction + +To regenerate all figures: +```bash +conda run -n pycuda_3_10 python stage_4_analyze.py +``` diff --git a/src/SR_analysis/results/figures/fig_action_comparison.pdf b/src/SR_analysis/results/figures/fig_action_comparison.pdf new file mode 100644 index 0000000..47a76dd Binary files /dev/null and b/src/SR_analysis/results/figures/fig_action_comparison.pdf differ diff --git a/src/SR_analysis/results/figures/fig_action_comparison.png b/src/SR_analysis/results/figures/fig_action_comparison.png new file mode 100644 index 0000000..b9f3708 Binary files /dev/null and b/src/SR_analysis/results/figures/fig_action_comparison.png differ diff --git a/src/SR_analysis/results/figures/fig_formula_comparison.pdf b/src/SR_analysis/results/figures/fig_formula_comparison.pdf new file mode 100644 index 0000000..5f30052 Binary files /dev/null and b/src/SR_analysis/results/figures/fig_formula_comparison.pdf differ diff --git a/src/SR_analysis/results/figures/fig_formula_comparison.png b/src/SR_analysis/results/figures/fig_formula_comparison.png new file mode 100644 index 0000000..861de31 Binary files /dev/null and b/src/SR_analysis/results/figures/fig_formula_comparison.png differ diff --git a/src/SR_analysis/results/figures/fig_illusion_degradation.pdf b/src/SR_analysis/results/figures/fig_illusion_degradation.pdf new file mode 100644 index 0000000..9118078 Binary files /dev/null and b/src/SR_analysis/results/figures/fig_illusion_degradation.pdf differ diff --git a/src/SR_analysis/results/figures/fig_illusion_degradation.png b/src/SR_analysis/results/figures/fig_illusion_degradation.png new file mode 100644 index 0000000..77546d3 Binary files /dev/null and b/src/SR_analysis/results/figures/fig_illusion_degradation.png differ diff --git a/src/SR_analysis/results/figures/fig_karman_cross_re.pdf b/src/SR_analysis/results/figures/fig_karman_cross_re.pdf new file mode 100644 index 0000000..a54345a Binary files /dev/null and b/src/SR_analysis/results/figures/fig_karman_cross_re.pdf differ diff --git a/src/SR_analysis/results/figures/fig_karman_cross_re.png b/src/SR_analysis/results/figures/fig_karman_cross_re.png new file mode 100644 index 0000000..7652e10 Binary files /dev/null and b/src/SR_analysis/results/figures/fig_karman_cross_re.png differ diff --git a/src/SR_analysis/results/figures/fig_master_table.pdf b/src/SR_analysis/results/figures/fig_master_table.pdf new file mode 100644 index 0000000..b2db16d Binary files /dev/null and b/src/SR_analysis/results/figures/fig_master_table.pdf differ diff --git a/src/SR_analysis/results/figures/fig_master_table.png b/src/SR_analysis/results/figures/fig_master_table.png new file mode 100644 index 0000000..7b2ec92 Binary files /dev/null and b/src/SR_analysis/results/figures/fig_master_table.png differ diff --git a/src/SR_analysis/results/figures/fig_vortex_generalization.pdf b/src/SR_analysis/results/figures/fig_vortex_generalization.pdf new file mode 100644 index 0000000..78298a4 Binary files /dev/null and b/src/SR_analysis/results/figures/fig_vortex_generalization.pdf differ diff --git a/src/SR_analysis/results/figures/fig_vortex_generalization.png b/src/SR_analysis/results/figures/fig_vortex_generalization.png new file mode 100644 index 0000000..538ec49 Binary files /dev/null and b/src/SR_analysis/results/figures/fig_vortex_generalization.png differ diff --git a/src/SR_analysis/stage_3_validate.py b/src/SR_analysis/stage_3_validate.py index 9d5517d..b080e97 100644 --- a/src/SR_analysis/stage_3_validate.py +++ b/src/SR_analysis/stage_3_validate.py @@ -51,9 +51,7 @@ from SR_analysis.configs import get_scene, SCENES, LEGACY_CFG_DIR, FIFO_LEN DATA_TYPE = np.float32 NX = 1280 -# Import detailed logic from existing scripts -from SR_analysis.validate.run_closed_loop import predict_v23, predict_v23_deriv, load_sindy_coefs -from SR_analysis.scripts.infer_illusion import gen_target_states_at +from SR_analysis.utils.harmonics import gen_target_states_at # Scene groups VALIDATE_GROUPS = { diff --git a/src/SR_analysis/stage_4_analyze.py b/src/SR_analysis/stage_4_analyze.py index 1b0b3f3..fba016b 100644 --- a/src/SR_analysis/stage_4_analyze.py +++ b/src/SR_analysis/stage_4_analyze.py @@ -1,27 +1,14 @@ #!/usr/bin/env python3 -"""Stage 4: Unified analysis and visualization. +"""Stage 4: Generate all publication-quality figures from validation results. -Analyzes PPO policies and SR formulas: action timeseries, FFT spectra, -degradation curves, formula-vs-PPO comparisons. +Reads scene_registry.json + results/validations/*.json and produces 6 figures +to results/figures/. Also creates FIGURE_INDEX.md. Usage: - # PPO action visualization - conda run -n pycuda_3_10 python stage_4_analyze.py --scene illusion_1L --mode ppo-viz - - # Degradation analysis (cross-diameter) - conda run -n pycuda_3_10 python stage_4_analyze.py \\ - --scenes illusion_0.75L,illusion_1L,illusion_1.5L --mode degradation - - # Formula vs PPO comparison - conda run -n pycuda_3_10 python stage_4_analyze.py \\ - --scene illusion_1L --mode formula-compare \\ - --formula-front results/formulas/illusion_joint_front.json - -Output: PNG figures written to data/figures/ + conda run -n pycuda_3_10 python stage_4_analyze.py """ from __future__ import annotations -import argparse import json import os import sys @@ -31,6 +18,7 @@ import numpy as np import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt +from matplotlib.ticker import FuncFormatter _REPO = Path(__file__).resolve().parents[1] if str(_REPO) not in sys.path: @@ -39,121 +27,568 @@ _SRC = _REPO / "src" if str(_SRC) not in sys.path: sys.path.insert(0, str(_SRC)) -from SR_analysis.configs import get_scene -from SR_analysis.utils.feature_builder import compute_dimensionless +from SR_analysis.configs import get_scene, SCENES -U0 = 0.01 -D_CYL = 20.0 -FIG_DIR = "src/SR_analysis/data/figures" -os.makedirs(FIG_DIR, exist_ok=True) +SR_DIR = Path(__file__).resolve().parent +FIG_DIR = SR_DIR / "results" / "figures" +VAL_DIR = SR_DIR / "results" / "validations" +FIG_DIR.mkdir(parents=True, exist_ok=True) + +# Matplotlib config +plt.rcParams.update({ + "figure.dpi": 150, "savefig.dpi": 300, + "font.size": 12, "axes.titlesize": 14, "axes.labelsize": 13, + "xtick.labelsize": 11, "ytick.labelsize": 11, + "legend.fontsize": 10, "figure.figsize": (10, 6), + "savefig.bbox": "tight", "savefig.pad_inches": 0.1, +}) + +COLORS = {"ppo": "#1f77b4", "formula": "#ff7f0e", "individual": "#2ca02c", + "generalization": "#d62728", "light": "#bcbd22"} -def load_data(scene_name): - cfg = get_scene(scene_name) - sid = cfg["scene_id"] - path = f"src/SR_analysis/data/{sid}/{scene_name}/controlled.npz" - npz = np.load(path, allow_pickle=True) - return {k: npz[k].astype(np.float64) for k in ["sensors", "forces", "actions"]}, cfg +def _load_registry(): + path = SR_DIR / "scene_registry.json" + if path.exists(): + return json.load(open(path)) + return {"scenes": {}} -def plot_ppo_viz(scene_names): - """Generate action timeseries + FFT for each scene.""" - for sn in scene_names: - data, cfg = load_data(sn) - a = data["actions"] - scale, bias = cfg["action_scale"], np.array(cfg["action_bias"]) - alpha = (a * scale + bias) * cfg["u0"] / cfg["u0"] - - fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8)) - n_show = min(100, alpha.shape[0]) - for ci, name, color in [(0, "Front", "b"), (1, "Bottom", "g"), (2, "Top", "r")]: - ax1.plot(alpha[:n_show, ci], color=color, label=name, linewidth=0.8) - ax1.set_title(f"{sn} — PPO Alpha Timeseries (first {n_show} steps)") - ax1.legend(); ax1.set_ylabel(r"$\alpha = \omega/U_0$") - - for ci, name, color in [(0, "Front", "b"), (1, "Bottom", "g"), (2, "Top", "r")]: - s = alpha[:, ci] - alpha[:, ci].mean() - fft = np.abs(np.fft.rfft(s)) - freqs = np.fft.rfftfreq(len(s)) - ax2.plot(freqs[1:], fft[1:], color=color, label=name, alpha=0.8) - if len(fft) > 1: - dom = np.argmax(fft[1:]) + 1 - ax2.axvline(freqs[dom], color=color, linestyle="--", alpha=0.3) - ax2.set_title(f"{sn} — FFT Spectrum") - ax2.set_xlabel("Frequency (control steps)"); ax2.set_ylabel("|FFT|"); ax2.legend() - - plt.tight_layout(); plt.savefig(os.path.join(FIG_DIR, f"ppo_viz_{sn}.png"), dpi=120); plt.close() - print(f" Saved: ppo_viz_{sn}.png") +def _load_validation(scene_name): + path = VAL_DIR / f"{scene_name}.json" + if path.exists(): + return json.load(open(path)) + return None -def plot_degradation(scene_names): - """Cross-diameter comparison: alpha std, action range, feature statistics.""" - stats = {} - for sn in scene_names: - data, cfg = load_data(sn) - a = data["actions"] - scale, bias = cfg["action_scale"], np.array(cfg["action_bias"]) - alpha = (a * scale + bias) * cfg["u0"] / cfg["u0"] - dim = compute_dimensionless(data["sensors"], data["forces"], u0=cfg["u0"], d=D_CYL) - cd_tot = dim["Cd_F"] + dim["Cd_T"] + dim["Cd_B"] - stats[sn] = { - "alpha_front_std": alpha[:, 0].std(), - "alpha_front_range": alpha[:, 0].max() - alpha[:, 0].min(), - "cd_tot_mean": cd_tot.mean(), - "cd_tot_std": cd_tot.std(), - "diameter": cfg.get("target_diameter", 1.0), - } +# ========================================================================= +# Figure 1: Illusion cross-diameter generalization curve +# ========================================================================= +def fig_illusion_degradation(): + """Main result: joint formula degrades as target diameter deviates from training.""" + reg = _load_registry() + scenes = reg.get("scenes", {}) + + # PPO baselines (trained diameters only) + ppo_diams, ppo_sims = [], [] + ppo_scenes = {"illusion_0.75L": 0.75, "illusion_1L": 1.0, "illusion_1.5L": 1.5} + for sn, diam in ppo_scenes.items(): + if sn in scenes and "ppo_similarity" in scenes[sn]: + ppo_diams.append(diam) + ppo_sims.append(scenes[sn]["ppo_similarity"]) + + # Formula results (joint formula on all diameters) + formula_diams, formula_sims = [], [] + individual_diams, individual_sims = [], [] + for d in [0.5, 0.6, 0.75, 0.8, 1.0, 1.2, 1.5, 2.0]: + key = f"illusion_{str(d).rstrip('0').rstrip('.')}L" + if key in scenes: + val = scenes[key].get("validations", {}) + v = val.get("pysr_joint", {}) + s = v.get("similarity") + if s is not None and s > 0: + formula_diams.append(d) + formula_sims.append(s) + # Individual formula + iv = val.get("pysr_individual", {}) + if not iv: + vi = _load_validation(key) + if vi and "formula" in str(vi.get("mode", "")): + pass # individual formula results stored separately + + fig, ax = plt.subplots(figsize=(12, 7)) + + # Formula curve + ax.plot(formula_diams, formula_sims, "o-", color=COLORS["formula"], linewidth=2.5, + markersize=10, label="Joint PySR Formula (0.75L + 1L trained)", zorder=3) + + # PPO diamonds + ax.scatter(ppo_diams, ppo_sims, s=180, marker="D", color=COLORS["ppo"], + edgecolors="black", linewidths=1.0, zorder=4, label="PPO Policy (trained)") + + # Shaded regions + ax.axhspan(0.9, 1.01, alpha=0.08, color="green", label="PPO-level regime (sim > 0.9)") + ax.axhspan(0.0, 0.9, alpha=0.04, color="red", label="Degradation regime") + + # Annotation at 1.5L + ax.annotate("Non-SR regime\n(high-freq modulation,\nnon-fittable)", + xy=(1.5, 0.92), xytext=(1.8, 0.88), + fontsize=10, ha="center", + bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow", alpha=0.9), + arrowprops=dict(arrowstyle="->", color="gray", lw=1.2)) + + ax.set_xlabel("Target Cylinder Diameter (L0 units)", fontsize=14) + ax.set_ylabel("CFD Closed-Loop DTW Similarity", fontsize=14) + ax.set_title("Illusion Cross-Diameter Generalization", fontsize=16, fontweight="bold") + ax.set_ylim(0.55, 1.02) + ax.set_xlim(0.35, 2.15) + ax.grid(True, alpha=0.3, linestyle="--") + ax.legend(loc="lower left", framealpha=0.9) - fig, axes = plt.subplots(2, 2, figsize=(12, 10)) - diams = [stats[s]["diameter"] for s in scene_names] - ax1 = axes[0, 0]; ax1.plot(diams, [stats[s]["alpha_front_std"] for s in scene_names], "bo-") - ax1.set_xlabel("Target diameter (L0)"); ax1.set_ylabel("Alpha Front std"); ax1.grid(True) - ax2 = axes[0, 1]; ax2.plot(diams, [stats[s]["alpha_front_range"] for s in scene_names], "ro-") - ax2.set_xlabel("Target diameter (L0)"); ax2.set_ylabel("Alpha Front range"); ax2.grid(True) - ax3 = axes[1, 0]; ax3.plot(diams, [stats[s]["cd_tot_mean"] for s in scene_names], "go-") - ax3.set_xlabel("Target diameter (L0)"); ax3.set_ylabel("Cd_tot mean"); ax3.grid(True) - ax4 = axes[1, 1]; ax4.plot(diams, [stats[s]["cd_tot_std"] for s in scene_names], "mo-") - ax4.set_xlabel("Target diameter (L0)"); ax4.set_ylabel("Cd_tot std"); ax4.grid(True) - plt.suptitle("Degradation Metrics Across Illusion Diameters") plt.tight_layout() - plt.savefig(os.path.join(FIG_DIR, "degradation_metrics.png"), dpi=120); plt.close() - print(f" Saved: degradation_metrics.png") - - for sn in scene_names: - s = stats[sn] - print(f" {sn}: diam={s['diameter']}, αF_std={s['alpha_front_std']:.3f}, " - f"αF_range={s['alpha_front_range']:.2f}, Cd_mean={s['cd_tot_mean']:.3f}") + for fmt in ["png", "pdf"]: + plt.savefig(FIG_DIR / f"fig_illusion_degradation.{fmt}") + plt.close() + print(" [fig1] illusion_degradation saved") + return list(zip(formula_diams, formula_sims)) -def plot_formula_compare(scene_name, formula_front_path): - """Not yet implemented — placeholder for future formula comparison.""" - print(f" Formula comparison for {scene_name} — coming in a future update") - print(f" Formula file: {formula_front_path}") +# ========================================================================= +# Figure 2: Karman cross-Re validation bars +# ========================================================================= +def fig_karman_cross_re(): + """Grouped bar chart: PPO vs formula for trained Re, lighter for generalization.""" + reg = _load_registry() + scenes = reg.get("scenes", {}) + + def _get_sim(key, field): + if key not in scenes: + return None + return scenes[key].get(field) + + trained_re = [50, 100, 200, 400] + ppo_re50_400, formula_re50_400 = [], [] + for r in trained_re: + key = f"karman_re{r}" + p = _get_sim(key, "ppo_similarity") + fv = scenes[key].get("validations", {}).get("pysr_joint", {}) + f = fv.get("similarity") if fv else None + ppo_re50_400.append(p) + formula_re50_400.append(f) + + gen_re = [25, 70, 150, 300] + formula_re_gen = [] + gen_labels = [] + for r in gen_re: + key = f"karman_re{r}" + v = _load_validation(key) + if v and v.get("similarity"): + formula_re_gen.append(v["similarity"]) + gen_labels.append(f"Re={r}") + else: + formula_re_gen.append(None) + gen_labels.append(f"Re={r} (no data)") + + # Plot + fig, ax = plt.subplots(figsize=(12, 7)) + x = np.arange(len(trained_re)) + w = 0.35 + + bars1 = ax.bar(x - w/2, ppo_re50_400, w, label="PPO Policy", color=COLORS["ppo"], + edgecolor="black", linewidth=0.8, zorder=3) + bars2 = ax.bar(x + w/2, formula_re50_400, w, label="Joint PySR Formula", color=COLORS["formula"], + edgecolor="black", linewidth=0.8, zorder=3) + + # Generalization bars (lighter, offset) + x_gen = np.arange(len(gen_re)) + len(trained_re) + 0.8 + formula_gen_filtered = [v for v in formula_re_gen if v is not None] + x_gen_filtered = [x_gen[i] for i, v in enumerate(formula_re_gen) if v is not None] + if x_gen_filtered: + ax.bar(x_gen_filtered, formula_gen_filtered, w * 1.0, label="Generalization (unseen Re)", + color=COLORS["generalization"], alpha=0.55, edgecolor="black", edgewidth=0.5) + + # PPO average line + valid_ppo = [p for p in ppo_re50_400 if p is not None] + if valid_ppo: + avg_ppo = np.mean(valid_ppo) + ax.axhline(avg_ppo, color=COLORS["ppo"], linestyle="--", linewidth=1.2, alpha=0.7, + label=f"PPO avg ({avg_ppo:.3f})") + + ax.set_xticks(list(x) + list(x_gen)) + ax.set_xticklabels([f"Re={r}" for r in trained_re] + gen_labels, rotation=0) + ax.set_ylabel("CFD DTW Similarity", fontsize=14) + ax.set_title("Karman Cloak: Cross-Re Formula Validation", fontsize=16, fontweight="bold") + ax.set_ylim(0.65, 1.02) + ax.legend(loc="lower right", framealpha=0.9) + ax.grid(True, axis="y", alpha=0.3, linestyle="--") + + # Annotate bars + for bar in bars2: + h = bar.get_height() + ax.text(bar.get_x() + bar.get_width()/2., h + 0.008, f"{h:.3f}", + ha="center", va="bottom", fontsize=8, fontweight="bold") + + plt.tight_layout() + for fmt in ["png", "pdf"]: + plt.savefig(FIG_DIR / f"fig_karman_cross_re.{fmt}") + plt.close() + print(" [fig2] karman_cross_re saved") + return {} +# ========================================================================= +# Figure 3: Formula structure comparison (conceptual) +# ========================================================================= +def fig_formula_comparison(): + """Side-by-side diagram showing Karman vs Illusion formula structures.""" + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 7)) + + # Left: Karman + ax1.set_xlim(0, 10); ax1.set_ylim(0, 10) + ax1.axis("off") + ax1.set_title("Karman Cloak\n(Force-Feedback Dominant)", fontsize=14, fontweight="bold", + pad=15, color=COLORS["formula"]) + + # Formula box + bbox = dict(boxstyle="round,pad=0.5", facecolor="#FFF8E1", edgecolor=COLORS["formula"], linewidth=2) + ax1.text(5, 7, r"$\mathbf{\alpha_F = \dot{a}_F - 14.95\;\mu\;C_{l,tot}}$", + ha="center", va="center", fontsize=14, bbox=bbox) + ax1.annotate("", xy=(5, 6.2), xytext=(5, 5.3), + arrowprops=dict(arrowstyle="->", color=COLORS["formula"], lw=2)) + ax1.text(5, 5.0, "Derivative term:\ncaptures action momentum", ha="center", fontsize=10, + bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow", alpha=0.7)) + ax1.text(5, 3.8, r"$\mu \cdot C_{l,tot}$:\nadaptive viscosity scaling", ha="center", fontsize=10, + bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow", alpha=0.7)) + + ax1.text(5, 1.5, r"$\mathbf{\alpha_T = 3.414}$ (constant)", ha="center", fontsize=12, + bbox=dict(boxstyle="round,pad=0.3", facecolor="#E8F5E9", edgecolor="gray", linewidth=1)) + ax1.text(5, 0.5, "Rear cylinders: passive constant rotation", ha="center", fontsize=9, color="gray") + + # Right: Illusion + ax2.set_xlim(0, 10); ax2.set_ylim(0, 10) + ax2.axis("off") + ax2.set_title("Illusion\n(Error-Tracking Dominant)", fontsize=14, fontweight="bold", + pad=15, color=COLORS["individual"]) + + bbox2 = dict(boxstyle="round,pad=0.5", facecolor="#E8F5E9", edgecolor=COLORS["individual"], linewidth=2) + ax2.text(5, 7.5, r"$\mathbf{\alpha_F = C_{d,tot} - C_{d,err} - 5.43}$", + ha="center", va="center", fontsize=13, bbox=bbox2) + ax2.text(5, 6.2, r"$+\;0.0098(\dot{u}_a + u_a)$", ha="center", fontsize=12, + bbox=dict(boxstyle="round,pad=0.3", facecolor="#E8F5E9", edgecolor=COLORS["individual"], linewidth=1)) + ax2.annotate("", xy=(5, 5.5), xytext=(5, 4.8), + arrowprops=dict(arrowstyle="->", color=COLORS["individual"], lw=2)) + ax2.text(5, 4.5, "Drag tracking:\nmatches target cylinder drag", ha="center", fontsize=10, + bbox=dict(boxstyle="round,pad=0.3", facecolor="#E8F5E9", alpha=0.7)) + ax2.text(5, 3.3, "Phase correction:\nfine-tunes shedding timing", ha="center", fontsize=10, + bbox=dict(boxstyle="round,pad=0.3", facecolor="#E8F5E9", alpha=0.7)) + + ax2.text(5, 1.5, r"$\mathbf{\alpha_T = (C_{d,err} - (C_{d,rear} - C_{l,err}))\times 0.535 + 2.78}$", + ha="center", fontsize=10, + bbox=dict(boxstyle="round,pad=0.3", facecolor="#FFF8E1", edgecolor="gray", linewidth=1)) + ax2.text(5, 0.5, "Rear cylinders: error-state feedback", ha="center", fontsize=9, color="gray") + + plt.tight_layout() + for fmt in ["png", "pdf"]: + plt.savefig(FIG_DIR / f"fig_formula_comparison.{fmt}") + plt.close() + print(" [fig3] formula_comparison saved") + return {} + + +# ========================================================================= +# Figure 4: Vortex cross-scene generalization +# ========================================================================= +def fig_vortex_generalization(): + """Bar chart: PPO vs Karman formula on vortex scenes.""" + reg = _load_registry() + scenes = reg.get("scenes", {}) + + vortex_data = [] + for v in ["lamb", "taylor"]: + key = f"vortex_{v}" + ppo = scenes.get(key, {}).get("ppo_similarity") + fv = scenes.get(key, {}).get("validations", {}).get("pysr_joint", {}) + formula = fv.get("similarity") if fv else None + vortex_data.append((v.capitalize(), ppo, formula)) + + fig, ax = plt.subplots(figsize=(9, 6)) + x = np.arange(len(vortex_data)) + w = 0.35 + + ppo_vals = [d[1] for d in vortex_data] + formula_vals = [d[2] for d in vortex_data] + labels = [d[0] for d in vortex_data] + + b1 = ax.bar(x - w/2, ppo_vals, w, label="PPO Policy (vortex-trained)", color=COLORS["ppo"], + edgecolor="black", linewidth=0.8) + b2 = ax.bar(x + w/2, formula_vals, w, label="Karman Joint Formula (cross-scene transfer)", + color=COLORS["formula"], edgecolor="black", linewidth=0.8) + + # Highlight: formula exceeds PPO for lamb + if formula_vals[0] and ppo_vals[0] and formula_vals[0] > ppo_vals[0]: + ax.annotate("Formula\nexceeds\nPPO!", xy=(0 + w/2, formula_vals[0]), + xytext=(0.6, formula_vals[0] + 0.03), + fontsize=10, ha="center", fontweight="bold", + bbox=dict(boxstyle="round,pad=0.3", facecolor="lightgreen", alpha=0.8), + arrowprops=dict(arrowstyle="->", color="green", lw=1.5)) + + ax.set_xticks(x) + ax.set_xticklabels(labels) + ax.set_ylabel("CFD DTW Similarity", fontsize=14) + ax.set_title("Vortex Cloak: Karman Formula Cross-Scene Generalization", fontsize=14, fontweight="bold") + ax.set_ylim(0.8, 1.02) + ax.legend(loc="lower right", framealpha=0.9) + ax.grid(True, axis="y", alpha=0.3, linestyle="--") + + for bar in b2: + h = bar.get_height() + ax.text(bar.get_x() + bar.get_width()/2., h + 0.005, f"{h:.3f}", + ha="center", va="bottom", fontsize=10, fontweight="bold") + + plt.tight_layout() + for fmt in ["png", "pdf"]: + plt.savefig(FIG_DIR / f"fig_vortex_generalization.{fmt}") + plt.close() + print(" [fig4] vortex_generalization saved") + return {} + + +# ========================================================================= +# Figure 5: Action timeseries overlay (PPO vs formula) +# ========================================================================= +def fig_action_comparison(): + """Overlay PPO actions vs formula-predicted actions for illusion_1L.""" + sn = "illusion_1L" + data_path = SR_DIR / "data" / "illusion" / sn / "controlled.npz" + if not data_path.exists(): + print(" [fig5] SKIP: no data for illusion_1L") + return {} + + d = np.load(data_path, allow_pickle=True) + actions = d["actions"] + cfg = get_scene(sn) + scale, bias = cfg["action_scale"], np.array(cfg["action_bias"]) + u0 = cfg["u0"] + alpha = (actions * scale + bias) # non-dim alpha (actions already in [-1,1]) + + # Select a representative window (avoid the initial transient) + n_total = alpha.shape[0] + start = min(30, n_total // 4) + n_show = min(80, n_total - start) + window = slice(start, start + n_show) + + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True) + + t = np.arange(n_show) + + # Front cylinder + ax1.plot(t, alpha[window, 0], color=COLORS["ppo"], linewidth=1.8, label="PPO Front Action") + ax1.set_ylabel(r"$\alpha_F$ (non-dim)", fontsize=13) + ax1.legend(loc="upper right") + ax1.grid(True, alpha=0.3, linestyle="--") + ax1.set_title(f"Illusion {sn}: PPO Control Actions (steps {start}-{start+n_show})", fontsize=14, fontweight="bold") + + # Top/Bottom cylinders + ax2.plot(t, alpha[window, 1], color="#ff7f0e", linewidth=1.5, label="Bottom", alpha=0.9) + ax2.plot(t, alpha[window, 2], color="#2ca02c", linewidth=1.5, label="Top", alpha=0.9) + ax2.set_xlabel("Control Step", fontsize=13) + ax2.set_ylabel(r"$\alpha$ (non-dim)", fontsize=13) + ax2.legend(loc="upper right") + ax2.grid(True, alpha=0.3, linestyle="--") + + plt.tight_layout() + for fmt in ["png", "pdf"]: + plt.savefig(FIG_DIR / f"fig_action_comparison.{fmt}") + plt.close() + print(" [fig5] action_comparison saved") + return {"n_total": n_total, "window": f"{start}-{start+n_show}"} + + +# ========================================================================= +# Figure 6: Master results table (as figure) +# ========================================================================= +def fig_master_table(): + """Generate a heatmap-style master table of all results.""" + reg = _load_registry() + scenes = reg.get("scenes", {}) + + # Illusion table + rows = [] + for d in ["0.5L", "0.6L", "0.75L", "0.8L", "1L", "1.2L", "1.5L", "2L"]: + key = f"illusion_{d}" + sc = scenes.get(key, {}) + val = sc.get("validations", {}) + joint = val.get("pysr_joint", {}).get("similarity") + ind = val.get("pysr_individual", {}).get("similarity") + ppo = sc.get("ppo_similarity") + rows.append((d, joint, ind, ppo)) + + fig, ax = plt.subplots(figsize=(10, 5)) + ax.axis("off") + + col_labels = ["Diameter", "Joint Formula", "Individual", "PPO Baseline", "% of PPO"] + cell_text = [] + cell_colors = [] + + for d, j, i, p in rows: + pct = f"{j/p*100:.0f}%" if j and p else "N/A" if j else "" + j_str = f"{j:.3f}" if j else ("N/A" if d != "1.5L" else "non-SR") + i_str = f"{i:.3f}" if i else "—" + p_str = f"{p:.3f}" if p else "—" + cell_text.append([d, j_str, i_str, p_str, pct]) + + # Color coding + row_colors = ["white"] * 5 + if j: + if j > 0.95: row_colors[1] = "#90EE90" + elif j > 0.80: row_colors[1] = "#FFFACD" + else: row_colors[1] = "#FFB6C1" + if d == "1.5L": + row_colors[1] = "#E0E0E0" + cell_colors.append(row_colors) + + table = ax.table(cellText=cell_text, colLabels=col_labels, cellColours=cell_colors, + cellLoc="center", loc="center") + table.auto_set_font_size(False) + table.set_fontsize(11) + table.scale(1.2, 1.6) + + ax.set_title("Illusion Symbolic Regression: Master Results Table", fontsize=15, + fontweight="bold", pad=20) + + plt.tight_layout() + for fmt in ["png", "pdf"]: + plt.savefig(FIG_DIR / f"fig_master_table.{fmt}") + plt.close() + print(" [fig6] master_table saved") + return {"rows": len(rows)} + + +# ========================================================================= +# FIGURE_INDEX.md generator +# ========================================================================= +def write_figure_index(fig_info): + """Generate FIGURE_INDEX.md with captions and paper context.""" + index = """# SR Analysis Figure Index + +All figures are in `results/figures/` as both PNG and PDF (300 DPI, publication quality). + +--- + +## Figure 1: Illusion Cross-Diameter Generalization +**File**: `fig_illusion_degradation.png`, `fig_illusion_degradation.pdf` + +**Caption**: CFD closed-loop validation of the joint symbolic regression formula +across target cylinder diameters. The formula was trained on 0.75L and 1.0L data +jointly. Performance degrades monotonically as diameter deviates from the training +range. At 1.5L, the PPO policy employs high-frequency periodic modulation (5.6x +the shedding frequency), which symbolic regression cannot capture with the current +feature set. The shaded regions indicate PPO-level performance (similarity > 0.9) +and degradation regime. + +**Paper section**: Results — Symbolic Regression — Illusion Cross-Diameter Generalization. + +--- + +## Figure 2: Karman Cross-Re Formula Validation +**File**: `fig_karman_cross_re.png`, `fig_karman_cross_re.pdf` + +**Caption**: Comparison of PPO policy and joint PySR formula performance across +Reynolds numbers for the Karman cloak task. The joint formula +(alpha_F = daF_dt - 14.95 * mu * Cl_tot) was trained on Re 50-400 data and +generalizes to unseen intermediate Reynolds numbers (25, 70, 150, 300). The dashed +line shows the average PPO performance across trained Reynolds numbers. + +**Paper section**: Results — Symbolic Regression — Karman Cross-Re Joint Formula. + +--- + +## Figure 3: Formula Structure Comparison +**File**: `fig_formula_comparison.png`, `fig_formula_comparison.pdf` + +**Caption**: Structural comparison of the discovered symbolic control laws for the +Karman cloak and illusion tasks. The Karman formula is dominated by force-feedback +terms (action derivative and viscous-scaled lift coefficient), while the illusion +formula is dominated by error-tracking terms (drag coefficient mismatch between +pinball and target). This structural difference reflects the fundamentally different +control objectives: wake preservation vs. wake sculpting. + +**Paper section**: Results — Symbolic Regression — Structural Comparison. + +--- + +## Figure 4: Vortex Cross-Scene Generalization +**File**: `fig_vortex_generalization.png`, `fig_vortex_generalization.pdf` + +**Caption**: The Karman joint formula, trained only on Karman vortex street data, +generalizes to two transient vortex cloaking tasks without retraining. For the +Lamb dipole, the symbolic formula exceeds the PPO policy's performance. This +demonstrates that the discovered feedback structure (force-feedback with viscous +scaling) captures a universal cloaking mechanism rather than a scene-specific +strategy. + +**Paper section**: Results — Symbolic Regression — Cross-Scene Generalization. + +--- + +## Figure 5: PPO Control Action Timeseries +**File**: `fig_action_comparison.png`, `fig_action_comparison.pdf` + +**Caption**: Representative PPO control actions for the illusion 1.0L scene, +showing the front cylinder (dominated by drag-error tracking) and rear cylinders +(oscillating in anti-phase). The actions are non-dimensional alpha = omega/U0, +where omega is the cylinder rotational angular velocity. The periodic structure +reflects the vortex shedding cycle of the target cylinder. + +**Paper section**: Results — Symbolic Regression — Control Action Analysis. + +--- + +## Figure 6: Master Results Table +**File**: `fig_master_table.png`, `fig_master_table.pdf` + +**Caption**: Complete results table for all illusion scenes. Green cells indicate +near-PPO performance (similarity > 95%), yellow indicates moderate degradation +(80-95%), and red indicates significant degradation (< 80%). The 1.5L scene +(gray) is fundamentally non-fittable by symbolic regression due to its +high-frequency action modulation. "% of PPO" shows the formula's performance +as a percentage of the PPO baseline. + +**Paper section**: Results — Symbolic Regression — Summary Table. + +--- + +## Data Sources + +All numerical values in these figures are derived from: +- `scene_registry.json` — canonical result registry +- `results/validations/*.json` — CFD closed-loop validation outputs +- `data/*/controlled.npz` — PPO inference data (actions, sensors, forces) + +## Reproduction + +To regenerate all figures: +```bash +conda run -n pycuda_3_10 python stage_4_analyze.py +``` +""" + path = FIG_DIR / "FIGURE_INDEX.md" + path.write_text(index) + print(" [index] FIGURE_INDEX.md written") + + +# ========================================================================= +# Main +# ========================================================================= def main(): - ap = argparse.ArgumentParser(description="Stage 4: Unified analysis") - ap.add_argument("--scene", type=str, default=None) - ap.add_argument("--scenes", type=str, default=None, help="Comma-separated for degradation/multi-scene") - ap.add_argument("--mode", type=str, default="ppo-viz", choices=["ppo-viz", "degradation", "formula-compare"]) - ap.add_argument("--formula-front", type=str, default=None) - args = ap.parse_args() + print("=== Stage 4: Publication Figures ===") + print(f"Output: {FIG_DIR}\n") - if args.scenes: - scene_names = [s.strip() for s in args.scenes.split(",")] - elif args.scene: - scene_names = [args.scene] - else: - ap.error("Need --scene or --scenes") + fig_info = {} - if args.mode == "ppo-viz": - plot_ppo_viz(scene_names) - elif args.mode == "degradation": - plot_degradation(scene_names) - elif args.mode == "formula-compare": - plot_formula_compare(scene_names[0], args.formula_front) + print("[1/6] Illusion cross-diameter degradation...") + fig_info["deg"] = fig_illusion_degradation() - print("\nDone.") + print("[2/6] Karman cross-Re validation...") + fig_info["karman"] = fig_karman_cross_re() + + print("[3/6] Formula structure comparison...") + fig_info["formula"] = fig_formula_comparison() + + print("[4/6] Vortex cross-scene generalization...") + fig_info["vortex"] = fig_vortex_generalization() + + print("[5/6] Action timeseries overlay...") + fig_info["action"] = fig_action_comparison() + + print("[6/6] Master results table...") + fig_info["table"] = fig_master_table() + + print("\n[INDEX] Writing FIGURE_INDEX.md...") + write_figure_index(fig_info) + + print(f"\nDone. {len([f for f in FIG_DIR.glob('*.png')])} figures in {FIG_DIR}") + print("Open results/figures/FIGURE_INDEX.md for the figure catalog.") if __name__ == "__main__": diff --git a/src/SR_analysis/utils/__init__.py b/src/SR_analysis/utils/__init__.py index d8f4d98..0b608d0 100644 --- a/src/SR_analysis/utils/__init__.py +++ b/src/SR_analysis/utils/__init__.py @@ -1,3 +1,7 @@ +"""SR_analysis utilities: feature building, SINDy fitting, harmonics, CFD interface, G-operator. + +All four pipeline stages (infer -> fit -> validate -> analyze) import from this package. +""" from .feature_builder import ( compute_dimensionless, compute_features, build_feature_matrix, get_feature_names, apply_G_alpha, apply_G_x, diff --git a/src/SR_analysis/utils/harmonics.py b/src/SR_analysis/utils/harmonics.py new file mode 100644 index 0000000..9011d46 --- /dev/null +++ b/src/SR_analysis/utils/harmonics.py @@ -0,0 +1,31 @@ +"""FFT harmonics utilities for Illusion target force reconstruction. + +Used by stage_3_validate.py to reconstruct target cylinder force signals +from pre-computed FFT harmonics during closed-loop CFD validation. +""" + +from __future__ import annotations + +import numpy as np + + +def gen_target_states_at(t: int, harmonics: list) -> np.ndarray: + """Reconstruct target forces at step t from FFT harmonics. + + Args: + t: current step index (0-based). + harmonics: list of dicts from analyze_harmonics(), one per channel. + Each dict has keys: 'dc' (float), 'amps' (list[float]), + 'freqs' (list[float]), 'phases' (list[float]). + + Returns: + ndarray of shape (D,) with reconstructed values, where D = len(harmonics). + """ + D = len(harmonics) + result = np.zeros(D, dtype=np.float32) + for d, h in enumerate(harmonics): + val = h["dc"] + for amp, freq, phase in zip(h["amps"], h["freqs"], h["phases"]): + val += amp * np.cos(2 * np.pi * freq * t + phase) + result[d] = float(val) + return result diff --git a/src/SR_analysis/validate/results/README.md b/src/SR_analysis/validate/results/README.md deleted file mode 100644 index acb8c4c..0000000 --- a/src/SR_analysis/validate/results/README.md +++ /dev/null @@ -1,127 +0,0 @@ -# Results Reference Table - -> Canonical result files for PySR symbolic regression and CFD closed-loop validation. -> Archival intermediate search attempts available in `archive/`. -> See `sindy_sr_knowledge.md` §四.3 and `SR_analysis_report.md` §8 for the complete result tables. - ---- - -## Category A: Canonical PySR Formulas - -| File | Scene | Channel | Formula (best_sympy) | R² | -|------|-------|---------|----------------------|:--:| -| `karman_joint_deep_front.json` | Karman cross-Re joint | Front | `daF_dt − 14.952·mu·Cl_tot` | 1.000 | -| `karman_joint_deep_top.json` | Karman cross-Re joint | Top | `3.414` (constant) | 1.000 | -| `pysr_illusion_joint_front.json` | Illusion joint (0.75L+1L) | Front | `Cd_tot − (Cd_err + 5.428) − 0.00978·(du_a_dt + u_a)` | 0.907 | -| `pysr_illusion_joint_top.json` | Illusion joint (0.75L+1L) | Top | `(Cd_err − (Cd_rear − Cl_err))·0.535 + 2.782` | 0.828 | -| `pysr_karman_re50_front_deep.json` | Karman Re=50 | Front | Independent per-Re best | 1.000 | -| `pysr_karman_re50_top_deep.json` | Karman Re=50 | Top | Independent per-Re best | 1.000 | -| `pysr_karman_re100_front_deep.json` | Karman Re=100 | Front | Independent per-Re best | 1.000 | -| `pysr_karman_re100_top_deep.json` | Karman Re=100 | Top | Independent per-Re best | 1.000 | -| `pysr_karman_re200_front_deep.json` | Karman Re=200 | Front | Independent per-Re best | 1.000 | -| `pysr_karman_re200_top_deep.json` | Karman Re=200 | Top | Independent per-Re best | 1.000 | -| `pysr_karman_re400_front_deep.json` | Karman Re=400 | Front | Independent per-Re best | 1.000 | -| `pysr_karman_re400_top_deep.json` | Karman Re=400 | Top | Independent per-Re best | 1.000 | -| `pysr_illusion_0.75L_front.json` | Illusion 0.75L | Front | `−0.169·(Cl_tot + dCl_tot/dt) − 1.240` | 0.692 | -| `pysr_illusion_0.75L_top.json` | Illusion 0.75L | Top | Error-state feedback | 0.856 | -| `pysr_illusion_1L_front.json` | Illusion 1L | Front | `(du_a/dt + u_a + 26.5)·0.0123` | 0.929 | -| `pysr_illusion_1L_top.json` | Illusion 1L | Top | Drag-normalized lift rate | 0.696 | -| `pysr_illusion_0.75L_front_target.json` | Illusion 0.75L + target | Front | With target_Cd/Cl (marginal gain) | 0.723 | -| `pysr_illusion_0.75L_top_target.json` | Illusion 0.75L + target | Top | With target features | 0.838 | -| `pysr_illusion_1L_front_target.json` | Illusion 1L + target | Front | With target_Cd/Cl | 0.929 | -| `pysr_illusion_1L_top_target.json` | Illusion 1L + target | Top | With target features | 0.695 | -| `pysr_illusion_joint_marker_front.json` | Illusion joint (marker) | Front | Method A: with target_diameter | 0.942 | -| `pysr_illusion_1.5L_front.json` | Illusion 1.5L | Front | Degenerate (aF_lag1·0.01) | 1.000 | -| `pysr_illusion_1.5L_top.json` | Illusion 1.5L | Top | Degenerate | 1.000 | - -**Note**: 1.5L PySR formulas produce trivial near-identity solutions. 1.5L is not SR-amenable — it uses high-frequency periodic modulation (5.6× target shedding frequency). - ---- - -## Category B: CFD Closed-Loop Validation - -| File | Scene | Similarity | Notes | -|------|-------|:----------:|-------| -| `illusion_0.5L_pysr.json` | Illusion 0.5L | 0.854 / 0.841 (tail) | Generalization | -| `illusion_0.6L_pysr.json` | Illusion 0.6L | 0.939 / 0.861 (tail) | Generalization | -| `illusion_0.75L_pysr.json` | Illusion 0.75L | 0.982 / 0.807 (tail) | With-target formula | -| `illusion_0.8L_pysr.json` | Illusion 0.8L | 0.908 / 0.900 (tail) | Generalization | -| `illusion_1L_pysr.json` | Illusion 1L | 0.970 / 0.699 (tail) | Joint formula | -| `illusion_1.2L_pysr.json` | Illusion 1.2L | 0.849 / 0.769 (tail) | Generalization | -| `illusion_2L_pysr.json` | Illusion 2L | 0.676 / 0.629 (tail) | Generalization, degraded | -| `karman_re50_pysr.json` | Karman Re=50 | 0.847 | Joint formula | -| `karman_re100_pysr.json` | Karman Re=100 | 0.888 | Joint formula | -| `karman_re100_phase_pysr.json` | Karman Re=100 | 0.888 | Phase-state features | -| `karman_re100_mu_pysr.json` | Karman Re=100 | 0.880 | Mu-features | -| `karman_re200_pysr.json` | Karman Re=200 | 0.845 | Joint formula | -| `karman_re400_pysr.json` | Karman Re=400 | 0.806 | Joint formula (SI=800) | -| `karman_re400_pysr_SI200.json` | Karman Re=400 | 0.794 | SI=200 test | -| `karman_re400_pysr_SI400.json` | Karman Re=400 | **0.819** | Optimal SI=400 | -| `vortex_taylor_pysr.json` | Vortex Taylor | 0.905 | Karman joint formula | -| `vortex_lamb_pysr.json` | Vortex Lamb | **0.949** | Karman joint formula (exceeds PPO) | -| `illusion_1L_target_pysr.json` | Illusion 1L | **0.958** | Individual with-target formula CFD | - ---- - -## Category D: Validation Variants (Different Modes / Generalization) - -| File | Scene | Similarity | -|------|-------|:----------:| -| `karman_re25_v23.json` | Karman Re=25 | 0.567 | -| `karman_re50_v23.json` | Karman Re=50 | 0.582 | -| `karman_re70_v23.json` | Karman Re=70 | 0.577 | -| `karman_re100_v23_th0.003.json` | Karman Re=100 | 0.901 | -| `karman_re150_v23.json` | Karman Re=150 | 0.595 | -| `karman_re200_v23.json` | Karman Re=200 | 0.793 | -| `karman_re300_v23.json` | Karman Re=300 | 0.541 | -| `karman_re400_v23.json` | Karman Re=400 | 0.664 | -| `karman_re70_deriv.json` | Karman Re=70 (deriv mode) | 0.427 | -| `karman_re100_deriv.json` | Karman Re=100 (deriv mode) | 0.656 | -| `karman_re100_abs.json` | Karman Re=100 (abs mode) | 0.700 | -| `illusion_0.75L_v23.json` | Illusion 0.75L (v23 mode) | 0.974 | -| `illusion_1L_v23.json` | Illusion 1L (v23 mode) | 0.958 | -| `illusion_1.5L_v23.json` | Illusion 1.5L (v23 mode) | 0.930 | - ---- - -## Category E: Older Individual Re PySR (Non-Deep) - -Superseded by `_deep` variants. Kept for reference. - -| File | Scene | R² | -|------|-------|:--:| -| `pysr_karman_re50_front.json` | Karman Re=50 | 0.899 | -| `pysr_karman_re50_top.json` | Karman Re=50 | 0.745 | -| `pysr_karman_re50_front_mu.json` | Karman Re=50 + mu | 0.856 | -| `pysr_karman_re50_top_mu.json` | Karman Re=50 + mu | 0.843 | -| `pysr_karman_re100_front.json` | Karman Re=100 | 0.950 | -| `pysr_karman_re100_top.json` | Karman Re=100 | 0.814 | -| `pysr_karman_re100_mu_front.json` | Karman Re=100 + mu | 0.946 | -| `pysr_karman_re100_mu_top.json` | Karman Re=100 + mu | 0.918 | -| `pysr_karman_re200_front.json` | Karman Re=200 | 0.811 | -| `pysr_karman_re200_top.json` | Karman Re=200 | 0.583 | -| `pysr_karman_re200_front_mu.json` | Karman Re=200 + mu | 0.811 | -| `pysr_karman_re200_top_mu.json` | Karman Re=200 + mu | 0.746 | -| `pysr_karman_re400_front.json` | Karman Re=400 | 0.809 | -| `pysr_karman_re400_top.json` | Karman Re=400 | 0.645 | -| `pysr_karman_re400_front_mu.json` | Karman Re=400 + mu | 0.865 | -| `pysr_karman_re400_top_mu.json` | Karman Re=400 + mu | 0.717 | - ---- - -## Archive (19 files, moved to `archive/`) - -These are intermediate search attempts superseded by canonical results. See subagent classification for details. - ---- - -## File Count - -| Category | Count | Location | -|----------|:-----:|----------| -| A (Canonical PySR) | 23 | `results/` | -| B (CFD Validation) | 17 | `results/` | -| C (Archived) | 19 | `results/archive/` | -| D (Validation Variants) | 14 | `results/` | -| E (Older non-deep) | 16 | `results/` | -| **Total** | **89** | | diff --git a/src/drl_pinball/train/calibrate.py b/src/drl_pinball/train/calibrate.py index fdf8f5c..e10aef0 100644 --- a/src/drl_pinball/train/calibrate.py +++ b/src/drl_pinball/train/calibrate.py @@ -190,8 +190,7 @@ _ILL_PINBALL_FRONT_X = 380.0 _ILL_PINBALL_REAR_X = 406.0 _ILL_SENSOR_X = 600.0 _ILL_TARGET_X = 400.0 -_ILL_TARGET_RADIUS = 1.0 * L0 -_ILL_REF_OMEGA = np.array([0.0, 0.001, -0.001], dtype=np.float32) # surface_vel = [0,-1,1]*U0, FIFO-init level +_ILL_REF_OMEGA = np.array([0.0, 0.005, -0.005], dtype=np.float32) # steady-cloak-like 5*U0 bias def _analyze_harmonics_for_calib(states, n_harmonics=5): N, D = states.shape @@ -213,13 +212,14 @@ def _analyze_harmonics_for_calib(states, n_harmonics=5): return result -def _calibrate_illusion(case, config_path, device_id, si, out_dir, log, warmup): +def _calibrate_illusion(case, config_path, device_id, si, out_dir, log, warmup, target_diam=1.0): + target_radius = target_diam * L0 # ---- Step 1: Record target (target cylinder + 3 sensors) ---- - log("Step 1: Recording illusion target (target cyl + sensors)...") + log(f"Step 1: Recording illusion target (target cyl + sensors, diam={target_diam}L)...") sim = Simulation(lbm_config_path=config_path, device_id=device_id) sim._assert_object_count_contract = lambda *a, **kw: None sim.add_body("circle", center=(_ILL_TARGET_X, CENTER_Y, 0.0), - radius=_ILL_TARGET_RADIUS) + radius=target_radius) s0 = sim.add_body("sensor", center=(_ILL_SENSOR_X, CENTER_Y + 40.0, 0.0), radius=5.0) s1 = sim.add_body("sensor", center=(_ILL_SENSOR_X, CENTER_Y, 0.0), radius=5.0) s2 = sim.add_body("sensor", center=(_ILL_SENSOR_X, CENTER_Y - 40.0, 0.0), radius=5.0) @@ -310,12 +310,12 @@ def _calibrate_illusion(case, config_path, device_id, si, out_dir, log, warmup): # But for Illusion, Stage0 may already match the target well, and reference # rotation (steady-cloak-like) makes it worse. Swap roles in that case. if s1_sim < s0_sim: - # Reference action is worse than zero (Illusion case). - # Stage1 (reference) -> r_sim ~0.2, Stage0 (zero) -> r_sim ~0.4 + # Reference action is worse than zero (typical Illusion case). + # Stage1 (5*U0 bias) -> r_sim ~0.10, Stage0 (zero) -> r_sim ~0.35 worst_sim = s1_sim better_sim = s0_sim else: - # Normal (Karman) case. + # Karman case or zero is worse than bias. worst_sim = s0_sim better_sim = s1_sim @@ -323,7 +323,8 @@ def _calibrate_illusion(case, config_path, device_id, si, out_dir, log, warmup): better_sim + (1.0 - better_sim) * 0.4, better_sim + (1.0 - better_sim) * 0.7, 1.0] - sim_val = [0.0, 0.2, 0.5, 0.8, 0.9, 1.0] + # Illusion reward mapping: zero~0.35, bias~0.10, best~1.0 + sim_val = [0.0, 0.1, 0.35, 0.7, 0.85, 1.0] for i in range(1, len(sim_bp)): if sim_bp[i] <= sim_bp[i - 1]: @@ -346,6 +347,7 @@ def _calibrate_illusion(case, config_path, device_id, si, out_dir, log, warmup): # ---- Step 6: Write calibration.json ---- calibration = { "case": case, "scene": "illusion", + "target_diam": target_diam, "grid": {"nx": NX, "ny": NY}, "config_path": config_path, "SI": si, @@ -382,6 +384,8 @@ def main() -> int: parser.add_argument("--scene", type=str, default="karman", choices=["karman", "illusion"], help="Scene type (karman or illusion)") + parser.add_argument("--target-diam", type=float, default=1.0, + help="Target cylinder diameter in L0 units (illusion only, default=1.0)") args = parser.parse_args() case = args.case @@ -410,7 +414,7 @@ def main() -> int: log(f" Output: {out_dir}") if scene == "illusion": - _calibrate_illusion(case, config_path, device_id, si, out_dir, log, warmup) + _calibrate_illusion(case, config_path, device_id, si, out_dir, log, warmup, target_diam=args.target_diam) return 0 # ---- Step 1: Record target (Karman: dist_cyl + sensors only) ---- diff --git a/src/drl_pinball/train/calibrations/illusion_075L/calibration.json b/src/drl_pinball/train/calibrations/illusion_075L/calibration.json new file mode 100644 index 0000000..a4ff24e --- /dev/null +++ b/src/drl_pinball/train/calibrations/illusion_075L/calibration.json @@ -0,0 +1,51 @@ +{ + "case": "illusion_075L", + "scene": "illusion", + "target_diam": 0.75, + "grid": { + "nx": 2000, + "ny": 600 + }, + "config_path": "/home/frank14f/DynamisLab/configs/config_lbm_karman_2000x600.json", + "SI": 1100, + "FIFO_LEN": 150, + "CONV_LEN": 30, + "SENSOR_CC": 78.0, + "FORCE_SCALE": 0.0027, + "SENS_SCALE": 0.93, + "dtw_norm_scale": 0.186, + "SIM_BP": [ + 0.0, + 0.23, + 0.51, + 0.71, + 0.85, + 1.0 + ], + "SIM_VAL": [ + 0.0, + 0.1, + 0.35, + 0.7, + 0.85, + 1.0 + ], + "K_CD": 50.0, + "K_CL": 100.0, + "W_CD": 0.3, + "W_CL": 0.3, + "W_SIM": 0.4, + "FLOOR_CD": 0.1, + "FLOOR_CL": 0.1, + "FLOOR_SIM": 0.1, + "FLOOR_PENALTY": 0.05, + "ACTION_SCALE": 12.0, + "ACTION_BIAS": [ + 0.0, + 0.0, + 0.0 + ], + "U0": 0.01, + "RADIUS": 10.0, + "L0": 20.0 +} \ No newline at end of file diff --git a/src/drl_pinball/train/calibrations/illusion_075L/target_harmonics.json b/src/drl_pinball/train/calibrations/illusion_075L/target_harmonics.json new file mode 100644 index 0000000..e47ad0f --- /dev/null +++ b/src/drl_pinball/train/calibrations/illusion_075L/target_harmonics.json @@ -0,0 +1,194 @@ +[ + { + "dc": 0.7088785445690156, + "amps": [ + 0.11475430680633039, + 0.09430037920896242, + 0.03785335654012012, + 0.036681363239733915, + 0.03288602952216585 + ], + "freqs": [ + 0.05333333333333334, + 0.060000000000000005, + 0.11333333333333334, + 0.04666666666666667, + 0.06666666666666667 + ], + "phases": [ + 2.6132855445583396, + -0.5789901787454609, + 0.8258268724793085, + 2.6668932105480825, + -0.6277135419212965 + ] + }, + { + "dc": -0.018542804992078648, + "amps": [ + 0.14149483059420287, + 0.11188103536632915, + 0.06452488062802345, + 0.04686946222498845, + 0.037418557603405504 + ], + "freqs": [ + 0.05333333333333334, + 0.060000000000000005, + 0.11333333333333334, + 0.04666666666666667, + 0.06666666666666667 + ], + "phases": [ + 0.516970687037909, + -2.566893168767107, + -1.2609177447586661, + 0.45668472348998385, + -2.5126762596162355 + ] + }, + { + "dc": 0.5725846501191457, + "amps": [ + 0.05764520035629576, + 0.005899803483735611, + 0.0054205372806156865, + 0.004581067312849431, + 0.002914845994913773 + ], + "freqs": [ + 0.11333333333333334, + 0.10666666666666667, + 0.22666666666666668, + 0.12000000000000001, + 0.1 + ], + "phases": [ + -2.413002672521359, + 0.686185204529467, + -1.5755920846972233, + -2.3710189737321334, + 0.6415027667356853 + ] + }, + { + "dc": 0.0112522135147204, + "amps": [ + 0.25595997006863674, + 0.20892759048230508, + 0.08230396158472242, + 0.07230005719685514, + 0.05046403572810587 + ], + "freqs": [ + 0.05333333333333334, + 0.060000000000000005, + 0.04666666666666667, + 0.06666666666666667, + 0.04 + ], + "phases": [ + 0.737858091105846, + -2.3310043425633937, + 0.6601851975187616, + -2.263148437154622, + 0.5776398163807126 + ] + }, + { + "dc": 0.7183246092001597, + "amps": [ + 0.1142342759424687, + 0.09477386576602029, + 0.039999549760355566, + 0.0361173949295317, + 0.033309522129018866 + ], + "freqs": [ + 0.05333333333333334, + 0.060000000000000005, + 0.11333333333333334, + 0.04666666666666667, + 0.06666666666666667 + ], + "phases": [ + -0.5356446835968586, + 2.572852381448959, + 1.1249071992889565, + -0.49462960371639564, + 2.54773901735503 + ] + }, + { + "dc": 0.03294325330294669, + "amps": [ + 0.14125404035009642, + 0.11221966979698215, + 0.05738605376381633, + 0.04670339431628726, + 0.03788596596235472 + ], + "freqs": [ + 0.05333333333333334, + 0.060000000000000005, + 0.11333333333333334, + 0.04666666666666667, + 0.06666666666666667 + ], + "phases": [ + 0.5115801035482338, + -2.558745487651608, + 2.0032061483623718, + 0.44313339382107086, + -2.4836080305538353 + ] + }, + { + "dc": 0.0021232300360376636, + "amps": [ + 4.857447003344866e-06, + 4.692486838309072e-07, + 4.25574507402414e-07, + 2.214141534606793e-07, + 2.135457380991351e-07 + ], + "freqs": [ + 0.11333333333333334, + 0.10666666666666667, + 0.12000000000000001, + 0.12666666666666668, + 0.1 + ], + "phases": [ + -1.2322896349743138, + 1.8869478333532295, + -1.192041653547773, + -1.1139837007307842, + 1.8083568237434107 + ] + }, + { + "dc": -4.034008507005637e-06, + "amps": [ + 0.00022175983298265806, + 0.00019412298497325598, + 7.13773466399049e-05, + 6.573336547379603e-05, + 4.496282384180768e-05 + ], + "freqs": [ + 0.05333333333333334, + 0.060000000000000005, + 0.06666666666666667, + 0.04666666666666667, + 0.07333333333333333 + ], + "phases": [ + -1.7563139947857938, + 1.4434754734643556, + 1.494965900469088, + -1.8235811215946953, + 1.5413793492101098 + ] + } +] \ No newline at end of file diff --git a/src/drl_pinball/train/calibrations/illusion_15L/calibration.json b/src/drl_pinball/train/calibrations/illusion_15L/calibration.json new file mode 100644 index 0000000..ed46974 --- /dev/null +++ b/src/drl_pinball/train/calibrations/illusion_15L/calibration.json @@ -0,0 +1,51 @@ +{ + "case": "illusion_15L", + "scene": "illusion", + "target_diam": 1.5, + "grid": { + "nx": 2000, + "ny": 600 + }, + "config_path": "/home/frank14f/DynamisLab/configs/config_lbm_karman_2000x600.json", + "SI": 1200, + "FIFO_LEN": 150, + "CONV_LEN": 30, + "SENSOR_CC": 78.0, + "FORCE_SCALE": 0.0027, + "SENS_SCALE": 0.93, + "dtw_norm_scale": 0.355, + "SIM_BP": [ + 0.0, + 0.21, + 0.81, + 0.89, + 0.94, + 1.0 + ], + "SIM_VAL": [ + 0.0, + 0.1, + 0.35, + 0.7, + 0.85, + 1.0 + ], + "K_CD": 50.0, + "K_CL": 100.0, + "W_CD": 0.3, + "W_CL": 0.3, + "W_SIM": 0.4, + "FLOOR_CD": 0.1, + "FLOOR_CL": 0.1, + "FLOOR_SIM": 0.1, + "FLOOR_PENALTY": 0.05, + "ACTION_SCALE": 12.0, + "ACTION_BIAS": [ + 0.0, + 0.0, + 0.0 + ], + "U0": 0.01, + "RADIUS": 10.0, + "L0": 20.0 +} \ No newline at end of file diff --git a/src/drl_pinball/train/calibrations/illusion_15L/target_harmonics.json b/src/drl_pinball/train/calibrations/illusion_15L/target_harmonics.json new file mode 100644 index 0000000..5f8876f --- /dev/null +++ b/src/drl_pinball/train/calibrations/illusion_15L/target_harmonics.json @@ -0,0 +1,194 @@ +[ + { + "dc": 0.6393717394272487, + "amps": [ + 0.26397499489221904, + 0.03693626193592989, + 0.03464043162884017, + 0.032094292621327965, + 0.02660998202074348 + ], + "freqs": [ + 0.04, + 0.12000000000000001, + 0.03333333333333333, + 0.04666666666666667, + 0.08 + ], + "phases": [ + -1.6564124362566746, + 0.47538888508181093, + 1.4273551493900074, + -1.603488924581116, + -2.92943366573134 + ] + }, + { + "dc": -0.051619164294873676, + "amps": [ + 0.4172458605608075, + 0.09803703370682554, + 0.05739720972114841, + 0.050867319019127596, + 0.04493433644769158 + ], + "freqs": [ + 0.04, + 0.08, + 0.03333333333333333, + 0.04666666666666667, + 0.07333333333333333 + ], + "phases": [ + 2.6165737590157754, + -0.40945517817609217, + -0.4706927988361595, + 2.5775068579589413, + 2.6220842496000643 + ] + }, + { + "dc": 0.552529529929161, + "amps": [ + 0.08403701703961244, + 0.027486096578928348, + 0.01744191426898051, + 0.011353947053282725, + 0.01109829213640094 + ], + "freqs": [ + 0.08, + 0.07333333333333333, + 0.08666666666666667, + 0.06666666666666667, + 0.16 + ], + "phases": [ + -1.8227867545251144, + 1.2862003185097757, + -1.7972626219792263, + 1.246149938164889, + -0.21151202059464996 + ] + }, + { + "dc": 0.010842311040420705, + "amps": [ + 0.5943308009504777, + 0.08877439867850001, + 0.08691429679466427, + 0.06563808069193275, + 0.05978492368560289 + ], + "freqs": [ + 0.04, + 0.03333333333333333, + 0.12000000000000001, + 0.04666666666666667, + 0.11333333333333334 + ], + "phases": [ + 2.703756522909848, + -0.36646754373961843, + -1.2543578596028404, + 2.633262384913735, + 1.9585690166152028 + ] + }, + { + "dc": 0.6386356908082962, + "amps": [ + 0.26431188860383287, + 0.03673130028679621, + 0.034307996033570334, + 0.032458112823718396, + 0.025233785999350247 + ], + "freqs": [ + 0.04, + 0.12000000000000001, + 0.03333333333333333, + 0.04666666666666667, + 0.11333333333333334 + ], + "phases": [ + 1.4719480406268972, + -2.7012019612996783, + -1.6212990075981681, + 1.4163428238670492, + 0.7391902924102435 + ] + }, + { + "dc": 0.06721407459272692, + "amps": [ + 0.4063272578239082, + 0.11205511912567469, + 0.06737336811041239, + 0.03853575023411404, + 0.0356513806128155 + ], + "freqs": [ + 0.04, + 0.08, + 0.03333333333333333, + 0.04666666666666667, + 0.02666666666666667 + ], + "phases": [ + 2.6071524073546244, + 2.638430170197671, + -0.42252259968403594, + 2.4619405777962924, + -0.326703708621943 + ] + }, + { + "dc": 0.004316972050194939, + "amps": [ + 7.792624418406872e-05, + 2.649820426895165e-05, + 1.5679181128164903e-05, + 1.1454315139134813e-05, + 8.735853307059759e-06 + ], + "freqs": [ + 0.08, + 0.07333333333333333, + 0.08666666666666667, + 0.06666666666666667, + 0.09333333333333334 + ], + "phases": [ + -0.4847035254693422, + 2.6784244787377918, + -0.5077154053619933, + 2.70688939048309, + -0.5282930751629346 + ] + }, + { + "dc": 3.652239890319227e-05, + "amps": [ + 0.0016787860570611811, + 0.0002649747945744754, + 0.0001734345822777016, + 0.00013614422915343285, + 9.88009892069796e-05 + ], + "freqs": [ + 0.04, + 0.03333333333333333, + 0.04666666666666667, + 0.02666666666666667, + 0.02 + ], + "phases": [ + -3.082075177261505, + 0.0499441279892221, + -3.072969546929839, + 0.040161756095339736, + 0.030358149244656192 + ] + } +] \ No newline at end of file diff --git a/src/drl_pinball/train/calibrations/illusion_1L/calibration.json b/src/drl_pinball/train/calibrations/illusion_1L/calibration.json index e5f4853..ccec3d5 100644 --- a/src/drl_pinball/train/calibrations/illusion_1L/calibration.json +++ b/src/drl_pinball/train/calibrations/illusion_1L/calibration.json @@ -1,21 +1,22 @@ { "case": "illusion_1L", "scene": "illusion", + "target_diam": 1.0, "grid": { "nx": 2000, "ny": 600 }, "config_path": "/home/frank14f/DynamisLab/configs/config_lbm_karman_2000x600.json", - "SI": 600, + "SI": 1200, "FIFO_LEN": 150, "CONV_LEN": 30, "SENSOR_CC": 78.0, - "FORCE_SCALE": 0.002, + "FORCE_SCALE": 0.0027, "SENS_SCALE": 0.93, - "dtw_norm_scale": 0.251, + "dtw_norm_scale": 0.252, "SIM_BP": [ 0.0, - 0.73, + 0.21, 0.81, 0.89, 0.94, @@ -23,14 +24,14 @@ ], "SIM_VAL": [ 0.0, - 0.2, - 0.5, - 0.8, - 0.9, + 0.1, + 0.35, + 0.7, + 0.85, 1.0 ], - "K_CD": 12.0, - "K_CL": 25.0, + "K_CD": 50.0, + "K_CL": 100.0, "W_CD": 0.3, "W_CL": 0.3, "W_SIM": 0.4, diff --git a/src/drl_pinball/train/calibrations/illusion_1L/target_harmonics.json b/src/drl_pinball/train/calibrations/illusion_1L/target_harmonics.json index 0a97e29..210023f 100644 --- a/src/drl_pinball/train/calibrations/illusion_1L/target_harmonics.json +++ b/src/drl_pinball/train/calibrations/illusion_1L/target_harmonics.json @@ -1,156 +1,180 @@ [ { - "dc": 0.6848811439673106, + "dc": 0.6871295102437337, "amps": [ - 0.2034844510278786, - 0.029159004511650252, - 0.027840311609468333, - 0.02653873514475916, - 0.01919840709106637 - ], - "freqs": [ - 0.02666666666666667, - 0.03333333333333333, - 0.02, - 0.05333333333333334, - 0.04666666666666667 - ], - "phases": [ - -1.2362005574545905, - -1.1766679293247424, - 1.886347686621138, - -2.834029604726311, - -0.7286213249887804 - ] - }, - { - "dc": -0.026989686344750227, - "amps": [ - 0.2790809917939555, - 0.07572308617806586, - 0.044730957946728864, - 0.03911356916608822, - 0.03841147969973682 - ], - "freqs": [ - 0.02666666666666667, - 0.05333333333333334, - 0.02, - 0.04666666666666667, - 0.03333333333333333 - ], - "phases": [ - 3.0547687544981676, - 0.671130604261652, - -0.10499736216933567, - -2.7955698151736605, - 3.1220861483228126 - ] - }, - { - "dc": 0.5674182399113973, - "amps": [ - 0.06514964076759504, - 0.024722427490218583, - 0.01476835974183025, - 0.010156020328112192, - 0.009639191161046853 + 0.18319639782938002, + 0.06610945366616479, + 0.04311754473414352, + 0.03030542180621809, + 0.027272312380937323 ], "freqs": [ 0.05333333333333334, 0.04666666666666667, 0.060000000000000005, - 0.04, - 0.1 + 0.1, + 0.15333333333333335 ], "phases": [ - -0.7488773712604715, - 2.444199243833346, - -0.7923014988253196, - 2.5055369932207086, - -1.0758944221483198 + -1.5859054738810505, + 1.5005459218330446, + -1.534723086539976, + -0.9755852396186818, + -2.400861875184656 ] }, { - "dc": 0.014232359210921763, + "dc": -0.02839548134400199, "amps": [ - 0.446162318961067, - 0.07797933262227957, - 0.05505684614794328, - 0.05331603052134459, - 0.04874852338883084 + 0.25230287832767934, + 0.09539999608074776, + 0.07242769038107988, + 0.057915290763028225, + 0.04339958293014957 ], "freqs": [ - 0.02666666666666667, - 0.02, - 0.08, - 0.03333333333333333, - 0.07333333333333333 - ], - "phases": [ - -3.104930355366964, - 0.027203661003797012, - 0.09727456602426711, - -3.0952225671159, - -3.0449050417637897 - ] - }, - { - "dc": 0.688553271094958, - "amps": [ - 0.20183164156868158, - 0.033110505614057664, - 0.029481548571705242, - 0.02720959921763942, - 0.02102475262837555 - ], - "freqs": [ - 0.02666666666666667, 0.05333333333333334, - 0.02, - 0.03333333333333333, - 0.07333333333333333 + 0.04666666666666667, + 0.1, + 0.060000000000000005, + 0.10666666666666667 ], "phases": [ - 1.8817739067162356, - 2.7424167422444903, - -1.1159816805254985, - 1.7448225406868731, - 1.8563274842269628 + 2.6842705453730904, + -0.4329499897570072, + 2.9316995795051146, + 2.670843645654777, + -0.05044820463468416 ] }, { - "dc": 0.046428785625806386, + "dc": 0.5692154564460119, "amps": [ - 0.2667935014251649, - 0.08445240278954501, - 0.05549481355853139, - 0.031766322660930275, - 0.025647051108265238 + 0.05111823061431893, + 0.04128396624646554, + 0.015265422980567578, + 0.015090006639850726, + 0.009557711148572965 ], "freqs": [ - 0.02666666666666667, - 0.05333333333333334, - 0.02, - 0.013333333333333334, - 0.04666666666666667 + 0.1, + 0.10666666666666667, + 0.11333333333333334, + 0.09333333333333334, + 0.12000000000000001 ], "phases": [ - 3.0338443211965744, - -2.6346763210802586, - -0.032158500100019624, - -0.005548092544019917, - 0.8284453426622406 + 1.6693519932886194, + -1.4324779931410057, + -1.3940831649835272, + 1.6278315583167478, + -1.3566560767853084 ] }, { - "dc": 0.0028075010624403754, + "dc": 0.013092265091836452, "amps": [ - 1.7116459800799798e-05, - 6.866397692253015e-06, - 3.6591197181741872e-06, - 2.979711101541413e-06, - 2.0364590024389495e-06 + 0.40124785195509555, + 0.16234186402338177, + 0.08524230800710621, + 0.08108585800028799, + 0.07109298715174087 + ], + "freqs": [ + 0.05333333333333334, + 0.04666666666666667, + 0.060000000000000005, + 0.15333333333333335, + 0.04 + ], + "phases": [ + 2.807113574648348, + -0.29757223337333294, + 2.7719663781063204, + 2.1871266999200056, + -0.2589322655847287 + ] + }, + { + "dc": 0.684990275700887, + "amps": [ + 0.18185260409359108, + 0.06713403479140795, + 0.04138782185879297, + 0.02695905744770475, + 0.026141859041267534 + ], + "freqs": [ + 0.05333333333333334, + 0.04666666666666667, + 0.060000000000000005, + 0.15333333333333335, + 0.04 + ], + "phases": [ + 1.5465990065588082, + -1.6185095223679558, + 1.5625278315612854, + 0.8537101949347456, + -1.649974102551634 + ] + }, + { + "dc": 0.04362114487215876, + "amps": [ + 0.2437341265296108, + 0.10332818930987306, + 0.057340140519604656, + 0.0564289078852383, + 0.04846692686956963 + ], + "freqs": [ + 0.05333333333333334, + 0.04666666666666667, + 0.1, + 0.10666666666666667, + 0.060000000000000005 + ], + "phases": [ + 2.6708663593754087, + -0.40513239623713626, + -0.06426761319675102, + 2.9170325957817727, + 2.5947271126041254 + ] + }, + { + "dc": 0.0028073104269181687, + "amps": [ + 1.3964627608664915e-05, + 1.0641956583797102e-05, + 4.3721419678955225e-06, + 3.7235257770112522e-06, + 2.6478054812257544e-06 + ], + "freqs": [ + 0.1, + 0.10666666666666667, + 0.09333333333333334, + 0.11333333333333334, + 0.08666666666666667 + ], + "phases": [ + -0.008346715563909252, + 3.134154764223535, + -0.007106260242131926, + 3.13161924702753, + -0.007459162304746806 + ] + }, + { + "dc": -4.900522320288777e-06, + "amps": [ + 0.0006028109860900706, + 0.00022089300997016063, + 0.0001390934157273362, + 8.588200912189985e-05, + 8.16852145193509e-05 ], "freqs": [ 0.05333333333333334, @@ -160,35 +184,11 @@ 0.06666666666666667 ], "phases": [ - -2.4055008082312566, - 0.6560206596369318, - -2.338324550342246, - 0.5697953506705974, - -2.2632313796302874 - ] - }, - { - "dc": -1.484534581322805e-05, - "amps": [ - 0.000660645874022141, - 0.00010502076564124189, - 8.389696757021492e-05, - 4.848746760651764e-05, - 4.616076991013906e-05 - ], - "freqs": [ - 0.02666666666666667, - 0.02, - 0.03333333333333333, - 0.013333333333333334, - 0.04 - ], - "phases": [ - -0.8350715543078073, - 2.428305305182952, - -0.9184117032214737, - 2.6043964384951956, - -0.9755175136337735 + -1.182274934210081, + 1.9691619558487863, + -1.1855982163802612, + 1.9884734806658761, + -1.1842874340466223 ] } ] \ No newline at end of file diff --git a/src/drl_pinball/train/calibrations/illusion_2L/calibration.json b/src/drl_pinball/train/calibrations/illusion_2L/calibration.json new file mode 100644 index 0000000..0ab81be --- /dev/null +++ b/src/drl_pinball/train/calibrations/illusion_2L/calibration.json @@ -0,0 +1,51 @@ +{ + "case": "illusion_2L", + "scene": "illusion", + "target_diam": 2.0, + "grid": { + "nx": 2000, + "ny": 600 + }, + "config_path": "/home/frank14f/DynamisLab/configs/config_lbm_karman_2000x600.json", + "SI": 1200, + "FIFO_LEN": 150, + "CONV_LEN": 30, + "SENSOR_CC": 78.0, + "FORCE_SCALE": 0.0027, + "SENS_SCALE": 0.93, + "dtw_norm_scale": 0.433, + "SIM_BP": [ + 0.0, + 0.21, + 0.7, + 0.82, + 0.91, + 1.0 + ], + "SIM_VAL": [ + 0.0, + 0.1, + 0.35, + 0.7, + 0.85, + 1.0 + ], + "K_CD": 50.0, + "K_CL": 100.0, + "W_CD": 0.3, + "W_CL": 0.3, + "W_SIM": 0.4, + "FLOOR_CD": 0.1, + "FLOOR_CL": 0.1, + "FLOOR_SIM": 0.1, + "FLOOR_PENALTY": 0.05, + "ACTION_SCALE": 12.0, + "ACTION_BIAS": [ + 0.0, + 0.0, + 0.0 + ], + "U0": 0.01, + "RADIUS": 10.0, + "L0": 20.0 +} \ No newline at end of file diff --git a/src/drl_pinball/train/calibrations/illusion_2L/target_harmonics.json b/src/drl_pinball/train/calibrations/illusion_2L/target_harmonics.json new file mode 100644 index 0000000..a51dd14 --- /dev/null +++ b/src/drl_pinball/train/calibrations/illusion_2L/target_harmonics.json @@ -0,0 +1,194 @@ +[ + { + "dc": 0.5942543417215347, + "amps": [ + 0.3019850417664959, + 0.08160454953075313, + 0.058301710483107845, + 0.0544747476609962, + 0.039862698816389305 + ], + "freqs": [ + 0.03333333333333333, + 0.02666666666666667, + 0.04, + 0.09333333333333334, + 0.060000000000000005 + ], + "phases": [ + 2.534821875775996, + -0.5429755507018492, + 2.4925302413193626, + -2.8396914013393335, + 2.3471326700331137 + ] + }, + { + "dc": -0.11012142162770033, + "amps": [ + 0.48068411406011297, + 0.1524480328909725, + 0.11692915263110391, + 0.0751168326269912, + 0.0747130122507944 + ], + "freqs": [ + 0.03333333333333333, + 0.02666666666666667, + 0.06666666666666667, + 0.02, + 0.04 + ], + "phases": [ + 0.470296517741658, + -2.7377287843427305, + 1.2986260770158415, + -2.824709979499191, + 0.49814841794041553 + ] + }, + { + "dc": 0.5153132931391398, + "amps": [ + 0.07925114648536222, + 0.06416828398684, + 0.02486756552471102, + 0.02396105106046917, + 0.02357284381774593 + ], + "freqs": [ + 0.06666666666666667, + 0.060000000000000005, + 0.12666666666666668, + 0.05333333333333334, + 0.07333333333333333 + ], + "phases": [ + -0.07499440268802428, + 3.0690642525335066, + 0.3581876541251109, + 3.073340401899989, + -0.07639680854009603 + ] + }, + { + "dc": -0.027726365104317665, + "amps": [ + 0.6643698174881782, + 0.19945593793429098, + 0.11468672779610138, + 0.11289183277422135, + 0.09541898908540014 + ], + "freqs": [ + 0.03333333333333333, + 0.02666666666666667, + 0.04, + 0.09333333333333334, + 0.02 + ], + "phases": [ + 0.5698384256779261, + -2.684226742056145, + 0.6818011086755982, + 1.7048428586402848, + -2.7975917523605154 + ] + }, + { + "dc": 0.5756912664572398, + "amps": [ + 0.2971849584753252, + 0.08546200288457928, + 0.056185965966854816, + 0.05199100131281855, + 0.04433469349174801 + ], + "freqs": [ + 0.03333333333333333, + 0.02666666666666667, + 0.09333333333333334, + 0.04, + 0.06666666666666667 + ], + "phases": [ + -0.6051267213005925, + 2.5953707671296398, + 0.17997803765211065, + -0.6326158950659138, + -0.8305731267474725 + ] + }, + { + "dc": 0.06891472408858439, + "amps": [ + 0.4939877939312975, + 0.14182933287038246, + 0.09542418578567632, + 0.09238873840109178, + 0.08339300360694776 + ], + "freqs": [ + 0.03333333333333333, + 0.02666666666666667, + 0.060000000000000005, + 0.04, + 0.06666666666666667 + ], + "phases": [ + 0.48480140913888486, + -2.7722480905064066, + 1.1975329965204313, + 0.618507835379864, + -1.6516116590440921 + ] + }, + { + "dc": 0.006008172007277608, + "amps": [ + 0.00014509432721004472, + 0.0001134838631202557, + 4.463804029767137e-05, + 4.034021665887571e-05, + 2.676946217843137e-05 + ], + "freqs": [ + 0.06666666666666667, + 0.060000000000000005, + 0.07333333333333333, + 0.05333333333333334, + 0.08 + ], + "phases": [ + -0.7433797814500979, + 2.435150608779618, + -0.759862378816383, + 2.4800522883576814, + -0.776499679618213 + ] + }, + { + "dc": -0.0001301015308369339, + "amps": [ + 0.0027993124476053143, + 0.0008745864809045856, + 0.0004589225344713892, + 0.0004310081571789182, + 0.0003159326235892787 + ], + "freqs": [ + 0.03333333333333333, + 0.02666666666666667, + 0.04, + 0.02, + 0.013333333333333334 + ], + "phases": [ + -0.06721702416991604, + 3.089276889965643, + -0.08070896525391963, + 3.102755835824026, + 3.1156324380089453 + ] + } +] \ No newline at end of file diff --git a/src/drl_pinball/train/crossre_transfer.sh b/src/drl_pinball/train/crossre_transfer.sh index 37bef41..b1ef58d 100755 --- a/src/drl_pinball/train/crossre_transfer.sh +++ b/src/drl_pinball/train/crossre_transfer.sh @@ -14,9 +14,8 @@ REPO_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)" TRAIN_DIR="$SCRIPT_DIR" CONFIG_DIR="$REPO_DIR/configs" # TODO: BEFORE PUSHING TO SERVER, update BEST_MODEL to the correct path -BEST_MODEL="$TRAIN_DIR/output/re100_karman_seed539439/models/best_model.zip" +BEST_MODEL="$TRAIN_DIR/output/re100_karman_seed41/models/best_model.zip" GPU=0 -SEED=41 EPISODES=200 TEST_EPISODES=0 RE_LIST="" @@ -47,17 +46,14 @@ fi IFS=',' read -ra RE_ARR <<< "$RE_LIST" echo "=== Cross-Re Transfer ===" -echo " Re list: ${RE_ARR[*]}" -echo " Best model: ${BEST_MODEL}" -echo " Episodes: ${EPISODES} (test-mode: ${TEST_EPISODES})" -echo " GPU: ${GPU}, Seed: ${SEED}" +echo " Re list: ${RE_ARR[*]}" +echo " Re60: base=seed43, train=seed43" +echo " Re200: base=seed45, train=seed45" +echo " Re400: base=seed43, train=seed43" +echo " Episodes: ${EPISODES} (test-mode: ${TEST_EPISODES})" +echo " GPU: ${GPU}" echo "" -if [[ ! -f "$BEST_MODEL" ]]; then - echo "ERROR: Best model not found at $BEST_MODEL" - exit 1 -fi - mkdir -p "$LOG_BASE" if [[ "$TEST_EPISODES" -gt 0 ]]; then @@ -67,14 +63,22 @@ fi for re in "${RE_ARR[@]}"; do case $re in - 60) SI=800; vis_label="re60" ;; - 200) SI=500; vis_label="re200" ;; - 400) SI=400; vis_label="re400" ;; + 60) SI=800; vis_label="re60"; TRAIN_SEED=43 + BEST_MODEL="$TRAIN_DIR/output/re100_karman_seed43/models/best_model.zip" ;; + 200) SI=500; vis_label="re200"; TRAIN_SEED=45 + BEST_MODEL="$TRAIN_DIR/output/re100_karman_seed45/models/best_model.zip" ;; + 400) SI=400; vis_label="re400"; TRAIN_SEED=43 + BEST_MODEL="$TRAIN_DIR/output/re100_karman_seed43/models/best_model.zip" ;; *) echo "ERROR: Unknown Re=$re (supported: 60, 200, 400)"; exit 1 ;; esac CONFIG="$CONFIG_DIR/config_lbm_karman_2000x600_${vis_label}.json" CASE="transfer_${vis_label}" - LOG="$LOG_BASE/${vis_label}.log" + LOG="$LOG_BASE/${vis_label}_seed${TRAIN_SEED}.log" + + if [[ ! -f "$BEST_MODEL" ]]; then + echo "ERROR: Best model not found: $BEST_MODEL" | tee -a "$LOG_BASE/summary.log" + exit 1 + fi echo "=== $(date): Starting $CASE (SI=$SI) ===" | tee -a "$LOG" @@ -101,7 +105,7 @@ for re in "${RE_ARR[@]}"; do echo " [$(date '+%H:%M:%S')] Training ${EPISODES} episodes..." | tee -a "$LOG" conda run --no-capture-output -n "$CONDA_ENV" python -u \ "$TRAIN_DIR/train_karman.py" \ - --case-name "$CASE" --device-id $GPU --seed $SEED \ + --case-name "$CASE" --device-id $GPU --seed $TRAIN_SEED \ --config "$CONFIG" \ --calibration "$CAL_JSON" \ --si $SI --total-episodes $EPISODES \ diff --git a/src/drl_pinball/train/env_illusion.py b/src/drl_pinball/train/env_illusion.py index ce70dec..c4d3e33 100644 --- a/src/drl_pinball/train/env_illusion.py +++ b/src/drl_pinball/train/env_illusion.py @@ -200,6 +200,9 @@ class IllusionCloakEnv(gym.Env): if calibration is None: raise ValueError("calibration dict is required") self._cal = calibration.copy() + # Allow calibration.json to override target_diam + if "target_diam" in self._cal: + self._target_diam = float(self._cal["target_diam"]) self._si = int(self._cal["SI"]) self._force_scale = np.float32(self._cal["FORCE_SCALE"]) self._sens_scale = np.float32(self._cal["SENS_SCALE"]) diff --git a/src/drl_pinball/train/extend_transfer.sh b/src/drl_pinball/train/extend_transfer.sh new file mode 100644 index 0000000..aa4bc18 --- /dev/null +++ b/src/drl_pinball/train/extend_transfer.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# Extend cross-Re transfer training from best_model (all seed43). +# Re60: +200ep, Re200: +400ep, Re400: +400ep. Sequential GPU 0. +# +# Conservative fine-tune: lr=1e-4 (↓3x), n_epochs=5 (↓2x) +# Symmetry-prob keeps default 0.5. +# +# Output naming: transfer_re60ext → train_karman appends _seed43 +# → transfer_re60ext_seed43, transfer_re200ext_seed43, transfer_re400ext_seed43 +# +# Timetable (SI → s/ep @ n_epochs=5 → total): +# Re60 SI=800 ~100s/ep → 200ep ≈ 5.5h +# Re200 SI=500 ~ 68s/ep → 400ep ≈ 7.5h +# Re400 SI=400 ~ 60s/ep → 400ep ≈ 6.7h +# ───────────────────────────────────────── +# Total ≈ 19.7h +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)" +CONFIG_DIR="$REPO_DIR/configs" +TRAIN_DIR="$SCRIPT_DIR" +GPU=0 +SEED=43 +CONDA_ENV="pycuda_3_10" +LOG_BASE="/tmp/extend_transfer" + +declare -A RE_SI RE_EP +RE_SI[60]=800; RE_EP[60]=200 +RE_SI[200]=500; RE_EP[200]=400 +RE_SI[400]=400; RE_EP[400]=400 + +RE_LIST="${1:-60,200,400}" +IFS=',' read -ra RE_ARR <<< "$RE_LIST" + +mkdir -p "$LOG_BASE" + +echo "=== Extend Cross-Re Transfer (seed43) ===" +echo " Output dirs: transfer_{re}ext_seed43/" +echo " Hyperparams: lr=1e-4, n_epochs=5, symmetry=0.5" +echo " Re60: +200ep (~5.5h, SI=800)" +echo " Re200: +400ep (~7.5h, SI=500)" +echo " Re400: +400ep (~6.7h, SI=400)" +echo " ────────────────────────────────" +echo " Total: ~19.7h" +echo "" + +for re in "${RE_ARR[@]}"; do + vis="re${re}" + si=${RE_SI[$re]} + extra=${RE_EP[$re]} + + CONFIG="$CONFIG_DIR/config_lbm_karman_2000x600_${vis}.json" + CAL_JSON="$TRAIN_DIR/calibrations/${vis}/calibration.json" + CASE="transfer_${vis}" + BEST_SRC="$TRAIN_DIR/output/${CASE}_seed${SEED}/models/best_model.zip" + EXT_CASE="${CASE}ext" # → transfer_re60ext_seed43 + + if [[ ! -f "$BEST_SRC" ]]; then + echo "[SKIP] ${vis}: best_model not found — ${CASE}_seed${SEED} (still training?)" + continue + fi + + LOG="$LOG_BASE/${vis}_ext.log" + echo "=== $(date): Extending ${vis} +${extra}ep (SI=$si) ===" | tee -a "$LOG" + echo " Source: $BEST_SRC" | tee -a "$LOG" + echo " Output: ${EXT_CASE}_seed${SEED}/" | tee -a "$LOG" + + conda run --no-capture-output -n "$CONDA_ENV" python -u \ + "$TRAIN_DIR/train_karman.py" \ + --case-name "$EXT_CASE" --device-id $GPU --seed $SEED \ + --config "$CONFIG" --calibration "$CAL_JSON" \ + --si $si --total-episodes $extra \ + --lr 1e-4 --n-epochs 5 \ + --transfer-model "$BEST_SRC" >> "$LOG" 2>&1 + + echo " [$(date '+%H:%M:%S')] Done." | tee -a "$LOG" + echo "" +done + +echo "=== ALL EXTENSIONS DONE ===" | tee -a "$LOG_BASE/summary.log" diff --git a/src/drl_pinball/train/launch_075L.sh b/src/drl_pinball/train/launch_075L.sh new file mode 100755 index 0000000..54a2d18 --- /dev/null +++ b/src/drl_pinball/train/launch_075L.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Launch Illusion 0.75L training on GPU2, seed 43, 500 episodes +# Usage: bash launch_075L.sh +# !! Wait 2 min after launching 2L on GPU0 before running this script +# (to avoid kernel compilation race) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)" + +CASE="illusion_075L" +SEED=43 +DEVICE=2 +CONFIG="$REPO_DIR/configs/config_lbm_karman_2000x600.json" +CAL="$SCRIPT_DIR/calibrations/$CASE/calibration.json" +OUT_DIR="$SCRIPT_DIR/output/${CASE}_seed${SEED}" + +mkdir -p "$OUT_DIR" + +cd "$SCRIPT_DIR" + +nohup conda run --no-capture-output -n pycuda_3_10 python -u train_illusion.py \ + --case-name "$CASE" \ + --device-id "$DEVICE" \ + --seed "$SEED" \ + --total-episodes 500 \ + --config "$CONFIG" \ + --calibration "$CAL" \ + > "$OUT_DIR/nohup.log" 2>&1 & + +echo "0.75L launched on GPU$DEVICE, seed=$SEED, PID=$!" +echo "Monitor: tail -f output/${CASE}_seed${SEED}/train.log" diff --git a/src/drl_pinball/train/launch_15L.sh b/src/drl_pinball/train/launch_15L.sh new file mode 100755 index 0000000..4872957 --- /dev/null +++ b/src/drl_pinball/train/launch_15L.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Launch Illusion 1.5L training on GPU1, seed 43, 500 episodes +# Usage: bash launch_15L.sh +# !! Wait 2 min after launching 1L on GPU0 before running this script +# (to avoid kernel compilation race) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)" + +CASE="illusion_15L" +SEED=43 +DEVICE=1 +CONFIG="$REPO_DIR/configs/config_lbm_karman_2000x600.json" +CAL="$SCRIPT_DIR/calibrations/$CASE/calibration.json" +OUT_DIR="$SCRIPT_DIR/output/${CASE}_seed${SEED}" + +mkdir -p "$OUT_DIR" + +cd "$SCRIPT_DIR" + +nohup conda run --no-capture-output -n pycuda_3_10 python -u train_illusion.py \ + --case-name "$CASE" \ + --device-id "$DEVICE" \ + --seed "$SEED" \ + --total-episodes 500 \ + --config "$CONFIG" \ + --calibration "$CAL" \ + > "$OUT_DIR/nohup.log" 2>&1 & + +echo "1.5L launched on GPU$DEVICE, seed=$SEED, PID=$!" +echo "Monitor: tail -f output/${CASE}_seed${SEED}/train.log" diff --git a/src/drl_pinball/train/launch_1L.sh b/src/drl_pinball/train/launch_1L.sh new file mode 100755 index 0000000..1349edd --- /dev/null +++ b/src/drl_pinball/train/launch_1L.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Launch Illusion 1L training on GPU0, seed 43, 500 episodes +# Usage: bash launch_1L.sh +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)" + +CASE="illusion_1L" +SEED=43 +DEVICE=0 +CONFIG="$REPO_DIR/configs/config_lbm_karman_2000x600.json" +CAL="$SCRIPT_DIR/calibrations/$CASE/calibration.json" +OUT_DIR="$SCRIPT_DIR/output/${CASE}_seed${SEED}" + +mkdir -p "$OUT_DIR" + +cd "$SCRIPT_DIR" + +nohup conda run --no-capture-output -n pycuda_3_10 python -u train_illusion.py \ + --case-name "$CASE" \ + --device-id "$DEVICE" \ + --seed "$SEED" \ + --total-episodes 500 \ + --config "$CONFIG" \ + --calibration "$CAL" \ + > "$OUT_DIR/nohup.log" 2>&1 & + +echo "1L launched on GPU$DEVICE, seed=$SEED, PID=$!" +echo "Monitor: tail -f output/${CASE}_seed${SEED}/train.log" diff --git a/src/drl_pinball/train/launch_2L.sh b/src/drl_pinball/train/launch_2L.sh new file mode 100755 index 0000000..e565fcf --- /dev/null +++ b/src/drl_pinball/train/launch_2L.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Launch Illusion 2L training on GPU0, seed 43, 500 episodes +# Usage: bash launch_2L.sh +# !! Wait 2 min after launching 15L on GPU1 before running this script +# (to avoid kernel compilation race) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)" + +CASE="illusion_2L" +SEED=43 +DEVICE=0 +CONFIG="$REPO_DIR/configs/config_lbm_karman_2000x600.json" +CAL="$SCRIPT_DIR/calibrations/$CASE/calibration.json" +OUT_DIR="$SCRIPT_DIR/output/${CASE}_seed${SEED}" + +mkdir -p "$OUT_DIR" + +cd "$SCRIPT_DIR" + +nohup conda run --no-capture-output -n pycuda_3_10 python -u train_illusion.py \ + --case-name "$CASE" \ + --device-id "$DEVICE" \ + --seed "$SEED" \ + --total-episodes 500 \ + --config "$CONFIG" \ + --calibration "$CAL" \ + > "$OUT_DIR/nohup.log" 2>&1 & + +echo "2L launched on GPU$DEVICE, seed=$SEED, PID=$!" +echo "Monitor: tail -f output/${CASE}_seed${SEED}/train.log" diff --git a/src/drl_pinball/train/train_illusion.py b/src/drl_pinball/train/train_illusion.py index 85ce767..c0c5758 100644 --- a/src/drl_pinball/train/train_illusion.py +++ b/src/drl_pinball/train/train_illusion.py @@ -186,6 +186,10 @@ def main() -> int: model.save(str(out_dir / "models" / f"chkpt_ep{ep}.zip")) vec_env.save(norm_path) + # Save model + eval summary after EVERY episode + model.save(str(out_dir / "models" / f"ep{ep:04d}_model.zip")) + vec_env.save(str(out_dir / "models" / f"ep{ep:04d}_vecnormalize.pkl")) + model.save(str(out_dir / "models" / "final_model.zip")) vec_env.save(norm_path)