File size: 8,751 Bytes
ebe0477 | 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 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | """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()
|