yushuang88's picture
Upload folder using huggingface_hub
eae86f2 verified
Raw History Blame Contribute Delete
5.45 kB
"""Shared configuration, analytic sampling, and reference-only evaluation."""
import hashlib
import json
from pathlib import Path
import numpy as np
import torch
import yaml
LOSS_NAMES = ('u_in', 'v_in', 'p_out', 'u_noslip', 'v_noslip', 'u_res', 'v_res', 'p_res')
def load_config(path):
path = Path(path).resolve()
cfg = yaml.safe_load(path.read_text())
root = (path.parent / cfg['paths']['output_dir']).resolve()
reference = Path(cfg['evaluation']['reference_vtu'])
if not reference.is_absolute():
cfg['evaluation']['reference_vtu'] = str(root / reference)
cfg['_root'] = str(root)
return cfg, root
def atomic_json(path, obj):
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_name(path.name + '.tmp')
tmp.write_text(json.dumps(obj, indent=2, ensure_ascii=False, allow_nan=False))
tmp.replace(path)
def atomic_save(path, obj):
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_name(path.name + '.tmp')
torch.save(obj, tmp)
tmp.replace(path)
def sample_interior(cfg, device, generator=None, count=None):
d = cfg['domain']
n = cfg['training']['batch_size'] if count is None else count
z = torch.rand(n, 2, device=device, generator=generator)
low = z.new_tensor([d['x_min'], d['y_min']])
span = z.new_tensor([d['x_max'] - d['x_min'], d['y_max'] - d['y_min']])
return low + span * z
def analytic_boundaries(cfg, device):
d, b = cfg['domain'], cfg['boundary']
yl = torch.linspace(d['y_min'], d['y_max'], b['left_points'], device=device)
left = torch.stack((torch.full_like(yl, d['x_min']), yl), -1)
# Group the whole left boundary in the two left-boundary loss terms,
# as in the author's discretization: its lower half has zero velocity.
inlet = left
yo = torch.linspace(d['y_min'], d['y_max'], b['outlet_points'], device=device)
outlet = torch.stack((torch.full_like(yo, d['x_max']), yo), -1)
x = torch.linspace(d['x_min'], d['x_max'], b['points_per_horizontal_wall'], device=device)
walls = torch.cat((torch.stack((x, torch.full_like(x, d['y_min'])), -1),
torch.stack((x, torch.full_like(x, d['y_max'])), -1)))
u_in = torch.where(yl >= 0, 24.0 * yl * (0.5 - yl), torch.zeros_like(yl))
return inlet, outlet, walls, u_in
def boundary_losses(model, boundary):
inlet, outlet, walls, target = boundary
sizes = [len(inlet), len(outlet), len(walls)]
pi, po, pw = model(torch.cat((inlet, outlet, walls))).split(sizes)
return torch.stack(((pi[:, 0] - target).square().mean(), pi[:, 1].square().mean(),
po[:, 2].square().mean(), pw[:, 0].square().mean(), pw[:, 1].square().mean()))
def read_reference(path, expected_points=None):
# VTU is a genuine SU2 field. Never called by sample_interior/analytic_boundaries.
from vtk import vtkXMLUnstructuredGridReader
from vtk.util.numpy_support import vtk_to_numpy
p = Path(path)
reader = vtkXMLUnstructuredGridReader()
reader.SetFileName(str(p))
reader.Update()
mesh = reader.GetOutput()
n = mesh.GetNumberOfPoints()
if expected_points is not None and n != expected_points:
raise ValueError(f'Expected {expected_points} reference points, got {n}')
vel = mesh.GetPointData().GetArray('Velocity')
pressure = mesh.GetPointData().GetArray('Pressure')
if vel is None or pressure is None:
raise ValueError('VTU must contain Velocity and Pressure PointData')
xy = vtk_to_numpy(mesh.GetPoints().GetData())[:, :2].copy()
velocity = vtk_to_numpy(vel).copy()
uvp = np.column_stack((velocity[:, 0], velocity[:, 1], vtk_to_numpy(pressure)))
if not np.isfinite(xy).all() or not np.isfinite(uvp).all():
raise ValueError('Nonfinite reference data')
metadata = {'source': str(p.resolve()), 'sha256': hashlib.sha256(p.read_bytes()).hexdigest(),
'points': n, 'target_field': 'Velocity', 'target_component': 0,
'training_labels_used': False, 'bounds': [xy.min(0).tolist(), xy.max(0).tolist()]}
return xy, uvp, metadata, mesh
@torch.no_grad()
def predict(model, xy, device, batch_size=4096):
pred = []
for start in range(0, len(xy), batch_size):
x = torch.as_tensor(xy[start:start + batch_size], dtype=torch.float32, device=device)
pred.append(model(x).cpu().numpy())
return np.concatenate(pred)
def errors(pred, ref):
# Accumulate in float64, unweighted over every original reference mesh point.
delta = pred.astype(np.float64) - ref.astype(np.float64)
return {'relative_l2_u': float(np.linalg.norm(delta[:, 0]) / np.linalg.norm(ref[:, 0].astype(np.float64))),
'relative_l2_v': float(np.linalg.norm(delta[:, 1]) / np.linalg.norm(ref[:, 1].astype(np.float64)))}
def build_model(cfg, device):
from model.PseudoTimePINN import PseudoTimePINN
d = cfg['domain']
return PseudoTimePINN(**cfg['model'], length=d['x_max'] - d['x_min'],
height=d['y_max'] - d['y_min']).to(device)
def require_compute_node(cfg):
import os
if cfg['training']['require_slurm'] and not os.environ.get('SLURM_JOB_ID'):
raise RuntimeError('DCU execution requires a Slurm allocation; login-node training is forbidden')
if not torch.cuda.is_available():
raise RuntimeError('Allocated DCU is not accessible through torch.cuda')