| from __future__ import annotations |
|
|
| import sys |
| from collections.abc import Mapping |
| from pathlib import Path |
|
|
| import matplotlib |
|
|
| matplotlib.use("Agg") |
| import matplotlib.pyplot as plt |
| import numpy as np |
| import torch |
| import yaml |
|
|
|
|
| PROJECT_ROOT = Path(__file__).resolve().parents[1] |
| sys.path.insert(0, str(PROJECT_ROOT)) |
|
|
| from model.vpinn import VPINN, infer_layers, unpack_checkpoint |
|
|
|
|
| DEFAULT_CONFIG = PROJECT_ROOT / "conf" / "config.yaml" |
| CASES = ("1d", "2d") |
|
|
|
|
| def load_config(path: Path) -> dict: |
| with path.open("r", encoding="utf-8") as stream: |
| config = yaml.safe_load(stream) |
| if not isinstance(config, dict) or "root" not in config: |
| raise ValueError(f"config must contain a 'root' mapping: {path}") |
| return config["root"] |
|
|
|
|
| def project_path(value: str | Path) -> Path: |
| path = Path(value).expanduser() |
| return path if path.is_absolute() else PROJECT_ROOT / path |
|
|
|
|
| def resolve_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("CUDA/DCU was requested but torch.cuda.is_available() is false") |
| return device |
|
|
|
|
| def resolve_dtype(name: str) -> torch.dtype: |
| try: |
| return {"float32": torch.float32, "float64": torch.float64}[name] |
| except KeyError as error: |
| raise ValueError(f"unsupported dtype: {name}") from error |
|
|
|
|
| def load_model( |
| checkpoint_path: Path, device: torch.device, dtype: torch.dtype |
| ) -> tuple[VPINN, dict]: |
| if not checkpoint_path.is_file(): |
| raise FileNotFoundError( |
| f"checkpoint not found: {checkpoint_path}. Run scripts/train.py first." |
| ) |
| checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=True) |
| if not isinstance(checkpoint, Mapping): |
| raise ValueError(f"invalid VPINN checkpoint: {checkpoint_path}") |
| state, metadata = unpack_checkpoint(checkpoint) |
| layers = metadata.get("layers") or infer_layers(state) |
| model = VPINN(layers, dtype=dtype).to(device=device, dtype=dtype) |
| model.load_state_dict(state, strict=True) |
| model.eval() |
| return model, metadata |
|
|
|
|
| def exact_poisson1d(x: np.ndarray) -> np.ndarray: |
| return 0.1 * np.sin(8.0 * np.pi * x) + np.tanh(80.0 * x) |
|
|
|
|
| def infer_poisson1d( |
| weight_dir: Path, |
| result_dir: Path, |
| device: torch.device, |
| dtype: torch.dtype, |
| ) -> float: |
| checkpoint_path = weight_dir / "hpvpinn_poisson1d.pt" |
| model, _ = load_model(checkpoint_path, device, dtype) |
| points = torch.linspace(-1.0, 1.0, 2001, dtype=dtype, device=device).unsqueeze(-1) |
| exact = exact_poisson1d(points.cpu().numpy().reshape(-1)) |
| with torch.no_grad(): |
| prediction = model(points).cpu().numpy().reshape(-1) |
| absolute_error = np.abs(prediction - exact) |
| relative_l2 = float(np.linalg.norm(prediction - exact) / np.linalg.norm(exact)) |
|
|
| figure, axes = plt.subplots(1, 2, figsize=(12, 4)) |
| coordinates = points.cpu().numpy().reshape(-1) |
| axes[0].plot(coordinates, exact, "k-", linewidth=1.5, label="Exact") |
| axes[0].plot(coordinates, prediction, "r--", linewidth=1.0, label="hp-VPINN") |
| axes[0].set_xlabel("x") |
| axes[0].set_ylabel("u") |
| axes[0].legend() |
| axes[1].semilogy(coordinates, np.maximum(absolute_error, 1.0e-16), "r-") |
| axes[1].set_xlabel("x") |
| axes[1].set_ylabel("Absolute error") |
| figure.tight_layout() |
| output_path = result_dir / "hpvpinn_poisson1d.png" |
| figure.savefig(output_path, dpi=150) |
| plt.close(figure) |
| print(f"[1D] relative L2={relative_l2:.6e}, plot={output_path}") |
| return relative_l2 |
|
|
|
|
| def exact_poisson2d(x: np.ndarray, y: np.ndarray) -> np.ndarray: |
| return (0.1 * np.sin(2.0 * np.pi * x) + np.tanh(10.0 * x)) * np.sin( |
| 2.0 * np.pi * y |
| ) |
|
|
|
|
| def infer_poisson2d( |
| weight_dir: Path, |
| result_dir: Path, |
| device: torch.device, |
| dtype: torch.dtype, |
| ) -> float: |
| checkpoint_path = weight_dir / "hpvpinn_poisson2d.pt" |
| model, _ = load_model(checkpoint_path, device, dtype) |
| axis = np.linspace(-1.0, 1.0, 100) |
| mesh_x, mesh_y = np.meshgrid(axis, axis, indexing="ij") |
| points = torch.as_tensor( |
| np.column_stack((mesh_x.ravel(), mesh_y.ravel())), dtype=dtype, device=device |
| ) |
| with torch.no_grad(): |
| prediction = model(points).cpu().numpy().reshape(mesh_x.shape) |
| exact = exact_poisson2d(mesh_x, mesh_y) |
| absolute_error = np.abs(prediction - exact) |
| relative_l2 = float( |
| np.linalg.norm(prediction.ravel() - exact.ravel()) / np.linalg.norm(exact.ravel()) |
| ) |
|
|
| figure, axes = plt.subplots(1, 3, figsize=(15, 4)) |
| for plot_axis, title, field in zip( |
| axes, |
| ("Exact", "hp-VPINN", "Absolute error"), |
| (exact, prediction, absolute_error), |
| strict=True, |
| ): |
| image = plot_axis.imshow( |
| field.T, |
| extent=(-1, 1, -1, 1), |
| origin="lower", |
| aspect="auto", |
| cmap="jet", |
| ) |
| plot_axis.set_title(title) |
| plot_axis.set_xlabel("x") |
| plot_axis.set_ylabel("y") |
| figure.colorbar(image, ax=plot_axis) |
| figure.tight_layout() |
| output_path = result_dir / "hpvpinn_poisson2d.png" |
| figure.savefig(output_path, dpi=150) |
| plt.close(figure) |
| print(f"[2D] relative L2={relative_l2:.6e}, plot={output_path}") |
| return relative_l2 |
|
|
|
|
| def main() -> None: |
| config_path = DEFAULT_CONFIG.resolve() |
| config = load_config(config_path) |
| common = config["common"] |
| device = resolve_device(str(common["device"])) |
| dtype = resolve_dtype(str(common["dtype"])) |
| weight_dir = project_path(common["weight_dir"]) |
| result_dir = project_path(common["result_dir"]) |
| result_dir.mkdir(parents=True, exist_ok=True) |
|
|
| print(f"Config: {config_path}") |
| print(f"Device: {device}") |
| selected_case = str(common["case"]).lower() |
| if selected_case not in (*CASES, "all"): |
| raise ValueError("common.case must be one of: 1d, 2d, all") |
| selected_cases = CASES if selected_case == "all" else (selected_case,) |
| if "1d" in selected_cases: |
| infer_poisson1d(weight_dir, result_dir, device, dtype) |
| if "2d" in selected_cases: |
| infer_poisson2d(weight_dir, result_dir, device, dtype) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|