yushuang88 commited on
Commit
d647964
·
verified ·
1 Parent(s): 1442679

Upload folder using huggingface_hub

Browse files
.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

  • SHA256: 1eb9445c2f1a50227d77b54c309a9218f3b3845d489d7433f940276289670775
  • Pointer size: 131 Bytes
  • Size of remote file: 244 kB
results/best_model/figures/fields_t10.png ADDED

Git LFS Details

  • SHA256: 7a589ea1cfa9365fa5e507d06cf2cb642b476c62f339677c19dbe9cf15814f7b
  • Pointer size: 131 Bytes
  • Size of remote file: 260 kB
results/best_model/figures/fields_t5.png ADDED

Git LFS Details

  • SHA256: 19d1c6b9f761074feca200f6a0efc3f66fb51c6fd6f6cca61d54ca938441f95c
  • Pointer size: 131 Bytes
  • Size of remote file: 256 kB
results/best_model/figures/streamlines_t1.png ADDED

Git LFS Details

  • SHA256: 04a9c7b9a0f1ad87647b172094e013ab2828d45b464e4caadde33ac47a8e611d
  • Pointer size: 131 Bytes
  • Size of remote file: 643 kB
results/best_model/figures/streamlines_t10.png ADDED

Git LFS Details

  • SHA256: daf3347434a19038de30381ed884f31365ead8a2c5ee82c2f7dd2d6610202166
  • Pointer size: 131 Bytes
  • Size of remote file: 584 kB
results/best_model/figures/streamlines_t5.png ADDED

Git LFS Details

  • SHA256: a946c8bc2ebe81e1e5150bac1d03c142eb5957dfef52ad2151512ec290d97933
  • Pointer size: 131 Bytes
  • Size of remote file: 607 kB
results/best_model/figures/training_curves.png ADDED

Git LFS Details

  • SHA256: fb41f2fe70daae0ade0665ba563ed6bd72436cc799f1d84823a23d987774e769
  • Pointer size: 131 Bytes
  • Size of remote file: 247 kB
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