# DynamisLab Understanding Notes

## 1. 网格尺寸 (Grid Dimensions)

### 旧版配置
- `config_cuda.json`: X_1U=128, Y_1U=32, Z_1U=1
- `config_flowfield.json`: field_dim_in_U=[10, 16, 1]
- 实际网格 = [128×10, 32×16, 1×1] = **[1280, 512, 1]**

### 新版配置
- `config_lbm_pinball.json`: nx=1280, ny=512 → **完全相同**

所以网格尺寸没有变化。L0=20 是基尺度，但网格的 U 单位 = (128, 32) 是为了对齐 CUDA SM 数。

---

## 2. Re 数定义

**关键：** 用户使用了两种 Re 定义：

| 符号 | 长度尺度 | 公式 | 默认值 |
|------|---------|------|--------|
| `Re_D` | 单个圆柱直径 D=20 | U0·D/ν | 0.01×20/0.004 = **50** |
| `Re` (代码中写) | 2×D = 40 | U0·(2D)/ν | 0.01×40/0.004 = **100** |

- Confirmation report 中的 `Re_D = 50` 对应代码中的 Re100
- 代码内写 re100 系列模型 → 实际物理 Re_D=50
- 上游扰流圆柱直径 = L0×1 = 20 → Re=U0×20/ν=50 (单直径)

---

## 3. 旧 API vs 新 API 关键差异

### 3.1 力的物理含义

**旧 API (`FlowField.run(N, action)`)：**
```
obs[:] = 0
for step in range(N):
    memset(obs_gpu, 0)                # 每步清零 obs_gpu
    step_kernel(...)                   # 每步 force 通过 atomicAdd 累加进 obs_gpu
    obs += obs_gpu (H2D copy)         # 每步读取到 host
obs /= N                               # 除以步数 = 每步平均力
```
→ 旧 `obs` = **每步平均力**

**新 API (`sim.run(N)` / `sim.stepper.step(N, ...)`)：**
```
# stepper 内部不会清零 obs
# 需要用户手动清零
sim.bodies.zero_force_segment_async(stream)  # 清零力/扭矩段
sim.stepper.step(N, ..., stream)
sim.read_force(id) → 累积 N 步后的值
```
→ 新 `sim.read_force()` = **N 步累积力**（需除以 N 得到每步平均）

**对应关系：**
- 旧 `obs` = 新 `sim.read_force(id)` ÷ N
- 旧 `obs[i]` 的 index 顺序 = 传感器在前 + 力在后
- 新 API 分别用 `read_force(id)` 和 `read_sensor(id)` 分开读取

### 3.2 传感器的物理含义

**旧 API：**
- Sensor 值通过 atomicAdd 累加，跟力一样每步清零、host 累积、除以步数
- 所以旧 `obs` 中的 sensor = **每步平均速度**

**新 API：**
- SensorKernel 用 atomicAdd 累加 `ux, uy` 到 `obs_gpu` 的 sensor 段
- `read_sensor(id, normalize=True)` 除以 `sensor_cell_counts[body_id]` （该 sensor 覆盖的格子数）
- **但没有除以步数！**

```python
# 新 API 读取 sensor：
# read_sensor(id) = (sum_{steps} sum_{cells} ux) / cell_count
# 要得到每步平均 = read_sensor(id) / N
```

**所以旧 → 新转换：**
```python
# 旧环境：
# flow_field.run(SAMPLE_INTERVAL, action)
# obs = flow_field.obs  # 已经是每步平均

# 新环境等效：
# sim.bodies.zero_force_segment_async(stream)
# sim.bodies.zero_sensor_segment_async(stream)
# sim.run(SAMPLE_INTERVAL)  # 或 stepper.step(SAMPLE_INTERVAL)
# fx_per_step = sim.read_force(id)[0] / SAMPLE_INTERVAL
# fy_per_step = sim.read_force(id)[1] / SAMPLE_INTERVAL
# ux_per_step = sim.read_sensor(id)[0] / SAMPLE_INTERVAL
# uy_per_step = sim.read_sensor(id)[1] / SAMPLE_INTERVAL
```

### 3.3 Obs 布局对比

**旧 API obs 数组（以 cloak env 为例，7 个 objects）:**
```
obs[0:6]   = sensor0_ux, sensor0_uy, sensor1_ux, sensor1_uy, sensor2_ux, sensor2_uy
obs[6:12]  = cylinder0_fx, cylinder0_fy, cylinder1_fx, cylinder1_fy, cylinder2_fx, cylinder2_fy
obs[12:14] = dist_cylinder_fx, dist_cylinder_fy (扰流圆柱)
```
objects 添加顺序：传感器 → 圆柱体。obs 是先 sensor 段（6个值），后 cylinder 段（每个 cyL 2 个值）。

**新 API：**
- `sim.read_force(body_id)` → `[fx, fy]` 按 body_id 读取
- `sim.read_sensor(body_id)` → `[ux, uy]` 按 body_id 读取
- `sim.read_torque(body_id)` → `[tz]` 按 body_id 读取

### 3.4 Checkpoint/恢复

**旧 API：**
```python
flow_field.get_ddf()      # host ← gpu
flow_field.save_ddf()      # 保存到 host 内存
flow_field.restore_ddf()   # host 内存恢复
flow_field.apply_ddf()     # host → gpu
```

**新 API：**
```python
sim.snapshot()         # 内存快照
sim.restore()          # 恢复
# 或
sim.save_checkpoint(path)   # HDF5 文件
sim.load_checkpoint(path)   # 从文件恢复
```

### 3.5 动作施加方式

**旧 API：**
```python
# 全部 action 打包成一个数组，按 object 顺序排列
action_array = np.zeros(n_objects, dtype=DATA_TYPE)
action_array[4:7] = ...  # 圆柱体的 omega 值
flow_field.run(N, action_array)
# 内部有指数平滑：action_pinned = (1-weight)*action_pinned + weight*action_target
```

**新 API：**
```python
sim.set_body(body_id, omega=value)  # 每个圆柱体单独设置
sim.run(N)
# 没有内置动作平滑（通过 ActionSmoother 可以实现）
```

### 3.6 数值错误检测

**旧 API：**
```python
flow_field.has_numeric_error()
flow_field.last_error_flag
```

**新 API：**
需要手动实现检查。

---

## 4. 五种场景详解

### 4.1 Karman Cloak (模型名: d1a3o12_re系列)

**目标：** 上游 2D 扰流圆柱产生涡街，控制 pinball 使下游传感器信号跟无 pinball 时一致

**几何布局：**
```
上游扰流圆柱：x=10*L0, y=中心, r=L0 (=200, 240, 20)
Pinball前：  x=30*L0, y=中心, r=L0/2 (=600, 240, 10)
Pinball下：  x=31.3*L0, y=中心-0.75*L0, r=L0/2 (=626, 225, 10)
Pinball上：  x=31.3*L0, y=中心+0.75*L0, r=L0/2 (=626, 255, 10)
Sensor上：   x=40*L0, y=中心+2*L0, r=L0/4 (=800, 280, 5)
Sensor中：   x=40*L0, y=中心, r=L0/4 (=800, 240, 5)
Sensor下：   x=40*L0, y=中心-2*L0, r=L0/4 (=800, 200, 5)
```

**目标信号:** 只有上游扰流圆柱（无 pinball）时，sensor 的时间序列
**观测:** `forces[6]+sens[6]` = 6 个 force 值 + 6 个 sensor 值 = 12 维
**动作缩放:** `action×8U₀ + [0, -4U₀, 4U₀]` (=0,0 代表 -4,4 的偏置动作)
**Reward:** `min(0.3×exp(-|Cd×20|) + 0.4×exp(-|Cl×80|) + 0.3×exp(-10×|sim-1|), 1.0)`
**SAMPLE_INTERVAL:** 800
**CONV_LEN:** 30
**检查点流程:**
1. 初始化只有扰流圆柱+3传感器的环境 → 运行稳定 → 记录150步目标信号
2. 添加 pinball → 稳定 → 保存 DDF(无控制状态)
3. 用零动作运行 150 步 → 记录 obs 到 fifo → 计算 norm
4. 用预设动作(-4,4)运行 150 步 → 记录 obs → 保存 save_states
5. reset=恢复DDF + 恢复fifo

### 4.2 Erase (模型名: d1a3o12_250729_250326_erase系列)

**目标：** 跟 Karman Cloak 场景基本一致，但目标是让下游流场恢复到"入口流状态"（干净来流），即抹除扰流圆柱尾迹

**关键区别：** 
- 目标信号 = 入口均匀流的均值（基本为零方差）
- Reward 主要看传感器与均值的偏差
- **这是未完全实现的部分**
- 动作缩放: `×8U₀ + [0, -8U₀, 8U₀]`

### 4.3 Illusion (模型名: d1a3o14_250525_imit系列)

**目标：** 上游干净来流，控制 pinball 使其下游流场跟指定直径的单个圆柱一致

**几何差异：**
- 没有上游扰流圆柱
- 目标圆柱位置不同（x=20*L0 或 x=31*L0，取决于场景）
- Sensor 位置可能调整（x=30*L0 而不是 40×L0，为了贴近目标圆柱等效距离）
- 目标信号通过 harmonics 分析（FFT 提取频域特征）进行重构

**观测:** 12 维 (forces) + 2 维 (target_cd, target_cl) = 14 维
**Reward:** `min(0.3×exp(-|(Cd-Cd_target)×10|) + 0.3×exp(-|(Cl-Cl_target)×10|) + 0.4×exp(-10×|sim-1|), 1.0)`
- 力的比较是和目标圆柱的谐波重构值比较

**IMPORTANT CORRECTION (2026-06-12):** "2U" in model name means S_DIM=14 (2 extra target force dimensions), NOT 2x velocity. ALL models train with u0=0.01. SAMPLE_INTERVAL varies by diameter:
- 0.75L: `..._075L_2U_400S` → S=400
- 1.0L: `..._1L_2U_600S` → S=600  
- 1.5L: `..._15L_2U` → no S suffix = default S=800
- nu=0.004 for all (no Vis suffix = default) — confirmed via sweep

### 4.4 Vortex (模型名: vortex_lamb / vortex_taylor)

**目标：** 在初始流场中加入涡量（Lamb dipole 或 Taylor vortex），控制 pinball 使下游信号跟无 pinball 时一致

**关键特征：**
- 要正确使用 `add_vortex` 进行初始化
- Dipole (Lamb) 强度较大 (0.5*U0)，Monopole (Taylor) 强度较小 (0.03*U0)
- Vortex 在保存 DDF 之后添加，随流场演化
- 有 `MAX_STEPS=150` 的终止条件（因为是 transient 事件）
- 动作缩放: `×4U₀ + [0, -4U₀, 4U₀]`
- Reward: `min(0.2×exp(-|Cd×20|) + 0.3×exp(-|Cl×80|) + 0.5×exp(-10×|sim-1|), 1.0)`

### 4.5 Reduced Obs (模型名: d1a3o12_250421系列)

**目标：** 同 Karman Cloak，但逐步减少观测维度
**观测:** 从 12 → 9 → 5 → 3 → 2 递减，观察模型是否能适应

---

## 5. Norm 采集过程（关键！）

**所有旧环境共有的模式：**

```python
# 环境初始化最后阶段：

# Step 1: 用零动作运行 FIFO_LEN 步，记录 obs
for i in range(FIFO_LEN):
    flow_field.run(SAMPLE_INTERVAL, zero_action)
    fifo_states.append(flow_field.obs[sensor_select])

# Step 2: 从 fifo 计算 norm
temp_states = np.array(fifo_states)
force_norm_fact = 6 * max(|forces|)    # 力归一化因子
for i in range(6):
    sens_deviation[i] = mean(sensor[i])       # 均值
    sens_norm_fact[i] = 5 * max(|sensor[i] - mean|)  # 波动范围×5

# Step 3: 恢复 DDF，用预设动作运行 FIFO_LEN 步
flow_field.apply_ddf()
for i in range(FIFO_LEN):
    flow_field.run(SAMPLE_INTERVAL, init_action)
    fifo_states.append(...)
save_states = fifo_states.copy()
```

这个 norm 值是**模型训练时就固化**的，**推理时也必须使用完全相同的值**。旧版环境在初始化时自动计算，新环境需要为每个场景预计算和存储这些 norm 值。

---

## 6. uni_test.ipynb 流程解析 (最重要参考)

这个 notebook 是一个连续的、视频式展示，包含以下序列：

### Phase 0: 初始化和元数据采集
1. 加载所有模型
2. 创建 DummyEnv 用于加载模型（只是一个匹配 obs/action space 的空壳）
3. 设置 CUDA context，创建 `FlowField`
4. 设置 `meta_*` 对象存储 norm 值

### Phase 1: 目标信号录制
1. **Steady 目标**：3 sensors × 150步 → 记录时序均值（干净来流）
2. **Dipole (Lamb)** 目标：restore DDF → add_vortex Lamb → 150步 → 记录
3. **Monopole (Taylor)** 目标：restore DDF → add_vortex Taylor → 150步 → 记录
4. **Illusion 目标**（1.0L 圆柱）：新 FlowField → 单圆柱 x=31*L0, r=1*L0 → 3 sensors → 150步 × 800 采样 → 记录 8 维 obs + harmonics 分析
5. **Illusion 目标**（0.75L 圆柱）：类似，SAMPLE_INTERVAL=400
6. **Illusion 目标**（1.5L 圆柱）：类似，SAMPLE_INTERVAL=800
7. **Karman 目标信号**：pinball 恢复 + 扰流圆柱 → 3 sensors → 150步 × 800 采样 → 记录 sensor+force obs

### Phase 2: Pinball + Norm 采集
- 创建 pinball → 运行到稳定 → save DDF
- 零动作 150步 → 计算 norm
- 预设动作 150步 → 记录 save_states

### Phase 3: Norm 采集（各个场景）
- 跟 Phase 2 类似，但针对各个特定场景恢复 DDF 后分别采集 norm

### Phase 4: 推理运行（控制演示序列）
1. **No Control (nc)** 基线：加载 pinball DDF → 零动作 100步 × 1000采样 → 保存场
2. **Steady Cloaking**: 从 nc 继续 → 逐步切换到 cloak 动作（75步渐入+保持）
3. **Dipole Cloaking**: restore DDF → add Lamb vortex → 用 cloak_lamb 模型推理 → 25步渐入/25步淡出 → 保存场
4. **Monopole Cloaking**: restore DDF → add Taylor vortex → 用 cloak_taylor 模型推理 → 类似渐入淡出
5. **Illusion (1L)**: restore DDF → 用 illusion_1L 模型推理 → 10步渐入 → 200步保持
6. **Illusion (0.75L)**: 类似
7. **Illusion (1.5L)**: 类似
8. **Karman NC + Cloak**: restore Karman场景DDF → 200步无控制 → 切换到 cloak_re100 模型推理
9. **Frame 合成 → ffmpeg 视频**

关键点：
- **场景间过渡**通过 `restore/apply DDF` 实现
- **动作渐入/淡出**用 `linear interpolation in action space`
- 场数据存在 `.dat` 文件（Tecplot格式），用 `save_field()` 保存
- 所有推理都用 `deterministic=True`

---

## 7. 旧 API 动作数组的索引规则

旧 `FlowField.run()` 的 `action_target` 数组按 object 添加顺序排列：

```
| Sensors 的 action slot | Cylinders 的 action slot |
| sensor0, sensor1, ... | cylinder0, cylinder1, ... |
```

每个 object 的 action slot 有 3×DIM 个值（DIM=2 时=6），但 LBM 中 sensor 只用第一个值（实际上被忽略），cylinder 用最后一个值（omega）。

```
action[0] = sensor0 (ignored)
action[1] = sensor0 (ignored) 
action[2] = sensor0 (ignored)
action[3] = sensor1 (ignored)
action[4] = sensor1 (ignored)  ← 对于 cloak env，这里放 cylinder0 的 omega
action[5] = sensor1 (ignored)
action[6] = cylinder0 omega (disturbance cylinder for cloak)
action[7] = cylinder1 omega (front pinball)  ← 注意！对象索引顺序取决于添加顺序
```

实际各环境设置 action 的方式：
```python
# cloak env (7 objects: 3 sensors + 1 dist_cyl + 3 pinball):
temp = np.zeros(7, dtype=DATA_TYPE)
temp[4:7] = (action*8 + [0,-4,4]) * U0  # action[4]=front omega, [5]=bottom, [6]=top

# erase env: 
temp[4:7] = (action*8 + [0,-8,8]) * U0

# imit env (6 objects: 3 sensors + 3 pinball):
temp[3:6] = (action*8 + [0,-2,2]) * U0

# vortex env:
temp[3:6] = (action*4 + [0,-4,4]) * U0
```

注意 `temp[4:7]` 是从 index 4 开始取 3 个值。这是因为添加顺序是：
1. sensor0 (id=0)
2. sensor1 (id=1) 
3. sensor2 (id=2)
4. dist_cylinder (id=3) — 旧 env 里添加上游扰流圆柱后固定为 0
5. pinball_front (id=4)
6. pinball_bottom (id=5)
7. pinball_top (id=6)

---

## 8. 重写注意事项

### 8.1 力和传感器的归一化
因为新 API 返回的是 N 步累积值（需要除以步数），所以 norm 的物理含义需要重新标定。

### 8.2 动作缩放关系
旧环境动作缩放矩阵（从归一化 [-1,1] action 到物理 U0 倍数的 omega）：

| 场景 | 公式 (θ=action) | 物理范围 |
|------|----------------|---------|
| Cloak | `action×8 + [0,-4,4]` | front: [-8,8], bottom:[-12,4], top:[-4,12] |
| Erase | `action×8 + [0,-8,8]` | front: [-8,8], bottom:[-16,0], top:[0,16] |
| Illusion | `action×8 + [0,-2,2]` | front: [-8,8], bottom:[-10,6], top:[-6,10] |
| Vortex | `action×4 + [0,-4,4]` | front: [-4,4], bottom:[-8,0], top:[0,8] |

然后乘以 U0=0.01 得到实际的晶格 omega 值。

### 8.3 边界条件
旧配置是 parabolic inlet + no-slip walls（因为 legacy solver 不支持 free-slip）
新 pinball 配置是 parabolic inlet + **bounce_back** walls（即 no-slip，跟旧版一致）
注意新配置 `config_lbm_pinball.json` 的 `y_wall_bc` 是 `bounce_back`，验证配置 `run_kan99b` 是 `free_slip`，要统一。

### 8.4 重写策略
1. **base_env.py**: 封装共享逻辑（Simulation 创建、几何布局、checkpoint 管理、norm 存储、DTW 工具）
2. **每个场景继承 base_env**: 只覆盖 reward、目标信号、动作缩放
3. **train/*.py**: 跟旧版一致的 PPO 训练循环（Sin 激活、SB3 PPO、TensorBoard）
4. **eval/*.py**: 推理脚本，对应 uni_test 功能，用配置文件驱动场景序列
5. **configs/**: 存储每个场景的 norm 值、几何参数、采样参数
6. **models/**: 从旧版复制训练好的 .zip 模型（在新 API 推理中使用）

### 8.5 与旧环境的数值一致性验证方法
新环境在完全实现后，必须用旧训练好的模型在新 env 上跑推理，对比：
- 传感器时序信号（旧 `obs` vs 新 `read_sensor/SAMPLE_INTERVAL`）
- 力信号（旧 `obs` vs 新 `read_force/SAMPLE_INTERVAL`）
- 场数据（旧 `save_field` vs 新 `get_macroscopic`）
