feat: eval benchmark + cloud-trained models replace train/
- eval/: 统一推理评估框架 (infer_train.py, infer_reproduce.py, viz_flow.py, viz_signals.py, generate_report.py, run_all.sh, scene_manifest.py) - train/: 替换为云端正式训练产出,覆盖 11 个场景 - Karman: Re100 (5 seeds), Re60/200/400 transfer - VarDist: d075/d15/d2 scratch - Illusion: 0.75L/1.0L/1.5L/2.0L scratch - train_karman.py: 新增 --resume-from 断点续训支持 - calibrations: 17 组标定数据 (kar/ill 命名规范) - scripts/: 7 个自动化训练脚本 - 清理旧 calibrations/shell scripts/visualize Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,206 @@
|
|||||||
|
# Eval Benchmark — 推理评估与流场分析
|
||||||
|
|
||||||
|
> 对 V5 Train 模型和 Legacy 旧模型进行全面推理采样和可视化对比。
|
||||||
|
> 复用 train env 保证与训练流程完全一致,不使用 skeleton injection。
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/drl_pinball/eval/
|
||||||
|
README.md # 本文件 (自我审查清单 + 复习流程)
|
||||||
|
__init__.py
|
||||||
|
scene_manifest.py # 所有 scene 的配置单点真理源
|
||||||
|
infer_train.py # V5 Train 模型推理引擎 (GPU 0)
|
||||||
|
infer_reproduce.py # Reproduce 模型推理引擎 (GPU 1)
|
||||||
|
viz_flow.py # 涡量图跨场景拼图 (统一 colormap)
|
||||||
|
viz_signals.py # obs/action/DTW/FFT 时序可视化
|
||||||
|
generate_report.py # 汇总表格 + 柱状图
|
||||||
|
run_all.sh # 主启动器 (GPU 调度)
|
||||||
|
output/
|
||||||
|
train/{scene}/ # signals.npz, vorticity_*.png, metrics.json, all_seeds.json
|
||||||
|
reproduce/{scene}/ # 同上
|
||||||
|
reports/
|
||||||
|
comparison_table.md # 汇总对比表
|
||||||
|
summary_dtw_barchart.png # DTW 柱状图
|
||||||
|
vorticity_panels/ # 跨场景拼图
|
||||||
|
signal_plots/ # 诊断信号图
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
conda activate pycuda_3_10
|
||||||
|
cd src/drl_pinball/eval
|
||||||
|
|
||||||
|
# === 全量跑 (双 GPU) ===
|
||||||
|
bash run_all.sh
|
||||||
|
|
||||||
|
# === 单场景测试 ===
|
||||||
|
python infer_train.py --scene re100 --device-id 0
|
||||||
|
python infer_train.py --scene illusion_1L --device-id 0
|
||||||
|
|
||||||
|
# === 仅跑报告生成 (已有 output 数据) ===
|
||||||
|
python generate_report.py
|
||||||
|
python viz_signals.py --side train
|
||||||
|
python viz_flow.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## GPU 调度策略
|
||||||
|
|
||||||
|
```
|
||||||
|
时间轴:
|
||||||
|
|
||||||
|
GPU0: [re100] [ill_1L] [ill_15L] [ill_075L] --120s--> [re60] --120s--> [re200] --120s--> [re400]
|
||||||
|
(config A: karman_2000x600, nu=0.004) (config B: re60) (config C: re200) (config D: re400)
|
||||||
|
|
||||||
|
GPU1: --120s--> [karman_cloak] [steady_cloak] [ill_075L] [ill_1L] [ill_15L] [vortex_lamb] [vortex_taylor]
|
||||||
|
(全部 config_pinball.json, 无额外编译)
|
||||||
|
```
|
||||||
|
|
||||||
|
- GPU 1 比 GPU 0 晚 120 秒启动,避免两个不同 config 同时触发编译
|
||||||
|
- 同 config 内 scene 连续运行,无 recompilation 开销
|
||||||
|
- 每个 scene 约 5-8 分钟(warmup + FIFO + 360 步推理)
|
||||||
|
|
||||||
|
## 场景覆盖率
|
||||||
|
|
||||||
|
### Train pipeline (GPU 0)
|
||||||
|
| Scene | Config | Seeds |
|
||||||
|
|-------|--------|-------|
|
||||||
|
| re100_karman | karman_2000x600 (Re=100) | 41-45, 539439 |
|
||||||
|
| transfer_re60 | karman_2000x600_re60 | 41, 43 |
|
||||||
|
| transfer_re200 | karman_2000x600_re200 | 41, 43, 45 |
|
||||||
|
| transfer_re400 | karman_2000x600_re400 | 43 |
|
||||||
|
| illusion_075L | karman_2000x600 | 41 |
|
||||||
|
| illusion_1L | karman_2000x600 | 41, 43 |
|
||||||
|
| illusion_15L | karman_2000x600 | 41, 43 |
|
||||||
|
|
||||||
|
### Reproduce pipeline (GPU 1)
|
||||||
|
| Scene | Config | 模型 |
|
||||||
|
|-------|--------|------|
|
||||||
|
| karman_cloak | pinball (1280x512) | d1a3o12_re100 |
|
||||||
|
| steady_cloak | pinball (1280x512) | open-loop [0,-5.1,5.1] |
|
||||||
|
| illusion_075L | pinball (1280x512) | d1a3o14_250525_imit_075L_2U_400S |
|
||||||
|
| illusion_1L | pinball (1280x512) | d1a3o14_250525_imit_1L_2U_600S |
|
||||||
|
| illusion_15L | pinball (1280x512) | d1a3o14_250525_imit_15L_2U |
|
||||||
|
| vortex_lamb | pinball (1280x512) | vortex_lamb |
|
||||||
|
| vortex_taylor | pinball (1280x512) | vortex_taylor |
|
||||||
|
|
||||||
|
## 核心设计原则
|
||||||
|
|
||||||
|
1. **复用 train env 而非重构 CFD** — KarmanCloakEnv / IllusionCloakEnv 已封装 norm、calibration、DTW、snapshot/restore
|
||||||
|
2. **PPO.load() + VecNormalize.load()** — SB3 标准 API,不使用 skeleton injection
|
||||||
|
3. **统一涡量 [-0.03, 0.03] colormap** — 所有场景跨可比
|
||||||
|
4. **每步全量时序** — sensors(6) + forces(6) + actions(3) + rewards 存入 signals.npz
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 自我审查清单
|
||||||
|
|
||||||
|
### 启动前
|
||||||
|
- [ ] `conda activate pycuda_3_10`
|
||||||
|
- [ ] 所有 `best_model.zip` 和 `vec_normalize.pkl` 路径存在 (见 scene_manifest.py)
|
||||||
|
- [ ] 所有 `calibration.json` 和 `target.npy` 存在 (train/calibrations/)
|
||||||
|
- [ ] `nvidia-smi` 显示两个 GPU 空闲
|
||||||
|
- [ ] 无僵尸 pycuda 进程 (`pkill -f pycuda` 后等待 3 分钟)
|
||||||
|
|
||||||
|
### Phase 1 — Train 推理
|
||||||
|
- [ ] `python infer_train.py --scene re100 --device-id 0` 单场景测试通过
|
||||||
|
- [ ] 输出的 `signals.npz` 形状: (360,6) sensors, (360,6) forces, (360,3) actions
|
||||||
|
- [ ] rewards 非 NaN,DTW sim 在 [0,1] 内
|
||||||
|
- [ ] Zero-action baseline 的 DTW 明显低于 controlled
|
||||||
|
|
||||||
|
### Phase 2 — Reproduce 推理
|
||||||
|
- [ ] `python infer_reproduce.py --scene karman_cloak --device-id 1` 测试通过
|
||||||
|
- [ ] 旧模型 PPO.load() 不报错
|
||||||
|
- [ ] Vortex 场景 add_vortex 正确初始化(check target.npz 非全零)
|
||||||
|
|
||||||
|
### Phase 3 — 涡量图
|
||||||
|
- [ ] `python viz_flow.py` 产出全部拼图
|
||||||
|
- [ ] vorticity range 统一 [-0.03, 0.03]
|
||||||
|
- [ ] 圆柱边界清晰可见
|
||||||
|
- [ ] target vs controlled vs zero 三栏对比正常
|
||||||
|
|
||||||
|
### Phase 4 — 时序图
|
||||||
|
- [ ] `python viz_signals.py --side train` 产出全部诊断图
|
||||||
|
- [ ] FFT 频谱确认 controlled 与 target Strouhal 匹配
|
||||||
|
- [ ] Action 时序 policy 收敛到稳态 (非振荡)
|
||||||
|
|
||||||
|
### Phase 5 — 汇总报告
|
||||||
|
- [ ] `python generate_report.py` 产出 comparison_table.md
|
||||||
|
- [ ] Train DTW >= Reproduce DTW (或合理的 gap 原因)
|
||||||
|
|
||||||
|
### 最终验收
|
||||||
|
- [ ] 7 train + 7 reproduce 目录完整
|
||||||
|
- [ ] 每个目录: signals.npz, vorticity_controlled.png, vorticity_target.png, vorticity_zero.png, metrics.json, all_seeds.json
|
||||||
|
- [ ] reports/ 下: comparison_table.md, summary_dtw_barchart.png, vorticity_panels/, signal_plots/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 复习流程 (中断后快速恢复)
|
||||||
|
|
||||||
|
按顺序读取以下文件重建上下文:
|
||||||
|
|
||||||
|
1. **本文件** — 确认当前进度和 GPU 调度图
|
||||||
|
|
||||||
|
2. **`eval/scene_manifest.py`** — 所有 scene 配置
|
||||||
|
```bash
|
||||||
|
python -c "from drl_pinball.eval.scene_manifest import TRAIN_SCENES; \
|
||||||
|
[print(s['scene_id'], s['si']) for s in TRAIN_SCENES]"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **`train/TRAIN_KNOWLEDGE.md`** sections 1-3 — V5 训练管线回顾
|
||||||
|
|
||||||
|
4. **验证环境可用**:
|
||||||
|
```bash
|
||||||
|
ls eval/output/train/*/metrics.json 2>/dev/null # 已完成的 scene
|
||||||
|
ls eval/output/reproduce/*/metrics.json 2>/dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **从断点继续**:
|
||||||
|
```bash
|
||||||
|
# 单 scene:
|
||||||
|
python infer_train.py --scene re200 --device-id 0
|
||||||
|
# 或 Kill pycuda 进程后重新启动显卡:
|
||||||
|
pkill -f pycuda; sleep 180; bash run_all.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见问题排查
|
||||||
|
|
||||||
|
| 问题 | 解决方案 |
|
||||||
|
|------|---------|
|
||||||
|
| `cuInit failed` | GPU 被占用。`pkill -f pycuda; sleep 180` 后重试 |
|
||||||
|
| `best_model.zip not found` | 训练未完成。查看 `train/output/{case}/models/` |
|
||||||
|
| `target.npy not found` | calibration 缺失。重新运行 `calibrate.py` |
|
||||||
|
| DTW sim = -1.0 (reproduce) | 正常。Reproduce 不计算 DTW,由 report 统一处理 |
|
||||||
|
| 涡量图空白 | 检查 `render_vorticity_field` 的 `vmin/vmax` 或 cylinders 坐标 |
|
||||||
|
| VecNormalize.pkl 不存在 | 训练时未保存。用 `best_model.zip` 同目录的 `vec_normalize.pkl` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 代码关键点
|
||||||
|
|
||||||
|
### infer_train.py 的关键差异点
|
||||||
|
|
||||||
|
- 每个 seed 独立 `PPO.load()` + `VecNormalize.load()`,不是共用 skeleton
|
||||||
|
- 推理时 `SymmetryAugmentWrapper(prob=0.0)`(deterministic mode)
|
||||||
|
- Raw obs 从 `env._read_obs()` 提取,通过 wrapper chain 穿透
|
||||||
|
- Target vorticity 单独创建 Simulation 生成(dist cylinder only)
|
||||||
|
|
||||||
|
### infer_reproduce.py 的关键差异点
|
||||||
|
|
||||||
|
- 1280x512 网格,parabolic inlet,bounce-back walls
|
||||||
|
- Legacy norm 从 `SR_analysis/data/*/norm.json` 加载
|
||||||
|
- 旧动作缩放: `norm * scale + bias` (各 scene 不同)
|
||||||
|
- Illusion 模型 obs_dim=14(含 target_cd, target_cl,通过 harmonics 重构)
|
||||||
|
- Vortex 用 `add_vortex()` 在不同 x 位置添加涡量
|
||||||
|
|
||||||
|
### 输出文件命名
|
||||||
|
|
||||||
|
| 文件 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| `signals.npz` | sensors (N,6), forces (N,6), actions (N,3), rewards (N,) |
|
||||||
|
| `vorticity_controlled.png` | DRL 控制后的 final 时刻涡量 |
|
||||||
|
| `vorticity_target.png` | 目标状态 (disturbance only / target cylinder) |
|
||||||
|
| `vorticity_zero.png` | 零动作 baseline (无控制) |
|
||||||
|
| `metrics.json` | DTW sim, reward, action stats |
|
||||||
|
| `all_seeds.json` | 每个 seed 的详细评分 |
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# eval package
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate master comparison report from eval outputs.
|
||||||
|
|
||||||
|
Reads metrics.json from all output/{train,reproduce}/ directories,
|
||||||
|
produces:
|
||||||
|
1. Master comparison table (markdown)
|
||||||
|
2. Summary bar chart (train vs reproduce DTW similarity)
|
||||||
|
3. Action statistics summary
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
conda run -n pycuda_3_10 python generate_report.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use("Agg")
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
_OUT_BASE = Path(__file__).resolve().parent / "output"
|
||||||
|
_REPORT_DIR = _OUT_BASE / "reports"
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg: str) -> None:
|
||||||
|
print(f"[report] {msg}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def load_metrics(scene_dir: Path) -> Dict[str, Any]:
|
||||||
|
p = scene_dir / "metrics.json"
|
||||||
|
if p.exists():
|
||||||
|
with open(p) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def load_all_seeds(scene_dir: Path) -> List[Dict]:
|
||||||
|
p = scene_dir / "all_seeds.json"
|
||||||
|
if p.exists():
|
||||||
|
with open(p) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tables
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def generate_comparison_table() -> str:
|
||||||
|
"""Generate markdown comparison table for overlapping scenes."""
|
||||||
|
overlap = [
|
||||||
|
("re100_karman", "karman_cloak", "Karman Cloak Re100"),
|
||||||
|
("illusion_075L", "illusion_075L", "Illusion 0.75L"),
|
||||||
|
("illusion_1L", "illusion_1L", "Illusion 1.0L"),
|
||||||
|
("illusion_15L", "illusion_15L", "Illusion 1.5L"),
|
||||||
|
]
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
lines.append("# Master Comparison Table")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("| Scene | Side | Best Seed | DTW sim | Reward | "
|
||||||
|
"aF mean | aB mean | aT mean | r_cd | r_cl | r_sim |")
|
||||||
|
lines.append("|-------|------|-----------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|")
|
||||||
|
|
||||||
|
for trn_key, rep_key, name in overlap:
|
||||||
|
m_trn = load_metrics(_OUT_BASE / "train" / trn_key)
|
||||||
|
m_rep = load_metrics(_OUT_BASE / "reproduce" / rep_key)
|
||||||
|
|
||||||
|
dtw_t = _fmt(m_trn.get("dtw_sim_v5") or m_trn.get("sim_raw_mean"))
|
||||||
|
dtw_r = _fmt(m_rep.get("dtw_sim_v5") or m_rep.get("sim_raw_mean"))
|
||||||
|
|
||||||
|
# Train row
|
||||||
|
lines.append(
|
||||||
|
f"| {name} | Train | {m_trn.get('best_seed', '-')} | "
|
||||||
|
f"{dtw_t} | {_fmt(m_trn.get('reward_mean'))} | "
|
||||||
|
f"{_fmt(m_trn.get('aF_mean'))} | {_fmt(m_trn.get('aB_mean'))} | "
|
||||||
|
f"{_fmt(m_trn.get('aT_mean'))} | "
|
||||||
|
f"{_fmt(m_trn.get('r_cd_mean'))} | {_fmt(m_trn.get('r_cl_mean'))} | "
|
||||||
|
f"{_fmt(m_trn.get('r_sim_mean'))} |"
|
||||||
|
)
|
||||||
|
# Reproduce row
|
||||||
|
lines.append(
|
||||||
|
f"| {name} | Reproduce | - | "
|
||||||
|
f"{dtw_r} | - | "
|
||||||
|
f"- | - | - | - | - | - |"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Train-only scenes
|
||||||
|
train_only = ["transfer_re60", "transfer_re200", "transfer_re400"]
|
||||||
|
lines.append("")
|
||||||
|
lines.append("## Train Only (Cross-Re Transfer)")
|
||||||
|
lines.append("| Scene | Best Seed | Reward | "
|
||||||
|
"r_cd | r_cl | r_sim | sim_raw |")
|
||||||
|
lines.append("|-------|-----------|---:|-----:|-----:|-----:|---:|")
|
||||||
|
for trn_key in train_only:
|
||||||
|
m = load_metrics(_OUT_BASE / "train" / trn_key)
|
||||||
|
lines.append(
|
||||||
|
f"| {trn_key} | {m.get('best_seed', '-')} | "
|
||||||
|
f"{_fmt(m.get('reward_mean'))} | "
|
||||||
|
f"{_fmt(m.get('r_cd_mean'))} | {_fmt(m.get('r_cl_mean'))} | "
|
||||||
|
f"{_fmt(m.get('r_sim_mean'))} | {_fmt(m.get('sim_raw_mean'))} |"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reproduce-only
|
||||||
|
repro_only = ["steady_cloak", "vortex_lamb", "vortex_taylor"]
|
||||||
|
lines.append("")
|
||||||
|
lines.append("## Reproduce Only")
|
||||||
|
lines.append("| Scene | Notes |")
|
||||||
|
lines.append("|-------|-------|")
|
||||||
|
for r_key in repro_only:
|
||||||
|
lines.append(f"| {r_key} | open-loop / vortex |")
|
||||||
|
|
||||||
|
# Per-scene all-seeds detail
|
||||||
|
lines.append("")
|
||||||
|
lines.append("## Per-Scene Seed Details (Train)")
|
||||||
|
for trn_key in ["re100_karman", "transfer_re60", "transfer_re200",
|
||||||
|
"transfer_re400", "illusion_075L", "illusion_1L", "illusion_15L"]:
|
||||||
|
seeds = load_all_seeds(_OUT_BASE / "train" / trn_key)
|
||||||
|
if not seeds:
|
||||||
|
continue
|
||||||
|
lines.append(f"\n### {trn_key}")
|
||||||
|
lines.append("| Seed | Reward | r_cd | r_cl | r_sim | sim_raw | Time (s) |")
|
||||||
|
lines.append("|------|--------|------|------|-------|---------|---------:|")
|
||||||
|
for s in sorted(seeds, key=lambda x: -x.get("reward", -float("inf"))):
|
||||||
|
lines.append(
|
||||||
|
f"| {s['seed']} | {_fmt(s.get('reward'))} | "
|
||||||
|
f"{_fmt(s.get('r_cd'))} | {_fmt(s.get('r_cl'))} | "
|
||||||
|
f"{_fmt(s.get('r_sim'))} | {_fmt(s.get('sim_raw'))} | "
|
||||||
|
f"{_fmt_int(s.get('dt_sec'))} |"
|
||||||
|
)
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt(v, precision=4):
|
||||||
|
if v is None or v == -1.0:
|
||||||
|
return "-"
|
||||||
|
if isinstance(v, float):
|
||||||
|
return f"{v:.{precision}f}"
|
||||||
|
return str(v)
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt_int(v):
|
||||||
|
if v is None:
|
||||||
|
return "-"
|
||||||
|
return f"{int(v)}"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Bar chart
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def generate_dtw_barchart() -> None:
|
||||||
|
overlap = [
|
||||||
|
("re100_karman", "karman_cloak", "Karman Cloak"),
|
||||||
|
("illusion_075L", "illusion_075L", "Illusion 0.75L"),
|
||||||
|
("illusion_1L", "illusion_1L", "Illusion 1L"),
|
||||||
|
("illusion_15L", "illusion_15L", "Illusion 1.5L"),
|
||||||
|
]
|
||||||
|
|
||||||
|
names = []
|
||||||
|
trn_vals = []
|
||||||
|
rep_vals = []
|
||||||
|
for trn_key, rep_key, name in overlap:
|
||||||
|
m_trn = load_metrics(_OUT_BASE / "train" / trn_key)
|
||||||
|
m_rep = load_metrics(_OUT_BASE / "reproduce" / rep_key)
|
||||||
|
t = m_trn.get("dtw_sim_v5") or m_trn.get("sim_raw_mean", 0)
|
||||||
|
r = m_rep.get("dtw_sim_v5") or m_rep.get("sim_raw_mean", 0)
|
||||||
|
names.append(name)
|
||||||
|
trn_vals.append(t if t and t > 0 else 0)
|
||||||
|
rep_vals.append(r if r and r > 0 else 0)
|
||||||
|
|
||||||
|
fig, ax = plt.subplots(figsize=(10, 5))
|
||||||
|
x = np.arange(len(names))
|
||||||
|
w = 0.35
|
||||||
|
ax.bar(x - w / 2, rep_vals, w, label="Reproduce (legacy)", color="#d62728")
|
||||||
|
ax.bar(x + w / 2, trn_vals, w, label="Train (V5)", color="#1f77b4")
|
||||||
|
ax.set_xticks(x); ax.set_xticklabels(names, rotation=15, ha="right")
|
||||||
|
ax.set_ylabel("DTW Similarity"); ax.set_ylim(0, 1.05)
|
||||||
|
ax.set_title("Train vs Reproduce - DTW Similarity")
|
||||||
|
ax.legend(); ax.grid(axis="y", alpha=0.3)
|
||||||
|
for i, (tv, rv) in enumerate(zip(trn_vals, rep_vals)):
|
||||||
|
if tv > 0: ax.text(i + w / 2, tv + 0.01, f"{tv:.3f}", ha="center", fontsize=9)
|
||||||
|
if rv > 0: ax.text(i - w / 2, rv + 0.01, f"{rv:.3f}", ha="center", fontsize=9)
|
||||||
|
fig.tight_layout()
|
||||||
|
fig.savefig(_REPORT_DIR / "summary_dtw_barchart.png", dpi=150,
|
||||||
|
bbox_inches="tight")
|
||||||
|
plt.close(fig)
|
||||||
|
log(" -> summary_dtw_barchart.png")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def main() -> int:
|
||||||
|
_REPORT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
log(f"Train dir: {_OUT_BASE / 'train'}")
|
||||||
|
log(f"Reproduce dir: {_OUT_BASE / 'reproduce'}")
|
||||||
|
log(f"Reports dir: {_REPORT_DIR}")
|
||||||
|
|
||||||
|
table = generate_comparison_table()
|
||||||
|
(_REPORT_DIR / "comparison_table.md").write_text(table)
|
||||||
|
log(" -> comparison_table.md")
|
||||||
|
|
||||||
|
generate_dtw_barchart()
|
||||||
|
|
||||||
|
log("All reports generated.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,610 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Reproduce pipeline inference: legacy models on 1280x512 config.
|
||||||
|
|
||||||
|
Loads pre-trained PPO models from models/old/ and models/250525/,
|
||||||
|
runs deterministic inference on config_lbm_pinball.json (parabolic, bounce-back).
|
||||||
|
|
||||||
|
Key differences from train pipeline:
|
||||||
|
- 1280x512 grid (not 2000x600)
|
||||||
|
- Legacy norm (force_norm_fact + sens_deviation + sens_norm_fact)
|
||||||
|
- Old action scaling with biases (match legacy env)
|
||||||
|
- No VecNormalize (old models use manual norm)
|
||||||
|
- DummyEnv for PPO.load()
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
conda run -n pycuda_3_10 python -u infer_reproduce.py --device-id 1
|
||||||
|
conda run -n pycuda_3_10 python -u infer_reproduce.py --device-id 1 --scene karman
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from collections import deque
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Tuple
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pycuda.driver as cuda; cuda.init()
|
||||||
|
|
||||||
|
_REPO = str(Path(__file__).resolve().parents[3])
|
||||||
|
_SRC = Path(_REPO) / "src"
|
||||||
|
for p in [_REPO, str(_SRC)]:
|
||||||
|
if p not in sys.path:
|
||||||
|
sys.path.insert(0, p)
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from torch.nn import Module as TorchModule
|
||||||
|
from stable_baselines3 import PPO
|
||||||
|
|
||||||
|
from CelerisLab import Simulation
|
||||||
|
from CelerisLab.common.render import compute_vorticity, render_vorticity_field
|
||||||
|
from CelerisLab.lbm.initializers import add_vortex
|
||||||
|
from drl_pinball.reproduce.configs.model_inventory import ModelInventory
|
||||||
|
from drl_pinball.eval.scene_manifest import REPRODUCE_SCENES
|
||||||
|
|
||||||
|
L0 = 20.0
|
||||||
|
U0 = 0.01
|
||||||
|
NX = 1280
|
||||||
|
NY = 512
|
||||||
|
CENTER_Y = float(NY - 1) / 2.0
|
||||||
|
RADIUS = L0 / 2.0
|
||||||
|
FIFO_LEN = 150
|
||||||
|
WARMUP = int(4.0 * NX / U0)
|
||||||
|
|
||||||
|
# Karman/Vortex/Steady geometry (1280x512)
|
||||||
|
DIST_X = 10.0 * L0 # 200
|
||||||
|
PB_FRONT_X = 30.0 * L0 # 600
|
||||||
|
PB_REAR_X = 31.3 * L0 # 626
|
||||||
|
SENSOR_X = 40.0 * L0 # 800
|
||||||
|
|
||||||
|
# Illusion geometry (shifted left)
|
||||||
|
ILL_PB_FRONT_X = 19.0 * L0 # 380
|
||||||
|
ILL_PB_REAR_X = 20.3 * L0 # 406
|
||||||
|
ILL_SENSOR_X = 30.0 * L0 # 600
|
||||||
|
ILL_TARGET_X = 20.0 * L0 # 400
|
||||||
|
|
||||||
|
VORTEX_RADIUS = 2.0 * L0
|
||||||
|
SR_DATA = _SRC / "SR_analysis" / "data"
|
||||||
|
_OUT_BASE = Path(__file__).resolve().parent / "output" / "reproduce"
|
||||||
|
|
||||||
|
|
||||||
|
class Sin(TorchModule):
|
||||||
|
def __init__(self): super().__init__()
|
||||||
|
def forward(self, x): return torch.sin(x)
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg: str) -> None:
|
||||||
|
print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Legacy helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class ActionSmoother:
|
||||||
|
def __init__(self, weight=0.1):
|
||||||
|
self.weight = weight; self._state = None
|
||||||
|
def __call__(self, target):
|
||||||
|
t = np.asarray(target, dtype=np.float32)
|
||||||
|
if self._state is None:
|
||||||
|
self._state = t.copy()
|
||||||
|
else:
|
||||||
|
self._state = (1.0 - self.weight) * self._state + self.weight * t
|
||||||
|
return self._state.copy()
|
||||||
|
def reset(self, value=None):
|
||||||
|
self._state = np.asarray(value, dtype=np.float32).copy() if value is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
def get_cc(sim, sid):
|
||||||
|
nx, ny = sim.lbm_cfg.nx, sim.lbm_cfg.ny
|
||||||
|
cells_arr, _ = sim.bodies.get(sid).get_sensor_list(nx, ny)
|
||||||
|
return float(len(cells_arr))
|
||||||
|
|
||||||
|
|
||||||
|
def action_to_omega_legacy(action_norm, scale=8.0, bias=(0.0, -4.0, 4.0)):
|
||||||
|
b = np.array(bias, dtype=np.float32)
|
||||||
|
sv = (np.asarray(action_norm, dtype=np.float32) * scale + b) * U0
|
||||||
|
return -sv / RADIUS
|
||||||
|
|
||||||
|
|
||||||
|
def load_legacy_norm(ref_dir: str) -> Dict[str, Any]:
|
||||||
|
with open(os.path.join(ref_dir, "norm.json")) as f:
|
||||||
|
d = json.load(f)
|
||||||
|
return {
|
||||||
|
"force_norm_fact": np.float32(d["force_norm_fact"]),
|
||||||
|
"sens_deviation": np.array(d["sens_deviation"], dtype=np.float32),
|
||||||
|
"sens_norm_fact": np.array(d["sens_norm_fact"], dtype=np.float32),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_obs(obs_slice, norm):
|
||||||
|
forces = obs_slice[6:12] / norm["force_norm_fact"]
|
||||||
|
sens = (obs_slice[0:6] - norm["sens_deviation"]) / norm["sens_norm_fact"]
|
||||||
|
return np.clip(np.hstack([forces, sens]), -1.0, 1.0).astype(np.float32)
|
||||||
|
|
||||||
|
|
||||||
|
def save_vorticity(sim, out_path, cylinders, nx=NX, ny=NY):
|
||||||
|
macro = sim.get_macroscopic()
|
||||||
|
vort = compute_vorticity(macro["ux"], macro["uy"])
|
||||||
|
render_vorticity_field(vort, nx=nx, ny=ny, out_path=str(out_path),
|
||||||
|
cylinders=cylinders, vmin=-0.03, vmax=0.03)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Obs readers (legacy ordering)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def read_obs_karman(sim, dist_id, sensor_ids, pinball_ids, cc):
|
||||||
|
"""dist_fx, dist_fy, s0_ux, s0_uy, s1_ux, s1_uy, s2_ux, s2_uy,
|
||||||
|
front_fx, front_fy, top_fx, top_fy, bot_fx, bot_fy"""
|
||||||
|
obs = list(sim.read_force(dist_id, normalize=True))
|
||||||
|
for sid in sensor_ids:
|
||||||
|
s = sim.read_sensor(sid, normalize=True)
|
||||||
|
obs.extend([float(s[0]) * cc, float(s[1]) * cc])
|
||||||
|
for pid in pinball_ids:
|
||||||
|
obs.extend(sim.read_force(pid, normalize=True))
|
||||||
|
return np.array(obs, dtype=np.float32)
|
||||||
|
|
||||||
|
|
||||||
|
def read_obs_6obj(sim, sensor_ids, pinball_ids, cc):
|
||||||
|
obs = []
|
||||||
|
for sid in sensor_ids:
|
||||||
|
s = sim.read_sensor(sid, normalize=True)
|
||||||
|
obs.extend([float(s[0]) * cc, float(s[1]) * cc])
|
||||||
|
for pid in pinball_ids:
|
||||||
|
obs.extend(sim.read_force(pid, normalize=True))
|
||||||
|
return np.array(obs, dtype=np.float32)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Karman Cloak
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def run_karman_reproduce(scene: Dict[str, Any], device_id: int, out_dir: Path) -> None:
|
||||||
|
log(f"=== Reproduce: Karman Cloak ===")
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
SI = scene["si"]
|
||||||
|
num_steps = scene["num_steps"]
|
||||||
|
model_name = scene["model_name"]
|
||||||
|
scale = scene["action_scale"]
|
||||||
|
bias = scene["action_bias"]
|
||||||
|
|
||||||
|
# Phase 1: Disturbance + sensors, record target
|
||||||
|
log(" Phase 1: Recording target...")
|
||||||
|
sim = Simulation(lbm_config_path=scene["config_path"], device_id=device_id)
|
||||||
|
dist_id = sim.add_body("circle", center=(DIST_X, CENTER_Y, 0.0), radius=1.0 * L0)
|
||||||
|
sensor_ids = [
|
||||||
|
sim.add_body("sensor", center=(SENSOR_X, CENTER_Y + 40.0, 0.0), radius=5.0),
|
||||||
|
sim.add_body("sensor", center=(SENSOR_X, CENTER_Y, 0.0), radius=5.0),
|
||||||
|
sim.add_body("sensor", center=(SENSOR_X, CENTER_Y - 40.0, 0.0), radius=5.0),
|
||||||
|
]
|
||||||
|
sim.initialize()
|
||||||
|
sim.run(WARMUP, zero_obs=True)
|
||||||
|
cc = get_cc(sim, sensor_ids[0])
|
||||||
|
log(f" Sensor cell count: {cc}")
|
||||||
|
|
||||||
|
target_states = np.zeros((FIFO_LEN, 6), dtype=np.float32)
|
||||||
|
for i in range(FIFO_LEN):
|
||||||
|
sim.run(SI, zero_obs=True)
|
||||||
|
obs = read_obs_karman(sim, dist_id, sensor_ids, [], cc)
|
||||||
|
target_states[i] = obs[2:8]
|
||||||
|
np.savez_compressed(out_dir / "target.npz", target_states=target_states)
|
||||||
|
|
||||||
|
# Save target vorticity
|
||||||
|
save_vorticity(sim, out_dir / "vorticity_target.png",
|
||||||
|
[((DIST_X, CENTER_Y), 1.0 * L0),
|
||||||
|
((SENSOR_X, CENTER_Y + 40.0), 5.0),
|
||||||
|
((SENSOR_X, CENTER_Y), 5.0),
|
||||||
|
((SENSOR_X, CENTER_Y - 40.0), 5.0)])
|
||||||
|
|
||||||
|
# Phase 2: Add pinball
|
||||||
|
log(" Phase 2: Adding pinball...")
|
||||||
|
n0 = sim.bodies.count
|
||||||
|
sim.add_body("circle", center=(PB_FRONT_X, CENTER_Y, 0.0), radius=RADIUS)
|
||||||
|
sim.add_body("circle", center=(PB_REAR_X, CENTER_Y + 15.0, 0.0), radius=RADIUS)
|
||||||
|
sim.add_body("circle", center=(PB_REAR_X, CENTER_Y - 15.0, 0.0), radius=RADIUS)
|
||||||
|
sim.sync_bodies()
|
||||||
|
fid, tid, bid = list(range(n0, n0 + 3))
|
||||||
|
sim.run(WARMUP, zero_obs=True)
|
||||||
|
|
||||||
|
# Load legacy norm
|
||||||
|
ref_dir = str(SR_DATA / "karman" / "karman_re100")
|
||||||
|
norm = load_legacy_norm(ref_dir)
|
||||||
|
|
||||||
|
# Bias FIFO
|
||||||
|
bias_norm = np.array([0.0, -1.0, 1.0])
|
||||||
|
bias_omega = action_to_omega_legacy(bias_norm, scale=scale, bias=bias)
|
||||||
|
ema = ActionSmoother(weight=0.1); ema.reset(np.zeros(3, dtype=np.float32))
|
||||||
|
for _ in range(FIFO_LEN):
|
||||||
|
s = ema(bias_omega)
|
||||||
|
sim.set_body(fid, omega=s[0]); sim.set_body(tid, omega=s[1]); sim.set_body(bid, omega=s[2])
|
||||||
|
sim.run(SI, zero_obs=True)
|
||||||
|
sim.snapshot()
|
||||||
|
|
||||||
|
# DRL inference
|
||||||
|
log(" Phase 3: DRL inference...")
|
||||||
|
sim.restore()
|
||||||
|
ema.reset(bias_omega.copy())
|
||||||
|
|
||||||
|
model = ModelInventory().load(model_name, device="cpu")
|
||||||
|
log(f" Model: {model_name} on CPU")
|
||||||
|
|
||||||
|
obs_init = read_obs_karman(sim, dist_id, sensor_ids, [fid, tid, bid], cc)
|
||||||
|
obs_norm = normalize_obs(obs_init[2:14], norm)
|
||||||
|
|
||||||
|
sig_s = np.zeros((num_steps, 6), dtype=np.float32)
|
||||||
|
sig_f = np.zeros((num_steps, 6), dtype=np.float32)
|
||||||
|
sig_a = np.zeros((num_steps, 3), dtype=np.float32)
|
||||||
|
fifo = deque(maxlen=FIFO_LEN)
|
||||||
|
[fifo.append(target_states[i, :].copy()) for i in range(FIFO_LEN)]
|
||||||
|
|
||||||
|
for step in range(num_steps):
|
||||||
|
action, _ = model.predict(obs_norm, deterministic=True)
|
||||||
|
action = np.asarray(action, dtype=np.float32).flatten()
|
||||||
|
target_omega = action_to_omega_legacy(action, scale=scale, bias=bias)
|
||||||
|
smoothed = ema(target_omega)
|
||||||
|
|
||||||
|
sim.set_body(fid, omega=smoothed[0]); sim.set_body(tid, omega=smoothed[1]); sim.set_body(bid, omega=smoothed[2])
|
||||||
|
sim.run(SI, zero_obs=True)
|
||||||
|
|
||||||
|
obs = read_obs_karman(sim, dist_id, sensor_ids, [fid, tid, bid], cc)
|
||||||
|
sl = obs[2:14]
|
||||||
|
fifo.append(sl[0:6].copy())
|
||||||
|
sig_s[step] = sl[0:6]
|
||||||
|
sig_f[step] = sl[6:12]
|
||||||
|
sig_a[step] = action
|
||||||
|
obs_norm = normalize_obs(sl, norm)
|
||||||
|
|
||||||
|
save_vorticity(sim, out_dir / "vorticity_controlled.png", [
|
||||||
|
((DIST_X, CENTER_Y), 1.0 * L0),
|
||||||
|
((PB_FRONT_X, CENTER_Y), RADIUS),
|
||||||
|
((PB_REAR_X, CENTER_Y + 15.0), RADIUS),
|
||||||
|
((PB_REAR_X, CENTER_Y - 15.0), RADIUS),
|
||||||
|
])
|
||||||
|
sim.close()
|
||||||
|
|
||||||
|
np.savez_compressed(out_dir / "signals.npz",
|
||||||
|
sensors=sig_s, forces=sig_f, actions=sig_a)
|
||||||
|
metrics = {"dtw_sim_v5": -1.0} # DTW computed by generate_report later
|
||||||
|
with open(out_dir / "metrics.json", "w") as f:
|
||||||
|
json.dump(metrics, f, indent=2)
|
||||||
|
log(" Done.")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Steady Cloak (open-loop)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def run_steady_reproduce(scene: Dict[str, Any], device_id: int, out_dir: Path) -> None:
|
||||||
|
log(f"=== Reproduce: Steady Cloak ===")
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
SI = scene["si"]
|
||||||
|
num_steps = scene["num_steps"]
|
||||||
|
surf_vel = scene.get("open_loop_surf_vel", (0.0, -5.1, 5.1))
|
||||||
|
bias_surf = np.array(surf_vel, dtype=np.float32) * U0
|
||||||
|
|
||||||
|
sim = Simulation(lbm_config_path=scene["config_path"], device_id=device_id)
|
||||||
|
sensor_ids = [
|
||||||
|
sim.add_body("sensor", center=(SENSOR_X, CENTER_Y + 40.0, 0.0), radius=5.0),
|
||||||
|
sim.add_body("sensor", center=(SENSOR_X, CENTER_Y, 0.0), radius=5.0),
|
||||||
|
sim.add_body("sensor", center=(SENSOR_X, CENTER_Y - 40.0, 0.0), radius=5.0),
|
||||||
|
]
|
||||||
|
sim.add_body("circle", center=(PB_FRONT_X, CENTER_Y, 0.0), radius=RADIUS)
|
||||||
|
sim.add_body("circle", center=(PB_REAR_X, CENTER_Y + 15.0, 0.0), radius=RADIUS)
|
||||||
|
sim.add_body("circle", center=(PB_REAR_X, CENTER_Y - 15.0, 0.0), radius=RADIUS)
|
||||||
|
sim.initialize()
|
||||||
|
sim.run(WARMUP, zero_obs=True)
|
||||||
|
cc = get_cc(sim, sensor_ids[0])
|
||||||
|
|
||||||
|
save_vorticity(sim, out_dir / "vorticity_zero.png", [
|
||||||
|
((PB_FRONT_X, CENTER_Y), RADIUS),
|
||||||
|
((PB_REAR_X, CENTER_Y + 15.0), RADIUS),
|
||||||
|
((PB_REAR_X, CENTER_Y - 15.0), RADIUS),
|
||||||
|
])
|
||||||
|
|
||||||
|
bias_omega = -bias_surf / RADIUS
|
||||||
|
ema = ActionSmoother(weight=0.1); ema.reset(np.zeros(3, dtype=np.float32))
|
||||||
|
for _ in range(FIFO_LEN):
|
||||||
|
s = ema(bias_omega)
|
||||||
|
sim.set_body(3, omega=s[0]); sim.set_body(4, omega=s[1]); sim.set_body(5, omega=s[2])
|
||||||
|
sim.run(SI, zero_obs=True)
|
||||||
|
sim.snapshot()
|
||||||
|
sim.restore()
|
||||||
|
ema.reset(bias_omega.copy())
|
||||||
|
|
||||||
|
sig_s = np.zeros((num_steps, 6), dtype=np.float32)
|
||||||
|
sig_f = np.zeros((num_steps, 6), dtype=np.float32)
|
||||||
|
for step in range(num_steps):
|
||||||
|
smoothed = ema(bias_omega)
|
||||||
|
sim.set_body(3, omega=smoothed[0]); sim.set_body(4, omega=smoothed[1]); sim.set_body(5, omega=smoothed[2])
|
||||||
|
sim.run(SI, zero_obs=True)
|
||||||
|
obs = read_obs_6obj(sim, sensor_ids, [3, 4, 5], cc)
|
||||||
|
sig_s[step] = obs[0:6]
|
||||||
|
sig_f[step] = obs[6:12]
|
||||||
|
|
||||||
|
save_vorticity(sim, out_dir / "vorticity_controlled.png", [
|
||||||
|
((PB_FRONT_X, CENTER_Y), RADIUS),
|
||||||
|
((PB_REAR_X, CENTER_Y + 15.0), RADIUS),
|
||||||
|
((PB_REAR_X, CENTER_Y - 15.0), RADIUS),
|
||||||
|
])
|
||||||
|
sim.close()
|
||||||
|
|
||||||
|
np.savez_compressed(out_dir / "signals.npz", sensors=sig_s, forces=sig_f,
|
||||||
|
actions=np.zeros((num_steps, 3), dtype=np.float32))
|
||||||
|
with open(out_dir / "metrics.json", "w") as f:
|
||||||
|
json.dump({"dtw_sim_v5": -1.0}, f, indent=2)
|
||||||
|
log(" Done.")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Illusion
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def run_illusion_reproduce(scene: Dict[str, Any], device_id: int, out_dir: Path) -> None:
|
||||||
|
scene_id = scene["scene_id"]
|
||||||
|
log(f"=== Reproduce: Illusion {scene_id} ===")
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
SI = scene["si"]
|
||||||
|
num_steps = scene["num_steps"]
|
||||||
|
model_name = scene["model_name"]
|
||||||
|
scale = scene["action_scale"]
|
||||||
|
bias = scene["action_bias"]
|
||||||
|
target_diam = scene.get("target_diam", 1.0)
|
||||||
|
|
||||||
|
# Load legacy data
|
||||||
|
ref_dir = str(SR_DATA / "illusion" / scene_id)
|
||||||
|
norm = load_legacy_norm(ref_dir)
|
||||||
|
with open(os.path.join(ref_dir, "target_harmonics.json")) as f:
|
||||||
|
target_harmonics = json.load(f)
|
||||||
|
legacy_target = np.load(os.path.join(ref_dir, "target.npz"))["target_states"]
|
||||||
|
|
||||||
|
def gen_target_at(t):
|
||||||
|
D = len(target_harmonics)
|
||||||
|
result = np.zeros(D, dtype=np.float32)
|
||||||
|
for d, h in enumerate(target_harmonics):
|
||||||
|
val = np.float32(h["dc"])
|
||||||
|
for amp, freq, phase in zip(h["amps"], h["freqs"], h["phases"]):
|
||||||
|
val += amp * np.cos(2.0 * np.pi * freq * t + phase)
|
||||||
|
result[d] = val
|
||||||
|
return result
|
||||||
|
|
||||||
|
sim = Simulation(lbm_config_path=scene["config_path"], device_id=device_id)
|
||||||
|
sensor_ids = [
|
||||||
|
sim.add_body("sensor", center=(ILL_SENSOR_X, CENTER_Y + 40.0, 0.0), radius=5.0),
|
||||||
|
sim.add_body("sensor", center=(ILL_SENSOR_X, CENTER_Y, 0.0), radius=5.0),
|
||||||
|
sim.add_body("sensor", center=(ILL_SENSOR_X, CENTER_Y - 40.0, 0.0), radius=5.0),
|
||||||
|
]
|
||||||
|
sim.add_body("circle", center=(ILL_PB_FRONT_X, CENTER_Y, 0.0), radius=RADIUS)
|
||||||
|
sim.add_body("circle", center=(ILL_PB_REAR_X, CENTER_Y + 15.0, 0.0), radius=RADIUS)
|
||||||
|
sim.add_body("circle", center=(ILL_PB_REAR_X, CENTER_Y - 15.0, 0.0), radius=RADIUS)
|
||||||
|
sim.initialize()
|
||||||
|
sim.run(WARMUP, zero_obs=True)
|
||||||
|
cc = get_cc(sim, sensor_ids[0])
|
||||||
|
fid, tid, bid = 3, 4, 5
|
||||||
|
|
||||||
|
# Bias FIFO
|
||||||
|
bias_surf = np.array([0.0, -1.0, 1.0], dtype=np.float32) * U0
|
||||||
|
bias_omega = -bias_surf / RADIUS
|
||||||
|
ema_b = ActionSmoother(weight=0.1); ema_b.reset(np.zeros(3, dtype=np.float32))
|
||||||
|
for _ in range(FIFO_LEN):
|
||||||
|
s = ema_b(bias_omega)
|
||||||
|
sim.set_body(fid, omega=s[0]); sim.set_body(tid, omega=s[1]); sim.set_body(bid, omega=s[2])
|
||||||
|
sim.run(SI, zero_obs=True)
|
||||||
|
sim.snapshot()
|
||||||
|
|
||||||
|
# DRL inference
|
||||||
|
sim.restore()
|
||||||
|
ema = ActionSmoother(weight=0.1); ema.reset(bias_omega.copy())
|
||||||
|
model = ModelInventory().load(model_name, device="cpu")
|
||||||
|
log(f" Model: {model_name} on CPU")
|
||||||
|
|
||||||
|
obs_init = read_obs_6obj(sim, sensor_ids, [fid, tid, bid], cc)
|
||||||
|
obs_norm_base = normalize_obs(obs_init, norm)
|
||||||
|
t0 = gen_target_at(0)
|
||||||
|
target_cd = np.float32(t0[0] / norm["force_norm_fact"])
|
||||||
|
target_cl = np.float32(t0[1] / norm["force_norm_fact"])
|
||||||
|
obs_norm = np.clip(np.hstack([obs_norm_base, [target_cd, target_cl]]), -1.0, 1.0).astype(np.float32)
|
||||||
|
|
||||||
|
sig_s = np.zeros((num_steps, 6), dtype=np.float32)
|
||||||
|
sig_f = np.zeros((num_steps, 6), dtype=np.float32)
|
||||||
|
sig_a = np.zeros((num_steps, 3), dtype=np.float32)
|
||||||
|
|
||||||
|
for step in range(num_steps):
|
||||||
|
action, _ = model.predict(obs_norm, deterministic=True)
|
||||||
|
action = np.asarray(action, dtype=np.float32).flatten()
|
||||||
|
target_omega = action_to_omega_legacy(action, scale=scale, bias=bias)
|
||||||
|
smoothed = ema(target_omega)
|
||||||
|
|
||||||
|
sim.set_body(fid, omega=smoothed[0]); sim.set_body(tid, omega=smoothed[1]); sim.set_body(bid, omega=smoothed[2])
|
||||||
|
sim.run(SI, zero_obs=True)
|
||||||
|
|
||||||
|
obs = read_obs_6obj(sim, sensor_ids, [fid, tid, bid], cc)
|
||||||
|
sig_s[step] = obs[0:6]
|
||||||
|
sig_f[step] = obs[6:12]
|
||||||
|
sig_a[step] = action
|
||||||
|
|
||||||
|
forces_n = obs[6:12] / norm["force_norm_fact"]
|
||||||
|
sens_n = (obs[0:6] - norm["sens_deviation"]) / norm["sens_norm_fact"]
|
||||||
|
t_h = gen_target_at(step)
|
||||||
|
target_cd = np.float32(t_h[0] / norm["force_norm_fact"])
|
||||||
|
target_cl = np.float32(t_h[1] / norm["force_norm_fact"])
|
||||||
|
obs_norm = np.clip(np.hstack([forces_n, sens_n, [target_cd, target_cl]]), -1.0, 1.0).astype(np.float32)
|
||||||
|
|
||||||
|
save_vorticity(sim, out_dir / "vorticity_controlled.png", [
|
||||||
|
((ILL_PB_FRONT_X, CENTER_Y), RADIUS),
|
||||||
|
((ILL_PB_REAR_X, CENTER_Y + 15.0), RADIUS),
|
||||||
|
((ILL_PB_REAR_X, CENTER_Y - 15.0), RADIUS),
|
||||||
|
])
|
||||||
|
sim.close()
|
||||||
|
|
||||||
|
np.savez_compressed(out_dir / "signals.npz",
|
||||||
|
sensors=sig_s, forces=sig_f, actions=sig_a)
|
||||||
|
with open(out_dir / "metrics.json", "w") as f:
|
||||||
|
json.dump({"dtw_sim_v5": -1.0}, f, indent=2)
|
||||||
|
log(" Done.")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Vortex
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def run_vortex_reproduce(scene: Dict[str, Any], device_id: int, out_dir: Path) -> None:
|
||||||
|
scene_id = scene["scene_id"]
|
||||||
|
vortex_type = scene["vortex_type"]
|
||||||
|
model_name = scene["model_name"]
|
||||||
|
SI = scene["si"]
|
||||||
|
num_steps = scene["num_steps"]
|
||||||
|
scale = scene["action_scale"]
|
||||||
|
bias = scene["action_bias"]
|
||||||
|
vortex_strength = scene["vortex_strength_factor"] * U0
|
||||||
|
|
||||||
|
log(f"=== Reproduce: Vortex {vortex_type} ===")
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
ref_dir = str(SR_DATA / "vortex" / scene_id)
|
||||||
|
norm = load_legacy_norm(ref_dir)
|
||||||
|
|
||||||
|
# Stage 1: Sensors only, record target with vortex at x=10
|
||||||
|
log(" Stage 1: Recording target with vortex...")
|
||||||
|
sim = Simulation(lbm_config_path=scene["config_path"], device_id=device_id)
|
||||||
|
sensor_ids = [
|
||||||
|
sim.add_body("sensor", center=(SENSOR_X, CENTER_Y + 40.0, 0.0), radius=5.0),
|
||||||
|
sim.add_body("sensor", center=(SENSOR_X, CENTER_Y, 0.0), radius=5.0),
|
||||||
|
sim.add_body("sensor", center=(SENSOR_X, CENTER_Y - 40.0, 0.0), radius=5.0),
|
||||||
|
]
|
||||||
|
sim.initialize()
|
||||||
|
sim.run(WARMUP, zero_obs=True)
|
||||||
|
cc = get_cc(sim, sensor_ids[0])
|
||||||
|
sim.snapshot()
|
||||||
|
|
||||||
|
add_vortex(sim.field, center=(10.0 * L0, CENTER_Y), radius=VORTEX_RADIUS,
|
||||||
|
strength=vortex_strength, vortex_type=vortex_type)
|
||||||
|
target_states = np.zeros((FIFO_LEN, 6), dtype=np.float32)
|
||||||
|
for i in range(FIFO_LEN):
|
||||||
|
sim.run(SI, zero_obs=True)
|
||||||
|
obs = read_obs_6obj(sim, sensor_ids, [], cc)
|
||||||
|
target_states[i] = obs
|
||||||
|
np.savez_compressed(out_dir / "target.npz", target_states=target_states)
|
||||||
|
|
||||||
|
# Stage 2: Add pinball + vortex at x=15
|
||||||
|
log(" Stage 2: Adding pinball + vortex at x=15...")
|
||||||
|
sim.restore()
|
||||||
|
n0 = sim.bodies.count
|
||||||
|
sim.add_body("circle", center=(PB_FRONT_X, CENTER_Y, 0.0), radius=RADIUS)
|
||||||
|
sim.add_body("circle", center=(PB_REAR_X, CENTER_Y + 15.0, 0.0), radius=RADIUS)
|
||||||
|
sim.add_body("circle", center=(PB_REAR_X, CENTER_Y - 15.0, 0.0), radius=RADIUS)
|
||||||
|
sim.sync_bodies()
|
||||||
|
fid, tid, bid = list(range(n0, n0 + 3))
|
||||||
|
|
||||||
|
bias_surf = np.array([0.0, -4.0, 4.0], dtype=np.float32) * U0
|
||||||
|
bias_omega = -bias_surf / RADIUS
|
||||||
|
ema_init = ActionSmoother(weight=0.1); ema_init.reset(np.zeros(3, dtype=np.float32))
|
||||||
|
for _ in range(100):
|
||||||
|
s = ema_init(bias_omega)
|
||||||
|
sim.set_body(fid, omega=s[0]); sim.set_body(tid, omega=s[1]); sim.set_body(bid, omega=s[2])
|
||||||
|
sim.run(SI, zero_obs=True)
|
||||||
|
|
||||||
|
add_vortex(sim.field, center=(15.0 * L0, CENTER_Y), radius=VORTEX_RADIUS,
|
||||||
|
strength=vortex_strength, vortex_type=vortex_type)
|
||||||
|
sim.snapshot()
|
||||||
|
|
||||||
|
# Bias FIFO
|
||||||
|
sim.restore()
|
||||||
|
ema_b = ActionSmoother(weight=0.1); ema_b.reset(np.zeros(3, dtype=np.float32))
|
||||||
|
for _ in range(FIFO_LEN):
|
||||||
|
s = ema_b(bias_omega)
|
||||||
|
sim.set_body(fid, omega=s[0]); sim.set_body(tid, omega=s[1]); sim.set_body(bid, omega=s[2])
|
||||||
|
sim.run(SI, zero_obs=True)
|
||||||
|
sim.snapshot()
|
||||||
|
|
||||||
|
# DRL inference
|
||||||
|
log(" Stage 3: DRL inference...")
|
||||||
|
sim.restore()
|
||||||
|
ema = ActionSmoother(weight=0.1); ema.reset(bias_omega.copy())
|
||||||
|
model = ModelInventory().load(model_name, device="cpu")
|
||||||
|
log(f" Model: {model_name} on CPU")
|
||||||
|
|
||||||
|
obs_init = read_obs_6obj(sim, sensor_ids, [fid, tid, bid], cc)
|
||||||
|
obs_norm = normalize_obs(obs_init, norm)
|
||||||
|
|
||||||
|
sig_s = np.zeros((num_steps, 6), dtype=np.float32)
|
||||||
|
sig_f = np.zeros((num_steps, 6), dtype=np.float32)
|
||||||
|
sig_a = np.zeros((num_steps, 3), dtype=np.float32)
|
||||||
|
|
||||||
|
for step in range(num_steps):
|
||||||
|
action, _ = model.predict(obs_norm, deterministic=True)
|
||||||
|
action = np.asarray(action, dtype=np.float32).flatten()
|
||||||
|
target_omega = action_to_omega_legacy(action, scale=scale, bias=bias)
|
||||||
|
smoothed = ema(target_omega)
|
||||||
|
|
||||||
|
sim.set_body(fid, omega=smoothed[0]); sim.set_body(tid, omega=smoothed[1]); sim.set_body(bid, omega=smoothed[2])
|
||||||
|
sim.run(SI, zero_obs=True)
|
||||||
|
|
||||||
|
obs = read_obs_6obj(sim, sensor_ids, [fid, tid, bid], cc)
|
||||||
|
sig_s[step] = obs[0:6]
|
||||||
|
sig_f[step] = obs[6:12]
|
||||||
|
sig_a[step] = action
|
||||||
|
forces_n = obs[6:12] / norm["force_norm_fact"]
|
||||||
|
sens_n = (obs[0:6] - norm["sens_deviation"]) / norm["sens_norm_fact"]
|
||||||
|
obs_norm = np.clip(np.hstack([forces_n, sens_n]), -1.0, 1.0).astype(np.float32)
|
||||||
|
|
||||||
|
save_vorticity(sim, out_dir / "vorticity_controlled.png", [
|
||||||
|
((PB_FRONT_X, CENTER_Y), RADIUS),
|
||||||
|
((PB_REAR_X, CENTER_Y + 15.0), RADIUS),
|
||||||
|
((PB_REAR_X, CENTER_Y - 15.0), RADIUS),
|
||||||
|
])
|
||||||
|
sim.close()
|
||||||
|
|
||||||
|
np.savez_compressed(out_dir / "signals.npz",
|
||||||
|
sensors=sig_s, forces=sig_f, actions=sig_a)
|
||||||
|
with open(out_dir / "metrics.json", "w") as f:
|
||||||
|
json.dump({"dtw_sim_v5": -1.0}, f, indent=2)
|
||||||
|
log(" Done.")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Scene dispatcher
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
SCENE_RUNNERS = {
|
||||||
|
"karman_cloak": run_karman_reproduce,
|
||||||
|
"steady_cloak": run_steady_reproduce,
|
||||||
|
"illusion_075L": run_illusion_reproduce,
|
||||||
|
"illusion_1L": run_illusion_reproduce,
|
||||||
|
"illusion_15L": run_illusion_reproduce,
|
||||||
|
"vortex_lamb": run_vortex_reproduce,
|
||||||
|
"vortex_taylor": run_vortex_reproduce,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Evaluate reproduce models")
|
||||||
|
parser.add_argument("--device-id", type=int, default=1)
|
||||||
|
parser.add_argument("--scene", type=str, default=None,
|
||||||
|
help="Run single scene (e.g. karman_cloak, illusion_1L)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
log(f"GPU: {args.device_id}")
|
||||||
|
|
||||||
|
scenes = REPRODUCE_SCENES
|
||||||
|
if args.scene:
|
||||||
|
key = args.scene
|
||||||
|
scenes = [s for s in REPRODUCE_SCENES if s["scene_id"] == key]
|
||||||
|
if not scenes:
|
||||||
|
log(f"ERROR: No scene matching '{args.scene}'")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
for scene in scenes:
|
||||||
|
scene_id = scene["scene_id"]
|
||||||
|
out_dir = _OUT_BASE / scene_id
|
||||||
|
runner = SCENE_RUNNERS.get(scene_id)
|
||||||
|
if runner is None:
|
||||||
|
log(f"WARNING: No runner for scene {scene_id}, skipping.")
|
||||||
|
continue
|
||||||
|
runner(scene, args.device_id, out_dir)
|
||||||
|
|
||||||
|
log("All reproduce scenes complete.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,472 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""V5 Train model inference engine — skeleton injection with per-seed VecNormalize.
|
||||||
|
|
||||||
|
For each scene in TRAIN_SCENES:
|
||||||
|
1. Create V5 CFD env (KarmanCloakEnv / IllusionCloakEnv)
|
||||||
|
2. For every seed: fresh skeleton + VecNormalize.load(seed.pkl) + weight injection
|
||||||
|
3. 360-step deterministic rollout, pick best by tail-180 reward
|
||||||
|
4. Save signals.npz + vorticity PNGs (controlled, target, zero)
|
||||||
|
|
||||||
|
Uses skeleton injection per-seed (one skeleton per seed, one VecNormalize load per seed)
|
||||||
|
to avoid cloudpickle/numpy PPO.load() deserialization issues while still using
|
||||||
|
trained VecNormalize stats for proper observation normalization.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
conda run -n pycuda_3_10 python -u infer_train.py --device-id 0
|
||||||
|
conda run -n pycuda_3_10 python -u infer_train.py --device-id 0 --scene re100
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Tuple
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pycuda.driver as cuda; cuda.init()
|
||||||
|
|
||||||
|
_REPO = str(Path(__file__).resolve().parents[3])
|
||||||
|
_SRC = Path(_REPO) / "src"
|
||||||
|
for p in [_REPO, str(_SRC)]:
|
||||||
|
if p not in sys.path:
|
||||||
|
sys.path.insert(0, p)
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from torch.nn import Module as TorchModule
|
||||||
|
from stable_baselines3 import PPO
|
||||||
|
from stable_baselines3.common.vec_env import DummyVecEnv, VecNormalize
|
||||||
|
|
||||||
|
from CelerisLab.common.render import compute_vorticity, render_vorticity_field
|
||||||
|
|
||||||
|
_TRAIN_DIR = _SRC / "drl_pinball" / "train"
|
||||||
|
sys.path.insert(0, str(_TRAIN_DIR))
|
||||||
|
|
||||||
|
from symmetry_wrapper import SymmetryAugmentWrapper
|
||||||
|
|
||||||
|
from drl_pinball.eval.scene_manifest import TRAIN_SCENES
|
||||||
|
|
||||||
|
_OUT_BASE = Path(__file__).resolve().parent / "output" / "train"
|
||||||
|
|
||||||
|
|
||||||
|
class Sin(TorchModule):
|
||||||
|
def __init__(self): super().__init__()
|
||||||
|
def forward(self, x): return torch.sin(x)
|
||||||
|
|
||||||
|
|
||||||
|
_device = None # set at startup
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg: str) -> None:
|
||||||
|
print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Env creation
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def _create_env(scene: Dict[str, Any], device_id: int):
|
||||||
|
scene_type = scene["scene_type"]
|
||||||
|
config_path = Path(scene["config_path"]).resolve()
|
||||||
|
cal_path = Path(scene["calibration_path"]).resolve()
|
||||||
|
si = scene["si"]
|
||||||
|
|
||||||
|
with open(cal_path) as f:
|
||||||
|
cal = json.load(f)
|
||||||
|
cal["SI"] = si
|
||||||
|
|
||||||
|
cal_dir = cal_path.parent
|
||||||
|
target_path = cal_dir / "target.npy"
|
||||||
|
if not target_path.exists():
|
||||||
|
raise FileNotFoundError(f"target.npy not found at {target_path}")
|
||||||
|
target_states = np.load(str(target_path))
|
||||||
|
|
||||||
|
if scene_type == "karman":
|
||||||
|
from env_karman import KarmanCloakEnv
|
||||||
|
log(f" Creating KarmanCloakEnv (config={config_path.name}, SI={si})...")
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
env = KarmanCloakEnv(
|
||||||
|
device_id=device_id, seed=41,
|
||||||
|
calibration=cal, config_path=str(config_path),
|
||||||
|
target_states=target_states,
|
||||||
|
)
|
||||||
|
log(f" Env ready in {time.perf_counter() - t0:.0f}s")
|
||||||
|
return env
|
||||||
|
elif scene_type == "illusion":
|
||||||
|
from env_illusion import IllusionCloakEnv
|
||||||
|
target_harmonics_path = cal_dir / "target_harmonics.json"
|
||||||
|
if not target_harmonics_path.exists():
|
||||||
|
raise FileNotFoundError(f"target_harmonics.json not found at {target_harmonics_path}")
|
||||||
|
with open(target_harmonics_path) as f:
|
||||||
|
target_harmonics = json.load(f)
|
||||||
|
target_diam = float(scene.get("target_diam", 1.0))
|
||||||
|
cal["target_diam"] = target_diam
|
||||||
|
log(f" Creating IllusionCloakEnv (config={config_path.name}, diam={target_diam}L, SI={si})...")
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
env = IllusionCloakEnv(
|
||||||
|
device_id=device_id, seed=41,
|
||||||
|
calibration=cal, config_path=str(config_path),
|
||||||
|
target_states=target_states,
|
||||||
|
target_harmonics=target_harmonics,
|
||||||
|
target_diam=target_diam,
|
||||||
|
)
|
||||||
|
log(f" Env ready in {time.perf_counter() - t0:.0f}s")
|
||||||
|
return env
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown scene_type: {scene_type}")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Extract policy weights from SB3 .zip
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def _extract_state_dict(zip_path: str) -> dict:
|
||||||
|
with zipfile.ZipFile(zip_path, "r") as zf:
|
||||||
|
with zf.open("policy.pth") as f:
|
||||||
|
return torch.load(io.BytesIO(f.read()), map_location="cpu",
|
||||||
|
weights_only=False)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Raw env extraction (walk wrapper chain)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def _get_raw_env(vec_env):
|
||||||
|
dummy = vec_env.venv
|
||||||
|
inner = dummy.envs[0]
|
||||||
|
if hasattr(inner, 'prob'):
|
||||||
|
return inner.env
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Saving
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def _save_vorticity(env, out_path: Path, scene: Dict[str, Any]) -> None:
|
||||||
|
sim = env.sim
|
||||||
|
macro = sim.get_macroscopic()
|
||||||
|
vort = compute_vorticity(macro["ux"], macro["uy"])
|
||||||
|
nx = int(sim.lbm_cfg.nx)
|
||||||
|
ny = int(sim.lbm_cfg.ny)
|
||||||
|
cylinders = []
|
||||||
|
for body_id in range(sim.bodies.count):
|
||||||
|
body = sim.bodies.get(body_id)
|
||||||
|
if hasattr(body, 'center'):
|
||||||
|
c = body.center
|
||||||
|
r = body.radius if hasattr(body, 'radius') else 10.0
|
||||||
|
cylinders.append(((c[0], c[1]), r))
|
||||||
|
render_vorticity_field(vort, nx=nx, ny=ny, out_path=str(out_path),
|
||||||
|
cylinders=cylinders, vmin=-0.03, vmax=0.03)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Per-seed eval — correct pattern: PPO.load(no env) + VecNormalize(env, training=False)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def _eval_one_seed(env, seed_label: str, seed_dir_str: str,
|
||||||
|
num_steps: int) -> Dict[str, Any]:
|
||||||
|
"""Load model WITHOUT env, wrap env with VecNormalize.load (single layer, frozen)."""
|
||||||
|
seed_dir = Path(seed_dir_str)
|
||||||
|
model_path = seed_dir / "best_model.zip"
|
||||||
|
norm_path = seed_dir.parent / "vec_normalize.pkl"
|
||||||
|
|
||||||
|
if not model_path.exists():
|
||||||
|
log(f" [seed {seed_label}] SKIP: no best_model.zip")
|
||||||
|
return {"seed": seed_label, "avg_reward": -float("inf"), "skip": True}
|
||||||
|
if not norm_path.exists():
|
||||||
|
log(f" [seed {seed_label}] SKIP: no vec_normalize.pkl")
|
||||||
|
return {"seed": seed_label, "avg_reward": -float("inf"), "skip": True}
|
||||||
|
|
||||||
|
log(f" [seed {seed_label}] loading...")
|
||||||
|
|
||||||
|
# All seeds: PPO.load fails (numpy._core.numeric cloudpickle issue).
|
||||||
|
# Use skeleton injection directly — avoids GPU pollution from failed load.
|
||||||
|
wrapped = SymmetryAugmentWrapper(env, prob=0.0, seed=41, rollout_len=2048)
|
||||||
|
vec_env = DummyVecEnv([lambda: wrapped])
|
||||||
|
vec_env = VecNormalize.load(str(norm_path), vec_env)
|
||||||
|
vec_env.norm_reward = False
|
||||||
|
|
||||||
|
skeleton = PPO(
|
||||||
|
"MlpPolicy",
|
||||||
|
policy_kwargs={"activation_fn": Sin, "net_arch": [64, 64]},
|
||||||
|
env=vec_env, device=_device,
|
||||||
|
n_steps=2048, batch_size=64, n_epochs=10,
|
||||||
|
learning_rate=3e-4, gamma=0.995, verbose=0,
|
||||||
|
)
|
||||||
|
sd = _extract_state_dict(str(model_path))
|
||||||
|
skeleton.policy.load_state_dict(sd, strict=False)
|
||||||
|
|
||||||
|
raw_env = _get_raw_env(vec_env)
|
||||||
|
sens_cc = float(raw_env._cal.get("SENSOR_CC", 78.0))
|
||||||
|
return _rollout(skeleton, vec_env, raw_env, sens_cc, seed_label, num_steps)
|
||||||
|
|
||||||
|
|
||||||
|
def _rollout(model, vec_env, raw_env, sens_cc, seed_label, num_steps):
|
||||||
|
"""Deterministic rollout, model.predict receives normalized obs from vec_env."""
|
||||||
|
ep_rew, ep_r_cd, ep_r_cl, ep_r_sim, ep_sim_raw = [], [], [], [], []
|
||||||
|
sig_s, sig_f, sig_a = [], [], []
|
||||||
|
obs = vec_env.reset()
|
||||||
|
|
||||||
|
for step in range(num_steps):
|
||||||
|
action, _ = model.predict(obs, deterministic=True)
|
||||||
|
obs, reward, done, info = vec_env.step(action)
|
||||||
|
|
||||||
|
inf = info[0] if isinstance(info, list) else info
|
||||||
|
ep_rew.append(float(reward[0]))
|
||||||
|
ep_r_cd.append(float(inf.get("r_cd", 0)))
|
||||||
|
ep_r_cl.append(float(inf.get("r_cl", 0)))
|
||||||
|
ep_r_sim.append(float(inf.get("r_sim", 0)))
|
||||||
|
ep_sim_raw.append(float(inf.get("sim", 0)))
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw = raw_env._read_obs()
|
||||||
|
if "Karman" in type(raw_env).__name__:
|
||||||
|
sl = raw[2:14]
|
||||||
|
else:
|
||||||
|
sl = raw[:]
|
||||||
|
sig_s.append(sl[0:6] * sens_cc)
|
||||||
|
sig_f.append(sl[6:12])
|
||||||
|
except Exception:
|
||||||
|
sig_s.append(np.zeros(6, dtype=np.float32))
|
||||||
|
sig_f.append(np.zeros(6, dtype=np.float32))
|
||||||
|
sig_a.append(np.asarray(action, dtype=np.float32).flatten())
|
||||||
|
if done[0]:
|
||||||
|
break
|
||||||
|
|
||||||
|
tail = 180
|
||||||
|
sl = slice(-tail, None) if len(ep_rew) >= tail else slice(None)
|
||||||
|
avg_r = float(np.mean(ep_rew[sl]))
|
||||||
|
|
||||||
|
log(f" reward={avg_r:.4f} r_cd={float(np.mean(ep_r_cd[sl])):.3f} "
|
||||||
|
f"r_cl={float(np.mean(ep_r_cl[sl])):.3f} "
|
||||||
|
f"r_sim={float(np.mean(ep_r_sim[sl])):.3f} "
|
||||||
|
f"sim_raw={float(np.mean(ep_sim_raw[sl])):.3f}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"seed": seed_label,
|
||||||
|
"avg_reward": avg_r,
|
||||||
|
"r_cd": float(np.mean(ep_r_cd[sl])), "r_cl": float(np.mean(ep_r_cl[sl])),
|
||||||
|
"r_sim": float(np.mean(ep_r_sim[sl])),
|
||||||
|
"sim_raw": float(np.mean(ep_sim_raw[sl])),
|
||||||
|
"sensors": np.array(sig_s, dtype=np.float32),
|
||||||
|
"forces": np.array(sig_f, dtype=np.float32),
|
||||||
|
"actions": np.array(sig_a, dtype=np.float32),
|
||||||
|
"rewards": np.array(ep_rew, dtype=np.float32),
|
||||||
|
"skip": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Per-scene pipeline
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def evaluate_train_scene(scene: Dict[str, Any], device_id: int) -> None:
|
||||||
|
scene_id = scene["scene_id"]
|
||||||
|
num_steps = scene["num_steps"]
|
||||||
|
out_dir = _OUT_BASE / scene_id
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
log(f"\n{'='*70}")
|
||||||
|
log(f" Train eval: {scene_id} (SI={scene['si']}, steps={num_steps})")
|
||||||
|
log(f"{'='*70}")
|
||||||
|
|
||||||
|
# Create CFD env once, reused across seeds
|
||||||
|
env = _create_env(scene, device_id)
|
||||||
|
|
||||||
|
# Evaluate each seed (independent skeleton + VecNormalize per seed)
|
||||||
|
all_results = []
|
||||||
|
for seed_label, seed_dir in scene["seeds"]:
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
result = _eval_one_seed(env, seed_label, seed_dir, num_steps)
|
||||||
|
result["_dt"] = time.perf_counter() - t0
|
||||||
|
all_results.append(result)
|
||||||
|
|
||||||
|
# Pick best seed
|
||||||
|
valid = [r for r in all_results if not r.get("skip", False)]
|
||||||
|
if not valid:
|
||||||
|
log(" WARNING: No valid seeds found, skipping scene.")
|
||||||
|
env.close()
|
||||||
|
return
|
||||||
|
best = max(valid, key=lambda r: r["avg_reward"])
|
||||||
|
log(f" Best: seed={best['seed']}, reward={best['avg_reward']:.4f}")
|
||||||
|
|
||||||
|
# Save all-seeds summary
|
||||||
|
summary = [{"seed": r["seed"], "reward": r["avg_reward"],
|
||||||
|
"r_cd": r.get("r_cd", 0), "r_cl": r.get("r_cl", 0),
|
||||||
|
"r_sim": r.get("r_sim", 0), "sim_raw": r.get("sim_raw", 0),
|
||||||
|
"dt_sec": r.get("_dt", 0)} for r in all_results]
|
||||||
|
with open(out_dir / "all_seeds.json", "w") as f:
|
||||||
|
json.dump(summary, f, indent=2)
|
||||||
|
|
||||||
|
# Save best seed's full signals
|
||||||
|
np.savez_compressed(out_dir / "signals.npz",
|
||||||
|
sensors=best["sensors"], forces=best["forces"],
|
||||||
|
actions=best["actions"], rewards=best["rewards"])
|
||||||
|
|
||||||
|
# Vorticity: re-create env, load best seed skeleton, run, capture
|
||||||
|
log(" Generating vorticity for best seed...")
|
||||||
|
env2 = _create_env(scene, device_id)
|
||||||
|
seed_dir = Path(next(s[1] for s in scene["seeds"] if s[0] == best["seed"]))
|
||||||
|
norm_path = seed_dir.parent / "vec_normalize.pkl"
|
||||||
|
model_path = seed_dir / "best_model.zip"
|
||||||
|
|
||||||
|
wrapped2 = SymmetryAugmentWrapper(env2, prob=0.0, seed=41, rollout_len=2048)
|
||||||
|
vec_env2 = DummyVecEnv([lambda: wrapped2])
|
||||||
|
vec_env2 = VecNormalize.load(str(norm_path), vec_env2)
|
||||||
|
vec_env2.norm_reward = False
|
||||||
|
skeleton2 = PPO(
|
||||||
|
"MlpPolicy",
|
||||||
|
policy_kwargs={"activation_fn": Sin, "net_arch": [64, 64]},
|
||||||
|
env=vec_env2, device=_device,
|
||||||
|
n_steps=2048, batch_size=64, n_epochs=10,
|
||||||
|
learning_rate=3e-4, gamma=0.995, verbose=0,
|
||||||
|
)
|
||||||
|
sd = _extract_state_dict(str(model_path))
|
||||||
|
skeleton2.policy.load_state_dict(sd, strict=False)
|
||||||
|
|
||||||
|
obs = vec_env2.reset()
|
||||||
|
for _ in range(num_steps):
|
||||||
|
action, _ = skeleton2.predict(obs, deterministic=True)
|
||||||
|
obs, reward, done, info = vec_env2.step(action)
|
||||||
|
if done[0]:
|
||||||
|
break
|
||||||
|
_save_vorticity(env2, out_dir / "vorticity_controlled.png", scene)
|
||||||
|
env2.close()
|
||||||
|
|
||||||
|
# Target vorticity
|
||||||
|
log(" Generating target vorticity...")
|
||||||
|
_generate_target_vorticity(scene, device_id, out_dir)
|
||||||
|
|
||||||
|
# Zero-action baseline
|
||||||
|
log(" Generating zero-action baseline...")
|
||||||
|
env3 = _create_env(scene, device_id)
|
||||||
|
zero_omega = env3._action_to_omega(np.zeros(3, dtype=np.float32))
|
||||||
|
for _ in range(num_steps):
|
||||||
|
smoothed = env3.smoother(zero_omega)
|
||||||
|
env3._set_omega(smoothed)
|
||||||
|
env3._gpu_block(lambda: env3.sim.run(scene["si"], zero_obs=True))
|
||||||
|
_save_vorticity(env3, out_dir / "vorticity_zero.png", scene)
|
||||||
|
env3.close()
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
metrics = {
|
||||||
|
"best_seed": best["seed"],
|
||||||
|
"dtw_sim_v5": float(best.get("sim_raw", 0)),
|
||||||
|
"reward_mean": float(best["avg_reward"]),
|
||||||
|
"r_cd_mean": float(best.get("r_cd", 0)),
|
||||||
|
"r_cl_mean": float(best.get("r_cl", 0)),
|
||||||
|
"r_sim_mean": float(best.get("r_sim", 0)),
|
||||||
|
"sim_raw_mean": float(best.get("sim_raw", 0)),
|
||||||
|
"aF_mean": float(np.mean(best["actions"][:, 0])),
|
||||||
|
"aB_mean": float(np.mean(best["actions"][:, 1])),
|
||||||
|
"aT_mean": float(np.mean(best["actions"][:, 2])),
|
||||||
|
}
|
||||||
|
with open(out_dir / "metrics.json", "w") as f:
|
||||||
|
json.dump(metrics, f, indent=2)
|
||||||
|
|
||||||
|
env.close()
|
||||||
|
log(f" {scene_id} complete.")
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_target_vorticity(scene: Dict[str, Any], device_id: int,
|
||||||
|
out_dir: Path) -> None:
|
||||||
|
from CelerisLab import Simulation
|
||||||
|
|
||||||
|
scene_type = scene["scene_type"]
|
||||||
|
config_path = Path(scene["config_path"]).resolve()
|
||||||
|
si = scene["si"]
|
||||||
|
warmup = int(4.0 * 2000 / 0.01)
|
||||||
|
L0 = 20.0
|
||||||
|
U0 = 0.01
|
||||||
|
|
||||||
|
sim = Simulation(lbm_config_path=str(config_path), device_id=device_id)
|
||||||
|
|
||||||
|
if scene_type == "karman":
|
||||||
|
with open(scene["calibration_path"]) as f:
|
||||||
|
cal = json.load(f)
|
||||||
|
dist_radius = float(cal.get("dist_radius", 1.0)) * L0
|
||||||
|
nx_cfg = int(cal.get("grid", {}).get("nx", 2000))
|
||||||
|
ny_cfg = int(cal.get("grid", {}).get("ny", 600))
|
||||||
|
CENTER_Y = float(ny_cfg - 1) / 2.0
|
||||||
|
DIST_X = 600.0
|
||||||
|
SENS_X = 1200.0
|
||||||
|
|
||||||
|
sim.add_body("circle", center=(DIST_X, CENTER_Y, 0.0), radius=dist_radius)
|
||||||
|
sim.add_body("sensor", center=(SENS_X, CENTER_Y + 40.0, 0.0), radius=5.0)
|
||||||
|
sim.add_body("sensor", center=(SENS_X, CENTER_Y, 0.0), radius=5.0)
|
||||||
|
sim.add_body("sensor", center=(SENS_X, CENTER_Y - 40.0, 0.0), radius=5.0)
|
||||||
|
elif scene_type == "illusion":
|
||||||
|
with open(scene["calibration_path"]) as f:
|
||||||
|
cal = json.load(f)
|
||||||
|
target_diam = float(scene.get("target_diam", 1.0))
|
||||||
|
nx_cfg = int(cal.get("grid", {}).get("nx", 2000))
|
||||||
|
ny_cfg = int(cal.get("grid", {}).get("ny", 600))
|
||||||
|
CENTER_Y = float(ny_cfg - 1) / 2.0
|
||||||
|
TARGET_X = 400.0
|
||||||
|
SENS_X = 600.0
|
||||||
|
|
||||||
|
sim.add_body("circle", center=(TARGET_X, CENTER_Y, 0.0), radius=target_diam * L0)
|
||||||
|
sim.add_body("sensor", center=(SENS_X, CENTER_Y + 40.0, 0.0), radius=5.0)
|
||||||
|
sim.add_body("sensor", center=(SENS_X, CENTER_Y, 0.0), radius=5.0)
|
||||||
|
sim.add_body("sensor", center=(SENS_X, CENTER_Y - 40.0, 0.0), radius=5.0)
|
||||||
|
else:
|
||||||
|
sim.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
sim.initialize()
|
||||||
|
sim.run(warmup, zero_obs=True)
|
||||||
|
for _ in range(30):
|
||||||
|
sim.run(si, zero_obs=True)
|
||||||
|
|
||||||
|
macro = sim.get_macroscopic()
|
||||||
|
vort = compute_vorticity(macro["ux"], macro["uy"])
|
||||||
|
cylinders = [
|
||||||
|
((ctr[0], ctr[1]), rad)
|
||||||
|
for body in [sim.bodies.get(i) for i in range(sim.bodies.count)]
|
||||||
|
if hasattr(body, 'center')
|
||||||
|
for ctr, rad in [((body.center[0], body.center[1]),
|
||||||
|
body.radius if hasattr(body, 'radius') else 10.0)]
|
||||||
|
]
|
||||||
|
render_vorticity_field(vort, nx=nx_cfg, ny=ny_cfg,
|
||||||
|
out_path=str(out_dir / "vorticity_target.png"),
|
||||||
|
cylinders=cylinders, vmin=-0.03, vmax=0.03)
|
||||||
|
sim.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def main() -> int:
|
||||||
|
global _device
|
||||||
|
parser = argparse.ArgumentParser(description="Evaluate V5 train models")
|
||||||
|
parser.add_argument("--device-id", type=int, default=0)
|
||||||
|
parser.add_argument("--scene", type=str, default=None)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
_device = torch.device(f"cuda:{args.device_id}")
|
||||||
|
log(f"GPU: {args.device_id}, torch device: {_device}")
|
||||||
|
|
||||||
|
scenes = TRAIN_SCENES
|
||||||
|
if args.scene:
|
||||||
|
key = args.scene
|
||||||
|
scenes = [s for s in TRAIN_SCENES if s["scene_id"] == key
|
||||||
|
or s["scene_id"].startswith(key)]
|
||||||
|
if not scenes:
|
||||||
|
log(f"ERROR: No scene matching '{args.scene}'")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
for i, scene in enumerate(scenes):
|
||||||
|
if i > 0:
|
||||||
|
prev_cfg = scenes[i - 1]["config_path"]
|
||||||
|
curr_cfg = scene["config_path"]
|
||||||
|
if prev_cfg != curr_cfg:
|
||||||
|
log(f"Waiting 120s before config switch...")
|
||||||
|
time.sleep(120)
|
||||||
|
|
||||||
|
evaluate_train_scene(scene, args.device_id)
|
||||||
|
|
||||||
|
log("All train scenes complete.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Executable
+126
@@ -0,0 +1,126 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# run_all.sh — Master launcher for eval benchmark.
|
||||||
|
#
|
||||||
|
# GPU 0: Train pipeline (7 scenes)
|
||||||
|
# GPU 1: Reproduce pipeline (7 scenes, 120s stagger)
|
||||||
|
#
|
||||||
|
# Each pipeline saves outputs to eval/output/{train,reproduce}/.
|
||||||
|
# After both complete, run generate_report.py and viz_*.py.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# conda activate pycuda_3_10
|
||||||
|
# bash run_all.sh
|
||||||
|
#
|
||||||
|
# Single scene mode:
|
||||||
|
# bash run_all.sh --train-scene re100 --gpu 0
|
||||||
|
# bash run_all.sh --repro-scene karman_cloak --gpu 1
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||||
|
TRAIN_SCRIPT="$SCRIPT_DIR/infer_train.py"
|
||||||
|
REPRO_SCRIPT="$SCRIPT_DIR/infer_reproduce.py"
|
||||||
|
CONDA_ENV="pycuda_3_10"
|
||||||
|
|
||||||
|
TRAIN_GPU=0
|
||||||
|
REPRO_GPU=1
|
||||||
|
TRAIN_SCENE=""
|
||||||
|
REPRO_SCENE=""
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: $0 [--train-scene SCENE] [--repro-scene SCENE] [--gpu N]"
|
||||||
|
echo " --train-scene Run single train scene (e.g. re100, illusion_1L)"
|
||||||
|
echo " --repro-scene Run single reproduce scene (e.g. karman_cloak)"
|
||||||
|
echo " --gpu GPU device ID"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--train-scene) TRAIN_SCENE="$2"; shift 2 ;;
|
||||||
|
--repro-scene) REPRO_SCENE="$2"; shift 2 ;;
|
||||||
|
--gpu) TRAIN_GPU="$2"; REPRO_GPU="$2"; shift 2 ;;
|
||||||
|
*) echo "Unknown option: $1"; usage ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Create output dirs
|
||||||
|
mkdir -p "$SCRIPT_DIR/output/train"
|
||||||
|
mkdir -p "$SCRIPT_DIR/output/reproduce"
|
||||||
|
|
||||||
|
echo "=== Eval Benchmark ==="
|
||||||
|
echo " Train GPU: $TRAIN_GPU"
|
||||||
|
echo " Reproduce GPU: $REPRO_GPU"
|
||||||
|
echo " Conda env: $CONDA_ENV"
|
||||||
|
echo " Script dir: $SCRIPT_DIR"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Launch Reproduce pipeline (GPU 1, staggered)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
launch_reproduce() {
|
||||||
|
echo "[$(date '+%H:%M:%S')] Launching Reproduce pipeline on GPU $REPRO_GPU..."
|
||||||
|
local arg=""
|
||||||
|
if [[ -n "$REPRO_SCENE" ]]; then
|
||||||
|
arg="--scene $REPRO_SCENE"
|
||||||
|
fi
|
||||||
|
conda run --no-capture-output -n "$CONDA_ENV" python -u \
|
||||||
|
"$REPRO_SCRIPT" --device-id "$REPRO_GPU" $arg \
|
||||||
|
2>&1 | tee "$SCRIPT_DIR/output/reproduce/run.log"
|
||||||
|
echo "[$(date '+%H:%M:%S')] Reproduce pipeline complete."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start reproduce after 120s delay (avoids kernel compilation race with GPU 0)
|
||||||
|
if [[ -z "$TRAIN_SCENE" ]] && [[ -z "$REPRO_SCENE" ]]; then
|
||||||
|
# Full run: launch reproduce in background with delay
|
||||||
|
(sleep 120 && launch_reproduce) &
|
||||||
|
REPRO_PID=$!
|
||||||
|
echo " Reproduce scheduled in 120s (PID=$REPRO_PID)"
|
||||||
|
else
|
||||||
|
# Single scene mode: just run the requested pipeline
|
||||||
|
if [[ -n "$REPRO_SCENE" ]]; then
|
||||||
|
launch_reproduce
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Train pipeline (GPU 0)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "[$(date '+%H:%M:%S')] Launching Train pipeline on GPU $TRAIN_GPU..."
|
||||||
|
TRAIN_ARG=""
|
||||||
|
if [[ -n "$TRAIN_SCENE" ]]; then
|
||||||
|
TRAIN_ARG="--scene $TRAIN_SCENE"
|
||||||
|
fi
|
||||||
|
conda run --no-capture-output -n "$CONDA_ENV" python -u \
|
||||||
|
"$TRAIN_SCRIPT" --device-id "$TRAIN_GPU" $TRAIN_ARG \
|
||||||
|
2>&1 | tee "$SCRIPT_DIR/output/train/run.log"
|
||||||
|
echo "[$(date '+%H:%M:%S')] Train pipeline complete."
|
||||||
|
|
||||||
|
# Wait for reproduce if it was launched
|
||||||
|
if [[ -n "${REPRO_PID:-}" ]]; then
|
||||||
|
echo "[$(date '+%H:%M:%S')] Waiting for Reproduce pipeline (PID=$REPRO_PID)..."
|
||||||
|
wait $REPRO_PID
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Generate reports
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "[$(date '+%H:%M:%S')] Both pipelines complete. Generating reports..."
|
||||||
|
|
||||||
|
echo " -> Signal plots..."
|
||||||
|
conda run --no-capture-output -n "$CONDA_ENV" python -u \
|
||||||
|
"$SCRIPT_DIR/viz_signals.py" 2>&1 | tail -5
|
||||||
|
|
||||||
|
echo " -> Vorticity panels..."
|
||||||
|
conda run --no-capture-output -n "$CONDA_ENV" python -u \
|
||||||
|
"$SCRIPT_DIR/viz_flow.py" 2>&1 | tail -5
|
||||||
|
|
||||||
|
echo " -> Summary report..."
|
||||||
|
conda run --no-capture-output -n "$CONDA_ENV" python -u \
|
||||||
|
"$SCRIPT_DIR/generate_report.py" 2>&1 | tail -10
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== ALL DONE ==="
|
||||||
|
echo "Reports in: $SCRIPT_DIR/output/reports/"
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
"""Scene manifest — single source of truth for all benchmark scenes.
|
||||||
|
|
||||||
|
Two pipelines:
|
||||||
|
TRAIN_SCENES — V5 PPO models on new 2000x600 config (uniform, free-slip).
|
||||||
|
REPRODUCE_SCENES — legacy PPO models on old 1280x512 config (parabolic, bounce-back).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from eval.scene_manifest import TRAIN_SCENES, REPRODUCE_SCENES
|
||||||
|
|
||||||
|
Each scene dict contains:
|
||||||
|
scene_id, config_path, calibration_path, si, num_steps, scene_type
|
||||||
|
seeds: list of (seed_label, model_dir) tuples
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
_REPO = Path(__file__).resolve().parents[3]
|
||||||
|
_TRAIN_DIR = _REPO / "src" / "drl_pinball" / "train"
|
||||||
|
_CONFIGS_DIR = _REPO / "configs"
|
||||||
|
|
||||||
|
KARMAN_CFG = str(_CONFIGS_DIR / "config_lbm_karman_2000x600.json")
|
||||||
|
KARMAN_CFG_RE60 = str(_CONFIGS_DIR / "config_lbm_karman_2000x600_re60.json")
|
||||||
|
KARMAN_CFG_RE200 = str(_CONFIGS_DIR / "config_lbm_karman_2000x600_re200.json")
|
||||||
|
KARMAN_CFG_RE400 = str(_CONFIGS_DIR / "config_lbm_karman_2000x600_re400.json")
|
||||||
|
PINBALL_CFG = str(_CONFIGS_DIR / "config_lbm_pinball.json")
|
||||||
|
|
||||||
|
|
||||||
|
def _model_dir(case_name: str, seed: int) -> str:
|
||||||
|
"""Path to model output directory for a trained case."""
|
||||||
|
return str(_TRAIN_DIR / "output" / f"{case_name}_seed{seed}" / "models")
|
||||||
|
|
||||||
|
|
||||||
|
def _cal_path(name: str) -> str:
|
||||||
|
return str(_TRAIN_DIR / "calibrations" / name / "calibration.json")
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# TRAIN SCENES (V5 PPO, 2000x600)
|
||||||
|
#
|
||||||
|
# Cloud naming: {kar|ill}_{case}_{sc|tr}_seed{N}
|
||||||
|
# sc = scratch, tr = transfer
|
||||||
|
# =============================================================================
|
||||||
|
TRAIN_SCENES: List[Dict[str, Any]] = [
|
||||||
|
# -- Karman Cloak Re100 scratch (5 seeds, best model) --
|
||||||
|
{
|
||||||
|
"scene_id": "kar_re100_sc",
|
||||||
|
"config_path": KARMAN_CFG,
|
||||||
|
"calibration_path": _cal_path("kar_re100"),
|
||||||
|
"si": 800,
|
||||||
|
"num_steps": 360,
|
||||||
|
"scene_type": "karman",
|
||||||
|
"seeds": [
|
||||||
|
("41", _model_dir("kar_re100_sc", 41)),
|
||||||
|
("42", _model_dir("kar_re100_sc", 42)),
|
||||||
|
("43", _model_dir("kar_re100_sc", 43)),
|
||||||
|
("44", _model_dir("kar_re100_sc", 44)),
|
||||||
|
("45", _model_dir("kar_re100_sc", 45)),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
# -- Cross-Re Re60 scratch + transfer --
|
||||||
|
{
|
||||||
|
"scene_id": "kar_re60_tr",
|
||||||
|
"config_path": KARMAN_CFG_RE60,
|
||||||
|
"calibration_path": _cal_path("kar_re60"),
|
||||||
|
"si": 800,
|
||||||
|
"num_steps": 360,
|
||||||
|
"scene_type": "karman",
|
||||||
|
"seeds": [
|
||||||
|
("43", _model_dir("kar_re60_tr", 43)),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
# -- Cross-Re Re200 scratch + transfer --
|
||||||
|
{
|
||||||
|
"scene_id": "kar_re200_tr",
|
||||||
|
"config_path": KARMAN_CFG_RE200,
|
||||||
|
"calibration_path": _cal_path("kar_re200"),
|
||||||
|
"si": 500,
|
||||||
|
"num_steps": 360,
|
||||||
|
"scene_type": "karman",
|
||||||
|
"seeds": [
|
||||||
|
("43", _model_dir("kar_re200_tr", 43)),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
# -- Cross-Re Re400 scratch + transfer --
|
||||||
|
{
|
||||||
|
"scene_id": "kar_re400_tr",
|
||||||
|
"config_path": KARMAN_CFG_RE400,
|
||||||
|
"calibration_path": _cal_path("kar_re400"),
|
||||||
|
"si": 400,
|
||||||
|
"num_steps": 360,
|
||||||
|
"scene_type": "karman",
|
||||||
|
"seeds": [
|
||||||
|
("43", _model_dir("kar_re400_tr", 43)),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
# -- VarDist d075 scratch --
|
||||||
|
{
|
||||||
|
"scene_id": "kar_d075_sc",
|
||||||
|
"config_path": KARMAN_CFG,
|
||||||
|
"calibration_path": _cal_path("kar_d075_sc"),
|
||||||
|
"si": 800,
|
||||||
|
"num_steps": 360,
|
||||||
|
"scene_type": "karman",
|
||||||
|
"seeds": [
|
||||||
|
("44", _model_dir("kar_d075_sc", 44)),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
# -- VarDist d15 scratch --
|
||||||
|
{
|
||||||
|
"scene_id": "kar_d15_sc",
|
||||||
|
"config_path": KARMAN_CFG,
|
||||||
|
"calibration_path": _cal_path("kar_d15_sc"),
|
||||||
|
"si": 800,
|
||||||
|
"num_steps": 360,
|
||||||
|
"scene_type": "karman",
|
||||||
|
"seeds": [
|
||||||
|
("45", _model_dir("kar_d15_sc", 45)),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
# -- VarDist d2 scratch --
|
||||||
|
{
|
||||||
|
"scene_id": "kar_d2_sc",
|
||||||
|
"config_path": KARMAN_CFG,
|
||||||
|
"calibration_path": _cal_path("kar_d2_sc"),
|
||||||
|
"si": 800,
|
||||||
|
"num_steps": 360,
|
||||||
|
"scene_type": "karman",
|
||||||
|
"seeds": [
|
||||||
|
("45", _model_dir("kar_d2_sc", 45)),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
# -- Illusion 0.75L scratch --
|
||||||
|
{
|
||||||
|
"scene_id": "ill_075L_sc",
|
||||||
|
"config_path": KARMAN_CFG,
|
||||||
|
"calibration_path": _cal_path("ill_075L"),
|
||||||
|
"si": 400,
|
||||||
|
"num_steps": 360,
|
||||||
|
"scene_type": "illusion",
|
||||||
|
"target_diam": 0.75,
|
||||||
|
"seeds": [
|
||||||
|
("43", _model_dir("ill_075L_sc", 43)),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
# -- Illusion 1.0L scratch --
|
||||||
|
{
|
||||||
|
"scene_id": "ill_1L_sc",
|
||||||
|
"config_path": KARMAN_CFG,
|
||||||
|
"calibration_path": _cal_path("ill_1L"),
|
||||||
|
"si": 600,
|
||||||
|
"num_steps": 360,
|
||||||
|
"scene_type": "illusion",
|
||||||
|
"target_diam": 1.0,
|
||||||
|
"seeds": [
|
||||||
|
("43", _model_dir("ill_1L_sc", 43)),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
# -- Illusion 1.5L scratch --
|
||||||
|
{
|
||||||
|
"scene_id": "ill_15L_sc",
|
||||||
|
"config_path": KARMAN_CFG,
|
||||||
|
"calibration_path": _cal_path("ill_15L"),
|
||||||
|
"si": 800,
|
||||||
|
"num_steps": 360,
|
||||||
|
"scene_type": "illusion",
|
||||||
|
"target_diam": 1.5,
|
||||||
|
"seeds": [
|
||||||
|
("43", _model_dir("ill_15L_sc", 43)),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
# -- Illusion 2.0L scratch (new!) --
|
||||||
|
{
|
||||||
|
"scene_id": "ill_2L_sc",
|
||||||
|
"config_path": KARMAN_CFG,
|
||||||
|
"calibration_path": _cal_path("ill_2L"),
|
||||||
|
"si": 800,
|
||||||
|
"num_steps": 360,
|
||||||
|
"scene_type": "illusion",
|
||||||
|
"target_diam": 2.0,
|
||||||
|
"seeds": [
|
||||||
|
("43", _model_dir("ill_2L_sc", 43)),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# REPRODUCE SCENES (legacy PPO, 1280x512)
|
||||||
|
# =============================================================================
|
||||||
|
REPRODUCE_SCENES: List[Dict[str, Any]] = [
|
||||||
|
{
|
||||||
|
"scene_id": "karman_cloak",
|
||||||
|
"config_path": PINBALL_CFG,
|
||||||
|
"si": 800,
|
||||||
|
"num_steps": 200,
|
||||||
|
"scene_type": "karman",
|
||||||
|
"model_name": "d1a3o12_re100",
|
||||||
|
"action_scale": 8.0,
|
||||||
|
"action_bias": (0.0, -4.0, 4.0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scene_id": "steady_cloak",
|
||||||
|
"config_path": PINBALL_CFG,
|
||||||
|
"si": 800,
|
||||||
|
"num_steps": 200,
|
||||||
|
"scene_type": "steady",
|
||||||
|
"model_name": None, # open-loop [0, -5.1, 5.1]*U0
|
||||||
|
"action_scale": 8.0,
|
||||||
|
"action_bias": (0.0, -4.0, 4.0),
|
||||||
|
"open_loop_surf_vel": (0.0, -5.1, 5.1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scene_id": "illusion_075L",
|
||||||
|
"config_path": PINBALL_CFG,
|
||||||
|
"si": 400,
|
||||||
|
"num_steps": 200,
|
||||||
|
"scene_type": "illusion",
|
||||||
|
"model_name": "d1a3o14_250525_imit_075L_2U_400S",
|
||||||
|
"action_scale": 8.0,
|
||||||
|
"action_bias": (0.0, -2.0, 2.0),
|
||||||
|
"target_diam": 0.75,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scene_id": "illusion_1L",
|
||||||
|
"config_path": PINBALL_CFG,
|
||||||
|
"si": 600,
|
||||||
|
"num_steps": 200,
|
||||||
|
"scene_type": "illusion",
|
||||||
|
"model_name": "d1a3o14_250525_imit_1L_2U_600S",
|
||||||
|
"action_scale": 8.0,
|
||||||
|
"action_bias": (0.0, -2.0, 2.0),
|
||||||
|
"target_diam": 1.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scene_id": "illusion_15L",
|
||||||
|
"config_path": PINBALL_CFG,
|
||||||
|
"si": 800,
|
||||||
|
"num_steps": 200,
|
||||||
|
"scene_type": "illusion",
|
||||||
|
"model_name": "d1a3o14_250525_imit_15L_2U",
|
||||||
|
"action_scale": 8.0,
|
||||||
|
"action_bias": (0.0, -2.0, 2.0),
|
||||||
|
"target_diam": 1.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scene_id": "vortex_lamb",
|
||||||
|
"config_path": PINBALL_CFG,
|
||||||
|
"si": 800,
|
||||||
|
"num_steps": 150,
|
||||||
|
"scene_type": "vortex",
|
||||||
|
"model_name": "vortex_lamb",
|
||||||
|
"action_scale": 4.0,
|
||||||
|
"action_bias": (0.0, -4.0, 4.0),
|
||||||
|
"vortex_type": "lamb",
|
||||||
|
"vortex_strength_factor": 0.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scene_id": "vortex_taylor",
|
||||||
|
"config_path": PINBALL_CFG,
|
||||||
|
"si": 800,
|
||||||
|
"num_steps": 150,
|
||||||
|
"scene_type": "vortex",
|
||||||
|
"model_name": "vortex_taylor",
|
||||||
|
"action_scale": 4.0,
|
||||||
|
"action_bias": (0.0, -4.0, 4.0),
|
||||||
|
"vortex_type": "taylor",
|
||||||
|
"vortex_strength_factor": 0.03,
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Flow field visualization — vorticity panels and error maps.
|
||||||
|
|
||||||
|
Works on pre-saved vorticity PNGs or raw Macroscopic data.
|
||||||
|
All panels use unified [-0.03, 0.03] vorticity range.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
conda run -n pycuda_3_10 python viz_flow.py [--scene re100]
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use("Agg")
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
_OUT_BASE = Path(__file__).resolve().parent / "output"
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg: str) -> None:
|
||||||
|
print(f"[viz_flow] {msg}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _find_vorticity_files(scene_dir: Path) -> dict:
|
||||||
|
"""Scan scene output dir for vorticity images. Returns dict keyed by label."""
|
||||||
|
files = {}
|
||||||
|
for name in ["vorticity_target", "vorticity_controlled", "vorticity_zero",
|
||||||
|
"vorticity_uncontrolled"]:
|
||||||
|
png = scene_dir / f"{name}.png"
|
||||||
|
if png.exists():
|
||||||
|
files[name.replace("vorticity_", "")] = png
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
def make_comparison_panel(scene_name: str, train_dir: Optional[Path],
|
||||||
|
repro_dir: Optional[Path], out_path: Path) -> None:
|
||||||
|
"""Create a multi-panel vorticity comparison for one scene.
|
||||||
|
|
||||||
|
Layout: rows = [repro, train], cols = [target, controlled, zero]
|
||||||
|
"""
|
||||||
|
panels: List[Tuple[str, Path]] = []
|
||||||
|
|
||||||
|
if repro_dir and repro_dir.exists():
|
||||||
|
rep_files = _find_vorticity_files(repro_dir)
|
||||||
|
for label, p in rep_files.items():
|
||||||
|
panels.append((f"Repro-{label}", p))
|
||||||
|
|
||||||
|
if train_dir and train_dir.exists():
|
||||||
|
trn_files = _find_vorticity_files(train_dir)
|
||||||
|
for label, p in trn_files.items():
|
||||||
|
panels.append((f"Train-{label}", p))
|
||||||
|
|
||||||
|
if not panels:
|
||||||
|
log(f" {scene_name}: no vorticity images found, skipping")
|
||||||
|
return
|
||||||
|
|
||||||
|
n = len(panels)
|
||||||
|
cols = min(3, n)
|
||||||
|
rows = (n + cols - 1) // cols
|
||||||
|
|
||||||
|
fig, axes = plt.subplots(rows, cols, figsize=(6 * cols, 4.5 * rows))
|
||||||
|
if rows * cols == 1:
|
||||||
|
axes = np.array([[axes]])
|
||||||
|
elif rows == 1:
|
||||||
|
axes = axes.reshape(1, -1)
|
||||||
|
elif cols == 1:
|
||||||
|
axes = axes.reshape(-1, 1)
|
||||||
|
|
||||||
|
for idx, (label, img_path) in enumerate(panels):
|
||||||
|
r, c = divmod(idx, cols)
|
||||||
|
ax = axes[r, c]
|
||||||
|
ax.imshow(plt.imread(str(img_path)))
|
||||||
|
ax.set_title(label, fontsize=10)
|
||||||
|
ax.axis("off")
|
||||||
|
|
||||||
|
# Hide unused axes
|
||||||
|
for idx in range(len(panels), rows * cols):
|
||||||
|
r, c = divmod(idx, cols)
|
||||||
|
axes[r, c].axis("off")
|
||||||
|
|
||||||
|
fig.suptitle(f"{scene_name} - Vorticity Comparison", fontsize=14)
|
||||||
|
fig.tight_layout()
|
||||||
|
fig.savefig(out_path, dpi=150, bbox_inches="tight")
|
||||||
|
plt.close(fig)
|
||||||
|
log(f" {scene_name}: {n} panels saved to {out_path.name}")
|
||||||
|
|
||||||
|
|
||||||
|
def make_side_by_side(scene_name: str, train_dir: Optional[Path],
|
||||||
|
repro_dir: Optional[Path], out_path: Path) -> None:
|
||||||
|
"""Simple side-by-side: repro controlled | train controlled."""
|
||||||
|
images = []
|
||||||
|
labels = []
|
||||||
|
for label, d in [("Reproduce", repro_dir), ("Train", train_dir)]:
|
||||||
|
if d is None or not d.exists():
|
||||||
|
continue
|
||||||
|
for name in ["controlled", "zero"]:
|
||||||
|
png = d / f"vorticity_{name}.png"
|
||||||
|
if png.exists():
|
||||||
|
images.append(png)
|
||||||
|
labels.append(f"{label} ({name})")
|
||||||
|
break # take first available
|
||||||
|
|
||||||
|
if len(images) < 2:
|
||||||
|
return
|
||||||
|
|
||||||
|
fig, axes = plt.subplots(1, len(images), figsize=(6 * len(images), 4.5))
|
||||||
|
if len(images) == 1:
|
||||||
|
axes = [axes]
|
||||||
|
for ax, img_path, label in zip(axes, images, labels):
|
||||||
|
ax.imshow(plt.imread(str(img_path)))
|
||||||
|
ax.set_title(label, fontsize=11)
|
||||||
|
ax.axis("off")
|
||||||
|
fig.suptitle(f"{scene_name}", fontsize=13)
|
||||||
|
fig.tight_layout()
|
||||||
|
fig.savefig(out_path, dpi=150, bbox_inches="tight")
|
||||||
|
plt.close(fig)
|
||||||
|
log(f" {scene_name}: side-by-side saved")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Vorticity visualization")
|
||||||
|
parser.add_argument("--scene", type=str, default=None,
|
||||||
|
help="Single scene to process")
|
||||||
|
parser.add_argument("--side", type=str, default="all",
|
||||||
|
choices=["all", "train", "reproduce"],
|
||||||
|
help="Which pipeline to render")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
report_dir = _OUT_BASE / "reports" / "vorticity_panels"
|
||||||
|
report_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Scene names to process
|
||||||
|
train_scenes = ["re100_karman", "transfer_re60", "transfer_re200",
|
||||||
|
"transfer_re400", "illusion_075L", "illusion_1L", "illusion_15L"]
|
||||||
|
repro_scenes = ["karman_cloak", "steady_cloak", "illusion_075L",
|
||||||
|
"illusion_1L", "illusion_15L", "vortex_lamb", "vortex_taylor"]
|
||||||
|
|
||||||
|
# Map train to repro for overlap
|
||||||
|
overlap_map = {
|
||||||
|
"re100_karman": "karman_cloak",
|
||||||
|
"illusion_075L": "illusion_075L",
|
||||||
|
"illusion_1L": "illusion_1L",
|
||||||
|
"illusion_15L": "illusion_15L",
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.side in ("all", "train"):
|
||||||
|
for scene in train_scenes:
|
||||||
|
if args.scene and scene != args.scene and not scene.startswith(args.scene):
|
||||||
|
continue
|
||||||
|
train_dir = _OUT_BASE / "train" / scene
|
||||||
|
repro_dir = _OUT_BASE / "reproduce" / overlap_map.get(scene, "")
|
||||||
|
make_comparison_panel(scene, train_dir,
|
||||||
|
repro_dir if repro_dir.exists() else None,
|
||||||
|
report_dir / f"vorticity_{scene}.png")
|
||||||
|
|
||||||
|
if args.side in ("all", "reproduce"):
|
||||||
|
for scene in repro_scenes:
|
||||||
|
if args.scene and scene != args.scene:
|
||||||
|
continue
|
||||||
|
if scene in overlap_map.values():
|
||||||
|
continue # already covered above
|
||||||
|
repro_dir = _OUT_BASE / "reproduce" / scene
|
||||||
|
make_comparison_panel(scene, None, repro_dir,
|
||||||
|
report_dir / f"vorticity_{scene}.png")
|
||||||
|
|
||||||
|
# Summary: side-by-side for overlapping scenes
|
||||||
|
for scene, rep_key in overlap_map.items():
|
||||||
|
if args.scene and scene != args.scene and not scene.startswith(args.scene):
|
||||||
|
continue
|
||||||
|
train_dir = _OUT_BASE / "train" / scene
|
||||||
|
repro_dir = _OUT_BASE / "reproduce" / rep_key
|
||||||
|
make_side_by_side(scene, train_dir, repro_dir,
|
||||||
|
report_dir / f"vorticity_compare_{scene}.png")
|
||||||
|
|
||||||
|
log(f"Reports in {report_dir}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Signal visualisation — obs/action timeseries, DTW, FFT spectrum.
|
||||||
|
|
||||||
|
Reads signals.npz and target.npz from scene output dirs.
|
||||||
|
Produces per-scene diagnostic plots.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
conda run -n pycuda_3_10 python viz_signals.py [--scene re100] [--side train]
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use("Agg")
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
_OUT_BASE = Path(__file__).resolve().parent / "output"
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg: str) -> None:
|
||||||
|
print(f"[viz_signals] {msg}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Per-scene plot
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def plot_scene_signals(scene_name: str, signals_path: Path,
|
||||||
|
target_path: Optional[Path],
|
||||||
|
out_dir: Path) -> None:
|
||||||
|
"""Full signal diagnostic for one scene."""
|
||||||
|
if not signals_path.exists():
|
||||||
|
log(f" {scene_name}: signals.npz not found at {signals_path}, skipping")
|
||||||
|
return
|
||||||
|
|
||||||
|
data = np.load(signals_path, allow_pickle=True)
|
||||||
|
sensors = data.get("sensors")
|
||||||
|
forces = data.get("forces")
|
||||||
|
actions = data.get("actions")
|
||||||
|
rewards = data.get("rewards")
|
||||||
|
|
||||||
|
fig = plt.figure(figsize=(16, 18))
|
||||||
|
|
||||||
|
n_channels = 6
|
||||||
|
ch_names_s = ["s0_ux", "s0_uy", "s1_ux", "s1_uy", "s2_ux", "s2_uy"]
|
||||||
|
ch_names_f = ["F_fx", "F_fy", "T_fx", "T_fy", "B_fx", "B_fy"]
|
||||||
|
|
||||||
|
# 1. Sensor timeseries
|
||||||
|
ax1 = fig.add_subplot(4, 2, 1)
|
||||||
|
if sensors is not None and sensors.shape[1] >= 6:
|
||||||
|
for i in range(6):
|
||||||
|
ax1.plot(sensors[:, i], lw=0.6, label=ch_names_s[i])
|
||||||
|
ax1.set_title("Sensors (raw, 6 channels)")
|
||||||
|
ax1.set_xlabel("Step"); ax1.legend(loc="upper right", fontsize=6, ncol=2)
|
||||||
|
ax1.grid(alpha=0.3)
|
||||||
|
|
||||||
|
# 2. Sensor uy channels only (for visual clarity)
|
||||||
|
ax2 = fig.add_subplot(4, 2, 2)
|
||||||
|
if sensors is not None and sensors.shape[1] >= 6:
|
||||||
|
for idx, lbl in [(1, "s0_uy"), (3, "s1_uy"), (5, "s2_uy")]:
|
||||||
|
ax2.plot(sensors[:, idx], lw=0.8, label=lbl)
|
||||||
|
ax2.set_title("Sensors (uy channels)")
|
||||||
|
ax2.set_xlabel("Step"); ax2.legend(fontsize=7)
|
||||||
|
ax2.grid(alpha=0.3)
|
||||||
|
|
||||||
|
# 3. Force timeseries
|
||||||
|
ax3 = fig.add_subplot(4, 2, 3)
|
||||||
|
if forces is not None and forces.shape[1] >= 6:
|
||||||
|
for i in range(6):
|
||||||
|
ax3.plot(forces[:, i], lw=0.6, label=ch_names_f[i])
|
||||||
|
ax3.set_title("Forces (raw, 6 channels)")
|
||||||
|
ax3.set_xlabel("Step"); ax3.legend(loc="upper right", fontsize=6, ncol=2)
|
||||||
|
ax3.grid(alpha=0.3)
|
||||||
|
|
||||||
|
# 4. Net force
|
||||||
|
ax4 = fig.add_subplot(4, 2, 4)
|
||||||
|
if forces is not None and forces.shape[1] >= 6:
|
||||||
|
cd = (forces[:, 0] + forces[:, 2] + forces[:, 4]) / 3.0
|
||||||
|
cl = (forces[:, 1] + forces[:, 3] + forces[:, 5]) / 3.0
|
||||||
|
ax4.plot(cd, lw=1, label="Cd (net)", color="tab:red")
|
||||||
|
ax4.plot(cl, lw=1, label="Cl (net)", color="tab:blue")
|
||||||
|
ax4.set_title("Net force (Cd, Cl)")
|
||||||
|
ax4.set_xlabel("Step"); ax4.legend(); ax4.grid(alpha=0.3)
|
||||||
|
|
||||||
|
# 5. Actions
|
||||||
|
ax5 = fig.add_subplot(4, 2, 5)
|
||||||
|
if actions is not None and actions.shape[1] >= 3:
|
||||||
|
ax5.plot(actions[:, 0], lw=0.8, label="aF (front)")
|
||||||
|
ax5.plot(actions[:, 1], lw=0.8, label="aB (bottom)")
|
||||||
|
ax5.plot(actions[:, 2], lw=0.8, label="aT (top)")
|
||||||
|
ax5.set_title("Actions [-1, 1]")
|
||||||
|
ax5.set_xlabel("Step"); ax5.legend(); ax5.grid(alpha=0.3)
|
||||||
|
ax5.set_ylim(-1.05, 1.05)
|
||||||
|
|
||||||
|
# 6. Reward (if available)
|
||||||
|
ax6 = fig.add_subplot(4, 2, 6)
|
||||||
|
if rewards is not None and len(rewards) > 0:
|
||||||
|
ax6.plot(rewards, lw=1, color="tab:green", label="reward")
|
||||||
|
ax6.axhline(y=np.mean(rewards[-180:]) if len(rewards) >= 180 else np.mean(rewards),
|
||||||
|
color="green", ls="--", lw=1, label="avg (tail)")
|
||||||
|
ax6.set_title("Reward")
|
||||||
|
ax6.set_xlabel("Step"); ax6.legend(); ax6.grid(alpha=0.3)
|
||||||
|
|
||||||
|
# 7. FFT spectrum (controlled sensor uy vs target)
|
||||||
|
ax7 = fig.add_subplot(4, 2, 7)
|
||||||
|
if sensors is not None and sensors.shape[1] > 3:
|
||||||
|
ax7, _ = _plot_fft(ax7, sensors[:, 3], label="Controlled s1_uy",
|
||||||
|
color="tab:blue")
|
||||||
|
if target_path and target_path.exists():
|
||||||
|
tgt = np.load(target_path, allow_pickle=True)
|
||||||
|
tgt_data = tgt.get("target_states", tgt.get("sensors"))
|
||||||
|
if tgt_data is not None and tgt_data.shape[1] > 3:
|
||||||
|
_plot_fft(ax7, tgt_data[:, 3], label="Target s1_uy",
|
||||||
|
color="tab:red", linestyle="--")
|
||||||
|
ax7.set_title("FFT: Center sensor uy")
|
||||||
|
ax7.legend(fontsize=7); ax7.grid(alpha=0.3)
|
||||||
|
|
||||||
|
# 8. Action histogram
|
||||||
|
ax8 = fig.add_subplot(4, 2, 8)
|
||||||
|
if actions is not None and actions.shape[1] >= 3:
|
||||||
|
tail = slice(-180, None) if actions.shape[0] >= 180 else slice(None)
|
||||||
|
ax8.hist(actions[tail, 0], bins=20, alpha=0.5, label="aF", color="tab:red")
|
||||||
|
ax8.hist(actions[tail, 1], bins=20, alpha=0.5, label="aB", color="tab:orange")
|
||||||
|
ax8.hist(actions[tail, 2], bins=20, alpha=0.5, label="aT", color="tab:green")
|
||||||
|
ax8.set_title("Action distribution (tail 180 steps)")
|
||||||
|
ax8.legend(fontsize=7); ax8.grid(alpha=0.3)
|
||||||
|
|
||||||
|
fig.suptitle(f"{scene_name} - Signal Analysis", fontsize=14)
|
||||||
|
fig.tight_layout()
|
||||||
|
fig.savefig(out_dir / f"signals_{scene_name}.png", dpi=150, bbox_inches="tight")
|
||||||
|
plt.close(fig)
|
||||||
|
log(f" {scene_name}: signal plot saved")
|
||||||
|
|
||||||
|
|
||||||
|
def _plot_fft(ax, signal, label="", color="tab:blue", linestyle="-"):
|
||||||
|
"""Plot single-sided FFT magnitude spectrum."""
|
||||||
|
n = len(signal)
|
||||||
|
sig = np.asarray(signal, dtype=np.float64)
|
||||||
|
sig = sig - np.mean(sig)
|
||||||
|
fft = np.abs(np.fft.rfft(sig)) / n
|
||||||
|
freqs = np.fft.rfftfreq(n, d=1)
|
||||||
|
ax.plot(freqs[1:], fft[1:], lw=1, color=color, ls=linestyle, label=label)
|
||||||
|
ax.set_xlim(0, 0.5)
|
||||||
|
ax.set_xlabel("Frequency (1/step)")
|
||||||
|
return ax, freqs, fft
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Cross-scene comparison: DTW / reward bars
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def plot_cross_scene_summary(scene_dirs: dict, out_dir: Path) -> None:
|
||||||
|
"""Bar chart comparing key metrics across scenes."""
|
||||||
|
import json
|
||||||
|
|
||||||
|
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
|
||||||
|
|
||||||
|
names = list(scene_dirs.keys())
|
||||||
|
dtw_vals = []
|
||||||
|
reward_vals = []
|
||||||
|
for name, d in scene_dirs.items():
|
||||||
|
m_path = d / "metrics.json"
|
||||||
|
if m_path.exists():
|
||||||
|
with open(m_path) as f:
|
||||||
|
m = json.load(f)
|
||||||
|
dtw_vals.append(m.get("dtw_sim_v5", m.get("sim_raw_mean", 0)))
|
||||||
|
reward_vals.append(m.get("reward_mean", 0))
|
||||||
|
else:
|
||||||
|
dtw_vals.append(0)
|
||||||
|
reward_vals.append(0)
|
||||||
|
|
||||||
|
ax = axes[0]
|
||||||
|
bars = ax.bar(names, dtw_vals, color="tab:blue", alpha=0.7)
|
||||||
|
for b, v in zip(bars, dtw_vals):
|
||||||
|
if v > 0:
|
||||||
|
ax.text(b.get_x() + b.get_width() / 2, b.get_height() + 0.01,
|
||||||
|
f"{v:.3f}", ha="center", fontsize=8)
|
||||||
|
ax.set_title("DTW Similarity (V5)")
|
||||||
|
ax.set_ylim(0, 1.1)
|
||||||
|
ax.tick_params(axis="x", rotation=30)
|
||||||
|
ax.grid(axis="y", alpha=0.3)
|
||||||
|
|
||||||
|
ax = axes[1]
|
||||||
|
bars = ax.bar(names, reward_vals, color="tab:green", alpha=0.7)
|
||||||
|
for b, v in zip(bars, reward_vals):
|
||||||
|
if v > 0:
|
||||||
|
ax.text(b.get_x() + b.get_width() / 2, b.get_height() + 0.01,
|
||||||
|
f"{v:.3f}", ha="center", fontsize=8)
|
||||||
|
ax.set_title("Average Reward (tail 180)")
|
||||||
|
ax.tick_params(axis="x", rotation=30)
|
||||||
|
ax.grid(axis="y", alpha=0.3)
|
||||||
|
|
||||||
|
fig.suptitle("Cross-Scene Summary")
|
||||||
|
fig.tight_layout()
|
||||||
|
fig.savefig(out_dir / "cross_scene_summary.png", dpi=150, bbox_inches="tight")
|
||||||
|
plt.close(fig)
|
||||||
|
log(f" Cross-scene summary saved")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Signal visualization")
|
||||||
|
parser.add_argument("--scene", type=str, default=None)
|
||||||
|
parser.add_argument("--side", type=str, default="all",
|
||||||
|
choices=["all", "train", "reproduce"])
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
report_dir = _OUT_BASE / "reports" / "signal_plots"
|
||||||
|
report_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
for side_name, side_key in [("train", "train"), ("reproduce", "reproduce")]:
|
||||||
|
if args.side != "all" and args.side != side_key:
|
||||||
|
continue
|
||||||
|
side_dir = _OUT_BASE / side_key
|
||||||
|
if not side_dir.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
scene_dirs_for_summary = {}
|
||||||
|
for scene_dir in sorted(side_dir.iterdir()):
|
||||||
|
if not scene_dir.is_dir():
|
||||||
|
continue
|
||||||
|
scene_name = scene_dir.name
|
||||||
|
if args.scene and scene_name != args.scene and not scene_name.startswith(args.scene):
|
||||||
|
continue
|
||||||
|
|
||||||
|
sig_path = scene_dir / "signals.npz"
|
||||||
|
tgt_paths = [scene_dir / "target.npz",
|
||||||
|
scene_dir / ".." / ".." / ".." / "calibrations" / "re100" / "target.npy"]
|
||||||
|
tgt_found = None
|
||||||
|
for tp in tgt_paths:
|
||||||
|
if tp.exists():
|
||||||
|
tgt_found = tp
|
||||||
|
break
|
||||||
|
|
||||||
|
plot_scene_signals(f"{side_key}_{scene_name}", sig_path, tgt_found,
|
||||||
|
report_dir)
|
||||||
|
scene_dirs_for_summary[f"{side_key}_{scene_name}"] = scene_dir
|
||||||
|
|
||||||
|
if scene_dirs_for_summary:
|
||||||
|
plot_cross_scene_summary(scene_dirs_for_summary, report_dir)
|
||||||
|
|
||||||
|
log(f"Reports in {report_dir}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,388 @@
|
|||||||
|
# Cross-Re Transfer Learning — 分析报告
|
||||||
|
|
||||||
|
> 日期: 2026-07-07
|
||||||
|
> 实验: 改变上游来流雷诺数 (Re60 / Re200 / Re400),从 re100 baseline 进行 transfer learning
|
||||||
|
> 实验阶段: base (crossre_transfer.sh, lr=3e-4) → extend (extend_transfer.sh, lr=1e-4) → ext2 (ext2_transfer.sh, lr=1e-4)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 实验配置总览
|
||||||
|
|
||||||
|
### 1.1 共享超参数
|
||||||
|
|
||||||
|
| 参数 | base (crossre_transfer) | extend / ext2 |
|
||||||
|
|------|:----------------------:|:-------------:|
|
||||||
|
| PPO n_steps | 2048 | 2048 |
|
||||||
|
| PPO batch_size | 64 | 64 |
|
||||||
|
| **PPO n_epochs** | **10** | **5** ← 减半 |
|
||||||
|
| **Learning rate** | **3e-4** | **1e-4** ← 降为 1/3 |
|
||||||
|
| Gamma | 0.995 | 0.995 |
|
||||||
|
| Policy net_arch | [64, 64] | [64, 64] |
|
||||||
|
| Activation | sin | sin |
|
||||||
|
| Symmetry augmentation | prob=0.5 | prob=0.5 |
|
||||||
|
| Episode 数 | 200 (base) | 200-400 (extend / ext2) |
|
||||||
|
| 基座模型 | re100_karman_seed43 (best≈0.92) | 各自 base/ext 的 best_model |
|
||||||
|
|
||||||
|
> **关键差异**: extend/ext2 相比 base 将 n_epochs 从 10 降到 5、lr 从 3e-4 降到 1e-4,**综合保守程度约为 base 的 6 倍**。原始意图是 fine-tune 更稳定,但实际效果适得其反。
|
||||||
|
|
||||||
|
### 1.2 校准参数差异(核心问题所在)
|
||||||
|
|
||||||
|
| 参数 | re60 | re100 (base) | re200 | re400 |
|
||||||
|
|------|:---:|:------------:|:-----:|:-----:|
|
||||||
|
| **SI** | 800 | 800 | 500 | 400 |
|
||||||
|
| **FORCE_SCALE** | 0.0021 | 0.0024 | 0.0026 | 0.0042 |
|
||||||
|
| **SENS_SCALE** | 0.72 | 0.75 | 0.90 | 0.98 |
|
||||||
|
| **dtw_norm_scale** | 0.107 | 0.204 | 0.269 | 0.310 |
|
||||||
|
| **SIM_BP[1]** (zero rot) | 0.41 | 0.32 | 0.45 | 0.56 |
|
||||||
|
| **SIM_BP[2]** (ref rot) | 0.61 | 0.82 | 0.77 | 0.73 |
|
||||||
|
| **SIM_BP gap** (spread) | **0.20** | 0.50 | **0.32** | **0.17** |
|
||||||
|
| **Δrev / Δsim 梯度比** | **2.5x** | 1.0x | **1.6x** | **2.9x** |
|
||||||
|
| **SIM_BP 来源** | 实测 | 实测 | 实测 | 实测 |
|
||||||
|
| **K_CD / K_CL** | **12/25** | 50/100 | **12/25** | **12/25** |
|
||||||
|
|
||||||
|
> **关键发现 1**: re60 (gap=0.20) 和 re400 (gap=0.17) 的 SIM_BP gap 远小于 re100 基准的 0.50。
|
||||||
|
> **关键发现 2**: re60/re200/re400 的 K_CD/K_CL = 12/25,只有 re100 的 1/4。这意味着跨 Re 迁移后 drag/lift 惩罚显著放宽,改变了 reward landscape。
|
||||||
|
> **关键发现 3**: calibrate.py 的 generic SIM_BP 兜底条件(`spread<0.10 或 better_sim<0.5`)对 re60/re200/re400 全部未触发。
|
||||||
|
|
||||||
|
### 1.3 SIM_BP 映射机制的数学解释
|
||||||
|
|
||||||
|
calibrate.py 将实测的 DTW similarity 范围 `[worst_sim, better_sim]` 固定映射到 reward 值 `[0.2, 0.5]`:
|
||||||
|
|
||||||
|
```
|
||||||
|
r_sim = piecewise_map(measured_sim, SIM_BP=[0, w, b, ...], SIM_VAL=[0, 0.2, 0.5, ...])
|
||||||
|
```
|
||||||
|
|
||||||
|
这意味着 **gap 越小的场景,单位 similarity 变化对应的 reward 变化越大**:
|
||||||
|
|
||||||
|
```
|
||||||
|
Δr_sim/Δsim ≈ (0.5 - 0.2) / gap = 0.3 / gap
|
||||||
|
```
|
||||||
|
|
||||||
|
| Case | gap | Δr/Δsim | 效应 |
|
||||||
|
|------|:---:|:-------:|------|
|
||||||
|
| re60 | 0.20 | **1.50** | reward 梯度过陡 → PPO advantage 方差偏大 → 训练不稳定 |
|
||||||
|
| re100 | 0.50 | **0.60** | 基准:梯度适中 |
|
||||||
|
| re200 | 0.32 | **0.94** | 梯度偏陡,可接受 |
|
||||||
|
| re400 | 0.17 | **1.76** | reward 梯度极陡 → PPO 估计噪音极大 → 早期 peak 后快速崩溃 |
|
||||||
|
| vardist d075 | 0.54 | **0.56** | 梯度偏平 → PPO 信号过弱 → 持续 peak-crash 震荡 |
|
||||||
|
|
||||||
|
**与 vardist 的对称性**:vardist d075 的问题方向是 gap 太大(梯度被摊薄),Karman re60/re400 的问题方向是 gap 太小(梯度被过压),但**根源完全相同:实测 SIM_BP 不适合非 re100 基线场景**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 训练结果
|
||||||
|
|
||||||
|
### 2.1 Base Transfer (lr=3e-4, n_epochs=10, 200ep)
|
||||||
|
|
||||||
|
| Case | Seed | Best Reward | Best @ Ep | Ep 1 Reward | Ep 200 Reward | 走势质量 |
|
||||||
|
|------|:----:|:-----------:|:---------:|:-----------:|:-------------:|:--------:|
|
||||||
|
| **re60** | 43 | **0.724** | 181 | 0.370 | 0.163 | ✅ 快速上升后波动 |
|
||||||
|
| re60 | 41 | 0.701 | 191 | 0.073 | 0.697 | ✅ 慢启动但稳定 |
|
||||||
|
| **re200** | 43 | **0.499** | 177 | 0.223 | 0.477 | ⚠️ 缓慢爬升 |
|
||||||
|
| re200 | 45 | 0.441 | 23 | 0.283 | 0.359 | ❌ 早期 peak 后停滞 |
|
||||||
|
| re200 | 41 | 0.489 | 60 | 0.190 | 0.381 (Ep105) | ⚠️ 日志截断 |
|
||||||
|
| **re400** | 43 | **0.553** | 28 | 0.326 | 0.264 | ❌ 极早 peak 后持续退化 |
|
||||||
|
|
||||||
|
### 2.2 Extend Transfer (lr=1e-4, n_epochs=5, 200-400ep)
|
||||||
|
|
||||||
|
从各自 base best_model 继续 fine-tune:
|
||||||
|
|
||||||
|
| Case | Seed | Best Reward | Best @ Ep | Ep 1 Reward | 末 Ep Reward | vs Base | 走势 |
|
||||||
|
|------|:----:|:-----------:|:---------:|:-----------:|:------------:|:-------:|------|
|
||||||
|
| re60 ext | 43 | 0.695 | 149 | 0.212 | 0.660 | **-0.029** ⬇ | ⚠️ 起始更低 |
|
||||||
|
| re200 ext | 43 | **0.535** | 182 | 0.436 | 0.244 (Ep400) | **+0.036** ⬆ | ❌ peak 后退至 0.24 |
|
||||||
|
| re400 ext | 43 | 0.538 | 107 | 0.341 | 0.345 (Ep400) | **-0.015** ⬇ | ⚠️ 微下降 |
|
||||||
|
|
||||||
|
### 2.3 Ext2 Transfer (lr=1e-4, n_epochs=5, 200-400ep)
|
||||||
|
|
||||||
|
从各自 base best_model 重新 fine-tune(不继续 ext):
|
||||||
|
|
||||||
|
| Case | Seed | Best Reward | Best @ Ep | Ep 1 Reward | 末 Ep Reward | vs Base | 走势 |
|
||||||
|
|------|:----:|:-----------:|:---------:|:-----------:|:------------:|:-------:|------|
|
||||||
|
| re60 ext2 | 43 | 0.711 | 187 | 0.375 | 0.711 | **-0.013** ⬇ | ⚠️ 接近但未超越 |
|
||||||
|
| re200 ext2 | 43 | 0.524 | 51 | 0.342 | 0.326 (Ep400) | **+0.025** ⬆ | ❌ 低于 ext |
|
||||||
|
| re400 ext2 | 43 | 0.548 | 373 | 0.365 | 0.393 (Ep400) | **-0.005** ⬇ | ⚠️ 勉强持平 |
|
||||||
|
|
||||||
|
### 2.4 跨实验总览
|
||||||
|
|
||||||
|
| Case | 最优 | Ep @ 最优 | Ep1→末 差值 | 退化幅度 |
|
||||||
|
|------|:----:|:---------:|:----------:|:--------:|
|
||||||
|
| re60 base seed43 | **0.724** | 181 | +0.354 → -0.207 | -0.561 |
|
||||||
|
| re200 ext seed43 | **0.535** | 182 | +0.436 → -0.192 | -0.628 |
|
||||||
|
| re400 base seed43 | **0.553** | 28 | +0.326 → -0.062 | -0.388 |
|
||||||
|
|
||||||
|
> **re200/re400 的 Ep1 还有提升空间,但都未能守住并持续改进,最终均出现不同程度的退化。**
|
||||||
|
|
||||||
|
### 2.5 Reward 走势定性图
|
||||||
|
|
||||||
|
```
|
||||||
|
re60 base seed43: ▄▂▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ─→ 0.724 (Ep181 peak,Ep200 崩至 0.16)
|
||||||
|
re60 ext seed43: ▄▂▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ─→ 0.695 (stable,低于 base 0.724)
|
||||||
|
re60 ext2 seed43: ▃▃▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ─→ 0.711 (接近 base,仍略低)
|
||||||
|
|
||||||
|
re200 base seed43: ▂▃▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ─→ 0.499 (慢升)
|
||||||
|
re200 ext seed43: ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▂▁ ─→ 0.535 (Ep182 peak 后退至 0.244)
|
||||||
|
re200 ext2 seed43: ▄▀▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃ ─→ 0.524 (Ep51 peak 后停滞)
|
||||||
|
|
||||||
|
re400 base seed43: ▄▀▂▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃ ─→ 0.553 (Ep28 peak 后退至 0.264)
|
||||||
|
re400 ext seed43: ▄▄▄▄▀▂▂▂▃▃▃▃▃▃▃▃▃▃▃▃ ─→ 0.538 (平稳退化)
|
||||||
|
re400 ext2 seed43: ▄▂▂▃▃▃▃▄▄▄▄▄▄▄▄▄▄▄▄▄ ─→ 0.548 (极慢恢复)
|
||||||
|
```
|
||||||
|
|
||||||
|
三条核心模式:
|
||||||
|
1. **re60**:base 能冲到 0.72 但非常波动,ext/ext2 更稳定但上限被压低
|
||||||
|
2. **re200**:"早期爬坡 → 高峰 → 严重退化"三部曲
|
||||||
|
3. **re400**:"极早 peak → 持续退化"二部曲,从未恢复
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 诊断分析
|
||||||
|
|
||||||
|
### 3.1 ✅ re60 base — 相对成功但波动大
|
||||||
|
|
||||||
|
**为什么相对成功:**
|
||||||
|
|
||||||
|
1. Re60 流场接近稳态(FFT 显示极弱涡脱落),控制难度最低
|
||||||
|
2. Re60 与 Re100 的物理差异小(FORCE_SCALE: 0.0021 vs 0.0024,仅差 12%)
|
||||||
|
3. Ep1 r_cd=0.793 → re100 的策略在 Re60 下 drag 控制仍然有效
|
||||||
|
4. Ep1 r_sim=0.236 → similarity 虽然初始低,但几何不变 + 流场简单可以快速调整
|
||||||
|
|
||||||
|
**潜在问题:**
|
||||||
|
|
||||||
|
1. 虽然最优能到 0.724,但 Ep200 跌回 0.163——极不稳定的后期表现
|
||||||
|
2. 可能已经撞到局部最优(randomly 达到高 reward 后随 PPO 探索而丢失)
|
||||||
|
3. SIM_BP gap=0.20 导致 reward 梯度 2.5x 于基准,PPO 在被放大的噪音中探步过大
|
||||||
|
|
||||||
|
**证据:re60 base seed41 vs seed43 差异巨大**
|
||||||
|
- seed41: Ep1=0.07(极低起点)→ 最终 0.701(稳步上升)
|
||||||
|
- seed43: Ep1=0.37(较好起点)→ 冲高 0.724 后末班 0.163(过山车)
|
||||||
|
- 不同 seed 从不同策略起点出发,但最终都达到了类似上限(0.70-0.72),说明 re60 的能力上限可能就在 0.72 左右
|
||||||
|
|
||||||
|
### 3.2 ❌ re200 ext — 最典型的退化模式
|
||||||
|
|
||||||
|
**re200 ext seed43 详细曲线:**
|
||||||
|
```
|
||||||
|
Ep 1: 0.436 (较高起点——ext 从 base best 继续,base Ep200=0.477)
|
||||||
|
Ep 50: 0.460 (缓慢上升)
|
||||||
|
Ep 150: 0.480 (继续缓慢上升)
|
||||||
|
Ep 182: 0.535 ← BEST (突然突破)
|
||||||
|
Ep 200: 0.488 (快速回落)
|
||||||
|
Ep 300: 0.304 (持续下降)
|
||||||
|
Ep 400: 0.244 (崩溃,回到接近 Ep1 水平)
|
||||||
|
```
|
||||||
|
|
||||||
|
**根因 1 — SIM_BP 梯度过陡(2.5x)**
|
||||||
|
|
||||||
|
gap=0.32,Δr/Δsim=0.94,比 re100 基准陡 56%。PPO 的 advantage 估计在这种放大后的 reward 空间中方差增大,导致:
|
||||||
|
- 偶尔能发现好策略(Ep182 的 0.535)
|
||||||
|
- 但下次 PPO 更新时在大方差下 over-shoot,丢失策略
|
||||||
|
- 之后再也找不回来——"过山车"无法逆转
|
||||||
|
|
||||||
|
这和一个经典现象一致:**advantage variance 过大时,即使 PPO 的 clipping 也无法防止 catastrophic update**。
|
||||||
|
|
||||||
|
**根因 2 — extend 超参过于保守(6x)**
|
||||||
|
|
||||||
|
lr=1e-4 + n_epochs=5 的组合相当于每次 PPO update 的有效学习量只有 base 的 ~1/6。当策略退化到 0.244 时,如此小的更新量已无法推动策略脱离低谷。
|
||||||
|
|
||||||
|
**根因 3 — K_CD/K_CL 不一致改变 reward landscape**
|
||||||
|
|
||||||
|
re100 基准使用 K_CD=50, K_CL=100,但 re200 的 calibration 只有 K_CD=12, K_CL=25(1/4)。这意味着:
|
||||||
|
- re100 专家策略训练时 drag/lift 受到非常严格的平方惩罚
|
||||||
|
- 迁移到 re200 后,同样的 drag 偏差只受到 1/4 的惩罚
|
||||||
|
- 相当于 reward 中 drag/lift 分量的"价格"大幅下降 → PPO 可以"偷工减料"仍然拿到最高 reward
|
||||||
|
- 但不一定意味着真正控制了流场——更像在 reward 空间中找到了捷径
|
||||||
|
|
||||||
|
### 3.3 ❌ re400 — 极早 peak 后永不恢复
|
||||||
|
|
||||||
|
**re400 base seed43 详细曲线:**
|
||||||
|
```
|
||||||
|
Ep 1: 0.326 (中等起点)
|
||||||
|
Ep 28: 0.553 ← BEST (极早 peak)
|
||||||
|
Ep 50: 0.451
|
||||||
|
Ep 100: 0.362 (持续退化)
|
||||||
|
Ep 150: 0.301 (继续退化)
|
||||||
|
Ep 200: 0.264 (接近起点)
|
||||||
|
```
|
||||||
|
|
||||||
|
**根因 1 — SIM_BP gap=0.17,梯度 2.9x 于基准**
|
||||||
|
|
||||||
|
这是所有 case 中最极端的 SIM_BP 异常。在 0.17 的窄 similarity 范围内,0.2→0.5 的 reward 映射意味着:
|
||||||
|
- 任何微小的 similarity 波动被 2.9 倍放大
|
||||||
|
- PPO 观测到的 reward signal 几乎全是噪音
|
||||||
|
- 类似在高噪声数据上用大步长 SGD——方向随机,永远无法收敛
|
||||||
|
|
||||||
|
**根因 2 — 流场混沌度高**
|
||||||
|
|
||||||
|
Re400 涡脱落更强更不规则(FORCE_SCALE=0.0042,比 re100 高 75%)。流场本身的随机性已经大到 PPO 无法在合理步数内学习有效的 value function,再加上被放大的 reward 梯度 → 不稳定的更新 → 策略崩坏。
|
||||||
|
|
||||||
|
**根因 3 — 三种退化叠加**
|
||||||
|
|
||||||
|
与 re200 不同,re400 同时受到:
|
||||||
|
1. SIM_BP 梯度异常(2.9x,最严重)
|
||||||
|
2. K_CD/K_CL 缩小(reward landscape 变形)
|
||||||
|
3. 流场混沌(自然信号噪声高)
|
||||||
|
|
||||||
|
三座大山共同压垮了训练。
|
||||||
|
|
||||||
|
### 3.4 extend/ext2 vs base 的退化对比
|
||||||
|
|
||||||
|
| Case | base best | ext best | Δ base→ext | ext2 best | Δ best→ext2 | 超参保守效应 |
|
||||||
|
|------|:---------:|:--------:|:----------:|:---------:|:-----------:|:----------:|
|
||||||
|
| re60 | **0.724** | 0.695 | **-0.029** | 0.711 | -0.013 | ⚠️ 有害 |
|
||||||
|
| re200 | 0.499 | **0.535** | **+0.036** | 0.524 | +0.025 | ⚠️ 边际改善但稳不住 |
|
||||||
|
| re400 | **0.553** | 0.538 | **-0.015** | 0.548 | -0.005 | ⚠️ 基本持平 |
|
||||||
|
|
||||||
|
**模式**:extend/ext2 的超参组合(lr=1e-4, n_epochs=5)对任何 Re 都没有产生显著正向帮助:
|
||||||
|
- re60: 明显有害(-0.029)
|
||||||
|
- re200: 微改善(+0.036)但后续崩塌
|
||||||
|
- re400: 基本持平但远低于可接受水平
|
||||||
|
|
||||||
|
**推论**:在 SIM_BP 梯度异常未修复的前提下,调整超参只是治标不治本。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 与 Vardist 的交叉验证
|
||||||
|
|
||||||
|
### 4.1 SIM_BP 问题的对称性
|
||||||
|
|
||||||
|
```
|
||||||
|
←── 梯度太陡(Karman re60/re400)──→
|
||||||
|
Δr/Δsim: 0.56 0.60 0.94 1.50 1.76
|
||||||
|
│ │ │ │ │
|
||||||
|
Case: d075 re100 re200 re60 re400
|
||||||
|
│ │ │ │ │
|
||||||
|
问题: 太平坦 基准 偏陡 太陡 极陡
|
||||||
|
表现: peak-crash 正常 慢升+退化 波动大 极早peak崩溃
|
||||||
|
│ │ │
|
||||||
|
└─ vardist 方向 ──────┴─ Karman 方向 ─────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**两种方向的修复都需要 generic SIM_BP (gap=0.35)**,恰好落在安全区间。
|
||||||
|
|
||||||
|
### 4.2 Early Peak 退化:d2 vs re400
|
||||||
|
|
||||||
|
两个案例共享完全相同的退化模式:
|
||||||
|
|
||||||
|
| 特征 | vardist d2 | Karman re400 | 是否同源? |
|
||||||
|
|------|:----------:|:------------:|:---------:|
|
||||||
|
| Ep1 r_sim | 0.455 | 0.326 | 均 < 0.5 |
|
||||||
|
| Best @ | Ep 2 | Ep 28 | 均极早 |
|
||||||
|
| 退化幅度 | 0.470 → 0.246 (48%) | 0.553 → 0.264 (52%) | 接近 |
|
||||||
|
| 恢复能力 | 无人力干预从未恢复 | 无人力干预从未恢复 | 相同 |
|
||||||
|
| **根因** | **几何不匹配** | **SIM_BP 梯度 2.9x** | 不同 |
|
||||||
|
|
||||||
|
> **d2 的根因是几何变化(2.0L vs 1.0L),re400 的根因是 reward 梯度异常。但两者的退化模式完全一致,说明 "早期 peak 后不可逆退化" 是一个通用的 training instability 信号,无论触发原因是什么。**
|
||||||
|
|
||||||
|
### 4.3 Vardist 的改善经验是否可以套用?
|
||||||
|
|
||||||
|
| Vardist v2 改进 | 原因 | 是否适用于 Karman? |
|
||||||
|
|----------------|------|:-------------------:|
|
||||||
|
| generic SIM_BP | d075 gap=0.54 太宽 | ✅ **高度适用**(re60/re400 gap 太窄,同因异向) |
|
||||||
|
| from scratch | d075 Ep1=0.023 完全不适用 | ⚠️ 部分适用(re60 Ep1=0.37 还行,re200/400 borderline) |
|
||||||
|
| lr=1e-4 | 减少 scratch 训练 over-shoot | ❌ **不适用**(Karman 是 fine-tune,降 lr 有害) |
|
||||||
|
| — | — | ✅ **需要升回 lr=3e-4, n_epochs=10**(与 base 保持一致) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 改进建议
|
||||||
|
|
||||||
|
### 5.1 针对 re60
|
||||||
|
|
||||||
|
| 优先级 | 方案 | 原理 |
|
||||||
|
|:------:|------|------|
|
||||||
|
| **P0** | 使用 generic SIM_BP 重新 calibrate | gap=0.20 → 0.35,降低 reward 梯度从 2.5x→1x,稳定训练 |
|
||||||
|
| **P1** | 从 re60 base best 继续 fine-tune,但保持 lr=3e-4 + n_epochs=10 | 当前 0.724 仍有波动,需要更多 episode 而非更保守更新 |
|
||||||
|
| **P2** | Multi-seed fine-tune (seed41/43/45) | 不同 seed 的 Ep1→最终轨迹差异大,多 seed 取优 |
|
||||||
|
|
||||||
|
### 5.2 针对 re200
|
||||||
|
|
||||||
|
| 优先级 | 方案 | 原理 |
|
||||||
|
|:------:|------|------|
|
||||||
|
| **P0** | 使用 generic SIM_BP 重新 calibrate | gap=0.32 → 0.35(差异较小但仍有 56% 的梯度过压) |
|
||||||
|
| **P1** | 恢复 lr=3e-4 + n_epochs=10 的原始超参 | re200 ext 是唯一从 extend 中获益的 case (+0.036),说明保守更新至少没完全搞砸——但应该用原始超参加速收敛 |
|
||||||
|
| **P2** | 延长 episode 数至 500+ | re200 base 200ep 仅到 0.499 且仍在上升,需要更长时间 |
|
||||||
|
| **P3** | 对比 K_CD/K_CL=50/100 vs 12/25 | 找出 reward landscape 变形的实际影响 |
|
||||||
|
| **P4** | 尝试 n_steps=4096 | 多采一倍经验做一次 PPO update,超参整体更保守但 advantage 估计更稳 |
|
||||||
|
|
||||||
|
### 5.3 针对 re400
|
||||||
|
|
||||||
|
| 优先级 | 方案 | 原理 |
|
||||||
|
|:------:|------|------|
|
||||||
|
| **P0** | 使用 generic SIM_BP 重新 calibrate | gap=0.17 → 0.35,梯度从 2.9x 降到 1x,是**最重要且最可能有效**的改动 |
|
||||||
|
| **P1** | 对比 from scratch vs transfer | Ep1 r_sim=0.326 borderline,可能需要从头学 |
|
||||||
|
| **P2** | 考虑 from scratch 500ep (lr=3e-4, n_epochs=10) | 如果 re100 策略对 re400 不适用,scratch 可能更快 |
|
||||||
|
| **P3** | 尝试更大模型容量 [128, 64] 或 [128, 128] | Re400 流场更混沌,64-d MLP 可能容量不足 |
|
||||||
|
| **P4** | 降低 symmetry probability 至 0.3 | 高 Re 下流场不对称性更强,强 symmetry 正则化可能有害 |
|
||||||
|
|
||||||
|
### 5.4 针对 extend/ext2 脚本
|
||||||
|
|
||||||
|
| 优先级 | 方案 | 原理 |
|
||||||
|
|:------:|------|------|
|
||||||
|
| **P0** | 恢复 lr=3e-4 + n_epochs=10 | 当前 6x 保守超参对所有 Re 无效甚至有害 |
|
||||||
|
| **P1** | 只增加 episode 数,不改变任何超参 | 保持与 base 完全一致的 PPO 设置 |
|
||||||
|
| **P2** | 在每个 Re 的 extend 中点(50% episodes)评估一次 | 如果 reward 持续下降,提前终止避免浪费时间 |
|
||||||
|
|
||||||
|
### 5.5 通用改进
|
||||||
|
|
||||||
|
| 优先级 | 方案 | 适用范围 |
|
||||||
|
|:------:|------|---------|
|
||||||
|
| **P0** | 修改 calibrate.py 的 generic SIM_BP 兜底条件:增加 gap<0.25(re100 基准的 50%)时触发 | 所有 future cross-Re / vardist case |
|
||||||
|
| **P1** | 将 K_CD/K_CL 固定在 50/100,而非让 calibration 测量不同值 | 保持 reward landscape 一致,跨 Re / 跨几何比较才有意义 |
|
||||||
|
| **P2** | Ep1 metrics 作为先行诊断指标:r_sim<0.3 时建议 from scratch;r_sim>0.5 + r_cd>0.6 时可放心 transfer | 减少无效实验 |
|
||||||
|
| **P3** | 对高 Re (200+) 增加 n_steps=4096 的实验分支 | 混沌流场需要更大 minibatch 做稳定 advantage 估计 |
|
||||||
|
| **P4** | 对比实验矩阵(见 §6) | 一次性验证多个假说 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 建议的对比实验矩阵
|
||||||
|
|
||||||
|
基于以上分析,最经济有效的实验设计(每个 200ep):
|
||||||
|
|
||||||
|
| 实验 | Re | SIM_BP | lr | n_ep | trans? | 目的 |
|
||||||
|
|------|:--:|:------:|:--:|:----:|:------:|------|
|
||||||
|
| **T0** (baseline) | 200/400 | 实测 | 3e-4 | 10 | re100→ | 已有数据,基线 |
|
||||||
|
| **T1** | 400 | generic | 3e-4 | 10 | re100→ | **P0 验证**:generic SIM_BP 是否消除早期崩溃 |
|
||||||
|
| **T2** | 400 | generic | 3e-4 | 10 | 无 | 验证 from scratch 是否更优 |
|
||||||
|
| **T3** | 200 | generic | 3e-4 | 10 | re100→ | 验证 re200 能否突破 0.50-0.53 瓶颈 |
|
||||||
|
| **T4** | 200 | 实测 | 3e-4 | 10 | re100→, K=50/100 | 验证 K_CD/K_CL 统一的效应 |
|
||||||
|
| **T5** | 400 | generic | 3e-4 | 10 | re100→, n_steps=4096 | 验证大 minibatch 的效果 |
|
||||||
|
|
||||||
|
**推荐路径**:先跑 T1,如果 re400 不再早期崩溃 → P0 确诊 → 再生产性跑 T3 + T4 + T5。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 与 Vardist 的对照总结
|
||||||
|
|
||||||
|
| 维度 | Vardist (变扰流尺寸) | Karman (变 Re) |
|
||||||
|
|------|---------------------|---------------|
|
||||||
|
| **物理变化** | 扰动圆柱直径 0.75L–2.0L | 来流粘度 → Re 变化 |
|
||||||
|
| **核心问题** | SIM_BP gap=0.54 太宽(梯度被摊薄) | SIM_BP gap=0.17-0.32 太窄(梯度被过压) |
|
||||||
|
| **典型退化** | d075: 持续 peak-crash 震荡 | re200/400: 早期 peak 后不可逆退化 |
|
||||||
|
| **兜底逻辑** | d15/d2 触发 generic;d075 未触发 | re60/200/400 均未触发 |
|
||||||
|
| **已尝试修复** | v2: generic SIM_BP + from scratch + lr=1e-4 | ext/ext2: lr=1e-4 + n_epochs=5(方向错误) |
|
||||||
|
| **正确修复** | generic SIM_BP + 合理超参 | generic SIM_BP + 恢复 lr=3e-4 + n_epochs=10 |
|
||||||
|
| **统一根因** | calibrate.py 的实测 SIM_BP 不适合非 re100 场景 | 同上 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 关键文件索引
|
||||||
|
|
||||||
|
| 文件 | 路径 |
|
||||||
|
|------|------|
|
||||||
|
| Base 训练脚本 | `train_karman.py` |
|
||||||
|
| 环境 | `env_karman.py` |
|
||||||
|
| 校准脚本 | `calibrate.py`(SIM_BP generic 兜底逻辑在第 526–527 行) |
|
||||||
|
| Base transfer 启动 | `crossre_transfer.sh`(lr=3e-4, n_epochs=10) |
|
||||||
|
| Extend transfer 启动 | `extend_transfer.sh`(lr=1e-4, n_epochs=5, 保守超参) |
|
||||||
|
| Ext2 transfer 启动 | `ext2_transfer.sh`(同上,重新 fine-tune) |
|
||||||
|
| Multi-GPU launcher | `launch_multi.sh`(lr=3e-4, n_epochs=10) |
|
||||||
|
| 对比分析 | `VARDIST_ANALYSIS.md` |
|
||||||
|
| re60 校准 | `calibrations/re60/calibration.json`(SIM_BP gap=0.20, K_CD=12) |
|
||||||
|
| re100 校准 | `calibrations/re100/calibration.json`(SIM_BP gap=0.50, K_CD=50) |
|
||||||
|
| re200 校准 | `calibrations/re200/calibration.json`(SIM_BP gap=0.32, K_CD=12) |
|
||||||
|
| re400 校准 | `calibrations/re400/calibration.json`(SIM_BP gap=0.17, K_CD=12) |
|
||||||
|
| re60 base 日志 | `output/transfer_re60_seed43/train.log` |
|
||||||
|
| re200 base 日志 | `output/transfer_re200_seed43/train.log` |
|
||||||
|
| re200 ext 日志 | `output/transfer_re200ext_seed43/train.log`(0.535→0.244 退化) |
|
||||||
|
| re400 base 日志 | `output/transfer_re400_seed43/train.log`(Ep28 0.553→0.264 退化) |
|
||||||
|
| re400 ext2 日志 | `output/transfer_re400ext2_seed43/train.log` |
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
# DynamisLab — 训练目录
|
||||||
|
|
||||||
|
> `src/drl_pinball/train/`
|
||||||
|
> 更新: 2026-07-12
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 命名规则
|
||||||
|
|
||||||
|
```
|
||||||
|
{domain}_{variant}_{method}_seed{seed}
|
||||||
|
|
||||||
|
domain: kar = Karman | ill = Illusion
|
||||||
|
variant: re{60,100,200,400} = 雷诺数 | d{075,15,2} = 扰动圆柱直径(L) | {1L,15L,075L,2L} = 目标圆柱尺寸
|
||||||
|
method: sc = from scratch | tr = transfer from re100 baseline
|
||||||
|
seed: seed{41..45}
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
kar_re100_sc_seed45 — Karman Re100 scratch, seed 45
|
||||||
|
kar_d075_sc_seed44 — Karman 0.75L dist-cyl scratch
|
||||||
|
kar_d15_tr_seed45 — Karman 1.5L dist-cyl transfer
|
||||||
|
kar_re200_sc_seed43 — Karman Re200 scratch
|
||||||
|
ill_2L_sc_seed43 — Illusion 2L target scratch
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
train/
|
||||||
|
├── train_karman.py # Karman PPO 训练 (核心)
|
||||||
|
├── train_illusion.py # Illusion PPO 训练
|
||||||
|
├── calibrate.py # Phase 0 校准
|
||||||
|
│
|
||||||
|
├── env_karman.py # Karman 环境
|
||||||
|
├── env_illusion.py # Illusion 环境
|
||||||
|
├── symmetry_wrapper.py # G-symmetry 数据增强
|
||||||
|
│
|
||||||
|
├── scripts/ # 启动脚本
|
||||||
|
│ ├── train_baseline.sh # kar_re100_sc multi-seed
|
||||||
|
│ ├── train_illusion.sh # ill_*_sc 4 个尺寸
|
||||||
|
│ ├── vardist_scratch.sh # kar_d*_sc (推荐)
|
||||||
|
│ ├── vardist_transfer.sh # kar_d*_tr (参考)
|
||||||
|
│ ├── crossre_scratch.sh # kar_re*_sc
|
||||||
|
│ ├── crossre_transfer.sh # kar_re*_tr (参考)
|
||||||
|
│ └── resume.sh # 中断恢复
|
||||||
|
│
|
||||||
|
├── calibrations/ # 校准文件
|
||||||
|
│ ├── kar_re100/ # Re100 基准
|
||||||
|
│ ├── kar_d*_sc/ # 变直径 scratch (generic SIM_BP)
|
||||||
|
│ ├── kar_d*_tr/ # 变直径 transfer (实测 SIM_BP)
|
||||||
|
│ ├── kar_re*_sc/ # 变雷诺数 scratch (generic SIM_BP)
|
||||||
|
│ ├── kar_re*/ # 变雷诺数 transfer (实测 SIM_BP)
|
||||||
|
│ └── ill_*/ # Illusion
|
||||||
|
│
|
||||||
|
├── output/ # 训练输出 (21 dirs)
|
||||||
|
│ ├── kar_re100_sc_seed{41..45}/
|
||||||
|
│ ├── kar_d*_sc_seed{44,45}/
|
||||||
|
│ ├── kar_d*_tr_seed{44,45}/
|
||||||
|
│ ├── kar_re*_sc_seed43/
|
||||||
|
│ ├── kar_re*_tr_seed43/
|
||||||
|
│ └── ill_*_sc_seed43/
|
||||||
|
│
|
||||||
|
├── archive/ # 旧版本 (保留)
|
||||||
|
├── README.md # 当前文件
|
||||||
|
├── VARDIST_ANALYSIS.md
|
||||||
|
├── CROSSRE_ANALYSIS.md
|
||||||
|
└── SERVER_DEPLOY.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 基准训练
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd scripts
|
||||||
|
bash train_baseline.sh --gpu 0 --episodes 500
|
||||||
|
# → output/kar_re100_sc_seed{41..45}/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 变直径 scratch (推荐)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd scripts
|
||||||
|
bash vardist_scratch.sh --gpu 0
|
||||||
|
bash vardist_scratch.sh --only d075
|
||||||
|
# → output/kar_d075_sc_seed44/ etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 变雷诺数 scratch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd scripts
|
||||||
|
bash crossre_scratch.sh --gpu 0
|
||||||
|
bash crossre_scratch.sh --only re200
|
||||||
|
# → output/kar_re200_sc_seed43/ etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Illusion
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd scripts
|
||||||
|
bash train_illusion.sh --gpu 0
|
||||||
|
# → output/ill_2L_sc_seed43/ etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 中断恢复
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd scripts
|
||||||
|
bash resume.sh --case kar_re60_sc --seed 43 --resume 460 --episodes 500
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心训练脚本
|
||||||
|
|
||||||
|
### `train_karman.py`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
conda run -n pycuda_3_10 python -u train_karman.py \
|
||||||
|
--case-name kar_re100_sc --device-id 0 --seed 42 \
|
||||||
|
--config config.json --calibration calibrations/kar_re100/calibration.json \
|
||||||
|
--total-episodes 500 [--transfer-model PATH] [--resume-from N]
|
||||||
|
```
|
||||||
|
|
||||||
|
| 参数 | 默认值 | 说明 |
|
||||||
|
|------|:-----:|------|
|
||||||
|
| n_steps | 2048 | PPO 每轮收集步数 |
|
||||||
|
| batch_size | 64 | minibatch |
|
||||||
|
| n_epochs | 10 | 经验重用次数 |
|
||||||
|
| lr | 3e-4 | 学习率 |
|
||||||
|
| gamma | 0.995 | 折扣因子 |
|
||||||
|
| net_arch | [64, 64] | MLP 两层 sin 激活 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最终结果
|
||||||
|
|
||||||
|
### Re100 基准 — `kar_re100_sc`
|
||||||
|
|
||||||
|
| Seed | r_sim | r_cd | r_cl |
|
||||||
|
|:----:|:-----:|:----:|:----:|
|
||||||
|
| 45 | 0.883 | 0.977 | 0.982 |
|
||||||
|
| Avg | 0.866 | 0.971 | 0.965 |
|
||||||
|
|
||||||
|
### Vardist 变直径
|
||||||
|
|
||||||
|
| Case | 方式 | r_sim | r_cd | r_cl |
|
||||||
|
|------|:----:|:-----:|:-----:|:-----:|
|
||||||
|
| **kar_d075_sc** | scratch | **0.918** | 0.954 | 0.879 |
|
||||||
|
| kar_d075_tr | transfer | 0.450 | 0.585 | 0.236 |
|
||||||
|
| **kar_d15_sc** | scratch | **0.904** | 0.991 | 0.992 |
|
||||||
|
| kar_d15_tr | transfer | 0.791 | 0.918 | 0.470 |
|
||||||
|
| **kar_d2_sc** | scratch | **0.793** | 0.967 | 0.961 |
|
||||||
|
| kar_d2_tr | transfer | 0.673 | 0.537 | 0.192 |
|
||||||
|
|
||||||
|
> Scratch 全面领先 transfer。
|
||||||
|
|
||||||
|
### Cross-Re 变雷诺数
|
||||||
|
|
||||||
|
| Case | 方式 | r_sim | r_cd | r_cl |
|
||||||
|
|------|:----:|:-----:|:-----:|:-----:|
|
||||||
|
| kar_re60_tr | transfer | 0.446 | **0.882** | 0.936 |
|
||||||
|
| kar_re60_sc | scratch | 0.261 | **0.987** | 0.981 |
|
||||||
|
| **kar_re200_sc** | scratch | **0.664** | 0.667 | 0.281 |
|
||||||
|
| kar_re200_tr | transfer | 0.374 | 0.749 | 0.427 |
|
||||||
|
| **kar_re400_sc** | scratch | **0.509** | 0.714 | 0.420 |
|
||||||
|
| kar_re400_tr | transfer | 0.293 | 0.883 | 0.571 |
|
||||||
|
|
||||||
|
> Scratch sim 最优,transfer CD 最优。两者均保留。
|
||||||
|
|
||||||
|
### Illusion
|
||||||
|
|
||||||
|
| Case | r_sim | r_cd | r_cl |
|
||||||
|
|------|:-----:|:-----:|:-----:|
|
||||||
|
| ill_1L_sc | 0.725 | 0.774 | 0.449 |
|
||||||
|
| ill_15L_sc | 0.810 | 0.825 | 0.384 |
|
||||||
|
| ill_075L_sc | 0.794 | 0.840 | 0.486 |
|
||||||
|
| **ill_2L_sc** | **0.896** | 0.863 | 0.570 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Calibration 参考
|
||||||
|
|
||||||
|
| Calibration | 物理 | SI | SIM_BP | K_CD/K_CL | 对应 output |
|
||||||
|
|------------|------|:--:|--------|:---------:|------------|
|
||||||
|
| `kar_re100` | Re=100, 1.0L | 800 | 实测 | 50/100 | `kar_re100_sc` |
|
||||||
|
| `kar_d075_sc` | Re=100, 0.75L | 800 | generic | 50/100 | `kar_d075_sc` |
|
||||||
|
| `kar_d075_tr` | Re=100, 0.75L | 800 | 实测 | 50/100 | `kar_d075_tr` |
|
||||||
|
| `kar_d15_sc` | Re=100, 1.5L | 800 | generic | 50/100 | `kar_d15_sc` |
|
||||||
|
| `kar_d15_tr` | Re=100, 1.5L | 800 | 实测 | 50/100 | `kar_d15_tr` |
|
||||||
|
| `kar_d2_sc` | Re=100, 2.0L | 800 | generic | 50/100 | `kar_d2_sc` |
|
||||||
|
| `kar_d2_tr` | Re=100, 2.0L | 800 | 实测 | 50/100 | `kar_d2_tr` |
|
||||||
|
| `kar_re60` | Re=60 | 800 | 实测 | 12/25 | `kar_re60_tr` |
|
||||||
|
| `kar_re60_sc` | Re=60 | 800 | generic | 50/100 | `kar_re60_sc` |
|
||||||
|
| `kar_re200` | Re=200 | 500 | 实测 | 12/25 | `kar_re200_tr` |
|
||||||
|
| `kar_re200_sc` | Re=200 | 500 | generic | 50/100 | `kar_re200_sc` |
|
||||||
|
| `kar_re400` | Re=400 | 400 | 实测 | 12/25 | `kar_re400_tr` |
|
||||||
|
| `kar_re400_sc` | Re=400 | 400 | generic | 50/100 | `kar_re400_sc` |
|
||||||
|
| `ill_*` | Illusion | — | 实测 | 12/25 | `ill_*_sc` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Findings
|
||||||
|
|
||||||
|
1. **SIM_BP 映射是跨场景训练的最关键变量。** 实测 gap ≠ 0.35 会导致 reward 梯度异常。
|
||||||
|
- Generic SIM_BP `[0, 0.30, 0.65, 0.79, 0.89, 1.0]` (gap=0.35) → 稳定学习
|
||||||
|
|
||||||
|
2. **变直径: scratch > transfer.** Generic SIM_BP + K_CD/CL=50/100 + lr=3e-4 + 500ep 是最优配方。
|
||||||
|
|
||||||
|
3. **变雷诺数: scratch 和 transfer 各有侧重。** 两者均保留。
|
||||||
|
|
||||||
|
4. **变雷诺数存在 CD/CL ↔ r_sim 根本性 trade-off。** 训练越久 r_cd 越高但 r_sim 越低(re60_sc: Ep60 r_sim=0.48 → Ep390 r_sim=0.26, r_cd=0.99)。CD/CL reward 分量主导了优化方向。
|
||||||
|
|
||||||
|
5. **降低 W_drag/W_lift (0.1) 有害。** CD/CL reward 是稳定器而非瓶颈。
|
||||||
|
|
||||||
|
6. **Ep1 r_sim ≥ 0.5 可预测 transfer 成功。**
|
||||||
@@ -1,352 +0,0 @@
|
|||||||
# Karman Cloak Training — Knowledge Document (V5)
|
|
||||||
|
|
||||||
> **V5 (2026-07-03)**: Parameterized, calibration-driven, no_bias only.
|
|
||||||
> All paths use DynamisLab submodule `CelerisLab/` (no external dev dir).
|
|
||||||
> Cross-Re transfer pipeline verified: re60, re200, re400.
|
|
||||||
> Every episode saves model checkpoint.
|
|
||||||
> V4 backups moved to `old/`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. What This Codebase Does
|
|
||||||
|
|
||||||
Trains a PPO agent to control a fluidic pinball (3 rotating cylinders) to achieve
|
|
||||||
hydrodynamic cloaking — making the downstream flow match the "undisturbed" flow
|
|
||||||
(as if the pinball weren't there). The upstream disturbance cylinder generates a
|
|
||||||
Kármán vortex street; the pinball must cancel it.
|
|
||||||
|
|
||||||
- **CFD**: CelerisLab LBM solver (DynamisLab submodule), D2Q9 MRT, 2000x600 grid, uniform inlet, free-slip walls
|
|
||||||
- **DRL**: PPO with Sin activation, 64x64 MLP, SB3 + VecNormalize
|
|
||||||
- **V5 mode**: No_bias only (ACTION_SCALE=12, ACTION_BIAS=[0,0,0]). All cases use calibration-first workflow.
|
|
||||||
|
|
||||||
## 0. Quick Start
|
|
||||||
|
|
||||||
### 0.1 Single-Re training (re100)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Calibrate (~5 min)
|
|
||||||
cd src/drl_pinball/train
|
|
||||||
conda run -n pycuda_3_10 python calibrate.py \
|
|
||||||
--case re100 --device-id 0 \
|
|
||||||
--config configs/config_lbm_karman_2000x600.json
|
|
||||||
|
|
||||||
# 2. Multi-seed training (server, sequential 7-min delay between GPUs)
|
|
||||||
bash launch_multi.sh \
|
|
||||||
--case-name re100_karman --seeds 42,43,44,45,46,47 \
|
|
||||||
--gpus 0,1,2,3,4,5 --episodes 500 \
|
|
||||||
--config configs/config_lbm_karman_2000x600.json \
|
|
||||||
--calibration calibrations/re100/calibration.json
|
|
||||||
|
|
||||||
# 3. Monitor
|
|
||||||
tail -f output/re100_karman_seed42/train.log
|
|
||||||
tensorboard --logdir output/re100_karman_seed42/tb --port 6006
|
|
||||||
```
|
|
||||||
|
|
||||||
### 0.2 Cross-Re transfer (re60, re200, re400)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Local test (5 episodes each, quick verification):
|
|
||||||
bash crossre_transfer.sh --re-list 60,200,400 --test-episodes 5
|
|
||||||
|
|
||||||
# Server production (200 episodes each):
|
|
||||||
# !! BEFORE PUSHING TO SERVER: update BEST_MODEL path in crossre_transfer.sh !!
|
|
||||||
bash crossre_transfer.sh --re-list 60,200,400
|
|
||||||
# Then push to server and run there.
|
|
||||||
```
|
|
||||||
|
|
||||||
### 0.3 Path notes for server deployment
|
|
||||||
|
|
||||||
`crossre_transfer.sh` has relative paths via `SCRIPT_DIR`/`REPO_DIR`.
|
|
||||||
Only `BEST_MODEL` needs updating — point to the best Re100 model from
|
|
||||||
multi-seed training (e.g. `output/re100_karman_seed42/models/best_model.zip`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. File Structure (V5 Final)
|
|
||||||
|
|
||||||
```
|
|
||||||
train/
|
|
||||||
├── __init__.py
|
|
||||||
│
|
|
||||||
├── # ACTIVE FILES
|
|
||||||
├── calibrate.py # Phase 0 calibration (produces calibration.json + target.npy)
|
|
||||||
├── env_karman.py # Parameterized Karman cloak env
|
|
||||||
├── env_illusion.py # Parameterized Illusion env
|
|
||||||
├── env_vortex.py # Vortex cloak env (lamb/taylor, MAX_STEPS=150)
|
|
||||||
├── train_karman.py # Parameterized training script (every ep saves model)
|
|
||||||
├── train_illusion.py # Illusion training script
|
|
||||||
├── launch_multi.sh # Sequential multi-GPU server launcher
|
|
||||||
├── crossre_transfer.sh # Cross-Re transfer: calibrate + train (re60→re200→re400)
|
|
||||||
├── symmetry_wrapper.py # G-mirror symmetry augmentation (per-rollout)
|
|
||||||
├── visualize_and_analyze.py # Flow-field visualization & analysis
|
|
||||||
├── SERVER_DEPLOY.md # Server deployment instructions
|
|
||||||
├── TRAIN_KNOWLEDGE.md # This file
|
|
||||||
│
|
|
||||||
├── old/ # Archived V4 files (NOT active)
|
|
||||||
│ ├── env_karman_2000x600.py
|
|
||||||
│ ├── train_karman_2000x600.py
|
|
||||||
│ ├── phase0_baseline_measure.py
|
|
||||||
│ └── analyze_final.py
|
|
||||||
│
|
|
||||||
├── calibrations/ # Per-case calibration files (IMMUTABLE)
|
|
||||||
│ ├── re60/
|
|
||||||
│ │ ├── calibration.json # SI=800, FORCE_SCALE=0.0021, SENS_SCALE=0.72
|
|
||||||
│ │ ├── target.npy
|
|
||||||
│ │ └── calibrate.log
|
|
||||||
│ ├── re100/
|
|
||||||
│ │ ├── calibration.json # SI=800, FORCE_SCALE=0.0024, SENS_SCALE=0.75
|
|
||||||
│ │ └── calibrate.log
|
|
||||||
│ ├── re200/
|
|
||||||
│ │ ├── calibration.json # SI=500, FORCE_SCALE=0.0026, SENS_SCALE=0.90
|
|
||||||
│ │ ├── target.npy
|
|
||||||
│ │ └── calibrate.log
|
|
||||||
│ ├── re400/
|
|
||||||
│ │ ├── calibration.json # SI=400, FORCE_SCALE=0.0042, SENS_SCALE=0.98
|
|
||||||
│ │ ├── target.npy
|
|
||||||
│ │ └── calibrate.log
|
|
||||||
│ └── illusion_1L/
|
|
||||||
│ ├── calibration.json
|
|
||||||
│ ├── target.npy
|
|
||||||
│ ├── target_harmonics.json
|
|
||||||
│ └── calibrate.log
|
|
||||||
│
|
|
||||||
└── output/ # Training outputs
|
|
||||||
└── <case>_seed<N>/
|
|
||||||
├── models/ # ep0001_model.zip, ..., best_model.zip, final_model.zip
|
|
||||||
├── tb/ # TensorBoard logs
|
|
||||||
├── train.log
|
|
||||||
├── calibration.json
|
|
||||||
├── vec_normalize.pkl
|
|
||||||
└── meta.json
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Calibration Workflow (ALWAYS RUN FIRST)
|
|
||||||
|
|
||||||
Every case MUST run `calibrate.py` before training. This produces:
|
|
||||||
|
|
||||||
- `calibration.json`: FORCE_SCALE, SENS_SCALE, dtw_norm_scale, SIM_BP, SIM_VAL, reward constants
|
|
||||||
- `target.npy`: Target sensor signals (150 steps x 6 channels)
|
|
||||||
|
|
||||||
The calibration measures Stage0 (zero rotation) and Stage1 (open-loop reference) to compute normalization
|
|
||||||
constants. Calibration is **IMMUTABLE** — once produced, never modify.
|
|
||||||
|
|
||||||
### Calibration Results (all Re)
|
|
||||||
|
|
||||||
| Case | SI | FORCE_SCALE | SENS_SCALE | dtw_norm_scale | Stage0 sim | Stage1 sim |
|
|
||||||
|------|-----|-------------|------------|----------------|-----------|-----------|
|
|
||||||
| re60 | 800 | 0.0021 | 0.72 | 0.107 | 0.41 | 0.61 |
|
|
||||||
| re100 | 800 | 0.0024 | 0.75 | 0.204 | 0.32 | 0.82 |
|
|
||||||
| re200 | 500 | 0.0026 | 0.90 | 0.269 | 0.45 | 0.77 |
|
|
||||||
| re400 | 400 | 0.0042 | 0.98 | 0.310 | 0.56 | 0.73 |
|
|
||||||
|
|
||||||
### Cross-Re SI guidance
|
|
||||||
|
|
||||||
Based on ~18 samples per vortex shedding cycle:
|
|
||||||
| Case | SI | Rationale |
|
|
||||||
|------|----|-----------|
|
|
||||||
| re60 | 800 | FFT shows very weak/absent shedding at this Re with free-slip |
|
|
||||||
| re100 | 800 | Verified (~19 samples/cycle) |
|
|
||||||
| re200 | 500 | ~18 samples/cycle |
|
|
||||||
| re400 | 400 | ~18 samples/cycle |
|
|
||||||
|
|
||||||
### re60 note
|
|
||||||
|
|
||||||
At Re=60 with uniform inlet + free-slip walls, the upstream disturbance cylinder
|
|
||||||
produces very weak periodic shedding (FFT dominant period ~5625 samples).
|
|
||||||
This is a different regime from legacy parabolic+no-slip where re50 did shed.
|
|
||||||
SI=800 is adequate; the DRL essentially learns a steady-state control policy.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Reward Design
|
|
||||||
|
|
||||||
### Formula
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Gaussian reward (no zero-crossing spikes)
|
|
||||||
r_cd_raw = exp(-cd_norm² * K_CD) # cd_norm = (Σfx)/3 / FORCE_SCALE
|
|
||||||
r_cl_raw = exp(-cl_norm² * K_CL)
|
|
||||||
|
|
||||||
# EMA smoothing for cd/cl (r_sim uses DTW, already smooth)
|
|
||||||
r_cd = EMA(r_cd_raw, weight=0.2)
|
|
||||||
r_cl = EMA(r_cl_raw, weight=0.2)
|
|
||||||
|
|
||||||
# Normalized DTW similarity (piecewise-mapped to [0,1])
|
|
||||||
r_sim = piecewise_map(sim, SIM_BP, SIM_VAL)
|
|
||||||
|
|
||||||
# Floor penalty: prevents sacrificing one component
|
|
||||||
penalty = 0.05 * sum(max(0, FLOOR - r) / FLOOR for r, FLOOR in zip(...))
|
|
||||||
|
|
||||||
reward = max(0, W_CD*r_cd + W_CL*r_cl + W_SIM*r_sim - penalty)
|
|
||||||
# W_CD=0.30, W_CL=0.30, W_SIM=0.40
|
|
||||||
```
|
|
||||||
|
|
||||||
### Three-stage targets
|
|
||||||
|
|
||||||
| Stage | r_cd | r_cl | r_sim | Description |
|
|
||||||
|-------|------|------|-------|-------------|
|
|
||||||
| Stage0 (zero rotation) | ~0 | ~0 | ~0.2 | No control baseline |
|
|
||||||
| Stage1 (reference open-loop) | ~0.7 | ~0.5 | ~0.5 | Legacy-equiv bias |
|
|
||||||
| Optimal (trained) | ~0.9 | ~0.9 | ~0.9 | Full cloaking |
|
|
||||||
|
|
||||||
### Why Gaussian not exp(-|x|)
|
|
||||||
|
|
||||||
`exp(-|x|)` has maximum gradient at x=0, causing reward spikes at zero-crossings
|
|
||||||
of oscillating cd/cl. `exp(-x²)` has zero gradient at x=0 → smooth near optimum.
|
|
||||||
|
|
||||||
### Why normalized DTW
|
|
||||||
|
|
||||||
Raw DTW has narrow dynamic range (0.70-0.97). Normalizing by target's uy-channel
|
|
||||||
std extends range to 0.0-0.9, giving DRL meaningful gradient.
|
|
||||||
|
|
||||||
### Why no EMA on r_sim
|
|
||||||
|
|
||||||
DTW is already a smooth 30-step windowed signal. Adding EMA over-smooths.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. PPO Configuration
|
|
||||||
|
|
||||||
```python
|
|
||||||
PPO(
|
|
||||||
"MlpPolicy",
|
|
||||||
policy_kwargs={"activation_fn": Sin, "net_arch": [64, 64]},
|
|
||||||
env=vec_env,
|
|
||||||
device=torch.device("cuda:X"),
|
|
||||||
n_steps=2048, # MUST be 2048. 512 → noisy curves.
|
|
||||||
batch_size=64,
|
|
||||||
n_epochs=10,
|
|
||||||
learning_rate=3e-4,
|
|
||||||
gamma=0.995, # Higher than default for DTW delay propagation
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Evaluation**: stochastic (no deterministic=True) — smoother curves
|
|
||||||
- **Symmetry**: per-rollout G-mirror (50% probability). Disabled during eval.
|
|
||||||
- **Every episode saves**: `ep0001_model.zip` + `ep0001_vecnormalize.pkl` saved each episode
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Obs Normalization (Two-layer)
|
|
||||||
|
|
||||||
1. **Env physical norm** (fixed, from calibration):
|
|
||||||
- forces / FORCE_SCALE, sensors / SENS_SCALE
|
|
||||||
- No clipping (VecNormalize handles)
|
|
||||||
|
|
||||||
2. **SB3 VecNormalize** (running mean/std):
|
|
||||||
- norm_obs=True, norm_reward=False, clip_obs=10.0
|
|
||||||
- Saved to `vec_normalize.pkl` for inference
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Action Configuration (V5: no_bias only)
|
|
||||||
|
|
||||||
```python
|
|
||||||
ACTION_SCALE = 12.0
|
|
||||||
ACTION_BIAS = [0, 0, 0]
|
|
||||||
# omega = -(action * 12) * U0 / RADIUS
|
|
||||||
# Physical range: all cylinders [-12, 12] × U0
|
|
||||||
```
|
|
||||||
|
|
||||||
Sign convention: `Uw = -omega * ry` (omega>0 = clockwise).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Environment Design
|
|
||||||
|
|
||||||
### Two-phase initialization
|
|
||||||
|
|
||||||
1. `record_target()`: dist_cyl + sensors only → record 150-step target → close
|
|
||||||
2. Training Simulation: all 7 objects → warmup → zero-action FIFO → snapshot
|
|
||||||
|
|
||||||
Body IDs (add order):
|
|
||||||
```
|
|
||||||
0: dist_cyl (force, skipped in obs)
|
|
||||||
1-3: sensors (top, center, bottom)
|
|
||||||
4-6: pinball (front, top_rear, bottom_rear)
|
|
||||||
```
|
|
||||||
|
|
||||||
Obs layout (12-dim): forces[6] + sensors[6]
|
|
||||||
|
|
||||||
### No-reset training
|
|
||||||
|
|
||||||
`step()` returns `terminated=False`. Eval does `reset()` for reproducible assessment.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Cross-Re Transfer Pipeline
|
|
||||||
|
|
||||||
### How to add a new Re
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Create config (copy existing, change viscosity)
|
|
||||||
cp configs/config_lbm_karman_2000x600.json configs/config_lbm_karman_2000x600_reNNN.json
|
|
||||||
# Edit "viscosity" to U0 * 2D / Re_NNN
|
|
||||||
|
|
||||||
# 2. Calibrate
|
|
||||||
python calibrate.py --case reNNN --device-id 0 --si <SI> --config <config>
|
|
||||||
|
|
||||||
# 3. Test (5 episodes, local)
|
|
||||||
python train_karman.py --case-name transfer_reNNN --device-id 0 --seed 41 \
|
|
||||||
--config <config> --calibration calibrations/reNNN/calibration.json \
|
|
||||||
--si <SI> --total-episodes 5 \
|
|
||||||
--transfer-model output/re100_karman_seed<BEST>/models/best_model.zip
|
|
||||||
|
|
||||||
# 4. Production (add to crossre_transfer.sh or run directly with 200 episodes)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Transfer test results (5-episode verification, 2026-07-02)
|
|
||||||
|
|
||||||
| Re | Best Reward | r_cd | r_cl | r_sim | Time/ep |
|
|
||||||
|----|------------|------|------|-------|---------|
|
|
||||||
| 60 | 0.637 | 0.879 | 0.402 | 0.641 | 281s |
|
|
||||||
| 200 | 0.428 | 0.677 | 0.259 | 0.387 | 186s |
|
|
||||||
| 400 | 0.489 | 0.787 | 0.491 | 0.274 | 153s |
|
|
||||||
|
|
||||||
All show rapid learning from Re100 base. r_cl remains the hardest component across all Re.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. CelerisLab Integration
|
|
||||||
|
|
||||||
**CelerisLab is a git submodule** at `DynamisLab/CelerisLab/`.
|
|
||||||
All Python imports use `from CelerisLab import Simulation`.
|
|
||||||
No external paths — everything is self-contained within the repo.
|
|
||||||
Do NOT reference `/home/frank14f/CelerisLab` anywhere.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Key Parameters Reference
|
|
||||||
|
|
||||||
| Parameter | Value | Where | Notes |
|
|
||||||
|-----------|-------|-------|-------|
|
|
||||||
| Grid | 2000×600 | config | uniform inlet, free_slip |
|
|
||||||
| U0 | 0.01 | config | lattice inlet velocity |
|
|
||||||
| ν (re100) | 0.004 | config | Re_D=50 (code Re=100) |
|
|
||||||
| SI | 400-800 | calibration | varies by Re |
|
|
||||||
| FIFO_LEN | 150 | env | history buffer |
|
|
||||||
| CONV_LEN | 30 | env | DTW comparison window |
|
|
||||||
| SENSOR_CC | 78 | env | sensor area→legacy conversion |
|
|
||||||
| K_CD/K_CL | 50/100 | env | Gaussian reward coefficients |
|
|
||||||
| W_CD/W_CL/W_SIM | 0.30/0.30/0.40 | env | reward weights |
|
|
||||||
| n_steps | 2048 | train | PPO rollout size |
|
|
||||||
| n_epochs | 10 | train | PPO epochs per update |
|
|
||||||
| gamma | 0.995 | train | discount factor |
|
|
||||||
| ACTION_SCALE | 12.0 | env | no_bias only |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. Bugs Found & Fixed
|
|
||||||
|
|
||||||
| # | Bug | Symptom | Fix |
|
|
||||||
|---|-----|---------|-----|
|
|
||||||
| 1 | Simultaneous GPU startup | Kernel compilation race → reward=0.000 | Sequential launch, 7-min delay |
|
|
||||||
| 2 | Bias FIFO not applying omega | Wrong normalization baseline | Added set_omega in bias FIFO |
|
|
||||||
| 3 | DTW not normalized | sim always 0.90+ → flat gradient | Normalize by target uy-channel avg std |
|
|
||||||
| 4 | Gaussian vs exp(-|x|) | Zero-crossing reward spikes | Use exp(-x² · K) |
|
|
||||||
| 5 | n_steps=512 | Noisy curves, degradation | Use 2048 (SB3 default) |
|
|
||||||
| 6 | deterministic eval | Sharp, noisy reward | Stochastic eval |
|
|
||||||
| 7 | External CelerisLab paths | Permission issues, server mismatch | Use DynamisLab submodule only |
|
|
||||||
| 8 | Manual kernel cache cleaning | Unnecessary, root-only files | CelerisLab handles internally |
|
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
# Vardist Transfer Learning — 分析报告
|
||||||
|
|
||||||
|
> 日期: 2026-07-07
|
||||||
|
> 实验: 改变上游扰动圆柱尺寸 (0.75L / 1.5L / 2.0L),从 re100 baseline 进行 transfer learning
|
||||||
|
> 所有实验使用相同超参数和物理配置(仅 dist_radius 不同)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 实验配置总览
|
||||||
|
|
||||||
|
### 1.1 共享超参数
|
||||||
|
|
||||||
|
| 参数 | 值 |
|
||||||
|
|------|-----|
|
||||||
|
| PPO n_steps | 2048 |
|
||||||
|
| PPO batch_size | 64 |
|
||||||
|
| PPO n_epochs | 10 |
|
||||||
|
| Learning rate | 3e-4 |
|
||||||
|
| Gamma | 0.995 |
|
||||||
|
| Policy net_arch | [64, 64] |
|
||||||
|
| Activation | sin |
|
||||||
|
| Symmetry augmentation | prob=0.5 |
|
||||||
|
| Episode 数 | 200 (transfer) |
|
||||||
|
| 基座模型 | re100_karman_seed44 或 seed45 (500ep from scratch, best ≈ 0.92) |
|
||||||
|
|
||||||
|
### 1.2 校准参数差异
|
||||||
|
|
||||||
|
| 参数 | d075 (0.75L) | re100 (1.0L) | d15 (1.5L) | d2 (2.0L) |
|
||||||
|
|------|:-----------:|:------------:|:----------:|:---------:|
|
||||||
|
| **FORCE_SCALE** | 0.0018 | 0.0024 | 0.0036 | 0.0046 |
|
||||||
|
| **SENS_SCALE** | 0.84 | 0.75 | 0.75 | 0.77 |
|
||||||
|
| **dtw_norm_scale** | 0.146 | 0.204 | 0.307 | 0.393 |
|
||||||
|
| **SIM_BP[1]** (zero rot) | 0.25 | 0.32 | 0.30 | 0.30 |
|
||||||
|
| **SIM_BP[2]** (ref rot) | 0.79 | 0.82 | 0.65 | 0.65 |
|
||||||
|
| **SIM_BP gap** | **0.54** | 0.50 | **0.35** | **0.35** |
|
||||||
|
| **SIM_BP 来源** | 实测 | 实测 | generic 兜底 | generic 兜底 |
|
||||||
|
| **K_CD / K_CL** | 50/100 | 50/100 | 50/100 | 50/100 |
|
||||||
|
|
||||||
|
> **关键**: d075 的 SIM_BP 使用了实测值 (gap=0.54),而 d15 和 d2 因为 `spread<0.10` 或 `better_sim<0.5` 触发了 calibrate.py 中的 generic fallback (gap=0.35)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 训练结果
|
||||||
|
|
||||||
|
### 2.1 最终指标
|
||||||
|
|
||||||
|
| Case | Best Reward | Ep 1 Reward | Ep 1 r_sim | Ep 1 r_cd | 走势质量 |
|
||||||
|
|------|:-----------:|:-----------:|:----------:|:---------:|:--------:|
|
||||||
|
| **d15 (1.5L)** | **0.7265** (Ep 198) | 0.570 | 0.748 | 0.650 | ✅ 稳定单调上升 |
|
||||||
|
| **d2 (2.0L)** | **0.4696** (Ep 2!) | 0.301 | 0.455 | 0.288 | ❌ Ep 2 peak 后持续退化 |
|
||||||
|
| **d075 (0.75L)** | **0.4175** (Ep 153) | 0.023 | 0.053 | 0.012 | ❌ 持续 peak-crash 震荡 |
|
||||||
|
|
||||||
|
### 2.2 对比基线:跨 Re transfer (来自同一基座模型)
|
||||||
|
|
||||||
|
| Case | Best Reward | 走势 |
|
||||||
|
|------|:-----------:|:----:|
|
||||||
|
| re60 transfer | **0.724** | ✅ 稳定上升 |
|
||||||
|
| re200 transfer | **0.499** | ⚠️ 缓慢但稳定 |
|
||||||
|
|
||||||
|
### 2.3 Reward 走势图 (定性)
|
||||||
|
|
||||||
|
```
|
||||||
|
d15: ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ───→ 0.727 (稳步爬坡)
|
||||||
|
d2: ▄▀▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ───→ 0.470 (Ep2 昙花一现,后再未超越)
|
||||||
|
d075: ▁▁▁▁▂▁▂▁▂▁▂▁▁▂▁▁▁▂ ───→ 0.418 (震荡不止,末班 0.053)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 诊断分析
|
||||||
|
|
||||||
|
### 3.1 ✅ d15 (1.5L) — 成功案例
|
||||||
|
|
||||||
|
**为什么成功:**
|
||||||
|
1. 上游圆柱放大 → 1.0L 的专家策略"不够用但方向对",可以在此基础上升级
|
||||||
|
2. SIM_BP 使用 generic mapping (gap=0.35),reward 梯度均匀且有意义
|
||||||
|
3. Ep 1 r_sim=0.748 说明初始策略在 1.5L 尾流下已经有相当不错的信号重建能力
|
||||||
|
4. r_cd 从 0.65 涨到 0.92,r_cl 从 0.27 涨到 0.48,三个 reward 分量协作上升
|
||||||
|
|
||||||
|
### 3.2 ❌ d075 (0.75L) — 最差案例
|
||||||
|
|
||||||
|
**根因 1 — SIM_BP 映射问题 (最主要)**
|
||||||
|
|
||||||
|
d075 是唯一一个使用实测 SIM_BP 的 case。测得的 zero-rotation similarity=0.25,reference rotation=0.79,gap=0.54。
|
||||||
|
|
||||||
|
这意味着 reward 映射中 SIM_VAL 从 0.2→0.5 覆盖了 0.54 的真实 similarity 跨度,梯度被显著摊薄。当 PPO 在 reward 空间中观测到的改进信号极小时,policy 更新方向近乎随机——这完美解释了反复的 peak-crash 行为。
|
||||||
|
|
||||||
|
**根因 2 — 物理域不匹配**
|
||||||
|
|
||||||
|
0.75L 扰动圆柱产生的卡门涡街更弱更窄(FORCE_SCALE=0.0018,比 1.0L 低 25%)。而 pinball 圆柱仍是 1.0L 固定尺寸,它们的旋转对弱流场的控制可能"过强"——类似于用力关门但门很轻。1.0L 的专家策略不适用于小扰动场景。
|
||||||
|
|
||||||
|
**根因 3 — 基座策略已接近局部最优**
|
||||||
|
|
||||||
|
reference rotation 在 0.75L 场景下已给出 similarity=0.79(接近 re100 baseline 的 0.82)。DRL 要超越一个已经很接近 1.0L baseline 水平的开环策略,且 reward 梯度又被摊薄,双重不利。
|
||||||
|
|
||||||
|
**证据 — Ep 1 collapse**
|
||||||
|
|
||||||
|
Ep 1 reward=0.023,r_cd=0.012,r_sim=0.053。这说明 1.0L 的 expert policy 在 0.75L 场景下几乎完全不适用,不是在微调而是在重新学习。
|
||||||
|
|
||||||
|
### 3.3 ❌ d2 (2.0L) — 早期 peak 后退化
|
||||||
|
|
||||||
|
**根因 1 — 几何差异导致初始 mismatch**
|
||||||
|
|
||||||
|
2.0L 圆柱是 1.0L 的两倍,FORCE_SCALE=0.0046(近乎翻倍)。Ep 1 r_sim=0.455(vs d15 的 0.748),说明在如此大差异下 transfer 效果甚微,几乎从接近 scratch 的状态开始。
|
||||||
|
|
||||||
|
**根因 2 — Ep 2 的 0.470 是偶发性好运**
|
||||||
|
|
||||||
|
```
|
||||||
|
Ep 2: 0.470 BEST
|
||||||
|
Ep 50: 0.138
|
||||||
|
Ep 125: 0.299 (轻微恢复)
|
||||||
|
Ep 200: 0.246
|
||||||
|
```
|
||||||
|
|
||||||
|
在独占 GPU 又跑了 150ep 后,从未超越 Ep 2 的 0.470。"早期 peak 后持续退化"的模式说明 Ep 2 的高 reward 是一次偶然发现的好策略,PPO 之后的探索却撞进了 reward 更低的方向且无力返回。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Ep 1 Metrics 作为先行指标的发现
|
||||||
|
|
||||||
|
| Case | Ep 1 r_sim | Ep 1 r_cd | 最终 Best | Transfer 成功? |
|
||||||
|
|------|:----------:|:---------:|:---------:|:--------------:|
|
||||||
|
| d15 | 0.748 | 0.650 | 0.727 | ✅ |
|
||||||
|
| d2 | 0.455 | 0.288 | 0.470 | ❌ |
|
||||||
|
| d075 | 0.053 | 0.012 | 0.417 | ❌ |
|
||||||
|
| re60 | 0.236 | 0.793 | 0.724 | ✅ |
|
||||||
|
|
||||||
|
**推论**: Ep 1 r_sim 是预测 transfer 能否成功的强信号。r_sim≥0.5 的 case 最终都达到了 0.7+,r_sim<0.5 的 case 均未超过 0.47。例外是 re60 —— 虽然 r_sim 不高 (0.236),但几何不变,PPO 可以快速"调回"相似度。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 改进建议
|
||||||
|
|
||||||
|
### 5.1 针对 d075 (0.75L)
|
||||||
|
|
||||||
|
| 优先级 | 方案 | 原理 |
|
||||||
|
|:------:|------|------|
|
||||||
|
| **P0** | 强制使用 generic SIM_BP mapping | 消除 SIM_BP gap=0.54 导致的平坦梯度问题 (即修改 calibrate.py 或手动替换 calibration.json 中 SIM_BP 为 [0, 0.30, 0.65, 0.79, 0.89, 1.0]) |
|
||||||
|
| **P1** | 降低 learning rate 至 1e-4 | 减少 PPO 在高方差环境中的 over-shoot,缓解 peak-crash |
|
||||||
|
| **P2** | 增加 episode 数至 500 | 当前 200ep 明显不足以收敛 |
|
||||||
|
| **P3** | 不 transfer,from scratch | 因为 Ep 1 已证明 re100 policy 对 d075 高度不适配 (reward=0.023),scratch 训练可能更稳定 |
|
||||||
|
|
||||||
|
### 5.2 针对 d2 (2.0L)
|
||||||
|
|
||||||
|
| 优先级 | 方案 | 原理 |
|
||||||
|
|:------:|------|------|
|
||||||
|
| **P0** | 不 transfer,from scratch + 500ep | Ep 1 初始 mismatch 太大,从头学习可能更高效 |
|
||||||
|
| **P1** | transfer 但降低 lr=1e-4 + 更多 episode | 如果仍用 transfer,更保守的更新速率有助于保持策略稳定 |
|
||||||
|
| **P2** | 加强 symmetry augmentation (prob=0.7) | 2.0L 尾流更宽,采样空间更大,更强的对称正则化可能帮助泛化 |
|
||||||
|
| **P3** | 调整 K_CD/K_CL 以匹配 2.0L 的力尺度 | FORCE_SCALE 翻倍但 K 值不变,reward 的分量权重可能已不平衡 |
|
||||||
|
|
||||||
|
### 5.3 通用改进
|
||||||
|
|
||||||
|
| 优先级 | 方案 | 适用范围 |
|
||||||
|
|:------:|------|---------|
|
||||||
|
| **P0** | 给 calibrate.py 的 generic SIM_BP 兜底条件加宽阈值 | 所有 future case,避免 d075 式的 SIM_BP gap 问题再次出现 |
|
||||||
|
| **P1** | 观察 Ep 1 metrics 作为 transfer 可行性的先行判断 | 如果 Ep 1 r_sim<0.5,建议直接 from scratch 而非 transfer |
|
||||||
|
| **P2** | 对不同直径使用不同的 K_CD/K_CL 缩放 | FORCE_SCALE 变化时,reward 中 drag/lift 贡献的比例可能需要调整 |
|
||||||
|
| **P3** | 启用 VecNormalize reward normalization | 当前 `norm_reward=False`,对小 reward 场景可能会让 PPO 的信号强度不足 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 关键文件索引
|
||||||
|
|
||||||
|
| 文件 | 路径 |
|
||||||
|
|------|------|
|
||||||
|
| 训练脚本 | `train_karman.py` |
|
||||||
|
| 环境 | `env_karman.py` |
|
||||||
|
| 校准脚本 | `calibrate.py` (generic SIM_BP 兜底逻辑在第 526-535 行) |
|
||||||
|
| Transfer 启动脚本 | `vardist_transfer.sh` |
|
||||||
|
| d075 校准 | `calibrations/karman_d075/calibration.json` |
|
||||||
|
| d15 校准 | `calibrations/karman_d15/calibration.json` |
|
||||||
|
| d2 校准 | `calibrations/karman_d2/calibration.json` |
|
||||||
|
| re100 基线校准 | `calibrations/re100/calibration.json` |
|
||||||
|
| d075 训练日志 | `output/transfer_karman_d075_seed44/train.log` |
|
||||||
|
| d15 训练日志 | `output/transfer_karman_d15_seed45/train.log` |
|
||||||
|
| d2 训练日志 | `output/transfer_karman_d2_seed45/train.log` |
|
||||||
@@ -386,6 +386,8 @@ def main() -> int:
|
|||||||
help="Scene type (karman or illusion)")
|
help="Scene type (karman or illusion)")
|
||||||
parser.add_argument("--target-diam", type=float, default=1.0,
|
parser.add_argument("--target-diam", type=float, default=1.0,
|
||||||
help="Target cylinder diameter in L0 units (illusion only, default=1.0)")
|
help="Target cylinder diameter in L0 units (illusion only, default=1.0)")
|
||||||
|
parser.add_argument("--dist-radius", type=float, default=1.0,
|
||||||
|
help="Disturbance cylinder radius in L0 units (karman only, default=1.0)")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
case = args.case
|
case = args.case
|
||||||
@@ -419,9 +421,11 @@ def main() -> int:
|
|||||||
|
|
||||||
# ---- Step 1: Record target (Karman: dist_cyl + sensors only) ----
|
# ---- Step 1: Record target (Karman: dist_cyl + sensors only) ----
|
||||||
log("Step 1: Recording target signal...")
|
log("Step 1: Recording target signal...")
|
||||||
|
dist_radius = args.dist_radius * L0
|
||||||
|
log(f" Dist cylinder radius = {args.dist_radius}L = {dist_radius:.1f} lattice")
|
||||||
sim = Simulation(lbm_config_path=config_path, device_id=device_id)
|
sim = Simulation(lbm_config_path=config_path, device_id=device_id)
|
||||||
sim._assert_object_count_contract = lambda *a, **kw: None
|
sim._assert_object_count_contract = lambda *a, **kw: None
|
||||||
dist_id_t = sim.add_body("circle", center=(DIST_X, CENTER_Y, 0.0), radius=1.0 * L0)
|
dist_id_t = sim.add_body("circle", center=(DIST_X, CENTER_Y, 0.0), radius=dist_radius)
|
||||||
s0_t = sim.add_body("sensor", center=(SENSOR_X, CENTER_Y + 40.0, 0.0), radius=5.0)
|
s0_t = sim.add_body("sensor", center=(SENSOR_X, CENTER_Y + 40.0, 0.0), radius=5.0)
|
||||||
s1_t = sim.add_body("sensor", center=(SENSOR_X, CENTER_Y, 0.0), radius=5.0)
|
s1_t = sim.add_body("sensor", center=(SENSOR_X, CENTER_Y, 0.0), radius=5.0)
|
||||||
s2_t = sim.add_body("sensor", center=(SENSOR_X, CENTER_Y - 40.0, 0.0), radius=5.0)
|
s2_t = sim.add_body("sensor", center=(SENSOR_X, CENTER_Y - 40.0, 0.0), radius=5.0)
|
||||||
@@ -453,7 +457,7 @@ def main() -> int:
|
|||||||
log("Step 2: Creating training sim (all 7 objects)...")
|
log("Step 2: Creating training sim (all 7 objects)...")
|
||||||
sim = Simulation(lbm_config_path=config_path, device_id=device_id)
|
sim = Simulation(lbm_config_path=config_path, device_id=device_id)
|
||||||
sim._assert_object_count_contract = lambda *a, **kw: None
|
sim._assert_object_count_contract = lambda *a, **kw: None
|
||||||
dist_id = sim.add_body("circle", center=(DIST_X, CENTER_Y, 0.0), radius=1.0 * L0)
|
dist_id = sim.add_body("circle", center=(DIST_X, CENTER_Y, 0.0), radius=dist_radius)
|
||||||
sensor_ids = [
|
sensor_ids = [
|
||||||
sim.add_body("sensor", center=(SENSOR_X, CENTER_Y + 40.0, 0.0), radius=5.0),
|
sim.add_body("sensor", center=(SENSOR_X, CENTER_Y + 40.0, 0.0), radius=5.0),
|
||||||
sim.add_body("sensor", center=(SENSOR_X, CENTER_Y, 0.0), radius=5.0),
|
sim.add_body("sensor", center=(SENSOR_X, CENTER_Y, 0.0), radius=5.0),
|
||||||
@@ -505,15 +509,34 @@ def main() -> int:
|
|||||||
log(f" Stage0 sim_mean = {s0_sim:.4f}")
|
log(f" Stage0 sim_mean = {s0_sim:.4f}")
|
||||||
log(f" Stage1 sim_mean = {s1_sim:.4f}")
|
log(f" Stage1 sim_mean = {s1_sim:.4f}")
|
||||||
|
|
||||||
sim_bp = [0.0, s0_sim, s1_sim,
|
# Build SIM_BP: always [0, worst_measured, better_measured, ..., 1.0]
|
||||||
s1_sim + (1.0 - s1_sim) * 0.4,
|
if s1_sim < s0_sim:
|
||||||
s1_sim + (1.0 - s1_sim) * 0.7,
|
worst_sim = s1_sim
|
||||||
1.0]
|
better_sim = s0_sim
|
||||||
sim_val = [0.0, 0.2, 0.5, 0.8, 0.9, 1.0]
|
else:
|
||||||
|
worst_sim = s0_sim
|
||||||
|
better_sim = s1_sim
|
||||||
|
|
||||||
for i in range(1, len(sim_bp)):
|
# When stage0 and stage1 are too close (|s1-s0| < 0.05), the measured
|
||||||
if sim_bp[i] <= sim_bp[i - 1]:
|
# SIM_BP collapses to near-zero dynamic range. Fall back to a generic
|
||||||
sim_bp[i] = sim_bp[i - 1] + 0.01
|
# mapping anchored on the well-behaved re100 baseline (s0~0.32, s1~0.82).
|
||||||
|
# This gives the DRL a meaningful reward gradient regardless of the
|
||||||
|
# open-loop baselines.
|
||||||
|
spread = abs(s1_sim - s0_sim)
|
||||||
|
if spread < 0.10 or better_sim < 0.5:
|
||||||
|
log(f" Spread={spread:.3f} too small or better<0.5 — using generic SIM_BP mapping")
|
||||||
|
sim_bp = [0.0, 0.30, 0.65, 0.79, 0.89, 1.0]
|
||||||
|
sim_val = [0.0, 0.20, 0.50, 0.80, 0.90, 1.0]
|
||||||
|
else:
|
||||||
|
sim_bp = [0.0, worst_sim, better_sim,
|
||||||
|
better_sim + (1.0 - better_sim) * 0.4,
|
||||||
|
better_sim + (1.0 - better_sim) * 0.7,
|
||||||
|
1.0]
|
||||||
|
sim_val = [0.0, 0.2, 0.5, 0.8, 0.9, 1.0]
|
||||||
|
|
||||||
|
for i in range(1, len(sim_bp)):
|
||||||
|
if sim_bp[i] <= sim_bp[i - 1]:
|
||||||
|
sim_bp[i] = sim_bp[i - 1] + 0.01
|
||||||
|
|
||||||
log(f" SIM_BP (raw) = {[f'{x:.4f}' for x in sim_bp]}")
|
log(f" SIM_BP (raw) = {[f'{x:.4f}' for x in sim_bp]}")
|
||||||
log(f" SIM_VAL = {sim_val}")
|
log(f" SIM_VAL = {sim_val}")
|
||||||
@@ -540,6 +563,7 @@ def main() -> int:
|
|||||||
# ---- Step 6: Write calibration.json ----
|
# ---- Step 6: Write calibration.json ----
|
||||||
calibration = {
|
calibration = {
|
||||||
"case": case,
|
"case": case,
|
||||||
|
"dist_radius": args.dist_radius,
|
||||||
"grid": {"nx": NX, "ny": NY},
|
"grid": {"nx": NX, "ny": NY},
|
||||||
"config_path": config_path,
|
"config_path": config_path,
|
||||||
"SI": si,
|
"SI": si,
|
||||||
@@ -551,8 +575,8 @@ def main() -> int:
|
|||||||
"dtw_norm_scale": float(dtw_norm_scale),
|
"dtw_norm_scale": float(dtw_norm_scale),
|
||||||
"SIM_BP": [float(x) for x in sim_bp],
|
"SIM_BP": [float(x) for x in sim_bp],
|
||||||
"SIM_VAL": [float(x) for x in sim_val],
|
"SIM_VAL": [float(x) for x in sim_val],
|
||||||
"K_CD": K_CD_ILLUSION,
|
"K_CD": K_CD,
|
||||||
"K_CL": K_CL_ILLUSION,
|
"K_CL": K_CL,
|
||||||
"W_CD": W_CD,
|
"W_CD": W_CD,
|
||||||
"W_CL": W_CL,
|
"W_CL": W_CL,
|
||||||
"W_SIM": W_SIM,
|
"W_SIM": W_SIM,
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"case": "illusion_075L",
|
"case": "ill_075L",
|
||||||
"scene": "illusion",
|
"scene": "illusion",
|
||||||
"target_diam": 0.75,
|
"target_diam": 0.75,
|
||||||
"grid": {
|
"grid": {
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"case": "illusion_15L",
|
"case": "ill_15L",
|
||||||
"scene": "illusion",
|
"scene": "illusion",
|
||||||
"target_diam": 1.5,
|
"target_diam": 1.5,
|
||||||
"grid": {
|
"grid": {
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"case": "illusion_1L",
|
"case": "ill_1L",
|
||||||
"scene": "illusion",
|
"scene": "illusion",
|
||||||
"target_diam": 1.0,
|
"target_diam": 1.0,
|
||||||
"grid": {
|
"grid": {
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"case": "illusion_2L",
|
"case": "ill_2L",
|
||||||
"scene": "illusion",
|
"scene": "illusion",
|
||||||
"target_diam": 2.0,
|
"target_diam": 2.0,
|
||||||
"grid": {
|
"grid": {
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"case": "kar_d075_sc",
|
||||||
|
"dist_radius": 0.75,
|
||||||
|
"grid": {
|
||||||
|
"nx": 2000,
|
||||||
|
"ny": 600
|
||||||
|
},
|
||||||
|
"config_path": "/root/private_data/DynamisLab/configs/config_lbm_karman_2000x600.json",
|
||||||
|
"SI": 800,
|
||||||
|
"FIFO_LEN": 150,
|
||||||
|
"CONV_LEN": 30,
|
||||||
|
"SENSOR_CC": 78.0,
|
||||||
|
"FORCE_SCALE": 0.0018,
|
||||||
|
"SENS_SCALE": 0.84,
|
||||||
|
"dtw_norm_scale": 0.146,
|
||||||
|
"SIM_BP": [
|
||||||
|
0.0,
|
||||||
|
0.3,
|
||||||
|
0.65,
|
||||||
|
0.79,
|
||||||
|
0.89,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"SIM_VAL": [
|
||||||
|
0.0,
|
||||||
|
0.2,
|
||||||
|
0.5,
|
||||||
|
0.8,
|
||||||
|
0.9,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"K_CD": 50.0,
|
||||||
|
"K_CL": 100.0,
|
||||||
|
"W_CD": 0.3,
|
||||||
|
"W_CL": 0.3,
|
||||||
|
"W_SIM": 0.4,
|
||||||
|
"FLOOR_CD": 0.1,
|
||||||
|
"FLOOR_CL": 0.1,
|
||||||
|
"FLOOR_SIM": 0.1,
|
||||||
|
"FLOOR_PENALTY": 0.05,
|
||||||
|
"ACTION_SCALE": 12.0,
|
||||||
|
"ACTION_BIAS": [
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"U0": 0.01,
|
||||||
|
"RADIUS": 10.0,
|
||||||
|
"L0": 20.0
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"case": "kar_d075_tr",
|
||||||
|
"dist_radius": 0.75,
|
||||||
|
"grid": {
|
||||||
|
"nx": 2000,
|
||||||
|
"ny": 600
|
||||||
|
},
|
||||||
|
"config_path": "/home/frank14f/DynamisLab/configs/config_lbm_karman_2000x600.json",
|
||||||
|
"SI": 800,
|
||||||
|
"FIFO_LEN": 150,
|
||||||
|
"CONV_LEN": 30,
|
||||||
|
"SENSOR_CC": 78.0,
|
||||||
|
"FORCE_SCALE": 0.0018,
|
||||||
|
"SENS_SCALE": 0.84,
|
||||||
|
"dtw_norm_scale": 0.146,
|
||||||
|
"SIM_BP": [
|
||||||
|
0.0,
|
||||||
|
0.25,
|
||||||
|
0.79,
|
||||||
|
0.88,
|
||||||
|
0.94,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"SIM_VAL": [
|
||||||
|
0.0,
|
||||||
|
0.2,
|
||||||
|
0.5,
|
||||||
|
0.8,
|
||||||
|
0.9,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"K_CD": 50.0,
|
||||||
|
"K_CL": 100.0,
|
||||||
|
"W_CD": 0.3,
|
||||||
|
"W_CL": 0.3,
|
||||||
|
"W_SIM": 0.4,
|
||||||
|
"FLOOR_CD": 0.1,
|
||||||
|
"FLOOR_CL": 0.1,
|
||||||
|
"FLOOR_SIM": 0.1,
|
||||||
|
"FLOOR_PENALTY": 0.05,
|
||||||
|
"ACTION_SCALE": 12.0,
|
||||||
|
"ACTION_BIAS": [
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"U0": 0.01,
|
||||||
|
"RADIUS": 10.0,
|
||||||
|
"L0": 20.0
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"case": "kar_d15_sc",
|
||||||
|
"dist_radius": 1.5,
|
||||||
|
"grid": {
|
||||||
|
"nx": 2000,
|
||||||
|
"ny": 600
|
||||||
|
},
|
||||||
|
"config_path": "/root/private_data/DynamisLab/configs/config_lbm_karman_2000x600.json",
|
||||||
|
"SI": 800,
|
||||||
|
"FIFO_LEN": 150,
|
||||||
|
"CONV_LEN": 30,
|
||||||
|
"SENSOR_CC": 78.0,
|
||||||
|
"FORCE_SCALE": 0.0036,
|
||||||
|
"SENS_SCALE": 0.75,
|
||||||
|
"dtw_norm_scale": 0.307,
|
||||||
|
"SIM_BP": [
|
||||||
|
0.0,
|
||||||
|
0.3,
|
||||||
|
0.65,
|
||||||
|
0.79,
|
||||||
|
0.89,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"SIM_VAL": [
|
||||||
|
0.0,
|
||||||
|
0.2,
|
||||||
|
0.5,
|
||||||
|
0.8,
|
||||||
|
0.9,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"K_CD": 50.0,
|
||||||
|
"K_CL": 100.0,
|
||||||
|
"W_CD": 0.3,
|
||||||
|
"W_CL": 0.3,
|
||||||
|
"W_SIM": 0.4,
|
||||||
|
"FLOOR_CD": 0.1,
|
||||||
|
"FLOOR_CL": 0.1,
|
||||||
|
"FLOOR_SIM": 0.1,
|
||||||
|
"FLOOR_PENALTY": 0.05,
|
||||||
|
"ACTION_SCALE": 12.0,
|
||||||
|
"ACTION_BIAS": [
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"U0": 0.01,
|
||||||
|
"RADIUS": 10.0,
|
||||||
|
"L0": 20.0
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"case": "kar_d15_tr",
|
||||||
|
"dist_radius": 1.5,
|
||||||
|
"grid": {
|
||||||
|
"nx": 2000,
|
||||||
|
"ny": 600
|
||||||
|
},
|
||||||
|
"config_path": "/home/frank14f/DynamisLab/configs/config_lbm_karman_2000x600.json",
|
||||||
|
"SI": 800,
|
||||||
|
"FIFO_LEN": 150,
|
||||||
|
"CONV_LEN": 30,
|
||||||
|
"SENSOR_CC": 78.0,
|
||||||
|
"FORCE_SCALE": 0.0036,
|
||||||
|
"SENS_SCALE": 0.75,
|
||||||
|
"dtw_norm_scale": 0.307,
|
||||||
|
"SIM_BP": [
|
||||||
|
0.0,
|
||||||
|
0.3,
|
||||||
|
0.65,
|
||||||
|
0.79,
|
||||||
|
0.89,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"SIM_VAL": [
|
||||||
|
0.0,
|
||||||
|
0.2,
|
||||||
|
0.5,
|
||||||
|
0.8,
|
||||||
|
0.9,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"K_CD": 50.0,
|
||||||
|
"K_CL": 100.0,
|
||||||
|
"W_CD": 0.3,
|
||||||
|
"W_CL": 0.3,
|
||||||
|
"W_SIM": 0.4,
|
||||||
|
"FLOOR_CD": 0.1,
|
||||||
|
"FLOOR_CL": 0.1,
|
||||||
|
"FLOOR_SIM": 0.1,
|
||||||
|
"FLOOR_PENALTY": 0.05,
|
||||||
|
"ACTION_SCALE": 12.0,
|
||||||
|
"ACTION_BIAS": [
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"U0": 0.01,
|
||||||
|
"RADIUS": 10.0,
|
||||||
|
"L0": 20.0
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"case": "kar_d2_sc",
|
||||||
|
"dist_radius": 2.0,
|
||||||
|
"grid": {
|
||||||
|
"nx": 2000,
|
||||||
|
"ny": 600
|
||||||
|
},
|
||||||
|
"config_path": "/root/private_data/DynamisLab/configs/config_lbm_karman_2000x600.json",
|
||||||
|
"SI": 800,
|
||||||
|
"FIFO_LEN": 150,
|
||||||
|
"CONV_LEN": 30,
|
||||||
|
"SENSOR_CC": 78.0,
|
||||||
|
"FORCE_SCALE": 0.0046,
|
||||||
|
"SENS_SCALE": 0.77,
|
||||||
|
"dtw_norm_scale": 0.393,
|
||||||
|
"SIM_BP": [
|
||||||
|
0.0,
|
||||||
|
0.3,
|
||||||
|
0.65,
|
||||||
|
0.79,
|
||||||
|
0.89,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"SIM_VAL": [
|
||||||
|
0.0,
|
||||||
|
0.2,
|
||||||
|
0.5,
|
||||||
|
0.8,
|
||||||
|
0.9,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"K_CD": 50.0,
|
||||||
|
"K_CL": 100.0,
|
||||||
|
"W_CD": 0.3,
|
||||||
|
"W_CL": 0.3,
|
||||||
|
"W_SIM": 0.4,
|
||||||
|
"FLOOR_CD": 0.1,
|
||||||
|
"FLOOR_CL": 0.1,
|
||||||
|
"FLOOR_SIM": 0.1,
|
||||||
|
"FLOOR_PENALTY": 0.05,
|
||||||
|
"ACTION_SCALE": 12.0,
|
||||||
|
"ACTION_BIAS": [
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"U0": 0.01,
|
||||||
|
"RADIUS": 10.0,
|
||||||
|
"L0": 20.0
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"case": "kar_d2_tr",
|
||||||
|
"dist_radius": 2.0,
|
||||||
|
"grid": {
|
||||||
|
"nx": 2000,
|
||||||
|
"ny": 600
|
||||||
|
},
|
||||||
|
"config_path": "/home/frank14f/DynamisLab/configs/config_lbm_karman_2000x600.json",
|
||||||
|
"SI": 800,
|
||||||
|
"FIFO_LEN": 150,
|
||||||
|
"CONV_LEN": 30,
|
||||||
|
"SENSOR_CC": 78.0,
|
||||||
|
"FORCE_SCALE": 0.0046,
|
||||||
|
"SENS_SCALE": 0.77,
|
||||||
|
"dtw_norm_scale": 0.393,
|
||||||
|
"SIM_BP": [
|
||||||
|
0.0,
|
||||||
|
0.3,
|
||||||
|
0.65,
|
||||||
|
0.79,
|
||||||
|
0.89,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"SIM_VAL": [
|
||||||
|
0.0,
|
||||||
|
0.2,
|
||||||
|
0.5,
|
||||||
|
0.8,
|
||||||
|
0.9,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"K_CD": 50.0,
|
||||||
|
"K_CL": 100.0,
|
||||||
|
"W_CD": 0.3,
|
||||||
|
"W_CL": 0.3,
|
||||||
|
"W_SIM": 0.4,
|
||||||
|
"FLOOR_CD": 0.1,
|
||||||
|
"FLOOR_CL": 0.1,
|
||||||
|
"FLOOR_SIM": 0.1,
|
||||||
|
"FLOOR_PENALTY": 0.05,
|
||||||
|
"ACTION_SCALE": 12.0,
|
||||||
|
"ACTION_BIAS": [
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"U0": 0.01,
|
||||||
|
"RADIUS": 10.0,
|
||||||
|
"L0": 20.0
|
||||||
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"case": "re100",
|
"case": "kar_re100",
|
||||||
"grid": {
|
"grid": {
|
||||||
"nx": 2000,
|
"nx": 2000,
|
||||||
"ny": 600
|
"ny": 600
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"case": "re200",
|
"case": "kar_re200",
|
||||||
"grid": {
|
"grid": {
|
||||||
"nx": 2000,
|
"nx": 2000,
|
||||||
"ny": 600
|
"ny": 600
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"case": "kar_re200_sc",
|
||||||
|
"grid": {
|
||||||
|
"nx": 2000,
|
||||||
|
"ny": 600
|
||||||
|
},
|
||||||
|
"config_path": "/root/private_data/DynamisLab/configs/config_lbm_karman_2000x600_re200.json",
|
||||||
|
"SI": 500,
|
||||||
|
"FIFO_LEN": 150,
|
||||||
|
"CONV_LEN": 30,
|
||||||
|
"SENSOR_CC": 78.0,
|
||||||
|
"FORCE_SCALE": 0.0026,
|
||||||
|
"SENS_SCALE": 0.9,
|
||||||
|
"dtw_norm_scale": 0.269,
|
||||||
|
"SIM_BP": [
|
||||||
|
0.0,
|
||||||
|
0.3,
|
||||||
|
0.65,
|
||||||
|
0.79,
|
||||||
|
0.89,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"SIM_VAL": [
|
||||||
|
0.0,
|
||||||
|
0.2,
|
||||||
|
0.5,
|
||||||
|
0.8,
|
||||||
|
0.9,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"K_CD": 50.0,
|
||||||
|
"K_CL": 100.0,
|
||||||
|
"W_CD": 0.3,
|
||||||
|
"W_CL": 0.3,
|
||||||
|
"W_SIM": 0.4,
|
||||||
|
"FLOOR_CD": 0.1,
|
||||||
|
"FLOOR_CL": 0.1,
|
||||||
|
"FLOOR_SIM": 0.1,
|
||||||
|
"FLOOR_PENALTY": 0.05,
|
||||||
|
"ACTION_SCALE": 12.0,
|
||||||
|
"ACTION_BIAS": [
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"U0": 0.01,
|
||||||
|
"RADIUS": 10.0,
|
||||||
|
"L0": 20.0
|
||||||
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"case": "re400",
|
"case": "kar_re400",
|
||||||
"grid": {
|
"grid": {
|
||||||
"nx": 2000,
|
"nx": 2000,
|
||||||
"ny": 600
|
"ny": 600
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"case": "kar_re400_sc",
|
||||||
|
"grid": {
|
||||||
|
"nx": 2000,
|
||||||
|
"ny": 600
|
||||||
|
},
|
||||||
|
"config_path": "/root/private_data/DynamisLab/configs/config_lbm_karman_2000x600_re400.json",
|
||||||
|
"SI": 400,
|
||||||
|
"FIFO_LEN": 150,
|
||||||
|
"CONV_LEN": 30,
|
||||||
|
"SENSOR_CC": 78.0,
|
||||||
|
"FORCE_SCALE": 0.0042,
|
||||||
|
"SENS_SCALE": 0.98,
|
||||||
|
"dtw_norm_scale": 0.31,
|
||||||
|
"SIM_BP": [
|
||||||
|
0.0,
|
||||||
|
0.3,
|
||||||
|
0.65,
|
||||||
|
0.79,
|
||||||
|
0.89,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"SIM_VAL": [
|
||||||
|
0.0,
|
||||||
|
0.2,
|
||||||
|
0.5,
|
||||||
|
0.8,
|
||||||
|
0.9,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"K_CD": 50.0,
|
||||||
|
"K_CL": 100.0,
|
||||||
|
"W_CD": 0.3,
|
||||||
|
"W_CL": 0.3,
|
||||||
|
"W_SIM": 0.4,
|
||||||
|
"FLOOR_CD": 0.1,
|
||||||
|
"FLOOR_CL": 0.1,
|
||||||
|
"FLOOR_SIM": 0.1,
|
||||||
|
"FLOOR_PENALTY": 0.05,
|
||||||
|
"ACTION_SCALE": 12.0,
|
||||||
|
"ACTION_BIAS": [
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"U0": 0.01,
|
||||||
|
"RADIUS": 10.0,
|
||||||
|
"L0": 20.0
|
||||||
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"case": "re60",
|
"case": "kar_re60",
|
||||||
"grid": {
|
"grid": {
|
||||||
"nx": 2000,
|
"nx": 2000,
|
||||||
"ny": 600
|
"ny": 600
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"case": "kar_re60_sc",
|
||||||
|
"grid": {
|
||||||
|
"nx": 2000,
|
||||||
|
"ny": 600
|
||||||
|
},
|
||||||
|
"config_path": "/root/private_data/DynamisLab/configs/config_lbm_karman_2000x600_re60.json",
|
||||||
|
"SI": 800,
|
||||||
|
"FIFO_LEN": 150,
|
||||||
|
"CONV_LEN": 30,
|
||||||
|
"SENSOR_CC": 78.0,
|
||||||
|
"FORCE_SCALE": 0.0021,
|
||||||
|
"SENS_SCALE": 0.72,
|
||||||
|
"dtw_norm_scale": 0.107,
|
||||||
|
"SIM_BP": [
|
||||||
|
0.0,
|
||||||
|
0.3,
|
||||||
|
0.65,
|
||||||
|
0.79,
|
||||||
|
0.89,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"SIM_VAL": [
|
||||||
|
0.0,
|
||||||
|
0.2,
|
||||||
|
0.5,
|
||||||
|
0.8,
|
||||||
|
0.9,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"K_CD": 50.0,
|
||||||
|
"K_CL": 100.0,
|
||||||
|
"W_CD": 0.3,
|
||||||
|
"W_CL": 0.3,
|
||||||
|
"W_SIM": 0.4,
|
||||||
|
"FLOOR_CD": 0.1,
|
||||||
|
"FLOOR_CL": 0.1,
|
||||||
|
"FLOOR_SIM": 0.1,
|
||||||
|
"FLOOR_PENALTY": 0.05,
|
||||||
|
"ACTION_SCALE": 12.0,
|
||||||
|
"ACTION_BIAS": [
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"U0": 0.01,
|
||||||
|
"RADIUS": 10.0,
|
||||||
|
"L0": 20.0
|
||||||
|
}
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Sequential cross-Re transfer learning: Re60 -> Re200 -> Re400
|
|
||||||
# Each: calibrate (~5 min) + train N episodes
|
|
||||||
#
|
|
||||||
# Usage (local test, 5 episodes):
|
|
||||||
# bash crossre_transfer.sh --re-list 60 --test-episodes 5
|
|
||||||
#
|
|
||||||
# Usage (production on server, 200 episodes each):
|
|
||||||
# bash crossre_transfer.sh --re-list 60,200,400
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
|
||||||
TRAIN_DIR="$SCRIPT_DIR"
|
|
||||||
CONFIG_DIR="$REPO_DIR/configs"
|
|
||||||
# TODO: BEFORE PUSHING TO SERVER, update BEST_MODEL to the correct path
|
|
||||||
BEST_MODEL="$TRAIN_DIR/output/re100_karman_seed41/models/best_model.zip"
|
|
||||||
GPU=0
|
|
||||||
EPISODES=200
|
|
||||||
TEST_EPISODES=0
|
|
||||||
RE_LIST=""
|
|
||||||
LOG_BASE="/tmp/crossre_transfer"
|
|
||||||
CONDA_ENV="pycuda_3_10"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
echo "Usage: $0 [--re-list 60,200,400] [--test-episodes N] [--best-model PATH]"
|
|
||||||
echo " --re-list Comma-separated Re numbers (default: 60,200,400)"
|
|
||||||
echo " --test-episodes Run only N episodes per Re for quick verification"
|
|
||||||
echo " --best-model Path to Re100 best model .zip"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--re-list) RE_LIST="$2"; shift 2 ;;
|
|
||||||
--test-episodes) TEST_EPISODES="$2"; shift 2 ;;
|
|
||||||
--best-model) BEST_MODEL="$2"; shift 2 ;;
|
|
||||||
*) echo "Unknown option: $1"; usage ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -z "$RE_LIST" ]]; then
|
|
||||||
RE_LIST="60,200,400"
|
|
||||||
fi
|
|
||||||
|
|
||||||
IFS=',' read -ra RE_ARR <<< "$RE_LIST"
|
|
||||||
|
|
||||||
echo "=== Cross-Re Transfer ==="
|
|
||||||
echo " Re list: ${RE_ARR[*]}"
|
|
||||||
echo " Re60: base=seed43, train=seed43"
|
|
||||||
echo " Re200: base=seed45, train=seed45"
|
|
||||||
echo " Re400: base=seed43, train=seed43"
|
|
||||||
echo " Episodes: ${EPISODES} (test-mode: ${TEST_EPISODES})"
|
|
||||||
echo " GPU: ${GPU}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
mkdir -p "$LOG_BASE"
|
|
||||||
|
|
||||||
if [[ "$TEST_EPISODES" -gt 0 ]]; then
|
|
||||||
EPISODES="$TEST_EPISODES"
|
|
||||||
echo " TEST MODE: only $TEST_EPISODES episodes per Re"
|
|
||||||
fi
|
|
||||||
|
|
||||||
for re in "${RE_ARR[@]}"; do
|
|
||||||
case $re in
|
|
||||||
60) SI=800; vis_label="re60"; TRAIN_SEED=43
|
|
||||||
BEST_MODEL="$TRAIN_DIR/output/re100_karman_seed43/models/best_model.zip" ;;
|
|
||||||
200) SI=500; vis_label="re200"; TRAIN_SEED=45
|
|
||||||
BEST_MODEL="$TRAIN_DIR/output/re100_karman_seed45/models/best_model.zip" ;;
|
|
||||||
400) SI=400; vis_label="re400"; TRAIN_SEED=43
|
|
||||||
BEST_MODEL="$TRAIN_DIR/output/re100_karman_seed43/models/best_model.zip" ;;
|
|
||||||
*) echo "ERROR: Unknown Re=$re (supported: 60, 200, 400)"; exit 1 ;;
|
|
||||||
esac
|
|
||||||
CONFIG="$CONFIG_DIR/config_lbm_karman_2000x600_${vis_label}.json"
|
|
||||||
CASE="transfer_${vis_label}"
|
|
||||||
LOG="$LOG_BASE/${vis_label}_seed${TRAIN_SEED}.log"
|
|
||||||
|
|
||||||
if [[ ! -f "$BEST_MODEL" ]]; then
|
|
||||||
echo "ERROR: Best model not found: $BEST_MODEL" | tee -a "$LOG_BASE/summary.log"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "=== $(date): Starting $CASE (SI=$SI) ===" | tee -a "$LOG"
|
|
||||||
|
|
||||||
if [[ ! -f "$CONFIG" ]]; then
|
|
||||||
echo " ERROR: Config not found: $CONFIG" | tee -a "$LOG"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Step 1: Calibrate (skip if calibration.json already exists)
|
|
||||||
CAL_JSON="$TRAIN_DIR/calibrations/$vis_label/calibration.json"
|
|
||||||
if [[ -f "$CAL_JSON" ]]; then
|
|
||||||
echo " [SKIP] Calibration already exists: $CAL_JSON" | tee -a "$LOG"
|
|
||||||
echo " (Delete calibrations/$vis_label/ to force re-calibration)"
|
|
||||||
else
|
|
||||||
echo " [$(date '+%H:%M:%S')] Calibrating..." | tee -a "$LOG"
|
|
||||||
conda run --no-capture-output -n "$CONDA_ENV" python -u \
|
|
||||||
"$TRAIN_DIR/calibrate.py" \
|
|
||||||
--case "$vis_label" --device-id $GPU --si $SI \
|
|
||||||
--config "$CONFIG" >> "$LOG" 2>&1
|
|
||||||
echo " [$(date '+%H:%M:%S')] Calibration done." | tee -a "$LOG"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Step 2: Train with transfer
|
|
||||||
echo " [$(date '+%H:%M:%S')] Training ${EPISODES} episodes..." | tee -a "$LOG"
|
|
||||||
conda run --no-capture-output -n "$CONDA_ENV" python -u \
|
|
||||||
"$TRAIN_DIR/train_karman.py" \
|
|
||||||
--case-name "$CASE" --device-id $GPU --seed $TRAIN_SEED \
|
|
||||||
--config "$CONFIG" \
|
|
||||||
--calibration "$CAL_JSON" \
|
|
||||||
--si $SI --total-episodes $EPISODES \
|
|
||||||
--transfer-model "$BEST_MODEL" >> "$LOG" 2>&1
|
|
||||||
echo " [$(date '+%H:%M:%S')] Training done." | tee -a "$LOG"
|
|
||||||
|
|
||||||
echo "=== $(date): $CASE complete ===" | tee -a "$LOG"
|
|
||||||
echo ""
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "=== ALL DONE ===" | tee -a "$LOG_BASE/summary.log"
|
|
||||||
@@ -114,12 +114,13 @@ class ActionSmoother:
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
def record_target(config_path: str, device_id: int, si: int) -> np.ndarray:
|
def record_target(config_path: str, device_id: int, si: int,
|
||||||
|
dist_radius: float = 1.0) -> np.ndarray:
|
||||||
"""Record target signal (dist_cyl + sensors, no pinball)."""
|
"""Record target signal (dist_cyl + sensors, no pinball)."""
|
||||||
warmup = int(4.0 * NX / U0)
|
warmup = int(4.0 * NX / U0)
|
||||||
sim = Simulation(lbm_config_path=config_path, device_id=device_id)
|
sim = Simulation(lbm_config_path=config_path, device_id=device_id)
|
||||||
sim._assert_object_count_contract = lambda *a, **kw: None
|
sim._assert_object_count_contract = lambda *a, **kw: None
|
||||||
sim.add_body("circle", center=(DIST_X, CENTER_Y, 0.0), radius=1.0 * L0)
|
sim.add_body("circle", center=(DIST_X, CENTER_Y, 0.0), radius=dist_radius * L0)
|
||||||
s0 = sim.add_body("sensor", center=(SENSOR_X, CENTER_Y + 40.0, 0.0), radius=5.0)
|
s0 = sim.add_body("sensor", center=(SENSOR_X, CENTER_Y + 40.0, 0.0), radius=5.0)
|
||||||
s1 = sim.add_body("sensor", center=(SENSOR_X, CENTER_Y, 0.0), radius=5.0)
|
s1 = sim.add_body("sensor", center=(SENSOR_X, CENTER_Y, 0.0), radius=5.0)
|
||||||
s2 = sim.add_body("sensor", center=(SENSOR_X, CENTER_Y - 40.0, 0.0), radius=5.0)
|
s2 = sim.add_body("sensor", center=(SENSOR_X, CENTER_Y - 40.0, 0.0), radius=5.0)
|
||||||
@@ -236,7 +237,8 @@ class KarmanCloakEnv(gym.Env):
|
|||||||
self.sim = Simulation(lbm_config_path=self._config_path, device_id=self.device_id)
|
self.sim = Simulation(lbm_config_path=self._config_path, device_id=self.device_id)
|
||||||
self.sim._assert_object_count_contract = lambda *a, **kw: None
|
self.sim._assert_object_count_contract = lambda *a, **kw: None
|
||||||
|
|
||||||
self.dist_id = self.sim.add_body("circle", center=(DIST_X, CENTER_Y, 0.0), radius=1.0 * L0)
|
dist_radius = float(self._cal.get("dist_radius", 1.0))
|
||||||
|
self.dist_id = self.sim.add_body("circle", center=(DIST_X, CENTER_Y, 0.0), radius=dist_radius * L0)
|
||||||
self.sensor_ids = [
|
self.sensor_ids = [
|
||||||
self.sim.add_body("sensor", center=(SENSOR_X, CENTER_Y + 40.0, 0.0), radius=5.0),
|
self.sim.add_body("sensor", center=(SENSOR_X, CENTER_Y + 40.0, 0.0), radius=5.0),
|
||||||
self.sim.add_body("sensor", center=(SENSOR_X, CENTER_Y, 0.0), radius=5.0),
|
self.sim.add_body("sensor", center=(SENSOR_X, CENTER_Y, 0.0), radius=5.0),
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Extend cross-Re transfer training from best_model (all seed43).
|
|
||||||
# Re60: +200ep, Re200: +400ep, Re400: +400ep. Sequential GPU 0.
|
|
||||||
#
|
|
||||||
# Conservative fine-tune: lr=1e-4 (↓3x), n_epochs=5 (↓2x)
|
|
||||||
# Symmetry-prob keeps default 0.5.
|
|
||||||
#
|
|
||||||
# Output naming: transfer_re60ext → train_karman appends _seed43
|
|
||||||
# → transfer_re60ext_seed43, transfer_re200ext_seed43, transfer_re400ext_seed43
|
|
||||||
#
|
|
||||||
# Timetable (SI → s/ep @ n_epochs=5 → total):
|
|
||||||
# Re60 SI=800 ~100s/ep → 200ep ≈ 5.5h
|
|
||||||
# Re200 SI=500 ~ 68s/ep → 400ep ≈ 7.5h
|
|
||||||
# Re400 SI=400 ~ 60s/ep → 400ep ≈ 6.7h
|
|
||||||
# ─────────────────────────────────────────
|
|
||||||
# Total ≈ 19.7h
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
|
||||||
CONFIG_DIR="$REPO_DIR/configs"
|
|
||||||
TRAIN_DIR="$SCRIPT_DIR"
|
|
||||||
GPU=0
|
|
||||||
SEED=43
|
|
||||||
CONDA_ENV="pycuda_3_10"
|
|
||||||
LOG_BASE="/tmp/extend_transfer"
|
|
||||||
|
|
||||||
declare -A RE_SI RE_EP
|
|
||||||
RE_SI[60]=800; RE_EP[60]=200
|
|
||||||
RE_SI[200]=500; RE_EP[200]=400
|
|
||||||
RE_SI[400]=400; RE_EP[400]=400
|
|
||||||
|
|
||||||
RE_LIST="${1:-60,200,400}"
|
|
||||||
IFS=',' read -ra RE_ARR <<< "$RE_LIST"
|
|
||||||
|
|
||||||
mkdir -p "$LOG_BASE"
|
|
||||||
|
|
||||||
echo "=== Extend Cross-Re Transfer (seed43) ==="
|
|
||||||
echo " Output dirs: transfer_{re}ext_seed43/"
|
|
||||||
echo " Hyperparams: lr=1e-4, n_epochs=5, symmetry=0.5"
|
|
||||||
echo " Re60: +200ep (~5.5h, SI=800)"
|
|
||||||
echo " Re200: +400ep (~7.5h, SI=500)"
|
|
||||||
echo " Re400: +400ep (~6.7h, SI=400)"
|
|
||||||
echo " ────────────────────────────────"
|
|
||||||
echo " Total: ~19.7h"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
for re in "${RE_ARR[@]}"; do
|
|
||||||
vis="re${re}"
|
|
||||||
si=${RE_SI[$re]}
|
|
||||||
extra=${RE_EP[$re]}
|
|
||||||
|
|
||||||
CONFIG="$CONFIG_DIR/config_lbm_karman_2000x600_${vis}.json"
|
|
||||||
CAL_JSON="$TRAIN_DIR/calibrations/${vis}/calibration.json"
|
|
||||||
CASE="transfer_${vis}"
|
|
||||||
BEST_SRC="$TRAIN_DIR/output/${CASE}_seed${SEED}/models/best_model.zip"
|
|
||||||
EXT_CASE="${CASE}ext" # → transfer_re60ext_seed43
|
|
||||||
|
|
||||||
if [[ ! -f "$BEST_SRC" ]]; then
|
|
||||||
echo "[SKIP] ${vis}: best_model not found — ${CASE}_seed${SEED} (still training?)"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
LOG="$LOG_BASE/${vis}_ext.log"
|
|
||||||
echo "=== $(date): Extending ${vis} +${extra}ep (SI=$si) ===" | tee -a "$LOG"
|
|
||||||
echo " Source: $BEST_SRC" | tee -a "$LOG"
|
|
||||||
echo " Output: ${EXT_CASE}_seed${SEED}/" | tee -a "$LOG"
|
|
||||||
|
|
||||||
conda run --no-capture-output -n "$CONDA_ENV" python -u \
|
|
||||||
"$TRAIN_DIR/train_karman.py" \
|
|
||||||
--case-name "$EXT_CASE" --device-id $GPU --seed $SEED \
|
|
||||||
--config "$CONFIG" --calibration "$CAL_JSON" \
|
|
||||||
--si $si --total-episodes $extra \
|
|
||||||
--lr 1e-4 --n-epochs 5 \
|
|
||||||
--transfer-model "$BEST_SRC" >> "$LOG" 2>&1
|
|
||||||
|
|
||||||
echo " [$(date '+%H:%M:%S')] Done." | tee -a "$LOG"
|
|
||||||
echo ""
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "=== ALL EXTENSIONS DONE ===" | tee -a "$LOG_BASE/summary.log"
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Launch Illusion 0.75L training on GPU2, seed 43, 500 episodes
|
|
||||||
# Usage: bash launch_075L.sh
|
|
||||||
# !! Wait 2 min after launching 2L on GPU0 before running this script
|
|
||||||
# (to avoid kernel compilation race)
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
|
||||||
|
|
||||||
CASE="illusion_075L"
|
|
||||||
SEED=43
|
|
||||||
DEVICE=2
|
|
||||||
CONFIG="$REPO_DIR/configs/config_lbm_karman_2000x600.json"
|
|
||||||
CAL="$SCRIPT_DIR/calibrations/$CASE/calibration.json"
|
|
||||||
OUT_DIR="$SCRIPT_DIR/output/${CASE}_seed${SEED}"
|
|
||||||
|
|
||||||
mkdir -p "$OUT_DIR"
|
|
||||||
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
|
|
||||||
nohup conda run --no-capture-output -n pycuda_3_10 python -u train_illusion.py \
|
|
||||||
--case-name "$CASE" \
|
|
||||||
--device-id "$DEVICE" \
|
|
||||||
--seed "$SEED" \
|
|
||||||
--total-episodes 500 \
|
|
||||||
--config "$CONFIG" \
|
|
||||||
--calibration "$CAL" \
|
|
||||||
> "$OUT_DIR/nohup.log" 2>&1 &
|
|
||||||
|
|
||||||
echo "0.75L launched on GPU$DEVICE, seed=$SEED, PID=$!"
|
|
||||||
echo "Monitor: tail -f output/${CASE}_seed${SEED}/train.log"
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Launch Illusion 1.5L training on GPU1, seed 43, 500 episodes
|
|
||||||
# Usage: bash launch_15L.sh
|
|
||||||
# !! Wait 2 min after launching 1L on GPU0 before running this script
|
|
||||||
# (to avoid kernel compilation race)
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
|
||||||
|
|
||||||
CASE="illusion_15L"
|
|
||||||
SEED=43
|
|
||||||
DEVICE=1
|
|
||||||
CONFIG="$REPO_DIR/configs/config_lbm_karman_2000x600.json"
|
|
||||||
CAL="$SCRIPT_DIR/calibrations/$CASE/calibration.json"
|
|
||||||
OUT_DIR="$SCRIPT_DIR/output/${CASE}_seed${SEED}"
|
|
||||||
|
|
||||||
mkdir -p "$OUT_DIR"
|
|
||||||
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
|
|
||||||
nohup conda run --no-capture-output -n pycuda_3_10 python -u train_illusion.py \
|
|
||||||
--case-name "$CASE" \
|
|
||||||
--device-id "$DEVICE" \
|
|
||||||
--seed "$SEED" \
|
|
||||||
--total-episodes 500 \
|
|
||||||
--config "$CONFIG" \
|
|
||||||
--calibration "$CAL" \
|
|
||||||
> "$OUT_DIR/nohup.log" 2>&1 &
|
|
||||||
|
|
||||||
echo "1.5L launched on GPU$DEVICE, seed=$SEED, PID=$!"
|
|
||||||
echo "Monitor: tail -f output/${CASE}_seed${SEED}/train.log"
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Launch Illusion 1L training on GPU0, seed 43, 500 episodes
|
|
||||||
# Usage: bash launch_1L.sh
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
|
||||||
|
|
||||||
CASE="illusion_1L"
|
|
||||||
SEED=43
|
|
||||||
DEVICE=0
|
|
||||||
CONFIG="$REPO_DIR/configs/config_lbm_karman_2000x600.json"
|
|
||||||
CAL="$SCRIPT_DIR/calibrations/$CASE/calibration.json"
|
|
||||||
OUT_DIR="$SCRIPT_DIR/output/${CASE}_seed${SEED}"
|
|
||||||
|
|
||||||
mkdir -p "$OUT_DIR"
|
|
||||||
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
|
|
||||||
nohup conda run --no-capture-output -n pycuda_3_10 python -u train_illusion.py \
|
|
||||||
--case-name "$CASE" \
|
|
||||||
--device-id "$DEVICE" \
|
|
||||||
--seed "$SEED" \
|
|
||||||
--total-episodes 500 \
|
|
||||||
--config "$CONFIG" \
|
|
||||||
--calibration "$CAL" \
|
|
||||||
> "$OUT_DIR/nohup.log" 2>&1 &
|
|
||||||
|
|
||||||
echo "1L launched on GPU$DEVICE, seed=$SEED, PID=$!"
|
|
||||||
echo "Monitor: tail -f output/${CASE}_seed${SEED}/train.log"
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Launch Illusion 2L training on GPU0, seed 43, 500 episodes
|
|
||||||
# Usage: bash launch_2L.sh
|
|
||||||
# !! Wait 2 min after launching 15L on GPU1 before running this script
|
|
||||||
# (to avoid kernel compilation race)
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
|
||||||
|
|
||||||
CASE="illusion_2L"
|
|
||||||
SEED=43
|
|
||||||
DEVICE=0
|
|
||||||
CONFIG="$REPO_DIR/configs/config_lbm_karman_2000x600.json"
|
|
||||||
CAL="$SCRIPT_DIR/calibrations/$CASE/calibration.json"
|
|
||||||
OUT_DIR="$SCRIPT_DIR/output/${CASE}_seed${SEED}"
|
|
||||||
|
|
||||||
mkdir -p "$OUT_DIR"
|
|
||||||
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
|
|
||||||
nohup conda run --no-capture-output -n pycuda_3_10 python -u train_illusion.py \
|
|
||||||
--case-name "$CASE" \
|
|
||||||
--device-id "$DEVICE" \
|
|
||||||
--seed "$SEED" \
|
|
||||||
--total-episodes 500 \
|
|
||||||
--config "$CONFIG" \
|
|
||||||
--calibration "$CAL" \
|
|
||||||
> "$OUT_DIR/nohup.log" 2>&1 &
|
|
||||||
|
|
||||||
echo "2L launched on GPU$DEVICE, seed=$SEED, PID=$!"
|
|
||||||
echo "Monitor: tail -f output/${CASE}_seed${SEED}/train.log"
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# launch_multi.sh — Sequential multi-GPU training launcher for server deployment.
|
|
||||||
#
|
|
||||||
# Starts Karman Cloak training on multiple GPUs sequentially, with a configurable
|
|
||||||
# delay between launches to avoid CelerisLab kernel compilation race conditions.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# bash launch_multi.sh --case-name re100_karman --seeds 42,43,44,45,46,47 \
|
|
||||||
# --gpus 0,1,2,3,4,5 --episodes 500 \
|
|
||||||
# --config configs/config_lbm_karman_2000x600.json \
|
|
||||||
# --calibration calibrations/re100/calibration.json
|
|
||||||
#
|
|
||||||
# # Transfer learning from a base model
|
|
||||||
# bash launch_multi.sh --case-name re200_karman --seeds 42,43,44 \
|
|
||||||
# --gpus 0,1,2 --episodes 500 \
|
|
||||||
# --config configs/config_lbm_karman_2000x600_re200.json \
|
|
||||||
# --calibration calibrations/re200/calibration.json \
|
|
||||||
# --transfer output/re100_karman_seed42/models/best_model.zip
|
|
||||||
#
|
|
||||||
# Requirements:
|
|
||||||
# - conda env pycuda_3_10
|
|
||||||
# - CelerisLab submodule at DynamisLab/CelerisLab
|
|
||||||
# - train_karman.py, env_karman.py, symmetry_wrapper.py in same directory
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
|
|
||||||
# --- Defaults ---
|
|
||||||
CASE_NAME=""
|
|
||||||
SEEDS=""
|
|
||||||
GPUS=""
|
|
||||||
EPISODES=500
|
|
||||||
CONFIG=""
|
|
||||||
CALIBRATION=""
|
|
||||||
TRANSFER=""
|
|
||||||
DELAY_SECONDS=420 # 7 minutes between launches
|
|
||||||
CONDA_ENV="pycuda_3_10"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
echo "Usage: $0 --case-name NAME --seeds S1,S2,... --gpus G1,G2,... [options]"
|
|
||||||
echo ""
|
|
||||||
echo "Required:"
|
|
||||||
echo " --case-name NAME Case name for output dirs"
|
|
||||||
echo " --seeds S1,S2 Comma-separated seed values"
|
|
||||||
echo " --gpus G1,G2 Comma-separated GPU device IDs"
|
|
||||||
echo " --config PATH LBM config JSON"
|
|
||||||
echo " --calibration PATH calibration.json"
|
|
||||||
echo ""
|
|
||||||
echo "Options:"
|
|
||||||
echo " --episodes N Total episodes (default: 500)"
|
|
||||||
echo " --transfer PATH .zip model for transfer learning"
|
|
||||||
echo " --delay SEC Seconds between launches (default: 420)"
|
|
||||||
echo " --env NAME Conda env name (default: pycuda_3_10)"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--case-name) CASE_NAME="$2"; shift 2 ;;
|
|
||||||
--seeds) SEEDS="$2"; shift 2 ;;
|
|
||||||
--gpus) GPUS="$2"; shift 2 ;;
|
|
||||||
--episodes) EPISODES="$2"; shift 2 ;;
|
|
||||||
--config) CONFIG="$2"; shift 2 ;;
|
|
||||||
--calibration) CALIBRATION="$2"; shift 2 ;;
|
|
||||||
--transfer) TRANSFER="$2"; shift 2 ;;
|
|
||||||
--delay) DELAY_SECONDS="$2"; shift 2 ;;
|
|
||||||
--env) CONDA_ENV="$2"; shift 2 ;;
|
|
||||||
*) echo "Unknown option: $1"; usage ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Validate
|
|
||||||
if [[ -z "$CASE_NAME" || -z "$SEEDS" || -z "$GPUS" || -z "$CONFIG" || -z "$CALIBRATION" ]]; then
|
|
||||||
echo "ERROR: Missing required arguments."
|
|
||||||
usage
|
|
||||||
fi
|
|
||||||
|
|
||||||
IFS=',' read -ra SEED_ARR <<< "$SEEDS"
|
|
||||||
IFS=',' read -ra GPU_ARR <<< "$GPUS"
|
|
||||||
|
|
||||||
if [[ ${#SEED_ARR[@]} -ne ${#GPU_ARR[@]} ]]; then
|
|
||||||
echo "ERROR: Number of seeds (${#SEED_ARR[@]}) must match number of GPUs (${#GPU_ARR[@]})."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "=== Multi-GPU Training Launcher ==="
|
|
||||||
echo " Case: $CASE_NAME"
|
|
||||||
echo " Seeds: $SEEDS"
|
|
||||||
echo " GPUs: $GPUS"
|
|
||||||
echo " Episodes: $EPISODES"
|
|
||||||
echo " Config: $CONFIG"
|
|
||||||
echo " Calibration: $CALIBRATION"
|
|
||||||
echo " Transfer: ${TRANSFER:-none}"
|
|
||||||
echo " Delay: ${DELAY_SECONDS}s between launches"
|
|
||||||
echo " Jobs: ${#SEED_ARR[@]}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Build transfer arg
|
|
||||||
TRANSFER_ARG=""
|
|
||||||
if [[ -n "$TRANSFER" ]]; then
|
|
||||||
TRANSFER_ARG="--transfer-model $TRANSFER"
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$SCRIPT_DIR/output"
|
|
||||||
|
|
||||||
for i in "${!SEED_ARR[@]}"; do
|
|
||||||
seed="${SEED_ARR[$i]}"
|
|
||||||
gpu="${GPU_ARR[$i]}"
|
|
||||||
run_name="${CASE_NAME}_seed${seed}"
|
|
||||||
logfile="$SCRIPT_DIR/output/${run_name}/nohup.log"
|
|
||||||
|
|
||||||
mkdir -p "$SCRIPT_DIR/output/${run_name}"
|
|
||||||
|
|
||||||
echo "[$(date '+%H:%M:%S')] Launching seed=$seed on GPU=$gpu ..."
|
|
||||||
nohup conda run --no-capture-output -n "$CONDA_ENV" python -u \
|
|
||||||
"$SCRIPT_DIR/train_karman.py" \
|
|
||||||
--case-name "$CASE_NAME" \
|
|
||||||
--device-id "$gpu" \
|
|
||||||
--seed "$seed" \
|
|
||||||
--total-episodes "$EPISODES" \
|
|
||||||
--config "$CONFIG" \
|
|
||||||
--calibration "$CALIBRATION" \
|
|
||||||
$TRANSFER_ARG \
|
|
||||||
> "$logfile" 2>&1 &
|
|
||||||
|
|
||||||
echo " PID: $!"
|
|
||||||
echo " Log: $logfile"
|
|
||||||
|
|
||||||
if [[ $i -lt $((${#SEED_ARR[@]} - 1)) ]]; then
|
|
||||||
echo " Waiting ${DELAY_SECONDS}s before next launch..."
|
|
||||||
sleep "$DELAY_SECONDS"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "All jobs launched. Monitor with:"
|
|
||||||
echo " tail -f $SCRIPT_DIR/output/${CASE_NAME}_seed*/nohup.log"
|
|
||||||
echo " tensorboard --logdir $SCRIPT_DIR/output/${CASE_NAME}_seed*/tb"
|
|
||||||
echo ""
|
|
||||||
echo "To stop all:"
|
|
||||||
echo " ps aux | grep train_karman | grep -v grep | awk '{print \$2}' | xargs kill"
|
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# crossre_scratch.sh — Cross-Re From-Scratch Training
|
||||||
|
#
|
||||||
|
# Trains re60/re200/re400 from scratch on single GPU, serially.
|
||||||
|
# Uses generic SIM_BP, K_CD/CL=50/100, lr=3e-4.
|
||||||
|
# Output: output/kar_{re60,re200,re400}_sc_seed43/
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash crossre_scratch.sh [--gpu 0] [--episodes 500]
|
||||||
|
# bash crossre_scratch.sh --only re200
|
||||||
|
# =============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
TRAIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
REPO_DIR="$(cd "$TRAIN_DIR/../../.." && pwd)"
|
||||||
|
CONDA_ENV="pycuda_3_10"
|
||||||
|
GPU=0; EPISODES=500; DELAY=120; ONLY=""; SEED=43
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--gpu) GPU="$2"; shift 2 ;;
|
||||||
|
--episodes) EPISODES="$2"; shift 2 ;;
|
||||||
|
--only) ONLY="$2"; shift 2 ;;
|
||||||
|
*) echo "Unknown: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "=== Cross-Re Scratch: GPU=$GPU, ep=$EPISODES ==="
|
||||||
|
|
||||||
|
declare -A RE_CFGS=(
|
||||||
|
[re60]="$REPO_DIR/configs/config_lbm_karman_2000x600_re60.json"
|
||||||
|
[re200]="$REPO_DIR/configs/config_lbm_karman_2000x600_re200.json"
|
||||||
|
[re400]="$REPO_DIR/configs/config_lbm_karman_2000x600_re400.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
for re in re60 re200 re400; do
|
||||||
|
[[ -n "$ONLY" && "$ONLY" != "$re" ]] && continue
|
||||||
|
CASE="kar_${re}_sc"
|
||||||
|
CAL="$TRAIN_DIR/calibrations/kar_${re}_sc/calibration.json"
|
||||||
|
CFG="${RE_CFGS[$re]}"
|
||||||
|
echo " [$(date '+%H:%M:%S')] Training $CASE..."
|
||||||
|
conda run --no-capture-output -n "$CONDA_ENV" python -u \
|
||||||
|
"$TRAIN_DIR/train_karman.py" \
|
||||||
|
--case-name "$CASE" --device-id "$GPU" --seed "$SEED" \
|
||||||
|
--config "$CFG" --calibration "$CAL" \
|
||||||
|
--total-episodes "$EPISODES" --lr 0.0003
|
||||||
|
echo " [$(date '+%H:%M:%S')] $CASE done. Sleeping ${DELAY}s..."
|
||||||
|
sleep "$DELAY"
|
||||||
|
done
|
||||||
|
echo "=== All done ==="
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# crossre_transfer.sh — Cross-Re Transfer Learning
|
||||||
|
#
|
||||||
|
# Transfers from kar_re100_sc baseline to re60/re200/re400.
|
||||||
|
# Uses original measured-SIM_BP calibrations (kar_re*).
|
||||||
|
# Kept for comparison; scratch better for r_sim, transfer better for r_cd.
|
||||||
|
#
|
||||||
|
# Output: output/kar_{re60,re200,re400}_tr_seed43/
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash crossre_transfer.sh [--gpu 0] [--episodes 200]
|
||||||
|
# =============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
TRAIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
REPO_DIR="$(cd "$TRAIN_DIR/../../.." && pwd)"
|
||||||
|
CONDA_ENV="pycuda_3_10"
|
||||||
|
GPU=0; EPISODES=200; DELAY=120; SEED=43
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--gpu) GPU="$2"; shift 2 ;;
|
||||||
|
--episodes) EPISODES="$2"; shift 2 ;;
|
||||||
|
*) echo "Unknown: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "=== Cross-Re Transfer: GPU=$GPU, ep=$EPISODES ==="
|
||||||
|
|
||||||
|
BASE_MODEL="$TRAIN_DIR/output/kar_re100_sc_seed${SEED}/models/best_model.zip"
|
||||||
|
if [[ ! -f "$BASE_MODEL" ]]; then
|
||||||
|
echo "ERROR: base model not found: $BASE_MODEL"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
declare -A RE_CFGS=(
|
||||||
|
[re60]="$REPO_DIR/configs/config_lbm_karman_2000x600_re60.json"
|
||||||
|
[re200]="$REPO_DIR/configs/config_lbm_karman_2000x600_re200.json"
|
||||||
|
[re400]="$REPO_DIR/configs/config_lbm_karman_2000x600_re400.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
for re in re60 re200 re400; do
|
||||||
|
CASE="kar_${re}_tr"
|
||||||
|
CAL="$TRAIN_DIR/calibrations/kar_${re}/calibration.json"
|
||||||
|
CFG="${RE_CFGS[$re]}"
|
||||||
|
echo " [$(date '+%H:%M:%S')] Training $CASE..."
|
||||||
|
conda run --no-capture-output -n "$CONDA_ENV" python -u \
|
||||||
|
"$TRAIN_DIR/train_karman.py" \
|
||||||
|
--case-name "$CASE" --device-id "$GPU" --seed "$SEED" \
|
||||||
|
--config "$CFG" --calibration "$CAL" \
|
||||||
|
--total-episodes "$EPISODES" --lr 0.0003 \
|
||||||
|
--transfer-model "$BASE_MODEL"
|
||||||
|
echo " [$(date '+%H:%M:%S')] $CASE done. Sleeping ${DELAY}s..."
|
||||||
|
sleep "$DELAY"
|
||||||
|
done
|
||||||
|
echo "=== All done ==="
|
||||||
Executable
+59
@@ -0,0 +1,59 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Resume interrupted training from an episode checkpoint.
|
||||||
|
# Loads ep{N}_model.zip + ep{N}_vecnormalize.pkl and continues.
|
||||||
|
#
|
||||||
|
# Case naming: {domain}_{variant}_{method}_seed{seed}
|
||||||
|
# domain: kar(man) | ill(usion)
|
||||||
|
# variant: re{60,100,200,400} | d{075,15,2} | {1L,15L,075L,2L}
|
||||||
|
# method: sc(ratch) | tr(ansfer)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash resume.sh --case kar_re60_sc --seed 43 --resume 460 [--gpu 0]
|
||||||
|
# =============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
TRAIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
REPO_DIR="$(cd "$TRAIN_DIR/../../.." && pwd)"
|
||||||
|
CONDA_ENV="pycuda_3_10"
|
||||||
|
GPU=0; EPISODES=500; CASE=""; SEED=43; RESUME_EP=0
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--case) CASE="$2"; shift 2 ;;
|
||||||
|
--seed) SEED="$2"; shift 2 ;;
|
||||||
|
--resume) RESUME_EP="$2"; shift 2 ;;
|
||||||
|
--episodes) EPISODES="$2"; shift 2 ;;
|
||||||
|
--gpu) GPU="$2"; shift 2 ;;
|
||||||
|
*) echo "Unknown: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -z "$CASE" || "$RESUME_EP" -eq 0 ]] && { echo "Usage: resume.sh --case CASE --resume EP [--seed 43]"; exit 1; }
|
||||||
|
|
||||||
|
# Map case prefix to LBM config
|
||||||
|
if [[ "$CASE" == kar_re60* ]]; then CFG="$REPO_DIR/configs/config_lbm_karman_2000x600_re60.json"
|
||||||
|
elif [[ "$CASE" == kar_re200* ]]; then CFG="$REPO_DIR/configs/config_lbm_karman_2000x600_re200.json"
|
||||||
|
elif [[ "$CASE" == kar_re400* ]]; then CFG="$REPO_DIR/configs/config_lbm_karman_2000x600_re400.json"
|
||||||
|
elif [[ "$CASE" == kar_* ]] || [[ "$CASE" == ill_* ]]; then CFG="$REPO_DIR/configs/config_lbm_karman_2000x600.json"
|
||||||
|
else echo "ERROR: unknown case pattern: $CASE"; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Map case name to calibration: kar_d075_sc → calibrations/kar_d075_sc/
|
||||||
|
CAL="$TRAIN_DIR/calibrations/${CASE%%_seed*}/calibration.json"
|
||||||
|
if [[ ! -f "$CAL" ]]; then
|
||||||
|
echo "ERROR: calibration not found: $CAL"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Resume: $CASE (seed=$SEED, resume=$RESUME_EP, total=$EPISODES, GPU=$GPU) ==="
|
||||||
|
echo " Config: $CFG"
|
||||||
|
echo " Calibration: $CAL"
|
||||||
|
|
||||||
|
conda run --no-capture-output -n "$CONDA_ENV" python -u \
|
||||||
|
"$TRAIN_DIR/train_karman.py" \
|
||||||
|
--case-name "$CASE" --device-id "$GPU" --seed "$SEED" \
|
||||||
|
--config "$CFG" --calibration "$CAL" \
|
||||||
|
--total-episodes "$EPISODES" --lr 0.0003 \
|
||||||
|
--resume-from "$RESUME_EP"
|
||||||
|
echo "=== Done ==="
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# train_baseline.sh — Karman Re100 Baseline (scratch, multi-seed)
|
||||||
|
#
|
||||||
|
# Trains kar_re100_sc from scratch with N seeds on a single GPU.
|
||||||
|
# Output: output/kar_re100_sc_seed{N}/
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash train_baseline.sh [--seeds 41,42,43,44,45] [--gpu 0] [--episodes 500]
|
||||||
|
# =============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
TRAIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
REPO_DIR="$(cd "$TRAIN_DIR/../../.." && pwd)"
|
||||||
|
CONDA_ENV="pycuda_3_10"
|
||||||
|
GPU=0; EPISODES=500; DELAY=120
|
||||||
|
SEEDS="41,42,43,44,45"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--seeds) SEEDS="$2"; shift 2 ;;
|
||||||
|
--gpu) GPU="$2"; shift 2 ;;
|
||||||
|
--episodes) EPISODES="$2"; shift 2 ;;
|
||||||
|
*) echo "Unknown: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
IFS=',' read -ra SEED_ARR <<< "$SEEDS"
|
||||||
|
CAL="$TRAIN_DIR/calibrations/kar_re100/calibration.json"
|
||||||
|
CFG="$REPO_DIR/configs/config_lbm_karman_2000x600.json"
|
||||||
|
|
||||||
|
echo "=== Baseline: kar_re100_sc, seeds=${SEED_ARR[*]}, ep=$EPISODES, GPU=$GPU ==="
|
||||||
|
|
||||||
|
for seed in "${SEED_ARR[@]}"; do
|
||||||
|
echo " [$(date '+%H:%M:%S')] Training seed=$seed..."
|
||||||
|
conda run --no-capture-output -n "$CONDA_ENV" python -u \
|
||||||
|
"$TRAIN_DIR/train_karman.py" \
|
||||||
|
--case-name kar_re100_sc --device-id "$GPU" --seed "$seed" \
|
||||||
|
--config "$CFG" --calibration "$CAL" \
|
||||||
|
--total-episodes "$EPISODES" --lr 0.0003
|
||||||
|
echo " [$(date '+%H:%M:%S')] Seed $seed done. Sleeping ${DELAY}s..."
|
||||||
|
sleep "$DELAY"
|
||||||
|
done
|
||||||
|
echo "=== All done ==="
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# train_illusion.sh — Hydrodynamic Illusion Training (scratch)
|
||||||
|
#
|
||||||
|
# Trains all 4 target cylinder sizes on a single GPU, serially.
|
||||||
|
# Output: output/ill_{1L,15L,075L,2L}_sc_seed43/
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash train_illusion.sh [--gpu 0] [--episodes 500]
|
||||||
|
# bash train_illusion.sh --only 2L
|
||||||
|
# =============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
TRAIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
REPO_DIR="$(cd "$TRAIN_DIR/../../.." && pwd)"
|
||||||
|
CONDA_ENV="pycuda_3_10"
|
||||||
|
GPU=0; EPISODES=500; DELAY=120; SEED=43; ONLY=""
|
||||||
|
CFG="$REPO_DIR/configs/config_lbm_karman_2000x600.json"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--gpu) GPU="$2"; shift 2 ;;
|
||||||
|
--episodes) EPISODES="$2"; shift 2 ;;
|
||||||
|
--only) ONLY="$2"; shift 2 ;;
|
||||||
|
*) echo "Unknown: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "=== Illusion: GPU=$GPU, ep=$EPISODES ==="
|
||||||
|
|
||||||
|
for size in 1L 075L 15L 2L; do
|
||||||
|
[[ -n "$ONLY" && "$ONLY" != "$size" ]] && continue
|
||||||
|
CASE="ill_${size}_sc"
|
||||||
|
CAL="$TRAIN_DIR/calibrations/ill_${size}/calibration.json"
|
||||||
|
echo " [$(date '+%H:%M:%S')] Training $CASE..."
|
||||||
|
conda run --no-capture-output -n "$CONDA_ENV" python -u \
|
||||||
|
"$TRAIN_DIR/train_illusion.py" \
|
||||||
|
--case-name "$CASE" --device-id "$GPU" --seed "$SEED" \
|
||||||
|
--config "$CFG" --calibration "$CAL" \
|
||||||
|
--total-episodes "$EPISODES"
|
||||||
|
echo " [$(date '+%H:%M:%S')] $CASE done. Sleeping ${DELAY}s..."
|
||||||
|
sleep "$DELAY"
|
||||||
|
done
|
||||||
|
echo "=== All done ==="
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# vardist_scratch.sh — Variable-Dist Cylinder From-Scratch Training
|
||||||
|
#
|
||||||
|
# Trains d075/d15/d2 from scratch on single GPU.
|
||||||
|
# Uses generic SIM_BP, K_CD/CL=50/100, lr=3e-4.
|
||||||
|
# Output: output/kar_{d075,d15,d2}_sc_seed{N}/
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash vardist_scratch.sh [--gpu 0] [--episodes 500]
|
||||||
|
# bash vardist_scratch.sh --only d075
|
||||||
|
# =============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
TRAIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
REPO_DIR="$(cd "$TRAIN_DIR/../../.." && pwd)"
|
||||||
|
CONDA_ENV="pycuda_3_10"
|
||||||
|
GPU=0; EPISODES=500; DELAY=120; ONLY=""
|
||||||
|
CFG="$REPO_DIR/configs/config_lbm_karman_2000x600.json"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--gpu) GPU="$2"; shift 2 ;;
|
||||||
|
--episodes) EPISODES="$2"; shift 2 ;;
|
||||||
|
--only) ONLY="$2"; shift 2 ;;
|
||||||
|
*) echo "Unknown: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "=== Vardist Scratch: GPU=$GPU, ep=$EPISODES ==="
|
||||||
|
|
||||||
|
declare -A SEEDS=( [d075]=44 [d15]=45 [d2]=45 )
|
||||||
|
|
||||||
|
for diam in d075 d15 d2; do
|
||||||
|
[[ -n "$ONLY" && "$ONLY" != "$diam" ]] && continue
|
||||||
|
SEED="${SEEDS[$diam]}"
|
||||||
|
CASE="kar_${diam}_sc"
|
||||||
|
CAL="$TRAIN_DIR/calibrations/kar_${diam}_sc/calibration.json"
|
||||||
|
echo " [$(date '+%H:%M:%S')] Training $CASE (seed=$SEED)..."
|
||||||
|
conda run --no-capture-output -n "$CONDA_ENV" python -u \
|
||||||
|
"$TRAIN_DIR/train_karman.py" \
|
||||||
|
--case-name "$CASE" --device-id "$GPU" --seed "$SEED" \
|
||||||
|
--config "$CFG" --calibration "$CAL" \
|
||||||
|
--total-episodes "$EPISODES" --lr 0.0003
|
||||||
|
echo " [$(date '+%H:%M:%S')] $CASE done. Sleeping ${DELAY}s..."
|
||||||
|
sleep "$DELAY"
|
||||||
|
done
|
||||||
|
echo "=== All done ==="
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# vardist_transfer.sh — Variable-Dist Cylinder Transfer Learning
|
||||||
|
#
|
||||||
|
# Transfers from kar_re100_sc baseline to different dist cylinder sizes.
|
||||||
|
# Uses original measured-SIM_BP calibrations (kar_d*_tr).
|
||||||
|
# Kept for comparison; scratch (vardist_scratch.sh) produces better results.
|
||||||
|
#
|
||||||
|
# Output: output/kar_{d075,d15,d2}_tr_seed{N}/
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash vardist_transfer.sh [--gpu 0] [--episodes 200]
|
||||||
|
# =============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
TRAIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
REPO_DIR="$(cd "$TRAIN_DIR/../../.." && pwd)"
|
||||||
|
CONDA_ENV="pycuda_3_10"
|
||||||
|
GPU=0; EPISODES=200; DELAY=120
|
||||||
|
CFG="$REPO_DIR/configs/config_lbm_karman_2000x600.json"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--gpu) GPU="$2"; shift 2 ;;
|
||||||
|
--episodes) EPISODES="$2"; shift 2 ;;
|
||||||
|
*) echo "Unknown: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "=== Vardist Transfer: GPU=$GPU, ep=$EPISODES ==="
|
||||||
|
|
||||||
|
declare -A SEEDS=( [d075]=44 [d15]=45 [d2]=45 )
|
||||||
|
declare -A BASE_SEEDS=( [d075]=44 [d15]=45 [d2]=45 )
|
||||||
|
|
||||||
|
for diam in d075 d15 d2; do
|
||||||
|
SEED="${SEEDS[$diam]}"
|
||||||
|
BS="${BASE_SEEDS[$diam]}"
|
||||||
|
BASE_MODEL="$TRAIN_DIR/output/kar_re100_sc_seed${BS}/models/best_model.zip"
|
||||||
|
|
||||||
|
if [[ ! -f "$BASE_MODEL" ]]; then
|
||||||
|
echo " SKIP $diam: base model not found: $BASE_MODEL"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
CASE="kar_${diam}_tr"
|
||||||
|
CAL="$TRAIN_DIR/calibrations/kar_${diam}_tr/calibration.json"
|
||||||
|
echo " [$(date '+%H:%M:%S')] Training $CASE (seed=$SEED)..."
|
||||||
|
conda run --no-capture-output -n "$CONDA_ENV" python -u \
|
||||||
|
"$TRAIN_DIR/train_karman.py" \
|
||||||
|
--case-name "$CASE" --device-id "$GPU" --seed "$SEED" \
|
||||||
|
--config "$CFG" --calibration "$CAL" \
|
||||||
|
--total-episodes "$EPISODES" --lr 0.0003 \
|
||||||
|
--transfer-model "$BASE_MODEL"
|
||||||
|
echo " [$(date '+%H:%M:%S')] $CASE done. Sleeping ${DELAY}s..."
|
||||||
|
sleep "$DELAY"
|
||||||
|
done
|
||||||
|
echo "=== All done ==="
|
||||||
@@ -61,6 +61,10 @@ def main() -> int:
|
|||||||
help="G-symmetry augmentation probability (0=off, 0.5=half)")
|
help="G-symmetry augmentation probability (0=off, 0.5=half)")
|
||||||
parser.add_argument("--transfer-model", type=str, default=None,
|
parser.add_argument("--transfer-model", type=str, default=None,
|
||||||
help="Path to .zip model for transfer learning")
|
help="Path to .zip model for transfer learning")
|
||||||
|
parser.add_argument("--resume-from", type=int, default=0,
|
||||||
|
help="Resume from episode N (0=start fresh). Loads ep{N}_model.zip "
|
||||||
|
"and ep{N}_vecnormalize.pkl, parses train.log for best_reward, "
|
||||||
|
"continues from ep N+1.")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Load calibration
|
# Load calibration
|
||||||
@@ -78,6 +82,38 @@ def main() -> int:
|
|||||||
out_dir = Path(__file__).resolve().parent / "output" / run_name
|
out_dir = Path(__file__).resolve().parent / "output" / run_name
|
||||||
(out_dir / "models").mkdir(parents=True, exist_ok=True)
|
(out_dir / "models").mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# ── Resume mode: load checkpoint and restore best_reward ──────────────
|
||||||
|
resume_ep = args.resume_from
|
||||||
|
resume_best_reward = -float("inf")
|
||||||
|
|
||||||
|
if resume_ep > 0:
|
||||||
|
log_path_tmp = out_dir / "train.log"
|
||||||
|
def _log(msg):
|
||||||
|
line = f"[{time.strftime('%H:%M:%S')}] {msg}"
|
||||||
|
print(line, flush=True)
|
||||||
|
with open(log_path_tmp, "a") as f: f.write(line + "\n"); f.flush()
|
||||||
|
_log(f"=== V5 {run_name} (RESUME from ep {resume_ep}) ===")
|
||||||
|
if log_path_tmp.exists():
|
||||||
|
with open(log_path_tmp, "r") as f:
|
||||||
|
for line in f:
|
||||||
|
if "BEST" in line:
|
||||||
|
import re
|
||||||
|
m = re.search(r'reward=([\d.]+)\s+\(BEST', line)
|
||||||
|
if m:
|
||||||
|
resume_best_reward = float(m.group(1))
|
||||||
|
_log(f" Recovered best_reward={resume_best_reward:.4f} from log")
|
||||||
|
else:
|
||||||
|
_log(" WARNING: train.log not found, best_reward set to -inf")
|
||||||
|
ckpt_model = out_dir / "models" / f"ep{resume_ep:04d}_model.zip"
|
||||||
|
ckpt_norm = out_dir / "models" / f"ep{resume_ep:04d}_vecnormalize.pkl"
|
||||||
|
if not ckpt_model.exists():
|
||||||
|
_log(f" ERROR: Checkpoint not found: {ckpt_model}")
|
||||||
|
return 1
|
||||||
|
if not ckpt_norm.exists():
|
||||||
|
_log(f" ERROR: Checkpoint not found: {ckpt_norm}")
|
||||||
|
return 1
|
||||||
|
_log(f" Resume checkpoint: {ckpt_model}")
|
||||||
|
|
||||||
log_path = out_dir / "train.log"
|
log_path = out_dir / "train.log"
|
||||||
def log(msg):
|
def log(msg):
|
||||||
line = f"[{time.strftime('%H:%M:%S')}] {msg}"
|
line = f"[{time.strftime('%H:%M:%S')}] {msg}"
|
||||||
@@ -96,8 +132,9 @@ def main() -> int:
|
|||||||
cal_copy["config_path"] = config_path
|
cal_copy["config_path"] = config_path
|
||||||
if args.transfer_model:
|
if args.transfer_model:
|
||||||
cal_copy["transfer_model"] = args.transfer_model
|
cal_copy["transfer_model"] = args.transfer_model
|
||||||
with open(out_dir / "calibration.json", "w") as f:
|
if not resume_ep:
|
||||||
json.dump(cal_copy, f, indent=2)
|
with open(out_dir / "calibration.json", "w") as f:
|
||||||
|
json.dump(cal_copy, f, indent=2)
|
||||||
|
|
||||||
# Load target
|
# Load target
|
||||||
cal_dir = Path(args.calibration).resolve().parent
|
cal_dir = Path(args.calibration).resolve().parent
|
||||||
@@ -108,7 +145,8 @@ def main() -> int:
|
|||||||
else:
|
else:
|
||||||
log(" Recording target...")
|
log(" Recording target...")
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
target_states = record_target(config_path, args.device_id, si_val)
|
dist_radius = float(cal.get("dist_radius", 1.0))
|
||||||
|
target_states = record_target(config_path, args.device_id, si_val, dist_radius=dist_radius)
|
||||||
np.save(str(out_dir / "target.npy"), target_states)
|
np.save(str(out_dir / "target.npy"), target_states)
|
||||||
log(f" Target recorded in {time.perf_counter()-t0:.0f}s")
|
log(f" Target recorded in {time.perf_counter()-t0:.0f}s")
|
||||||
|
|
||||||
@@ -120,8 +158,16 @@ def main() -> int:
|
|||||||
env = SymmetryAugmentWrapper(env, prob=args.symmetry_prob, seed=args.seed,
|
env = SymmetryAugmentWrapper(env, prob=args.symmetry_prob, seed=args.seed,
|
||||||
rollout_len=args.n_steps)
|
rollout_len=args.n_steps)
|
||||||
vec_env = DummyVecEnv([lambda: env])
|
vec_env = DummyVecEnv([lambda: env])
|
||||||
vec_env = VecNormalize(vec_env, norm_obs=True, norm_reward=False,
|
if resume_ep and ckpt_norm.exists():
|
||||||
clip_obs=10.0, gamma=0.99)
|
# Resume: restore single-layer VecNormalize from checkpoint
|
||||||
|
vec_env = VecNormalize.load(str(ckpt_norm), vec_env)
|
||||||
|
vec_env.training = True
|
||||||
|
vec_env.norm_reward = False
|
||||||
|
log(" Loaded VecNormalize from checkpoint.")
|
||||||
|
else:
|
||||||
|
# Fresh training or transfer: create new VecNormalize
|
||||||
|
vec_env = VecNormalize(vec_env, norm_obs=True, norm_reward=False,
|
||||||
|
clip_obs=10.0, gamma=0.99)
|
||||||
log(f" Env ready in {time.perf_counter()-t0:.0f}s")
|
log(f" Env ready in {time.perf_counter()-t0:.0f}s")
|
||||||
|
|
||||||
device = torch.device(f"cuda:{args.device_id}")
|
device = torch.device(f"cuda:{args.device_id}")
|
||||||
@@ -132,6 +178,11 @@ def main() -> int:
|
|||||||
model = PPO.load(args.transfer_model, env=vec_env, device=device,
|
model = PPO.load(args.transfer_model, env=vec_env, device=device,
|
||||||
custom_objects={"activation_fn": Sin})
|
custom_objects={"activation_fn": Sin})
|
||||||
log(" Loaded base model.")
|
log(" Loaded base model.")
|
||||||
|
elif resume_ep:
|
||||||
|
log(f" Resuming from: {ckpt_model}")
|
||||||
|
model = PPO.load(str(ckpt_model), env=vec_env, device=device,
|
||||||
|
custom_objects={"activation_fn": Sin})
|
||||||
|
log(" Loaded checkpoint model.")
|
||||||
else:
|
else:
|
||||||
model = PPO(
|
model = PPO(
|
||||||
"MlpPolicy",
|
"MlpPolicy",
|
||||||
@@ -147,11 +198,16 @@ def main() -> int:
|
|||||||
)
|
)
|
||||||
log(" Created from scratch.")
|
log(" Created from scratch.")
|
||||||
|
|
||||||
best_reward = -float("inf")
|
best_reward = resume_best_reward if resume_ep else -float("inf")
|
||||||
t_last = time.perf_counter()
|
t_last = time.perf_counter()
|
||||||
norm_path = str(out_dir / "vec_normalize.pkl")
|
norm_path = str(out_dir / "vec_normalize.pkl")
|
||||||
|
|
||||||
for ep in range(1, args.total_episodes + 1):
|
# Save initial vec_normalize state (important after checkpoint load)
|
||||||
|
if resume_ep:
|
||||||
|
vec_env.save(norm_path)
|
||||||
|
|
||||||
|
start_ep = resume_ep + 1 if resume_ep else 1
|
||||||
|
for ep in range(start_ep, args.total_episodes + 1):
|
||||||
model.learn(total_timesteps=args.learn_timesteps, reset_num_timesteps=False)
|
model.learn(total_timesteps=args.learn_timesteps, reset_num_timesteps=False)
|
||||||
|
|
||||||
# Evaluation (disable symmetry for clean policy eval)
|
# Evaluation (disable symmetry for clean policy eval)
|
||||||
|
|||||||
@@ -1,492 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Post-training flow-field visualization and quantitative analysis.
|
|
||||||
|
|
||||||
Compares control modes for Karman Cloak 2000x600:
|
|
||||||
- target: disturbance-only reference (no pinball)
|
|
||||||
- zero: pinball with zero rotation
|
|
||||||
- bias: open-loop bias action (zeros -> [0,-4,4]*U0)
|
|
||||||
- bias_drl: trained Bias best model
|
|
||||||
- nobias_drl: trained NoBias best model
|
|
||||||
|
|
||||||
Outputs to output/flow_analysis_v4/:
|
|
||||||
vorticity_*.png, macro_*.npz, metrics summary, comparison plots.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
conda run -n pycuda_3_10 python -u visualize_and_analyze.py --device-id 0
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Callable, Dict, List, Optional, Tuple
|
|
||||||
|
|
||||||
import matplotlib
|
|
||||||
matplotlib.use("Agg")
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import numpy as np
|
|
||||||
import pycuda.driver as cuda
|
|
||||||
|
|
||||||
cuda.init()
|
|
||||||
|
|
||||||
_REPO = Path(__file__).resolve().parents[3]
|
|
||||||
if str(_REPO) not in sys.path:
|
|
||||||
sys.path.insert(0, str(_REPO))
|
|
||||||
|
|
||||||
import torch
|
|
||||||
from torch.nn import Module as TorchModule
|
|
||||||
from stable_baselines3 import PPO
|
|
||||||
from stable_baselines3.common.vec_env import DummyVecEnv, VecNormalize
|
|
||||||
from CelerisLab.common.render import compute_vorticity, render_vorticity_field
|
|
||||||
|
|
||||||
from env_karman_2000x600 import (
|
|
||||||
KarmanCloakEnv,
|
|
||||||
SI,
|
|
||||||
FIFO_LEN,
|
|
||||||
CONV_LEN,
|
|
||||||
NX,
|
|
||||||
NY,
|
|
||||||
L0,
|
|
||||||
U0,
|
|
||||||
RADIUS,
|
|
||||||
CENTER_Y,
|
|
||||||
DIST_X,
|
|
||||||
PINBALL_FRONT_X,
|
|
||||||
PINBALL_REAR_X,
|
|
||||||
SENSOR_X,
|
|
||||||
SENSOR_CC,
|
|
||||||
FORCE_SCALE,
|
|
||||||
compute_similarity,
|
|
||||||
record_target,
|
|
||||||
WARMUP_STEPS,
|
|
||||||
CFG_PATH,
|
|
||||||
)
|
|
||||||
|
|
||||||
TRAIN_DIR = Path(__file__).resolve().parent
|
|
||||||
BIAS_RUN = TRAIN_DIR / "output" / "bias_seed42_s2048_e10_v4"
|
|
||||||
NOBIAS_RUN = TRAIN_DIR / "output" / "nobias_seed42_s2048_e10_v4"
|
|
||||||
|
|
||||||
CYLINDERS_FULL = [
|
|
||||||
((DIST_X, CENTER_Y), 1.0 * L0),
|
|
||||||
((PINBALL_FRONT_X, CENTER_Y), RADIUS),
|
|
||||||
((PINBALL_REAR_X, CENTER_Y + 15.0), RADIUS),
|
|
||||||
((PINBALL_REAR_X, CENTER_Y - 15.0), RADIUS),
|
|
||||||
]
|
|
||||||
CYLINDERS_DIST = [((DIST_X, CENTER_Y), 1.0 * L0)]
|
|
||||||
|
|
||||||
|
|
||||||
class Sin(TorchModule):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
def forward(self, x):
|
|
||||||
return torch.sin(x)
|
|
||||||
|
|
||||||
|
|
||||||
def log(msg: str) -> None:
|
|
||||||
print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
|
|
||||||
|
|
||||||
|
|
||||||
def save_flow(sim, out_dir: Path, name: str, cylinders, nx: int = NX, ny: int = NY) -> None:
|
|
||||||
macro = sim.get_macroscopic()
|
|
||||||
np.savez_compressed(
|
|
||||||
out_dir / f"macro_{name}.npz",
|
|
||||||
rho=macro["rho"], ux=macro["ux"], uy=macro["uy"],
|
|
||||||
)
|
|
||||||
vort = compute_vorticity(macro["ux"], macro["uy"])
|
|
||||||
render_vorticity_field(
|
|
||||||
vort, nx=nx, ny=ny,
|
|
||||||
out_path=str(out_dir / f"vorticity_{name}.png"),
|
|
||||||
cylinders=cylinders,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def save_target_field(device_id: int, out_dir: Path) -> None:
|
|
||||||
"""Disturbance-only reference flow (no pinball)."""
|
|
||||||
from env_karman_2000x600 import _clean_cache
|
|
||||||
from CelerisLab import Simulation
|
|
||||||
|
|
||||||
log("Recording target reference flow field...")
|
|
||||||
_clean_cache()
|
|
||||||
sim = Simulation(lbm_config_path=CFG_PATH, device_id=device_id)
|
|
||||||
sim._assert_object_count_contract = lambda *a, **kw: None
|
|
||||||
sim.add_body("circle", center=(DIST_X, CENTER_Y, 0.0), radius=1.0 * L0)
|
|
||||||
sim.add_body("sensor", center=(SENSOR_X, CENTER_Y + 40.0, 0.0), radius=5.0)
|
|
||||||
sim.add_body("sensor", center=(SENSOR_X, CENTER_Y, 0.0), radius=5.0)
|
|
||||||
sim.add_body("sensor", center=(SENSOR_X, CENTER_Y - 40.0, 0.0), radius=5.0)
|
|
||||||
sim.initialize()
|
|
||||||
sim.run(WARMUP_STEPS + FIFO_LEN * SI, zero_obs=True)
|
|
||||||
save_flow(sim, out_dir, "target", CYLINDERS_DIST)
|
|
||||||
sim.close()
|
|
||||||
|
|
||||||
|
|
||||||
def run_manual_rollout(
|
|
||||||
env: KarmanCloakEnv,
|
|
||||||
n_steps: int,
|
|
||||||
*,
|
|
||||||
action: Optional[np.ndarray] = None,
|
|
||||||
fixed_omega: Optional[np.ndarray] = None,
|
|
||||||
action_provider: Optional[Callable[[np.ndarray], np.ndarray]] = None,
|
|
||||||
) -> Dict[str, np.ndarray]:
|
|
||||||
"""Run rollout bypassing gym step when fixed_omega is set."""
|
|
||||||
obs, _ = env.reset()
|
|
||||||
if fixed_omega is not None:
|
|
||||||
env.smoother.reset(np.asarray(fixed_omega, dtype=np.float32))
|
|
||||||
rewards, r_cd, r_cl, r_sim = [], [], [], []
|
|
||||||
cd_vals, cl_vals, sim_vals = [], [], []
|
|
||||||
actions = []
|
|
||||||
sensor_hist = []
|
|
||||||
force_hist = []
|
|
||||||
|
|
||||||
for _ in range(n_steps):
|
|
||||||
if fixed_omega is not None:
|
|
||||||
omega = np.asarray(fixed_omega, dtype=np.float32)
|
|
||||||
act = np.zeros(3, dtype=np.float32)
|
|
||||||
elif action_provider is not None:
|
|
||||||
act = np.asarray(action_provider(obs), dtype=np.float32).flatten()
|
|
||||||
omega = env.smoother(env._action_to_omega(act))
|
|
||||||
else:
|
|
||||||
act = np.zeros(3, dtype=np.float32) if action is None else np.asarray(action, dtype=np.float32)
|
|
||||||
omega = env.smoother(env._action_to_omega(act))
|
|
||||||
|
|
||||||
env._set_omega(omega)
|
|
||||||
env._gpu_block(lambda: env.sim.run(SI, zero_obs=True))
|
|
||||||
|
|
||||||
obs_raw = env._read_obs()
|
|
||||||
obs_slice = obs_raw[2:14]
|
|
||||||
obs = env._normalize_obs(obs_slice)
|
|
||||||
env.fifo_states.append(obs_slice[0:6] * SENSOR_CC)
|
|
||||||
reward, info = env._compute_reward(obs_slice)
|
|
||||||
|
|
||||||
rewards.append(reward)
|
|
||||||
r_cd.append(info["r_cd"])
|
|
||||||
r_cl.append(info["r_cl"])
|
|
||||||
r_sim.append(info["r_sim"])
|
|
||||||
cd_vals.append(info["cd"])
|
|
||||||
cl_vals.append(info["cl"])
|
|
||||||
sim_vals.append(info["sim"])
|
|
||||||
actions.append(act.copy())
|
|
||||||
sensor_hist.append(obs_slice[0:6] * SENSOR_CC)
|
|
||||||
force_hist.append(obs_slice[6:12].copy())
|
|
||||||
|
|
||||||
return {
|
|
||||||
"rewards": np.array(rewards, dtype=np.float64),
|
|
||||||
"r_cd": np.array(r_cd, dtype=np.float64),
|
|
||||||
"r_cl": np.array(r_cl, dtype=np.float64),
|
|
||||||
"r_sim": np.array(r_sim, dtype=np.float64),
|
|
||||||
"cd": np.array(cd_vals, dtype=np.float64),
|
|
||||||
"cl": np.array(cl_vals, dtype=np.float64),
|
|
||||||
"sim": np.array(sim_vals, dtype=np.float64),
|
|
||||||
"actions": np.array(actions, dtype=np.float32),
|
|
||||||
"sensors": np.array(sensor_hist, dtype=np.float32),
|
|
||||||
"forces": np.array(force_hist, dtype=np.float32),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def run_model_rollout(
|
|
||||||
env: KarmanCloakEnv,
|
|
||||||
model_path: Path,
|
|
||||||
norm_path: Path,
|
|
||||||
device_id: int,
|
|
||||||
n_steps: int,
|
|
||||||
deterministic: bool = True,
|
|
||||||
) -> Dict[str, np.ndarray]:
|
|
||||||
vec_env = DummyVecEnv([lambda: env])
|
|
||||||
vec_env = VecNormalize.load(str(norm_path), vec_env)
|
|
||||||
vec_env.training = False
|
|
||||||
vec_env.norm_reward = False
|
|
||||||
|
|
||||||
model = PPO.load(
|
|
||||||
str(model_path),
|
|
||||||
env=vec_env,
|
|
||||||
device=torch.device(f"cuda:{device_id}"),
|
|
||||||
custom_objects={"policy_kwargs": {"activation_fn": Sin, "net_arch": [64, 64]}},
|
|
||||||
)
|
|
||||||
|
|
||||||
obs = vec_env.reset()
|
|
||||||
rewards, r_cd, r_cl, r_sim = [], [], [], []
|
|
||||||
cd_vals, cl_vals, sim_vals = [], [], []
|
|
||||||
actions = []
|
|
||||||
sensor_hist = []
|
|
||||||
force_hist = []
|
|
||||||
|
|
||||||
for _ in range(n_steps):
|
|
||||||
action, _ = model.predict(obs, deterministic=deterministic)
|
|
||||||
obs, reward, done, info = vec_env.step(action)
|
|
||||||
inf = info[0] if isinstance(info, list) else info
|
|
||||||
rewards.append(float(reward[0]))
|
|
||||||
r_cd.append(float(inf.get("r_cd", 0.0)))
|
|
||||||
r_cl.append(float(inf.get("r_cl", 0.0)))
|
|
||||||
r_sim.append(float(inf.get("r_sim", 0.0)))
|
|
||||||
cd_vals.append(float(inf.get("cd", 0.0)))
|
|
||||||
cl_vals.append(float(inf.get("cl", 0.0)))
|
|
||||||
sim_vals.append(float(inf.get("sim", 0.0)))
|
|
||||||
actions.append(np.asarray(action[0], dtype=np.float32))
|
|
||||||
raw = env._read_obs()[2:14]
|
|
||||||
sensor_hist.append(raw[0:6] * SENSOR_CC)
|
|
||||||
force_hist.append(raw[6:12].copy())
|
|
||||||
|
|
||||||
return {
|
|
||||||
"rewards": np.array(rewards, dtype=np.float64),
|
|
||||||
"r_cd": np.array(r_cd, dtype=np.float64),
|
|
||||||
"r_cl": np.array(r_cl, dtype=np.float64),
|
|
||||||
"r_sim": np.array(r_sim, dtype=np.float64),
|
|
||||||
"cd": np.array(cd_vals, dtype=np.float64),
|
|
||||||
"cl": np.array(cl_vals, dtype=np.float64),
|
|
||||||
"sim": np.array(sim_vals, dtype=np.float64),
|
|
||||||
"actions": np.array(actions, dtype=np.float32),
|
|
||||||
"sensors": np.array(sensor_hist, dtype=np.float32),
|
|
||||||
"forces": np.array(force_hist, dtype=np.float32),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def summarize_rollout(name: str, data: Dict[str, np.ndarray], tail: int = 180) -> Dict[str, float]:
|
|
||||||
sl = slice(-tail, None) if len(data["rewards"]) >= tail else slice(None)
|
|
||||||
return {
|
|
||||||
"name": name,
|
|
||||||
"reward": float(np.mean(data["rewards"][sl])),
|
|
||||||
"r_cd": float(np.mean(data["r_cd"][sl])),
|
|
||||||
"r_cl": float(np.mean(data["r_cl"][sl])),
|
|
||||||
"r_sim": float(np.mean(data["r_sim"][sl])),
|
|
||||||
"cd_norm": float(np.mean(data["cd"][sl])),
|
|
||||||
"cl_norm": float(np.mean(data["cl"][sl])),
|
|
||||||
"sim_raw": float(np.mean(data["sim"][sl])),
|
|
||||||
"cd_force": float(np.mean((data["forces"][sl, 0] + data["forces"][sl, 2] + data["forces"][sl, 4]) / 3.0)),
|
|
||||||
"cl_force": float(np.mean((data["forces"][sl, 1] + data["forces"][sl, 3] + data["forces"][sl, 5]) / 3.0)),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def save_flow_from_env(env: KarmanCloakEnv, out_dir: Path, name: str) -> None:
|
|
||||||
env.sim.ctx._ctx.push()
|
|
||||||
try:
|
|
||||||
save_flow(env.sim, out_dir, name, CYLINDERS_FULL)
|
|
||||||
finally:
|
|
||||||
env.sim.ctx._ctx.pop()
|
|
||||||
|
|
||||||
|
|
||||||
def plot_vorticity_panel(out_dir: Path, names: List[str], titles: List[str]) -> None:
|
|
||||||
n = len(names)
|
|
||||||
fig, axes = plt.subplots(2, 3, figsize=(18, 7))
|
|
||||||
axes = axes.flatten()
|
|
||||||
for ax, name, title in zip(axes, names, titles):
|
|
||||||
img_path = out_dir / f"vorticity_{name}.png"
|
|
||||||
if img_path.exists():
|
|
||||||
ax.imshow(plt.imread(img_path))
|
|
||||||
ax.set_title(title, fontsize=11)
|
|
||||||
ax.axis("off")
|
|
||||||
for ax in axes[n:]:
|
|
||||||
ax.axis("off")
|
|
||||||
fig.suptitle("Karman Cloak — Vorticity Comparison (2000×600)", fontsize=14)
|
|
||||||
fig.tight_layout()
|
|
||||||
fig.savefig(out_dir / "vorticity_panel.png", dpi=150, bbox_inches="tight")
|
|
||||||
plt.close(fig)
|
|
||||||
|
|
||||||
|
|
||||||
def plot_metrics(summary: List[Dict], out_dir: Path) -> None:
|
|
||||||
names = [s["name"] for s in summary]
|
|
||||||
x = np.arange(len(names))
|
|
||||||
width = 0.2
|
|
||||||
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
|
|
||||||
|
|
||||||
for i, key in enumerate(["reward", "r_cd", "r_cl", "r_sim"]):
|
|
||||||
axes[0].bar(x + (i - 1.5) * width, [s[key] for s in summary], width, label=key)
|
|
||||||
axes[0].set_xticks(x)
|
|
||||||
axes[0].set_xticklabels(names, rotation=15)
|
|
||||||
axes[0].set_ylim(0, 1.05)
|
|
||||||
axes[0].set_ylabel("Reward component")
|
|
||||||
axes[0].set_title("Eval reward (last 180 steps mean)")
|
|
||||||
axes[0].legend()
|
|
||||||
axes[0].grid(axis="y", alpha=0.3)
|
|
||||||
|
|
||||||
axes[1].bar(x - width / 2, [s["cd_norm"] for s in summary], width, label="|Cd| norm")
|
|
||||||
axes[1].bar(x + width / 2, [s["cl_norm"] for s in summary], width, label="|Cl| norm")
|
|
||||||
axes[1].set_xticks(x)
|
|
||||||
axes[1].set_xticklabels(names, rotation=15)
|
|
||||||
axes[1].set_ylabel("Force norm")
|
|
||||||
axes[1].set_title("Hydrodynamic forces (normalized)")
|
|
||||||
axes[1].legend()
|
|
||||||
axes[1].grid(axis="y", alpha=0.3)
|
|
||||||
|
|
||||||
fig.tight_layout()
|
|
||||||
fig.savefig(out_dir / "metrics_comparison.png", dpi=150)
|
|
||||||
plt.close(fig)
|
|
||||||
|
|
||||||
|
|
||||||
def plot_sensor_signals(
|
|
||||||
target_states: np.ndarray,
|
|
||||||
rollouts: Dict[str, Dict[str, np.ndarray]],
|
|
||||||
out_dir: Path,
|
|
||||||
) -> None:
|
|
||||||
"""Plot center-sensor uy vs target reference."""
|
|
||||||
fig, axes = plt.subplots(2, 1, figsize=(14, 8), sharex=True)
|
|
||||||
|
|
||||||
t_ref = target_states[CONV_LEN:2 * CONV_LEN, 3] # center uy
|
|
||||||
axes[0].plot(t_ref, "k-", lw=2, label="target (disturbance only)")
|
|
||||||
colors = {"zero": "#d62728", "bias": "#ff7f0e", "bias_drl": "#2ca02c", "nobias_drl": "#1f77b4"}
|
|
||||||
for name, data in rollouts.items():
|
|
||||||
if name == "target":
|
|
||||||
continue
|
|
||||||
s = data["sensors"][-CONV_LEN:, 3]
|
|
||||||
axes[0].plot(s, color=colors.get(name, "gray"), alpha=0.85, label=name)
|
|
||||||
|
|
||||||
axes[0].set_ylabel("Center sensor uy (legacy-equiv)")
|
|
||||||
axes[0].set_title("Downstream velocity signal vs target")
|
|
||||||
axes[0].legend(loc="upper right")
|
|
||||||
axes[0].grid(alpha=0.3)
|
|
||||||
|
|
||||||
for name, data in rollouts.items():
|
|
||||||
if name in ("target", "zero"):
|
|
||||||
continue
|
|
||||||
axes[1].plot(data["actions"][:, 0], alpha=0.7, label=f"{name} a_front")
|
|
||||||
axes[1].plot(data["actions"][:, 1], alpha=0.7, ls="--", label=f"{name} a_top")
|
|
||||||
axes[1].plot(data["actions"][:, 2], alpha=0.7, ls=":", label=f"{name} a_bot")
|
|
||||||
axes[1].set_xlabel("Step")
|
|
||||||
axes[1].set_ylabel("Action [-1, 1]")
|
|
||||||
axes[1].set_title("Control actions")
|
|
||||||
axes[1].legend(ncol=2, fontsize=8)
|
|
||||||
axes[1].grid(alpha=0.3)
|
|
||||||
|
|
||||||
fig.tight_layout()
|
|
||||||
fig.savefig(out_dir / "sensor_action_timeseries.png", dpi=150)
|
|
||||||
plt.close(fig)
|
|
||||||
|
|
||||||
|
|
||||||
def plot_reward_traces(rollouts: Dict[str, Dict[str, np.ndarray]], out_dir: Path) -> None:
|
|
||||||
fig, ax = plt.subplots(figsize=(12, 4))
|
|
||||||
colors = {"zero": "#d62728", "bias": "#ff7f0e", "bias_drl": "#2ca02c", "nobias_drl": "#1f77b4"}
|
|
||||||
for name, data in rollouts.items():
|
|
||||||
if name == "target":
|
|
||||||
continue
|
|
||||||
r = data["rewards"]
|
|
||||||
w = min(30, len(r))
|
|
||||||
smooth = np.convolve(r, np.ones(w) / w, mode="same")
|
|
||||||
ax.plot(smooth, color=colors.get(name, "gray"), label=name, alpha=0.9)
|
|
||||||
ax.set_xlabel("Step")
|
|
||||||
ax.set_ylabel("Reward (smoothed)")
|
|
||||||
ax.set_title("Reward traces during eval rollout")
|
|
||||||
ax.legend()
|
|
||||||
ax.grid(alpha=0.3)
|
|
||||||
fig.tight_layout()
|
|
||||||
fig.savefig(out_dir / "reward_traces.png", dpi=150)
|
|
||||||
plt.close(fig)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--device-id", type=int, default=0)
|
|
||||||
parser.add_argument("--target", type=str, default=str(TRAIN_DIR / "target.npy"))
|
|
||||||
parser.add_argument("--n-steps", type=int, default=360)
|
|
||||||
parser.add_argument("--out", type=str, default=str(TRAIN_DIR / "output" / "flow_analysis_v4"))
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
out_dir = Path(args.out)
|
|
||||||
out_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
target_states = np.load(args.target)
|
|
||||||
n_steps = args.n_steps
|
|
||||||
|
|
||||||
log(f"Output -> {out_dir}")
|
|
||||||
log(f"Eval steps per case: {n_steps}")
|
|
||||||
|
|
||||||
# --- Target reference field ---
|
|
||||||
save_target_field(args.device_id, out_dir)
|
|
||||||
|
|
||||||
rollouts: Dict[str, Dict[str, np.ndarray]] = {}
|
|
||||||
summary: List[Dict] = []
|
|
||||||
|
|
||||||
# --- Bias env cases (zero, bias, bias_drl) ---
|
|
||||||
log("Creating Bias env...")
|
|
||||||
bias_env = KarmanCloakEnv(
|
|
||||||
device_id=args.device_id, seed=42, target_states=target_states,
|
|
||||||
)
|
|
||||||
|
|
||||||
dtw_norm_scale = bias_env._dtw_norm_scale
|
|
||||||
|
|
||||||
log("Case: zero rotation...")
|
|
||||||
rollouts["zero"] = run_manual_rollout(
|
|
||||||
bias_env, n_steps, fixed_omega=np.zeros(3, dtype=np.float32),
|
|
||||||
)
|
|
||||||
save_flow_from_env(bias_env, out_dir, "zero")
|
|
||||||
|
|
||||||
log("Case: bias (open-loop)...")
|
|
||||||
rollouts["bias"] = run_manual_rollout(
|
|
||||||
bias_env, n_steps, action=np.zeros(3, dtype=np.float32),
|
|
||||||
)
|
|
||||||
save_flow_from_env(bias_env, out_dir, "bias")
|
|
||||||
|
|
||||||
log("Case: Bias DRL best...")
|
|
||||||
rollouts["bias_drl"] = run_model_rollout(
|
|
||||||
bias_env,
|
|
||||||
BIAS_RUN / "models" / "best_model.zip",
|
|
||||||
BIAS_RUN / "vec_normalize.pkl",
|
|
||||||
args.device_id,
|
|
||||||
n_steps,
|
|
||||||
deterministic=True,
|
|
||||||
)
|
|
||||||
save_flow_from_env(bias_env, out_dir, "bias_drl")
|
|
||||||
|
|
||||||
bias_env.close()
|
|
||||||
|
|
||||||
# --- NoBias DRL ---
|
|
||||||
log("Creating NoBias env...")
|
|
||||||
nobias_env = KarmanCloakEnv(
|
|
||||||
device_id=args.device_id, seed=42, target_states=target_states,
|
|
||||||
action_bias=np.array([0.0, 0.0, 0.0], dtype=np.float32),
|
|
||||||
action_scale=12.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
log("Case: NoBias DRL best...")
|
|
||||||
rollouts["nobias_drl"] = run_model_rollout(
|
|
||||||
nobias_env,
|
|
||||||
NOBIAS_RUN / "models" / "best_model.zip",
|
|
||||||
NOBIAS_RUN / "vec_normalize.pkl",
|
|
||||||
args.device_id,
|
|
||||||
n_steps,
|
|
||||||
deterministic=True,
|
|
||||||
)
|
|
||||||
save_flow_from_env(nobias_env, out_dir, "nobias_drl")
|
|
||||||
nobias_env.close()
|
|
||||||
|
|
||||||
# --- Summaries ---
|
|
||||||
for name, data in rollouts.items():
|
|
||||||
s = summarize_rollout(name, data)
|
|
||||||
# DTW on full fifo at end
|
|
||||||
if len(data["sensors"]) >= CONV_LEN:
|
|
||||||
fifo = np.zeros((FIFO_LEN, 6), dtype=np.float32)
|
|
||||||
n = min(FIFO_LEN, len(data["sensors"]))
|
|
||||||
fifo[-n:] = data["sensors"][-n:]
|
|
||||||
s["sim_dtw_end"] = float(compute_similarity(
|
|
||||||
target_states, fifo, conv_len=CONV_LEN,
|
|
||||||
norm_scale=dtw_norm_scale,
|
|
||||||
))
|
|
||||||
summary.append(s)
|
|
||||||
log(f" {name:12s}: reward={s['reward']:.4f} r_cd={s['r_cd']:.3f} "
|
|
||||||
f"r_cl={s['r_cl']:.3f} r_sim={s['r_sim']:.3f} sim_raw={s['sim_raw']:.3f}")
|
|
||||||
|
|
||||||
with open(out_dir / "summary.json", "w") as f:
|
|
||||||
json.dump(summary, f, indent=2)
|
|
||||||
|
|
||||||
# --- Plots ---
|
|
||||||
plot_vorticity_panel(
|
|
||||||
out_dir,
|
|
||||||
["target", "zero", "bias", "bias_drl", "nobias_drl"],
|
|
||||||
["Target (no pinball)", "Zero rotation", "Bias open-loop",
|
|
||||||
"Bias DRL (best)", "NoBias DRL (best)"],
|
|
||||||
)
|
|
||||||
plot_metrics(summary, out_dir)
|
|
||||||
plot_sensor_signals(target_states, rollouts, out_dir)
|
|
||||||
plot_reward_traces(rollouts, out_dir)
|
|
||||||
|
|
||||||
np.savez_compressed(
|
|
||||||
out_dir / "rollout_data.npz",
|
|
||||||
**{f"{k}_{fld}": v for k, d in rollouts.items() for fld, v in d.items()},
|
|
||||||
target_states=target_states,
|
|
||||||
)
|
|
||||||
|
|
||||||
log(f"Done. Results in {out_dir}")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
Reference in New Issue
Block a user