typhoon-predict / release_tools /verify_pressure_field_export.py
euler314's picture
Publish Trackformer 1.2 announcement assets and matched DeepMind benchmark protocol
4ebcd24 verified
Raw History Blame Contribute Delete
5.78 kB
"""CPU-only export regression using an existing causal batched issue packet.
This checks packaging/field consistency, not forecast skill. It never calls MPS,
changes neural weights, or writes into historical archive/training directories.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import shutil
import sys
from pathlib import Path
from unittest.mock import patch
import numpy as np
import torch
ROOT = Path(__file__).resolve().parents[1]
MODEL = ROOT / "models/trackformer_1_2_field"
sys.path.insert(0, str(MODEL))
import predict
from model import CoreForecaster
from baseline_model import base
from plot_pressure import render_pressure_map
def sha(path):
with Path(path).open("rb") as stream:
return hashlib.file_digest(stream, "sha256").hexdigest()
def verify(batched_packet, weights, work):
work = work.resolve()
if not work.is_relative_to(Path("/Volumes/D")):
raise ValueError("Diagnostic artifacts must remain on D")
work.mkdir(parents=True, exist_ok=True)
directory = work / "inference"
directory.mkdir(exist_ok=True)
for source in MODEL.iterdir():
if source.is_file() and source.suffix in (".py", ".md", ".json"):
shutil.copy2(source, directory / source.name)
shutil.copy2(weights, directory / "weights.pt")
metadata = json.loads((directory / "manifest.json").read_text())
for name, expected in metadata["source_module_sha256"].items():
if sha(directory / name) != expected:
raise ValueError("Frozen neural module changed: " + name)
with np.load(batched_packet, allow_pickle=False) as source:
arrays = {key: np.asarray(source[key])[0] for key in predict.INPUT_SHAPES}
arrays["history_time_ns"] = np.asarray(source["history_time_ns"], dtype=np.int64)
arrays["issue_time_ns"] = np.asarray(arrays["history_time_ns"][-1], dtype=np.int64)
packet = work / "causal_issue_packet.npz"
np.savez_compressed(packet, **arrays)
captured, origins = [], []
class CapturingModel(CoreForecaster):
def step(self, state):
next_state, out = super().step(state)
captured.append(out)
origins.append(next_state["origin"])
return next_state, out
torch.set_num_threads(2)
torch.set_num_interop_threads(1)
with patch.object(predict, "CoreForecaster", CapturingModel):
result = predict.forecast(packet, directory, "cpu")
contract = metadata["data_contract"]
scale, offset = contract["normalization"]["std"][0], contract["normalization"]["mean"][0]
reference = {
"track_lat_lon": torch.stack([o["center"][0] for o in captured]).numpy(),
"central_pressure_hpa": torch.stack([o["pressure"][0] for o in captured]).numpy(),
"basin_mslp_hpa": torch.stack([o["global"][0,0] for o in captured]).numpy()*scale+offset,
"regional_mslp_hpa": torch.stack([o["regional"][0,0] for o in captured]).numpy()*scale+offset,
"maximum_wind_auxiliary_kt": torch.stack([o["vmax"][0] for o in captured]).numpy(),
}
for key, expected in reference.items():
np.testing.assert_array_equal(result[key], expected, err_msg="Original readout changed: " + key)
alignment = []
for i, (out, origin) in enumerate(zip(captured, origins)):
# The original model's own geographic sampler, not a centre/minimum fit.
grid = CoreForecaster.core_grid(None, out["center"][:,0,None,None], out["center"][:,1,None,None], origin)
sampled = base.sample_field(torch.from_numpy(result["core_mslp_hpa"][i])[None,None], grid)[0,0,0,0]
alignment.append(abs(float(sampled) - float(result["central_pressure_hpa"][i])))
np.testing.assert_array_equal(result["core_latitude_deg"][i], out["core_lat"][0].numpy())
np.testing.assert_array_equal(result["core_longitude_deg"][i], out["core_lon"][0].numpy())
np.testing.assert_array_equal(result["core_valid"][i], out["core_valid"][0,0].numpy())
if max(alignment) > 1e-3:
raise ValueError("Exported core does not reproduce its original central-pressure readout")
np.savez_compressed(work / "forecast.npz", **result)
for lead in (6, 120):
render_pressure_map(result, work / f"pressure_{lead:03d}h.png", lead, 4)
receipt = {
"state": "verified_cpu_export_regression",
"scientific_benchmark": False,
"model": "Trackformer 1.2", "members": 1,
"device": "cpu", "forecast_steps": 20,
"issue_time_ns": int(result["issue_time_ns"]),
"core_shape": list(result["core_mslp_hpa"].shape),
"core_min_hpa": float(result["core_mslp_hpa"][result["core_valid"]].min()),
"core_max_hpa": float(result["core_mslp_hpa"][result["core_valid"]].max()),
"central_pressure_alignment_max_error_hpa": max(alignment),
"original_outputs_bit_identical": list(reference),
"coordinates_and_masks_match_forward_outputs": True,
"inference_weights_sha256": sha(weights),
"source_packet_sha256": sha(batched_packet),
"forecast_sha256": sha(work / "forecast.npz"),
"native_detail_history_available": bool(arrays["detail_available"][0]),
"core_is_learned_reconstruction_not_native_observations": True,
}
(work / "verification.json").write_text(json.dumps(receipt, indent=2) + "\n")
print(json.dumps(receipt, indent=2))
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--batched-packet", type=Path, required=True)
parser.add_argument("--weights", type=Path, required=True)
parser.add_argument("--work", type=Path, required=True)
args = parser.parse_args()
verify(args.batched_packet, args.weights, args.work)