Deeponet/cnn_branch_inference.py

892 lines
33 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import json
import time
import math
from dataclasses import dataclass, asdict
from pathlib import Path
import numpy as np
import pandas as pd
from tqdm.auto import tqdm
import scipy.sparse as sp
from scipy.sparse.linalg import spilu
from scipy.io import loadmat, savemat
import torch
from torch.autograd import Function
import torch.nn as nn
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
from getdata import GetDataset
# ============================================================
# Device
# ============================================================
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# ============================================================
# Custom autograd for ILU preconditioner
# ============================================================
class ILUApply(Function):
@staticmethod
def forward(ctx, r_torch, ilu):
"""
r_torch: torch complex tensor, shape (Mi,)
ilu: fixed SciPy spilu object
"""
ctx.ilu = ilu
r_np = r_torch.detach().cpu().numpy()
z_np = ilu.solve(r_np)
z = torch.from_numpy(z_np).to(r_torch.device).to(r_torch.dtype)
return z
@staticmethod
def backward(ctx, grad_out):
ilu = ctx.ilu
g_np = grad_out.detach().cpu().numpy()
gr_np = ilu.solve(g_np, trans='H')
grad_r = torch.from_numpy(gr_np).to(grad_out.device).to(grad_out.dtype)
return grad_r, None
# ============================================================
# Config
# ============================================================
@dataclass
class PINNConfig:
# 数据
matpath: str = "deepOnet_data_A1_1558_8_2"
# 模型
ckpt_path: str = "./model_save/model_A1_size_1558.pth"
# 保存
results_dir: str = "./results"
# 设备/精度
device: str = "cuda" if torch.cuda.is_available() else "cpu"
dtype: torch.dtype = torch.float64
# DataLoader
batch_size: int = 64
num_workers: int = 4
pin_memory: bool = True
# 模型结构
trunk_input_dim: int = 2
hidden_channel: int = 128
output_dim: int = 64
# 推理
warmup_steps: int = 3
build_ilu_cache: bool = True
# 保存选项
save_npz: bool = True
save_mat: bool = True
save_csv: bool = True
save_plots: bool = True
# ============================================================
# Networks
# ============================================================
class Modified_MLP_Block(nn.Module):
def __init__(self, input_dim, hidden_channel, output_dim, hidden_size=6):
super(Modified_MLP_Block, self).__init__()
self.activation = nn.Tanh()
self.encodeU = nn.Linear(input_dim, hidden_channel)
self.encodeV = nn.Linear(input_dim, hidden_channel)
self.In = nn.Linear(input_dim, hidden_channel)
self.hidden_layers = nn.ModuleList(
[nn.Linear(hidden_channel, hidden_channel) for _ in range(hidden_size)]
)
self.out = nn.Linear(hidden_channel, output_dim)
self._init_weights()
def _init_weights(self):
torch.manual_seed(123)
gain = nn.init.calculate_gain('tanh')
for m in self.modules():
if isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight, gain=gain)
nn.init.zeros_(m.bias)
def forward(self, x):
U = self.activation(self.encodeU(x))
V = self.activation(self.encodeV(x))
hidden = self.activation(self.In(x))
for layer in self.hidden_layers:
Z = self.activation(layer(hidden))
hidden = (1 - Z) * U + Z * V
x = self.out(hidden)
return x
def _branch_norm2d(channels):
return nn.InstanceNorm2d(channels)
def add_spatial_coord_channels(epsilon_data):
"""
epsilon_data: (B, 1, H, W)
return: (B, 3, H, W) -> [epsilon, x_norm, y_norm]
"""
B, _, H, W = epsilon_data.shape
dev = epsilon_data.device
dtype = epsilon_data.dtype
x = torch.linspace(0, 1, W, device=dev, dtype=dtype).view(1, 1, 1, W).expand(B, 1, H, W)
y = torch.linspace(0, 1, H, device=dev, dtype=dtype).view(1, 1, H, 1).expand(B, 1, H, W)
return torch.cat([epsilon_data, x, y], dim=1)
class ResidualBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1, norm_layer=None):
super(ResidualBlock, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
self.conv1 = nn.Conv2d(
in_channels, out_channels, kernel_size=3,
stride=stride, padding=1, bias=False
)
self.bn1 = norm_layer(out_channels)
self.conv2 = nn.Conv2d(
out_channels, out_channels, kernel_size=3,
stride=1, padding=1, bias=False
)
self.bn2 = norm_layer(out_channels)
self.relu = nn.ReLU(inplace=True)
self.downsample = None
if stride != 1 or in_channels != out_channels:
self.downsample = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
norm_layer(out_channels)
)
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
class CNN_Branch_Residual(nn.Module):
def __init__(self, in_channels=3, num_classes=128):
super(CNN_Branch_Residual, self).__init__()
self.initial = nn.Sequential(
nn.Conv2d(in_channels, 32, kernel_size=3, padding=1, bias=False),
_branch_norm2d(32),
nn.ReLU(inplace=True),
nn.Conv2d(32, 32, kernel_size=3, padding=1, bias=False),
_branch_norm2d(32),
nn.ReLU(inplace=True),
)
self.res_block1 = ResidualBlock(32, 64, stride=2, norm_layer=_branch_norm2d)
self.res_block2 = ResidualBlock(64, 128, stride=2, norm_layer=_branch_norm2d)
self.global_avg_pool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(128, num_classes)
def forward(self, x):
x = self.initial(x)
x = self.res_block1(x)
x = self.res_block2(x)
x = self.global_avg_pool(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
class DeepONet(nn.Module):
def __init__(self, branch_input_dim, trunk_input_dim, hidden_channel, output_dim):
super(DeepONet, self).__init__()
self.output_dim = output_dim
self.branch_net = CNN_Branch_Residual(in_channels=3, num_classes=output_dim)
self.trunk_net = Modified_MLP_Block(trunk_input_dim, hidden_channel, output_dim)
def forward(self, branch_input, trunk_input):
branch_input = add_spatial_coord_channels(branch_input)
branch_out = self.branch_net(branch_input) # (B, output_dim)
trunk_out = self.trunk_net(trunk_input) # (B, N, output_dim)
B1 = branch_out[:, :self.output_dim // 2]
B2 = branch_out[:, self.output_dim // 2:]
T1 = trunk_out[:, :, :self.output_dim // 2]
T2 = trunk_out[:, :, self.output_dim // 2:]
s_re = torch.einsum('bi,bni->bn', B1, T1)
s_im = torch.einsum('bi,bni->bn', B2, T2)
return s_re, s_im
# ============================================================
# Inference / Evaluation
# ============================================================
class PINN_maxwell:
def __init__(self, model, config: PINNConfig):
self.cfg = config
self.device = torch.device(self.cfg.device)
self.results_dir = Path(self.cfg.results_dir)
self.results_dir.mkdir(parents=True, exist_ok=True)
self.model = model.to(self.device, dtype=self.cfg.dtype)
self.loss_fn = nn.MSELoss()
self.train_set, self.test_set = self.load_dataset()
pin_memory = bool(self.cfg.pin_memory and self.device.type == "cuda")
self.eval_train_loader = DataLoader(
self.train_set,
batch_size=self.cfg.batch_size,
shuffle=False,
num_workers=self.cfg.num_workers,
pin_memory=pin_memory
)
self.eval_test_loader = DataLoader(
self.test_set,
batch_size=self.cfg.batch_size,
shuffle=False,
num_workers=self.cfg.num_workers,
pin_memory=pin_memory
)
self.ilu_cache = {}
if self.cfg.build_ilu_cache:
self._build_ilu_cache()
# ========================================================
# Dataset
# ========================================================
def load_dataset(self):
"""
训练集和测试集已经在 mat 内分好,这里直接读取,不重新划分。
"""
data_set = loadmat(self.cfg.matpath)
Epsilon_train = data_set['Eplison_train']
X_train = data_set['X_train']
Ez_train = data_set['Ez_train']
Epsilon_test = data_set['Eplison_test']
X_test = data_set['X_test']
Ez_test = data_set['Ez_test']
coord_len_train = data_set['coord_len_train']
coord_len_test = data_set['coord_len_test']
Ai_train, Aj_train = data_set['Ai_train'], data_set['Aj_train']
Ai_test, Aj_test = data_set['Ai_test'], data_set['Aj_test']
Av_train, Av_test = data_set['Av_train'], data_set['Av_test']
b_train, b_test = data_set['b_train'], data_set['b_test']
n_train, n_test = len(Epsilon_train), len(Epsilon_test)
print(f"Train samples: {n_train}, Test samples: {n_test}")
print(f"Train shapes: epsilon {Epsilon_train.shape}, X {X_train.shape}, Ez {Ez_train.shape}")
print(f"Test shapes: epsilon {Epsilon_test.shape}, X {X_test.shape}, Ez {Ez_test.shape}")
Train_dataset = GetDataset(
Epsilon_train, X_train, Ez_train,
Ai_train, Aj_train, Av_train, b_train, coord_len_train,
index_offset=0
)
Test_dataset = GetDataset(
Epsilon_test, X_test, Ez_test,
Ai_test, Aj_test, Av_test, b_test, coord_len_test,
index_offset=n_train
)
return Train_dataset, Test_dataset
# ========================================================
# Model IO
# ========================================================
def load_model(self):
ckpt_path = Path(self.cfg.ckpt_path)
if not ckpt_path.exists():
raise FileNotFoundError(f"Checkpoint not found: {ckpt_path}")
print(f"Loading checkpoint from: {ckpt_path}")
state_dict = torch.load(ckpt_path, map_location=self.device)
self.model.load_state_dict(state_dict, strict=True)
self.model.to(self.device, dtype=self.cfg.dtype)
self.model.eval()
# ========================================================
# Utils
# ========================================================
def _sync_device(self):
if self.device.type == "cuda":
torch.cuda.synchronize(self.device)
def _build_valid_mask(self, coord_len, max_len):
Mi = coord_len.view(-1).long().to(self.device)
arange = torch.arange(max_len, device=self.device).unsqueeze(0)
mask = arange < Mi.unsqueeze(1)
return mask, Mi
def _move_E_true_to_device(self, E_true):
"""
保持真值原始类型迁移到 device
- complex -> complex128/complex64
- real -> float64/float32
"""
if torch.is_complex(E_true):
target_dtype = torch.complex128 if self.cfg.dtype == torch.float64 else torch.complex64
return E_true.to(self.device, dtype=target_dtype, non_blocking=True)
return E_true.to(self.device, dtype=self.cfg.dtype, non_blocking=True)
def _parse_E_true(self, E_true):
"""
兼容:
1) complex tensor: (B, M)
2) two-channel real tensor: (B, M, 2)
3) real tensor: (B, M)
return:
E_re_true, E_im_true, E_true_complex
"""
if torch.is_complex(E_true):
E_true_complex = E_true
E_re_true = E_true.real.to(self.cfg.dtype)
E_im_true = E_true.imag.to(self.cfg.dtype)
return E_re_true, E_im_true, E_true_complex
if E_true.ndim == 3 and E_true.shape[-1] == 2:
E_re_true = E_true[:, :, 0].to(self.cfg.dtype)
E_im_true = E_true[:, :, 1].to(self.cfg.dtype)
E_true_complex = torch.complex(E_re_true, E_im_true)
return E_re_true, E_im_true, E_true_complex
if E_true.ndim == 2:
E_re_true = E_true.to(self.cfg.dtype)
E_im_true = torch.zeros_like(E_re_true)
E_true_complex = torch.complex(E_re_true, E_im_true)
return E_re_true, E_im_true, E_true_complex
raise ValueError(
f"Unsupported E_true format: shape={tuple(E_true.shape)}, dtype={E_true.dtype}"
)
# ========================================================
# Forward / Loss
# ========================================================
def E_function(self, epsilon_data, coord_data):
epsilon_data = epsilon_data.to(self.device, dtype=self.cfg.dtype)
coord_data = coord_data.to(self.device, dtype=self.cfg.dtype)
return self.model(epsilon_data, coord_data)
def get_data_loss(self, epsilon_data, coord_data, E_true):
E_re_pred, E_im_pred = self.E_function(epsilon_data, coord_data)
E_true = self._move_E_true_to_device(E_true)
E_re_true, E_im_true, _ = self._parse_E_true(E_true)
data_loss = self.loss_fn(E_re_pred, E_re_true) + self.loss_fn(E_im_pred, E_im_true)
return data_loss
def get_fem_loss(self, indices, epsilon_data, coord_data, E_true, Ai, Aj, Av, b, coord_len):
"""
这里 E_true 其实不参与 fem_loss 计算,保留接口只是为了兼容你原来的调用方式。
"""
Ere_pred, Eim_pred = self.E_function(epsilon_data, coord_data)
E = torch.complex(Ere_pred, Eim_pred)
B, Mmax = E.shape
Mi = coord_len.squeeze(-1).long().to(self.device)
arangeM = torch.arange(Mmax, device=self.device)
mask_x = arangeM[None, :] < Mi[:, None]
x_flat = E[mask_x]
b_flat = b.to(self.device)[mask_x].to(x_flat.dtype)
sumMi = int(Mi.sum().item())
offsets = torch.cumsum(
torch.cat([
torch.zeros(1, device=self.device, dtype=torch.long),
Mi[:-1]
]),
dim=0
)
Ai = Ai.to(self.device).long()
Aj = Aj.to(self.device).long()
Av = Av.to(self.device).to(x_flat.dtype)
mask_nnz = (Ai > 0) & (Aj > 0)
rows = (Ai - 1 + offsets.unsqueeze(1)).masked_select(mask_nnz)
cols = (Aj - 1 + offsets.unsqueeze(1)).masked_select(mask_nnz)
vals = Av.masked_select(mask_nnz)
y = torch.zeros(sumMi, dtype=x_flat.dtype, device=self.device)
y.scatter_add_(0, rows, vals * x_flat.index_select(0, cols))
r = y - b_flat
z_parts = []
for i in range(B):
start = int(offsets[i].item())
m = int(Mi[i].item())
r_i = r[start:start + m]
ilu = self.ilu_cache[int(indices[i].item())]
z_i = ILUApply.apply(r_i, ilu)
z_parts.append(z_i)
z = torch.cat(z_parts, dim=0)
loss = (z.abs() ** 2).mean()
return loss
# ========================================================
# ILU Cache
# ========================================================
def _build_ilu_cache(self):
for idx in tqdm(range(len(self.train_set)), desc="Building ILU cache (train)"):
ai = self.train_set.Ai[idx].numpy()
aj = self.train_set.Aj[idx].numpy()
av = self.train_set.Av[idx].numpy()
Mi = int(self.train_set.coord_len[idx].item())
mask = (ai > 0) & (aj > 0)
rows = (ai[mask] - 1).astype(np.int64)
cols = (aj[mask] - 1).astype(np.int64)
vals = av[mask]
A = sp.coo_matrix((vals, (rows, cols)), shape=(Mi, Mi)).tocsc()
self.ilu_cache[idx] = spilu(A)
for idx in tqdm(range(len(self.test_set)), desc="Building ILU cache (test)"):
ai = self.test_set.Ai[idx].numpy()
aj = self.test_set.Aj[idx].numpy()
av = self.test_set.Av[idx].numpy()
Mi = int(self.test_set.coord_len[idx].item())
mask = (ai > 0) & (aj > 0)
rows = (ai[mask] - 1).astype(np.int64)
cols = (aj[mask] - 1).astype(np.int64)
vals = av[mask]
A = sp.coo_matrix((vals, (rows, cols)), shape=(Mi, Mi)).tocsc()
self.ilu_cache[len(self.train_set) + idx] = spilu(A)
print(f"ILU cache built: {len(self.ilu_cache)} samples.")
def _compute_sample_fem_metrics(self, global_idx, pred_complex_np, ai, aj, av, b_vec, Mi):
mask = (ai > 0) & (aj > 0)
rows = (ai[mask] - 1).astype(np.int64)
cols = (aj[mask] - 1).astype(np.int64)
vals = av[mask]
A = sp.coo_matrix((vals, (rows, cols)), shape=(Mi, Mi)).tocsc()
x = pred_complex_np[:Mi]
b_valid = b_vec[:Mi]
r = A.dot(x) - b_valid
residual_l2 = float(np.linalg.norm(r))
relative_residual_l2 = float(residual_l2 / (np.linalg.norm(b_valid) + 1e-12))
if self.cfg.build_ilu_cache:
ilu = self.ilu_cache[int(global_idx)]
else:
ilu = spilu(A)
z = ilu.solve(r)
fem_precond_mse = float(np.mean(np.abs(z) ** 2))
return fem_precond_mse, residual_l2, relative_residual_l2
# ========================================================
# Inference / Evaluation
# ========================================================
@torch.no_grad()
def infer_and_save_split(self, loader, split_name="train"):
self.model.eval()
split_dir = self.results_dir / split_name
split_dir.mkdir(parents=True, exist_ok=True)
# warmup
if self.device.type == "cuda" and self.cfg.warmup_steps > 0:
warmup_count = 0
for batch in loader:
if warmup_count >= self.cfg.warmup_steps:
break
_, epsilon_data, coord_data, *_ = batch
epsilon_data = epsilon_data.to(self.device, dtype=self.cfg.dtype, non_blocking=True)
coord_data = coord_data.to(self.device, dtype=self.cfg.dtype, non_blocking=True)
_ = self.model(epsilon_data, coord_data)
warmup_count += 1
self._sync_device()
all_indices = []
all_epsilon = []
all_coord = []
all_E_true_raw = []
all_E_true_real = []
all_E_true_imag = []
all_E_true_complex = []
all_E_pred_real = []
all_E_pred_imag = []
all_E_pred_complex = []
all_Ai = []
all_Aj = []
all_Av = []
all_b = []
all_coord_len = []
sample_metrics = []
total_forward_time = 0.0
total_samples = 0
total_valid_points = 0
sum_sq_err = 0.0
sum_abs_err = 0.0
sum_sq_true = 0.0
sum_sq_err_re = 0.0
sum_sq_err_im = 0.0
sum_abs_err_re = 0.0
sum_abs_err_im = 0.0
fem_loss_list = []
for batch in tqdm(loader, desc=f"Infer {split_name}"):
indices, epsilon_data, coord_data, E_true, Ai, Aj, Av, b, coord_len = batch
indices = indices.to(self.device, non_blocking=True)
epsilon_data = epsilon_data.to(self.device, dtype=self.cfg.dtype, non_blocking=True)
coord_data = coord_data.to(self.device, dtype=self.cfg.dtype, non_blocking=True)
E_true = self._move_E_true_to_device(E_true)
B = epsilon_data.shape[0]
Mmax = coord_data.shape[1]
mask, Mi = self._build_valid_mask(coord_len, Mmax)
self._sync_device()
t0 = time.perf_counter()
E_re_pred, E_im_pred = self.E_function(epsilon_data, coord_data)
self._sync_device()
t1 = time.perf_counter()
forward_time = t1 - t0
total_forward_time += forward_time
total_samples += B
total_valid_points += int(mask.sum().item())
E_re_true, E_im_true, E_true_complex = self._parse_E_true(E_true)
E_pred_complex = torch.complex(E_re_pred, E_im_pred)
diff_complex = E_pred_complex - E_true_complex
diff_re = E_re_pred - E_re_true
diff_im = E_im_pred - E_im_true
sum_sq_err += float((diff_complex.abs()[mask] ** 2).sum().item())
sum_abs_err += float(diff_complex.abs()[mask].sum().item())
sum_sq_true += float((E_true_complex.abs()[mask] ** 2).sum().item())
sum_sq_err_re += float((diff_re[mask] ** 2).sum().item())
sum_sq_err_im += float((diff_im[mask] ** 2).sum().item())
sum_abs_err_re += float(diff_re.abs()[mask].sum().item())
sum_abs_err_im += float(diff_im.abs()[mask].sum().item())
fem_loss = self.get_fem_loss(indices, epsilon_data, coord_data, E_true, Ai, Aj, Av, b, coord_len)
fem_loss_list.append(float(fem_loss.item()))
all_indices.append(indices.detach().cpu().numpy())
all_epsilon.append(epsilon_data.detach().cpu().numpy())
all_coord.append(coord_data.detach().cpu().numpy())
all_E_true_raw.append(E_true.detach().cpu().numpy())
all_E_true_real.append(E_re_true.detach().cpu().numpy())
all_E_true_imag.append(E_im_true.detach().cpu().numpy())
all_E_true_complex.append(E_true_complex.detach().cpu().numpy())
all_E_pred_real.append(E_re_pred.detach().cpu().numpy())
all_E_pred_imag.append(E_im_pred.detach().cpu().numpy())
all_E_pred_complex.append(E_pred_complex.detach().cpu().numpy())
all_Ai.append(Ai.detach().cpu().numpy())
all_Aj.append(Aj.detach().cpu().numpy())
all_Av.append(Av.detach().cpu().numpy())
all_b.append(b.detach().cpu().numpy())
all_coord_len.append(coord_len.detach().cpu().numpy())
pred_np = E_pred_complex.detach().cpu().numpy()
true_np = E_true_complex.detach().cpu().numpy()
Mi_np = Mi.detach().cpu().numpy()
Ai_np = Ai.detach().cpu().numpy()
Aj_np = Aj.detach().cpu().numpy()
Av_np = Av.detach().cpu().numpy()
b_np = b.detach().cpu().numpy()
forward_time_per_sample_ms = forward_time * 1000.0 / B
for i in range(B):
m = int(Mi_np[i])
pred_i = pred_np[i, :m]
true_i = true_np[i, :m]
diff_i = pred_i - true_i
mse_i = float(np.mean(np.abs(diff_i) ** 2))
rmse_i = float(np.sqrt(mse_i))
mae_i = float(np.mean(np.abs(diff_i)))
rel_l2_i = float(np.linalg.norm(diff_i) / (np.linalg.norm(true_i) + 1e-12))
mse_re_i = float(np.mean((pred_i.real - true_i.real) ** 2))
mse_im_i = float(np.mean((pred_i.imag - true_i.imag) ** 2))
mae_re_i = float(np.mean(np.abs(pred_i.real - true_i.real)))
mae_im_i = float(np.mean(np.abs(pred_i.imag - true_i.imag)))
fem_precond_mse_i = np.nan
residual_l2_i = np.nan
relative_residual_l2_i = np.nan
if self.cfg.build_ilu_cache:
fem_precond_mse_i, residual_l2_i, relative_residual_l2_i = \
self._compute_sample_fem_metrics(
global_idx=int(indices[i].item()),
pred_complex_np=pred_np[i],
ai=Ai_np[i],
aj=Aj_np[i],
av=Av_np[i],
b_vec=b_np[i],
Mi=m
)
sample_metrics.append({
"split": split_name,
"global_index": int(indices[i].item()),
"coord_len": m,
"mse_complex": mse_i,
"rmse_complex": rmse_i,
"mae_complex": mae_i,
"rel_l2": rel_l2_i,
"mse_real": mse_re_i,
"mse_imag": mse_im_i,
"mae_real": mae_re_i,
"mae_imag": mae_im_i,
"fem_precond_mse": fem_precond_mse_i,
"residual_l2": residual_l2_i,
"relative_residual_l2": relative_residual_l2_i,
"forward_time_ms": forward_time_per_sample_ms
})
mse_complex = sum_sq_err / max(total_valid_points, 1)
rmse_complex = math.sqrt(mse_complex)
mae_complex = sum_abs_err / max(total_valid_points, 1)
rel_l2_global = math.sqrt(sum_sq_err / (sum_sq_true + 1e-12))
mse_real = sum_sq_err_re / max(total_valid_points, 1)
mse_imag = sum_sq_err_im / max(total_valid_points, 1)
mae_real = sum_abs_err_re / max(total_valid_points, 1)
mae_imag = sum_abs_err_im / max(total_valid_points, 1)
mean_fem_loss = float(np.mean(fem_loss_list)) if len(fem_loss_list) > 0 else float("nan")
mean_forward_time_ms = total_forward_time * 1000.0 / max(total_samples, 1)
throughput = total_samples / max(total_forward_time, 1e-12)
sample_df = pd.DataFrame(sample_metrics)
summary = {
"split": split_name,
"num_samples": int(total_samples),
"num_valid_points": int(total_valid_points),
"mse_complex": float(mse_complex),
"rmse_complex": float(rmse_complex),
"mae_complex": float(mae_complex),
"rel_l2_global": float(rel_l2_global),
"mse_real": float(mse_real),
"mse_imag": float(mse_imag),
"mae_real": float(mae_real),
"mae_imag": float(mae_imag),
"mean_fem_loss": float(mean_fem_loss),
"total_forward_time_s": float(total_forward_time),
"mean_forward_time_ms_per_sample": float(mean_forward_time_ms),
"throughput_samples_per_s": float(throughput),
}
if len(sample_df) > 0:
for col in ["rel_l2", "fem_precond_mse", "residual_l2", "relative_residual_l2"]:
if col in sample_df.columns and sample_df[col].notna().any():
summary[f"mean_{col}"] = float(np.nanmean(sample_df[col].values))
summary[f"median_{col}"] = float(np.nanmedian(sample_df[col].values))
summary[f"max_{col}"] = float(np.nanmax(sample_df[col].values))
save_data = {
"indices": np.concatenate(all_indices, axis=0),
"epsilon_data": np.concatenate(all_epsilon, axis=0),
"coord_data": np.concatenate(all_coord, axis=0),
"E_true_raw": np.concatenate(all_E_true_raw, axis=0),
"E_true_real": np.concatenate(all_E_true_real, axis=0),
"E_true_imag": np.concatenate(all_E_true_imag, axis=0),
"E_true_complex": np.concatenate(all_E_true_complex, axis=0),
"E_pred_real": np.concatenate(all_E_pred_real, axis=0),
"E_pred_imag": np.concatenate(all_E_pred_imag, axis=0),
"E_pred_complex": np.concatenate(all_E_pred_complex, axis=0),
"Ai": np.concatenate(all_Ai, axis=0),
"Aj": np.concatenate(all_Aj, axis=0),
"Av": np.concatenate(all_Av, axis=0),
"b": np.concatenate(all_b, axis=0),
"coord_len": np.concatenate(all_coord_len, axis=0),
}
if self.cfg.save_npz:
np.savez_compressed(split_dir / f"{split_name}_all_data.npz", **save_data)
if self.cfg.save_mat:
mat_dict = dict(save_data)
for k, v in summary.items():
if isinstance(v, (int, float, np.number)):
mat_dict[f"summary_{k}"] = np.array([[v]])
elif isinstance(v, str):
mat_dict[f"summary_{k}"] = np.array([v], dtype=object)
if len(sample_df) > 0:
for col in sample_df.columns:
if pd.api.types.is_numeric_dtype(sample_df[col]):
mat_dict[f"sample_{col}"] = sample_df[col].to_numpy()
else:
mat_dict[f"sample_{col}"] = sample_df[col].astype(str).to_numpy(dtype=object)
savemat(split_dir / f"{split_name}_all_data.mat", mat_dict)
if self.cfg.save_csv:
sample_df.to_csv(split_dir / f"{split_name}_sample_metrics.csv", index=False, encoding="utf-8-sig")
with open(split_dir / f"{split_name}_summary.json", "w", encoding="utf-8") as f:
json.dump(summary, f, ensure_ascii=False, indent=2)
if self.cfg.save_plots and len(sample_df) > 0:
self._plot_split_metrics(split_name, sample_df, split_dir)
print(f"\n[{split_name}] Summary:")
for k, v in summary.items():
print(f"{k}: {v}")
return summary, sample_df, save_data
def _plot_split_metrics(self, split_name, sample_df, split_dir):
if "rel_l2" in sample_df.columns:
plt.figure(figsize=(8, 5))
plt.hist(sample_df["rel_l2"].dropna().values, bins=30)
plt.xlabel("Relative L2 Error")
plt.ylabel("Count")
plt.title(f"{split_name} - Relative L2 Error")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig(split_dir / f"{split_name}_rel_l2_hist.png", dpi=200)
plt.close()
if "forward_time_ms" in sample_df.columns:
plt.figure(figsize=(8, 5))
plt.hist(sample_df["forward_time_ms"].dropna().values, bins=30)
plt.xlabel("Forward Time per Sample (ms)")
plt.ylabel("Count")
plt.title(f"{split_name} - Forward Time")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig(split_dir / f"{split_name}_forward_time_hist.png", dpi=200)
plt.close()
if "fem_precond_mse" in sample_df.columns and sample_df["fem_precond_mse"].notna().any():
plt.figure(figsize=(8, 5))
plt.hist(sample_df["fem_precond_mse"].dropna().values, bins=30)
plt.xlabel("FEM Preconditioned MSE")
plt.ylabel("Count")
plt.title(f"{split_name} - FEM Preconditioned MSE")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig(split_dir / f"{split_name}_fem_precond_mse_hist.png", dpi=200)
plt.close()
def evaluate_and_infer_all(self):
self.results_dir.mkdir(parents=True, exist_ok=True)
train_summary, train_df, _ = self.infer_and_save_split(
self.eval_train_loader, split_name="train"
)
test_summary, test_df, _ = self.infer_and_save_split(
self.eval_test_loader, split_name="test"
)
summary_df = pd.DataFrame([train_summary, test_summary])
summary_df.to_csv(self.results_dir / "all_summary.csv", index=False, encoding="utf-8-sig")
cfg_dump = {}
for k, v in asdict(self.cfg).items():
if isinstance(v, torch.dtype):
cfg_dump[k] = str(v)
else:
cfg_dump[k] = v
with open(self.results_dir / "config.json", "w", encoding="utf-8") as f:
json.dump(cfg_dump, f, ensure_ascii=False, indent=2)
print("\n================ Overall Summary ================")
print(summary_df.to_string(index=False))
# ============================================================
# Main
# ============================================================
if __name__ == "__main__":
cfg = PINNConfig(
matpath="deepOnet_data_A1_1558_8_2", # 改成你的 .mat 路径
ckpt_path="./model_save/model_A1_size_1558_2_epoch1000.pth", # 改成你的模型权重路径
results_dir="./results",
batch_size=64,
num_workers=4,
device="cuda" if torch.cuda.is_available() else "cpu",
dtype=torch.float64,
trunk_input_dim=2,
hidden_channel=128,
output_dim=64,
build_ilu_cache=True,
)
model = DeepONet(
branch_input_dim=1,
trunk_input_dim=cfg.trunk_input_dim,
hidden_channel=cfg.hidden_channel,
output_dim=cfg.output_dim
).double()
pinn = PINN_maxwell(model, cfg)
# 只加载模型,不训练
pinn.load_model()
# 直接对 train/test 推理评估并保存
pinn.evaluate_and_infer_all()