| |
| """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 |
| from scripts.common import ( |
| 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() |
|
|