# Stage 1: PPO Inference

Generates `controlled.npz` data by running trained PPO models in LegacyCelerisLab CFD.

## Usage

```bash
conda run -n pycuda_3_10 python stage_1_infer.py --scene karman_re100 --device 2
conda run -n pycuda_3_10 python stage_1_infer.py --group karman_trained --device 2
conda run -n pycuda_3_10 python stage_1_infer.py --scene illusion_0.6L --target-only --device 2
```

## Scene Groups

karman_trained (re50-400), illusion_trained (0.75L/1L/1.5L), illusion_generalization (0.5L-2L), vortex_all.

## Output per Scene

`data/{scene_id}/{scene_name}/`: target.npz, controlled.npz, config.json, norm.json, result.json, target_harmonics.json (Illusion only).

actions in controlled.npz are normalized [-1,+1]. Physical omega = (action*scale+bias)*U0.

## Per-Scene Notes

- **Karman**: 7 objects, obs_slice=(2,14), action_bias=[0,-4,4], s_dim=12
- **Illusion**: 6 objects, obs_slice=(0,12), action_bias=[0,-2,2], s_dim=14, FIFO bias=[0,-U0,U0] differs from DRL bias.
  **target.npz format**: 8 columns = [cyl_fx, cyl_fy, s0_ux, s0_uy, s1_ux, s1_uy, s2_ux, s2_uy].
  Consumers must use `target[:, 2:8]` to extract the 6 pure-sensor channels.
  (Historical: stage_1_infer.py had a `[:, :6]` write bug that omitted s2; fixed 2026-07-14.
  On-disk files were regenerated with gen_illusion_target.py and are correct.)
- **Vortex**: 6 objects, MAX_STEPS=150, action_scale=4, vortex added after DDF checkpoint

## Expected Similarities

Karman re100 ~0.90, Illusion 1L ~0.97, Illusion 0.75L ~0.98, Illusion 1.5L ~0.95
