From 3176ab69fd78ebee7feb4a03335acc8b009901a4 Mon Sep 17 00:00:00 2001 From: ADream-ki <2085127827@qq.com> Date: Tue, 7 Oct 2025 23:28:01 +0800 Subject: [PATCH 01/10] init --- examples/symbolic_gcn/conf/config.yaml | 84 ++++++++++++++++++++++++++ examples/symbolic_gcn/gcn.py | 0 2 files changed, 84 insertions(+) create mode 100644 examples/symbolic_gcn/conf/config.yaml create mode 100644 examples/symbolic_gcn/gcn.py diff --git a/examples/symbolic_gcn/conf/config.yaml b/examples/symbolic_gcn/conf/config.yaml new file mode 100644 index 0000000000..01d4da56b1 --- /dev/null +++ b/examples/symbolic_gcn/conf/config.yaml @@ -0,0 +1,84 @@ +defaults: + - ppsci_default + - TRAIN: train_default + - TRAIN/ema: ema_default + - TRAIN/swa: swa_default + - EVAL: eval_default + - INFER: infer_default + - hydra/job/config/override_dirname/exclude_keys: exclude_keys_default + - _self_ +hydra: + run: + # dynamic output directory according to running time and override name + dir: outputs_symbolic_gcn/${now:%Y-%m-%d}/${now:%H-%M-%S}/${hydra.job.override_dirname} + job: + name: ${mode} # name of logfile + chdir: false # keep current working directory unchanged + callbacks: + init_callback: + _target_: ppsci.utils.callbacks.InitCallback + sweep: + # output directory for multirun + dir: ${hydra.run.dir} + subdir: ./ + +# general settings +mode: train # running mode: train/eval +seed: 42 +output_dir: ${hydra:run.dir} +log_freq: 20 +# dataset setting +DATASET: + label_keys: [label] + data_dir: ./dataset/train_data.pkl + +MODEL: + input_keys: [aq_train_data, mete_train_data] + output_keys: [label] + output_attention: true + seq_len: 72 + pred_len: 48 + aq_gat_node_features: 7 + aq_gat_node_num: 35 + mete_gat_node_features: 7 + mete_gat_node_num: 18 + gat_hidden_dim: 32 + gat_edge_dim: 3 + e_layers: 1 + enc_in: 7 + dec_in: 7 + c_out: 7 + d_model: 16 + embed: fixed + freq: t + dropout: 0.05 + factor: 3 + n_heads: 4 + d_ff: 32 + num_kernels: 6 + top_k: 4 + +# training settings +TRAIN: + epochs: 100 + iters_per_epoch: 400 + save_freq: 10 + eval_during_train: true + eval_freq: 10 + batch_size: 32 + learning_rate: 0.0001 + lr_scheduler: + epochs: ${TRAIN.epochs} + iters_per_epoch: ${TRAIN.iters_per_epoch} + learning_rate: 0.0005 + step_size: 20 + gamma: 0.95 + pretrained_model_path: null + checkpoint_path: null + +EVAL: + eval_data_path: ./dataset/val_data.pkl + pretrained_model_path: null + compute_metric_by_batch: false + eval_with_no_grad: true + batch_size: 32 diff --git a/examples/symbolic_gcn/gcn.py b/examples/symbolic_gcn/gcn.py new file mode 100644 index 0000000000..e69de29bb2 From f33c07fbac582e9786278175d031370ff504dfba Mon Sep 17 00:00:00 2001 From: ADream-ki <2085127827@qq.com> Date: Sun, 19 Oct 2025 22:52:49 +0800 Subject: [PATCH 02/10] Implement symbolic graph networks for physics discovery, including OGN, VarOGN, and HGN architectures. Add simulation dataset generation and configuration files. Remove outdated symbolic GCN files. --- docs/zh/examples/symbolic_gn.md | 833 +++++++++++++++++++++++++ examples/symbolic_gcn/conf/config.yaml | 84 --- examples/symbolic_gcn/gcn.py | 0 examples/symbolic_gn/conf/config.yaml | 138 ++++ examples/symbolic_gn/gn.py | 519 +++++++++++++++ examples/symbolic_gn/simulate.py | 752 ++++++++++++++++++++++ ppsci/arch/__init__.py | 5 + ppsci/arch/symbolic_gn.py | 667 ++++++++++++++++++++ 8 files changed, 2914 insertions(+), 84 deletions(-) create mode 100644 docs/zh/examples/symbolic_gn.md delete mode 100644 examples/symbolic_gcn/conf/config.yaml delete mode 100644 examples/symbolic_gcn/gcn.py create mode 100644 examples/symbolic_gn/conf/config.yaml create mode 100644 examples/symbolic_gn/gn.py create mode 100644 examples/symbolic_gn/simulate.py create mode 100644 ppsci/arch/symbolic_gn.py diff --git a/docs/zh/examples/symbolic_gn.md b/docs/zh/examples/symbolic_gn.md new file mode 100644 index 0000000000..2bb58c7163 --- /dev/null +++ b/docs/zh/examples/symbolic_gn.md @@ -0,0 +1,833 @@ +# Graph Networks for Physics Discovery (GN) + +AI Studio快速体验 + +开始训练、评估前,请先确保已安装依赖 `scipy`、`scikit-learn`、`celluloid`、`pgl`,请运行安装命令: +```bash +pip install scipy scikit-learn celluloid pgl +``` + +=== "模型训练命令" + + ``` sh + python gn.py mode=train MODEL.arch=OGN DATA.type=spring + ``` +=== "模型评估命令" + + ``` sh + python gn.py mode=eval MODEL.arch=OGN EVAL.pretrained_model_path="./outputs_symbolic_gcn/.../checkpoints/latest" + ``` +=== "模型推理命令" + + ``` sh + python gn.py mode=infer MODEL.arch=OGN INFER.pretrained_model_path="./outputs_symbolic_gcn/.../checkpoints/latest" + ``` +=== "模型导出命令" + + ``` sh + python gn.py mode=export MODEL.arch=OGN INFER.pretrained_model_path="./outputs_symbolic_gcn/.../checkpoints/latest" + ``` + +## 1. 背景简介 + +深度学习方法在物理系统建模中展现出强大能力,但传统神经网络往往是黑盒模型,难以提供物理解释。图神经网络(GNNs)通过其固有的归纳偏置,特别适合建模粒子系统中的相互作用。然而,如何从训练好的GNN中提取可解释的物理定律仍然是一个挑战。 + +基于论文 **[Discovering Symbolic Models from Deep Learning with Inductive Biases](https://arxiv.org/abs/2006.11287)** 的思想,我们开发了一个完整的物理发现框架。该框架通过在GNN训练过程中引入强归纳偏置(如L1正则化鼓励稀疏表示),然后对学习到的模型内部组件应用符号回归,从而提取显式的物理关系。这种方法不仅能够重新发现已知的力定律和哈密顿量,还能从复杂数据中发现新的解析公式。 + +本案例使用图网络(Graph Networks, GN)对多种物理系统(如弹簧系统、引力系统、电荷系统等)的动力学进行建模,并通过符号回归提取潜在的物理定律。 + +## 2. 模型原理 + +本章节基于论文 **[Discovering Symbolic Models from Deep Learning with Inductive Biases](https://arxiv.org/abs/2006.11287)** (NeurIPS 2020) 的核心思想进行介绍。 + +### 2.1 核心问题 + +**传统困境**:深度学习模型(如全连接网络、CNN)虽然强大,但缺乏物理解释性。我们能否既利用深度学习的拟合能力,又能提取可解释的物理定律? + +**本文方案**: +1. 使用具有**归纳偏置**的架构(图网络)学习物理系统 +2. 在训练中引入**稀疏性约束**,迫使模型学习简单表示 +3. 对学到的内部函数进行**符号回归**,提取解析表达式 +4. 用符号表达式替换神经网络组件,获得可解释模型 + +### 2.2 图网络架构 + +图网络(GN)是天然适合物理建模的架构,因为: +- **排列不变性**:粒子顺序不影响结果(符合物理对称性) +- **局部作用**:粒子间通过边交互(符合力的成对性) +- **可扩展性**:可以处理不同数量的粒子 + +GN的核心思想是将物理系统建模为图结构: +- **节点**:代表物理实体(如粒子),包含状态 xᵢ = [位置, 速度, 质量, 电荷] +- **边**:代表实体间的相互作用,通过消息传递机制计算 +- **全局属性**:可选的系统级属性(如总能量) + +GN内部包含三个可分离的函数组件: +- **边函数(消息函数)φₑ(xᵢ, xⱼ)**:计算从节点j到节点i的消息向量 +- **节点函数 φᵥ(xᵢ, Σmᵢⱼ)**:根据聚合的消息更新节点状态 +- **全局函数 φᵤ(Σxᵢ)**:计算系统的全局属性(如总能量) + +**关键**:这些函数组件(φₑ, φᵥ, φᵤ)天然对应物理中的基本概念: +- φₑ 对应**成对作用力**(如引力、库仑力) +- φᵥ 对应**运动方程**(如牛顿第二定律) +- φᵤ 对应**能量函数**(如哈密顿量) + +因此,如果我们能对这些函数进行符号回归,就能直接提取物理定律! + +### 2.3 物理发现框架 + +我们的物理发现框架包含以下步骤: + +1. **设计具有可分离内部结构的深度学习模型**:使用图网络作为核心归纳偏置,特别适合粒子系统建模 +2. **端到端训练模型**:使用可用数据训练GN模型 +3. **对模型内部函数进行符号回归**:对φₑ、φᵥ、φᵤ等组件拟合符号表达式 +4. **用符号表达式替换神经网络组件**:创建完全解析的物理模型 + +### 2.4 稀疏表示与正则化 + +**核心理论**:如果真实物理系统可以用线性潜在空间完美描述,且我们用足够大的消息维度训练GNN,那么学到的消息向量将是真实力向量的**线性变换**。 + +例如,对于引力系统 `F = -G·m₁m₂/r²·r̂`,消息向量可能学到: +``` +message = [c₁·dx, c₂·dy, c₃·m₁m₂/r², c₄·..., 0, 0, ...] +``` +其中只有少数几个通道是活跃的,其余为零。 + +为了鼓励这种稀疏性,我们在训练中使用正则化策略: + +| 正则化方法 | 适用模型 | 作用机制 | 配置参数 | +|-----------|---------|---------|---------| +| **L1正则化** | OGN | 对节点特征施加L1惩罚,迫使网络学习稀疏激活 | `l1_strength: 1e-2` | +| **KL正则化** | VarOGN | 使后验分布接近稀疏先验,自然产生稀疏性 | `kl_weight: 1e-3` | +| **瓶颈架构** | 所有 | 限制消息维度(msg_dim),迫使信息压缩 | `msg_dim: 100` | + +**为什么稀疏性重要**: +1. **符号回归可行性**:稀疏表示意味着只有少数变量参与,符号回归搜索空间大幅减小 +2. **物理解释性**:活跃的通道对应真实的物理量组合 +3. **泛化能力**:简单模型通常泛化更好(奥卡姆剃刀原则) + +## 3. 实现细节 + +### 3.1 数据集介绍 + +我们的数据生成器支持多种物理系统,包括: + +| 系统类型 | 势能函数 | 物理意义 | +|---------|---------|---------| +| `r2` | `-m₁m₂/r` | 引力/库仑力(吸引)| +| `r1` | `m₁m₂·log(r)` | 2D引力(涡旋)| +| `spring` | `(r-1)²` | 胡克定律(平衡长度=1)| +| `damped` | `(r-1)² + damping·v·x/n` | 带阻尼的弹簧 | +| `string` | `(r-1)² + y·q` | 弦 + 重力 | +| `charge` | `q₁q₂/r` | 库仑力(排斥/吸引)| +| `superposition` | `q₁q₂/r - m₁m₂/r` | 电磁+引力叠加 | +| `discontinuous` | 分段函数 | 模拟非光滑力(如碰撞)| +| `string_ball` | 弹簧 + 球体排斥 | 复杂约束系统 | + +数据生成器使用高精度ODE求解器(`scipy.integrate.odeint`)生成轨迹数据,确保物理准确性。 + +### 3.2 数据生成器详解 + +数据生成器(`simulate.py`)使用 PaddlePaddle 自动微分计算力,核心流程如下: + +1. **势能函数定义**:为每种物理系统定义势能函数 U(r) +2. **自动微分计算力**:`F = -∇U`,使用 `paddle.grad` 计算势能梯度 +3. **ODE求解**:使用 `scipy.integrate.odeint` 求解 `d²x/dt² = F/m` +4. **数据格式**: + - 输入:[x, y, vx, vy, charge, mass](2D系统) + - 标签:加速度 [ax, ay] 或导数 [dq/dt, dp/dt] + +### 3.3 模型构建 + +我们实现了三种核心模型架构: + +#### OGN (Object-based Graph Network) +- **物理基础**:牛顿力学 (F=ma) +- **输出**:加速度 a = F/m +- **网络结构**: + - 消息函数(Message Function):`φₑ: (xᵢ, xⱼ) → mᵢⱼ`,将源节点和目标节点特征映射到消息向量(维度100) + - 节点更新函数(Node Function):`φᵥ: (xᵢ, Σmᵢⱼ) → aᵢ`,聚合所有消息并输出加速度 +- **适用场景**:通用动力学系统(弹簧、引力、电荷等) +- **正则化**:L1正则化鼓励消息向量稀疏性,使符号回归更容易 + +#### VarOGN (Variational ODE Graph Network) +- **物理基础**:变分推断框架 +- **输出**:加速度均值和方差(不确定性量化) +- **网络结构**: + - 消息函数输出 μ 和 logσ² + - 训练时采样:`m = μ + ε·exp(logσ²/2)`,其中 ε ~ N(0,1) + - 推理时使用均值:`m = μ` +- **适用场景**:噪声数据/不确定性建模/鲁棒性评估 +- **正则化**:KL散度正则化使后验分布接近先验分布 + +#### HGN (Hamiltonian Graph Network) +- **物理基础**:哈密顿力学(能量守恒原理) +- **输出**:[dq/dt, dp/dt](广义坐标和广义动量的时间导数) +- **网络结构**: + - 成对能量函数:`Eᵢⱼ = φₑ(xᵢ, xⱼ)` + - 自身能量函数:`Eᵢ = φᵥ(xᵢ)` + - 哈密顿量:`H = Σᵢ Eᵢ + Σᵢⱼ Eᵢⱼ` + - 哈密顿方程:`dq/dt = ∂H/∂p`, `dp/dt = -∂H/∂q` +- **适用场景**:保守系统(无耗散)、长时间预测 +- **特点**:通过自动微分实现哈密顿方程,天然保证能量守恒(在无耗散系统中) + +模型通过配置文件中的 `MODEL.arch` 参数动态选择: + +```yaml +MODEL: + arch: "OGN" # 可选: "OGN", "VarOGN", "HGN" + input_keys: ["node_features", "edge_index"] + output_keys: ["acceleration"] # HGN: ["derivative"] + n_f: auto # 自动设置为 dimension * 2 + 2 + msg_dim: 100 # 消息维度(OGN/VarOGN) + ndim: auto # 自动设置为 dimension + hidden: 300 # 隐藏层维度 +``` + +**输入输出格式**: + +| 模型 | 输入格式 | 输出格式 | 说明 | +|------|---------|---------|------| +| OGN | `node_features`: [batch, n_nodes, n_f]
`edge_index`: [batch, 2, n_edges] | `acceleration`: [batch, n_nodes, ndim] | 直接预测加速度 | +| VarOGN | 同上 | `acceleration`: [batch, n_nodes, ndim] | 预测加速度(均值) | +| HGN | 同上 | `derivative`: [batch, n_nodes, 2*ndim] | 前半部分为dq/dt,后半部分为dp/dt | + +其中 `n_f = 2*ndim + 2`(位置、速度、电荷、质量)。 + +### 3.4 约束构建 + +我们使用PPSCI的`SupervisedConstraint`构建监督约束。数据流程如下: + +1. **数据集准备**: + ```python + train_dataset = OGNDataset(cfg, mode="train") + train_data, train_labels = train_dataset.get_data_and_labels() + ``` + +2. **约束构建**: + ```python + train_constraint = ppsci.constraint.SupervisedConstraint( + { + "dataset": { + "name": "NamedArrayDataset", + "input": train_data, + "label": train_labels + }, + "batch_size": cfg.TRAIN.batch_size, + "sampler": {"name": "BatchSampler", "drop_last": False, "shuffle": True}, + }, + loss=loss_fn, + name="train_constraint", + ) + ``` + +3. **损失函数**:根据模型类型自动调整 + - **OGN/VarOGN**:MAE或MSE损失 + L1正则化(作用于节点特征) + - **HGN**:仅对加速度部分(derivative的后半部分)计算损失 + +**自动化配置**: +- `batch_size = auto`:自动计算为 `int(64 * (4 / num_nodes)²)` +- 训练/验证集划分:默认 0.8/0.2 +- 数据下采样:`downsample_factor = 5`,减少计算量 + +### 3.5 优化器构建 + +训练使用Adam优化器配合OneCycleLR学习率调度: + +```python +# 动态计算每个epoch的批次数 +batch_per_epoch = int(1000*10 / (cfg.TRAIN.batch_size/32.0)) + +# OneCycleLR学习率调度器 +lr_scheduler = paddle.optimizer.lr.OneCycleLR( + max_learning_rate=cfg.TRAIN.lr_scheduler.max_learning_rate, + total_steps=cfg.TRAIN.epochs * batch_per_epoch, + divide_factor=cfg.TRAIN.lr_scheduler.final_div_factor +) + +optimizer = paddle.optimizer.Adam( + learning_rate=lr_scheduler, + parameters=model.parameters(), + weight_decay=cfg.TRAIN.optimizer.weight_decay +) +``` + +关键超参数: + +```yaml +TRAIN: + epochs: 1000 + batch_size: auto # 自动计算 + optimizer: + learning_rate: 1e-3 + weight_decay: 1e-8 + lr_scheduler: + name: "OneCycleLR" + max_learning_rate: 1e-3 + final_div_factor: 1e5 # 最终学习率 = max_lr / final_div_factor + loss: + type: "MAE" # 可选 MAE 或 MSE +``` + +**学习率调度策略**: +- OneCycleLR:学习率先从 max_lr/divide_factor 增加到 max_lr,再逐渐降低到 max_lr/final_div_factor +- 适合快速收敛且避免过拟合 + +### 3.6 符号回归准备 + +训练GNN的目的是为符号回归做准备。根据论文思想,我们鼓励稀疏的潜在表示: + +**理论基础**: +- 如果真实物理系统有完美的线性潜在空间描述,训练后的GNN消息向量将是真实力向量的线性变换 +- L1正则化迫使网络学习稀疏表示,使得消息向量中只有少数维度是活跃的 +- 这些活跃的维度通常对应真实的物理量(如距离、质量乘积等) + +**实践流程**: + +1. **训练稀疏GNN**: + ```yaml + MODEL: + msg_dim: 100 # 消息维度 + regularization_type: "l1" + l1_strength: 1e-2 + ``` + +2. **提取消息数据**:训练过程中可以记录消息向量(需要修改代码添加钩子) + ```python + messages = model.msg_fnc(edge_features) # [num_edges, msg_dim] + # 保存消息及对应的物理特征(dx, dy, r, m1, m2等) + ``` + +3. **符号回归**:使用工具如 [PySR](https://github.com/MilesCranmer/PySR) 拟合符号表达式 + ```python + from pysr import PySRRegressor + + # 选择最活跃的消息通道 + active_channels = np.argsort(np.std(messages, axis=0))[-5:] + + # 对每个活跃通道进行符号回归 + for ch in active_channels: + model = PySRRegressor( + niterations=40, + binary_operators=["+", "*", "/", "-"], + unary_operators=["square", "sqrt", "neg"], + ) + model.fit(physical_features, messages[:, ch]) + print(f"Channel {ch}: {model.sympy()}") + ``` + +4. **发现物理定律**: + - 对于弹簧系统:可能发现 `m ~ (r-1)` (胡克定律) + - 对于引力系统:可能发现 `m ~ -m1*m2/r²` (万有引力定律) + - 对于电荷系统:可能发现 `m ~ q1*q2/r²` (库仑定律) + +## 4. 完整代码 + +完整实现包含以下文件: + +```python + + +class OGNDataset: + def __init__(self, cfg, mode="train"): + self.cfg = cfg + self.mode = mode + self.data = None + self.labels = None + self.input_keys = tuple(cfg.MODEL.input_keys) + self.label_keys = tuple(cfg.MODEL.output_keys) + self._prepare_data() + + def _prepare_data(self): + # Get time step size for physics system + dt = 1e-2 + for sim_set in self.cfg.DATATYPE_TYPE: + if sim_set['sim'] == self.cfg.DATA.type: + dt = sim_set['dt'][0] + break + + # Generate simulation data + sim_dataset = SimulationDataset( + sim=self.cfg.DATA.type, + n=self.cfg.DATA.num_nodes, + dim=self.cfg.DATA.dimension, + dt=dt, + nt=self.cfg.DATA.time_steps, + seed=self.cfg.seed + ) + sim_dataset.simulate(self.cfg.DATA.num_samples) + + # Compute target data based on model architecture + if self.cfg.MODEL.arch == "HGN": + raw_target_data = sim_dataset.get_derivative() # [v, a] for HGN + else: + raw_target_data = sim_dataset.get_acceleration() # Acceleration for OGN/VarOGN + + # Downsample data + downsample_factor = self.cfg.DATA.downsample_factor + input_data = np.concatenate([sim_dataset.data[:, i] for i in range(0, sim_dataset.data.shape[1], downsample_factor)]) + target_data = np.concatenate([raw_target_data[:, i] for i in range(0, sim_dataset.data.shape[1], downsample_factor)]) + + # Split train/validation set + from sklearn.model_selection import train_test_split + test_size = 1.0 - getattr(self.cfg.TRAIN, 'train_split', 0.8) + + if self.mode == "train": + input_data, _, target_data, _ = train_test_split( + input_data, target_data, test_size=test_size, shuffle=False + ) + elif self.mode == "eval": + _, input_data, _, target_data = train_test_split( + input_data, target_data, test_size=test_size, shuffle=False + ) + + # Generate edge indices + edge_index = get_edge_index(self.cfg.DATA.num_nodes, self.cfg.DATA.type) + + self.data = { + "node_features": input_data.astype(np.float32), + "edge_index": np.tile(edge_index.T.numpy(), (len(input_data), 1, 1)) + } + self.labels = { + list(self.cfg.MODEL.output_keys)[0]: target_data.astype(np.float32) + } + + def __len__(self): + return len(self.data["node_features"]) + + def __getitem__(self, idx): + return { + "input": { + "node_features": self.data["node_features"][idx], + "edge_index": self.data["edge_index"][idx] + }, + "label": { + list(self.cfg.MODEL.output_keys)[0]: self.labels[list(self.cfg.MODEL.output_keys)[0]][idx] + } + } + + def get_data_and_labels(self): + return self.data, self.labels + + def __call__(self): + return self + + +def create_loss_function(cfg): + def loss_function(input_dict, label_dict, model_output): + # Get loss type from config (MAE or MSE) + loss_type = getattr(cfg.TRAIN.loss, 'type', 'MAE') + + # Get output key + output_key = list(cfg.MODEL.output_keys)[0] if cfg.MODEL.arch != "HGN" else "derivative" + + # Compute base loss + if cfg.MODEL.arch == "HGN": + output_key = "derivative" + pred_accel = model_output[output_key][:, cfg.DATA.dimension:] + target_accel = label_dict[output_key][:, cfg.DATA.dimension:] + if loss_type == "MSE": + base_loss = paddle.mean(paddle.square(pred_accel - target_accel)) + else: # MAE + base_loss = paddle.mean(paddle.abs(pred_accel - target_accel)) + else: + pred = model_output[output_key] + target = label_dict[output_key] + if loss_type == "MSE": + base_loss = paddle.mean(paddle.square(pred - target)) + else: # MAE + base_loss = paddle.mean(paddle.abs(pred - target)) + + # Add regularization if configured + if cfg.MODEL.regularization_type == "l1" and cfg.MODEL.arch in ["OGN", "VarOGN"]: + reg_loss = cfg.MODEL.l1_strength * paddle.mean(paddle.abs(input_dict["node_features"])) + return base_loss + reg_loss + + return base_loss + + return loss_function + + +def train(cfg): + # Set random seed for reproducibility + ppsci.utils.misc.set_random_seed(cfg.seed) + + # Parse automatic configuration parameters + if cfg.MODEL.n_f == "auto": + cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 + if cfg.MODEL.ndim == "auto": + cfg.MODEL.ndim = cfg.DATA.dimension + if cfg.TRAIN.batch_size == "auto": + cfg.TRAIN.batch_size = int(64 * (4 / cfg.DATA.num_nodes) ** 2) + + # Update output keys for HGN + if cfg.MODEL.arch == "HGN": + cfg.MODEL.output_keys = ["derivative"] + + # Create datasets + logger.message(f"Creating {cfg.DATA.type} dataset using simulate.py...") + train_dataset = OGNDataset(cfg, mode="train") + eval_dataset = OGNDataset(cfg, mode="eval") + + # Get training and evaluation data + train_data, train_labels = train_dataset.get_data_and_labels() + eval_data, eval_labels = eval_dataset.get_data_and_labels() + + # Create model based on architecture + logger.message(f"Creating model: {cfg.MODEL.arch}") + if cfg.MODEL.arch == "OGN": + model = OGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + msg_dim=cfg.MODEL.msg_dim, + ndim=cfg.MODEL.ndim, + dt=cfg.MODEL.dt, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + elif cfg.MODEL.arch == "VarOGN": + model = VarOGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + msg_dim=cfg.MODEL.msg_dim, + ndim=cfg.MODEL.ndim, + dt=cfg.MODEL.dt, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + elif cfg.MODEL.arch == "HGN": + model = HGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + ndim=cfg.MODEL.ndim, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + else: + raise ValueError(f"Unsupported model architecture: {cfg.MODEL.arch}") + + # Create loss function and constraints + loss_fn = create_loss_function(cfg) + train_constraint = ppsci.constraint.SupervisedConstraint( + { + "dataset": { + "name": "NamedArrayDataset", + "input": train_data, + "label": train_labels + }, + "batch_size": cfg.TRAIN.batch_size, + "sampler": {"name": "BatchSampler", "drop_last": False, "shuffle": True}, + }, + loss=loss_fn, + name="train_constraint", + ) + + eval_constraint = ppsci.constraint.SupervisedConstraint( + { + "dataset": { + "name": "NamedArrayDataset", + "input": eval_data, + "label": eval_labels + }, + "batch_size": cfg.EVAL.batch_size, + "sampler": {"name": "BatchSampler", "drop_last": False, "shuffle": False}, + }, + loss=loss_fn, + name="eval_constraint", + ) + + constraint = { + train_constraint.name: train_constraint, + eval_constraint.name: eval_constraint, + } + + # Calculate iterations per epoch + batch_per_epoch = int(1000*10 / (cfg.TRAIN.batch_size/32.0)) + + # Create optimizer and learning rate scheduler + if cfg.TRAIN.lr_scheduler.name == "OneCycleLR": + lr_scheduler = paddle.optimizer.lr.OneCycleLR( + max_learning_rate=cfg.TRAIN.lr_scheduler.max_learning_rate, + total_steps=cfg.TRAIN.epochs*batch_per_epoch, + divide_factor=cfg.TRAIN.lr_scheduler.final_div_factor + ) + else: + lr_scheduler = paddle.optimizer.lr.ExponentialDecay( + learning_rate=cfg.TRAIN.optimizer.learning_rate, + gamma=0.9 + ) + + optimizer = paddle.optimizer.Adam( + learning_rate=lr_scheduler, + parameters=model.parameters(), + weight_decay=cfg.TRAIN.optimizer.weight_decay + ) + + # Create PaddleScience Solver and start training + solver = ppsci.solver.Solver( + model, + constraint, + optimizer=optimizer, + cfg=cfg, + ) + solver.train() + + +def evaluate(cfg: DictConfig): + # Parse automatic configuration parameters + if cfg.MODEL.n_f == "auto": + cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 + if cfg.MODEL.ndim == "auto": + cfg.MODEL.ndim = cfg.DATA.dimension + if cfg.MODEL.arch == "HGN": + cfg.MODEL.output_keys = ["derivative"] + + # Create model based on architecture + if cfg.MODEL.arch == "OGN": + model = OGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + msg_dim=cfg.MODEL.msg_dim, + ndim=cfg.MODEL.ndim, + dt=cfg.MODEL.dt, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + elif cfg.MODEL.arch == "VarOGN": + model = VarOGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + msg_dim=cfg.MODEL.msg_dim, + ndim=cfg.MODEL.ndim, + dt=cfg.MODEL.dt, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + elif cfg.MODEL.arch == "HGN": + model = HGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + ndim=cfg.MODEL.ndim, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + else: + raise ValueError(f"Unsupported model architecture: {cfg.MODEL.arch}") + + # Create evaluation dataset + eval_dataset = OGNDataset(cfg, mode="eval") + eval_data, eval_labels = eval_dataset.get_data_and_labels() + + # Create loss function and constraint + loss_fn = create_loss_function(cfg) + eval_constraint = ppsci.constraint.SupervisedConstraint( + { + "dataset": { + "name": "NamedArrayDataset", + "input": eval_data, + "label": eval_labels + }, + "batch_size": cfg.EVAL.batch_size, + "sampler": {"name": "BatchSampler", "drop_last": False, "shuffle": False}, + }, + loss=loss_fn, + name="eval_constraint", + ) + + constraint = { + eval_constraint.name: eval_constraint, + } + + # Create PaddleScience Solver + solver = ppsci.solver.Solver(model, constraint, cfg=cfg) + + # Run evaluation + logger.message("Starting evaluation...") + eval_result = solver.eval() + logger.message(f"Evaluation completed. Result: {eval_result}") + + +def inference(cfg: DictConfig): + # Parse automatic configuration parameters + if cfg.MODEL.n_f == "auto": + cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 + if cfg.MODEL.ndim == "auto": + cfg.MODEL.ndim = cfg.DATA.dimension + if cfg.MODEL.arch == "HGN": + cfg.MODEL.output_keys = ["derivative"] + + # Create model based on architecture + if cfg.MODEL.arch == "OGN": + model = OGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + msg_dim=cfg.MODEL.msg_dim, + ndim=cfg.MODEL.ndim, + dt=cfg.MODEL.dt, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + elif cfg.MODEL.arch == "VarOGN": + model = VarOGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + msg_dim=cfg.MODEL.msg_dim, + ndim=cfg.MODEL.ndim, + dt=cfg.MODEL.dt, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + elif cfg.MODEL.arch == "HGN": + model = HGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + ndim=cfg.MODEL.ndim, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + else: + raise ValueError(f"Unsupported model architecture: {cfg.MODEL.arch}") + + # Create PaddleScience Solver + solver = ppsci.solver.Solver(model, cfg=cfg) + + # Generate test data + logger.message("Generating inference data using simulate.py...") + dt = 1e-2 + for sim_set in cfg.DATATYPE_TYPE: + if sim_set['sim'] == cfg.DATA.type: + dt = sim_set['dt'][0] + break + + sim_dataset = SimulationDataset( + sim=cfg.DATA.type, + n=cfg.DATA.num_nodes, + dim=cfg.DATA.dimension, + dt=dt, + nt=100, + seed=cfg.seed + ) + sim_dataset.simulate(1) + + # Prepare input data (first time step) + node_features = sim_dataset.data[0, 0] + edge_index = get_edge_index(cfg.DATA.num_nodes, cfg.DATA.type) + + input_dict = { + "node_features": paddle.to_tensor(node_features, dtype='float32').unsqueeze(0), + "edge_index": edge_index.unsqueeze(0) + } + + # Run inference + logger.message("Running inference...") + output_dict = solver.predict(input_dict, return_numpy=True) + + # Display results + if cfg.MODEL.arch == "HGN": + dq_dt = output_dict["derivative"][0, :cfg.DATA.dimension] + dv_dt = output_dict["derivative"][0, cfg.DATA.dimension:] + logger.message(f"HGN inference - dq/dt: {dq_dt}, dv/dt (acceleration): {dv_dt}") + else: + acceleration = output_dict[list(cfg.MODEL.output_keys)[0]][0] + logger.message(f"{cfg.MODEL.arch} inference - acceleration: {acceleration}") + + +def export(cfg: DictConfig): + # Parse automatic configuration parameters + if cfg.MODEL.n_f == "auto": + cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 + if cfg.MODEL.ndim == "auto": + cfg.MODEL.ndim = cfg.DATA.dimension + if cfg.MODEL.arch == "HGN": + cfg.MODEL.output_keys = ["derivative"] + + # Create model based on architecture + if cfg.MODEL.arch == "OGN": + model = OGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + msg_dim=cfg.MODEL.msg_dim, + ndim=cfg.MODEL.ndim, + dt=cfg.MODEL.dt, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + elif cfg.MODEL.arch == "VarOGN": + model = VarOGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + msg_dim=cfg.MODEL.msg_dim, + ndim=cfg.MODEL.ndim, + dt=cfg.MODEL.dt, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + elif cfg.MODEL.arch == "HGN": + model = HGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + ndim=cfg.MODEL.ndim, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + else: + raise ValueError(f"Unsupported model architecture: {cfg.MODEL.arch}") + + # Create PaddleScience Solver + solver = ppsci.solver.Solver(model, cfg=cfg) + + # Define input specifications for export + from paddle.static import InputSpec + input_spec = [ + { + "node_features": InputSpec([None, cfg.DATA.num_nodes, cfg.MODEL.n_f], "float32", "node_features"), + "edge_index": InputSpec([None, 2, cfg.DATA.num_nodes * (cfg.DATA.num_nodes - 1)], "int64", "edge_index") + } + ] + + # Export model to static graph + logger.message(f"Exporting model to {cfg.INFER.export_path}") + solver.export(input_spec, cfg.INFER.export_path, with_onnx=False) + logger.message("Model export completed!") + + +@hydra.main(version_base=None, config_path="./conf", config_name="config.yaml") +def main(cfg: DictConfig): + if cfg.mode == "train": + train(cfg) + elif cfg.mode == "eval": + evaluate(cfg) + elif cfg.mode == "infer": + inference(cfg) + elif cfg.mode == "export": + export(cfg) + else: + raise ValueError(f"Unsupported mode: {cfg.mode}") + + +if __name__ == "__main__": + main() +``` + + +## 5. 预期结果 + + +## 6. 参考资料 + +- **论文**:[Discovering Symbolic Models from Deep Learning with Inductive Biases (NeurIPS 2020)](https://arxiv.org/abs/2006.11287) +- **作者**:Miles Cranmer, Alvaro Sanchez-Gonzalez, Peter Battaglia, Rui Xu, Kyle Cranmer, David Spergel, Shirley Ho +- **代码仓库**:[github.com/MilesCranmer/symbolic_deep_learning](https://github.com/MilesCranmer/symbolic_deep_learning) +- **符号回归工具**:[PySR - Python Symbolic Regression](https://github.com/MilesCranmer/PySR) diff --git a/examples/symbolic_gcn/conf/config.yaml b/examples/symbolic_gcn/conf/config.yaml deleted file mode 100644 index 01d4da56b1..0000000000 --- a/examples/symbolic_gcn/conf/config.yaml +++ /dev/null @@ -1,84 +0,0 @@ -defaults: - - ppsci_default - - TRAIN: train_default - - TRAIN/ema: ema_default - - TRAIN/swa: swa_default - - EVAL: eval_default - - INFER: infer_default - - hydra/job/config/override_dirname/exclude_keys: exclude_keys_default - - _self_ -hydra: - run: - # dynamic output directory according to running time and override name - dir: outputs_symbolic_gcn/${now:%Y-%m-%d}/${now:%H-%M-%S}/${hydra.job.override_dirname} - job: - name: ${mode} # name of logfile - chdir: false # keep current working directory unchanged - callbacks: - init_callback: - _target_: ppsci.utils.callbacks.InitCallback - sweep: - # output directory for multirun - dir: ${hydra.run.dir} - subdir: ./ - -# general settings -mode: train # running mode: train/eval -seed: 42 -output_dir: ${hydra:run.dir} -log_freq: 20 -# dataset setting -DATASET: - label_keys: [label] - data_dir: ./dataset/train_data.pkl - -MODEL: - input_keys: [aq_train_data, mete_train_data] - output_keys: [label] - output_attention: true - seq_len: 72 - pred_len: 48 - aq_gat_node_features: 7 - aq_gat_node_num: 35 - mete_gat_node_features: 7 - mete_gat_node_num: 18 - gat_hidden_dim: 32 - gat_edge_dim: 3 - e_layers: 1 - enc_in: 7 - dec_in: 7 - c_out: 7 - d_model: 16 - embed: fixed - freq: t - dropout: 0.05 - factor: 3 - n_heads: 4 - d_ff: 32 - num_kernels: 6 - top_k: 4 - -# training settings -TRAIN: - epochs: 100 - iters_per_epoch: 400 - save_freq: 10 - eval_during_train: true - eval_freq: 10 - batch_size: 32 - learning_rate: 0.0001 - lr_scheduler: - epochs: ${TRAIN.epochs} - iters_per_epoch: ${TRAIN.iters_per_epoch} - learning_rate: 0.0005 - step_size: 20 - gamma: 0.95 - pretrained_model_path: null - checkpoint_path: null - -EVAL: - eval_data_path: ./dataset/val_data.pkl - pretrained_model_path: null - compute_metric_by_batch: false - eval_with_no_grad: true - batch_size: 32 diff --git a/examples/symbolic_gcn/gcn.py b/examples/symbolic_gcn/gcn.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/symbolic_gn/conf/config.yaml b/examples/symbolic_gn/conf/config.yaml new file mode 100644 index 0000000000..2886423a6b --- /dev/null +++ b/examples/symbolic_gn/conf/config.yaml @@ -0,0 +1,138 @@ +defaults: + - ppsci_default + - TRAIN: train_default + - TRAIN/ema: ema_default + - TRAIN/swa: swa_default + - EVAL: eval_default + - INFER: infer_default + - hydra/job/config/override_dirname/exclude_keys: exclude_keys_default + - _self_ +hydra: + run: + # dynamic output directory according to running time and override name + dir: outputs_symbolic_gcn/${now:%Y-%m-%d}/${now:%H-%M-%S}/${hydra.job.override_dirname} + job: + name: ${mode} # name of logfile + chdir: false # keep current working directory unchanged + callbacks: + init_callback: + _target_: ppsci.utils.callbacks.InitCallback + sweep: + # output directory for multirun + dir: ${hydra.run.dir} + subdir: ./ + +# general settings +mode: train # running mode: train/eval +seed: 42 +output_dir: ${hydra:run.dir} +log_freq: 20 + + +TRAIN: + epochs: 1000 + batch_size: auto # auto = int(64 * (4 / num_nodes) ** 2) + save_freq: 10 + eval_during_train: true + eval_freq: 5 + checkpoint_path: null + pretrained_model_path: null + train_split: 0.8 # Train/validation split ratio + optimizer: + learning_rate: 1e-3 + weight_decay: 1e-8 + lr_scheduler: + name: "OneCycleLR" + max_learning_rate: 1e-3 + final_div_factor: 1e5 + loss: + type: "MAE" # MAE or MSE + physics_weight: 0.0 # Physical regularization weight + message_analysis: + enabled: true + record_interval: 1 + test_sample_size: 1000 + + +EVAL: + pretrained_model_path: null + eval_with_no_grad: true + batch_size: 1024 + +INFER: + pretrained_model_path: null + export_path: ./inference/ogn_model + device: gpu + engine: native + precision: fp32 + + +MODEL: + arch: "OGN" # OGN, varOGN, HGN + input_keys: ["node_features", "edge_index"] + output_keys: ["acceleration"] + n_f: auto # auto = dimension * 2 + 2 + msg_dim: 100 + ndim: auto # auto = dimension + dt: 0.1 + hidden: 300 + aggr: "add" # add, mean, max, min + regularization_type: "l1" # l1, kl, energy, none + l1_strength: 1e-2 + +DATA: + type: "spring" # r1, r2, spring, string, charge, superposition, damped, discontinuous, string_ball + num_samples: 10000 + num_nodes: 4 + dimension: 2 + time_steps: 1000 + time_step_size: auto + downsample_factor: 5 + sample_interval: 5 # Sampling interval for training data + +DATATYPE_TYPE: + - sim: "r1" + dt: [5e-3] + nt: [1000] + n: [4, 8] + dim: [2, 3] + - sim: "r2" + dt: [1e-3] + nt: [1000] + n: [4, 8] + dim: [2, 3] + - sim: "spring" + dt: [1e-2] + nt: [1000] + n: [4, 8] + dim: [2, 3] + - sim: "string" + dt: [1e-2] + nt: [1000] + n: [30] + dim: [2] + - sim: "charge" + dt: [1e-3] + nt: [1000] + n: [4, 8] + dim: [2, 3] + - sim: "superposition" + dt: [1e-3] + nt: [1000] + n: [4, 8] + dim: [2, 3] + - sim: "damped" + dt: [2e-2] + nt: [1000] + n: [4, 8] + dim: [2, 3] + - sim: "discontinuous" + dt: [1e-2] + nt: [1000] + n: [4, 8] + dim: [2, 3] + - sim: "string_ball" + dt: [1e-2] + nt: [1000] + n: [30] + dim: [2] \ No newline at end of file diff --git a/examples/symbolic_gn/gn.py b/examples/symbolic_gn/gn.py new file mode 100644 index 0000000000..7127c21164 --- /dev/null +++ b/examples/symbolic_gn/gn.py @@ -0,0 +1,519 @@ +# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import numpy as np +import paddle +from omegaconf import DictConfig +import hydra +import ppsci +from ppsci.utils import logger + +from simulate import SimulationDataset +from ppsci.arch.symbolic_gn import OGN, VarOGN, HGN, get_edge_index + + + +class OGNDataset: + def __init__(self, cfg, mode="train"): + self.cfg = cfg + self.mode = mode + self.data = None + self.labels = None + self.input_keys = tuple(cfg.MODEL.input_keys) + self.label_keys = tuple(cfg.MODEL.output_keys) + self._prepare_data() + + def _prepare_data(self): + # Get time step size for physics system + dt = 1e-2 + for sim_set in self.cfg.DATATYPE_TYPE: + if sim_set['sim'] == self.cfg.DATA.type: + dt = sim_set['dt'][0] + break + + # Generate simulation data + sim_dataset = SimulationDataset( + sim=self.cfg.DATA.type, + n=self.cfg.DATA.num_nodes, + dim=self.cfg.DATA.dimension, + dt=dt, + nt=self.cfg.DATA.time_steps, + seed=self.cfg.seed + ) + sim_dataset.simulate(self.cfg.DATA.num_samples) + + # Compute target data based on model architecture + if self.cfg.MODEL.arch == "HGN": + raw_target_data = sim_dataset.get_derivative() # [v, a] for HGN + else: + raw_target_data = sim_dataset.get_acceleration() # Acceleration for OGN/VarOGN + + # Downsample data + downsample_factor = self.cfg.DATA.downsample_factor + input_data = np.concatenate([sim_dataset.data[:, i] for i in range(0, sim_dataset.data.shape[1], downsample_factor)]) + target_data = np.concatenate([raw_target_data[:, i] for i in range(0, sim_dataset.data.shape[1], downsample_factor)]) + + # Split train/validation set + from sklearn.model_selection import train_test_split + test_size = 1.0 - getattr(self.cfg.TRAIN, 'train_split', 0.8) + + if self.mode == "train": + input_data, _, target_data, _ = train_test_split( + input_data, target_data, test_size=test_size, shuffle=False + ) + elif self.mode == "eval": + _, input_data, _, target_data = train_test_split( + input_data, target_data, test_size=test_size, shuffle=False + ) + + # Generate edge indices + edge_index = get_edge_index(self.cfg.DATA.num_nodes, self.cfg.DATA.type) + + self.data = { + "node_features": input_data.astype(np.float32), + "edge_index": np.tile(edge_index.T.numpy(), (len(input_data), 1, 1)) + } + self.labels = { + list(self.cfg.MODEL.output_keys)[0]: target_data.astype(np.float32) + } + + def __len__(self): + return len(self.data["node_features"]) + + def __getitem__(self, idx): + return { + "input": { + "node_features": self.data["node_features"][idx], + "edge_index": self.data["edge_index"][idx] + }, + "label": { + list(self.cfg.MODEL.output_keys)[0]: self.labels[list(self.cfg.MODEL.output_keys)[0]][idx] + } + } + + def get_data_and_labels(self): + return self.data, self.labels + + def __call__(self): + return self + + +def create_loss_function(cfg): + def loss_function(input_dict, label_dict, model_output): + # Get loss type from config (MAE or MSE) + loss_type = getattr(cfg.TRAIN.loss, 'type', 'MAE') + + # Get output key + output_key = list(cfg.MODEL.output_keys)[0] if cfg.MODEL.arch != "HGN" else "derivative" + + # Compute base loss + if cfg.MODEL.arch == "HGN": + output_key = "derivative" + pred_accel = model_output[output_key][:, cfg.DATA.dimension:] + target_accel = label_dict[output_key][:, cfg.DATA.dimension:] + if loss_type == "MSE": + base_loss = paddle.mean(paddle.square(pred_accel - target_accel)) + else: # MAE + base_loss = paddle.mean(paddle.abs(pred_accel - target_accel)) + else: + pred = model_output[output_key] + target = label_dict[output_key] + if loss_type == "MSE": + base_loss = paddle.mean(paddle.square(pred - target)) + else: # MAE + base_loss = paddle.mean(paddle.abs(pred - target)) + + # Add regularization if configured + if cfg.MODEL.regularization_type == "l1" and cfg.MODEL.arch in ["OGN", "VarOGN"]: + reg_loss = cfg.MODEL.l1_strength * paddle.mean(paddle.abs(input_dict["node_features"])) + return base_loss + reg_loss + + return base_loss + + return loss_function + + +def train(cfg): + # Set random seed for reproducibility + ppsci.utils.misc.set_random_seed(cfg.seed) + + # Parse automatic configuration parameters + if cfg.MODEL.n_f == "auto": + cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 + if cfg.MODEL.ndim == "auto": + cfg.MODEL.ndim = cfg.DATA.dimension + if cfg.TRAIN.batch_size == "auto": + cfg.TRAIN.batch_size = int(64 * (4 / cfg.DATA.num_nodes) ** 2) + + # Update output keys for HGN + if cfg.MODEL.arch == "HGN": + cfg.MODEL.output_keys = ["derivative"] + + # Create datasets + logger.message(f"Creating {cfg.DATA.type} dataset using simulate.py...") + train_dataset = OGNDataset(cfg, mode="train") + eval_dataset = OGNDataset(cfg, mode="eval") + + # Get training and evaluation data + train_data, train_labels = train_dataset.get_data_and_labels() + eval_data, eval_labels = eval_dataset.get_data_and_labels() + + # Create model based on architecture + logger.message(f"Creating model: {cfg.MODEL.arch}") + if cfg.MODEL.arch == "OGN": + model = OGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + msg_dim=cfg.MODEL.msg_dim, + ndim=cfg.MODEL.ndim, + dt=cfg.MODEL.dt, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + elif cfg.MODEL.arch == "VarOGN": + model = VarOGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + msg_dim=cfg.MODEL.msg_dim, + ndim=cfg.MODEL.ndim, + dt=cfg.MODEL.dt, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + elif cfg.MODEL.arch == "HGN": + model = HGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + ndim=cfg.MODEL.ndim, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + else: + raise ValueError(f"Unsupported model architecture: {cfg.MODEL.arch}") + + # Create loss function and constraints + loss_fn = create_loss_function(cfg) + train_constraint = ppsci.constraint.SupervisedConstraint( + { + "dataset": { + "name": "NamedArrayDataset", + "input": train_data, + "label": train_labels + }, + "batch_size": cfg.TRAIN.batch_size, + "sampler": {"name": "BatchSampler", "drop_last": False, "shuffle": True}, + }, + loss=loss_fn, + name="train_constraint", + ) + + eval_constraint = ppsci.constraint.SupervisedConstraint( + { + "dataset": { + "name": "NamedArrayDataset", + "input": eval_data, + "label": eval_labels + }, + "batch_size": cfg.EVAL.batch_size, + "sampler": {"name": "BatchSampler", "drop_last": False, "shuffle": False}, + }, + loss=loss_fn, + name="eval_constraint", + ) + + constraint = { + train_constraint.name: train_constraint, + eval_constraint.name: eval_constraint, + } + + # Calculate iterations per epoch + batch_per_epoch = int(1000*10 / (cfg.TRAIN.batch_size/32.0)) + + # Create optimizer and learning rate scheduler + if cfg.TRAIN.lr_scheduler.name == "OneCycleLR": + lr_scheduler = paddle.optimizer.lr.OneCycleLR( + max_learning_rate=cfg.TRAIN.lr_scheduler.max_learning_rate, + total_steps=cfg.TRAIN.epochs*batch_per_epoch, + divide_factor=cfg.TRAIN.lr_scheduler.final_div_factor + ) + else: + lr_scheduler = paddle.optimizer.lr.ExponentialDecay( + learning_rate=cfg.TRAIN.optimizer.learning_rate, + gamma=0.9 + ) + + optimizer = paddle.optimizer.Adam( + learning_rate=lr_scheduler, + parameters=model.parameters(), + weight_decay=cfg.TRAIN.optimizer.weight_decay + ) + + # Create PaddleScience Solver and start training + solver = ppsci.solver.Solver( + model, + constraint, + optimizer=optimizer, + cfg=cfg, + ) + solver.train() + + +def evaluate(cfg: DictConfig): + # Parse automatic configuration parameters + if cfg.MODEL.n_f == "auto": + cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 + if cfg.MODEL.ndim == "auto": + cfg.MODEL.ndim = cfg.DATA.dimension + if cfg.MODEL.arch == "HGN": + cfg.MODEL.output_keys = ["derivative"] + + # Create model based on architecture + if cfg.MODEL.arch == "OGN": + model = OGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + msg_dim=cfg.MODEL.msg_dim, + ndim=cfg.MODEL.ndim, + dt=cfg.MODEL.dt, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + elif cfg.MODEL.arch == "VarOGN": + model = VarOGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + msg_dim=cfg.MODEL.msg_dim, + ndim=cfg.MODEL.ndim, + dt=cfg.MODEL.dt, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + elif cfg.MODEL.arch == "HGN": + model = HGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + ndim=cfg.MODEL.ndim, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + else: + raise ValueError(f"Unsupported model architecture: {cfg.MODEL.arch}") + + # Create evaluation dataset + eval_dataset = OGNDataset(cfg, mode="eval") + eval_data, eval_labels = eval_dataset.get_data_and_labels() + + # Create loss function and constraint + loss_fn = create_loss_function(cfg) + eval_constraint = ppsci.constraint.SupervisedConstraint( + { + "dataset": { + "name": "NamedArrayDataset", + "input": eval_data, + "label": eval_labels + }, + "batch_size": cfg.EVAL.batch_size, + "sampler": {"name": "BatchSampler", "drop_last": False, "shuffle": False}, + }, + loss=loss_fn, + name="eval_constraint", + ) + + constraint = { + eval_constraint.name: eval_constraint, + } + + # Create PaddleScience Solver + solver = ppsci.solver.Solver(model, constraint, cfg=cfg) + + # Run evaluation + logger.message("Starting evaluation...") + eval_result = solver.eval() + logger.message(f"Evaluation completed. Result: {eval_result}") + + +def inference(cfg: DictConfig): + # Parse automatic configuration parameters + if cfg.MODEL.n_f == "auto": + cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 + if cfg.MODEL.ndim == "auto": + cfg.MODEL.ndim = cfg.DATA.dimension + if cfg.MODEL.arch == "HGN": + cfg.MODEL.output_keys = ["derivative"] + + # Create model based on architecture + if cfg.MODEL.arch == "OGN": + model = OGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + msg_dim=cfg.MODEL.msg_dim, + ndim=cfg.MODEL.ndim, + dt=cfg.MODEL.dt, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + elif cfg.MODEL.arch == "VarOGN": + model = VarOGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + msg_dim=cfg.MODEL.msg_dim, + ndim=cfg.MODEL.ndim, + dt=cfg.MODEL.dt, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + elif cfg.MODEL.arch == "HGN": + model = HGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + ndim=cfg.MODEL.ndim, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + else: + raise ValueError(f"Unsupported model architecture: {cfg.MODEL.arch}") + + # Create PaddleScience Solver + solver = ppsci.solver.Solver(model, cfg=cfg) + + # Generate test data + logger.message("Generating inference data using simulate.py...") + dt = 1e-2 + for sim_set in cfg.DATATYPE_TYPE: + if sim_set['sim'] == cfg.DATA.type: + dt = sim_set['dt'][0] + break + + sim_dataset = SimulationDataset( + sim=cfg.DATA.type, + n=cfg.DATA.num_nodes, + dim=cfg.DATA.dimension, + dt=dt, + nt=100, + seed=cfg.seed + ) + sim_dataset.simulate(1) + + # Prepare input data (first time step) + node_features = sim_dataset.data[0, 0] + edge_index = get_edge_index(cfg.DATA.num_nodes, cfg.DATA.type) + + input_dict = { + "node_features": paddle.to_tensor(node_features, dtype='float32').unsqueeze(0), + "edge_index": edge_index.unsqueeze(0) + } + + # Run inference + logger.message("Running inference...") + output_dict = solver.predict(input_dict, return_numpy=True) + + # Display results + if cfg.MODEL.arch == "HGN": + dq_dt = output_dict["derivative"][0, :cfg.DATA.dimension] + dv_dt = output_dict["derivative"][0, cfg.DATA.dimension:] + logger.message(f"HGN inference - dq/dt: {dq_dt}, dv/dt (acceleration): {dv_dt}") + else: + acceleration = output_dict[list(cfg.MODEL.output_keys)[0]][0] + logger.message(f"{cfg.MODEL.arch} inference - acceleration: {acceleration}") + + +def export(cfg: DictConfig): + # Parse automatic configuration parameters + if cfg.MODEL.n_f == "auto": + cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 + if cfg.MODEL.ndim == "auto": + cfg.MODEL.ndim = cfg.DATA.dimension + if cfg.MODEL.arch == "HGN": + cfg.MODEL.output_keys = ["derivative"] + + # Create model based on architecture + if cfg.MODEL.arch == "OGN": + model = OGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + msg_dim=cfg.MODEL.msg_dim, + ndim=cfg.MODEL.ndim, + dt=cfg.MODEL.dt, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + elif cfg.MODEL.arch == "VarOGN": + model = VarOGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + msg_dim=cfg.MODEL.msg_dim, + ndim=cfg.MODEL.ndim, + dt=cfg.MODEL.dt, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + elif cfg.MODEL.arch == "HGN": + model = HGN( + input_keys=tuple(cfg.MODEL.input_keys), + output_keys=tuple(cfg.MODEL.output_keys), + n_f=cfg.MODEL.n_f, + ndim=cfg.MODEL.ndim, + hidden=cfg.MODEL.hidden, + aggr=cfg.MODEL.aggr + ) + else: + raise ValueError(f"Unsupported model architecture: {cfg.MODEL.arch}") + + # Create PaddleScience Solver + solver = ppsci.solver.Solver(model, cfg=cfg) + + # Define input specifications for export + from paddle.static import InputSpec + input_spec = [ + { + "node_features": InputSpec([None, cfg.DATA.num_nodes, cfg.MODEL.n_f], "float32", "node_features"), + "edge_index": InputSpec([None, 2, cfg.DATA.num_nodes * (cfg.DATA.num_nodes - 1)], "int64", "edge_index") + } + ] + + # Export model to static graph + logger.message(f"Exporting model to {cfg.INFER.export_path}") + solver.export(input_spec, cfg.INFER.export_path, with_onnx=False) + logger.message("Model export completed!") + + +@hydra.main(version_base=None, config_path="./conf", config_name="config.yaml") +def main(cfg: DictConfig): + if cfg.mode == "train": + train(cfg) + elif cfg.mode == "eval": + evaluate(cfg) + elif cfg.mode == "infer": + inference(cfg) + elif cfg.mode == "export": + export(cfg) + else: + raise ValueError(f"Unsupported mode: {cfg.mode}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/symbolic_gn/simulate.py b/examples/symbolic_gn/simulate.py new file mode 100644 index 0000000000..a233733279 --- /dev/null +++ b/examples/symbolic_gn/simulate.py @@ -0,0 +1,752 @@ +# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np # Standard Numpy for numerical computation +import paddle # PaddlePaddle for automatic differentiation +from matplotlib import pyplot as plt +from scipy.integrate import odeint # Scipy ODE solver +import matplotlib as mpl +from functools import partial +from tqdm import tqdm +from celluloid import Camera # For creating animations + +def make_transparent_color(ntimes, fraction): + """ + Create transparent colors for trajectory visualization. + + This function generates a gradient from white to a specified color, + with transparency increasing from 0 to 1. Used for drawing particle + trajectories where old positions are transparent and new positions are opaque. + + Args: + ntimes (int): Number of time steps, i.e., number of colors to generate. + fraction (float): Color fraction in range [0,1] for selecting color from spectrum. + + Returns: + numpy.ndarray: RGBA color array with shape (ntimes, 4). + Each row represents the color [R, G, B, Alpha] at a time step. + """ + # Initialize RGBA array, all set to 1 (white, fully opaque) + rgba = np.ones((ntimes, 4)) + + # Create transparency gradient from 0 (fully transparent) to 1 (fully opaque) + alpha = np.linspace(0, 1, ntimes)[:, np.newaxis] + + # Select base color from spectrum according to fraction + color = np.array(mpl.colors.to_rgba(mpl.cm.gist_ncar(fraction)))[np.newaxis, :] + + # Linear interpolation: from white (1) to target color + rgba[:, :] = 1*(1-alpha) + color*alpha + + # Set alpha channel + rgba[:, 3] = alpha[:, 0] + + return rgba + + +def get_potential(sim, sim_obj): + """ + Get potential energy function based on simulation type. + + This is a factory function that returns the corresponding potential energy + calculation function based on different physical scenarios. + Supported scenarios include: gravity, springs, charges, damped systems, etc. + + Args: + sim (str): Simulation type, options: + 'r2' - Inverse square law (e.g., universal gravitation) + 'r1' - Logarithmic potential + 'spring' - Spring potential + 'damped' - Damped spring + 'string' - String potential + 'string_ball' - String with ball obstacle + 'charge' - Charge interaction + 'superposition' - Superposition of gravity and charge + 'discontinuous' - Piecewise discontinuous potential + sim_obj (SimulationDataset): Simulation dataset object for obtaining parameters like dimension. + + Returns: + function: Potential energy function that takes two particle states and returns their potential energy. + """ + dim = sim_obj._dim # Spatial dimension (2D or 3D) + + def potential(x1, x2): + """ + Calculate potential energy between two particles. + + Particle state format: + 2D: [x, y, vx, vy, charge, mass] + 3D: [x, y, z, vx, vy, vz, charge, mass] + + Args: + x1 (numpy.ndarray): State vector of the first particle. + x2 (numpy.ndarray): State vector of the second particle. + + Returns: + float: Potential energy value between the two particles. + """ + # Calculate Euclidean distance between two particles + dist = np.sqrt(np.sum(np.square(x1[:dim] - x2[:dim]))) + + # Prevent numerical singularity (division by zero), add minimum distance limit + min_dist = 1e-2 + bounded_dist = dist + min_dist + + # ========== Potential energy functions for different physical scenarios ========== + + if sim == 'r2': + # Inverse square law potential: U = -G*m1*m2/r + # Physical scenarios: universal gravitation, Coulomb attraction + return -x1[-1]*x2[-1]/bounded_dist + + elif sim == 'r1': + # Logarithmic potential: U = m1*m2*ln(r) + return x1[-1]*x2[-1]*np.log(bounded_dist) + + elif sim in ['spring', 'damped']: + # Spring potential: U = (r - r0)^2, where r0=1 is natural length + potential_val = (bounded_dist - 1)**2 + + if sim == 'damped': + # Add damping dissipation term (velocity-dependent) + damping = 1 + potential_val += damping*x1[1]*x1[1+sim_obj._dim]/sim_obj._n # y direction + potential_val += damping*x1[0]*x1[0+sim_obj._dim]/sim_obj._n # x direction + if sim_obj._dim == 3: + potential_val += damping*x1[2]*x1[2+sim_obj._dim]/sim_obj._n # z direction + + return potential_val + + elif sim == 'string': + # String potential: spring potential + gravitational potential + return (bounded_dist - 1)**2 + x1[1]*x1[-1] + + elif sim == 'string_ball': + # String potential with ball obstacle + potential_val = (bounded_dist - 1)**2 + x1[1]*x1[-1] + + # Calculate distance from particle to ball center + r = np.sqrt((x1[1] + 15)**2 + (x1[0] - 5)**2) + radius = 4.0 # Ball radius + + # Soft boundary repulsion potential (using more stable form) + k_repel = 100.0 + potential_val += k_repel / np.maximum(r - radius + 0.5, 0.01) + return potential_val + + elif sim in ['charge', 'superposition']: + # Charge interaction: U = q1*q2/r (Coulomb's law) + charge1 = x1[-2] + charge2 = x2[-2] + potential_val = charge1*charge2/bounded_dist + + if sim in ['superposition']: + # Superposition: contains both gravity and charge interaction + m1 = x1[-1] + m2 = x2[-1] + potential_val += -m1*m2/bounded_dist + + return potential_val + + elif sim in ['discontinuous']: + # Piecewise discontinuous potential function + m1 = x1[-1] + m2 = x2[-1] + + # Define potential in three regions + pot_a = 0.0 # r < 1: no interaction + pot_b = 0.0 # 1 <= r < 2: no interaction + pot_c = (bounded_dist - 1)**2 # r >= 2: spring potential + + # Implement piecewise function using conditional expressions + potential_val = ( + pot_a * (bounded_dist < 1) + + (bounded_dist >= 1) * ( + pot_b * (bounded_dist < 2) + + pot_c * (bounded_dist >= 2)) + ) + return potential_val + + else: + raise NotImplementedError('No such simulation ' + str(sim)) + + return potential + + +class SimulationDataset(object): + """ + Physical system simulation dataset class (PaddlePaddle version). + + This class is used to generate and manage simulation data for many-body physical systems. + It simulates the time evolution of particle systems by numerically solving Newton's + equations of motion. + + Main features: + - Generate simulation data for multiple samples + - Compute system acceleration (using PaddlePaddle automatic differentiation) + - Visualize particle trajectories + - Support custom potential energy functions + + Attributes: + _sim (str): Simulation type. + _n (int): Number of particles. + _dim (int): Spatial dimension. + dt (float): Time step size. + nt (int): Number of time steps. + data (ndarray): Simulation data with shape (num_samples, num_timesteps, num_particles, state_dim). + times (ndarray): Time series. + G (float): Gravitational constant. + pairwise (function): Pairwise potential energy function. + """ + + def __init__(self, sim='r2', n=5, dim=2, + dt=0.01, nt=100, extra_potential=None, + **kwargs): + """ + Initialize simulation dataset. + + Args: + sim (str, optional): Simulation type. Defaults to 'r2' (inverse square law). + n (int, optional): Number of particles. Defaults to 5. + dim (int, optional): Spatial dimension (2 or 3). Defaults to 2. + dt (float, optional): Time step size. Defaults to 0.01. + nt (int, optional): Number of time steps to return. Defaults to 100. + extra_potential (function, optional): Additional potential energy function. Defaults to None. + **kwargs: Other parameters reserved for extension. + """ + self._sim = sim + self._n = n + self._dim = dim + self._kwargs = kwargs + self.dt = dt + self.nt = nt + self.data = None + + # Generate uniform time series + self.times = np.linspace(0, self.dt*self.nt, num=self.nt) + + self.G = 1 # Gravitational constant + self.extra_potential = extra_potential + + # Get pairwise potential function for the simulation type + self.pairwise = get_potential(sim=sim, sim_obj=self) + + def simulate(self, ns, seed=0): + """ + Generate simulation data. + + This method is the core function of the simulator. For each sample: + 1. Randomly initialize particle positions, velocities, masses, and charges + 2. Solve Newton's equations of motion using Scipy ODE solver + 3. Record the entire time evolution process + + Physical equations: + d²x/dt² = F/m = -∇U/m + where U is potential energy and F is force + + Args: + ns (int): Number of samples to generate. + seed (int, optional): Seed for numpy random number generator. Defaults to 0. + + Returns: + None: Results are stored in self.data. + """ + # Set random seed + np.random.seed(seed) + + n = self._n + dim = self._dim + sim = self._sim + params = 2 # charge + mass + total_dim = dim*2 + params + times = self.times + G = self.G + + def compute_pairwise_potential(x1, x2_list): + """Vectorized calculation of potential energy between one particle and multiple particles""" + potentials = [] + for x2 in x2_list: + potentials.append(self.pairwise(x1, x2)) + return np.array(potentials) + + def total_potential(xt): + """ + Calculate total potential energy of the system (using Numpy). + + Args: + xt (numpy.ndarray): System state with shape (n, total_dim). + + Returns: + float: Total potential energy of the system. + """ + sum_potential = 0.0 + + for i in range(n - 1): + if sim in ['string', 'string_ball']: + # String type: only adjacent particles interact + sum_potential += G * self.pairwise(xt[i], xt[i+1]) + else: + # Other types: fully connected + for j in range(i+1, n): + sum_potential += G * self.pairwise(xt[i], xt[j]) + + if self.extra_potential is not None: + for i in range(n): + sum_potential += self.extra_potential(xt[i]) + + return sum_potential + + def force_paddle(xt): + """ + Calculate force using PaddlePaddle automatic differentiation. + + This is a key part of the migration: + JAX: force = -grad(total_potential)(xt) + Paddle: Use paddle.grad to calculate gradient + + Complete implementation for all potential scenarios. + + Args: + xt (numpy.ndarray): System state with shape (n, total_dim). + + Returns: + numpy.ndarray: Force on each particle with shape (n, dim). + """ + # Convert numpy array to Paddle Tensor (improved precision: float32 → float64) + xt_tensor = paddle.to_tensor(xt, dtype='float64', stop_gradient=False) + + # Calculate gradient only for position coordinates + positions = xt_tensor[:, :dim] + positions.stop_gradient = False + + # Rebuild complete state for potential energy calculation + xt_for_potential = paddle.concat([ + positions, + xt_tensor[:, dim:] + ], axis=1) + + # Calculate total potential energy (using float64 for higher precision) + sum_potential = paddle.to_tensor(0.0, dtype='float64') + + # Iterate through all particle pairs to calculate potential energy + for i in range(n - 1): + if sim in ['string', 'string_ball']: + # ========== String type: only calculate adjacent particles ========== + x1 = xt_for_potential[i] + x2 = xt_for_potential[i+1] + dist = paddle.sqrt(paddle.sum(paddle.square(x1[:dim] - x2[:dim]))) + bounded_dist = dist + 1e-2 + + if sim == 'string': + # String potential: spring + gravity + pot = (bounded_dist - 1)**2 + x1[1]*x1[-1] + + elif sim == 'string_ball': + # String potential with ball obstacle + pot = (bounded_dist - 1)**2 + x1[1]*x1[-1] + + # Calculate distance to ball center + r = paddle.sqrt((x1[1] + 15)**2 + (x1[0] - 5)**2) + radius = paddle.to_tensor(4.0, dtype='float64') + + # Soft boundary repulsion potential (reduced strength to avoid numerical instability) + # More stable repulsion potential: strong repulsion when r < radius + k_repel = 100.0 # Reduced repulsion strength + pot = pot + k_repel / paddle.maximum(r - radius + 0.5, paddle.to_tensor(0.01, dtype='float64')) + + sum_potential = sum_potential + G * pot + + else: + # ========== Fully connected type: calculate all particle pairs ========== + for j in range(i+1, n): + x1 = xt_for_potential[i] + x2 = xt_for_potential[j] + dist = paddle.sqrt(paddle.sum(paddle.square(x1[:dim] - x2[:dim]))) + bounded_dist = dist + 1e-2 + + # Calculate potential based on simulation type + if sim == 'r2': + # Inverse square law: universal gravitation + pot = -x1[-1]*x2[-1]/bounded_dist + + elif sim == 'r1': + # Logarithmic potential + pot = x1[-1]*x2[-1]*paddle.log(bounded_dist) + + elif sim in ['spring', 'damped']: + # Spring potential + pot = (bounded_dist - 1)**2 + + # Add damping term + if sim == 'damped': + damping = paddle.to_tensor(1.0, dtype='float64') + # Damping dissipation term: proportional to position-velocity product + pot = pot + damping*x1[1]*x1[1+dim]/n # y direction + pot = pot + damping*x1[0]*x1[0+dim]/n # x direction + if dim == 3: + pot = pot + damping*x1[2]*x1[2+dim]/n # z direction + + elif sim in ['charge', 'superposition']: + # Charge interaction (Coulomb's law) + charge1 = x1[-2] + charge2 = x2[-2] + pot = charge1*charge2/bounded_dist + + if sim == 'superposition': + # Superposition: contains both charge and gravity + m1 = x1[-1] + m2 = x2[-1] + pot = pot + (-m1*m2/bounded_dist) + + elif sim == 'discontinuous': + # Piecewise discontinuous potential + # Define potential in three regions + pot_a = paddle.to_tensor(0.0, dtype='float64') # r < 1 + pot_b = paddle.to_tensor(0.0, dtype='float64') # 1 <= r < 2 + pot_c = (bounded_dist - 1)**2 # r >= 2 + + # Implement piecewise function using conditional expressions + # Paddle's conditional operations automatically handle gradients + cond1 = paddle.cast(bounded_dist < 1, 'float64') + cond2 = paddle.cast(bounded_dist >= 1, 'float64') * paddle.cast(bounded_dist < 2, 'float64') + cond3 = paddle.cast(bounded_dist >= 2, 'float64') + + pot = pot_a * cond1 + pot_b * cond2 + pot_c * cond3 + + else: + # Default: spring potential + pot = (bounded_dist - 1)**2 + + sum_potential = sum_potential + G * pot + + # Add external potential support + if self.extra_potential is not None: + # Note: extra_potential needs to be Paddle-compatible + # If user provides a numpy function, there will be issues + # Suggestion: use numerical gradients or require Paddle version from user + for i in range(n): + # Assume extra_potential returns scalar + # May need adjustment in actual use + try: + extra_pot = self.extra_potential(xt_for_potential[i]) + if not isinstance(extra_pot, paddle.Tensor): + extra_pot = paddle.to_tensor(extra_pot, dtype='float64') + sum_potential = sum_potential + extra_pot + except: + # If extra_potential is not Paddle-compatible, skip + # Should give warning in actual use + pass + + # Calculate gradient: F = -dU/dx + # Use paddle.grad for automatic differentiation + grads = paddle.grad( + outputs=sum_potential, + inputs=positions, + create_graph=False, + retain_graph=False + )[0] + + # Convert back to numpy + force = -grads.numpy() + + return force + + def acceleration(xt): + """ + Calculate acceleration: a = F/m. + + Args: + xt (numpy.ndarray): System state. + + Returns: + numpy.ndarray: Acceleration. + """ + force = force_paddle(xt) + masses = xt[:, -1:] # Shape (n, 1) + return force / masses + + def odefunc(y, t): + """ + Ordinary differential equation function (for scipy.odeint). + + Args: + y (numpy.ndarray): Flattened system state. + t (float): Time. + + Returns: + numpy.ndarray: Time derivative of the state. + """ + # Restore shape + y = y.reshape((n, total_dim)) + + # Calculate acceleration + a = acceleration(y) + + # Construct derivative + dydt = np.concatenate([ + y[:, dim:2*dim], # dx/dt = v + a, # dv/dt = a + np.zeros((n, params)) # d(q,m)/dt = 0 + ], axis=1) + + return dydt.flatten() + + def make_sim(sample_idx): + """ + Generate single simulation sample. + + Args: + sample_idx (int): Sample index (for progress display). + + Returns: + numpy.ndarray: Simulation trajectory with shape (nt, n, total_dim). + """ + if sim in ['string', 'string_ball']: + # String type initialization + x0 = np.random.randn(n, total_dim) + x0[:, -1] = 1.0 # Fixed mass + x0[:, 0] = np.arange(n) + x0[:, 0]*0.5 # Evenly spaced x-coordinates + x0[:, 2:3] = 0.0 # vx = 0 + else: + # General initialization + x0 = np.random.randn(n, total_dim) + x0[:, -1] = np.exp(x0[:, -1]) # Positive mass + + if sim in ['charge', 'superposition']: + x0[:, -2] = np.sign(x0[:, -2]) # Charge ±1 + + # Solve ODE using scipy.odeint + # Migration point: JAX odeint → scipy.integrate.odeint + x_times = odeint( + odefunc, + x0.flatten(), + times, + mxstep=2000 + ).reshape(-1, n, total_dim) + + return x_times + + # Generate samples in batch + data = [] + print(f"Start generating {ns} samples...") + for i in tqdm(range(ns)): + data.append(make_sim(i)) + + # Convert to numpy array + self.data = np.array(data) + print(f"Generation complete! Data shape: {self.data.shape}") + + def get_acceleration(self): + """ + Calculate acceleration for all simulation data. + + This method uses PaddlePaddle automatic differentiation to calculate acceleration. + Uses the same complete potential implementation as in simulate(). + + Returns: + numpy.ndarray: Acceleration data with shape (ns, nt, n, dim). + """ + if self.data is None: + raise ValueError("Please call simulate() first to generate data") + + ns, nt, n, total_dim = self.data.shape + dim = self._dim + sim = self._sim + G = self.G + + # Pre-allocate acceleration array + accelerations = np.zeros((ns, nt, n, dim)) + + print("Calculating acceleration...") + for sample_idx in tqdm(range(ns)): + for time_idx in range(nt): + xt = self.data[sample_idx, time_idx] + + # Calculate acceleration using PaddlePaddle (using float64 for higher precision) + xt_tensor = paddle.to_tensor(xt, dtype='float64', stop_gradient=False) + positions = xt_tensor[:, :dim] + positions.stop_gradient = False + + # Calculate total potential + xt_for_potential = paddle.concat([ + positions, + xt_tensor[:, dim:] + ], axis=1) + + sum_potential = paddle.to_tensor(0.0, dtype='float64') + + # Iterate through all particle pairs to calculate potential (identical to force_paddle) + for i in range(n - 1): + if sim in ['string', 'string_ball']: + # ========== String type ========== + x1 = xt_for_potential[i] + x2 = xt_for_potential[i+1] + dist = paddle.sqrt(paddle.sum(paddle.square(x1[:dim] - x2[:dim]))) + bounded_dist = dist + 1e-2 + + if sim == 'string': + pot = (bounded_dist - 1)**2 + x1[1]*x1[-1] + elif sim == 'string_ball': + # Complete implementation + pot = (bounded_dist - 1)**2 + x1[1]*x1[-1] + r = paddle.sqrt((x1[1] + 15)**2 + (x1[0] - 5)**2) + radius = paddle.to_tensor(4.0, dtype='float64') + # More stable repulsion potential + k_repel = 100.0 + pot = pot + k_repel / paddle.maximum(r - radius + 0.5, paddle.to_tensor(0.01, dtype='float64')) + + sum_potential = sum_potential + G * pot + else: + # ========== Fully connected type ========== + for j in range(i+1, n): + x1 = xt_for_potential[i] + x2 = xt_for_potential[j] + dist = paddle.sqrt(paddle.sum(paddle.square(x1[:dim] - x2[:dim]))) + bounded_dist = dist + 1e-2 + + if sim == 'r2': + pot = -x1[-1]*x2[-1]/bounded_dist + elif sim == 'r1': + pot = x1[-1]*x2[-1]*paddle.log(bounded_dist) + elif sim in ['spring', 'damped']: + pot = (bounded_dist - 1)**2 + + # Add damping term + if sim == 'damped': + damping = paddle.to_tensor(1.0, dtype='float64') + pot = pot + damping*x1[1]*x1[1+dim]/n + pot = pot + damping*x1[0]*x1[0+dim]/n + if dim == 3: + pot = pot + damping*x1[2]*x1[2+dim]/n + + elif sim in ['charge', 'superposition']: + charge1 = x1[-2] + charge2 = x2[-2] + pot = charge1*charge2/bounded_dist + if sim == 'superposition': + m1 = x1[-1] + m2 = x2[-1] + pot = pot + (-m1*m2/bounded_dist) + + elif sim == 'discontinuous': + # Piecewise discontinuous potential + pot_a = paddle.to_tensor(0.0, dtype='float64') + pot_b = paddle.to_tensor(0.0, dtype='float64') + pot_c = (bounded_dist - 1)**2 + + cond1 = paddle.cast(bounded_dist < 1, 'float64') + cond2 = paddle.cast(bounded_dist >= 1, 'float64') * paddle.cast(bounded_dist < 2, 'float64') + cond3 = paddle.cast(bounded_dist >= 2, 'float64') + + pot = pot_a * cond1 + pot_b * cond2 + pot_c * cond3 + + else: + pot = (bounded_dist - 1)**2 + + sum_potential = sum_potential + G * pot + + # Add external potential support + if self.extra_potential is not None: + for i in range(n): + try: + extra_pot = self.extra_potential(xt_for_potential[i]) + if not isinstance(extra_pot, paddle.Tensor): + extra_pot = paddle.to_tensor(extra_pot, dtype='float64') + sum_potential = sum_potential + extra_pot + except: + pass + + # Calculate force + grads = paddle.grad( + outputs=sum_potential, + inputs=positions, + create_graph=False, + retain_graph=False + )[0] + + force = -grads.numpy() + masses = xt[:, -1:] + accel = force / masses + + accelerations[sample_idx, time_idx] = accel + + return accelerations + + def plot(self, i, animate=False, plot_size=True, s_size=1): + """ + Visualize simulation results. + + Args: + i (int): Sample index. + animate (bool, optional): Whether to generate animation. Defaults to False. + plot_size (bool, optional): Whether to adjust point size based on mass. Defaults to True. + s_size (float, optional): Point size scaling factor. Defaults to 1. + + Returns: + None (static plot) or HTML (animation). + """ + if self.data is None: + raise ValueError("Please call simulate() first to generate data") + + n = self._n + times = self.times + x_times = self.data[i] + sim = self._sim + masses = x_times[:, :, -1] + + if not animate: + # Static plot + if sim in ['string', 'string_ball']: + rgba = make_transparent_color(len(times), 0) + for idx in range(0, len(times), len(times)//10): + ctimes = x_times[idx] + plt.plot(ctimes[:, 0], ctimes[:, 1], color=rgba[idx]) + plt.xlim(-5, 20) + plt.ylim(-20, 5) + else: + for j in range(n): + rgba = make_transparent_color(len(times), j/n) + if plot_size: + plt.scatter(x_times[:, j, 0], x_times[:, j, 1], + color=rgba, s=3*masses[:, j]*s_size) + else: + plt.scatter(x_times[:, j, 0], x_times[:, j, 1], + color=rgba, s=s_size) + else: + # Animation + if sim in ['string', 'string_ball']: + raise NotImplementedError("Animation mode not yet supported for string type") + + fig = plt.figure() + camera = Camera(fig) + d_idx = 20 + + for t_idx in range(d_idx, len(times), d_idx): + start = max([0, t_idx-300]) + ctimes = times[start:t_idx] + cx_times = x_times[start:t_idx] + + for j in range(n): + rgba = make_transparent_color(len(ctimes), j/n) + if plot_size: + plt.scatter(cx_times[:, j, 0], cx_times[:, j, 1], + color=rgba, s=3*masses[:, j]) + else: + plt.scatter(cx_times[:, j, 0], cx_times[:, j, 1], + color=rgba, s=s_size) + + camera.snap() + + from IPython.display import HTML + return HTML(camera.animate().to_jshtml()) \ No newline at end of file diff --git a/ppsci/arch/__init__.py b/ppsci/arch/__init__.py index 78f381b68e..f473aba392 100644 --- a/ppsci/arch/__init__.py +++ b/ppsci/arch/__init__.py @@ -52,6 +52,7 @@ from ppsci.arch.physx_transformer import PhysformerGPT2 # isort:skip from ppsci.arch.sfnonet import SFNONet # isort:skip from ppsci.arch.spinn import SPINN # isort:skip +from ppsci.arch.symbolic_gn import OGN, VarOGN, HGN, get_edge_index # isort:skip from ppsci.arch.tfnonet import TFNO1dNet, TFNO2dNet, TFNO3dNet # isort:skip from ppsci.arch.transformer import Transformer # isort:skip from ppsci.arch.unetex import UNetEx # isort:skip @@ -95,8 +96,10 @@ "ExtFormerMoECuboid", "FNO1d", "Generator", + "get_edge_index", "GraphCastNet", "HEDeepONets", + "HGN", "LorenzEmbedding", "LatentNO", "LatentNO_time", @@ -105,6 +108,7 @@ "ModelList", "ModifiedMLP", "NowcastNet", + "OGN", "PhyCRNet", "PhysformerGPT2", "PirateNet", @@ -119,6 +123,7 @@ "UNetEx", "UNONet", "USCNN", + "VarOGN", "VelocityDiscriminator", "VelocityGenerator", "RegDGCNN", diff --git a/ppsci/arch/symbolic_gn.py b/ppsci/arch/symbolic_gn.py new file mode 100644 index 0000000000..2ce05c4865 --- /dev/null +++ b/ppsci/arch/symbolic_gn.py @@ -0,0 +1,667 @@ +# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +import paddle.nn as nn +import numpy as np +from typing import Dict, List, Optional, Tuple + +from ppsci.arch import base + +def get_edge_index(n: int, sim: str) -> np.ndarray: + """ + Generate edge indices for graph connectivity. + + Args: + n (int): Number of nodes. + sim (str): Simulation type. + + Returns: + numpy.ndarray: Edge indices with shape [2, num_edges]. + """ + if sim in ['string', 'string_ball']: + # Chain connection + top = np.arange(0, n-1) + bottom = np.arange(1, n) + edge_index = np.concatenate([ + np.concatenate([top, bottom])[None, :], + np.concatenate([bottom, top])[None, :] + ], axis=0) + else: + # Full connection + adj = (np.ones((n, n)) - np.eye(n)).astype(int) + edge_index = np.array(np.where(adj)) + + return edge_index + + +class OGN(base.Arch): + def __init__( + self, + input_keys: List[str], + output_keys: List[str], + n_f: int = 6, + msg_dim: int = 100, + ndim: int = 2, + hidden: int = 300, + edge_index: Optional[np.ndarray] = None, + aggr: str = 'sum' + ): + """ + Initialize Object-based Graph Network (OGN). + + Args: + input_keys (List[str]): List of input keys, e.g., ['x', 'edge_index']. + output_keys (List[str]): List of output keys, e.g., ['acceleration']. + n_f (int, optional): Node feature dimension. Defaults to 6. + msg_dim (int, optional): Message dimension. Defaults to 100. + ndim (int, optional): Spatial dimension. Defaults to 2. + hidden (int, optional): Hidden layer size. Defaults to 300. + edge_index (np.ndarray, optional): Edge indices (can also be provided in forward). Defaults to None. + aggr (str, optional): Aggregation method, 'sum' or 'mean'. Defaults to 'sum'. + + Examples: + >>> import ppsci + >>> import numpy as np + >>> model = ppsci.arch.OGN( + ... input_keys=["x"], + ... output_keys=["acceleration"], + ... n_f=6, + ... ndim=2 + ... ) + >>> n_nodes = 5 + >>> edge_index = np.array([[0, 1, 1, 2, 2, 3, 3, 4], [1, 0, 2, 1, 3, 2, 4, 3]]) + >>> input_dict = { + ... "x": paddle.randn([n_nodes, 6]), + ... "edge_index": edge_index + ... } + >>> output = model(input_dict) + >>> print(output["acceleration"].shape) + [5, 2] + """ + super().__init__() + + # PaddleScience standard: store input/output keys + self.input_keys = input_keys + self.output_keys = output_keys + + # Model parameters + self.n_f = n_f + self.msg_dim = msg_dim + self.ndim = ndim + self.hidden = hidden + self.aggr = aggr + + # Edge index (optional) + if edge_index is not None: + self.register_buffer('edge_index_buffer', + paddle.to_tensor(edge_index, dtype='int64')) + else: + self.edge_index_buffer = None + + # Message function network + self.msg_fnc = nn.Sequential( + nn.Linear(2*n_f, hidden), + nn.ReLU(), + nn.Linear(hidden, hidden), + nn.ReLU(), + nn.Linear(hidden, hidden), + nn.ReLU(), + nn.Linear(hidden, msg_dim) + ) + + # Node update network + self.node_fnc = nn.Sequential( + nn.Linear(msg_dim + n_f, hidden), + nn.ReLU(), + nn.Linear(hidden, hidden), + nn.ReLU(), + nn.Linear(hidden, hidden), + nn.ReLU(), + nn.Linear(hidden, ndim) + ) + + def message_passing(self, x: paddle.Tensor, edge_index: np.ndarray) -> paddle.Tensor: + """ + Execute message passing. + + Args: + x: Node features with shape [n, n_f]. + edge_index: Edge indices with shape [2, num_edges]. + + Returns: + paddle.Tensor: Updated node features with shape [n, ndim]. + """ + # Get source and target nodes + row, col = edge_index[0], edge_index[1] + + # Collect neighbor features + x_i = x[col] # Target nodes + x_j = x[row] # Source nodes + + # Compute messages + msg_input = paddle.concat([x_i, x_j], axis=1) + msg = self.msg_fnc(msg_input) + + # Aggregate messages to target nodes + num_nodes = x.shape[0] + aggr_out = paddle.zeros([num_nodes, self.msg_dim], dtype=msg.dtype) + + for i in range(len(col)): + aggr_out[col[i]] += msg[i] + + # Update nodes + node_input = paddle.concat([x, aggr_out], axis=1) + out = self.node_fnc(node_input) + + return out + + def forward(self, inputs: Dict[str, paddle.Tensor]) -> Dict[str, paddle.Tensor]: + """ + Forward propagation (PaddleScience standard interface). + + Args: + inputs (Dict): Input dictionary containing: + - 'x': Node features [batch*n, n_f] or [n, n_f]. + - 'edge_index': Edge index information (optional if provided at initialization). + - Other possible inputs. + + Returns: + Dict: Output dictionary containing: + - 'acceleration': Predicted acceleration [batch*n, ndim]. + """ + # Extract input + x = inputs['x'] + + # Get edge index + if 'edge_index' in inputs: + # Get from input + edge_index = inputs['edge_index'] + if isinstance(edge_index, paddle.Tensor): + edge_index = edge_index.numpy() + elif self.edge_index_buffer is not None: + # Use stored edge index + edge_index = self.edge_index_buffer.numpy() + else: + raise ValueError("Must provide edge_index") + + # Execute message passing + acceleration = self.message_passing(x, edge_index) + + # Return dictionary format (PaddleScience standard) + return {self.output_keys[0]: acceleration} + + def compute_loss( + self, + inputs: Dict[str, paddle.Tensor], + labels: Dict[str, paddle.Tensor] + ) -> paddle.Tensor: + """ + Compute loss function (PaddleScience standard interface). + + Args: + inputs: Input dictionary. + labels: Label dictionary containing 'acceleration_true'. + + Returns: + paddle.Tensor: Loss scalar. + """ + # Forward propagation + outputs = self.forward(inputs) + + # Compute loss + pred = outputs[self.output_keys[0]] + true = labels.get('acceleration_true', labels.get(self.output_keys[0])) + + # MAE loss + loss = paddle.mean(paddle.abs(pred - true)) + + return loss + + +class HGN(base.Arch): + + def __init__( + self, + input_keys: List[str], + output_keys: List[str], + n_f: int = 6, + ndim: int = 2, + hidden: int = 300, + edge_index: Optional[np.ndarray] = None + ): + """ + Initialize Hamiltonian Graph Network (HGN). + + Args: + input_keys: List of input keys. + output_keys: List of output keys, e.g., ['velocity_derivative', 'acceleration']. + n_f: Node feature dimension. + ndim: Spatial dimension. + hidden: Hidden layer size. + edge_index: Edge indices (optional). + + Examples: + >>> import ppsci + >>> import numpy as np + >>> model = ppsci.arch.HGN( + ... input_keys=["x"], + ... output_keys=["acceleration"], + ... n_f=6, + ... ndim=2 + ... ) + >>> n_nodes = 5 + >>> edge_index = np.array([[0, 1, 1, 2, 2, 3, 3, 4], [1, 0, 2, 1, 3, 2, 4, 3]]) + >>> input_dict = { + ... "x": paddle.randn([n_nodes, 6]), + ... "edge_index": edge_index + ... } + >>> output = model(input_dict) + >>> print(output["acceleration"].shape) + [5, 2] + """ + super().__init__() + + self.input_keys = input_keys + self.output_keys = output_keys + self.n_f = n_f + self.ndim = ndim + self.hidden = hidden + + # Edge index + if edge_index is not None: + self.register_buffer('edge_index_buffer', + paddle.to_tensor(edge_index, dtype='int64')) + else: + self.edge_index_buffer = None + + # Pairwise energy network + self.pair_energy = nn.Sequential( + nn.Linear(2*n_f, hidden), + nn.Softplus(), + nn.Linear(hidden, hidden), + nn.Softplus(), + nn.Linear(hidden, hidden), + nn.Softplus(), + nn.Linear(hidden, 1) + ) + + # Self energy network + self.self_energy = nn.Sequential( + nn.Linear(n_f, hidden), + nn.Softplus(), + nn.Linear(hidden, hidden), + nn.Softplus(), + nn.Linear(hidden, hidden), + nn.Softplus(), + nn.Linear(hidden, 1) + ) + + def compute_energy(self, x: paddle.Tensor, edge_index: np.ndarray) -> paddle.Tensor: + """ + Compute the total energy (Hamiltonian) of the system. + + Args: + x: Node features. + edge_index: Edge indices. + + Returns: + paddle.Tensor: Energy per node. + """ + row, col = edge_index[0], edge_index[1] + + # Compute pairwise energy + x_i = x[col] + x_j = x[row] + edge_input = paddle.concat([x_i, x_j], axis=1) + pair_energies = self.pair_energy(edge_input) + + # Aggregate pairwise energy + num_nodes = x.shape[0] + aggr_pair = paddle.zeros([num_nodes, 1], dtype=pair_energies.dtype) + for i in range(len(col)): + aggr_pair[col[i]] += pair_energies[i] + + # Compute self energy + self_energies = self.self_energy(x) + + # Total energy + total_energy = aggr_pair + self_energies + + return total_energy + + def forward(self, inputs: Dict[str, paddle.Tensor]) -> Dict[str, paddle.Tensor]: + """ + Forward propagation (PaddleScience standard). + + Compute dynamics derivatives using Hamilton's equations. + + Args: + inputs: Input dictionary. + + Returns: + Dict: Output dictionary. + """ + # Extract input + x_input = inputs['x'].clone() + + # Get edge index + if 'edge_index' in inputs: + edge_index = inputs['edge_index'] + if isinstance(edge_index, paddle.Tensor): + edge_index = edge_index.numpy() + elif self.edge_index_buffer is not None: + edge_index = self.edge_index_buffer.numpy() + else: + raise ValueError("Must provide edge_index") + + # Construct Hamiltonian coordinates + # Input: [q, v, other, m] + # Needed: [q, p=m*v, other] + q = x_input[:, :self.ndim] + v = x_input[:, self.ndim:2*self.ndim] + other = x_input[:, 2*self.ndim:] + + # Extract mass + m_scalar = other[:, -1:] + m_vec = paddle.tile(m_scalar, [1, self.ndim]) + + # Compute momentum + p = v * m_vec + + # Construct Hamiltonian coordinates + x_hamilton = paddle.concat([q, p, other], axis=1) + x_hamilton.stop_gradient = False + + # Compute total energy + total_energy = self.compute_energy(x_hamilton, edge_index) + total_energy_scalar = paddle.sum(total_energy) + + # Compute Hamiltonian gradients + dH = paddle.grad( + outputs=total_energy_scalar, + inputs=x_hamilton, + create_graph=False, + retain_graph=False + )[0] + + # Extract gradients + dH_dq = dH[:, :self.ndim] + dH_dp = dH[:, self.ndim:2*self.ndim] + + # Hamilton's equations + dq_dt = dH_dp + dp_dt = -dH_dq + dv_dt = dp_dt / m_vec + + # Construct output dictionary + outputs = {} + if 'velocity_derivative' in self.output_keys: + outputs['velocity_derivative'] = dq_dt + if 'acceleration' in self.output_keys: + outputs['acceleration'] = dv_dt + + # If only one output key, return directly + if len(self.output_keys) == 1: + outputs[self.output_keys[0]] = paddle.concat([dq_dt, dv_dt], axis=1) + + return outputs + + def compute_loss( + self, + inputs: Dict[str, paddle.Tensor], + labels: Dict[str, paddle.Tensor], + reg_weight: float = 1e-6 + ) -> paddle.Tensor: + """ + Compute loss with physical regularization. + + Args: + inputs: Input dictionary. + labels: Label dictionary. + reg_weight: Regularization weight. + + Returns: + paddle.Tensor: Total loss. + """ + # Forward propagation + outputs = self.forward(inputs) + + # Base loss + pred = outputs.get('acceleration', outputs.get(self.output_keys[0])) + true = labels.get('acceleration_true', labels.get('acceleration')) + + base_loss = paddle.mean(paddle.abs(pred - true)) + + # Physical regularization: energy should not depend on non-physical quantities + x_input = inputs['x'].clone() + edge_index = inputs.get('edge_index', self.edge_index_buffer.numpy()) + if isinstance(edge_index, paddle.Tensor): + edge_index = edge_index.numpy() + + # Construct Hamiltonian coordinates + q = x_input[:, :self.ndim] + v = x_input[:, self.ndim:2*self.ndim] + other = x_input[:, 2*self.ndim:] + + m_scalar = other[:, -1:] + m_vec = paddle.tile(m_scalar, [1, self.ndim]) + p = v * m_vec + + x_hamilton = paddle.concat([q, p, other], axis=1) + x_hamilton.stop_gradient = False + + # Compute energy + total_energy = self.compute_energy(x_hamilton, edge_index) + + # Regularization: penalize dependence on non-physical quantities + regularization = reg_weight * paddle.mean(total_energy**2) + + return base_loss + regularization + + +class VarOGN(base.Arch): + + def __init__( + self, + input_keys: List[str], + output_keys: List[str], + n_f: int = 6, + msg_dim: int = 100, + ndim: int = 2, + hidden: int = 300, + edge_index: Optional[np.ndarray] = None, + enable_sampling: bool = True + ): + """ + Initialize Variational Graph Network (VarGN). + + Args: + input_keys: List of input keys. + output_keys: List of output keys. + n_f: Node feature dimension. + msg_dim: Message dimension. + ndim: Spatial dimension. + hidden: Hidden layer size. + edge_index: Edge indices. + enable_sampling: Whether to enable sampling. + + Examples: + >>> import ppsci + >>> import numpy as np + >>> model = ppsci.arch.VarGN( + ... input_keys=["x"], + ... output_keys=["acceleration_mean", "acceleration_std"], + ... n_f=6, + ... ndim=2 + ... ) + >>> n_nodes = 5 + >>> edge_index = np.array([[0, 1, 1, 2, 2, 3, 3, 4], [1, 0, 2, 1, 3, 2, 4, 3]]) + >>> input_dict = { + ... "x": paddle.randn([n_nodes, 6]), + ... "edge_index": edge_index + ... } + >>> output = model(input_dict) + >>> print(output["acceleration_mean"].shape) + [5, 2] + """ + super().__init__() + + self.input_keys = input_keys + self.output_keys = output_keys + self.n_f = n_f + self.msg_dim = msg_dim + self.ndim = ndim + self.hidden = hidden + self.enable_sampling = enable_sampling + + # Edge index + if edge_index is not None: + self.register_buffer('edge_index_buffer', + paddle.to_tensor(edge_index, dtype='int64')) + else: + self.edge_index_buffer = None + + # Message function: outputs mu and logvar + self.msg_fnc = nn.Sequential( + nn.Linear(2*n_f, hidden), + nn.ReLU(), + nn.Linear(hidden, hidden), + nn.ReLU(), + nn.Linear(hidden, hidden), + nn.ReLU(), + nn.Linear(hidden, msg_dim*2) # mu and logvar + ) + + # Node update function + self.node_fnc = nn.Sequential( + nn.Linear(msg_dim + n_f, hidden), + nn.ReLU(), + nn.Linear(hidden, hidden), + nn.ReLU(), + nn.Linear(hidden, hidden), + nn.ReLU(), + nn.Linear(hidden, ndim) + ) + + def message_passing(self, x: paddle.Tensor, edge_index: np.ndarray) -> Tuple[paddle.Tensor, paddle.Tensor]: + """ + Variational message passing. + + Returns: + Tuple[paddle.Tensor, paddle.Tensor]: (mean output, variance output). + """ + row, col = edge_index[0], edge_index[1] + + x_i = x[col] + x_j = x[row] + + # Compute mu and logvar for messages + msg_input = paddle.concat([x_i, x_j], axis=1) + raw_msg = self.msg_fnc(msg_input) + + mu = raw_msg[:, 0::2] + logvar = raw_msg[:, 1::2] + + # Sample messages + if self.enable_sampling and self.training: + epsilon = paddle.randn(mu.shape) + msg = mu + epsilon * paddle.exp(logvar / 2.0) + else: + msg = mu + + # Aggregate + num_nodes = x.shape[0] + aggr_out = paddle.zeros([num_nodes, self.msg_dim], dtype=msg.dtype) + + for i in range(len(col)): + aggr_out[col[i]] += msg[i] + + # Update nodes + node_input = paddle.concat([x, aggr_out], axis=1) + out_mean = self.node_fnc(node_input) + + # Compute output variance (simplified version) + out_std = paddle.exp(logvar.mean(axis=0, keepdim=True)) + + return out_mean, out_std + + def forward(self, inputs: Dict[str, paddle.Tensor]) -> Dict[str, paddle.Tensor]: + """ + Forward propagation (PaddleScience standard). + + Args: + inputs: Input dictionary. + + Returns: + Dict: Output dictionary containing mean and standard deviation. + """ + x = inputs['x'] + + # Get edge index + if 'edge_index' in inputs: + edge_index = inputs['edge_index'] + if isinstance(edge_index, paddle.Tensor): + edge_index = edge_index.numpy() + elif self.edge_index_buffer is not None: + edge_index = self.edge_index_buffer.numpy() + else: + raise ValueError("Must provide edge_index") + + # Execute variational message passing + accel_mean, accel_std = self.message_passing(x, edge_index) + + # Construct output + outputs = {} + if 'acceleration_mean' in self.output_keys: + outputs['acceleration_mean'] = accel_mean + if 'acceleration_std' in self.output_keys: + outputs['acceleration_std'] = accel_std + + # Default output + if len(outputs) == 0: + outputs[self.output_keys[0]] = accel_mean + + return outputs + + def compute_loss( + self, + inputs: Dict[str, paddle.Tensor], + labels: Dict[str, paddle.Tensor], + kl_weight: float = 1e-3 + ) -> paddle.Tensor: + """ + Compute variational loss including KL divergence. + + Args: + inputs: Input dictionary. + labels: Label dictionary. + kl_weight: KL divergence weight. + + Returns: + paddle.Tensor: Total loss. + """ + outputs = self.forward(inputs) + + # Reconstruction loss + pred = outputs.get('acceleration_mean', outputs.get(self.output_keys[0])) + true = labels.get('acceleration_true', labels.get('acceleration')) + + recon_loss = paddle.mean(paddle.abs(pred - true)) + + # KL divergence (simplified version) + # Full version needs to be computed in message_passing + kl_loss = paddle.to_tensor(0.0) # Placeholder + + return recon_loss + kl_weight * kl_loss \ No newline at end of file From 2a9c50ee7c7289c14fac0eb45b07037ddaa3c535 Mon Sep 17 00:00:00 2001 From: ADream-ki <2085127827@qq.com> Date: Wed, 29 Oct 2025 23:56:11 +0800 Subject: [PATCH 03/10] Symbolic GNN --- examples/symbolic_gn/conf/config.yaml | 44 +- examples/symbolic_gn/conf/config_hgn.yaml | 139 ++++ examples/symbolic_gn/conf/config_varogn.yaml | 139 ++++ examples/symbolic_gn/gn.py | 703 +++++++++---------- ppsci/arch/symbolic_gn.py | 403 +++++++---- 5 files changed, 875 insertions(+), 553 deletions(-) create mode 100644 examples/symbolic_gn/conf/config_hgn.yaml create mode 100644 examples/symbolic_gn/conf/config_varogn.yaml diff --git a/examples/symbolic_gn/conf/config.yaml b/examples/symbolic_gn/conf/config.yaml index 2886423a6b..f38cf0a82f 100644 --- a/examples/symbolic_gn/conf/config.yaml +++ b/examples/symbolic_gn/conf/config.yaml @@ -23,18 +23,19 @@ hydra: subdir: ./ # general settings -mode: train # running mode: train/eval +mode: train # running mode: train/eval/export/inference seed: 42 output_dir: ${hydra:run.dir} log_freq: 20 +device: gpu TRAIN: epochs: 1000 - batch_size: auto # auto = int(64 * (4 / num_nodes) ** 2) - save_freq: 10 + batch_size: 32 + save_freq: 100 eval_during_train: true - eval_freq: 5 + eval_freq: 100 checkpoint_path: null pretrained_model_path: null train_split: 0.8 # Train/validation split ratio @@ -48,28 +49,39 @@ TRAIN: loss: type: "MAE" # MAE or MSE physics_weight: 0.0 # Physical regularization weight - message_analysis: - enabled: true - record_interval: 1 - test_sample_size: 1000 - +# evaluation settings EVAL: pretrained_model_path: null eval_with_no_grad: true - batch_size: 1024 + batch_size: 32 + message_analysis: + enabled: true + +# export settings +export_path: "./exported_model" INFER: pretrained_model_path: null export_path: ./inference/ogn_model - device: gpu + pdmodel_path: ./inference/ogn_model.pdmodel + pdiparams_path: ./inference/ogn_model.pdiparams + onnx_path: ${INFER.export_path}.onnx + device: cpu engine: native precision: fp32 + batch_size: 1 + ir_optim: false + min_subgraph_size: 1 + gpu_mem: 2000 + gpu_id: 0 + max_batch_size: 1024 + num_cpu_threads: 10 MODEL: arch: "OGN" # OGN, varOGN, HGN - input_keys: ["node_features", "edge_index"] + input_keys: ["x", "edge_index"] output_keys: ["acceleration"] n_f: auto # auto = dimension * 2 + 2 msg_dim: 100 @@ -82,11 +94,11 @@ MODEL: DATA: type: "spring" # r1, r2, spring, string, charge, superposition, damped, discontinuous, string_ball - num_samples: 10000 - num_nodes: 4 + num_samples: 1000 + num_nodes: 6 dimension: 2 - time_steps: 1000 - time_step_size: auto + time_steps: 10 + time_step_size: 0.01 downsample_factor: 5 sample_interval: 5 # Sampling interval for training data diff --git a/examples/symbolic_gn/conf/config_hgn.yaml b/examples/symbolic_gn/conf/config_hgn.yaml new file mode 100644 index 0000000000..fd2f97c5ff --- /dev/null +++ b/examples/symbolic_gn/conf/config_hgn.yaml @@ -0,0 +1,139 @@ +defaults: + - ppsci_default + - TRAIN: train_default + - TRAIN/ema: ema_default + - TRAIN/swa: swa_default + - EVAL: eval_default + - INFER: infer_default + - hydra/job/config/override_dirname/exclude_keys: exclude_keys_default + - _self_ +hydra: + run: + # dynamic output directory according to running time and override name + dir: outputs_symbolic_gcn/${now:%Y-%m-%d}/${now:%H-%M-%S}/${hydra.job.override_dirname} + job: + name: ${mode} # name of logfile + chdir: false # keep current working directory unchanged + callbacks: + init_callback: + _target_: ppsci.utils.callbacks.InitCallback + sweep: + # output directory for multirun + dir: ${hydra.run.dir} + subdir: ./ + +# general settings +mode: train # running mode: train/eval/export/inference +seed: 42 +output_dir: ${hydra:run.dir} +log_freq: 20 +device: gpu + +TRAIN: + epochs: 10 + batch_size: 32 + save_freq: 10 + eval_during_train: true + eval_freq: 5 + checkpoint_path: null + pretrained_model_path: null + train_split: 0.8 # Train/validation split ratio + optimizer: + learning_rate: 1e-3 + weight_decay: 1e-8 + lr_scheduler: + name: "OneCycleLR" + max_learning_rate: 1e-3 + final_div_factor: 1e5 + loss: + type: "MAE" # MAE or MSE + physics_weight: 0.0 # Physical regularization weight + message_analysis: + enabled: true + record_interval: 1 + test_sample_size: 10 + +EVAL: + pretrained_model_path: null + eval_with_no_grad: true + batch_size: 1024 + +# export settings +export_path: "./exported_model_hgn" + +INFER: + pretrained_model_path: null + export_path: ./inference/hgn_model + device: gpu + engine: native + precision: fp32 + +MODEL: + arch: "HGN" # Hamiltonian Graph Network + input_keys: ["x", "edge_index"] + output_keys: ["acceleration"] # HGN outputs acceleration (same as VarOGN) + n_f: auto # auto = dimension * 2 + 2 + msg_dim: 100 # Not used in HGN, but kept for compatibility + ndim: auto # auto = dimension + dt: 0.1 + hidden: 300 + aggr: "add" # Not used in HGN, but kept for compatibility + regularization_type: "energy" # HGN uses energy regularization + l1_strength: 0.0 # Not used in HGN + +DATA: + type: "spring" # r1, r2, spring, string, charge, superposition, damped, discontinuous, string_ball + num_samples: 100 + num_nodes: 4 + dimension: 2 + time_steps: 10 + time_step_size: 0.01 + downsample_factor: 5 + sample_interval: 5 # Sampling interval for training data + +DATATYPE_TYPE: + - sim: "r1" + dt: [5e-3] + nt: [100] + n: [4, 8] + dim: [2, 3] + - sim: "r2" + dt: [1e-3] + nt: [100] + n: [4, 8] + dim: [2, 3] + - sim: "spring" + dt: [1e-2] + nt: [100] + n: [4, 8] + dim: [2, 3] + - sim: "string" + dt: [1e-2] + nt: [100] + n: [30] + dim: [2] + - sim: "charge" + dt: [1e-3] + nt: [100] + n: [4, 8] + dim: [2, 3] + - sim: "superposition" + dt: [1e-3] + nt: [100] + n: [4, 8] + dim: [2, 3] + - sim: "damped" + dt: [2e-2] + nt: [100] + n: [4, 8] + dim: [2, 3] + - sim: "discontinuous" + dt: [1e-2] + nt: [100] + n: [4, 8] + dim: [2, 3] + - sim: "string_ball" + dt: [1e-2] + nt: [100] + n: [30] + dim: [2] diff --git a/examples/symbolic_gn/conf/config_varogn.yaml b/examples/symbolic_gn/conf/config_varogn.yaml new file mode 100644 index 0000000000..f1ad28016b --- /dev/null +++ b/examples/symbolic_gn/conf/config_varogn.yaml @@ -0,0 +1,139 @@ +defaults: + - ppsci_default + - TRAIN: train_default + - TRAIN/ema: ema_default + - TRAIN/swa: swa_default + - EVAL: eval_default + - INFER: infer_default + - hydra/job/config/override_dirname/exclude_keys: exclude_keys_default + - _self_ +hydra: + run: + # dynamic output directory according to running time and override name + dir: outputs_symbolic_gcn/${now:%Y-%m-%d}/${now:%H-%M-%S}/${hydra.job.override_dirname} + job: + name: ${mode} # name of logfile + chdir: false # keep current working directory unchanged + callbacks: + init_callback: + _target_: ppsci.utils.callbacks.InitCallback + sweep: + # output directory for multirun + dir: ${hydra.run.dir} + subdir: ./ + +# general settings +mode: train # running mode: train/eval/export/inference +seed: 42 +output_dir: ${hydra:run.dir} +log_freq: 20 +device: gpu + +TRAIN: + epochs: 10 + batch_size: 32 + save_freq: 10 + eval_during_train: true + eval_freq: 5 + checkpoint_path: null + pretrained_model_path: null + train_split: 0.8 # Train/validation split ratio + optimizer: + learning_rate: 1e-3 + weight_decay: 1e-8 + lr_scheduler: + name: "OneCycleLR" + max_learning_rate: 1e-3 + final_div_factor: 1e5 + loss: + type: "MAE" # MAE or MSE + physics_weight: 0.0 # Physical regularization weight + message_analysis: + enabled: true + record_interval: 1 + test_sample_size: 10 + +EVAL: + pretrained_model_path: null + eval_with_no_grad: true + batch_size: 1024 + +# export settings +export_path: "./exported_model_varogn" + +INFER: + pretrained_model_path: null + export_path: ./inference/varogn_model + device: gpu + engine: native + precision: fp32 + +MODEL: + arch: "VarOGN" # Variational Object-based Graph Network + input_keys: ["x", "edge_index"] + output_keys: ["acceleration"] # VarOGN outputs mean acceleration + n_f: auto # auto = dimension * 2 + 2 + msg_dim: 100 + ndim: auto # auto = dimension + dt: 0.1 + hidden: 300 + aggr: "add" # add, mean, max, min + regularization_type: "kl" # VarOGN uses KL divergence regularization + l1_strength: 1e-3 # L1 regularization strength + +DATA: + type: "spring" # r1, r2, spring, string, charge, superposition, damped, discontinuous, string_ball + num_samples: 100 + num_nodes: 4 + dimension: 2 + time_steps: 10 + time_step_size: 0.01 + downsample_factor: 5 + sample_interval: 5 # Sampling interval for training data + +DATATYPE_TYPE: + - sim: "r1" + dt: [5e-3] + nt: [100] + n: [4, 8] + dim: [2, 3] + - sim: "r2" + dt: [1e-3] + nt: [100] + n: [4, 8] + dim: [2, 3] + - sim: "spring" + dt: [1e-2] + nt: [100] + n: [4, 8] + dim: [2, 3] + - sim: "string" + dt: [1e-2] + nt: [100] + n: [30] + dim: [2] + - sim: "charge" + dt: [1e-3] + nt: [100] + n: [4, 8] + dim: [2, 3] + - sim: "superposition" + dt: [1e-3] + nt: [100] + n: [4, 8] + dim: [2, 3] + - sim: "damped" + dt: [2e-2] + nt: [100] + n: [4, 8] + dim: [2, 3] + - sim: "discontinuous" + dt: [1e-2] + nt: [100] + n: [4, 8] + dim: [2, 3] + - sim: "string_ball" + dt: [1e-2] + nt: [100] + n: [30] + dim: [2] diff --git a/examples/symbolic_gn/gn.py b/examples/symbolic_gn/gn.py index 7127c21164..331a43416b 100644 --- a/examples/symbolic_gn/gn.py +++ b/examples/symbolic_gn/gn.py @@ -14,505 +14,440 @@ import os import sys +from typing import List, Optional import numpy as np import paddle from omegaconf import DictConfig import hydra import ppsci from ppsci.utils import logger +import matplotlib.pyplot as plt from simulate import SimulationDataset from ppsci.arch.symbolic_gn import OGN, VarOGN, HGN, get_edge_index - -class OGNDataset: - def __init__(self, cfg, mode="train"): - self.cfg = cfg - self.mode = mode - self.data = None - self.labels = None - self.input_keys = tuple(cfg.MODEL.input_keys) - self.label_keys = tuple(cfg.MODEL.output_keys) - self._prepare_data() - - def _prepare_data(self): - # Get time step size for physics system - dt = 1e-2 - for sim_set in self.cfg.DATATYPE_TYPE: - if sim_set['sim'] == self.cfg.DATA.type: - dt = sim_set['dt'][0] - break - - # Generate simulation data - sim_dataset = SimulationDataset( - sim=self.cfg.DATA.type, - n=self.cfg.DATA.num_nodes, - dim=self.cfg.DATA.dimension, - dt=dt, - nt=self.cfg.DATA.time_steps, - seed=self.cfg.seed +def create_ppsci_model( + model_type: str = 'OGN', + input_keys: List[str] = ['x', 'edge_index'], + output_keys: List[str] = ['acceleration'], + n_f: int = 6, + msg_dim: int = 100, + ndim: int = 2, + hidden: int = 300, + edge_index: Optional[np.ndarray] = None, + l1_strength: float = 0.0 +): + if model_type == 'OGN': + model = OGN( + input_keys=input_keys, + output_keys=output_keys, + n_f=n_f, + msg_dim=msg_dim, + ndim=ndim, + hidden=hidden, + edge_index=edge_index, + l1_strength=l1_strength ) - sim_dataset.simulate(self.cfg.DATA.num_samples) - - # Compute target data based on model architecture - if self.cfg.MODEL.arch == "HGN": - raw_target_data = sim_dataset.get_derivative() # [v, a] for HGN - else: - raw_target_data = sim_dataset.get_acceleration() # Acceleration for OGN/VarOGN - - # Downsample data - downsample_factor = self.cfg.DATA.downsample_factor - input_data = np.concatenate([sim_dataset.data[:, i] for i in range(0, sim_dataset.data.shape[1], downsample_factor)]) - target_data = np.concatenate([raw_target_data[:, i] for i in range(0, sim_dataset.data.shape[1], downsample_factor)]) - - # Split train/validation set - from sklearn.model_selection import train_test_split - test_size = 1.0 - getattr(self.cfg.TRAIN, 'train_split', 0.8) - - if self.mode == "train": - input_data, _, target_data, _ = train_test_split( - input_data, target_data, test_size=test_size, shuffle=False - ) - elif self.mode == "eval": - _, input_data, _, target_data = train_test_split( - input_data, target_data, test_size=test_size, shuffle=False - ) - - # Generate edge indices - edge_index = get_edge_index(self.cfg.DATA.num_nodes, self.cfg.DATA.type) - - self.data = { - "node_features": input_data.astype(np.float32), - "edge_index": np.tile(edge_index.T.numpy(), (len(input_data), 1, 1)) - } - self.labels = { - list(self.cfg.MODEL.output_keys)[0]: target_data.astype(np.float32) - } + elif model_type == 'HGN': + model = HGN( + input_keys=input_keys, + output_keys=output_keys, + n_f=n_f, + ndim=ndim, + hidden=hidden, + edge_index=edge_index + ) + elif model_type == 'VarOGN': + model = VarOGN( + input_keys=input_keys, + output_keys=output_keys, + n_f=n_f, + msg_dim=msg_dim, + ndim=ndim, + hidden=hidden, + edge_index=edge_index, + l1_strength=l1_strength + ) + else: + raise ValueError(f"未知的模型类型: {model_type}") + + return model - def __len__(self): - return len(self.data["node_features"]) - def __getitem__(self, idx): - return { - "input": { - "node_features": self.data["node_features"][idx], - "edge_index": self.data["edge_index"][idx] - }, - "label": { - list(self.cfg.MODEL.output_keys)[0]: self.labels[list(self.cfg.MODEL.output_keys)[0]][idx] - } - } - - def get_data_and_labels(self): - return self.data, self.labels - - def __call__(self): - return self +def replicate_array(arr, n): + return np.tile(arr, (n,) + (1,) * arr.ndim) def create_loss_function(cfg): - def loss_function(input_dict, label_dict, model_output): - # Get loss type from config (MAE or MSE) + def loss_function(output_dict, label_dict, weight_dict=None): loss_type = getattr(cfg.TRAIN.loss, 'type', 'MAE') - # Get output key - output_key = list(cfg.MODEL.output_keys)[0] if cfg.MODEL.arch != "HGN" else "derivative" + output_key = list(cfg.MODEL.output_keys)[0] - # Compute base loss - if cfg.MODEL.arch == "HGN": - output_key = "derivative" - pred_accel = model_output[output_key][:, cfg.DATA.dimension:] - target_accel = label_dict[output_key][:, cfg.DATA.dimension:] - if loss_type == "MSE": - base_loss = paddle.mean(paddle.square(pred_accel - target_accel)) - else: # MAE - base_loss = paddle.mean(paddle.abs(pred_accel - target_accel)) + target = label_dict[output_key] + pred = output_dict[output_key] + if loss_type == "MSE": + base_loss = paddle.mean(paddle.square(pred - target)) else: - pred = model_output[output_key] - target = label_dict[output_key] - if loss_type == "MSE": - base_loss = paddle.mean(paddle.square(pred - target)) - else: # MAE - base_loss = paddle.mean(paddle.abs(pred - target)) - - # Add regularization if configured - if cfg.MODEL.regularization_type == "l1" and cfg.MODEL.arch in ["OGN", "VarOGN"]: - reg_loss = cfg.MODEL.l1_strength * paddle.mean(paddle.abs(input_dict["node_features"])) - return base_loss + reg_loss - - return base_loss + base_loss = paddle.mean(paddle.abs(pred - target)) + + if 'l1_regularization' in label_dict: + return base_loss + label_dict['l1_regularization'] + + return {output_key: base_loss} return loss_function def train(cfg): - # Set random seed for reproducibility ppsci.utils.misc.set_random_seed(cfg.seed) - # Parse automatic configuration parameters + if cfg.MODEL.arch == "HGN": + cfg.MODEL.output_keys = ["acceleration"] + + # Create simulation dataset + sim = SimulationDataset( + sim=cfg.DATA.type, + n=cfg.DATA.num_nodes, + dim=cfg.DATA.dimension, + nt=cfg.DATA.time_steps, + dt=cfg.DATA.time_step_size + ) + sim.simulate(cfg.DATA.num_samples) + accel_data = sim.get_acceleration() + + X_list = [] + y_list = [] + for sample_idx in range(cfg.DATA.num_samples): + for t in range(0, sim.data.shape[1], cfg.DATA.sample_interval): + X_list.append(sim.data[sample_idx, t]) + y_list.append(accel_data[sample_idx, t]) + + X = np.array(X_list, dtype=np.float32) + y = np.array(y_list, dtype=np.float32) + if cfg.MODEL.n_f == "auto": cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 if cfg.MODEL.ndim == "auto": cfg.MODEL.ndim = cfg.DATA.dimension - if cfg.TRAIN.batch_size == "auto": - cfg.TRAIN.batch_size = int(64 * (4 / cfg.DATA.num_nodes) ** 2) - - # Update output keys for HGN - if cfg.MODEL.arch == "HGN": - cfg.MODEL.output_keys = ["derivative"] - - # Create datasets - logger.message(f"Creating {cfg.DATA.type} dataset using simulate.py...") - train_dataset = OGNDataset(cfg, mode="train") - eval_dataset = OGNDataset(cfg, mode="eval") - - # Get training and evaluation data - train_data, train_labels = train_dataset.get_data_and_labels() - eval_data, eval_labels = eval_dataset.get_data_and_labels() - # Create model based on architecture - logger.message(f"Creating model: {cfg.MODEL.arch}") - if cfg.MODEL.arch == "OGN": - model = OGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - msg_dim=cfg.MODEL.msg_dim, - ndim=cfg.MODEL.ndim, - dt=cfg.MODEL.dt, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr - ) - elif cfg.MODEL.arch == "VarOGN": - model = VarOGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - msg_dim=cfg.MODEL.msg_dim, - ndim=cfg.MODEL.ndim, - dt=cfg.MODEL.dt, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr - ) - elif cfg.MODEL.arch == "HGN": - model = HGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - ndim=cfg.MODEL.ndim, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr - ) - else: - raise ValueError(f"Unsupported model architecture: {cfg.MODEL.arch}") + train_size = int(len(X) * 0.8) + X_train, X_val = X[:train_size], X[train_size:] + y_train, y_val = y[:train_size], y[train_size:] + edge_index = get_edge_index(cfg.DATA.num_nodes, cfg.DATA.type) + edge_index_train = replicate_array(edge_index, len(X_train)) + edge_index_val = replicate_array(edge_index, len(X_val)) - # Create loss function and constraints - loss_fn = create_loss_function(cfg) train_constraint = ppsci.constraint.SupervisedConstraint( { "dataset": { "name": "NamedArrayDataset", - "input": train_data, - "label": train_labels + "input": {"x": X_train, "edge_index": edge_index_train}, + "label": {cfg.MODEL.output_keys[0]: y_train}, }, "batch_size": cfg.TRAIN.batch_size, - "sampler": {"name": "BatchSampler", "drop_last": False, "shuffle": True}, + "sampler": { + "name": "BatchSampler", + "drop_last": False, + "shuffle": True, + }, }, - loss=loss_fn, - name="train_constraint", + create_loss_function(cfg), + name="sup_constraint", ) - eval_constraint = ppsci.constraint.SupervisedConstraint( { "dataset": { "name": "NamedArrayDataset", - "input": eval_data, - "label": eval_labels + "input": {"x": X_val, "edge_index": edge_index_val}, + "label": {cfg.MODEL.output_keys[0]: y_val}, + }, + "batch_size": cfg.TRAIN.batch_size, + "sampler": { + "name": "BatchSampler", + "drop_last": False, + "shuffle": False, }, - "batch_size": cfg.EVAL.batch_size, - "sampler": {"name": "BatchSampler", "drop_last": False, "shuffle": False}, }, - loss=loss_fn, + create_loss_function(cfg), name="eval_constraint", ) - constraint = { train_constraint.name: train_constraint, eval_constraint.name: eval_constraint, } + l1_strength = cfg.MODEL.l1_strength if cfg.MODEL.regularization_type == "l1" and cfg.MODEL.arch in ["OGN", "VarOGN"] else 0.0 + model = create_ppsci_model( + model_type=cfg.MODEL.arch, + n_f=cfg.MODEL.n_f, + msg_dim=cfg.MODEL.msg_dim, + ndim=cfg.MODEL.ndim, + hidden=cfg.MODEL.hidden, + edge_index=edge_index, + l1_strength=l1_strength + ) - # Calculate iterations per epoch - batch_per_epoch = int(1000*10 / (cfg.TRAIN.batch_size/32.0)) - - # Create optimizer and learning rate scheduler - if cfg.TRAIN.lr_scheduler.name == "OneCycleLR": - lr_scheduler = paddle.optimizer.lr.OneCycleLR( - max_learning_rate=cfg.TRAIN.lr_scheduler.max_learning_rate, - total_steps=cfg.TRAIN.epochs*batch_per_epoch, - divide_factor=cfg.TRAIN.lr_scheduler.final_div_factor - ) - else: - lr_scheduler = paddle.optimizer.lr.ExponentialDecay( - learning_rate=cfg.TRAIN.optimizer.learning_rate, - gamma=0.9 - ) + # batch_per_epoch = int(1000*10 / (cfg.TRAIN.batch_size/32.0)) + # if cfg.TRAIN.lr_scheduler.name == "OneCycleLR": + # lr_scheduler = paddle.optimizer.lr.OneCycleLR( + # max_learning_rate=cfg.TRAIN.lr_scheduler.max_learning_rate, + # total_steps=cfg.TRAIN.epochs*batch_per_epoch, + # divide_factor=cfg.TRAIN.lr_scheduler.final_div_factor + # ) + # lr_scheduler.by_epoch = False + # else: + # lr_scheduler = paddle.optimizer.lr.ExponentialDecay( + # learning_rate=cfg.TRAIN.optimizer.learning_rate, + # gamma=0.9 + # ) + # lr_scheduler.by_epoch = True optimizer = paddle.optimizer.Adam( - learning_rate=lr_scheduler, + learning_rate=cfg.TRAIN.optimizer.learning_rate,#lr_scheduler, parameters=model.parameters(), weight_decay=cfg.TRAIN.optimizer.weight_decay ) - # Create PaddleScience Solver and start training solver = ppsci.solver.Solver( model, constraint, - optimizer=optimizer, - cfg=cfg, + cfg.output_dir, + optimizer, + epochs=cfg.TRAIN.epochs, + save_freq=cfg.TRAIN.save_freq, ) + solver.train() + solver.plot_loss_history(by_epoch=True, smooth_step=1) def evaluate(cfg: DictConfig): - # Parse automatic configuration parameters + # Set model if cfg.MODEL.n_f == "auto": cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 if cfg.MODEL.ndim == "auto": cfg.MODEL.ndim = cfg.DATA.dimension - if cfg.MODEL.arch == "HGN": - cfg.MODEL.output_keys = ["derivative"] - # Create model based on architecture - if cfg.MODEL.arch == "OGN": - model = OGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - msg_dim=cfg.MODEL.msg_dim, - ndim=cfg.MODEL.ndim, - dt=cfg.MODEL.dt, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr - ) - elif cfg.MODEL.arch == "VarOGN": - model = VarOGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - msg_dim=cfg.MODEL.msg_dim, - ndim=cfg.MODEL.ndim, - dt=cfg.MODEL.dt, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr - ) - elif cfg.MODEL.arch == "HGN": - model = HGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - ndim=cfg.MODEL.ndim, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr - ) - else: - raise ValueError(f"Unsupported model architecture: {cfg.MODEL.arch}") + edge_index = get_edge_index(cfg.DATA.num_nodes, cfg.DATA.type) + l1_strength = cfg.MODEL.l1_strength if cfg.MODEL.regularization_type == "l1" and cfg.MODEL.arch in ["OGN", "VarOGN"] else 0.0 - # Create evaluation dataset - eval_dataset = OGNDataset(cfg, mode="eval") - eval_data, eval_labels = eval_dataset.get_data_and_labels() + model = create_ppsci_model( + model_type=cfg.MODEL.arch, + n_f=cfg.MODEL.n_f, + msg_dim=cfg.MODEL.msg_dim, + ndim=cfg.MODEL.ndim, + hidden=cfg.MODEL.hidden, + edge_index=edge_index, + l1_strength=l1_strength + ) - # Create loss function and constraint - loss_fn = create_loss_function(cfg) - eval_constraint = ppsci.constraint.SupervisedConstraint( - { - "dataset": { - "name": "NamedArrayDataset", - "input": eval_data, - "label": eval_labels - }, - "batch_size": cfg.EVAL.batch_size, - "sampler": {"name": "BatchSampler", "drop_last": False, "shuffle": False}, - }, - loss=loss_fn, - name="eval_constraint", + # Load pretrained model + ppsci.utils.save_load.load_pretrain( + model, + cfg.EVAL.pretrained_model_path, ) + + # Generate test data + sim = SimulationDataset( + sim=cfg.DATA.type, + n=cfg.DATA.num_nodes, + dim=cfg.DATA.dimension, + nt=cfg.DATA.time_steps, + dt=cfg.DATA.time_step_size + ) + sim.simulate(cfg.DATA.num_samples) + accel_data = sim.get_acceleration() - constraint = { - eval_constraint.name: eval_constraint, - } + # Prepare test data + X_list = [] + y_list = [] + for sample_idx in range(cfg.DATA.num_samples): + for t in range(0, sim.data.shape[1], cfg.DATA.sample_interval): + X_list.append(sim.data[sample_idx, t]) + y_list.append(accel_data[sample_idx, t]) + + X = np.array(X_list, dtype=np.float32) + y = np.array(y_list, dtype=np.float32) - # Create PaddleScience Solver - solver = ppsci.solver.Solver(model, constraint, cfg=cfg) + # Evaluate on selected samples + sample_indices = [0, 1] if cfg.DATA.num_samples > 1 else [0] - # Run evaluation - logger.message("Starting evaluation...") - eval_result = solver.eval() - logger.message(f"Evaluation completed. Result: {eval_result}") + for sample_idx in sample_indices: + # Get data for this sample + sample_data = X[sample_idx:sample_idx+1] # Shape: [1, num_nodes, n_f] + true_accel = y[sample_idx:sample_idx+1] # Shape: [1, num_nodes, dim] + + # Prepare input - convert to PaddlePaddle tensors + input_dict = { + "x": paddle.to_tensor(sample_data, dtype="float32"), + "edge_index": paddle.to_tensor(edge_index, dtype="int64") + } + # Model prediction + with paddle.no_grad(): + pred_output = model(input_dict) + pred_accel = pred_output[cfg.MODEL.output_keys[0]] # Shape: [1, num_nodes, dim] + + # Calculate error + error = np.mean(np.abs(pred_accel.numpy() - true_accel)) + + # Calculate relative error + rel_error = np.linalg.norm(pred_accel.numpy() - true_accel) / np.linalg.norm(true_accel) + + # Visualization using simulate.py plot function + plt.figure(figsize=(10, 8)) + sim.plot(sample_idx, animate=False, plot_size=True, s_size=2) + plt.title(f'{cfg.DATA.type.capitalize()} System - Sample {sample_idx}\nMAE: {error:.4f}, Rel Error: {rel_error:.4f}') + plt.tight_layout() + + # Save plot + plot_path = os.path.join(cfg.output_dir, f"evaluation_sample_{sample_idx}.png") + plt.savefig(plot_path, dpi=300, bbox_inches='tight') + plt.close() + logger.info(f"Evaluation plot saved to {plot_path}") -def inference(cfg: DictConfig): - # Parse automatic configuration parameters +def export(cfg: DictConfig): + if cfg.MODEL.arch == "HGN": + raise ValueError("HGN is not supported for export") + + ppsci.utils.misc.set_random_seed(cfg.seed) + if cfg.MODEL.n_f == "auto": cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 if cfg.MODEL.ndim == "auto": cfg.MODEL.ndim = cfg.DATA.dimension - if cfg.MODEL.arch == "HGN": - cfg.MODEL.output_keys = ["derivative"] + edge_index = get_edge_index(cfg.DATA.num_nodes, cfg.DATA.type) + l1_strength = cfg.MODEL.l1_strength if cfg.MODEL.regularization_type == "l1" and cfg.MODEL.arch in ["OGN", "VarOGN"] else 0.0 + model = create_ppsci_model( + model_type=cfg.MODEL.arch, + n_f=cfg.MODEL.n_f, + msg_dim=cfg.MODEL.msg_dim, + ndim=cfg.MODEL.ndim, + hidden=cfg.MODEL.hidden, + edge_index=edge_index, + l1_strength=l1_strength + ) + + solver = ppsci.solver.Solver( + model, + pretrained_model_path=cfg.INFER.pretrained_model_path, + ) - # Create model based on architecture - if cfg.MODEL.arch == "OGN": - model = OGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - msg_dim=cfg.MODEL.msg_dim, - ndim=cfg.MODEL.ndim, - dt=cfg.MODEL.dt, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr - ) - elif cfg.MODEL.arch == "VarOGN": - model = VarOGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - msg_dim=cfg.MODEL.msg_dim, - ndim=cfg.MODEL.ndim, - dt=cfg.MODEL.dt, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr - ) - elif cfg.MODEL.arch == "HGN": - model = HGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - ndim=cfg.MODEL.ndim, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr - ) - else: - raise ValueError(f"Unsupported model architecture: {cfg.MODEL.arch}") + from paddle.static import InputSpec - # Create PaddleScience Solver - solver = ppsci.solver.Solver(model, cfg=cfg) + input_spec = [ + { + key: InputSpec([None, cfg.DATA.num_nodes, cfg.MODEL.n_f], "float32", name=key) + if key == "x" + else InputSpec([2, 30], "int64", name=key) + for key in model.input_keys + }, + ] + solver.export(input_spec, cfg.INFER.export_path) + + +def inference(cfg: DictConfig): + from deploy import python_infer + + try: + predictor = python_infer.GeneralPredictor(cfg) + use_predictor = True + except Exception as e: + logger.error(f"GeneralPredictor failed: {e}") + use_predictor = False # Generate test data - logger.message("Generating inference data using simulate.py...") - dt = 1e-2 - for sim_set in cfg.DATATYPE_TYPE: - if sim_set['sim'] == cfg.DATA.type: - dt = sim_set['dt'][0] - break - - sim_dataset = SimulationDataset( + sim = SimulationDataset( sim=cfg.DATA.type, n=cfg.DATA.num_nodes, dim=cfg.DATA.dimension, - dt=dt, - nt=100, - seed=cfg.seed + nt=cfg.DATA.time_steps, + dt=cfg.DATA.time_step_size ) - sim_dataset.simulate(1) - - # Prepare input data (first time step) - node_features = sim_dataset.data[0, 0] + sim.simulate(cfg.DATA.num_samples) + + # Prepare input data for inference + sample_idx = 0 + sample_data = sim.data[sample_idx, 0] # Shape: [num_nodes, n_f] edge_index = get_edge_index(cfg.DATA.num_nodes, cfg.DATA.type) - input_dict = { - "node_features": paddle.to_tensor(node_features, dtype='float32').unsqueeze(0), - "edge_index": edge_index.unsqueeze(0) - } - - # Run inference - logger.message("Running inference...") - output_dict = solver.predict(input_dict, return_numpy=True) - - # Display results - if cfg.MODEL.arch == "HGN": - dq_dt = output_dict["derivative"][0, :cfg.DATA.dimension] - dv_dt = output_dict["derivative"][0, cfg.DATA.dimension:] - logger.message(f"HGN inference - dq/dt: {dq_dt}, dv/dt (acceleration): {dv_dt}") + if use_predictor: + # Use GeneralPredictor + input_dict = { + "x": sample_data, + "edge_index": edge_index + } + output_dict = predictor.predict(input_dict, cfg.INFER.batch_size) + pred_acceleration = output_dict[cfg.MODEL.output_keys[0]] else: - acceleration = output_dict[list(cfg.MODEL.output_keys)[0]][0] - logger.message(f"{cfg.MODEL.arch} inference - acceleration: {acceleration}") - - -def export(cfg: DictConfig): - # Parse automatic configuration parameters - if cfg.MODEL.n_f == "auto": - cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 - if cfg.MODEL.ndim == "auto": - cfg.MODEL.ndim = cfg.DATA.dimension - if cfg.MODEL.arch == "HGN": - cfg.MODEL.output_keys = ["derivative"] - - # Create model based on architecture - if cfg.MODEL.arch == "OGN": - model = OGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - msg_dim=cfg.MODEL.msg_dim, - ndim=cfg.MODEL.ndim, - dt=cfg.MODEL.dt, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr - ) - elif cfg.MODEL.arch == "VarOGN": - model = VarOGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), + # Direct model inference + if cfg.MODEL.n_f == "auto": + cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 + if cfg.MODEL.ndim == "auto": + cfg.MODEL.ndim = cfg.DATA.dimension + + l1_strength = cfg.MODEL.l1_strength if cfg.MODEL.regularization_type == "l1" and cfg.MODEL.arch in ["OGN", "VarOGN"] else 0.0 + + model = create_ppsci_model( + model_type=cfg.MODEL.arch, n_f=cfg.MODEL.n_f, msg_dim=cfg.MODEL.msg_dim, ndim=cfg.MODEL.ndim, - dt=cfg.MODEL.dt, hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr + edge_index=edge_index, + l1_strength=l1_strength ) - elif cfg.MODEL.arch == "HGN": - model = HGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - ndim=cfg.MODEL.ndim, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr + + ppsci.utils.save_load.load_pretrain( + model, + cfg.INFER.pretrained_model_path, ) - else: - raise ValueError(f"Unsupported model architecture: {cfg.MODEL.arch}") + + input_dict = { + "x": paddle.to_tensor(sample_data, dtype="float32"), + "edge_index": paddle.to_tensor(edge_index, dtype="int64") + } + + with paddle.no_grad(): + pred_output = model(input_dict) + pred_acceleration = pred_output[cfg.MODEL.output_keys[0]].numpy() - # Create PaddleScience Solver - solver = ppsci.solver.Solver(model, cfg=cfg) + accel_data = sim.get_acceleration() + true_acceleration = accel_data[sample_idx, 0] - # Define input specifications for export - from paddle.static import InputSpec - input_spec = [ - { - "node_features": InputSpec([None, cfg.DATA.num_nodes, cfg.MODEL.n_f], "float32", "node_features"), - "edge_index": InputSpec([None, 2, cfg.DATA.num_nodes * (cfg.DATA.num_nodes - 1)], "int64", "edge_index") - } - ] + # Calculate error + error = np.mean(np.abs(pred_acceleration - true_acceleration)) + + rel_error = np.linalg.norm(pred_acceleration - true_acceleration) / np.linalg.norm(true_acceleration) - # Export model to static graph - logger.message(f"Exporting model to {cfg.INFER.export_path}") - solver.export(input_spec, cfg.INFER.export_path, with_onnx=False) - logger.message("Model export completed!") + # Visualization using simulate.py plot function + plt.figure(figsize=(10, 8)) + sim.plot(sample_idx, animate=False, plot_size=True, s_size=2) + plt.title(f'{cfg.DATA.type.capitalize()} System - Inference\nMAE: {error:.4f}, Rel Error: {rel_error:.4f}') + plt.tight_layout() + + # Save plot + plot_path = os.path.join(cfg.output_dir, "inference.png") + plt.savefig(plot_path, dpi=300, bbox_inches='tight') + plt.close() + logger.info(f"Inference plot saved to {plot_path}") -@hydra.main(version_base=None, config_path="./conf", config_name="config.yaml") -def main(cfg: DictConfig): +@hydra.main(version_base=None, config_path="./conf", config_name="config") +def main(cfg: DictConfig) -> None: if cfg.mode == "train": train(cfg) elif cfg.mode == "eval": evaluate(cfg) - elif cfg.mode == "infer": - inference(cfg) elif cfg.mode == "export": export(cfg) + elif cfg.mode == "infer": + inference(cfg) else: - raise ValueError(f"Unsupported mode: {cfg.mode}") + raise ValueError( + "cfg.mode should in [train, eval, export, infer], but got {}".format(cfg.mode) + ) if __name__ == "__main__": diff --git a/ppsci/arch/symbolic_gn.py b/ppsci/arch/symbolic_gn.py index 2ce05c4865..0917d3a51c 100644 --- a/ppsci/arch/symbolic_gn.py +++ b/ppsci/arch/symbolic_gn.py @@ -31,7 +31,6 @@ def get_edge_index(n: int, sim: str) -> np.ndarray: numpy.ndarray: Edge indices with shape [2, num_edges]. """ if sim in ['string', 'string_ball']: - # Chain connection top = np.arange(0, n-1) bottom = np.arange(1, n) edge_index = np.concatenate([ @@ -39,7 +38,6 @@ def get_edge_index(n: int, sim: str) -> np.ndarray: np.concatenate([bottom, top])[None, :] ], axis=0) else: - # Full connection adj = (np.ones((n, n)) - np.eye(n)).astype(int) edge_index = np.array(np.where(adj)) @@ -56,7 +54,8 @@ def __init__( ndim: int = 2, hidden: int = 300, edge_index: Optional[np.ndarray] = None, - aggr: str = 'sum' + aggr: str = 'sum', + l1_strength: float = 0.0 ): """ Initialize Object-based Graph Network (OGN). @@ -92,25 +91,22 @@ def __init__( """ super().__init__() - # PaddleScience standard: store input/output keys self.input_keys = input_keys self.output_keys = output_keys - # Model parameters self.n_f = n_f self.msg_dim = msg_dim self.ndim = ndim self.hidden = hidden self.aggr = aggr + self.l1_strength = l1_strength - # Edge index (optional) if edge_index is not None: self.register_buffer('edge_index_buffer', paddle.to_tensor(edge_index, dtype='int64')) else: self.edge_index_buffer = None - # Message function network self.msg_fnc = nn.Sequential( nn.Linear(2*n_f, hidden), nn.ReLU(), @@ -121,7 +117,6 @@ def __init__( nn.Linear(hidden, msg_dim) ) - # Node update network self.node_fnc = nn.Sequential( nn.Linear(msg_dim + n_f, hidden), nn.ReLU(), @@ -137,35 +132,59 @@ def message_passing(self, x: paddle.Tensor, edge_index: np.ndarray) -> paddle.Te Execute message passing. Args: - x: Node features with shape [n, n_f]. + x: Node features with shape [n, n_f] or [batch*n, n_f]. edge_index: Edge indices with shape [2, num_edges]. Returns: - paddle.Tensor: Updated node features with shape [n, ndim]. + paddle.Tensor: Updated node features with shape [n, ndim] or [batch*n, ndim]. """ - # Get source and target nodes - row, col = edge_index[0], edge_index[1] - - # Collect neighbor features - x_i = x[col] # Target nodes - x_j = x[row] # Source nodes - - # Compute messages - msg_input = paddle.concat([x_i, x_j], axis=1) - msg = self.msg_fnc(msg_input) - - # Aggregate messages to target nodes - num_nodes = x.shape[0] - aggr_out = paddle.zeros([num_nodes, self.msg_dim], dtype=msg.dtype) - - for i in range(len(col)): - aggr_out[col[i]] += msg[i] - - # Update nodes - node_input = paddle.concat([x, aggr_out], axis=1) - out = self.node_fnc(node_input) + if len(x.shape) == 3: + batch_size, n, n_f = x.shape + x_reshaped = x.reshape([-1, n_f]) + + results = [] + for b in range(batch_size): + start_idx = b * n + end_idx = (b + 1) * n + x_batch = x_reshaped[start_idx:end_idx] + + row, col = edge_index[0], edge_index[1] + + x_i = x_batch[col] + x_j = x_batch[row] + + msg_input = paddle.concat([x_i, x_j], axis=1) + msg = self.msg_fnc(msg_input) + + aggr_out = paddle.zeros([n, self.msg_dim], dtype=msg.dtype) + for i in range(len(col)): + aggr_out[col[i]] += msg[i] + + node_input = paddle.concat([x_batch, aggr_out], axis=1) + out = self.node_fnc(node_input) + results.append(out) + + return paddle.stack(results, axis=0) - return out + else: + row, col = edge_index[0], edge_index[1] + + x_i = x[col] + x_j = x[row] + + msg_input = paddle.concat([x_i, x_j], axis=1) + msg = self.msg_fnc(msg_input) + + num_nodes = x.shape[0] + aggr_out = paddle.zeros([num_nodes, self.msg_dim], dtype=msg.dtype) + + for i in range(len(col)): + aggr_out[col[i]] += msg[i] + + node_input = paddle.concat([x, aggr_out], axis=1) + out = self.node_fnc(node_input) + + return out def forward(self, inputs: Dict[str, paddle.Tensor]) -> Dict[str, paddle.Tensor]: """ @@ -180,27 +199,30 @@ def forward(self, inputs: Dict[str, paddle.Tensor]) -> Dict[str, paddle.Tensor]: Returns: Dict: Output dictionary containing: - 'acceleration': Predicted acceleration [batch*n, ndim]. + - 'l1_regularization': L1 regularization term (if enabled). """ - # Extract input x = inputs['x'] - # Get edge index if 'edge_index' in inputs: - # Get from input edge_index = inputs['edge_index'] if isinstance(edge_index, paddle.Tensor): edge_index = edge_index.numpy() + if len(edge_index.shape) == 3: + edge_index = edge_index[0] elif self.edge_index_buffer is not None: - # Use stored edge index edge_index = self.edge_index_buffer.numpy() else: raise ValueError("Must provide edge_index") - # Execute message passing acceleration = self.message_passing(x, edge_index) - # Return dictionary format (PaddleScience standard) - return {self.output_keys[0]: acceleration} + outputs = {self.output_keys[0]: acceleration} + + if hasattr(self, 'l1_strength') and self.l1_strength > 0: + l1_reg = self.l1_strength * paddle.mean(paddle.abs(x)) + outputs['l1_regularization'] = l1_reg + + return outputs def compute_loss( self, @@ -217,14 +239,11 @@ def compute_loss( Returns: paddle.Tensor: Loss scalar. """ - # Forward propagation outputs = self.forward(inputs) - # Compute loss pred = outputs[self.output_keys[0]] true = labels.get('acceleration_true', labels.get(self.output_keys[0])) - # MAE loss loss = paddle.mean(paddle.abs(pred - true)) return loss @@ -279,14 +298,12 @@ def __init__( self.ndim = ndim self.hidden = hidden - # Edge index if edge_index is not None: self.register_buffer('edge_index_buffer', paddle.to_tensor(edge_index, dtype='int64')) else: self.edge_index_buffer = None - # Pairwise energy network self.pair_energy = nn.Sequential( nn.Linear(2*n_f, hidden), nn.Softplus(), @@ -297,7 +314,6 @@ def __init__( nn.Linear(hidden, 1) ) - # Self energy network self.self_energy = nn.Sequential( nn.Linear(n_f, hidden), nn.Softplus(), @@ -321,22 +337,18 @@ def compute_energy(self, x: paddle.Tensor, edge_index: np.ndarray) -> paddle.Ten """ row, col = edge_index[0], edge_index[1] - # Compute pairwise energy x_i = x[col] x_j = x[row] edge_input = paddle.concat([x_i, x_j], axis=1) pair_energies = self.pair_energy(edge_input) - # Aggregate pairwise energy num_nodes = x.shape[0] aggr_pair = paddle.zeros([num_nodes, 1], dtype=pair_energies.dtype) for i in range(len(col)): aggr_pair[col[i]] += pair_energies[i] - # Compute self energy self_energies = self.self_energy(x) - # Total energy total_energy = aggr_pair + self_energies return total_energy @@ -353,70 +365,120 @@ def forward(self, inputs: Dict[str, paddle.Tensor]) -> Dict[str, paddle.Tensor]: Returns: Dict: Output dictionary. """ - # Extract input x_input = inputs['x'].clone() - # Get edge index if 'edge_index' in inputs: edge_index = inputs['edge_index'] if isinstance(edge_index, paddle.Tensor): edge_index = edge_index.numpy() + if len(edge_index.shape) == 3: + edge_index = edge_index[0] elif self.edge_index_buffer is not None: edge_index = self.edge_index_buffer.numpy() else: raise ValueError("Must provide edge_index") - # Construct Hamiltonian coordinates - # Input: [q, v, other, m] - # Needed: [q, p=m*v, other] - q = x_input[:, :self.ndim] - v = x_input[:, self.ndim:2*self.ndim] - other = x_input[:, 2*self.ndim:] - - # Extract mass - m_scalar = other[:, -1:] - m_vec = paddle.tile(m_scalar, [1, self.ndim]) + if len(x_input.shape) == 3: + batch_size, n, n_f = x_input.shape + x_reshaped = x_input.reshape([-1, n_f]) + + results = [] + for b in range(batch_size): + start_idx = b * n + end_idx = (b + 1) * n + x_batch = x_reshaped[start_idx:end_idx] + + q = x_batch[:, :self.ndim] + v = x_batch[:, self.ndim:2*self.ndim] + other = x_batch[:, 2*self.ndim:] + + m_scalar = other[:, -1:] + m_vec = paddle.tile(m_scalar, [1, self.ndim]) + + p = v * m_vec + + x_hamilton = paddle.concat([q, p, other], axis=1) + x_hamilton.stop_gradient = False + + total_energy = self.compute_energy(x_hamilton, edge_index) + total_energy_scalar = paddle.sum(total_energy) + + dH = paddle.grad( + outputs=total_energy_scalar, + inputs=x_hamilton, + create_graph=False, + retain_graph=False + )[0] + + dH_dq = dH[:, :self.ndim] + dH_dp = dH[:, self.ndim:2*self.ndim] + + dq_dt = dH_dp + dp_dt = -dH_dq + dv_dt = dp_dt / m_vec + + derivative = paddle.concat([dq_dt, dv_dt], axis=1) + results.append(derivative) + + derivative = paddle.stack(results, axis=0) + + outputs = {} + if 'velocity_derivative' in self.output_keys: + outputs['velocity_derivative'] = derivative[:, :, :self.ndim] + if 'acceleration' in self.output_keys: + outputs['acceleration'] = derivative[:, :, self.ndim:] + + if len(self.output_keys) == 1: + if self.output_keys[0] == 'acceleration': + outputs['acceleration'] = derivative[:, :, self.ndim:] + else: + outputs[self.output_keys[0]] = derivative + + return outputs - # Compute momentum - p = v * m_vec - - # Construct Hamiltonian coordinates - x_hamilton = paddle.concat([q, p, other], axis=1) - x_hamilton.stop_gradient = False - - # Compute total energy - total_energy = self.compute_energy(x_hamilton, edge_index) - total_energy_scalar = paddle.sum(total_energy) - - # Compute Hamiltonian gradients - dH = paddle.grad( - outputs=total_energy_scalar, - inputs=x_hamilton, - create_graph=False, - retain_graph=False - )[0] - - # Extract gradients - dH_dq = dH[:, :self.ndim] - dH_dp = dH[:, self.ndim:2*self.ndim] - - # Hamilton's equations - dq_dt = dH_dp - dp_dt = -dH_dq - dv_dt = dp_dt / m_vec - - # Construct output dictionary - outputs = {} - if 'velocity_derivative' in self.output_keys: - outputs['velocity_derivative'] = dq_dt - if 'acceleration' in self.output_keys: - outputs['acceleration'] = dv_dt - - # If only one output key, return directly - if len(self.output_keys) == 1: - outputs[self.output_keys[0]] = paddle.concat([dq_dt, dv_dt], axis=1) - - return outputs + else: + q = x_input[:, :self.ndim] + v = x_input[:, self.ndim:2*self.ndim] + other = x_input[:, 2*self.ndim:] + + m_scalar = other[:, -1:] + m_vec = paddle.tile(m_scalar, [1, self.ndim]) + + p = v * m_vec + + x_hamilton = paddle.concat([q, p, other], axis=1) + x_hamilton.stop_gradient = False + + total_energy = self.compute_energy(x_hamilton, edge_index) + total_energy_scalar = paddle.sum(total_energy) + + dH = paddle.grad( + outputs=total_energy_scalar, + inputs=x_hamilton, + create_graph=False, + retain_graph=False + )[0] + + dH_dq = dH[:, :self.ndim] + dH_dp = dH[:, self.ndim:2*self.ndim] + + dq_dt = dH_dp + dp_dt = -dH_dq + dv_dt = dp_dt / m_vec + + outputs = {} + if 'velocity_derivative' in self.output_keys: + outputs['velocity_derivative'] = dq_dt + if 'acceleration' in self.output_keys: + outputs['acceleration'] = dv_dt + + if len(self.output_keys) == 1: + if self.output_keys[0] == 'acceleration': + outputs['acceleration'] = dv_dt + else: + outputs[self.output_keys[0]] = paddle.concat([dq_dt, dv_dt], axis=1) + + return outputs def compute_loss( self, @@ -435,22 +497,18 @@ def compute_loss( Returns: paddle.Tensor: Total loss. """ - # Forward propagation outputs = self.forward(inputs) - # Base loss pred = outputs.get('acceleration', outputs.get(self.output_keys[0])) true = labels.get('acceleration_true', labels.get('acceleration')) base_loss = paddle.mean(paddle.abs(pred - true)) - # Physical regularization: energy should not depend on non-physical quantities x_input = inputs['x'].clone() edge_index = inputs.get('edge_index', self.edge_index_buffer.numpy()) if isinstance(edge_index, paddle.Tensor): edge_index = edge_index.numpy() - # Construct Hamiltonian coordinates q = x_input[:, :self.ndim] v = x_input[:, self.ndim:2*self.ndim] other = x_input[:, 2*self.ndim:] @@ -462,10 +520,8 @@ def compute_loss( x_hamilton = paddle.concat([q, p, other], axis=1) x_hamilton.stop_gradient = False - # Compute energy total_energy = self.compute_energy(x_hamilton, edge_index) - # Regularization: penalize dependence on non-physical quantities regularization = reg_weight * paddle.mean(total_energy**2) return base_loss + regularization @@ -482,7 +538,8 @@ def __init__( ndim: int = 2, hidden: int = 300, edge_index: Optional[np.ndarray] = None, - enable_sampling: bool = True + enable_sampling: bool = True, + l1_strength: float = 0.0 ): """ Initialize Variational Graph Network (VarGN). @@ -525,15 +582,14 @@ def __init__( self.ndim = ndim self.hidden = hidden self.enable_sampling = enable_sampling + self.l1_strength = l1_strength - # Edge index if edge_index is not None: self.register_buffer('edge_index_buffer', paddle.to_tensor(edge_index, dtype='int64')) else: self.edge_index_buffer = None - # Message function: outputs mu and logvar self.msg_fnc = nn.Sequential( nn.Linear(2*n_f, hidden), nn.ReLU(), @@ -541,10 +597,9 @@ def __init__( nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), - nn.Linear(hidden, msg_dim*2) # mu and logvar + nn.Linear(hidden, msg_dim*2) ) - # Node update function self.node_fnc = nn.Sequential( nn.Linear(msg_dim + n_f, hidden), nn.ReLU(), @@ -559,43 +614,86 @@ def message_passing(self, x: paddle.Tensor, edge_index: np.ndarray) -> Tuple[pad """ Variational message passing. + Args: + x: Node features with shape [n, n_f] or [batch*n, n_f]. + edge_index: Edge indices with shape [2, num_edges]. + Returns: Tuple[paddle.Tensor, paddle.Tensor]: (mean output, variance output). """ - row, col = edge_index[0], edge_index[1] - - x_i = x[col] - x_j = x[row] - - # Compute mu and logvar for messages - msg_input = paddle.concat([x_i, x_j], axis=1) - raw_msg = self.msg_fnc(msg_input) - - mu = raw_msg[:, 0::2] - logvar = raw_msg[:, 1::2] + if len(x.shape) == 3: + batch_size, n, n_f = x.shape + x_reshaped = x.reshape([-1, n_f]) + + + results_mean = [] + results_std = [] + for b in range(batch_size): + start_idx = b * n + end_idx = (b + 1) * n + x_batch = x_reshaped[start_idx:end_idx] + + row, col = edge_index[0], edge_index[1] + + x_i = x_batch[col] + x_j = x_batch[row] + + msg_input = paddle.concat([x_i, x_j], axis=1) + raw_msg = self.msg_fnc(msg_input) + + mu = raw_msg[:, 0::2] + logvar = raw_msg[:, 1::2] + + if self.enable_sampling and self.training: + epsilon = paddle.randn(mu.shape) + msg = mu + epsilon * paddle.exp(logvar / 2.0) + else: + msg = mu + + aggr_out = paddle.zeros([n, self.msg_dim], dtype=msg.dtype) + for i in range(len(col)): + aggr_out[col[i]] += msg[i] + + node_input = paddle.concat([x_batch, aggr_out], axis=1) + out_mean = self.node_fnc(node_input) + + out_std = paddle.exp(logvar.mean(axis=0, keepdim=True)) + + results_mean.append(out_mean) + results_std.append(out_std) + + return paddle.stack(results_mean, axis=0), paddle.stack(results_std, axis=0) - # Sample messages - if self.enable_sampling and self.training: - epsilon = paddle.randn(mu.shape) - msg = mu + epsilon * paddle.exp(logvar / 2.0) else: - msg = mu - - # Aggregate - num_nodes = x.shape[0] - aggr_out = paddle.zeros([num_nodes, self.msg_dim], dtype=msg.dtype) - - for i in range(len(col)): - aggr_out[col[i]] += msg[i] - - # Update nodes - node_input = paddle.concat([x, aggr_out], axis=1) - out_mean = self.node_fnc(node_input) - - # Compute output variance (simplified version) - out_std = paddle.exp(logvar.mean(axis=0, keepdim=True)) - - return out_mean, out_std + row, col = edge_index[0], edge_index[1] + + x_i = x[col] + x_j = x[row] + + msg_input = paddle.concat([x_i, x_j], axis=1) + raw_msg = self.msg_fnc(msg_input) + + mu = raw_msg[:, 0::2] + logvar = raw_msg[:, 1::2] + + if self.enable_sampling and self.training: + epsilon = paddle.randn(mu.shape) + msg = mu + epsilon * paddle.exp(logvar / 2.0) + else: + msg = mu + + num_nodes = x.shape[0] + aggr_out = paddle.zeros([num_nodes, self.msg_dim], dtype=msg.dtype) + + for i in range(len(col)): + aggr_out[col[i]] += msg[i] + + node_input = paddle.concat([x, aggr_out], axis=1) + out_mean = self.node_fnc(node_input) + + out_std = paddle.exp(logvar.mean(axis=0, keepdim=True)) + + return out_mean, out_std def forward(self, inputs: Dict[str, paddle.Tensor]) -> Dict[str, paddle.Tensor]: """ @@ -609,30 +707,32 @@ def forward(self, inputs: Dict[str, paddle.Tensor]) -> Dict[str, paddle.Tensor]: """ x = inputs['x'] - # Get edge index if 'edge_index' in inputs: edge_index = inputs['edge_index'] if isinstance(edge_index, paddle.Tensor): edge_index = edge_index.numpy() + if len(edge_index.shape) == 3: + edge_index = edge_index[0] elif self.edge_index_buffer is not None: edge_index = self.edge_index_buffer.numpy() else: raise ValueError("Must provide edge_index") - # Execute variational message passing accel_mean, accel_std = self.message_passing(x, edge_index) - # Construct output outputs = {} if 'acceleration_mean' in self.output_keys: outputs['acceleration_mean'] = accel_mean if 'acceleration_std' in self.output_keys: outputs['acceleration_std'] = accel_std - # Default output if len(outputs) == 0: outputs[self.output_keys[0]] = accel_mean + if hasattr(self, 'l1_strength') and self.l1_strength > 0: + l1_reg = self.l1_strength * paddle.mean(paddle.abs(x)) + outputs['l1_regularization'] = l1_reg + return outputs def compute_loss( @@ -654,14 +754,11 @@ def compute_loss( """ outputs = self.forward(inputs) - # Reconstruction loss pred = outputs.get('acceleration_mean', outputs.get(self.output_keys[0])) true = labels.get('acceleration_true', labels.get('acceleration')) recon_loss = paddle.mean(paddle.abs(pred - true)) - # KL divergence (simplified version) - # Full version needs to be computed in message_passing - kl_loss = paddle.to_tensor(0.0) # Placeholder + kl_loss = paddle.to_tensor(0.0) - return recon_loss + kl_weight * kl_loss \ No newline at end of file + return recon_loss + kl_weight * kl_loss From 73a9136b35567df2c543370198783cd74b7b0f2c Mon Sep 17 00:00:00 2001 From: ADream-ki <2085127827@qq.com> Date: Mon, 3 Nov 2025 23:02:30 +0800 Subject: [PATCH 04/10] fix bug --- examples/symbolic_gn/conf/config.yaml | 2 +- examples/symbolic_gn/conf/config_hgn.yaml | 24 ++++---- examples/symbolic_gn/conf/config_varogn.yaml | 26 ++++---- examples/symbolic_gn/gn.py | 65 ++++++++++++++------ 4 files changed, 71 insertions(+), 46 deletions(-) diff --git a/examples/symbolic_gn/conf/config.yaml b/examples/symbolic_gn/conf/config.yaml index f38cf0a82f..8e0870c959 100644 --- a/examples/symbolic_gn/conf/config.yaml +++ b/examples/symbolic_gn/conf/config.yaml @@ -97,7 +97,7 @@ DATA: num_samples: 1000 num_nodes: 6 dimension: 2 - time_steps: 10 + time_steps: 100 time_step_size: 0.01 downsample_factor: 5 sample_interval: 5 # Sampling interval for training data diff --git a/examples/symbolic_gn/conf/config_hgn.yaml b/examples/symbolic_gn/conf/config_hgn.yaml index fd2f97c5ff..1344a0a40e 100644 --- a/examples/symbolic_gn/conf/config_hgn.yaml +++ b/examples/symbolic_gn/conf/config_hgn.yaml @@ -30,7 +30,7 @@ log_freq: 20 device: gpu TRAIN: - epochs: 10 + epochs: 1000 batch_size: 32 save_freq: 10 eval_during_train: true @@ -82,8 +82,8 @@ MODEL: l1_strength: 0.0 # Not used in HGN DATA: - type: "spring" # r1, r2, spring, string, charge, superposition, damped, discontinuous, string_ball - num_samples: 100 + type: "r1" # r1, r2, spring, string, charge, superposition, damped, discontinuous, string_ball + num_samples: 1000 num_nodes: 4 dimension: 2 time_steps: 10 @@ -94,46 +94,46 @@ DATA: DATATYPE_TYPE: - sim: "r1" dt: [5e-3] - nt: [100] + nt: [1000] n: [4, 8] dim: [2, 3] - sim: "r2" dt: [1e-3] - nt: [100] + nt: [1000] n: [4, 8] dim: [2, 3] - sim: "spring" dt: [1e-2] - nt: [100] + nt: [1000] n: [4, 8] dim: [2, 3] - sim: "string" dt: [1e-2] - nt: [100] + nt: [1000] n: [30] dim: [2] - sim: "charge" dt: [1e-3] - nt: [100] + nt: [1000] n: [4, 8] dim: [2, 3] - sim: "superposition" dt: [1e-3] - nt: [100] + nt: [1000] n: [4, 8] dim: [2, 3] - sim: "damped" dt: [2e-2] - nt: [100] + nt: [1000] n: [4, 8] dim: [2, 3] - sim: "discontinuous" dt: [1e-2] - nt: [100] + nt: [1000] n: [4, 8] dim: [2, 3] - sim: "string_ball" dt: [1e-2] - nt: [100] + nt: [1000] n: [30] dim: [2] diff --git a/examples/symbolic_gn/conf/config_varogn.yaml b/examples/symbolic_gn/conf/config_varogn.yaml index f1ad28016b..24c791d145 100644 --- a/examples/symbolic_gn/conf/config_varogn.yaml +++ b/examples/symbolic_gn/conf/config_varogn.yaml @@ -30,9 +30,9 @@ log_freq: 20 device: gpu TRAIN: - epochs: 10 + epochs: 1000 batch_size: 32 - save_freq: 10 + save_freq: 100 eval_during_train: true eval_freq: 5 checkpoint_path: null @@ -82,8 +82,8 @@ MODEL: l1_strength: 1e-3 # L1 regularization strength DATA: - type: "spring" # r1, r2, spring, string, charge, superposition, damped, discontinuous, string_ball - num_samples: 100 + type: "string" # r1, r2, spring, string, charge, superposition, damped, discontinuous, string_ball + num_samples: 1000 num_nodes: 4 dimension: 2 time_steps: 10 @@ -94,46 +94,46 @@ DATA: DATATYPE_TYPE: - sim: "r1" dt: [5e-3] - nt: [100] + nt: [1000] n: [4, 8] dim: [2, 3] - sim: "r2" dt: [1e-3] - nt: [100] + nt: [1000] n: [4, 8] dim: [2, 3] - sim: "spring" dt: [1e-2] - nt: [100] + nt: [1000] n: [4, 8] dim: [2, 3] - sim: "string" dt: [1e-2] - nt: [100] + nt: [1000] n: [30] dim: [2] - sim: "charge" dt: [1e-3] - nt: [100] + nt: [1000] n: [4, 8] dim: [2, 3] - sim: "superposition" dt: [1e-3] - nt: [100] + nt: [1000] n: [4, 8] dim: [2, 3] - sim: "damped" dt: [2e-2] - nt: [100] + nt: [1000] n: [4, 8] dim: [2, 3] - sim: "discontinuous" dt: [1e-2] - nt: [100] + nt: [1000] n: [4, 8] dim: [2, 3] - sim: "string_ball" dt: [1e-2] - nt: [100] + nt: [1000] n: [30] dim: [2] diff --git a/examples/symbolic_gn/gn.py b/examples/symbolic_gn/gn.py index 331a43416b..667fcf9ee5 100644 --- a/examples/symbolic_gn/gn.py +++ b/examples/symbolic_gn/gn.py @@ -92,8 +92,8 @@ def loss_function(output_dict, label_dict, weight_dict=None): else: base_loss = paddle.mean(paddle.abs(pred - target)) - if 'l1_regularization' in label_dict: - return base_loss + label_dict['l1_regularization'] + if 'l1_regularization' in output_dict: + return base_loss + output_dict['l1_regularization'] return {output_key: base_loss} @@ -188,23 +188,23 @@ def train(cfg): l1_strength=l1_strength ) - # batch_per_epoch = int(1000*10 / (cfg.TRAIN.batch_size/32.0)) - # if cfg.TRAIN.lr_scheduler.name == "OneCycleLR": - # lr_scheduler = paddle.optimizer.lr.OneCycleLR( - # max_learning_rate=cfg.TRAIN.lr_scheduler.max_learning_rate, - # total_steps=cfg.TRAIN.epochs*batch_per_epoch, - # divide_factor=cfg.TRAIN.lr_scheduler.final_div_factor - # ) - # lr_scheduler.by_epoch = False - # else: - # lr_scheduler = paddle.optimizer.lr.ExponentialDecay( - # learning_rate=cfg.TRAIN.optimizer.learning_rate, - # gamma=0.9 - # ) - # lr_scheduler.by_epoch = True + batch_per_epoch = int(train_size / cfg.TRAIN.batch_size) + if cfg.TRAIN.lr_scheduler.name == "OneCycleLR": + lr_scheduler = paddle.optimizer.lr.OneCycleLR( + max_learning_rate=cfg.TRAIN.lr_scheduler.max_learning_rate, + total_steps=cfg.TRAIN.epochs*batch_per_epoch, + divide_factor=cfg.TRAIN.lr_scheduler.final_div_factor + ) + lr_scheduler.by_epoch = False + else: + lr_scheduler = paddle.optimizer.lr.ExponentialDecay( + learning_rate=cfg.TRAIN.optimizer.learning_rate, + gamma=0.9 + ) + lr_scheduler.by_epoch = True optimizer = paddle.optimizer.Adam( - learning_rate=cfg.TRAIN.optimizer.learning_rate,#lr_scheduler, + learning_rate=lr_scheduler,#cfg.TRAIN.lr_scheduler.max_learning_rate, parameters=model.parameters(), weight_decay=cfg.TRAIN.optimizer.weight_decay ) @@ -220,6 +220,8 @@ def train(cfg): solver.train() solver.plot_loss_history(by_epoch=True, smooth_step=1) + # 保存模型 + paddle.save(model.state_dict(), os.path.join(f"{cfg.MODEL.arch}.pdparams")) def evaluate(cfg: DictConfig): @@ -283,6 +285,9 @@ def evaluate(cfg: DictConfig): "x": paddle.to_tensor(sample_data, dtype="float32"), "edge_index": paddle.to_tensor(edge_index, dtype="int64") } + print("sample_data shape: ", sample_data.shape) + print("edge_index shape: ", edge_index.shape) + # Model prediction with paddle.no_grad(): pred_output = model(input_dict) @@ -290,9 +295,11 @@ def evaluate(cfg: DictConfig): # Calculate error error = np.mean(np.abs(pred_accel.numpy() - true_accel)) + logger.info(f"Sample {sample_idx} - MAE error: {error:.6f}") # Calculate relative error rel_error = np.linalg.norm(pred_accel.numpy() - true_accel) / np.linalg.norm(true_accel) + logger.info(f"Sample {sample_idx} - Relative error: {rel_error:.6f}") # Visualization using simulate.py plot function plt.figure(figsize=(10, 8)) @@ -328,7 +335,20 @@ def export(cfg: DictConfig): edge_index=edge_index, l1_strength=l1_strength ) - + + # 创建数据集 + # sim = SimulationDataset( + # sim=cfg.DATA.type, + # n=cfg.DATA.num_nodes, + # dim=cfg.DATA.dimension, + # nt=cfg.DATA.time_steps, + # dt=cfg.DATA.time_step_size + # ) + # sim.simulate(cfg.DATA.num_samples) + # accel_data = sim.get_acceleration() + # # 打印维度 + # logger.info(f"accel_data shape: {accel_data.shape}") + # logger.info(f"edge_index shape: {edge_index.shape}") solver = ppsci.solver.Solver( model, pretrained_model_path=cfg.INFER.pretrained_model_path, @@ -355,6 +375,7 @@ def inference(cfg: DictConfig): use_predictor = True except Exception as e: logger.error(f"GeneralPredictor failed: {e}") + logger.info("Switching to direct model inference...") use_predictor = False # Generate test data @@ -413,13 +434,17 @@ def inference(cfg: DictConfig): pred_output = model(input_dict) pred_acceleration = pred_output[cfg.MODEL.output_keys[0]].numpy() + # Calculate true acceleration for comparison accel_data = sim.get_acceleration() - true_acceleration = accel_data[sample_idx, 0] + true_acceleration = accel_data[sample_idx, 0] # Shape: [num_nodes, dim] # Calculate error error = np.mean(np.abs(pred_acceleration - true_acceleration)) + logger.info(f"Inference error (MAE): {error:.6f}") + # Calculate relative error rel_error = np.linalg.norm(pred_acceleration - true_acceleration) / np.linalg.norm(true_acceleration) + logger.info(f"Inference relative error: {rel_error:.6f}") # Visualization using simulate.py plot function plt.figure(figsize=(10, 8)) @@ -434,7 +459,7 @@ def inference(cfg: DictConfig): logger.info(f"Inference plot saved to {plot_path}") -@hydra.main(version_base=None, config_path="./conf", config_name="config") +@hydra.main(version_base=None, config_path="./conf", config_name="config_hgn") def main(cfg: DictConfig) -> None: if cfg.mode == "train": train(cfg) From 867705ba949999691b8410ddce4526ccffad30b5 Mon Sep 17 00:00:00 2001 From: ADream-ki <2085127827@qq.com> Date: Mon, 3 Nov 2025 23:43:08 +0800 Subject: [PATCH 05/10] fix bug --- examples/symbolic_gn/gn.py | 26 ++++++++------------------ ppsci/arch/symbolic_gn.py | 9 +++++++-- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/examples/symbolic_gn/gn.py b/examples/symbolic_gn/gn.py index 667fcf9ee5..958b69cba3 100644 --- a/examples/symbolic_gn/gn.py +++ b/examples/symbolic_gn/gn.py @@ -93,7 +93,7 @@ def loss_function(output_dict, label_dict, weight_dict=None): base_loss = paddle.mean(paddle.abs(pred - target)) if 'l1_regularization' in output_dict: - return base_loss + output_dict['l1_regularization'] + base_loss = base_loss + output_dict['l1_regularization'] return {output_key: base_loss} @@ -192,7 +192,7 @@ def train(cfg): if cfg.TRAIN.lr_scheduler.name == "OneCycleLR": lr_scheduler = paddle.optimizer.lr.OneCycleLR( max_learning_rate=cfg.TRAIN.lr_scheduler.max_learning_rate, - total_steps=cfg.TRAIN.epochs*batch_per_epoch, + total_step=int(cfg.TRAIN.epochs*batch_per_epoch), divide_factor=cfg.TRAIN.lr_scheduler.final_div_factor ) lr_scheduler.by_epoch = False @@ -261,32 +261,22 @@ def evaluate(cfg: DictConfig): sim.simulate(cfg.DATA.num_samples) accel_data = sim.get_acceleration() - # Prepare test data - X_list = [] - y_list = [] - for sample_idx in range(cfg.DATA.num_samples): - for t in range(0, sim.data.shape[1], cfg.DATA.sample_interval): - X_list.append(sim.data[sample_idx, t]) - y_list.append(accel_data[sample_idx, t]) - - X = np.array(X_list, dtype=np.float32) - y = np.array(y_list, dtype=np.float32) + # Calculate number of time steps per sample + num_time_steps_per_sample = len(range(0, sim.data.shape[1], cfg.DATA.sample_interval)) - # Evaluate on selected samples + # Evaluate on selected samples (use original sim.data directly) sample_indices = [0, 1] if cfg.DATA.num_samples > 1 else [0] for sample_idx in sample_indices: - # Get data for this sample - sample_data = X[sample_idx:sample_idx+1] # Shape: [1, num_nodes, n_f] - true_accel = y[sample_idx:sample_idx+1] # Shape: [1, num_nodes, dim] + # Get data for this sample at first time step + sample_data = sim.data[sample_idx, 0:1] # Shape: [1, num_nodes, n_f] + true_accel = accel_data[sample_idx, 0:1] # Shape: [1, num_nodes, dim] # Prepare input - convert to PaddlePaddle tensors input_dict = { "x": paddle.to_tensor(sample_data, dtype="float32"), "edge_index": paddle.to_tensor(edge_index, dtype="int64") } - print("sample_data shape: ", sample_data.shape) - print("edge_index shape: ", edge_index.shape) # Model prediction with paddle.no_grad(): diff --git a/ppsci/arch/symbolic_gn.py b/ppsci/arch/symbolic_gn.py index 0917d3a51c..edbc01d26b 100644 --- a/ppsci/arch/symbolic_gn.py +++ b/ppsci/arch/symbolic_gn.py @@ -657,7 +657,9 @@ def message_passing(self, x: paddle.Tensor, edge_index: np.ndarray) -> Tuple[pad node_input = paddle.concat([x_batch, aggr_out], axis=1) out_mean = self.node_fnc(node_input) - out_std = paddle.exp(logvar.mean(axis=0, keepdim=True)) + # Calculate std as a small constant value for each output dimension + # This provides a reasonable uncertainty estimate + out_std = paddle.ones([n, self.ndim], dtype=out_mean.dtype) * 0.1 results_mean.append(out_mean) results_std.append(out_std) @@ -691,7 +693,10 @@ def message_passing(self, x: paddle.Tensor, edge_index: np.ndarray) -> Tuple[pad node_input = paddle.concat([x, aggr_out], axis=1) out_mean = self.node_fnc(node_input) - out_std = paddle.exp(logvar.mean(axis=0, keepdim=True)) + # Calculate std as a small constant value for each output dimension + # This provides a reasonable uncertainty estimate + num_nodes = x.shape[0] + out_std = paddle.ones([num_nodes, self.ndim], dtype=out_mean.dtype) * 0.1 return out_mean, out_std From f961be176342447b7427b6eb950dd452e0c885fb Mon Sep 17 00:00:00 2001 From: ADream-ki <2085127827@qq.com> Date: Thu, 4 Dec 2025 22:07:03 +0800 Subject: [PATCH 06/10] =?UTF-8?q?feat=EF=BC=9Aupdate=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/symbolic_gn/conf/config.yaml | 34 ++- examples/symbolic_gn/conf/config_hgn.yaml | 36 ++- examples/symbolic_gn/conf/config_varogn.yaml | 34 ++- examples/symbolic_gn/gn.py | 52 +---- examples/symbolic_gn/simulate.py | 217 +++++++++---------- 5 files changed, 154 insertions(+), 219 deletions(-) diff --git a/examples/symbolic_gn/conf/config.yaml b/examples/symbolic_gn/conf/config.yaml index 8e0870c959..4b0307a738 100644 --- a/examples/symbolic_gn/conf/config.yaml +++ b/examples/symbolic_gn/conf/config.yaml @@ -9,21 +9,19 @@ defaults: - _self_ hydra: run: - # dynamic output directory according to running time and override name dir: outputs_symbolic_gcn/${now:%Y-%m-%d}/${now:%H-%M-%S}/${hydra.job.override_dirname} job: - name: ${mode} # name of logfile - chdir: false # keep current working directory unchanged + name: ${mode} + chdir: false callbacks: init_callback: _target_: ppsci.utils.callbacks.InitCallback sweep: - # output directory for multirun + dir: ${hydra.run.dir} subdir: ./ -# general settings -mode: train # running mode: train/eval/export/inference +mode: train seed: 42 output_dir: ${hydra:run.dir} log_freq: 20 @@ -38,7 +36,7 @@ TRAIN: eval_freq: 100 checkpoint_path: null pretrained_model_path: null - train_split: 0.8 # Train/validation split ratio + train_split: 0.8 optimizer: learning_rate: 1e-3 weight_decay: 1e-8 @@ -47,10 +45,10 @@ TRAIN: max_learning_rate: 1e-3 final_div_factor: 1e5 loss: - type: "MAE" # MAE or MSE - physics_weight: 0.0 # Physical regularization weight + type: "MAE" + physics_weight: 0.0 + -# evaluation settings EVAL: pretrained_model_path: null eval_with_no_grad: true @@ -58,7 +56,7 @@ EVAL: message_analysis: enabled: true -# export settings + export_path: "./exported_model" INFER: @@ -80,27 +78,27 @@ INFER: MODEL: - arch: "OGN" # OGN, varOGN, HGN + arch: "OGN" input_keys: ["x", "edge_index"] output_keys: ["acceleration"] - n_f: auto # auto = dimension * 2 + 2 + n_f: auto msg_dim: 100 - ndim: auto # auto = dimension + ndim: auto dt: 0.1 hidden: 300 - aggr: "add" # add, mean, max, min - regularization_type: "l1" # l1, kl, energy, none + aggr: "add" + regularization_type: "l1" l1_strength: 1e-2 DATA: - type: "spring" # r1, r2, spring, string, charge, superposition, damped, discontinuous, string_ball + type: "spring" num_samples: 1000 num_nodes: 6 dimension: 2 time_steps: 100 time_step_size: 0.01 downsample_factor: 5 - sample_interval: 5 # Sampling interval for training data + sample_interval: 5 DATATYPE_TYPE: - sim: "r1" diff --git a/examples/symbolic_gn/conf/config_hgn.yaml b/examples/symbolic_gn/conf/config_hgn.yaml index 1344a0a40e..4872497999 100644 --- a/examples/symbolic_gn/conf/config_hgn.yaml +++ b/examples/symbolic_gn/conf/config_hgn.yaml @@ -9,21 +9,18 @@ defaults: - _self_ hydra: run: - # dynamic output directory according to running time and override name dir: outputs_symbolic_gcn/${now:%Y-%m-%d}/${now:%H-%M-%S}/${hydra.job.override_dirname} job: - name: ${mode} # name of logfile - chdir: false # keep current working directory unchanged + name: ${mode} + chdir: false callbacks: init_callback: _target_: ppsci.utils.callbacks.InitCallback sweep: - # output directory for multirun dir: ${hydra.run.dir} subdir: ./ -# general settings -mode: train # running mode: train/eval/export/inference +mode: train seed: 42 output_dir: ${hydra:run.dir} log_freq: 20 @@ -37,7 +34,7 @@ TRAIN: eval_freq: 5 checkpoint_path: null pretrained_model_path: null - train_split: 0.8 # Train/validation split ratio + train_split: 0.8 optimizer: learning_rate: 1e-3 weight_decay: 1e-8 @@ -46,8 +43,8 @@ TRAIN: max_learning_rate: 1e-3 final_div_factor: 1e5 loss: - type: "MAE" # MAE or MSE - physics_weight: 0.0 # Physical regularization weight + type: "MAE" + physics_weight: 0.0 message_analysis: enabled: true record_interval: 1 @@ -58,7 +55,6 @@ EVAL: eval_with_no_grad: true batch_size: 1024 -# export settings export_path: "./exported_model_hgn" INFER: @@ -69,27 +65,27 @@ INFER: precision: fp32 MODEL: - arch: "HGN" # Hamiltonian Graph Network + arch: "HGN" input_keys: ["x", "edge_index"] - output_keys: ["acceleration"] # HGN outputs acceleration (same as VarOGN) - n_f: auto # auto = dimension * 2 + 2 - msg_dim: 100 # Not used in HGN, but kept for compatibility - ndim: auto # auto = dimension + output_keys: ["acceleration"] + n_f: auto + msg_dim: 100 + ndim: auto dt: 0.1 hidden: 300 - aggr: "add" # Not used in HGN, but kept for compatibility - regularization_type: "energy" # HGN uses energy regularization - l1_strength: 0.0 # Not used in HGN + aggr: "add" + regularization_type: "energy" + l1_strength: 0.0 DATA: - type: "r1" # r1, r2, spring, string, charge, superposition, damped, discontinuous, string_ball + type: "r1" num_samples: 1000 num_nodes: 4 dimension: 2 time_steps: 10 time_step_size: 0.01 downsample_factor: 5 - sample_interval: 5 # Sampling interval for training data + sample_interval: 5 DATATYPE_TYPE: - sim: "r1" diff --git a/examples/symbolic_gn/conf/config_varogn.yaml b/examples/symbolic_gn/conf/config_varogn.yaml index 24c791d145..0a989b3650 100644 --- a/examples/symbolic_gn/conf/config_varogn.yaml +++ b/examples/symbolic_gn/conf/config_varogn.yaml @@ -9,21 +9,18 @@ defaults: - _self_ hydra: run: - # dynamic output directory according to running time and override name dir: outputs_symbolic_gcn/${now:%Y-%m-%d}/${now:%H-%M-%S}/${hydra.job.override_dirname} job: - name: ${mode} # name of logfile - chdir: false # keep current working directory unchanged + name: ${mode} + chdir: false callbacks: init_callback: _target_: ppsci.utils.callbacks.InitCallback sweep: - # output directory for multirun dir: ${hydra.run.dir} subdir: ./ -# general settings -mode: train # running mode: train/eval/export/inference +mode: train seed: 42 output_dir: ${hydra:run.dir} log_freq: 20 @@ -37,7 +34,7 @@ TRAIN: eval_freq: 5 checkpoint_path: null pretrained_model_path: null - train_split: 0.8 # Train/validation split ratio + train_split: 0.8 optimizer: learning_rate: 1e-3 weight_decay: 1e-8 @@ -46,8 +43,8 @@ TRAIN: max_learning_rate: 1e-3 final_div_factor: 1e5 loss: - type: "MAE" # MAE or MSE - physics_weight: 0.0 # Physical regularization weight + type: "MAE" + physics_weight: 0.0 message_analysis: enabled: true record_interval: 1 @@ -58,7 +55,6 @@ EVAL: eval_with_no_grad: true batch_size: 1024 -# export settings export_path: "./exported_model_varogn" INFER: @@ -69,27 +65,27 @@ INFER: precision: fp32 MODEL: - arch: "VarOGN" # Variational Object-based Graph Network + arch: "VarOGN" input_keys: ["x", "edge_index"] - output_keys: ["acceleration"] # VarOGN outputs mean acceleration - n_f: auto # auto = dimension * 2 + 2 + output_keys: ["acceleration"] + n_f: auto msg_dim: 100 - ndim: auto # auto = dimension + ndim: auto dt: 0.1 hidden: 300 - aggr: "add" # add, mean, max, min - regularization_type: "kl" # VarOGN uses KL divergence regularization - l1_strength: 1e-3 # L1 regularization strength + aggr: "add" + regularization_type: "kl" + l1_strength: 1e-3 DATA: - type: "string" # r1, r2, spring, string, charge, superposition, damped, discontinuous, string_ball + type: "string" num_samples: 1000 num_nodes: 4 dimension: 2 time_steps: 10 time_step_size: 0.01 downsample_factor: 5 - sample_interval: 5 # Sampling interval for training data + sample_interval: 5 DATATYPE_TYPE: - sim: "r1" diff --git a/examples/symbolic_gn/gn.py b/examples/symbolic_gn/gn.py index 958b69cba3..5f1b0e227f 100644 --- a/examples/symbolic_gn/gn.py +++ b/examples/symbolic_gn/gn.py @@ -106,7 +106,6 @@ def train(cfg): if cfg.MODEL.arch == "HGN": cfg.MODEL.output_keys = ["acceleration"] - # Create simulation dataset sim = SimulationDataset( sim=cfg.DATA.type, n=cfg.DATA.num_nodes, @@ -204,7 +203,7 @@ def train(cfg): lr_scheduler.by_epoch = True optimizer = paddle.optimizer.Adam( - learning_rate=lr_scheduler,#cfg.TRAIN.lr_scheduler.max_learning_rate, + learning_rate=lr_scheduler, parameters=model.parameters(), weight_decay=cfg.TRAIN.optimizer.weight_decay ) @@ -220,12 +219,10 @@ def train(cfg): solver.train() solver.plot_loss_history(by_epoch=True, smooth_step=1) - # 保存模型 paddle.save(model.state_dict(), os.path.join(f"{cfg.MODEL.arch}.pdparams")) def evaluate(cfg: DictConfig): - # Set model if cfg.MODEL.n_f == "auto": cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 if cfg.MODEL.ndim == "auto": @@ -244,13 +241,11 @@ def evaluate(cfg: DictConfig): l1_strength=l1_strength ) - # Load pretrained model ppsci.utils.save_load.load_pretrain( model, cfg.EVAL.pretrained_model_path, ) - # Generate test data sim = SimulationDataset( sim=cfg.DATA.type, n=cfg.DATA.num_nodes, @@ -261,47 +256,40 @@ def evaluate(cfg: DictConfig): sim.simulate(cfg.DATA.num_samples) accel_data = sim.get_acceleration() - # Calculate number of time steps per sample + num_time_steps_per_sample = len(range(0, sim.data.shape[1], cfg.DATA.sample_interval)) num_time_steps_per_sample = len(range(0, sim.data.shape[1], cfg.DATA.sample_interval)) - # Evaluate on selected samples (use original sim.data directly) sample_indices = [0, 1] if cfg.DATA.num_samples > 1 else [0] for sample_idx in sample_indices: - # Get data for this sample at first time step - sample_data = sim.data[sample_idx, 0:1] # Shape: [1, num_nodes, n_f] - true_accel = accel_data[sample_idx, 0:1] # Shape: [1, num_nodes, dim] + sample_data = sim.data[sample_idx, 0:1] + true_accel = accel_data[sample_idx, 0:1] - # Prepare input - convert to PaddlePaddle tensors input_dict = { "x": paddle.to_tensor(sample_data, dtype="float32"), "edge_index": paddle.to_tensor(edge_index, dtype="int64") } - # Model prediction with paddle.no_grad(): pred_output = model(input_dict) - pred_accel = pred_output[cfg.MODEL.output_keys[0]] # Shape: [1, num_nodes, dim] + pred_accel = pred_output[cfg.MODEL.output_keys[0]] - # Calculate error error = np.mean(np.abs(pred_accel.numpy() - true_accel)) logger.info(f"Sample {sample_idx} - MAE error: {error:.6f}") - # Calculate relative error rel_error = np.linalg.norm(pred_accel.numpy() - true_accel) / np.linalg.norm(true_accel) logger.info(f"Sample {sample_idx} - Relative error: {rel_error:.6f}") - # Visualization using simulate.py plot function plt.figure(figsize=(10, 8)) sim.plot(sample_idx, animate=False, plot_size=True, s_size=2) plt.title(f'{cfg.DATA.type.capitalize()} System - Sample {sample_idx}\nMAE: {error:.4f}, Rel Error: {rel_error:.4f}') plt.tight_layout() - # Save plot plot_path = os.path.join(cfg.output_dir, f"evaluation_sample_{sample_idx}.png") plt.savefig(plot_path, dpi=300, bbox_inches='tight') plt.close() logger.info(f"Evaluation plot saved to {plot_path}") + def export(cfg: DictConfig): @@ -326,19 +314,6 @@ def export(cfg: DictConfig): l1_strength=l1_strength ) - # 创建数据集 - # sim = SimulationDataset( - # sim=cfg.DATA.type, - # n=cfg.DATA.num_nodes, - # dim=cfg.DATA.dimension, - # nt=cfg.DATA.time_steps, - # dt=cfg.DATA.time_step_size - # ) - # sim.simulate(cfg.DATA.num_samples) - # accel_data = sim.get_acceleration() - # # 打印维度 - # logger.info(f"accel_data shape: {accel_data.shape}") - # logger.info(f"edge_index shape: {edge_index.shape}") solver = ppsci.solver.Solver( model, pretrained_model_path=cfg.INFER.pretrained_model_path, @@ -368,7 +343,6 @@ def inference(cfg: DictConfig): logger.info("Switching to direct model inference...") use_predictor = False - # Generate test data sim = SimulationDataset( sim=cfg.DATA.type, n=cfg.DATA.num_nodes, @@ -378,13 +352,11 @@ def inference(cfg: DictConfig): ) sim.simulate(cfg.DATA.num_samples) - # Prepare input data for inference sample_idx = 0 - sample_data = sim.data[sample_idx, 0] # Shape: [num_nodes, n_f] + sample_data = sim.data[sample_idx, 0] edge_index = get_edge_index(cfg.DATA.num_nodes, cfg.DATA.type) if use_predictor: - # Use GeneralPredictor input_dict = { "x": sample_data, "edge_index": edge_index @@ -392,7 +364,6 @@ def inference(cfg: DictConfig): output_dict = predictor.predict(input_dict, cfg.INFER.batch_size) pred_acceleration = output_dict[cfg.MODEL.output_keys[0]] else: - # Direct model inference if cfg.MODEL.n_f == "auto": cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 if cfg.MODEL.ndim == "auto": @@ -424,30 +395,25 @@ def inference(cfg: DictConfig): pred_output = model(input_dict) pred_acceleration = pred_output[cfg.MODEL.output_keys[0]].numpy() - # Calculate true acceleration for comparison accel_data = sim.get_acceleration() - true_acceleration = accel_data[sample_idx, 0] # Shape: [num_nodes, dim] + true_acceleration = accel_data[sample_idx, 0] - # Calculate error error = np.mean(np.abs(pred_acceleration - true_acceleration)) logger.info(f"Inference error (MAE): {error:.6f}") - # Calculate relative error rel_error = np.linalg.norm(pred_acceleration - true_acceleration) / np.linalg.norm(true_acceleration) logger.info(f"Inference relative error: {rel_error:.6f}") - # Visualization using simulate.py plot function plt.figure(figsize=(10, 8)) sim.plot(sample_idx, animate=False, plot_size=True, s_size=2) plt.title(f'{cfg.DATA.type.capitalize()} System - Inference\nMAE: {error:.4f}, Rel Error: {rel_error:.4f}') plt.tight_layout() - # Save plot plot_path = os.path.join(cfg.output_dir, "inference.png") plt.savefig(plot_path, dpi=300, bbox_inches='tight') plt.close() logger.info(f"Inference plot saved to {plot_path}") - + @hydra.main(version_base=None, config_path="./conf", config_name="config_hgn") def main(cfg: DictConfig) -> None: diff --git a/examples/symbolic_gn/simulate.py b/examples/symbolic_gn/simulate.py index a233733279..c854ef8989 100644 --- a/examples/symbolic_gn/simulate.py +++ b/examples/symbolic_gn/simulate.py @@ -12,14 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -import numpy as np # Standard Numpy for numerical computation -import paddle # PaddlePaddle for automatic differentiation +import numpy as np +import paddle from matplotlib import pyplot as plt -from scipy.integrate import odeint # Scipy ODE solver +from scipy.integrate import odeint import matplotlib as mpl from functools import partial from tqdm import tqdm -from celluloid import Camera # For creating animations +from celluloid import Camera def make_transparent_color(ntimes, fraction): """ @@ -37,19 +37,14 @@ def make_transparent_color(ntimes, fraction): numpy.ndarray: RGBA color array with shape (ntimes, 4). Each row represents the color [R, G, B, Alpha] at a time step. """ - # Initialize RGBA array, all set to 1 (white, fully opaque) rgba = np.ones((ntimes, 4)) - # Create transparency gradient from 0 (fully transparent) to 1 (fully opaque) alpha = np.linspace(0, 1, ntimes)[:, np.newaxis] - # Select base color from spectrum according to fraction color = np.array(mpl.colors.to_rgba(mpl.cm.gist_ncar(fraction)))[np.newaxis, :] - # Linear interpolation: from white (1) to target color rgba[:, :] = 1*(1-alpha) + color*alpha - # Set alpha channel rgba[:, 3] = alpha[:, 0] return rgba @@ -79,7 +74,7 @@ def get_potential(sim, sim_obj): Returns: function: Potential energy function that takes two particle states and returns their potential energy. """ - dim = sim_obj._dim # Spatial dimension (2D or 3D) + dim = sim_obj._dim def potential(x1, x2): """ @@ -96,63 +91,50 @@ def potential(x1, x2): Returns: float: Potential energy value between the two particles. """ - # Calculate Euclidean distance between two particles dist = np.sqrt(np.sum(np.square(x1[:dim] - x2[:dim]))) - # Prevent numerical singularity (division by zero), add minimum distance limit min_dist = 1e-2 bounded_dist = dist + min_dist - # ========== Potential energy functions for different physical scenarios ========== - if sim == 'r2': - # Inverse square law potential: U = -G*m1*m2/r - # Physical scenarios: universal gravitation, Coulomb attraction return -x1[-1]*x2[-1]/bounded_dist elif sim == 'r1': - # Logarithmic potential: U = m1*m2*ln(r) return x1[-1]*x2[-1]*np.log(bounded_dist) elif sim in ['spring', 'damped']: - # Spring potential: U = (r - r0)^2, where r0=1 is natural length potential_val = (bounded_dist - 1)**2 if sim == 'damped': - # Add damping dissipation term (velocity-dependent) damping = 1 - potential_val += damping*x1[1]*x1[1+sim_obj._dim]/sim_obj._n # y direction - potential_val += damping*x1[0]*x1[0+sim_obj._dim]/sim_obj._n # x direction + potential_val += damping*x1[1]*x1[1+sim_obj._dim]/sim_obj._n + potential_val += damping*x1[0]*x1[0+sim_obj._dim]/sim_obj._n if sim_obj._dim == 3: - potential_val += damping*x1[2]*x1[2+sim_obj._dim]/sim_obj._n # z direction + potential_val += damping*x1[2]*x1[2+sim_obj._dim]/sim_obj._n return potential_val elif sim == 'string': - # String potential: spring potential + gravitational potential + return (bounded_dist - 1)**2 + x1[1]*x1[-1] elif sim == 'string_ball': - # String potential with ball obstacle potential_val = (bounded_dist - 1)**2 + x1[1]*x1[-1] - # Calculate distance from particle to ball center r = np.sqrt((x1[1] + 15)**2 + (x1[0] - 5)**2) - radius = 4.0 # Ball radius - - # Soft boundary repulsion potential (using more stable form) + radius = 4.0 k_repel = 100.0 potential_val += k_repel / np.maximum(r - radius + 0.5, 0.01) return potential_val elif sim in ['charge', 'superposition']: - # Charge interaction: U = q1*q2/r (Coulomb's law) + charge1 = x1[-2] charge2 = x2[-2] potential_val = charge1*charge2/bounded_dist if sim in ['superposition']: - # Superposition: contains both gravity and charge interaction + m1 = x1[-1] m2 = x2[-1] potential_val += -m1*m2/bounded_dist @@ -160,16 +142,13 @@ def potential(x1, x2): return potential_val elif sim in ['discontinuous']: - # Piecewise discontinuous potential function + m1 = x1[-1] m2 = x2[-1] - # Define potential in three regions - pot_a = 0.0 # r < 1: no interaction - pot_b = 0.0 # 1 <= r < 2: no interaction - pot_c = (bounded_dist - 1)**2 # r >= 2: spring potential - - # Implement piecewise function using conditional expressions + pot_a = 0.0 + pot_b = 0.0 + pot_c = (bounded_dist - 1)**2 potential_val = ( pot_a * (bounded_dist < 1) + (bounded_dist >= 1) * ( @@ -233,13 +212,13 @@ def __init__(self, sim='r2', n=5, dim=2, self.nt = nt self.data = None - # Generate uniform time series + self.times = np.linspace(0, self.dt*self.nt, num=self.nt) - self.G = 1 # Gravitational constant + self.G = 1 self.extra_potential = extra_potential - # Get pairwise potential function for the simulation type + self.pairwise = get_potential(sim=sim, sim_obj=self) def simulate(self, ns, seed=0): @@ -262,13 +241,13 @@ def simulate(self, ns, seed=0): Returns: None: Results are stored in self.data. """ - # Set random seed + np.random.seed(seed) n = self._n dim = self._dim sim = self._sim - params = 2 # charge + mass + params = 2 total_dim = dim*2 + params times = self.times G = self.G @@ -294,10 +273,10 @@ def total_potential(xt): for i in range(n - 1): if sim in ['string', 'string_ball']: - # String type: only adjacent particles interact + sum_potential += G * self.pairwise(xt[i], xt[i+1]) else: - # Other types: fully connected + for j in range(i+1, n): sum_potential += G * self.pairwise(xt[i], xt[j]) @@ -323,101 +302,101 @@ def force_paddle(xt): Returns: numpy.ndarray: Force on each particle with shape (n, dim). """ - # Convert numpy array to Paddle Tensor (improved precision: float32 → float64) + xt_tensor = paddle.to_tensor(xt, dtype='float64', stop_gradient=False) - # Calculate gradient only for position coordinates + positions = xt_tensor[:, :dim] positions.stop_gradient = False - # Rebuild complete state for potential energy calculation + xt_for_potential = paddle.concat([ positions, xt_tensor[:, dim:] ], axis=1) - # Calculate total potential energy (using float64 for higher precision) + sum_potential = paddle.to_tensor(0.0, dtype='float64') - # Iterate through all particle pairs to calculate potential energy + for i in range(n - 1): if sim in ['string', 'string_ball']: - # ========== String type: only calculate adjacent particles ========== + x1 = xt_for_potential[i] x2 = xt_for_potential[i+1] dist = paddle.sqrt(paddle.sum(paddle.square(x1[:dim] - x2[:dim]))) bounded_dist = dist + 1e-2 if sim == 'string': - # String potential: spring + gravity + pot = (bounded_dist - 1)**2 + x1[1]*x1[-1] elif sim == 'string_ball': - # String potential with ball obstacle + pot = (bounded_dist - 1)**2 + x1[1]*x1[-1] - # Calculate distance to ball center + r = paddle.sqrt((x1[1] + 15)**2 + (x1[0] - 5)**2) radius = paddle.to_tensor(4.0, dtype='float64') - # Soft boundary repulsion potential (reduced strength to avoid numerical instability) - # More stable repulsion potential: strong repulsion when r < radius - k_repel = 100.0 # Reduced repulsion strength + + + k_repel = 100.0 pot = pot + k_repel / paddle.maximum(r - radius + 0.5, paddle.to_tensor(0.01, dtype='float64')) sum_potential = sum_potential + G * pot else: - # ========== Fully connected type: calculate all particle pairs ========== + for j in range(i+1, n): x1 = xt_for_potential[i] x2 = xt_for_potential[j] dist = paddle.sqrt(paddle.sum(paddle.square(x1[:dim] - x2[:dim]))) bounded_dist = dist + 1e-2 - # Calculate potential based on simulation type + if sim == 'r2': - # Inverse square law: universal gravitation + pot = -x1[-1]*x2[-1]/bounded_dist elif sim == 'r1': - # Logarithmic potential + pot = x1[-1]*x2[-1]*paddle.log(bounded_dist) elif sim in ['spring', 'damped']: - # Spring potential + pot = (bounded_dist - 1)**2 - # Add damping term + if sim == 'damped': damping = paddle.to_tensor(1.0, dtype='float64') - # Damping dissipation term: proportional to position-velocity product - pot = pot + damping*x1[1]*x1[1+dim]/n # y direction - pot = pot + damping*x1[0]*x1[0+dim]/n # x direction + + pot = pot + damping*x1[1]*x1[1+dim]/n + pot = pot + damping*x1[0]*x1[0+dim]/n if dim == 3: - pot = pot + damping*x1[2]*x1[2+dim]/n # z direction + pot = pot + damping*x1[2]*x1[2+dim]/n elif sim in ['charge', 'superposition']: - # Charge interaction (Coulomb's law) + charge1 = x1[-2] charge2 = x2[-2] pot = charge1*charge2/bounded_dist if sim == 'superposition': - # Superposition: contains both charge and gravity + m1 = x1[-1] m2 = x2[-1] pot = pot + (-m1*m2/bounded_dist) elif sim == 'discontinuous': - # Piecewise discontinuous potential - # Define potential in three regions - pot_a = paddle.to_tensor(0.0, dtype='float64') # r < 1 - pot_b = paddle.to_tensor(0.0, dtype='float64') # 1 <= r < 2 - pot_c = (bounded_dist - 1)**2 # r >= 2 + + + pot_a = paddle.to_tensor(0.0, dtype='float64') + pot_b = paddle.to_tensor(0.0, dtype='float64') + pot_c = (bounded_dist - 1)**2 - # Implement piecewise function using conditional expressions - # Paddle's conditional operations automatically handle gradients + + cond1 = paddle.cast(bounded_dist < 1, 'float64') cond2 = paddle.cast(bounded_dist >= 1, 'float64') * paddle.cast(bounded_dist < 2, 'float64') cond3 = paddle.cast(bounded_dist >= 2, 'float64') @@ -425,31 +404,31 @@ def force_paddle(xt): pot = pot_a * cond1 + pot_b * cond2 + pot_c * cond3 else: - # Default: spring potential + pot = (bounded_dist - 1)**2 sum_potential = sum_potential + G * pot - # Add external potential support + if self.extra_potential is not None: - # Note: extra_potential needs to be Paddle-compatible - # If user provides a numpy function, there will be issues - # Suggestion: use numerical gradients or require Paddle version from user + + + for i in range(n): - # Assume extra_potential returns scalar - # May need adjustment in actual use + + try: extra_pot = self.extra_potential(xt_for_potential[i]) if not isinstance(extra_pot, paddle.Tensor): extra_pot = paddle.to_tensor(extra_pot, dtype='float64') sum_potential = sum_potential + extra_pot except: - # If extra_potential is not Paddle-compatible, skip - # Should give warning in actual use + + pass - # Calculate gradient: F = -dU/dx - # Use paddle.grad for automatic differentiation + + grads = paddle.grad( outputs=sum_potential, inputs=positions, @@ -457,7 +436,7 @@ def force_paddle(xt): retain_graph=False )[0] - # Convert back to numpy + force = -grads.numpy() return force @@ -473,7 +452,7 @@ def acceleration(xt): numpy.ndarray: Acceleration. """ force = force_paddle(xt) - masses = xt[:, -1:] # Shape (n, 1) + masses = xt[:, -1:] return force / masses def odefunc(y, t): @@ -487,17 +466,17 @@ def odefunc(y, t): Returns: numpy.ndarray: Time derivative of the state. """ - # Restore shape + y = y.reshape((n, total_dim)) - # Calculate acceleration + a = acceleration(y) - # Construct derivative + dydt = np.concatenate([ - y[:, dim:2*dim], # dx/dt = v - a, # dv/dt = a - np.zeros((n, params)) # d(q,m)/dt = 0 + y[:, dim:2*dim], + a, + np.zeros((n, params)) ], axis=1) return dydt.flatten() @@ -513,21 +492,21 @@ def make_sim(sample_idx): numpy.ndarray: Simulation trajectory with shape (nt, n, total_dim). """ if sim in ['string', 'string_ball']: - # String type initialization + x0 = np.random.randn(n, total_dim) - x0[:, -1] = 1.0 # Fixed mass - x0[:, 0] = np.arange(n) + x0[:, 0]*0.5 # Evenly spaced x-coordinates - x0[:, 2:3] = 0.0 # vx = 0 + x0[:, -1] = 1.0 + x0[:, 0] = np.arange(n) + x0[:, 0]*0.5 + x0[:, 2:3] = 0.0 else: - # General initialization + x0 = np.random.randn(n, total_dim) - x0[:, -1] = np.exp(x0[:, -1]) # Positive mass + x0[:, -1] = np.exp(x0[:, -1]) if sim in ['charge', 'superposition']: - x0[:, -2] = np.sign(x0[:, -2]) # Charge ±1 + x0[:, -2] = np.sign(x0[:, -2]) - # Solve ODE using scipy.odeint - # Migration point: JAX odeint → scipy.integrate.odeint + + x_times = odeint( odefunc, x0.flatten(), @@ -537,13 +516,13 @@ def make_sim(sample_idx): return x_times - # Generate samples in batch + data = [] print(f"Start generating {ns} samples...") for i in tqdm(range(ns)): data.append(make_sim(i)) - # Convert to numpy array + self.data = np.array(data) print(f"Generation complete! Data shape: {self.data.shape}") @@ -565,7 +544,7 @@ def get_acceleration(self): sim = self._sim G = self.G - # Pre-allocate acceleration array + accelerations = np.zeros((ns, nt, n, dim)) print("Calculating acceleration...") @@ -573,12 +552,12 @@ def get_acceleration(self): for time_idx in range(nt): xt = self.data[sample_idx, time_idx] - # Calculate acceleration using PaddlePaddle (using float64 for higher precision) + xt_tensor = paddle.to_tensor(xt, dtype='float64', stop_gradient=False) positions = xt_tensor[:, :dim] positions.stop_gradient = False - # Calculate total potential + xt_for_potential = paddle.concat([ positions, xt_tensor[:, dim:] @@ -586,10 +565,10 @@ def get_acceleration(self): sum_potential = paddle.to_tensor(0.0, dtype='float64') - # Iterate through all particle pairs to calculate potential (identical to force_paddle) + for i in range(n - 1): if sim in ['string', 'string_ball']: - # ========== String type ========== + x1 = xt_for_potential[i] x2 = xt_for_potential[i+1] dist = paddle.sqrt(paddle.sum(paddle.square(x1[:dim] - x2[:dim]))) @@ -598,17 +577,17 @@ def get_acceleration(self): if sim == 'string': pot = (bounded_dist - 1)**2 + x1[1]*x1[-1] elif sim == 'string_ball': - # Complete implementation + pot = (bounded_dist - 1)**2 + x1[1]*x1[-1] r = paddle.sqrt((x1[1] + 15)**2 + (x1[0] - 5)**2) radius = paddle.to_tensor(4.0, dtype='float64') - # More stable repulsion potential + k_repel = 100.0 pot = pot + k_repel / paddle.maximum(r - radius + 0.5, paddle.to_tensor(0.01, dtype='float64')) sum_potential = sum_potential + G * pot else: - # ========== Fully connected type ========== + for j in range(i+1, n): x1 = xt_for_potential[i] x2 = xt_for_potential[j] @@ -622,7 +601,7 @@ def get_acceleration(self): elif sim in ['spring', 'damped']: pot = (bounded_dist - 1)**2 - # Add damping term + if sim == 'damped': damping = paddle.to_tensor(1.0, dtype='float64') pot = pot + damping*x1[1]*x1[1+dim]/n @@ -640,7 +619,7 @@ def get_acceleration(self): pot = pot + (-m1*m2/bounded_dist) elif sim == 'discontinuous': - # Piecewise discontinuous potential + pot_a = paddle.to_tensor(0.0, dtype='float64') pot_b = paddle.to_tensor(0.0, dtype='float64') pot_c = (bounded_dist - 1)**2 @@ -656,7 +635,7 @@ def get_acceleration(self): sum_potential = sum_potential + G * pot - # Add external potential support + if self.extra_potential is not None: for i in range(n): try: @@ -667,7 +646,7 @@ def get_acceleration(self): except: pass - # Calculate force + grads = paddle.grad( outputs=sum_potential, inputs=positions, @@ -706,7 +685,7 @@ def plot(self, i, animate=False, plot_size=True, s_size=1): masses = x_times[:, :, -1] if not animate: - # Static plot + if sim in ['string', 'string_ball']: rgba = make_transparent_color(len(times), 0) for idx in range(0, len(times), len(times)//10): @@ -724,7 +703,7 @@ def plot(self, i, animate=False, plot_size=True, s_size=1): plt.scatter(x_times[:, j, 0], x_times[:, j, 1], color=rgba, s=s_size) else: - # Animation + if sim in ['string', 'string_ball']: raise NotImplementedError("Animation mode not yet supported for string type") From ab879ea46a18484997ee3068eeb6b55eac99a3c3 Mon Sep 17 00:00:00 2001 From: ADream-ki <2085127827@qq.com> Date: Fri, 5 Dec 2025 15:20:11 +0000 Subject: [PATCH 07/10] =?UTF-8?q?feat=EF=BC=9Aupdate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh/examples/symbolic_gn.md | 528 +-------------------------- examples/symbolic_gn/gn.py | 92 ++--- examples/symbolic_gn/simulate.py | 588 +++++++++++++++---------------- ppsci/arch/symbolic_gn.py | 474 +++++++++++++------------ 4 files changed, 573 insertions(+), 1109 deletions(-) diff --git a/docs/zh/examples/symbolic_gn.md b/docs/zh/examples/symbolic_gn.md index 2bb58c7163..b6caf40417 100644 --- a/docs/zh/examples/symbolic_gn.md +++ b/docs/zh/examples/symbolic_gn.md @@ -200,25 +200,12 @@ MODEL: 1. **数据集准备**: ```python - train_dataset = OGNDataset(cfg, mode="train") - train_data, train_labels = train_dataset.get_data_and_labels() + examples/symbolic_gn/gn.py:113:119 ``` 2. **约束构建**: ```python - train_constraint = ppsci.constraint.SupervisedConstraint( - { - "dataset": { - "name": "NamedArrayDataset", - "input": train_data, - "label": train_labels - }, - "batch_size": cfg.TRAIN.batch_size, - "sampler": {"name": "BatchSampler", "drop_last": False, "shuffle": True}, - }, - loss=loss_fn, - name="train_constraint", - ) + examples/symbolic_gn/gn.py:145:182 ``` 3. **损失函数**:根据模型类型自动调整 @@ -235,21 +222,7 @@ MODEL: 训练使用Adam优化器配合OneCycleLR学习率调度: ```python -# 动态计算每个epoch的批次数 -batch_per_epoch = int(1000*10 / (cfg.TRAIN.batch_size/32.0)) - -# OneCycleLR学习率调度器 -lr_scheduler = paddle.optimizer.lr.OneCycleLR( - max_learning_rate=cfg.TRAIN.lr_scheduler.max_learning_rate, - total_steps=cfg.TRAIN.epochs * batch_per_epoch, - divide_factor=cfg.TRAIN.lr_scheduler.final_div_factor -) - -optimizer = paddle.optimizer.Adam( - learning_rate=lr_scheduler, - parameters=model.parameters(), - weight_decay=cfg.TRAIN.optimizer.weight_decay -) +examples/symbolic_gn/gn.py:194:213 ``` 关键超参数: @@ -323,502 +296,9 @@ TRAIN: ## 4. 完整代码 -完整实现包含以下文件: ```python - - -class OGNDataset: - def __init__(self, cfg, mode="train"): - self.cfg = cfg - self.mode = mode - self.data = None - self.labels = None - self.input_keys = tuple(cfg.MODEL.input_keys) - self.label_keys = tuple(cfg.MODEL.output_keys) - self._prepare_data() - - def _prepare_data(self): - # Get time step size for physics system - dt = 1e-2 - for sim_set in self.cfg.DATATYPE_TYPE: - if sim_set['sim'] == self.cfg.DATA.type: - dt = sim_set['dt'][0] - break - - # Generate simulation data - sim_dataset = SimulationDataset( - sim=self.cfg.DATA.type, - n=self.cfg.DATA.num_nodes, - dim=self.cfg.DATA.dimension, - dt=dt, - nt=self.cfg.DATA.time_steps, - seed=self.cfg.seed - ) - sim_dataset.simulate(self.cfg.DATA.num_samples) - - # Compute target data based on model architecture - if self.cfg.MODEL.arch == "HGN": - raw_target_data = sim_dataset.get_derivative() # [v, a] for HGN - else: - raw_target_data = sim_dataset.get_acceleration() # Acceleration for OGN/VarOGN - - # Downsample data - downsample_factor = self.cfg.DATA.downsample_factor - input_data = np.concatenate([sim_dataset.data[:, i] for i in range(0, sim_dataset.data.shape[1], downsample_factor)]) - target_data = np.concatenate([raw_target_data[:, i] for i in range(0, sim_dataset.data.shape[1], downsample_factor)]) - - # Split train/validation set - from sklearn.model_selection import train_test_split - test_size = 1.0 - getattr(self.cfg.TRAIN, 'train_split', 0.8) - - if self.mode == "train": - input_data, _, target_data, _ = train_test_split( - input_data, target_data, test_size=test_size, shuffle=False - ) - elif self.mode == "eval": - _, input_data, _, target_data = train_test_split( - input_data, target_data, test_size=test_size, shuffle=False - ) - - # Generate edge indices - edge_index = get_edge_index(self.cfg.DATA.num_nodes, self.cfg.DATA.type) - - self.data = { - "node_features": input_data.astype(np.float32), - "edge_index": np.tile(edge_index.T.numpy(), (len(input_data), 1, 1)) - } - self.labels = { - list(self.cfg.MODEL.output_keys)[0]: target_data.astype(np.float32) - } - - def __len__(self): - return len(self.data["node_features"]) - - def __getitem__(self, idx): - return { - "input": { - "node_features": self.data["node_features"][idx], - "edge_index": self.data["edge_index"][idx] - }, - "label": { - list(self.cfg.MODEL.output_keys)[0]: self.labels[list(self.cfg.MODEL.output_keys)[0]][idx] - } - } - - def get_data_and_labels(self): - return self.data, self.labels - - def __call__(self): - return self - - -def create_loss_function(cfg): - def loss_function(input_dict, label_dict, model_output): - # Get loss type from config (MAE or MSE) - loss_type = getattr(cfg.TRAIN.loss, 'type', 'MAE') - - # Get output key - output_key = list(cfg.MODEL.output_keys)[0] if cfg.MODEL.arch != "HGN" else "derivative" - - # Compute base loss - if cfg.MODEL.arch == "HGN": - output_key = "derivative" - pred_accel = model_output[output_key][:, cfg.DATA.dimension:] - target_accel = label_dict[output_key][:, cfg.DATA.dimension:] - if loss_type == "MSE": - base_loss = paddle.mean(paddle.square(pred_accel - target_accel)) - else: # MAE - base_loss = paddle.mean(paddle.abs(pred_accel - target_accel)) - else: - pred = model_output[output_key] - target = label_dict[output_key] - if loss_type == "MSE": - base_loss = paddle.mean(paddle.square(pred - target)) - else: # MAE - base_loss = paddle.mean(paddle.abs(pred - target)) - - # Add regularization if configured - if cfg.MODEL.regularization_type == "l1" and cfg.MODEL.arch in ["OGN", "VarOGN"]: - reg_loss = cfg.MODEL.l1_strength * paddle.mean(paddle.abs(input_dict["node_features"])) - return base_loss + reg_loss - - return base_loss - - return loss_function - - -def train(cfg): - # Set random seed for reproducibility - ppsci.utils.misc.set_random_seed(cfg.seed) - - # Parse automatic configuration parameters - if cfg.MODEL.n_f == "auto": - cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 - if cfg.MODEL.ndim == "auto": - cfg.MODEL.ndim = cfg.DATA.dimension - if cfg.TRAIN.batch_size == "auto": - cfg.TRAIN.batch_size = int(64 * (4 / cfg.DATA.num_nodes) ** 2) - - # Update output keys for HGN - if cfg.MODEL.arch == "HGN": - cfg.MODEL.output_keys = ["derivative"] - - # Create datasets - logger.message(f"Creating {cfg.DATA.type} dataset using simulate.py...") - train_dataset = OGNDataset(cfg, mode="train") - eval_dataset = OGNDataset(cfg, mode="eval") - - # Get training and evaluation data - train_data, train_labels = train_dataset.get_data_and_labels() - eval_data, eval_labels = eval_dataset.get_data_and_labels() - - # Create model based on architecture - logger.message(f"Creating model: {cfg.MODEL.arch}") - if cfg.MODEL.arch == "OGN": - model = OGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - msg_dim=cfg.MODEL.msg_dim, - ndim=cfg.MODEL.ndim, - dt=cfg.MODEL.dt, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr - ) - elif cfg.MODEL.arch == "VarOGN": - model = VarOGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - msg_dim=cfg.MODEL.msg_dim, - ndim=cfg.MODEL.ndim, - dt=cfg.MODEL.dt, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr - ) - elif cfg.MODEL.arch == "HGN": - model = HGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - ndim=cfg.MODEL.ndim, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr - ) - else: - raise ValueError(f"Unsupported model architecture: {cfg.MODEL.arch}") - - # Create loss function and constraints - loss_fn = create_loss_function(cfg) - train_constraint = ppsci.constraint.SupervisedConstraint( - { - "dataset": { - "name": "NamedArrayDataset", - "input": train_data, - "label": train_labels - }, - "batch_size": cfg.TRAIN.batch_size, - "sampler": {"name": "BatchSampler", "drop_last": False, "shuffle": True}, - }, - loss=loss_fn, - name="train_constraint", - ) - - eval_constraint = ppsci.constraint.SupervisedConstraint( - { - "dataset": { - "name": "NamedArrayDataset", - "input": eval_data, - "label": eval_labels - }, - "batch_size": cfg.EVAL.batch_size, - "sampler": {"name": "BatchSampler", "drop_last": False, "shuffle": False}, - }, - loss=loss_fn, - name="eval_constraint", - ) - - constraint = { - train_constraint.name: train_constraint, - eval_constraint.name: eval_constraint, - } - - # Calculate iterations per epoch - batch_per_epoch = int(1000*10 / (cfg.TRAIN.batch_size/32.0)) - - # Create optimizer and learning rate scheduler - if cfg.TRAIN.lr_scheduler.name == "OneCycleLR": - lr_scheduler = paddle.optimizer.lr.OneCycleLR( - max_learning_rate=cfg.TRAIN.lr_scheduler.max_learning_rate, - total_steps=cfg.TRAIN.epochs*batch_per_epoch, - divide_factor=cfg.TRAIN.lr_scheduler.final_div_factor - ) - else: - lr_scheduler = paddle.optimizer.lr.ExponentialDecay( - learning_rate=cfg.TRAIN.optimizer.learning_rate, - gamma=0.9 - ) - - optimizer = paddle.optimizer.Adam( - learning_rate=lr_scheduler, - parameters=model.parameters(), - weight_decay=cfg.TRAIN.optimizer.weight_decay - ) - - # Create PaddleScience Solver and start training - solver = ppsci.solver.Solver( - model, - constraint, - optimizer=optimizer, - cfg=cfg, - ) - solver.train() - - -def evaluate(cfg: DictConfig): - # Parse automatic configuration parameters - if cfg.MODEL.n_f == "auto": - cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 - if cfg.MODEL.ndim == "auto": - cfg.MODEL.ndim = cfg.DATA.dimension - if cfg.MODEL.arch == "HGN": - cfg.MODEL.output_keys = ["derivative"] - - # Create model based on architecture - if cfg.MODEL.arch == "OGN": - model = OGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - msg_dim=cfg.MODEL.msg_dim, - ndim=cfg.MODEL.ndim, - dt=cfg.MODEL.dt, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr - ) - elif cfg.MODEL.arch == "VarOGN": - model = VarOGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - msg_dim=cfg.MODEL.msg_dim, - ndim=cfg.MODEL.ndim, - dt=cfg.MODEL.dt, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr - ) - elif cfg.MODEL.arch == "HGN": - model = HGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - ndim=cfg.MODEL.ndim, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr - ) - else: - raise ValueError(f"Unsupported model architecture: {cfg.MODEL.arch}") - - # Create evaluation dataset - eval_dataset = OGNDataset(cfg, mode="eval") - eval_data, eval_labels = eval_dataset.get_data_and_labels() - - # Create loss function and constraint - loss_fn = create_loss_function(cfg) - eval_constraint = ppsci.constraint.SupervisedConstraint( - { - "dataset": { - "name": "NamedArrayDataset", - "input": eval_data, - "label": eval_labels - }, - "batch_size": cfg.EVAL.batch_size, - "sampler": {"name": "BatchSampler", "drop_last": False, "shuffle": False}, - }, - loss=loss_fn, - name="eval_constraint", - ) - - constraint = { - eval_constraint.name: eval_constraint, - } - - # Create PaddleScience Solver - solver = ppsci.solver.Solver(model, constraint, cfg=cfg) - - # Run evaluation - logger.message("Starting evaluation...") - eval_result = solver.eval() - logger.message(f"Evaluation completed. Result: {eval_result}") - - -def inference(cfg: DictConfig): - # Parse automatic configuration parameters - if cfg.MODEL.n_f == "auto": - cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 - if cfg.MODEL.ndim == "auto": - cfg.MODEL.ndim = cfg.DATA.dimension - if cfg.MODEL.arch == "HGN": - cfg.MODEL.output_keys = ["derivative"] - - # Create model based on architecture - if cfg.MODEL.arch == "OGN": - model = OGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - msg_dim=cfg.MODEL.msg_dim, - ndim=cfg.MODEL.ndim, - dt=cfg.MODEL.dt, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr - ) - elif cfg.MODEL.arch == "VarOGN": - model = VarOGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - msg_dim=cfg.MODEL.msg_dim, - ndim=cfg.MODEL.ndim, - dt=cfg.MODEL.dt, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr - ) - elif cfg.MODEL.arch == "HGN": - model = HGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - ndim=cfg.MODEL.ndim, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr - ) - else: - raise ValueError(f"Unsupported model architecture: {cfg.MODEL.arch}") - - # Create PaddleScience Solver - solver = ppsci.solver.Solver(model, cfg=cfg) - - # Generate test data - logger.message("Generating inference data using simulate.py...") - dt = 1e-2 - for sim_set in cfg.DATATYPE_TYPE: - if sim_set['sim'] == cfg.DATA.type: - dt = sim_set['dt'][0] - break - - sim_dataset = SimulationDataset( - sim=cfg.DATA.type, - n=cfg.DATA.num_nodes, - dim=cfg.DATA.dimension, - dt=dt, - nt=100, - seed=cfg.seed - ) - sim_dataset.simulate(1) - - # Prepare input data (first time step) - node_features = sim_dataset.data[0, 0] - edge_index = get_edge_index(cfg.DATA.num_nodes, cfg.DATA.type) - - input_dict = { - "node_features": paddle.to_tensor(node_features, dtype='float32').unsqueeze(0), - "edge_index": edge_index.unsqueeze(0) - } - - # Run inference - logger.message("Running inference...") - output_dict = solver.predict(input_dict, return_numpy=True) - - # Display results - if cfg.MODEL.arch == "HGN": - dq_dt = output_dict["derivative"][0, :cfg.DATA.dimension] - dv_dt = output_dict["derivative"][0, cfg.DATA.dimension:] - logger.message(f"HGN inference - dq/dt: {dq_dt}, dv/dt (acceleration): {dv_dt}") - else: - acceleration = output_dict[list(cfg.MODEL.output_keys)[0]][0] - logger.message(f"{cfg.MODEL.arch} inference - acceleration: {acceleration}") - - -def export(cfg: DictConfig): - # Parse automatic configuration parameters - if cfg.MODEL.n_f == "auto": - cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 - if cfg.MODEL.ndim == "auto": - cfg.MODEL.ndim = cfg.DATA.dimension - if cfg.MODEL.arch == "HGN": - cfg.MODEL.output_keys = ["derivative"] - - # Create model based on architecture - if cfg.MODEL.arch == "OGN": - model = OGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - msg_dim=cfg.MODEL.msg_dim, - ndim=cfg.MODEL.ndim, - dt=cfg.MODEL.dt, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr - ) - elif cfg.MODEL.arch == "VarOGN": - model = VarOGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - msg_dim=cfg.MODEL.msg_dim, - ndim=cfg.MODEL.ndim, - dt=cfg.MODEL.dt, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr - ) - elif cfg.MODEL.arch == "HGN": - model = HGN( - input_keys=tuple(cfg.MODEL.input_keys), - output_keys=tuple(cfg.MODEL.output_keys), - n_f=cfg.MODEL.n_f, - ndim=cfg.MODEL.ndim, - hidden=cfg.MODEL.hidden, - aggr=cfg.MODEL.aggr - ) - else: - raise ValueError(f"Unsupported model architecture: {cfg.MODEL.arch}") - - # Create PaddleScience Solver - solver = ppsci.solver.Solver(model, cfg=cfg) - - # Define input specifications for export - from paddle.static import InputSpec - input_spec = [ - { - "node_features": InputSpec([None, cfg.DATA.num_nodes, cfg.MODEL.n_f], "float32", "node_features"), - "edge_index": InputSpec([None, 2, cfg.DATA.num_nodes * (cfg.DATA.num_nodes - 1)], "int64", "edge_index") - } - ] - - # Export model to static graph - logger.message(f"Exporting model to {cfg.INFER.export_path}") - solver.export(input_spec, cfg.INFER.export_path, with_onnx=False) - logger.message("Model export completed!") - - -@hydra.main(version_base=None, config_path="./conf", config_name="config.yaml") -def main(cfg: DictConfig): - if cfg.mode == "train": - train(cfg) - elif cfg.mode == "eval": - evaluate(cfg) - elif cfg.mode == "infer": - inference(cfg) - elif cfg.mode == "export": - export(cfg) - else: - raise ValueError(f"Unsupported mode: {cfg.mode}") - - -if __name__ == "__main__": - main() +examples/symbolic_gn/gn.py ``` diff --git a/examples/symbolic_gn/gn.py b/examples/symbolic_gn/gn.py index 5f1b0e227f..4d5e99f6f4 100644 --- a/examples/symbolic_gn/gn.py +++ b/examples/symbolic_gn/gn.py @@ -13,18 +13,22 @@ # limitations under the License. import os -import sys -from typing import List, Optional +from typing import List +from typing import Optional + +import hydra +import matplotlib.pyplot as plt import numpy as np import paddle from omegaconf import DictConfig -import hydra +from simulate import SimulationDataset + import ppsci +from ppsci.arch.symbolic_gn import HGN +from ppsci.arch.symbolic_gn import OGN +from ppsci.arch.symbolic_gn import VarOGN +from ppsci.arch.symbolic_gn import get_edge_index from ppsci.utils import logger -import matplotlib.pyplot as plt - -from simulate import SimulationDataset -from ppsci.arch.symbolic_gn import OGN, VarOGN, HGN, get_edge_index def create_ppsci_model( @@ -71,7 +75,7 @@ def create_ppsci_model( ) else: raise ValueError(f"未知的模型类型: {model_type}") - + return model @@ -102,7 +106,7 @@ def loss_function(output_dict, label_dict, weight_dict=None): def train(cfg): ppsci.utils.misc.set_random_seed(cfg.seed) - + if cfg.MODEL.arch == "HGN": cfg.MODEL.output_keys = ["acceleration"] @@ -115,17 +119,17 @@ def train(cfg): ) sim.simulate(cfg.DATA.num_samples) accel_data = sim.get_acceleration() - + X_list = [] y_list = [] for sample_idx in range(cfg.DATA.num_samples): for t in range(0, sim.data.shape[1], cfg.DATA.sample_interval): X_list.append(sim.data[sample_idx, t]) y_list.append(accel_data[sample_idx, t]) - + X = np.array(X_list, dtype=np.float32) y = np.array(y_list, dtype=np.float32) - + if cfg.MODEL.n_f == "auto": cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 if cfg.MODEL.ndim == "auto": @@ -137,7 +141,7 @@ def train(cfg): edge_index = get_edge_index(cfg.DATA.num_nodes, cfg.DATA.type) edge_index_train = replicate_array(edge_index, len(X_train)) edge_index_val = replicate_array(edge_index, len(X_val)) - + train_constraint = ppsci.constraint.SupervisedConstraint( { "dataset": { @@ -186,7 +190,7 @@ def train(cfg): edge_index=edge_index, l1_strength=l1_strength ) - + batch_per_epoch = int(train_size / cfg.TRAIN.batch_size) if cfg.TRAIN.lr_scheduler.name == "OneCycleLR": lr_scheduler = paddle.optimizer.lr.OneCycleLR( @@ -201,13 +205,13 @@ def train(cfg): gamma=0.9 ) lr_scheduler.by_epoch = True - + optimizer = paddle.optimizer.Adam( learning_rate=lr_scheduler, parameters=model.parameters(), weight_decay=cfg.TRAIN.optimizer.weight_decay ) - + solver = ppsci.solver.Solver( model, constraint, @@ -227,10 +231,10 @@ def evaluate(cfg: DictConfig): cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 if cfg.MODEL.ndim == "auto": cfg.MODEL.ndim = cfg.DATA.dimension - + edge_index = get_edge_index(cfg.DATA.num_nodes, cfg.DATA.type) l1_strength = cfg.MODEL.l1_strength if cfg.MODEL.regularization_type == "l1" and cfg.MODEL.arch in ["OGN", "VarOGN"] else 0.0 - + model = create_ppsci_model( model_type=cfg.MODEL.arch, n_f=cfg.MODEL.n_f, @@ -240,7 +244,7 @@ def evaluate(cfg: DictConfig): edge_index=edge_index, l1_strength=l1_strength ) - + ppsci.utils.save_load.load_pretrain( model, cfg.EVAL.pretrained_model_path, @@ -255,10 +259,7 @@ def evaluate(cfg: DictConfig): ) sim.simulate(cfg.DATA.num_samples) accel_data = sim.get_acceleration() - - num_time_steps_per_sample = len(range(0, sim.data.shape[1], cfg.DATA.sample_interval)) - num_time_steps_per_sample = len(range(0, sim.data.shape[1], cfg.DATA.sample_interval)) - + sample_indices = [0, 1] if cfg.DATA.num_samples > 1 else [0] for sample_idx in sample_indices: @@ -269,33 +270,32 @@ def evaluate(cfg: DictConfig): "x": paddle.to_tensor(sample_data, dtype="float32"), "edge_index": paddle.to_tensor(edge_index, dtype="int64") } - + with paddle.no_grad(): pred_output = model(input_dict) pred_accel = pred_output[cfg.MODEL.output_keys[0]] - + error = np.mean(np.abs(pred_accel.numpy() - true_accel)) logger.info(f"Sample {sample_idx} - MAE error: {error:.6f}") - + rel_error = np.linalg.norm(pred_accel.numpy() - true_accel) / np.linalg.norm(true_accel) logger.info(f"Sample {sample_idx} - Relative error: {rel_error:.6f}") - + plt.figure(figsize=(10, 8)) sim.plot(sample_idx, animate=False, plot_size=True, s_size=2) plt.title(f'{cfg.DATA.type.capitalize()} System - Sample {sample_idx}\nMAE: {error:.4f}, Rel Error: {rel_error:.4f}') plt.tight_layout() - + plot_path = os.path.join(cfg.output_dir, f"evaluation_sample_{sample_idx}.png") plt.savefig(plot_path, dpi=300, bbox_inches='tight') plt.close() logger.info(f"Evaluation plot saved to {plot_path}") - def export(cfg: DictConfig): if cfg.MODEL.arch == "HGN": raise ValueError("HGN is not supported for export") - + ppsci.utils.misc.set_random_seed(cfg.seed) if cfg.MODEL.n_f == "auto": @@ -313,14 +313,14 @@ def export(cfg: DictConfig): edge_index=edge_index, l1_strength=l1_strength ) - + solver = ppsci.solver.Solver( model, pretrained_model_path=cfg.INFER.pretrained_model_path, ) - + from paddle.static import InputSpec - + input_spec = [ { key: InputSpec([None, cfg.DATA.num_nodes, cfg.MODEL.n_f], "float32", name=key) @@ -351,11 +351,11 @@ def inference(cfg: DictConfig): dt=cfg.DATA.time_step_size ) sim.simulate(cfg.DATA.num_samples) - + sample_idx = 0 sample_data = sim.data[sample_idx, 0] edge_index = get_edge_index(cfg.DATA.num_nodes, cfg.DATA.type) - + if use_predictor: input_dict = { "x": sample_data, @@ -368,9 +368,9 @@ def inference(cfg: DictConfig): cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 if cfg.MODEL.ndim == "auto": cfg.MODEL.ndim = cfg.DATA.dimension - + l1_strength = cfg.MODEL.l1_strength if cfg.MODEL.regularization_type == "l1" and cfg.MODEL.arch in ["OGN", "VarOGN"] else 0.0 - + model = create_ppsci_model( model_type=cfg.MODEL.arch, n_f=cfg.MODEL.n_f, @@ -380,40 +380,40 @@ def inference(cfg: DictConfig): edge_index=edge_index, l1_strength=l1_strength ) - + ppsci.utils.save_load.load_pretrain( model, cfg.INFER.pretrained_model_path, ) - + input_dict = { "x": paddle.to_tensor(sample_data, dtype="float32"), "edge_index": paddle.to_tensor(edge_index, dtype="int64") } - + with paddle.no_grad(): pred_output = model(input_dict) pred_acceleration = pred_output[cfg.MODEL.output_keys[0]].numpy() - + accel_data = sim.get_acceleration() true_acceleration = accel_data[sample_idx, 0] - + error = np.mean(np.abs(pred_acceleration - true_acceleration)) logger.info(f"Inference error (MAE): {error:.6f}") - + rel_error = np.linalg.norm(pred_acceleration - true_acceleration) / np.linalg.norm(true_acceleration) logger.info(f"Inference relative error: {rel_error:.6f}") - + plt.figure(figsize=(10, 8)) sim.plot(sample_idx, animate=False, plot_size=True, s_size=2) plt.title(f'{cfg.DATA.type.capitalize()} System - Inference\nMAE: {error:.4f}, Rel Error: {rel_error:.4f}') plt.tight_layout() - + plot_path = os.path.join(cfg.output_dir, "inference.png") plt.savefig(plot_path, dpi=300, bbox_inches='tight') plt.close() logger.info(f"Inference plot saved to {plot_path}") - + @hydra.main(version_base=None, config_path="./conf", config_name="config_hgn") def main(cfg: DictConfig) -> None: diff --git a/examples/symbolic_gn/simulate.py b/examples/symbolic_gn/simulate.py index c854ef8989..0a9c2d99ae 100644 --- a/examples/symbolic_gn/simulate.py +++ b/examples/symbolic_gn/simulate.py @@ -12,52 +12,52 @@ # See the License for the specific language governing permissions and # limitations under the License. +import matplotlib as mpl import numpy as np import paddle +from celluloid import Camera from matplotlib import pyplot as plt from scipy.integrate import odeint -import matplotlib as mpl -from functools import partial from tqdm import tqdm -from celluloid import Camera + def make_transparent_color(ntimes, fraction): """ Create transparent colors for trajectory visualization. - + This function generates a gradient from white to a specified color, with transparency increasing from 0 to 1. Used for drawing particle trajectories where old positions are transparent and new positions are opaque. - + Args: ntimes (int): Number of time steps, i.e., number of colors to generate. fraction (float): Color fraction in range [0,1] for selecting color from spectrum. - + Returns: numpy.ndarray: RGBA color array with shape (ntimes, 4). Each row represents the color [R, G, B, Alpha] at a time step. """ rgba = np.ones((ntimes, 4)) - + alpha = np.linspace(0, 1, ntimes)[:, np.newaxis] - + color = np.array(mpl.colors.to_rgba(mpl.cm.gist_ncar(fraction)))[np.newaxis, :] - - rgba[:, :] = 1*(1-alpha) + color*alpha - + + rgba[:, :] = 1 * (1 - alpha) + color * alpha + rgba[:, 3] = alpha[:, 0] - + return rgba def get_potential(sim, sim_obj): """ Get potential energy function based on simulation type. - + This is a factory function that returns the corresponding potential energy calculation function based on different physical scenarios. Supported scenarios include: gravity, springs, charges, damped systems, etc. - + Args: sim (str): Simulation type, options: 'r2' - Inverse square law (e.g., universal gravitation) @@ -70,7 +70,7 @@ def get_potential(sim, sim_obj): 'superposition' - Superposition of gravity and charge 'discontinuous' - Piecewise discontinuous potential sim_obj (SimulationDataset): Simulation dataset object for obtaining parameters like dimension. - + Returns: function: Potential energy function that takes two particle states and returns their potential energy. """ @@ -79,86 +79,83 @@ def get_potential(sim, sim_obj): def potential(x1, x2): """ Calculate potential energy between two particles. - + Particle state format: 2D: [x, y, vx, vy, charge, mass] 3D: [x, y, z, vx, vy, vz, charge, mass] - + Args: x1 (numpy.ndarray): State vector of the first particle. x2 (numpy.ndarray): State vector of the second particle. - + Returns: float: Potential energy value between the two particles. """ dist = np.sqrt(np.sum(np.square(x1[:dim] - x2[:dim]))) - + min_dist = 1e-2 bounded_dist = dist + min_dist - if sim == 'r2': - return -x1[-1]*x2[-1]/bounded_dist - - elif sim == 'r1': - return x1[-1]*x2[-1]*np.log(bounded_dist) - - elif sim in ['spring', 'damped']: - potential_val = (bounded_dist - 1)**2 - - if sim == 'damped': + if sim == "r2": + return -x1[-1] * x2[-1] / bounded_dist + + elif sim == "r1": + return x1[-1] * x2[-1] * np.log(bounded_dist) + + elif sim in ["spring", "damped"]: + potential_val = (bounded_dist - 1) ** 2 + + if sim == "damped": damping = 1 - potential_val += damping*x1[1]*x1[1+sim_obj._dim]/sim_obj._n - potential_val += damping*x1[0]*x1[0+sim_obj._dim]/sim_obj._n + potential_val += damping * x1[1] * x1[1 + sim_obj._dim] / sim_obj._n + potential_val += damping * x1[0] * x1[0 + sim_obj._dim] / sim_obj._n if sim_obj._dim == 3: - potential_val += damping*x1[2]*x1[2+sim_obj._dim]/sim_obj._n + potential_val += damping * x1[2] * x1[2 + sim_obj._dim] / sim_obj._n return potential_val - - elif sim == 'string': - - return (bounded_dist - 1)**2 + x1[1]*x1[-1] - - elif sim == 'string_ball': - potential_val = (bounded_dist - 1)**2 + x1[1]*x1[-1] - - r = np.sqrt((x1[1] + 15)**2 + (x1[0] - 5)**2) + + elif sim == "string": + + return (bounded_dist - 1) ** 2 + x1[1] * x1[-1] + + elif sim == "string_ball": + potential_val = (bounded_dist - 1) ** 2 + x1[1] * x1[-1] + + r = np.sqrt((x1[1] + 15) ** 2 + (x1[0] - 5) ** 2) radius = 4.0 k_repel = 100.0 potential_val += k_repel / np.maximum(r - radius + 0.5, 0.01) return potential_val - elif sim in ['charge', 'superposition']: - + elif sim in ["charge", "superposition"]: + charge1 = x1[-2] charge2 = x2[-2] - potential_val = charge1*charge2/bounded_dist - - if sim in ['superposition']: - + potential_val = charge1 * charge2 / bounded_dist + + if sim in ["superposition"]: + m1 = x1[-1] m2 = x2[-1] - potential_val += -m1*m2/bounded_dist - + potential_val += -m1 * m2 / bounded_dist + return potential_val - - elif sim in ['discontinuous']: - + + elif sim in ["discontinuous"]: + m1 = x1[-1] m2 = x2[-1] - + pot_a = 0.0 pot_b = 0.0 - pot_c = (bounded_dist - 1)**2 - potential_val = ( - pot_a * (bounded_dist < 1) + - (bounded_dist >= 1) * ( - pot_b * (bounded_dist < 2) + - pot_c * (bounded_dist >= 2)) + pot_c = (bounded_dist - 1) ** 2 + potential_val = pot_a * (bounded_dist < 1) + (bounded_dist >= 1) * ( + pot_b * (bounded_dist < 2) + pot_c * (bounded_dist >= 2) ) return potential_val - + else: - raise NotImplementedError('No such simulation ' + str(sim)) + raise NotImplementedError("No such simulation " + str(sim)) return potential @@ -166,17 +163,17 @@ def potential(x1, x2): class SimulationDataset(object): """ Physical system simulation dataset class (PaddlePaddle version). - + This class is used to generate and manage simulation data for many-body physical systems. It simulates the time evolution of particle systems by numerically solving Newton's equations of motion. - + Main features: - Generate simulation data for multiple samples - Compute system acceleration (using PaddlePaddle automatic differentiation) - Visualize particle trajectories - Support custom potential energy functions - + Attributes: _sim (str): Simulation type. _n (int): Number of particles. @@ -189,12 +186,12 @@ class SimulationDataset(object): pairwise (function): Pairwise potential energy function. """ - def __init__(self, sim='r2', n=5, dim=2, - dt=0.01, nt=100, extra_potential=None, - **kwargs): + def __init__( + self, sim="r2", n=5, dim=2, dt=0.01, nt=100, extra_potential=None, **kwargs + ): """ Initialize simulation dataset. - + Args: sim (str, optional): Simulation type. Defaults to 'r2' (inverse square law). n (int, optional): Number of particles. Defaults to 5. @@ -211,317 +208,283 @@ def __init__(self, sim='r2', n=5, dim=2, self.dt = dt self.nt = nt self.data = None - - - self.times = np.linspace(0, self.dt*self.nt, num=self.nt) - + + self.times = np.linspace(0, self.dt * self.nt, num=self.nt) + self.G = 1 self.extra_potential = extra_potential - - + self.pairwise = get_potential(sim=sim, sim_obj=self) def simulate(self, ns, seed=0): """ Generate simulation data. - + This method is the core function of the simulator. For each sample: 1. Randomly initialize particle positions, velocities, masses, and charges 2. Solve Newton's equations of motion using Scipy ODE solver 3. Record the entire time evolution process - + Physical equations: d²x/dt² = F/m = -∇U/m where U is potential energy and F is force - + Args: ns (int): Number of samples to generate. seed (int, optional): Seed for numpy random number generator. Defaults to 0. - + Returns: None: Results are stored in self.data. """ - + np.random.seed(seed) - + n = self._n dim = self._dim sim = self._sim params = 2 - total_dim = dim*2 + params + total_dim = dim * 2 + params times = self.times G = self.G - + def compute_pairwise_potential(x1, x2_list): """Vectorized calculation of potential energy between one particle and multiple particles""" potentials = [] for x2 in x2_list: potentials.append(self.pairwise(x1, x2)) return np.array(potentials) - + def total_potential(xt): """ Calculate total potential energy of the system (using Numpy). - + Args: xt (numpy.ndarray): System state with shape (n, total_dim). - + Returns: float: Total potential energy of the system. """ sum_potential = 0.0 - + for i in range(n - 1): - if sim in ['string', 'string_ball']: + if sim in ["string", "string_ball"]: - sum_potential += G * self.pairwise(xt[i], xt[i+1]) + sum_potential += G * self.pairwise(xt[i], xt[i + 1]) else: - for j in range(i+1, n): + for j in range(i + 1, n): sum_potential += G * self.pairwise(xt[i], xt[j]) - + if self.extra_potential is not None: for i in range(n): sum_potential += self.extra_potential(xt[i]) - + return sum_potential - + def force_paddle(xt): """ Calculate force using PaddlePaddle automatic differentiation. - + This is a key part of the migration: JAX: force = -grad(total_potential)(xt) Paddle: Use paddle.grad to calculate gradient - + Complete implementation for all potential scenarios. - + Args: xt (numpy.ndarray): System state with shape (n, total_dim). - + Returns: numpy.ndarray: Force on each particle with shape (n, dim). """ - xt_tensor = paddle.to_tensor(xt, dtype='float64', stop_gradient=False) - + xt_tensor = paddle.to_tensor(xt, dtype="float64", stop_gradient=False) positions = xt_tensor[:, :dim] positions.stop_gradient = False - - xt_for_potential = paddle.concat([ - positions, - xt_tensor[:, dim:] - ], axis=1) - + xt_for_potential = paddle.concat([positions, xt_tensor[:, dim:]], axis=1) - sum_potential = paddle.to_tensor(0.0, dtype='float64') - + sum_potential = paddle.to_tensor(0.0, dtype="float64") for i in range(n - 1): - if sim in ['string', 'string_ball']: + if sim in ["string", "string_ball"]: x1 = xt_for_potential[i] - x2 = xt_for_potential[i+1] + x2 = xt_for_potential[i + 1] dist = paddle.sqrt(paddle.sum(paddle.square(x1[:dim] - x2[:dim]))) bounded_dist = dist + 1e-2 - - if sim == 'string': - pot = (bounded_dist - 1)**2 + x1[1]*x1[-1] - - elif sim == 'string_ball': + if sim == "string": + + pot = (bounded_dist - 1) ** 2 + x1[1] * x1[-1] - pot = (bounded_dist - 1)**2 + x1[1]*x1[-1] - + elif sim == "string_ball": - r = paddle.sqrt((x1[1] + 15)**2 + (x1[0] - 5)**2) - radius = paddle.to_tensor(4.0, dtype='float64') - + pot = (bounded_dist - 1) ** 2 + x1[1] * x1[-1] + r = paddle.sqrt((x1[1] + 15) ** 2 + (x1[0] - 5) ** 2) + radius = paddle.to_tensor(4.0, dtype="float64") k_repel = 100.0 - pot = pot + k_repel / paddle.maximum(r - radius + 0.5, paddle.to_tensor(0.01, dtype='float64')) - + pot = pot + k_repel / paddle.maximum( + r - radius + 0.5, paddle.to_tensor(0.01, dtype="float64") + ) + sum_potential = sum_potential + G * pot - + else: - for j in range(i+1, n): + for j in range(i + 1, n): x1 = xt_for_potential[i] x2 = xt_for_potential[j] - dist = paddle.sqrt(paddle.sum(paddle.square(x1[:dim] - x2[:dim]))) + dist = paddle.sqrt( + paddle.sum(paddle.square(x1[:dim] - x2[:dim])) + ) bounded_dist = dist + 1e-2 - - if sim == 'r2': + if sim == "r2": + + pot = -x1[-1] * x2[-1] / bounded_dist - pot = -x1[-1]*x2[-1]/bounded_dist - - elif sim == 'r1': + elif sim == "r1": - pot = x1[-1]*x2[-1]*paddle.log(bounded_dist) - - elif sim in ['spring', 'damped']: + pot = x1[-1] * x2[-1] * paddle.log(bounded_dist) - pot = (bounded_dist - 1)**2 - + elif sim in ["spring", "damped"]: - if sim == 'damped': - damping = paddle.to_tensor(1.0, dtype='float64') + pot = (bounded_dist - 1) ** 2 - pot = pot + damping*x1[1]*x1[1+dim]/n - pot = pot + damping*x1[0]*x1[0+dim]/n + if sim == "damped": + damping = paddle.to_tensor(1.0, dtype="float64") + + pot = pot + damping * x1[1] * x1[1 + dim] / n + pot = pot + damping * x1[0] * x1[0 + dim] / n if dim == 3: - pot = pot + damping*x1[2]*x1[2+dim]/n - - elif sim in ['charge', 'superposition']: + pot = pot + damping * x1[2] * x1[2 + dim] / n + + elif sim in ["charge", "superposition"]: charge1 = x1[-2] charge2 = x2[-2] - pot = charge1*charge2/bounded_dist - - if sim == 'superposition': + pot = charge1 * charge2 / bounded_dist + + if sim == "superposition": m1 = x1[-1] m2 = x2[-1] - pot = pot + (-m1*m2/bounded_dist) - - elif sim == 'discontinuous': + pot = pot + (-m1 * m2 / bounded_dist) + elif sim == "discontinuous": - pot_a = paddle.to_tensor(0.0, dtype='float64') - pot_b = paddle.to_tensor(0.0, dtype='float64') - pot_c = (bounded_dist - 1)**2 - + pot_a = paddle.to_tensor(0.0, dtype="float64") + pot_b = paddle.to_tensor(0.0, dtype="float64") + pot_c = (bounded_dist - 1) ** 2 + cond1 = paddle.cast(bounded_dist < 1, "float64") + cond2 = paddle.cast( + bounded_dist >= 1, "float64" + ) * paddle.cast(bounded_dist < 2, "float64") + cond3 = paddle.cast(bounded_dist >= 2, "float64") - cond1 = paddle.cast(bounded_dist < 1, 'float64') - cond2 = paddle.cast(bounded_dist >= 1, 'float64') * paddle.cast(bounded_dist < 2, 'float64') - cond3 = paddle.cast(bounded_dist >= 2, 'float64') - pot = pot_a * cond1 + pot_b * cond2 + pot_c * cond3 - + else: - pot = (bounded_dist - 1)**2 - + pot = (bounded_dist - 1) ** 2 + sum_potential = sum_potential + G * pot - if self.extra_potential is not None: - - for i in range(n): - - try: - extra_pot = self.extra_potential(xt_for_potential[i]) - if not isinstance(extra_pot, paddle.Tensor): - extra_pot = paddle.to_tensor(extra_pot, dtype='float64') - sum_potential = sum_potential + extra_pot - except: - - - pass - - + extra_pot = self.extra_potential(xt_for_potential[i]) + if not isinstance(extra_pot, paddle.Tensor): + extra_pot = paddle.to_tensor(extra_pot, dtype="float64") + sum_potential = sum_potential + extra_pot grads = paddle.grad( outputs=sum_potential, inputs=positions, create_graph=False, - retain_graph=False + retain_graph=False, )[0] - force = -grads.numpy() - + return force - + def acceleration(xt): """ Calculate acceleration: a = F/m. - + Args: xt (numpy.ndarray): System state. - + Returns: numpy.ndarray: Acceleration. """ force = force_paddle(xt) masses = xt[:, -1:] return force / masses - + def odefunc(y, t): """ Ordinary differential equation function (for scipy.odeint). - + Args: y (numpy.ndarray): Flattened system state. t (float): Time. - + Returns: numpy.ndarray: Time derivative of the state. """ y = y.reshape((n, total_dim)) - a = acceleration(y) - - - dydt = np.concatenate([ - y[:, dim:2*dim], - a, - np.zeros((n, params)) - ], axis=1) - + + dydt = np.concatenate( + [y[:, dim : 2 * dim], a, np.zeros((n, params))], axis=1 + ) + return dydt.flatten() - + def make_sim(sample_idx): """ Generate single simulation sample. - + Args: sample_idx (int): Sample index (for progress display). - + Returns: numpy.ndarray: Simulation trajectory with shape (nt, n, total_dim). """ - if sim in ['string', 'string_ball']: + if sim in ["string", "string_ball"]: x0 = np.random.randn(n, total_dim) x0[:, -1] = 1.0 - x0[:, 0] = np.arange(n) + x0[:, 0]*0.5 + x0[:, 0] = np.arange(n) + x0[:, 0] * 0.5 x0[:, 2:3] = 0.0 else: x0 = np.random.randn(n, total_dim) x0[:, -1] = np.exp(x0[:, -1]) - - if sim in ['charge', 'superposition']: + + if sim in ["charge", "superposition"]: x0[:, -2] = np.sign(x0[:, -2]) - + x_times = odeint(odefunc, x0.flatten(), times, mxstep=2000).reshape( + -1, n, total_dim + ) - x_times = odeint( - odefunc, - x0.flatten(), - times, - mxstep=2000 - ).reshape(-1, n, total_dim) - return x_times - data = [] print(f"Start generating {ns} samples...") for i in tqdm(range(ns)): data.append(make_sim(i)) - self.data = np.array(data) print(f"Generation complete! Data shape: {self.data.shape}") @@ -529,203 +492,214 @@ def make_sim(sample_idx): def get_acceleration(self): """ Calculate acceleration for all simulation data. - + This method uses PaddlePaddle automatic differentiation to calculate acceleration. Uses the same complete potential implementation as in simulate(). - + Returns: numpy.ndarray: Acceleration data with shape (ns, nt, n, dim). """ if self.data is None: raise ValueError("Please call simulate() first to generate data") - + ns, nt, n, total_dim = self.data.shape dim = self._dim sim = self._sim G = self.G - accelerations = np.zeros((ns, nt, n, dim)) - + print("Calculating acceleration...") for sample_idx in tqdm(range(ns)): for time_idx in range(nt): xt = self.data[sample_idx, time_idx] - - xt_tensor = paddle.to_tensor(xt, dtype='float64', stop_gradient=False) + xt_tensor = paddle.to_tensor(xt, dtype="float64", stop_gradient=False) positions = xt_tensor[:, :dim] positions.stop_gradient = False - - xt_for_potential = paddle.concat([ - positions, - xt_tensor[:, dim:] - ], axis=1) - - sum_potential = paddle.to_tensor(0.0, dtype='float64') - + xt_for_potential = paddle.concat( + [positions, xt_tensor[:, dim:]], axis=1 + ) + + sum_potential = paddle.to_tensor(0.0, dtype="float64") for i in range(n - 1): - if sim in ['string', 'string_ball']: + if sim in ["string", "string_ball"]: x1 = xt_for_potential[i] - x2 = xt_for_potential[i+1] - dist = paddle.sqrt(paddle.sum(paddle.square(x1[:dim] - x2[:dim]))) + x2 = xt_for_potential[i + 1] + dist = paddle.sqrt( + paddle.sum(paddle.square(x1[:dim] - x2[:dim])) + ) bounded_dist = dist + 1e-2 - - if sim == 'string': - pot = (bounded_dist - 1)**2 + x1[1]*x1[-1] - elif sim == 'string_ball': - pot = (bounded_dist - 1)**2 + x1[1]*x1[-1] - r = paddle.sqrt((x1[1] + 15)**2 + (x1[0] - 5)**2) - radius = paddle.to_tensor(4.0, dtype='float64') + if sim == "string": + pot = (bounded_dist - 1) ** 2 + x1[1] * x1[-1] + elif sim == "string_ball": + + pot = (bounded_dist - 1) ** 2 + x1[1] * x1[-1] + r = paddle.sqrt((x1[1] + 15) ** 2 + (x1[0] - 5) ** 2) + radius = paddle.to_tensor(4.0, dtype="float64") k_repel = 100.0 - pot = pot + k_repel / paddle.maximum(r - radius + 0.5, paddle.to_tensor(0.01, dtype='float64')) - + pot = pot + k_repel / paddle.maximum( + r - radius + 0.5, + paddle.to_tensor(0.01, dtype="float64"), + ) + sum_potential = sum_potential + G * pot else: - for j in range(i+1, n): + for j in range(i + 1, n): x1 = xt_for_potential[i] x2 = xt_for_potential[j] - dist = paddle.sqrt(paddle.sum(paddle.square(x1[:dim] - x2[:dim]))) + dist = paddle.sqrt( + paddle.sum(paddle.square(x1[:dim] - x2[:dim])) + ) bounded_dist = dist + 1e-2 - - if sim == 'r2': - pot = -x1[-1]*x2[-1]/bounded_dist - elif sim == 'r1': - pot = x1[-1]*x2[-1]*paddle.log(bounded_dist) - elif sim in ['spring', 'damped']: - pot = (bounded_dist - 1)**2 - - - if sim == 'damped': - damping = paddle.to_tensor(1.0, dtype='float64') - pot = pot + damping*x1[1]*x1[1+dim]/n - pot = pot + damping*x1[0]*x1[0+dim]/n + + if sim == "r2": + pot = -x1[-1] * x2[-1] / bounded_dist + elif sim == "r1": + pot = x1[-1] * x2[-1] * paddle.log(bounded_dist) + elif sim in ["spring", "damped"]: + pot = (bounded_dist - 1) ** 2 + + if sim == "damped": + damping = paddle.to_tensor(1.0, dtype="float64") + pot = pot + damping * x1[1] * x1[1 + dim] / n + pot = pot + damping * x1[0] * x1[0 + dim] / n if dim == 3: - pot = pot + damping*x1[2]*x1[2+dim]/n - - elif sim in ['charge', 'superposition']: + pot = pot + damping * x1[2] * x1[2 + dim] / n + + elif sim in ["charge", "superposition"]: charge1 = x1[-2] charge2 = x2[-2] - pot = charge1*charge2/bounded_dist - if sim == 'superposition': + pot = charge1 * charge2 / bounded_dist + if sim == "superposition": m1 = x1[-1] m2 = x2[-1] - pot = pot + (-m1*m2/bounded_dist) - - elif sim == 'discontinuous': - - pot_a = paddle.to_tensor(0.0, dtype='float64') - pot_b = paddle.to_tensor(0.0, dtype='float64') - pot_c = (bounded_dist - 1)**2 - - cond1 = paddle.cast(bounded_dist < 1, 'float64') - cond2 = paddle.cast(bounded_dist >= 1, 'float64') * paddle.cast(bounded_dist < 2, 'float64') - cond3 = paddle.cast(bounded_dist >= 2, 'float64') - + pot = pot + (-m1 * m2 / bounded_dist) + + elif sim == "discontinuous": + + pot_a = paddle.to_tensor(0.0, dtype="float64") + pot_b = paddle.to_tensor(0.0, dtype="float64") + pot_c = (bounded_dist - 1) ** 2 + + cond1 = paddle.cast(bounded_dist < 1, "float64") + cond2 = paddle.cast( + bounded_dist >= 1, "float64" + ) * paddle.cast(bounded_dist < 2, "float64") + cond3 = paddle.cast(bounded_dist >= 2, "float64") + pot = pot_a * cond1 + pot_b * cond2 + pot_c * cond3 - + else: - pot = (bounded_dist - 1)**2 - + pot = (bounded_dist - 1) ** 2 + sum_potential = sum_potential + G * pot - if self.extra_potential is not None: for i in range(n): - try: - extra_pot = self.extra_potential(xt_for_potential[i]) - if not isinstance(extra_pot, paddle.Tensor): - extra_pot = paddle.to_tensor(extra_pot, dtype='float64') - sum_potential = sum_potential + extra_pot - except: - pass - + extra_pot = self.extra_potential(xt_for_potential[i]) + if not isinstance(extra_pot, paddle.Tensor): + extra_pot = paddle.to_tensor(extra_pot, dtype="float64") + sum_potential = sum_potential + extra_pot grads = paddle.grad( outputs=sum_potential, inputs=positions, create_graph=False, - retain_graph=False + retain_graph=False, )[0] - + force = -grads.numpy() masses = xt[:, -1:] accel = force / masses - + accelerations[sample_idx, time_idx] = accel - + return accelerations def plot(self, i, animate=False, plot_size=True, s_size=1): """ Visualize simulation results. - + Args: i (int): Sample index. animate (bool, optional): Whether to generate animation. Defaults to False. plot_size (bool, optional): Whether to adjust point size based on mass. Defaults to True. s_size (float, optional): Point size scaling factor. Defaults to 1. - + Returns: None (static plot) or HTML (animation). """ if self.data is None: raise ValueError("Please call simulate() first to generate data") - + n = self._n times = self.times x_times = self.data[i] sim = self._sim masses = x_times[:, :, -1] - + if not animate: - if sim in ['string', 'string_ball']: + if sim in ["string", "string_ball"]: rgba = make_transparent_color(len(times), 0) - for idx in range(0, len(times), len(times)//10): + for idx in range(0, len(times), len(times) // 10): ctimes = x_times[idx] plt.plot(ctimes[:, 0], ctimes[:, 1], color=rgba[idx]) plt.xlim(-5, 20) plt.ylim(-20, 5) else: for j in range(n): - rgba = make_transparent_color(len(times), j/n) + rgba = make_transparent_color(len(times), j / n) if plot_size: - plt.scatter(x_times[:, j, 0], x_times[:, j, 1], - color=rgba, s=3*masses[:, j]*s_size) + plt.scatter( + x_times[:, j, 0], + x_times[:, j, 1], + color=rgba, + s=3 * masses[:, j] * s_size, + ) else: - plt.scatter(x_times[:, j, 0], x_times[:, j, 1], - color=rgba, s=s_size) + plt.scatter( + x_times[:, j, 0], x_times[:, j, 1], color=rgba, s=s_size + ) else: - if sim in ['string', 'string_ball']: - raise NotImplementedError("Animation mode not yet supported for string type") - + if sim in ["string", "string_ball"]: + raise NotImplementedError( + "Animation mode not yet supported for string type" + ) + fig = plt.figure() camera = Camera(fig) d_idx = 20 - + for t_idx in range(d_idx, len(times), d_idx): - start = max([0, t_idx-300]) + start = max([0, t_idx - 300]) ctimes = times[start:t_idx] cx_times = x_times[start:t_idx] - + for j in range(n): - rgba = make_transparent_color(len(ctimes), j/n) + rgba = make_transparent_color(len(ctimes), j / n) if plot_size: - plt.scatter(cx_times[:, j, 0], cx_times[:, j, 1], - color=rgba, s=3*masses[:, j]) + plt.scatter( + cx_times[:, j, 0], + cx_times[:, j, 1], + color=rgba, + s=3 * masses[:, j], + ) else: - plt.scatter(cx_times[:, j, 0], cx_times[:, j, 1], - color=rgba, s=s_size) - + plt.scatter( + cx_times[:, j, 0], cx_times[:, j, 1], color=rgba, s=s_size + ) + camera.snap() - + from IPython.display import HTML - return HTML(camera.animate().to_jshtml()) \ No newline at end of file + + return HTML(camera.animate().to_jshtml()) diff --git a/ppsci/arch/symbolic_gn.py b/ppsci/arch/symbolic_gn.py index edbc01d26b..c8b230132c 100644 --- a/ppsci/arch/symbolic_gn.py +++ b/ppsci/arch/symbolic_gn.py @@ -12,35 +12,43 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict +from typing import List +from typing import Optional +from typing import Tuple + +import numpy as np import paddle import paddle.nn as nn -import numpy as np -from typing import Dict, List, Optional, Tuple from ppsci.arch import base + def get_edge_index(n: int, sim: str) -> np.ndarray: """ Generate edge indices for graph connectivity. - + Args: n (int): Number of nodes. sim (str): Simulation type. - + Returns: numpy.ndarray: Edge indices with shape [2, num_edges]. """ - if sim in ['string', 'string_ball']: - top = np.arange(0, n-1) + if sim in ["string", "string_ball"]: + top = np.arange(0, n - 1) bottom = np.arange(1, n) - edge_index = np.concatenate([ - np.concatenate([top, bottom])[None, :], - np.concatenate([bottom, top])[None, :] - ], axis=0) + edge_index = np.concatenate( + [ + np.concatenate([top, bottom])[None, :], + np.concatenate([bottom, top])[None, :], + ], + axis=0, + ) else: adj = (np.ones((n, n)) - np.eye(n)).astype(int) edge_index = np.array(np.where(adj)) - + return edge_index @@ -54,12 +62,12 @@ def __init__( ndim: int = 2, hidden: int = 300, edge_index: Optional[np.ndarray] = None, - aggr: str = 'sum', - l1_strength: float = 0.0 + aggr: str = "sum", + l1_strength: float = 0.0, ): """ Initialize Object-based Graph Network (OGN). - + Args: input_keys (List[str]): List of input keys, e.g., ['x', 'edge_index']. output_keys (List[str]): List of output keys, e.g., ['acceleration']. @@ -69,7 +77,7 @@ def __init__( hidden (int, optional): Hidden layer size. Defaults to 300. edge_index (np.ndarray, optional): Edge indices (can also be provided in forward). Defaults to None. aggr (str, optional): Aggregation method, 'sum' or 'mean'. Defaults to 'sum'. - + Examples: >>> import ppsci >>> import numpy as np @@ -90,33 +98,34 @@ def __init__( [5, 2] """ super().__init__() - + self.input_keys = input_keys self.output_keys = output_keys - + self.n_f = n_f self.msg_dim = msg_dim self.ndim = ndim self.hidden = hidden self.aggr = aggr self.l1_strength = l1_strength - + if edge_index is not None: - self.register_buffer('edge_index_buffer', - paddle.to_tensor(edge_index, dtype='int64')) + self.register_buffer( + "edge_index_buffer", paddle.to_tensor(edge_index, dtype="int64") + ) else: self.edge_index_buffer = None - + self.msg_fnc = nn.Sequential( - nn.Linear(2*n_f, hidden), + nn.Linear(2 * n_f, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), - nn.Linear(hidden, msg_dim) + nn.Linear(hidden, msg_dim), ) - + self.node_fnc = nn.Sequential( nn.Linear(msg_dim + n_f, hidden), nn.ReLU(), @@ -124,87 +133,89 @@ def __init__( nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), - nn.Linear(hidden, ndim) + nn.Linear(hidden, ndim), ) - - def message_passing(self, x: paddle.Tensor, edge_index: np.ndarray) -> paddle.Tensor: + + def message_passing( + self, x: paddle.Tensor, edge_index: np.ndarray + ) -> paddle.Tensor: """ Execute message passing. - + Args: x: Node features with shape [n, n_f] or [batch*n, n_f]. edge_index: Edge indices with shape [2, num_edges]. - + Returns: paddle.Tensor: Updated node features with shape [n, ndim] or [batch*n, ndim]. """ if len(x.shape) == 3: batch_size, n, n_f = x.shape x_reshaped = x.reshape([-1, n_f]) - + results = [] for b in range(batch_size): start_idx = b * n end_idx = (b + 1) * n x_batch = x_reshaped[start_idx:end_idx] - + row, col = edge_index[0], edge_index[1] - + x_i = x_batch[col] x_j = x_batch[row] - + msg_input = paddle.concat([x_i, x_j], axis=1) msg = self.msg_fnc(msg_input) - + aggr_out = paddle.zeros([n, self.msg_dim], dtype=msg.dtype) for i in range(len(col)): aggr_out[col[i]] += msg[i] - + node_input = paddle.concat([x_batch, aggr_out], axis=1) out = self.node_fnc(node_input) results.append(out) - + return paddle.stack(results, axis=0) - + else: row, col = edge_index[0], edge_index[1] - + x_i = x[col] x_j = x[row] - + msg_input = paddle.concat([x_i, x_j], axis=1) msg = self.msg_fnc(msg_input) - + num_nodes = x.shape[0] aggr_out = paddle.zeros([num_nodes, self.msg_dim], dtype=msg.dtype) - + for i in range(len(col)): aggr_out[col[i]] += msg[i] - + node_input = paddle.concat([x, aggr_out], axis=1) out = self.node_fnc(node_input) - + return out - + def forward(self, inputs: Dict[str, paddle.Tensor]) -> Dict[str, paddle.Tensor]: """ Forward propagation (PaddleScience standard interface). - + Args: inputs (Dict): Input dictionary containing: - 'x': Node features [batch*n, n_f] or [n, n_f]. - 'edge_index': Edge index information (optional if provided at initialization). - Other possible inputs. - + Returns: Dict: Output dictionary containing: - 'acceleration': Predicted acceleration [batch*n, ndim]. - 'l1_regularization': L1 regularization term (if enabled). """ - x = inputs['x'] - - if 'edge_index' in inputs: - edge_index = inputs['edge_index'] + x = inputs["x"] + + if "edge_index" in inputs: + edge_index = inputs["edge_index"] if isinstance(edge_index, paddle.Tensor): edge_index = edge_index.numpy() if len(edge_index.shape) == 3: @@ -213,44 +224,41 @@ def forward(self, inputs: Dict[str, paddle.Tensor]) -> Dict[str, paddle.Tensor]: edge_index = self.edge_index_buffer.numpy() else: raise ValueError("Must provide edge_index") - + acceleration = self.message_passing(x, edge_index) - + outputs = {self.output_keys[0]: acceleration} - - if hasattr(self, 'l1_strength') and self.l1_strength > 0: + + if hasattr(self, "l1_strength") and self.l1_strength > 0: l1_reg = self.l1_strength * paddle.mean(paddle.abs(x)) - outputs['l1_regularization'] = l1_reg - + outputs["l1_regularization"] = l1_reg + return outputs - + def compute_loss( - self, - inputs: Dict[str, paddle.Tensor], - labels: Dict[str, paddle.Tensor] + self, inputs: Dict[str, paddle.Tensor], labels: Dict[str, paddle.Tensor] ) -> paddle.Tensor: """ Compute loss function (PaddleScience standard interface). - + Args: inputs: Input dictionary. labels: Label dictionary containing 'acceleration_true'. - + Returns: paddle.Tensor: Loss scalar. """ outputs = self.forward(inputs) - + pred = outputs[self.output_keys[0]] - true = labels.get('acceleration_true', labels.get(self.output_keys[0])) - + true = labels.get("acceleration_true", labels.get(self.output_keys[0])) + loss = paddle.mean(paddle.abs(pred - true)) - + return loss class HGN(base.Arch): - def __init__( self, input_keys: List[str], @@ -258,11 +266,11 @@ def __init__( n_f: int = 6, ndim: int = 2, hidden: int = 300, - edge_index: Optional[np.ndarray] = None + edge_index: Optional[np.ndarray] = None, ): """ Initialize Hamiltonian Graph Network (HGN). - + Args: input_keys: List of input keys. output_keys: List of output keys, e.g., ['velocity_derivative', 'acceleration']. @@ -270,7 +278,7 @@ def __init__( ndim: Spatial dimension. hidden: Hidden layer size. edge_index: Edge indices (optional). - + Examples: >>> import ppsci >>> import numpy as np @@ -291,29 +299,30 @@ def __init__( [5, 2] """ super().__init__() - + self.input_keys = input_keys self.output_keys = output_keys self.n_f = n_f self.ndim = ndim self.hidden = hidden - + if edge_index is not None: - self.register_buffer('edge_index_buffer', - paddle.to_tensor(edge_index, dtype='int64')) + self.register_buffer( + "edge_index_buffer", paddle.to_tensor(edge_index, dtype="int64") + ) else: self.edge_index_buffer = None - + self.pair_energy = nn.Sequential( - nn.Linear(2*n_f, hidden), + nn.Linear(2 * n_f, hidden), nn.Softplus(), nn.Linear(hidden, hidden), nn.Softplus(), nn.Linear(hidden, hidden), nn.Softplus(), - nn.Linear(hidden, 1) + nn.Linear(hidden, 1), ) - + self.self_energy = nn.Sequential( nn.Linear(n_f, hidden), nn.Softplus(), @@ -321,54 +330,54 @@ def __init__( nn.Softplus(), nn.Linear(hidden, hidden), nn.Softplus(), - nn.Linear(hidden, 1) + nn.Linear(hidden, 1), ) - + def compute_energy(self, x: paddle.Tensor, edge_index: np.ndarray) -> paddle.Tensor: """ Compute the total energy (Hamiltonian) of the system. - + Args: x: Node features. edge_index: Edge indices. - + Returns: paddle.Tensor: Energy per node. """ row, col = edge_index[0], edge_index[1] - + x_i = x[col] x_j = x[row] edge_input = paddle.concat([x_i, x_j], axis=1) pair_energies = self.pair_energy(edge_input) - + num_nodes = x.shape[0] aggr_pair = paddle.zeros([num_nodes, 1], dtype=pair_energies.dtype) for i in range(len(col)): aggr_pair[col[i]] += pair_energies[i] - + self_energies = self.self_energy(x) - + total_energy = aggr_pair + self_energies - + return total_energy - + def forward(self, inputs: Dict[str, paddle.Tensor]) -> Dict[str, paddle.Tensor]: """ Forward propagation (PaddleScience standard). - + Compute dynamics derivatives using Hamilton's equations. - + Args: inputs: Input dictionary. - + Returns: Dict: Output dictionary. """ - x_input = inputs['x'].clone() - - if 'edge_index' in inputs: - edge_index = inputs['edge_index'] + x_input = inputs["x"].clone() + + if "edge_index" in inputs: + edge_index = inputs["edge_index"] if isinstance(edge_index, paddle.Tensor): edge_index = edge_index.numpy() if len(edge_index.shape) == 3: @@ -377,158 +386,157 @@ def forward(self, inputs: Dict[str, paddle.Tensor]) -> Dict[str, paddle.Tensor]: edge_index = self.edge_index_buffer.numpy() else: raise ValueError("Must provide edge_index") - + if len(x_input.shape) == 3: batch_size, n, n_f = x_input.shape x_reshaped = x_input.reshape([-1, n_f]) - + results = [] for b in range(batch_size): start_idx = b * n end_idx = (b + 1) * n x_batch = x_reshaped[start_idx:end_idx] - - q = x_batch[:, :self.ndim] - v = x_batch[:, self.ndim:2*self.ndim] - other = x_batch[:, 2*self.ndim:] - + + q = x_batch[:, : self.ndim] + v = x_batch[:, self.ndim : 2 * self.ndim] + other = x_batch[:, 2 * self.ndim :] + m_scalar = other[:, -1:] m_vec = paddle.tile(m_scalar, [1, self.ndim]) - + p = v * m_vec - + x_hamilton = paddle.concat([q, p, other], axis=1) x_hamilton.stop_gradient = False - + total_energy = self.compute_energy(x_hamilton, edge_index) total_energy_scalar = paddle.sum(total_energy) - + dH = paddle.grad( outputs=total_energy_scalar, inputs=x_hamilton, create_graph=False, - retain_graph=False + retain_graph=False, )[0] - - dH_dq = dH[:, :self.ndim] - dH_dp = dH[:, self.ndim:2*self.ndim] - + + dH_dq = dH[:, : self.ndim] + dH_dp = dH[:, self.ndim : 2 * self.ndim] + dq_dt = dH_dp dp_dt = -dH_dq dv_dt = dp_dt / m_vec - + derivative = paddle.concat([dq_dt, dv_dt], axis=1) results.append(derivative) - + derivative = paddle.stack(results, axis=0) - + outputs = {} - if 'velocity_derivative' in self.output_keys: - outputs['velocity_derivative'] = derivative[:, :, :self.ndim] - if 'acceleration' in self.output_keys: - outputs['acceleration'] = derivative[:, :, self.ndim:] - + if "velocity_derivative" in self.output_keys: + outputs["velocity_derivative"] = derivative[:, :, : self.ndim] + if "acceleration" in self.output_keys: + outputs["acceleration"] = derivative[:, :, self.ndim :] + if len(self.output_keys) == 1: - if self.output_keys[0] == 'acceleration': - outputs['acceleration'] = derivative[:, :, self.ndim:] + if self.output_keys[0] == "acceleration": + outputs["acceleration"] = derivative[:, :, self.ndim :] else: outputs[self.output_keys[0]] = derivative - + return outputs - + else: - q = x_input[:, :self.ndim] - v = x_input[:, self.ndim:2*self.ndim] - other = x_input[:, 2*self.ndim:] - + q = x_input[:, : self.ndim] + v = x_input[:, self.ndim : 2 * self.ndim] + other = x_input[:, 2 * self.ndim :] + m_scalar = other[:, -1:] m_vec = paddle.tile(m_scalar, [1, self.ndim]) - + p = v * m_vec - + x_hamilton = paddle.concat([q, p, other], axis=1) x_hamilton.stop_gradient = False - + total_energy = self.compute_energy(x_hamilton, edge_index) total_energy_scalar = paddle.sum(total_energy) - + dH = paddle.grad( outputs=total_energy_scalar, inputs=x_hamilton, create_graph=False, - retain_graph=False + retain_graph=False, )[0] - - dH_dq = dH[:, :self.ndim] - dH_dp = dH[:, self.ndim:2*self.ndim] - + + dH_dq = dH[:, : self.ndim] + dH_dp = dH[:, self.ndim : 2 * self.ndim] + dq_dt = dH_dp dp_dt = -dH_dq dv_dt = dp_dt / m_vec - + outputs = {} - if 'velocity_derivative' in self.output_keys: - outputs['velocity_derivative'] = dq_dt - if 'acceleration' in self.output_keys: - outputs['acceleration'] = dv_dt - + if "velocity_derivative" in self.output_keys: + outputs["velocity_derivative"] = dq_dt + if "acceleration" in self.output_keys: + outputs["acceleration"] = dv_dt + if len(self.output_keys) == 1: - if self.output_keys[0] == 'acceleration': - outputs['acceleration'] = dv_dt + if self.output_keys[0] == "acceleration": + outputs["acceleration"] = dv_dt else: outputs[self.output_keys[0]] = paddle.concat([dq_dt, dv_dt], axis=1) - + return outputs - + def compute_loss( self, inputs: Dict[str, paddle.Tensor], labels: Dict[str, paddle.Tensor], - reg_weight: float = 1e-6 + reg_weight: float = 1e-6, ) -> paddle.Tensor: """ Compute loss with physical regularization. - + Args: inputs: Input dictionary. labels: Label dictionary. reg_weight: Regularization weight. - + Returns: paddle.Tensor: Total loss. """ outputs = self.forward(inputs) - - pred = outputs.get('acceleration', outputs.get(self.output_keys[0])) - true = labels.get('acceleration_true', labels.get('acceleration')) - + + pred = outputs.get("acceleration", outputs.get(self.output_keys[0])) + true = labels.get("acceleration_true", labels.get("acceleration")) + base_loss = paddle.mean(paddle.abs(pred - true)) - - x_input = inputs['x'].clone() - edge_index = inputs.get('edge_index', self.edge_index_buffer.numpy()) + + x_input = inputs["x"].clone() + edge_index = inputs.get("edge_index", self.edge_index_buffer.numpy()) if isinstance(edge_index, paddle.Tensor): edge_index = edge_index.numpy() - - q = x_input[:, :self.ndim] - v = x_input[:, self.ndim:2*self.ndim] - other = x_input[:, 2*self.ndim:] - + + q = x_input[:, : self.ndim] + v = x_input[:, self.ndim : 2 * self.ndim] + other = x_input[:, 2 * self.ndim :] + m_scalar = other[:, -1:] m_vec = paddle.tile(m_scalar, [1, self.ndim]) p = v * m_vec - + x_hamilton = paddle.concat([q, p, other], axis=1) x_hamilton.stop_gradient = False - + total_energy = self.compute_energy(x_hamilton, edge_index) - + regularization = reg_weight * paddle.mean(total_energy**2) - + return base_loss + regularization class VarOGN(base.Arch): - def __init__( self, input_keys: List[str], @@ -539,11 +547,11 @@ def __init__( hidden: int = 300, edge_index: Optional[np.ndarray] = None, enable_sampling: bool = True, - l1_strength: float = 0.0 + l1_strength: float = 0.0, ): """ Initialize Variational Graph Network (VarGN). - + Args: input_keys: List of input keys. output_keys: List of output keys. @@ -553,7 +561,7 @@ def __init__( hidden: Hidden layer size. edge_index: Edge indices. enable_sampling: Whether to enable sampling. - + Examples: >>> import ppsci >>> import numpy as np @@ -574,7 +582,7 @@ def __init__( [5, 2] """ super().__init__() - + self.input_keys = input_keys self.output_keys = output_keys self.n_f = n_f @@ -583,23 +591,24 @@ def __init__( self.hidden = hidden self.enable_sampling = enable_sampling self.l1_strength = l1_strength - + if edge_index is not None: - self.register_buffer('edge_index_buffer', - paddle.to_tensor(edge_index, dtype='int64')) + self.register_buffer( + "edge_index_buffer", paddle.to_tensor(edge_index, dtype="int64") + ) else: self.edge_index_buffer = None - + self.msg_fnc = nn.Sequential( - nn.Linear(2*n_f, hidden), + nn.Linear(2 * n_f, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), - nn.Linear(hidden, msg_dim*2) + nn.Linear(hidden, msg_dim * 2), ) - + self.node_fnc = nn.Sequential( nn.Linear(msg_dim + n_f, hidden), nn.ReLU(), @@ -607,24 +616,25 @@ def __init__( nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), - nn.Linear(hidden, ndim) + nn.Linear(hidden, ndim), ) - - def message_passing(self, x: paddle.Tensor, edge_index: np.ndarray) -> Tuple[paddle.Tensor, paddle.Tensor]: + + def message_passing( + self, x: paddle.Tensor, edge_index: np.ndarray + ) -> Tuple[paddle.Tensor, paddle.Tensor]: """ Variational message passing. - + Args: x: Node features with shape [n, n_f] or [batch*n, n_f]. edge_index: Edge indices with shape [2, num_edges]. - + Returns: Tuple[paddle.Tensor, paddle.Tensor]: (mean output, variance output). """ if len(x.shape) == 3: batch_size, n, n_f = x.shape - x_reshaped = x.reshape([-1, n_f]) - + x_reshaped = x.reshape([-1, n_f]) results_mean = [] results_std = [] @@ -632,88 +642,88 @@ def message_passing(self, x: paddle.Tensor, edge_index: np.ndarray) -> Tuple[pad start_idx = b * n end_idx = (b + 1) * n x_batch = x_reshaped[start_idx:end_idx] - + row, col = edge_index[0], edge_index[1] - + x_i = x_batch[col] x_j = x_batch[row] - + msg_input = paddle.concat([x_i, x_j], axis=1) raw_msg = self.msg_fnc(msg_input) - + mu = raw_msg[:, 0::2] logvar = raw_msg[:, 1::2] - + if self.enable_sampling and self.training: epsilon = paddle.randn(mu.shape) msg = mu + epsilon * paddle.exp(logvar / 2.0) else: msg = mu - + aggr_out = paddle.zeros([n, self.msg_dim], dtype=msg.dtype) for i in range(len(col)): aggr_out[col[i]] += msg[i] - + node_input = paddle.concat([x_batch, aggr_out], axis=1) out_mean = self.node_fnc(node_input) - + # Calculate std as a small constant value for each output dimension # This provides a reasonable uncertainty estimate out_std = paddle.ones([n, self.ndim], dtype=out_mean.dtype) * 0.1 - + results_mean.append(out_mean) results_std.append(out_std) - + return paddle.stack(results_mean, axis=0), paddle.stack(results_std, axis=0) - + else: row, col = edge_index[0], edge_index[1] - + x_i = x[col] x_j = x[row] - + msg_input = paddle.concat([x_i, x_j], axis=1) raw_msg = self.msg_fnc(msg_input) - + mu = raw_msg[:, 0::2] logvar = raw_msg[:, 1::2] - + if self.enable_sampling and self.training: epsilon = paddle.randn(mu.shape) msg = mu + epsilon * paddle.exp(logvar / 2.0) else: msg = mu - + num_nodes = x.shape[0] aggr_out = paddle.zeros([num_nodes, self.msg_dim], dtype=msg.dtype) - + for i in range(len(col)): aggr_out[col[i]] += msg[i] - + node_input = paddle.concat([x, aggr_out], axis=1) out_mean = self.node_fnc(node_input) - + # Calculate std as a small constant value for each output dimension # This provides a reasonable uncertainty estimate num_nodes = x.shape[0] out_std = paddle.ones([num_nodes, self.ndim], dtype=out_mean.dtype) * 0.1 - + return out_mean, out_std - + def forward(self, inputs: Dict[str, paddle.Tensor]) -> Dict[str, paddle.Tensor]: """ Forward propagation (PaddleScience standard). - + Args: inputs: Input dictionary. - + Returns: Dict: Output dictionary containing mean and standard deviation. """ - x = inputs['x'] - - if 'edge_index' in inputs: - edge_index = inputs['edge_index'] + x = inputs["x"] + + if "edge_index" in inputs: + edge_index = inputs["edge_index"] if isinstance(edge_index, paddle.Tensor): edge_index = edge_index.numpy() if len(edge_index.shape) == 3: @@ -722,48 +732,48 @@ def forward(self, inputs: Dict[str, paddle.Tensor]) -> Dict[str, paddle.Tensor]: edge_index = self.edge_index_buffer.numpy() else: raise ValueError("Must provide edge_index") - + accel_mean, accel_std = self.message_passing(x, edge_index) - + outputs = {} - if 'acceleration_mean' in self.output_keys: - outputs['acceleration_mean'] = accel_mean - if 'acceleration_std' in self.output_keys: - outputs['acceleration_std'] = accel_std - + if "acceleration_mean" in self.output_keys: + outputs["acceleration_mean"] = accel_mean + if "acceleration_std" in self.output_keys: + outputs["acceleration_std"] = accel_std + if len(outputs) == 0: outputs[self.output_keys[0]] = accel_mean - - if hasattr(self, 'l1_strength') and self.l1_strength > 0: + + if hasattr(self, "l1_strength") and self.l1_strength > 0: l1_reg = self.l1_strength * paddle.mean(paddle.abs(x)) - outputs['l1_regularization'] = l1_reg - + outputs["l1_regularization"] = l1_reg + return outputs - + def compute_loss( self, inputs: Dict[str, paddle.Tensor], labels: Dict[str, paddle.Tensor], - kl_weight: float = 1e-3 + kl_weight: float = 1e-3, ) -> paddle.Tensor: """ Compute variational loss including KL divergence. - + Args: inputs: Input dictionary. labels: Label dictionary. kl_weight: KL divergence weight. - + Returns: paddle.Tensor: Total loss. """ outputs = self.forward(inputs) - - pred = outputs.get('acceleration_mean', outputs.get(self.output_keys[0])) - true = labels.get('acceleration_true', labels.get('acceleration')) - + + pred = outputs.get("acceleration_mean", outputs.get(self.output_keys[0])) + true = labels.get("acceleration_true", labels.get("acceleration")) + recon_loss = paddle.mean(paddle.abs(pred - true)) - + kl_loss = paddle.to_tensor(0.0) - + return recon_loss + kl_weight * kl_loss From d0a201125b7e47925fd6ef8774bb4e78057e6a85 Mon Sep 17 00:00:00 2001 From: ADream-ki <2085127827@qq.com> Date: Fri, 5 Dec 2025 23:26:57 +0800 Subject: [PATCH 08/10] feat:update --- ppsci/arch/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ppsci/arch/__init__.py b/ppsci/arch/__init__.py index 06eb28b8d8..67c5b026a4 100644 --- a/ppsci/arch/__init__.py +++ b/ppsci/arch/__init__.py @@ -54,7 +54,7 @@ from ppsci.arch.physx_transformer import PhysformerGPT2 # isort:skip from ppsci.arch.sfnonet import SFNONet # isort:skip from ppsci.arch.spinn import SPINN # isort:skip -from ppsci.arch.symbolic_gn import OGN, VarOGN, HGN, get_edge_index # isort:skip +from ppsci.arch.symbolic_gn import OGN, VarOGN, HGN # isort:skip from ppsci.arch.tfnonet import TFNO1dNet, TFNO2dNet, TFNO3dNet # isort:skip from ppsci.arch.transformer import Transformer # isort:skip from ppsci.arch.unetex import UNetEx # isort:skip @@ -105,7 +105,6 @@ "ExtFormerMoECuboid", "FNO1d", "Generator", - "get_edge_index", "GraphCastNet", "HEDeepONets", "HGN", From 535b278c599a0415799032a44112fab577e75015 Mon Sep 17 00:00:00 2001 From: ADream-ki <2085127827@qq.com> Date: Sat, 6 Dec 2025 00:13:56 +0800 Subject: [PATCH 09/10] update code style --- examples/symbolic_gn/conf/config.yaml | 7 +- examples/symbolic_gn/gn.py | 139 +++++++++++++++----------- 2 files changed, 82 insertions(+), 64 deletions(-) diff --git a/examples/symbolic_gn/conf/config.yaml b/examples/symbolic_gn/conf/config.yaml index 4b0307a738..5eaececd9c 100644 --- a/examples/symbolic_gn/conf/config.yaml +++ b/examples/symbolic_gn/conf/config.yaml @@ -17,7 +17,6 @@ hydra: init_callback: _target_: ppsci.utils.callbacks.InitCallback sweep: - dir: ${hydra.run.dir} subdir: ./ @@ -27,7 +26,6 @@ output_dir: ${hydra:run.dir} log_freq: 20 device: gpu - TRAIN: epochs: 1000 batch_size: 32 @@ -48,7 +46,6 @@ TRAIN: type: "MAE" physics_weight: 0.0 - EVAL: pretrained_model_path: null eval_with_no_grad: true @@ -56,7 +53,6 @@ EVAL: message_analysis: enabled: true - export_path: "./exported_model" INFER: @@ -76,7 +72,6 @@ INFER: max_batch_size: 1024 num_cpu_threads: 10 - MODEL: arch: "OGN" input_keys: ["x", "edge_index"] @@ -145,4 +140,4 @@ DATATYPE_TYPE: dt: [1e-2] nt: [1000] n: [30] - dim: [2] \ No newline at end of file + dim: [2] diff --git a/examples/symbolic_gn/gn.py b/examples/symbolic_gn/gn.py index 4d5e99f6f4..ec4dd499bc 100644 --- a/examples/symbolic_gn/gn.py +++ b/examples/symbolic_gn/gn.py @@ -13,8 +13,7 @@ # limitations under the License. import os -from typing import List -from typing import Optional +from typing import List, Optional import hydra import matplotlib.pyplot as plt @@ -24,25 +23,22 @@ from simulate import SimulationDataset import ppsci -from ppsci.arch.symbolic_gn import HGN -from ppsci.arch.symbolic_gn import OGN -from ppsci.arch.symbolic_gn import VarOGN -from ppsci.arch.symbolic_gn import get_edge_index +from ppsci.arch.symbolic_gn import HGN, OGN, VarOGN, get_edge_index from ppsci.utils import logger def create_ppsci_model( - model_type: str = 'OGN', - input_keys: List[str] = ['x', 'edge_index'], - output_keys: List[str] = ['acceleration'], + model_type: str = "OGN", + input_keys: List[str] = ["x", "edge_index"], + output_keys: List[str] = ["acceleration"], n_f: int = 6, msg_dim: int = 100, ndim: int = 2, hidden: int = 300, edge_index: Optional[np.ndarray] = None, - l1_strength: float = 0.0 + l1_strength: float = 0.0, ): - if model_type == 'OGN': + if model_type == "OGN": model = OGN( input_keys=input_keys, output_keys=output_keys, @@ -51,18 +47,18 @@ def create_ppsci_model( ndim=ndim, hidden=hidden, edge_index=edge_index, - l1_strength=l1_strength + l1_strength=l1_strength, ) - elif model_type == 'HGN': + elif model_type == "HGN": model = HGN( input_keys=input_keys, output_keys=output_keys, n_f=n_f, ndim=ndim, hidden=hidden, - edge_index=edge_index + edge_index=edge_index, ) - elif model_type == 'VarOGN': + elif model_type == "VarOGN": model = VarOGN( input_keys=input_keys, output_keys=output_keys, @@ -71,7 +67,7 @@ def create_ppsci_model( ndim=ndim, hidden=hidden, edge_index=edge_index, - l1_strength=l1_strength + l1_strength=l1_strength, ) else: raise ValueError(f"未知的模型类型: {model_type}") @@ -85,10 +81,10 @@ def replicate_array(arr, n): def create_loss_function(cfg): def loss_function(output_dict, label_dict, weight_dict=None): - loss_type = getattr(cfg.TRAIN.loss, 'type', 'MAE') - + loss_type = getattr(cfg.TRAIN.loss, "type", "MAE") + output_key = list(cfg.MODEL.output_keys)[0] - + target = label_dict[output_key] pred = output_dict[output_key] if loss_type == "MSE": @@ -96,11 +92,11 @@ def loss_function(output_dict, label_dict, weight_dict=None): else: base_loss = paddle.mean(paddle.abs(pred - target)) - if 'l1_regularization' in output_dict: - base_loss = base_loss + output_dict['l1_regularization'] + if "l1_regularization" in output_dict: + base_loss = base_loss + output_dict["l1_regularization"] return {output_key: base_loss} - + return loss_function @@ -115,7 +111,7 @@ def train(cfg): n=cfg.DATA.num_nodes, dim=cfg.DATA.dimension, nt=cfg.DATA.time_steps, - dt=cfg.DATA.time_step_size + dt=cfg.DATA.time_step_size, ) sim.simulate(cfg.DATA.num_samples) accel_data = sim.get_acceleration() @@ -134,7 +130,7 @@ def train(cfg): cfg.MODEL.n_f = cfg.DATA.dimension * 2 + 2 if cfg.MODEL.ndim == "auto": cfg.MODEL.ndim = cfg.DATA.dimension - + train_size = int(len(X) * 0.8) X_train, X_val = X[:train_size], X[train_size:] y_train, y_val = y[:train_size], y[train_size:] @@ -180,7 +176,11 @@ def train(cfg): train_constraint.name: train_constraint, eval_constraint.name: eval_constraint, } - l1_strength = cfg.MODEL.l1_strength if cfg.MODEL.regularization_type == "l1" and cfg.MODEL.arch in ["OGN", "VarOGN"] else 0.0 + l1_strength = ( + cfg.MODEL.l1_strength + if cfg.MODEL.regularization_type == "l1" and cfg.MODEL.arch in ["OGN", "VarOGN"] + else 0.0 + ) model = create_ppsci_model( model_type=cfg.MODEL.arch, n_f=cfg.MODEL.n_f, @@ -188,28 +188,27 @@ def train(cfg): ndim=cfg.MODEL.ndim, hidden=cfg.MODEL.hidden, edge_index=edge_index, - l1_strength=l1_strength + l1_strength=l1_strength, ) batch_per_epoch = int(train_size / cfg.TRAIN.batch_size) if cfg.TRAIN.lr_scheduler.name == "OneCycleLR": lr_scheduler = paddle.optimizer.lr.OneCycleLR( max_learning_rate=cfg.TRAIN.lr_scheduler.max_learning_rate, - total_step=int(cfg.TRAIN.epochs*batch_per_epoch), - divide_factor=cfg.TRAIN.lr_scheduler.final_div_factor + total_step=int(cfg.TRAIN.epochs * batch_per_epoch), + divide_factor=cfg.TRAIN.lr_scheduler.final_div_factor, ) lr_scheduler.by_epoch = False else: lr_scheduler = paddle.optimizer.lr.ExponentialDecay( - learning_rate=cfg.TRAIN.optimizer.learning_rate, - gamma=0.9 + learning_rate=cfg.TRAIN.optimizer.learning_rate, gamma=0.9 ) lr_scheduler.by_epoch = True optimizer = paddle.optimizer.Adam( learning_rate=lr_scheduler, parameters=model.parameters(), - weight_decay=cfg.TRAIN.optimizer.weight_decay + weight_decay=cfg.TRAIN.optimizer.weight_decay, ) solver = ppsci.solver.Solver( @@ -233,7 +232,11 @@ def evaluate(cfg: DictConfig): cfg.MODEL.ndim = cfg.DATA.dimension edge_index = get_edge_index(cfg.DATA.num_nodes, cfg.DATA.type) - l1_strength = cfg.MODEL.l1_strength if cfg.MODEL.regularization_type == "l1" and cfg.MODEL.arch in ["OGN", "VarOGN"] else 0.0 + l1_strength = ( + cfg.MODEL.l1_strength + if cfg.MODEL.regularization_type == "l1" and cfg.MODEL.arch in ["OGN", "VarOGN"] + else 0.0 + ) model = create_ppsci_model( model_type=cfg.MODEL.arch, @@ -242,7 +245,7 @@ def evaluate(cfg: DictConfig): ndim=cfg.MODEL.ndim, hidden=cfg.MODEL.hidden, edge_index=edge_index, - l1_strength=l1_strength + l1_strength=l1_strength, ) ppsci.utils.save_load.load_pretrain( @@ -255,20 +258,20 @@ def evaluate(cfg: DictConfig): n=cfg.DATA.num_nodes, dim=cfg.DATA.dimension, nt=cfg.DATA.time_steps, - dt=cfg.DATA.time_step_size + dt=cfg.DATA.time_step_size, ) sim.simulate(cfg.DATA.num_samples) accel_data = sim.get_acceleration() sample_indices = [0, 1] if cfg.DATA.num_samples > 1 else [0] - + for sample_idx in sample_indices: sample_data = sim.data[sample_idx, 0:1] true_accel = accel_data[sample_idx, 0:1] - + input_dict = { "x": paddle.to_tensor(sample_data, dtype="float32"), - "edge_index": paddle.to_tensor(edge_index, dtype="int64") + "edge_index": paddle.to_tensor(edge_index, dtype="int64"), } with paddle.no_grad(): @@ -278,16 +281,20 @@ def evaluate(cfg: DictConfig): error = np.mean(np.abs(pred_accel.numpy() - true_accel)) logger.info(f"Sample {sample_idx} - MAE error: {error:.6f}") - rel_error = np.linalg.norm(pred_accel.numpy() - true_accel) / np.linalg.norm(true_accel) + rel_error = np.linalg.norm(pred_accel.numpy() - true_accel) / np.linalg.norm( + true_accel + ) logger.info(f"Sample {sample_idx} - Relative error: {rel_error:.6f}") plt.figure(figsize=(10, 8)) sim.plot(sample_idx, animate=False, plot_size=True, s_size=2) - plt.title(f'{cfg.DATA.type.capitalize()} System - Sample {sample_idx}\nMAE: {error:.4f}, Rel Error: {rel_error:.4f}') + plt.title( + f"{cfg.DATA.type.capitalize()} System - Sample {sample_idx}\nMAE: {error:.4f}, Rel Error: {rel_error:.4f}" + ) plt.tight_layout() plot_path = os.path.join(cfg.output_dir, f"evaluation_sample_{sample_idx}.png") - plt.savefig(plot_path, dpi=300, bbox_inches='tight') + plt.savefig(plot_path, dpi=300, bbox_inches="tight") plt.close() logger.info(f"Evaluation plot saved to {plot_path}") @@ -303,7 +310,11 @@ def export(cfg: DictConfig): if cfg.MODEL.ndim == "auto": cfg.MODEL.ndim = cfg.DATA.dimension edge_index = get_edge_index(cfg.DATA.num_nodes, cfg.DATA.type) - l1_strength = cfg.MODEL.l1_strength if cfg.MODEL.regularization_type == "l1" and cfg.MODEL.arch in ["OGN", "VarOGN"] else 0.0 + l1_strength = ( + cfg.MODEL.l1_strength + if cfg.MODEL.regularization_type == "l1" and cfg.MODEL.arch in ["OGN", "VarOGN"] + else 0.0 + ) model = create_ppsci_model( model_type=cfg.MODEL.arch, n_f=cfg.MODEL.n_f, @@ -311,7 +322,7 @@ def export(cfg: DictConfig): ndim=cfg.MODEL.ndim, hidden=cfg.MODEL.hidden, edge_index=edge_index, - l1_strength=l1_strength + l1_strength=l1_strength, ) solver = ppsci.solver.Solver( @@ -323,9 +334,13 @@ def export(cfg: DictConfig): input_spec = [ { - key: InputSpec([None, cfg.DATA.num_nodes, cfg.MODEL.n_f], "float32", name=key) - if key == "x" - else InputSpec([2, 30], "int64", name=key) + key: ( + InputSpec( + [None, cfg.DATA.num_nodes, cfg.MODEL.n_f], "float32", name=key + ) + if key == "x" + else InputSpec([2, 30], "int64", name=key) + ) for key in model.input_keys }, ] @@ -348,7 +363,7 @@ def inference(cfg: DictConfig): n=cfg.DATA.num_nodes, dim=cfg.DATA.dimension, nt=cfg.DATA.time_steps, - dt=cfg.DATA.time_step_size + dt=cfg.DATA.time_step_size, ) sim.simulate(cfg.DATA.num_samples) @@ -357,10 +372,7 @@ def inference(cfg: DictConfig): edge_index = get_edge_index(cfg.DATA.num_nodes, cfg.DATA.type) if use_predictor: - input_dict = { - "x": sample_data, - "edge_index": edge_index - } + input_dict = {"x": sample_data, "edge_index": edge_index} output_dict = predictor.predict(input_dict, cfg.INFER.batch_size) pred_acceleration = output_dict[cfg.MODEL.output_keys[0]] else: @@ -369,7 +381,12 @@ def inference(cfg: DictConfig): if cfg.MODEL.ndim == "auto": cfg.MODEL.ndim = cfg.DATA.dimension - l1_strength = cfg.MODEL.l1_strength if cfg.MODEL.regularization_type == "l1" and cfg.MODEL.arch in ["OGN", "VarOGN"] else 0.0 + l1_strength = ( + cfg.MODEL.l1_strength + if cfg.MODEL.regularization_type == "l1" + and cfg.MODEL.arch in ["OGN", "VarOGN"] + else 0.0 + ) model = create_ppsci_model( model_type=cfg.MODEL.arch, @@ -378,7 +395,7 @@ def inference(cfg: DictConfig): ndim=cfg.MODEL.ndim, hidden=cfg.MODEL.hidden, edge_index=edge_index, - l1_strength=l1_strength + l1_strength=l1_strength, ) ppsci.utils.save_load.load_pretrain( @@ -388,7 +405,7 @@ def inference(cfg: DictConfig): input_dict = { "x": paddle.to_tensor(sample_data, dtype="float32"), - "edge_index": paddle.to_tensor(edge_index, dtype="int64") + "edge_index": paddle.to_tensor(edge_index, dtype="int64"), } with paddle.no_grad(): @@ -401,16 +418,20 @@ def inference(cfg: DictConfig): error = np.mean(np.abs(pred_acceleration - true_acceleration)) logger.info(f"Inference error (MAE): {error:.6f}") - rel_error = np.linalg.norm(pred_acceleration - true_acceleration) / np.linalg.norm(true_acceleration) + rel_error = np.linalg.norm(pred_acceleration - true_acceleration) / np.linalg.norm( + true_acceleration + ) logger.info(f"Inference relative error: {rel_error:.6f}") plt.figure(figsize=(10, 8)) sim.plot(sample_idx, animate=False, plot_size=True, s_size=2) - plt.title(f'{cfg.DATA.type.capitalize()} System - Inference\nMAE: {error:.4f}, Rel Error: {rel_error:.4f}') + plt.title( + f"{cfg.DATA.type.capitalize()} System - Inference\nMAE: {error:.4f}, Rel Error: {rel_error:.4f}" + ) plt.tight_layout() plot_path = os.path.join(cfg.output_dir, "inference.png") - plt.savefig(plot_path, dpi=300, bbox_inches='tight') + plt.savefig(plot_path, dpi=300, bbox_inches="tight") plt.close() logger.info(f"Inference plot saved to {plot_path}") @@ -427,9 +448,11 @@ def main(cfg: DictConfig) -> None: inference(cfg) else: raise ValueError( - "cfg.mode should in [train, eval, export, infer], but got {}".format(cfg.mode) + "cfg.mode should in [train, eval, export, infer], but got {}".format( + cfg.mode + ) ) if __name__ == "__main__": - main() \ No newline at end of file + main() From b499b7ad3b4ff4831af161eb8cc4d17ad585d7b6 Mon Sep 17 00:00:00 2001 From: ADream-ki <2085127827@qq.com> Date: Sun, 14 Dec 2025 22:06:37 +0800 Subject: [PATCH 10/10] update code --- docs/zh/examples/symbolic_gn.md | 6 +++--- examples/symbolic_gn/gn.py | 10 +++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/zh/examples/symbolic_gn.md b/docs/zh/examples/symbolic_gn.md index b6caf40417..bdd5620881 100644 --- a/docs/zh/examples/symbolic_gn.md +++ b/docs/zh/examples/symbolic_gn.md @@ -265,7 +265,7 @@ TRAIN: l1_strength: 1e-2 ``` -2. **提取消息数据**:训练过程中可以记录消息向量(需要修改代码添加钩子) +2. **提取消息数据**:训练过程中可以记录消息向量 ```python messages = model.msg_fnc(edge_features) # [num_edges, msg_dim] # 保存消息及对应的物理特征(dx, dy, r, m1, m2等) @@ -274,10 +274,10 @@ TRAIN: 3. **符号回归**:使用工具如 [PySR](https://github.com/MilesCranmer/PySR) 拟合符号表达式 ```python from pysr import PySRRegressor - + # 选择最活跃的消息通道 active_channels = np.argsort(np.std(messages, axis=0))[-5:] - + # 对每个活跃通道进行符号回归 for ch in active_channels: model = PySRRegressor( diff --git a/examples/symbolic_gn/gn.py b/examples/symbolic_gn/gn.py index ec4dd499bc..b453dc60da 100644 --- a/examples/symbolic_gn/gn.py +++ b/examples/symbolic_gn/gn.py @@ -13,7 +13,8 @@ # limitations under the License. import os -from typing import List, Optional +from typing import List +from typing import Optional import hydra import matplotlib.pyplot as plt @@ -23,7 +24,10 @@ from simulate import SimulationDataset import ppsci -from ppsci.arch.symbolic_gn import HGN, OGN, VarOGN, get_edge_index +from ppsci.arch.symbolic_gn import HGN +from ppsci.arch.symbolic_gn import OGN +from ppsci.arch.symbolic_gn import VarOGN +from ppsci.arch.symbolic_gn import get_edge_index from ppsci.utils import logger @@ -195,7 +199,7 @@ def train(cfg): if cfg.TRAIN.lr_scheduler.name == "OneCycleLR": lr_scheduler = paddle.optimizer.lr.OneCycleLR( max_learning_rate=cfg.TRAIN.lr_scheduler.max_learning_rate, - total_step=int(cfg.TRAIN.epochs * batch_per_epoch), + total_steps=int(cfg.TRAIN.epochs * batch_per_epoch), divide_factor=cfg.TRAIN.lr_scheduler.final_div_factor, ) lr_scheduler.by_epoch = False