File size: 6,029 Bytes
0f0d1f4 324ad96 0f0d1f4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | """Configuration, exact MAT contract, sampling and checkpoint utilities."""
from __future__ import annotations
import hashlib
import json
import random
from pathlib import Path
import numpy as np
import torch
import yaml
from scipy.io import loadmat
ROOT = Path(__file__).resolve().parents[1]
def read_config(path=None):
path = Path(path) if path else ROOT / "config/config.yaml"
with path.open() as handle:
cfg = yaml.safe_load(handle)
if cfg["model"]["hidden_layers"] != [50] * 4:
raise ValueError("Formal experiment requires exactly four 50-unit hidden layers")
if cfg["training"]["iterations"] != 4000:
raise ValueError("Formal experiment requires exactly 4000 iterations")
return cfg
def resolve(path):
p = Path(path)
return p if p.is_absolute() else ROOT / p
def save_json(path, value):
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(path.suffix + ".tmp")
tmp.write_text(json.dumps(value, indent=2, ensure_ascii=False, allow_nan=False))
tmp.replace(path)
def setup(cfg, device=None):
seed = int(cfg["seed"])
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.set_num_threads(int(cfg["runtime"]["cpu_threads"]))
dtype = getattr(torch, cfg["model"]["dtype"])
torch.set_default_dtype(dtype)
device = torch.device(device or cfg["runtime"]["device"])
if device.type == "cuda" and not torch.cuda.is_available():
raise RuntimeError("Requested accelerator is unavailable; run inside SLURM allocation")
return device, dtype
def build_model(cfg, device):
from model.TSAPINN import TSAPINN
m = cfg["model"]
return TSAPINN(**{k: m[k] for k in ["hidden_layers", "sigma", "zeta_sin", "zeta_cos", "reynolds", "bias_initialization"]}).to(device)
def load_dns(cfg):
path = Path(cfg["data"]["path"])
if not path.is_file():
raise FileNotFoundError(f"Required author DNS data absent: {path}")
raw = loadmat(path)
for key, shape in cfg["data"]["expected_shapes"].items():
if raw[key].shape != tuple(shape) or not np.isfinite(raw[key]).all():
raise ValueError(f"Invalid DNS field {key}: expected finite {shape}")
xy = np.asarray(raw["X_star"], dtype=np.float64)
times = np.asarray(raw["t"], dtype=np.float64).ravel()
if not np.all(np.diff(times) > 0):
raise ValueError("DNS time must be strictly increasing")
return {"xy": xy, "t": times, "uv": np.asarray(raw["U_star"]).transpose(2, 0, 1),
"p": np.asarray(raw["p_star"]).T, "path": str(path),
"sha256": hashlib.sha256(path.read_bytes()).hexdigest()}
def sample_indices(cfg, dns):
"""Literal paper per-time counts, geometric boundary; fixed throughout training.
Only 296 unique boundary nodes exist. Balanced sampling with replacement
provides 100 direct DNS observations per side without inventing new labels.
Boundary corner duplication and replacement are explicit implementation choices.
"""
rng = np.random.RandomState(cfg["seed"])
xy, times = dns["xy"], dns["t"]
xmin, ymin = xy.min(0)
xmax, ymax = xy.max(0)
sides = [np.flatnonzero(np.isclose(xy[:, 0], xmin)), np.flatnonzero(np.isclose(xy[:, 0], xmax)),
np.flatnonzero(np.isclose(xy[:, 1], ymin)), np.flatnonzero(np.isclose(xy[:, 1], ymax))]
boundary = np.unique(np.concatenate(sides))
interior = np.setdiff1d(np.arange(len(xy)), boundary)
nt, tr = len(times), cfg["training"]
nb, nr = int(tr["boundary_points_per_time"]), int(tr["residual_points_per_time"])
if nb % 4:
raise ValueError("Balanced side sampling needs boundary count divisible by 4")
bc = np.stack([np.concatenate([rng.choice(side, nb // 4, replace=True) for side in sides]) for _ in times])
res = np.stack([rng.choice(interior, nr, replace=False) for _ in times])
val = np.stack([rng.choice(np.setdiff1d(interior, res[k]), tr["validation_points_per_time"], replace=False) for k in range(nt)])
return {"boundary": bc, "residual": res, "validation": val}
def gather(dns, indices):
nt, n = indices.shape
xy = dns["xy"][indices]
time = np.broadcast_to(dns["t"][:, None, None], (nt, n, 1))
coordinates = np.concatenate((xy, time), 2).reshape(-1, 3)
labels = dns["uv"][np.arange(nt)[:, None], indices].reshape(-1, 2)
return coordinates, labels
def tensor(array, device, dtype):
return torch.as_tensor(np.ascontiguousarray(array), dtype=dtype, device=device)
def predict(model, coordinates, device, dtype, batch_size):
values = []
model.eval()
with torch.no_grad():
for start in range(0, len(coordinates), batch_size):
q = tensor(coordinates[start:start + batch_size], device, dtype)
values.append(model.flow_taylor(q).cpu().numpy())
return np.concatenate(values)
def relative_l2(prediction, reference):
difference = np.asarray(prediction, dtype=np.float64) - reference
numerator = np.sum(difference * difference, axis=0, dtype=np.float64)
denominator = np.sum(np.asarray(reference, dtype=np.float64)**2, axis=0, dtype=np.float64)
return np.sqrt(numerator / denominator)
def atomic_checkpoint(path, payload):
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(".tmp")
torch.save(payload, tmp)
tmp.replace(path)
def load_checkpoint(cfg, device, path=None):
path = resolve(path or cfg["outputs"]["checkpoint"])
checkpoint = torch.load(path, map_location=device, weights_only=False)
saved_config = checkpoint["config"].copy()
saved_config["data"] = saved_config["data"].copy()
saved_config["data"]["path"] = cfg["data"]["path"]
if saved_config != cfg:
raise ValueError("Checkpoint configuration does not match requested configuration")
model = build_model(cfg, device)
model.load_state_dict(checkpoint["model_state_dict"], strict=True)
model.eval()
return model, checkpoint
|