# CCD Analysis Knowledge Base — 论文知识库 (2026-06-25)

> 统一文档。整合了 Round 5-6 的完整分析结果、correction-field 框架、Vortex/Cloak/Illusion 对比、几何对齐方案、以及所有操作流程。写论文时以本文档为准。

---

## 1. 项目概览

### 1.1 物理系统

**Fluidic Pinball**: 三个等间距圆柱（直径 D=20 格点，中心间距 0.75D）在二维通道内呈倒三角排列（间距 30°）。每个圆柱可独立旋转。DRL 控制器（PPO）每 SAMPLE_INTERVAL 步输出三个转速动作 [-1,1]^3，映射到物理表面速度。

**场景分类**:

```mermaid
flowchart LR
    subgraph Cloak [Cloak：涡街伪装]
        Steady[均匀来流 → 下游恢复均匀]
        Karman[涡街来流 → 下游维持涡街]
        Vortex[瞬态涡事件 → 涡不受畸变]
    end
    subgraph Illusion [Illusion：流场欺骗]
        I075[0.75L: 伪装成更小圆柱]
        I10[1.0L: 伪装成等大圆柱]
        I15[1.5L: 伪装成更大圆柱]
    end
```

**控制机制**（经 SR 和 correction-field 双重验证）:
- 后两圆柱（Top/Bottom）: 恒速反向旋转（cloak 约 ±0.313），补偿 pinball 后速度亏损
- 前端圆柱（Front）: 动态调节，控制升力（$\alpha_F = \Delta a_F/\Delta t - 14.952\mu C_{l,\text{tot}}$）

### 1.2 核心方法：Correction-field 框架

**基本思想**: 不直接分析受控流场 `q_ctl`，而是分析**控制施加的修正**：

| 符号 | 含义 | 计算方式 |
|------|------|----------|
| `q_in` | 均匀来流（干净通道） | 空通道采集 |
| `q_blk` | pinball 无控制 | 原始 pinball 涡街 |
| `q_ctl` | pinball + DRL 控制 | PPO 推理采集 |
| `q_tar` | 目标流场 | 目标圆柱/涡街参考 |
| `dq_blk = q_blk - q_in` | pinball 阻塞场 | pinball 对来流的畸变 |
| `dq_ctl = q_ctl - q_blk` | **控制修正场** | 控制器在 pinball 基础上加的改变 |
| `dq_tar = q_tar - q_blk` | **目标修正场** | 理论需达到的修正 |

**核心问题**: `dq_ctl` 是否等于 `dq_tar`？即控制做的"修正"是否就是理论上需要的"修正"？

### 1.3 关键发现

1. **Cloak 机制统一**：Steady/Karman/Vortex 三种场景的 dq_ctl 高度一致——补偿速度亏损（正 ux）+ 偶极子效应。控制策略相同，不论上游来流条件。

2. **Illusion 1.0L 与 Cloak 共享机制**：dq_ctl 结构定性一致。Illusion 本质是通过"Cloak 机制"让下游看起来像目标圆柱。

3. **Illusion 0.75L 效率降低**：O(dqctl,dqtar)=0.564，控制效率低于 1.0L

4. **Illusion 1.5L 特殊机制**：O(dqctl,dqtar)=0.667，action sigma1=0.28（其他 1.13-1.39），强相位漂移，修正集中在近体区

5. **Force/Signature 分离**：力相关结构和传感器相关结构在瞬时 tau=0 时分离（O=0.01-0.55），对流延迟后共享（O=0.72-0.81）

---

## 2. 方法细节

### 2.1 物理参数

| 参数 | 值 | 说明 |
|------|-----|------|
| NX, NY | 1280, 512 | LBM 格点数 |
| L0 | 20 | 基准长度单位（1 圆柱直径） |
| U0 | 0.01 | 入口中心流速（抛物线分布） |
| nu | 0.004 | 默认运动粘度 |
| Re_code | 100 | 参考长度 2×D_cyl → Re_D=50 |
| D_cyl | L0=20 | 单一圆柱直径 |
| CENTER_Y | 255.5 | 通道中心 y 坐标 |

**Reynolds 数约定**：
- `re_code` 使用参考长度 D_REF = 2*D = 40（匹配模型文件命名）
- `Re_D = re_code / 2` 是真实物理雷诺数（使用单圆柱直径）
- 默认场景 `re_code=100` → `Re_D=50`

### 2.2 场景几何位置（2026-06-25 统一更新）

**所有场景已统一几何**：采集端直接在 `configs.py` 中设置统一坐标，无需后处理平移。

| 组 | 场景 | pinball 中心 | sensor x | 来源 configs |
|----|------|-------------|--------|-------------|
| **所有场景** | pinball, steady, karman, vortex, illusion_*, target_cylinder_* | **613 px** | **800 px** | configs.py UNIFIED 注释 |

- pinball: front x=30×L0=600, rear x=31.3×L0=626 → 中心 ≈ 613 px
- target cylinder: x=30.65×L0=613 px
- sensors: x=40×L0=800 px

### 2.3 几何对齐方案（已废弃，仅保留备用）

统一几何后不再需要后处理平移。`utils/field_translate.py` 保留作为可选工具，但不参与默认 pipeline。

<details>
<summary>旧方案（参考）</summary>

```python
SHIFT_ILLUSION_TO_CLOAK = +220 px  # 旧 illusion → cloak
SHIFT_CLOAK_TO_ILLUSION = -220 px
```
</details>

---

## 3. 数据采集

### 3.1 GPU 采集脚本

| 脚本 | 功能 | 输出目录 |
|------|------|----------|
| `scripts/collect_target_cylinder.py` | 目标圆柱（单圆柱涡街） | `data/target_cylinder/{diam}L/` |
| `scripts/collect_pinball.py` | pinball 无控制基线 | `data/pinball/pinball/` |
| `scripts/collect_empty_channel.py` | 空通道 | `data/target_channel/target_channel/` |
| `scripts/collect_illusion.py` | Illusion PPO 推理 | `data/illusion/{scene}/` |
| `scripts/collect_karman.py` | Karman cloak PPO 推理 | `data/karman/karman_re100/` |
| `scripts/collect_karman_q_in.py` | 涡街无 pinball（Karman 参考） | `data/karman_target/karman_q_in/` |
| `scripts/collect_karman_q_blk.py` | 涡街+pinball 无控制（Karman 参考） | `data/karman_blocked/karman_q_blk/` |
| `scripts/collect_steady_cloak.py` | 稳态 cloak（开环恒速） | `data/steady_cloak/steady_cloak/` |
| `scripts/collect_vortex.py` | Vortex cloak（Taylor/Lamb） | `data/vortex_{type}/` |

### 3.2 采集流程（以 Karman 为例）

```
Phase 1: Target recording
  FlowField → add_sensor(40*L0) ×3 → stabilize → add_vortex/cylinder
  → run(SI, zero) × F来O_LEN → save target.npz

Phase 2: Add pinball + Norm
  restore → add_cylinder(pinball) ×3 → stabilize
  → run(SI, zero) × FIFO_LEN → compute norm
  → run(SI, bias_action) × FIFO_LEN → save ddf+fifo checkpoint

Phase 3: Controlled PPO inference
  restore + warmup → for step in range(n_steps):
    model.predict(obs) → action → run(SI, action_arr) → save telemetry + field
```

### 3.3 Norm 计算

```
force_norm_fact = 6 × max|forces|
sens_deviation = mean(sensors, axis=0)
sens_norm_fact[i] = 5 × max|sensors[:,i] - sens_deviation[i]|
obs = clip[forces/force_norm, (sens - deviation)/sens_norm], to [-1, 1]
```

**注意**：Norm 是在 scene-specific 采集时计算的，不同场景的 norm 值不同。推理时必须使用对应场景的 norm。

### 3.4 Phase Alignment 流程

```
detect_period.py:
  sensors[:, 3] → FFT → dominant frequency + period
  zero-crossing detection → cycle boundaries (CV_T)
  select best 4-cycle window → map 4×24=96 uniform phase points
  → save phase_plan.json

replay_fields.py:
  load phase_plan + ddf_checkpoint + actions
  replay PPO → at each phase_plan step_index → save velocity field
  → save fields_aligned.npz + replay_verify.json
```

### 3.5 数据状态

| 场景 | scene_id | 帧数 | 格式 |
|------|----------|------|------|
| pinball | pinball | 96 | fields_aligned.npz |
| target_cylinder_{0.75,1.0,1.5}L | target_cylinder | 96 | fields_aligned.npz |
| illusion_{0.75,1.0,1.5}L | illusion | 96 | fields_aligned.npz |
| karman_re100 | karman | 72 (3周期) | fields_aligned.npz |
| karman_q_in | karman_target | 96 | fields_aligned.npz |
| karman_q_blk | karman_blocked | 96 | fields_aligned.npz |
| steady_cloak | steady_cloak | 500 | fields.npz (旧格式) |
| target_channel | target_channel | 100 | fields.npz (旧格式) |
| vortex_{lamb/taylor}/target/uncontrolled | 各自 scene_id | 150 | fields.npz (瞬态) |

---

## 4. CCD 方法

### 4.1 算法原理（Lyu23）

CCD（Canonical Correlation Decomposition）通过 CCA 在流场和可观测量之间找到相关性最大的方向：

```python
# Reduced CCD in POD coefficient space
def compute_reduced_ccd(pod_coeffs, observable, Q_delay=6):
    # 1. Construct lagged observable matrix P (with symmetric delay window)
    # 2. Standardize P and A (POD coefficients)
    # 3. Cross-correlation matrix: C = P @ A^T / (N * sqrt(Q))
    # 4. SVD(C) → R, sigma, W
    # W = CCD directions in POD space
    # sigma = correlation strength (sorted descending)
    # z = CCD temporal coefficients
```

**物理意义**：
- sigma[0] = 第一 CCD 模式的相关系数
- m80 = 需要多少模式来捕获 80% 的总相关性（compactness 指标）
- O_k = 两个 case 之间第 k 模的重叠（内积绝对值）

### 4.2 三条分析线

| 线 | Observable | 问题 | 物理意义 |
|----|-----------|------|----------|
| **Force** (主) | SigmaFy = sum(Fy_i) | 哪些修正结构决定升力？ | 力相关流场结构 |
| **Force** (次) | SigmaFx | 哪些修正结构决定阻力？ | 不可靠（R2~0.4） |
| **Action** | [omega1, omega2, omega3] | 控制器直接调制哪些结构？ | 动作相关流场结构 |
| **Signature** | e(t+tau) = s_ctl(t+tau) - s_tar(t+tau) | 哪些结构决定未来传感器误差？ | 传感器相关流场结构 |

### 4.3 验证方法

**LOCO (Leave-One-Cycle-Out)**:
- 4 个涡街周期，留 1 做测试，3 做训练
- 训练 CCD → 预测可观测量 → 计算 R2
- 通过阈值：R2_m80 > 0.4

| Observable | 0.75L R2_m80 | 1.0L R2_m80 | 结论 |
|-----------|-------------|-------------|------|
| force_fy | 0.65 ± 0.08 | 0.64 ± 0.02 | PASS |
| force_fx | 0.38 ± 0.23 | 0.43 ± 0.11 | WARNING |
| signature tau=0 | 0.50 ± 0.09 | 0.49 ± 0.04 | PASS |
| signature tau=tau_c | 0.51 ± 0.09 | 0.53 ± 0.03 | PASS |

### 4.4 Zone-Restricted CCD

统一几何后所有场景共用一套 zone 定义（定义见 `diagnose_corrections.py` 的 `define_zones_karman()`）：

| 区域 | x 范围 (像素) | 包含 |
|------|-------------|------|
| near_body | 580-720 | pinball 周围 |
| body_wake | 720-850 | 近尾流 |
| sensor_zone | 780-850 | 传感器区域 |

---

## 5. 结果

### 5.1 Correction-field CCD 主表（2026-06-28 更新，统一几何后重跑）

| 指标 | 0.75L | 1.0L | 1.5L |
|------|-------|------|------|
| **O(dqctl, dqtar) mode1 (r=6)** | **0.383** | **0.926** | **0.922** |
| **O(dqctl, dqtar) mode1 (r=10)** | **0.320** | **0.684** | **0.661** |
| force_fy m80 (r=6) | 2 | 2 | **1** |
| action sigma1 (r=6) | 1.49 | 1.17 | **0.20** |
| Phase drift | low | low | **high** |
| 1.5L special: rank sensitivity | — | — | O drops 0.922→0.661 (r=6→10) |

**关键变化**（vs 6月15日旧版，Illusion 旧几何 pinball x≈393px）：
- 0.75L O 从 0.564 → **0.383**（-32%）：旧几何的空间错位虚高了 overlap。统一几何后揭示真实匹配度远低于预期。
- 1.0L O 从 0.913 → **0.926**（+1.4%）：基本不变，1.0L 的控制修正与目标一致。
- 1.5L O(r=6)=**0.922**（首次获得）：dominant mode 匹配度高，但更高阶 mode 快速发散（r=10 时降至 0.661），反映多尺度控制策略。
- 1.5L action sigma1=**0.20**（远低于 0.75L 的 1.49 和 1.0L 的 1.17）：确认高频调制机制下动作与流场结构的映射极其分散。

以下 force-sig overlap 和 zone 数据来自 6 月 15 日旧版（待用统一几何和新 zone 定义重跑）：

### 5.2 三区域 Force-Signature Overlap

**0.75L** — sensor_zone 在 tau=0 时近乎正交：

| Zone | O(force, sig) tau=0 | O(force, sig) tau=tau_c |
|------|--------------------|------------------------|
| near_body | 0.262 | 0.827 |
| body_wake | 0.269 | **0.917** |
| sensor_zone | **0.010** | 0.722 |

**1.0L** — 更均匀，没有近零区域：

| Zone | O(force, sig) tau=0 | O(force, sig) tau=tau_c |
|------|--------------------|------------------------|
| near_body | 0.596 | 0.596 |
| body_wake | 0.509 | 0.483 |
| sensor_zone | 0.594 | **0.730** |

### 5.3 Cloak 全景对比（Steady / Karman / Vortex dq_ctl）

所有 cloak 场景的 dq_ctl 展现了**一致的物理机制**：
1. **速度亏损补偿**：pinball 后方正 ux（红色），控制加速尾流
2. **偶极子效应**：圆柱附近旋转产生的偶极子模式
3. **涡量结构**：三种场景涡量分布高度相似

量化指标：

| 场景 | dq_ctl RMS (crop) | 性质 |
|------|------------------|------|
| steady_cloak | 0.196 | 稳态，开环 |
| karman_re100 | 0.397 | 周期，PPO 闭环 |
| vortex_lamb | 0.164 | 瞬态，PPO 闭环（fade-in/out + swapped norm） |
| vortex_taylor | 0.203 | 瞬态，PPO 闭环（fade-in/out + swapped norm） |

**结论**：不论上游条件如何（稳态/周期涡街/瞬态涡），控制策略的基本物理机制一致——"通过后两圆柱旋转补偿 pinball 阻塞引起的速度亏损，前端圆柱调节升力"。这与 SR 分析的结论完全吻合。

### 5.4 Steady Cloak 定量化

| 指标 | 值 |
|------|-----|
| 全局波动抑制 | ≈0% |
| 残留/阻塞比 | 81% |
| 传感器区残留 | 13% |

**结论**：开环恒速旋转几乎无法抑制波动。需要闭环 DRL 控制。

### 5.5 Action-CCD Mode 1

Action-CCD 找出了控制器直接调制的主要结构。对 Cloak 场景，action-CCD mode 1 ≈ dq_ctl mean field，确认了"控制调制的结构 = correction-field 的主成分"的直觉。

---

## 6. Correction-field 诊断图

### 6.1 图例说明

所有位于 `data/figures/` 下（无 colorbar，干净布局，裁剪到 x=300-1100）：

| 图 | 内容 |
|----|------|
| `corr_comparison_all_scenes.png` | 7 场景全景（4 cloak + 3 illusion），4 行 × 7 列 |
| `corr_cloak_comparison_dqctl.png` | Cloak 四场景 dq_ctl 对比 |
| `corr_illusion_comparison_dqctl.png` | Illusion 三直径 dq_ctl 对比 |
| `corr_{scene}_ctl_vs_tar.png` | 单场景 dq_ctl vs dq_tar 对比（2×2） |
| `steady_cloak_cancel_test.png` | Steady cloak 抵消检验 |

### 6.2 全景对比图的读法

每行 = 一个物理量（ux_mean / uy_mean / RMS / vorticity）
每列 = 一个场景
颜色统一（同一行在所有列间共享 vmax）

**核心观察**：
- cloak 四列间 dq_ctl 高度一致 → 控制策略不依赖上游条件
- illusion_1.0L 与 cloak 定性一致 → 1.0L illusion=cloak 机制
- illusion_0.75L 偏弱但定性相似 → 控制效率降低
- illusion_1.5L 结构突变 → 完全不同的策略

---

## 7. 操作流程

### 7.1 环境

```bash
conda run -n pycuda_3_10
# Python 3.10+, numpy, matplotlib, LegacyCelerisLab, stable-baselines3
# CUDA device 2 (采集用)
```

### 7.2 快速重新生成所有图

```bash
# 1. Correction-field pipeline（CPU）
conda run -n pycuda_3_10 python3 correction_analysis/diagnose_corrections.py

# 2. 对比图（CPU）
conda run -n pycuda_3_10 python3 correction_analysis/compare_dqctl_scenes.py
```

### 7.3 从零开始完整流程

```bash
# Step 1: GPU 数据采集（每次 > ~4min）
# （已有数据则可跳过）

# Step 2: Phase alignment（CPU）
python3 scripts/detect_period.py --scene {scene_name}

# Step 3: Field replay（GPU）
conda run -n pycuda_3_10 python3 scripts/replay_fields.py --scene {scene_name} --device 2

# Step 4: Correction-field 分析（CPU）
conda run -n pycuda_3_10 python3 correction_analysis/compute_correction_fields.py
conda run -n pycuda_3_10 python3 correction_analysis/diagnose_corrections.py

# Step 5: 对比图（CPU）
conda run -n pycuda_3_10 python3 correction_analysis/compare_dqctl_scenes.py
```

### 7.4 添加新场景

1. 在 `configs.py` 中添加场景注册
2. 在 `compute_correction_fields.py` 的 `_SCENE_MAP` 中添加映射
3. 在 `_resolve_source()` 中添加加载器分发
4. 编写 GPU 采集脚本（参考 `collect_vortex.py`）
5. 在 `diagnose_corrections.py` 的 `SCENE_TYPES` 中添加场景
6. 运行采集 → detect_period → replay_fields → diagnose_corrections

---

## 8. 代码结构

```
src/CCD_analysis/
  ccd_knowledge.md                 ← 本文档（唯一知识库）
  configs.py                       ← 场景元数据（统一几何）
  README.md                        ← 快速入口
  Lyu23.md                         ← CCD 方法文献
  ccd/                             ← Round 5 冻结基线（勿改）
  utils/
    resampling.py                  ← POD, CCD, 场加载
    field_translate.py             ← 场平移（备用，不参与默认 pipeline）
    load_vortex_fields.py          ← 瞬态 vortex 场加载
    cfd_interface.py               ← LegacyCelerisLab 封装 (GPU)
  scripts/
    detect_period.py               ← 周期检测 → phase_plan.json
    replay_fields.py               ← 场回放 → fields_aligned.npz
    collect_*.py                   ← GPU 数据采集
  correction_analysis/
    compute_correction_fields.py   ← correction-field 计算
    diagnose_corrections.py        ← 诊断图生成
    compare_dqctl_scenes.py        ← 多场景对比图
    decompose_corrections.py       ← CCD 定量分解（POD + force/action CCD）
    run_signature_line.py          ← signature CCD
    run_15L_correction.py          ← 1.5L 专项分析
    run_zone_ccd.py                ← zone-restricted CCD
    run_steady_metrics.py          ← steady cloak 定量度量
    visualize_action_ccd.py        ← action-CCD mode1 可视化
    process_legacy_steady.py       ← 旧格式加载
  data/
    {scene_id}/{scene_name}/       ← 各场景数据
    resampled/                     ← phase_plan.json
    ccd/                           ← JSON 结果文件
    figures/                       ← PNG 诊断图
    old_data/                      ← 归档（旧报告、旧脚本、旧 resampled 数据）
```

---

## 9. 硬规则

1. **Illusion 只用 o14 模型**（S_DIM=14，`_2U` 系列）。不使用 o12 模型。
2. **`_2U` 表示 S_DIM=14**（2 个额外的目标力通道），不是 2× 速度。U0 始终是 0.01。
3. **action_bias ≠ preset_action**。bias 是 DRL action scaling（illusions=[0,-2,2]），preset 是 FIFO warmup 用的动作数组。
4. **默认物理参数**：u0=0.01, nu=0.004（所有模型一致）。
5. **主分析对象 = dq_ctl**（不是 q_ctl）。
6. **Steady cloak 不要使用 phase-based CCD**——它是稳态问题。
7. **Karman 的物理问题不同**（incoming-street preservation vs target generation），不要硬套 illusion 模板。
8. **所有场景已在采集时统一几何**（pinball 中心 613 px，sensor x=800 px）；`field_translate.py` 仅作备用工具，不参与默认 pipeline。
9. **旧数据格式**（`fields.npz`）：(N, NX, NY)；**新对齐格式**（`fields_aligned.npz`）：(N, NX, NY)；**加载后统一转 (N, NY, NX)**。

---

## 10. 不能写进论文的结论（内部参考，勿引用）

以下陈述看似合理但没有被充分证据支持，不得写进正式论文：
- "Illusion 已证明使用完全不同于 target 的物理机制。"
- "低 overlap 已足以证明 force 通道与 target 正交。"
- "CCD 已经直接识别了壁面涡量生成机制。"
- "far wake 模态就是瞬时力的主载体。"

---

## 11. 未完成工作（2026-06-28 更新）

| 方向 | 状态 | 说明 |
|------|------|------|
| Karman cloak CCD 分析 | 数据已齐，分析延后 | 问题定义不同（distortion compensation） |
| Vortex 数据采集 Bug 修复 | 已完成 | 修复 cylinder order swap（BOTTOM=id4, TOP=id5）+ FIFO warmup 导致涡量消失；重采集 Taylor/Lamb |
| SR-CCD-OID 映射 | 草稿 | 归档 `data/old_data/sr_ccd_oid_mapping.md`，需最终校正 |
| 统一几何后 CCD 重跑 | 已完成 | `correction_ccd_results.json` 已更新（含 1.5L） |
| Vortex 对比图更新 | 已完成 | `compare_dqctl_scenes.py` 已用修正数据重跑 |
| 项目目录清理 + 文档更新 | 已完成 | 移除废弃目录、SI=200 错误数据、旧诊断图 |

---

## 12. Vortex 采集 Bug 排查经验（2026-06-29）

`collect_vortex.py` 中发现了四个独立 bug，按发现顺序：

**Bug 1 — 圆柱顺序对调**
- 训练 env `legacy_env_vortex.py` 添加顺序：front(id3) → TOP(+y, id4) → BOTTOM(-y, id5)
- action 映射：`temp[3]=front, temp[4]=TOP(bias=-4), temp[5]=BOTTOM(bias=+4)`
- 脚本先后把 TOP/BOTTOM 添加顺序写反 → 后两圆柱旋转方向全错
- 症状：Lamb front 剧烈振荡 (std=0.28)，dipole 对称性完全破坏

**Bug 2 — 涡量消失**
- FIFO warmup 在涡加入后跑 150×800=120000 步 → 涡从 x=15 漂出域外 (1280 lu)
- 症状：涡量场只有 pinball 尾流，完全看不到 vortex 结构

**Bug 3（核心）— 观测值归一化顺序错误**
- 训练 env 产出：`obs = [forces/force_norm, sensors/sens_norm]`（force 先）
- 脚本将 channel 和 norm 互换：`[sensors/force_norm, forces/sens_norm]`
- 模型收到不匹配分布的反馈
- 症状：Lamb cross-corr 仅 0.73, Taylor 仅 0.50；后圆柱同向转而非反向

**Bug 4 — fade-in/out 缺失**
- `uni_test.ipynb` 有 25 步渐入 + 25 步渐出到 steady-cloak bias
- 脚本直接给全量 PPO action，无过渡

**最终修正方案**：
1. Cylinder order 匹配训练 env
2. Bias 使用 uni_test 值：[-5, +5]（FIFO），[-5.1, +5.1]（fade target）
3. Obs 归一化：`forces_norm = obs[6:12]/force_norm`，`sens_norm = (obs[0:6]-sens_dev)/sens_norm`，`hstack([forces_norm, sens_norm])`
4. 25-step fade-in / 25-step fade-out

**修正后验证**：

| 场景 | sim | cross-corr | active front mean | dq_ctl RMS |
|------|:---:|:----------:|:-----------------:|:----------:|
| vortex_lamb | 0.946 | 0.974 | 0.007 | 0.146 |
| vortex_taylor | 0.923 | 0.953 | -0.030 | 0.188 |

**其他脚本审计**：`collect_karman.py` 和 `collect_illusion.py` 使用 `build_observation()` 函数，该函数正确实现了 force-first 归一化，无需修改。`collect_vortex.py` 是唯一手动构建 obs 的脚本。

**代码注释**：`collect_vortex.py` 的文件头 docstring 和关键行号均有 BUG HISTORY 和 BUG-FIX 标记。
