Efradeca's picture
Upload folder using huggingface_hub
237a3b2 verified
Raw
History Blame Contribute Delete
3.39 kB
"""CPU inference helpers for the demo (loads a trained checkpoint; handles 1- and 3-channel heads).
Kept dependency-light so it also works inside a Hugging Face Space. The model architecture is
imported from the installed ``stress_operator`` package; the HF export bundles a self-contained
copy (see scripts/05_export_to_hf.sh).
"""
from __future__ import annotations
import os
import sys
import numpy as np
import torch
# Make `stress_operator` importable in BOTH layouts: the local repo (package under ../src) and a
# bundled Hugging Face Space (package is a sibling of this file).
_HERE = os.path.dirname(os.path.abspath(__file__))
for _p in (_HERE, os.path.join(_HERE, "..", "src")):
if _p not in sys.path:
sys.path.insert(0, _p)
from stress_operator.models.transolver import build_model # noqa: E402
def load_checkpoint(ckpt_path: str, device: str = "cpu"):
"""Load a trained model. Supports two formats:
- ``*.safetensors`` (HF deployment): weights from safetensors + ``config.json`` (sibling)
holding the model config, normalizer mean/std, and scale_S.
- ``*.pt`` (local training checkpoint): a dict with state_dict / normalizer / config.
"""
if ckpt_path.endswith(".safetensors"):
import json
from safetensors.torch import load_file
cfg_path = os.path.join(os.path.dirname(ckpt_path) or ".", "config.json")
with open(cfg_path) as f:
cfg = json.load(f)
model_cfg = cfg["model"]
state_dict = load_file(ckpt_path, device=device)
mean = torch.tensor(float(cfg["normalizer"]["mean"]), device=device).reshape(1, 1, 1)
std = torch.tensor(float(cfg["normalizer"]["std"]), device=device).reshape(1, 1, 1)
scale_S = cfg.get("scale_S", None)
metrics = cfg.get("metrics", {})
else:
ckpt = torch.load(ckpt_path, map_location=device, weights_only=False)
model_cfg = ckpt["config"]["model"]
state_dict = ckpt["state_dict"]
mean = ckpt["normalizer"]["mean"].to(device)
std = ckpt["normalizer"]["std"].to(device)
scale_S = ckpt.get("scale_S", None)
metrics = ckpt.get("metrics", {})
model = build_model(model_cfg).to(device)
model.load_state_dict(state_dict)
model.eval()
info = {
"out_dim": model_cfg.get("out_dim", 1),
"scale_S": scale_S,
"attention": model_cfg.get("attention", "physics"),
"metrics": metrics,
}
return model, (mean, std), info
@torch.no_grad()
def predict_stress(model, coords: np.ndarray, norm, info, device: str = "cpu") -> np.ndarray:
"""coords: (N, 2) -> per-node von Mises stress (N,) in physical units."""
mean, std = norm
x = torch.as_tensor(coords, dtype=torch.float32, device=device).unsqueeze(0) # (1,N,2)
out = model(x, None)[0] # (N, out_dim)
if info["out_dim"] == 3:
from stress_operator.losses.equilibrium import von_mises
S = info["scale_S"] or 1.0
stress = von_mises(out) * S # 3-channel tensor -> von Mises, undo target scale
else:
# de-normalize scalar prediction; reshape so the broadcast of a (1,1[,1]) normalizer
# against out[:,0] (N,) cannot leak a leading axis (audit bug 1). Always returns (N,).
stress = (out[:, 0] * std.reshape(()) + mean.reshape(()))
return stress.reshape(-1).detach().cpu().numpy()