# CCD Analysis Pipeline

> Correction-field CCD analysis for fluidic pinball DRL control.
> Core question: does `dq_ctl` (what the controller adds) match `dq_tar` (what the target requires)?

## Quick Start

```bash
cd src/CCD_analysis

# Panorama comparison figure (primary output)
conda run -n pycuda_3_10 python3 correction_analysis/compare_dqctl_scenes.py

# CCD quantitative decomposition
conda run -n pycuda_3_10 python3 correction_analysis/decompose_corrections.py

# Single-scene zone diagnostics
conda run -n pycuda_3_10 python3 correction_analysis/diagnose_corrections.py
```

## Pipeline Architecture

```
[Data Collection]          [Phase Alignment]         [Correction Fields]        [Analysis]
scripts/collect_*.py  →   detect_period.py     →   compute_correction_     →   compare_dqctl_scenes.py
    (GPU, device 2)       replay_fields.py          fields.py                   decompose_corrections.py
                           (CPU/GPU)                (CPU)                       diagnose_corrections.py
                                                                                (CPU)
```

## Dual-clock field sampling

Legacy DRL 的控制时钟与 DDF 保存时钟必须独立：模型仍只在固定控制周期边界读取完整 observation 和预测动作，DDF 可以在周期内任意 lattice step 读取。通用 API、OID 复用方式、输出时间轴合同和 GPU A/B 结果见 [`DUAL_CLOCK_SAMPLING.md`](DUAL_CLOCK_SAMPLING.md)。

## Key Conventions

### Geometry (unified 2026-06-25)
- **All scenes**: pinball center ≈ 613 px, sensors at 800 px (40×L0)
- Collected at source — no post-processing translation needed

### Correction Fields
- **`dq_blk = q_blk − q_in`**: pinball blockage (passive)
- **`dq_ctl = q_ctl − q_blk`**: control correction (active) — **primary analysis object**
- **`dq_tar = q_tar − q_blk`**: target correction (theoretical)
- Core question: **O(dq_ctl, dq_tar)** — how well does control match theory?

### Observation Normalization
- **Force-first**: `obs = [forces/force_norm, sensors/sens_norm]`
- Each scene computes its own `force_norm_fact`, `sens_deviation`, `sens_norm_fact` during collection
- Same norm values MUST be used during inference

### Reynolds Number
- Code Re uses reference length 2D = 40: `Re = U0×40/ν`
- Physical Re_D uses D = 20: `Re_D = Re/2`
- Default: Re=100 → Re_D=50, nu=0.004

### Inlet
- Parabolic velocity profile (not uniform)
- Top/bottom walls: no-slip bounce-back
- U0 = 0.01 (centerline, lattice units)

### Vortex Bug History (2026-06-29)
Three bugs in `collect_vortex.py` caused incorrect Lamb data:
1. **Cylinder order**: add order is front→TOP(+y)→BOTTOM(−y); reversed caused wrong bias mapping
2. **Obs swap**: used `[sensors/force_norm, forces/sens_norm]` instead of force-first `[forces/force_norm, sensors/sens_norm]`
3. **Missing fade-in/out**: 25-step transition from steady-cloak bias to PPO action required

See `collect_vortex.py` header and `ccd_knowledge.md` §12 for full details.

## Results Index

All figures in `results/figures/`; CCD JSON in `results/ccd/`.

### Panorama (main deliverable)

| # | Figure | Content |
|---|--------|---------|
| 01 | `01_panorama_all_scenes.png` | 7 scenes × 4 quantities (ux_mean, uy_mean, RMS, vorticity) |
| 02 | `02_cloak_comparison.png` | 4 cloak scenes (steady, karman, vortex_lamb, vortex_taylor) |
| 03 | `03_illusion_comparison.png` | 3 illusion scenes (0.75L, 1.0L, 1.5L) |

### Per-Scene dq_ctl vs dq_tar

| # | Figure | Scene |
|---|--------|-------|
| 04 | `04_steady_cloak_cancel.png` | Steady cloak cancellation test |
| 05 | `05_illusion_075L_ctl_vs_tar.png` | Illusion 0.75L |
| 06 | `06_illusion_10L_ctl_vs_tar.png` | Illusion 1.0L |
| 07 | `07_illusion_15L_ctl_vs_tar.png` | Illusion 1.5L |
| 08 | `08_karman_ctl_vs_tar.png` | Karman cloak re100 |
| 09 | `09_vortex_lamb_ctl_vs_tar.png` | Vortex Lamb |
| 10 | `10_vortex_taylor_ctl_vs_tar.png` | Vortex Taylor |

### Vortex Diagnosis

| # | Figure | Content |
|---|--------|---------|
| 11 | `11_vortex_lamb_diagnosis.png` | Lamb sensor + action comparison |
| 12 | `12_vortex_taylor_diagnosis.png` | Taylor sensor + action comparison |
| 13 | `13_vortex_lamb_vorticity.png` | Lamb vorticity field evolution |
| 14 | `14_vortex_taylor_vorticity.png` | Taylor vorticity field evolution |
| 15 | `15_vortex_lamb_target_vorticity.png` | Lamb target (no pinball) reference |
| 16 | `16_vortex_taylor_target_vorticity.png` | Taylor target (no pinball) reference |

### Key Numerical Results

**Correction-field CCD (2026-06-28, unified geometry)**

| Metric | 0.75L | 1.0L | 1.5L |
|--------|:-----:|:----:|:----:|
| O(dq_ctl, dq_tar) mode1 (r=6) | 0.383 | **0.926** | **0.922** |
| O(dq_ctl, dq_tar) mode1 (r=10) | 0.320 | 0.684 | 0.661 |
| Force-CCD m80 (r=6) | 2 | 2 | 1 |
| Action sigma1 (r=6) | 1.49 | 1.17 | **0.20** |

**Cloak dq_ctl RMS (cropped x=[300,1100])**

| Scene | RMS | Type |
|-------|:---:|------|
| steady_cloak | 0.196 | Steady, open-loop |
| karman_re100 | 0.397 | Periodic, PPO closed-loop |
| vortex_lamb | 0.146 | Transient, PPO closed-loop |
| vortex_taylor | 0.188 | Transient, PPO closed-loop |

**Key findings:**
- Cloak mechanism is **independent of upstream condition** (steady/vortex street/transient vortex all share the same dq_ctl structure)
- Illusion 1.0L achieves near-perfect overlap (O=0.926) via "cloak physics + target frequency modulation"
- Illusion 1.5L uses a fundamentally different mechanism (high-freq modulation, action sigma1=0.20 vs 1.17-1.49)
- 0.75L overlap dropped from 0.564→0.383 after fixing geometry alignment — old number was inflated

### CCD Quantitative (JSON)

| File | Content |
|------|---------|
| `results/ccd/correction_ccd_results.json` | Force/Action-CCD per scene (r=6,8,10), O(dq_ctl,dq_tar) per mode |
| `results/ccd/zone_metrics.json` | Per-zone KE and enstrophy |

## Full Documentation

| File | Content |
|------|---------|
| `PIPELINE.md` | This file — pipeline overview, conventions, results index |
| `README.md` | Quick start |
| `ccd_knowledge.md` | Complete knowledge base (theory, methodology, detailed results, bug history) |
| `Lyu23.md` | CCD method paper (Lyu 2023) |

## Adding New Training Results

When new DRL models are trained (e.g. on new CelerisLab solver):

1. **Collect fields**: Run the appropriate `scripts/collect_*.py` with the new model path
2. **Phase alignment**: `detect_period.py` → `replay_fields.py` (for periodic scenes)
3. **Correction fields**: Already handled by `compute_correction_fields.py` — just ensure the scene name is registered in `configs.py` `_SCENE_MAP`
4. **Regenerate figures**: `compare_dqctl_scenes.py` and `diagnose_corrections.py`
5. **Regenerate CCD**: `decompose_corrections.py`
6. **Symlink**: Add new figures to `results/figures/`

The `configs.py` `SCENES` dict is the single source of truth — add new scenes there and all analysis scripts automatically pick them up.

## Environment

- **Conda**: `pycuda_3_10`
- **GPU**: Device 2 (check with `nvidia-smi` before collection)
- **CPU-only steps**: POD, CCD, analysis scripts
