Upload folder using huggingface_hub
Browse files- .gitattributes +7 -0
- README.md +86 -0
- config.json +12 -0
- config/config.yaml +62 -0
- model/QCPINN.py +53 -0
- model/__init__.py +1 -0
- requirements.txt +10 -0
- results/best_checkpoint_selection.json +30 -0
- results/best_model/figures/fields_t1.png +3 -0
- results/best_model/figures/fields_t10.png +3 -0
- results/best_model/figures/fields_t5.png +3 -0
- results/best_model/figures/streamlines_t1.png +3 -0
- results/best_model/figures/streamlines_t10.png +3 -0
- results/best_model/figures/streamlines_t5.png +3 -0
- results/best_model/figures/training_curves.png +3 -0
- results/best_model/metrics.json +168 -0
- results/best_model/report.md +43 -0
- results/compute_hardware.txt +25 -0
- results/seed_6/history.csv +0 -0
- results/table3_comparison.json +179 -0
- scripts/common.py +198 -0
- scripts/inference.py +98 -0
- scripts/result.py +188 -0
- scripts/run_all.slurm +30 -0
- scripts/train.py +153 -0
- weight/best_model.pt +3 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,10 @@ saved_model/**/* 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 36 |
+
results/best_model/figures/fields_t1.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
results/best_model/figures/fields_t10.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
results/best_model/figures/fields_t5.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
results/best_model/figures/streamlines_t1.png filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
results/best_model/figures/streamlines_t10.png filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
results/best_model/figures/streamlines_t5.png filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
results/best_model/figures/training_curves.png filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: other
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
tags:
|
| 6 |
+
- OneScience
|
| 7 |
+
- cavity-flow
|
| 8 |
+
- physics-informed-neural-network
|
| 9 |
+
- quantum-machine-learning
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# QCPINN-CavityFlow
|
| 13 |
+
|
| 14 |
+
## Model Introduction
|
| 15 |
+
|
| 16 |
+
This package reproduces the unsteady lid-driven cavity experiment in [QCPINN: Quantum-Classical Physics-Informed Neural Networks for Solving PDEs](https://arxiv.org/abs/2503.16678), Table 3. The Angle-Cascade quantum-classical PINN maps `(t, x, y)` to `(u, v, p)`. Sobol-sampled PDE and boundary points drive training; the author's reference field is used for validation and evaluation, not gradient updates.
|
| 17 |
+
|
| 18 |
+
Ten independent runs of 20,000 epochs were completed on CPU quantum simulation. The included `weight/best_model.pt` is the validation-selected checkpoint from seed 6, epoch 1, not a claim of paper-level accuracy. Only the reference-data path in its checkpoint metadata was changed for portability; all nine parameter tensors are unchanged. The published checkpoint SHA256 is `498bcc0dbfc053ca4824b1b6cd2914e85536d0aed1e07ad50d5ef3b4a0b11b87`; the original training checkpoint SHA256 was `f24e5f3357b045ed2a49fdb8a377cd3668a5cbb8100ff3c13906e3bfce9aff11`.
|
| 19 |
+
|
| 20 |
+
## Intended Use
|
| 21 |
+
|
| 22 |
+
| Task | Description |
|
| 23 |
+
| --- | --- |
|
| 24 |
+
| Cavity-flow field inference | Predict velocity and pressure at spatiotemporal coordinates |
|
| 25 |
+
| PINN research | Examine a quantum-classical Navier-Stokes training protocol |
|
| 26 |
+
|
| 27 |
+
## Usage
|
| 28 |
+
|
| 29 |
+
### Download the Model Package
|
| 30 |
+
|
| 31 |
+
```bash
|
| 32 |
+
hf download OneScience-Group/QCPINN-CavityFlow --local-dir ./QCPINN-CavityFlow
|
| 33 |
+
cd QCPINN-CavityFlow
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
### Install Dependencies
|
| 37 |
+
|
| 38 |
+
The formal run used Python 3.10, CPU PyTorch 2.6.0 and PennyLane 0.40.0. The `+cpu` PyTorch wheel requires the official CPU index:
|
| 39 |
+
|
| 40 |
+
```bash
|
| 41 |
+
conda create -n qcpinn-cavity python=3.10 -y
|
| 42 |
+
conda activate qcpinn-cavity
|
| 43 |
+
pip install -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cpu
|
| 44 |
+
pip install 'modelscope[datasets]'
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
### Reference Data
|
| 48 |
+
|
| 49 |
+
Hugging Face hosts only the model package. Obtain the author's `cavity.mat` from [OneScience/QCPINN-Cavity on ModelScope](https://www.modelscope.cn/datasets/OneScience/QCPINN-Cavity):
|
| 50 |
+
|
| 51 |
+
```bash
|
| 52 |
+
modelscope download --dataset OneScience/QCPINN-Cavity --local_dir ./data
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
The expected file is `data/cavity.mat`, SHA256 `4010d55dc23aa0f8ba79b24baf75e3903044bc4333d772168121cd30bc95c3ba`. It contains `cavity_internal`, `cavity_gamma0`, and `cavity_gamma1` in MATLAB v7.3/HDF5 format. No external labeled training dataset is needed.
|
| 56 |
+
|
| 57 |
+
### Train
|
| 58 |
+
|
| 59 |
+
From a fresh package copy on a SLURM compute cluster, with the Python environment activated:
|
| 60 |
+
|
| 61 |
+
```bash
|
| 62 |
+
sbatch scripts/run_all.slurm
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
The script runs ten seeds, 20,000 epochs per seed, then reloads checkpoints for inference and figures. Adjust the cluster's time/partition settings as needed. Preserve the bundled weight and results before retraining.
|
| 66 |
+
|
| 67 |
+
### Inference and Results
|
| 68 |
+
|
| 69 |
+
```bash
|
| 70 |
+
python scripts/inference.py --checkpoint weight/best_model.pt
|
| 71 |
+
python scripts/result.py
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
The selected checkpoint's internal-field relative L2 errors for `(u, v, p)` were `(123.70%, 121.43%, 164.19%)`. Across ten validation-best checkpoints, the means were `(200.42%, 274.73%, 148.86%)`, versus `(25.73%, 38.00%, 42.65%)` in paper Table 3. The full training was completed, but the paper accuracy was not reproduced. Historical `results/best_model/metrics.json` records the original checkpoint hash; rerunning inference records the published checkpoint hash.
|
| 75 |
+
|
| 76 |
+
## OneScience Repositories
|
| 77 |
+
|
| 78 |
+
| Platform | Main repository | Skills repository |
|
| 79 |
+
| --- | --- | --- |
|
| 80 |
+
| Gitee | [OneScience](https://gitee.com/onescience-ai/onescience) | [OneSkills](https://gitee.com/onescience-ai/oneskills) |
|
| 81 |
+
| GitHub | [OneScience](https://github.com/onescience-ai/OneScience) | [OneSkills](https://github.com/onescience-ai/oneskills) |
|
| 82 |
+
|
| 83 |
+
## Citation and License
|
| 84 |
+
|
| 85 |
+
- Paper: [QCPINN](https://arxiv.org/abs/2503.16678).
|
| 86 |
+
- Reference field: [authors' QCPINN repository](https://github.com/afrah/QCPINN/tree/main/data). The repository has an MIT software license; the data is distributed separately with its own provenance information.
|
config.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_type": "qcpinn_cavity_angle_cascade",
|
| 3 |
+
"architectures": ["QCPINN"],
|
| 4 |
+
"framework": "pytorch",
|
| 5 |
+
"experiment": "table3_unsteady_cavity",
|
| 6 |
+
"input_dim": 3,
|
| 7 |
+
"output_dim": 3,
|
| 8 |
+
"hidden_dim": 50,
|
| 9 |
+
"n_qubits": 5,
|
| 10 |
+
"checkpoint": "weight/best_model.pt",
|
| 11 |
+
"reference_dataset": "OneScience/QCPINN-Cavity"
|
| 12 |
+
}
|
config/config.yaml
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment: QCPINN_Table3_Cavity_Angle_Cascade
|
| 2 |
+
paper: https://arxiv.org/pdf/2503.16678
|
| 3 |
+
paths:
|
| 4 |
+
reference: data/cavity.mat
|
| 5 |
+
results: results
|
| 6 |
+
weights: weight
|
| 7 |
+
model:
|
| 8 |
+
input_dim: 3
|
| 9 |
+
output_dim: 3
|
| 10 |
+
hidden_dim: 50
|
| 11 |
+
n_qubits: 5
|
| 12 |
+
quantum_layers: 1
|
| 13 |
+
expected_parameters: 923
|
| 14 |
+
dtype: float32
|
| 15 |
+
device: cpu
|
| 16 |
+
backend: default.qubit
|
| 17 |
+
shots: null
|
| 18 |
+
physics:
|
| 19 |
+
rho: 1056.0
|
| 20 |
+
mu: 0.01
|
| 21 |
+
t_max: 10.0
|
| 22 |
+
loss_formula: paper_literal
|
| 23 |
+
loss_weights: {physics: 0.1, lid: 2.0, walls: 2.0, initial: 4.0}
|
| 24 |
+
training:
|
| 25 |
+
epochs: 20000
|
| 26 |
+
batch_size: 64
|
| 27 |
+
seeds: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
| 28 |
+
optimizer: Adam
|
| 29 |
+
learning_rate: 0.005
|
| 30 |
+
betas: [0.9, 0.999]
|
| 31 |
+
epsilon: 1.0e-8
|
| 32 |
+
weight_decay: 0.0
|
| 33 |
+
scheduler: {name: ReduceLROnPlateau, factor: 0.9, patience: 1000, threshold: 0.0001}
|
| 34 |
+
gradient_clip: 1.0
|
| 35 |
+
early_stopping: false
|
| 36 |
+
num_threads: 1
|
| 37 |
+
checkpoint_every: 1000
|
| 38 |
+
sampling:
|
| 39 |
+
method: Sobol
|
| 40 |
+
scramble: true
|
| 41 |
+
interior: 16384
|
| 42 |
+
per_wall: 4096
|
| 43 |
+
initial: 4096
|
| 44 |
+
validation:
|
| 45 |
+
reference_key: cavity_internal
|
| 46 |
+
size: 1024
|
| 47 |
+
seed: 20260325
|
| 48 |
+
every: 10
|
| 49 |
+
batch_size: 512
|
| 50 |
+
selection: mean_relative_l2_u_v_p
|
| 51 |
+
inference:
|
| 52 |
+
batch_size: 4096
|
| 53 |
+
reference_keys: [cavity_internal, cavity_gamma0, cavity_gamma1]
|
| 54 |
+
visualization:
|
| 55 |
+
times: [1.0, 5.0, 10.0]
|
| 56 |
+
dpi: 200
|
| 57 |
+
assumptions:
|
| 58 |
+
- Sobol pool sizes, scrambling, seeds 0..9 and validation split are not specified by the paper.
|
| 59 |
+
- Use literal printed Appendix A.2 losses; component cancellation is a known limitation.
|
| 60 |
+
- Use independent CRX parameters per Table 2; do not reproduce official parameter reuse bug.
|
| 61 |
+
- Input coordinates and reference pressure are not normalized, shifted or rescaled.
|
| 62 |
+
- One epoch is one independently sampled batch of 64 per constraint and one Adam update.
|
model/QCPINN.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Independent implementation of arXiv:2503.16678, Fig.2b and Tables 2/3.
|
| 2 |
+
|
| 3 |
+
Inputs are physical (t,x,y), outputs (u,v,p). No reference fields enter forward.
|
| 4 |
+
The three groups of n circuit angles are independent, as specified by 3*n*L.
|
| 5 |
+
"""
|
| 6 |
+
import pennylane as qml
|
| 7 |
+
import torch
|
| 8 |
+
from torch import nn
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class QCPINN(nn.Module):
|
| 12 |
+
def __init__(self, input_dim=3, output_dim=3, hidden_dim=50,
|
| 13 |
+
n_qubits=5, quantum_layers=1, backend="default.qubit", shots=None):
|
| 14 |
+
super().__init__()
|
| 15 |
+
if shots is not None:
|
| 16 |
+
raise ValueError("The paper requires analytic simulation: shots=None")
|
| 17 |
+
self.n_qubits = n_qubits
|
| 18 |
+
self.quantum_layers = quantum_layers
|
| 19 |
+
self.preprocessor = nn.Sequential(nn.Linear(input_dim, hidden_dim), nn.Tanh(),
|
| 20 |
+
nn.Linear(hidden_dim, n_qubits))
|
| 21 |
+
self.quantum_angles = nn.Parameter(torch.empty(quantum_layers, 3*n_qubits))
|
| 22 |
+
self.postprocessor = nn.Sequential(nn.Linear(n_qubits, hidden_dim), nn.Tanh(),
|
| 23 |
+
nn.Linear(hidden_dim, output_dim))
|
| 24 |
+
for block in (self.preprocessor, self.postprocessor):
|
| 25 |
+
for module in block:
|
| 26 |
+
if isinstance(module, nn.Linear):
|
| 27 |
+
nn.init.xavier_normal_(module.weight)
|
| 28 |
+
nn.init.zeros_(module.bias)
|
| 29 |
+
nn.init.xavier_normal_(self.quantum_angles)
|
| 30 |
+
device = qml.device(backend, wires=n_qubits, shots=None)
|
| 31 |
+
|
| 32 |
+
@qml.qnode(device, interface="torch", diff_method="backprop")
|
| 33 |
+
def circuit(features, angles):
|
| 34 |
+
qml.AngleEmbedding(features, wires=range(n_qubits), rotation="X")
|
| 35 |
+
for layer in range(quantum_layers):
|
| 36 |
+
for wire in range(n_qubits):
|
| 37 |
+
qml.RX(angles[layer, wire], wires=wire)
|
| 38 |
+
for wire in range(n_qubits):
|
| 39 |
+
qml.RZ(angles[layer, n_qubits+wire], wires=wire)
|
| 40 |
+
# Ring order matches Fig.2b, with distinct angles for every CRX.
|
| 41 |
+
edges = [(n_qubits-1, 0)] + [(i-1, i) for i in range(n_qubits-1, 0, -1)]
|
| 42 |
+
for index, edge in enumerate(edges):
|
| 43 |
+
qml.CRX(angles[layer, 2*n_qubits+index], wires=edge)
|
| 44 |
+
return tuple(qml.expval(qml.PauliZ(w)) for w in range(n_qubits))
|
| 45 |
+
|
| 46 |
+
self.circuit = circuit
|
| 47 |
+
|
| 48 |
+
def forward(self, coordinates):
|
| 49 |
+
if coordinates.ndim != 2 or coordinates.shape[1] != 3:
|
| 50 |
+
raise ValueError(f"Expected (batch,3) (t,x,y); got {coordinates.shape}")
|
| 51 |
+
encoded = self.preprocessor(coordinates)
|
| 52 |
+
features = torch.stack(self.circuit(encoded, self.quantum_angles), dim=-1)
|
| 53 |
+
return self.postprocessor(features.to(dtype=coordinates.dtype))
|
model/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .QCPINN import QCPINN
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch==2.6.0+cpu
|
| 2 |
+
pennylane==0.40.0
|
| 3 |
+
autoray==0.6.12
|
| 4 |
+
numpy==1.26.4
|
| 5 |
+
scipy==1.15.2
|
| 6 |
+
h5py
|
| 7 |
+
matplotlib
|
| 8 |
+
PyYAML
|
| 9 |
+
jax==0.4.35
|
| 10 |
+
jaxlib==0.4.35
|
results/best_checkpoint_selection.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"seed": 6,
|
| 3 |
+
"epoch": 1,
|
| 4 |
+
"selection_score": 1.3976091685792522,
|
| 5 |
+
"metrics": {
|
| 6 |
+
"u": {
|
| 7 |
+
"relative_l2": 1.2418883137227157,
|
| 8 |
+
"relative_l2_percent": 124.18883137227157,
|
| 9 |
+
"error_sum_squares": 67.4534003712654,
|
| 10 |
+
"reference_sum_squares": 43.73597039713087,
|
| 11 |
+
"reason": null
|
| 12 |
+
},
|
| 13 |
+
"v": {
|
| 14 |
+
"relative_l2": 1.2429718585473652,
|
| 15 |
+
"relative_l2_percent": 124.29718585473653,
|
| 16 |
+
"error_sum_squares": 27.36086485363873,
|
| 17 |
+
"reference_sum_squares": 17.709537880486465,
|
| 18 |
+
"reason": null
|
| 19 |
+
},
|
| 20 |
+
"p": {
|
| 21 |
+
"relative_l2": 1.7079673334676762,
|
| 22 |
+
"relative_l2_percent": 170.7967333467676,
|
| 23 |
+
"error_sum_squares": 22.096531139899867,
|
| 24 |
+
"reference_sum_squares": 7.574692034445659,
|
| 25 |
+
"reason": null
|
| 26 |
+
}
|
| 27 |
+
},
|
| 28 |
+
"checkpoint": "weight/best_model.pt",
|
| 29 |
+
"phase": "all_ten_formal_runs_completed"
|
| 30 |
+
}
|
results/best_model/figures/fields_t1.png
ADDED
|
Git LFS Details
|
results/best_model/figures/fields_t10.png
ADDED
|
Git LFS Details
|
results/best_model/figures/fields_t5.png
ADDED
|
Git LFS Details
|
results/best_model/figures/streamlines_t1.png
ADDED
|
Git LFS Details
|
results/best_model/figures/streamlines_t10.png
ADDED
|
Git LFS Details
|
results/best_model/figures/streamlines_t5.png
ADDED
|
Git LFS Details
|
results/best_model/figures/training_curves.png
ADDED
|
Git LFS Details
|
results/best_model/metrics.json
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"checkpoint": "weight/best_model.pt",
|
| 3 |
+
"checkpoint_sha256": "f24e5f3357b045ed2a49fdb8a377cd3668a5cbb8100ff3c13906e3bfce9aff11",
|
| 4 |
+
"checkpoint_sha256_scope": "original_training_file; published_copy_changes_reference_path_metadata_only",
|
| 5 |
+
"source": "data/cavity.mat",
|
| 6 |
+
"source_sha256": "4010d55dc23aa0f8ba79b24baf75e3903044bc4333d772168121cd30bc95c3ba",
|
| 7 |
+
"seed": 6,
|
| 8 |
+
"epoch": 1,
|
| 9 |
+
"selection_score": 1.3976091685792522,
|
| 10 |
+
"validation_metrics": {
|
| 11 |
+
"u": {
|
| 12 |
+
"relative_l2": 1.2418883137227157,
|
| 13 |
+
"relative_l2_percent": 124.18883137227157,
|
| 14 |
+
"error_sum_squares": 67.4534003712654,
|
| 15 |
+
"reference_sum_squares": 43.73597039713087,
|
| 16 |
+
"reason": null
|
| 17 |
+
},
|
| 18 |
+
"v": {
|
| 19 |
+
"relative_l2": 1.2429718585473652,
|
| 20 |
+
"relative_l2_percent": 124.29718585473653,
|
| 21 |
+
"error_sum_squares": 27.36086485363873,
|
| 22 |
+
"reference_sum_squares": 17.709537880486465,
|
| 23 |
+
"reason": null
|
| 24 |
+
},
|
| 25 |
+
"p": {
|
| 26 |
+
"relative_l2": 1.7079673334676762,
|
| 27 |
+
"relative_l2_percent": 170.7967333467676,
|
| 28 |
+
"error_sum_squares": 22.096531139899867,
|
| 29 |
+
"reference_sum_squares": 7.574692034445659,
|
| 30 |
+
"reason": null
|
| 31 |
+
}
|
| 32 |
+
},
|
| 33 |
+
"metric": "raw ||pred-ref||2/||ref||2; no pressure gauge alignment",
|
| 34 |
+
"groups": {
|
| 35 |
+
"cavity_internal": {
|
| 36 |
+
"count": 1010000,
|
| 37 |
+
"metrics": {
|
| 38 |
+
"u": {
|
| 39 |
+
"relative_l2": 1.2369659153068235,
|
| 40 |
+
"relative_l2_percent": 123.69659153068235,
|
| 41 |
+
"error_sum_squares": 64793.906059837216,
|
| 42 |
+
"reference_sum_squares": 42346.61459707969,
|
| 43 |
+
"reason": null
|
| 44 |
+
},
|
| 45 |
+
"v": {
|
| 46 |
+
"relative_l2": 1.2143086629586421,
|
| 47 |
+
"relative_l2_percent": 121.43086629586422,
|
| 48 |
+
"error_sum_squares": 27837.676390170618,
|
| 49 |
+
"reference_sum_squares": 18878.817807850282,
|
| 50 |
+
"reason": null
|
| 51 |
+
},
|
| 52 |
+
"p": {
|
| 53 |
+
"relative_l2": 1.6418813661092047,
|
| 54 |
+
"relative_l2_percent": 164.18813661092045,
|
| 55 |
+
"error_sum_squares": 20744.156324328287,
|
| 56 |
+
"reference_sum_squares": 7695.063862736005,
|
| 57 |
+
"reason": null
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
},
|
| 61 |
+
"cavity_gamma0": {
|
| 62 |
+
"count": 30300,
|
| 63 |
+
"metrics": {
|
| 64 |
+
"u": {
|
| 65 |
+
"relative_l2": null,
|
| 66 |
+
"relative_l2_percent": null,
|
| 67 |
+
"error_sum_squares": 721.6388498068019,
|
| 68 |
+
"reference_sum_squares": 0.0,
|
| 69 |
+
"reason": "zero_reference_norm"
|
| 70 |
+
},
|
| 71 |
+
"v": {
|
| 72 |
+
"relative_l2": null,
|
| 73 |
+
"relative_l2_percent": null,
|
| 74 |
+
"error_sum_squares": 299.3688306501471,
|
| 75 |
+
"reference_sum_squares": 0.0,
|
| 76 |
+
"reason": "zero_reference_norm"
|
| 77 |
+
},
|
| 78 |
+
"p": {
|
| 79 |
+
"relative_l2": 1.0554335687908523,
|
| 80 |
+
"relative_l2_percent": 105.54335687908522,
|
| 81 |
+
"error_sum_squares": 2809.0431303861237,
|
| 82 |
+
"reference_sum_squares": 2521.718480946791,
|
| 83 |
+
"reason": null
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
},
|
| 87 |
+
"cavity_gamma1": {
|
| 88 |
+
"count": 10100,
|
| 89 |
+
"metrics": {
|
| 90 |
+
"u": {
|
| 91 |
+
"relative_l2": 0.8486121910397583,
|
| 92 |
+
"relative_l2_percent": 84.86121910397583,
|
| 93 |
+
"error_sum_squares": 7273.440772891121,
|
| 94 |
+
"reference_sum_squares": 10100.0,
|
| 95 |
+
"reason": null
|
| 96 |
+
},
|
| 97 |
+
"v": {
|
| 98 |
+
"relative_l2": null,
|
| 99 |
+
"relative_l2_percent": null,
|
| 100 |
+
"error_sum_squares": 75.20828866056323,
|
| 101 |
+
"reference_sum_squares": 0.0,
|
| 102 |
+
"reason": "zero_reference_norm"
|
| 103 |
+
},
|
| 104 |
+
"p": {
|
| 105 |
+
"relative_l2": 1.003925357845928,
|
| 106 |
+
"relative_l2_percent": 100.3925357845928,
|
| 107 |
+
"error_sum_squares": 2125.860076265305,
|
| 108 |
+
"reference_sum_squares": 2109.2683099242454,
|
| 109 |
+
"reason": null
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
},
|
| 114 |
+
"reference_regenerated": false,
|
| 115 |
+
"checkpoint_reloaded": true,
|
| 116 |
+
"internal_holdout": {
|
| 117 |
+
"count": 1008976,
|
| 118 |
+
"metrics": {
|
| 119 |
+
"u": {
|
| 120 |
+
"relative_l2": 1.2369608160205556,
|
| 121 |
+
"relative_l2_percent": 123.69608160205556,
|
| 122 |
+
"error_sum_squares": 64726.45265992043,
|
| 123 |
+
"reference_sum_squares": 42302.87862668262,
|
| 124 |
+
"reason": null
|
| 125 |
+
},
|
| 126 |
+
"v": {
|
| 127 |
+
"relative_l2": 1.2142814318609751,
|
| 128 |
+
"relative_l2_percent": 121.42814318609751,
|
| 129 |
+
"error_sum_squares": 27810.31552531313,
|
| 130 |
+
"reference_sum_squares": 18861.10826996979,
|
| 131 |
+
"reason": null
|
| 132 |
+
},
|
| 133 |
+
"p": {
|
| 134 |
+
"relative_l2": 1.6418149379825397,
|
| 135 |
+
"relative_l2_percent": 164.18149379825397,
|
| 136 |
+
"error_sum_squares": 20722.05979287031,
|
| 137 |
+
"reference_sum_squares": 7687.489170701568,
|
| 138 |
+
"reason": null
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
},
|
| 142 |
+
"all_reference": {
|
| 143 |
+
"metrics": {
|
| 144 |
+
"u": {
|
| 145 |
+
"relative_l2": 1.1780781571826924,
|
| 146 |
+
"relative_l2_percent": 117.80781571826924,
|
| 147 |
+
"error_sum_squares": 72788.98568253515,
|
| 148 |
+
"reference_sum_squares": 52446.61459707969,
|
| 149 |
+
"reason": null
|
| 150 |
+
},
|
| 151 |
+
"v": {
|
| 152 |
+
"relative_l2": 1.222451087407963,
|
| 153 |
+
"relative_l2_percent": 122.2451087407963,
|
| 154 |
+
"error_sum_squares": 28212.25350948133,
|
| 155 |
+
"reference_sum_squares": 18878.817807850282,
|
| 156 |
+
"reason": null
|
| 157 |
+
},
|
| 158 |
+
"p": {
|
| 159 |
+
"relative_l2": 1.44336969627177,
|
| 160 |
+
"relative_l2_percent": 144.33696962717698,
|
| 161 |
+
"error_sum_squares": 25679.059530979714,
|
| 162 |
+
"reference_sum_squares": 12326.050653607042,
|
| 163 |
+
"reason": null
|
| 164 |
+
}
|
| 165 |
+
},
|
| 166 |
+
"count": 1050400
|
| 167 |
+
}
|
| 168 |
+
}
|
results/best_model/report.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
执行结论:完整运行已完成,但未复现Table 3数值精度。全部训练仅使用本案例新提交的计算作业975143;没有借用、取消或修改账号中的既有作业。10个种子各完成20000epoch,正式权重重载与可视化均已完成。
|
| 2 |
+
|
| 3 |
+
按论文印刷式逐字实现的loss允许u=.5,v=-.5,p=0的零损失非物理解。本实验保留这一公式,未用官方额外监督项替换它,也未另造参考真值;来源冲突与实现假设详见[审计规格](../../reproduction_spec.md)。
|
| 4 |
+
|
| 5 |
+
# QCPINN Angle-Cascade cavity reproduction
|
| 6 |
+
|
| 7 |
+
Selected seed: 6; epoch: 1; 923 parameters.
|
| 8 |
+
|
| 9 |
+
Reference values were read directly from the supplied cavity.mat; no generated reference truth.
|
| 10 |
+
|
| 11 |
+
| Field | Internal relative L2 (%) | Paper Table 3 mean ± std (%) |
|
| 12 |
+
|---|---:|---:|
|
| 13 |
+
| u | 123.696592 | 25.73 ± 2.49 |
|
| 14 |
+
| v | 121.430866 | 38.00 ± 3.76 |
|
| 15 |
+
| p | 164.188137 | 42.65 ± 8.69 |
|
| 16 |
+
|
| 17 |
+
## Ten independent complete runs (20000 epochs each)
|
| 18 |
+
|
| 19 |
+
| Field | Final checkpoints mean ± std (%) | Validation-best checkpoints mean ± std (%) | Paper Table 3 (%) |
|
| 20 |
+
|---|---:|---:|---:|
|
| 21 |
+
| u | 263.827208 ± 0.072903 | 200.415311 ± 61.786860 | 25.73 ± 2.49 |
|
| 22 |
+
| v | 379.130934 ± 0.111758 | 274.729384 ± 87.551613 | 38.00 ± 3.76 |
|
| 23 |
+
| p | 100.122990 ± 0.609755 | 148.862860 ± 73.002451 | 42.65 ± 8.69 |
|
| 24 |
+
|
| 25 |
+
Final loss: 4.71713587e-07 ± 4.71234223e-07; paper: 0.10 ± 0.02.
|
| 26 |
+
Time per update: 0.216483 ± 0.003915 s; wall time for PDE/BC/IC forward, backward, gradient clip and Adam step; validation/checkpoint I/O excluded.
|
| 27 |
+
Peak process RSS: 712.232 ± 28.112 MB. Actual hardware is recorded in results/compute_hardware.txt; timing/memory are not hardware-identical to the paper.
|
| 28 |
+
Population standard deviation (ddof=0) is used; the paper does not specify this convention.
|
| 29 |
+
|
| 30 |
+
The selected best checkpoint is a single model; the paper entries are ten-run statistics.
|
| 31 |
+
See table3_comparison.json for separately aggregated final and best checkpoints.
|
| 32 |
+
|
| 33 |
+
## Explicit limitations
|
| 34 |
+
- Sobol pool sizes, scrambling, seeds 0..9 and validation split are not specified by the paper.
|
| 35 |
+
- Use literal printed Appendix A.2 losses; component cancellation is a known limitation.
|
| 36 |
+
- Use independent CRX parameters per Table 2; do not reproduce official parameter reuse bug.
|
| 37 |
+
- Input coordinates and reference pressure are not normalized, shifted or rescaled.
|
| 38 |
+
- One epoch is one independently sampled batch of 64 per constraint and one Adam update.
|
| 39 |
+
- Appendix A.2 printed sums can cancel between components. The literal printed losses were retained.
|
| 40 |
+
- The reference contains saved time intervals of 0.1 s, while the paper describes a solver step of 0.01 s.
|
| 41 |
+
- Validation uses a fixed reference subset for checkpoint selection. Holdout metrics exclude those records.
|
| 42 |
+
|
| 43 |
+
Source: [paper](https://arxiv.org/pdf/2503.16678), sections 4/5, Figure 2, Tables 2/3 and Appendix A.2.
|
results/compute_hardware.txt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
a01r3n10
|
| 2 |
+
Architecture: x86_64
|
| 3 |
+
CPU op-mode(s): 32-bit, 64-bit
|
| 4 |
+
Byte Order: Little Endian
|
| 5 |
+
CPU(s): 128
|
| 6 |
+
On-line CPU(s) list: 0-127
|
| 7 |
+
Thread(s) per core: 1
|
| 8 |
+
Core(s) per socket: 64
|
| 9 |
+
Socket(s): 2
|
| 10 |
+
NUMA node(s): 8
|
| 11 |
+
Vendor ID: HygonGenuine
|
| 12 |
+
CPU family: 24
|
| 13 |
+
Model: 7
|
| 14 |
+
Model name: Hygon C86 Processor
|
| 15 |
+
Stepping: 0
|
| 16 |
+
CPU MHz: 2400.017
|
| 17 |
+
BogoMIPS: 4800.03
|
| 18 |
+
Virtualization: AMD-V
|
| 19 |
+
L1d cache: 32K
|
| 20 |
+
L1i cache: 32K
|
| 21 |
+
L2 cache: 512K
|
| 22 |
+
L3 cache: 16384K
|
| 23 |
+
NUMA node0 CPU(s): 0-15
|
| 24 |
+
NUMA node1 CPU(s): 16-31
|
| 25 |
+
NUMA node2 CPU(s): 32-47
|
results/seed_6/history.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
results/table3_comparison.json
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"status": "completed",
|
| 3 |
+
"runs": 10,
|
| 4 |
+
"epochs_per_run": 20000,
|
| 5 |
+
"statistics": {
|
| 6 |
+
"final": {
|
| 7 |
+
"u": {
|
| 8 |
+
"mean_percent": 263.82720846357546,
|
| 9 |
+
"std_percent": 0.07290313116389122,
|
| 10 |
+
"values_percent": [
|
| 11 |
+
263.8550091118908,
|
| 12 |
+
263.9313754674022,
|
| 13 |
+
263.8202844356478,
|
| 14 |
+
263.7872862846581,
|
| 15 |
+
263.86905668107204,
|
| 16 |
+
263.7875723279372,
|
| 17 |
+
263.8015990771984,
|
| 18 |
+
263.9572048027661,
|
| 19 |
+
263.7136317704975,
|
| 20 |
+
263.74906467668444
|
| 21 |
+
]
|
| 22 |
+
},
|
| 23 |
+
"v": {
|
| 24 |
+
"mean_percent": 379.1309335658924,
|
| 25 |
+
"std_percent": 0.11175777875192125,
|
| 26 |
+
"values_percent": [
|
| 27 |
+
379.0429768655644,
|
| 28 |
+
379.2406101730609,
|
| 29 |
+
379.0715007982627,
|
| 30 |
+
379.04294338051415,
|
| 31 |
+
379.1214781463276,
|
| 32 |
+
379.04151966547096,
|
| 33 |
+
379.3142254787664,
|
| 34 |
+
379.2905145628615,
|
| 35 |
+
379.17106663969815,
|
| 36 |
+
378.9724999483974
|
| 37 |
+
]
|
| 38 |
+
},
|
| 39 |
+
"p": {
|
| 40 |
+
"mean_percent": 100.12298999068985,
|
| 41 |
+
"std_percent": 0.6097553354196278,
|
| 42 |
+
"values_percent": [
|
| 43 |
+
99.99971594555477,
|
| 44 |
+
99.96530609848863,
|
| 45 |
+
101.92920783109811,
|
| 46 |
+
99.79079939688157,
|
| 47 |
+
99.76182788947703,
|
| 48 |
+
99.81738607251161,
|
| 49 |
+
100.08049441555151,
|
| 50 |
+
99.93770620014934,
|
| 51 |
+
99.95562455821036,
|
| 52 |
+
99.9918314989756
|
| 53 |
+
]
|
| 54 |
+
}
|
| 55 |
+
},
|
| 56 |
+
"best": {
|
| 57 |
+
"u": {
|
| 58 |
+
"mean_percent": 200.41531063983444,
|
| 59 |
+
"std_percent": 61.786859912537416,
|
| 60 |
+
"values_percent": [
|
| 61 |
+
216.73178735741482,
|
| 62 |
+
257.8566730563977,
|
| 63 |
+
244.05346210075695,
|
| 64 |
+
223.0109183184541,
|
| 65 |
+
209.36992710804304,
|
| 66 |
+
98.65206632622584,
|
| 67 |
+
123.69659153068235,
|
| 68 |
+
261.36667138461894,
|
| 69 |
+
108.33561949271215,
|
| 70 |
+
261.0793897230385
|
| 71 |
+
]
|
| 72 |
+
},
|
| 73 |
+
"v": {
|
| 74 |
+
"mean_percent": 274.72938385999566,
|
| 75 |
+
"std_percent": 87.55161329864875,
|
| 76 |
+
"values_percent": [
|
| 77 |
+
306.2224081242297,
|
| 78 |
+
348.16036507422353,
|
| 79 |
+
351.4166323499106,
|
| 80 |
+
308.4572411231171,
|
| 81 |
+
299.9983810702734,
|
| 82 |
+
166.83810214589104,
|
| 83 |
+
121.43086629586422,
|
| 84 |
+
339.46700708938135,
|
| 85 |
+
147.04576328216456,
|
| 86 |
+
358.25707204490146
|
| 87 |
+
]
|
| 88 |
+
},
|
| 89 |
+
"p": {
|
| 90 |
+
"mean_percent": 148.86286014952978,
|
| 91 |
+
"std_percent": 73.00245075399947,
|
| 92 |
+
"values_percent": [
|
| 93 |
+
103.59704594765071,
|
| 94 |
+
100.38273398618911,
|
| 95 |
+
99.33596465585998,
|
| 96 |
+
113.92679231218537,
|
| 97 |
+
128.94455232416792,
|
| 98 |
+
227.51965246296444,
|
| 99 |
+
164.18813661092045,
|
| 100 |
+
111.02781481911512,
|
| 101 |
+
335.8090833036171,
|
| 102 |
+
103.89682507262745
|
| 103 |
+
]
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
},
|
| 107 |
+
"std_ddof": 0,
|
| 108 |
+
"paper_table3_percent": {
|
| 109 |
+
"u": {
|
| 110 |
+
"mean": 25.73,
|
| 111 |
+
"std": 2.49
|
| 112 |
+
},
|
| 113 |
+
"v": {
|
| 114 |
+
"mean": 38.0,
|
| 115 |
+
"std": 3.76
|
| 116 |
+
},
|
| 117 |
+
"p": {
|
| 118 |
+
"mean": 42.65,
|
| 119 |
+
"std": 8.69
|
| 120 |
+
}
|
| 121 |
+
},
|
| 122 |
+
"final_loss": {
|
| 123 |
+
"mean": 4.7171358659170436e-07,
|
| 124 |
+
"std": 4.712342234329683e-07
|
| 125 |
+
},
|
| 126 |
+
"parameters": 923,
|
| 127 |
+
"time_per_update_seconds": {
|
| 128 |
+
"mean": 0.21648326783038208,
|
| 129 |
+
"std": 0.003914749140125143,
|
| 130 |
+
"run_means": [
|
| 131 |
+
0.21697817574576475,
|
| 132 |
+
0.21848320898595266,
|
| 133 |
+
0.2186092604946112,
|
| 134 |
+
0.22009400647231378,
|
| 135 |
+
0.22067649887888693,
|
| 136 |
+
0.2170402910890989,
|
| 137 |
+
0.21741632961235008,
|
| 138 |
+
0.2171733768468257,
|
| 139 |
+
0.21084768179748206,
|
| 140 |
+
0.2075138483805349
|
| 141 |
+
],
|
| 142 |
+
"definition": "wall time for PDE/BC/IC forward, backward, gradient clip and Adam step; validation/checkpoint I/O excluded"
|
| 143 |
+
},
|
| 144 |
+
"peak_process_rss_mb": {
|
| 145 |
+
"mean": 712.231640625,
|
| 146 |
+
"std": 28.112347417123058,
|
| 147 |
+
"values": [
|
| 148 |
+
676.7890625,
|
| 149 |
+
680.02734375,
|
| 150 |
+
677.796875,
|
| 151 |
+
676.68359375,
|
| 152 |
+
735.2109375,
|
| 153 |
+
734.66796875,
|
| 154 |
+
735.0390625,
|
| 155 |
+
736.55859375,
|
| 156 |
+
735.4140625,
|
| 157 |
+
734.12890625
|
| 158 |
+
]
|
| 159 |
+
},
|
| 160 |
+
"paper_table3_other": {
|
| 161 |
+
"parameters": 923,
|
| 162 |
+
"loss_mean": 0.1,
|
| 163 |
+
"loss_std": 0.02,
|
| 164 |
+
"time_seconds_mean": 0.36,
|
| 165 |
+
"time_seconds_std": 0.01,
|
| 166 |
+
"memory_mb_mean": 540.0,
|
| 167 |
+
"memory_mb_std": 0.0
|
| 168 |
+
},
|
| 169 |
+
"selected_seed": 6,
|
| 170 |
+
"selection_metric": "mean validation relative L2",
|
| 171 |
+
"best_model": "weight/best_model.pt",
|
| 172 |
+
"limitations": [
|
| 173 |
+
"Sobol pool sizes, scrambling, seeds 0..9 and validation split are not specified by the paper.",
|
| 174 |
+
"Use literal printed Appendix A.2 losses; component cancellation is a known limitation.",
|
| 175 |
+
"Use independent CRX parameters per Table 2; do not reproduce official parameter reuse bug.",
|
| 176 |
+
"Input coordinates and reference pressure are not normalized, shifted or rescaled.",
|
| 177 |
+
"One epoch is one independently sampled batch of 64 per constraint and one Adam update."
|
| 178 |
+
]
|
| 179 |
+
}
|
scripts/common.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Data, Sobol, PDE and metric contracts shared by the three entry points."""
|
| 2 |
+
import hashlib
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
import random
|
| 6 |
+
import socket
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
import h5py
|
| 10 |
+
import numpy as np
|
| 11 |
+
import torch
|
| 12 |
+
import yaml
|
| 13 |
+
|
| 14 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def load_config(path):
|
| 18 |
+
with open(path, encoding="utf-8") as stream:
|
| 19 |
+
config = yaml.safe_load(stream)
|
| 20 |
+
if config["training"]["batch_size"] != 64 or config["training"]["epochs"] != 20000:
|
| 21 |
+
raise ValueError("Formal protocol must retain batch_size=64 and epochs=20000")
|
| 22 |
+
if config["training"]["early_stopping"]:
|
| 23 |
+
raise ValueError("User requires complete formal training; early stopping disabled")
|
| 24 |
+
return config
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def resolved(config, name):
|
| 28 |
+
path = Path(config["paths"][name])
|
| 29 |
+
return path if path.is_absolute() else ROOT / path
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def atomic_json(path, payload):
|
| 33 |
+
path = Path(path)
|
| 34 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 35 |
+
tmp = path.with_suffix(path.suffix + ".tmp")
|
| 36 |
+
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2, allow_nan=False), encoding="utf-8")
|
| 37 |
+
tmp.replace(path)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def atomic_save(path, payload):
|
| 41 |
+
path = Path(path)
|
| 42 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 43 |
+
tmp = path.with_suffix(path.suffix + ".tmp")
|
| 44 |
+
torch.save(payload, tmp)
|
| 45 |
+
tmp.replace(path)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def require_compute_node():
|
| 49 |
+
# A self-submitted allocation also records a job name unique to this case.
|
| 50 |
+
jid, name = os.environ.get("SLURM_JOB_ID"), os.environ.get("SLURM_JOB_NAME", "")
|
| 51 |
+
if not jid or not name.startswith("qcpinn_cavity_") or "login" in socket.gethostname().lower():
|
| 52 |
+
raise RuntimeError("Training/smoke requires a new case-owned SLURM compute allocation")
|
| 53 |
+
return {"job_id": jid, "job_name": name, "hostname": socket.gethostname(),
|
| 54 |
+
"array_job_id": os.environ.get("SLURM_ARRAY_JOB_ID"),
|
| 55 |
+
"array_task_id": os.environ.get("SLURM_ARRAY_TASK_ID")}
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def seed_everything(seed, threads=1):
|
| 59 |
+
random.seed(seed)
|
| 60 |
+
np.random.seed(seed)
|
| 61 |
+
torch.manual_seed(seed)
|
| 62 |
+
torch.set_num_threads(threads)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def build_model(config):
|
| 66 |
+
from model.QCPINN import QCPINN
|
| 67 |
+
m = config["model"]
|
| 68 |
+
model = QCPINN(**{key: m[key] for key in ["input_dim", "output_dim", "hidden_dim", "n_qubits", "quantum_layers", "backend", "shots"]})
|
| 69 |
+
model = model.to(dtype=getattr(torch, m["dtype"]), device=m["device"])
|
| 70 |
+
n = sum(p.numel() for p in model.parameters())
|
| 71 |
+
if n != m["expected_parameters"]:
|
| 72 |
+
raise ValueError(f"Parameter count {n} != paper count {m['expected_parameters']}")
|
| 73 |
+
return model
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def make_sobol_pools(config, seed):
|
| 77 |
+
s = config["sampling"]
|
| 78 |
+
pools = {}
|
| 79 |
+
for index, kind in enumerate(["interior", "lid", "left", "right", "bottom", "initial"]):
|
| 80 |
+
count = s["interior"] if kind == "interior" else s["initial"] if kind == "initial" else s["per_wall"]
|
| 81 |
+
engine = torch.quasirandom.SobolEngine(3, scramble=s["scramble"], seed=seed*17+index)
|
| 82 |
+
xyz = engine.draw(count).to(dtype=getattr(torch, config["model"]["dtype"]))
|
| 83 |
+
xyz[:, 0] *= config["physics"]["t_max"]
|
| 84 |
+
if kind == "lid": xyz[:, 2] = 1.0
|
| 85 |
+
if kind == "left": xyz[:, 1] = 0.0
|
| 86 |
+
if kind == "right": xyz[:, 1] = 1.0
|
| 87 |
+
if kind == "bottom": xyz[:, 2] = 0.0
|
| 88 |
+
if kind == "initial": xyz[:, 0] = 0.0
|
| 89 |
+
pools[kind] = xyz.to(config["model"]["device"])
|
| 90 |
+
return pools
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def sample_batches(pools, size, rng):
|
| 94 |
+
return {key: points[torch.as_tensor(rng.choice(len(points), size=size, replace=False), device=points.device)]
|
| 95 |
+
for key, points in pools.items()}
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def derivative(field, coords):
|
| 99 |
+
return torch.autograd.grad(field, coords, torch.ones_like(field), create_graph=True)[0]
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def pde_residuals(model, coordinates, physics):
|
| 103 |
+
coords = coordinates.detach().clone().requires_grad_(True)
|
| 104 |
+
uvp = model(coords)
|
| 105 |
+
u, v, p = uvp.unbind(-1)
|
| 106 |
+
du, dv, dp = [derivative(field, coords) for field in (u, v, p)]
|
| 107 |
+
uxx = derivative(du[:, 1], coords)[:, 1]
|
| 108 |
+
uyy = derivative(du[:, 2], coords)[:, 2]
|
| 109 |
+
vxx = derivative(dv[:, 1], coords)[:, 1]
|
| 110 |
+
vyy = derivative(dv[:, 2], coords)[:, 2]
|
| 111 |
+
ru = du[:, 0] + u*du[:, 1] + v*du[:, 2] + dp[:, 1]/physics["rho"] - physics["mu"]*(uxx+uyy)
|
| 112 |
+
rv = dv[:, 0] + u*dv[:, 1] + v*dv[:, 2] + dp[:, 2]/physics["rho"] - physics["mu"]*(vxx+vyy)
|
| 113 |
+
rc = du[:, 1] + dv[:, 2]
|
| 114 |
+
return torch.stack((ru, rv, rc), dim=-1)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def loss_terms(model, batches, config):
|
| 118 |
+
physics = config["physics"]
|
| 119 |
+
if physics["loss_formula"] != "paper_literal":
|
| 120 |
+
raise ValueError("This reproduction implements the literal Appendix A.2 losses")
|
| 121 |
+
residuals = pde_residuals(model, batches["interior"], physics)
|
| 122 |
+
terms = {"physics": residuals.square().mean(0).sum()}
|
| 123 |
+
names = ["lid", "left", "right", "bottom", "initial"]
|
| 124 |
+
sizes = [len(batches[k]) for k in names]
|
| 125 |
+
fields = torch.split(model(torch.cat([batches[k] for k in names])), sizes)
|
| 126 |
+
lid, *rest = fields
|
| 127 |
+
terms["lid"] = (1-lid[:, 0]+lid[:, 1]).square().mean()
|
| 128 |
+
terms["walls"] = sum(out[:, :2].sum(-1).square().mean() for out in rest[:3])
|
| 129 |
+
terms["initial"] = rest[3].sum(-1).square().mean()
|
| 130 |
+
terms["total"] = sum(physics["loss_weights"][key]*value for key, value in terms.items())
|
| 131 |
+
return terms
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def validation_indices(config, total):
|
| 135 |
+
size = config["validation"]["size"]
|
| 136 |
+
if size >= total:
|
| 137 |
+
raise ValueError("Validation must leave nonempty holdout")
|
| 138 |
+
return np.sort(np.random.default_rng(config["validation"]["seed"]).choice(total, size, replace=False))
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def load_validation(config):
|
| 142 |
+
key = config["validation"]["reference_key"]
|
| 143 |
+
with h5py.File(resolved(config, "reference"), "r") as source:
|
| 144 |
+
data = source[key]
|
| 145 |
+
if data.shape[0] != 6:
|
| 146 |
+
raise ValueError("Expected MATLAB v7.3 dataset of shape (6,N)")
|
| 147 |
+
ids = validation_indices(config, data.shape[1])
|
| 148 |
+
rows = data[:, ids].T
|
| 149 |
+
if not np.isfinite(rows).all():
|
| 150 |
+
raise ValueError("Reference validation contains NaN/Inf")
|
| 151 |
+
return rows[:, :3], rows[:, 3:], ids
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def relative_metrics(error_squares, reference_squares):
|
| 155 |
+
metrics = {}
|
| 156 |
+
for i, key in enumerate(("u", "v", "p")):
|
| 157 |
+
den, num = float(reference_squares[i]), float(error_squares[i])
|
| 158 |
+
value = float(np.sqrt(num/den)) if den > 0 else None
|
| 159 |
+
metrics[key] = {"relative_l2": value, "relative_l2_percent": None if value is None else 100*value,
|
| 160 |
+
"error_sum_squares": num, "reference_sum_squares": den,
|
| 161 |
+
"reason": None if value is not None else "zero_reference_norm"}
|
| 162 |
+
return metrics
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
@torch.no_grad()
|
| 166 |
+
def predict(model, coords, size):
|
| 167 |
+
param = next(model.parameters())
|
| 168 |
+
return np.concatenate([model(torch.as_tensor(coords[i:i+size], dtype=param.dtype, device=param.device)).cpu().numpy()
|
| 169 |
+
for i in range(0, len(coords), size)], axis=0)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def evaluate(model, coordinates, reference, size):
|
| 173 |
+
was_training = model.training
|
| 174 |
+
model.eval()
|
| 175 |
+
predicted = predict(model, coordinates, size).astype(np.float64)
|
| 176 |
+
model.train(was_training)
|
| 177 |
+
errors = predicted-reference
|
| 178 |
+
return relative_metrics(np.sum(errors**2, 0), np.sum(reference**2, 0))
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def selection_score(metrics):
|
| 182 |
+
values = [metrics[k]["relative_l2"] for k in ("u", "v", "p")]
|
| 183 |
+
if any(v is None or not np.isfinite(v) for v in values):
|
| 184 |
+
raise ValueError("Validation L2 is undefined/nonfinite")
|
| 185 |
+
return float(np.mean(values))
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def sha256(path):
|
| 189 |
+
digest = hashlib.sha256()
|
| 190 |
+
with open(path, "rb") as stream:
|
| 191 |
+
for block in iter(lambda: stream.read(1024*1024), b""): digest.update(block)
|
| 192 |
+
return digest.hexdigest()
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def load_checkpoint(path, model):
|
| 196 |
+
checkpoint = torch.load(path, map_location=next(model.parameters()).device, weights_only=False)
|
| 197 |
+
model.load_state_dict(checkpoint["model_state_dict"], strict=True)
|
| 198 |
+
return checkpoint
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Reload a trained checkpoint; stream evaluation from the supplied MATLAB HDF5."""
|
| 2 |
+
import argparse
|
| 3 |
+
import json
|
| 4 |
+
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 7 |
+
import h5py
|
| 8 |
+
import numpy as np
|
| 9 |
+
import torch
|
| 10 |
+
from common import (ROOT, atomic_json, build_model, load_checkpoint, load_config,
|
| 11 |
+
predict, relative_metrics, resolved, seed_everything, sha256,
|
| 12 |
+
validation_indices)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def infer(config, checkpoint_path, output_dir, metrics_only=False):
|
| 16 |
+
seed_everything(0, config["training"]["num_threads"])
|
| 17 |
+
model = build_model(config)
|
| 18 |
+
checkpoint = load_checkpoint(checkpoint_path, model)
|
| 19 |
+
if checkpoint["config"]["model"] != config["model"]:
|
| 20 |
+
raise ValueError("Checkpoint and requested model configuration differ")
|
| 21 |
+
model.eval()
|
| 22 |
+
out = Path(output_dir)
|
| 23 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 24 |
+
source_path = resolved(config, "reference")
|
| 25 |
+
report = {"checkpoint": str(Path(checkpoint_path).resolve()), "checkpoint_sha256": sha256(checkpoint_path),
|
| 26 |
+
"source": str(source_path), "source_sha256": sha256(source_path),
|
| 27 |
+
"seed": checkpoint["seed"], "epoch": checkpoint["epoch"], "selection_score": checkpoint["selection_score"],
|
| 28 |
+
"validation_metrics": checkpoint["validation_metrics"],
|
| 29 |
+
"metric": "raw ||pred-ref||2/||ref||2; no pressure gauge alignment",
|
| 30 |
+
"groups": {}, "reference_regenerated": False, "checkpoint_reloaded": True}
|
| 31 |
+
size = config["inference"]["batch_size"]
|
| 32 |
+
global_num, global_den = np.zeros(3), np.zeros(3)
|
| 33 |
+
output = None if metrics_only else h5py.File(out/"predictions.h5.tmp", "w")
|
| 34 |
+
try:
|
| 35 |
+
with h5py.File(source_path, "r") as source:
|
| 36 |
+
for key in config["inference"]["reference_keys"]:
|
| 37 |
+
data = source[key]
|
| 38 |
+
if data.ndim != 2 or data.shape[0] != 6: raise ValueError(f"Invalid {key} shape: {data.shape}")
|
| 39 |
+
count = data.shape[1]
|
| 40 |
+
numer, denom, hold_num, hold_den = [np.zeros(3) for _ in range(4)]
|
| 41 |
+
if key == config["validation"]["reference_key"]:
|
| 42 |
+
valids = validation_indices(config, count)
|
| 43 |
+
group = None
|
| 44 |
+
if output is not None:
|
| 45 |
+
group = output.create_group(key)
|
| 46 |
+
group.create_dataset("coordinates", (count, 3), dtype="f8", chunks=True)
|
| 47 |
+
group.create_dataset("prediction", (count, 3), dtype="f4", chunks=True)
|
| 48 |
+
for start in range(0, count, size):
|
| 49 |
+
end = min(start+size, count)
|
| 50 |
+
rows = data[:, start:end].T
|
| 51 |
+
if not np.isfinite(rows).all(): raise ValueError(f"Nonfinite input in {key} at {start}")
|
| 52 |
+
preds = predict(model, rows[:, :3], size)
|
| 53 |
+
if not np.isfinite(preds).all(): raise FloatingPointError("Nonfinite predictions")
|
| 54 |
+
error2 = (preds.astype(np.float64)-rows[:, 3:])**2
|
| 55 |
+
ref2 = rows[:, 3:]**2
|
| 56 |
+
numer += error2.sum(0)
|
| 57 |
+
denom += ref2.sum(0)
|
| 58 |
+
if key == config["validation"]["reference_key"]:
|
| 59 |
+
keep = ~np.isin(np.arange(start, end), valids, assume_unique=True)
|
| 60 |
+
hold_num += error2[keep].sum(0)
|
| 61 |
+
hold_den += ref2[keep].sum(0)
|
| 62 |
+
if group is not None:
|
| 63 |
+
group["coordinates"][start:end] = rows[:, :3]
|
| 64 |
+
group["prediction"][start:end] = preds
|
| 65 |
+
metrics = relative_metrics(numer, denom)
|
| 66 |
+
report["groups"][key] = {"count": count, "metrics": metrics}
|
| 67 |
+
global_num += numer
|
| 68 |
+
global_den += denom
|
| 69 |
+
if key == config["validation"]["reference_key"]:
|
| 70 |
+
report["internal_holdout"] = {"count": count-len(valids), "metrics": relative_metrics(hold_num, hold_den)}
|
| 71 |
+
print(f"INFERENCE key={key} count={count} metrics={json.dumps(metrics)}", flush=True)
|
| 72 |
+
report["all_reference"] = {"metrics": relative_metrics(global_num, global_den),
|
| 73 |
+
"count": sum(v["count"] for v in report["groups"].values())}
|
| 74 |
+
if output is not None:
|
| 75 |
+
output.attrs["provenance"] = json.dumps({k: v for k, v in report.items() if k not in ["groups", "internal_holdout", "all_reference"]})
|
| 76 |
+
output.close()
|
| 77 |
+
output = None
|
| 78 |
+
(out/"predictions.h5.tmp").replace(out/"predictions.h5")
|
| 79 |
+
atomic_json(out/"metrics.json", report)
|
| 80 |
+
finally:
|
| 81 |
+
if output is not None: output.close()
|
| 82 |
+
print("INFERENCE_COMPLETED", flush=True)
|
| 83 |
+
return report
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
if __name__ == "__main__":
|
| 87 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 88 |
+
parser.add_argument("--config", default=str(ROOT/"config/config.yaml"))
|
| 89 |
+
parser.add_argument("--checkpoint", type=Path)
|
| 90 |
+
parser.add_argument("--output-dir", type=Path)
|
| 91 |
+
parser.add_argument("--seed", type=int)
|
| 92 |
+
parser.add_argument("--kind", choices=["best", "last"], default="best")
|
| 93 |
+
parser.add_argument("--metrics-only", action="store_true")
|
| 94 |
+
args = parser.parse_args()
|
| 95 |
+
cfg = load_config(args.config)
|
| 96 |
+
checkpoint = args.checkpoint or (resolved(cfg, "weights")/f"seed_{args.seed}"/("best_model.pt" if args.kind == "best" else "last_model.pt") if args.seed is not None else resolved(cfg, "weights")/"best_model.pt")
|
| 97 |
+
output = args.output_dir or (resolved(cfg, "results")/f"seed_{args.seed}"/("best_eval" if args.kind == "best" else "final_eval") if args.seed is not None else resolved(cfg, "results")/"best_model")
|
| 98 |
+
infer(cfg, checkpoint, output, args.metrics_only)
|
scripts/result.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Create scientific figures and aggregate ten independent Table 3 runs."""
|
| 2 |
+
import argparse
|
| 3 |
+
import csv
|
| 4 |
+
import json
|
| 5 |
+
import shutil
|
| 6 |
+
import sys
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 9 |
+
import h5py
|
| 10 |
+
import matplotlib
|
| 11 |
+
matplotlib.use("Agg")
|
| 12 |
+
import matplotlib.pyplot as plt
|
| 13 |
+
import numpy as np
|
| 14 |
+
from common import ROOT, atomic_json, load_config, resolved
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def aggregate(config):
|
| 18 |
+
results, weights = resolved(config, "results"), resolved(config, "weights")
|
| 19 |
+
records = []
|
| 20 |
+
for seed in config["training"]["seeds"]:
|
| 21 |
+
directory = results/f"seed_{seed}"
|
| 22 |
+
summary = json.loads((directory/"training_summary.json").read_text())
|
| 23 |
+
if summary["completed_epochs"] != 20000: raise ValueError(f"Incomplete seed {seed}")
|
| 24 |
+
best = json.loads((directory/"best_eval/metrics.json").read_text())
|
| 25 |
+
final = json.loads((directory/"final_eval/metrics.json").read_text())
|
| 26 |
+
records.append({"seed": seed, "best": best, "final": final, "training": summary})
|
| 27 |
+
winner = min(records, key=lambda row: row["best"]["selection_score"])
|
| 28 |
+
shutil.copy2(weights/f"seed_{winner['seed']}"/"best_model.pt", weights/"best_model.pt")
|
| 29 |
+
atomic_json(results/"best_checkpoint_selection.json", {"seed": winner["seed"],
|
| 30 |
+
"epoch": winner["best"]["epoch"], "selection_score": winner["best"]["selection_score"],
|
| 31 |
+
"metrics": winner["best"]["validation_metrics"],
|
| 32 |
+
"checkpoint": str(weights/"best_model.pt"), "phase": "all_ten_formal_runs_completed"})
|
| 33 |
+
reference = {"u": {"mean": 25.73, "std": 2.49}, "v": {"mean": 38.00, "std": 3.76}, "p": {"mean": 42.65, "std": 8.69}}
|
| 34 |
+
statistics = {}
|
| 35 |
+
for kind in ["final", "best"]:
|
| 36 |
+
statistics[kind] = {}
|
| 37 |
+
for field in ("u", "v", "p"):
|
| 38 |
+
values = np.array([r[kind]["groups"]["cavity_internal"]["metrics"][field]["relative_l2_percent"] for r in records])
|
| 39 |
+
statistics[kind][field] = {"mean_percent": float(values.mean()), "std_percent": float(values.std(ddof=0)), "values_percent": values.tolist()}
|
| 40 |
+
losses = np.array([r["training"]["last_loss"]["total"] for r in records])
|
| 41 |
+
run_step_times = []
|
| 42 |
+
for row in records:
|
| 43 |
+
with open(results/f"seed_{row['seed']}"/"history.csv") as stream:
|
| 44 |
+
times = [float(item["step_seconds"]) for item in csv.DictReader(stream)]
|
| 45 |
+
if len(times) != 20000:
|
| 46 |
+
raise ValueError(f"Expected exactly 20000 recorded updates for seed {row['seed']}")
|
| 47 |
+
run_step_times.append(float(np.mean(times)))
|
| 48 |
+
peak_rss = np.array([r["training"]["peak_rss_mb"] for r in records])
|
| 49 |
+
report = {"status": "completed", "runs": len(records), "epochs_per_run": 20000,
|
| 50 |
+
"statistics": statistics, "std_ddof": 0, "paper_table3_percent": reference,
|
| 51 |
+
"final_loss": {"mean": float(losses.mean()), "std": float(losses.std())},
|
| 52 |
+
"parameters": 923,
|
| 53 |
+
"time_per_update_seconds": {"mean": float(np.mean(run_step_times)), "std": float(np.std(run_step_times)), "run_means": run_step_times, "definition": "wall time for PDE/BC/IC forward, backward, gradient clip and Adam step; validation/checkpoint I/O excluded"},
|
| 54 |
+
"peak_process_rss_mb": {"mean": float(peak_rss.mean()), "std": float(peak_rss.std()), "values": peak_rss.tolist()},
|
| 55 |
+
"paper_table3_other": {"parameters": 923, "loss_mean": 0.10, "loss_std": 0.02, "time_seconds_mean": 0.36, "time_seconds_std": 0.01, "memory_mb_mean": 540.0, "memory_mb_std": 0.0},
|
| 56 |
+
"selected_seed": winner["seed"], "selection_metric": "mean validation relative L2",
|
| 57 |
+
"best_model": str(weights/"best_model.pt"),
|
| 58 |
+
"limitations": config["assumptions"]}
|
| 59 |
+
atomic_json(results/"table3_comparison.json", report)
|
| 60 |
+
print(json.dumps(report, indent=2), flush=True)
|
| 61 |
+
return report
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def plot_history(config, seed, destination):
|
| 65 |
+
path = resolved(config, "results")/f"seed_{seed}"/"history.csv"
|
| 66 |
+
with open(path) as stream: rows = list(csv.DictReader(stream))
|
| 67 |
+
epochs = np.array([int(r["epoch"]) for r in rows])
|
| 68 |
+
fig, axes = plt.subplots(1, 2, figsize=(12, 4.2), constrained_layout=True)
|
| 69 |
+
for key in ("total", "physics", "lid", "walls", "initial"):
|
| 70 |
+
axes[0].semilogy(epochs, [max(float(r[key]), 1e-30) for r in rows], label=key, linewidth=.8)
|
| 71 |
+
axes[0].set(xlabel="Epoch (one Adam update)", ylabel="Loss", title=f"Angle-Cascade, seed={seed}")
|
| 72 |
+
valid = [r for r in rows if r["val_u_l2"]]
|
| 73 |
+
for key in ("u", "v", "p"):
|
| 74 |
+
axes[1].plot([int(r["epoch"]) for r in valid], [100*float(r[f"val_{key}_l2"]) for r in valid], label=key)
|
| 75 |
+
axes[1].set(xlabel="Epoch", ylabel="Validation relative L2 (%)", title="Raw u/v/p, no pressure shift")
|
| 76 |
+
for ax in axes: ax.grid(alpha=.2); ax.legend()
|
| 77 |
+
target = destination/"training_curves.png"
|
| 78 |
+
fig.savefig(target, dpi=config["visualization"]["dpi"])
|
| 79 |
+
plt.close(fig)
|
| 80 |
+
return str(target)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def plot_fields(config, predictions, destination):
|
| 84 |
+
images = []
|
| 85 |
+
with h5py.File(resolved(config, "reference"), "r") as source, h5py.File(predictions, "r") as inference:
|
| 86 |
+
key = "cavity_internal"
|
| 87 |
+
raw = source[key]
|
| 88 |
+
checkpoint_meta = json.loads(inference.attrs["provenance"])
|
| 89 |
+
actual_times = np.unique(raw[0, :])
|
| 90 |
+
for desired in config["visualization"]["times"]:
|
| 91 |
+
t = float(actual_times[np.argmin(abs(actual_times-desired))])
|
| 92 |
+
ids = np.flatnonzero(np.isclose(raw[0, :], t, atol=1e-8, rtol=0))
|
| 93 |
+
rows = raw[:, ids].T
|
| 94 |
+
pred = inference[key]["prediction"][ids]
|
| 95 |
+
xs, ys = np.unique(rows[:, 1]), np.unique(rows[:, 2])
|
| 96 |
+
if len(xs)*len(ys) != len(rows): raise ValueError("Reference snapshot is not a complete rectangular grid")
|
| 97 |
+
order = np.lexsort((rows[:, 1], rows[:, 2]))
|
| 98 |
+
truth = rows[order, 3:].reshape(len(ys), len(xs), 3)
|
| 99 |
+
predicted = pred[order].reshape(len(ys), len(xs), 3)
|
| 100 |
+
fig, axes = plt.subplots(3, 3, figsize=(11, 10), constrained_layout=True)
|
| 101 |
+
for index, field in enumerate(("u", "v", "p")):
|
| 102 |
+
low, high = min(truth[..., index].min(), predicted[..., index].min()), max(truth[..., index].max(), predicted[..., index].max())
|
| 103 |
+
if low == high: high = low+1e-12
|
| 104 |
+
for col, values in enumerate((truth[..., index], predicted[..., index], abs(predicted[..., index]-truth[..., index]))):
|
| 105 |
+
cmap = "coolwarm" if index == 2 and col < 2 else "viridis" if col < 2 else "magma"
|
| 106 |
+
kwargs = {"vmin": low, "vmax": high} if col < 2 else {"vmin": 0}
|
| 107 |
+
artist = axes[index, col].pcolormesh(xs, ys, values, shading="auto", cmap=cmap, **kwargs)
|
| 108 |
+
axes[index, col].set(xlabel="x (m)", ylabel="y (m)", title=f"{field}: {['Reference', 'QCPINN', 'Absolute error'][col]}")
|
| 109 |
+
axes[index, col].set_aspect("equal")
|
| 110 |
+
fig.colorbar(artist, ax=axes[index, col], shrink=.8)
|
| 111 |
+
fig.suptitle(f"Lid-driven cavity, t={t:g} s | seed={checkpoint_meta['seed']}, epoch={checkpoint_meta['epoch']} | raw fields")
|
| 112 |
+
target = destination/f"fields_t{t:g}.png"
|
| 113 |
+
fig.savefig(target, dpi=config["visualization"]["dpi"])
|
| 114 |
+
plt.close(fig)
|
| 115 |
+
images.append(str(target))
|
| 116 |
+
fig, axes = plt.subplots(1, 2, figsize=(10, 4.5), constrained_layout=True)
|
| 117 |
+
for ax, data, title in zip(axes, (truth, predicted), ("Reference", "QCPINN")):
|
| 118 |
+
speed = np.linalg.norm(data[..., :2], axis=-1)
|
| 119 |
+
artist = ax.streamplot(xs, ys, data[..., 0], data[..., 1], color=speed, cmap="viridis", density=1.1)
|
| 120 |
+
fig.colorbar(artist.lines, ax=ax, label="Speed (m/s)")
|
| 121 |
+
ax.set(xlabel="x (m)", ylabel="y (m)", title=f"{title}, t={t:g} s", xlim=(0, 1), ylim=(0, 1))
|
| 122 |
+
ax.set_aspect("equal")
|
| 123 |
+
target = destination/f"streamlines_t{t:g}.png"
|
| 124 |
+
fig.savefig(target, dpi=config["visualization"]["dpi"])
|
| 125 |
+
plt.close(fig)
|
| 126 |
+
images.append(str(target))
|
| 127 |
+
return images
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def make_results(config, seed=None, output_dir=None, predictions=None):
|
| 131 |
+
results = resolved(config, "results")
|
| 132 |
+
comparison = None
|
| 133 |
+
if seed is None:
|
| 134 |
+
comparison = json.loads((results/"table3_comparison.json").read_text())
|
| 135 |
+
seed = comparison["selected_seed"]
|
| 136 |
+
evaluation = results/"best_model"
|
| 137 |
+
else: evaluation = results/f"seed_{seed}"/"best_eval"
|
| 138 |
+
output = Path(output_dir) if output_dir else evaluation/"figures"
|
| 139 |
+
output.mkdir(parents=True, exist_ok=True)
|
| 140 |
+
paths = [plot_history(config, seed, output)]
|
| 141 |
+
paths += plot_fields(config, Path(predictions) if predictions else evaluation/"predictions.h5", output)
|
| 142 |
+
metrics = json.loads((evaluation/"metrics.json").read_text())
|
| 143 |
+
lines = ["# QCPINN Angle-Cascade cavity reproduction", "", f"Selected seed: {seed}; epoch: {metrics['epoch']}; 923 parameters.",
|
| 144 |
+
"", "Reference values were read directly from the supplied cavity.mat; no generated reference truth.",
|
| 145 |
+
"", "| Field | Internal relative L2 (%) | Paper Table 3 mean ± std (%) |", "|---|---:|---:|"]
|
| 146 |
+
for k, paper in zip(("u", "v", "p"), ("25.73 ± 2.49", "38.00 ± 3.76", "42.65 ± 8.69")):
|
| 147 |
+
value = metrics["groups"]["cavity_internal"]["metrics"][k]["relative_l2_percent"]
|
| 148 |
+
lines.append(f"| {k} | {value:.6f} | {paper} |")
|
| 149 |
+
if comparison is not None:
|
| 150 |
+
lines += ["", "## Ten independent complete runs (20000 epochs each)", "",
|
| 151 |
+
"| Field | Final checkpoints mean ± std (%) | Validation-best checkpoints mean ± std (%) | Paper Table 3 (%) |",
|
| 152 |
+
"|---|---:|---:|---:|"]
|
| 153 |
+
for field in ("u", "v", "p"):
|
| 154 |
+
final = comparison["statistics"]["final"][field]
|
| 155 |
+
best = comparison["statistics"]["best"][field]
|
| 156 |
+
paper = comparison["paper_table3_percent"][field]
|
| 157 |
+
lines.append(f"| {field} | {final['mean_percent']:.6f} ± {final['std_percent']:.6f} | {best['mean_percent']:.6f} ± {best['std_percent']:.6f} | {paper['mean']:.2f} ± {paper['std']:.2f} |")
|
| 158 |
+
loss = comparison["final_loss"]
|
| 159 |
+
timing = comparison["time_per_update_seconds"]
|
| 160 |
+
memory = comparison["peak_process_rss_mb"]
|
| 161 |
+
lines += ["", f"Final loss: {loss['mean']:.8e} ± {loss['std']:.8e}; paper: 0.10 ± 0.02.",
|
| 162 |
+
f"Time per update: {timing['mean']:.6f} ± {timing['std']:.6f} s; {timing['definition']}.",
|
| 163 |
+
f"Peak process RSS: {memory['mean']:.3f} ± {memory['std']:.3f} MB. Actual hardware is recorded in results/compute_hardware.txt; timing/memory are not hardware-identical to the paper.",
|
| 164 |
+
"Population standard deviation (ddof=0) is used; the paper does not specify this convention."]
|
| 165 |
+
lines += ["", "The selected best checkpoint is a single model; the paper entries are ten-run statistics.",
|
| 166 |
+
"See table3_comparison.json for separately aggregated final and best checkpoints.", "", "## Explicit limitations"]
|
| 167 |
+
lines += [f"- {item}" for item in config["assumptions"]]
|
| 168 |
+
lines += ["- Appendix A.2 printed sums can cancel between components. The literal printed losses were retained.",
|
| 169 |
+
"- The reference contains saved time intervals of 0.1 s, while the paper describes a solver step of 0.01 s.",
|
| 170 |
+
"- Validation uses a fixed reference subset for checkpoint selection. Holdout metrics exclude those records.",
|
| 171 |
+
"", "Source: [paper](https://arxiv.org/pdf/2503.16678), sections 4/5, Figure 2, Tables 2/3 and Appendix A.2."]
|
| 172 |
+
report = evaluation/"report.md"
|
| 173 |
+
report.write_text("\n".join(lines)+"\n")
|
| 174 |
+
atomic_json(evaluation/"visualization_manifest.json", {"figures": paths, "report": str(report), "dpi": config["visualization"]["dpi"], "reference": str(resolved(config, "reference"))})
|
| 175 |
+
print(f"VISUALIZATION_COMPLETED figures={len(paths)} report={report}", flush=True)
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
if __name__ == "__main__":
|
| 179 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 180 |
+
parser.add_argument("--config", default=str(ROOT/"config/config.yaml"))
|
| 181 |
+
parser.add_argument("--aggregate", action="store_true")
|
| 182 |
+
parser.add_argument("--seed", type=int)
|
| 183 |
+
parser.add_argument("--output-dir", type=Path)
|
| 184 |
+
parser.add_argument("--predictions", type=Path)
|
| 185 |
+
args = parser.parse_args()
|
| 186 |
+
cfg = load_config(args.config)
|
| 187 |
+
if args.aggregate: aggregate(cfg)
|
| 188 |
+
else: make_results(cfg, args.seed, args.output_dir, args.predictions)
|
scripts/run_all.slurm
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
#SBATCH --job-name=qcpinn_cavity_public
|
| 3 |
+
#SBATCH --cpus-per-task=4
|
| 4 |
+
#SBATCH --mem=16G
|
| 5 |
+
#SBATCH --time=16:00:00
|
| 6 |
+
set -euo pipefail
|
| 7 |
+
cd "${SLURM_SUBMIT_DIR:?Submit from the model package root}"
|
| 8 |
+
export OMP_NUM_THREADS=1
|
| 9 |
+
export OPENBLAS_NUM_THREADS=1
|
| 10 |
+
export MKL_NUM_THREADS=1
|
| 11 |
+
export MPLBACKEND=Agg
|
| 12 |
+
|
| 13 |
+
pids=()
|
| 14 |
+
for seed in {0..9}; do
|
| 15 |
+
(
|
| 16 |
+
python -u scripts/train.py --config config/config.yaml --seed "$seed"
|
| 17 |
+
python -u scripts/inference.py --config config/config.yaml --seed "$seed" --kind best
|
| 18 |
+
python -u scripts/inference.py --config config/config.yaml --seed "$seed" --kind last --metrics-only
|
| 19 |
+
python -u scripts/result.py --config config/config.yaml --seed "$seed"
|
| 20 |
+
) &
|
| 21 |
+
pids+=("$!")
|
| 22 |
+
if ((${#pids[@]} == 4)); then
|
| 23 |
+
for pid in "${pids[@]}"; do wait "$pid"; done
|
| 24 |
+
pids=()
|
| 25 |
+
fi
|
| 26 |
+
done
|
| 27 |
+
for pid in "${pids[@]}"; do wait "$pid"; done
|
| 28 |
+
python -u scripts/result.py --config config/config.yaml --aggregate
|
| 29 |
+
python -u scripts/inference.py --config config/config.yaml
|
| 30 |
+
python -u scripts/result.py --config config/config.yaml
|
scripts/train.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Formal Table 3 training: 20,000 Adam updates, batch64 per constraint."""
|
| 2 |
+
import argparse
|
| 3 |
+
import csv
|
| 4 |
+
import fcntl
|
| 5 |
+
import json
|
| 6 |
+
import random
|
| 7 |
+
import resource
|
| 8 |
+
import sys
|
| 9 |
+
import time
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 13 |
+
import numpy as np
|
| 14 |
+
import torch
|
| 15 |
+
from common import (ROOT, atomic_json, atomic_save, build_model, evaluate, load_checkpoint,
|
| 16 |
+
load_config, load_validation, loss_terms, make_sobol_pools,
|
| 17 |
+
require_compute_node, resolved, sample_batches, seed_everything,
|
| 18 |
+
selection_score, sha256)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def train(config, seed, resume=None):
|
| 22 |
+
allocation = require_compute_node()
|
| 23 |
+
if seed not in config["training"]["seeds"]:
|
| 24 |
+
raise ValueError("Seed is not in the formal experiment list")
|
| 25 |
+
tcfg = config["training"]
|
| 26 |
+
seed_everything(seed, tcfg["num_threads"])
|
| 27 |
+
out = resolved(config, "results") / f"seed_{seed}"
|
| 28 |
+
weights = resolved(config, "weights") / f"seed_{seed}"
|
| 29 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 30 |
+
weights.mkdir(parents=True, exist_ok=True)
|
| 31 |
+
if (out/"history.csv").exists() and resume is None:
|
| 32 |
+
raise FileExistsError("Existing training history; use --resume to preserve run provenance")
|
| 33 |
+
model = build_model(config)
|
| 34 |
+
pools = make_sobol_pools(config, seed)
|
| 35 |
+
rng = np.random.default_rng(seed+100000)
|
| 36 |
+
xval, yval, valids = load_validation(config)
|
| 37 |
+
np.save(out/"validation_indices.npy", valids)
|
| 38 |
+
optimizer = torch.optim.Adam(model.parameters(), lr=tcfg["learning_rate"],
|
| 39 |
+
betas=tuple(tcfg["betas"]), eps=tcfg["epsilon"],
|
| 40 |
+
weight_decay=tcfg["weight_decay"])
|
| 41 |
+
sched = tcfg["scheduler"]
|
| 42 |
+
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode="min", factor=sched["factor"],
|
| 43 |
+
patience=sched["patience"], threshold=sched["threshold"])
|
| 44 |
+
start_epoch, best_score, best_epoch = 1, float("inf"), None
|
| 45 |
+
accumulated_seconds = 0.0
|
| 46 |
+
if resume:
|
| 47 |
+
previous = load_checkpoint(resume, model)
|
| 48 |
+
if previous["config"] != config or previous["seed"] != seed:
|
| 49 |
+
raise ValueError("Resume config/seed differs from checkpoint")
|
| 50 |
+
optimizer.load_state_dict(previous["optimizer_state_dict"])
|
| 51 |
+
scheduler.load_state_dict(previous["scheduler_state_dict"])
|
| 52 |
+
start_epoch = previous["epoch"]+1
|
| 53 |
+
best_score, best_epoch = previous["best_score"], previous["best_epoch"]
|
| 54 |
+
torch.set_rng_state(previous["torch_rng"])
|
| 55 |
+
random.setstate(previous["python_rng"])
|
| 56 |
+
rng.bit_generator.state = previous["batch_rng"]
|
| 57 |
+
accumulated_seconds = previous["elapsed_seconds"]
|
| 58 |
+
provenance = {"config": config, "seed": seed, "allocation": allocation,
|
| 59 |
+
"reference": str(resolved(config, "reference")),
|
| 60 |
+
"reference_sha256": sha256(resolved(config, "reference")),
|
| 61 |
+
"parameter_count": sum(p.numel() for p in model.parameters()),
|
| 62 |
+
"torch_version": torch.__version__, "epoch_semantics": "one Adam step, 64 points per constraint",
|
| 63 |
+
"reference_used_for_gradient_training": False,
|
| 64 |
+
"validation_indices": str(out/"validation_indices.npy")}
|
| 65 |
+
atomic_json(out/"run_manifest.json", provenance)
|
| 66 |
+
print(f"START seed={seed} allocation={json.dumps(allocation)} parameters=923 epochs=20000 batch=64", flush=True)
|
| 67 |
+
columns = ["epoch", "total", "physics", "lid", "walls", "initial", "lr", "gradient_norm",
|
| 68 |
+
"step_seconds", "elapsed_seconds", "val_u_l2", "val_v_l2", "val_p_l2", "val_mean_l2"]
|
| 69 |
+
mode = "a" if resume else "w"
|
| 70 |
+
start = time.monotonic()
|
| 71 |
+
with open(out/"history.csv", mode, newline="", buffering=1) as history:
|
| 72 |
+
writer = csv.DictWriter(history, fieldnames=columns)
|
| 73 |
+
if not resume: writer.writeheader()
|
| 74 |
+
for epoch in range(start_epoch, tcfg["epochs"]+1):
|
| 75 |
+
tick = time.monotonic()
|
| 76 |
+
model.train()
|
| 77 |
+
optimizer.zero_grad(set_to_none=True)
|
| 78 |
+
terms = loss_terms(model, sample_batches(pools, tcfg["batch_size"], rng), config)
|
| 79 |
+
total = terms["total"]
|
| 80 |
+
if not torch.isfinite(total): raise FloatingPointError(f"Nonfinite loss at {epoch}")
|
| 81 |
+
total.backward()
|
| 82 |
+
grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), tcfg["gradient_clip"], error_if_nonfinite=True)
|
| 83 |
+
optimizer.step()
|
| 84 |
+
values = {k: float(v.detach()) for k, v in terms.items()}
|
| 85 |
+
scheduler.step(values["total"])
|
| 86 |
+
step_seconds = time.monotonic()-tick
|
| 87 |
+
elapsed = accumulated_seconds+time.monotonic()-start
|
| 88 |
+
row = {"epoch": epoch, **values, "lr": optimizer.param_groups[0]["lr"],
|
| 89 |
+
"gradient_norm": float(grad_norm), "step_seconds": step_seconds, "elapsed_seconds": elapsed}
|
| 90 |
+
metrics = None
|
| 91 |
+
if epoch == 1 or epoch % config["validation"]["every"] == 0 or epoch == tcfg["epochs"]:
|
| 92 |
+
metrics = evaluate(model, xval, yval, config["validation"]["batch_size"])
|
| 93 |
+
score = selection_score(metrics)
|
| 94 |
+
row.update({f"val_{k}_l2": metrics[k]["relative_l2"] for k in ("u", "v", "p")})
|
| 95 |
+
row["val_mean_l2"] = score
|
| 96 |
+
if score < best_score:
|
| 97 |
+
best_score, best_epoch = score, epoch
|
| 98 |
+
is_best = True
|
| 99 |
+
else: is_best = False
|
| 100 |
+
state = {"model_state_dict": model.state_dict(), "config": config, "seed": seed,
|
| 101 |
+
"epoch": epoch, "validation_metrics": metrics, "selection_score": score,
|
| 102 |
+
"best_score": best_score, "best_epoch": best_epoch,
|
| 103 |
+
"optimizer_state_dict": optimizer.state_dict(), "scheduler_state_dict": scheduler.state_dict(),
|
| 104 |
+
"torch_rng": torch.get_rng_state(), "python_rng": random.getstate(),
|
| 105 |
+
"batch_rng": rng.bit_generator.state, "elapsed_seconds": elapsed,
|
| 106 |
+
"allocation": allocation, "loss": values}
|
| 107 |
+
if is_best:
|
| 108 |
+
atomic_save(weights/"best_model.pt", state)
|
| 109 |
+
# Publish the validation-best checkpoint at the requested root path
|
| 110 |
+
# throughout concurrent training, with one writer at a time.
|
| 111 |
+
root_weights = resolved(config, "weights")
|
| 112 |
+
root_meta = resolved(config, "results")/"best_checkpoint_selection.json"
|
| 113 |
+
with open(root_weights/".best_model.lock", "a") as lock:
|
| 114 |
+
try:
|
| 115 |
+
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
| 116 |
+
except BlockingIOError:
|
| 117 |
+
# Another seed is publishing; per-seed checkpoint remains authoritative.
|
| 118 |
+
pass
|
| 119 |
+
else:
|
| 120 |
+
incumbent = json.loads(root_meta.read_text()) if root_meta.exists() else None
|
| 121 |
+
if incumbent is None or score < incumbent["selection_score"]:
|
| 122 |
+
atomic_save(root_weights/"best_model.pt", state)
|
| 123 |
+
atomic_json(root_meta, {"seed": seed, "epoch": epoch, "selection_score": score,
|
| 124 |
+
"metrics": metrics, "checkpoint": str(root_weights/"best_model.pt")})
|
| 125 |
+
fcntl.flock(lock, fcntl.LOCK_UN)
|
| 126 |
+
if epoch % tcfg["checkpoint_every"] == 0 or epoch == tcfg["epochs"]:
|
| 127 |
+
atomic_save(weights/"last_model.pt", state)
|
| 128 |
+
atomic_json(out/"progress.json", {"status": "training", "epoch": epoch,
|
| 129 |
+
"epochs": tcfg["epochs"], "loss": values, "validation_metrics": metrics,
|
| 130 |
+
"best_score": best_score, "best_epoch": best_epoch, "allocation": allocation})
|
| 131 |
+
writer.writerow(row)
|
| 132 |
+
line = f"seed={seed} epoch={epoch}/20000 loss={values['total']:.8e} phy={values['physics']:.6e} lid={values['lid']:.6e} walls={values['walls']:.6e} ic={values['initial']:.6e} lr={row['lr']:.6e}"
|
| 133 |
+
if metrics is not None:
|
| 134 |
+
line += " " + " ".join(f"{k}_relL2={metrics[k]['relative_l2']:.8e} ({metrics[k]['relative_l2_percent']:.5f}%)" for k in ("u", "v", "p"))
|
| 135 |
+
print(line, flush=True)
|
| 136 |
+
elapsed = accumulated_seconds + time.monotonic()-start
|
| 137 |
+
atomic_json(out/"training_summary.json", {"status": "completed", "completed_epochs": tcfg["epochs"],
|
| 138 |
+
"seed": seed, "best_epoch": best_epoch, "best_validation_mean_l2": best_score,
|
| 139 |
+
"last_loss": values, "elapsed_seconds": elapsed,
|
| 140 |
+
"peak_rss_mb": resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024,
|
| 141 |
+
"best_checkpoint": str(weights/"best_model.pt"), "last_checkpoint": str(weights/"last_model.pt"),
|
| 142 |
+
"allocation": allocation})
|
| 143 |
+
atomic_json(out/"progress.json", {"status": "completed", "epoch": tcfg["epochs"], "best_epoch": best_epoch})
|
| 144 |
+
print(f"TRAINING_COMPLETED seed={seed} epochs=20000 best_epoch={best_epoch}", flush=True)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
if __name__ == "__main__":
|
| 148 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 149 |
+
parser.add_argument("--config", default=str(ROOT/"config/config.yaml"))
|
| 150 |
+
parser.add_argument("--seed", type=int, required=True)
|
| 151 |
+
parser.add_argument("--resume", type=Path)
|
| 152 |
+
args = parser.parse_args()
|
| 153 |
+
train(load_config(args.config), args.seed, args.resume)
|
weight/best_model.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:498bcc0dbfc053ca4824b1b6cd2914e85536d0aed1e07ad50d5ef3b4a0b11b87
|
| 3 |
+
size 34274
|