OneScience commited on
Commit
ede74c0
·
verified ·
1 Parent(s): e2703dc

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -1,35 +1,49 @@
1
  *.7z filter=lfs diff=lfs merge=lfs -text
2
  *.arrow filter=lfs diff=lfs merge=lfs -text
3
  *.bin filter=lfs diff=lfs merge=lfs -text
 
4
  *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
  *.ftz filter=lfs diff=lfs merge=lfs -text
7
  *.gz filter=lfs diff=lfs merge=lfs -text
8
  *.h5 filter=lfs diff=lfs merge=lfs -text
9
  *.joblib filter=lfs diff=lfs merge=lfs -text
10
  *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
  *.model filter=lfs diff=lfs merge=lfs -text
13
  *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
  *.onnx filter=lfs diff=lfs merge=lfs -text
17
  *.ot filter=lfs diff=lfs merge=lfs -text
18
  *.parquet filter=lfs diff=lfs merge=lfs -text
19
  *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
  *.pt filter=lfs diff=lfs merge=lfs -text
23
  *.pth filter=lfs diff=lfs merge=lfs -text
24
  *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
  saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
  *.tflite filter=lfs diff=lfs merge=lfs -text
30
  *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
  *.xz filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
1
  *.7z filter=lfs diff=lfs merge=lfs -text
2
  *.arrow filter=lfs diff=lfs merge=lfs -text
3
  *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bin.* filter=lfs diff=lfs merge=lfs -text
5
  *.bz2 filter=lfs diff=lfs merge=lfs -text
 
6
  *.ftz filter=lfs diff=lfs merge=lfs -text
7
  *.gz filter=lfs diff=lfs merge=lfs -text
8
  *.h5 filter=lfs diff=lfs merge=lfs -text
9
  *.joblib filter=lfs diff=lfs merge=lfs -text
10
  *.lfs.* filter=lfs diff=lfs merge=lfs -text
 
11
  *.model filter=lfs diff=lfs merge=lfs -text
12
  *.msgpack filter=lfs diff=lfs merge=lfs -text
 
 
13
  *.onnx filter=lfs diff=lfs merge=lfs -text
14
  *.ot filter=lfs diff=lfs merge=lfs -text
15
  *.parquet filter=lfs diff=lfs merge=lfs -text
16
  *.pb filter=lfs diff=lfs merge=lfs -text
 
 
17
  *.pt filter=lfs diff=lfs merge=lfs -text
18
  *.pth filter=lfs diff=lfs merge=lfs -text
19
  *.rar filter=lfs diff=lfs merge=lfs -text
 
20
  saved_model/**/* filter=lfs diff=lfs merge=lfs -text
21
+
 
22
  *.tflite filter=lfs diff=lfs merge=lfs -text
23
  *.tgz filter=lfs diff=lfs merge=lfs -text
 
24
  *.xz filter=lfs diff=lfs merge=lfs -text
25
  *.zip filter=lfs diff=lfs merge=lfs -text
26
+ *.zstandard filter=lfs diff=lfs merge=lfs -text
27
+ *.tfevents* filter=lfs diff=lfs merge=lfs -text
28
+ *.db* filter=lfs diff=lfs merge=lfs -text
29
+ *.ark* filter=lfs diff=lfs merge=lfs -text
30
+ **/*ckpt*data* filter=lfs diff=lfs merge=lfs -text
31
+ **/*ckpt*.meta filter=lfs diff=lfs merge=lfs -text
32
+ **/*ckpt*.index filter=lfs diff=lfs merge=lfs -text
33
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
34
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
35
+ *.gguf* filter=lfs diff=lfs merge=lfs -text
36
+ *.ggml filter=lfs diff=lfs merge=lfs -text
37
+ *.llamafile* filter=lfs diff=lfs merge=lfs -text
38
+ *.pt2 filter=lfs diff=lfs merge=lfs -text
39
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
40
+ *.npy filter=lfs diff=lfs merge=lfs -text
41
+ *.npz filter=lfs diff=lfs merge=lfs -text
42
+ *.pickle filter=lfs diff=lfs merge=lfs -text
43
+ *.pkl filter=lfs diff=lfs merge=lfs -text
44
+
45
+ *.wasm filter=lfs diff=lfs merge=lfs -text
46
  *.zst filter=lfs diff=lfs merge=lfs -text
47
  *tfevents* filter=lfs diff=lfs merge=lfs -text
48
+
49
+ navier_stokes/cylinder_nektar_wake.mat filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -4,37 +4,38 @@ language:
4
  - en
5
  tags:
6
  - OneScience
7
- - PDE
8
- - CFD
9
- - neural-operator
10
- - PDEBench
11
  frameworks: PyTorch
12
-
13
- datasets:
14
- - OneScience/pdenneval
15
  ---
16
  <p align="center">
17
  <strong>
18
- <span style="font-size: 30px;">PDENNEval</span>
19
  </strong>
20
  </p>
21
 
22
  # Model Overview
23
- PDENNEval is a comprehensive benchmark developed by researchers at Sun Yat-sen University for evaluating neural-network methods for solving partial differential equations. It enables systematic comparison of function-learning and operator-learning approaches across a broad range of PDE tasks.
24
 
25
- Paper: [PDENNEval: A Comprehensive Evaluation of Neural Network Methods for Solving PDEs](https://www.ijcai.org/proceedings/2024/0573.pdf)
 
 
26
 
27
  # Model Description
28
- PDENNEval provides a unified framework for evaluating neural-network PDE solvers. It contains 16 datasets generated with high-accuracy conventional scientific computing methods, covering 19 PDE problem classes from fluid dynamics, materials science, finance, electromagnetics, and other domains. The benchmark evaluates the solution accuracy, computational efficiency, and robustness of 12 neural-network methods.
29
 
30
- ## Use Cases
 
 
 
 
31
 
32
  | Use Case | Description |
33
- | --- | --- |
34
- | PDE neural-operator validation | Complete a minimal FNO training and inference workflow using PDEBench-style HDF5 data |
35
- | Neural PDE method development | Extend the model collection in `model/`, which already includes FNO, DeepONet, PINO-FNO, UNO, MPNN, and UNet definitions |
 
36
  | ModelScope/OneCode execution | Download the standalone model package, install its dependencies, and run the provided scripts |
37
- | Rapid pipeline checks | Use `fake_data.py` and `smoke_models.py` to verify the data, models, and scripts |
38
 
39
  # Usage
40
 
@@ -48,16 +49,15 @@ Use the online OneCode environment for an intelligent, one-click AI for Science
48
 
49
  **Hardware Requirements**
50
 
51
- - A GPU or DCU is recommended.
52
- - A CPU can be used for import checks and small-scale pipeline validation, but full training and inference will be slow.
53
- - DCU users must install DTK in advance. DTK 25.04.2 or later, or the OneScience-recommended version for the target cluster, is recommended.
54
-
55
 
56
  ### Download the Model Package
57
 
58
  ```bash
59
- modelscope download --model OneScience/PDENNEval --local_dir ./PDENNEval
60
- cd PDENNEval
61
  ```
62
 
63
  ### Set Up the Runtime Environment
@@ -84,20 +84,30 @@ pip install onescience[cfd-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple
84
 
85
  ### Training Data
86
 
87
- The OneScience community provides the `pdenneval` dataset for training. Download it with the command below and verify that the data path in `conf/config.yaml` is configured correctly:
88
 
89
- ```bash
90
- modelscope download --dataset OneScience/pdenneval --local_dir ./data
91
- ```
92
 
93
  ### Training
94
 
95
  ```bash
96
  python scripts/train.py
97
  ```
 
 
 
 
 
 
 
 
 
 
98
 
99
  ### Model Weights
100
- This repository will provide weights trained on the `pdenneval` dataset in the `weights/` directory. The weights will be uploaded soon.
101
 
102
  ### Inference
103
 
@@ -105,7 +115,11 @@ This repository will provide weights trained on the `pdenneval` dataset in the `
105
  python scripts/inference.py
106
  ```
107
 
108
- By default, inference loads `weight/best_model.pt` and writes `.npz` results to `result/output/`.
 
 
 
 
109
 
110
  ### Evaluation and Visualization
111
 
@@ -122,5 +136,5 @@ python scripts/result.py
122
 
123
  # Citations and License
124
 
125
- - Original PDENNEval paper: [PDENNEval: A Comprehensive Evaluation of Neural Network Methods for Solving PDEs](https://www.ijcai.org/proceedings/2024/0573.pdf).
126
- - This repository retains the relevant source and attribution notices. Follow all applicable license requirements when using, modifying, or distributing its contents.
 
4
  - en
5
  tags:
6
  - OneScience
7
+ - fluid dynamics
8
+ - physics-informed neural networks
9
+ - long-horizon physical prediction
 
10
  frameworks: PyTorch
 
 
 
11
  ---
12
  <p align="center">
13
  <strong>
14
+ <span style="font-size: 30px;">PINNsformer</span>
15
  </strong>
16
  </p>
17
 
18
  # Model Overview
 
19
 
20
+ PINNsFormer is a Transformer-based physics-informed neural network framework developed by researchers at the Georgia Institute of Technology and Carnegie Mellon University. It enables rapid prediction of solutions to time-dependent partial differential equations and their associated physical fields.
21
+
22
+ Paper: [PINNsFormer: A Transformer-Based Framework For Physics-Informed Neural Networks](https://arxiv.org/abs/2307.11833).
23
 
24
  # Model Description
 
25
 
26
+ PINNsFormer uses a Transformer encoder–decoder with multi-head attention to numerically solve time-dependent partial differential equations, including convection, reaction, wave, and Navier–Stokes equations.
27
+
28
+
29
+
30
+ # Use Cases
31
 
32
  | Use Case | Description |
33
+ | :--- | :--- |
34
+ | Time-dependent PDE solving | Train a continuous-field surrogate constrained by physical residuals, boundary conditions, and initial conditions |
35
+ | Physics-informed neural network validation | Rapidly validate the PINNsFormer network, loss functions, weight serialization, and inference pipeline |
36
+ | One-dimensional reaction equation example | Generate target fields from an analytical solution for pipeline validation and error analysis |
37
  | ModelScope/OneCode execution | Download the standalone model package, install its dependencies, and run the provided scripts |
38
+
39
 
40
  # Usage
41
 
 
49
 
50
  **Hardware Requirements**
51
 
52
+ * A CPU can be used for small-scale pipeline validation.
53
+ * A GPU or DCU is recommended for training on larger grids or for more epochs.
54
+ * DCU users must install DTK in advance. DTK 25.04.2 or later, or the OneScience-recommended version for the target cluster, is recommended.
 
55
 
56
  ### Download the Model Package
57
 
58
  ```bash
59
+ modelscope download --model OneScience/PINNsformer --local_dir ./PINNsformer
60
+ cd PINNsformer
61
  ```
62
 
63
  ### Set Up the Runtime Environment
 
84
 
85
  ### Training Data
86
 
87
+ To use real data, download it from the link below and set `data.data_dir` in `conf/config.yaml` to the correct path.
88
 
89
+ | Source | Link | Extraction Code | Destination |
90
+ |---|---|---|---|
91
+ | Baidu Netdisk | https://pan.baidu.com/s/1pM4ICc6FJX5pLF7WEoozxQ?pwd=5gha | `5gha` | `convection/convection.mat` and `navier_stokes/cylinder_nektar_wake.mat` |
92
 
93
  ### Training
94
 
95
  ```bash
96
  python scripts/train.py
97
  ```
98
+ The default configuration uses a smaller grid and fewer L-BFGS iterations for rapid end-to-end validation. To restore the scale of the original example, edit `conf/config.yaml`:
99
+
100
+ ```yaml
101
+ data:
102
+ x_num: 101
103
+ t_num: 101
104
+
105
+ training:
106
+ epochs: 500
107
+ ```
108
 
109
  ### Model Weights
110
+ This repository will provide PINNsFormer model weights in the `weights/` directory. The weights will be uploaded soon.
111
 
112
  ### Inference
113
 
 
115
  python scripts/inference.py
116
  ```
117
 
118
+ Inference loads `weight/1dreaction_pinnsformer.pt` and saves:
119
+
120
+ ```text
121
+ result/prediction.npz
122
+ ```
123
 
124
  ### Evaluation and Visualization
125
 
 
136
 
137
  # Citations and License
138
 
139
+ * Original PINNsformer paper: [PINNsFormer: A Transformer-Based Framework For Physics-Informed Neural Networks](https://arxiv.org/abs/2307.11833).
140
+ * This repository retains the relevant source and attribution notices. Follow all applicable license requirements when using, modifying, or distributing its contents.
conf/config.yaml CHANGED
@@ -1,52 +1,46 @@
1
- fno_config:
2
- datapipe:
3
- source:
4
- data_dir: "./data"
5
- file_name: "2D_DarcyFlow_beta0.1_Train.hdf5"
6
- verbose: true
7
- data:
8
- single_file: true
9
- reduced_resolution: 1
10
- reduced_resolution_t: 1
11
- reduced_batch: 1
12
- initial_step: 1
13
- test_ratio: 0.1
14
- pde_name: "2D_Darcy_Flow"
15
- dataloader:
16
- batch_size: 2
17
- num_workers: 0
18
- pin_memory: false
19
 
20
- model:
21
- name: "FNO"
22
- num_channels: 1
23
- modes: 4
24
- width: 8
25
 
26
- training:
27
- output_dir: "./weight"
28
- if_training: true
29
- continue_training: false
30
- model_path: null
31
- save_period: 1
32
- seed: 0
33
- training_type: "single"
34
- t_train: 2
35
- epochs: 1
36
- optimizer:
37
- name: "Adam"
38
- lr: 1.0e-3
39
- weight_decay: 1.0e-4
40
- scheduler:
41
- name: "StepLR"
42
- step_size: 100
43
- gamma: 0.5
44
 
45
- inference:
46
- checkpoint: "./weight/best_model.pt"
47
- output_dir: "./result/output"
48
- max_batches: 2
 
 
 
 
49
 
50
- result:
51
- prediction_dir: "./result/output"
52
- output_dir: "./result"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ project:
2
+ name: PINNsformer
3
+ task: 1d_reaction
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
+ runtime:
6
+ seed: 0
7
+ device: auto
 
 
8
 
9
+ model:
10
+ name: PINNsformer1D
11
+ d_out: 1
12
+ d_hidden: 128
13
+ d_model: 32
14
+ num_layers: 1
15
+ heads: 2
 
 
 
 
 
 
 
 
 
 
 
16
 
17
+ data:
18
+ x_range: [0.0, 6.283185307179586]
19
+ t_range: [0.0, 1.0]
20
+ x_num: 31
21
+ t_num: 31
22
+ sequence:
23
+ num_step: 5
24
+ step: 0.0001
25
 
26
+ equation:
27
+ reaction_rate: 5.0
28
+ initial:
29
+ center: 3.141592653589793
30
+ sigma: 0.7853981633974483
31
+
32
+ training:
33
+ epochs: 2
34
+ optimizer:
35
+ name: LBFGS
36
+ lr: 1.0
37
+ max_iter: 5
38
+ line_search_fn: strong_wolfe
39
+ checkpoint: weight/1dreaction_pinnsformer.pt
40
+
41
+ paths:
42
+ fake_data: data/reaction_fake.npz
43
+ prediction: result/prediction.npz
44
+ metrics: result/metrics.json
45
+ loss: result/train_loss.npy
46
+ figure: result/1dreaction_pinnsformer.png
configuration.json CHANGED
@@ -1 +1 @@
1
- {"framework":"Pytorch","task":"other"}
 
1
+ {"framework":"Pytorch","task":"other","model":"PINNsformer","domain":"cfd"}
model/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
- from .fno import FNO1d, FNO2d, FNO3d, FNO_maxwell
2
 
3
- __all__ = ["FNO1d", "FNO2d", "FNO3d", "FNO_maxwell"]
 
1
+ from .pinnsformer import PINNsformer1D, PINNsformer2D, WaveAct
2
 
3
+ __all__ = ["PINNsformer1D", "PINNsformer2D", "WaveAct"]
model/pinnsformer.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # implementation of PINNsformer
2
+ # paper: PINNsFormer: A Transformer-Based Framework For Physics-Informed Neural Networks
3
+ # link: https://arxiv.org/abs/2307.11833
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+
8
+ from onescience.utils.pinnsformer_util import get_clones
9
+
10
+ class WaveAct(nn.Module):
11
+ def __init__(self):
12
+ super(WaveAct, self).__init__()
13
+ self.w1 = nn.Parameter(torch.ones(1), requires_grad=True)
14
+ self.w2 = nn.Parameter(torch.ones(1), requires_grad=True)
15
+
16
+ def forward(self, x):
17
+ return self.w1 * torch.sin(x)+ self.w2 * torch.cos(x)
18
+
19
+ class FeedForward(nn.Module):
20
+ def __init__(self, d_model, d_ff=256):
21
+ super(FeedForward, self).__init__()
22
+ self.linear = nn.Sequential(*[
23
+ nn.Linear(d_model, d_ff),
24
+ WaveAct(),
25
+ nn.Linear(d_ff, d_ff),
26
+ WaveAct(),
27
+ nn.Linear(d_ff, d_model)
28
+ ])
29
+
30
+ def forward(self, x):
31
+ return self.linear(x)
32
+
33
+
34
+ class EncoderLayer(nn.Module):
35
+ def __init__(self, d_model, heads):
36
+ super(EncoderLayer, self).__init__()
37
+
38
+ self.attn = nn.MultiheadAttention(embed_dim=d_model, num_heads=heads, batch_first=True)
39
+ self.ff = FeedForward(d_model)
40
+ self.act1 = WaveAct()
41
+ self.act2 = WaveAct()
42
+
43
+ def forward(self, x):
44
+ x2 = self.act1(x)
45
+ # pdb.set_trace()
46
+ x = x + self.attn(x2,x2,x2)[0]
47
+ x2 = self.act2(x)
48
+ x = x + self.ff(x2)
49
+ return x
50
+
51
+
52
+ class DecoderLayer(nn.Module):
53
+ def __init__(self, d_model, heads):
54
+ super(DecoderLayer, self).__init__()
55
+
56
+ self.attn = nn.MultiheadAttention(embed_dim=d_model, num_heads=heads, batch_first=True)
57
+ self.ff = FeedForward(d_model)
58
+ self.act1 = WaveAct()
59
+ self.act2 = WaveAct()
60
+
61
+ def forward(self, x, e_outputs):
62
+ x2 = self.act1(x)
63
+ x = x + self.attn(x2, e_outputs, e_outputs)[0]
64
+ x2 = self.act2(x)
65
+ x = x + self.ff(x2)
66
+ return x
67
+
68
+
69
+ class Encoder(nn.Module):
70
+ def __init__(self, d_model, N, heads):
71
+ super(Encoder, self).__init__()
72
+ self.N = N
73
+ self.layers = get_clones(EncoderLayer(d_model, heads), N)
74
+ self.act = WaveAct()
75
+
76
+ def forward(self, x):
77
+ for i in range(self.N):
78
+ x = self.layers[i](x)
79
+ return self.act(x)
80
+
81
+
82
+ class Decoder(nn.Module):
83
+ def __init__(self, d_model, N, heads):
84
+ super(Decoder, self).__init__()
85
+ self.N = N
86
+ self.layers = get_clones(DecoderLayer(d_model, heads), N)
87
+ self.act = WaveAct()
88
+
89
+ def forward(self, x, e_outputs):
90
+ for i in range(self.N):
91
+ x = self.layers[i](x, e_outputs)
92
+ return self.act(x)
93
+
94
+
95
+
96
+ class PINNsformer1D(nn.Module):
97
+ def __init__(self, d_out, d_model, d_hidden, N, heads):
98
+ super(PINNsformer1D, self).__init__()
99
+
100
+ self.linear_emb = nn.Linear(2, d_model)
101
+
102
+ self.encoder = Encoder(d_model, N, heads)
103
+ self.decoder = Decoder(d_model, N, heads)
104
+ self.linear_out = nn.Sequential(*[
105
+ nn.Linear(d_model, d_hidden),
106
+ WaveAct(),
107
+ nn.Linear(d_hidden, d_hidden),
108
+ WaveAct(),
109
+ nn.Linear(d_hidden, d_out)
110
+ ])
111
+
112
+ def forward(self, x, t):
113
+ src = torch.cat((x,t), dim=-1)
114
+ src = self.linear_emb(src)
115
+
116
+ e_outputs = self.encoder(src)
117
+ d_output = self.decoder(src, e_outputs)
118
+ output = self.linear_out(d_output)
119
+ # pdb.set_trace()
120
+ # raise Exception('stop')
121
+ return output
122
+
123
+
124
+ class PINNsformer2D(nn.Module):
125
+ def __init__(self, d_out, d_model, d_hidden, N, heads):
126
+ super(PINNsformer2D, self).__init__()
127
+
128
+ self.linear_emb = nn.Linear(3, d_model)
129
+
130
+ self.encoder = Encoder(d_model, N, heads)
131
+ self.decoder = Decoder(d_model, N, heads)
132
+ self.linear_out = nn.Sequential(*[
133
+ nn.Linear(d_model, d_hidden),
134
+ WaveAct(),
135
+ nn.Linear(d_hidden, d_hidden),
136
+ WaveAct(),
137
+ nn.Linear(d_hidden, d_out)
138
+ ])
139
+
140
+ def forward(self, x, y, t):
141
+ src = torch.cat((x,y,t), dim=-1)
142
+ src = self.linear_emb(src)
143
+ e_outputs = self.encoder(src)
144
+ d_output = self.decoder(src, e_outputs)
145
+ output = self.linear_out(d_output)
146
+ return output
result/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+
scripts/common.py CHANGED
@@ -1,337 +1,100 @@
1
  from __future__ import annotations
2
 
3
- import os
 
4
  import sys
5
- import warnings
6
  from pathlib import Path
7
- from types import SimpleNamespace
8
- from typing import Any, Optional
9
 
10
- import h5py
11
  import numpy as np
12
  import torch
13
- from torch.utils.data import DataLoader, Dataset
14
-
15
 
16
  PROJECT_ROOT = Path(__file__).resolve().parents[1]
17
- DEFAULT_CONFIG = PROJECT_ROOT / "conf" / "config.yaml"
18
-
19
  if str(PROJECT_ROOT) not in sys.path:
20
  sys.path.insert(0, str(PROJECT_ROOT))
21
 
22
- from model.fno import FNO1d, FNO2d, FNO3d, FNO_maxwell
23
-
24
- try:
25
- from onescience.utils.YParams import YParams
26
- except ModuleNotFoundError as exc:
27
- YParams = None
28
- YPARAMS_IMPORT_ERROR = exc
29
-
30
- try:
31
- from onescience.distributed.manager import DistributedManager
32
- except ModuleNotFoundError as exc:
33
- DistributedManager = None
34
- DISTRIBUTED_IMPORT_ERROR = exc
35
-
36
- try:
37
- from onescience.datapipes.cfd.PDENNEval import PDEBenchFNODatapipe
38
- except (ImportError, ModuleNotFoundError, OSError) as exc:
39
- PDEBenchFNODatapipe = None
40
- DATAPIPE_IMPORT_ERROR = exc
41
-
42
-
43
- def get_attr(obj: Any, name: str, default: Any = None) -> Any:
44
- if isinstance(obj, dict):
45
- return obj.get(name, default)
46
- return getattr(obj, name, default)
47
-
48
-
49
- def set_attr(obj: Any, name: str, value: Any) -> None:
50
- if isinstance(obj, dict):
51
- obj[name] = value
52
- else:
53
- setattr(obj, name, value)
54
-
55
-
56
- def to_namespace(value: Any) -> Any:
57
- if isinstance(value, dict):
58
- return SimpleNamespace(**{key: to_namespace(val) for key, val in value.items()})
59
- if isinstance(value, list):
60
- return [to_namespace(item) for item in value]
61
- return value
62
-
63
-
64
- def load_config(config_path: Path | str = DEFAULT_CONFIG) -> Any:
65
- config_path = Path(config_path).expanduser().resolve()
66
- if YParams is not None:
67
- return YParams(str(config_path), "fno_config")
68
-
69
- import yaml
70
-
71
- with config_path.open("r", encoding="utf-8") as handle:
72
- raw = yaml.safe_load(handle)
73
- if not isinstance(raw, dict) or "fno_config" not in raw:
74
- raise ValueError(f"{config_path} must contain root key 'fno_config'")
75
- return to_namespace(raw["fno_config"])
76
-
77
 
78
- def resolve_path(value: Any, root: Path = PROJECT_ROOT) -> Path:
79
- if value is None:
80
- raise ValueError("path value can not be null")
81
- expanded = os.path.expandvars(os.path.expanduser(str(value)))
82
- path = Path(expanded)
83
  if not path.is_absolute():
84
- path = root / path
85
- return path.resolve()
 
86
 
87
 
88
- def prepare_config(
89
- cfg: Any,
90
- data_dir: Optional[str] = None,
91
- output_dir: Optional[str] = None,
92
- checkpoint: Optional[str] = None,
93
- ) -> Any:
94
- source = cfg.datapipe.source
95
- training = cfg.training
96
- inference = get_attr(cfg, "inference", None)
97
 
98
- resolved_data = resolve_path(data_dir or source.data_dir)
99
- set_attr(source, "data_dir", str(resolved_data))
100
 
101
- resolved_output = resolve_path(output_dir or training.output_dir)
102
- set_attr(training, "output_dir", str(resolved_output))
 
 
 
 
103
 
104
- model_path = get_attr(training, "model_path", None)
105
- if model_path:
106
- set_attr(training, "model_path", str(resolve_path(model_path)))
107
 
108
- if inference is not None:
109
- infer_output = get_attr(inference, "output_dir", "./result/output")
110
- infer_checkpoint = checkpoint or get_attr(inference, "checkpoint", None)
111
- set_attr(inference, "output_dir", str(resolve_path(infer_output)))
112
- if infer_checkpoint:
113
- set_attr(inference, "checkpoint", str(resolve_path(infer_checkpoint)))
114
 
115
- return cfg
116
 
 
 
117
 
118
- class SingleProcessManager:
119
- rank = 0
120
- local_rank = 0
121
- world_size = 1
122
- distributed = False
123
- device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
124
 
 
 
 
 
125
 
126
- def initialize_distributed() -> Any:
127
- if DistributedManager is None:
128
- warnings.warn(
129
- f"Falling back to a single-process manager: {DISTRIBUTED_IMPORT_ERROR}",
130
- RuntimeWarning,
131
- )
132
- return SingleProcessManager()
133
- DistributedManager.initialize()
134
- return DistributedManager()
135
 
 
 
136
 
137
- def cleanup_distributed() -> None:
138
- if DistributedManager is not None and DistributedManager.is_initialized():
139
- DistributedManager.cleanup()
 
 
 
 
 
140
 
141
 
142
- class LocalPDEBenchFNODataset(Dataset):
143
- """Small FNO-compatible reader used when optional OneScience graph deps are absent."""
 
 
 
 
 
144
 
145
- def __init__(self, datapipe_cfg: Any, mode: str):
146
- self.mode = mode
147
- self.data_cfg = datapipe_cfg.data
148
- self.source_cfg = datapipe_cfg.source
149
- self.initial_step = int(self.data_cfg.initial_step)
150
- self.reduced_resolution = int(self.data_cfg.reduced_resolution)
151
- self.reduced_resolution_t = int(self.data_cfg.reduced_resolution_t)
152
- self.reduced_batch = int(self.data_cfg.reduced_batch)
153
- self.test_ratio = float(get_attr(self.data_cfg, "test_ratio", 0.1))
154
- self.file_path = Path(self.source_cfg.data_dir) / self.source_cfg.file_name
155
- self._load_single_file()
156
 
157
- def _load_single_file(self) -> None:
158
- if not self.file_path.is_file():
159
- raise FileNotFoundError(f"HDF5 file not found: {self.file_path}")
160
-
161
- with h5py.File(self.file_path, "r") as handle:
162
- if "tensor" not in handle:
163
- raise ValueError("local fallback datapipe requires HDF5 dataset 'tensor'")
164
- tensor = np.asarray(handle["tensor"], dtype=np.float32)
165
- if tensor.ndim == 3:
166
- data = tensor[
167
- :: self.reduced_batch,
168
- :: self.reduced_resolution_t,
169
- :: self.reduced_resolution,
170
- ]
171
- data = np.transpose(data, (0, 2, 1))
172
- self.data = data[:, :, :, None]
173
- x = np.asarray(handle["x-coordinate"], dtype=np.float32)
174
- self.grid = torch.tensor(
175
- x[:: self.reduced_resolution], dtype=torch.float32
176
- ).unsqueeze(-1)
177
- elif tensor.ndim == 4:
178
- data = tensor[
179
- :: self.reduced_batch,
180
- :,
181
- :: self.reduced_resolution,
182
- :: self.reduced_resolution,
183
- ]
184
- data = np.transpose(data, (0, 2, 3, 1))
185
- if "nu" in handle:
186
- nu = np.asarray(handle["nu"], dtype=np.float32)[
187
- :: self.reduced_batch,
188
- :: self.reduced_resolution,
189
- :: self.reduced_resolution,
190
- ]
191
- data = np.concatenate([nu[:, :, :, None], data], axis=-1)
192
- self.data = data[:, :, :, :, None]
193
- x = torch.tensor(np.asarray(handle["x-coordinate"], dtype=np.float32))
194
- y = torch.tensor(np.asarray(handle["y-coordinate"], dtype=np.float32))
195
- xx, yy = torch.meshgrid(x, y, indexing="ij")
196
- self.grid = torch.stack((xx, yy), dim=-1)[
197
- :: self.reduced_resolution, :: self.reduced_resolution
198
- ].float()
199
- else:
200
- raise ValueError(f"unsupported tensor ndim for FNO fallback: {tensor.ndim}")
201
-
202
- sample_count = self.data.shape[0]
203
- val_count = max(1, int(sample_count * self.test_ratio)) if sample_count > 1 else 0
204
- if self.mode == "train":
205
- self.data = self.data[val_count:]
206
- else:
207
- self.data = self.data[:val_count]
208
- self.data = torch.tensor(self.data, dtype=torch.float32)
209
- self.spatial_dim = len(self.data.shape) - 3
210
-
211
- def __len__(self) -> int:
212
- return len(self.data)
213
-
214
- def __getitem__(self, idx: int):
215
- sample = self.data[idx]
216
- return sample[..., : self.initial_step, :], sample, self.grid
217
-
218
-
219
- class LocalPDEBenchFNODatapipe:
220
- def __init__(self, cfg: Any, distributed: bool = False):
221
- self.config = cfg
222
- self.distributed = distributed
223
- self.train_dataset = LocalPDEBenchFNODataset(cfg.datapipe, "train")
224
- self.val_dataset = LocalPDEBenchFNODataset(cfg.datapipe, "val")
225
- self.spatial_dim = self.train_dataset.spatial_dim
226
-
227
- def train_dataloader(self):
228
- loader_args = self.config.datapipe.dataloader
229
- return DataLoader(
230
- self.train_dataset,
231
- batch_size=int(loader_args.batch_size),
232
- num_workers=int(loader_args.num_workers),
233
- pin_memory=bool(loader_args.pin_memory),
234
- shuffle=True,
235
- drop_last=True,
236
- ), None
237
-
238
- def val_dataloader(self):
239
- loader_args = self.config.datapipe.dataloader
240
- return DataLoader(
241
- self.val_dataset,
242
- batch_size=int(loader_args.batch_size),
243
- num_workers=int(loader_args.num_workers),
244
- pin_memory=bool(loader_args.pin_memory),
245
- shuffle=False,
246
- drop_last=False,
247
- ), None
248
-
249
-
250
- def build_datapipe(cfg: Any, distributed: bool = False, force_local: bool = False) -> Any:
251
- if PDEBenchFNODatapipe is not None and not force_local:
252
- return PDEBenchFNODatapipe(cfg, distributed=distributed)
253
- if PDEBenchFNODatapipe is None:
254
- warnings.warn(
255
- f"Using local FNO HDF5 datapipe because OneScience datapipe import failed: "
256
- f"{DATAPIPE_IMPORT_ERROR}",
257
- RuntimeWarning,
258
- )
259
- return LocalPDEBenchFNODatapipe(cfg, distributed=distributed)
260
-
261
-
262
- def build_model(spatial_dim: int, cfg: Any) -> torch.nn.Module:
263
- model_args = cfg.model
264
- data_cfg = cfg.datapipe.data
265
- initial_step = int(data_cfg.initial_step)
266
- pde_name = get_attr(data_cfg, "pde_name", "")
267
- modes = int(model_args.modes)
268
-
269
- if pde_name == "3D_Maxwell":
270
- return FNO_maxwell(
271
- num_channels=int(model_args.num_channels),
272
- width=int(model_args.width),
273
- modes1=modes,
274
- modes2=modes,
275
- modes3=modes,
276
- initial_step=initial_step,
277
- )
278
- if spatial_dim == 1:
279
- return FNO1d(
280
- num_channels=int(model_args.num_channels),
281
- width=int(model_args.width),
282
- modes=modes,
283
- initial_step=initial_step,
284
- )
285
- if spatial_dim == 2:
286
- return FNO2d(
287
- num_channels=int(model_args.num_channels),
288
- width=int(model_args.width),
289
- modes1=modes,
290
- modes2=modes,
291
- initial_step=initial_step,
292
- )
293
- if spatial_dim == 3:
294
- return FNO3d(
295
- num_channels=int(model_args.num_channels),
296
- width=int(model_args.width),
297
- modes1=modes,
298
- modes2=modes,
299
- modes3=modes,
300
- initial_step=initial_step,
301
- )
302
- raise ValueError(f"unsupported spatial dimension: {spatial_dim}")
303
-
304
-
305
- def predict_batch(model: torch.nn.Module, x: torch.Tensor, y: torch.Tensor, grid: torch.Tensor, cfg: Any):
306
- data_cfg = cfg.datapipe.data
307
- train_cfg = cfg.training
308
- initial_step = int(data_cfg.initial_step)
309
- t_train = min(int(train_cfg.t_train), y.shape[-2])
310
- input_shape = list(x.shape)[:-2] + [-1]
311
 
312
- if get_attr(train_cfg, "training_type", "single") == "autoregressive":
313
- pred = y[..., :initial_step, :]
314
- for _ in range(initial_step, t_train):
315
- model_input = x.reshape(input_shape)
316
- model_output = model(model_input, grid)
317
- if model_output.dim() == pred.dim() - 1:
318
- model_output = model_output.unsqueeze(-2)
319
- pred = torch.cat((pred, model_output), dim=-2)
320
- x = torch.cat((x[..., 1:, :], model_output), dim=-2)
321
- return pred, y[..., :t_train, :]
322
 
323
- model_input = x.reshape(input_shape)
324
- target = y[..., t_train - 1 : t_train, :]
325
- pred = model(model_input, grid)
326
- if pred.dim() == target.dim() - 1:
327
- pred = pred.unsqueeze(-2)
328
- return pred, target
329
 
330
 
331
- def load_model_state(path: Path, device: torch.device) -> dict[str, torch.Tensor]:
332
- checkpoint = torch.load(path, map_location=device, weights_only=False)
333
- if isinstance(checkpoint, dict) and "model_state_dict" in checkpoint:
334
- return checkpoint["model_state_dict"]
335
- if isinstance(checkpoint, dict):
336
- return checkpoint
337
- raise ValueError(f"unsupported checkpoint format: {path}")
 
1
  from __future__ import annotations
2
 
3
+ import json
4
+ import random
5
  import sys
 
6
  from pathlib import Path
7
+ from typing import Any
 
8
 
 
9
  import numpy as np
10
  import torch
11
+ import yaml
 
12
 
13
  PROJECT_ROOT = Path(__file__).resolve().parents[1]
 
 
14
  if str(PROJECT_ROOT) not in sys.path:
15
  sys.path.insert(0, str(PROJECT_ROOT))
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
+ def load_config(config_path: str | Path | None = None) -> dict[str, Any]:
19
+ path = Path(config_path) if config_path else PROJECT_ROOT / "conf" / "config.yaml"
 
 
 
20
  if not path.is_absolute():
21
+ path = PROJECT_ROOT / path
22
+ with path.open("r", encoding="utf-8") as f:
23
+ return yaml.safe_load(f)
24
 
25
 
26
+ def project_path(path: str | Path) -> Path:
27
+ path = Path(path)
28
+ return path if path.is_absolute() else PROJECT_ROOT / path
 
 
 
 
 
 
29
 
 
 
30
 
31
+ def seed_everything(seed: int) -> None:
32
+ random.seed(seed)
33
+ np.random.seed(seed)
34
+ torch.manual_seed(seed)
35
+ if torch.cuda.is_available():
36
+ torch.cuda.manual_seed_all(seed)
37
 
 
 
 
38
 
39
+ def select_device(value: str = "auto") -> torch.device:
40
+ if value == "auto":
41
+ return torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
42
+ return torch.device(value)
 
 
43
 
 
44
 
45
+ def ensure_parent(path: Path) -> None:
46
+ path.parent.mkdir(parents=True, exist_ok=True)
47
 
 
 
 
 
 
 
48
 
49
+ def ensure_runtime_dirs(cfg: dict[str, Any]) -> None:
50
+ for key in ("fake_data", "prediction", "metrics", "loss", "figure"):
51
+ ensure_parent(project_path(cfg["paths"][key]))
52
+ ensure_parent(project_path(cfg["training"]["checkpoint"]))
53
 
 
 
 
 
 
 
 
 
 
54
 
55
+ def build_model(cfg: dict[str, Any]) -> torch.nn.Module:
56
+ from model import PINNsformer1D
57
 
58
+ model_cfg = cfg["model"]
59
+ return PINNsformer1D(
60
+ d_out=int(model_cfg["d_out"]),
61
+ d_hidden=int(model_cfg["d_hidden"]),
62
+ d_model=int(model_cfg["d_model"]),
63
+ N=int(model_cfg["num_layers"]),
64
+ heads=int(model_cfg["heads"]),
65
+ )
66
 
67
 
68
+ def initial_condition(x: np.ndarray | torch.Tensor, cfg: dict[str, Any]):
69
+ initial = cfg["equation"]["initial"]
70
+ center = initial["center"]
71
+ sigma = initial["sigma"]
72
+ if isinstance(x, torch.Tensor):
73
+ return torch.exp(-((x - center) ** 2) / (2 * sigma**2))
74
+ return np.exp(-((x - center) ** 2) / (2 * sigma**2))
75
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
+ def exact_reaction_solution(
78
+ x: np.ndarray | torch.Tensor,
79
+ t: np.ndarray | torch.Tensor,
80
+ cfg: dict[str, Any],
81
+ ):
82
+ h = initial_condition(x, cfg)
83
+ rate = cfg["equation"]["reaction_rate"]
84
+ if isinstance(h, torch.Tensor):
85
+ exp_term = torch.exp(rate * t)
86
+ else:
87
+ exp_term = np.exp(rate * t)
88
+ return h * exp_term / (h * exp_term + 1 - h)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
 
 
 
 
 
 
 
 
 
 
90
 
91
+ def relative_errors(pred: np.ndarray, target: np.ndarray) -> dict[str, float]:
92
+ return {
93
+ "relative_l1": float(np.sum(np.abs(target - pred)) / np.sum(np.abs(target))),
94
+ "relative_l2": float(np.sqrt(np.sum((target - pred) ** 2) / np.sum(target**2))),
95
+ }
 
96
 
97
 
98
+ def write_json(path: Path, payload: dict[str, Any]) -> None:
99
+ ensure_parent(path)
100
+ path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
 
 
 
 
scripts/fake_data.py CHANGED
@@ -1,59 +1,31 @@
1
  from __future__ import annotations
2
 
3
  import argparse
4
- from pathlib import Path
5
 
6
- import h5py
7
  import numpy as np
8
 
9
- from common import DEFAULT_CONFIG, load_config, prepare_config
10
-
11
-
12
- def generate_darcy_file(path: Path, samples: int, grid_size: int, seed: int, overwrite: bool) -> None:
13
- if path.exists() and not overwrite:
14
- print(f"Fake data already exists: {path}")
15
- return
16
-
17
- path.parent.mkdir(parents=True, exist_ok=True)
18
- rng = np.random.default_rng(seed)
19
- x = np.linspace(0.0, 1.0, grid_size, dtype=np.float32)
20
- y = np.linspace(0.0, 1.0, grid_size, dtype=np.float32)
21
- xx, yy = np.meshgrid(x, y, indexing="ij")
22
-
23
- nu = rng.uniform(0.05, 0.25, size=(samples, grid_size, grid_size)).astype(np.float32)
24
- tensor = np.empty((samples, 2, grid_size, grid_size), dtype=np.float32)
25
- base = np.sin(np.pi * xx) * np.sin(np.pi * yy)
26
- for idx in range(samples):
27
- tensor[idx, 0] = base + 0.05 * rng.standard_normal(base.shape)
28
- tensor[idx, 1] = tensor[idx, 0] * (1.0 + nu[idx]) + 0.01 * rng.standard_normal(base.shape)
29
-
30
- with h5py.File(path, "w") as handle:
31
- handle.create_dataset("tensor", data=tensor)
32
- handle.create_dataset("nu", data=nu)
33
- handle.create_dataset("x-coordinate", data=x)
34
- handle.create_dataset("y-coordinate", data=y)
35
- handle.create_dataset("t-coordinate", data=np.array([0.0, 1.0], dtype=np.float32))
36
-
37
- print(f"Fake Darcy HDF5 written: {path}")
38
- print(f"tensor={tensor.shape}, nu={nu.shape}")
39
-
40
-
41
- def main() -> int:
42
- parser = argparse.ArgumentParser(description="Generate a small PDENNEval FNO fake dataset.")
43
- parser.add_argument("--config", default=str(DEFAULT_CONFIG), help="Path to conf/config.yaml")
44
- parser.add_argument("--data-dir", default=None, help="Override datapipe.source.data_dir")
45
- parser.add_argument("--samples", type=int, default=20)
46
- parser.add_argument("--grid-size", type=int, default=16)
47
- parser.add_argument("--seed", type=int, default=0)
48
- parser.add_argument("--overwrite", action="store_true")
49
  args = parser.parse_args()
50
 
51
- cfg = prepare_config(load_config(args.config), data_dir=args.data_dir)
52
- data_dir = Path(cfg.datapipe.source.data_dir)
53
- file_path = data_dir / cfg.datapipe.source.file_name
54
- generate_darcy_file(file_path, args.samples, args.grid_size, args.seed, args.overwrite)
55
- return 0
 
 
 
 
 
 
 
 
56
 
57
 
58
  if __name__ == "__main__":
59
- raise SystemExit(main())
 
1
  from __future__ import annotations
2
 
3
  import argparse
 
4
 
 
5
  import numpy as np
6
 
7
+ from common import exact_reaction_solution, load_config, project_path
8
+
9
+
10
+ def main() -> None:
11
+ parser = argparse.ArgumentParser(description="Generate fake PINNsformer reaction data.")
12
+ parser.add_argument("--config", default=None, help="Path to config.yaml.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  args = parser.parse_args()
14
 
15
+ cfg = load_config(args.config)
16
+ data_cfg = cfg["data"]
17
+ x = np.linspace(*data_cfg["x_range"], int(data_cfg["x_num"]), dtype=np.float32)
18
+ t = np.linspace(*data_cfg["t_range"], int(data_cfg["t_num"]), dtype=np.float32)
19
+ x_mesh, t_mesh = np.meshgrid(x, t)
20
+ u = exact_reaction_solution(x_mesh, t_mesh, cfg).astype(np.float32)
21
+
22
+ output_path = project_path(cfg["paths"]["fake_data"])
23
+ output_path.parent.mkdir(parents=True, exist_ok=True)
24
+ np.savez(output_path, x=x, t=t, u=u)
25
+
26
+ print(f"fake data saved to {output_path}")
27
+ print(f"x={x.shape}, t={t.shape}, u={u.shape}")
28
 
29
 
30
  if __name__ == "__main__":
31
+ main()
scripts/inference.py CHANGED
@@ -1,83 +1,85 @@
1
  from __future__ import annotations
2
 
3
  import argparse
4
- from pathlib import Path
5
 
6
  import numpy as np
7
  import torch
8
 
 
 
9
  from common import (
10
- DEFAULT_CONFIG,
11
- build_datapipe,
12
  build_model,
13
- cleanup_distributed,
14
- get_attr,
15
- initialize_distributed,
16
  load_config,
17
- load_model_state,
18
- predict_batch,
19
- prepare_config,
20
  )
21
 
22
 
23
- @torch.no_grad()
24
- def main() -> int:
25
- parser = argparse.ArgumentParser(description="Run PDENNEval FNO inference.")
26
- parser.add_argument("--config", default=str(DEFAULT_CONFIG), help="Path to conf/config.yaml")
27
- parser.add_argument("--data-dir", default=None, help="Override datapipe.source.data_dir")
28
- parser.add_argument("--checkpoint", default=None, help="Checkpoint path")
29
- parser.add_argument("--output-dir", default=None, help="Directory for npz predictions")
30
- parser.add_argument("--max-batches", type=int, default=None)
31
- parser.add_argument("--force-local-datapipe", action="store_true")
32
  args = parser.parse_args()
33
 
34
- dist = initialize_distributed()
35
- device = dist.device
36
- cfg = prepare_config(load_config(args.config), data_dir=args.data_dir, checkpoint=args.checkpoint)
37
- if args.output_dir:
38
- cfg.inference.output_dir = str(Path(args.output_dir).expanduser().resolve())
39
-
40
- checkpoint = Path(args.checkpoint or cfg.inference.checkpoint)
41
- if not checkpoint.is_file():
42
- raise FileNotFoundError(f"checkpoint not found: {checkpoint}")
43
 
44
- datapipe = build_datapipe(
45
- cfg,
46
- distributed=False,
47
- force_local=args.force_local_datapipe,
48
- )
49
- val_loader, _ = datapipe.val_dataloader()
50
- model = build_model(datapipe.spatial_dim, cfg).to(device)
51
- model.load_state_dict(load_model_state(checkpoint, device))
52
  model.eval()
53
 
54
- output_dir = Path(args.output_dir or cfg.inference.output_dir)
55
- output_dir.mkdir(parents=True, exist_ok=True)
56
- max_batches = args.max_batches or int(get_attr(cfg.inference, "max_batches", 1))
 
 
 
 
 
 
 
 
 
 
 
57
 
58
- written = 0
59
- for batch_idx, (x, y, grid) in enumerate(val_loader):
60
- if batch_idx >= max_batches:
61
- break
62
- x = x.to(device)
63
- y = y.to(device)
64
- grid = grid.to(device)
65
- pred, target = predict_batch(model, x, y, grid, cfg)
66
- path = output_dir / f"batch_{batch_idx:04d}.npz"
67
- np.savez_compressed(
68
- path,
69
- prediction=pred.detach().cpu().numpy(),
70
- target=target.detach().cpu().numpy(),
71
  )
72
- print(f"wrote {path}")
73
- written += 1
74
 
75
- if written == 0:
76
- raise RuntimeError("no validation batches were available for inference")
 
 
77
 
78
- cleanup_distributed()
79
- return 0
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
 
82
  if __name__ == "__main__":
83
- raise SystemExit(main())
 
1
  from __future__ import annotations
2
 
3
  import argparse
 
4
 
5
  import numpy as np
6
  import torch
7
 
8
+ from onescience.utils.pinnsformer_util import get_data, make_time_sequence
9
+
10
  from common import (
 
 
11
  build_model,
12
+ exact_reaction_solution,
 
 
13
  load_config,
14
+ project_path,
15
+ relative_errors,
16
+ select_device,
17
  )
18
 
19
 
20
+ def main() -> None:
21
+ parser = argparse.ArgumentParser(description="Run PINNsformer inference for the 1D reaction equation.")
22
+ parser.add_argument("--config", default=None, help="Path to config.yaml.")
23
+ parser.add_argument("--checkpoint", default=None, help="Path to a trained checkpoint.")
24
+ parser.add_argument("--device", default=None, help="Override runtime.device.")
 
 
 
 
25
  args = parser.parse_args()
26
 
27
+ cfg = load_config(args.config)
28
+ device = select_device(args.device or cfg["runtime"]["device"])
29
+ checkpoint = project_path(args.checkpoint or cfg["training"]["checkpoint"])
30
+ if not checkpoint.exists():
31
+ raise FileNotFoundError(f"Checkpoint not found: {checkpoint}. Run scripts/train.py first.")
 
 
 
 
32
 
33
+ model = build_model(cfg).to(device)
34
+ try:
35
+ state = torch.load(checkpoint, map_location=device, weights_only=True)
36
+ except TypeError:
37
+ state = torch.load(checkpoint, map_location=device)
38
+ model.load_state_dict(state.get("model_state_dict", state))
 
 
39
  model.eval()
40
 
41
+ data_cfg = cfg["data"]
42
+ res, _, _, _, _ = get_data(
43
+ data_cfg["x_range"],
44
+ data_cfg["t_range"],
45
+ int(data_cfg["x_num"]),
46
+ int(data_cfg["t_num"]),
47
+ )
48
+ seq = make_time_sequence(
49
+ res,
50
+ num_step=int(data_cfg["sequence"]["num_step"]),
51
+ step=float(data_cfg["sequence"]["step"]),
52
+ )
53
+ res_tensor = torch.tensor(seq, dtype=torch.float32, requires_grad=False, device=device)
54
+ x_test, t_test = res_tensor[:, :, 0:1], res_tensor[:, :, 1:2]
55
 
56
+ with torch.no_grad():
57
+ pred = model(x_test, t_test)[:, 0:1].detach().cpu().numpy().reshape(
58
+ int(data_cfg["t_num"]),
59
+ int(data_cfg["x_num"]),
 
 
 
 
 
 
 
 
 
60
  )
 
 
61
 
62
+ x = res[:, 0].reshape(int(data_cfg["t_num"]), int(data_cfg["x_num"]))
63
+ t = res[:, 1].reshape(int(data_cfg["t_num"]), int(data_cfg["x_num"]))
64
+ target = exact_reaction_solution(x, t, cfg).astype(np.float32)
65
+ metrics = relative_errors(pred, target)
66
 
67
+ output_path = project_path(cfg["paths"]["prediction"])
68
+ output_path.parent.mkdir(parents=True, exist_ok=True)
69
+ np.savez(
70
+ output_path,
71
+ x=x.astype(np.float32),
72
+ t=t.astype(np.float32),
73
+ prediction=pred.astype(np.float32),
74
+ target=target,
75
+ absolute_error=np.abs(pred - target).astype(np.float32),
76
+ relative_l1=metrics["relative_l1"],
77
+ relative_l2=metrics["relative_l2"],
78
+ )
79
+ print(f"prediction saved to {output_path}")
80
+ print(f"relative L1 error: {metrics['relative_l1']:.6f}")
81
+ print(f"relative L2 error: {metrics['relative_l2']:.6f}")
82
 
83
 
84
  if __name__ == "__main__":
85
+ main()
scripts/result.py CHANGED
@@ -1,50 +1,71 @@
1
  from __future__ import annotations
2
 
3
  import argparse
4
- import json
5
- from pathlib import Path
6
 
7
  import numpy as np
8
 
9
- from common import DEFAULT_CONFIG, load_config, prepare_config
10
 
11
 
12
- def main() -> int:
13
- parser = argparse.ArgumentParser(description="Summarize PDENNEval FNO inference outputs.")
14
- parser.add_argument("--config", default=str(DEFAULT_CONFIG), help="Path to conf/config.yaml")
15
- parser.add_argument("--prediction-dir", default=None)
16
- parser.add_argument("--output-dir", default=None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  args = parser.parse_args()
18
 
19
- cfg = prepare_config(load_config(args.config))
20
- prediction_dir = Path(args.prediction_dir or cfg.result.prediction_dir).expanduser().resolve()
21
- output_dir = Path(args.output_dir or cfg.result.output_dir).expanduser().resolve()
22
- files = sorted(prediction_dir.glob("*.npz"))
23
- if not files:
24
- raise FileNotFoundError(f"no prediction files found in {prediction_dir}")
25
-
26
- mse_values = []
27
- mae_values = []
28
- for path in files:
29
- data = np.load(path)
30
- pred = data["prediction"]
31
- target = data["target"]
32
- diff = pred - target
33
- mse_values.append(float(np.mean(diff ** 2)))
34
- mae_values.append(float(np.mean(np.abs(diff))))
35
-
36
- metrics = {
37
- "num_files": len(files),
38
- "mse": float(np.mean(mse_values)),
39
- "mae": float(np.mean(mae_values)),
40
- }
41
- output_dir.mkdir(parents=True, exist_ok=True)
42
- metrics_path = output_dir / "metrics.json"
43
- metrics_path.write_text(json.dumps(metrics, indent=2), encoding="utf-8")
44
- print(json.dumps(metrics, indent=2))
45
- print(f"wrote {metrics_path}")
46
- return 0
47
 
48
 
49
  if __name__ == "__main__":
50
- raise SystemExit(main())
 
1
  from __future__ import annotations
2
 
3
  import argparse
 
 
4
 
5
  import numpy as np
6
 
7
+ from common import load_config, project_path, relative_errors, write_json
8
 
9
 
10
+ def plot_result(prediction: np.ndarray, target: np.ndarray, cfg: dict, figure_path) -> bool:
11
+ try:
12
+ import matplotlib
13
+
14
+ matplotlib.use("Agg")
15
+ import matplotlib.pyplot as plt
16
+ except Exception as exc:
17
+ print(f"matplotlib unavailable, skip figure: {exc}")
18
+ return False
19
+
20
+ figure_path.parent.mkdir(parents=True, exist_ok=True)
21
+ fig, axes = plt.subplots(1, 3, figsize=(12, 4))
22
+ x_min, x_max = cfg["data"]["x_range"]
23
+ t_min, t_max = cfg["data"]["t_range"]
24
+ items = [
25
+ (prediction, "Predicted u(x,t)"),
26
+ (target, "Exact u(x,t)"),
27
+ (np.abs(prediction - target), "Absolute Error"),
28
+ ]
29
+ for ax, (data, title) in zip(axes, items):
30
+ image = ax.imshow(data, extent=[x_min, x_max, t_max, t_min], aspect="auto")
31
+ ax.set_xlabel("x")
32
+ ax.set_ylabel("t")
33
+ ax.set_title(title)
34
+ fig.colorbar(image, ax=ax)
35
+
36
+ plt.tight_layout()
37
+ plt.savefig(figure_path)
38
+ plt.close(fig)
39
+ return True
40
+
41
+
42
+ def main() -> None:
43
+ parser = argparse.ArgumentParser(description="Evaluate and plot PINNsformer inference output.")
44
+ parser.add_argument("--config", default=None, help="Path to config.yaml.")
45
+ parser.add_argument("--prediction", default=None, help="Path to prediction .npz.")
46
  args = parser.parse_args()
47
 
48
+ cfg = load_config(args.config)
49
+ prediction_path = project_path(args.prediction or cfg["paths"]["prediction"])
50
+ if not prediction_path.exists():
51
+ raise FileNotFoundError(f"Prediction file not found: {prediction_path}. Run scripts/inference.py first.")
52
+
53
+ data = np.load(prediction_path)
54
+ prediction = data["prediction"]
55
+ target = data["target"]
56
+ metrics = relative_errors(prediction, target)
57
+
58
+ metrics_path = project_path(cfg["paths"]["metrics"])
59
+ write_json(metrics_path, metrics)
60
+ figure_path = project_path(cfg["paths"]["figure"])
61
+ plotted = plot_result(prediction, target, cfg, figure_path)
62
+
63
+ print(f"metrics saved to {metrics_path}")
64
+ if plotted:
65
+ print(f"figure saved to {figure_path}")
66
+ print(f"relative L1 error: {metrics['relative_l1']:.6f}")
67
+ print(f"relative L2 error: {metrics['relative_l2']:.6f}")
 
 
 
 
 
 
 
 
68
 
69
 
70
  if __name__ == "__main__":
71
+ main()
scripts/train.py CHANGED
@@ -6,138 +6,167 @@ from pathlib import Path
6
  import numpy as np
7
  import torch
8
  import torch.nn as nn
9
- from torch.nn.parallel import DistributedDataParallel as DDP
 
 
10
 
11
  from common import (
12
- DEFAULT_CONFIG,
13
- build_datapipe,
14
  build_model,
15
- cleanup_distributed,
16
- get_attr,
17
- initialize_distributed,
18
  load_config,
19
- load_model_state,
20
- predict_batch,
21
- prepare_config,
22
  )
23
 
24
 
25
- def train_one_epoch(model, loader, optimizer, loss_fn, device, cfg) -> float:
26
- model.train()
27
- losses = []
28
- for x, y, grid in loader:
29
- x = x.to(device)
30
- y = y.to(device)
31
- grid = grid.to(device)
32
- pred, target = predict_batch(model, x, y, grid, cfg)
33
- if pred.numel() == 0:
34
- continue
35
- loss = loss_fn(pred.reshape(pred.size(0), -1), target.reshape(target.size(0), -1))
36
- optimizer.zero_grad()
37
- loss.backward()
38
- optimizer.step()
39
- losses.append(float(loss.item()))
40
- return float(np.mean(losses)) if losses else 0.0
41
-
42
-
43
- @torch.no_grad()
44
- def validate(model, loader, loss_fn, device, cfg) -> float:
45
- model.eval()
46
- losses = []
47
- for x, y, grid in loader:
48
- x = x.to(device)
49
- y = y.to(device)
50
- grid = grid.to(device)
51
- pred, target = predict_batch(model, x, y, grid, cfg)
52
- if pred.numel() == 0:
53
- continue
54
- loss = loss_fn(pred.reshape(pred.size(0), -1), target.reshape(target.size(0), -1))
55
- losses.append(float(loss.item()))
56
- return float(np.mean(losses)) if losses else 0.0
57
-
58
-
59
- def main() -> int:
60
- parser = argparse.ArgumentParser(description="Train the standard PDENNEval FNO package.")
61
- parser.add_argument("--config", default=str(DEFAULT_CONFIG), help="Path to conf/config.yaml")
62
- parser.add_argument("--data-dir", default=None, help="Override datapipe.source.data_dir")
63
- parser.add_argument("--output-dir", default=None, help="Override training.output_dir")
64
- parser.add_argument("--force-local-datapipe", action="store_true")
65
- args = parser.parse_args()
66
 
67
- dist = initialize_distributed()
68
- device = dist.device
69
- cfg = prepare_config(load_config(args.config), data_dir=args.data_dir, output_dir=args.output_dir)
70
 
71
- seed = int(get_attr(cfg.training, "seed", 0))
72
- torch.manual_seed(seed + dist.rank)
73
- np.random.seed(seed + dist.rank)
74
 
75
- output_dir = Path(cfg.training.output_dir)
76
- if dist.rank == 0:
77
- output_dir.mkdir(parents=True, exist_ok=True)
78
- print(f"Output directory: {output_dir}")
79
- print(f"Device: {device}, world_size={dist.world_size}")
80
 
81
- datapipe = build_datapipe(
82
- cfg,
83
- distributed=(dist.world_size > 1),
84
- force_local=args.force_local_datapipe,
85
- )
86
- train_loader, train_sampler = datapipe.train_dataloader()
87
- val_loader, _ = datapipe.val_dataloader()
88
-
89
- model = build_model(datapipe.spatial_dim, cfg).to(device)
90
- if dist.world_size > 1:
91
- model = DDP(model, device_ids=[dist.local_rank], output_device=dist.local_rank)
92
-
93
- optimizer_cfg = cfg.training.optimizer
94
- scheduler_cfg = cfg.training.scheduler
95
- optimizer = getattr(torch.optim, optimizer_cfg.name)(
96
- model.parameters(),
97
- lr=float(optimizer_cfg.lr),
98
- weight_decay=float(optimizer_cfg.weight_decay),
99
  )
100
- scheduler = getattr(torch.optim.lr_scheduler, scheduler_cfg.name)(
101
- optimizer,
102
- step_size=int(scheduler_cfg.step_size),
103
- gamma=float(scheduler_cfg.gamma),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  )
105
- loss_fn = nn.MSELoss()
106
 
107
- if bool(get_attr(cfg.training, "continue_training", False)) and cfg.training.model_path:
108
- model_to_load = model.module if hasattr(model, "module") else model
109
- model_to_load.load_state_dict(load_model_state(Path(cfg.training.model_path), device))
110
 
111
- best_val = float("inf")
112
- epochs = int(cfg.training.epochs)
113
- save_period = max(1, int(cfg.training.save_period))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
 
115
  for epoch in range(epochs):
116
- if train_sampler is not None:
117
- train_sampler.set_epoch(epoch)
118
- train_loss = train_one_epoch(model, train_loader, optimizer, loss_fn, device, cfg)
119
- scheduler.step()
120
- val_loss = validate(model, val_loader, loss_fn, device, cfg)
121
-
122
- if dist.rank == 0:
123
- print(f"epoch={epoch} train_loss={train_loss:.6e} val_loss={val_loss:.6e}")
124
- model_to_save = model.module if hasattr(model, "module") else model
125
- state = {
126
- "epoch": epoch,
127
- "model_state_dict": model_to_save.state_dict(),
128
- "optimizer_state_dict": optimizer.state_dict(),
129
- "loss": val_loss,
130
- }
131
- torch.save(state, output_dir / "latest_model.pt")
132
- if (epoch + 1) % save_period == 0:
133
- torch.save(state, output_dir / f"model_epoch_{epoch}.pt")
134
- if val_loss <= best_val:
135
- best_val = val_loss
136
- torch.save(state, output_dir / "best_model.pt")
137
-
138
- cleanup_distributed()
139
- return 0
 
 
 
 
 
 
 
140
 
141
 
142
  if __name__ == "__main__":
143
- raise SystemExit(main())
 
6
  import numpy as np
7
  import torch
8
  import torch.nn as nn
9
+ from torch.optim import Adam, LBFGS
10
+
11
+ from onescience.utils.pinnsformer_util import get_data, get_n_params, make_time_sequence
12
 
13
  from common import (
 
 
14
  build_model,
15
+ ensure_runtime_dirs,
16
+ initial_condition,
 
17
  load_config,
18
+ project_path,
19
+ seed_everything,
20
+ select_device,
21
  )
22
 
23
 
24
+ def init_weights(module: nn.Module) -> None:
25
+ if isinstance(module, nn.Linear):
26
+ torch.nn.init.xavier_uniform_(module.weight)
27
+ module.bias.data.fill_(0.01)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
 
 
 
29
 
30
+ def tensorize(array: np.ndarray, device: torch.device) -> torch.Tensor:
31
+ return torch.tensor(array, dtype=torch.float32, requires_grad=True, device=device)
 
32
 
 
 
 
 
 
33
 
34
+ def prepare_tensors(cfg: dict, device: torch.device, args: argparse.Namespace):
35
+ data_cfg = cfg["data"]
36
+ x_num = int(args.x_num or data_cfg["x_num"])
37
+ t_num = int(args.t_num or data_cfg["t_num"])
38
+ num_step = int(args.num_step or data_cfg["sequence"]["num_step"])
39
+ step = float(data_cfg["sequence"]["step"])
40
+
41
+ res, b_left, b_right, b_upper, b_lower = get_data(
42
+ data_cfg["x_range"],
43
+ data_cfg["t_range"],
44
+ x_num,
45
+ t_num,
 
 
 
 
 
 
46
  )
47
+
48
+ tensors = []
49
+ for values in (res, b_left, b_right, b_upper, b_lower):
50
+ tensors.append(tensorize(make_time_sequence(values, num_step=num_step, step=step), device))
51
+
52
+ return tuple(tensors)
53
+
54
+
55
+ def loss_components(model: nn.Module, tensors: tuple[torch.Tensor, ...], cfg: dict):
56
+ res, b_left, b_right, b_upper, b_lower = tensors
57
+ x_res, t_res = res[:, :, 0:1], res[:, :, 1:2]
58
+ x_left, t_left = b_left[:, :, 0:1], b_left[:, :, 1:2]
59
+ x_upper, t_upper = b_upper[:, :, 0:1], b_upper[:, :, 1:2]
60
+ x_lower, t_lower = b_lower[:, :, 0:1], b_lower[:, :, 1:2]
61
+
62
+ pred_res = model(x_res, t_res)
63
+ pred_left = model(x_left, t_left)
64
+ pred_upper = model(x_upper, t_upper)
65
+ pred_lower = model(x_lower, t_lower)
66
+
67
+ u_t = torch.autograd.grad(
68
+ pred_res,
69
+ t_res,
70
+ grad_outputs=torch.ones_like(pred_res),
71
+ retain_graph=True,
72
+ create_graph=True,
73
+ )[0]
74
+
75
+ rate = float(cfg["equation"]["reaction_rate"])
76
+ target_ic = initial_condition(x_left[:, 0, :], cfg)
77
+ loss_res = torch.mean((u_t - rate * pred_res * (1 - pred_res)) ** 2)
78
+ loss_bc = torch.mean((pred_upper - pred_lower) ** 2)
79
+ loss_ic = torch.mean((pred_left[:, 0, :] - target_ic) ** 2)
80
+ loss = loss_res + loss_bc + loss_ic
81
+ return loss, (loss_res, loss_bc, loss_ic)
82
+
83
+
84
+ def build_optimizer(model: nn.Module, cfg: dict):
85
+ opt_cfg = cfg["training"]["optimizer"]
86
+ name = opt_cfg["name"].lower()
87
+ if name == "adam":
88
+ return Adam(model.parameters(), lr=float(opt_cfg.get("lr", 1e-3)))
89
+ if name == "lbfgs":
90
+ return LBFGS(
91
+ model.parameters(),
92
+ lr=float(opt_cfg.get("lr", 1.0)),
93
+ max_iter=int(opt_cfg.get("max_iter", 20)),
94
+ line_search_fn=opt_cfg.get("line_search_fn", "strong_wolfe"),
95
+ )
96
+ raise ValueError(f"Unsupported optimizer: {opt_cfg['name']}")
97
+
98
+
99
+ def save_checkpoint(path: Path, model: nn.Module, cfg: dict, loss_history: list[list[float]]) -> None:
100
+ path.parent.mkdir(parents=True, exist_ok=True)
101
+ torch.save(
102
+ {
103
+ "model_state_dict": model.state_dict(),
104
+ "config": cfg,
105
+ "loss_history": loss_history,
106
+ },
107
+ path,
108
  )
 
109
 
 
 
 
110
 
111
+ def main() -> None:
112
+ parser = argparse.ArgumentParser(description="Train PINNsformer on the 1D reaction equation.")
113
+ parser.add_argument("--config", default=None, help="Path to config.yaml.")
114
+ parser.add_argument("--epochs", type=int, default=None, help="Override training epochs.")
115
+ parser.add_argument("--x-num", type=int, default=None, help="Override x grid count.")
116
+ parser.add_argument("--t-num", type=int, default=None, help="Override t grid count.")
117
+ parser.add_argument("--num-step", type=int, default=None, help="Override pseudo-sequence length.")
118
+ parser.add_argument("--device", default=None, help="Override runtime.device.")
119
+ args = parser.parse_args()
120
+
121
+ cfg = load_config(args.config)
122
+ ensure_runtime_dirs(cfg)
123
+ seed_everything(int(cfg["runtime"]["seed"]))
124
+ device = select_device(args.device or cfg["runtime"]["device"])
125
+
126
+ tensors = prepare_tensors(cfg, device, args)
127
+ model = build_model(cfg).to(device)
128
+ model.apply(init_weights)
129
+ optimizer = build_optimizer(model, cfg)
130
+ epochs = int(args.epochs or cfg["training"]["epochs"])
131
+
132
+ print(model)
133
+ print(f"parameters: {get_n_params(model)}")
134
+ print(f"device: {device}")
135
 
136
+ loss_history: list[list[float]] = []
137
  for epoch in range(epochs):
138
+ latest: dict[str, float] = {}
139
+
140
+ def closure():
141
+ loss, parts = loss_components(model, tensors, cfg)
142
+ optimizer.zero_grad()
143
+ loss.backward()
144
+ latest["loss"] = float(loss.detach().cpu())
145
+ latest["loss_res"] = float(parts[0].detach().cpu())
146
+ latest["loss_bc"] = float(parts[1].detach().cpu())
147
+ latest["loss_ic"] = float(parts[2].detach().cpu())
148
+ return loss
149
+
150
+ if isinstance(optimizer, LBFGS):
151
+ optimizer.step(closure)
152
+ else:
153
+ closure()
154
+ optimizer.step()
155
+
156
+ loss_history.append([latest["loss_res"], latest["loss_bc"], latest["loss_ic"], latest["loss"]])
157
+ print(
158
+ f"epoch {epoch + 1}/{epochs} "
159
+ f"loss={latest['loss']:.6f} "
160
+ f"res={latest['loss_res']:.6f} "
161
+ f"bc={latest['loss_bc']:.6f} "
162
+ f"ic={latest['loss_ic']:.6f}"
163
+ )
164
+
165
+ checkpoint = project_path(cfg["training"]["checkpoint"])
166
+ save_checkpoint(checkpoint, model, cfg, loss_history)
167
+ np.save(project_path(cfg["paths"]["loss"]), np.asarray(loss_history, dtype=np.float32))
168
+ print(f"checkpoint saved to {checkpoint}")
169
 
170
 
171
  if __name__ == "__main__":
172
+ main()