Download predict.py from minjiechen/MagNet-Transformers: direct link, hf CLI and curl.
- Browser
- Download file 8.75 kB
-
https://huggingface.co/minjiechen/MagNet-Transformers/resolve/main/predict.py
- Command line
-
hf download hf://minjiechen/MagNet-Transformers/predict.py
-
curl -L -o predict.py https://huggingface.co/minjiechen/MagNet-Transformers/resolve/main/predict.py
8.75 kB
| """Standalone CPU inference for the converted MagNet steady-state Transformers. | |
| Run from an unpacked model repository. This example only loads ONNX/JSON/NPY | |
| files and never unpickles the original PyTorch checkpoints. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import math | |
| from pathlib import Path | |
| import numpy as np | |
| import onnxruntime as ort | |
| ROOT = Path(__file__).resolve().parent | |
| MATERIALS = ("3C90", "3C94", "3E6", "3F4", "77", "78", "N27", "N30", "N49", "N87") | |
| def _finite(value: float, name: str) -> float: | |
| number = float(value) | |
| if not math.isfinite(number): | |
| raise ValueError(f"{name} must be finite") | |
| return number | |
| def make_waveform(kind: str, flux_t: float, duty: float = 0.5) -> np.ndarray: | |
| """Return 128 B samples in T, following the historical MagNet convention.""" | |
| flux_t = _finite(flux_t, "flux_t") | |
| if flux_t <= 0: | |
| raise ValueError("flux_t must be a positive peak AC flux density in T") | |
| if kind == "sine": | |
| return (flux_t * np.sin(np.linspace(0, 2 * np.pi, 129)))[:-1] | |
| if kind != "triangle": | |
| raise ValueError("waveform must be sine or triangle") | |
| duty = _finite(duty, "duty") | |
| if not 0 < duty < 1: | |
| raise ValueError("triangle duty ratio must lie strictly between 0 and 1") | |
| b = np.interp(np.linspace(0, 1, 129), [0, duty, 1], [-flux_t, flux_t, -flux_t])[:-1] | |
| phase = 0.5 - duty / 2 if duty <= 0.5 else -duty / 2 | |
| return np.roll(b, int(phase * 128)) | |
| def load_waveform_json(path: Path) -> np.ndarray: | |
| values = json.loads(path.read_text(encoding="utf-8")) | |
| if not isinstance(values, list) or len(values) != 128: | |
| raise ValueError("--b-json must contain a JSON array of exactly 128 B values in T") | |
| if any(isinstance(value, bool) or not isinstance(value, (float, int)) for value in values): | |
| raise ValueError("B values must all be JSON numbers") | |
| return np.asarray(values, dtype=np.float64) | |
| def _normal(value: float, center: np.float32, scale: np.float32) -> np.float32: | |
| # Preserve the float32 arithmetic of the legacy PyTorch inference. | |
| return np.float32((np.float32(value) - center) / scale) | |
| def measured_hull( | |
| material: str, | |
| frequency_hz: float, | |
| flux_t: float, | |
| bias_a_per_m: float, | |
| temperature_c: float, | |
| duty: float, | |
| ) -> bool: | |
| equations = np.load(ROOT / "domain" / f"hull_{material}.npy", allow_pickle=False) | |
| if ( | |
| equations.ndim != 2 | |
| or equations.shape[1] != 6 | |
| or not np.issubdtype(equations.dtype, np.number) | |
| ): | |
| raise ValueError(f"invalid numeric measured-domain hull for {material}") | |
| point = np.array([frequency_hz, flux_t, bias_a_per_m, temperature_c, duty], dtype=np.float64) | |
| return bool(np.all(equations[:, :5] @ point + equations[:, 5] <= 1e-10)) | |
| def predict( | |
| material: str, | |
| b_waveform_t: np.ndarray, | |
| frequency_hz: float, | |
| temperature_c: float, | |
| bias_a_per_m: float, | |
| nominal_flux_t: float | None = None, | |
| duty: float | None = None, | |
| ) -> dict: | |
| """Predict the 128-point H waveform and signed loss density. | |
| nominal_flux_t and duty enable an ideal-waveform hull check. Omit both for | |
| an arbitrary B waveform, for which this historical hull is not applicable. | |
| """ | |
| if material not in MATERIALS: | |
| raise ValueError(f"unknown material {material!r}; choose from {', '.join(MATERIALS)}") | |
| b = np.asarray(b_waveform_t, dtype=np.float64) | |
| if b.shape != (128,) or not np.isfinite(b).all() or np.max(b) == np.min(b): | |
| raise ValueError("B waveform must have 128 finite, varying samples in T") | |
| frequency_hz = _finite(frequency_hz, "frequency_hz") | |
| temperature_c = _finite(temperature_c, "temperature_c") | |
| bias_a_per_m = _finite(bias_a_per_m, "bias_a_per_m") | |
| if frequency_hz <= 0: | |
| raise ValueError("frequency_hz must be greater than zero") | |
| if (nominal_flux_t is None) != (duty is None): | |
| raise ValueError("nominal_flux_t and duty must both be given or both omitted") | |
| if nominal_flux_t is not None: | |
| nominal_flux_t = _finite(nominal_flux_t, "nominal_flux_t") | |
| duty = _finite(duty, "duty") | |
| if nominal_flux_t <= 0 or not 0 < duty < 1: | |
| raise ValueError("nominal_flux_t must be positive and duty strictly between 0 and 1") | |
| norm_values = json.loads((ROOT / material / "norm.json").read_text(encoding="utf-8")) | |
| if not isinstance(norm_values, list) or len(norm_values) != 10: | |
| raise ValueError("normalizer must contain ten values") | |
| norm = np.asarray(norm_values, dtype=np.float32) | |
| if not np.isfinite(norm).all() or np.any(norm[[1, 3, 5, 7]] == 0): | |
| raise ValueError("invalid normalization values") | |
| source = ((b.astype(np.float32) - norm[0]) / norm[1]).reshape(1, 128, 1) | |
| variables = np.array( | |
| [ | |
| _normal(np.float32(np.log10(frequency_hz)), norm[2], norm[3]), | |
| _normal(temperature_c, norm[4], norm[5]), | |
| _normal(bias_a_per_m, norm[6], norm[7]), | |
| ], | |
| dtype=np.float32, | |
| ).reshape(1, 3) | |
| options = ort.SessionOptions() | |
| options.intra_op_num_threads = 1 | |
| encoder = ort.InferenceSession( | |
| str(ROOT / material / "encoder.onnx"), options, providers=["CPUExecutionProvider"] | |
| ) | |
| decoder = ort.InferenceSession( | |
| str(ROOT / material / "decoder.onnx"), options, providers=["CPUExecutionProvider"] | |
| ) | |
| memory = encoder.run(["memory"], {"source": source, "variables": variables})[0] | |
| if memory.shape != (1, 128, 24): | |
| raise ValueError("unexpected encoder output shape") | |
| target = np.zeros((1, 129, 1), dtype=np.float32) | |
| target[0, 0, 0] = 0.1 | |
| for step in range(1, 129): | |
| output = decoder.run(["output"], {"memory": memory, "target": target})[0] | |
| target[0, step, 0] = output[0, step - 1, 0] | |
| output = decoder.run(["output"], {"memory": memory, "target": target})[0] | |
| if output.shape != (1, 129, 1): | |
| raise ValueError("unexpected decoder output shape") | |
| h = (output[0, :128, 0] * norm[9] + norm[8]).astype(np.float32) | |
| loss = float(frequency_hz * np.trapezoid(h, b)) | |
| if not np.isfinite(h).all() or not math.isfinite(loss): | |
| raise ValueError("model returned a non-finite prediction") | |
| in_hull = None | |
| if nominal_flux_t is not None: | |
| in_hull = measured_hull( | |
| material, frequency_hz, nominal_flux_t, bias_a_per_m, temperature_c, duty | |
| ) | |
| warnings = [] | |
| if in_hull is False: | |
| warnings.append("Operating point lies outside the historical measured-data convex hull.") | |
| if in_hull is None: | |
| warnings.append("Measured-domain hull is not applicable to an arbitrary waveform.") | |
| if loss < 0: | |
| warnings.append("Negative signed B-H loop integral; inspect waveform and operating point.") | |
| return { | |
| "model_id": "legacy-transformer-v1", | |
| "material_id": material, | |
| "frequency_hz": frequency_hz, | |
| "temperature_c": temperature_c, | |
| "bias_a_per_m": bias_a_per_m, | |
| "b_waveform_t": b.tolist(), | |
| "h_waveform_a_per_m": h.tolist(), | |
| "loss_w_per_m3": loss, | |
| "inside_measured_hull": in_hull, | |
| "units": {"flux_density": "T", "field_strength": "A/m", "frequency": "Hz", "loss": "W/m^3"}, | |
| "warnings": warnings, | |
| } | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--material", required=True, choices=MATERIALS) | |
| parser.add_argument("--frequency-hz", type=float, required=True) | |
| parser.add_argument("--temperature-c", type=float, default=25.0) | |
| parser.add_argument("--bias-a-per-m", type=float, default=0.0) | |
| waveform = parser.add_mutually_exclusive_group() | |
| waveform.add_argument("--waveform", choices=("sine", "triangle"), default="sine") | |
| waveform.add_argument("--b-json", type=Path) | |
| parser.add_argument( | |
| "--flux-t", type=float, help="positive peak AC B in T for an ideal waveform" | |
| ) | |
| parser.add_argument("--duty", type=float, default=0.5, help="triangle peak-time fraction (0,1)") | |
| args = parser.parse_args() | |
| if args.b_json is not None: | |
| if args.flux_t is not None: | |
| parser.error("--flux-t cannot be combined with --b-json") | |
| b = load_waveform_json(args.b_json) | |
| flux = duty = None | |
| else: | |
| if args.flux_t is None: | |
| parser.error("--flux-t is required for --waveform") | |
| b = make_waveform(args.waveform, args.flux_t, args.duty) | |
| flux = args.flux_t | |
| duty = args.duty if args.waveform == "triangle" else 0.5 | |
| result = predict( | |
| args.material, b, args.frequency_hz, args.temperature_c, args.bias_a_per_m, flux, duty | |
| ) | |
| print(json.dumps(result, indent=2)) | |
| if __name__ == "__main__": | |
| main() | |