# Stage 2: PySR Symbolic Regression

Fits interpretable formulas (obs → act) from controlled.npz data using PySR.

## Usage

```bash
# Per-scene fitting
conda run -n sr_env python stage_2_fit.py --scene karman_re100 --mode per-scene

# Joint cross-scene
conda run -n sr_env python stage_2_fit.py --scenes karman_re50,karman_re100,karman_re200,karman_re400 --mode joint

# Deep search
conda run -n sr_env python stage_2_fit.py --scenes illusion_0.75L,illusion_1L --mode joint --deep
```

## Output

`results/formulas/{label}_{front,top}.json`: best_sympy formula, feature_keys, R2 score.

Fitting target is non-dimensional **alpha = omega/U0** (not physical omega).

## Feature Sets

- **PHASE_STATE_KEYS** (6): u_a, du_a_dt, Cl_tot, dCl_tot_dt, Cd_tot, Cd_rear — Karman per-Re
- **ILLUSION_PHASE_KEYS** (10): above + Cd_err, Cl_err, dCd_err_dt, dCl_err_dt — Illusion
- **PHYS_DADT + mu** (17): physics + daF/dt + mu modulation — Karman joint

## Per-Scene vs Joint

- **per-scene**: Fit one formula per scene. Use for individual Re/diameter analysis.
- **joint**: Concatenate multiple scenes' data, fit single formula. Use for cross-scene generalization.

## Formula Review Checklist

After fitting, check:
1. **No spurious terms**: `daB_dt` = 0 at deployment (rear constant), remove if present
2. **Front no-bias**: α_F ≈ 0 when all features ≈ 0
3. **Rear shared-head**: α_B = -α_T when flow is symmetric (G-mirror applied)
4. **One-step R2 ≠ closed-loop**: Always validate in Stage 3

## Environments

`conda run -n sr_env` (PySR installed, separate from pycuda to avoid CUDA conflicts).
