diff --git a/.gitignore b/.gitignore index de194d3..25070ac 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,18 @@ ParaView/ src/drl_pinball/legacy_test/output/ src/drl_pinball/reproduce/output/ *.tar.gz +# DRL Train release boundary +# Historical/failed runs remain local; active best-policy bundles are allowlisted. +src/drl_pinball/train/archive/ +!src/drl_pinball/train/output/ +src/drl_pinball/train/output/* +!src/drl_pinball/train/output/*/ +src/drl_pinball/train/output/*/* +!src/drl_pinball/train/output/*/meta.json +!src/drl_pinball/train/output/*/calibration.json +!src/drl_pinball/train/output/*/target.npy +!src/drl_pinball/train/output/*/best_vecnormalize.pkl +!src/drl_pinball/train/output/*/models/ +src/drl_pinball/train/output/*/models/* +!src/drl_pinball/train/output/*/models/best_model.zip +!src/drl_pinball/train/calibrations/*/target.npy diff --git a/src/drl_pinball/train/CROSSRE_ANALYSIS.md b/src/drl_pinball/train/CROSSRE_ANALYSIS.md index a3d2697..1fd9049 100644 --- a/src/drl_pinball/train/CROSSRE_ANALYSIS.md +++ b/src/drl_pinball/train/CROSSRE_ANALYSIS.md @@ -1,4 +1,8 @@ -# Cross-Re Transfer Learning — 分析报告 +# Cross-Re Transfer Learning — 历史归档分析 + +> **历史归档 / ARCHIVED TRANSFER ANALYSIS** +> 本文记录 2026-07-07 的 `_tr` 迁移实验及当时诊断,保留其科学内容作为历史记录;它不描述当前 active scratch workflow,不是模型发布或当前验证声明。当前规范见 `README.md`、`TRAIN_PIPELINE.md` 与 `DESIGN_DECISIONS.md`。 +> 当前归档位置:迁移脚本在 `archive/scripts_deprecated/crossre_transfer.sh`;保留的 `_tr` 日志、校准副本和 metadata 在 `archive/transfer_instability/kar_re*_tr_seed*/`;旧校准在 `archive/calibrations_deprecated/`。active case ID 不带 `_sc`,`_tr` 仅用于这些历史归档。 > 日期: 2026-07-07 > 实验: 改变上游来流雷诺数 (Re60 / Re200 / Re400),从 re100 baseline 进行 transfer learning @@ -372,17 +376,17 @@ Case: d075 re100 re200 re60 re400 | 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) | +| Base transfer 启动 | `archive/scripts_deprecated/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) | +| re60 校准 | `archive/transfer_instability/kar_re60_tr_seed43/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 校准 | `archive/transfer_instability/kar_re200_tr_seed43/calibration.json`(SIM_BP gap=0.32, K_CD=12) | +| re400 校准 | `archive/transfer_instability/kar_re400_tr_seed43/calibration.json`(SIM_BP gap=0.17, K_CD=12) | +| re60 base 日志 | `archive/transfer_instability/kar_re60_tr_seed43/train.log` | +| re200 base 日志 | `archive/transfer_instability/kar_re200_tr_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 base 日志 | `archive/transfer_instability/kar_re400_tr_seed43/train.log`(Ep28 0.553→0.264 退化) | | re400 ext2 日志 | `output/transfer_re400ext2_seed43/train.log` | diff --git a/src/drl_pinball/train/DESIGN_DECISIONS.md b/src/drl_pinball/train/DESIGN_DECISIONS.md new file mode 100644 index 0000000..8b61d78 --- /dev/null +++ b/src/drl_pinball/train/DESIGN_DECISIONS.md @@ -0,0 +1,19 @@ +# Training design decisions and compatibility notes + +These are current contracts and historical caveats for `src/drl_pinball/train`. + +- **Shared train/eval environment is intentional.** Each outer iteration evaluates on the same wrapped CFD environment used for learning. Reset/rollout changes environment state and updates `VecNormalize` observation statistics while it remains in training mode. Evaluation does not update policy weights. +- **Outer “episode” means PPO chunk.** One logged episode is normally `learn(2048)` plus a 360-step evaluation. The Gym environments do not terminate; this is not a Gym episode. +- **Symmetry is default-off.** The wrapper remains available for explicit experiments. Historical runs configured with 0.5 were effectively unaugmented because each learn/eval reset restarted the rollout counter before mirrored training transitions could occur; do not label those models symmetry-augmented. +- **Force aggregation differs by scene.** Karman averages the three controlled-cylinder drag/lift values and targets zero. Illusion sums all three and matches one target cylinder. +- **Calibration stages are sorted by quality.** Stage0 is zero rotation and Stage1 is reference open loop. Their DTW scores are sorted into worst/better anchors; stage number does not imply quality. +- **Reward constants differ.** Karman uses `K_CD/K_CL=50/100`; Illusion uses `12/25` because summed three-cylinder forces are compared with one target-cylinder force. +- **`target_diam` is a historical misnomer.** The value is passed to CelerisLab as `radius = target_diam * L0`. With `L0=20`, ratios 0.75/1/1.5/2 mean radii 15/20/30/40 and physical diameters 30/40/60/80 lattice units. +- **Normalization has three layers.** CelerisLab first area/time-averages sensor observations, the env divides force/sensor channels by calibrated `FORCE_SCALE`/`SENS_SCALE`, and `VecNormalize` learns running observation statistics online. Schema-less legacy artifacts inserted `SENSOR_CC=78` before pre-scaling; active native-v2 artifacts do not. The six-channel `legacy-policy-v1` path remains only to load an existing legacy model with its matching normalizer. +- **`VecNormalize` is online, not calibration.** Phase 0 produces fixed physical/reward scales. SB3 running moments evolve through both learning and shared-env evaluation. +- **Best policy requires best normalizer.** `best_model.zip` is incomplete without `best_vecnormalize.pkl` saved at that same best-selection point. Freeze it for inference. +- **Resume is continuation, not exact replay.** It restores a policy checkpoint and matching normalizer, then constructs a new process/environment. It does not promise exact optimizer, RNG, CFD, wrapper, or trajectory state recovery. +- **Seeds distinguish runs, not deterministic replay.** GPU CFD, CUDA/PyTorch behavior, PPO sampling, and process state prevent that stronger guarantee. +- **Targets must be calibrated artifacts.** Keep `target.npy` (and Illusion harmonics) matched to case, config, geometry, SI, force convention, and `calibration.json`. +- **Result retention is historical.** Checked-in summaries/figures support prior writing. They are not model-release artifacts and do not establish full validation or retraining after code changes. +- **Release boundary.** The release provides code, configs, canonical targets, and compact retained best-policy bundles. Each policy is paired with its same-selection normalizer and provenance; intermediate/final checkpoints, logs, TensorBoard streams, and archived failed runs are excluded. diff --git a/src/drl_pinball/train/README.md b/src/drl_pinball/train/README.md index 22000f2..f29c915 100644 --- a/src/drl_pinball/train/README.md +++ b/src/drl_pinball/train/README.md @@ -1,221 +1,71 @@ -# DynamisLab — 训练目录 +# DRL pinball training quickstart -> `src/drl_pinball/train/` -> 更新: 2026-07-12 +This directory contains the source workflow for calibrating and training the modern 2000×600 CelerisLab PPO environments. The release reproduces the workflow and includes compact best-policy bundles for the retained active runs. Existing `results/` material remains historical support for prior writing and is not a claim that current code has been fully retrained or validated. ---- +## Setup -## 命名规则 - -``` -{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 -``` - ---- - -## 快速开始 - -### 基准训练 +From the repository root: ```bash -cd scripts -bash train_baseline.sh --gpu 0 --episodes 500 -# → output/kar_re100_sc_seed{41..45}/ +git submodule update --init --recursive +conda create -n pycuda_3_10 python=3.10 -y +conda activate pycuda_3_10 +pip install pycuda +pip install -e CelerisLab +pip install -e . ``` -### 变直径 scratch (推荐) +Run on Linux with an NVIDIA GPU. Commands below assume the repository's `pycuda_3_10` environment. + +## Calibrate the target + +Training requires a matched, calibrated artifact set: `calibration.json` and `target.npy`, plus `target_harmonics.json` for Illusion. A JSON alone is not a complete target artifact. Generate the target on the training server before training; do not substitute an uncalibrated target. For example: ```bash -cd scripts -bash vardist_scratch.sh --gpu 0 -bash vardist_scratch.sh --only d075 -# → output/kar_d075_sc_seed44/ etc. -``` +cd src/drl_pinball/train +conda run --no-capture-output -n pycuda_3_10 python -u calibrate.py \ + --case kar_re100 --device-id 0 --si 800 \ + --config ../../../configs/config_lbm_karman_2000x600.json \ + --out-dir calibrations/kar_re100 +``` Canonical active case IDs omit method suffixes: -### 变雷诺数 scratch +- Karman: `kar_re100`, `kar_re60`, `kar_re200`, `kar_re400`, `kar_d075`, `kar_d15`, `kar_d2` +- Illusion: `ill_075L`, `ill_1L`, `ill_15L`, `ill_2L` + +`_tr` identifies historical archived transfer runs only. Older `_sc` names may still appear inside retained artifacts and history; they are not canonical active IDs. + +## Train + +Launchers run cases serially on one GPU: ```bash -cd scripts -bash crossre_scratch.sh --gpu 0 -bash crossre_scratch.sh --only re200 -# → output/kar_re200_sc_seed43/ etc. +cd src/drl_pinball/train +bash scripts/train_baseline.sh --gpu 0 --seeds 41,42 --episodes 500 +bash scripts/crossre_scratch.sh --gpu 0 --only re200 --episodes 500 +bash scripts/vardist_scratch.sh --gpu 0 --only d15 --episodes 500 +bash scripts/train_illusion.sh --gpu 0 --only 1L --episodes 500 ``` -### Illusion +For an explicit single run, use the Python entry point and the matching config/calibration: ```bash -cd scripts -bash train_illusion.sh --gpu 0 -# → output/ill_2L_sc_seed43/ etc. +conda run --no-capture-output -n pycuda_3_10 python -u train_karman.py \ + --case-name kar_re100 --device-id 0 --seed 42 \ + --config ../../../configs/config_lbm_karman_2000x600.json \ + --calibration calibrations/kar_re100/calibration.json \ + --total-episodes 500 --symmetry-prob 0 ``` -### 中断恢复 +Illusion uses `train_illusion.py` and an `ill_*` calibration. Symmetry is intentionally default-off; see `DESIGN_DECISIONS.md`. + +Resume requires the matching policy and normalizer checkpoint: ```bash -cd scripts -bash resume.sh --case kar_re60_sc --seed 43 --resume 460 --episodes 500 +bash scripts/resume.sh --case kar_re200 --seed 43 --resume 460 --episodes 500 --gpu 0 ``` ---- +A training “episode” in these interfaces is one PPO learning chunk (normally 2048 environment steps) followed by a 360-step deterministic evaluation. It is not a Gym episode. -## 核心训练脚本 +Outputs are written below `output/{case}_seed{seed}/`. The repository retains only each active run’s compact inference bundle: `models/best_model.zip`, `best_vecnormalize.pkl`, `meta.json`, `calibration.json`, and a run-local `target.npy` when available. Canonical calibration targets provide the documented fallback for retained legacy bundles. Intermediate/final checkpoints, TensorBoard streams, and logs remain local. Resume is policy-plus-normalizer continuation, not exact process replay. -### `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 成功。** +See `TRAIN_PIPELINE.md` for the full data path, `SERVER_DEPLOY.md` for server commands, and `DESIGN_DECISIONS.md` for conventions and limitations. diff --git a/src/drl_pinball/train/SERVER_DEPLOY.md b/src/drl_pinball/train/SERVER_DEPLOY.md index fc0b110..165b895 100644 --- a/src/drl_pinball/train/SERVER_DEPLOY.md +++ b/src/drl_pinball/train/SERVER_DEPLOY.md @@ -1,189 +1,108 @@ -# Server Deployment Guide +# Server deployment -## Prerequisites +This guide uses only scripts and configs currently present in the repository. The release reproduces the training workflow and distributes compact best-policy bundles for retained active runs. -- Linux with NVIDIA GPU(s) and CUDA toolkit installed -- Python 3.10+ -- CelerisLab submodule - -## One-Time Setup +## Install ```bash -# 1. Clone with submodules -git clone --recurse-submodules +git clone --recurse-submodules DynamisLab cd DynamisLab - -# 2. Create conda environment -conda create -n pycuda_3_10 python=3.10 +conda create -n pycuda_3_10 python=3.10 -y conda activate pycuda_3_10 - -# 3. Install PyCUDA (match your CUDA version) pip install pycuda - -# 4. Install CelerisLab pip install -e CelerisLab - -# 5. Install DynamisLab pip install -e . - -# 6. Verify -python -c "from CelerisLab import Simulation; print('OK')" +python -c "from CelerisLab import Simulation; print('CelerisLab import OK')" ``` -## Calibration (one per case, before training) +Requirements: Linux, NVIDIA GPU/driver and a compatible CUDA toolkit. Run commands from `src/drl_pinball/train` unless stated otherwise. -Every case MUST be calibrated once. This produces calibration.json + target files. +## Canonical cases and configs -### Karman Cloak (Re100, SI=800) -```bash -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 -``` +Active IDs have no `_sc` suffix: -### Karman Cloak (Re200, SI=500) -```bash -conda run -n pycuda_3_10 python calibrate.py \ - --case re200 --device-id 0 --si 500 \ - --config ../../../configs/config_lbm_karman_2000x600_re200.json -``` +- `kar_re100`, `kar_d075`, `kar_d15`, `kar_d2`, and all `ill_*` use `../../../configs/config_lbm_karman_2000x600.json`. +- `kar_re60`, `kar_re200`, `kar_re400` use the matching `..._re60.json`, `..._re200.json`, `..._re400.json`. +- `_tr` appears only in archived transfer experiments. -### Karman Cloak (Re50, SI=1600 / Re400, SI=400) -```bash -conda run -n pycuda_3_10 python calibrate.py \ - --case re50 --device-id 0 --si 1600 \ - --config ../../../configs/config_lbm_karman_2000x600_re50.json +## Calibrate a new artifact set -conda run -n pycuda_3_10 python calibrate.py \ - --case re400 --device-id 0 --si 400 \ - --config ../../../configs/config_lbm_karman_2000x600_re400.json -``` - -### Illusion (1L target, SI=600) -```bash -conda run -n pycuda_3_10 python calibrate.py \ - --case illusion_1L --device-id 0 --si 600 --scene illusion \ - --config ../../../configs/config_lbm_karman_2000x600.json -``` - -## Training - -### CRITICAL: Sequential GPU Startup - -Each GPU needs ~7 minutes between starts because CelerisLab compiles CUDA kernels -during the first Simulation() constructor. If two start simultaneously, the kernel -cache gets corrupted → reward=0.000 forever. - -### Single-GPU Training (for testing) +Generate a matched calibrated target set on the training server before training. To create the Karman baseline set used by the active launcher: ```bash -# Karman Re100, 20 episodes -conda run -n pycuda_3_10 python -u train_karman.py \ - --case-name re100_test --device-id 0 --seed 42 \ - --config ../../../configs/config_lbm_karman_2000x600.json \ - --calibration calibrations/re100/calibration.json \ - --total-episodes 20 - -# Illusion 1L, 20 episodes -conda run -n pycuda_3_10 python -u train_illusion.py \ - --case-name illusion_test --device-id 0 --seed 42 \ - --config ../../../configs/config_lbm_karman_2000x600.json \ - --calibration calibrations/illusion_1L/calibration.json \ - --total-episodes 20 +conda run --no-capture-output -n pycuda_3_10 python -u calibrate.py \ + --case kar_re100 --device-id 0 --si 800 \ + --config ../../../configs/config_lbm_karman_2000x600.json \ + --out-dir calibrations/kar_re100 ``` -### Multi-GPU Training (6 GPUs, using launch script) +For Illusion, remember that the historical `--target-diam` option is passed as a radius ratio: `1.0` produces radius `1.0*L0=20` lattice units, hence physical diameter 40. ```bash -# Re100 Karman, 6 seeds -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 - -# Re200 Karman, transfer learning from Re100 -bash launch_multi.sh \ - --case-name re200_karman --seeds 42,43,44,45,46,47 \ - --gpus 0,1,2,3,4,5 --episodes 500 \ - --config ../../../configs/config_lbm_karman_2000x600_re200.json \ - --calibration calibrations/re200/calibration.json \ - --transfer output/re100_karman_seed42/models/best_model.zip +conda run --no-capture-output -n pycuda_3_10 python -u calibrate.py \ + --case ill_1L --scene illusion --target-diam 1.0 \ + --device-id 0 --si 1200 \ + --config ../../../configs/config_lbm_karman_2000x600.json \ + --out-dir calibrations/ill_1L ``` -### Manual Multi-GPU (tmux/screen) +Keep each generated `calibration.json` with its `target.npy` and, for Illusion, `target_harmonics.json`. Targets are calibrated artifacts, not interchangeable input samples. -If launch_multi.sh doesn't work, start manually with delays: +## Train + +Real launchers in `scripts/` run serially on one GPU: ```bash -# GPU 0 -nohup conda run -n pycuda_3_10 python -u train_karman.py \ - --case-name re100_karman --device-id 0 --seed 42 \ - --config ../../../configs/config_lbm_karman_2000x600.json \ - --calibration calibrations/re100/calibration.json \ - --total-episodes 500 > output/re100_karman_seed42/nohup.log 2>&1 & - -# WAIT 7 minutes (until "Env ready" appears in log) -sleep 420 - -# GPU 1 -nohup conda run -n pycuda_3_10 python -u train_karman.py \ - --case-name re100_karman --device-id 1 --seed 43 \ - --config ../../../configs/config_lbm_karman_2000x600.json \ - --calibration calibrations/re100/calibration.json \ - --total-episodes 500 > output/re100_karman_seed43/nohup.log 2>&1 & - -# Repeat for GPU 2,3,4,5 with seeds 44,45,46,47 +bash scripts/train_baseline.sh --gpu 0 --seeds 41,42,43 --episodes 500 +bash scripts/crossre_scratch.sh --gpu 1 --only re200 --episodes 500 +bash scripts/vardist_scratch.sh --gpu 2 --only d15 --episodes 500 +bash scripts/train_illusion.sh --gpu 3 --only 1L --episodes 500 ``` -## Monitoring +There is no active `launch_multi.sh`. For multiple GPUs, start one existing launcher per GPU and give each a disjoint case/seed selection. Stagger heavy CelerisLab initialization if the shared server or CUDA compilation cache requires it; no universal seven-minute delay is guaranteed by this repository. + +Explicit single-run example: ```bash -# Check training progress -tail -f output/re100_karman_seed42/train.log - -# TensorBoard -tensorboard --logdir output/re100_karman_seed42/tb --port 6006 --bind_all - -# Monitor GPU usage -watch -n 1 nvidia-smi +conda run --no-capture-output -n pycuda_3_10 python -u train_karman.py \ + --case-name kar_re100 --device-id 0 --seed 42 \ + --config ../../../configs/config_lbm_karman_2000x600.json \ + --calibration calibrations/kar_re100/calibration.json \ + --total-episodes 500 --symmetry-prob 0 ``` -## Output Structure - -``` -output/ -└── re100_karman_seed42/ - ├── models/ - │ ├── best_model.zip # Best reward model - │ ├── final_model.zip # Final iteration model - │ └── chkpt_ep*.zip # Checkpoints every 10 episodes - ├── tb/ # TensorBoard logs - ├── train.log # Training log - ├── calibration.json # Copy of calibration used - ├── vec_normalize.pkl # VecNormalize statistics - └── meta.json # Run metadata -``` - -## Stopping +Resume with both checkpoint members present: ```bash -# Stop all training -pkill -f train_karman -pkill -f train_illusion - -# Or by PID -ps aux | grep train_karman | grep -v grep | awk '{print $2}' | xargs kill +bash scripts/resume.sh --case kar_re200 --seed 43 --resume 460 --episodes 500 --gpu 0 ``` -## Troubleshooting +Resume continues policy and `VecNormalize`; it is not exact deterministic replay. Seeds distinguish runs but do not guarantee replay. -| Symptom | Cause | Fix | -|---------|-------|-----| -| reward=0.000 forever | Kernel compilation race (two inits at once) | Kill both, `rm -f ~/CelerisLab/src/CelerisLab/lbm/kernels/config/config_objects.h ~/CelerisLab/src/CelerisLab/lbm/kernels/kernel.ptx`, restart sequentially | -| reward=NaN | FORCE_SCALE too small | Check calibration.json, re-run calibrate.py | -| reward flat at Stage0 | Action not being applied | Check `_action_to_omega` sign, `set_body` call | -| CUDA OOM | PyTorch+PyCUDA memory conflict | Reduce `--batch-size 32` | -| ImportError: CelerisLab | Not installed | Run `pip install -e CelerisLab` in conda env | -| Conda env not found | Wrong environment | Use `conda env list` to verify `pycuda_3_10` exists | +## Monitor and retain + +```bash +tail -f output/kar_re100_seed42/train.log +tensorboard --logdir output/kar_re100_seed42/tb --port 6006 --bind_all +nvidia-smi +``` + +Expected run layout: + +```text +output/kar_re100_seed42/ +├── calibration.json +├── meta.json +├── train.log +├── tb/ +├── best_vecnormalize.pkl +├── final_vecnormalize.pkl +├── vec_normalize.pkl # legacy final-state alias +└── models/ + ├── best_model.zip + ├── final_model.zip + ├── epNNNN_model.zip + └── epNNNN_vecnormalize.pkl +``` + +Treat `models/best_model.zip` and root `best_vecnormalize.pkl` as one artifact. Only the allowlisted best-policy inference bundle is released from each active `output/` run. Intermediate/final checkpoints, logs, and TensorBoard streams remain local. Retained repository results are historical writing basis rather than evidence of complete post-fix retraining. diff --git a/src/drl_pinball/train/TRAIN_PIPELINE.md b/src/drl_pinball/train/TRAIN_PIPELINE.md new file mode 100644 index 0000000..b254241 --- /dev/null +++ b/src/drl_pinball/train/TRAIN_PIPELINE.md @@ -0,0 +1,132 @@ +# Current DRL training pipeline + +This document describes the executable modern training path in this directory. It is a workflow specification, not evidence that the current source has been fully retrained or validated after later fixes. The release includes compact best-policy bundles for retained active runs; retained result summaries remain historical writing inputs rather than post-fix retraining evidence. + +## 1. Scope and cases + +The active path uses CelerisLab on a 2000×600 D2Q9 grid, uniform regularized inlet, free-slip y walls, MRT collision, double buffering, zero action bias, Gymnasium environments, Stable-Baselines3 PPO, and observation-only `VecNormalize`. + +Canonical active IDs omit `_sc`: + +| Family | Cases | Config | SI | +|---|---|---|---:| +| Karman baseline | `kar_re100` | `configs/config_lbm_karman_2000x600.json` | 800 | +| Karman cross-Re | `kar_re60`, `kar_re200`, `kar_re400` | matching `_re60`, `_re200`, `_re400` JSON | 800, 500, 400 | +| Karman variable disturbance radius | `kar_d075`, `kar_d15`, `kar_d2` | base JSON | 800 | +| Illusion | `ill_075L`, `ill_1L`, `ill_15L`, `ill_2L` | base JSON | 1100, 1200, 1200, 1200 | + +Code-level Re uses reference length `2L0=40`, so code `re100` corresponds to physical cylinder-diameter `Re_D=50`. `_tr` is reserved for archived transfer experiments. `_sc` survives only in historical paths/artifacts and must not be introduced into active prose or new case IDs. + +## 2. End-to-end flow + +```text +calibrate.py + ├─ record calibrated target signal + ├─ warm training geometry and snapshot it + ├─ measure Stage0 (zero rotation) + ├─ measure Stage1 (reference open loop) + └─ write calibration.json + target artifacts + │ + ▼ +train_karman.py / train_illusion.py + ├─ load the matched calibration and target artifacts + ├─ build physical env → symmetry wrapper → DummyVecEnv → VecNormalize + ├─ repeat: PPO learn chunk → same-env deterministic evaluation → checkpoint + └─ write policy, paired normalizer, log, TensorBoard, metadata + │ + ▼ +../eval/ (separate inference/evaluation workflow) +``` + +Training and evaluation intentionally share one wrapped environment. This preserves CFD data continuity and avoids constructing a second GPU simulation, but evaluation is not side-effect-free: resetting/stepping changes environment state and, while `VecNormalize.training` remains true, updates observation running statistics. The policy weights do not update during evaluation. + +## 3. Phase 0: calibration + +`calibrate.py` performs the expensive target and baseline measurements. For Karman it records the wake of one upstream disturbance cylinder at three sensors. For Illusion it records a standalone target cylinder at three sensors plus target drag/lift, then stores a five-harmonic reconstruction of all eight channels. + +Artifacts are a contract: + +- `calibration.json`: geometry metadata, SI, scales, DTW mapping, reward constants, action mapping. +- `target.npy`: calibrated six-channel sensor target, shape `(150, 6)`. +- `target_harmonics.json`: Illusion only; calibrated sensor/force harmonic model. +- `calibrate.log`: provenance log when calibration is run locally. + +Target artifacts must come from the same calibrated case, config, geometry, SI, and force convention as `calibration.json`. Training fallback target recording is convenience behavior, not a substitute for producing and preserving a calibrated artifact set. + +Calibration warms the target simulation for `4*NX/U0 = 800000` lattice steps. It then warms the training geometry, snapshots it, and measures: + +1. **Stage0** — zero rotation. +2. **Stage1** — reference open-loop rotation (`[0, 0.004, -0.004]` Karman; `[0, 0.005, -0.005]` Illusion). + +Stage numbers describe acquisition order, not quality. The code sorts their mean DTW similarities into `worst_sim` and `better_sim` before constructing `SIM_BP`; Stage1 is not assumed better. Karman falls back to the generic `[0, .30, .65, .79, .89, 1]` mapping when the measured spread is below 0.10 or the better baseline is below 0.5. Illusion uses its scene-specific sorted mapping and `SIM_VAL=[0, .1, .35, .7, .85, 1]`. + +Reward force sensitivities are: + +- Karman: `K_CD=50`, `K_CL=100`. +- Illusion: `K_CD=12`, `K_CL=25`. + +## 4. Geometry, observations, and actions + +Karman contains disturbance cylinder + three sensors + three controlled cylinders. Its 12-vector is six controlled-cylinder forces followed by six sensor velocities. Drag and lift rewards use the **average** across the three controlled cylinders and target zero force. + +Illusion contains three sensors + three controlled cylinders; the target cylinder exists only during target acquisition. Its 14-vector appends reconstructed target drag and lift to the same 12 channels. Its force reward compares the **sum** of all three controlled-cylinder forces with the single target-cylinder force. This wider mismatch motivates the lower `12/25` Gaussian constants. + +The historical field name `target_diam` is misleading. The implementation passes `target_diam * L0` to CelerisLab as the circle **radius**. Therefore values `0.75, 1.0, 1.5, 2.0` are radius ratios relative to `L0=20`, giving physical lattice radii `15, 20, 30, 40` and diameters `30, 40, 60, 80`. Preserve this interpretation when reading or regenerating artifacts. + +Actions are three normalized rotations: + +```text +omega = -(action * ACTION_SCALE) * U0 / RADIUS +ACTION_SCALE=12, U0=0.01, RADIUS=10 +``` + +A 0.1 EMA smooths commanded omega. There is no legacy action bias. + +## 5. Three normalization layers + +The observation path has three distinct layers; they must not be collapsed conceptually: + +1. **Solver observation averaging** — CelerisLab `read_sensor(..., normalize=True)` returns a sensor-area and sampling-time average; force reads are sampling-time averages. +2. **Calibration pre-scaling** — force channels divide by `FORCE_SCALE`; sensor channels divide by `SENS_SCALE`. Illusion target-force channels divide by `FORCE_SCALE` before entering the 14-vector. Current `drl-pinball-calibration-v2` artifacts store native CelerisLab sensor units. +3. **Online `VecNormalize`** — running mean/variance whiten the complete observation (`norm_obs=True`, `norm_reward=False`, `clip_obs=10`, `gamma=0.99`). This is learned online during training; it is not the Phase 0 calibration. + +Legacy schema-less calibrations instead used `SENSOR_CC=78` to convert native sensor values into the old policy/DTW unit convention before pre-scaling. `normalization.py` keeps that compatibility path for an existing legacy policy plus its matching `VecNormalize`; the active v2 schema does not apply 78. + +Inference must load the model's matching `VecNormalize` and freeze it (`training=False`). A “best model” without the normalizer state saved at the same best-selection point is incomplete. + +## 6. PPO loop and shared evaluation + +Defaults: sinusoidal MLP `[64,64]`, `n_steps=2048`, `learn_timesteps=2048`, batch 64, 10 epochs, learning rate `3e-4`, PPO gamma `0.995`. + +One outer `total_episodes` iteration is: + +1. `model.learn(learn_timesteps, reset_num_timesteps=False)`. +2. Disable symmetry probability on the same wrapped env. +3. Reset and run up to 360 policy steps with `model.predict(..., deterministic=True)`. +4. Score mean reward over the last 180 steps. +5. If improved, save `best_model.zip` and root `best_vecnormalize.pkl` at the same point. +6. Save per-iteration model and normalizer checkpoints; finally save `final_model.zip`. + +The environments never terminate a Gym episode themselves. “Episode” in logs and CLI means this outer PPO chunk, not a Gym episode. + +Because evaluation uses the training `VecNormalize`, its observations contribute to running statistics. This is intentional and must be reproduced when continuing this workflow. + +## 7. Symmetry status + +`SymmetryAugmentWrapper` implements the up/down G transform. The supported training default is off (`--symmetry-prob 0`); enable it only as an explicit experiment. + +History matters: legacy runs were configured with probability 0.5, but repeated `model.learn(...)` and evaluation resets reset the wrapper counter at each chunk. The mirror coin flip occurred only at the chunk boundary, after the last transition, so those models were effectively trained without augmented transitions. They must not be described as symmetry-augmented evidence. + +## 8. Checkpoints, resume, and seeds + +Each iteration pairs `epNNNN_model.zip` with `epNNNN_vecnormalize.pkl`. Resume loads both, reconstructs a new CFD environment, parses historical best reward from `train.log`, and continues at `N+1`. + +Resume is policy-and-normalizer continuation. It is **not** exact state restoration: optimizer/RNG details, CFD process/context, environment trajectory, wrapper state, TensorBoard state, and wall-clock scheduling are not guaranteed to replay identically. Likewise, `--seed` differentiates stochastic runs; it is not a deterministic replay guarantee for GPU CFD + PyTorch + PPO. + +Transfer loading remains in the Python entry points for historical compatibility, but active launchers train from scratch. Historical transfer scripts, logs, calibrations, and metadata live below `archive/`; see the warnings in `CROSSRE_ANALYSIS.md` and `VARDIST_ANALYSIS.md`. + +## 9. Output contract and release status + +A run directory contains `calibration.json`, `train.log`, `tb/`, `meta.json`, `models/`, and normalizer files. `best_model.zip` must travel with `best_vecnormalize.pkl`; `final_model.zip` uses `final_vecnormalize.pkl`, and per-iteration policies use the matching `epNNNN_vecnormalize.pkl`. The legacy root `vec_normalize.pkl` remains a final-state compatibility alias. + +The repository publishes the source and canonical targets needed to reproduce the workflow, plus compact retained best-policy bundles (`best_model.zip` with the same-selection `best_vecnormalize.pkl` and provenance files). It does not publish intermediate/final checkpoints, full training logs, or TensorBoard streams. Any retained `results/` summaries and figures are historical writing basis only. Do not infer current post-fix validation, complete retraining, reproducibility across hardware, or release qualification from them. diff --git a/src/drl_pinball/train/VARDIST_ANALYSIS.md b/src/drl_pinball/train/VARDIST_ANALYSIS.md index cf2a645..001f2a9 100644 --- a/src/drl_pinball/train/VARDIST_ANALYSIS.md +++ b/src/drl_pinball/train/VARDIST_ANALYSIS.md @@ -1,4 +1,8 @@ -# Vardist Transfer Learning — 分析报告 +# Vardist Transfer Learning — 历史归档分析 + +> **历史归档 / ARCHIVED TRANSFER ANALYSIS** +> 本文记录 2026-07-07 的 `_tr` 迁移实验及当时诊断,保留其科学内容作为历史记录;它不描述当前 active scratch workflow,不是模型发布或当前验证声明。当前规范见 `README.md`、`TRAIN_PIPELINE.md` 与 `DESIGN_DECISIONS.md`。 +> 当前归档位置:迁移脚本在 `archive/scripts_deprecated/vardist_transfer.sh`;保留的 `_tr` 日志、校准副本和 metadata 在 `archive/transfer_instability/kar_d*_tr_seed*/`;旧校准在 `archive/calibrations_deprecated/kar_d*_tr/`。active case ID 不带 `_sc`,`_tr` 仅用于这些历史归档。 > 日期: 2026-07-07 > 实验: 改变上游扰动圆柱尺寸 (0.75L / 1.5L / 2.0L),从 re100 baseline 进行 transfer learning @@ -167,11 +171,11 @@ Ep 200: 0.246 | 训练脚本 | `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` | +| Transfer 启动脚本 | `archive/scripts_deprecated/vardist_transfer.sh` | +| d075 校准 | `archive/transfer_instability/kar_d075_tr_seed44/calibration.json` | +| d15 校准 | `archive/transfer_instability/kar_d15_tr_seed45/calibration.json` | +| d2 校准 | `archive/transfer_instability/kar_d2_tr_seed45/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` | +| d075 训练日志 | `archive/transfer_instability/kar_d075_tr_seed44/train.log` | +| d15 训练日志 | `archive/transfer_instability/kar_d15_tr_seed45/train.log` | +| d2 训练日志 | `archive/transfer_instability/kar_d2_tr_seed45/train.log` | diff --git a/src/drl_pinball/train/calibrate.py b/src/drl_pinball/train/calibrate.py index 95071f0..35480b9 100644 --- a/src/drl_pinball/train/calibrate.py +++ b/src/drl_pinball/train/calibrate.py @@ -2,7 +2,7 @@ """Phase 0 calibration: measure baselines and produce calibration.json for any case. Runs on new CelerisLab solver with 2000x600 config. Produces: - - target.npy (FIFO_LEN, 6) sensor signals (legacy-equiv) + - target.npy (FIFO_LEN, 6) sensor signals (CelerisLab native units) - calibration.json FORCE_SCALE, SENS_SCALE, dtw_norm_scale, SIM_BP, SIM_VAL Workflow: @@ -34,6 +34,7 @@ if str(_REPO) not in sys.path: sys.path.insert(0, str(_REPO)) from CelerisLab import Simulation +from normalization import CALIBRATION_SCHEMA_NATIVE_V2, SENSOR_UNITS_NATIVE # --------------------------------------------------------------------------- # Physics / geometry constants @@ -52,7 +53,6 @@ SENSOR_X = 1200.0 FIFO_LEN = 150 CONV_LEN = 30 -SENSOR_CC = 78.0 N_MEASURE = 100 @@ -160,7 +160,6 @@ def run_stage(sim, dist_id, sensor_ids, pinball_ids, target_states, gpu_block(sim, lambda: sim.run(si, zero_obs=True)) obs = read_obs(sim, dist_id, sensor_ids, pinball_ids) sl = obs[2:14].copy() - sl[0:6] *= SENSOR_CC fifo.append(sl) obs_slices, sims = [], [] @@ -168,7 +167,6 @@ def run_stage(sim, dist_id, sensor_ids, pinball_ids, target_states, gpu_block(sim, lambda: sim.run(si, zero_obs=True)) obs = read_obs(sim, dist_id, sensor_ids, pinball_ids) sl = obs[2:14].copy() - sl[0:6] *= SENSOR_CC fifo.append(sl) obs_slices.append(sl.copy()) sim_val = compute_similarity(target_states, np.array(list(fifo)), @@ -231,12 +229,12 @@ def _calibrate_illusion(case, config_path, device_id, si, out_dir, log, warmup, for i in range(FIFO_LEN): gpu_block(sim, lambda: sim.run(si, zero_obs=True)) target_states[i] = [ - sim.read_sensor(s0, normalize=True)[0] * SENSOR_CC, - sim.read_sensor(s0, normalize=True)[1] * SENSOR_CC, - sim.read_sensor(s1, normalize=True)[0] * SENSOR_CC, - sim.read_sensor(s1, normalize=True)[1] * SENSOR_CC, - sim.read_sensor(s2, normalize=True)[0] * SENSOR_CC, - sim.read_sensor(s2, normalize=True)[1] * SENSOR_CC, + sim.read_sensor(s0, normalize=True)[0], + sim.read_sensor(s0, normalize=True)[1], + sim.read_sensor(s1, normalize=True)[0], + sim.read_sensor(s1, normalize=True)[1], + sim.read_sensor(s2, normalize=True)[0], + sim.read_sensor(s2, normalize=True)[1], sim.read_force(0, normalize=True)[0], sim.read_force(0, normalize=True)[1], ] @@ -252,7 +250,7 @@ def _calibrate_illusion(case, config_path, device_id, si, out_dir, log, warmup, target_std = np.std(target_sensor, axis=0) uy_std_avg = float(np.mean([target_std[1], target_std[3], target_std[5]])) - dtw_norm_scale = max(uy_std_avg, 0.01) + dtw_norm_scale = max(uy_std_avg, np.finfo(np.float32).eps) log(f" Target recorded. DTW norm_scale: {dtw_norm_scale:.4f}") # ---- Step 2: Training sim (3 sensors + 3 pinball) ---- @@ -300,7 +298,7 @@ def _calibrate_illusion(case, config_path, device_id, si, out_dir, log, warmup, all_sens = np.concatenate([stage0["obs_slices"][:, 0:6], stage1["obs_slices"][:, 0:6]], axis=0) sens_scale = float(np.max(np.abs(all_sens))) - sens_scale = max(sens_scale, 0.01) + sens_scale = max(sens_scale, np.finfo(np.float32).eps) s0_sim = float(np.mean(stage0["sims"])) s1_sim = float(np.mean(stage1["sims"])) @@ -332,32 +330,33 @@ def _calibrate_illusion(case, config_path, device_id, si, out_dir, log, warmup, # Rounding force_scale = round(force_scale, 4) - sens_scale = round(sens_scale, 2) - dtw_norm_scale = round(dtw_norm_scale, 3) + sens_scale = round(sens_scale, 6) + dtw_norm_scale = round(dtw_norm_scale, 6) sim_bp = [round(x, 2) for x in sim_bp] for i in range(1, len(sim_bp)): if sim_bp[i] <= sim_bp[i - 1]: sim_bp[i] = sim_bp[i - 1] + 0.01 sim_bp[-1] = 1.0 - log(f" FORCE_SCALE = {force_scale:.4f}, SENS_SCALE = {sens_scale:.2f}") + log(f" FORCE_SCALE = {force_scale:.4f}, SENS_SCALE = {sens_scale:.6f}") log(f" Stage0 sim = {s0_sim:.4f}, Stage1 sim = {s1_sim:.4f}") log(f" SIM_BP = {[f'{x:.2f}' for x in sim_bp]}") # ---- Step 6: Write calibration.json ---- calibration = { + "schema_version": CALIBRATION_SCHEMA_NATIVE_V2, + "sensor_units": SENSOR_UNITS_NATIVE, "case": case, "scene": "illusion", "target_diam": target_diam, "grid": {"nx": NX, "ny": NY}, "config_path": config_path, "SI": si, "FIFO_LEN": FIFO_LEN, "CONV_LEN": CONV_LEN, - "SENSOR_CC": SENSOR_CC, "FORCE_SCALE": force_scale, "SENS_SCALE": sens_scale, "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, "K_CL": K_CL, + "K_CD": K_CD_ILLUSION, "K_CL": K_CL_ILLUSION, "W_CD": W_CD, "W_CL": W_CL, "W_SIM": W_SIM, "FLOOR_CD": FLOOR_CD, "FLOOR_CL": FLOOR_CL, "FLOOR_SIM": FLOOR_SIM, "FLOOR_PENALTY": FLOOR_PENALTY, @@ -438,18 +437,18 @@ def main() -> int: for i in range(FIFO_LEN): gpu_block(sim, lambda: sim.run(si, zero_obs=True)) target[i] = [ - sim.read_sensor(s0_t, normalize=True)[0] * SENSOR_CC, - sim.read_sensor(s0_t, normalize=True)[1] * SENSOR_CC, - sim.read_sensor(s1_t, normalize=True)[0] * SENSOR_CC, - sim.read_sensor(s1_t, normalize=True)[1] * SENSOR_CC, - sim.read_sensor(s2_t, normalize=True)[0] * SENSOR_CC, - sim.read_sensor(s2_t, normalize=True)[1] * SENSOR_CC, + sim.read_sensor(s0_t, normalize=True)[0], + sim.read_sensor(s0_t, normalize=True)[1], + sim.read_sensor(s1_t, normalize=True)[0], + sim.read_sensor(s1_t, normalize=True)[1], + sim.read_sensor(s2_t, normalize=True)[0], + sim.read_sensor(s2_t, normalize=True)[1], ] sim.close() np.save(str(out_dir / "target.npy"), target) target_std = np.std(target, axis=0) uy_std_avg = float(np.mean([target_std[1], target_std[3], target_std[5]])) - dtw_norm_scale = max(uy_std_avg, 0.01) + dtw_norm_scale = max(uy_std_avg, np.finfo(np.float32).eps) log(f" Target recorded. s1_uy std={target[:, 3].std():.4f}") log(f" DTW norm_scale: {dtw_norm_scale:.4f}") @@ -499,7 +498,7 @@ def main() -> int: all_sens = np.concatenate([stage0["obs_slices"][:, 0:6], stage1["obs_slices"][:, 0:6]], axis=0) sens_scale = float(np.max(np.abs(all_sens))) - sens_scale = max(sens_scale, 0.01) + sens_scale = max(sens_scale, np.finfo(np.float32).eps) log(f" FORCE_SCALE = {force_scale:.6f}") log(f" SENS_SCALE = {sens_scale:.4f}") @@ -545,8 +544,8 @@ def main() -> int: # Round measured values to reasonable precision so calibration files # are human-readable and robust to small measurement noise. force_scale = round(force_scale, 4) # 0.002429 -> 0.0024 - sens_scale = round(sens_scale, 2) # 0.7543 -> 0.75 - dtw_norm_scale = round(dtw_norm_scale, 3) # 0.2043 -> 0.204 + sens_scale = round(sens_scale, 6) # 0.7543 -> 0.75 + dtw_norm_scale = round(dtw_norm_scale, 6) # 0.2043 -> 0.204 sim_bp = [round(x, 2) for x in sim_bp] # 0.3166 -> 0.32 etc. # Ensure sim_bp monotonicity preserved after rounding for i in range(1, len(sim_bp)): @@ -556,12 +555,14 @@ def main() -> int: sim_bp[-1] = 1.0 log(f" FORCE_SCALE (rounded) = {force_scale:.4f}") - log(f" SENS_SCALE (rounded) = {sens_scale:.2f}") - log(f" dtw_norm_scale (rounded) = {dtw_norm_scale:.3f}") + log(f" SENS_SCALE (rounded) = {sens_scale:.6f}") + log(f" dtw_norm_scale (rounded) = {dtw_norm_scale:.6f}") log(f" SIM_BP (rounded) = {[f'{x:.2f}' for x in sim_bp]}") # ---- Step 6: Write calibration.json ---- calibration = { + "schema_version": CALIBRATION_SCHEMA_NATIVE_V2, + "sensor_units": SENSOR_UNITS_NATIVE, "case": case, "dist_radius": args.dist_radius, "grid": {"nx": NX, "ny": NY}, @@ -569,7 +570,6 @@ def main() -> int: "SI": si, "FIFO_LEN": FIFO_LEN, "CONV_LEN": CONV_LEN, - "SENSOR_CC": SENSOR_CC, "FORCE_SCALE": force_scale, "SENS_SCALE": sens_scale, "dtw_norm_scale": float(dtw_norm_scale), diff --git a/src/drl_pinball/train/calibrations/ill_075L/calibration.json b/src/drl_pinball/train/calibrations/ill_075L/calibration.json index b23598f..509be95 100644 --- a/src/drl_pinball/train/calibrations/ill_075L/calibration.json +++ b/src/drl_pinball/train/calibrations/ill_075L/calibration.json @@ -10,10 +10,9 @@ "SI": 1100, "FIFO_LEN": 150, "CONV_LEN": 30, - "SENSOR_CC": 78.0, "FORCE_SCALE": 0.0027, - "SENS_SCALE": 0.93, - "dtw_norm_scale": 0.186, + "SENS_SCALE": 0.011923076923076923, + "dtw_norm_scale": 0.0023846153846153848, "SIM_BP": [ 0.0, 0.23, @@ -30,8 +29,8 @@ 0.85, 1.0 ], - "K_CD": 50.0, - "K_CL": 100.0, + "K_CD": 12.0, + "K_CL": 25.0, "W_CD": 0.3, "W_CL": 0.3, "W_SIM": 0.4, @@ -47,5 +46,7 @@ ], "U0": 0.01, "RADIUS": 10.0, - "L0": 20.0 -} \ No newline at end of file + "L0": 20.0, + "schema_version": "drl-pinball-calibration-v2", + "sensor_units": "celeris-area-time-average" +} diff --git a/src/drl_pinball/train/calibrations/ill_075L/target.npy b/src/drl_pinball/train/calibrations/ill_075L/target.npy new file mode 100644 index 0000000..ff8e03d Binary files /dev/null and b/src/drl_pinball/train/calibrations/ill_075L/target.npy differ diff --git a/src/drl_pinball/train/calibrations/ill_075L/target_harmonics.json b/src/drl_pinball/train/calibrations/ill_075L/target_harmonics.json index e47ad0f..aceda5d 100644 --- a/src/drl_pinball/train/calibrations/ill_075L/target_harmonics.json +++ b/src/drl_pinball/train/calibrations/ill_075L/target_harmonics.json @@ -1,12 +1,12 @@ [ { - "dc": 0.7088785445690156, + "dc": 0.009088186382228492, "amps": [ - 0.11475430680633039, - 0.09430037920896242, - 0.03785335654012012, - 0.036681363239733915, - 0.03288602952216585 + 0.0014712091979745966, + 0.0012089794014262208, + 0.0004852994315191922, + 0.00047027385375095166, + 0.0004216160103361111 ], "freqs": [ 0.05333333333333334, @@ -16,21 +16,21 @@ 0.06666666666666667 ], "phases": [ - 2.6132855445583396, - -0.5789901787454609, - 0.8258268724793085, - 2.6668932105480825, - -0.6277135419212965 + 2.613285564653542, + -0.5789903549409586, + 0.8258264822665325, + 2.6668929594105673, + -0.6277137721461957 ] }, { - "dc": -0.018542804992078648, + "dc": -0.00023772825889685392, "amps": [ - 0.14149483059420287, - 0.11188103536632915, - 0.06452488062802345, - 0.04686946222498845, - 0.037418557603405504 + 0.0018140363060154814, + 0.0014343722327366626, + 0.0008272420402038255, + 0.00060089054400642, + 0.00047972508467335407 ], "freqs": [ 0.05333333333333334, @@ -40,21 +40,21 @@ 0.06666666666666667 ], "phases": [ - 0.516970687037909, - -2.566893168767107, - -1.2609177447586661, - 0.45668472348998385, - -2.5126762596162355 + 0.5169706675945374, + -2.5668931920978584, + -1.2609177711751982, + 0.4566847221498629, + -2.5126761488737994 ] }, { - "dc": 0.5725846501191457, + "dc": 0.007340828847681355, "amps": [ - 0.05764520035629576, - 0.005899803483735611, - 0.0054205372806156865, - 0.004581067312849431, - 0.002914845994913773 + 0.00073904103020892, + 7.56385062017386e-05, + 6.949406770020111e-05, + 5.8731632216018344e-05, + 3.736982044761247e-05 ], "freqs": [ 0.11333333333333334, @@ -72,13 +72,13 @@ ] }, { - "dc": 0.0112522135147204, + "dc": 0.00014425914889822403, "amps": [ - 0.25595997006863674, - 0.20892759048230508, - 0.08230396158472242, - 0.07230005719685514, - 0.05046403572810587 + 0.003281538078215719, + 0.0026785588523531027, + 0.0010551789938645613, + 0.0009269238105851669, + 0.0006469748182269073 ], "freqs": [ 0.05333333333333334, @@ -88,21 +88,21 @@ 0.04 ], "phases": [ - 0.737858091105846, - -2.3310043425633937, - 0.6601851975187616, - -2.263148437154622, - 0.5776398163807126 + 0.7378580903398775, + -2.331004343514337, + 0.6601851998070257, + -2.263148434435599, + 0.5776398129078066 ] }, { - "dc": 0.7183246092001597, + "dc": 0.00920928986154051, "amps": [ - 0.1142342759424687, - 0.09477386576602029, - 0.039999549760355566, - 0.0361173949295317, - 0.033309522129018866 + 0.0014645418427236946, + 0.0012150497277476015, + 0.0005128146895743391, + 0.0004630435034955362, + 0.0004270450024366168 ], "freqs": [ 0.05333333333333334, @@ -112,21 +112,21 @@ 0.06666666666666667 ], "phases": [ - -0.5356446835968586, - 2.572852381448959, - 1.1249071992889565, - -0.49462960371639564, - 2.54773901735503 + -0.5356447051921064, + 2.572852488600483, + 1.1249074874330494, + -0.494629857878323, + 2.5477396481423766 ] }, { - "dc": 0.03294325330294669, + "dc": 0.0004223494148916668, "amps": [ - 0.14125404035009642, - 0.11221966979698215, - 0.05738605376381633, - 0.04670339431628726, - 0.03788596596235472 + 0.0018109492567327406, + 0.001438713773975289, + 0.0007357185931237228, + 0.0005987614417660785, + 0.00048571744742215996 ], "freqs": [ 0.05333333333333334, @@ -136,21 +136,21 @@ 0.06666666666666667 ], "phases": [ - 0.5115801035482338, - -2.558745487651608, - 2.0032061483623718, - 0.44313339382107086, - -2.4836080305538353 + 0.5115801341045253, + -2.558745481973567, + 2.0032061539153947, + 0.44313339555376424, + -2.483608042896834 ] }, { - "dc": 0.0021232300360376636, + "dc": 0.002123213455391427, "amps": [ - 4.857447003344866e-06, - 4.692486838309072e-07, - 4.25574507402414e-07, - 2.214141534606793e-07, - 2.135457380991351e-07 + 4.881182406944959e-06, + 4.64207874535886e-07, + 4.2991218787600116e-07, + 2.1476436487057784e-07, + 2.0345852246869883e-07 ], "freqs": [ 0.11333333333333334, @@ -160,21 +160,21 @@ 0.1 ], "phases": [ - -1.2322896349743138, - 1.8869478333532295, - -1.192041653547773, - -1.1139837007307842, - 1.8083568237434107 + -1.2297210709636235, + 1.8387927036937368, + -1.1880140519319704, + -1.1166642329064165, + 1.891792473030483 ] }, { - "dc": -4.034008507005637e-06, + "dc": -4.03449388613808e-06, "amps": [ - 0.00022175983298265806, - 0.00019412298497325598, - 7.13773466399049e-05, - 6.573336547379603e-05, - 4.496282384180768e-05 + 0.00022175969504500717, + 0.00019412209599268325, + 7.137768872188961e-05, + 6.573323897561949e-05, + 4.496354600204898e-05 ], "freqs": [ 0.05333333333333334, @@ -184,11 +184,11 @@ 0.07333333333333333 ], "phases": [ - -1.7563139947857938, - 1.4434754734643556, - 1.494965900469088, - -1.8235811215946953, - 1.5413793492101098 + -1.756313893414596, + 1.4434737977480265, + 1.4949686639333424, + -1.823578135966167, + 1.5413904324220307 ] } -] \ No newline at end of file +] diff --git a/src/drl_pinball/train/calibrations/ill_15L/calibration.json b/src/drl_pinball/train/calibrations/ill_15L/calibration.json index c8cb15b..3a30d5d 100644 --- a/src/drl_pinball/train/calibrations/ill_15L/calibration.json +++ b/src/drl_pinball/train/calibrations/ill_15L/calibration.json @@ -10,10 +10,9 @@ "SI": 1200, "FIFO_LEN": 150, "CONV_LEN": 30, - "SENSOR_CC": 78.0, "FORCE_SCALE": 0.0027, - "SENS_SCALE": 0.93, - "dtw_norm_scale": 0.355, + "SENS_SCALE": 0.011923076923076923, + "dtw_norm_scale": 0.004551282051282051, "SIM_BP": [ 0.0, 0.21, @@ -30,8 +29,8 @@ 0.85, 1.0 ], - "K_CD": 50.0, - "K_CL": 100.0, + "K_CD": 12.0, + "K_CL": 25.0, "W_CD": 0.3, "W_CL": 0.3, "W_SIM": 0.4, @@ -47,5 +46,7 @@ ], "U0": 0.01, "RADIUS": 10.0, - "L0": 20.0 -} \ No newline at end of file + "L0": 20.0, + "schema_version": "drl-pinball-calibration-v2", + "sensor_units": "celeris-area-time-average" +} diff --git a/src/drl_pinball/train/calibrations/ill_15L/target.npy b/src/drl_pinball/train/calibrations/ill_15L/target.npy new file mode 100644 index 0000000..f71d704 Binary files /dev/null and b/src/drl_pinball/train/calibrations/ill_15L/target.npy differ diff --git a/src/drl_pinball/train/calibrations/ill_15L/target_harmonics.json b/src/drl_pinball/train/calibrations/ill_15L/target_harmonics.json index 5f8876f..f54e4d4 100644 --- a/src/drl_pinball/train/calibrations/ill_15L/target_harmonics.json +++ b/src/drl_pinball/train/calibrations/ill_15L/target_harmonics.json @@ -1,12 +1,12 @@ [ { - "dc": 0.6393717394272487, + "dc": 0.008197073444851443, "amps": [ - 0.26397499489221904, - 0.03693626193592989, - 0.03464043162884017, - 0.032094292621327965, - 0.02660998202074348 + 0.003384294725463689, + 0.0004735415677137682, + 0.000444108123074602, + 0.0004114650735564253, + 0.0003411534092189 ], "freqs": [ 0.04, @@ -16,21 +16,21 @@ 0.08 ], "phases": [ - -1.6564124362566746, - 0.47538888508181093, - 1.4273551493900074, - -1.603488924581116, - -2.92943366573134 + -1.6564123840424922, + 0.4753893835927066, + 1.4273561147886427, + -1.6034890004851452, + -2.9294339155988105 ] }, { - "dc": -0.051619164294873676, + "dc": -0.0006617840408132627, "amps": [ - 0.4172458605608075, - 0.09803703370682554, - 0.05739720972114841, - 0.050867319019127596, - 0.04493433644769158 + 0.005349305839112549, + 0.0012568852204159961, + 0.000735861702597037, + 0.0006521452634752281, + 0.0005760812144873168 ], "freqs": [ 0.04, @@ -40,21 +40,21 @@ 0.07333333333333333 ], "phases": [ - 2.6165737590157754, - -0.40945517817609217, - -0.4706927988361595, - 2.5775068579589413, - 2.6220842496000643 + 2.6165737492640235, + -0.4094552198542029, + -0.47069275860313814, + 2.5775070384067775, + 2.622084395211405 ] }, { - "dc": 0.552529529929161, + "dc": 0.007083711924715939, "amps": [ - 0.08403701703961244, - 0.027486096578928348, - 0.01744191426898051, - 0.011353947053282725, - 0.01109829213640094 + 0.0010773976518604574, + 0.0003523858577092583, + 0.00022361428193052584, + 0.00014556342756051445, + 0.0001422857950891452 ], "freqs": [ 0.08, @@ -64,21 +64,21 @@ 0.16 ], "phases": [ - -1.8227867545251144, - 1.2862003185097757, - -1.7972626219792263, - 1.246149938164889, - -0.21151202059464996 + -1.8227867656243208, + 1.2862003602555783, + -1.7972626414660602, + 1.2461500153317588, + -0.21151212576819925 ] }, { - "dc": 0.010842311040420705, + "dc": 0.00013900397687207142, "amps": [ - 0.5943308009504777, - 0.08877439867850001, - 0.08691429679466427, - 0.06563808069193275, - 0.05978492368560289 + 0.007619625642294911, + 0.0011381333231880646, + 0.0011142858604923952, + 0.0008415138469899309, + 0.000766473377462724 ], "freqs": [ 0.04, @@ -88,21 +88,21 @@ 0.11333333333333334 ], "phases": [ - 2.703756522909848, - -0.36646754373961843, - -1.2543578596028404, - 2.633262384913735, - 1.9585690166152028 + 2.7037565236172956, + -0.366467535468687, + -1.2543578473672652, + 2.633262379823474, + 1.9585690000549598 ] }, { - "dc": 0.6386356908082962, + "dc": 0.008187637163533105, "amps": [ - 0.26431188860383287, - 0.03673130028679621, - 0.034307996033570334, - 0.032458112823718396, - 0.025233785999350247 + 0.003388614153647068, + 0.0004709143072201187, + 0.00043984600852931016, + 0.00041612935020591836, + 0.00032351014847826316 ], "freqs": [ 0.04, @@ -112,21 +112,21 @@ 0.11333333333333334 ], "phases": [ - 1.4719480406268972, - -2.7012019612996783, - -1.6212990075981681, - 1.4163428238670492, - 0.7391902924102435 + 1.4719481032982067, + -2.701201752954105, + -1.6212988121851675, + 1.4163424133157365, + 0.7391904060167546 ] }, { - "dc": 0.06721407459272692, + "dc": 0.000861718871503368, "amps": [ - 0.4063272578239082, - 0.11205511912567469, - 0.06737336811041239, - 0.03853575023411404, - 0.0356513806128155 + 0.005209323808828321, + 0.0014366041070790069, + 0.000863761150834127, + 0.0004940481317028024, + 0.0004570687905458198 ], "freqs": [ 0.04, @@ -136,21 +136,21 @@ 0.02666666666666667 ], "phases": [ - 2.6071524073546244, - 2.638430170197671, - -0.42252259968403594, - 2.4619405777962924, - -0.326703708621943 + 2.607152407579822, + 2.638430150624339, + -0.422522740774566, + 2.4619403762108343, + -0.32670380566107676 ] }, { - "dc": 0.004316972050194939, + "dc": 0.004316989940901597, "amps": [ - 7.792624418406872e-05, - 2.649820426895165e-05, - 1.5679181128164903e-05, - 1.1454315139134813e-05, - 8.735853307059759e-06 + 7.795356046161481e-05, + 2.6496761508385513e-05, + 1.568389958595757e-05, + 1.144380116726034e-05, + 8.718164895308449e-06 ], "freqs": [ 0.08, @@ -160,21 +160,21 @@ 0.09333333333333334 ], "phases": [ - -0.4847035254693422, - 2.6784244787377918, - -0.5077154053619933, - 2.70688939048309, - -0.5282930751629346 + -0.48435056342716537, + 2.678749890666711, + -0.5079233374296729, + 2.706864645609396, + -0.5281751178643231 ] }, { - "dc": 3.652239890319227e-05, + "dc": 3.652287718068692e-05, "amps": [ - 0.0016787860570611811, - 0.0002649747945744754, - 0.0001734345822777016, - 0.00013614422915343285, - 9.88009892069796e-05 + 0.0016787853093318894, + 0.000264974341796682, + 0.00017343643445143914, + 0.00013614475076224097, + 9.879987270803766e-05 ], "freqs": [ 0.04, @@ -184,11 +184,11 @@ 0.02 ], "phases": [ - -3.082075177261505, - 0.0499441279892221, - -3.072969546929839, - 0.040161756095339736, - 0.030358149244656192 + -3.0820752123815973, + 0.0499456525053739, + -3.072978092744599, + 0.04017383012652507, + 0.030365220287297087 ] } -] \ No newline at end of file +] diff --git a/src/drl_pinball/train/calibrations/ill_1L/calibration.json b/src/drl_pinball/train/calibrations/ill_1L/calibration.json index f789041..f70f169 100644 --- a/src/drl_pinball/train/calibrations/ill_1L/calibration.json +++ b/src/drl_pinball/train/calibrations/ill_1L/calibration.json @@ -10,10 +10,9 @@ "SI": 1200, "FIFO_LEN": 150, "CONV_LEN": 30, - "SENSOR_CC": 78.0, "FORCE_SCALE": 0.0027, - "SENS_SCALE": 0.93, - "dtw_norm_scale": 0.252, + "SENS_SCALE": 0.011923076923076923, + "dtw_norm_scale": 0.0032307692307692306, "SIM_BP": [ 0.0, 0.21, @@ -30,8 +29,8 @@ 0.85, 1.0 ], - "K_CD": 50.0, - "K_CL": 100.0, + "K_CD": 12.0, + "K_CL": 25.0, "W_CD": 0.3, "W_CL": 0.3, "W_SIM": 0.4, @@ -47,5 +46,7 @@ ], "U0": 0.01, "RADIUS": 10.0, - "L0": 20.0 -} \ No newline at end of file + "L0": 20.0, + "schema_version": "drl-pinball-calibration-v2", + "sensor_units": "celeris-area-time-average" +} diff --git a/src/drl_pinball/train/calibrations/ill_1L/target.npy b/src/drl_pinball/train/calibrations/ill_1L/target.npy new file mode 100644 index 0000000..6beacef Binary files /dev/null and b/src/drl_pinball/train/calibrations/ill_1L/target.npy differ diff --git a/src/drl_pinball/train/calibrations/ill_1L/target_harmonics.json b/src/drl_pinball/train/calibrations/ill_1L/target_harmonics.json index 210023f..4db0723 100644 --- a/src/drl_pinball/train/calibrations/ill_1L/target_harmonics.json +++ b/src/drl_pinball/train/calibrations/ill_1L/target_harmonics.json @@ -1,12 +1,12 @@ [ { - "dc": 0.6871295102437337, + "dc": 0.008809352519675196, "amps": [ - 0.18319639782938002, - 0.06610945366616479, - 0.04311754473414352, - 0.03030542180621809, - 0.027272312380937323 + 0.0023486717451016467, + 0.0008475572997023936, + 0.0005527892157108422, + 0.00038853096776261196, + 0.00034964489187612246 ], "freqs": [ 0.05333333333333334, @@ -16,21 +16,21 @@ 0.15333333333333335 ], "phases": [ - -1.5859054738810505, - 1.5005459218330446, - -1.534723086539976, - -0.9755852396186818, - -2.400861875184656 + -1.585905345022505, + 1.5005460959271122, + -1.534723047518141, + -0.9755863118580124, + -2.400862304033253 ] }, { - "dc": -0.02839548134400199, + "dc": -0.0003640446137899581, "amps": [ - 0.25230287832767934, - 0.09539999608074776, - 0.07242769038107988, - 0.057915290763028225, - 0.04339958293014957 + 0.0032346521490483984, + 0.001223076837162871, + 0.000928560102758218, + 0.0007425037479272481, + 0.0005564049377962642 ], "freqs": [ 0.05333333333333334, @@ -40,21 +40,21 @@ 0.10666666666666667 ], "phases": [ - 2.6842705453730904, - -0.4329499897570072, - 2.9316995795051146, - 2.670843645654777, - -0.05044820463468416 + 2.6842705358614727, + -0.4329499704530815, + 2.9316995034379096, + 2.6708436544501115, + -0.05044802385514657 ] }, { - "dc": 0.5692154564460119, + "dc": 0.007297634102849879, "amps": [ - 0.05111823061431893, - 0.04128396624646554, - 0.015265422980567578, - 0.015090006639850726, - 0.009557711148572965 + 0.0006553619123178633, + 0.0005292816319405183, + 0.0001957105429863926, + 0.00019346160862069542, + 0.00012253468626713745 ], "freqs": [ 0.1, @@ -64,21 +64,21 @@ 0.12000000000000001 ], "phases": [ - 1.6693519932886194, - -1.4324779931410057, - -1.3940831649835272, - 1.6278315583167478, - -1.3566560767853084 + 1.669352077197208, + -1.4324779140862236, + -1.3940830731486458, + 1.6278311988042837, + -1.356655867596017 ] }, { - "dc": 0.013092265091836452, + "dc": 0.00016784953526579417, "amps": [ - 0.40124785195509555, - 0.16234186402338177, - 0.08524230800710621, - 0.08108585800028799, - 0.07109298715174087 + 0.005144203252605854, + 0.0020813059431478423, + 0.0010928500855496533, + 0.0010395622982109472, + 0.00091144856065585 ], "freqs": [ 0.05333333333333334, @@ -88,21 +88,21 @@ 0.04 ], "phases": [ - 2.807113574648348, - -0.29757223337333294, - 2.7719663781063204, - 2.1871266999200056, - -0.2589322655847287 + 2.807113579737809, + -0.29757222637005815, + 2.7719663779834143, + 2.1871266917002354, + -0.25893229515432825 ] }, { - "dc": 0.684990275700887, + "dc": 0.0087819265988138, "amps": [ - 0.18185260409359108, - 0.06713403479140795, - 0.04138782185879297, - 0.02695905744770475, - 0.026141859041267534 + 0.002331443960080372, + 0.0008606927859714298, + 0.0005306130993882779, + 0.0003456288683524138, + 0.00033515181152231707 ], "freqs": [ 0.05333333333333334, @@ -112,21 +112,21 @@ 0.04 ], "phases": [ - 1.5465990065588082, - -1.6185095223679558, - 1.5625278315612854, - 0.8537101949347456, - -1.649974102551634 + 1.546599113619548, + -1.61850976860708, + 1.5625281914270723, + 0.8537100384203797, + -1.6499744631577824 ] }, { - "dc": 0.04362114487215876, + "dc": 0.0005592454961922943, "amps": [ - 0.2437341265296108, - 0.10332818930987306, - 0.057340140519604656, - 0.0564289078852383, - 0.04846692686956963 + 0.0031247964556233307, + 0.0013247203231757793, + 0.0007351299724521773, + 0.0007234475293688269, + 0.0006213708431280353 ], "freqs": [ 0.05333333333333334, @@ -136,21 +136,21 @@ 0.060000000000000005 ], "phases": [ - 2.6708663593754087, - -0.40513239623713626, - -0.06426761319675102, - 2.9170325957817727, - 2.5947271126041254 + 2.6708663676491753, + -0.4051324007724031, + -0.06426744894600595, + 2.9170326355916663, + 2.5947269806545727 ] }, { - "dc": 0.0028073104269181687, + "dc": 0.0028073479825009903, "amps": [ - 1.3964627608664915e-05, - 1.0641956583797102e-05, - 4.3721419678955225e-06, - 3.7235257770112522e-06, - 2.6478054812257544e-06 + 1.4015937181383077e-05, + 1.0671813575533977e-05, + 4.409131521263914e-06, + 3.7382921829427647e-06, + 2.6881525102464034e-06 ], "freqs": [ 0.1, @@ -160,21 +160,21 @@ 0.08666666666666667 ], "phases": [ - -0.008346715563909252, - 3.134154764223535, - -0.007106260242131926, - 3.13161924702753, - -0.007459162304746806 + -0.004601597990179667, + 3.1357694057171637, + -0.004769742879130775, + 3.1361106883689014, + -0.0015047488047546214 ] }, { - "dc": -4.900522320288777e-06, + "dc": -4.900578702896989e-06, "amps": [ - 0.0006028109860900706, - 0.00022089300997016063, - 0.0001390934157273362, - 8.588200912189985e-05, - 8.16852145193509e-05 + 0.0006028104026365945, + 0.00022089374238093185, + 0.0001390907528064047, + 8.588062207428474e-05, + 8.168758713528594e-05 ], "freqs": [ 0.05333333333333334, @@ -184,11 +184,11 @@ 0.06666666666666667 ], "phases": [ - -1.182274934210081, - 1.9691619558487863, - -1.1855982163802612, - 1.9884734806658761, - -1.1842874340466223 + -1.182276270844418, + 1.9691535128302826, + -1.1856146746053084, + 1.9884708784318685, + -1.1842880436059668 ] } -] \ No newline at end of file +] diff --git a/src/drl_pinball/train/calibrations/ill_2L/calibration.json b/src/drl_pinball/train/calibrations/ill_2L/calibration.json index 1a0be5b..81b1be5 100644 --- a/src/drl_pinball/train/calibrations/ill_2L/calibration.json +++ b/src/drl_pinball/train/calibrations/ill_2L/calibration.json @@ -10,10 +10,9 @@ "SI": 1200, "FIFO_LEN": 150, "CONV_LEN": 30, - "SENSOR_CC": 78.0, "FORCE_SCALE": 0.0027, - "SENS_SCALE": 0.93, - "dtw_norm_scale": 0.433, + "SENS_SCALE": 0.011923076923076923, + "dtw_norm_scale": 0.005551282051282051, "SIM_BP": [ 0.0, 0.21, @@ -30,8 +29,8 @@ 0.85, 1.0 ], - "K_CD": 50.0, - "K_CL": 100.0, + "K_CD": 12.0, + "K_CL": 25.0, "W_CD": 0.3, "W_CL": 0.3, "W_SIM": 0.4, @@ -47,5 +46,7 @@ ], "U0": 0.01, "RADIUS": 10.0, - "L0": 20.0 -} \ No newline at end of file + "L0": 20.0, + "schema_version": "drl-pinball-calibration-v2", + "sensor_units": "celeris-area-time-average" +} diff --git a/src/drl_pinball/train/calibrations/ill_2L/target.npy b/src/drl_pinball/train/calibrations/ill_2L/target.npy new file mode 100644 index 0000000..f655bf8 Binary files /dev/null and b/src/drl_pinball/train/calibrations/ill_2L/target.npy differ diff --git a/src/drl_pinball/train/calibrations/ill_2L/target_harmonics.json b/src/drl_pinball/train/calibrations/ill_2L/target_harmonics.json index a51dd14..cecd28b 100644 --- a/src/drl_pinball/train/calibrations/ill_2L/target_harmonics.json +++ b/src/drl_pinball/train/calibrations/ill_2L/target_harmonics.json @@ -1,12 +1,12 @@ [ { - "dc": 0.5942543417215347, + "dc": 0.007618645325175717, "amps": [ - 0.3019850417664959, - 0.08160454953075313, - 0.058301710483107845, - 0.0544747476609962, - 0.039862698816389305 + 0.003871603015245751, + 0.0010462122889282762, + 0.0007474577474848674, + 0.0006983942182890194, + 0.0005110604085490717 ], "freqs": [ 0.03333333333333333, @@ -16,21 +16,21 @@ 0.060000000000000005 ], "phases": [ - 2.534821875775996, - -0.5429755507018492, - 2.4925302413193626, - -2.8396914013393335, - 2.3471326700331137 + 2.534821909359247, + -0.5429753720059705, + 2.4925302033304453, + -2.8396912754309813, + 2.3471329891866146 ] }, { - "dc": -0.11012142162770033, + "dc": -0.0014118131527151817, "amps": [ - 0.48068411406011297, - 0.1524480328909725, - 0.11692915263110391, - 0.0751168326269912, - 0.0747130122507944 + 0.006162616862476316, + 0.001954461998065287, + 0.0014990917631886387, + 0.0009630363501510042, + 0.000957859081783658 ], "freqs": [ 0.03333333333333333, @@ -40,21 +40,21 @@ 0.04 ], "phases": [ - 0.470296517741658, - -2.7377287843427305, - 1.2986260770158415, - -2.824709979499191, - 0.49814841794041553 + 0.47029650601052814, + -2.7377287391337943, + 1.298626107017752, + -2.824709901348531, + 0.4981483716433705 ] }, { - "dc": 0.5153132931391398, + "dc": 0.006606580681271022, "amps": [ - 0.07925114648536222, - 0.06416828398684, - 0.02486756552471102, - 0.02396105106046917, - 0.02357284381774593 + 0.0010160403395559259, + 0.0008226703075235898, + 0.00031881494262450027, + 0.0003071929623137073, + 0.0003022159463813581 ], "freqs": [ 0.06666666666666667, @@ -72,13 +72,13 @@ ] }, { - "dc": -0.027726365104317665, + "dc": -0.0003554662192861239, "amps": [ - 0.6643698174881782, - 0.19945593793429098, - 0.11468672779610138, - 0.11289183277422135, - 0.09541898908540014 + 0.008517561762668952, + 0.002557127409413987, + 0.0014703426640525817, + 0.0014473311894130942, + 0.0012233203728897453 ], "freqs": [ 0.03333333333333333, @@ -96,13 +96,13 @@ ] }, { - "dc": 0.5756912664572398, + "dc": 0.007380657148921592, "amps": [ - 0.2971849584753252, - 0.08546200288457928, - 0.056185965966854816, - 0.05199100131281855, - 0.04433469349174801 + 0.003810063451946285, + 0.0010956668000717703, + 0.0007203326546256061, + 0.0006665512859131556, + 0.0005683935774500977 ], "freqs": [ 0.03333333333333333, @@ -112,21 +112,21 @@ 0.06666666666666667 ], "phases": [ - -0.6051267213005925, - 2.5953707671296398, - 0.17997803765211065, - -0.6326158950659138, - -0.8305731267474725 + -0.6051267718289992, + 2.595370797578676, + 0.1799783276612615, + -0.6326160831777162, + -0.8305732564007975 ] }, { - "dc": 0.06891472408858439, + "dc": 0.0008835220666458973, "amps": [ - 0.4939877939312975, - 0.14182933287038246, - 0.09542418578567632, - 0.09238873840109178, - 0.08339300360694776 + 0.00633317691350893, + 0.0018183245845542044, + 0.0012233871402188069, + 0.0011844710656437464, + 0.0010691410504508059 ], "freqs": [ 0.03333333333333333, @@ -136,21 +136,21 @@ 0.06666666666666667 ], "phases": [ - 0.48480140913888486, - -2.7722480905064066, - 1.1975329965204313, - 0.618507835379864, - -1.6516116590440921 + 0.48480138985190574, + -2.7722480319964093, + 1.1975329047854553, + 0.6185078695760705, + -1.65161160244187 ] }, { - "dc": 0.006008172007277608, + "dc": 0.006008192943409085, "amps": [ - 0.00014509432721004472, - 0.0001134838631202557, - 4.463804029767137e-05, - 4.034021665887571e-05, - 2.676946217843137e-05 + 0.00014509050761529017, + 0.00011350527400744749, + 4.463450917123621e-05, + 4.032513685922175e-05, + 2.675954746131117e-05 ], "freqs": [ 0.06666666666666667, @@ -160,21 +160,21 @@ 0.08 ], "phases": [ - -0.7433797814500979, - 2.435150608779618, - -0.759862378816383, - 2.4800522883576814, - -0.776499679618213 + -0.7432301917689894, + 2.4349577971505947, + -0.7600834524079255, + 2.4798509912210784, + -0.7750185371428461 ] }, { - "dc": -0.0001301015308369339, + "dc": -0.00013009978716581826, "amps": [ - 0.0027993124476053143, - 0.0008745864809045856, - 0.0004589225344713892, - 0.0004310081571789182, - 0.0003159326235892787 + 0.002799315770214687, + 0.0008745901223606793, + 0.00045892253463317015, + 0.00043100868605214275, + 0.00031593153487008906 ], "freqs": [ 0.03333333333333333, @@ -184,11 +184,11 @@ 0.013333333333333334 ], "phases": [ - -0.06721702416991604, - 3.089276889965643, - -0.08070896525391963, - 3.102755835824026, - 3.1156324380089453 + -0.06721775751807832, + 3.0892715336597796, + -0.0807108437730493, + 3.1027491698456315, + 3.115628328525561 ] } -] \ No newline at end of file +] diff --git a/src/drl_pinball/train/calibrations/kar_d075/calibration.json b/src/drl_pinball/train/calibrations/kar_d075/calibration.json new file mode 100644 index 0000000..da1c8d3 --- /dev/null +++ b/src/drl_pinball/train/calibrations/kar_d075/calibration.json @@ -0,0 +1,51 @@ +{ + "case": "kar_d075", + "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, + "FORCE_SCALE": 0.0018, + "SENS_SCALE": 0.010769230769230769, + "dtw_norm_scale": 0.0018717948717948717, + "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, + "schema_version": "drl-pinball-calibration-v2", + "sensor_units": "celeris-area-time-average" +} diff --git a/src/drl_pinball/train/calibrations/kar_d15/calibration.json b/src/drl_pinball/train/calibrations/kar_d15/calibration.json new file mode 100644 index 0000000..065ca01 --- /dev/null +++ b/src/drl_pinball/train/calibrations/kar_d15/calibration.json @@ -0,0 +1,51 @@ +{ + "case": "kar_d15", + "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, + "FORCE_SCALE": 0.0036, + "SENS_SCALE": 0.009615384615384616, + "dtw_norm_scale": 0.003935897435897436, + "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, + "schema_version": "drl-pinball-calibration-v2", + "sensor_units": "celeris-area-time-average" +} diff --git a/src/drl_pinball/train/calibrations/kar_d2/calibration.json b/src/drl_pinball/train/calibrations/kar_d2/calibration.json new file mode 100644 index 0000000..d10a4d2 --- /dev/null +++ b/src/drl_pinball/train/calibrations/kar_d2/calibration.json @@ -0,0 +1,51 @@ +{ + "case": "kar_d2", + "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, + "FORCE_SCALE": 0.0046, + "SENS_SCALE": 0.009871794871794872, + "dtw_norm_scale": 0.0050384615384615385, + "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, + "schema_version": "drl-pinball-calibration-v2", + "sensor_units": "celeris-area-time-average" +} diff --git a/src/drl_pinball/train/calibrations/kar_re100/calibration.json b/src/drl_pinball/train/calibrations/kar_re100/calibration.json index 0653215..c293be9 100644 --- a/src/drl_pinball/train/calibrations/kar_re100/calibration.json +++ b/src/drl_pinball/train/calibrations/kar_re100/calibration.json @@ -8,10 +8,9 @@ "SI": 800, "FIFO_LEN": 150, "CONV_LEN": 30, - "SENSOR_CC": 78.0, "FORCE_SCALE": 0.0024, - "SENS_SCALE": 0.75, - "dtw_norm_scale": 0.204, + "SENS_SCALE": 0.009615384615384616, + "dtw_norm_scale": 0.0026153846153846153, "SIM_BP": [ 0.0, 0.32, @@ -45,5 +44,7 @@ ], "U0": 0.01, "RADIUS": 10.0, - "L0": 20.0 -} \ No newline at end of file + "L0": 20.0, + "schema_version": "drl-pinball-calibration-v2", + "sensor_units": "celeris-area-time-average" +} diff --git a/src/drl_pinball/train/calibrations/kar_re200/calibration.json b/src/drl_pinball/train/calibrations/kar_re200/calibration.json index 492b577..333e71c 100644 --- a/src/drl_pinball/train/calibrations/kar_re200/calibration.json +++ b/src/drl_pinball/train/calibrations/kar_re200/calibration.json @@ -4,20 +4,19 @@ "nx": 2000, "ny": 600 }, - "config_path": "/home/frank14f/DynamisLab/configs/config_lbm_karman_2000x600_re200.json", + "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, + "SENS_SCALE": 0.011538461538461539, + "dtw_norm_scale": 0.003448717948717949, "SIM_BP": [ 0.0, - 0.45, - 0.77, - 0.86, - 0.93, + 0.3, + 0.65, + 0.79, + 0.89, 1.0 ], "SIM_VAL": [ @@ -28,8 +27,8 @@ 0.9, 1.0 ], - "K_CD": 12.0, - "K_CL": 25.0, + "K_CD": 50.0, + "K_CL": 100.0, "W_CD": 0.3, "W_CL": 0.3, "W_SIM": 0.4, @@ -45,5 +44,7 @@ ], "U0": 0.01, "RADIUS": 10.0, - "L0": 20.0 -} \ No newline at end of file + "L0": 20.0, + "schema_version": "drl-pinball-calibration-v2", + "sensor_units": "celeris-area-time-average" +} diff --git a/src/drl_pinball/train/calibrations/kar_re400/calibration.json b/src/drl_pinball/train/calibrations/kar_re400/calibration.json index a28b43d..f9dd4ff 100644 --- a/src/drl_pinball/train/calibrations/kar_re400/calibration.json +++ b/src/drl_pinball/train/calibrations/kar_re400/calibration.json @@ -4,20 +4,19 @@ "nx": 2000, "ny": 600 }, - "config_path": "/home/frank14f/DynamisLab/configs/config_lbm_karman_2000x600_re400.json", + "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, + "SENS_SCALE": 0.012564102564102564, + "dtw_norm_scale": 0.0039743589743589745, "SIM_BP": [ 0.0, - 0.56, - 0.73, - 0.84, - 0.92, + 0.3, + 0.65, + 0.79, + 0.89, 1.0 ], "SIM_VAL": [ @@ -28,8 +27,8 @@ 0.9, 1.0 ], - "K_CD": 12.0, - "K_CL": 25.0, + "K_CD": 50.0, + "K_CL": 100.0, "W_CD": 0.3, "W_CL": 0.3, "W_SIM": 0.4, @@ -45,5 +44,7 @@ ], "U0": 0.01, "RADIUS": 10.0, - "L0": 20.0 -} \ No newline at end of file + "L0": 20.0, + "schema_version": "drl-pinball-calibration-v2", + "sensor_units": "celeris-area-time-average" +} diff --git a/src/drl_pinball/train/calibrations/kar_re60/calibration.json b/src/drl_pinball/train/calibrations/kar_re60/calibration.json index 0fd6d40..a446478 100644 --- a/src/drl_pinball/train/calibrations/kar_re60/calibration.json +++ b/src/drl_pinball/train/calibrations/kar_re60/calibration.json @@ -4,20 +4,19 @@ "nx": 2000, "ny": 600 }, - "config_path": "/home/frank14f/DynamisLab/configs/config_lbm_karman_2000x600_re60.json", + "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, + "SENS_SCALE": 0.00923076923076923, + "dtw_norm_scale": 0.0013717948717948717, "SIM_BP": [ 0.0, - 0.41, - 0.61, - 0.77, - 0.88, + 0.3, + 0.65, + 0.79, + 0.89, 1.0 ], "SIM_VAL": [ @@ -28,8 +27,8 @@ 0.9, 1.0 ], - "K_CD": 12.0, - "K_CL": 25.0, + "K_CD": 50.0, + "K_CL": 100.0, "W_CD": 0.3, "W_CL": 0.3, "W_SIM": 0.4, @@ -45,5 +44,7 @@ ], "U0": 0.01, "RADIUS": 10.0, - "L0": 20.0 -} \ No newline at end of file + "L0": 20.0, + "schema_version": "drl-pinball-calibration-v2", + "sensor_units": "celeris-area-time-average" +} diff --git a/src/drl_pinball/train/case_registry.py b/src/drl_pinball/train/case_registry.py new file mode 100644 index 0000000..71338e7 --- /dev/null +++ b/src/drl_pinball/train/case_registry.py @@ -0,0 +1,89 @@ +"""Canonical registry for active V5 scratch-training cases. + +Canonical case IDs do not encode the training method. Historical transfer +(``_tr``) names are archive-only and are intentionally absent here. +""" +from __future__ import annotations + +import argparse +from dataclasses import dataclass +from pathlib import Path +from typing import Mapping + +REPO_ROOT = Path(__file__).resolve().parents[2] +TRAIN_DIR = REPO_ROOT / "src" / "drl_pinball" / "train" +CONFIG_DIR = REPO_ROOT / "configs" + + +@dataclass(frozen=True) +class CaseSpec: + case_id: str + config: str + calibration: str + trainer: str + si: int + seeds: tuple[int, ...] + scene_type: str + target_diam: float | None = None + + @property + def config_path(self) -> Path: + return CONFIG_DIR / self.config + + @property + def calibration_path(self) -> Path: + return TRAIN_DIR / "calibrations" / self.calibration / "calibration.json" + + @property + def trainer_path(self) -> Path: + return TRAIN_DIR / self.trainer + + def model_dir(self, seed: int) -> Path: + return TRAIN_DIR / "output" / f"{self.case_id}_seed{seed}" / "models" + + +_CASES = ( + CaseSpec("kar_re100", "config_lbm_karman_2000x600.json", "kar_re100", "train_karman.py", 800, (41, 42, 43, 44, 45), "karman"), + CaseSpec("kar_d075", "config_lbm_karman_2000x600.json", "kar_d075", "train_karman.py", 800, (44,), "karman"), + CaseSpec("kar_d15", "config_lbm_karman_2000x600.json", "kar_d15", "train_karman.py", 800, (45,), "karman"), + CaseSpec("kar_d2", "config_lbm_karman_2000x600.json", "kar_d2", "train_karman.py", 800, (45,), "karman"), + CaseSpec("ill_075L", "config_lbm_karman_2000x600.json", "ill_075L", "train_illusion.py", 1100, (43,), "illusion", 0.75), + CaseSpec("ill_1L", "config_lbm_karman_2000x600.json", "ill_1L", "train_illusion.py", 1200, (43,), "illusion", 1.0), + CaseSpec("ill_15L", "config_lbm_karman_2000x600.json", "ill_15L", "train_illusion.py", 1200, (43,), "illusion", 1.5), + CaseSpec("ill_2L", "config_lbm_karman_2000x600.json", "ill_2L", "train_illusion.py", 1200, (43,), "illusion", 2.0), + CaseSpec("kar_re60", "config_lbm_karman_2000x600_re60.json", "kar_re60", "train_karman.py", 800, (43,), "karman"), + CaseSpec("kar_re200", "config_lbm_karman_2000x600_re200.json", "kar_re200", "train_karman.py", 500, (43,), "karman"), + CaseSpec("kar_re400", "config_lbm_karman_2000x600_re400.json", "kar_re400", "train_karman.py", 400, (43,), "karman"), +) + +CASE_REGISTRY: Mapping[str, CaseSpec] = {case.case_id: case for case in _CASES} +CASE_IDS = tuple(CASE_REGISTRY) + + +def get_case(case_id: str) -> CaseSpec: + try: + return CASE_REGISTRY[case_id] + except KeyError as exc: + raise KeyError(f"Unknown canonical V5 case {case_id!r}; expected one of {CASE_IDS}") from exc + + +def main() -> int: + parser = argparse.ArgumentParser(description="Query the canonical V5 case registry") + parser.add_argument("case_id", choices=CASE_IDS) + parser.add_argument("field", choices=("config", "calibration", "trainer", "si", "seeds", "scene_type")) + args = parser.parse_args() + case = get_case(args.case_id) + values = { + "config": case.config_path, + "calibration": case.calibration_path, + "trainer": case.trainer_path, + "si": case.si, + "seeds": ",".join(map(str, case.seeds)), + "scene_type": case.scene_type, + } + print(values[args.field]) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/drl_pinball/train/env_illusion.py b/src/drl_pinball/train/env_illusion.py index c4d3e33..a670e49 100644 --- a/src/drl_pinball/train/env_illusion.py +++ b/src/drl_pinball/train/env_illusion.py @@ -45,6 +45,7 @@ if str(_REPO) not in sys.path: sys.path.insert(0, str(_REPO)) from CelerisLab import Simulation +from normalization import dtw_sensor_factor, policy_sensor_scale # --------------------------------------------------------------------------- L0 = 20.0; U0 = 0.01; RADIUS = L0 / 2.0 @@ -61,7 +62,6 @@ TARGET_CYL_RADIUS = 1.0 * L0 # 1L target diameter FIFO_LEN = 150; CONV_LEN = 30; MAX_STEPS = 500 EMA_FAST = 0.2 S_DIM = 14; A_DIM = 3 -SENSOR_CC = 78.0 ACTION_SCALE = 12.0 ACTION_BIAS = np.array([0.0, 0.0, 0.0], dtype=np.float32) @@ -154,7 +154,7 @@ def gen_target_states_at(t, harmonics): # --------------------------------------------------------------------------- -def record_illusion_target(config_path, device_id, si, target_diam=1.0): +def record_illusion_target(config_path, device_id, si, target_diam=1.0, sensor_factor=1.0): 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 @@ -170,12 +170,12 @@ def record_illusion_target(config_path, device_id, si, target_diam=1.0): for i in range(FIFO_LEN): sim.run(si, zero_obs=True) target_states[i] = [ - sim.read_sensor(s0, normalize=True)[0] * SENSOR_CC, - sim.read_sensor(s0, normalize=True)[1] * SENSOR_CC, - sim.read_sensor(s1, normalize=True)[0] * SENSOR_CC, - sim.read_sensor(s1, normalize=True)[1] * SENSOR_CC, - sim.read_sensor(s2, normalize=True)[0] * SENSOR_CC, - sim.read_sensor(s2, normalize=True)[1] * SENSOR_CC, + sim.read_sensor(s0, normalize=True)[0] * sensor_factor, + sim.read_sensor(s0, normalize=True)[1] * sensor_factor, + sim.read_sensor(s1, normalize=True)[0] * sensor_factor, + sim.read_sensor(s1, normalize=True)[1] * sensor_factor, + sim.read_sensor(s2, normalize=True)[0] * sensor_factor, + sim.read_sensor(s2, normalize=True)[1] * sensor_factor, sim.read_force(0, normalize=True)[0], sim.read_force(0, normalize=True)[1], ] @@ -190,7 +190,7 @@ class IllusionCloakEnv(gym.Env): def __init__(self, device_id=0, seed=42, calibration=None, config_path=None, target_states=None, target_harmonics=None, - target_diam=1.0): + target_diam=1.0, policy_sensor_units=None): super().__init__() self.device_id = device_id self.seed = seed @@ -205,7 +205,8 @@ class IllusionCloakEnv(gym.Env): self._target_diam = float(self._cal["target_diam"]) self._si = int(self._cal["SI"]) self._force_scale = np.float32(self._cal["FORCE_SCALE"]) - self._sens_scale = np.float32(self._cal["SENS_SCALE"]) + self._sens_scale = np.float32(policy_sensor_scale(self._cal, policy_sensor_units)) + self._dtw_sensor_factor = np.float32(dtw_sensor_factor(self._cal)) self._dtw_norm_scale = float(self._cal["dtw_norm_scale"]) self._sim_bp = np.array(self._cal["SIM_BP"], dtype=np.float64) self._sim_val = np.array(self._cal["SIM_VAL"], dtype=np.float64) @@ -254,7 +255,8 @@ class IllusionCloakEnv(gym.Env): if self._target_states is None or self._target_harmonics is None: print(" [illusion] Phase 1: Recording target + harmonics...", flush=True) self._target_states, self._target_harmonics = record_illusion_target( - self._config_path, self.device_id, self._si, self._target_diam) + self._config_path, self.device_id, self._si, self._target_diam, + sensor_factor=float(self._dtw_sensor_factor)) print(" [illusion] Target recorded.", flush=True) self.target_sensor = self._target_states[:, 0:6].copy() @@ -284,11 +286,23 @@ class IllusionCloakEnv(gym.Env): self._set_omega(zero_omega) self._gpu_block(lambda: self.sim.run(self._si, zero_obs=True)) obs = self._read_obs() - sl = obs[0:6] * SENSOR_CC + sl = obs[0:6] * self._dtw_sensor_factor fifo_save.append(sl.copy()) self.save_states = np.array(fifo_save, dtype=np.float32) self._gpu_block(lambda: self.sim.snapshot()) + self._snapshot_fifo = self.save_states.copy() + self._snapshot_omega = zero_omega.copy() + self._snapshot_ema = (0.0, 0.0) + self._snapshot_step = 0 + snapshot_raw = self._read_obs() + snapshot_target = gen_target_states_at( + self._snapshot_step, self._target_harmonics) + self._snapshot_obs = self._make_obs( + snapshot_raw, + float(snapshot_target[6]) / self._force_scale, + float(snapshot_target[7]) / self._force_scale, + ).copy() print(f" [illusion] Init done ({time.perf_counter()-t0:.0f}s)") def _read_obs(self): @@ -361,18 +375,13 @@ class IllusionCloakEnv(gym.Env): def reset(self, seed=None, options=None): super().reset(seed=seed) self._gpu_block(lambda: self.sim.restore()) - self.smoother.reset(self._action_to_omega(np.zeros(3, dtype=np.float32))) + self.smoother.reset(self._snapshot_omega) self.fifo_states.clear() - for i in range(len(self.save_states)): - self.fifo_states.append(self.save_states[i, :]) - self.current_step = 0 - self._ema_r_cd = 0.0; self._ema_r_cl = 0.0 - obs_raw = self._read_obs() - tgt = gen_target_states_at(self.current_step, self._target_harmonics) - tgt_cd = float(tgt[6]) / self._force_scale - tgt_cl = float(tgt[7]) / self._force_scale - obs = self._make_obs(obs_raw, tgt_cd, tgt_cl) - return obs, {} + for row in self._snapshot_fifo: + self.fifo_states.append(row.copy()) + self.current_step = self._snapshot_step + self._ema_r_cd, self._ema_r_cl = self._snapshot_ema + return self._snapshot_obs.copy(), {} def step(self, action): assert self.action_space.contains(action), f"Invalid action: {action}" @@ -384,7 +393,7 @@ class IllusionCloakEnv(gym.Env): self.current_step += 1 obs_raw = self._read_obs() - self.fifo_states.append(obs_raw[0:6] * SENSOR_CC) + self.fifo_states.append(obs_raw[0:6] * self._dtw_sensor_factor) reward, info = self._compute_reward(obs_raw) tgt = gen_target_states_at(self.current_step, self._target_harmonics) diff --git a/src/drl_pinball/train/env_karman.py b/src/drl_pinball/train/env_karman.py index c94b4cf..8ec34f0 100644 --- a/src/drl_pinball/train/env_karman.py +++ b/src/drl_pinball/train/env_karman.py @@ -40,6 +40,7 @@ if str(_REPO) not in sys.path: sys.path.insert(0, str(_REPO)) from CelerisLab import Simulation +from normalization import dtw_sensor_factor, policy_sensor_scale # --------------------------------------------------------------------------- # Geometry constants (fixed across all Karman cloak cases) @@ -167,7 +168,8 @@ class KarmanCloakEnv(gym.Env): def __init__(self, device_id: int = 0, seed: int = 42, calibration: Optional[dict] = None, config_path: Optional[str] = None, - target_states: Optional[np.ndarray] = None): + target_states: Optional[np.ndarray] = None, + policy_sensor_units: Optional[str] = None): super().__init__() self.device_id = device_id self.seed = seed @@ -179,7 +181,9 @@ class KarmanCloakEnv(gym.Env): self._cal = calibration.copy() self._si = int(self._cal["SI"]) self._force_scale = np.float32(self._cal["FORCE_SCALE"]) - self._sens_scale = np.float32(self._cal["SENS_SCALE"]) + self._sens_scale = np.float32( + policy_sensor_scale(self._cal, policy_sensor_units)) + self._dtw_sensor_factor = np.float32(dtw_sensor_factor(self._cal)) self._dtw_norm_scale = float(self._cal["dtw_norm_scale"]) self._sim_bp = np.array(self._cal["SIM_BP"], dtype=np.float64) self._sim_val = np.array(self._cal["SIM_VAL"], dtype=np.float64) @@ -269,7 +273,7 @@ class KarmanCloakEnv(gym.Env): self._gpu_block(lambda: self.sim.run(self._si, zero_obs=True)) obs = self._read_obs() sl = obs[2:14].copy() - sl[0:6] *= SENSOR_CC + sl[0:6] *= self._dtw_sensor_factor fifo_save.append(sl) self.save_states = np.array(fifo_save, dtype=np.float32) print("done.") @@ -361,7 +365,7 @@ class KarmanCloakEnv(gym.Env): obs_raw = self._read_obs() obs_slice = obs_raw[2:14] obs = self._normalize_obs(obs_slice) - self.fifo_states.append(obs_slice[0:6] * SENSOR_CC) + self.fifo_states.append(obs_slice[0:6] * self._dtw_sensor_factor) reward, info = self._compute_reward(obs_slice) self.current_step += 1 diff --git a/src/drl_pinball/train/normalization.py b/src/drl_pinball/train/normalization.py new file mode 100644 index 0000000..0f0ab74 --- /dev/null +++ b/src/drl_pinball/train/normalization.py @@ -0,0 +1,83 @@ +"""Sensor-unit schema helpers for calibration, policy observations, and DTW.""" +from __future__ import annotations + +from typing import Any, Mapping + +import numpy as np + +CALIBRATION_SCHEMA_NATIVE_V2 = "drl-pinball-calibration-v2" +SENSOR_UNITS_NATIVE = "celeris-area-time-average" +POLICY_SENSOR_UNITS_LEGACY = "legacy-policy-v1" +LEGACY_SENSOR_FACTOR = 78.0 + + +def is_native_sensor_schema(calibration: Mapping[str, Any]) -> bool: + """Return whether sensor-valued fields use CelerisLab native units.""" + return ( + calibration.get("schema_version") == CALIBRATION_SCHEMA_NATIVE_V2 + and calibration.get("sensor_units") == SENSOR_UNITS_NATIVE + ) + + +def dtw_sensor_factor(calibration: Mapping[str, Any]) -> float: + """Scale a native solver sensor sample into the calibration's DTW units.""" + if is_native_sensor_schema(calibration): + return 1.0 + return float(calibration.get("SENSOR_CC", LEGACY_SENSOR_FACTOR)) + + +def policy_sensor_scale( + calibration: Mapping[str, Any], policy_sensor_units: str | None = None +) -> float: + """Return the divisor that produces the policy's historical raw observation. + + Native-v2 policies default to native sensor units. ``legacy-policy-v1`` is an + explicit compatibility mode for an existing model/VecNormalize pair. + Schema-less calibrations already store the legacy policy divisor and remain + unchanged. + """ + scale = float(calibration["SENS_SCALE"]) + if is_native_sensor_schema(calibration) and policy_sensor_units == POLICY_SENSOR_UNITS_LEGACY: + return scale * LEGACY_SENSOR_FACTOR + return scale + + +def convert_legacy_calibration(calibration: Mapping[str, Any]) -> dict[str, Any]: + """Convert sensor-valued legacy calibration fields to native-v2 units.""" + if is_native_sensor_schema(calibration): + return dict(calibration) + factor = float(calibration.get("SENSOR_CC", LEGACY_SENSOR_FACTOR)) + converted = dict(calibration) + converted["schema_version"] = CALIBRATION_SCHEMA_NATIVE_V2 + converted["sensor_units"] = SENSOR_UNITS_NATIVE + converted["SENS_SCALE"] = float(calibration["SENS_SCALE"]) / factor + converted["dtw_norm_scale"] = float(calibration["dtw_norm_scale"]) / factor + converted.pop("SENSOR_CC", None) + return converted + + +def convert_legacy_target(target: np.ndarray, factor: float = LEGACY_SENSOR_FACTOR) -> np.ndarray: + """Convert a sensor target array from legacy-equivalent to native units.""" + converted = np.asarray(target).copy() + if converted.ndim < 2 or converted.shape[-1] <= 6: + return converted / float(factor) + converted[..., :6] /= float(factor) + return converted + + +def convert_legacy_harmonics(document: Any, factor: float = LEGACY_SENSOR_FACTOR) -> Any: + """Convert the first six sensor channels in an illusion harmonics document.""" + wrapped = isinstance(document, dict) + harmonics = document["harmonics"] if wrapped else document + converted = [] + for index, source in enumerate(harmonics): + item = dict(source) + if index < 6: + item["dc"] = float(item["dc"]) / factor + item["amps"] = [float(value) / factor for value in item["amps"]] + converted.append(item) + if not wrapped: + return converted + result = dict(document) + result["harmonics"] = converted + return result diff --git a/src/drl_pinball/train/output/ill_075L_seed43/best_vecnormalize.pkl b/src/drl_pinball/train/output/ill_075L_seed43/best_vecnormalize.pkl new file mode 100644 index 0000000..789a89e Binary files /dev/null and b/src/drl_pinball/train/output/ill_075L_seed43/best_vecnormalize.pkl differ diff --git a/src/drl_pinball/train/output/ill_075L_seed43/calibration.json b/src/drl_pinball/train/output/ill_075L_seed43/calibration.json new file mode 100644 index 0000000..88e40a5 --- /dev/null +++ b/src/drl_pinball/train/output/ill_075L_seed43/calibration.json @@ -0,0 +1,51 @@ +{ + "case": "ill_075L", + "scene": "illusion", + "target_diam": 0.75, + "grid": { + "nx": 2000, + "ny": 600 + }, + "config_path": "/home/frank14f/DynamisLab/configs/config_lbm_karman_2000x600.json", + "SI": 1100, + "FIFO_LEN": 150, + "CONV_LEN": 30, + "SENSOR_CC": 78.0, + "FORCE_SCALE": 0.0027, + "SENS_SCALE": 0.93, + "dtw_norm_scale": 0.186, + "SIM_BP": [ + 0.0, + 0.23, + 0.51, + 0.71, + 0.85, + 1.0 + ], + "SIM_VAL": [ + 0.0, + 0.1, + 0.35, + 0.7, + 0.85, + 1.0 + ], + "K_CD": 12.0, + "K_CL": 25.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 +} \ No newline at end of file diff --git a/src/drl_pinball/train/output/ill_075L_seed43/meta.json b/src/drl_pinball/train/output/ill_075L_seed43/meta.json new file mode 100644 index 0000000..1db6889 --- /dev/null +++ b/src/drl_pinball/train/output/ill_075L_seed43/meta.json @@ -0,0 +1,13 @@ +{ + "case_name": "ill_075L", + "seed": 43, + "total_episodes": 500, + "best_reward": 0.8949789073732164, + "n_steps": 2048, + "batch_size": 64, + "n_epochs": 10, + "lr": 0.0003, + "config_path": "/home/frank14f/DynamisLab/configs/config_lbm_karman_2000x600.json", + "calibration_path": "calibrations/ill_075L/calibration.json", + "best_episode": 468 +} diff --git a/src/drl_pinball/train/output/ill_075L_seed43/models/best_model.zip b/src/drl_pinball/train/output/ill_075L_seed43/models/best_model.zip new file mode 100644 index 0000000..04c8881 Binary files /dev/null and b/src/drl_pinball/train/output/ill_075L_seed43/models/best_model.zip differ diff --git a/src/drl_pinball/train/output/ill_075L_seed43/target.npy b/src/drl_pinball/train/output/ill_075L_seed43/target.npy new file mode 120000 index 0000000..28401bc --- /dev/null +++ b/src/drl_pinball/train/output/ill_075L_seed43/target.npy @@ -0,0 +1 @@ +../../calibrations/ill_075L/target.npy \ No newline at end of file diff --git a/src/drl_pinball/train/output/ill_15L_seed43/best_vecnormalize.pkl b/src/drl_pinball/train/output/ill_15L_seed43/best_vecnormalize.pkl new file mode 100644 index 0000000..5cf3d67 Binary files /dev/null and b/src/drl_pinball/train/output/ill_15L_seed43/best_vecnormalize.pkl differ diff --git a/src/drl_pinball/train/output/ill_15L_seed43/calibration.json b/src/drl_pinball/train/output/ill_15L_seed43/calibration.json new file mode 100644 index 0000000..fa81c1a --- /dev/null +++ b/src/drl_pinball/train/output/ill_15L_seed43/calibration.json @@ -0,0 +1,51 @@ +{ + "case": "ill_15L", + "scene": "illusion", + "target_diam": 1.5, + "grid": { + "nx": 2000, + "ny": 600 + }, + "config_path": "/home/frank14f/DynamisLab/configs/config_lbm_karman_2000x600.json", + "SI": 1200, + "FIFO_LEN": 150, + "CONV_LEN": 30, + "SENSOR_CC": 78.0, + "FORCE_SCALE": 0.0027, + "SENS_SCALE": 0.93, + "dtw_norm_scale": 0.355, + "SIM_BP": [ + 0.0, + 0.21, + 0.81, + 0.89, + 0.94, + 1.0 + ], + "SIM_VAL": [ + 0.0, + 0.1, + 0.35, + 0.7, + 0.85, + 1.0 + ], + "K_CD": 12.0, + "K_CL": 25.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 +} \ No newline at end of file diff --git a/src/drl_pinball/train/output/ill_15L_seed43/meta.json b/src/drl_pinball/train/output/ill_15L_seed43/meta.json new file mode 100644 index 0000000..b196ab0 --- /dev/null +++ b/src/drl_pinball/train/output/ill_15L_seed43/meta.json @@ -0,0 +1,13 @@ +{ + "case_name": "ill_15L", + "seed": 43, + "total_episodes": 500, + "best_reward": 0.6258453220129013, + "n_steps": 2048, + "batch_size": 64, + "n_epochs": 10, + "lr": 0.0003, + "config_path": "/home/frank14f/DynamisLab/configs/config_lbm_karman_2000x600.json", + "calibration_path": "calibrations/ill_15L/calibration.json", + "best_episode": 349 +} diff --git a/src/drl_pinball/train/output/ill_15L_seed43/models/best_model.zip b/src/drl_pinball/train/output/ill_15L_seed43/models/best_model.zip new file mode 100644 index 0000000..e903f67 Binary files /dev/null and b/src/drl_pinball/train/output/ill_15L_seed43/models/best_model.zip differ diff --git a/src/drl_pinball/train/output/ill_15L_seed43/target.npy b/src/drl_pinball/train/output/ill_15L_seed43/target.npy new file mode 120000 index 0000000..f536a06 --- /dev/null +++ b/src/drl_pinball/train/output/ill_15L_seed43/target.npy @@ -0,0 +1 @@ +../../calibrations/ill_15L/target.npy \ No newline at end of file diff --git a/src/drl_pinball/train/output/ill_1L_seed43/best_vecnormalize.pkl b/src/drl_pinball/train/output/ill_1L_seed43/best_vecnormalize.pkl new file mode 100644 index 0000000..7bac72e Binary files /dev/null and b/src/drl_pinball/train/output/ill_1L_seed43/best_vecnormalize.pkl differ diff --git a/src/drl_pinball/train/calibrations/kar_d075_tr/calibration.json b/src/drl_pinball/train/output/ill_1L_seed43/calibration.json similarity index 67% rename from src/drl_pinball/train/calibrations/kar_d075_tr/calibration.json rename to src/drl_pinball/train/output/ill_1L_seed43/calibration.json index 72c55b4..92dc68b 100644 --- a/src/drl_pinball/train/calibrations/kar_d075_tr/calibration.json +++ b/src/drl_pinball/train/output/ill_1L_seed43/calibration.json @@ -1,36 +1,37 @@ { - "case": "kar_d075_tr", - "dist_radius": 0.75, + "case": "ill_1L", + "scene": "illusion", + "target_diam": 1.0, "grid": { "nx": 2000, "ny": 600 }, "config_path": "/home/frank14f/DynamisLab/configs/config_lbm_karman_2000x600.json", - "SI": 800, + "SI": 1200, "FIFO_LEN": 150, "CONV_LEN": 30, "SENSOR_CC": 78.0, - "FORCE_SCALE": 0.0018, - "SENS_SCALE": 0.84, - "dtw_norm_scale": 0.146, + "FORCE_SCALE": 0.0027, + "SENS_SCALE": 0.93, + "dtw_norm_scale": 0.252, "SIM_BP": [ 0.0, - 0.25, - 0.79, - 0.88, + 0.21, + 0.81, + 0.89, 0.94, 1.0 ], "SIM_VAL": [ 0.0, - 0.2, - 0.5, - 0.8, - 0.9, + 0.1, + 0.35, + 0.7, + 0.85, 1.0 ], - "K_CD": 50.0, - "K_CL": 100.0, + "K_CD": 12.0, + "K_CL": 25.0, "W_CD": 0.3, "W_CL": 0.3, "W_SIM": 0.4, diff --git a/src/drl_pinball/train/output/ill_1L_seed43/meta.json b/src/drl_pinball/train/output/ill_1L_seed43/meta.json new file mode 100644 index 0000000..2a377e5 --- /dev/null +++ b/src/drl_pinball/train/output/ill_1L_seed43/meta.json @@ -0,0 +1,13 @@ +{ + "case_name": "ill_1L", + "seed": 43, + "total_episodes": 500, + "best_reward": 0.7991066402859158, + "n_steps": 2048, + "batch_size": 64, + "n_epochs": 10, + "lr": 0.0003, + "config_path": "/home/frank14f/DynamisLab/configs/config_lbm_karman_2000x600.json", + "calibration_path": "calibrations/ill_1L/calibration.json", + "best_episode": 401 +} diff --git a/src/drl_pinball/train/output/ill_1L_seed43/models/best_model.zip b/src/drl_pinball/train/output/ill_1L_seed43/models/best_model.zip new file mode 100644 index 0000000..7fe6b12 Binary files /dev/null and b/src/drl_pinball/train/output/ill_1L_seed43/models/best_model.zip differ diff --git a/src/drl_pinball/train/output/ill_1L_seed43/target.npy b/src/drl_pinball/train/output/ill_1L_seed43/target.npy new file mode 120000 index 0000000..4a8259f --- /dev/null +++ b/src/drl_pinball/train/output/ill_1L_seed43/target.npy @@ -0,0 +1 @@ +../../calibrations/ill_1L/target.npy \ No newline at end of file diff --git a/src/drl_pinball/train/output/ill_2L_seed43/best_vecnormalize.pkl b/src/drl_pinball/train/output/ill_2L_seed43/best_vecnormalize.pkl new file mode 100644 index 0000000..bb86086 Binary files /dev/null and b/src/drl_pinball/train/output/ill_2L_seed43/best_vecnormalize.pkl differ diff --git a/src/drl_pinball/train/output/ill_2L_seed43/calibration.json b/src/drl_pinball/train/output/ill_2L_seed43/calibration.json new file mode 100644 index 0000000..1a481ea --- /dev/null +++ b/src/drl_pinball/train/output/ill_2L_seed43/calibration.json @@ -0,0 +1,51 @@ +{ + "case": "ill_2L", + "scene": "illusion", + "target_diam": 2.0, + "grid": { + "nx": 2000, + "ny": 600 + }, + "config_path": "/home/frank14f/DynamisLab/configs/config_lbm_karman_2000x600.json", + "SI": 1200, + "FIFO_LEN": 150, + "CONV_LEN": 30, + "SENSOR_CC": 78.0, + "FORCE_SCALE": 0.0027, + "SENS_SCALE": 0.93, + "dtw_norm_scale": 0.433, + "SIM_BP": [ + 0.0, + 0.21, + 0.7, + 0.82, + 0.91, + 1.0 + ], + "SIM_VAL": [ + 0.0, + 0.1, + 0.35, + 0.7, + 0.85, + 1.0 + ], + "K_CD": 12.0, + "K_CL": 25.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 +} \ No newline at end of file diff --git a/src/drl_pinball/train/output/ill_2L_seed43/meta.json b/src/drl_pinball/train/output/ill_2L_seed43/meta.json new file mode 100644 index 0000000..137a36c --- /dev/null +++ b/src/drl_pinball/train/output/ill_2L_seed43/meta.json @@ -0,0 +1,13 @@ +{ + "case_name": "ill_2L", + "seed": 43, + "total_episodes": 500, + "best_reward": 0.6285770369900597, + "n_steps": 2048, + "batch_size": 64, + "n_epochs": 10, + "lr": 0.0003, + "config_path": "/home/frank14f/DynamisLab/configs/config_lbm_karman_2000x600.json", + "calibration_path": "calibrations/ill_2L/calibration.json", + "best_episode": 234 +} diff --git a/src/drl_pinball/train/output/ill_2L_seed43/models/best_model.zip b/src/drl_pinball/train/output/ill_2L_seed43/models/best_model.zip new file mode 100644 index 0000000..bbac085 Binary files /dev/null and b/src/drl_pinball/train/output/ill_2L_seed43/models/best_model.zip differ diff --git a/src/drl_pinball/train/output/ill_2L_seed43/target.npy b/src/drl_pinball/train/output/ill_2L_seed43/target.npy new file mode 120000 index 0000000..28f0958 --- /dev/null +++ b/src/drl_pinball/train/output/ill_2L_seed43/target.npy @@ -0,0 +1 @@ +../../calibrations/ill_2L/target.npy \ No newline at end of file diff --git a/src/drl_pinball/train/output/kar_d075_seed44/best_vecnormalize.pkl b/src/drl_pinball/train/output/kar_d075_seed44/best_vecnormalize.pkl new file mode 100644 index 0000000..6947e13 Binary files /dev/null and b/src/drl_pinball/train/output/kar_d075_seed44/best_vecnormalize.pkl differ diff --git a/src/drl_pinball/train/calibrations/kar_d075_sc/calibration.json b/src/drl_pinball/train/output/kar_d075_seed44/calibration.json similarity index 96% rename from src/drl_pinball/train/calibrations/kar_d075_sc/calibration.json rename to src/drl_pinball/train/output/kar_d075_seed44/calibration.json index d0ef4a9..003056e 100644 --- a/src/drl_pinball/train/calibrations/kar_d075_sc/calibration.json +++ b/src/drl_pinball/train/output/kar_d075_seed44/calibration.json @@ -1,5 +1,5 @@ { - "case": "kar_d075_sc", + "case": "karman_d075_v2", "dist_radius": 0.75, "grid": { "nx": 2000, diff --git a/src/drl_pinball/train/output/kar_d075_seed44/meta.json b/src/drl_pinball/train/output/kar_d075_seed44/meta.json new file mode 100644 index 0000000..93acae2 --- /dev/null +++ b/src/drl_pinball/train/output/kar_d075_seed44/meta.json @@ -0,0 +1,14 @@ +{ + "case_name": "kar_d075", + "seed": 44, + "total_episodes": 500, + "best_reward": 0.9170221057203081, + "n_steps": 2048, + "batch_size": 64, + "n_epochs": 10, + "lr": 0.0001, + "config_path": "/root/private_data/DynamisLab/configs/config_lbm_karman_2000x600.json", + "calibration_path": "/root/private_data/DynamisLab/src/drl_pinball/train/calibrations/kar_d075/calibration.json", + "transfer_model": null, + "best_episode": 492 +} diff --git a/src/drl_pinball/train/output/kar_d075_seed44/models/best_model.zip b/src/drl_pinball/train/output/kar_d075_seed44/models/best_model.zip new file mode 100644 index 0000000..b0399ad Binary files /dev/null and b/src/drl_pinball/train/output/kar_d075_seed44/models/best_model.zip differ diff --git a/src/drl_pinball/train/output/kar_d075_seed44/target.npy b/src/drl_pinball/train/output/kar_d075_seed44/target.npy new file mode 100644 index 0000000..e60646d Binary files /dev/null and b/src/drl_pinball/train/output/kar_d075_seed44/target.npy differ diff --git a/src/drl_pinball/train/output/kar_d15_seed45/best_vecnormalize.pkl b/src/drl_pinball/train/output/kar_d15_seed45/best_vecnormalize.pkl new file mode 100644 index 0000000..8ac7df9 Binary files /dev/null and b/src/drl_pinball/train/output/kar_d15_seed45/best_vecnormalize.pkl differ diff --git a/src/drl_pinball/train/calibrations/kar_d15_sc/calibration.json b/src/drl_pinball/train/output/kar_d15_seed45/calibration.json similarity index 96% rename from src/drl_pinball/train/calibrations/kar_d15_sc/calibration.json rename to src/drl_pinball/train/output/kar_d15_seed45/calibration.json index 2e1574a..f7e43f1 100644 --- a/src/drl_pinball/train/calibrations/kar_d15_sc/calibration.json +++ b/src/drl_pinball/train/output/kar_d15_seed45/calibration.json @@ -1,5 +1,5 @@ { - "case": "kar_d15_sc", + "case": "karman_d15_v2", "dist_radius": 1.5, "grid": { "nx": 2000, diff --git a/src/drl_pinball/train/output/kar_d15_seed45/meta.json b/src/drl_pinball/train/output/kar_d15_seed45/meta.json new file mode 100644 index 0000000..76d6505 --- /dev/null +++ b/src/drl_pinball/train/output/kar_d15_seed45/meta.json @@ -0,0 +1,14 @@ +{ + "case_name": "kar_d15", + "seed": 45, + "total_episodes": 500, + "best_reward": 0.9565292182895873, + "n_steps": 2048, + "batch_size": 64, + "n_epochs": 10, + "lr": 0.0003, + "config_path": "/root/private_data/DynamisLab/configs/config_lbm_karman_2000x600.json", + "calibration_path": "/root/private_data/DynamisLab/src/drl_pinball/train/calibrations/kar_d15/calibration.json", + "transfer_model": null, + "best_episode": 492 +} diff --git a/src/drl_pinball/train/output/kar_d15_seed45/models/best_model.zip b/src/drl_pinball/train/output/kar_d15_seed45/models/best_model.zip new file mode 100644 index 0000000..540a200 Binary files /dev/null and b/src/drl_pinball/train/output/kar_d15_seed45/models/best_model.zip differ diff --git a/src/drl_pinball/train/output/kar_d15_seed45/target.npy b/src/drl_pinball/train/output/kar_d15_seed45/target.npy new file mode 100644 index 0000000..6b7361b Binary files /dev/null and b/src/drl_pinball/train/output/kar_d15_seed45/target.npy differ diff --git a/src/drl_pinball/train/output/kar_d2_seed45/best_vecnormalize.pkl b/src/drl_pinball/train/output/kar_d2_seed45/best_vecnormalize.pkl new file mode 100644 index 0000000..77d4217 Binary files /dev/null and b/src/drl_pinball/train/output/kar_d2_seed45/best_vecnormalize.pkl differ diff --git a/src/drl_pinball/train/calibrations/kar_d2_sc/calibration.json b/src/drl_pinball/train/output/kar_d2_seed45/calibration.json similarity index 96% rename from src/drl_pinball/train/calibrations/kar_d2_sc/calibration.json rename to src/drl_pinball/train/output/kar_d2_seed45/calibration.json index 701a899..291959e 100644 --- a/src/drl_pinball/train/calibrations/kar_d2_sc/calibration.json +++ b/src/drl_pinball/train/output/kar_d2_seed45/calibration.json @@ -1,5 +1,5 @@ { - "case": "kar_d2_sc", + "case": "karman_d2_v2", "dist_radius": 2.0, "grid": { "nx": 2000, diff --git a/src/drl_pinball/train/output/kar_d2_seed45/meta.json b/src/drl_pinball/train/output/kar_d2_seed45/meta.json new file mode 100644 index 0000000..cbb8e87 --- /dev/null +++ b/src/drl_pinball/train/output/kar_d2_seed45/meta.json @@ -0,0 +1,14 @@ +{ + "case_name": "kar_d2", + "seed": 45, + "total_episodes": 500, + "best_reward": 0.8954825901322895, + "n_steps": 2048, + "batch_size": 64, + "n_epochs": 10, + "lr": 0.0001, + "config_path": "/root/private_data/DynamisLab/configs/config_lbm_karman_2000x600.json", + "calibration_path": "/root/private_data/DynamisLab/src/drl_pinball/train/calibrations/kar_d2/calibration.json", + "transfer_model": null, + "best_episode": 499 +} diff --git a/src/drl_pinball/train/output/kar_d2_seed45/models/best_model.zip b/src/drl_pinball/train/output/kar_d2_seed45/models/best_model.zip new file mode 100644 index 0000000..f1ff112 Binary files /dev/null and b/src/drl_pinball/train/output/kar_d2_seed45/models/best_model.zip differ diff --git a/src/drl_pinball/train/output/kar_d2_seed45/target.npy b/src/drl_pinball/train/output/kar_d2_seed45/target.npy new file mode 100644 index 0000000..509da1c Binary files /dev/null and b/src/drl_pinball/train/output/kar_d2_seed45/target.npy differ diff --git a/src/drl_pinball/train/output/kar_re100_seed41/best_vecnormalize.pkl b/src/drl_pinball/train/output/kar_re100_seed41/best_vecnormalize.pkl new file mode 100644 index 0000000..181c0e8 Binary files /dev/null and b/src/drl_pinball/train/output/kar_re100_seed41/best_vecnormalize.pkl differ diff --git a/src/drl_pinball/train/calibrations/kar_d15_tr/calibration.json b/src/drl_pinball/train/output/kar_re100_seed41/calibration.json similarity index 71% rename from src/drl_pinball/train/calibrations/kar_d15_tr/calibration.json rename to src/drl_pinball/train/output/kar_re100_seed41/calibration.json index a4fb1b3..6ac5476 100644 --- a/src/drl_pinball/train/calibrations/kar_d15_tr/calibration.json +++ b/src/drl_pinball/train/output/kar_re100_seed41/calibration.json @@ -1,24 +1,23 @@ { - "case": "kar_d15_tr", - "dist_radius": 1.5, + "case": "re100", "grid": { "nx": 2000, "ny": 600 }, - "config_path": "/home/frank14f/DynamisLab/configs/config_lbm_karman_2000x600.json", + "config_path": "../../../configs/config_lbm_karman_2000x600.json", "SI": 800, "FIFO_LEN": 150, "CONV_LEN": 30, "SENSOR_CC": 78.0, - "FORCE_SCALE": 0.0036, + "FORCE_SCALE": 0.0024, "SENS_SCALE": 0.75, - "dtw_norm_scale": 0.307, + "dtw_norm_scale": 0.204, "SIM_BP": [ 0.0, - 0.3, - 0.65, - 0.79, + 0.32, + 0.82, 0.89, + 0.94, 1.0 ], "SIM_VAL": [ diff --git a/src/drl_pinball/train/output/kar_re100_seed41/meta.json b/src/drl_pinball/train/output/kar_re100_seed41/meta.json new file mode 100644 index 0000000..37ef5b1 --- /dev/null +++ b/src/drl_pinball/train/output/kar_re100_seed41/meta.json @@ -0,0 +1,14 @@ +{ + "case_name": "kar_re100", + "seed": 41, + "total_episodes": 500, + "best_reward": 0.9266385002268686, + "n_steps": 2048, + "batch_size": 64, + "n_epochs": 10, + "lr": 0.0003, + "config_path": "/root/private_data/DynamisLab/configs/config_lbm_karman_2000x600.json", + "calibration_path": "/root/private_data/DynamisLab/src/drl_pinball/train/calibrations/kar_re100/calibration.json", + "transfer_model": null, + "best_episode": 471 +} diff --git a/src/drl_pinball/train/output/kar_re100_seed41/models/best_model.zip b/src/drl_pinball/train/output/kar_re100_seed41/models/best_model.zip new file mode 100644 index 0000000..7c5ab9e Binary files /dev/null and b/src/drl_pinball/train/output/kar_re100_seed41/models/best_model.zip differ diff --git a/src/drl_pinball/train/output/kar_re100_seed41/target.npy b/src/drl_pinball/train/output/kar_re100_seed41/target.npy new file mode 120000 index 0000000..c7c42b5 --- /dev/null +++ b/src/drl_pinball/train/output/kar_re100_seed41/target.npy @@ -0,0 +1 @@ +../kar_re100_seed42/target.npy \ No newline at end of file diff --git a/src/drl_pinball/train/output/kar_re100_seed42/best_vecnormalize.pkl b/src/drl_pinball/train/output/kar_re100_seed42/best_vecnormalize.pkl new file mode 100644 index 0000000..d75b2dd Binary files /dev/null and b/src/drl_pinball/train/output/kar_re100_seed42/best_vecnormalize.pkl differ diff --git a/src/drl_pinball/train/calibrations/kar_d2_tr/calibration.json b/src/drl_pinball/train/output/kar_re100_seed42/calibration.json similarity index 69% rename from src/drl_pinball/train/calibrations/kar_d2_tr/calibration.json rename to src/drl_pinball/train/output/kar_re100_seed42/calibration.json index 9c75555..b155736 100644 --- a/src/drl_pinball/train/calibrations/kar_d2_tr/calibration.json +++ b/src/drl_pinball/train/output/kar_re100_seed42/calibration.json @@ -1,24 +1,23 @@ { - "case": "kar_d2_tr", - "dist_radius": 2.0, + "case": "re100", "grid": { "nx": 2000, "ny": 600 }, - "config_path": "/home/frank14f/DynamisLab/configs/config_lbm_karman_2000x600.json", + "config_path": "/root/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, + "FORCE_SCALE": 0.0024, + "SENS_SCALE": 0.75, + "dtw_norm_scale": 0.204, "SIM_BP": [ 0.0, - 0.3, - 0.65, - 0.79, + 0.32, + 0.82, 0.89, + 0.94, 1.0 ], "SIM_VAL": [ diff --git a/src/drl_pinball/train/output/kar_re100_seed42/meta.json b/src/drl_pinball/train/output/kar_re100_seed42/meta.json new file mode 100644 index 0000000..22df46c --- /dev/null +++ b/src/drl_pinball/train/output/kar_re100_seed42/meta.json @@ -0,0 +1,14 @@ +{ + "case_name": "kar_re100", + "seed": 42, + "total_episodes": 500, + "best_reward": 0.9301960359017054, + "n_steps": 2048, + "batch_size": 64, + "n_epochs": 10, + "lr": 0.0003, + "config_path": "/root/private_data/DynamisLab/configs/config_lbm_karman_2000x600.json", + "calibration_path": "/root/private_data/DynamisLab/src/drl_pinball/train/calibrations/kar_re100/calibration.json", + "transfer_model": null, + "best_episode": 315 +} diff --git a/src/drl_pinball/train/output/kar_re100_seed42/models/best_model.zip b/src/drl_pinball/train/output/kar_re100_seed42/models/best_model.zip new file mode 100644 index 0000000..b6df456 Binary files /dev/null and b/src/drl_pinball/train/output/kar_re100_seed42/models/best_model.zip differ diff --git a/src/drl_pinball/train/output/kar_re100_seed42/target.npy b/src/drl_pinball/train/output/kar_re100_seed42/target.npy new file mode 100644 index 0000000..899b5e0 Binary files /dev/null and b/src/drl_pinball/train/output/kar_re100_seed42/target.npy differ diff --git a/src/drl_pinball/train/output/kar_re100_seed43/best_vecnormalize.pkl b/src/drl_pinball/train/output/kar_re100_seed43/best_vecnormalize.pkl new file mode 100644 index 0000000..9405ad2 Binary files /dev/null and b/src/drl_pinball/train/output/kar_re100_seed43/best_vecnormalize.pkl differ diff --git a/src/drl_pinball/train/output/kar_re100_seed43/calibration.json b/src/drl_pinball/train/output/kar_re100_seed43/calibration.json new file mode 100644 index 0000000..b155736 --- /dev/null +++ b/src/drl_pinball/train/output/kar_re100_seed43/calibration.json @@ -0,0 +1,49 @@ +{ + "case": "re100", + "grid": { + "nx": 2000, + "ny": 600 + }, + "config_path": "/root/DynamisLab/configs/config_lbm_karman_2000x600.json", + "SI": 800, + "FIFO_LEN": 150, + "CONV_LEN": 30, + "SENSOR_CC": 78.0, + "FORCE_SCALE": 0.0024, + "SENS_SCALE": 0.75, + "dtw_norm_scale": 0.204, + "SIM_BP": [ + 0.0, + 0.32, + 0.82, + 0.89, + 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 +} \ No newline at end of file diff --git a/src/drl_pinball/train/output/kar_re100_seed43/meta.json b/src/drl_pinball/train/output/kar_re100_seed43/meta.json new file mode 100644 index 0000000..89ba8c4 --- /dev/null +++ b/src/drl_pinball/train/output/kar_re100_seed43/meta.json @@ -0,0 +1,14 @@ +{ + "case_name": "kar_re100", + "seed": 43, + "total_episodes": 500, + "best_reward": 0.916006507145034, + "n_steps": 2048, + "batch_size": 64, + "n_epochs": 10, + "lr": 0.0003, + "config_path": "/root/private_data/DynamisLab/configs/config_lbm_karman_2000x600.json", + "calibration_path": "/root/private_data/DynamisLab/src/drl_pinball/train/calibrations/kar_re100/calibration.json", + "transfer_model": null, + "best_episode": 360 +} diff --git a/src/drl_pinball/train/output/kar_re100_seed43/models/best_model.zip b/src/drl_pinball/train/output/kar_re100_seed43/models/best_model.zip new file mode 100644 index 0000000..6ac893e Binary files /dev/null and b/src/drl_pinball/train/output/kar_re100_seed43/models/best_model.zip differ diff --git a/src/drl_pinball/train/output/kar_re100_seed43/target.npy b/src/drl_pinball/train/output/kar_re100_seed43/target.npy new file mode 100644 index 0000000..e276565 Binary files /dev/null and b/src/drl_pinball/train/output/kar_re100_seed43/target.npy differ diff --git a/src/drl_pinball/train/output/kar_re100_seed44/best_vecnormalize.pkl b/src/drl_pinball/train/output/kar_re100_seed44/best_vecnormalize.pkl new file mode 100644 index 0000000..2ae57ed Binary files /dev/null and b/src/drl_pinball/train/output/kar_re100_seed44/best_vecnormalize.pkl differ diff --git a/src/drl_pinball/train/output/kar_re100_seed44/calibration.json b/src/drl_pinball/train/output/kar_re100_seed44/calibration.json new file mode 100644 index 0000000..b155736 --- /dev/null +++ b/src/drl_pinball/train/output/kar_re100_seed44/calibration.json @@ -0,0 +1,49 @@ +{ + "case": "re100", + "grid": { + "nx": 2000, + "ny": 600 + }, + "config_path": "/root/DynamisLab/configs/config_lbm_karman_2000x600.json", + "SI": 800, + "FIFO_LEN": 150, + "CONV_LEN": 30, + "SENSOR_CC": 78.0, + "FORCE_SCALE": 0.0024, + "SENS_SCALE": 0.75, + "dtw_norm_scale": 0.204, + "SIM_BP": [ + 0.0, + 0.32, + 0.82, + 0.89, + 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 +} \ No newline at end of file diff --git a/src/drl_pinball/train/output/kar_re100_seed44/meta.json b/src/drl_pinball/train/output/kar_re100_seed44/meta.json new file mode 100644 index 0000000..cbc2672 --- /dev/null +++ b/src/drl_pinball/train/output/kar_re100_seed44/meta.json @@ -0,0 +1,14 @@ +{ + "case_name": "kar_re100", + "seed": 44, + "total_episodes": 500, + "best_reward": 0.9221958074304792, + "n_steps": 2048, + "batch_size": 64, + "n_epochs": 10, + "lr": 0.0003, + "config_path": "/root/private_data/DynamisLab/configs/config_lbm_karman_2000x600.json", + "calibration_path": "/root/private_data/DynamisLab/src/drl_pinball/train/calibrations/kar_re100/calibration.json", + "transfer_model": null, + "best_episode": 465 +} diff --git a/src/drl_pinball/train/output/kar_re100_seed44/models/best_model.zip b/src/drl_pinball/train/output/kar_re100_seed44/models/best_model.zip new file mode 100644 index 0000000..e6b0b61 Binary files /dev/null and b/src/drl_pinball/train/output/kar_re100_seed44/models/best_model.zip differ diff --git a/src/drl_pinball/train/output/kar_re100_seed44/target.npy b/src/drl_pinball/train/output/kar_re100_seed44/target.npy new file mode 100644 index 0000000..16cf830 Binary files /dev/null and b/src/drl_pinball/train/output/kar_re100_seed44/target.npy differ diff --git a/src/drl_pinball/train/output/kar_re100_seed45/best_vecnormalize.pkl b/src/drl_pinball/train/output/kar_re100_seed45/best_vecnormalize.pkl new file mode 100644 index 0000000..0675f63 Binary files /dev/null and b/src/drl_pinball/train/output/kar_re100_seed45/best_vecnormalize.pkl differ diff --git a/src/drl_pinball/train/output/kar_re100_seed45/calibration.json b/src/drl_pinball/train/output/kar_re100_seed45/calibration.json new file mode 100644 index 0000000..b155736 --- /dev/null +++ b/src/drl_pinball/train/output/kar_re100_seed45/calibration.json @@ -0,0 +1,49 @@ +{ + "case": "re100", + "grid": { + "nx": 2000, + "ny": 600 + }, + "config_path": "/root/DynamisLab/configs/config_lbm_karman_2000x600.json", + "SI": 800, + "FIFO_LEN": 150, + "CONV_LEN": 30, + "SENSOR_CC": 78.0, + "FORCE_SCALE": 0.0024, + "SENS_SCALE": 0.75, + "dtw_norm_scale": 0.204, + "SIM_BP": [ + 0.0, + 0.32, + 0.82, + 0.89, + 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 +} \ No newline at end of file diff --git a/src/drl_pinball/train/output/kar_re100_seed45/meta.json b/src/drl_pinball/train/output/kar_re100_seed45/meta.json new file mode 100644 index 0000000..27fb1b0 --- /dev/null +++ b/src/drl_pinball/train/output/kar_re100_seed45/meta.json @@ -0,0 +1,14 @@ +{ + "case_name": "kar_re100", + "seed": 45, + "total_episodes": 500, + "best_reward": 0.9411612408028709, + "n_steps": 2048, + "batch_size": 64, + "n_epochs": 10, + "lr": 0.0003, + "config_path": "/root/private_data/DynamisLab/configs/config_lbm_karman_2000x600.json", + "calibration_path": "/root/private_data/DynamisLab/src/drl_pinball/train/calibrations/kar_re100/calibration.json", + "transfer_model": null, + "best_episode": 447 +} diff --git a/src/drl_pinball/train/output/kar_re100_seed45/models/best_model.zip b/src/drl_pinball/train/output/kar_re100_seed45/models/best_model.zip new file mode 100644 index 0000000..8841c26 Binary files /dev/null and b/src/drl_pinball/train/output/kar_re100_seed45/models/best_model.zip differ diff --git a/src/drl_pinball/train/output/kar_re100_seed45/target.npy b/src/drl_pinball/train/output/kar_re100_seed45/target.npy new file mode 100644 index 0000000..074f057 Binary files /dev/null and b/src/drl_pinball/train/output/kar_re100_seed45/target.npy differ diff --git a/src/drl_pinball/train/output/kar_re200_seed43/best_vecnormalize.pkl b/src/drl_pinball/train/output/kar_re200_seed43/best_vecnormalize.pkl new file mode 100644 index 0000000..99781fb Binary files /dev/null and b/src/drl_pinball/train/output/kar_re200_seed43/best_vecnormalize.pkl differ diff --git a/src/drl_pinball/train/calibrations/kar_re200_sc/calibration.json b/src/drl_pinball/train/output/kar_re200_seed43/calibration.json similarity index 96% rename from src/drl_pinball/train/calibrations/kar_re200_sc/calibration.json rename to src/drl_pinball/train/output/kar_re200_seed43/calibration.json index af0b742..c6885a7 100644 --- a/src/drl_pinball/train/calibrations/kar_re200_sc/calibration.json +++ b/src/drl_pinball/train/output/kar_re200_seed43/calibration.json @@ -1,5 +1,5 @@ { - "case": "kar_re200_sc", + "case": "re200_v2", "grid": { "nx": 2000, "ny": 600 diff --git a/src/drl_pinball/train/output/kar_re200_seed43/meta.json b/src/drl_pinball/train/output/kar_re200_seed43/meta.json new file mode 100644 index 0000000..389d9d8 --- /dev/null +++ b/src/drl_pinball/train/output/kar_re200_seed43/meta.json @@ -0,0 +1,14 @@ +{ + "case_name": "kar_re200", + "seed": 43, + "total_episodes": 500, + "best_reward": 0.5458830540378888, + "n_steps": 2048, + "batch_size": 64, + "n_epochs": 10, + "lr": 0.0003, + "config_path": "/root/private_data/DynamisLab/configs/config_lbm_karman_2000x600_re200.json", + "calibration_path": "/root/private_data/DynamisLab/src/drl_pinball/train/calibrations/kar_re200/calibration.json", + "transfer_model": null, + "best_episode": 114 +} diff --git a/src/drl_pinball/train/output/kar_re200_seed43/models/best_model.zip b/src/drl_pinball/train/output/kar_re200_seed43/models/best_model.zip new file mode 100644 index 0000000..2701824 Binary files /dev/null and b/src/drl_pinball/train/output/kar_re200_seed43/models/best_model.zip differ diff --git a/src/drl_pinball/train/output/kar_re200_seed43/target.npy b/src/drl_pinball/train/output/kar_re200_seed43/target.npy new file mode 100644 index 0000000..e4cbc54 Binary files /dev/null and b/src/drl_pinball/train/output/kar_re200_seed43/target.npy differ diff --git a/src/drl_pinball/train/output/kar_re400_seed43/best_vecnormalize.pkl b/src/drl_pinball/train/output/kar_re400_seed43/best_vecnormalize.pkl new file mode 100644 index 0000000..59888d3 Binary files /dev/null and b/src/drl_pinball/train/output/kar_re400_seed43/best_vecnormalize.pkl differ diff --git a/src/drl_pinball/train/calibrations/kar_re400_sc/calibration.json b/src/drl_pinball/train/output/kar_re400_seed43/calibration.json similarity index 96% rename from src/drl_pinball/train/calibrations/kar_re400_sc/calibration.json rename to src/drl_pinball/train/output/kar_re400_seed43/calibration.json index 67df582..4d265b1 100644 --- a/src/drl_pinball/train/calibrations/kar_re400_sc/calibration.json +++ b/src/drl_pinball/train/output/kar_re400_seed43/calibration.json @@ -1,5 +1,5 @@ { - "case": "kar_re400_sc", + "case": "re400_v2", "grid": { "nx": 2000, "ny": 600 diff --git a/src/drl_pinball/train/output/kar_re400_seed43/meta.json b/src/drl_pinball/train/output/kar_re400_seed43/meta.json new file mode 100644 index 0000000..c972dfa --- /dev/null +++ b/src/drl_pinball/train/output/kar_re400_seed43/meta.json @@ -0,0 +1,14 @@ +{ + "case_name": "kar_re400", + "seed": 43, + "total_episodes": 500, + "best_reward": 0.5418410102526346, + "n_steps": 2048, + "batch_size": 64, + "n_epochs": 10, + "lr": 0.0003, + "config_path": "/root/private_data/DynamisLab/configs/config_lbm_karman_2000x600_re400.json", + "calibration_path": "/root/private_data/DynamisLab/src/drl_pinball/train/calibrations/kar_re400/calibration.json", + "transfer_model": null, + "best_episode": 87 +} diff --git a/src/drl_pinball/train/output/kar_re400_seed43/models/best_model.zip b/src/drl_pinball/train/output/kar_re400_seed43/models/best_model.zip new file mode 100644 index 0000000..064a35d Binary files /dev/null and b/src/drl_pinball/train/output/kar_re400_seed43/models/best_model.zip differ diff --git a/src/drl_pinball/train/output/kar_re400_seed43/target.npy b/src/drl_pinball/train/output/kar_re400_seed43/target.npy new file mode 100644 index 0000000..5faef7e Binary files /dev/null and b/src/drl_pinball/train/output/kar_re400_seed43/target.npy differ diff --git a/src/drl_pinball/train/output/kar_re60_seed43/best_vecnormalize.pkl b/src/drl_pinball/train/output/kar_re60_seed43/best_vecnormalize.pkl new file mode 100644 index 0000000..8015208 Binary files /dev/null and b/src/drl_pinball/train/output/kar_re60_seed43/best_vecnormalize.pkl differ diff --git a/src/drl_pinball/train/calibrations/kar_re60_sc/calibration.json b/src/drl_pinball/train/output/kar_re60_seed43/calibration.json similarity index 96% rename from src/drl_pinball/train/calibrations/kar_re60_sc/calibration.json rename to src/drl_pinball/train/output/kar_re60_seed43/calibration.json index 523ccc9..6821a8d 100644 --- a/src/drl_pinball/train/calibrations/kar_re60_sc/calibration.json +++ b/src/drl_pinball/train/output/kar_re60_seed43/calibration.json @@ -1,5 +1,5 @@ { - "case": "kar_re60_sc", + "case": "re60_v2", "grid": { "nx": 2000, "ny": 600 diff --git a/src/drl_pinball/train/output/kar_re60_seed43/meta.json b/src/drl_pinball/train/output/kar_re60_seed43/meta.json new file mode 100644 index 0000000..c2acafe --- /dev/null +++ b/src/drl_pinball/train/output/kar_re60_seed43/meta.json @@ -0,0 +1,14 @@ +{ + "case_name": "kar_re60", + "seed": 43, + "total_episodes": 500, + "best_reward": 0.6949, + "n_steps": 2048, + "batch_size": 64, + "n_epochs": 10, + "lr": 0.0003, + "config_path": "/root/private_data/DynamisLab/configs/config_lbm_karman_2000x600_re60.json", + "calibration_path": "/root/private_data/DynamisLab/src/drl_pinball/train/calibrations/kar_re60/calibration.json", + "transfer_model": null, + "best_episode": 390 +} diff --git a/src/drl_pinball/train/output/kar_re60_seed43/models/best_model.zip b/src/drl_pinball/train/output/kar_re60_seed43/models/best_model.zip new file mode 100644 index 0000000..8ec6f8b Binary files /dev/null and b/src/drl_pinball/train/output/kar_re60_seed43/models/best_model.zip differ diff --git a/src/drl_pinball/train/output/kar_re60_seed43/target.npy b/src/drl_pinball/train/output/kar_re60_seed43/target.npy new file mode 100644 index 0000000..24122ce Binary files /dev/null and b/src/drl_pinball/train/output/kar_re60_seed43/target.npy differ diff --git a/src/drl_pinball/train/plot_latest_results.py b/src/drl_pinball/train/plot_latest_results.py new file mode 100644 index 0000000..24e57ba --- /dev/null +++ b/src/drl_pinball/train/plot_latest_results.py @@ -0,0 +1,396 @@ +#!/usr/bin/env python3 +"""Build a provenance-explicit CPU/offline package for retained V5 artifacts.""" +from __future__ import annotations + +import argparse +import csv +import json +import re +import sys +from pathlib import Path +from typing import Any + +_SRC = Path(__file__).resolve().parents[2] +if str(_SRC) not in sys.path: + sys.path.insert(0, str(_SRC)) + +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np +from PIL import Image + +from drl_pinball.train.case_registry import CASE_REGISTRY +from drl_pinball.train import plot_scratch_results as trainplot + +C = { + "blue": "#2474B5", "orange": "#D67A1F", "green": "#278A62", + "red": "#C8463A", "purple": "#7356A5", "gray": "#68717A", + "black": "#202428", "light": "#E7EAED", +} +L = { + "kar_re60": "Re60", "kar_re100": "Re100", "kar_re200": "Re200", + "kar_re400": "Re400", "kar_d075": "d/L₀=0.75", "kar_d15": "d/L₀=1.5", + "kar_d2": "d/L₀=2.0", "ill_075L": "Dₜ/L₀=0.75", "ill_1L": "Dₜ/L₀=1.0", + "ill_15L": "Dₜ/L₀=1.5", "ill_2L": "Dₜ/L₀=2.0", +} +GROUPS = [ + ("Cross-Re", ["kar_re60", "kar_re100", "kar_re200", "kar_re400"]), + ("Variable disturbance", ["kar_d075", "kar_re100", "kar_d15", "kar_d2"]), + ("Illusion", ["ill_075L", "ill_1L", "ill_15L", "ill_2L"]), +] +ACTION_NAMES = ("front", "top", "bottom") +LOG_BEST = re.compile( + r"Ep\s+(?P\d+): reward=(?P[0-9.]+).*?" + r"r_cd=(?P[0-9.]+) r_cl=(?P[0-9.]+) r_sim=(?P[0-9.]+)" +) +TB_REWARD_ATOL = 5e-5 +REPORTED_TB_AUDIT = { + "ill_075L": {"best_reward": 0.7154, "best_iteration": 470}, + "ill_1L": {"best_reward": 0.6570, "best_iteration": 290}, + "ill_15L": {"best_reward": 0.6865, "best_iteration": 491}, + "ill_2L": {"best_reward": 0.7882, "best_iteration": 484}, +} + + +def save(fig: plt.Figure, out: Path, stem: str, pdf: bool = True) -> None: + out.mkdir(parents=True, exist_ok=True) + fig.savefig(out / f"{stem}.png", dpi=300, bbox_inches="tight", facecolor="white") + if pdf: + fig.savefig(out / f"{stem}.pdf", bbox_inches="tight", facecolor="white") + plt.close(fig) + + +def family(case_id: str, scene_type: str) -> str: + if scene_type == "illusion": + return "Illusion" + return "Cross-Re" if case_id.startswith("kar_re") else "Variable disturbance" + + +def parse_best_log(log_path: Path, best_iteration: int) -> dict[str, float] | None: + matches = [m.groupdict() for m in LOG_BEST.finditer(log_path.read_text())] + exact = [m for m in matches if int(m["iteration"]) == best_iteration] + if not exact: + return None + item = exact[-1] + return {key: float(item[key]) for key in ("reward", "r_cd", "r_cl", "r_sim")} + + +def load_training(runs: list[trainplot.RunData]) -> list[dict[str, Any]]: + records = [] + for run in runs: + meta_path = run.path / "meta.json" + log_path = run.path / "train.log" + if not meta_path.exists() or not log_path.exists(): + raise FileNotFoundError(f"Required training provenance missing for {run.name}") + meta = json.loads(meta_path.read_text()) + tb = run.summary() + meta_reward = float(meta["best_reward"]) + meta_iteration = int(meta["best_episode"]) + reward_delta = float(tb["best_reward"] - meta_reward) + iteration_delta = int(tb["best_episode"] - meta_iteration) + consistent = abs(reward_delta) <= TB_REWARD_ATOL and iteration_delta == 0 + log_components = parse_best_log(log_path, meta_iteration) + record: dict[str, Any] = { + "run": run.name, + "case_id": run.case, + "label": L[run.case], + "family": family(run.case, CASE_REGISTRY[run.case].scene_type), + "seed": run.seed, + "training_iterations": int(meta["total_episodes"]), + "authoritative_best_reward": meta_reward, + "authoritative_best_iteration": meta_iteration, + "learning_rate": float(meta["lr"]), + "authoritative_source": "meta.json; same-iteration components from train.log when present", + "tensorboard_best_reward": float(tb["best_reward"]), + "tensorboard_best_iteration": int(tb["best_episode"]), + "tensorboard_reward_delta": reward_delta, + "tensorboard_iteration_delta": iteration_delta, + "tensorboard_consistent": consistent, + "tensorboard_tolerance_abs": TB_REWARD_ATOL, + "tensorboard_status": "validated" if consistent else "mismatch_not_used_for_scientific_values", + "best_components_from_log": log_components, + "reported_audit_disagreement": None, + } + if run.case in REPORTED_TB_AUDIT: + audited = REPORTED_TB_AUDIT[run.case] + record["reported_audit_disagreement"] = { + "reported_tensorboard_best_reward": audited["best_reward"], + "reported_tensorboard_best_iteration": audited["best_iteration"], + "authoritative_meta_best_reward": meta_reward, + "authoritative_meta_best_iteration": meta_iteration, + "resolution": ( + "reported mismatched values are not used; current on-disk TensorBoard " + "stream was independently re-read and agrees with meta within tolerance" + ), + } + if consistent and run.case == "kar_re100": + record["validated_tensorboard_retention"] = { + "last50_mean": tb["last50_mean"], + "last50_median": tb["last50_median"], + "last50_std": tb["last50_std"], + "final_reward": tb["final_reward"], + "retention_ratio": tb["retention_ratio"], + "source": "validated TensorBoard stream; 500 training iterations", + } + records.append(record) + return records + + +def validate_eval_artifacts(case_dir: Path) -> None: + required = [case_dir / "metrics.json", case_dir / "all_seeds.json", case_dir / "signals.npz"] + required.extend(case_dir / f"vorticity_{state}.png" for state in ("target", "controlled", "zero")) + missing = [str(path) for path in required if not path.exists()] + if missing: + raise FileNotFoundError("Missing required retained evaluation artifacts: " + ", ".join(missing)) + + +def load_eval(root: Path) -> tuple[list[dict[str, Any]], dict[str, dict[str, np.ndarray]]]: + rows, signals = [], {} + for case_id, spec in CASE_REGISTRY.items(): + case_dir = root / case_id + validate_eval_artifacts(case_dir) + metrics = json.loads((case_dir / "metrics.json").read_text()) + seeds = json.loads((case_dir / "all_seeds.json").read_text()) + selected = max(seeds, key=lambda item: float(item["reward"])) + if str(selected["seed"]) != str(metrics["best_seed"]): + raise ValueError(f"{case_id}: metrics best_seed does not maximize all_seeds reward") + with np.load(case_dir / "signals.npz") as archive: + signals[case_id] = {key: np.asarray(archive[key]) for key in archive.files} + actions = signals[case_id]["actions"] + if actions.ndim != 2 or actions.shape[1] != 3 or len(actions) < 180: + raise ValueError(f"{case_id}: expected actions with shape (>=180, 3), got {actions.shape}") + tail = actions[-180:] + row: dict[str, Any] = { + "case_id": case_id, "label": L[case_id], "group": family(case_id, spec.scene_type), + "selected_seed": int(metrics["best_seed"]), "seed_count": len(seeds), + "evaluation_scope": "multi-seed selection" if len(seeds) > 1 else "single-seed demonstration", + "eval_reward_mean": float(metrics["reward_mean"]), + "eval_r_cd_mean": float(metrics["r_cd_mean"]), + "eval_r_cl_mean": float(metrics["r_cl_mean"]), + "eval_r_sim_mean": float(metrics["r_sim_mean"]), + "dtw_similarity": float(metrics["dtw_sim_v5"]), + "artifact_lineage": "retained canonical historical-policy evaluation artifact", + "policy_normalization_contract": "legacy-policy-v1 compatibility path", + "not_retraining_evidence": True, + } + for index, name in enumerate(ACTION_NAMES): + row[f"action_{name}_mean"] = float(tail[:, index].mean()) + row[f"action_{name}_rms"] = float(np.sqrt(np.mean(tail[:, index] ** 2))) + row[f"action_{name}_saturation_fraction"] = float(np.mean(np.abs(tail[:, index]) >= 0.95)) + row["action_any_saturation_fraction"] = float(np.mean(np.any(np.abs(tail) >= 0.95, axis=1))) + rows.append(row) + return rows, signals + + +def plot_abstract(out: Path) -> None: + fig, ax = plt.subplots(figsize=(13.33, 7.5)) + ax.axis("off") + ax.text(.03, .94, "How to read retained V5 scratch/evaluation evidence", fontsize=22, weight="bold", va="top") + titles = ["1 Training provenance", "2 Validate stream", "3 Retained evaluation", "4 Independent evidence"] + bodies = [ + "15 historical scratch runs\nmeta.json + train.log authoritative\ntransfer excluded", + "TB checked against meta\nRe100 validated\nmismatches never used scientifically", + "360 recorded steps\nlegacy-policy-v1 compatibility\nnot a current-v2 retraining claim", + "reward components + DTW\nactions + signals + vorticity\ndifferent scientific questions", + ] + for x, title, body in zip([.04, .275, .51, .745], titles, bodies): + ax.add_patch(plt.Rectangle((x, .57), .205, .23, fc="white", ec=C["blue"], lw=1.5)) + ax.text(x + .012, .76, title, fontsize=11.5, weight="bold", color=C["blue"], va="top") + ax.text(x + .012, .71, body, fontsize=9.5, va="top", linespacing=1.5) + for x in [.252, .487, .722]: + ax.annotate("", xy=(x + .012, .685), xytext=(x - .012, .685), arrowprops={"arrowstyle": "->", "color": C["gray"]}) + notes = [ + (.45, "Training unit: one PPO learn chunk + deterministic evaluation; call it training iteration, not Gym episode.", 12, "black"), + (.37, "Higher reward/components are better. DTW is signal similarity—not physical field error.", 11, "black"), + (.29, "Target = desired reference; Controlled = policy outcome; Zero = no-rotation baseline.", 11, "black"), + (.21, "Retained actions/vorticity predate the calibration-v2/native-normalization source contract.", 11, "red"), + (.13, "d/L₀=0.75 and 2.0 used lr=1e-4; most runs used 3e-4. Geometry is not a pure one-factor ablation.", 11, "red"), + (.055, "Only Re100 has n=5 replication; all other points are n=1 demonstrations.", 11, "black"), + ] + for y, text, size, color in notes: + ax.text(.04, y, text, fontsize=size, color=C[color], weight="bold" if color == "red" or y == .055 else None) + save(fig, out, "fig00_graphical_abstract_reading_guide") + + +def plot_authoritative_training(training: list[dict[str, Any]], evaluation: list[dict[str, Any]], out: Path) -> None: + eval_by = {row["case_id"]: row for row in evaluation} + by_case: dict[str, list[dict[str, Any]]] = {} + for record in training: + by_case.setdefault(record["case_id"], []).append(record) + fig, axes = plt.subplots(1, 3, figsize=(12, 4.4), sharey=True) + for ax, (title, cases) in zip(axes, GROUPS): + train_values = [np.median([r["authoritative_best_reward"] for r in by_case[c]]) for c in cases] + eval_values = [eval_by[c]["eval_reward_mean"] for c in cases] + x = np.arange(len(cases)); width = .36 + ax.bar(x - width / 2, train_values, width, label="Authoritative training best", color=C["blue"]) + ax.bar(x + width / 2, eval_values, width, label="Retained deterministic eval", color=C["green"]) + ax.set_xticks(x, [L[c] for c in cases], rotation=25, ha="right") + ax.set_title(title); ax.set_ylim(0, 1.05); ax.grid(axis="y") + if title == "Cross-Re": ax.set_ylabel("Reward [0–1]") + axes[0].legend(frameon=False, fontsize=7) + fig.suptitle("Authoritative best-checkpoint metadata versus retained deterministic evaluation", fontsize=14) + fig.text(.5, .01, "Training: meta.json (Re100 median across n=5; all others n=1). Eval: compatibility-path historical policy artifacts. Values are distinct evidence streams.", ha="center", fontsize=8, color=C["gray"]) + fig.tight_layout(rect=(0, .04, 1, .95)); save(fig, out, "fig04_authoritative_training_vs_eval") + + order = list(CASE_REGISTRY); x = np.arange(len(order)) + best_iterations = [np.median([r["authoritative_best_iteration"] for r in by_case[c]]) for c in order] + gaps = [eval_by[c]["eval_reward_mean"] - np.median([r["authoritative_best_reward"] for r in by_case[c]]) for c in order] + fig, axes = plt.subplots(2, 1, figsize=(11, 7), sharex=True) + axes[0].bar(x, best_iterations, color=C["purple"]) + axes[0].set_ylabel("Best training iteration"); axes[0].set_title("Best-checkpoint location from authoritative run metadata") + axes[1].bar(x, gaps, color=[C["green"] if value >= 0 else C["red"] for value in gaps]) + axes[1].axhline(0, color=C["black"], lw=.8); axes[1].set_ylabel("Eval − training-best reward"); axes[1].set_xlabel("Canonical case") + axes[1].set_xticks(x, [L[c] for c in order], rotation=28, ha="right") + for ax in axes: ax.grid(axis="y") + fig.text(.5, .01, "Metadata and retained eval are not paired measurements under one normalization contract; the gap is descriptive, not a generalization estimate. d0.75/d2 use lr=1e-4.", ha="center", fontsize=8, color=C["gray"]) + fig.tight_layout(rect=(0, .04, 1, 1)); save(fig, out, "fig05_authoritative_iteration_and_eval_gap") + + +def plot_eval(rows: list[dict[str, Any]], out: Path) -> None: + by = {row["case_id"]: row for row in rows} + metrics = [("eval_reward_mean", "Deterministic eval reward", C["blue"]), ("eval_r_cd_mean", r"Drag component $r_{CD}$", C["green"]), ("dtw_similarity", "DTW signal similarity", C["purple"])] + fig, axes = plt.subplots(3, 3, figsize=(12, 9), sharey="row") + for column, (group, cases) in enumerate(GROUPS): + for row_index, (key, ylabel, color) in enumerate(metrics): + ax = axes[row_index, column]; values = [by[case][key] for case in cases] + bars = ax.bar(range(4), values, color=color, width=.68) + ax.set_xticks(range(4), [L[case] for case in cases], rotation=25, ha="right"); ax.set_ylim(0, 1.04); ax.grid(axis="y") + if row_index == 0: ax.set_title(group, weight="bold") + if column == 0: ax.set_ylabel(ylabel) + for bar, value in zip(bars, values): ax.text(bar.get_x() + bar.get_width() / 2, value + .015, f"{value:.2f}", ha="center", fontsize=7) + fig.suptitle("Retained canonical deterministic evaluations (historical-policy compatibility path)", fontsize=14) + fig.text(.5, .01, "Source: metrics.json for reward-maximizing seed in all_seeds.json. Retained artifact evidence, not proof of current calibration-v2/native retraining. DTW is signal similarity.", ha="center", fontsize=8, color=C["gray"]) + fig.tight_layout(rect=(0, .035, 1, .96)); save(fig, out, "fig07_eval_cross_scene_metrics") + order = list(CASE_REGISTRY); x = np.arange(len(order)); width = .19; fig, ax = plt.subplots(figsize=(11, 4.8)) + for index, (key, label, color) in enumerate([("eval_reward_mean", "Reward", C["black"]), ("eval_r_cd_mean", r"$r_{CD}$", C["blue"]), ("eval_r_cl_mean", r"$r_{CL}$", C["orange"]), ("eval_r_sim_mean", r"$r_{sim}$", C["purple"])]): + ax.bar(x + (index - 1.5) * width, [by[c][key] for c in order], width, label=label, color=color) + ax.set_xticks(x, [L[c] for c in order], rotation=28, ha="right"); ax.set_ylim(0, 1.05); ax.set_ylabel("Mean component value"); ax.set_title("Retained deterministic-evaluation reward components"); ax.grid(axis="y"); ax.legend(ncol=4, frameon=False) + fig.text(.5, .01, "Historical-policy compatibility path; components do not establish current-contract retraining or physical field agreement.", ha="center", fontsize=8, color=C["gray"]) + fig.tight_layout(rect=(0, .04, 1, 1)); save(fig, out, "fig08_eval_reward_components") + + +def plot_actions(rows: list[dict[str, Any]], signals: dict[str, dict[str, np.ndarray]], out: Path) -> None: + order = list(CASE_REGISTRY); by = {row["case_id"]: row for row in rows}; x = np.arange(len(order)); width = .23 + fig, axes = plt.subplots(2, 1, figsize=(11, 7.2), sharex=True) + for index, (name, color) in enumerate(zip(ACTION_NAMES, [C["red"], C["orange"], C["green"]])): + axes[0].bar(x + (index - 1) * width, [by[c][f"action_{name}_mean"] for c in order], width, label=name.title(), color=color) + axes[1].bar(x + (index - 1) * width, [by[c][f"action_{name}_saturation_fraction"] for c in order], width, label=name.title(), color=color) + axes[0].axhline(0, color=C["black"], lw=.8); axes[0].set_ylabel("Tail-180 action mean"); axes[0].set_title("Retained action bias and saturation diagnostics") + axes[1].set_ylabel("Saturation fraction"); axes[1].set_xticks(x, [L[c] for c in order], rotation=28, ha="right") + for ax in axes: ax.grid(axis="y"); ax.legend(ncol=3, frameon=False) + fig.text(.5, .01, "Action layout verified from active env/symmetry code: [front, top, bottom]. Saturation |a|≥0.95 over tail 180. Retained compatibility-path artifacts.", ha="center", fontsize=8, color=C["gray"]) + fig.tight_layout(rect=(0, .04, 1, 1)); save(fig, out, "fig09_action_profile_saturation") + reps = ["kar_re100", "kar_re400", "ill_1L"]; fig, axes = plt.subplots(3, 2, figsize=(11, 7), sharex=True) + for row_index, case in enumerate(reps): + signal = signals[case]; time = np.arange(len(signal["rewards"])) + for action_index, label, color in [(0, "Front", C["red"]), (1, "Top", C["orange"]), (2, "Bottom", C["green"])]: + axes[row_index, 0].plot(time, signal["actions"][:, action_index], lw=.7, label=label, color=color) + axes[row_index, 0].axvspan(len(time) - 180, len(time) - 1, color=C["light"], alpha=.45); axes[row_index, 0].set_ylim(-1.05, 1.05); axes[row_index, 0].set_ylabel(L[case] + "\nAction") + ax = axes[row_index, 1]; ax.plot(time, signal["sensors"][:, 3], lw=.8, color=C["blue"]); ax2 = ax.twinx(); ax2.plot(time, signal["rewards"], lw=.7, color=C["green"]); ax2.tick_params(axis="y", labelcolor=C["green"]) + axes[0, 0].legend(frameon=False, ncol=3, fontsize=7); axes[0, 0].set_title("Policy actions (shaded: reported tail)"); axes[0, 1].set_title("Sensor uy (blue) and reward (green, right axis)") + for ax in axes[-1]: ax.set_xlabel("Recorded evaluation step") + fig.suptitle("Representative retained deterministic signal/action diagnostics", fontsize=14) + fig.text(.5, .01, "Historical-policy compatibility artifacts; not evidence of current-contract retraining. Forces remain unaggregated across scene semantics.", ha="center", fontsize=8, color=C["gray"]) + fig.tight_layout(rect=(0, .035, 1, .96)); save(fig, out, "fig10_representative_signal_diagnostics") + + +def plot_scope(rows: list[dict[str, Any]], out: Path) -> None: + order = list(CASE_REGISTRY); by = {row["case_id"]: row for row in rows}; counts = [by[c]["seed_count"] for c in order] + fig, ax = plt.subplots(figsize=(10.5, 3.9)); bars = ax.bar(range(11), counts, color=[C["blue"] if n > 1 else C["gray"] for n in counts]) + ax.set_xticks(range(11), [L[c] for c in order], rotation=28, ha="right"); ax.set_yticks(range(6)); ax.set_ylim(0, 5.7); ax.set_ylabel("Retained evaluated scratch seeds, n"); ax.set_title("Evidence scope: replication exists only for Re100"); ax.grid(axis="y") + for bar, n in zip(bars, counts): ax.text(bar.get_x() + bar.get_width() / 2, n + .12, f"n={n}", ha="center", weight="bold") + fig.text(.5, .01, "n=1 points are demonstrations. Retained historical artifacts do not establish current-contract retraining.", ha="center", color=C["red"]) + fig.tight_layout(rect=(0, .05, 1, 1)); save(fig, out, "fig11_evidence_scope") + + +def contact_sheet(root: Path, out: Path, title: str, cases: list[str], stem: str) -> None: + fig, axes = plt.subplots(len(cases), 3, figsize=(12, 2.25 * len(cases))) + for row_index, case in enumerate(cases): + for column, state in enumerate(("target", "controlled", "zero")): + path = root / case / f"vorticity_{state}.png" + if not path.exists(): raise FileNotFoundError(path) + with Image.open(path) as image: axes[row_index, column].imshow(image.convert("RGB")) + axes[row_index, column].axis("off") + if row_index == 0: axes[row_index, column].set_title(state.title(), weight="bold") + if column == 0: axes[row_index, column].text(-.02, .5, L[case], transform=axes[row_index, column].transAxes, ha="right", va="center", weight="bold") + fig.suptitle(title + " — retained compatibility-path artifacts", fontsize=14) + fig.tight_layout(rect=(.04, 0, 1, .96)); save(fig, out, stem, False) + + +def write_data(rows: list[dict[str, Any]], training: list[dict[str, Any]], tables: Path) -> None: + tables.mkdir(parents=True, exist_ok=True) + with (tables / "latest_eval_summary.csv").open("w", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=list(rows[0])); writer.writeheader(); writer.writerows(rows) + payload = { + "schema": "drl-pinball-latest-results-v2", + "scope": {"canonical_cases": 11, "scratch_runs": 15, "transfer_excluded": True, "eval_steps": 360, "action_tail_steps": 180, "saturation_threshold_abs": .95, "action_layout": ["front", "top", "bottom"]}, + "lineage": {"training_authority": "run meta.json and train.log", "tensorboard_role": "validated auxiliary stream; mismatch values not authoritative", "evaluation_authority": "retained canonical artifacts evaluated through legacy-policy-v1 compatibility", "current_contract_caveat": "artifacts predate and do not prove complete retraining under calibration-v2/native policy normalization"}, + "confounders": [{"cases": ["kar_d075", "kar_d2"], "learning_rate": 1e-4, "comparison_warning": "variable-disturbance geometry sweep is not a pure one-factor ablation; most runs use 3e-4"}], + "training": training, + "re100_validated_tensorboard_retention": [record for record in training if record["case_id"] == "kar_re100"], + "evaluation": rows, + } + (tables / "latest_results_summary.json").write_text(json.dumps(payload, indent=2) + "\n") + + +def write_guide(root: Path) -> None: + text = """# 最新 Train 保留证据图包阅读指南 + +## 最重要的证据谱系 +本图包使用 15 个历史 scratch run 和 11 个 canonical eval 目录中的**保留 artifacts**。它们早于当前 calibration-v2/native policy normalization 源码契约,不能被表述为“修复后已完整重训”。历史 PPO/VecNormalize 评估使用明确的 `legacy-policy-v1` compatibility path。 + +训练最佳值以每个 run 的 `meta.json` 为权威,并用 `train.log` 提取同一 training iteration 的分量(若存在)。TensorBoard 仅作为被校验的辅助流;若与 meta 不一致,不使用其科学数值。一个所谓“episode”实际是 PPO learn chunk 加确定性评估,本图包称为 **training iteration**,不是终止型 Gym episode。 + +## 证据边界与混杂 +只有 `kar_re100` 有 5 个种子;其余点均为 n=1 演示,不能估计种子不确定性。`kar_d075` 和 `kar_d2` 使用学习率 `1e-4`,多数 run 使用 `3e-4`,因此 variable-disturbance geometry 比较不是纯粹单因素消融。 + +## 两条不可混淆的证据链 +1. **训练期:** authoritative best reward/iteration 来自 `meta.json`;同 iteration 分量来自 `train.log`。仅 Re100 的 retention/final 使用经 meta 校验一致的 TensorBoard。 +2. **保留确定性评估:** `metrics.json` 对应 `all_seeds.json` 中 reward 最高 seed;signals/vorticity 是历史 policy compatibility-path artifacts,不等同于训练 reward,也不证明当前契约重训。 + +## 指标、动作与图像 +Reward、`r_CD`、`r_CL`、`r_sim`、DTW similarity 均越高越好;DTW 是时间信号相似度,不是物理场误差。Target/Controlled/Zero 用于涡量结构对照。动作顺序已由 active env 与 symmetry code 核实为 `[front, top, bottom]`。动作均值接近零可能是振荡抵消;饱和定义为 `|a| >= 0.95`,窗口为最后 180 步。 + +## 图号与实际文件 +- `fig00_graphical_abstract_reading_guide`:谱系与阅读规则。 +- 仅 Re100、且 TB 已经 meta 校验:`fig01_re100_multiseed_learning`、`fig02_re100_discovery_drawdown`、`fig02_re100_policy_retention`、`fig03_re100_reward_components`。 +- 全 case authoritative/meta 与 retained eval:`fig04_authoritative_training_vs_eval`、`fig05_authoritative_iteration_and_eval_gap`。本包没有 `fig06`。 +- retained eval:`fig07_eval_cross_scene_metrics`、`fig08_eval_reward_components`、`fig09_action_profile_saturation`、`fig10_representative_signal_diagnostics`、`fig11_evidence_scope`。 +- retained vorticity:`fig12_vorticity_cross_re`、`fig13_vorticity_disturbance`、`fig14_vorticity_illusion`。 + +## 复现 +```bash +conda run -n pycuda_3_10 python src/drl_pinball/train/plot_latest_results.py +``` +脚本要求 signals 和三类 vorticity PNG 全部存在,否则失败;不加载 fields.npz、不重跑 CFD。 +""" + (root / "HOW_TO_READ.md").write_text(text) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__); here = Path(__file__).resolve().parent + parser.add_argument("--train-root", type=Path, default=here / "output") + parser.add_argument("--eval-root", type=Path, default=here.parent / "eval" / "output" / "train") + parser.add_argument("--output-dir", type=Path, default=here / "results" / "latest") + args = parser.parse_args(); figures = args.output_dir / "figures"; tables = args.output_dir / "tables" + figures.mkdir(parents=True, exist_ok=True); trainplot.configure_style() + runs = trainplot.discover_runs(args.train_root) + if len(runs) != 15 or {run.case for run in runs} != set(CASE_REGISTRY): raise ValueError("Expected 15 runs across 11 canonical cases") + training = load_training(runs); rows, signals = load_eval(args.eval_root) + plot_abstract(figures) + re100_runs = [run for run in runs if run.case == "kar_re100"] + if not all(record["tensorboard_consistent"] for record in training if record["case_id"] == "kar_re100"): raise ValueError("Re100 TensorBoard is not consistent with metadata") + trainplot.plot_multiseed(re100_runs, figures, 20); trainplot.plot_exploration_drawdown(re100_runs, figures); trainplot.plot_retention(re100_runs, figures); trainplot.plot_components(re100_runs, figures, 20) + plot_authoritative_training(training, rows, figures); plot_eval(rows, figures); plot_actions(rows, signals, figures); plot_scope(rows, figures) + contact_sheet(args.eval_root, figures, "Cross-Re vorticity evidence", GROUPS[0][1], "fig12_vorticity_cross_re") + contact_sheet(args.eval_root, figures, "Variable-disturbance vorticity evidence", GROUPS[1][1], "fig13_vorticity_disturbance") + contact_sheet(args.eval_root, figures, "Illusion vorticity evidence", GROUPS[2][1], "fig14_vorticity_illusion") + write_data(rows, training, tables); write_guide(args.output_dir) + mismatches = sum(not record["tensorboard_consistent"] for record in training) + print(f"Generated {len(rows)} retained cases / {len(training)} runs; TensorBoard mismatches recorded: {mismatches} -> {args.output_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/drl_pinball/train/plot_scratch_results.py b/src/drl_pinball/train/plot_scratch_results.py new file mode 100644 index 0000000..59cdae4 --- /dev/null +++ b/src/drl_pinball/train/plot_scratch_results.py @@ -0,0 +1,527 @@ +#!/usr/bin/env python3 +"""Generate publication-quality figures for scratch-only DRL training results. + +The script discovers completed canonical ``*_seed*`` runs, reads their TensorBoard +metrics, and deliberately excludes transfer-learning runs. It produces PNG and +PDF figures plus CSV/JSON summaries without rerunning CFD. + +Usage: + python src/drl_pinball/train/plot_scratch_results.py + python src/drl_pinball/train/plot_scratch_results.py --window 20 +""" +from __future__ import annotations + +import argparse +import csv +import json +import re +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, Iterable, List, Sequence + +from drl_pinball.train.case_registry import CASE_IDS + +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.lines import Line2D +from tensorboard.backend.event_processing.event_accumulator import EventAccumulator + +_SRC = Path(__file__).resolve().parents[2] +if str(_SRC) not in sys.path: + sys.path.insert(0, str(_SRC)) + +TAGS = { + "reward": "eval/avg_reward", + "r_cd": "eval/r_cd", + "r_cl": "eval/r_cl", + "r_sim": "eval/r_sim", +} +SEED_RE = re.compile(r"^(?P.+)_seed(?P\d+)$") +COLORS = { + "blue": "#2474B5", + "orange": "#D67A1F", + "green": "#278A62", + "red": "#C8463A", + "purple": "#7356A5", + "gray": "#68717A", + "black": "#202428", +} + + +@dataclass +class RunData: + name: str + case: str + seed: int + path: Path + steps: np.ndarray + metrics: Dict[str, np.ndarray] + + @property + def reward(self) -> np.ndarray: + return self.metrics["reward"] + + def summary(self) -> dict: + best_idx = int(np.argmax(self.reward)) + tail = self.reward[-50:] + return { + "run": self.name, + "case": self.case, + "seed": self.seed, + "episodes": len(self.reward), + "best_reward": float(self.reward[best_idx]), + "best_episode": int(self.steps[best_idx]), + "final_reward": float(self.reward[-1]), + "last50_mean": float(np.mean(tail)), + "last50_median": float(np.median(tail)), + "last50_std": float(np.std(tail)), + "retention_ratio": float(np.median(tail) / self.reward[best_idx]), + "collapse_severity": float(self.reward[best_idx] - np.median(tail)), + "best_r_cd": float(self.metrics["r_cd"][best_idx]), + "best_r_cl": float(self.metrics["r_cl"][best_idx]), + "best_r_sim": float(self.metrics["r_sim"][best_idx]), + } + + +def configure_style() -> None: + plt.rcParams.update({ + "font.family": "DejaVu Sans", + "font.size": 9, + "axes.titlesize": 10, + "axes.labelsize": 9, + "legend.fontsize": 8, + "xtick.labelsize": 8, + "ytick.labelsize": 8, + "axes.spines.top": False, + "axes.spines.right": False, + "axes.linewidth": 0.8, + "grid.alpha": 0.18, + "grid.linewidth": 0.6, + "figure.dpi": 150, + "savefig.dpi": 300, + "pdf.fonttype": 42, + "ps.fonttype": 42, + }) + + +def smooth(values: np.ndarray, window: int) -> np.ndarray: + if window <= 1 or len(values) < window: + return values.copy() + kernel = np.ones(window, dtype=float) / window + core = np.convolve(values, kernel, mode="valid") + left = window // 2 + right = len(values) - len(core) - left + return np.pad(core, (left, right), mode="edge") + + +def incumbent_indices(reward: np.ndarray) -> np.ndarray: + """Index of the best checkpoint discovered up to each episode.""" + indices = np.empty(len(reward), dtype=int) + best_idx = 0 + for idx, value in enumerate(reward): + if value > reward[best_idx]: + best_idx = idx + indices[idx] = best_idx + return indices + + +def running_best(reward: np.ndarray) -> np.ndarray: + """Monotone best-so-far reward envelope.""" + return np.maximum.accumulate(reward) + + +def load_tag(tb_dir: Path, tag: str) -> tuple[np.ndarray, np.ndarray]: + accumulator = EventAccumulator(str(tb_dir), size_guidance={"scalars": 0}) + accumulator.Reload() + if tag not in accumulator.Tags().get("scalars", []): + raise KeyError(f"Missing TensorBoard tag {tag!r} in {tb_dir}") + by_step = {int(event.step): float(event.value) for event in accumulator.Scalars(tag)} + steps = np.array(sorted(by_step), dtype=int) + return steps, np.array([by_step[step] for step in steps], dtype=float) + + +def discover_runs(output_dir: Path) -> List[RunData]: + runs: List[RunData] = [] + for run_dir in sorted(output_dir.glob("*_seed*")): + if not run_dir.is_dir(): + continue + match = SEED_RE.match(run_dir.name) + if match is None or match.group("case") not in CASE_IDS: + continue + metrics: Dict[str, np.ndarray] = {} + canonical_steps = None + for short, tag in TAGS.items(): + steps, values = load_tag(run_dir / "tb", tag) + if canonical_steps is None: + canonical_steps = steps + elif not np.array_equal(canonical_steps, steps): + raise ValueError(f"Metric step mismatch in {run_dir}") + metrics[short] = values + runs.append(RunData( + name=run_dir.name, + case=match.group("case"), + seed=int(match.group("seed")), + path=run_dir, + steps=canonical_steps, + metrics=metrics, + )) + if not runs: + raise FileNotFoundError(f"No scratch runs found under {output_dir}") + return runs + + +def save_figure(fig: plt.Figure, out_dir: Path, stem: str) -> None: + fig.savefig(out_dir / f"{stem}.png", bbox_inches="tight", facecolor="white") + fig.savefig(out_dir / f"{stem}.pdf", bbox_inches="tight", facecolor="white") + plt.close(fig) + + +def panel_label(ax: plt.Axes, label: str) -> None: + ax.text(-0.11, 1.04, label, transform=ax.transAxes, fontsize=11, + fontweight="bold", va="bottom") + + +def baseline_runs(runs: Sequence[RunData]) -> List[RunData]: + result = sorted((run for run in runs if run.case == "kar_re100"), key=lambda run: run.seed) + if len(result) < 2: + raise ValueError("Re100 multi-seed plot requires at least two scratch seeds") + return result + + +def plot_multiseed(runs: Sequence[RunData], out_dir: Path, window: int) -> None: + base = baseline_runs(runs) + steps = base[0].steps + best_matrix = np.vstack([running_best(run.reward) for run in base]) + best_median = np.median(best_matrix, axis=0) + best_q25, best_q75 = np.percentile(best_matrix, [25, 75], axis=0) + best_minimum, best_maximum = best_matrix.min(axis=0), best_matrix.max(axis=0) + + fig, axes = plt.subplots(2, 1, figsize=(7.2, 7.0), sharex=True, + gridspec_kw={"height_ratios": [1.15, 1.0]}) + ax = axes[0] + seed_colors = [COLORS["blue"], COLORS["orange"], COLORS["green"], + COLORS["red"], COLORS["purple"]] + for run, color in zip(base, seed_colors): + ax.plot(run.steps, run.reward, color=color, alpha=0.10, lw=0.45) + ax.step(run.steps, running_best(run.reward), where="post", color=color, lw=1.45, + label=f"Seed {run.seed}") + best = int(np.argmax(run.reward)) + ax.scatter(run.steps[best], run.reward[best], s=23, color=color, + edgecolor="white", linewidth=0.5, zorder=4) + ax.set_ylabel("Evaluation reward") + ax.set_title("Raw exploration outcomes and best-so-far policy envelopes") + ax.set_ylim(0, 1.02) + ax.grid(True) + ax.legend(ncol=5, loc="lower center", frameon=False) + panel_label(ax, "a") + + ax = axes[1] + ax.fill_between(steps, best_minimum, best_maximum, color=COLORS["blue"], alpha=0.10, + step="post", label="Seed range") + ax.fill_between(steps, best_q25, best_q75, color=COLORS["blue"], alpha=0.24, + step="post", label="Interquartile range") + ax.step(steps, best_median, where="post", color=COLORS["black"], lw=2.0, + label="Median best-so-far") + ax.set_xlabel("Training iteration") + ax.set_ylabel("Best reward discovered") + ax.set_title("Across-seed progress of the incumbent policy") + ax.set_ylim(0, 1.02) + ax.grid(True) + ax.legend(frameon=False, ncol=3, loc="lower center") + panel_label(ax, "b") + fig.suptitle("Re100 exploration is volatile while policy discovery progresses monotonically", + fontsize=12, y=0.995) + fig.text(0.5, 0.005, + "Thin lines are per-iteration evaluations; stair-step curves retain the best checkpoint found up to each training iteration.", + ha="center", fontsize=8, color=COLORS["gray"]) + fig.tight_layout(rect=(0, 0.03, 1, 0.97)) + save_figure(fig, out_dir, "fig01_re100_multiseed_learning") + + +def plot_exploration_drawdown(runs: Sequence[RunData], out_dir: Path) -> None: + """Show discovery progress separately from exploratory policy drawdown.""" + base = baseline_runs(runs) + steps = base[0].steps + best_matrix = np.vstack([running_best(run.reward) for run in base]) + drawdown_matrix = np.vstack([ + running_best(run.reward) - run.reward for run in base + ]) + + fig, axes = plt.subplots(2, 1, figsize=(7.2, 6.2), sharex=True) + ax = axes[0] + best_med = np.median(best_matrix, axis=0) + best_q25, best_q75 = np.percentile(best_matrix, [25, 75], axis=0) + ax.fill_between(steps, best_q25, best_q75, step="post", + color=COLORS["green"], alpha=0.24) + ax.step(steps, best_med, where="post", color=COLORS["green"], lw=2.0) + ax.set_ylabel("Best reward discovered") + ax.set_title("Incumbent policy improves as new checkpoints are discovered") + ax.set_ylim(0, 1.02) + ax.grid(True) + panel_label(ax, "a") + + ax = axes[1] + draw_med = np.median(drawdown_matrix, axis=0) + draw_q25, draw_q75 = np.percentile(drawdown_matrix, [25, 75], axis=0) + draw_max = np.max(drawdown_matrix, axis=0) + ax.fill_between(steps, 0, draw_max, color=COLORS["red"], alpha=0.08, + label="Maximum seed drawdown") + ax.fill_between(steps, draw_q25, draw_q75, color=COLORS["red"], alpha=0.22, + label="Interquartile range") + ax.plot(steps, draw_med, color=COLORS["red"], lw=1.6, + label="Median drawdown") + ax.set_xlabel("Training iteration") + ax.set_ylabel("Incumbent − current reward") + ax.set_title("Aggressive exploration causes temporary performance drawdown") + ax.set_ylim(bottom=0) + ax.grid(True) + ax.legend(frameon=False, ncol=3, loc="upper left") + panel_label(ax, "b") + fig.suptitle("Policy discovery and exploratory volatility are distinct training outcomes", + fontsize=12) + fig.text(0.5, 0.005, + "Drawdown measures the loss of the current exploratory policy relative to the best checkpoint already retained.", + ha="center", fontsize=8, color=COLORS["gray"]) + fig.tight_layout(rect=(0, 0.035, 1, 0.95)) + save_figure(fig, out_dir, "fig02_re100_discovery_drawdown") + + +def plot_retention(runs: Sequence[RunData], out_dir: Path) -> None: + base = baseline_runs(runs) + summaries = [run.summary() for run in base] + seeds = [str(item["seed"]) for item in summaries] + best = np.array([item["best_reward"] for item in summaries]) + stable = np.array([item["last50_median"] for item in summaries]) + final = np.array([item["final_reward"] for item in summaries]) + retention = stable / best + + fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.5), gridspec_kw={"width_ratios": [1.2, 1]}) + ax = axes[0] + x = np.arange(len(seeds)) + for idx in range(len(seeds)): + ax.plot([x[idx]] * 3, [best[idx], stable[idx], final[idx]], color="#B8BEC4", lw=1.2, zorder=1) + ax.scatter(x, best, color=COLORS["green"], s=40, label="Best checkpoint", zorder=3) + ax.scatter(x, stable, color=COLORS["blue"], s=40, marker="s", label="Last-50 median", zorder=3) + ax.scatter(x, final, color=COLORS["red"], s=42, marker="^", label="Iteration 500", zorder=3) + ax.set_xticks(x, seeds) + ax.set_xlabel("Random seed") + ax.set_ylabel("Evaluation reward") + ax.set_ylim(0, 1.02) + ax.set_title("Attained, retained, and final performance") + ax.grid(True, axis="y") + ax.legend(frameon=False, loc="lower left") + panel_label(ax, "a") + + ax = axes[1] + bars = ax.bar(x, retention, width=0.62, color=COLORS["blue"], alpha=0.82) + ax.axhline(1.0, color=COLORS["black"], lw=0.8, ls="--") + for bar, value in zip(bars, retention): + ax.text(bar.get_x() + bar.get_width() / 2, value + 0.025, f"{value:.2f}", + ha="center", va="bottom", fontsize=8) + ax.set_xticks(x, seeds) + ax.set_xlabel("Random seed") + ax.set_ylabel("Retention ratio") + ax.set_ylim(0, 1.08) + ax.set_title("Last-50 median / best reward") + ax.grid(True, axis="y") + panel_label(ax, "b") + fig.suptitle("High-reward policies are repeatedly found but not always preserved", fontsize=12) + fig.tight_layout(rect=(0, 0, 1, 0.94)) + save_figure(fig, out_dir, "fig02_re100_policy_retention") + + +def plot_components(runs: Sequence[RunData], out_dir: Path, window: int) -> None: + base = baseline_runs(runs) + steps = base[0].steps + labels = [("reward", "Total reward"), ("r_cd", r"Drag component $r_{CD}$"), + ("r_cl", r"Lift component $r_{CL}$"), ("r_sim", r"DTW component $r_{sim}$")] + fig, axes = plt.subplots(2, 2, figsize=(7.2, 5.8), sharex=True, sharey=True) + for letter, ax, (metric, title) in zip("abcd", axes.flat, labels): + matrix = np.vstack([ + run.metrics[metric][incumbent_indices(run.reward)] for run in base + ]) + median = np.median(matrix, axis=0) + q25, q75 = np.percentile(matrix, [25, 75], axis=0) + minimum, maximum = matrix.min(axis=0), matrix.max(axis=0) + ax.fill_between(steps, minimum, maximum, color=COLORS["purple"], alpha=0.09, + step="post") + ax.fill_between(steps, q25, q75, color=COLORS["purple"], alpha=0.23, + step="post") + ax.step(steps, median, where="post", color=COLORS["purple"], lw=1.8) + ax.set_title(title) + ax.set_ylim(0, 1.02) + ax.grid(True) + panel_label(ax, letter) + for ax in axes[-1, :]: + ax.set_xlabel("Training iteration") + for ax in axes[:, 0]: + ax.set_ylabel("Component value") + legend = [ + Line2D([0], [0], color=COLORS["purple"], lw=1.8, label="Median incumbent policy"), + Line2D([0], [0], color=COLORS["purple"], lw=7, alpha=0.23, label="Interquartile range"), + Line2D([0], [0], color=COLORS["purple"], lw=7, alpha=0.09, label="Seed range"), + ] + fig.legend(handles=legend, frameon=False, ncol=3, loc="lower center", bbox_to_anchor=(0.5, 0.005)) + fig.suptitle("Reward composition of the best policy discovered so far", fontsize=12) + fig.tight_layout(rect=(0, 0.06, 1, 0.95)) + save_figure(fig, out_dir, "fig03_re100_reward_components") + + +def select_one(runs: Sequence[RunData], case: str) -> RunData: + matches = [run for run in runs if run.case == case] + if len(matches) != 1: + raise ValueError(f"Expected one scratch run for {case}, found {len(matches)}") + return matches[0] + + +def plot_case_overview(runs: Sequence[RunData], out_dir: Path, window: int) -> None: + base = baseline_runs(runs) + base_steps = base[0].steps + base_envelopes = np.vstack([running_best(run.reward) for run in base]) + base_median = np.median(base_envelopes, axis=0) + base_q25, base_q75 = np.percentile(base_envelopes, [25, 75], axis=0) + + groups = [ + ("Cross-Reynolds-number scratch training", + [("Re60", "kar_re60"), ("Re100 (5-seed median)", None), + ("Re200", "kar_re200"), ("Re400", "kar_re400")]), + ("Variable disturbance-cylinder size", + [(r"$d/L_0=0.75$", "kar_d075"), (r"$d/L_0=1.0$ (baseline)", None), + (r"$d/L_0=1.5$", "kar_d15"), (r"$d/L_0=2.0$", "kar_d2")]), + ("Hydrodynamic illusion target size", + [(r"$D_t/L_0=0.75$", "ill_075L"), (r"$D_t/L_0=1.0$", "ill_1L"), + (r"$D_t/L_0=1.5$", "ill_15L"), (r"$D_t/L_0=2.0$", "ill_2L")]), + ] + palette = [COLORS["blue"], COLORS["orange"], COLORS["green"], COLORS["red"]] + fig, axes = plt.subplots(1, 3, figsize=(11.3, 3.6), sharex=True, sharey=True) + for letter, ax, (title, entries) in zip("abc", axes, groups): + for (label, case), color in zip(entries, palette): + if case is None: + ax.fill_between(base_steps, base_q25, base_q75, color=color, alpha=0.13, + step="post") + ax.step(base_steps, base_median, where="post", color=color, lw=1.8, + label=label) + else: + run = select_one(runs, case) + ax.plot(run.steps, run.reward, color=color, alpha=0.10, lw=0.45) + ax.step(run.steps, running_best(run.reward), where="post", color=color, + lw=1.7, label=label) + best = int(np.argmax(run.reward)) + ax.scatter(run.steps[best], run.reward[best], color=color, s=18, + edgecolor="white", linewidth=0.4, zorder=3) + ax.set_title(title) + ax.set_xlabel("Training iteration") + ax.set_ylim(0, 1.02) + ax.grid(True) + ax.legend(frameon=False, loc="lower right") + panel_label(ax, letter) + axes[0].set_ylabel("Evaluation reward") + fig.suptitle("Best-so-far policy discovery across the expanded control matrix", fontsize=12) + fig.text(0.5, 0.005, + "Stair-step curves are incumbent best checkpoints; faint lines show exploratory evaluations. Re100 reports five-seed median and IQR.", + ha="center", fontsize=8, color=COLORS["gray"]) + fig.tight_layout(rect=(0, 0.04, 1, 0.94)) + save_figure(fig, out_dir, "fig04_scratch_case_overview") + + +def plot_case_stability(runs: Sequence[RunData], out_dir: Path) -> None: + ordered_cases = [ + "kar_re60", "kar_re100", "kar_re200", "kar_re400", + "kar_d075", "kar_d15", "kar_d2", + "ill_075L", "ill_1L", "ill_15L", "ill_2L", + ] + labels = ["Re60", "Re100", "Re200", "Re400", "d0.75", "d1.5", "d2.0", + "Ill 0.75", "Ill 1.0", "Ill 1.5", "Ill 2.0"] + values = [] + for case in ordered_cases: + case_runs = [run for run in runs if run.case == case] + per_run = [run.summary() for run in case_runs] + values.append({ + "best": float(np.median([item["best_reward"] for item in per_run])), + "stable": float(np.median([item["last50_median"] for item in per_run])), + "final": float(np.median([item["final_reward"] for item in per_run])), + "best_min": float(np.min([item["best_reward"] for item in per_run])), + "best_max": float(np.max([item["best_reward"] for item in per_run])), + }) + x = np.arange(len(labels)) + width = 0.24 + fig, ax = plt.subplots(figsize=(10.2, 4.1)) + ax.bar(x - width, [v["best"] for v in values], width, color=COLORS["green"], label="Best checkpoint") + ax.bar(x, [v["stable"] for v in values], width, color=COLORS["blue"], label="Last-50 median") + ax.bar(x + width, [v["final"] for v in values], width, color=COLORS["red"], label="Iteration 500") + re100_idx = ordered_cases.index("kar_re100") + low = values[re100_idx]["best_min"] + high = values[re100_idx]["best_max"] + center = values[re100_idx]["best"] + ax.errorbar(re100_idx - width, center, yerr=[[center - low], [high - center]], fmt="none", + color=COLORS["black"], capsize=3, lw=1.0, label="Re100 seed range") + ax.axvline(3.5, color="#BBC1C6", lw=0.8) + ax.axvline(6.5, color="#BBC1C6", lw=0.8) + ax.text(1.5, 1.015, "Cross-Re", ha="center", va="bottom", fontsize=8) + ax.text(5.0, 1.015, "Variable disturbance", ha="center", va="bottom", fontsize=8) + ax.text(8.5, 1.015, "Illusion", ha="center", va="bottom", fontsize=8) + ax.set_xticks(x, labels, rotation=32, ha="right") + ax.set_ylabel("Evaluation reward") + ax.set_ylim(0, 1.08) + ax.set_title("Attained performance and late-training stability across scratch cases") + ax.grid(True, axis="y") + ax.legend(frameon=False, ncol=4, loc="lower center") + fig.text(0.5, 0.005, + "Re100 bars are medians across five seeds; all other cases contain one seed and do not represent seed uncertainty.", + ha="center", fontsize=8, color=COLORS["gray"]) + fig.tight_layout(rect=(0, 0.04, 1, 1)) + save_figure(fig, out_dir, "fig05_case_performance_stability") + + +def write_summaries(runs: Sequence[RunData], out_dir: Path) -> None: + rows = [run.summary() for run in runs] + fields = list(rows[0]) + with (out_dir / "scratch_training_summary.csv").open("w", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=fields) + writer.writeheader() + writer.writerows(rows) + base_rows = [row for row in rows if row["case"] == "kar_re100"] + report = { + "scope": "Scratch-only DRL training; transfer runs excluded", + "run_count": len(rows), + "case_count": len(set(row["case"] for row in rows)), + "re100_multiseed": { + "seed_count": len(base_rows), + "best_reward_mean": float(np.mean([row["best_reward"] for row in base_rows])), + "best_reward_std": float(np.std([row["best_reward"] for row in base_rows])), + "best_reward_min": float(np.min([row["best_reward"] for row in base_rows])), + "best_reward_max": float(np.max([row["best_reward"] for row in base_rows])), + "retention_ratio_mean": float(np.mean([row["retention_ratio"] for row in base_rows])), + }, + "runs": rows, + } + with (out_dir / "scratch_training_summary.json").open("w") as handle: + json.dump(report, handle, indent=2) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + script_dir = Path(__file__).resolve().parent + parser.add_argument("--output-root", type=Path, default=script_dir / "output") + parser.add_argument("--figure-dir", type=Path, default=script_dir / "results" / "figures") + parser.add_argument("--window", type=int, default=20, help="Moving-average window in episodes") + args = parser.parse_args() + args.figure_dir.mkdir(parents=True, exist_ok=True) + configure_style() + runs = discover_runs(args.output_root) + write_summaries(runs, args.figure_dir) + plot_multiseed(runs, args.figure_dir, args.window) + plot_exploration_drawdown(runs, args.figure_dir) + plot_retention(runs, args.figure_dir) + plot_components(runs, args.figure_dir, args.window) + plot_case_overview(runs, args.figure_dir, args.window) + plot_case_stability(runs, args.figure_dir) + print(f"Generated 6 figure sets from {len(runs)} scratch runs in {args.figure_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/drl_pinball/train/results/figures/fig01_re100_multiseed_learning.pdf b/src/drl_pinball/train/results/figures/fig01_re100_multiseed_learning.pdf new file mode 100644 index 0000000..547ca4f Binary files /dev/null and b/src/drl_pinball/train/results/figures/fig01_re100_multiseed_learning.pdf differ diff --git a/src/drl_pinball/train/results/figures/fig01_re100_multiseed_learning.png b/src/drl_pinball/train/results/figures/fig01_re100_multiseed_learning.png new file mode 100644 index 0000000..196c8ca Binary files /dev/null and b/src/drl_pinball/train/results/figures/fig01_re100_multiseed_learning.png differ diff --git a/src/drl_pinball/train/results/figures/fig02_re100_discovery_drawdown.pdf b/src/drl_pinball/train/results/figures/fig02_re100_discovery_drawdown.pdf new file mode 100644 index 0000000..62e1667 Binary files /dev/null and b/src/drl_pinball/train/results/figures/fig02_re100_discovery_drawdown.pdf differ diff --git a/src/drl_pinball/train/results/figures/fig02_re100_discovery_drawdown.png b/src/drl_pinball/train/results/figures/fig02_re100_discovery_drawdown.png new file mode 100644 index 0000000..8ef2dd4 Binary files /dev/null and b/src/drl_pinball/train/results/figures/fig02_re100_discovery_drawdown.png differ diff --git a/src/drl_pinball/train/results/figures/fig02_re100_policy_retention.pdf b/src/drl_pinball/train/results/figures/fig02_re100_policy_retention.pdf new file mode 100644 index 0000000..16c1fe5 Binary files /dev/null and b/src/drl_pinball/train/results/figures/fig02_re100_policy_retention.pdf differ diff --git a/src/drl_pinball/train/results/figures/fig02_re100_policy_retention.png b/src/drl_pinball/train/results/figures/fig02_re100_policy_retention.png new file mode 100644 index 0000000..8719e63 Binary files /dev/null and b/src/drl_pinball/train/results/figures/fig02_re100_policy_retention.png differ diff --git a/src/drl_pinball/train/results/figures/fig03_re100_reward_components.pdf b/src/drl_pinball/train/results/figures/fig03_re100_reward_components.pdf new file mode 100644 index 0000000..6b6bc98 Binary files /dev/null and b/src/drl_pinball/train/results/figures/fig03_re100_reward_components.pdf differ diff --git a/src/drl_pinball/train/results/figures/fig03_re100_reward_components.png b/src/drl_pinball/train/results/figures/fig03_re100_reward_components.png new file mode 100644 index 0000000..3f00a30 Binary files /dev/null and b/src/drl_pinball/train/results/figures/fig03_re100_reward_components.png differ diff --git a/src/drl_pinball/train/results/figures/fig04_scratch_case_overview.pdf b/src/drl_pinball/train/results/figures/fig04_scratch_case_overview.pdf new file mode 100644 index 0000000..fcdc9cc Binary files /dev/null and b/src/drl_pinball/train/results/figures/fig04_scratch_case_overview.pdf differ diff --git a/src/drl_pinball/train/results/figures/fig04_scratch_case_overview.png b/src/drl_pinball/train/results/figures/fig04_scratch_case_overview.png new file mode 100644 index 0000000..a7f99a2 Binary files /dev/null and b/src/drl_pinball/train/results/figures/fig04_scratch_case_overview.png differ diff --git a/src/drl_pinball/train/results/figures/fig05_case_performance_stability.pdf b/src/drl_pinball/train/results/figures/fig05_case_performance_stability.pdf new file mode 100644 index 0000000..ff6f3aa Binary files /dev/null and b/src/drl_pinball/train/results/figures/fig05_case_performance_stability.pdf differ diff --git a/src/drl_pinball/train/results/figures/fig05_case_performance_stability.png b/src/drl_pinball/train/results/figures/fig05_case_performance_stability.png new file mode 100644 index 0000000..f3a0487 Binary files /dev/null and b/src/drl_pinball/train/results/figures/fig05_case_performance_stability.png differ diff --git a/src/drl_pinball/train/results/figures/scratch_training_summary.csv b/src/drl_pinball/train/results/figures/scratch_training_summary.csv new file mode 100644 index 0000000..c79ad51 --- /dev/null +++ b/src/drl_pinball/train/results/figures/scratch_training_summary.csv @@ -0,0 +1,16 @@ +run,case,seed,episodes,best_reward,best_episode,final_reward,last50_mean,last50_median,last50_std,retention_ratio,collapse_severity,best_r_cd,best_r_cl,best_r_sim +ill_075L_seed43,ill_075L,43,500,0.8949788808822632,468,0.3543687164783478,0.8100551897287369,0.8480762541294098,0.10734073457288355,0.9475935938213234,0.046902626752853394,0.9411876797676086,0.8466622829437256,0.8965597748756409 +ill_15L_seed43,ill_15L,43,500,0.6258453130722046,349,0.469502717256546,0.4775660252571106,0.4899969846010208,0.05301908372002394,0.7829362533621614,0.13584832847118378,0.8188472390174866,0.33807867765426636,0.6969188451766968 +ill_1L_seed43,ill_1L,43,500,0.7991066575050354,401,0.7299954891204834,0.688692057132721,0.7164521217346191,0.08471020983303178,0.8965663281689085,0.08265453577041626,0.9122732877731323,0.7774831652641296,0.7304493188858032 +ill_2L_seed43,ill_2L,43,500,0.6285770535469055,234,0.48302701115608215,0.5218911731243133,0.5301360189914703,0.043948755614976874,0.8433906646767383,0.09844103455543518,0.75786954164505,0.2639721930027008,0.8061436414718628 +kar_d075_seed44,kar_d075,44,500,0.9170221090316772,492,0.91397625207901,0.8614093574881554,0.8959560692310333,0.1356591382391172,0.9770277732748577,0.02106603980064392,0.9543845057487488,0.8789703249931335,0.9175391793251038 +kar_d15_seed45,kar_d15,45,500,0.9565292000770569,492,0.9455604553222656,0.9477025198936463,0.9537805616855621,0.023678353619417905,0.9971264459137543,0.002748638391494751,0.9910281896591187,0.9922819137573242,0.9038404822349548 +kar_d2_seed45,kar_d2,45,500,0.89548259973526,499,0.890360414981842,0.8822143721580505,0.882226288318634,0.007207887885272474,0.9851964611924955,0.013256311416625977,0.9673635363578796,0.9606836438179016,0.7926710844039917 +kar_re100_seed41,kar_re100,41,500,0.9266384840011597,471,0.7574263215065002,0.5689359574019909,0.6211617290973663,0.29057404072988435,0.6703387996743172,0.30547675490379333,0.9610787630081177,0.9644614458084106,0.8724410533905029 +kar_re100_seed42,kar_re100,42,500,0.9301960468292236,315,0.136348694562912,0.3415829308703542,0.18643790483474731,0.29739984435696604,0.2004286144520412,0.7437581419944763,0.9665245413780212,0.9648280143737793,0.8769757151603699 +kar_re100_seed43,kar_re100,43,500,0.9160065054893494,360,0.17807762324810028,0.2821507265418768,0.1489720195531845,0.26868901193779704,0.16263205409616674,0.7670344859361649,0.9737851023674011,0.9516407251358032,0.8459469079971313 +kar_re100_seed44,kar_re100,44,500,0.9221957921981812,465,0.13273084163665771,0.5908909816294908,0.7913762331008911,0.3363356303314897,0.8581434005619745,0.13081955909729004,0.9741253852844238,0.9621089100837708,0.8533138036727905 +kar_re100_seed45,kar_re100,45,500,0.9411612153053284,447,0.5848420262336731,0.5769316917657852,0.5998142063617706,0.2910784324098124,0.6373129242976517,0.34134700894355774,0.9773698449134827,0.9821967482566833,0.8832281231880188 +kar_re200_seed43,kar_re200,43,500,0.545883059501648,114,0.30112120509147644,0.3234307673573494,0.32098662853240967,0.06260119542379697,0.5880135368652902,0.22489643096923828,0.6673523783683777,0.2809300422668457,0.6640539169311523 +kar_re400_seed43,kar_re400,43,500,0.5418410301208496,87,0.24795560538768768,0.27186549693346024,0.27098220586776733,0.050212837097326816,0.5001138540714216,0.2708588242530823,0.7141196727752686,0.42046111822128296,0.5091568827629089 +kar_re60_seed43,kar_re60,43,500,0.6949201822280884,390,0.6503804922103882,0.42600065663456915,0.45982564985752106,0.16207247818963108,0.6616956329908346,0.23509453237056732,0.9870983362197876,0.9809917211532593,0.2612328827381134 diff --git a/src/drl_pinball/train/results/figures/scratch_training_summary.json b/src/drl_pinball/train/results/figures/scratch_training_summary.json new file mode 100644 index 0000000..aa2b86d --- /dev/null +++ b/src/drl_pinball/train/results/figures/scratch_training_summary.json @@ -0,0 +1,270 @@ +{ + "scope": "Scratch-only DRL training; transfer runs excluded", + "run_count": 15, + "case_count": 11, + "re100_multiseed": { + "seed_count": 5, + "best_reward_mean": 0.9272396087646484, + "best_reward_std": 0.008420637748224958, + "best_reward_min": 0.9160065054893494, + "best_reward_max": 0.9411612153053284, + "retention_ratio_mean": 0.5057711586164303 + }, + "runs": [ + { + "run": "ill_075L_seed43", + "case": "ill_075L", + "seed": 43, + "episodes": 500, + "best_reward": 0.8949788808822632, + "best_episode": 468, + "final_reward": 0.3543687164783478, + "last50_mean": 0.8100551897287369, + "last50_median": 0.8480762541294098, + "last50_std": 0.10734073457288355, + "retention_ratio": 0.9475935938213234, + "collapse_severity": 0.046902626752853394, + "best_r_cd": 0.9411876797676086, + "best_r_cl": 0.8466622829437256, + "best_r_sim": 0.8965597748756409 + }, + { + "run": "ill_15L_seed43", + "case": "ill_15L", + "seed": 43, + "episodes": 500, + "best_reward": 0.6258453130722046, + "best_episode": 349, + "final_reward": 0.469502717256546, + "last50_mean": 0.4775660252571106, + "last50_median": 0.4899969846010208, + "last50_std": 0.05301908372002394, + "retention_ratio": 0.7829362533621614, + "collapse_severity": 0.13584832847118378, + "best_r_cd": 0.8188472390174866, + "best_r_cl": 0.33807867765426636, + "best_r_sim": 0.6969188451766968 + }, + { + "run": "ill_1L_seed43", + "case": "ill_1L", + "seed": 43, + "episodes": 500, + "best_reward": 0.7991066575050354, + "best_episode": 401, + "final_reward": 0.7299954891204834, + "last50_mean": 0.688692057132721, + "last50_median": 0.7164521217346191, + "last50_std": 0.08471020983303178, + "retention_ratio": 0.8965663281689085, + "collapse_severity": 0.08265453577041626, + "best_r_cd": 0.9122732877731323, + "best_r_cl": 0.7774831652641296, + "best_r_sim": 0.7304493188858032 + }, + { + "run": "ill_2L_seed43", + "case": "ill_2L", + "seed": 43, + "episodes": 500, + "best_reward": 0.6285770535469055, + "best_episode": 234, + "final_reward": 0.48302701115608215, + "last50_mean": 0.5218911731243133, + "last50_median": 0.5301360189914703, + "last50_std": 0.043948755614976874, + "retention_ratio": 0.8433906646767383, + "collapse_severity": 0.09844103455543518, + "best_r_cd": 0.75786954164505, + "best_r_cl": 0.2639721930027008, + "best_r_sim": 0.8061436414718628 + }, + { + "run": "kar_d075_seed44", + "case": "kar_d075", + "seed": 44, + "episodes": 500, + "best_reward": 0.9170221090316772, + "best_episode": 492, + "final_reward": 0.91397625207901, + "last50_mean": 0.8614093574881554, + "last50_median": 0.8959560692310333, + "last50_std": 0.1356591382391172, + "retention_ratio": 0.9770277732748577, + "collapse_severity": 0.02106603980064392, + "best_r_cd": 0.9543845057487488, + "best_r_cl": 0.8789703249931335, + "best_r_sim": 0.9175391793251038 + }, + { + "run": "kar_d15_seed45", + "case": "kar_d15", + "seed": 45, + "episodes": 500, + "best_reward": 0.9565292000770569, + "best_episode": 492, + "final_reward": 0.9455604553222656, + "last50_mean": 0.9477025198936463, + "last50_median": 0.9537805616855621, + "last50_std": 0.023678353619417905, + "retention_ratio": 0.9971264459137543, + "collapse_severity": 0.002748638391494751, + "best_r_cd": 0.9910281896591187, + "best_r_cl": 0.9922819137573242, + "best_r_sim": 0.9038404822349548 + }, + { + "run": "kar_d2_seed45", + "case": "kar_d2", + "seed": 45, + "episodes": 500, + "best_reward": 0.89548259973526, + "best_episode": 499, + "final_reward": 0.890360414981842, + "last50_mean": 0.8822143721580505, + "last50_median": 0.882226288318634, + "last50_std": 0.007207887885272474, + "retention_ratio": 0.9851964611924955, + "collapse_severity": 0.013256311416625977, + "best_r_cd": 0.9673635363578796, + "best_r_cl": 0.9606836438179016, + "best_r_sim": 0.7926710844039917 + }, + { + "run": "kar_re100_seed41", + "case": "kar_re100", + "seed": 41, + "episodes": 500, + "best_reward": 0.9266384840011597, + "best_episode": 471, + "final_reward": 0.7574263215065002, + "last50_mean": 0.5689359574019909, + "last50_median": 0.6211617290973663, + "last50_std": 0.29057404072988435, + "retention_ratio": 0.6703387996743172, + "collapse_severity": 0.30547675490379333, + "best_r_cd": 0.9610787630081177, + "best_r_cl": 0.9644614458084106, + "best_r_sim": 0.8724410533905029 + }, + { + "run": "kar_re100_seed42", + "case": "kar_re100", + "seed": 42, + "episodes": 500, + "best_reward": 0.9301960468292236, + "best_episode": 315, + "final_reward": 0.136348694562912, + "last50_mean": 0.3415829308703542, + "last50_median": 0.18643790483474731, + "last50_std": 0.29739984435696604, + "retention_ratio": 0.2004286144520412, + "collapse_severity": 0.7437581419944763, + "best_r_cd": 0.9665245413780212, + "best_r_cl": 0.9648280143737793, + "best_r_sim": 0.8769757151603699 + }, + { + "run": "kar_re100_seed43", + "case": "kar_re100", + "seed": 43, + "episodes": 500, + "best_reward": 0.9160065054893494, + "best_episode": 360, + "final_reward": 0.17807762324810028, + "last50_mean": 0.2821507265418768, + "last50_median": 0.1489720195531845, + "last50_std": 0.26868901193779704, + "retention_ratio": 0.16263205409616674, + "collapse_severity": 0.7670344859361649, + "best_r_cd": 0.9737851023674011, + "best_r_cl": 0.9516407251358032, + "best_r_sim": 0.8459469079971313 + }, + { + "run": "kar_re100_seed44", + "case": "kar_re100", + "seed": 44, + "episodes": 500, + "best_reward": 0.9221957921981812, + "best_episode": 465, + "final_reward": 0.13273084163665771, + "last50_mean": 0.5908909816294908, + "last50_median": 0.7913762331008911, + "last50_std": 0.3363356303314897, + "retention_ratio": 0.8581434005619745, + "collapse_severity": 0.13081955909729004, + "best_r_cd": 0.9741253852844238, + "best_r_cl": 0.9621089100837708, + "best_r_sim": 0.8533138036727905 + }, + { + "run": "kar_re100_seed45", + "case": "kar_re100", + "seed": 45, + "episodes": 500, + "best_reward": 0.9411612153053284, + "best_episode": 447, + "final_reward": 0.5848420262336731, + "last50_mean": 0.5769316917657852, + "last50_median": 0.5998142063617706, + "last50_std": 0.2910784324098124, + "retention_ratio": 0.6373129242976517, + "collapse_severity": 0.34134700894355774, + "best_r_cd": 0.9773698449134827, + "best_r_cl": 0.9821967482566833, + "best_r_sim": 0.8832281231880188 + }, + { + "run": "kar_re200_seed43", + "case": "kar_re200", + "seed": 43, + "episodes": 500, + "best_reward": 0.545883059501648, + "best_episode": 114, + "final_reward": 0.30112120509147644, + "last50_mean": 0.3234307673573494, + "last50_median": 0.32098662853240967, + "last50_std": 0.06260119542379697, + "retention_ratio": 0.5880135368652902, + "collapse_severity": 0.22489643096923828, + "best_r_cd": 0.6673523783683777, + "best_r_cl": 0.2809300422668457, + "best_r_sim": 0.6640539169311523 + }, + { + "run": "kar_re400_seed43", + "case": "kar_re400", + "seed": 43, + "episodes": 500, + "best_reward": 0.5418410301208496, + "best_episode": 87, + "final_reward": 0.24795560538768768, + "last50_mean": 0.27186549693346024, + "last50_median": 0.27098220586776733, + "last50_std": 0.050212837097326816, + "retention_ratio": 0.5001138540714216, + "collapse_severity": 0.2708588242530823, + "best_r_cd": 0.7141196727752686, + "best_r_cl": 0.42046111822128296, + "best_r_sim": 0.5091568827629089 + }, + { + "run": "kar_re60_seed43", + "case": "kar_re60", + "seed": 43, + "episodes": 500, + "best_reward": 0.6949201822280884, + "best_episode": 390, + "final_reward": 0.6503804922103882, + "last50_mean": 0.42600065663456915, + "last50_median": 0.45982564985752106, + "last50_std": 0.16207247818963108, + "retention_ratio": 0.6616956329908346, + "collapse_severity": 0.23509453237056732, + "best_r_cd": 0.9870983362197876, + "best_r_cl": 0.9809917211532593, + "best_r_sim": 0.2612328827381134 + } + ] +} \ No newline at end of file diff --git a/src/drl_pinball/train/results/latest/HOW_TO_READ.md b/src/drl_pinball/train/results/latest/HOW_TO_READ.md new file mode 100644 index 0000000..75f5940 --- /dev/null +++ b/src/drl_pinball/train/results/latest/HOW_TO_READ.md @@ -0,0 +1,29 @@ +# 最新 Train 保留证据图包阅读指南 + +## 最重要的证据谱系 +本图包使用 15 个历史 scratch run 和 11 个 canonical eval 目录中的**保留 artifacts**。它们早于当前 calibration-v2/native policy normalization 源码契约,不能被表述为“修复后已完整重训”。历史 PPO/VecNormalize 评估使用明确的 `legacy-policy-v1` compatibility path。 + +训练最佳值以每个 run 的 `meta.json` 为权威,并用 `train.log` 提取同一 training iteration 的分量(若存在)。TensorBoard 仅作为被校验的辅助流;若与 meta 不一致,不使用其科学数值。一个所谓“episode”实际是 PPO learn chunk 加确定性评估,本图包称为 **training iteration**,不是终止型 Gym episode。 + +## 证据边界与混杂 +只有 `kar_re100` 有 5 个种子;其余点均为 n=1 演示,不能估计种子不确定性。`kar_d075` 和 `kar_d2` 使用学习率 `1e-4`,多数 run 使用 `3e-4`,因此 variable-disturbance geometry 比较不是纯粹单因素消融。 + +## 两条不可混淆的证据链 +1. **训练期:** authoritative best reward/iteration 来自 `meta.json`;同 iteration 分量来自 `train.log`。仅 Re100 的 retention/final 使用经 meta 校验一致的 TensorBoard。 +2. **保留确定性评估:** `metrics.json` 对应 `all_seeds.json` 中 reward 最高 seed;signals/vorticity 是历史 policy compatibility-path artifacts,不等同于训练 reward,也不证明当前契约重训。 + +## 指标、动作与图像 +Reward、`r_CD`、`r_CL`、`r_sim`、DTW similarity 均越高越好;DTW 是时间信号相似度,不是物理场误差。Target/Controlled/Zero 用于涡量结构对照。动作顺序已由 active env 与 symmetry code 核实为 `[front, top, bottom]`。动作均值接近零可能是振荡抵消;饱和定义为 `|a| >= 0.95`,窗口为最后 180 步。 + +## 图号与实际文件 +- `fig00_graphical_abstract_reading_guide`:谱系与阅读规则。 +- 仅 Re100、且 TB 已经 meta 校验:`fig01_re100_multiseed_learning`、`fig02_re100_discovery_drawdown`、`fig02_re100_policy_retention`、`fig03_re100_reward_components`。 +- 全 case authoritative/meta 与 retained eval:`fig04_authoritative_training_vs_eval`、`fig05_authoritative_iteration_and_eval_gap`。本包没有 `fig06`。 +- retained eval:`fig07_eval_cross_scene_metrics`、`fig08_eval_reward_components`、`fig09_action_profile_saturation`、`fig10_representative_signal_diagnostics`、`fig11_evidence_scope`。 +- retained vorticity:`fig12_vorticity_cross_re`、`fig13_vorticity_disturbance`、`fig14_vorticity_illusion`。 + +## 复现 +```bash +conda run -n pycuda_3_10 python src/drl_pinball/train/plot_latest_results.py +``` +脚本要求 signals 和三类 vorticity PNG 全部存在,否则失败;不加载 fields.npz、不重跑 CFD。 diff --git a/src/drl_pinball/train/results/latest/figures/fig00_graphical_abstract_reading_guide.pdf b/src/drl_pinball/train/results/latest/figures/fig00_graphical_abstract_reading_guide.pdf new file mode 100644 index 0000000..0dce693 Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig00_graphical_abstract_reading_guide.pdf differ diff --git a/src/drl_pinball/train/results/latest/figures/fig00_graphical_abstract_reading_guide.png b/src/drl_pinball/train/results/latest/figures/fig00_graphical_abstract_reading_guide.png new file mode 100644 index 0000000..6e30a27 Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig00_graphical_abstract_reading_guide.png differ diff --git a/src/drl_pinball/train/results/latest/figures/fig01_re100_multiseed_learning.pdf b/src/drl_pinball/train/results/latest/figures/fig01_re100_multiseed_learning.pdf new file mode 100644 index 0000000..33cf3b8 Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig01_re100_multiseed_learning.pdf differ diff --git a/src/drl_pinball/train/results/latest/figures/fig01_re100_multiseed_learning.png b/src/drl_pinball/train/results/latest/figures/fig01_re100_multiseed_learning.png new file mode 100644 index 0000000..d548e15 Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig01_re100_multiseed_learning.png differ diff --git a/src/drl_pinball/train/results/latest/figures/fig02_re100_discovery_drawdown.pdf b/src/drl_pinball/train/results/latest/figures/fig02_re100_discovery_drawdown.pdf new file mode 100644 index 0000000..83c9ae6 Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig02_re100_discovery_drawdown.pdf differ diff --git a/src/drl_pinball/train/results/latest/figures/fig02_re100_discovery_drawdown.png b/src/drl_pinball/train/results/latest/figures/fig02_re100_discovery_drawdown.png new file mode 100644 index 0000000..baa6ac3 Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig02_re100_discovery_drawdown.png differ diff --git a/src/drl_pinball/train/results/latest/figures/fig02_re100_policy_retention.pdf b/src/drl_pinball/train/results/latest/figures/fig02_re100_policy_retention.pdf new file mode 100644 index 0000000..4fe3a4f Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig02_re100_policy_retention.pdf differ diff --git a/src/drl_pinball/train/results/latest/figures/fig02_re100_policy_retention.png b/src/drl_pinball/train/results/latest/figures/fig02_re100_policy_retention.png new file mode 100644 index 0000000..2bf1cdf Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig02_re100_policy_retention.png differ diff --git a/src/drl_pinball/train/results/latest/figures/fig03_re100_reward_components.pdf b/src/drl_pinball/train/results/latest/figures/fig03_re100_reward_components.pdf new file mode 100644 index 0000000..24909c0 Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig03_re100_reward_components.pdf differ diff --git a/src/drl_pinball/train/results/latest/figures/fig03_re100_reward_components.png b/src/drl_pinball/train/results/latest/figures/fig03_re100_reward_components.png new file mode 100644 index 0000000..19716f1 Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig03_re100_reward_components.png differ diff --git a/src/drl_pinball/train/results/latest/figures/fig04_authoritative_training_vs_eval.pdf b/src/drl_pinball/train/results/latest/figures/fig04_authoritative_training_vs_eval.pdf new file mode 100644 index 0000000..8f50c41 Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig04_authoritative_training_vs_eval.pdf differ diff --git a/src/drl_pinball/train/results/latest/figures/fig04_authoritative_training_vs_eval.png b/src/drl_pinball/train/results/latest/figures/fig04_authoritative_training_vs_eval.png new file mode 100644 index 0000000..2fb9629 Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig04_authoritative_training_vs_eval.png differ diff --git a/src/drl_pinball/train/results/latest/figures/fig05_authoritative_iteration_and_eval_gap.pdf b/src/drl_pinball/train/results/latest/figures/fig05_authoritative_iteration_and_eval_gap.pdf new file mode 100644 index 0000000..d7f2c9f Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig05_authoritative_iteration_and_eval_gap.pdf differ diff --git a/src/drl_pinball/train/results/latest/figures/fig05_authoritative_iteration_and_eval_gap.png b/src/drl_pinball/train/results/latest/figures/fig05_authoritative_iteration_and_eval_gap.png new file mode 100644 index 0000000..4f37fde Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig05_authoritative_iteration_and_eval_gap.png differ diff --git a/src/drl_pinball/train/results/latest/figures/fig07_eval_cross_scene_metrics.pdf b/src/drl_pinball/train/results/latest/figures/fig07_eval_cross_scene_metrics.pdf new file mode 100644 index 0000000..a400b3e Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig07_eval_cross_scene_metrics.pdf differ diff --git a/src/drl_pinball/train/results/latest/figures/fig07_eval_cross_scene_metrics.png b/src/drl_pinball/train/results/latest/figures/fig07_eval_cross_scene_metrics.png new file mode 100644 index 0000000..e7c4124 Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig07_eval_cross_scene_metrics.png differ diff --git a/src/drl_pinball/train/results/latest/figures/fig08_eval_reward_components.pdf b/src/drl_pinball/train/results/latest/figures/fig08_eval_reward_components.pdf new file mode 100644 index 0000000..209ea80 Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig08_eval_reward_components.pdf differ diff --git a/src/drl_pinball/train/results/latest/figures/fig08_eval_reward_components.png b/src/drl_pinball/train/results/latest/figures/fig08_eval_reward_components.png new file mode 100644 index 0000000..db2aa6e Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig08_eval_reward_components.png differ diff --git a/src/drl_pinball/train/results/latest/figures/fig09_action_profile_saturation.pdf b/src/drl_pinball/train/results/latest/figures/fig09_action_profile_saturation.pdf new file mode 100644 index 0000000..76eb037 Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig09_action_profile_saturation.pdf differ diff --git a/src/drl_pinball/train/results/latest/figures/fig09_action_profile_saturation.png b/src/drl_pinball/train/results/latest/figures/fig09_action_profile_saturation.png new file mode 100644 index 0000000..703c422 Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig09_action_profile_saturation.png differ diff --git a/src/drl_pinball/train/results/latest/figures/fig10_representative_signal_diagnostics.pdf b/src/drl_pinball/train/results/latest/figures/fig10_representative_signal_diagnostics.pdf new file mode 100644 index 0000000..fe4abe9 Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig10_representative_signal_diagnostics.pdf differ diff --git a/src/drl_pinball/train/results/latest/figures/fig10_representative_signal_diagnostics.png b/src/drl_pinball/train/results/latest/figures/fig10_representative_signal_diagnostics.png new file mode 100644 index 0000000..43b9c9f Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig10_representative_signal_diagnostics.png differ diff --git a/src/drl_pinball/train/results/latest/figures/fig11_evidence_scope.pdf b/src/drl_pinball/train/results/latest/figures/fig11_evidence_scope.pdf new file mode 100644 index 0000000..5e148a6 Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig11_evidence_scope.pdf differ diff --git a/src/drl_pinball/train/results/latest/figures/fig11_evidence_scope.png b/src/drl_pinball/train/results/latest/figures/fig11_evidence_scope.png new file mode 100644 index 0000000..dc705b5 Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig11_evidence_scope.png differ diff --git a/src/drl_pinball/train/results/latest/figures/fig12_vorticity_cross_re.png b/src/drl_pinball/train/results/latest/figures/fig12_vorticity_cross_re.png new file mode 100644 index 0000000..87ca6ed Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig12_vorticity_cross_re.png differ diff --git a/src/drl_pinball/train/results/latest/figures/fig13_vorticity_disturbance.png b/src/drl_pinball/train/results/latest/figures/fig13_vorticity_disturbance.png new file mode 100644 index 0000000..4f56c8c Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig13_vorticity_disturbance.png differ diff --git a/src/drl_pinball/train/results/latest/figures/fig14_vorticity_illusion.png b/src/drl_pinball/train/results/latest/figures/fig14_vorticity_illusion.png new file mode 100644 index 0000000..5963afc Binary files /dev/null and b/src/drl_pinball/train/results/latest/figures/fig14_vorticity_illusion.png differ diff --git a/src/drl_pinball/train/results/latest/tables/latest_eval_summary.csv b/src/drl_pinball/train/results/latest/tables/latest_eval_summary.csv new file mode 100644 index 0000000..f9e11a5 --- /dev/null +++ b/src/drl_pinball/train/results/latest/tables/latest_eval_summary.csv @@ -0,0 +1,12 @@ +case_id,label,group,selected_seed,seed_count,evaluation_scope,eval_reward_mean,eval_r_cd_mean,eval_r_cl_mean,eval_r_sim_mean,dtw_similarity,artifact_lineage,policy_normalization_contract,not_retraining_evidence,action_front_mean,action_front_rms,action_front_saturation_fraction,action_top_mean,action_top_rms,action_top_saturation_fraction,action_bottom_mean,action_bottom_rms,action_bottom_saturation_fraction,action_any_saturation_fraction +kar_re100,Re100,Cross-Re,45,5,multi-seed selection,0.931258084376653,0.9817873881798175,0.9806409231822657,0.856323978689029,0.9184580909003668,retained canonical historical-policy evaluation artifact,legacy-policy-v1 compatibility path,True,0.015262158587574959,0.14887863397598267,0.0,-0.23983442783355713,0.36826059222221375,0.0,0.2262122631072998,0.337697297334671,0.0,0.0 +kar_d075,d/L₀=0.75,Variable disturbance,44,1,single-seed demonstration,0.949619029296769,0.9662661961057779,0.974395176574359,0.9185515453139159,0.9107358065936991,retained canonical historical-policy evaluation artifact,legacy-policy-v1 compatibility path,True,-0.0509461984038353,0.0695478767156601,0.0,-0.22998987138271332,0.275506854057312,0.0,0.2698838412761688,0.35259807109832764,0.0,0.0 +kar_d15,d/L₀=1.5,Variable disturbance,45,1,single-seed demonstration,0.9576558629671733,0.9936834316737825,0.9968578444047387,0.9012337017581697,0.8923897908328682,retained canonical historical-policy evaluation artifact,legacy-policy-v1 compatibility path,True,-0.005895668175071478,0.10076317936182022,0.0,-0.24864523112773895,0.3381381332874298,0.0,0.2871551215648651,0.39643362164497375,0.0,0.0 +kar_d2,d/L₀=2.0,Variable disturbance,45,1,single-seed demonstration,0.9094357927640279,0.9760473133689648,0.9887188996926131,0.8000148225505987,0.8164819106857604,retained canonical historical-policy evaluation artifact,legacy-policy-v1 compatibility path,True,-0.001523345708847046,0.12317975610494614,0.0,-0.24988043308258057,0.3632534146308899,0.0,0.2640746533870697,0.3867751657962799,0.0,0.0 +ill_075L,Dₜ/L₀=0.75,Illusion,43,1,single-seed demonstration,0.8783123321003384,0.9359052653783863,0.8185511179968449,0.8799385423045856,0.8801878272221011,retained canonical historical-policy evaluation artifact,legacy-policy-v1 compatibility path,True,0.005702689755707979,0.8444677591323853,0.5388888888888889,-0.08058005571365356,0.8399393558502197,0.5444444444444444,0.0787195935845375,0.824846625328064,0.5055555555555555,0.8222222222222222 +ill_1L,Dₜ/L₀=1.0,Illusion,43,1,single-seed demonstration,0.7668705566061867,0.9143604461825459,0.7060577991689442,0.7018627106920268,0.8930617809617113,retained canonical historical-policy evaluation artifact,legacy-policy-v1 compatibility path,True,0.020097287371754646,0.9425567984580994,0.7888888888888889,-0.06362324208021164,0.8370528817176819,0.5055555555555555,0.04176275059580803,0.9072447419166565,0.7333333333333333,0.9611111111111111 +ill_15L,Dₜ/L₀=1.5,Illusion,43,1,single-seed demonstration,0.5962481594747967,0.7876442489508029,0.3490862899134269,0.6380724948213564,0.8774998497227487,retained canonical historical-policy evaluation artifact,legacy-policy-v1 compatibility path,True,0.0022057273890823126,0.7828248739242554,0.45,0.010405314154922962,0.8173584938049316,0.5111111111111111,-0.03220375254750252,0.8242339491844177,0.5166666666666667,0.8444444444444444 +ill_2L,Dₜ/L₀=2.0,Illusion,43,1,single-seed demonstration,0.5795887307988272,0.7281109763452592,0.17397245273139753,0.7832477669419142,0.8699486601651488,retained canonical historical-policy evaluation artifact,legacy-policy-v1 compatibility path,True,-0.08093838393688202,0.827529788017273,0.5444444444444444,0.09372266381978989,0.8684227466583252,0.6222222222222222,-0.11212200671434402,0.7661906480789185,0.4,0.9 +kar_re60,Re60,Cross-Re,43,1,single-seed demonstration,0.6046428362528483,0.9357095534968107,0.7389455110103176,0.2556157946961166,0.3642869435977384,retained canonical historical-policy evaluation artifact,legacy-policy-v1 compatibility path,True,0.00611850805580616,0.1382889300584793,0.0,-0.22956350445747375,0.2710033655166626,0.0,0.21949893236160278,0.27047809958457947,0.0,0.0 +kar_re200,Re200,Cross-Re,43,1,single-seed demonstration,0.4985730968415737,0.49807168582387157,0.3149279015525174,0.6524996054470258,0.7117417724911091,retained canonical historical-policy evaluation artifact,legacy-policy-v1 compatibility path,True,-0.03642887622117996,0.3612419068813324,0.005555555555555556,-0.22376421093940735,0.40986403822898865,0.005555555555555556,0.24788175523281097,0.561007559299469,0.1,0.1111111111111111 +kar_re400,Re400,Cross-Re,43,1,single-seed demonstration,0.44625382820765175,0.6111649451760678,0.30585729164295866,0.4485978755444211,0.5646298803919224,retained canonical historical-policy evaluation artifact,legacy-policy-v1 compatibility path,True,-0.04013345390558243,0.37192121148109436,0.011111111111111112,-0.2058258056640625,0.4411083459854126,0.03888888888888889,0.2571697235107422,0.4469101130962372,0.05,0.08888888888888889 diff --git a/src/drl_pinball/train/results/latest/tables/latest_results_summary.json b/src/drl_pinball/train/results/latest/tables/latest_results_summary.json new file mode 100644 index 0000000..67452e1 --- /dev/null +++ b/src/drl_pinball/train/results/latest/tables/latest_results_summary.json @@ -0,0 +1,948 @@ +{ + "schema": "drl-pinball-latest-results-v2", + "scope": { + "canonical_cases": 11, + "scratch_runs": 15, + "transfer_excluded": true, + "eval_steps": 360, + "action_tail_steps": 180, + "saturation_threshold_abs": 0.95, + "action_layout": [ + "front", + "top", + "bottom" + ] + }, + "lineage": { + "training_authority": "run meta.json and train.log", + "tensorboard_role": "validated auxiliary stream; mismatch values not authoritative", + "evaluation_authority": "retained canonical artifacts evaluated through legacy-policy-v1 compatibility", + "current_contract_caveat": "artifacts predate and do not prove complete retraining under calibration-v2/native policy normalization" + }, + "confounders": [ + { + "cases": [ + "kar_d075", + "kar_d2" + ], + "learning_rate": 0.0001, + "comparison_warning": "variable-disturbance geometry sweep is not a pure one-factor ablation; most runs use 3e-4" + } + ], + "training": [ + { + "run": "ill_075L_seed43", + "case_id": "ill_075L", + "label": "D\u209c/L\u2080=0.75", + "family": "Illusion", + "seed": 43, + "training_iterations": 500, + "authoritative_best_reward": 0.8949789073732164, + "authoritative_best_iteration": 468, + "learning_rate": 0.0003, + "authoritative_source": "meta.json; same-iteration components from train.log when present", + "tensorboard_best_reward": 0.8949788808822632, + "tensorboard_best_iteration": 468, + "tensorboard_reward_delta": -2.6490953208835322e-08, + "tensorboard_iteration_delta": 0, + "tensorboard_consistent": true, + "tensorboard_tolerance_abs": 5e-05, + "tensorboard_status": "validated", + "best_components_from_log": { + "reward": 0.895, + "r_cd": 0.941, + "r_cl": 0.847, + "r_sim": 0.897 + }, + "reported_audit_disagreement": { + "reported_tensorboard_best_reward": 0.7154, + "reported_tensorboard_best_iteration": 470, + "authoritative_meta_best_reward": 0.8949789073732164, + "authoritative_meta_best_iteration": 468, + "resolution": "reported mismatched values are not used; current on-disk TensorBoard stream was independently re-read and agrees with meta within tolerance" + } + }, + { + "run": "ill_15L_seed43", + "case_id": "ill_15L", + "label": "D\u209c/L\u2080=1.5", + "family": "Illusion", + "seed": 43, + "training_iterations": 500, + "authoritative_best_reward": 0.6258453220129013, + "authoritative_best_iteration": 349, + "learning_rate": 0.0003, + "authoritative_source": "meta.json; same-iteration components from train.log when present", + "tensorboard_best_reward": 0.6258453130722046, + "tensorboard_best_iteration": 349, + "tensorboard_reward_delta": -8.940696738513054e-09, + "tensorboard_iteration_delta": 0, + "tensorboard_consistent": true, + "tensorboard_tolerance_abs": 5e-05, + "tensorboard_status": "validated", + "best_components_from_log": { + "reward": 0.6258, + "r_cd": 0.819, + "r_cl": 0.338, + "r_sim": 0.697 + }, + "reported_audit_disagreement": { + "reported_tensorboard_best_reward": 0.6865, + "reported_tensorboard_best_iteration": 491, + "authoritative_meta_best_reward": 0.6258453220129013, + "authoritative_meta_best_iteration": 349, + "resolution": "reported mismatched values are not used; current on-disk TensorBoard stream was independently re-read and agrees with meta within tolerance" + } + }, + { + "run": "ill_1L_seed43", + "case_id": "ill_1L", + "label": "D\u209c/L\u2080=1.0", + "family": "Illusion", + "seed": 43, + "training_iterations": 500, + "authoritative_best_reward": 0.7991066402859158, + "authoritative_best_iteration": 401, + "learning_rate": 0.0003, + "authoritative_source": "meta.json; same-iteration components from train.log when present", + "tensorboard_best_reward": 0.7991066575050354, + "tensorboard_best_iteration": 401, + "tensorboard_reward_delta": 1.721911957464073e-08, + "tensorboard_iteration_delta": 0, + "tensorboard_consistent": true, + "tensorboard_tolerance_abs": 5e-05, + "tensorboard_status": "validated", + "best_components_from_log": { + "reward": 0.7991, + "r_cd": 0.912, + "r_cl": 0.777, + "r_sim": 0.73 + }, + "reported_audit_disagreement": { + "reported_tensorboard_best_reward": 0.657, + "reported_tensorboard_best_iteration": 290, + "authoritative_meta_best_reward": 0.7991066402859158, + "authoritative_meta_best_iteration": 401, + "resolution": "reported mismatched values are not used; current on-disk TensorBoard stream was independently re-read and agrees with meta within tolerance" + } + }, + { + "run": "ill_2L_seed43", + "case_id": "ill_2L", + "label": "D\u209c/L\u2080=2.0", + "family": "Illusion", + "seed": 43, + "training_iterations": 500, + "authoritative_best_reward": 0.6285770369900597, + "authoritative_best_iteration": 234, + "learning_rate": 0.0003, + "authoritative_source": "meta.json; same-iteration components from train.log when present", + "tensorboard_best_reward": 0.6285770535469055, + "tensorboard_best_iteration": 234, + "tensorboard_reward_delta": 1.6556845783277652e-08, + "tensorboard_iteration_delta": 0, + "tensorboard_consistent": true, + "tensorboard_tolerance_abs": 5e-05, + "tensorboard_status": "validated", + "best_components_from_log": { + "reward": 0.6286, + "r_cd": 0.758, + "r_cl": 0.264, + "r_sim": 0.806 + }, + "reported_audit_disagreement": { + "reported_tensorboard_best_reward": 0.7882, + "reported_tensorboard_best_iteration": 484, + "authoritative_meta_best_reward": 0.6285770369900597, + "authoritative_meta_best_iteration": 234, + "resolution": "reported mismatched values are not used; current on-disk TensorBoard stream was independently re-read and agrees with meta within tolerance" + } + }, + { + "run": "kar_d075_seed44", + "case_id": "kar_d075", + "label": "d/L\u2080=0.75", + "family": "Variable disturbance", + "seed": 44, + "training_iterations": 500, + "authoritative_best_reward": 0.9170221057203081, + "authoritative_best_iteration": 492, + "learning_rate": 0.0001, + "authoritative_source": "meta.json; same-iteration components from train.log when present", + "tensorboard_best_reward": 0.9170221090316772, + "tensorboard_best_iteration": 492, + "tensorboard_reward_delta": 3.311369178859991e-09, + "tensorboard_iteration_delta": 0, + "tensorboard_consistent": true, + "tensorboard_tolerance_abs": 5e-05, + "tensorboard_status": "validated", + "best_components_from_log": { + "reward": 0.917, + "r_cd": 0.954, + "r_cl": 0.879, + "r_sim": 0.918 + }, + "reported_audit_disagreement": null + }, + { + "run": "kar_d15_seed45", + "case_id": "kar_d15", + "label": "d/L\u2080=1.5", + "family": "Variable disturbance", + "seed": 45, + "training_iterations": 500, + "authoritative_best_reward": 0.9565292182895873, + "authoritative_best_iteration": 492, + "learning_rate": 0.0003, + "authoritative_source": "meta.json; same-iteration components from train.log when present", + "tensorboard_best_reward": 0.9565292000770569, + "tensorboard_best_iteration": 492, + "tensorboard_reward_delta": -1.8212530372707647e-08, + "tensorboard_iteration_delta": 0, + "tensorboard_consistent": true, + "tensorboard_tolerance_abs": 5e-05, + "tensorboard_status": "validated", + "best_components_from_log": { + "reward": 0.9565, + "r_cd": 0.991, + "r_cl": 0.992, + "r_sim": 0.904 + }, + "reported_audit_disagreement": null + }, + { + "run": "kar_d2_seed45", + "case_id": "kar_d2", + "label": "d/L\u2080=2.0", + "family": "Variable disturbance", + "seed": 45, + "training_iterations": 500, + "authoritative_best_reward": 0.8954825901322895, + "authoritative_best_iteration": 499, + "learning_rate": 0.0001, + "authoritative_source": "meta.json; same-iteration components from train.log when present", + "tensorboard_best_reward": 0.89548259973526, + "tensorboard_best_iteration": 499, + "tensorboard_reward_delta": 9.602970529876131e-09, + "tensorboard_iteration_delta": 0, + "tensorboard_consistent": true, + "tensorboard_tolerance_abs": 5e-05, + "tensorboard_status": "validated", + "best_components_from_log": { + "reward": 0.8955, + "r_cd": 0.967, + "r_cl": 0.961, + "r_sim": 0.793 + }, + "reported_audit_disagreement": null + }, + { + "run": "kar_re100_seed41", + "case_id": "kar_re100", + "label": "Re100", + "family": "Cross-Re", + "seed": 41, + "training_iterations": 500, + "authoritative_best_reward": 0.9266385002268686, + "authoritative_best_iteration": 471, + "learning_rate": 0.0003, + "authoritative_source": "meta.json; same-iteration components from train.log when present", + "tensorboard_best_reward": 0.9266384840011597, + "tensorboard_best_iteration": 471, + "tensorboard_reward_delta": -1.6225708887596113e-08, + "tensorboard_iteration_delta": 0, + "tensorboard_consistent": true, + "tensorboard_tolerance_abs": 5e-05, + "tensorboard_status": "validated", + "best_components_from_log": { + "reward": 0.9266, + "r_cd": 0.961, + "r_cl": 0.964, + "r_sim": 0.872 + }, + "reported_audit_disagreement": null, + "validated_tensorboard_retention": { + "last50_mean": 0.5689359574019909, + "last50_median": 0.6211617290973663, + "last50_std": 0.29057404072988435, + "final_reward": 0.7574263215065002, + "retention_ratio": 0.6703387996743172, + "source": "validated TensorBoard stream; 500 training iterations" + } + }, + { + "run": "kar_re100_seed42", + "case_id": "kar_re100", + "label": "Re100", + "family": "Cross-Re", + "seed": 42, + "training_iterations": 500, + "authoritative_best_reward": 0.9301960359017054, + "authoritative_best_iteration": 315, + "learning_rate": 0.0003, + "authoritative_source": "meta.json; same-iteration components from train.log when present", + "tensorboard_best_reward": 0.9301960468292236, + "tensorboard_best_iteration": 315, + "tensorboard_reward_delta": 1.0927518223624588e-08, + "tensorboard_iteration_delta": 0, + "tensorboard_consistent": true, + "tensorboard_tolerance_abs": 5e-05, + "tensorboard_status": "validated", + "best_components_from_log": { + "reward": 0.9302, + "r_cd": 0.967, + "r_cl": 0.965, + "r_sim": 0.877 + }, + "reported_audit_disagreement": null, + "validated_tensorboard_retention": { + "last50_mean": 0.3415829308703542, + "last50_median": 0.18643790483474731, + "last50_std": 0.29739984435696604, + "final_reward": 0.136348694562912, + "retention_ratio": 0.2004286144520412, + "source": "validated TensorBoard stream; 500 training iterations" + } + }, + { + "run": "kar_re100_seed43", + "case_id": "kar_re100", + "label": "Re100", + "family": "Cross-Re", + "seed": 43, + "training_iterations": 500, + "authoritative_best_reward": 0.916006507145034, + "authoritative_best_iteration": 360, + "learning_rate": 0.0003, + "authoritative_source": "meta.json; same-iteration components from train.log when present", + "tensorboard_best_reward": 0.9160065054893494, + "tensorboard_best_iteration": 360, + "tensorboard_reward_delta": -1.6556845894299954e-09, + "tensorboard_iteration_delta": 0, + "tensorboard_consistent": true, + "tensorboard_tolerance_abs": 5e-05, + "tensorboard_status": "validated", + "best_components_from_log": { + "reward": 0.916, + "r_cd": 0.974, + "r_cl": 0.952, + "r_sim": 0.846 + }, + "reported_audit_disagreement": null, + "validated_tensorboard_retention": { + "last50_mean": 0.2821507265418768, + "last50_median": 0.1489720195531845, + "last50_std": 0.26868901193779704, + "final_reward": 0.17807762324810028, + "retention_ratio": 0.16263205409616674, + "source": "validated TensorBoard stream; 500 training iterations" + } + }, + { + "run": "kar_re100_seed44", + "case_id": "kar_re100", + "label": "Re100", + "family": "Cross-Re", + "seed": 44, + "training_iterations": 500, + "authoritative_best_reward": 0.9221958074304792, + "authoritative_best_iteration": 465, + "learning_rate": 0.0003, + "authoritative_source": "meta.json; same-iteration components from train.log when present", + "tensorboard_best_reward": 0.9221957921981812, + "tensorboard_best_iteration": 465, + "tensorboard_reward_delta": -1.5232298089529195e-08, + "tensorboard_iteration_delta": 0, + "tensorboard_consistent": true, + "tensorboard_tolerance_abs": 5e-05, + "tensorboard_status": "validated", + "best_components_from_log": { + "reward": 0.9222, + "r_cd": 0.974, + "r_cl": 0.962, + "r_sim": 0.853 + }, + "reported_audit_disagreement": null, + "validated_tensorboard_retention": { + "last50_mean": 0.5908909816294908, + "last50_median": 0.7913762331008911, + "last50_std": 0.3363356303314897, + "final_reward": 0.13273084163665771, + "retention_ratio": 0.8581434005619745, + "source": "validated TensorBoard stream; 500 training iterations" + } + }, + { + "run": "kar_re100_seed45", + "case_id": "kar_re100", + "label": "Re100", + "family": "Cross-Re", + "seed": 45, + "training_iterations": 500, + "authoritative_best_reward": 0.9411612408028709, + "authoritative_best_iteration": 447, + "learning_rate": 0.0003, + "authoritative_source": "meta.json; same-iteration components from train.log when present", + "tensorboard_best_reward": 0.9411612153053284, + "tensorboard_best_iteration": 447, + "tensorboard_reward_delta": -2.5497542521790706e-08, + "tensorboard_iteration_delta": 0, + "tensorboard_consistent": true, + "tensorboard_tolerance_abs": 5e-05, + "tensorboard_status": "validated", + "best_components_from_log": { + "reward": 0.9412, + "r_cd": 0.977, + "r_cl": 0.982, + "r_sim": 0.883 + }, + "reported_audit_disagreement": null, + "validated_tensorboard_retention": { + "last50_mean": 0.5769316917657852, + "last50_median": 0.5998142063617706, + "last50_std": 0.2910784324098124, + "final_reward": 0.5848420262336731, + "retention_ratio": 0.6373129242976517, + "source": "validated TensorBoard stream; 500 training iterations" + } + }, + { + "run": "kar_re200_seed43", + "case_id": "kar_re200", + "label": "Re200", + "family": "Cross-Re", + "seed": 43, + "training_iterations": 500, + "authoritative_best_reward": 0.5458830540378888, + "authoritative_best_iteration": 114, + "learning_rate": 0.0003, + "authoritative_source": "meta.json; same-iteration components from train.log when present", + "tensorboard_best_reward": 0.545883059501648, + "tensorboard_best_iteration": 114, + "tensorboard_reward_delta": 5.463759111812294e-09, + "tensorboard_iteration_delta": 0, + "tensorboard_consistent": true, + "tensorboard_tolerance_abs": 5e-05, + "tensorboard_status": "validated", + "best_components_from_log": { + "reward": 0.5459, + "r_cd": 0.667, + "r_cl": 0.281, + "r_sim": 0.664 + }, + "reported_audit_disagreement": null + }, + { + "run": "kar_re400_seed43", + "case_id": "kar_re400", + "label": "Re400", + "family": "Cross-Re", + "seed": 43, + "training_iterations": 500, + "authoritative_best_reward": 0.5418410102526346, + "authoritative_best_iteration": 87, + "learning_rate": 0.0003, + "authoritative_source": "meta.json; same-iteration components from train.log when present", + "tensorboard_best_reward": 0.5418410301208496, + "tensorboard_best_iteration": 87, + "tensorboard_reward_delta": 1.9868214962137642e-08, + "tensorboard_iteration_delta": 0, + "tensorboard_consistent": true, + "tensorboard_tolerance_abs": 5e-05, + "tensorboard_status": "validated", + "best_components_from_log": { + "reward": 0.5418, + "r_cd": 0.714, + "r_cl": 0.42, + "r_sim": 0.509 + }, + "reported_audit_disagreement": null + }, + { + "run": "kar_re60_seed43", + "case_id": "kar_re60", + "label": "Re60", + "family": "Cross-Re", + "seed": 43, + "training_iterations": 500, + "authoritative_best_reward": 0.6949, + "authoritative_best_iteration": 390, + "learning_rate": 0.0003, + "authoritative_source": "meta.json; same-iteration components from train.log when present", + "tensorboard_best_reward": 0.6949201822280884, + "tensorboard_best_iteration": 390, + "tensorboard_reward_delta": 2.0182228088416743e-05, + "tensorboard_iteration_delta": 0, + "tensorboard_consistent": true, + "tensorboard_tolerance_abs": 5e-05, + "tensorboard_status": "validated", + "best_components_from_log": { + "reward": 0.6949, + "r_cd": 0.987, + "r_cl": 0.981, + "r_sim": 0.261 + }, + "reported_audit_disagreement": null + } + ], + "re100_validated_tensorboard_retention": [ + { + "run": "kar_re100_seed41", + "case_id": "kar_re100", + "label": "Re100", + "family": "Cross-Re", + "seed": 41, + "training_iterations": 500, + "authoritative_best_reward": 0.9266385002268686, + "authoritative_best_iteration": 471, + "learning_rate": 0.0003, + "authoritative_source": "meta.json; same-iteration components from train.log when present", + "tensorboard_best_reward": 0.9266384840011597, + "tensorboard_best_iteration": 471, + "tensorboard_reward_delta": -1.6225708887596113e-08, + "tensorboard_iteration_delta": 0, + "tensorboard_consistent": true, + "tensorboard_tolerance_abs": 5e-05, + "tensorboard_status": "validated", + "best_components_from_log": { + "reward": 0.9266, + "r_cd": 0.961, + "r_cl": 0.964, + "r_sim": 0.872 + }, + "reported_audit_disagreement": null, + "validated_tensorboard_retention": { + "last50_mean": 0.5689359574019909, + "last50_median": 0.6211617290973663, + "last50_std": 0.29057404072988435, + "final_reward": 0.7574263215065002, + "retention_ratio": 0.6703387996743172, + "source": "validated TensorBoard stream; 500 training iterations" + } + }, + { + "run": "kar_re100_seed42", + "case_id": "kar_re100", + "label": "Re100", + "family": "Cross-Re", + "seed": 42, + "training_iterations": 500, + "authoritative_best_reward": 0.9301960359017054, + "authoritative_best_iteration": 315, + "learning_rate": 0.0003, + "authoritative_source": "meta.json; same-iteration components from train.log when present", + "tensorboard_best_reward": 0.9301960468292236, + "tensorboard_best_iteration": 315, + "tensorboard_reward_delta": 1.0927518223624588e-08, + "tensorboard_iteration_delta": 0, + "tensorboard_consistent": true, + "tensorboard_tolerance_abs": 5e-05, + "tensorboard_status": "validated", + "best_components_from_log": { + "reward": 0.9302, + "r_cd": 0.967, + "r_cl": 0.965, + "r_sim": 0.877 + }, + "reported_audit_disagreement": null, + "validated_tensorboard_retention": { + "last50_mean": 0.3415829308703542, + "last50_median": 0.18643790483474731, + "last50_std": 0.29739984435696604, + "final_reward": 0.136348694562912, + "retention_ratio": 0.2004286144520412, + "source": "validated TensorBoard stream; 500 training iterations" + } + }, + { + "run": "kar_re100_seed43", + "case_id": "kar_re100", + "label": "Re100", + "family": "Cross-Re", + "seed": 43, + "training_iterations": 500, + "authoritative_best_reward": 0.916006507145034, + "authoritative_best_iteration": 360, + "learning_rate": 0.0003, + "authoritative_source": "meta.json; same-iteration components from train.log when present", + "tensorboard_best_reward": 0.9160065054893494, + "tensorboard_best_iteration": 360, + "tensorboard_reward_delta": -1.6556845894299954e-09, + "tensorboard_iteration_delta": 0, + "tensorboard_consistent": true, + "tensorboard_tolerance_abs": 5e-05, + "tensorboard_status": "validated", + "best_components_from_log": { + "reward": 0.916, + "r_cd": 0.974, + "r_cl": 0.952, + "r_sim": 0.846 + }, + "reported_audit_disagreement": null, + "validated_tensorboard_retention": { + "last50_mean": 0.2821507265418768, + "last50_median": 0.1489720195531845, + "last50_std": 0.26868901193779704, + "final_reward": 0.17807762324810028, + "retention_ratio": 0.16263205409616674, + "source": "validated TensorBoard stream; 500 training iterations" + } + }, + { + "run": "kar_re100_seed44", + "case_id": "kar_re100", + "label": "Re100", + "family": "Cross-Re", + "seed": 44, + "training_iterations": 500, + "authoritative_best_reward": 0.9221958074304792, + "authoritative_best_iteration": 465, + "learning_rate": 0.0003, + "authoritative_source": "meta.json; same-iteration components from train.log when present", + "tensorboard_best_reward": 0.9221957921981812, + "tensorboard_best_iteration": 465, + "tensorboard_reward_delta": -1.5232298089529195e-08, + "tensorboard_iteration_delta": 0, + "tensorboard_consistent": true, + "tensorboard_tolerance_abs": 5e-05, + "tensorboard_status": "validated", + "best_components_from_log": { + "reward": 0.9222, + "r_cd": 0.974, + "r_cl": 0.962, + "r_sim": 0.853 + }, + "reported_audit_disagreement": null, + "validated_tensorboard_retention": { + "last50_mean": 0.5908909816294908, + "last50_median": 0.7913762331008911, + "last50_std": 0.3363356303314897, + "final_reward": 0.13273084163665771, + "retention_ratio": 0.8581434005619745, + "source": "validated TensorBoard stream; 500 training iterations" + } + }, + { + "run": "kar_re100_seed45", + "case_id": "kar_re100", + "label": "Re100", + "family": "Cross-Re", + "seed": 45, + "training_iterations": 500, + "authoritative_best_reward": 0.9411612408028709, + "authoritative_best_iteration": 447, + "learning_rate": 0.0003, + "authoritative_source": "meta.json; same-iteration components from train.log when present", + "tensorboard_best_reward": 0.9411612153053284, + "tensorboard_best_iteration": 447, + "tensorboard_reward_delta": -2.5497542521790706e-08, + "tensorboard_iteration_delta": 0, + "tensorboard_consistent": true, + "tensorboard_tolerance_abs": 5e-05, + "tensorboard_status": "validated", + "best_components_from_log": { + "reward": 0.9412, + "r_cd": 0.977, + "r_cl": 0.982, + "r_sim": 0.883 + }, + "reported_audit_disagreement": null, + "validated_tensorboard_retention": { + "last50_mean": 0.5769316917657852, + "last50_median": 0.5998142063617706, + "last50_std": 0.2910784324098124, + "final_reward": 0.5848420262336731, + "retention_ratio": 0.6373129242976517, + "source": "validated TensorBoard stream; 500 training iterations" + } + } + ], + "evaluation": [ + { + "case_id": "kar_re100", + "label": "Re100", + "group": "Cross-Re", + "selected_seed": 45, + "seed_count": 5, + "evaluation_scope": "multi-seed selection", + "eval_reward_mean": 0.931258084376653, + "eval_r_cd_mean": 0.9817873881798175, + "eval_r_cl_mean": 0.9806409231822657, + "eval_r_sim_mean": 0.856323978689029, + "dtw_similarity": 0.9184580909003668, + "artifact_lineage": "retained canonical historical-policy evaluation artifact", + "policy_normalization_contract": "legacy-policy-v1 compatibility path", + "not_retraining_evidence": true, + "action_front_mean": 0.015262158587574959, + "action_front_rms": 0.14887863397598267, + "action_front_saturation_fraction": 0.0, + "action_top_mean": -0.23983442783355713, + "action_top_rms": 0.36826059222221375, + "action_top_saturation_fraction": 0.0, + "action_bottom_mean": 0.2262122631072998, + "action_bottom_rms": 0.337697297334671, + "action_bottom_saturation_fraction": 0.0, + "action_any_saturation_fraction": 0.0 + }, + { + "case_id": "kar_d075", + "label": "d/L\u2080=0.75", + "group": "Variable disturbance", + "selected_seed": 44, + "seed_count": 1, + "evaluation_scope": "single-seed demonstration", + "eval_reward_mean": 0.949619029296769, + "eval_r_cd_mean": 0.9662661961057779, + "eval_r_cl_mean": 0.974395176574359, + "eval_r_sim_mean": 0.9185515453139159, + "dtw_similarity": 0.9107358065936991, + "artifact_lineage": "retained canonical historical-policy evaluation artifact", + "policy_normalization_contract": "legacy-policy-v1 compatibility path", + "not_retraining_evidence": true, + "action_front_mean": -0.0509461984038353, + "action_front_rms": 0.0695478767156601, + "action_front_saturation_fraction": 0.0, + "action_top_mean": -0.22998987138271332, + "action_top_rms": 0.275506854057312, + "action_top_saturation_fraction": 0.0, + "action_bottom_mean": 0.2698838412761688, + "action_bottom_rms": 0.35259807109832764, + "action_bottom_saturation_fraction": 0.0, + "action_any_saturation_fraction": 0.0 + }, + { + "case_id": "kar_d15", + "label": "d/L\u2080=1.5", + "group": "Variable disturbance", + "selected_seed": 45, + "seed_count": 1, + "evaluation_scope": "single-seed demonstration", + "eval_reward_mean": 0.9576558629671733, + "eval_r_cd_mean": 0.9936834316737825, + "eval_r_cl_mean": 0.9968578444047387, + "eval_r_sim_mean": 0.9012337017581697, + "dtw_similarity": 0.8923897908328682, + "artifact_lineage": "retained canonical historical-policy evaluation artifact", + "policy_normalization_contract": "legacy-policy-v1 compatibility path", + "not_retraining_evidence": true, + "action_front_mean": -0.005895668175071478, + "action_front_rms": 0.10076317936182022, + "action_front_saturation_fraction": 0.0, + "action_top_mean": -0.24864523112773895, + "action_top_rms": 0.3381381332874298, + "action_top_saturation_fraction": 0.0, + "action_bottom_mean": 0.2871551215648651, + "action_bottom_rms": 0.39643362164497375, + "action_bottom_saturation_fraction": 0.0, + "action_any_saturation_fraction": 0.0 + }, + { + "case_id": "kar_d2", + "label": "d/L\u2080=2.0", + "group": "Variable disturbance", + "selected_seed": 45, + "seed_count": 1, + "evaluation_scope": "single-seed demonstration", + "eval_reward_mean": 0.9094357927640279, + "eval_r_cd_mean": 0.9760473133689648, + "eval_r_cl_mean": 0.9887188996926131, + "eval_r_sim_mean": 0.8000148225505987, + "dtw_similarity": 0.8164819106857604, + "artifact_lineage": "retained canonical historical-policy evaluation artifact", + "policy_normalization_contract": "legacy-policy-v1 compatibility path", + "not_retraining_evidence": true, + "action_front_mean": -0.001523345708847046, + "action_front_rms": 0.12317975610494614, + "action_front_saturation_fraction": 0.0, + "action_top_mean": -0.24988043308258057, + "action_top_rms": 0.3632534146308899, + "action_top_saturation_fraction": 0.0, + "action_bottom_mean": 0.2640746533870697, + "action_bottom_rms": 0.3867751657962799, + "action_bottom_saturation_fraction": 0.0, + "action_any_saturation_fraction": 0.0 + }, + { + "case_id": "ill_075L", + "label": "D\u209c/L\u2080=0.75", + "group": "Illusion", + "selected_seed": 43, + "seed_count": 1, + "evaluation_scope": "single-seed demonstration", + "eval_reward_mean": 0.8783123321003384, + "eval_r_cd_mean": 0.9359052653783863, + "eval_r_cl_mean": 0.8185511179968449, + "eval_r_sim_mean": 0.8799385423045856, + "dtw_similarity": 0.8801878272221011, + "artifact_lineage": "retained canonical historical-policy evaluation artifact", + "policy_normalization_contract": "legacy-policy-v1 compatibility path", + "not_retraining_evidence": true, + "action_front_mean": 0.005702689755707979, + "action_front_rms": 0.8444677591323853, + "action_front_saturation_fraction": 0.5388888888888889, + "action_top_mean": -0.08058005571365356, + "action_top_rms": 0.8399393558502197, + "action_top_saturation_fraction": 0.5444444444444444, + "action_bottom_mean": 0.0787195935845375, + "action_bottom_rms": 0.824846625328064, + "action_bottom_saturation_fraction": 0.5055555555555555, + "action_any_saturation_fraction": 0.8222222222222222 + }, + { + "case_id": "ill_1L", + "label": "D\u209c/L\u2080=1.0", + "group": "Illusion", + "selected_seed": 43, + "seed_count": 1, + "evaluation_scope": "single-seed demonstration", + "eval_reward_mean": 0.7668705566061867, + "eval_r_cd_mean": 0.9143604461825459, + "eval_r_cl_mean": 0.7060577991689442, + "eval_r_sim_mean": 0.7018627106920268, + "dtw_similarity": 0.8930617809617113, + "artifact_lineage": "retained canonical historical-policy evaluation artifact", + "policy_normalization_contract": "legacy-policy-v1 compatibility path", + "not_retraining_evidence": true, + "action_front_mean": 0.020097287371754646, + "action_front_rms": 0.9425567984580994, + "action_front_saturation_fraction": 0.7888888888888889, + "action_top_mean": -0.06362324208021164, + "action_top_rms": 0.8370528817176819, + "action_top_saturation_fraction": 0.5055555555555555, + "action_bottom_mean": 0.04176275059580803, + "action_bottom_rms": 0.9072447419166565, + "action_bottom_saturation_fraction": 0.7333333333333333, + "action_any_saturation_fraction": 0.9611111111111111 + }, + { + "case_id": "ill_15L", + "label": "D\u209c/L\u2080=1.5", + "group": "Illusion", + "selected_seed": 43, + "seed_count": 1, + "evaluation_scope": "single-seed demonstration", + "eval_reward_mean": 0.5962481594747967, + "eval_r_cd_mean": 0.7876442489508029, + "eval_r_cl_mean": 0.3490862899134269, + "eval_r_sim_mean": 0.6380724948213564, + "dtw_similarity": 0.8774998497227487, + "artifact_lineage": "retained canonical historical-policy evaluation artifact", + "policy_normalization_contract": "legacy-policy-v1 compatibility path", + "not_retraining_evidence": true, + "action_front_mean": 0.0022057273890823126, + "action_front_rms": 0.7828248739242554, + "action_front_saturation_fraction": 0.45, + "action_top_mean": 0.010405314154922962, + "action_top_rms": 0.8173584938049316, + "action_top_saturation_fraction": 0.5111111111111111, + "action_bottom_mean": -0.03220375254750252, + "action_bottom_rms": 0.8242339491844177, + "action_bottom_saturation_fraction": 0.5166666666666667, + "action_any_saturation_fraction": 0.8444444444444444 + }, + { + "case_id": "ill_2L", + "label": "D\u209c/L\u2080=2.0", + "group": "Illusion", + "selected_seed": 43, + "seed_count": 1, + "evaluation_scope": "single-seed demonstration", + "eval_reward_mean": 0.5795887307988272, + "eval_r_cd_mean": 0.7281109763452592, + "eval_r_cl_mean": 0.17397245273139753, + "eval_r_sim_mean": 0.7832477669419142, + "dtw_similarity": 0.8699486601651488, + "artifact_lineage": "retained canonical historical-policy evaluation artifact", + "policy_normalization_contract": "legacy-policy-v1 compatibility path", + "not_retraining_evidence": true, + "action_front_mean": -0.08093838393688202, + "action_front_rms": 0.827529788017273, + "action_front_saturation_fraction": 0.5444444444444444, + "action_top_mean": 0.09372266381978989, + "action_top_rms": 0.8684227466583252, + "action_top_saturation_fraction": 0.6222222222222222, + "action_bottom_mean": -0.11212200671434402, + "action_bottom_rms": 0.7661906480789185, + "action_bottom_saturation_fraction": 0.4, + "action_any_saturation_fraction": 0.9 + }, + { + "case_id": "kar_re60", + "label": "Re60", + "group": "Cross-Re", + "selected_seed": 43, + "seed_count": 1, + "evaluation_scope": "single-seed demonstration", + "eval_reward_mean": 0.6046428362528483, + "eval_r_cd_mean": 0.9357095534968107, + "eval_r_cl_mean": 0.7389455110103176, + "eval_r_sim_mean": 0.2556157946961166, + "dtw_similarity": 0.3642869435977384, + "artifact_lineage": "retained canonical historical-policy evaluation artifact", + "policy_normalization_contract": "legacy-policy-v1 compatibility path", + "not_retraining_evidence": true, + "action_front_mean": 0.00611850805580616, + "action_front_rms": 0.1382889300584793, + "action_front_saturation_fraction": 0.0, + "action_top_mean": -0.22956350445747375, + "action_top_rms": 0.2710033655166626, + "action_top_saturation_fraction": 0.0, + "action_bottom_mean": 0.21949893236160278, + "action_bottom_rms": 0.27047809958457947, + "action_bottom_saturation_fraction": 0.0, + "action_any_saturation_fraction": 0.0 + }, + { + "case_id": "kar_re200", + "label": "Re200", + "group": "Cross-Re", + "selected_seed": 43, + "seed_count": 1, + "evaluation_scope": "single-seed demonstration", + "eval_reward_mean": 0.4985730968415737, + "eval_r_cd_mean": 0.49807168582387157, + "eval_r_cl_mean": 0.3149279015525174, + "eval_r_sim_mean": 0.6524996054470258, + "dtw_similarity": 0.7117417724911091, + "artifact_lineage": "retained canonical historical-policy evaluation artifact", + "policy_normalization_contract": "legacy-policy-v1 compatibility path", + "not_retraining_evidence": true, + "action_front_mean": -0.03642887622117996, + "action_front_rms": 0.3612419068813324, + "action_front_saturation_fraction": 0.005555555555555556, + "action_top_mean": -0.22376421093940735, + "action_top_rms": 0.40986403822898865, + "action_top_saturation_fraction": 0.005555555555555556, + "action_bottom_mean": 0.24788175523281097, + "action_bottom_rms": 0.561007559299469, + "action_bottom_saturation_fraction": 0.1, + "action_any_saturation_fraction": 0.1111111111111111 + }, + { + "case_id": "kar_re400", + "label": "Re400", + "group": "Cross-Re", + "selected_seed": 43, + "seed_count": 1, + "evaluation_scope": "single-seed demonstration", + "eval_reward_mean": 0.44625382820765175, + "eval_r_cd_mean": 0.6111649451760678, + "eval_r_cl_mean": 0.30585729164295866, + "eval_r_sim_mean": 0.4485978755444211, + "dtw_similarity": 0.5646298803919224, + "artifact_lineage": "retained canonical historical-policy evaluation artifact", + "policy_normalization_contract": "legacy-policy-v1 compatibility path", + "not_retraining_evidence": true, + "action_front_mean": -0.04013345390558243, + "action_front_rms": 0.37192121148109436, + "action_front_saturation_fraction": 0.011111111111111112, + "action_top_mean": -0.2058258056640625, + "action_top_rms": 0.4411083459854126, + "action_top_saturation_fraction": 0.03888888888888889, + "action_bottom_mean": 0.2571697235107422, + "action_bottom_rms": 0.4469101130962372, + "action_bottom_saturation_fraction": 0.05, + "action_any_saturation_fraction": 0.08888888888888889 + } + ] +} diff --git a/src/drl_pinball/train/scripts/crossre_scratch.sh b/src/drl_pinball/train/scripts/crossre_scratch.sh index 5f2ffe4..63fe30b 100755 --- a/src/drl_pinball/train/scripts/crossre_scratch.sh +++ b/src/drl_pinball/train/scripts/crossre_scratch.sh @@ -4,7 +4,7 @@ # # 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/ +# Output: output/kar_{re60,re200,re400}_seed43/ # # Usage: # bash crossre_scratch.sh [--gpu 0] [--episodes 500] @@ -16,7 +16,8 @@ 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 +registry() { PYTHONPATH="$REPO_DIR/src" python3 -m drl_pinball.train.case_registry "$CASE" "$1"; } +GPU=0; EPISODES=500; DELAY=120; ONLY="" while [[ $# -gt 0 ]]; do case "$1" in @@ -29,20 +30,16 @@ 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]}" + CASE="kar_${re}" + CAL="$(registry calibration)" + CFG="$(registry config)" + TRAINER="$(registry trainer)" + SEED="$(registry seeds)" echo " [$(date '+%H:%M:%S')] Training $CASE..." conda run --no-capture-output -n "$CONDA_ENV" python -u \ - "$TRAIN_DIR/train_karman.py" \ + "$TRAINER" \ --case-name "$CASE" --device-id "$GPU" --seed "$SEED" \ --config "$CFG" --calibration "$CAL" \ --total-episodes "$EPISODES" --lr 0.0003 diff --git a/src/drl_pinball/train/scripts/crossre_transfer.sh b/src/drl_pinball/train/scripts/crossre_transfer.sh deleted file mode 100755 index 9874f88..0000000 --- a/src/drl_pinball/train/scripts/crossre_transfer.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/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 ===" diff --git a/src/drl_pinball/train/scripts/resume.sh b/src/drl_pinball/train/scripts/resume.sh index 78fe709..f102710 100755 --- a/src/drl_pinball/train/scripts/resume.sh +++ b/src/drl_pinball/train/scripts/resume.sh @@ -1,23 +1,11 @@ #!/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] -# ============================================================================= +# Resume a canonical V5 case from an episode checkpoint. 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 ;; @@ -28,32 +16,22 @@ while [[ $# -gt 0 ]]; do *) 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 - +registry() { PYTHONPATH="$REPO_DIR/src" python3 -m drl_pinball.train.case_registry "$CASE" "$1"; } +CFG="$(registry config)" || exit 1 +CAL="$(registry calibration)" || exit 1 +TRAINER="$(registry trainer)" || exit 1 +[[ -f "$CFG" ]] || { echo "ERROR: config not found: $CFG"; exit 1; } +[[ -f "$CAL" ]] || { echo "ERROR: calibration not found: $CAL"; exit 1; } +[[ -f "$TRAINER" ]] || { echo "ERROR: trainer not found: $TRAINER"; exit 1; } echo "=== Resume: $CASE (seed=$SEED, resume=$RESUME_EP, total=$EPISODES, GPU=$GPU) ===" +echo " Trainer: $TRAINER" echo " Config: $CFG" echo " Calibration: $CAL" - -conda run --no-capture-output -n "$CONDA_ENV" python -u \ - "$TRAIN_DIR/train_karman.py" \ +EXTRA=() +[[ "$TRAINER" == *train_karman.py ]] && EXTRA=(--lr 0.0003) +conda run --no-capture-output -n "$CONDA_ENV" python -u "$TRAINER" \ --case-name "$CASE" --device-id "$GPU" --seed "$SEED" \ --config "$CFG" --calibration "$CAL" \ - --total-episodes "$EPISODES" --lr 0.0003 \ - --resume-from "$RESUME_EP" + --total-episodes "$EPISODES" "${EXTRA[@]}" --resume-from "$RESUME_EP" echo "=== Done ===" diff --git a/src/drl_pinball/train/scripts/train_baseline.sh b/src/drl_pinball/train/scripts/train_baseline.sh index 1c2ecde..7d1907e 100755 --- a/src/drl_pinball/train/scripts/train_baseline.sh +++ b/src/drl_pinball/train/scripts/train_baseline.sh @@ -2,8 +2,8 @@ # ============================================================================= # 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}/ +# Trains kar_re100 from scratch with N seeds on a single GPU. +# Output: output/kar_re100_seed{N}/ # # Usage: # bash train_baseline.sh [--seeds 41,42,43,44,45] [--gpu 0] [--episodes 500] @@ -15,7 +15,7 @@ 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" +SEEDS="" while [[ $# -gt 0 ]]; do case "$1" in @@ -26,17 +26,20 @@ while [[ $# -gt 0 ]]; do esac done +registry() { PYTHONPATH="$REPO_DIR/src" python3 -m drl_pinball.train.case_registry kar_re100 "$1"; } +CAL="$(registry calibration)" +CFG="$(registry config)" +TRAINER="$(registry trainer)" +[[ -n "$SEEDS" ]] || SEEDS="$(registry seeds)" 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 ===" +echo "=== Baseline: kar_re100, 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" \ + "$TRAINER" \ + --case-name kar_re100 --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..." diff --git a/src/drl_pinball/train/scripts/train_illusion.sh b/src/drl_pinball/train/scripts/train_illusion.sh index cd8a79f..c88b127 100755 --- a/src/drl_pinball/train/scripts/train_illusion.sh +++ b/src/drl_pinball/train/scripts/train_illusion.sh @@ -3,7 +3,7 @@ # 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/ +# Output: output/ill_{1L,15L,075L,2L}_seed43/ # # Usage: # bash train_illusion.sh [--gpu 0] [--episodes 500] @@ -15,8 +15,8 @@ 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" +registry() { PYTHONPATH="$REPO_DIR/src" python3 -m drl_pinball.train.case_registry "$CASE" "$1"; } +GPU=0; EPISODES=500; DELAY=120; ONLY="" while [[ $# -gt 0 ]]; do case "$1" in @@ -31,11 +31,14 @@ 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" + CASE="ill_${size}" + CAL="$(registry calibration)" + CFG="$(registry config)" + TRAINER="$(registry trainer)" + SEED="$(registry seeds)" echo " [$(date '+%H:%M:%S')] Training $CASE..." conda run --no-capture-output -n "$CONDA_ENV" python -u \ - "$TRAIN_DIR/train_illusion.py" \ + "$TRAINER" \ --case-name "$CASE" --device-id "$GPU" --seed "$SEED" \ --config "$CFG" --calibration "$CAL" \ --total-episodes "$EPISODES" diff --git a/src/drl_pinball/train/scripts/vardist_scratch.sh b/src/drl_pinball/train/scripts/vardist_scratch.sh index 8c1bf1f..b4c7abd 100755 --- a/src/drl_pinball/train/scripts/vardist_scratch.sh +++ b/src/drl_pinball/train/scripts/vardist_scratch.sh @@ -4,7 +4,7 @@ # # 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}/ +# Output: output/kar_{d075,d15,d2}_seed{N}/ # # Usage: # bash vardist_scratch.sh [--gpu 0] [--episodes 500] @@ -16,8 +16,8 @@ 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" +registry() { PYTHONPATH="$REPO_DIR/src" python3 -m drl_pinball.train.case_registry "$CASE" "$1"; } GPU=0; EPISODES=500; DELAY=120; ONLY="" -CFG="$REPO_DIR/configs/config_lbm_karman_2000x600.json" while [[ $# -gt 0 ]]; do case "$1" in @@ -30,16 +30,16 @@ 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" + CASE="kar_${diam}" + CAL="$(registry calibration)" + CFG="$(registry config)" + TRAINER="$(registry trainer)" + SEED="$(registry seeds)" echo " [$(date '+%H:%M:%S')] Training $CASE (seed=$SEED)..." conda run --no-capture-output -n "$CONDA_ENV" python -u \ - "$TRAIN_DIR/train_karman.py" \ + "$TRAINER" \ --case-name "$CASE" --device-id "$GPU" --seed "$SEED" \ --config "$CFG" --calibration "$CAL" \ --total-episodes "$EPISODES" --lr 0.0003 diff --git a/src/drl_pinball/train/scripts/vardist_transfer.sh b/src/drl_pinball/train/scripts/vardist_transfer.sh deleted file mode 100755 index 403be3b..0000000 --- a/src/drl_pinball/train/scripts/vardist_transfer.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/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 ===" diff --git a/src/drl_pinball/train/symmetry_wrapper.py b/src/drl_pinball/train/symmetry_wrapper.py index a14f9c6..6a0e976 100644 --- a/src/drl_pinball/train/symmetry_wrapper.py +++ b/src/drl_pinball/train/symmetry_wrapper.py @@ -48,6 +48,11 @@ def apply_G_obs(obs: np.ndarray) -> np.ndarray: Returns G(obs) with same shape. G is its own inverse (G^2 = identity). """ + obs = np.asarray(obs) + if obs.ndim != 1 or obs.shape[0] not in (12, 14): + raise ValueError( + f"Symmetry observation must be 12D Karman or 14D Illusion; got {obs.shape}." + ) out = np.empty_like(obs) # Forces: [f_front_fx, f_front_fy, f_top_fx, f_top_fy, f_bot_fx, f_bot_fy] out[0] = obs[0] # front_fx stays @@ -63,6 +68,9 @@ def apply_G_obs(obs: np.ndarray) -> np.ndarray: out[9] = -obs[9] # s1_uy negated out[10] = obs[6] # s2_ux <- s0_ux out[11] = -obs[7] # s2_uy <- -s0_uy + if obs.shape[0] == 14: + out[12] = obs[12] # target_cd stays + out[13] = -obs[13] # target_cl negated return out @@ -87,12 +95,21 @@ class SymmetryAugmentWrapper(gym.Wrapper): steps in that rollout use G-transform consistently. This avoids the noise of per-step random switching. - reset() always returns non-mirrored obs (for clean eval). + reset() selects the frame for the first rollout; evaluation sets prob=0. """ def __init__(self, env: gym.Env, prob: float = 0.5, seed: int = 42, rollout_len: int = 2048): super().__init__(env) + obs_shape = getattr(env.observation_space, "shape", None) + if obs_shape not in ((12,), (14,)): + raise ValueError( + f"Symmetry observation must be 12D Karman or 14D Illusion; got {obs_shape}." + ) + if not 0.0 <= prob <= 1.0: + raise ValueError(f"prob must be in [0, 1], got {prob}") + if rollout_len <= 0: + raise ValueError(f"rollout_len must be positive, got {rollout_len}") self.prob = prob self._rollout_len = rollout_len self._rng = np.random.default_rng(seed) @@ -101,8 +118,12 @@ class SymmetryAugmentWrapper(gym.Wrapper): def reset(self, *, seed=None, options=None) -> Tuple[np.ndarray, dict]: obs, info = self.env.reset(seed=seed, options=options) - self._mirrored = False # eval always non-mirrored + # Select the frame before returning the first observation and retain it + # for this rollout. Evaluation sets prob=0 explicitly. + self._mirrored = self._rng.random() < self.prob self._step_count = 0 + if self._mirrored: + obs = apply_G_obs(obs) return obs, info def step(self, action: np.ndarray) -> Tuple[np.ndarray, float, bool, bool, dict]: diff --git a/src/drl_pinball/train/tests/test_normalization_schema.py b/src/drl_pinball/train/tests/test_normalization_schema.py new file mode 100644 index 0000000..2d08afc --- /dev/null +++ b/src/drl_pinball/train/tests/test_normalization_schema.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +import numpy as np + +_TRAIN = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(_TRAIN)) + +from env_karman import compute_similarity +from normalization import ( + CALIBRATION_SCHEMA_NATIVE_V2, + LEGACY_SENSOR_FACTOR, + POLICY_SENSOR_UNITS_LEGACY, + SENSOR_UNITS_NATIVE, + convert_legacy_calibration, + convert_legacy_harmonics, + convert_legacy_target, + dtw_sensor_factor, + policy_sensor_scale, +) + + +def _legacy_calibration(): + return {"SENSOR_CC": 78.0, "SENS_SCALE": 0.78, "dtw_norm_scale": 0.234} + + +def test_calibration_conversion_and_policy_schema_behavior(): + legacy = _legacy_calibration() + native = convert_legacy_calibration(legacy) + assert native["schema_version"] == CALIBRATION_SCHEMA_NATIVE_V2 + assert native["sensor_units"] == SENSOR_UNITS_NATIVE + assert "SENSOR_CC" not in native + assert np.isclose(native["SENS_SCALE"], 0.01) + assert np.isclose(native["dtw_norm_scale"], 0.003) + assert dtw_sensor_factor(legacy) == LEGACY_SENSOR_FACTOR + assert dtw_sensor_factor(native) == 1.0 + assert np.isclose(policy_sensor_scale(native), 0.01) + assert np.isclose(policy_sensor_scale(native, POLICY_SENSOR_UNITS_LEGACY), 0.78) + assert np.isclose(policy_sensor_scale(legacy), 0.78) + + +def test_target_and_harmonics_conversion_only_scale_sensor_channels(): + target = np.arange(24, dtype=np.float32).reshape(3, 8) + converted = convert_legacy_target(target) + np.testing.assert_allclose(converted[:, :6], target[:, :6] / 78.0) + np.testing.assert_allclose(converted[:, 6:], target[:, 6:]) + + harmonics = [ + {"dc": float(i + 1), "amps": [float(i + 2)], "freqs": [0.1], "phases": [0.2]} + for i in range(8) + ] + converted_h = convert_legacy_harmonics(harmonics) + for i in range(6): + assert np.isclose(converted_h[i]["dc"], harmonics[i]["dc"] / 78.0) + assert np.isclose(converted_h[i]["amps"][0], harmonics[i]["amps"][0] / 78.0) + assert converted_h[6:] == harmonics[6:] + + +def test_dtw_similarity_is_invariant_under_unit_conversion(): + rng = np.random.default_rng(7) + target = rng.normal(size=(150, 6)) + state = rng.normal(size=(90, 6)) + legacy_scale = 0.25 + legacy_sim = compute_similarity(target, state, 30, legacy_scale) + native_sim = compute_similarity( + target / LEGACY_SENSOR_FACTOR, + state / LEGACY_SENSOR_FACTOR, + 30, + legacy_scale / LEGACY_SENSOR_FACTOR, + ) + assert np.isclose(native_sim, legacy_sim, atol=1e-12) diff --git a/src/drl_pinball/train/tests/test_symmetry_wrapper.py b/src/drl_pinball/train/tests/test_symmetry_wrapper.py new file mode 100644 index 0000000..a94aa0a --- /dev/null +++ b/src/drl_pinball/train/tests/test_symmetry_wrapper.py @@ -0,0 +1,98 @@ +"""CPU-only contracts for symmetry augmentation.""" +from __future__ import annotations + +import sys +from pathlib import Path + +import gymnasium as gym +import numpy as np +import pytest + +_TRAIN_DIR = Path(__file__).resolve().parents[1] +if str(_TRAIN_DIR) not in sys.path: + sys.path.insert(0, str(_TRAIN_DIR)) + +from symmetry_wrapper import ( # noqa: E402 + SymmetryAugmentWrapper, + apply_G_action, + apply_G_obs, +) + + +class RecordingEnv(gym.Env): + metadata = {} + + def __init__(self, obs_dim: int): + self.observation_space = gym.spaces.Box(-np.inf, np.inf, (obs_dim,), dtype=np.float32) + self.action_space = gym.spaces.Box(-1.0, 1.0, (3,), dtype=np.float32) + self.obs = np.arange(1, obs_dim + 1, dtype=np.float32) + self.actions: list[np.ndarray] = [] + + def reset(self, *, seed=None, options=None): + super().reset(seed=seed) + return self.obs.copy(), {} + + def step(self, action): + self.actions.append(np.asarray(action).copy()) + self.obs = self.obs + 1 + return self.obs.copy(), 1.0, False, False, {} + + +@pytest.mark.parametrize("dim", [12, 14]) +def test_observation_transform_is_an_involution(dim): + obs = np.arange(1, dim + 1, dtype=np.float32) + np.testing.assert_array_equal(apply_G_obs(apply_G_obs(obs)), obs) + + +def test_action_transform_and_involution(): + action = np.array([1.0, 2.0, 3.0], dtype=np.float32) + np.testing.assert_array_equal(apply_G_action(action), [-1.0, -3.0, -2.0]) + np.testing.assert_array_equal(apply_G_action(apply_G_action(action)), action) + + +def test_exact_12d_karman_transform(): + obs = np.arange(1, 13, dtype=np.float32) + expected = np.array([1, -2, 5, -6, 3, -4, 11, -12, 9, -10, 7, -8]) + np.testing.assert_array_equal(apply_G_obs(obs), expected) + + +def test_exact_14d_illusion_transform_preserves_cd_and_negates_cl(): + obs = np.arange(1, 15, dtype=np.float32) + expected = np.array([1, -2, 5, -6, 3, -4, 11, -12, 9, -10, 7, -8, 13, -14]) + np.testing.assert_array_equal(apply_G_obs(obs), expected) + + +@pytest.mark.parametrize("dim", [0, 11, 13, 15]) +def test_unsupported_observation_dimensions_are_rejected(dim): + with pytest.raises(ValueError, match="12D Karman or 14D Illusion"): + apply_G_obs(np.zeros(dim, dtype=np.float32)) + with pytest.raises(ValueError, match="12D Karman or 14D Illusion"): + SymmetryAugmentWrapper(RecordingEnv(dim)) + + +def test_mirrored_frame_is_selected_before_and_fixed_through_rollout(): + raw = RecordingEnv(12) + wrapped = SymmetryAugmentWrapper(raw, prob=1.0, seed=7, rollout_len=2) + + initial_raw = raw.obs.copy() + obs, _ = wrapped.reset() + np.testing.assert_array_equal(obs, apply_G_obs(initial_raw)) + + action = np.array([0.25, -0.5, 0.75], dtype=np.float32) + for _ in range(5): + obs, reward, terminated, truncated, _ = wrapped.step(action) + np.testing.assert_array_equal(raw.actions[-1], apply_G_action(action)) + np.testing.assert_array_equal(obs, apply_G_obs(raw.obs)) + assert reward == 1.0 + assert not terminated and not truncated + + +def test_probability_zero_preserves_evaluation_frame(): + raw = RecordingEnv(14) + wrapped = SymmetryAugmentWrapper(raw, prob=0.0, seed=7) + obs, _ = wrapped.reset() + np.testing.assert_array_equal(obs, raw.obs) + action = np.array([0.25, -0.5, 0.75], dtype=np.float32) + obs, *_ = wrapped.step(action) + np.testing.assert_array_equal(raw.actions[-1], action) + np.testing.assert_array_equal(obs, raw.obs) diff --git a/src/drl_pinball/train/train_illusion.py b/src/drl_pinball/train/train_illusion.py index c0c5758..929cda8 100644 --- a/src/drl_pinball/train/train_illusion.py +++ b/src/drl_pinball/train/train_illusion.py @@ -10,7 +10,7 @@ Usage: """ from __future__ import annotations -import argparse, json, os, sys, time +import argparse, json, re, sys, time from pathlib import Path import numpy as np @@ -26,7 +26,8 @@ from stable_baselines3 import PPO from stable_baselines3.common.vec_env import DummyVecEnv, VecNormalize from torch.utils.tensorboard import SummaryWriter -from env_illusion import IllusionCloakEnv, record_illusion_target +from env_illusion import IllusionCloakEnv +from normalization import POLICY_SENSOR_UNITS_LEGACY from symmetry_wrapper import SymmetryAugmentWrapper @@ -49,8 +50,12 @@ def main() -> int: parser.add_argument("--config", type=str, required=True) parser.add_argument("--calibration", type=str, required=True) parser.add_argument("--si", type=int, default=None) - parser.add_argument("--symmetry-prob", type=float, default=0.5) + parser.add_argument("--symmetry-prob", type=float, default=0.0) parser.add_argument("--transfer-model", type=str, default=None) + 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() with open(args.calibration, "r") as f: @@ -62,55 +67,101 @@ 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" + log_existed = log_path_tmp.exists() + 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 Illusion {run_name} (RESUME from ep {resume_ep}) ===") + # Resume is checkpoint-based: model and normalization state are restored, + # while CFD state and exact RNG streams intentionally restart. + best_ep = None + if log_existed: + with open(log_path_tmp, "r") as f: + for line in f: + m = re.search(r'Ep\s+(\d+):\s+reward=([-+\d.eE]+)\s+\(BEST', line) + if m and int(m.group(1)) <= resume_ep: + best_ep = int(m.group(1)) + resume_best_reward = float(m.group(2)) + _log(f" Recovered best through ep {resume_ep}: " + f"episode={best_ep}, reward={resume_best_reward:.4f}") + else: + _log(" WARNING: train.log not found; resumed best_reward starts at -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}" print(line, flush=True) with open(log_path, "a") as f: f.write(line + "\n"); f.flush() - writer = SummaryWriter(log_dir=str(out_dir / "tb")) log(f"=== V5 Illusion {run_name} ===") log(f" Device={args.device_id}, Seed={args.seed}, Ep={args.total_episodes}") log(f" Config={args.config}, SI={cal['SI']}") cal_copy = dict(cal) cal_copy["config_path"] = args.config - with open(out_dir / "calibration.json", "w") as f: - json.dump(cal_copy, f, indent=2) + if not resume_ep: + with open(out_dir / "calibration.json", "w") as f: + json.dump(cal_copy, f, indent=2) - # Load pre-recorded target + # Illusion training requires both products from the same calibration. cal_dir = Path(args.calibration).resolve().parent target_npy = cal_dir / "target.npy" harmonics_path = cal_dir / "target_harmonics.json" - if target_npy.exists() and harmonics_path.exists(): - target_states = np.load(str(target_npy)) - with open(harmonics_path, "r") as f: - target_harmonics = json.load(f) - log(f" Loaded target from {cal_dir}") - else: - log(" Recording target...") - t0 = time.perf_counter() - config_path = args.config - si_val = cal["SI"] - target_states, target_harmonics = record_illusion_target( - config_path, args.device_id, si_val) - np.save(str(out_dir / "target.npy"), target_states) - with open(out_dir / "target_harmonics.json", "w") as f: - json.dump(target_harmonics, f, indent=2) - log(f" Target recorded in {time.perf_counter()-t0:.0f}s") + missing = [path for path in (target_npy, harmonics_path) if not path.exists()] + if missing: + missing_text = "\n".join(f" - {path}" for path in missing) + raise FileNotFoundError( + f"Required illusion calibration artifact(s) missing:\n{missing_text}\n" + "Run illusion calibration first, for example:\n" + f" conda run -n pycuda_3_10 python -u " + f"{Path(__file__).resolve().parent / 'calibrate.py'} " + f"--case {args.case_name} --device-id {args.device_id} " + f"--config {args.config} --scene illusion\n" + "Then pass the generated calibration.json via --calibration." + ) + target_states = np.load(str(target_npy)) + with open(harmonics_path, "r") as f: + target_harmonics = json.load(f) + log(f" Loaded target from {cal_dir}") config_path = args.config t0 = time.perf_counter() + writer = SummaryWriter(log_dir=str(out_dir / "tb")) + log(" Creating env...") env = IllusionCloakEnv(device_id=args.device_id, seed=args.seed, calibration=cal, config_path=config_path, target_states=target_states, - target_harmonics=target_harmonics) + target_harmonics=target_harmonics, + policy_sensor_units=(POLICY_SENSOR_UNITS_LEGACY + if resume_ep or args.transfer_model else None)) env = SymmetryAugmentWrapper(env, prob=args.symmetry_prob, seed=args.seed, rollout_len=args.n_steps) vec_env = DummyVecEnv([lambda: env]) - vec_env = VecNormalize(vec_env, norm_obs=True, norm_reward=False, - clip_obs=10.0, gamma=0.99) + if resume_ep and ckpt_norm.exists(): + vec_env = VecNormalize.load(str(ckpt_norm), vec_env) + vec_env.training = True + vec_env.norm_reward = False + log(" Loaded VecNormalize from checkpoint.") + else: + 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") device = torch.device(f"cuda:{args.device_id}") @@ -121,6 +172,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", @@ -132,11 +188,17 @@ def main() -> int: ) log(" Created from scratch.") - best_reward = -float("inf") + best_reward = resume_best_reward if resume_ep else -float("inf") + best_ep = best_ep if resume_ep else None 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) eval_env = model.get_env() @@ -149,7 +211,7 @@ def main() -> int: eval_obs = eval_env.reset() ep_rewards, ep_r_cd, ep_r_cl, ep_r_sim = [], [], [], [] for _ in range(360): - action, _ = model.predict(eval_obs) + action, _ = model.predict(eval_obs, deterministic=True) eval_obs, reward, done, info = eval_env.step(action) ep_rewards.append(float(reward[0])) inf = info[0] if isinstance(info, list) else info @@ -175,8 +237,9 @@ def main() -> int: if avg_r > best_reward: best_reward = avg_r + best_ep = ep model.save(str(out_dir / "models" / "best_model.zip")) - vec_env.save(norm_path) + vec_env.save(str(out_dir / "best_vecnormalize.pkl")) log(f" Ep {ep:3d}: reward={avg_r:.4f} (BEST) r_cd={np.mean(ep_r_cd[-180:]):.3f} " f"r_cl={np.mean(ep_r_cl[-180:]):.3f} r_sim={np.mean(ep_r_sim[-180:]):.3f}") elif ep % 5 == 0: @@ -191,10 +254,13 @@ def main() -> int: vec_env.save(str(out_dir / "models" / f"ep{ep:04d}_vecnormalize.pkl")) model.save(str(out_dir / "models" / "final_model.zip")) + vec_env.save(str(out_dir / "final_vecnormalize.pkl")) + # Keep the historical root name as a final-state alias for old consumers. vec_env.save(norm_path) meta = {"case_name": args.case_name, "seed": args.seed, "total_episodes": args.total_episodes, + "best_episode": int(best_ep) if best_ep is not None else None, "best_reward": float(best_reward), "n_steps": args.n_steps, "batch_size": args.batch_size, "n_epochs": args.n_epochs, "lr": args.lr, @@ -202,6 +268,7 @@ def main() -> int: with (out_dir / "meta.json").open("w") as f: json.dump(meta, f, indent=2) + writer.close() env.close() log(f"Done. Best reward: {best_reward:.4f}") return 0 diff --git a/src/drl_pinball/train/train_karman.py b/src/drl_pinball/train/train_karman.py index d9de1ce..b501b14 100644 --- a/src/drl_pinball/train/train_karman.py +++ b/src/drl_pinball/train/train_karman.py @@ -57,7 +57,7 @@ def main() -> int: help="Path to calibration.json") parser.add_argument("--si", type=int, default=None, help="Override sample interval (default: from calibration)") - parser.add_argument("--symmetry-prob", type=float, default=0.5, + parser.add_argument("--symmetry-prob", type=float, default=0.0, 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") @@ -199,6 +199,7 @@ def main() -> int: log(" Created from scratch.") best_reward = resume_best_reward if resume_ep else -float("inf") + best_ep = None t_last = time.perf_counter() norm_path = str(out_dir / "vec_normalize.pkl") @@ -222,7 +223,7 @@ def main() -> int: ep_rewards = [] ep_r_cd, ep_r_cl, ep_r_sim = [], [], [] for _ in range(360): - action, _ = model.predict(eval_obs) + action, _ = model.predict(eval_obs, deterministic=True) eval_obs, reward, done, info = eval_env.step(action) ep_rewards.append(float(reward[0])) inf = info[0] if isinstance(info, list) else info @@ -248,8 +249,9 @@ def main() -> int: if avg_r > best_reward: best_reward = avg_r + best_ep = ep model.save(str(out_dir / "models" / "best_model.zip")) - vec_env.save(norm_path) + vec_env.save(str(out_dir / "best_vecnormalize.pkl")) log(f" Ep {ep:3d}: reward={avg_r:.4f} (BEST, {dt:.0f}s/ep) ** " f"r_cd={np.mean(ep_r_cd[-180:]):.3f} r_cl={np.mean(ep_r_cl[-180:]):.3f} " f"r_sim={np.mean(ep_r_sim[-180:]):.3f}") @@ -261,6 +263,8 @@ def main() -> int: vec_env.save(str(out_dir / "models" / f"ep{ep:04d}_vecnormalize.pkl")) model.save(str(out_dir / "models" / "final_model.zip")) + vec_env.save(str(out_dir / "final_vecnormalize.pkl")) + # Keep the historical root name as a final-state alias for old consumers. vec_env.save(norm_path) meta = {"case_name": args.case_name, "seed": args.seed, @@ -269,7 +273,8 @@ def main() -> int: "n_steps": args.n_steps, "batch_size": args.batch_size, "n_epochs": args.n_epochs, "lr": args.lr, "config_path": config_path, "calibration_path": args.calibration, - "transfer_model": args.transfer_model} + "transfer_model": args.transfer_model, + "best_episode": best_ep} with (out_dir / "meta.json").open("w") as f: json.dump(meta, f, indent=2)