phanerozoic's picture
Direct coil predictors for Coilstellaration
44c12b3
Raw History Blame Contribute Delete
3.38 kB
"""A trained direct predictor as an ONNX file under the benchmark's contract: python export.py NAME OUT.onnx
Reads ml/runs/NAME.pt and writes a graph from boundary_r_cos, boundary_z_sin (5, 9), requirement_metrics (4),
n_field_periods and n_coils_per_half_period to coil_x_n, coil_y_n, coil_z_n (K, 15) and currents (K) in amperes,
with the metadata key supported_tracks set to fixed_shape. The scalings of the inputs and outputs are constants of
the graph. Prints the largest difference between the graph's outputs in onnxruntime and the network's in PyTorch.
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import onnx
import onnxruntime as ort
import torch
from torch import nn
from train import Net
HERE = Path(__file__).resolve().parent
class Wrapped(nn.Module):
def __init__(self, ck):
super().__init__()
self.k = int(ck["k"])
self.net = Net(int(ck["varies"].sum()), self.k * 45 + self.k, int(ck["width"]), int(ck["depth"]))
self.net.load_state_dict(ck["net"])
self.register_buffer("pick", torch.nonzero(ck["varies"]).reshape(-1))
for name in ("x_mean", "x_std", "y_mean", "y_std"):
self.register_buffer(name, ck[name].float())
def forward(self, boundary_r_cos, boundary_z_sin, requirement_metrics, n_field_periods, n_coils_per_half_period):
x = torch.cat([boundary_r_cos.reshape(45), boundary_z_sin.reshape(45), requirement_metrics.log()])
x = (x.index_select(0, self.pick) - self.x_mean) / self.x_std
y = self.net(x.reshape(1, -1)).reshape(-1) * self.y_std + self.y_mean
# the two shape inputs are part of the contract; the graph is for one shape, so they add nothing
y = y + 0.0 * (n_field_periods + n_coils_per_half_period).to(y.dtype)
coef = y[:self.k * 45].reshape(self.k, 3, 15)
return coef[:, 0], coef[:, 1], coef[:, 2], y[self.k * 45:] * 1e6
def main(name, out):
ck = torch.load(HERE / "runs" / f"{name}.pt", map_location="cpu")
model = Wrapped(ck).eval()
feed = (torch.randn(5, 9) * 0.05, torch.randn(5, 9) * 0.05, torch.tensor([0.4, 0.8, 3.0, 0.05]), torch.tensor(3), torch.tensor(5))
feed[0][0, 4] = 1.0
names_in = ["boundary_r_cos", "boundary_z_sin", "requirement_metrics", "n_field_periods", "n_coils_per_half_period"]
names_out = ["coil_x_n", "coil_y_n", "coil_z_n", "currents"]
torch.onnx.export(model, feed, out, input_names=names_in, output_names=names_out, opset_version=17, dynamo=False)
proto = onnx.load(out)
proto.metadata_props.append(onnx.StringStringEntryProto(key="supported_tracks", value="fixed_shape"))
onnx.save(proto, out)
onnx.checker.check_model(out)
sess = ort.InferenceSession(out, providers=["CPUExecutionProvider"])
got = sess.run(None, {n: (v.numpy().astype(np.int64) if v.dtype == torch.int64 else v.numpy()) for n, v in zip(names_in, feed)})
with torch.no_grad():
want = model(*feed)
worst = max(float(np.abs(g - w.numpy()).max() / max(1.0, float(w.abs().max()))) for g, w in zip(got, want))
print(f"wrote {out}: {Path(out).stat().st_size / 2 ** 20:.1f} MiB, dev feasible in training {ck.get('dev_feasible')}, "
f"largest relative difference from PyTorch {worst:.2e}, inputs {[i.name for i in sess.get_inputs()]}")
if __name__ == "__main__":
main(*sys.argv[1:3])