Auto-sync from Server A at 2026-06-09 12:21:00

This commit is contained in:
vvvvonly 2026-06-09 12:21:02 +08:00
parent 2c09f91cfa
commit fa2c670f8d
2270 changed files with 168233 additions and 343 deletions

459
README.md
View File

@ -16,7 +16,7 @@ afem/
│ ├── mesh_refinement.py # ★ 核心:网格细化 RL 环境
│ │ # - GNN 图观测构建(节点 + 边特征)
│ │ # - continuous_sizing_field (score-based + budget) 细化策略
│ │ # - spatial 奖励
│ │ # - spatial 奖励 + step0 penalty 降权
│ ├── helmholtz.py # Helmholtz FEM 求解器 + 残差误差估计
│ ├── fem_problem.py # FEM 问题封装 + PDE 循环缓冲区
│ ├── fem_util.py # 三角形面积、中点、随机采样、尺寸场函数
@ -46,7 +46,7 @@ afem/
- **边界条件**: SBC (Sommerfeld) `∂u/∂n = i·k·u`
- **域**: 可配矩形域,初始网格密度自适应 + domain area 线性缩放:`N_init = N_base × (k/k_ref)^k_exponent × domain_area`。k_ref 和 k_exponent 均可通过 helmholtz config 配置(默认 k_exponent=2.0, k_ref=6.0),保证不同域尺寸下每单位面积单元数一致
- 可配 exponent^2 = P1 Helmholtz 理论最优 (污染误差 ∝ k²)。建议 N_base 配合 exponent 调整,使 N_init 约为 COMSOL 目标 (λ/10√ε_r) 的 30-50%,为 RL agent 留出充分细化空间
- **介质区前渐近区边缘约束**: 介质内 λ_d = 2π/(k√ε_r) 更短,强制迭代细化至 h ≤ λ_d/N默认 N=1.5helmholtz.pre_asymptotic_N 可配)。约 1.5 点/波长,刚好跨过渐近区门槛,赋予初始网格基本相位解析能力但不过度消耗物理预算,为 RL agent 留出充分的选择性细化空间
- **介质区前渐近区边缘约束**: 介质内 λ_d = 2π/(k√ε_r) 更短,强制迭代细化至 h ≤ λ_d/N默认 N=2.0helmholtz.pre_asymptotic_N 可配)。约 2 点/波长,赋予初始网格基本相位解析能力但不过度消耗物理预算,为 RL agent 留出充分的选择性细化空间
- **后验误差**: 残差型 indicatorAinsworth & Oden 风格),含单元内部残差 + 梯度跳变 + SBC 边界残差
### 强化学习建模
@ -54,29 +54,80 @@ afem/
| 概念 | 对应实体 |
|------|---------|
| **智能体** | 每个三角形网格单元 |
| **状态** | GNN 节点特征(几何 + PDE 残差 + 振幅 + 相位方向 + 物理参数,节点 13 维 + 边 1 维) |
| **动作** | 1 维连续标量 x_i → score = -x_i 排序,在物理预算内 top-k 选细化单元x 越小优先级越高 |
| **奖励** | 局部子单元 η 的 log-ratio 改善spatial: sum 聚合 / spatial_max: max 聚合)+ α 衰减全局 η log-ratio shaping |
| **终止** | 达到最大步数或超过最大单元数 |
| **状态** | GNN 节点特征(几何 + PDE 残差 + 振幅 + 相位方向 + 物理参数,节点 13 维 + 边 1 维) + 13 维全局统计向量 |
| **动作** | 1 维连续标量 δ_i → `score_i = log(η_i + ε) + c·tanh(δ_i)` 降序 top-k 选择(η baseline + bounded Actor correction |
| **奖励** | 纯局部 r_local = log(η_old) log(l2(η_child))clip [0, 2.0]减去动作惩罚step0 降权);未细化单元 r=0 |
| **终止** | 达到最大步数、超过最大单元数、或 sel=0无单元可选 |
---
## 网络架构
GNN 架构policy / value 各自独立基座
流 GNN 架构Actor / Critic 共享基座,各自独立头
```
图观测 → MessagePassingBase → MLP → 动作分布 / value 标量
├─ nn.Linear嵌入
├─ MessagePassingStack2 层消息传递 + GVN 全局广播inner 残差 + LayerNorm
│ ├─ MessagePassingStep × N
图观测 → MessagePassingBase → Actor MLP → δ_i (连续动作)
├─ nn.Linear嵌入 → Critic MLP → V(s) (标量)
├─ MessagePassingStack2 层消息传递 + MultiPoolGVN 全局广播)
│ ├─ MessagePassingStep × 2
│ │ ├─ EdgeModule: MLP([src | dst | edge_attr])
│ │ └─ NodeModule: MLP([node | scatter(入边)])
│ └─ GlobalVirtualNode (GVN): η_K 加权注意力池化 → 注意力门控广播
│ h_V = Σ(η_v/Ση)·h_vα_v = σ(W_att[h_v || h_V])h_v ← h_v + α_v ⊙ W_V·h_V
└─ 输出: 节点隐向量
│ │ 内残差 + LayerNorm
│ └─ MultiPoolGVN: 多策略池化 + 13 维全局统计 → 注意力门控广播
│ Stage A: g_global = MLP(concat(g_mean, g_eta, global_stats))
│ Stage B: α_v = σ(W_att[h_v || g_global])
│ h_v ← h_v + scale · α_v ⊙ W_V · g_global
└─ 输出: 节点隐向量 h_i
Actor 输入: concat(h_i, g_global, rel_logeta, rel_area, is_top_eta, budget_stats) [2D+6]
Critic 输入: concat(h_i, g_global) [2D]
```
### MultiPoolGVN — 多池化全局虚拟节点
替代原始单一 η 加权 GVN用多种池化策略聚合节点嵌入拼接全局统计后生成 `g_global`
| 池化模式 | 公式 | 说明 |
|----------|------|------|
| `mean` | `g_mean = Σ h_v / N` | 均匀平均 |
| `eta_softmax` | `g_eta = Σ (η_v / Ση) · h_v` | η 加权 softmax高误差节点主导 |
| `top_eta` | `g_top = mean(h_v : log η_v > μ + σ)` | top-η 节点均值log 空间 >1σ |
配置项 `gvn_pooling: [mean, eta_softmax]`,可选加 `top_eta`
### Global Stats — 13 维图级统计
每个图观测附带 13 维全局统计向量(`graph.global_stats`),用于 GVN 和 Actor/Critic 的条件输入:
| 索引 | 名称 | 说明 |
|------|------|------|
| 0 | `remaining_ratio` | (N_budget N_current) / N_budget |
| 1 | `step_ratio` | current_step / max_steps |
| 2 | `elem_ratio` | N_current / N_budget |
| 3 | `logeta_mean` | log(η) 均值 |
| 4 | `logeta_std` | log(η) 标准差 |
| 5 | `logeta_max` | log(η) 最大值 |
| 6 | `logeta_p90` | log(η) P90 |
| 7 | `logeta_p75` | log(η) P75 |
| 8 | `top10_eta_energy` | top 10% η² 能量占比 |
| 9 | `eligible_ratio` | 面积安全阈值以上元素占比 |
| 10 | `inside_eta_energy` | 散射体内 η² 能量占比 |
| 11 | `outside_eta_energy` | 散射体外 η² 能量占比 |
| 12 | `interface_eta_energy` | 界面附近 η² 能量占比 |
### 全局条件化 Actor/Critic
`use_global_conditioned_correction: true`Actor 和 Critic 的输入额外拼接全局上下文:
- **Actor**: `concat(h_i, g_global, rel_logeta, rel_area, is_top_eta, budget_stats)` → 维度 `2D + 6`
- `rel_logeta`: `(log η_i μ) / σ`per-graph 标准化
- `rel_area`: `log(area_i / mean_area)`per-graph 相对面积
- `is_top_eta`: `rel_logeta > 1.0` 的 0/1 标记
- `budget_stats`: `[remaining_ratio, step_ratio, elem_ratio]`
- **Critic**: `concat(h_i, g_global)` → 维度 `2D`
### 超参数
| 超参数 | 值 |
|--------|-----|
| latent_dim | 64 |
@ -86,18 +137,23 @@ afem/
| 边 dropout | 0.1 |
| Actor MLP | 2 层 tanh |
| Critic MLP | 2 层 tanh |
| Optimizer | Adam, lr=3e-4, lr_decay=0.995 |
| **动作分布** | `DiagGaussianDistribution`(连续 Box 动作空间),`log_std` 可学习clamp 在 [-4.0, -1.0] |
| **log_std 策略** | 初始化 -2.0std≈0.135),每步 optimizer.step() 后 clamp 到 [-4.0, -1.0]std ∈ [0.018, 0.368]),熵系数 0.001 |
| Optimizer | Adam, lr=3e-4, lr_decay=1.0 |
| **动作分布** | `DiagGaussianDistribution`(连续 Box 动作空间),`log_std` 可学习clamp 在 [-2.5, -1.0] |
| **log_std 策略** | 初始化 -2.0std≈0.135),每步 optimizer.step() 后 clamp 到 [-2.5, -1.0]std ∈ [0.082, 0.368]),熵系数 0.01 |
| **correction_scale** | 0.3 — Actor 修正幅值 c·tanh(δ) ∈ [0.3, +0.3] |
| **correction_reg_coef** | 0.03 — correction 正则化系数L_corr = coef × mean(correction²) |
| **step0_penalty_scale** | 0.3 — 第一步 element penalty 降权系数 |
### 动作分布策略说明
环境定义的是 `_action_space`(下划线前缀),网络初始化时必须用 `environment._action_space` 而非 `environment.action_space`(后者默认为 None会错误回退到 `CategoricalDistribution(1)`,导致 policy gradient 恒为零)。
`continuous_sizing_field`score-based的动作有效范围约 [-3, 3]
- score = -x_ix 越小 ⇒ 优先级越高(纯排序,不设正负门槛)
- `initial_log_std=-2.0`std≈0.135clamp 在 [-4.0, -1.0]std ∈ [0.018, 0.368]
- 加 `entropy_coefficient=0.001` 提供微弱探索压力,避免 log_std 过早收敛到下限
`continuous_sizing_field`score-based的 scoring 公式:
- `score_i = log(η_i + ε) + c·tanh(δ_i)`,其中 c=`correction_scale`
- Actor 输出 δ_i经 tanh 限幅,只能微调 log(η) 基准排序,不能覆盖物理先验
- 选 top-k 按 score 降序(越大越优先)
- `initial_log_std=-2.0`std≈0.135clamp 在 [-2.5, -1.0]std ∈ [0.082, 0.368]
- `entropy_coefficient=0.01`
---
@ -152,7 +208,8 @@ main.py --mode train/test/viz
└─ train_step() # 多 epoch PPO 更新
├─ policy_loss() # Clipped PPO
├─ value_loss() # Clipped value loss
└─ entropy_loss() # 熵正则
├─ entropy_loss() # 熵正则
└─ correction_reg() # Correction 正则化 L_corr
```
### 环境内部调用
@ -163,16 +220,15 @@ MeshRefinement.reset()
└─→ initial_mesh (meshpy → 介质内 前渐近区边缘迭代细化)
MeshRefinement.step(action)
├─→ score = -x 排序 + 物理预算约束 → top-k 细化单元
├─→ score = log(η) + c·tanh(δ) 排序 + 物理预算约束 → top-k 细化单元
├─→ FEMProblemWrapper.refine_mesh() # scikit-fem refine
├─→ calculate_solution_and_get_error()
│ ├─→ HelmholtzProblem.calculate_solution() # FEM 求解
│ └─→ _compute_residual_indicator() # 残差误差
├─→ _get_reward_by_type() # spatial 奖励
└─→ last_observation # 构建 Data(x, edge_index, edge_attr)
├─→ _get_reward_by_type() # spatial 奖励 + step0 penalty 降权
└─→ last_observation # 构建 Data(x, edge_index, edge_attr, eta, area, global_stats)
```
### 训练
```bash
@ -182,16 +238,25 @@ CUDA_VISIBLE_DEVICES=7 python src/main.py --mode train --config src/config.yaml
首次迭代需收集 256 步 rollout含 FEM 求解),后续打印:
```
it | loss ev agents reward x<0 elig sel time
it | loss ev agents avg_r sum_r corr_m corr_s r_le_sc δ<0 elig sel rem_r corr_reg corr_l2 corr_a p_sc avg_p avg_rl step_id time
```
| 字段 | 含义 | 健康范围 |
|------|------|---------|
| `x<0` | `mean(x_i < 0)`,负值动作比例(纯诊断) | 越负的单元优先级越高 |
| `elig` | 通过双过滤器的候选占比 | 排除数值退化 + 低误差的单元 |
| `mask` | 被 Reverse Dörfler 剔除的噪声尾部占比(累积能量 <1% 总误差的底部单元 | 因场景而异非固定比例 |
| `corr_m` | `c·tanh(δ)` 均值 | 接近 0Actor 修正无系统性偏差 |
| `corr_s` | `c·tanh(δ)` 标准差 | 应稳定在 0.030.08,不应持续涨到 0.15 |
| `r_le_sc` | Pearson r(log_η, score) | 接近 1.0 → Actor 修正小;<0.9 Actor 在主动修正 |
| `δ<0` | Actor 输出负值的比例(纯诊断) | — |
| `elig` | 通过双过滤器的候选占比 | — |
| `sel` | 实际选中的细化单元数 | 每步最多 N_current // 4 |
| `n_budget` | 全局物理预算(每 episode 固定) | k=30 → ~1800 |
| `rem_r` | remaining / N_budget | — |
| `corr_reg` | correction 正则化损失 L_corr | 监控 correction drift |
| `corr_l2` | mean(correction²) | 监控 correction 幅值增长 |
| `corr_a` | mean(\|correction\|) | 监控 correction 绝对值 |
| `p_sc` | penalty_scale | step0=0.3,后续=1.0 |
| `avg_p` | 平均 element penalty | step0 应明显小于后续 |
| `avg_r_local` | 平均 r_localpenalty 前) | — |
| `step_id` | 当前步数 | — |
### 测试
@ -203,12 +268,12 @@ python src/main.py --mode test --checkpoint checkpoints/model_final.pt \
输出:
```
Step 0: reward=--- error=1.0000 elements=174 budget=1885
Step 1: reward=+12.345 error=0.7160 elements=618 x<0=0.45 sel=87
Step 0: reward=--- aw_rel=79.28% max_err=2.2133 elements=1078 budget=...
Step 1: reward=+2.345 aw_rel=30.10% max_err=0.7096 elements=2020 sel=269
...
```
每步打印 `reward error elements x<0 sel`,第 0 步额外显示 `N_budget`
每步打印 `reward aw_rel max_err elements sel`,第 0 步额外显示 `N_budget`
### 可视化
@ -251,134 +316,52 @@ SBC 边界条件仍用 $k_{local}$(物理正确),仅归一化因子改用
### 连续尺寸场策略score-based + 物理预算约束 + 动作掩码)
Actor 输出标量 x_i → score = -x_i 直接排序,在预算和上限内选 top-k
Actor 输出标量 δ_i → `score_i = log(η_i + ε) + c·tanh(δ_i)`,在预算和上限内选 top-k
```
A_budget_i = ½(λ_local_i / 6)² // 每局部波长方向 ~6 尺度点(仅用于 N_budget 计算)
λ_local_i = 2π / (k · √ε_r_i)
N_budget = max(N_phys, ⌈5·N_init⌉) // rho_min=5.0,至少 5× 初始单元数,保证 RL 多步细化空间
N_phys = ⌈ Σ |K_i| / A_budget_i ⌉ // 全局物理预算k=30 真空 ~1800
ε = max(0.01·median(η), 1e-12) // 动态 eps防止 log(0)
corr_i = c · tanh(δ_i) c = correction_scale // Actor 修正幅值 ∈ [c, +c]
score_i = log(η_i + ε) + corr_i // 降序 top-k
A_budget_i = ½(λ_local_i / 6)² // 每局部波长方向 ~6 尺度点(仅用于 N_budget
N_budget = max(N_phys, ⌈5·N_init⌉) // rho_min=5.0
remaining = N_budget N_current
V_min_safeguard = 1e-10 × domain_area // 纯数值底线(防止 FEM 求解器退化)
eligible: area > V_min_safeguard AND η_K ∈ Reverse Dörfler 保留集 // 数值底线 + 能量尾部淘汰 (ε_noise=0.01, ≥20% floor)
V_min_safeguard = 1e-10 × domain_area
eligible: area > V_min_safeguard AND η_K ∈ Reverse Dörfler 保留集 (ε_noise=0.01, ≥20% floor)
num = min(|eligible|, N_current//4, remaining//3)
selected = top-k by score = -x_i → 1-to-4 切分
selected = top-k by score descending → 1-to-4 切分
```
- score = -x_ix 越小 ⇒ 优先级越高(纯排序,不设正负门槛)
- 不再使用 `0.25·A_budget` 启发式面积地板RL 应自主学会"细化到多细",而非被人类经验 (12 点/波长) 限制。仅保留数值底线 V_min_safeguard = 1e-10 × domain_area 防止浮点精度问题。
- per-step cap 从固定 200 改为自适应 `N_current // 4`随网格规模缩放但增速更缓避免大网格时单步消耗过多预算。rho_min 从 3.0 提升到 5.0,赋予更多预算余量。
- **sel=0 提前终止**:当 agent 选中 0 个单元细化(预算耗尽或 Reverse Dörfler 屏蔽所有候选)时 episode 自动结束,不再浪费 FEM 求解
- **k_exponent 可配**:初始网格缩放指数可通过 `helmholtz.k_exponent` 配置(默认 2.0),² 为 P1 Helmholtz 理论最优;对 k=30 的 $N_{init}$ 为 k=6 的 25×
- **动作掩码 (Reverse Dörfler)**:按 η_K 升序排列,剔除累积平方误差贡献 < ε_noise·Ση² 的底部单元数值噪声/已收敛区)。基于能量分布而非密度分位数在重尾和均匀误差分布下均自适应保留率不低于 20% 确保 Agent 始终有充分的选择空间
- Actor 通过 bounded correction 微调排序,不能覆盖物理先验 log(η)
- Reverse Dörfler 动作掩码剔除噪声尾部≥20% floor 确保 Agent 始终有选择空间
- sel=0 提前终止agent 选中 0 个单元时 episode 自动结束
- k_exponent=2.0P1 Helmholtz 理论最优初始网格缩放
### 奖励计算
---
#### 变量
| 符号 | 含义 |
|------|------|
| `η_K = √(r_int² + r_jump² + r_sbc²)` | 逐单元误差指示子,`r_*` 定义见式 (1)(3) |
| `C(i)` | 父单元 i 经 1-to-4 切分产生的子单元集合 |
| `M_new[j]` | 子单元 j 对应的父单元索引 |
| `n_i = |C(i)|` | 父单元 i 的子单元数1 表示未切分) |
| `E_global = √(Σ η_K²) / \|\|u_h\|\|_{L₂(Ω)}` | 全局无量纲误差 |
---
#### 算法
**Step 0 — 保存旧状态** (`_set_previous_step`)
纯局部改善 reward无调制、无 bonus
```
η_old ← 旧逐单元 η_K
||u_h_old|| ← 旧解 L₂ 范数 (≈ √(Σ |ū_K|² · area_K))
r_local_i = log(η_old_i + ε) log(l2_η_children_i + ε)
l2_η_children_i = √(Σ_{j∈C(i)} η_new_j²)
reward_i = clip(r_local_i, 0, rmax) penalty_scale · λ · (n_child_i 1)
rmax = 2.0
λ = 0.02 (element_penalty.value)
penalty_scale = step0_penalty_scale if current_step == 0 (默认 0.3)
= 1.0 otherwise
```
**Step 1 — 网格细化** (`_refine_mesh`)
```
x = action.flatten()
score = -x // x 越小 ⇒ 优先级越高
remaining = N_budget N_old
max_by_budget = max(0, remaining // 3)
// 数值底线 + Reverse Dörfler 能量尾部淘汰
V_min_safeguard = 1e-10 × domain_area // 纯数值安全底线,防止 FEM 退化
η_sq = η_old²; total_energy = Σ η_sq
k_dorfler = searchsorted(cumsum(sort_asc(η_sq)), ε_noise·total_energy) // ε_noise=0.01
k = min(k_dorfler, N max(1, N//5)) // ≥20% floor
eligible = {i | V_old[i] > V_min_safeguard AND i ∈ sort_asc_idx[k:] }
num = min(|eligible|, N_old//3, max_by_budget)
elements_to_refine = top-k of eligible by score
M_new[j] ∈ {0,…,N_old-1} // 子→父映射
```
**Step 2 — FEM 求解 + 误差估计**
```
η_new ← 新逐单元 η_K
||u_h_new|| ← 新解 L₂ 范数
```
**Step 3 — 因果奖励**(零和预算审查)
ε_dynamic = max(0.01 × η_P95, 1e-6)
// Refined parents: r_local + zero-sum bonus penalty
if i ∈ refined_parents:
r_i = log(η_old + ε) log(√(Σ η_child²) + ε) // r_local ≥ 0 (L₂ 聚合)
+ 0.3 × (η_old / μ 1.0) // zero-sum bonus (Σ = 0)
0.06 // action penalty
// Unrefined parents: causal isolation
else:
r_i = 0
> **零和奖金**:α·(η/μ1) 全场求和为零。细化高于均值的单元得正奖金,低于均值的倒扣。
> 这是 Dörfler 准则的 RL 对偶Agent 必须选出误差超过全均水平的单元。
> **因果隔离**:未细化单元 r ≡ 0。零和奖金本身足够强介质内 +0.51)、
> 不再需要忽视惩罚的推力,排序机制自动淘汰不划算的单元。
> **L₂ 聚合**:√(Σ η_child²) ≤ η_parent 天然成立r_local ≥ 0 永不惩罚细化。
**Step 4 — 全局误差(仅诊断)**
global_bonus = α·[log(E_old) log(E_new)]α = 0.5
不注入 Actor reward。Helmholtz 污染误差可使 E_new > E_old 在正确细化后发生,
注入 global_bonus 导致因果断裂。Actor 仅优化 Step 3 的 per-element reward。
---
#### 奖励标度校准旧尺寸场下测量score-based 后需重新标定)
在随机策略下实测各分量量级1321 个 refined-parent 样本):
| 分量 | 均值 | 占 r_local 比例 |
|------|------|:---:|
| `r_local` (仅 refined parents) | +0.364 | — |
| `penalty` λ·(n1), λ=0.02 | +0.045 | 1/8 |
| `α·ΔlogE` α=0.2 | +0.069 | 1/5 |
| **net** | **+0.387** | |
满足 `r_local ≫ penalty``α·ΔlogE ≈ r_local / 5`,局部 credit assignment 不被全局信号淹没。
---
#### 设计要点
| 组件 | 聚合 | 作用 |
|------|------|------|
| 局部项 `log(η_old / √(Σ η_child²))` | scatter_add仅 refined parents | L₂ 保证 r_local ≥ 0int 主导 +0.69 |
| 零和奖金 `0.3×(η/μ1)` | 仅 refined parents | Σ=0高于 μ 得正奖,低于 μ 倒扣 (Dörfler 准则的 RL 对偶) |
| 动作惩罚 `λ=0.06` | per-refined-parent | 轻微抑制网格膨胀1-to-4 扣 0.06 |
| 因果隔离 `r=0` | unrefined parents | 零和奖金足够强,不需额外推力 |
| 全局项 `α·ΔlogE` α=0.5 | 仅诊断 | 不注入 Actor避免污染误差因果断裂 |
| 局部项 `log(η_old / √(Σ η_child²))` | scatter_add仅 refined parents | L₂ 保证 r_local ≥ 0clip 到 [0, 2.0] |
| 动作惩罚 `λ=0.02` | per-refined-parent | 轻微抑制网格膨胀1-to-4 扣 0.06 |
| **step0 降权** | step 0 时 penalty × 0.3 | 防止第一步"真实误差改善但 reward 给负反馈" |
| 因果隔离 `r=0` | unrefined parents | 未细化元素干净零基准 |
**step0 penalty 降权动机**:诊断发现 step 0 经常出现 reward < 0 但真实 aw_rel 改善的情况说明 element penalty 淹没了真实的物理改善信号降权后 step 0 reward Δaw_rel 符号一致性提高
---
@ -388,7 +371,185 @@ global_bonus = α·[log(E_old) log(E_new)]α = 0.5
- **奖励归一化**: rollout 内 reward 做 z-score 归一化std < 1e-8 则跳过
- **Value clipping**: 默认 clip_range=0.2
- **梯度裁剪**: max_grad_norm=0.5
- **log_std clamp**: 每步 `optimizer.step()` 后将 `log_std` clamp 到 `[-3.0, -1.0]`σ ∈ [0.05, 0.37]<br>
- **log_std clamp**: 每步 `optimizer.step()` 后将 `log_std` clamp 到 `[-2.5, -1.0]`σ ∈ [0.082, 0.37]<br>
初始化 `-2.0` (σ≈0.135),放宽下限防止策略过早确定化
- **熵正则**: `entropy_coefficient=0.005`,施加有意义的探索压力防止 x<0 崩塌
- **epochs_per_iteration**: 3减少对同一 rollout 的过拟合
- **熵正则**: `entropy_coefficient=0.01`
- **epochs_per_iteration**: 3
### Correction 正则化
为防止 Actor 学会利用"大 correction"刷局部 residual rewardcorrection drift / reward hacking在 PPO loss 中加入 correction 正则项:
```
correction_i = correction_scale · tanh(action_i)
L_corr = correction_reg_coef · mean(correction²)
loss = policy_loss + value_coef · value_loss + entropy_coef · entropy_loss + L_corr
```
- `correction_reg_coef` 默认 0.03,设为 0 可禁用
- correction 在 PPO 训练时从 stored actions 重新计算(与环境中的公式一致)
- 目标corr_std 不再从 ~0.03 持续涨到 ~0.15r_le_sc 保持更接近 η baselinevalidation aw_rel 不随训练后期变差
- 训练 reward 可能因正则化而下降,这是正常现象;成功标准是测试误差更稳定
### 训练诊断字段
| 字段 | 来源 | 说明 |
|------|------|------|
| `corr_reg` | train_step | L_corr = coef × mean(corr²),监控 correction drift |
| `corr_l2` | train_step | mean(correction²)correction 幅值 |
| `corr_abs` | train_step | mean(\|correction\|)correction 绝对值 |
| `penalty_scale` | environment | step0=0.3,后续=1.0 |
| `avg_penalty` | environment | 平均 element penaltyrefined parents |
| `avg_r_local` | environment | 平均 r_localpenalty 前refined parents |
| `step_id` | environment | 当前 timestep |
---
## 配置参考
```yaml
algorithm:
batch_size: 32
discount_factor: 1.0
ppo:
clip_range: 0.2
entropy_coefficient: 0.01
correction_reg_coef: 0.03 # correction 正则化系数
epochs_per_iteration: 3
gae_lambda: 0.95
initial_log_std: -2.0
max_grad_norm: 0.5
num_rollout_steps: 256
value_function_coefficient: 0.5
use_gpu: true
environment:
mesh_refinement:
correction_scale: 0.3 # c in score = log(η) + c·tanh(δ)
step0_penalty_scale: 0.3 # step 0 element penalty 降权
num_timesteps: 4
refinement_strategy: continuous_sizing_field
reward_type: spatial
element_penalty:
value: 0.02
maximum_elements: 50000
element_limit_penalty: 10000
# ... (FEM / Helmholtz / 特征配置见 config.yaml)
network:
latent_dimension: 64
use_global_conditioned_correction: true # Actor/Critic 拼接 g_global + 局部相对特征
use_global_stats: true # 启用 MultiPoolGVN + 13 维全局统计
gvn_pooling: [mean, eta_softmax] # 池化策略(可选加 top_eta
correction_centering: true # correction 在 eligible 集内中心化
base:
edge_dropout: 0.1
scatter_reduce: mean
stack:
num_steps: 2
mlp:
activation_function: leakyrelu
num_layers: 2
actor:
mlp:
activation_function: tanh
num_layers: 2
critic:
mlp:
activation_function: tanh
num_layers: 2
training:
learning_rate: 0.0003
lr_decay: 1.0
```
### 实验对照建议
| 实验 | `correction_reg_coef` | `step0_penalty_scale` | 目的 |
|------|----------------------|----------------------|------|
| A (baseline) | 0.0 | 1.0 | 无正则、无降权 |
| B (corr reg only) | 0.03 | 1.0 | 验证 correction 正则效果(优先) |
| C (both) | 0.03 | 0.3 | 正则 + step0 降权 |
成功标准(非训练 reward 高低):
- `corr_std` 不再持续涨到 ~0.15
- `r_le_sc` 保持更接近 η baseline
- top-k overlap 不随训练后期明显下降
- validation `aw_rel / max_err` 更稳定
---
## Correction GNN 训练数据
Correction GNN 用于二分类预测给定当前网格哪些单元需要加密teacher_mark=1
训练数据由 `outlook/src/gen.py` 生成。
### 参数采样
每个样本随机采样物理参数:
| 参数 | 分布 | 说明 |
|------|------|------|
| `k` | Uniform(3, 15) | 波数 |
| `eps_r` | Uniform(2, 8) | 介质相对介电常数 |
| `cx` | Uniform(0.2, 0.8) | 散射体中心 x |
| `cy` | Uniform(0.2, 0.8) | 散射体中心 y |
| `radius` | Uniform(0.05, 0.25) | 散射体半径 |
### 初始网格
采用物理自适应初始网格(`make_initial_mesh`),元素尺寸由局域波长决定:
- **介质外**: h ≤ λ₀ / q, λ₀ = 2π / k
- **介质内/散射体附近**: h ≤ λ_eff / q, λ_eff = 2π / (k √ε_r)
- **q = 2**(每波长 2 个单元)
网格在 [0,1]×[0,1] 域上通过张量积生成x/y 方向各自根据散射体位置做分级加密:
- 远离散射体粗网格h = λ₀/q
- 散射体附近含过渡区细网格h = λ_eff/q
### AMR 循环与标签生成
对每个参数样本,运行残差驱动 AMR 循环,每步保存快照:
```
for step in range(max_steps):
1. FEM 求解 → u_scat
2. 计算残差指示子 ηteacher 信号)
3. 计算 physics_score = h / λ_eff物理 baseline
4. teacher_mark = top-fraction(η, mark_fraction) # 二值标签
physics_mark = top-fraction(physics_score, mark_fraction) # 物理 baseline
correction_label = teacher_mark - physics_mark # {-1, 0, +1}
5. 提取 16 维节点特征 + 边索引
6. 保存 .npz → 残差指示子 top-k 加密 → 下一步
```
- **mark_fraction**: 默认 0.03(每步标记 top 3% 的单元为正样本)
- **top-fraction**: 按 score 降序取 top `n × fraction` 个单元,标记为 1
- **teacher_mark**: 以 η(残差指示子)为 score代表"最优加密目标"
- **physics_mark**: 以 h/λ_eff 为 score代表"纯物理 baseline"
- **correction_label**: teacher 与 physics 的差集,+1 = teacher 独有GNN 应补充),-1 = physics 独有GNN 应抑制)
### 数据文件格式
每个样本每步保存为 `sample{id}_step{step}.npz`,包含:
| 字段 | 形状 | 说明 |
|------|------|------|
| `features` | (n_elem, 16) | 15 维几何/PDE 特征 + 1 维 physics_score |
| `edge_index` | (2, n_edges) | 双向边 + 自环 |
| `physics_score` | (n_elem,) | h / λ_eff |
| `teacher_eta` | (n_elem,) | 残差指示子 η |
| `teacher_mark` | (n_elem,) | 二值标签 (0/1) |
| `physics_mark` | (n_elem,) | 物理 baseline 标签 (0/1) |
| `correction_label` | (n_elem,) | 差集标签 (-1/0/+1) |
| `k, eps_r, cx, cy, radius` | scalar | 物理参数 |
| `elements` | scalar | 当前单元数 |
| `step` | scalar | AMR 步数 |
---
## One-Shot Density Prediction
One-shot final mesh density prediction experiments are documented in [`outlook/README.md`](outlook/README.md).

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,3 +1,4 @@
import time
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
import gym
@ -55,7 +56,6 @@ class MeshRefinement(gym.Env):
# graph connectivity, feature and action space #
################################################
self._reward_type = environment_config.get("reward_type")
self._global_reward_alpha = float(environment_config.get("global_reward_alpha", 0.2))
_rho_w = environment_config.get("rho_weights", {})
self._w_rho_int = float(_rho_w.get("w_int", 1.0))
self._w_rho_jump = float(_rho_w.get("w_jump", 1.0))
@ -223,6 +223,25 @@ class MeshRefinement(gym.Env):
self._diag_selected_count = -1 # 防止跨 episode 残留触发 is_terminal
self._diag_dorfler_tail_ratio = 0.0
self._diag_dorfler_floor_active = False
self._diag_corr_raw_mean = 0.0
self._diag_corr_mean = 0.0
self._diag_corr_std = 0.0
self._diag_corr_abs = 0.0
self._diag_neg_ratio = 0.0
self._diag_score_eta_corr = 0.0
self._diag_max_by_budget = 0
self._diag_max_by_growth = 0
self._diag_n_budget = 0
self._diag_remaining = 0
self._diag_n_eligible = 0
self._diag_n_next = 0
self._diag_corr_rel_eta_corr = 0.0
self._diag_corr_inside_mean = 0.0
self._diag_corr_outside_mean = 0.0
self._diag_corr_top_eta_mean = 0.0
self._diag_corr_low_eta_mean = 0.0
self._diag_global_top10_eta_energy = 0.0
self._diag_remaining_ratio = 0.0
# reset internal state that tracks statistics over the episode
self._previous_error_per_element = self.error_per_element
@ -325,9 +344,11 @@ class MeshRefinement(gym.Env):
# solve equation and calculate error per element/element
self._previous_error_per_element = self.error_per_element
t_fem = time.perf_counter()
self._error_estimation_dict = (
self.fem_problem.calculate_solution_and_get_error()
)
self._last_fem_solve_ms = (time.perf_counter() - t_fem) * 1e3
# query returns
observation = self.last_observation
@ -346,12 +367,30 @@ class MeshRefinement(gym.Env):
"is_truncated": self.is_truncated,
"return": self._cumulative_return,
"neg_action_ratio": getattr(self, "_diag_neg_ratio", 0.0),
"corr_raw_mean": getattr(self, "_diag_corr_raw_mean", 0.0),
"corr_mean": getattr(self, "_diag_corr_mean", 0.0),
"corr_std": getattr(self, "_diag_corr_std", 0.0),
"corr_abs": getattr(self, "_diag_corr_abs", 0.0),
"score_eta_corr": getattr(self, "_diag_score_eta_corr", 0.0),
"eligible_ratio": getattr(self, "_diag_eligible_ratio", 0.0),
"masked_ratio": getattr(self, "_diag_masked_ratio", 0.0),
"selected_count": getattr(self, "_diag_selected_count", 0),
"dorfler_tail_ratio": getattr(self, "_diag_dorfler_tail_ratio", 0.0),
"dorfler_floor_active": float(getattr(self, "_diag_dorfler_floor_active", False)),
"n_budget": self._n_budget,
"remaining": getattr(self, "_diag_remaining", 0),
"max_by_budget": getattr(self, "_diag_max_by_budget", 0),
"max_by_growth": getattr(self, "_diag_max_by_growth", 0),
"n_eligible": getattr(self, "_diag_n_eligible", 0),
"n_next": getattr(self, "_diag_n_next", 0),
"fem_solve_ms": self._last_fem_solve_ms,
"corr_rel_eta_corr": getattr(self, "_diag_corr_rel_eta_corr", 0.0),
"corr_inside_mean": getattr(self, "_diag_corr_inside_mean", 0.0),
"corr_outside_mean": getattr(self, "_diag_corr_outside_mean", 0.0),
"corr_top_eta_mean": getattr(self, "_diag_corr_top_eta_mean", 0.0),
"corr_low_eta_mean": getattr(self, "_diag_corr_low_eta_mean", 0.0),
"global_top10_eta_energy": getattr(self, "_diag_global_top10_eta_energy", 0.0),
"remaining_ratio": getattr(self, "_diag_remaining_ratio", 0.0),
}
)
return observation, self._reward, done, info
@ -527,31 +566,56 @@ class MeshRefinement(gym.Env):
if self._refinement_strategy == "continuous_sizing_field":
# ================================================================
# Score-based 细化选择(由 actor 直接排序,物理预算约束)
# Score-based 细化选择log(η) baseline + bounded Actor correction
#
# Actor 输出标量 x_i: x_i < 0 → 希望细化; x_i > 0 → 不希望细化
# 排序依据 score = -x_i在预算和上限内选 top-k
# score_i = log(η_i + eps) + c · tanh(δ_i)
#
# 物理预算 N_budget: Σ area_K / A_budget其中
# A_budget = ½(λ_local/6)²,对应每局部波长方向 ~6 个尺度点
# η_i = current-step residual indicator (physical prior)
# δ_i = Actor output (continuous scalar per element)
# c = correction_scale (0.7) — bounds Actor influence
# eps = dynamic: 0.01 · median(η) — prevents log(0)
#
# 动作掩码 (Reverse Dörfler): 按 η_K 升序排列,剔除累积平方误差
# 贡献 < ε_noise·Ση² 的底部单元(数值噪声/已收敛区),保留 ≥20%
# 的单元确保 Agent 始终有充分的选择空间
# Selection: top-k by score descending (higher score → refine).
# Actor can boost or suppress priority by at most ±c in the log-η
# domain, but cannot override the physical prior.
# ================================================================
x = action.flatten()
delta = action.flatten()
# ── 训练监控指标(在所有 early return 之前计算)──
self._diag_neg_ratio = float(np.mean(x < 0.0))
eta = self._eta_indicator
eps_score = max(0.01 * float(np.median(eta)), 1e-12)
log_eta = np.log(np.maximum(eta, 1e-30) + eps_score)
c = float(self._environment_config.get("correction_scale", 0.7))
corr_raw = c * np.tanh(delta)
remaining = self._n_budget - self._num_elements
max_parents_by_budget = max(0, remaining // 3)
max_parents_by_budget = max(0, remaining // 6)
self._diag_max_by_budget = max_parents_by_budget
if max_parents_by_budget <= 0:
self._diag_eligible_ratio = 0.0
self._diag_selected_count = 0
self._diag_dorfler_tail_ratio = 0.0
self._diag_dorfler_floor_active = False
self._diag_max_by_growth = max(1, self._num_elements // 4)
self._diag_n_budget = self._n_budget
self._diag_remaining = remaining
self._diag_n_eligible = 0
self._diag_n_next = self._num_elements
self._diag_corr_raw_mean = float(np.mean(corr_raw))
self._diag_corr_mean = 0.0
self._diag_corr_std = 0.0
self._diag_corr_abs = 0.0
self._diag_neg_ratio = float(np.mean(delta < 0.0))
self._diag_score_eta_corr = 0.0
self._diag_corr_rel_eta_corr = 0.0
self._diag_corr_inside_mean = 0.0
self._diag_corr_outside_mean = 0.0
self._diag_corr_top_eta_mean = 0.0
self._diag_corr_low_eta_mean = 0.0
self._diag_global_top10_eta_energy = 0.0
self._diag_remaining_ratio = remaining / max(self._n_budget, 1)
return np.array([], dtype=np.int64)
# 动态计算每单元预算面积(仅用于 N_budget 全局资源上限)
@ -560,32 +624,23 @@ class MeshRefinement(gym.Env):
lambda_local = 2.0 * np.pi / (k * np.sqrt(np.maximum(eps_r_elem, 1.0)))
A_budget = 0.5 * (lambda_local / 6.0) ** 2
# 纯数值安全底线:仅防止 scikit-fem 因浮点精度导致的退化/奇异。
# 不再用 0.25*A_budget —— RL 应自主学会"多细才够"
# 而非被人为启发式 (12 点/波长) 限制。
domain_area = float(np.prod(self.fem_problem.plot_boundary[2:] - self.fem_problem.plot_boundary[:2]))
V_min_safeguard = 1e-10 * domain_area
# Filter 1: numerical safeguard only — no physics heuristic
# Filter 1: numerical safeguard only
area_eligible = np.where(self.element_volumes > V_min_safeguard)[0]
# Filter 2: Reverse Dörfler — eliminate the noise tail, not select the elite.
# Sort η_K ascending; remove the smallest elements whose cumulative η²
# contributes < ε_noise of total error energy. These are numerically
# converged or noise — not worth the agent's attention.
# A 20% floor on the eligible ratio guarantees the agent meaningful
# choices even in heavy-tailed distributions where energy is concentrated.
# Filter 2: Reverse Dörfler — eliminate noise tail
eta_current = self._eta_indicator
eta_sq = eta_current ** 2
total_energy = np.sum(eta_sq)
if total_energy > 0:
idx_asc = np.argsort(eta_current) # ascending
idx_asc = np.argsort(eta_current)
cumsum_asc = np.cumsum(eta_sq[idx_asc])
eps_noise = 0.01 # bottom 1% of energy = noise tail
eps_noise = 0.01
k_dorfler = int(np.searchsorted(cumsum_asc, eps_noise * total_energy))
self._diag_dorfler_tail_ratio = float(k_dorfler) / max(self._num_elements, 1)
# floor: keep at least 20% of elements for RL agent choice
min_keep = max(1, self._num_elements // 5)
k = min(k_dorfler, self._num_elements - min_keep)
self._diag_dorfler_floor_active = k < k_dorfler
@ -597,26 +652,104 @@ class MeshRefinement(gym.Env):
eligible = np.intersect1d(area_eligible, error_eligible)
# ── correction centering (eligible only) ──
# Global shift is meaningless for top-k ranking; center within
# eligible candidates so the Actor only controls relative priority.
self._diag_corr_raw_mean = float(np.mean(corr_raw))
if len(eligible) > 0:
corr = corr_raw - np.mean(corr_raw[eligible])
else:
corr = corr_raw - np.mean(corr_raw)
score = log_eta + corr
# ── diagnostics ──
self._diag_neg_ratio = float(np.mean(delta < 0.0))
self._diag_corr_mean = float(np.mean(corr))
self._diag_corr_std = float(np.std(corr))
self._diag_corr_abs = float(np.mean(np.abs(corr)))
# Spearman-like: Pearson r between log_eta and score
le = log_eta - log_eta.mean()
sc = score - score.mean()
denom = np.sqrt(np.sum(le**2) * np.sum(sc**2))
self._diag_score_eta_corr = float(np.sum(le * sc) / max(denom, 1e-12))
self._diag_eligible_ratio = float(len(eligible)) / max(self._num_elements, 1)
self._diag_masked_ratio = (
1.0 - float(len(eligible)) / max(len(area_eligible), 1)
if len(area_eligible) > 0 else 0.0
)
# ── GVN global-conditioned correction diagnostics ──
# corr-rel_logeta correlation
rel_le = log_eta - log_eta.mean()
rel_corr = corr - corr.mean()
denom_rc = np.sqrt(np.sum(rel_le**2) * np.sum(rel_corr**2))
self._diag_corr_rel_eta_corr = float(
np.sum(rel_le * rel_corr) / max(denom_rc, 1e-12)
)
# correction by region (inside/outside scatterer)
eps_r = self._epsilon_r_elements
inside_mask = eps_r > 1.0
outside_mask = ~inside_mask
self._diag_corr_inside_mean = float(np.mean(corr[inside_mask])) if inside_mask.any() else 0.0
self._diag_corr_outside_mean = float(np.mean(corr[outside_mask])) if outside_mask.any() else 0.0
# correction by eta rank
eta = self._eta_indicator
k10 = max(1, int(0.1 * self._num_elements))
top_idx = np.argsort(eta)[-k10:]
low_idx = np.argsort(eta)[:self._num_elements // 2]
self._diag_corr_top_eta_mean = float(np.mean(corr[top_idx]))
self._diag_corr_low_eta_mean = float(np.mean(corr[low_idx]))
# global top10 eta energy and remaining ratio
eta_sq = eta ** 2
total_energy = float(np.sum(eta_sq))
self._diag_global_top10_eta_energy = (
float(np.sum(eta_sq[top_idx])) / (total_energy + 1e-12)
)
self._diag_remaining_ratio = remaining / max(self._n_budget, 1)
max_by_growth = max(1, self._num_elements // 4)
self._diag_max_by_growth = max_by_growth
self._diag_n_budget = self._n_budget
self._diag_remaining = remaining
self._diag_n_eligible = len(eligible)
num = min(
len(eligible),
max(1, self._num_elements // 4),
max_by_growth,
max_parents_by_budget,
)
if num <= 0:
self._diag_selected_count = 0
self._diag_n_next = self._num_elements
return np.array([], dtype=np.int64)
# x 越小 ⇒ 优先级越高(纯排序,不设正负门槛)
score = -x
selected = eligible[np.argsort(score[eligible])[-num:]]
# top-k by score descending with physical tie-breaking
# (avoids spatially arbitrary selection when scores are tied)
_fp = self.fem_problem.fem_problem
_cx = getattr(_fp, "_cx", 0.5)
_cy = getattr(_fp, "_cy", 0.5)
_radius = getattr(_fp, "_radius", 0.2)
_mesh = self.mesh
_p = _mesh.p
_t = _mesh.t
_mx = (_p[0, _t[0]] + _p[0, _t[1]] + _p[0, _t[2]]) / 3.0
_my = (_p[1, _t[0]] + _p[1, _t[1]] + _p[1, _t[2]]) / 3.0
_dist = np.sqrt((_mx - _cx)**2 + (_my - _cy)**2)
_sd = _dist - _radius
_inside = (_dist <= _radius).astype(np.float32)
_abs_sd = np.abs(_sd[eligible])
_inside_elig = _inside[eligible]
_tie_key = -_abs_sd + _inside_elig
_composite = score[eligible] * 1e6 + _tie_key
selected = eligible[np.argsort(_composite)[-num:]]
self._diag_selected_count = len(selected)
self._diag_n_next = self._num_elements + len(selected) * 3 # estimate
elements_to_refine = selected
elif self._refinement_strategy in ["absolute", "absolute_discrete"]:
@ -700,6 +833,93 @@ class MeshRefinement(gym.Env):
error_per_dim = np.sqrt(np.sum(error_per_element**2, axis=0))
return float(self.project_to_scalar(error_per_dim))
@property
def num_global_stats(self) -> int:
"""Number of global statistics attached to each graph observation."""
return 13
def _compute_global_stats(self) -> np.ndarray:
"""Compute graph-level global statistics for GVN conditioning.
Returns: np.ndarray of shape (num_global_stats,) with keys:
[0] remaining_ratio (N_budget - N_current) / N_budget
[1] step_ratio current_step / max_steps
[2] elem_ratio N_current / N_budget
[3] logeta_mean
[4] logeta_std
[5] logeta_max
[6] logeta_p90
[7] logeta_p75
[8] top10_eta_energy_ratio top 10% eta^2 energy / total
[9] eligible_ratio elements above area safeguard / total
[10] inside_eta_energy eta energy inside scatterer / total
[11] outside_eta_energy eta energy outside scatterer / total
[12] interface_eta_energy eta energy near interface / total
"""
eta = self._eta_indicator
N = self._num_elements
eps = 1e-12
# log-eta for percentile stats
eps_score = max(0.01 * float(np.median(eta)), 1e-12)
logeta = np.log(np.maximum(eta, 1e-30) + eps_score)
# budget stats
remaining = max(0, self._n_budget - N)
remaining_ratio = remaining / max(self._n_budget, 1)
step_ratio = self._timestep / max(self._max_timesteps, 1)
elem_ratio = N / max(self._n_budget, 1)
# logeta stats
le_mean = float(np.mean(logeta))
le_std = float(np.std(logeta)) + 1e-8
le_max = float(np.max(logeta))
le_p90 = float(np.percentile(logeta, 90))
le_p75 = float(np.percentile(logeta, 75))
# top 10% eta energy ratio
eta_sq = eta ** 2
total_energy = float(np.sum(eta_sq))
if total_energy > 0:
k10 = max(1, int(0.1 * N))
top10_idx = np.argsort(eta_sq)[-k10:]
top10_ratio = float(np.sum(eta_sq[top10_idx])) / (total_energy + eps)
else:
top10_ratio = 0.0
# eligible ratio (area safeguard)
domain_area = float(np.prod(
self.fem_problem.plot_boundary[2:] - self.fem_problem.plot_boundary[:2]
))
V_min = 1e-10 * domain_area
eligible_ratio = float(np.sum(self.element_volumes > V_min)) / max(N, 1)
# region eta energy ratios
eps_r = self._epsilon_r_elements
inside_mask = eps_r > 1.0
outside_mask = ~inside_mask
fp = self.fem_problem.fem_problem
cx = getattr(fp, "_cx", 0.5)
cy = getattr(fp, "_cy", 0.5)
radius = getattr(fp, "_radius", 0.2)
midpoints = self._element_midpoints
dist_raw = np.sqrt((midpoints[:, 0] - cx)**2 + (midpoints[:, 1] - cy)**2)
lam = 2.0 * np.pi / max(self._wave_number, 1e-8)
interface_mask = np.abs(dist_raw - radius) < 0.2 * lam
inside_energy = float(np.sum(eta_sq[inside_mask])) / (total_energy + eps)
outside_energy = float(np.sum(eta_sq[outside_mask])) / (total_energy + eps)
interface_energy = float(np.sum(eta_sq[interface_mask])) / (total_energy + eps)
stats = np.array([
remaining_ratio, step_ratio, elem_ratio,
le_mean, le_std, le_max, le_p90, le_p75,
top10_ratio, eligible_ratio,
inside_energy, outside_energy, interface_energy,
], dtype=np.float32)
return stats
@property
def last_observation(self) -> Data:
"""
@ -716,6 +936,10 @@ class MeshRefinement(gym.Env):
observation_graph = Data(**graph_dict)
observation_graph.eta = torch.tensor(self._eta_indicator, dtype=torch.float32)
observation_graph.area = torch.tensor(self.element_volumes, dtype=torch.float32)
observation_graph.global_stats = torch.tensor(
self._compute_global_stats(), dtype=torch.float32
).unsqueeze(0) # [1, num_global_stats]
return observation_graph
@ -932,25 +1156,16 @@ class MeshRefinement(gym.Env):
reward_per_agent = self.project_to_scalar(reward_per_agent_and_dim)
# ── Causal isolation + bounded signals ──
# r_local: clipped to [1, +1] — prevents pollution-error inversions
# (±4.6) from hijacking the Critic's value estimate.
# r_bonus: 0.5·tanh(η/μ 1) — linear near μ (preserves Dörfler),
# saturates at ±0.5 for extreme η, bounded and safe.
# Unrefined parents: r = 0 (causal isolation).
# ── Pure local improvement reward (no modulation, no bonus) ──
# r_i = clip(log(η_old) log(l2(η_child)), 0, rmax)
# L₂ aggregation guarantees r_local ≥ 0; clip lower bound at 0 as
# a safety floor against floating-point noise.
rmax = 2.0
unique_old, counts = np.unique(self.agent_mapping, return_counts=True)
refined_mask = np.zeros(len(reward_per_agent), dtype=bool)
refined_mask[unique_old[counts > 1]] = True
# Clip r_local to prevent outlier-driven value collapse
reward_per_agent = np.clip(reward_per_agent, -1.0, 1.0)
# Bounded state bonus: tanh preserves Dörfler near μ, caps at extreme η
eta_raw = self._previous_eta_indicator
mu_eta = float(np.mean(eta_raw))
reward_per_agent[refined_mask] += 0.5 * np.tanh(
eta_raw[refined_mask] / (mu_eta + 1e-8) - 1.0
)
reward_per_agent = np.clip(reward_per_agent, 0.0, rmax)
# Unrefined: clean zero (causal isolation)
reward_per_agent[~refined_mask] = 0.0
@ -958,31 +1173,30 @@ class MeshRefinement(gym.Env):
# apply action/element penalty (refined parents only)
element_penalty = np.zeros(len(reward_per_agent), dtype=reward_per_agent.dtype)
element_penalty[unique_old] = self._element_penalty_lambda * (counts - 1)
# Step 0 penalty scaling: reduce element penalty on first refinement step
# to prevent "reward < 0 but aw_rel improved" feedback inversion.
step0_scale = float(self._environment_config.get("step0_penalty_scale", 1.0))
penalty_scale = step0_scale if self._timestep == 1 else 1.0
element_penalty = element_penalty * penalty_scale
element_limit_penalty = (
(self._element_limit_penalty / self._previous_num_elements)
if self.reached_element_limits
else 0
)
# Step 0 diagnostics (r_local = reward before penalty, already clipped)
r_local_pre_penalty = reward_per_agent.copy()
_step0_penalty_scale = penalty_scale
_step0_avg_penalty = float(np.mean(element_penalty[refined_mask])) if refined_mask.any() else 0.0
_step0_avg_r_local = float(np.mean(r_local_pre_penalty[refined_mask])) if refined_mask.any() else 0.0
_step0_step_id = self._timestep
reward_per_agent = (
reward_per_agent - element_penalty - element_limit_penalty
)
# ── Global error change (diagnostic only, NOT injected into Actor reward) ──
# Removing global_bonus from per-element reward eliminates the broken causal
# chain: Helmholtz pollution error can make E_new > E_old even when the
# selected elements were the right choice, punishing agents for physics
# they didn't cause. Actor optimises r_local only; Critic captures global
# effects through value estimation.
l2_old = self._previous_solution_l2_norm
l2_new = self._compute_solution_l2_norm()
eta_l2_old = float(np.sqrt(np.sum(old_eta ** 2)))
eta_l2_new = float(np.sqrt(np.sum(new_eta ** 2)))
eps_l2 = 1e-12
E_old = eta_l2_old / max(l2_old, eps_l2)
E_new = eta_l2_new / max(l2_new, eps_l2)
global_bonus = self._global_reward_alpha * float(np.log(E_old + eps_l2) - np.log(E_new + eps_l2))
# global_bonus intentionally NOT added to reward_per_agent — see above.
self._reward_per_agent = reward_per_agent
self._cumulative_reward_per_agent = (
self._cumulative_reward_per_agent[self._previous_agent_mapping]
@ -991,11 +1205,15 @@ class MeshRefinement(gym.Env):
reward = reward_per_agent
reward_dict["reward"] = reward
reward_dict["potential_bonus"] = global_bonus
reward_dict["penalty"] = -reward
reward_dict["element_limit_penalty"] = element_limit_penalty
reward_dict["element_penalty"] = element_penalty
reward_dict["element_penalty_lambda"] = self._element_penalty_lambda
# Step 0 penalty scaling diagnostics
reward_dict["penalty_scale"] = _step0_penalty_scale
reward_dict["avg_penalty"] = _step0_avg_penalty
reward_dict["avg_r_local"] = _step0_avg_r_local
reward_dict["step_id"] = _step0_step_id
return reward, reward_dict
@property

469
outlook/README.md Normal file
View File

@ -0,0 +1,469 @@
# Outlook: GNN-Guided Adaptive Mesh Refinement for 2D Helmholtz Scattering
## 1. 问题定义
求解 2D 介质圆柱的电磁散射(散射场公式):
```
∇²u + k²·ε_r·u = k²·(ε_r 1)·u_inc
∂u/∂n i·k_local·u = 0 (Sommerfeld 辐射 BC)
```
- 入射波:`u_inc = exp(i·k·x)`参考解Mie 解析解
- 参数空间k∈[3,15]eps_r∈[2,8]cx/cy∈[0.2,0.8]radius∈[0.05,0.25]
- 核心目标:在参数化 Helmholtz 散射问题中,学习一个无在线求解的预算约束网格预测器,用于近似 residual-AMR 的最终加密分布,并在低预算下优于简单物理启发式网格。
---
## 2. 算法全流程
```
Step 1. 数据生成 (gen.py)
残差驱动 AMR → 每步保存 cell 状态 + 标签
Step 2. 训练 (train_correction.py)
features(15-dim) + physics_score → GNN → sigmoid → 二分类 teacher_mark
Step 3. 评估
3a. 离线指标 (test_correction.py) — top-k overlap / AUC
3b. Rollout 评估 (eval_correction.py) — 迭代加密 → FEM → aw_rel
Step 4. 可视化 (viz_correction.py)
amr 模式: GNN 驱动完整 AMR → 网格 + 场 + 误差
step 模式: 单步 GNN vs teacher vs physics 标记对比
```
---
## 3. 数据生成 (`gen.py`)
### 3.1 初始网格
物理自适应初始网格(`build_physics_safe_initial_mesh`
- 均匀基底网格 + 迭代局部加密
- 介质外h ≤ λ₀/q介质内h ≤ λ_eff/qq=2每波长 2 个单元)
- 加密准则:`score = max_edge / h_target > 1` 的单元被加密
- 二分搜索控制每批加密数量,不超预算
### 3.2 AMR 循环(每步保存)
```
初始 mesh → FEM solve → 残差估计器 η → teacher_mark (η top-k)
├── physics_score → physics_mark (top-k)
├── correction_label = teacher_mark physics_mark
└── 保存 .npz → 按 η 选单元加密 → 下一步
```
**残差估计器 η**`environment/helmholtz.py:_compute_residual_indicator`
- 内部残差:`h_K/k · √V_K · |k²ε_r·u_h + k²(ε_r1)·u_inc|`
- 梯度跳变:`√(½ Σ h_e/k · |[[∇u_h·n]]|²)`
- SBC 边界残差:`h_bnd/k · |∂u/∂n i·k_local·u|`
**标记策略**
- teacher_mark按 η 取 top `mark_fraction`(默认 3%
- physics_mark`physics_score` 取 top `mark_fraction`
- correction_label = +1teacher 独有、0一致1physics 独有)
**安全过滤**
1. 面积过滤:排除面积 ≤ 1e-10 的退化单元
2. 反向 Dörfler排除误差贡献最低 1% 的单元
### 3.3 输出格式
```
outlook/data_correction/
├── params_list.npz # (n_samples, 5) PDE 参数
├── sample0000_step000.npz # 逐样本逐步数据
└── summary.json
```
每个 step .npz
| 字段 | 形状 | 说明 |
|------|------|------|
| `features` | `(n_elem, 15)` | 几何/物理特征 |
| `edge_index` | `(2, n_edges)` | 网格图结构 |
| `physics_score` | `(n_elem,)` | h/λ_eff |
| `teacher_eta` | `(n_elem,)` | 残差估计器 |
| `teacher_mark` | `(n_elem,)` | η top-k 标记 (0/1) |
| `physics_mark` | `(n_elem,)` | physics top-k 标记 (0/1) |
| `correction_label` | `(n_elem,)` | teacher physics (1/0/+1) |
---
## 4. 特征工程
### 4.1 15 维基础特征
| 维 | 特征 | 说明 |
|----|------|------|
| 0,1 | x, y | 单元中点坐标 |
| 2 | area | 单元面积 |
| 3 | dist_to_center | 到圆柱中心距离 |
| 4 | signed_dist | dist radius负=介质内) |
| 5 | inside | 是否在圆柱内 (0/1) |
| 6 | k | 波数 |
| 7 | eps_r | 介电常数 |
| 8 | radius | 半径 |
| 9,10 | cx, cy | 圆柱中心 |
| 11 | k_h | k × √area |
| 12 | k_eps_h | k × √eps_r × √area |
| 13,14 | sin(k·x), cos(k·x) | 入射波相位 |
### 4.2 Physics Score第 16 维输入)
```python
lambda_eff = 2π / (k · √eps_r) # 介质内
或 2π / k # 介质外
physics_score = max_edge / lambda_eff # >1 = 分辨率不足
```
### 4.3 归一化
训练集 z-score 归一化,推理复用同一统计量:
```python
x_norm = (concat(features, physics_score) - mean) / scale
```
---
## 5. GNN 架构
### 5.1 CorrectionGNN
基于 DensityGNN 骨干,替换密度回归头为二分类 logit 头:
```
Input: (n_cells, 16) node features
├── node_embedding: Linear(16 → latent_dim)
├── edge_embedding: Linear(16 → latent_dim)
├── N × MessagePassingStep:
│ ├── EdgeModule: MLP([src | dst | edge_attr]) → latent_dim
│ ├── NodeModule: MLP([node | mean(incoming_edges)]) → latent_dim
│ └── LayerNorm + Residual
├── GlobalVirtualNode: mean_pool → attention_gate → broadcast
└── head: Linear(latent → hidden) → ReLU → Linear(hidden → 1 logit)
```
### 5.2 关键设计
- 边特征:`edge_attr = |x[src] - x[dst]|`(节点特征差绝对值)
- 边丢弃:训练 0.1,推理 0.0
- 损失BCEWithLogitsLoss + per-graph `pos_weight = neg/pos`
### 5.3 训练配置
| 参数 | 值 |
|------|-----|
| latent_dim | 64 |
| num_mp_steps | 3 |
| head_hidden | 64 |
| lr | 1e-3 |
| optimizer | Adam |
| scheduler | ReduceLROnPlateau (patience=10) |
---
## 6. 评估体系
### 6.1 离线指标test_correction.py
- **top-k overlap**GNN 概率最高的 k 个 cell 与 teacher_mark 的交集 / k
- **AUC**ROC-AUCGNN vs physics baseline
- **gnn_beats_physics_ratio**GNN 优于 physics 的样本比例
### 6.2 Rollout 评估eval_correction.py
从初始 mesh 出发,**无 teacher_eta无残差标记无中间 FEM solve**。每步只用分数决定加密单元,最终一次 FEM solve 计算误差。
三种方法:
| 方法 | 打分 | 说明 |
|------|------|------|
| `physics` | `physics_score` | 纯物理先验 |
| `neural` | `model(features + physics_score)` | 纯 GNN |
| `hybrid` | `α·zscore(physics) + β·zscore(neural)` | 混合(默认 α=β=0.5 |
### 6.3 误差指标
**aw_rel**(面积加权相对误差):
```
aw_rel = √( Σ err²_tri · area / Σ ref²_tri · area )
```
**max_err**(最大逐点误差):
```
max_err = max |Re(u_fem) Re(u_mie)|
```
---
## 7. 可视化
### 7.1 amr 模式
GNN 驱动完整 AMR每步 FEM solve展示网格和场演变。
输出:
- `amr_overview.png` — 所有步骤总览
- `amr_steps/step{XX}.png` — 每步 3 面板FEM 场 / Mie 参考 / 误差)
- `ground_truth.png` — 高保真参考解(`--compare` 时)
- `compare.png` — Physics vs GNN vs Eta 对比(`--compare` 时)
### 7.2 step 模式
重建指定 AMR 步的 mesh对比 GNN / teacher / physics 标记。
输出:
- `marks_*.png` — 2×2 对比图teacher / GNN / physics / TP/FP/FN/TN
- `field_gnn_*.png` — GNN 加密后 3 面板图
- `field_eta_*.png` — 传统 η 加密后 3 面板图
---
## 8. 使用方法
### 8.1 数据生成
```bash
python outlook/src/gen.py \
--n-samples 100 \
--max-elements 12000 \
--mark-fraction 0.03 \
--output-dir outlook/data_correction
```
### 8.2 训练
```bash
python outlook/src/train_correction.py \
--data-dir outlook/data_correction \
--epochs 100 \
--batch-size 32 \
--lr 1e-3 \
--device cuda \
--checkpoint-out outlook/ckpt/correction.pt
```
输出:
- `correction.pt` — 最终模型
- `correction_best.pt` — val_loss 最低 checkpoint
- `correction_train_log.json` — 逐 epoch 日志
### 8.3 离线指标评估
```bash
python outlook/src/test_correction.py \
--checkpoint outlook/ckpt/correction.pt \
--data-dir outlook/data_correction \
--device cuda \
--visualize \
--output-dir outlook/result/correction/test
```
### 8.4 Rollout 评估
```bash
python outlook/src/eval_correction.py \
--checkpoint outlook/ckpt/correction.pt \
--data-dir outlook/data_correction \
--target-elements 2000,4000,8000,12000 \
--mark-fraction 0.03 \
--max-steps 40 \
--methods physics,neural,hybrid \
--alpha 0.5 --beta 0.5 \
--device cuda \
--output-dir outlook/result/correction/rollout
```
输出:
- `eval_results.json` — 详细结果 + 聚合统计 + 改善比例
- `summary.csv` — 每行一个 (method, target) 汇总
- `aw_rel_vs_elements.png` / `max_err_vs_elements.png`
### 8.5 可视化
```bash
# 端到端 AMR
python outlook/src/viz_correction.py \
--checkpoint outlook/ckpt/correction.pt \
--data-dir outlook/data_correction \
--mode amr --sample-id 0 --max-elements 5000 \
--device cuda \
--output-dir outlook/result/correction/viz
# 单步对比
python outlook/src/viz_correction.py \
--checkpoint outlook/ckpt/correction.pt \
--data-dir outlook/data_correction \
--mode step --sample-id 0 --step 0 \
--device cuda \
--output-dir outlook/result/correction/viz
# OOD 评估(自定义物理参数,需同时指定 k, eps-r, cx, cy, radius
python outlook/src/viz_correction.py \
--checkpoint outlook/ckpt/correction.pt \
--data-dir outlook/data_correction \
--mode amr \
--k 30 --eps-r 4 --cx 0.5 --cy 0.5 --radius 0.15 \
--max-elements 6000 --compare \
--device cuda \
--output-dir outlook/result/correction/viz
```
---
## 9. 目录结构
```
outlook/
├── README.md # 本文档
├── train.sh # SLURM 训练脚本(可配置 batch_size默认 1
├── analyze_budget_teacher.py # Budget teacher 数据集分析
├── check_correction_data.py # 数据质量校验
├── src/
│ ├── gen.py # 数据生成
│ ├── train_correction.py # 训练
│ ├── test_correction.py # 离线指标评估
│ ├── eval_correction.py # Rollout 评估
│ ├── viz_correction.py # 可视化
│ ├── rollout.py # 统一 AMR rollout核心循环 + 共享 refinement 工具)
│ ├── gnn.py # DensityGNN 模型
│ ├── feat.py # 特征提取
│ ├── graph.py # mesh → PyG graph含 build_edge_index_np
│ ├── mesh.py # score → refined mesh
│ ├── metrics.py # aw_rel / max_err
│ ├── problem.py # PDE 参数 → HelmholtzProblem
│ └── amr.py # 残差 AMR teacher
├── ckpt/ # checkpoint
├── data_correction/ # 训练数据
└── result/ # 评估结果
```
---
## 10. 辅助模块
| 模块 | 职责 |
|------|------|
| `rollout.py` | 统一 AMR rollout`run_rollout_to_budget()` 驱动完整加密循环,支持 physics/neural/hybrid/eta 四种打分方法 |
| `feat.py` | 构建 15 维基础特征 + budget_code |
| `graph.py` | mesh → PyG Data边特征 = phase_distance |
| `mesh.py` | score → 迭代 top-k 加密(叶子继承初始单元 score |
| `problem.py` | 参数字典 → HelmholtzProblem 实例 |
| `amr.py` | 纯残差驱动 AMR teacher无网络 |
| `metrics.py` | `compute_mie_error`aw_rel + max_err |
---
## 11. 测试结果
### 11.1 训练配置
- 数据集100 样本,~19 步/样本1888 个 step 文件
- 训练/验证划分80/20按 sample_idseed=42
- 训练图1513 个验证图375 个
- 正样本比例3.0%teacher_mark top-3%
- 训练 100 epoch最佳 epoch 57val_loss=0.5873
### 11.2 离线指标test_correction.py
在 20 个验证样本的 375 个 step 图上评估:
| 指标 | GNN | Physics Baseline |
|------|-----|-----------------|
| **AUC** | **0.9412** | 0.0000 |
| **top-k overlap 均值** | **0.440** | 0.131 |
| GNN beats physics | **372/375 (99.2%)** | — |
> Physics AUC=0.0 是因为 physics_score 在均匀网格上只有 2 个离散值无法区分排序。GNN 通过学习 η 的空间分布模式top-k overlap 提升 **3.4 倍**
### 11.3 Rollout 评估eval_correction.py
20 个验证样本 × 3 种方法 × 3 个目标预算,每次 rollout 最终做 1 次 FEM solve
**aw_rel面积加权相对误差**
| 方法 | target=2000 | target=4000 | target=8000 |
|------|------------|------------|------------|
| physics | 19.19% | 14.04% | 13.25% |
| neural | 15.96% | 13.72% | 12.98% |
| **hybrid** | **15.78%** | **13.57%** | **12.94%** |
**max_err最大逐点误差**
| 方法 | target=2000 | target=4000 | target=8000 |
|------|------------|------------|------------|
| physics | 0.3301 | 0.2514 | 0.2418 |
| neural | 0.2741 | 0.2431 | 0.2376 |
| **hybrid** | **0.2686** | **0.2430** | **0.2383** |
**相对 physics-only 的改善**
| 方法 | target | aw_rel 改善 | max_err 改善 |
|------|--------|-----------|-------------|
| hybrid | 2000 | **+9.8%** | **+6.9%** |
| neural | 2000 | +9.9% | +6.2% |
| hybrid | 4000 | +1.9% | +1.5% |
| neural | 4000 | +1.1% | +1.4% |
| hybrid | 8000 | +1.9% | +1.3% |
| neural | 8000 | +1.8% | +1.5% |
### 11.4 关键发现
1. **GNN 显著优于 physics baseline**:离线 top-k overlap 从 0.131 提升到 0.4403.4×99.2% 的验证图上 GNN 胜出
2. **低预算改善最大**target=2000 时 aw_rel 改善 ~10%max_err 改善 ~7%;高预算时改善收窄到 ~2%(因为预算充足时 physics 也够用)
3. **hybrid 略优于 neural**z-score 混合策略在多数场景下比纯 GNN 更稳定
4. **GNN 推理效率**neural 方法比 physics 方法少用 ~12% 的 refinement 步数达到相同预算8.9 vs 9.4 步 @2000),因为 GNN 的标记更精准
### 11.5 OOD 评估k 超出训练范围 [3,15]
固定 `eps_r=4, cx=cy=0.5, radius=0.15`,在 k=20/30/50/80 上做三方法对比target=5000/10000/20000
**aw_rel (%)**
| k | 方法 | target=5000 | target=10000 | target=20000 |
|---|------|------------|-------------|-------------|
| 20 | physics | 32.17 | 24.55 | 10.61 |
| 20 | neural | 32.56 | **17.85** | 10.68 |
| 20 | hybrid | **31.73** | 19.57 | 14.03 |
| 30 | physics | 33.94 | 15.15 | 10.14 |
| 30 | neural | **23.52** | 13.40 | 8.38 |
| 30 | hybrid | 27.94 | **13.34** | **7.96** |
| 50 | physics | 94.00 | 73.56 | 33.00 |
| 50 | neural | **88.16** | **52.92** | **27.99** |
| 50 | hybrid | 90.97 | 68.47 | 29.27 |
| 80 | physics | 122.06 | 135.78 | 101.64 |
| 80 | neural | 139.84 | **127.65** | **92.67** |
| 80 | hybrid | 139.84 | 127.68 | 105.94 |
**相对 physics-only 的改善 (%)**
| k | 方法 | target=5000 | target=10000 | target=20000 |
|---|------|------------|-------------|-------------|
| 20 | neural | 1.2 | **+27.3** | 0.7 |
| 20 | hybrid | +1.4 | +20.3 | 32.3 |
| 30 | neural | **+30.7** | +11.6 | +17.4 |
| 30 | hybrid | +17.7 | +12.0 | +21.5 |
| 50 | neural | +6.2 | **+28.1** | **+15.2** |
| 50 | hybrid | +3.2 | +6.9 | +11.3 |
| 80 | neural | 14.6 | +6.0 | **+8.8** |
| 80 | hybrid | 14.6 | +6.0 | 4.2 |
**OOD 关键发现**
1. **neural 在远 OODk=50优势最大**target=10000 时 aw_rel 从 73.56% 降至 52.92%(改善 +28.1%hybrid 仅改善 +6.9%
2. **hybrid 的 physics 先验在 OOD 时成为拖累**z-score 混合中 physics_score 的 `max_edge/h_target` 在高 k 下不再准确,导致 hybrid 的标记不如纯 neural
3. **neural 的泛化来自几何特征**GNN 学到的是"界面附近 + 介质内 → 高密度"的空间模式,这一模式在 k 超出训练范围时仍然成立;而 physics_score 依赖 `lambda_eff = 2π/(k√ε_r)` 的具体数值
4. **极端 OODk=80两者都差**:误差超过 100%,说明 20000 个单元完全不够分辨 k=80 的短波结构λ_eff≈0.028,需要 ~70000+ 单元)
5. **k=20/30 时 hybrid 反而更好**:接近训练分布时 physics 先验有价值hybrid 通过混合两种信号获得更稳定的标记

View File

@ -0,0 +1,203 @@
#!/usr/bin/env python3
"""Analyze budgeted teacher dataset: per-budget stats, error improvement, saturation.
Usage:
python outlook/analyze_budget_teacher.py
python outlook/analyze_budget_teacher.py --data-dir outlook/data/budget_density_dataset_check
python outlook/analyze_budget_teacher.py --sat-threshold 0.05
"""
import argparse
from pathlib import Path
import numpy as np
def load_dataset(data_dir: Path):
"""Load all sample .npz files, return list of dicts."""
data_dir = Path(data_dir)
params_path = data_dir / "params_list.npz"
if params_path.exists():
n_samples = len(np.load(params_path)["params"])
else:
# Fall back to scanning for numbered .npz files
n_samples = len(sorted(data_dir.glob("[0-9]*.npz")))
samples = []
for sid in range(n_samples):
npz_path = data_dir / f"{sid:04d}.npz"
if not npz_path.exists():
continue
d = np.load(npz_path)
samples.append({
"sid": sid,
"budgets": d["budgets"],
"actual_elements": d["actual_elements"],
"teacher_aw_rel": d["teacher_aw_rel"],
"teacher_max_err": d["teacher_max_err"],
"valid_budgets": d["valid_budgets"] if "valid_budgets" in d else np.ones(len(d["budgets"]), dtype=bool),
"match_ratios": d["match_ratios"] if "match_ratios" in d else d["actual_elements"] / d["budgets"],
"k": float(d["k"]),
"eps_r": float(d["eps_r"]),
})
return samples
def per_budget_summary(samples):
"""Aggregate stats grouped by budget value."""
groups = {} # budget -> list of sample dicts
for s in samples:
for bi, b in enumerate(s["budgets"]):
b = int(b)
groups.setdefault(b, {"aw_rel": [], "max_err": [], "actual": [], "valid": []})
groups[b]["aw_rel"].append(s["teacher_aw_rel"][bi])
groups[b]["max_err"].append(s["teacher_max_err"][bi])
groups[b]["actual"].append(s["actual_elements"][bi])
groups[b]["valid"].append(bool(s["valid_budgets"][bi]))
rows = []
for b in sorted(groups.keys()):
g = groups[b]
aw = np.array(g["aw_rel"])
me = np.array(g["max_err"])
rows.append({
"budget": b,
"n_samples": len(aw),
"n_valid": sum(g["valid"]),
"mean_actual": np.mean(g["actual"]),
"mean_aw_rel": np.mean(aw),
"std_aw_rel": np.std(aw),
"mean_max_err": np.mean(me),
})
return rows
def compute_improvements(rows):
"""Compute Δaw_rel and relative improvement between adjacent budgets."""
for i in range(len(rows)):
rows[i]["delta_aw_rel"] = None
rows[i]["rel_improvement"] = None
for i in range(1, len(rows)):
prev = rows[i - 1]["mean_aw_rel"]
curr = rows[i]["mean_aw_rel"]
delta = prev - curr # positive = improvement
rows[i]["delta_aw_rel"] = delta
rows[i]["rel_improvement"] = delta / prev if prev > 0 else 0.0
return rows
def estimate_saturation(samples, threshold=0.02):
"""For each sample, find the first budget where further improvement < threshold.
Saturation budget = first budget b such that
(aw_rel[b-1] - aw_rel[b]) / aw_rel[b-1] < threshold
i.e. relative improvement from previous budget is below threshold.
Returns list of (sid, saturation_budget, sat_reason).
"""
results = []
for s in samples:
budgets = s["budgets"]
aw = s["teacher_aw_rel"]
sat_budget = None
for bi in range(1, len(budgets)):
if np.isnan(aw[bi - 1]) or np.isnan(aw[bi]):
continue
if aw[bi - 1] <= 0:
continue
rel_imp = (aw[bi - 1] - aw[bi]) / aw[bi - 1]
if rel_imp < threshold:
sat_budget = int(budgets[bi])
break
# If never saturated, mark as the largest budget
if sat_budget is None:
sat_budget = int(budgets[-1])
results.append({
"sid": s["sid"],
"saturation_budget": sat_budget,
})
return results
def print_table(rows):
"""Print the per-budget summary table."""
hdr = (f"{'budget':>8s} {'n':>4s} {'valid':>5s} "
f"{'mean_act':>9s} {'mean_aw':>9s} {'std_aw':>9s} {'mean_me':>9s} "
f"{'Δaw_rel':>9s} {'rel_imp':>9s}")
print(hdr)
print("-" * len(hdr))
for r in rows:
delta = f"{r['delta_aw_rel']:.6f}" if r["delta_aw_rel"] is not None else ""
rel = f"{r['rel_improvement']:.4f}" if r["rel_improvement"] is not None else ""
print(f"{r['budget']:8d} {r['n_samples']:4d} {r['n_valid']:5d} "
f"{r['mean_actual']:9.1f} {r['mean_aw_rel']:.6f} {r['std_aw_rel']:.6f} "
f"{r['mean_max_err']:.6f} {delta:>9s} {rel:>9s}")
def print_saturation(sat_results, threshold):
"""Print saturation budget distribution."""
budgets = [r["saturation_budget"] for r in sat_results]
unique = sorted(set(budgets))
print(f"\nSaturation budget distribution (threshold={threshold:.0%} rel. improvement):")
print(f" {'sat_budget':>12s} {'count':>6s} {'pct':>6s}")
for b in unique:
count = budgets.count(b)
pct = 100.0 * count / len(budgets)
print(f" {b:12d} {count:6d} {pct:5.1f}%")
print(f" {'median':>12s} {np.median(budgets):.0f}")
print(f" {'mean':>12s} {np.mean(budgets):.1f}")
def main():
parser = argparse.ArgumentParser(description="Analyze budgeted teacher dataset")
parser.add_argument("--data-dir", type=str, default="outlook/data",
help="Path to budget dataset directory (default: outlook/data)")
parser.add_argument("--sat-threshold", type=float, default=0.02,
help="Relative improvement threshold for saturation (default: 0.02)")
parser.add_argument("--sat-threshold-2", type=float, default=0.05,
help="Second saturation threshold to report (default: 0.05)")
args = parser.parse_args()
data_dir = Path(args.data_dir)
print("=" * 70)
print("Budget Teacher Dataset Analysis")
print("=" * 70)
print(f" Data dir: {data_dir}")
samples = load_dataset(data_dir)
print(f" Samples: {len(samples)}")
budgets_all = samples[0]["budgets"]
print(f" Budgets: {[int(b) for b in budgets_all]}")
print()
# 1. Per-budget summary
rows = per_budget_summary(samples)
rows = compute_improvements(rows)
print("Per-budget summary:")
print_table(rows)
# 2. Saturation analysis
for thresh, label in [(args.sat_threshold, "primary"),
(args.sat_threshold_2, "secondary")]:
sat = estimate_saturation(samples, threshold=thresh)
print_saturation(sat, thresh)
# 3. Per-sample detail (compact)
print(f"\nPer-sample detail:")
print(f" {'sid':>4s} {'k':>6s} {'eps_r':>6s} "
+ " ".join(f"b{int(b):>5d}" for b in budgets_all))
print(f" {'':>4s} {'':>6s} {'':>6s} "
+ " ".join(f"{'aw_rel':>5s}" for _ in budgets_all))
print(" " + "-" * (20 + 8 * len(budgets_all)))
for s in samples:
vals = " ".join(f"{v:.4f}" for v in s["teacher_aw_rel"])
print(f" {s['sid']:4d} {s['k']:6.2f} {s['eps_r']:6.2f} {vals}")
print("\nDone.")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,457 @@
#!/usr/bin/env python3
"""
check_correction_data.py Validate step-wise correction dataset.
Checks:
1. Global statistics (elements, marks, overlap, IoU, correction ratios)
2. Per-step trend analysis (correction signal vs AMR step)
3. Spatial visualization of 5 random samples
4. Field & shape consistency
5. Final verdict: PASS / WARNING / FAIL
Usage:
python outlook/check_correction_data.py \
--data-dir outlook/data_correction \
--output-dir outlook/data_correction_check
"""
import argparse
import sys
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
# ──────────────────────────────────────────────────────────────────────────────
# Required fields and their expected properties
# ──────────────────────────────────────────────────────────────────────────────
REQUIRED_FIELDS = {
"features": {"ndim": 2, "dtype_kind": "f"},
"edge_index": {"ndim": 2, "dtype_kind": "i"},
"physics_score": {"ndim": 1, "dtype_kind": "f"},
"teacher_eta": {"ndim": 1, "dtype_kind": "f"},
"teacher_mark": {"ndim": 1, "dtype_kind": "i"},
"physics_mark": {"ndim": 1, "dtype_kind": "i"},
"correction_label": {"ndim": 1, "dtype_kind": "i"},
"elements": {"ndim": 0},
"step": {"ndim": 0},
"aw_rel_before": {"ndim": 0},
"max_err_before": {"ndim": 0},
"k": {"ndim": 0},
"eps_r": {"ndim": 0},
"cx": {"ndim": 0},
"cy": {"ndim": 0},
"radius": {"ndim": 0},
}
# ──────────────────────────────────────────────────────────────────────────────
# Helpers
# ──────────────────────────────────────────────────────────────────────────────
def iou_binary(a: np.ndarray, b: np.ndarray) -> float:
"""IoU between two binary (0/1) arrays."""
a = a.astype(bool)
b = b.astype(bool)
inter = np.sum(a & b)
union = np.sum(a | b)
return float(inter) / float(union) if union > 0 else 1.0
def load_all_samples(data_dir: Path):
"""Load all sampleXXXX_stepYYY.npz files, return list of dicts."""
files = sorted(data_dir.glob("sample*_step*.npz"))
samples = []
for f in files:
try:
d = dict(np.load(f, allow_pickle=True))
d["_path"] = str(f)
d["_name"] = f.name
samples.append(d)
except Exception as e:
print(f" [WARN] Cannot load {f.name}: {e}")
return samples
# ──────────────────────────────────────────────────────────────────────────────
# 1. Field & shape consistency
# ──────────────────────────────────────────────────────────────────────────────
def check_fields(samples, issues):
"""Check every file has the required fields with correct ndim."""
print("\n" + "=" * 70)
print(" [1/5] Field & Shape Consistency")
print("=" * 70)
n_files = len(samples)
n_issues_before = len(issues)
for s in samples:
name = s["_name"]
for field, spec in REQUIRED_FIELDS.items():
if field not in s:
issues.append(f"MISSING field '{field}' in {name}")
continue
arr = s[field]
if arr.ndim != spec["ndim"]:
issues.append(
f"BAD ndim for '{field}' in {name}: "
f"expected {spec['ndim']}, got {arr.ndim}"
)
if "dtype_kind" in spec and arr.dtype.kind != spec["dtype_kind"]:
issues.append(
f"BAD dtype for '{field}' in {name}: "
f"expected kind={spec['dtype_kind']}, got {arr.dtype}"
)
# Cross-check: features rows == len(physics_score) == elements
if "features" in s and "physics_score" in s:
nf = s["features"].shape[0]
ns = len(s["physics_score"])
ne = int(s["elements"])
if not (nf == ns == ne):
issues.append(
f"SIZE MISMATCH in {name}: features={nf}, "
f"physics_score={ns}, elements={ne}"
)
n_new = len(issues) - n_issues_before
if n_new == 0:
print(f" All {n_files} files have consistent fields & shapes.")
else:
print(f" Found {n_new} field/shape issues.")
# ──────────────────────────────────────────────────────────────────────────────
# 2. Global statistics
# ──────────────────────────────────────────────────────────────────────────────
def compute_global_stats(samples):
"""Compute and print global statistics across all files."""
print("\n" + "=" * 70)
print(" [2/5] Global Statistics")
print("=" * 70)
elems = []
teacher_ratios = []
physics_ratios = []
overlaps = []
overlap_teacher = []
overlap_physics = []
ious = []
pos_ratios = []
neg_ratios = []
for s in samples:
n = int(s["elements"])
tm = s["teacher_mark"]
pm = s["physics_mark"]
cl = s["correction_label"]
elems.append(n)
n_tm = int(np.sum(tm))
n_pm = int(np.sum(pm))
ov = int(np.sum((tm == 1) & (pm == 1)))
teacher_ratios.append(n_tm / max(n, 1))
physics_ratios.append(n_pm / max(n, 1))
overlaps.append(ov)
overlap_teacher.append(ov / max(n_tm, 1))
overlap_physics.append(ov / max(n_pm, 1))
ious.append(iou_binary(tm, pm))
pos_ratios.append(int(np.sum(cl == 1)) / max(n, 1))
neg_ratios.append(int(np.sum(cl == -1)) / max(n, 1))
stats = {
"total_files": len(samples),
"elements": elems,
"teacher_mark_ratio": teacher_ratios,
"physics_mark_ratio": physics_ratios,
"overlap_count": overlaps,
"overlap/teacher": overlap_teacher,
"overlap/physics": overlap_physics,
"IoU": ious,
"correction_pos_ratio": pos_ratios,
"correction_neg_ratio": neg_ratios,
}
print(f" {'Metric':<30s} {'mean':>10s} {'min':>10s} {'max':>10s}")
print(f" {'-'*30} {'-'*10} {'-'*10} {'-'*10}")
for key in [
"elements", "teacher_mark_ratio", "physics_mark_ratio",
"overlap_count", "overlap/teacher", "overlap/physics",
"IoU", "correction_pos_ratio", "correction_neg_ratio",
]:
arr = np.array(stats[key])
print(f" {key:<30s} {arr.mean():10.4f} {arr.min():10.4f} {arr.max():10.4f}")
return stats
# ──────────────────────────────────────────────────────────────────────────────
# 3. Per-step trend analysis
# ──────────────────────────────────────────────────────────────────────────────
def check_per_step_trends(samples, issues):
"""Group by step, print trend table, flag anomalies."""
print("\n" + "=" * 70)
print(" [3/5] Per-Step Trend Analysis")
print("=" * 70)
from collections import defaultdict
step_data = defaultdict(lambda: {
"elems": [], "tm_ratio": [], "pm_ratio": [],
"overlap": [], "iou": [], "pos_ratio": [], "neg_ratio": [],
})
for s in samples:
step = int(s["step"])
n = int(s["elements"])
tm = s["teacher_mark"]
pm = s["physics_mark"]
cl = s["correction_label"]
n_tm = int(np.sum(tm))
n_pm = int(np.sum(pm))
ov = int(np.sum((tm == 1) & (pm == 1)))
d = step_data[step]
d["elems"].append(n)
d["tm_ratio"].append(n_tm / max(n, 1))
d["pm_ratio"].append(n_pm / max(n, 1))
d["overlap"].append(ov)
d["iou"].append(iou_binary(tm, pm))
d["pos_ratio"].append(int(np.sum(cl == 1)) / max(n, 1))
d["neg_ratio"].append(int(np.sum(cl == -1)) / max(n, 1))
steps = sorted(step_data.keys())
header = f" {'step':>4s} {'n_files':>7s} {'mean_elem':>9s} {'tm_ratio':>9s} {'pm_ratio':>9s} {'IoU':>7s} {'pos%':>7s} {'neg%':>7s}"
print(header)
print(f" {'-'*4} {'-'*7} {'-'*9} {'-'*9} {'-'*9} {'-'*7} {'-'*7} {'-'*7}")
for st in steps:
d = step_data[st]
nf = len(d["elems"])
me = np.mean(d["elems"])
mt = np.mean(d["tm_ratio"])
mp = np.mean(d["pm_ratio"])
mi = np.mean(d["iou"])
mpos = np.mean(d["pos_ratio"])
mneg = np.mean(d["neg_ratio"])
print(f" {st:4d} {nf:7d} {me:9.1f} {mt:9.4f} {mp:9.4f} {mi:7.4f} {mpos:7.4f} {mneg:7.4f}")
# Trend checks
if len(steps) >= 3:
# Check: correction signal should diminish at late steps (elements grow → marks shrink)
late = steps[-1]
early = steps[0]
late_pos = np.mean(step_data[late]["pos_ratio"])
early_pos = np.mean(step_data[early]["pos_ratio"])
late_neg = np.mean(step_data[late]["neg_ratio"])
early_neg = np.mean(step_data[early]["neg_ratio"])
late_iou = np.mean(step_data[late]["iou"])
early_iou = np.mean(step_data[early]["iou"])
print(f"\n Trend summary (step {early}{late}):")
print(f" pos_ratio: {early_pos:.4f}{late_pos:.4f}")
print(f" neg_ratio: {early_neg:.4f}{late_neg:.4f}")
print(f" IoU: {early_iou:.4f}{late_iou:.4f}")
# Warning: if late-step IoU is much lower than early, marks diverge
if late_iou < early_iou * 0.5:
issues.append(
f"TREND: IoU drops significantly from step {early} ({early_iou:.3f}) "
f"to step {late} ({late_iou:.3f}) — marks diverge at late steps."
)
# Warning: if correction signal vanishes (pos+neg → 0) at late steps
late_corr = late_pos + late_neg
early_corr = early_pos + early_neg
if early_corr > 0.01 and late_corr < early_corr * 0.1:
issues.append(
f"TREND: Correction signal nearly vanishes at step {late} "
f"({late_corr:.4f}) vs step {early} ({early_corr:.4f})."
)
# Warning: if IoU is very low overall (teacher and physics barely agree)
all_ious = [iou for s in steps for iou in step_data[s]["iou"]]
overall_iou = np.mean(all_ious)
if overall_iou < 0.1:
issues.append(
f"LOW IoU: mean IoU across all steps is {overall_iou:.4f}"
f"teacher_mark and physics_mark barely overlap."
)
# ──────────────────────────────────────────────────────────────────────────────
# 4. Spatial visualization
# ──────────────────────────────────────────────────────────────────────────────
def plot_spatial_samples(samples, output_dir: Path, n_plot: int = 5):
"""Randomly select n_plot files and save 4-panel spatial maps."""
print("\n" + "=" * 70)
print(" [4/5] Spatial Visualization")
print("=" * 70)
output_dir.mkdir(parents=True, exist_ok=True)
rng = np.random.RandomState(1234)
indices = rng.choice(len(samples), size=min(n_plot, len(samples)), replace=False)
selected = [samples[i] for i in sorted(indices)]
for s in selected:
name = s["_name"].replace(".npz", "")
features = s["features"]
x = features[:, 0]
y = features[:, 1]
physics_score = s["physics_score"]
teacher_eta = s["teacher_eta"]
teacher_mark = s["teacher_mark"]
correction_label = s["correction_label"]
fig, axes = plt.subplots(1, 4, figsize=(20, 5))
fig.suptitle(
f"{name} (elem={int(s['elements'])}, k={float(s['k']):.2f}, "
f"step={int(s['step'])})",
fontsize=12,
)
# Panel 1: physics_score
sc0 = axes[0].scatter(x, y, c=physics_score, s=8, cmap="viridis")
axes[0].set_title("physics_score")
axes[0].set_aspect("equal")
plt.colorbar(sc0, ax=axes[0], shrink=0.8)
# Panel 2: teacher_eta
sc1 = axes[1].scatter(x, y, c=teacher_eta, s=8, cmap="magma")
axes[1].set_title("teacher_eta")
axes[1].set_aspect("equal")
plt.colorbar(sc1, ax=axes[1], shrink=0.8)
# Panel 3: teacher_mark
sc2 = axes[2].scatter(x, y, c=teacher_mark, s=8, cmap="Reds", vmin=0, vmax=1)
axes[2].set_title("teacher_mark")
axes[2].set_aspect("equal")
plt.colorbar(sc2, ax=axes[2], shrink=0.8)
# Panel 4: correction_label
sc3 = axes[3].scatter(
x, y, c=correction_label, s=8, cmap="coolwarm", vmin=-1, vmax=1
)
axes[3].set_title("correction_label")
axes[3].set_aspect("equal")
plt.colorbar(sc3, ax=axes[3], shrink=0.8)
for ax in axes:
ax.set_xlabel("x")
ax.set_ylabel("y")
plt.tight_layout()
out_path = output_dir / f"{name}.png"
fig.savefig(out_path, dpi=150, bbox_inches="tight")
plt.close(fig)
print(f" ✓ Saved: {out_path}")
# ──────────────────────────────────────────────────────────────────────────────
# 5. Verdict
# ──────────────────────────────────────────────────────────────────────────────
def print_verdict(issues):
"""Print final PASS / WARNING / FAIL."""
print("\n" + "=" * 70)
print(" [5/5] Verdict")
print("=" * 70)
if not issues:
print("\n ✅ PASS — Data is consistent and ready for training.")
return "PASS"
# Classify issues
errors = [i for i in issues if any(k in i.upper() for k in ["MISSING", "BAD ", "MISMATCH"])]
warnings = [i for i in issues if i not in errors]
if errors:
print(f"\n ❌ FAIL — {len(errors)} error(s) found:")
for e in errors:
print(f"{e}")
if warnings:
print(f"\n ⚠️ Also {len(warnings)} warning(s):")
for w in warnings:
print(f"{w}")
return "FAIL"
print(f"\n ⚠️ WARNING — {len(warnings)} suspicious pattern(s):")
for w in warnings:
print(f"{w}")
return "WARNING"
# ──────────────────────────────────────────────────────────────────────────────
# Main
# ──────────────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="Check step-wise correction dataset quality"
)
parser.add_argument(
"--data-dir", type=str, required=True,
help="Directory containing sampleXXXX_stepYYY.npz files",
)
parser.add_argument(
"--output-dir", type=str, default="outlook/data_correction_check",
help="Directory to save visualization plots",
)
args = parser.parse_args()
data_dir = Path(args.data_dir)
output_dir = Path(args.output_dir)
print("=" * 70)
print(" Correction Data Quality Check")
print("=" * 70)
print(f" Data dir: {data_dir}")
print(f" Output dir: {output_dir}")
# Load
print("\n Loading files...")
samples = load_all_samples(data_dir)
if not samples:
print(" ✗ No sample files found!")
sys.exit(1)
print(f" Loaded {len(samples)} files.")
issues = []
# 1. Field consistency
check_fields(samples, issues)
# 2. Global stats
compute_global_stats(samples)
# 3. Per-step trends
check_per_step_trends(samples, issues)
# 4. Visualization
plot_spatial_samples(samples, output_dir)
# 5. Verdict
verdict = print_verdict(issues)
print()
return 0 if verdict == "PASS" else 1
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,47 @@
{
"config": {
"data_dir": "outlook/data/budget_density_dataset_debug",
"epochs": 2,
"batch_size": 2,
"lr": 0.001,
"density_weight": 1.0,
"mono_weight": 0.0,
"seed": 42
},
"per_budget": {
"budget_1000": {
"mse": 2.209566116333008,
"corr": -0.5653519502499024,
"top20_overlap": 0.037037037037037035,
"n_graphs": 1
},
"budget_2000": {
"mse": 4.281199932098389,
"corr": -0.4546216071701198,
"top20_overlap": 0.07407407407407407,
"n_graphs": 1
},
"overall": {
"mse": 3.2453832626342773,
"corr": -0.4747666722392795
}
},
"training_log": [
{
"epoch": 0,
"train_loss": 10.175954818725586,
"val_loss": 3.5997772216796875,
"val_corr": -0.5083683560193551,
"val_top20_overlap": 0.07407407407407407,
"lr": 0.001
},
{
"epoch": 1,
"train_loss": 7.313832759857178,
"val_loss": 3.2453830242156982,
"val_corr": -0.4747666660841676,
"val_top20_overlap": 0.05555555555555555,
"lr": 0.001
}
]
}

Binary file not shown.

BIN
outlook/ckpt/correction.pt Normal file

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,902 @@
[
{
"epoch": 0,
"train_loss": 1.214073695242405,
"val_loss": 1.1141467094421387,
"val_auc": 0.7793237914165878,
"val_topk_overlap": 0.18174046167047223,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 1,
"train_loss": 0.9936319440603256,
"val_loss": 0.8714833855628967,
"val_auc": 0.868452222365281,
"val_topk_overlap": 0.25161985623836425,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 2,
"train_loss": 0.8490312211215496,
"val_loss": 0.8512038290500641,
"val_auc": 0.8743725472266488,
"val_topk_overlap": 0.27610961191386213,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 3,
"train_loss": 0.789196622868379,
"val_loss": 0.8102231820424398,
"val_auc": 0.8853419495372643,
"val_topk_overlap": 0.27663024403011677,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 4,
"train_loss": 0.7635955549776554,
"val_loss": 0.7610340118408203,
"val_auc": 0.897967248359232,
"val_topk_overlap": 0.3101760507386457,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 5,
"train_loss": 0.7362100593745708,
"val_loss": 0.748906339208285,
"val_auc": 0.9006133073599438,
"val_topk_overlap": 0.31304398098650305,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 6,
"train_loss": 0.7291262559592724,
"val_loss": 0.738037109375,
"val_auc": 0.9036701915596148,
"val_topk_overlap": 0.32063751054106754,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 7,
"train_loss": 0.7118012420833111,
"val_loss": 0.7175028175115585,
"val_auc": 0.9082043418016371,
"val_topk_overlap": 0.33353017529134166,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 8,
"train_loss": 0.7020367321868738,
"val_loss": 0.7400095015764236,
"val_auc": 0.9038924270293778,
"val_topk_overlap": 0.3313267280645091,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 9,
"train_loss": 0.6895044669508934,
"val_loss": 0.7122037708759308,
"val_auc": 0.9097822096475472,
"val_topk_overlap": 0.33196020452732494,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 10,
"train_loss": 0.6835353250304858,
"val_loss": 0.7104852745930353,
"val_auc": 0.9102591502276932,
"val_topk_overlap": 0.3377191635459625,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 11,
"train_loss": 0.6724687827130159,
"val_loss": 0.7091294328371683,
"val_auc": 0.9097279573159085,
"val_topk_overlap": 0.32852482645677916,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 12,
"train_loss": 0.6608287704487642,
"val_loss": 0.7088778764009476,
"val_auc": 0.9102058348049611,
"val_topk_overlap": 0.3395986399411693,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 13,
"train_loss": 0.6516422840456167,
"val_loss": 0.6851696570714315,
"val_auc": 0.9161228078916188,
"val_topk_overlap": 0.3576108115171014,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 14,
"train_loss": 0.641690748433272,
"val_loss": 0.6870623429616293,
"val_auc": 0.9166719819324596,
"val_topk_overlap": 0.35467355015247576,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 15,
"train_loss": 0.6213903104265531,
"val_loss": 0.7060682823260626,
"val_auc": 0.9133957909689026,
"val_topk_overlap": 0.3541738121319473,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 16,
"train_loss": 0.6141671799123287,
"val_loss": 0.6649165799220403,
"val_auc": 0.9223772612302986,
"val_topk_overlap": 0.3727640261277016,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 17,
"train_loss": 0.6156036208073298,
"val_loss": 0.6852359771728516,
"val_auc": 0.917546789308805,
"val_topk_overlap": 0.3619924910260988,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 18,
"train_loss": 0.6078479687372843,
"val_loss": 0.6580855945746104,
"val_auc": 0.9232592198731486,
"val_topk_overlap": 0.36904381643112044,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 19,
"train_loss": 0.6046239572266737,
"val_loss": 0.6743976672490438,
"val_auc": 0.9211166203403452,
"val_topk_overlap": 0.3716380846859304,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 20,
"train_loss": 0.6046414325634638,
"val_loss": 0.6747777163982391,
"val_auc": 0.9219827510935892,
"val_topk_overlap": 0.36950146318342597,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 21,
"train_loss": 0.5860095359385014,
"val_loss": 0.6683605263630549,
"val_auc": 0.9212946098630218,
"val_topk_overlap": 0.36769860427540924,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 22,
"train_loss": 0.5924560849865278,
"val_loss": 0.6608883539835612,
"val_auc": 0.9237303628010585,
"val_topk_overlap": 0.38014786877240736,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 23,
"train_loss": 0.5699630603194237,
"val_loss": 0.6451186140378317,
"val_auc": 0.9271866474545916,
"val_topk_overlap": 0.3881152462988639,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 24,
"train_loss": 0.5860506445169449,
"val_loss": 0.6344831412037214,
"val_auc": 0.9279389861019536,
"val_topk_overlap": 0.37646285728709716,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 25,
"train_loss": 0.5696679763495922,
"val_loss": 0.651521734893322,
"val_auc": 0.926708376460331,
"val_topk_overlap": 0.3929882611353039,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 26,
"train_loss": 0.5605074142416319,
"val_loss": 0.6179426908493042,
"val_auc": 0.9313555002762031,
"val_topk_overlap": 0.38825309368394906,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 27,
"train_loss": 0.5452606951196989,
"val_loss": 0.6548436383406321,
"val_auc": 0.9257514085577712,
"val_topk_overlap": 0.3863040639453161,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 28,
"train_loss": 0.5649935603141785,
"val_loss": 0.6346206516027451,
"val_auc": 0.9282934601660542,
"val_topk_overlap": 0.3894207633159005,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 29,
"train_loss": 0.5343000267942747,
"val_loss": 0.6193932965397835,
"val_auc": 0.9335666994117104,
"val_topk_overlap": 0.40213981828158085,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 30,
"train_loss": 0.5222514060636362,
"val_loss": 0.6292086839675903,
"val_auc": 0.9315723170567752,
"val_topk_overlap": 0.4018058930665952,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 31,
"train_loss": 0.5288418171306452,
"val_loss": 0.6447375019391378,
"val_auc": 0.9296940196560114,
"val_topk_overlap": 0.3995336538081403,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 32,
"train_loss": 0.5321331315984329,
"val_loss": 0.6151211510101954,
"val_auc": 0.9339459494864105,
"val_topk_overlap": 0.41235340711455215,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 33,
"train_loss": 0.530803769826889,
"val_loss": 0.6180161188046137,
"val_auc": 0.9337274851175723,
"val_topk_overlap": 0.40770859907428864,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 34,
"train_loss": 0.5169201834748188,
"val_loss": 0.6140060871839523,
"val_auc": 0.9357011768509869,
"val_topk_overlap": 0.41396157069825373,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 35,
"train_loss": 0.5327582576622566,
"val_loss": 0.5903470193346342,
"val_auc": 0.9380921686251513,
"val_topk_overlap": 0.41993146997097147,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 36,
"train_loss": 0.5328234620392323,
"val_loss": 0.619572805861632,
"val_auc": 0.9348548623436956,
"val_topk_overlap": 0.42379147798654376,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 37,
"train_loss": 0.5179037377238274,
"val_loss": 0.6465846449136734,
"val_auc": 0.9321262023401722,
"val_topk_overlap": 0.4210270920253441,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 38,
"train_loss": 0.5000059170027574,
"val_loss": 0.6163648466269175,
"val_auc": 0.9355412748925351,
"val_topk_overlap": 0.4168377015768949,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 39,
"train_loss": 0.5333476103842258,
"val_loss": 0.601667749385039,
"val_auc": 0.9388670833680122,
"val_topk_overlap": 0.4324906240826149,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 40,
"train_loss": 0.49654081215461093,
"val_loss": 0.6376668587327003,
"val_auc": 0.9323076689591827,
"val_topk_overlap": 0.42067692553266867,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 41,
"train_loss": 0.49814322777092457,
"val_loss": 0.5965311030546824,
"val_auc": 0.9379983546564077,
"val_topk_overlap": 0.4248828237011408,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 42,
"train_loss": 0.4916490235676368,
"val_loss": 0.6603853727380434,
"val_auc": 0.9284689503947617,
"val_topk_overlap": 0.40912795311449807,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 43,
"train_loss": 0.5014090954015652,
"val_loss": 0.606838454802831,
"val_auc": 0.9374487658477999,
"val_topk_overlap": 0.42794334160595554,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 44,
"train_loss": 0.4867809346566598,
"val_loss": 0.5999851673841476,
"val_auc": 0.9387288792950925,
"val_topk_overlap": 0.43031923154259843,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 45,
"train_loss": 0.47708118582765263,
"val_loss": 0.5924614369869232,
"val_auc": 0.9402622606363618,
"val_topk_overlap": 0.4322550191316707,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.001
},
{
"epoch": 46,
"train_loss": 0.47802630874017876,
"val_loss": 0.6203606501221657,
"val_auc": 0.9358948083662981,
"val_topk_overlap": 0.4312379379630456,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.0005
},
{
"epoch": 47,
"train_loss": 0.4643762943645318,
"val_loss": 0.6020812268058459,
"val_auc": 0.9401552072421557,
"val_topk_overlap": 0.4339950425104022,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.0005
},
{
"epoch": 48,
"train_loss": 0.45041724294424057,
"val_loss": 0.6030316427350044,
"val_auc": 0.940497758680752,
"val_topk_overlap": 0.44206971785040405,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.0005
},
{
"epoch": 49,
"train_loss": 0.446329349031051,
"val_loss": 0.607378251850605,
"val_auc": 0.9405052575110606,
"val_topk_overlap": 0.44424490947288175,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.0005
},
{
"epoch": 50,
"train_loss": 0.4445270175735156,
"val_loss": 0.6005118936300278,
"val_auc": 0.940766237758347,
"val_topk_overlap": 0.43260791390184855,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.0005
},
{
"epoch": 51,
"train_loss": 0.4460391675432523,
"val_loss": 0.6029875800013542,
"val_auc": 0.9409514039912139,
"val_topk_overlap": 0.45019310765449216,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.0005
},
{
"epoch": 52,
"train_loss": 0.4418986613551776,
"val_loss": 0.6034468238552412,
"val_auc": 0.9406844266315528,
"val_topk_overlap": 0.44199077988820984,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.0005
},
{
"epoch": 53,
"train_loss": 0.45017045301695663,
"val_loss": 0.6176436940828959,
"val_auc": 0.9395928665376544,
"val_topk_overlap": 0.43845852941961805,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.0005
},
{
"epoch": 54,
"train_loss": 0.4438604520012935,
"val_loss": 0.6102191135287285,
"val_auc": 0.9402374042257553,
"val_topk_overlap": 0.44187147223059947,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.0005
},
{
"epoch": 55,
"train_loss": 0.43919234598676365,
"val_loss": 0.6168825899561247,
"val_auc": 0.9411819435660866,
"val_topk_overlap": 0.4501981058770216,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.0005
},
{
"epoch": 56,
"train_loss": 0.44451989606022835,
"val_loss": 0.6126844038565954,
"val_auc": 0.9395184630110243,
"val_topk_overlap": 0.43740834990699734,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.0005
},
{
"epoch": 57,
"train_loss": 0.4568589733292659,
"val_loss": 0.587329626083374,
"val_auc": 0.941194203700295,
"val_topk_overlap": 0.4401501924816575,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.0005
},
{
"epoch": 58,
"train_loss": 0.44505749208231765,
"val_loss": 0.6224502697587013,
"val_auc": 0.9398294219991805,
"val_topk_overlap": 0.44548887871606835,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.0005
},
{
"epoch": 59,
"train_loss": 0.43547111190855503,
"val_loss": 0.5913231869538625,
"val_auc": 0.9435686505726166,
"val_topk_overlap": 0.45369382113032325,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.0005
},
{
"epoch": 60,
"train_loss": 0.43305354565382004,
"val_loss": 0.6314671859145164,
"val_auc": 0.9385988844361027,
"val_topk_overlap": 0.4377768264505535,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.0005
},
{
"epoch": 61,
"train_loss": 0.4373071851829688,
"val_loss": 0.6283594146370888,
"val_auc": 0.9401304130793475,
"val_topk_overlap": 0.4329978451141574,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.0005
},
{
"epoch": 62,
"train_loss": 0.43299777743717033,
"val_loss": 0.6291215494275093,
"val_auc": 0.9410761593885777,
"val_topk_overlap": 0.44497516710926627,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.0005
},
{
"epoch": 63,
"train_loss": 0.42864579521119595,
"val_loss": 0.6106193959712982,
"val_auc": 0.9424943034022305,
"val_topk_overlap": 0.4532936274616251,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.0005
},
{
"epoch": 64,
"train_loss": 0.42673514783382416,
"val_loss": 0.6450835888584455,
"val_auc": 0.9371212242970672,
"val_topk_overlap": 0.43187084953076466,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.0005
},
{
"epoch": 65,
"train_loss": 0.4351537538071473,
"val_loss": 0.6039384330312411,
"val_auc": 0.9416574503767061,
"val_topk_overlap": 0.4413121605675935,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.0005
},
{
"epoch": 66,
"train_loss": 0.4369606903443734,
"val_loss": 0.6139999578396479,
"val_auc": 0.9417945131346139,
"val_topk_overlap": 0.44869988908245123,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.0005
},
{
"epoch": 67,
"train_loss": 0.42625842802226543,
"val_loss": 0.614420086145401,
"val_auc": 0.9418002442661444,
"val_topk_overlap": 0.4466080667540172,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.0005
},
{
"epoch": 68,
"train_loss": 0.4289086237549782,
"val_loss": 0.6421931609511375,
"val_auc": 0.9381999297311365,
"val_topk_overlap": 0.4407300883699639,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.00025
},
{
"epoch": 69,
"train_loss": 0.4162418972700834,
"val_loss": 0.6197224507729212,
"val_auc": 0.9418537016432265,
"val_topk_overlap": 0.4524438704921057,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.00025
},
{
"epoch": 70,
"train_loss": 0.4120306807259719,
"val_loss": 0.6089134141802788,
"val_auc": 0.943210214554455,
"val_topk_overlap": 0.45175368788257725,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.00025
},
{
"epoch": 71,
"train_loss": 0.40985787970324356,
"val_loss": 0.6260952974359194,
"val_auc": 0.9419729299378439,
"val_topk_overlap": 0.45117531843183434,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.00025
},
{
"epoch": 72,
"train_loss": 0.4100499761601289,
"val_loss": 0.6254942963520685,
"val_auc": 0.9414634241732339,
"val_topk_overlap": 0.4515264870513763,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.00025
},
{
"epoch": 73,
"train_loss": 0.4103688560426235,
"val_loss": 0.617545910179615,
"val_auc": 0.9422998727410377,
"val_topk_overlap": 0.45038961083279627,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.00025
},
{
"epoch": 74,
"train_loss": 0.40805083389083546,
"val_loss": 0.6290716951092085,
"val_auc": 0.9419815230403903,
"val_topk_overlap": 0.45038819117177503,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.00025
},
{
"epoch": 75,
"train_loss": 0.40792630054056644,
"val_loss": 0.6340545068184534,
"val_auc": 0.9403481996467591,
"val_topk_overlap": 0.44560532650107126,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.00025
},
{
"epoch": 76,
"train_loss": 0.40834025479853153,
"val_loss": 0.6236351157228152,
"val_auc": 0.9428621339939343,
"val_topk_overlap": 0.4546709457187485,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.00025
},
{
"epoch": 77,
"train_loss": 0.4071060884743929,
"val_loss": 0.6236031403144201,
"val_auc": 0.9420169620152015,
"val_topk_overlap": 0.4530651445404334,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.00025
},
{
"epoch": 78,
"train_loss": 0.4074497365703185,
"val_loss": 0.6290433133641878,
"val_auc": 0.9421106588716064,
"val_topk_overlap": 0.45288347151685654,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.00025
},
{
"epoch": 79,
"train_loss": 0.40854040533304214,
"val_loss": 0.6204406345884005,
"val_auc": 0.9422574500711209,
"val_topk_overlap": 0.4520385582247283,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.000125
},
{
"epoch": 80,
"train_loss": 0.40153354965150356,
"val_loss": 0.632063552737236,
"val_auc": 0.9424195175064664,
"val_topk_overlap": 0.45488917815907826,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.000125
},
{
"epoch": 81,
"train_loss": 0.4004344331721465,
"val_loss": 0.6369497875372568,
"val_auc": 0.9416930329987134,
"val_topk_overlap": 0.44976943200120656,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.000125
},
{
"epoch": 82,
"train_loss": 0.39863520363966626,
"val_loss": 0.6321395362416903,
"val_auc": 0.9425178927376926,
"val_topk_overlap": 0.45331703729372624,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.000125
},
{
"epoch": 83,
"train_loss": 0.40041249555846053,
"val_loss": 0.6306998108824095,
"val_auc": 0.9420066732924216,
"val_topk_overlap": 0.4527129685740117,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.000125
},
{
"epoch": 84,
"train_loss": 0.3980693680544694,
"val_loss": 0.6334150905410448,
"val_auc": 0.9417373821686397,
"val_topk_overlap": 0.45229146535383963,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.000125
},
{
"epoch": 85,
"train_loss": 0.39918220477799576,
"val_loss": 0.6338345656792322,
"val_auc": 0.9418289696568314,
"val_topk_overlap": 0.45314173622093856,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.000125
},
{
"epoch": 86,
"train_loss": 0.3980860977123181,
"val_loss": 0.6285603841145834,
"val_auc": 0.942747009903466,
"val_topk_overlap": 0.4547239031491113,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.000125
},
{
"epoch": 87,
"train_loss": 0.39842924910287064,
"val_loss": 0.6385276938478152,
"val_auc": 0.9419261166158088,
"val_topk_overlap": 0.45597434103509366,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.000125
},
{
"epoch": 88,
"train_loss": 0.3973500070472558,
"val_loss": 0.655018168191115,
"val_auc": 0.9400152699086647,
"val_topk_overlap": 0.44998755110062144,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.000125
},
{
"epoch": 89,
"train_loss": 0.3958093722661336,
"val_loss": 0.6387847339113554,
"val_auc": 0.9418482854933052,
"val_topk_overlap": 0.4550794129001621,
"physics_topk_overlap": 0.13027826412738905,
"lr": 0.000125
},
{
"epoch": 90,
"train_loss": 0.39699620256821316,
"val_loss": 0.6364182805021604,
"val_auc": 0.9422100800575627,
"val_topk_overlap": 0.45455014977480973,
"physics_topk_overlap": 0.13027826412738905,
"lr": 6.25e-05
},
{
"epoch": 91,
"train_loss": 0.39377099089324474,
"val_loss": 0.6385861511031786,
"val_auc": 0.9422414241184843,
"val_topk_overlap": 0.4553409776868369,
"physics_topk_overlap": 0.13027826412738905,
"lr": 6.25e-05
},
{
"epoch": 92,
"train_loss": 0.393089081471165,
"val_loss": 0.6448530877629916,
"val_auc": 0.9416143742782719,
"val_topk_overlap": 0.45326054623552575,
"physics_topk_overlap": 0.13027826412738905,
"lr": 6.25e-05
},
{
"epoch": 93,
"train_loss": 0.3924831257512172,
"val_loss": 0.6419519757231077,
"val_auc": 0.9417058895247528,
"val_topk_overlap": 0.4531113294679451,
"physics_topk_overlap": 0.13027826412738905,
"lr": 6.25e-05
},
{
"epoch": 94,
"train_loss": 0.3940933719277382,
"val_loss": 0.6359312161803246,
"val_auc": 0.9424735513283455,
"val_topk_overlap": 0.4535435590373251,
"physics_topk_overlap": 0.13027826412738905,
"lr": 6.25e-05
},
{
"epoch": 95,
"train_loss": 0.3928225214282672,
"val_loss": 0.6437094608942667,
"val_auc": 0.941819742134597,
"val_topk_overlap": 0.45096912604225076,
"physics_topk_overlap": 0.13027826412738905,
"lr": 6.25e-05
},
{
"epoch": 96,
"train_loss": 0.39205564993123215,
"val_loss": 0.6462149644891421,
"val_auc": 0.9415178562729243,
"val_topk_overlap": 0.4536663302717089,
"physics_topk_overlap": 0.13027826412738905,
"lr": 6.25e-05
},
{
"epoch": 97,
"train_loss": 0.39256475990017253,
"val_loss": 0.6427710602680842,
"val_auc": 0.9416758225940753,
"val_topk_overlap": 0.4539412408745828,
"physics_topk_overlap": 0.13027826412738905,
"lr": 6.25e-05
},
{
"epoch": 98,
"train_loss": 0.3940690668920676,
"val_loss": 0.6387203360597292,
"val_auc": 0.9422565119894593,
"val_topk_overlap": 0.4544070442694809,
"physics_topk_overlap": 0.13027826412738905,
"lr": 6.25e-05
},
{
"epoch": 99,
"train_loss": 0.3922309031089147,
"val_loss": 0.6448526407281557,
"val_auc": 0.9419215473887895,
"val_topk_overlap": 0.45427252886297304,
"physics_topk_overlap": 0.13027826412738905,
"lr": 6.25e-05
}
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More