SR Analysis: Phase-state SINDy + ablation study + documentation

Core changes:
- New phase-state features (PHASE_STATE_KEYS, ILLUSION_PHASE_KEYS) with obs dynamics
- Derivative and absolute output modes (output_mode="deriv"|"absolute")
- predict_v23_deriv() with integration support for closed-loop
- Offline multi-step rollout evaluator (eval_rollout.py)

Key results:
- Illusion 0.75L/1L: phase-state+error-state+abs achieves 0.974/0.958 closed-loop
  with zero action history features — proving the new route works
- Karman re100: phase-state+abs reaches 0.699 (vs 0.901 with action history)
- 1.5L confirmed as bang-bang regime (R2=0.12 for linear SINDy)
- Feature ablation: 6-dim phase-state outperforms 16-dim full-lag in closed-loop

Documentation:
- docs/SR_analysis_results.md: comprehensive analysis report
- docs/HANDOVER_SR_ANALYSIS.md: handover notes for next coder
- 6 figures in docs/figures/SR_analysis/
- Updated README.md, sindy_sr_notes.md, sindy_sr_knowledge.md
- Updated configs.py with generalization scenes

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Frank14f
2026-06-22 16:55:03 +08:00
co-authored by Cursor
parent dd71af901d
commit 8e62716ce4
26 changed files with 3591 additions and 630 deletions
+109 -213
View File
@@ -12,278 +12,174 @@ cloak and illusion scenes, using dimensionless physical features,
G-equivariant structural constraints, and STLSQ threshold grids.
For background, see:
- `src/sindy_sr_notes.md` -- execution plan
- `src/sindy_sr_knoeledge.md` -- confirmed facts and known pitfalls
- `sindy_sr_notes.md` -- execution plan and task tracking
- `sindy_sr_knowledge.md` -- confirmed facts and known pitfalls
- `../../docs/SR_analysis_results.md` -- comprehensive results report
---
## Directory Structure
```
SR_analysis/
configs.py # Unified scene metadata (all 10 scenes)
configs.py # Unified scene metadata (all 10+ scenes)
configs/
legacy/ # Legacy CFD configs (config_cuda.json, config_flowfield.json)
legacy/ # Legacy CFD configs
utils/
__init__.py # Selective exports (no pycuda dependency)
feature_builder.py # Dimensionless features + G-operator (from analysis_cloak)
sindy_fitter.py # STLSQ threshold grid, feature matrix builder
feature_builder.py # Dimensionless features + G-operator + phase-state features
sindy_fitter.py # STLSQ + feature matrices + derivative/absolute modes
cfd_interface.py # LegacyCelerisLab wrapper (requires pycuda_3_10)
g_operator.py # Equivariance diagnostics
data/
karman/ # Karman cloak: karman_re50, re100, re200, re400
steady/ # Steady cloak: steady_data.npz
illusion/ # Illusion: illusion_0.75L, illusion_1L, illusion_1.5L
vortex/ # Vortex cloak: vortex_lamb, vortex_taylor
karman/ # Karman cloak: karman_re50/100/200/400
steady/ # Steady cloak
illusion/ # Illusion: illusion_0.75L/1L/1.5L
vortex/ # Vortex cloak
scripts/
infer_karman.py # Inference: LegacyCFD + PPO -> controlled.npz
infer_illusion.py # Inference: for 0.75L, 1L, 1.5L diameters
infer_vortex.py # Inference: for Lamb dipole + Taylor monopole
infer_illusion.py # Inference for illusion scenes
infer_vortex.py # Inference for vortex scenes
sindy/
run_karman.py # SINDy fitting for Karman scenes
run_illusion.py # SINDy fitting for Illusion scenes
run_vortex.py # SINDy fitting for Vortex scenes
run_pareto.py # Pareto-front analysis from SINDy results
karman/ # Output: sindy_results.json, pareto_*.json
illusion/ # Output: sindy_results.json, pareto_*.json
vortex/ # Output: sindy_results.json, pareto_*.json
run_all_v2.py # Unified SINDy fitting (supports --deriv, --phase, --output-mode etc.)
run_pysr.py # Restricted PySR symbolic regression
wrap_joint.py # Joint model -> wrapped format for validator
compare_v2.py # Cross-scene comparison report
karman/illusion/vortex/ # SINDy output JSONs
validate/
run_closed_loop.py # Unified closed-loop validator (v23 + unstructured modes)
run_closed_loop.py # Karman closed-loop validator (v23/deriv/abs modes)
run_closed_loop_illusion.py # Illusion closed-loop validator
eval_rollout.py # Offline multi-step rollout evaluation
results/ # Validation result JSONs
compare/
support_overlap.py # Pairwise support set comparison
shared_core.py # Multi-scene shared-core detection
support_overlap.py # Support set comparison
shared_core.py # Shared core detection
```
---
## Key Design Decisions
### 1. Scene Metadata Driven
All scene parameters (Re, action scaling, geometry, model paths) are defined
once in `configs.py`, not hard-coded in scripts. Adding a new scene means
adding one dict to `configs.py`.
All scene parameters defined once in `configs.py`.
### 2. Data / Features / Models Separation
### 2. Feature Levels
- `data/` -- raw sensor/force/action arrays (.npz), one-time generation
- `sindy/` -- SINDy fitting results (JSON), reusable for comparison
- `scripts/` -- inference pipelines that produce `data/`
| Level | Features | Dim | Description |
|-------|----------|:---:|-------------|
| Static | u_m, u_a, u_c, v_a, Cd_tot, Cd_rear, Cl_tot, Cl_diff | 8 | Current-step only |
| **Phase-state** | u_a, du_a/dt, Cl_tot, dCl_tot/dt, Cd_tot, Cd_rear | **6** | Oscillation phase + rate |
| Illusion-phase | Phase-state + Cd_err, Cl_err, dCd_err/dt, dCl_err/dt | **10** | Phase + error-state |
| Karman-expanded | Phase-state + u_m, u_c, v_a, Cl_diff | **10** | Phase + supplementary |
| Full-lag | Static + lag-1 | 16 | Full temporal context |
### 3. Unified Feature Builder
### 3. Output Modes
Every scene uses the same `utils/feature_builder.py`, which produces
21 dimensionless features from raw lattice-unit sensor/force data:
**Sensor features (nondim):**
- `u_m`, `u_a`, `u_c` -- streamwise: mean, antisymmetric, centre
- `v_a` -- antisymmetric cross-stream
- `sin_ua`, `cos_ua` -- phase encoding via u_a
**Force features (Cd/Cl):**
- `Cd_tot`, `Cd_rear` -- total and rear-cylinder drag
- `Cl_tot`, `Cl_diff` -- total and differential lift
**Memory features (nondim alpha):**
- `aF_lag1`, `aB_lag1`, `aT_lag1` -- lagged actions (t-1)
- `daF`, `daB`, `daT` -- action increments (t-1)-(t-2)
**Reynolds modulation:**
- `mu` (= 1/Re_D), `mu_u_a`, `mu_v_a`, `mu_Cd_tot`, `mu_Cl_diff`
- **deriv**: predict `d(alpha)/dt`, then `alpha(t) = alpha(t-1) + dt_c * dalpha/dt`
- **absolute**: predict `alpha(t)` directly (no integration drift)
### 4. G-Equivariant Structure (v23)
Default control law structure (confirmed as the best v23 model):
```
Front(t) = f_front(x(t)) # no bias, odd under G
Top(t) = f_rear(x(t)) # with bias
Bottom(t) = -f_rear(G[x(t)]) # shared-head: bottom = -top(Gx)
Front(t) = f_front(x(t)) # no bias, odd under G
Top(t) = f_rear(x(t)) # with bias
Bottom(t) = -f_rear(G[x(t)]) # shared-head
```
Where G is the mirror operator (y -> -y) with corrected sign rules:
- `[aF, aT, aB] -> [-aF, -aB, -aT]`
- Sensor swap: top <-> bottom, negate v
- Force swap: front unchanged, bottom <-> top, negate Cl
---
### 5. STLSQ Threshold Grid
## Current Best Results (2026-06-15)
Default thresholds: `[0, 0.001, 0.002, 0.005, 0.01, 0.015, 0.02, 0.03, 0.05, 0.1]`
Per-channel: front (no bias), top (shared-head), bottom (independent, for comparison)
### Illusion — New Route: Phase-state + Error-state + Absolute Action
## Scene Inventory
| Scene | Closed-loop | % of PPO | Action history? | Features |
|-------|:----------:|:--------:|:---------------:|----------|
| 0.75L | **0.974** | 100.2% | **No** | ILLUSION_PHASE (10dim) |
| 1L | **0.958** | 98.5% | **No** | ILLUSION_PHASE (10dim) |
| 1.5L | N/A | — | **No** | Bang-bang regime |
| Scene Name | Description | Re_code | Sample Interval | Action | U0 |
|---|---|---|---|---|---|
| karman_re50 | Karman cloak at low Re | 50 | 800 | 8x + [0,-4,4] | 0.01 |
| karman_re100 | Karman cloak (default) | 100 | 800 | 8x + [0,-4,4] | 0.01 |
| karman_re200 | Karman cloak at high Re | 200 | 800 | 8x + [0,-4,4] | 0.01 |
| karman_re400 | Karman cloak at highest Re | 400 | 800 | 8x + [0,-4,4] | 0.01 |
| steady | Open-loop constant rotation | 100 | 800 | 8x + [0,-5.1,5.1] | 0.01 |
| illusion_0.75L | Imitate 0.75D cylinder | 100 | 600 | 8x + [0,-2,2] | 0.01 |
| illusion_1L | Imitate 1.0D cylinder | 100 | 600 | 8x + [0,-2,2] | 0.01 |
| illusion_1.5L | Imitate 1.5D cylinder | 100 | 600 | 8x + [0,-2,2] | 0.02 |
| vortex_lamb | Cloak Lamb dipole | 100 | 800 | 4x + [0,-4,4] | 0.01 |
| vortex_taylor | Cloak Taylor monopole | 100 | 800 | 4x + [0,-4,4] | 0.01 |
### Karman re100 — Ablation
Note: "Re_code" uses reference length 2*D (code convention).
Physical Re_D = Re_code / 2. E.g. Re_code=100 -> Re_D=50.
| Config | Feat | Output | R2 | Closed-loop | Note |
|--------|:----:|:-----:|:--:|:----------:|------|
| old v23 (a_lag) | 14+3 | alpha | 0.996 | **0.901** | Baseline |
| **Phase->abs** | **6** | **alpha** | **0.965** | **0.699** | Best new route |
| Phase->deriv | 6 | dalpha/dt | 0.837 | 0.656 | |
| Phase+mu->abs | 9 | alpha | 0.979 | 0.700 | mu helps cross-Re |
| Expanded->abs | 10 | alpha | 0.980 | 0.580 | Overfitting |
## Re-generation Commands
---
All commands run from repo root (`/home/frank14f/DynamisLab`).
## Commands
### Data Generation (requires GPU, pycuda_3_10 env)
All from repo root (`/home/frank14f/DynamisLab`).
### SINDy Fitting
```bash
# Karman cloak -- all 4 training Re
conda run -n pycuda_3_10 python src/SR_analysis/scripts/infer_karman.py --re all --device 0
# Illusion phase-state + absolute (recommended for 0.75L/1L)
conda run -n pycuda_3_10 python src/SR_analysis/sindy/run_all_v2.py \
--scenes illusion_0.75L,illusion_1L --deriv --phase --output-mode absolute
# Karman cloak -- single Re
conda run -n pycuda_3_10 python src/SR_analysis/scripts/infer_karman.py --re 100 --device 0 --steps 200
# Karman phase-state + absolute
conda run -n pycuda_3_10 python src/SR_analysis/sindy/run_all_v2.py \
--scenes karman_re100 --deriv --phase --output-mode absolute
# Illusion -- all 3 diameters
conda run -n pycuda_3_10 python src/SR_analysis/scripts/infer_illusion.py --diameter all --device 0
# Karman expanded (10 dim)
conda run -n pycuda_3_10 python src/SR_analysis/sindy/run_all_v2.py \
--scenes karman_re100 --deriv --karman-expand --output-mode absolute
# Vortex -- both types
conda run -n pycuda_3_10 python src/SR_analysis/scripts/infer_vortex.py --type all --device 0
# Karman with mu modulation
conda run -n pycuda_3_10 python src/SR_analysis/sindy/run_all_v2.py \
--scenes karman_re100 --deriv --karman-mu --output-mode absolute
# Old-style (v2, with action history)
conda run -n pycuda_3_10 python src/SR_analysis/sindy/run_all_v2.py \
--scenes karman_re50,karman_re100 --joint
```
### SINDy Fitting (no GPU needed, pycuda_3_10 env for pysindy)
```bash
conda run -n pycuda_3_10 python src/SR_analysis/sindy/run_karman.py
conda run -n pycuda_3_10 python src/SR_analysis/sindy/run_illusion.py
conda run -n pycuda_3_10 python src/SR_analysis/sindy/run_vortex.py
```
### Pareto Analysis (no GPU, no conda needed)
```bash
python3 src/SR_analysis/sindy/run_pareto.py --scene karman_re100
python3 src/SR_analysis/sindy/run_pareto.py --scene illusion_1L
```
### Closed-loop Validation (requires GPU)
### Closed-loop Validation
```bash
# Karman with absolute action
conda run -n pycuda_3_10 python src/SR_analysis/validate/run_closed_loop.py \
--scene karman_re70 --device 2 \
--sindy-results src/SR_analysis/sindy/karman/sindy_results.json
--scene karman_re100 --device 0 --steps 200 --mode abs \
--sindy-results src/SR_analysis/sindy/karman/sindy_results_deriv.json
# With custom mode
# Karman old v23
conda run -n pycuda_3_10 python src/SR_analysis/validate/run_closed_loop.py \
--scene karman_re70 --device 2 --mode unstructured
--scene karman_re100 --device 0 --steps 200 --mode v23 \
--sindy-results src/SR_analysis/sindy/karman/sindy_joint_wrapped.json
# Illusion with absolute action
conda run -n pycuda_3_10 python src/SR_analysis/validate/run_closed_loop_illusion.py \
--scene illusion_1L --device 0 --steps 320 \
--sindy-results src/SR_analysis/sindy/illusion/sindy_results_deriv.json
```
### Cross-scene Comparison (no GPU)
### PySR Symbolic Regression
```bash
# Pairwise support overlap
python3 src/SR_analysis/compare/support_overlap.py \
--sindy-results src/SR_analysis/sindy/karman/sindy_results.json \
--scenes karman_re100 illusion_1L
# Multi-scene shared core
python3 src/SR_analysis/compare/shared_core.py \
--sindy-results src/SR_analysis/sindy/karman/sindy_results.json \
--scenes karman_re50 karman_re100 karman_re200 karman_re400
conda run -n sr_env python src/SR_analysis/sindy/run_pysr.py --scene illusion_1L
```
## Key Results Summary
### Offline Rollout Evaluation
### Data Quality (similarity scores)
```bash
python3 src/SR_analysis/validate/eval_rollout.py \
--sindy-results src/SR_analysis/sindy/karman/sindy_results_deriv.json \
--scene karman_re100
```
| Scene | PPO Similarity |
|---|---|
| karman_re50 | 0.962 |
| karman_re100 | 0.954 |
| karman_re200 | 0.884 |
| karman_re400 | 0.795 (inferred, not verified) |
| vortex_lamb | 0.942 |
| vortex_taylor | 0.916 |
| illusion_1L | ~0.55 (metric not directly comparable) |
---
### SINDy Fit Quality (R2 scores for one-step prediction)
## Important Reminders
| Scene | Front | Top (shared) | Bottom |
|---|---|---|---|
| karman_re50 | 0.998 | 0.989 | 0.996 |
| karman_re100 | 0.995 | 0.993 | 0.997 |
| karman_re200 | 0.957 | 0.914 | 0.918 |
| karman_re400 | 0.991 | 0.979 | 0.969 |
| illusion_0.75L | 0.991 | 0.989 | 0.990 |
| illusion_1L | 0.979 | 0.984 | 0.984 |
| illusion_1.5L | 0.959 | 0.928 | 0.932 |
| vortex_lamb | 0.904 | 0.980 | 0.933 |
| vortex_taylor | 0.960 | 0.810 | 0.643 |
### Shared Core Features
**Karman cross-Re (active in all re50/100/200):**
- Front core: `mu`, `mu_Cd_tot`, `mu_Cl_diff`, `mu_v_a` (mu-modulated terms dominate)
- Top core: `Cl_tot`, `bias`, `mu_Cd_tot`, `mu_Cl_diff`, `mu_u_a`, `mu_v_a`
- Scene-specific: lower-Re scenes have additional `Cd_tot`, `Cl_diff`, `aT_lag1` etc.
**Illusion cross-diameter (active in all 0.75L/1L/1.5L):**
- Front core: `mu`, `mu_Cd_tot`, `mu_Cl_diff` (same structure as Karman front!)
- Top core: `Cd_rear`, `Cl_tot`, `bias`, `mu_Cd_tot`, `mu_Cl_diff`
- This suggests a **shared mu-modulated feedback structure** exists across both scenes
## Known Issues and Caveats
1. **Vortex Taylor rear channels** have low R2 (0.64-0.81). The weak monopole
produces near-zero rear action, making SINDy fitting noisy. Use Lamb as the
primary vortex reference.
2. **Closed-loop validator** (`validate/run_closed_loop.py`) has been ported but
NOT yet tested end-to-end. The original `validate_v23.py` verified Karman
but the new unified version has not been run.
3. **Illusion similarity scores** use the Karman CONV_LEN=30 metric, giving
lower raw numbers. The controlled.npz data itself is valid for SINDy.
4. **Steady cloak** is open-loop constant rotation, not PPO-derived. It serves
as a physical consistency check, not a primary comparison scene.
5. **SINDy one-step R2 is not sufficient** -- a high R2 does not guarantee good
closed-loop performance. Always validate via `validate/run_closed_loop.py`.
6. **Scene key naming**: keys like `illusion_1L`, `illusion_1.5L` use the short
float format from Python (1.0 -> "1L", 1.5 -> "1.5L", 0.75 -> "0.75L").
## Next Steps (Future Work)
1. **PySR symbolic regression** -- Run PySR on the SINDy-identified active
features (in `sr_env` conda env) to find closed-form formulas. Essential
reading: `src/pysr.md`.
2. **Closed-loop validation of all new scenes** -- Run
`validate/run_closed_loop.py` for illusion and vortex scenes using their
SINDy coefficients.
3. **Cross-scene shared backbone test** -- Fit a single SINDy model on merged
Karman + Illusion data, test if it performs on both.
4. **Time-scale explicit formulation** -- Make the sample interval an explicit
feature to compare control laws across different frequencies.
5. **Steady as consistency check** -- Validate that Karman-derived control laws
can reproduce the steady cloak result as a sanity check.
## File Reference
| File | Lines | Purpose |
|---|---|---|
| configs.py | ~205 | Unified scene metadata |
| utils/feature_builder.py | ~212 | Dimensionless features + G-op |
| utils/sindy_fitter.py | ~175 | STLSQ fitting, feature matrix builder |
| utils/cfd_interface.py | ~370 | LegacyCelerisLab wrapper |
| utils/g_operator.py | ~170 | Equivariance diagnostics |
| utils/__init__.py | ~10 | Selective exports |
| scripts/infer_karman.py | ~250 | Karman inference pipeline |
| scripts/infer_illusion.py | ~270 | Illusion inference pipeline |
| scripts/infer_vortex.py | ~280 | Vortex inference pipeline |
| sindy/run_karman.py | ~160 | Karman SINDy fitting |
| sindy/run_illusion.py | ~110 | Illusion SINDy fitting |
| sindy/run_vortex.py | ~110 | Vortex SINDy fitting |
| sindy/run_pareto.py | ~140 | Pareto analysis |
| validate/run_closed_loop.py | ~270 | Closed-loop validator |
| compare/support_overlap.py | ~150 | Pairwise support comparison |
| compare/shared_core.py | ~140 | Multi-scene shared core detection |
- `controlled.npz` actions are **normalized [-1,1]** — must convert via `(norm * scale + bias) * u0`
- **FIFO bias ≠ DRL action bias** for Illusion: FIFO=[0, -0.01, 0.01], decode=[0, -0.02, 0.02]
- "2U" in model name = S_DIM=14 (not 2x velocity), u0 always 0.01
- SAMPLE_INTERVAL: 0.75L=400, 1L=600, 1.5L/Karman=800
- Closed-loop steps auto-set: S=400→320, S=600→214, S=800→160
- One-step R² high ≠ closed-loop good — always validate
- For phase-state features, always pass `sensors_raw`/`forces_raw` to enable derivative computation