| """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 |
|
|
| |
| |
| _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 |
|
|
|
|
| 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) |
| out = model(x, None)[0] |
| 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 |
| else: |
| |
| |
| stress = (out[:, 0] * std.reshape(()) + mean.reshape(())) |
| return stress.reshape(-1).detach().cpu().numpy() |
|
|