Download scripts/inference.py from OneScience-Group/StormCast: direct link, hf CLI and curl.
- Browser
- Download file 9.14 kB
-
https://huggingface.co/OneScience-Group/StormCast/resolve/main/scripts/inference.py
- Command line
-
hf download hf://OneScience-Group/StormCast/scripts/inference.py
-
curl -L -o inference.py https://huggingface.co/OneScience-Group/StormCast/resolve/main/scripts/inference.py
9.14 kB
| from __future__ import annotations | |
| import argparse | |
| import sys | |
| from pathlib import Path | |
| from typing import Any | |
| PROJECT_ROOT = Path(__file__).resolve().parent.parent | |
| MODEL_ROOT = PROJECT_ROOT / "model" | |
| SCRIPT_ROOT = PROJECT_ROOT / "scripts" | |
| for path in (PROJECT_ROOT, MODEL_ROOT, SCRIPT_ROOT): | |
| if str(path) not in sys.path: | |
| sys.path.insert(0, str(path)) | |
| import h5py | |
| import numpy as np | |
| import torch | |
| import yaml | |
| from stormer import StormCast, edm_heun_sample | |
| from data_loader import StormCastDataset | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description="Run StormCast inference") | |
| parser.add_argument("--config", type=Path, default=Path("conf/config.yaml")) | |
| parser.add_argument("--regression-weights", type=Path) | |
| parser.add_argument("--diffusion-weights", type=Path) | |
| parser.add_argument("--output", type=Path) | |
| parser.add_argument("--num-steps", type=int) | |
| parser.add_argument("--diffusion-steps", type=int) | |
| parser.add_argument("--seed", type=int) | |
| return parser.parse_args() | |
| def main() -> None: | |
| args = parse_args() | |
| config_path = args.config.resolve() | |
| with config_path.open("r", encoding="utf-8") as handle: | |
| config = yaml.safe_load(handle) | |
| _resolve_paths(config, config_path.parent.parent) | |
| inference_config = config["inference"] | |
| regression_weights = args.regression_weights or Path( | |
| config["model"]["regression_weights"] | |
| ) | |
| diffusion_weights = args.diffusion_weights or Path( | |
| config["model"]["diffusion_weights"] | |
| ) | |
| output = args.output or Path(inference_config["output_dir"]) / "forecast.h5" | |
| run_inference( | |
| config=config, | |
| regression_weights=regression_weights, | |
| diffusion_weights=diffusion_weights, | |
| output=output, | |
| num_steps=args.num_steps or inference_config["num_steps"], | |
| diffusion_steps=args.diffusion_steps or inference_config["diffusion_steps"], | |
| seed=config["project"]["seed"] if args.seed is None else args.seed, | |
| ) | |
| def run_inference( | |
| config: dict[str, Any], | |
| regression_weights: Path, | |
| diffusion_weights: Path, | |
| output: Path, | |
| num_steps: int, | |
| diffusion_steps: int, | |
| seed: int, | |
| ) -> Path: | |
| if not torch.cuda.is_available(): | |
| raise RuntimeError("StormCast inference requires a CUDA/HIP device") | |
| if num_steps < 1: | |
| raise ValueError("num_steps must be at least 1") | |
| device = torch.device("cuda", 0) | |
| torch.cuda.set_device(device) | |
| data_config = config["data"] | |
| inference_config = config["inference"] | |
| split = inference_config["split"] | |
| years_key = f"{split}_years" | |
| if years_key not in data_config: | |
| raise ValueError(f"Unknown inference split: {split}") | |
| dataset = StormCastDataset( | |
| data_root=data_config["root_dir"], | |
| years=data_config[years_key], | |
| era5_variables=data_config["era5_variables"], | |
| state_variables=data_config["state_variables"], | |
| invariant_variables=data_config["invariant_variables"], | |
| image_size=data_config["image_size"], | |
| input_steps=data_config["input_steps"], | |
| output_steps=data_config["output_steps"], | |
| normalize=data_config["normalize"], | |
| ) | |
| if num_steps > len(dataset): | |
| raise ValueError(f"Requested {num_steps} steps but split contains {len(dataset)}") | |
| regression = _load_weights(regression_weights, "regression").to(device).eval() | |
| diffusion = _load_weights(diffusion_weights, "diffusion").to(device).eval() | |
| model = StormCast(regression, diffusion) | |
| generator = torch.Generator(device=device).manual_seed(seed) | |
| output.parent.mkdir(parents=True, exist_ok=True) | |
| first = dataset[0] | |
| state = first["state"][0].unsqueeze(0).to(device, dtype=torch.float32) | |
| invariant = first["invariant"].to(device, dtype=torch.float32) | |
| state_channels = len(data_config["state_variables"]) | |
| background_channels = len(data_config["era5_variables"]) | |
| height, width = data_config["image_size"] | |
| if [height, width] != list(config["model"]["image_size"]): | |
| raise ValueError("Data and model image sizes must match") | |
| if list(data_config["era5_image_size"]) != [721, 1440]: | |
| raise ValueError("ERA5 grid must be 721 x 1440") | |
| with h5py.File(output, "w") as handle: | |
| handle.attrs["normalized"] = bool(data_config["normalize"]) | |
| handle.attrs["seed"] = seed | |
| handle.attrs["diffusion_steps"] = diffusion_steps | |
| handle.attrs["sigma_min"] = inference_config["sigma_min"] | |
| handle.attrs["sigma_max"] = inference_config["sigma_max"] | |
| handle.attrs["rho"] = inference_config["rho"] | |
| handle.attrs["regression_weights"] = str(regression_weights.resolve()) | |
| handle.attrs["diffusion_weights"] = str(diffusion_weights.resolve()) | |
| handle.attrs["state_variables"] = np.asarray( | |
| data_config["state_variables"], dtype="S" | |
| ) | |
| handle.attrs["background_variables"] = np.asarray( | |
| data_config["era5_variables"], dtype="S" | |
| ) | |
| prediction_store = handle.create_dataset( | |
| "prediction", (num_steps, state_channels, height, width), dtype="f4" | |
| ) | |
| regression_store = handle.create_dataset( | |
| "regression", (num_steps, state_channels, height, width), dtype="f4" | |
| ) | |
| target_store = handle.create_dataset( | |
| "target", (num_steps, state_channels, height, width), dtype="f4" | |
| ) | |
| background_store = handle.create_dataset( | |
| "background", (num_steps, background_channels, height, width), dtype="f4" | |
| ) | |
| time_store = handle.create_dataset("time_index", (num_steps,), dtype="i8") | |
| for index in range(num_steps): | |
| sample = dataset[index] | |
| background = sample["background"].unsqueeze(0).to( | |
| device, dtype=torch.float32 | |
| ) | |
| regression_prediction = model.predict_regression( | |
| state, background, invariant | |
| ) | |
| condition = model.diffusion_condition( | |
| state, regression_prediction, invariant | |
| ) | |
| residual = edm_heun_sample( | |
| diffusion, | |
| condition, | |
| output_channels=state_channels, | |
| num_steps=diffusion_steps, | |
| sigma_min=inference_config["sigma_min"], | |
| sigma_max=inference_config["sigma_max"], | |
| rho=inference_config["rho"], | |
| generator=generator, | |
| ) | |
| prediction = regression_prediction + residual | |
| prediction_store[index] = prediction[0].cpu().numpy() | |
| regression_store[index] = regression_prediction[0].cpu().numpy() | |
| target_store[index] = sample["state"][1].numpy() | |
| background_store[index] = sample["background"].numpy() | |
| time_store[index] = int(np.asarray(sample["time_index"]).reshape(-1)[-1]) | |
| state = prediction | |
| print(f"forecast_step={index + 1}/{num_steps}") | |
| print(f"output={output}") | |
| return output | |
| def _load_weights(path: Path, kind: str) -> torch.nn.Module: | |
| if not path.is_file(): | |
| raise FileNotFoundError( | |
| f"Missing {kind} weights: {path}. " | |
| "Train the corresponding stage first or pass an explicit weight path." | |
| ) | |
| checkpoint = torch.load(path, map_location="cpu", weights_only=False) | |
| saved_config = checkpoint.get("config") | |
| if saved_config is None: | |
| raise ValueError("Project .pt weights must include their training config") | |
| model = _build_stage_models_from_config(saved_config, kind) | |
| model.load_state_dict(checkpoint["model_state_dict"], strict=True) | |
| return model | |
| def _build_stage_models_from_config(config: dict[str, Any], kind: str) -> torch.nn.Module: | |
| from stormer import build_diffusion_model, build_regression_model | |
| data_config = config["data"] | |
| model_config = config["model"] | |
| common = { | |
| "image_size": model_config["image_size"], | |
| "state_channels": len(data_config["state_variables"]), | |
| "invariant_channels": len(data_config["invariant_variables"]), | |
| "model_channels": model_config["model_channels"], | |
| "channel_mult": model_config["channel_mult"], | |
| "num_blocks": model_config["num_blocks"], | |
| "attn_resolutions": model_config["attention_resolutions"], | |
| } | |
| if kind == "regression": | |
| return build_regression_model( | |
| **common, background_channels=len(data_config["era5_variables"]) | |
| ) | |
| return build_diffusion_model(**common) | |
| def _resolve_paths(config: dict[str, Any], project_root: Path) -> None: | |
| for section, key in (("data", "root_dir"), ("inference", "output_dir")): | |
| path = Path(config[section][key]) | |
| if not path.is_absolute(): | |
| config[section][key] = str((project_root / path).resolve()) | |
| for key in ("regression_weights", "diffusion_weights"): | |
| path = Path(config["model"][key]) | |
| if not path.is_absolute(): | |
| config["model"][key] = str((project_root / path).resolve()) | |
| if __name__ == "__main__": | |
| main() | |