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)")
|
||||
parser.add_argument("--target-diam", type=float, 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()
|
||||
|
||||
case = args.case
|
||||
@@ -419,9 +421,11 @@ def main() -> int:
|
||||
|
||||
# ---- Step 1: Record target (Karman: dist_cyl + sensors only) ----
|
||||
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._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)
|
||||
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)
|
||||
@@ -453,7 +457,7 @@ def main() -> int:
|
||||
log("Step 2: Creating training sim (all 7 objects)...")
|
||||
sim = Simulation(lbm_config_path=config_path, device_id=device_id)
|
||||
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 = [
|
||||
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),
|
||||
@@ -505,9 +509,28 @@ def main() -> int:
|
||||
log(f" Stage0 sim_mean = {s0_sim:.4f}")
|
||||
log(f" Stage1 sim_mean = {s1_sim:.4f}")
|
||||
|
||||
sim_bp = [0.0, s0_sim, s1_sim,
|
||||
s1_sim + (1.0 - s1_sim) * 0.4,
|
||||
s1_sim + (1.0 - s1_sim) * 0.7,
|
||||
# Build SIM_BP: always [0, worst_measured, better_measured, ..., 1.0]
|
||||
if s1_sim < s0_sim:
|
||||
worst_sim = s1_sim
|
||||
better_sim = s0_sim
|
||||
else:
|
||||
worst_sim = s0_sim
|
||||
better_sim = s1_sim
|
||||
|
||||
# When stage0 and stage1 are too close (|s1-s0| < 0.05), the measured
|
||||
# SIM_BP collapses to near-zero dynamic range. Fall back to a generic
|
||||
# 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]
|
||||
|
||||
@@ -540,6 +563,7 @@ def main() -> int:
|
||||
# ---- Step 6: Write calibration.json ----
|
||||
calibration = {
|
||||
"case": case,
|
||||
"dist_radius": args.dist_radius,
|
||||
"grid": {"nx": NX, "ny": NY},
|
||||
"config_path": config_path,
|
||||
"SI": si,
|
||||
@@ -551,8 +575,8 @@ def main() -> int:
|
||||
"dtw_norm_scale": float(dtw_norm_scale),
|
||||
"SIM_BP": [float(x) for x in sim_bp],
|
||||
"SIM_VAL": [float(x) for x in sim_val],
|
||||
"K_CD": K_CD_ILLUSION,
|
||||
"K_CL": K_CL_ILLUSION,
|
||||
"K_CD": K_CD,
|
||||
"K_CL": K_CL,
|
||||
"W_CD": W_CD,
|
||||
"W_CL": W_CL,
|
||||
"W_SIM": W_SIM,
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"case": "illusion_075L",
|
||||
"case": "ill_075L",
|
||||
"scene": "illusion",
|
||||
"target_diam": 0.75,
|
||||
"grid": {
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"case": "illusion_15L",
|
||||
"case": "ill_15L",
|
||||
"scene": "illusion",
|
||||
"target_diam": 1.5,
|
||||
"grid": {
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"case": "illusion_1L",
|
||||
"case": "ill_1L",
|
||||
"scene": "illusion",
|
||||
"target_diam": 1.0,
|
||||
"grid": {
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"case": "illusion_2L",
|
||||
"case": "ill_2L",
|
||||
"scene": "illusion",
|
||||
"target_diam": 2.0,
|
||||
"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": {
|
||||
"nx": 2000,
|
||||
"ny": 600
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"case": "re200",
|
||||
"case": "kar_re200",
|
||||
"grid": {
|
||||
"nx": 2000,
|
||||
"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": {
|
||||
"nx": 2000,
|
||||
"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": {
|
||||
"nx": 2000,
|
||||
"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)."""
|
||||
warmup = int(4.0 * NX / U0)
|
||||
sim = Simulation(lbm_config_path=config_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("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)
|
||||
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)
|
||||
@@ -236,7 +237,8 @@ class KarmanCloakEnv(gym.Env):
|
||||
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.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.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),
|
||||
|
||||
@@ -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)")
|
||||
parser.add_argument("--transfer-model", type=str, default=None,
|
||||
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()
|
||||
|
||||
# Load calibration
|
||||
@@ -78,6 +82,38 @@ def main() -> int:
|
||||
out_dir = Path(__file__).resolve().parent / "output" / run_name
|
||||
(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"
|
||||
def log(msg):
|
||||
line = f"[{time.strftime('%H:%M:%S')}] {msg}"
|
||||
@@ -96,6 +132,7 @@ def main() -> int:
|
||||
cal_copy["config_path"] = config_path
|
||||
if args.transfer_model:
|
||||
cal_copy["transfer_model"] = args.transfer_model
|
||||
if not resume_ep:
|
||||
with open(out_dir / "calibration.json", "w") as f:
|
||||
json.dump(cal_copy, f, indent=2)
|
||||
|
||||
@@ -108,7 +145,8 @@ def main() -> int:
|
||||
else:
|
||||
log(" Recording target...")
|
||||
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)
|
||||
log(f" Target recorded in {time.perf_counter()-t0:.0f}s")
|
||||
|
||||
@@ -120,6 +158,14 @@ def main() -> int:
|
||||
env = SymmetryAugmentWrapper(env, prob=args.symmetry_prob, seed=args.seed,
|
||||
rollout_len=args.n_steps)
|
||||
vec_env = DummyVecEnv([lambda: env])
|
||||
if resume_ep and ckpt_norm.exists():
|
||||
# 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")
|
||||
@@ -132,6 +178,11 @@ def main() -> int:
|
||||
model = PPO.load(args.transfer_model, env=vec_env, device=device,
|
||||
custom_objects={"activation_fn": Sin})
|
||||
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:
|
||||
model = PPO(
|
||||
"MlpPolicy",
|
||||
@@ -147,11 +198,16 @@ def main() -> int:
|
||||
)
|
||||
log(" Created from scratch.")
|
||||
|
||||
best_reward = -float("inf")
|
||||
best_reward = resume_best_reward if resume_ep else -float("inf")
|
||||
t_last = time.perf_counter()
|
||||
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)
|
||||
|
||||
# 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