Download scripts/inference.py from OneScience-Group/PointNetCFD: direct link, hf CLI and curl.
- Browser
- Download file 7.21 kB
-
https://huggingface.co/OneScience-Group/PointNetCFD/resolve/main/scripts/inference.py
- Command line
-
hf download hf://OneScience-Group/PointNetCFD/scripts/inference.py
-
curl -L -o inference.py https://huggingface.co/OneScience-Group/PointNetCFD/resolve/main/scripts/inference.py
7.21 kB
| #!/usr/bin/env python3 | |
| """Evaluate a PointCFD checkpoint on the fixed test split.""" | |
| from __future__ import annotations | |
| import argparse | |
| import sys | |
| from pathlib import Path | |
| from typing import Any, Dict | |
| import numpy as np | |
| import torch | |
| PROJECT_ROOT = Path(__file__).resolve().parents[1] | |
| project_root_string = str(PROJECT_ROOT) | |
| if project_root_string in sys.path: | |
| sys.path.remove(project_root_string) | |
| sys.path.insert(0, project_root_string) | |
| from models import PointNetCFD, count_trainable_parameters # noqa: E402 | |
| from scripts.common import ( # noqa: E402 | |
| AVAILABLE_SAMPLE_COUNT, | |
| PAPER_SAMPLE_COUNT, | |
| PointCFDDataset, | |
| choose_device, | |
| configured_paths, | |
| evaluate_model, | |
| load_checkpoint, | |
| load_config, | |
| load_data_and_splits, | |
| make_loader, | |
| resolve_path, | |
| selected_indices, | |
| set_deterministic_seed, | |
| write_json, | |
| write_npz, | |
| ) | |
| 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, | |
| help="Checkpoint path (default: paths.checkpoint from config)", | |
| ) | |
| parser.add_argument("--device", default="auto", help="auto, cpu, cuda, or cuda:N") | |
| parser.add_argument("--batch-size", type=int, default=None) | |
| parser.add_argument("--num-workers", type=int, default=None) | |
| parser.add_argument( | |
| "--output-dir", | |
| type=Path, | |
| default=None, | |
| help="Output directory (default: paths.results_dir from config)", | |
| ) | |
| parser.add_argument( | |
| "--max-cases", | |
| type=int, | |
| default=None, | |
| help="Evaluate only the first N fixed test cases (smoke testing only)", | |
| ) | |
| return parser.parse_args() | |
| def validate_checkpoint_contract(checkpoint: Dict[str, Any], config: Dict[str, Any]) -> None: | |
| expected_metadata = { | |
| "source_channels": list(config["data"]["source_channels"]), | |
| "input_names": list(config["data"]["input_names"]), | |
| "target_names": list(config["data"]["target_names"]), | |
| "input_indices": list(config["data"]["input_indices"]), | |
| "target_indices": list(config["data"]["target_indices"]), | |
| } | |
| for key, expected in expected_metadata.items(): | |
| if list(checkpoint.get(key, [])) != expected: | |
| raise ValueError(f"Checkpoint {key} metadata does not match config: {key}") | |
| target_min = np.asarray(checkpoint.get("target_min"), dtype=np.float32) | |
| target_max = np.asarray(checkpoint.get("target_max"), dtype=np.float32) | |
| if target_min.shape != (3,) or target_max.shape != (3,): | |
| raise ValueError("Checkpoint target normalization must contain three variables") | |
| if np.any(target_max <= target_min): | |
| raise ValueError("Checkpoint target normalization spans must be positive") | |
| def main() -> None: | |
| args = parse_args() | |
| config = load_config(args.config) | |
| seed = int(config["training"]["seed"]) | |
| set_deterministic_seed(seed) | |
| device = choose_device(args.device) | |
| paths = configured_paths(config, PROJECT_ROOT) | |
| checkpoint_path = ( | |
| resolve_path(PROJECT_ROOT, str(args.checkpoint)) | |
| if args.checkpoint is not None | |
| else paths["checkpoint"] | |
| ) | |
| output_dir = ( | |
| resolve_path(PROJECT_ROOT, str(args.output_dir)) | |
| if args.output_dir is not None | |
| else paths["results_dir"] | |
| ) | |
| checkpoint = load_checkpoint(checkpoint_path, device) | |
| validate_checkpoint_contract(checkpoint, config) | |
| checkpoint_model_config = checkpoint.get("model_config", config["model"]) | |
| model = PointNetCFD( | |
| input_dim=int(checkpoint_model_config["input_dim"]), | |
| output_dim=int(checkpoint_model_config["output_dim"]), | |
| ).to(device=device, dtype=torch.float32) | |
| model.load_state_dict(checkpoint["model_state_dict"], strict=True) | |
| print( | |
| f"checkpoint={checkpoint_path} epoch={checkpoint.get('epoch')} device={device} " | |
| f"trainable_parameters={count_trainable_parameters(model)}", | |
| flush=True, | |
| ) | |
| data, splits, _ = load_data_and_splits(config, PROJECT_ROOT) | |
| test_indices = selected_indices(splits["test"], args.max_cases) | |
| target_min = np.asarray(checkpoint["target_min"], dtype=np.float32) | |
| target_max = np.asarray(checkpoint["target_max"], dtype=np.float32) | |
| test_dataset = PointCFDDataset( | |
| data, | |
| test_indices, | |
| config["data"]["input_indices"], | |
| config["data"]["target_indices"], | |
| target_min, | |
| target_max, | |
| ) | |
| batch_size = int( | |
| args.batch_size if args.batch_size is not None else config["training"]["batch_size"] | |
| ) | |
| num_workers = int( | |
| args.num_workers if args.num_workers is not None else config["training"]["num_workers"] | |
| ) | |
| test_loader = make_loader( | |
| test_dataset, | |
| batch_size=batch_size, | |
| shuffle=False, | |
| num_workers=num_workers, | |
| seed=seed, | |
| pin_memory=device.type == "cuda", | |
| ) | |
| target_names = list(config["data"]["target_names"]) | |
| metrics, arrays = evaluate_model( | |
| model, | |
| test_loader, | |
| device, | |
| target_min, | |
| target_max, | |
| target_names, | |
| float(config["evaluation"]["relative_l2_epsilon"]), | |
| ) | |
| metrics_payload: Dict[str, Any] = { | |
| "checkpoint": str(checkpoint_path), | |
| "checkpoint_epoch": int(checkpoint.get("epoch", -1)), | |
| "device": str(device), | |
| "evaluated_test_cases": len(test_dataset), | |
| "fixed_test_split_cases": int(splits["test"].size), | |
| "available_sample_count": AVAILABLE_SAMPLE_COUNT, | |
| "paper_sample_count": PAPER_SAMPLE_COUNT, | |
| "metrics": metrics, | |
| "paper_reference_mean_relative_l2": config["evaluation"][ | |
| "paper_reference_mean_relative_l2" | |
| ], | |
| "dataset_limitation": ( | |
| "The supplied dataset has 2215 cases rather than the paper's 2595; " | |
| "these metrics are a best-available subset reproduction." | |
| ), | |
| } | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| metrics_path = output_dir / "test_metrics.json" | |
| predictions_path = output_dir / "predictions.npz" | |
| write_json(metrics_path, metrics_payload) | |
| write_npz( | |
| predictions_path, | |
| coordinates=arrays["coordinates"], | |
| predictions=arrays["predictions"], | |
| targets=arrays["targets"], | |
| case_indices=arrays["case_indices"], | |
| target_names=np.asarray(target_names), | |
| target_min=target_min, | |
| target_max=target_max, | |
| ) | |
| print( | |
| "normalized_mse={:.9e}".format(metrics["normalized_mse"]), flush=True | |
| ) | |
| for name in target_names: | |
| relative = metrics["relative_l2"][name] | |
| print( | |
| f"variable={name} rmse={metrics['rmse'][name]:.9e} " | |
| f"relative_l2_mean={relative['mean']:.9e} " | |
| f"relative_l2_max={relative['max']:.9e} " | |
| f"relative_l2_min={relative['min']:.9e}", | |
| flush=True, | |
| ) | |
| print(f"metrics={metrics_path}", flush=True) | |
| print(f"predictions={predictions_path}", flush=True) | |
| if __name__ == "__main__": | |
| main() | |