Download scripts/inference.py from OneScience-Group/DeepONet: direct link, hf CLI and curl.
- Browser
- Download file 11 kB
-
https://huggingface.co/OneScience-Group/DeepONet/resolve/main/scripts/inference.py
- Command line
-
hf download hf://OneScience-Group/DeepONet/scripts/inference.py
-
curl -L -o inference.py https://huggingface.co/OneScience-Group/DeepONet/resolve/main/scripts/inference.py
11 kB
| #!/usr/bin/env python3 | |
| """Inference for random-test, OOD-curve, and PDE-grid DeepONet cases.""" | |
| from __future__ import annotations | |
| import argparse | |
| import csv | |
| import json | |
| import os | |
| import sys | |
| import tempfile | |
| from pathlib import Path | |
| from typing import Any, Dict, Mapping, Tuple | |
| import numpy as np | |
| import torch | |
| import yaml | |
| from torch.utils.data import DataLoader | |
| PROJECT_ROOT = Path(__file__).resolve().parents[1] | |
| if str(PROJECT_ROOT) not in sys.path: | |
| sys.path.insert(0, str(PROJECT_ROOT)) | |
| from models.DeepONet import build_model # noqa: E402 | |
| from models.dataset import ( # noqa: E402 | |
| OperatorDataset, | |
| build_split, | |
| generate_ood_data, | |
| generate_pde_grid_case, | |
| ) | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--config", type=Path, default=PROJECT_ROOT / "config/config.yaml") | |
| parser.add_argument("--checkpoint", type=Path, default=None) | |
| parser.add_argument("--experiment", default="antiderivative") | |
| parser.add_argument("--variant", default="unstacked_bias") | |
| parser.add_argument("--mode", choices=("random_test", "ood", "pde_grid"), default="random_test") | |
| parser.add_argument("--device", default=None) | |
| parser.add_argument("--batch-size", type=int, default=None) | |
| parser.add_argument("--seed", type=int, default=None) | |
| parser.add_argument("--output-dir", type=Path, default=None) | |
| parser.add_argument("--smoke-test", action="store_true") | |
| return parser.parse_args() | |
| def load_yaml(path: Path) -> Dict[str, Any]: | |
| with path.expanduser().resolve().open("r", encoding="utf-8") as handle: | |
| value = yaml.safe_load(handle) | |
| if not isinstance(value, dict): | |
| raise ValueError(f"Configuration {path} must contain a mapping") | |
| return value | |
| def resolve_path(value: str | Path) -> Path: | |
| path = Path(value).expanduser() | |
| return path if path.is_absolute() else PROJECT_ROOT / path | |
| def torch_load(path: Path, map_location: str | torch.device = "cpu") -> Any: | |
| try: | |
| return torch.load(path, map_location=map_location, weights_only=False) | |
| except TypeError: | |
| return torch.load(path, map_location=map_location) | |
| def load_entry(checkpoint_path: Path, experiment: str, variant: str) -> Mapping[str, Any]: | |
| if not checkpoint_path.exists(): | |
| raise FileNotFoundError( | |
| f"Checkpoint {checkpoint_path} does not exist. Train the requested entry first; " | |
| "inference never evaluates random weights." | |
| ) | |
| bundle = torch_load(checkpoint_path) | |
| if not isinstance(bundle, dict) or not isinstance(bundle.get("entries"), dict): | |
| raise ValueError(f"Checkpoint {checkpoint_path} is not a DeepONet indexed bundle") | |
| key = f"{experiment}/{variant}" | |
| if key not in bundle["entries"]: | |
| available = sorted(bundle["entries"]) | |
| raise KeyError(f"Checkpoint entry {key!r} is missing; available entries: {available}") | |
| entry = bundle["entries"][key] | |
| if not isinstance(entry.get("run_config"), dict): | |
| raise ValueError(f"Checkpoint entry {key!r} has no resolved run_config") | |
| return entry | |
| def select_device(requested: str) -> torch.device: | |
| if requested == "auto": | |
| return torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| device = torch.device(requested) | |
| if device.type == "cuda" and not torch.cuda.is_available(): | |
| raise RuntimeError(f"CUDA device {requested!r} requested but CUDA is unavailable") | |
| return device | |
| def metrics(prediction: np.ndarray, target: np.ndarray, trim_fraction: float) -> Dict[str, float]: | |
| error = prediction.astype(np.float64).reshape(-1) - target.astype(np.float64).reshape(-1) | |
| squared = error**2 | |
| output = { | |
| "test_mse": float(np.mean(squared)), | |
| "relative_l2": float( | |
| np.linalg.norm(error) | |
| / max(np.linalg.norm(target.astype(np.float64).reshape(-1)), np.finfo(np.float64).eps) | |
| ), | |
| } | |
| if trim_fraction > 0.0: | |
| remove_count = min(len(squared) - 1, int(np.ceil(len(squared) * trim_fraction))) | |
| kept_count = len(squared) - remove_count | |
| output["trimmed_test_mse"] = float(np.mean(np.partition(squared, kept_count - 1)[:kept_count])) | |
| return output | |
| def predict_dataset( | |
| model: torch.nn.Module, | |
| dataset: OperatorDataset, | |
| device: torch.device, | |
| batch_size: int, | |
| ) -> Tuple[np.ndarray, np.ndarray]: | |
| loader = DataLoader(dataset, batch_size=batch_size, shuffle=False, num_workers=0) | |
| predictions, targets = [], [] | |
| model.eval() | |
| for branch, trunk, target in loader: | |
| prediction = model(branch.to(device), trunk.to(device)) | |
| predictions.append(prediction.detach().cpu().numpy()) | |
| targets.append(target.numpy()) | |
| prediction_array = np.concatenate(predictions).astype(np.float32) | |
| target_array = np.concatenate(targets).astype(np.float32) | |
| if prediction_array.shape != target_array.shape or prediction_array.ndim != 2: | |
| raise ValueError( | |
| f"prediction/target shape mismatch: {prediction_array.shape} versus {target_array.shape}" | |
| ) | |
| if not np.isfinite(prediction_array).all(): | |
| raise FloatingPointError("model prediction contains NaN or infinity") | |
| return prediction_array, target_array | |
| def _safe_output_dir( | |
| requested: Path | None, | |
| results_root: Path, | |
| experiment: str, | |
| variant: str, | |
| mode: str, | |
| ) -> Path: | |
| output = results_root / experiment / variant / mode if requested is None else resolve_path(requested) | |
| results_root = results_root.resolve() | |
| output = output.resolve() | |
| if os.path.commonpath((str(results_root), str(output))) != str(results_root): | |
| raise ValueError(f"Inference outputs must remain below {results_root}, got {output}") | |
| output.mkdir(parents=True, exist_ok=True) | |
| return output | |
| def _atomic_json(path: Path, value: Mapping[str, Any]) -> None: | |
| with tempfile.NamedTemporaryFile( | |
| mode="w", encoding="utf-8", dir=path.parent, suffix=".json", delete=False | |
| ) as handle: | |
| json.dump(value, handle, indent=2, sort_keys=True) | |
| handle.write("\n") | |
| temporary_path = Path(handle.name) | |
| try: | |
| os.replace(temporary_path, path) | |
| finally: | |
| if temporary_path.exists(): | |
| temporary_path.unlink() | |
| def _atomic_npz(path: Path, arrays: Mapping[str, np.ndarray]) -> None: | |
| with tempfile.NamedTemporaryFile(dir=path.parent, suffix=".npz", delete=False) as handle: | |
| temporary_path = Path(handle.name) | |
| try: | |
| np.savez_compressed(temporary_path, **arrays) | |
| os.replace(temporary_path, path) | |
| finally: | |
| if temporary_path.exists(): | |
| temporary_path.unlink() | |
| def final_train_loss(results_root: Path, experiment: str, variant: str) -> float | None: | |
| history_path = results_root / experiment / variant / "history.csv" | |
| if not history_path.exists(): | |
| return None | |
| with history_path.open("r", encoding="utf-8", newline="") as handle: | |
| rows = list(csv.DictReader(handle)) | |
| return float(rows[-1]["train_loss"]) if rows else None | |
| def prepare_data( | |
| config: Mapping[str, Any], experiment: str, mode: str, seed: int | |
| ) -> Tuple[OperatorDataset, Dict[str, np.ndarray]]: | |
| if mode == "random_test": | |
| dataset = build_split(config, experiment, "test", PROJECT_ROOT) | |
| return dataset, { | |
| "branch_functions": dataset.branch_functions, | |
| "function_index": dataset.function_index, | |
| "trunk": dataset.trunk, | |
| } | |
| if mode == "ood": | |
| payload = generate_ood_data(config, experiment) | |
| dataset = OperatorDataset(payload["branch"], payload["trunk"], payload["target"]) | |
| return dataset, {"branch": payload["branch"], "trunk": payload["trunk"], "labels": payload["labels"]} | |
| if experiment != "diffusion_reaction": | |
| raise ValueError("pde_grid mode is only valid for diffusion_reaction") | |
| payload = generate_pde_grid_case(config, seed) | |
| dataset = OperatorDataset(payload["branch"], payload["trunk"], payload["target"]) | |
| extras = {key: value for key, value in payload.items() if key != "target"} | |
| return dataset, extras | |
| def main() -> None: | |
| args = parse_args() | |
| file_config = load_yaml(args.config) | |
| checkpoint_path = resolve_path(args.checkpoint or file_config["paths"]["checkpoint"]) | |
| entry = load_entry(checkpoint_path, args.experiment, args.variant) | |
| config = entry["run_config"] | |
| stored_paper_scale = bool(entry.get("paper_scale", config["project"].get("paper_scale", True))) | |
| if args.smoke_test and stored_paper_scale: | |
| raise ValueError("--smoke-test was requested, but the selected checkpoint is paper-scale") | |
| device_name = args.device or str(config["runtime"].get("device", "auto")) | |
| device = select_device(device_name) | |
| model = build_model(config, args.experiment, args.variant).to(device) | |
| model.load_state_dict(entry["model_state"], strict=True) | |
| seed = int(args.seed if args.seed is not None else config["runtime"]["seed"] + 200_000) | |
| dataset, extra_arrays = prepare_data(config, args.experiment, args.mode, seed) | |
| batch_size = int(args.batch_size or config["inference"]["batch_size"]) | |
| prediction, target = predict_dataset(model, dataset, device, batch_size) | |
| trim_fraction = float(config["experiments"][args.experiment].get("trim_fraction", 0.0)) | |
| result_metrics = metrics(prediction, target, trim_fraction) | |
| results_root = resolve_path(config["paths"]["results"]) | |
| output_dir = _safe_output_dir( | |
| args.output_dir, results_root, args.experiment, args.variant, args.mode | |
| ) | |
| train_loss = final_train_loss(results_root, args.experiment, args.variant) | |
| if train_loss is not None and args.mode == "random_test": | |
| result_metrics["generalization_error"] = result_metrics["test_mse"] - train_loss | |
| metadata = { | |
| "experiment": args.experiment, | |
| "variant": args.variant, | |
| "mode": args.mode, | |
| "checkpoint": str(checkpoint_path), | |
| "checkpoint_iteration": int(entry["iteration"]), | |
| "checkpoint_best_metric": float(entry["best_metric"]), | |
| "seed": seed, | |
| "paper_scale": stored_paper_scale, | |
| "sample_count": len(dataset), | |
| } | |
| arrays: Dict[str, np.ndarray] = { | |
| "prediction": prediction, | |
| "target": target, | |
| "metadata": np.asarray(json.dumps(metadata, sort_keys=True)), | |
| } | |
| arrays.update(extra_arrays) | |
| _atomic_npz(output_dir / "predictions.npz", arrays) | |
| _atomic_json(output_dir / "metrics.json", {**metadata, **result_metrics}) | |
| metric_text = " ".join(f"{name}={value:.8e}" for name, value in result_metrics.items()) | |
| print( | |
| f"INFERENCE experiment={args.experiment} variant={args.variant} mode={args.mode} " | |
| f"paper_scale={stored_paper_scale} {metric_text}", | |
| flush=True, | |
| ) | |
| print( | |
| f"SAVED metrics={output_dir / 'metrics.json'} predictions={output_dir / 'predictions.npz'}", | |
| flush=True, | |
| ) | |
| if __name__ == "__main__": | |
| main() | |