| from __future__ import annotations |
|
|
| import argparse |
| import os |
| import sys |
| import time |
| from pathlib import Path |
|
|
| import numpy as np |
| import torch |
| import yaml |
|
|
|
|
| PROJECT_ROOT = Path(__file__).resolve().parents[1] |
| sys.path.insert(0, str(PROJECT_ROOT)) |
|
|
| from model.gpinn import ( |
| burgers_gpinn_terms, |
| burgers_residual, |
| exact_poisson1d, |
| exact_poisson2d, |
| gPINN, |
| gPINNBurgers, |
| gPINNPoisson2D, |
| gpinn_loss_poisson1d, |
| gpinn_loss_poisson2d, |
| ) |
|
|
|
|
| DEFAULT_CONFIG = PROJECT_ROOT / "conf" / "config.yaml" |
| CASES = ("1d", "2d", "burgers") |
|
|
|
|
| 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 seed_everything(seed: int) -> None: |
| np.random.seed(seed) |
| torch.manual_seed(seed) |
| if torch.cuda.is_available(): |
| torch.cuda.manual_seed_all(seed) |
|
|
|
|
| def ensure_finite(loss: torch.Tensor, case: str, step: int) -> None: |
| if not torch.isfinite(loss): |
| raise FloatingPointError(f"{case} loss became non-finite at step {step}") |
|
|
|
|
| def save_checkpoint(weight_dir: Path, filename: str, payload: dict) -> Path: |
| weight_dir.mkdir(parents=True, exist_ok=True) |
| checkpoint_path = weight_dir / filename |
| torch.save(payload, checkpoint_path) |
| print(f"Saved checkpoint: {checkpoint_path}") |
| return checkpoint_path |
|
|
|
|
| def train_poisson1d( |
| config: dict, |
| device: torch.device, |
| dtype: torch.dtype, |
| weight_dir: Path, |
| epochs_override: int | None, |
| nf_override: int | None, |
| lbfgs_override: int | None, |
| ) -> None: |
| layers = [int(value) for value in config["layers"]] |
| nf = nf_override if nf_override is not None else int(config["nf"]) |
| epochs = epochs_override if epochs_override is not None else int(config["epochs"]) |
| lbfgs_iters = ( |
| lbfgs_override if lbfgs_override is not None else int(config["lbfgs_iters"]) |
| ) |
| log_every = int(config["log_every"]) |
|
|
| interior = torch.linspace(0.0, np.pi, nf, dtype=dtype, device=device).unsqueeze(-1) |
| boundary = torch.tensor([[0.0], [np.pi]], dtype=dtype, device=device) |
| boundary_values = boundary.clone() |
| test_x = torch.linspace(0.0, np.pi, 1000, dtype=dtype, device=device).unsqueeze(-1) |
| exact = torch.as_tensor( |
| exact_poisson1d(test_x.detach().cpu().numpy().reshape(-1)), |
| dtype=dtype, |
| device=device, |
| ).unsqueeze(-1) |
|
|
| model = gPINN(layers).to(device=device, dtype=dtype) |
| optimizer = torch.optim.Adam(model.parameters(), lr=float(config["lr"])) |
| scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5000, gamma=0.5) |
| started = time.time() |
| print( |
| f"[1D] device={device} nf={nf} epochs={epochs} " |
| f"lbfgs_iters={lbfgs_iters}" |
| ) |
|
|
| for step in range(1, epochs + 1): |
| loss, parts = gpinn_loss_poisson1d( |
| model, interior, boundary, boundary_values, float(config["w_g"]) |
| ) |
| ensure_finite(loss, "1D Poisson", step) |
| optimizer.zero_grad(set_to_none=True) |
| loss.backward() |
| optimizer.step() |
| scheduler.step() |
| if step == 1 or step % log_every == 0 or step == epochs: |
| with torch.no_grad(): |
| relative_l2 = torch.linalg.vector_norm(model(test_x) - exact) / torch.linalg.vector_norm( |
| exact |
| ) |
| print( |
| f"[1D] step={step:5d} loss={loss.item():.3e} " |
| f"residual={parts['residual']:.3e} boundary={parts['boundary']:.3e} " |
| f"gradient={parts['gradient']:.3e} l2={relative_l2.item():.3e}" |
| ) |
|
|
| if lbfgs_iters > 0: |
| lbfgs = torch.optim.LBFGS( |
| model.parameters(), |
| lr=1.0, |
| max_iter=lbfgs_iters, |
| line_search_fn="strong_wolfe", |
| ) |
|
|
| def closure() -> torch.Tensor: |
| lbfgs.zero_grad(set_to_none=True) |
| closure_loss, _ = gpinn_loss_poisson1d( |
| model, interior, boundary, boundary_values, float(config["w_g"]) |
| ) |
| ensure_finite(closure_loss, "1D Poisson L-BFGS", 0) |
| closure_loss.backward() |
| return closure_loss |
|
|
| lbfgs.step(closure) |
|
|
| with torch.no_grad(): |
| relative_l2 = torch.linalg.vector_norm(model(test_x) - exact) / torch.linalg.vector_norm(exact) |
| print(f"[1D] finished in {time.time() - started:.1f}s, relative L2={relative_l2.item():.6e}") |
| save_checkpoint( |
| weight_dir, |
| "gpinn_poisson1d.pt", |
| { |
| "case": "1d", |
| "architecture": "gpinn", |
| "model_state": model.state_dict(), |
| "layers": layers, |
| "nf": nf, |
| "w_g": float(config["w_g"]), |
| }, |
| ) |
|
|
|
|
| def train_poisson2d( |
| config: dict, |
| device: torch.device, |
| dtype: torch.dtype, |
| weight_dir: Path, |
| epochs_override: int | None, |
| nf_override: int | None, |
| ) -> None: |
| layers = [int(value) for value in config["layers"]] |
| nf = nf_override if nf_override is not None else int(config["nf"]) |
| epochs = epochs_override if epochs_override is not None else int(config["epochs"]) |
| exponent = float(config["a"]) |
| log_every = int(config["log_every"]) |
|
|
| interior = torch.rand(nf, 2, dtype=dtype, device=device) |
| model = gPINNPoisson2D(layers).to(device=device, dtype=dtype) |
| optimizer = torch.optim.Adam(model.parameters(), lr=float(config["lr"])) |
| scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5000, gamma=0.5) |
| started = time.time() |
| print(f"[2D] device={device} nf={nf} epochs={epochs} a={exponent:g}") |
|
|
| for step in range(1, epochs + 1): |
| loss, parts = gpinn_loss_poisson2d( |
| model, interior, float(config["w_g"]), exponent |
| ) |
| ensure_finite(loss, "2D Poisson", step) |
| optimizer.zero_grad(set_to_none=True) |
| loss.backward() |
| optimizer.step() |
| scheduler.step() |
| if step == 1 or step % log_every == 0 or step == epochs: |
| print( |
| f"[2D] step={step:5d} loss={loss.item():.3e} " |
| f"residual={parts['residual']:.3e} gradient={parts['gradient']:.3e}" |
| ) |
|
|
| axis = np.linspace(0.0, 1.0, 100) |
| grid_x, grid_y = np.meshgrid(axis, axis) |
| test_coordinates = torch.as_tensor( |
| np.column_stack([grid_x.ravel(), grid_y.ravel()]), dtype=dtype, device=device |
| ) |
| with torch.no_grad(): |
| prediction = model(test_coordinates).cpu().numpy().reshape(grid_x.shape) |
| exact = exact_poisson2d(grid_x, grid_y, exponent) |
| relative_l2 = np.linalg.norm(prediction.ravel() - exact.ravel()) / np.linalg.norm( |
| exact.ravel() |
| ) |
| print(f"[2D] finished in {time.time() - started:.1f}s, relative L2={relative_l2:.6e}") |
| save_checkpoint( |
| weight_dir, |
| "gpinn_poisson2d.pt", |
| { |
| "case": "2d", |
| "architecture": "poisson2d_hard_bc", |
| "model_state": model.state_dict(), |
| "layers": layers, |
| "a": exponent, |
| "nf": nf, |
| "w_g": float(config["w_g"]), |
| }, |
| ) |
|
|
|
|
| def load_burgers_reference( |
| data_path: Path, device: torch.device, dtype: torch.dtype |
| ) -> tuple[torch.Tensor, torch.Tensor]: |
| if not data_path.is_file(): |
| raise FileNotFoundError(f"Burgers data not found: {data_path}") |
| with np.load(data_path) as data: |
| missing = {"t", "x", "usol"}.difference(data.files) |
| if missing: |
| raise ValueError(f"Burgers data is missing arrays: {sorted(missing)}") |
| time_axis = np.asarray(data["t"]).reshape(-1) |
| space_axis = np.asarray(data["x"]).reshape(-1) |
| exact = np.asarray(data["usol"]) |
| expected_shape = (space_axis.size, time_axis.size) |
| if exact.shape != expected_shape: |
| raise ValueError(f"usol shape must be {expected_shape}, got {exact.shape}") |
| time_grid, space_grid = np.meshgrid(time_axis, space_axis) |
| coordinates = torch.as_tensor( |
| np.column_stack([space_grid.ravel(), time_grid.ravel()]), |
| dtype=dtype, |
| device=device, |
| ) |
| values = torch.as_tensor(exact.reshape(-1, 1), dtype=dtype, device=device) |
| return coordinates, values |
|
|
|
|
| def burgers_boundary_points( |
| device: torch.device, dtype: torch.dtype |
| ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: |
| boundary = torch.cat( |
| [ |
| torch.cat( |
| [ |
| torch.full((100, 1), -1.0, dtype=dtype, device=device), |
| torch.rand(100, 1, dtype=dtype, device=device), |
| ], |
| dim=1, |
| ), |
| torch.cat( |
| [ |
| torch.full((100, 1), 1.0, dtype=dtype, device=device), |
| torch.rand(100, 1, dtype=dtype, device=device), |
| ], |
| dim=1, |
| ), |
| ] |
| ) |
| initial = torch.cat( |
| [ |
| torch.linspace(-1.0, 1.0, 200, dtype=dtype, device=device).unsqueeze(-1), |
| torch.zeros(200, 1, dtype=dtype, device=device), |
| ], |
| dim=1, |
| ) |
| initial_values = -torch.sin(torch.pi * initial[:, 0:1]) |
| return boundary, initial, initial_values |
|
|
|
|
| def add_rar_points( |
| model: torch.nn.Module, |
| config: dict, |
| device: torch.device, |
| dtype: torch.dtype, |
| ) -> torch.Tensor: |
| candidate_count = int(config["rar_candidate_points"]) |
| batch_size = int(config["rar_candidate_batch_size"]) |
| add_count = int(config["rar_add_points"]) |
| if min(candidate_count, batch_size, add_count) <= 0: |
| raise ValueError("RAR point counts must be positive") |
|
|
| shortlisted_points = [] |
| shortlisted_residuals = [] |
| was_training = model.training |
| model.eval() |
| for start in range(0, candidate_count, batch_size): |
| current_size = min(batch_size, candidate_count - start) |
| candidates = torch.rand(current_size, 2, dtype=dtype, device=device) |
| candidates[:, 0] = 2.0 * candidates[:, 0] - 1.0 |
| residual, inputs = burgers_residual(model, candidates, create_graph=False) |
| count = min(add_count, current_size) |
| values, indices = torch.topk(residual.detach().abs().reshape(-1), count) |
| shortlisted_points.append(inputs.detach()[indices]) |
| shortlisted_residuals.append(values) |
| if was_training: |
| model.train() |
|
|
| residuals = torch.cat(shortlisted_residuals) |
| points = torch.cat(shortlisted_points) |
| final_count = min(add_count, residuals.numel()) |
| indices = torch.topk(residuals, final_count).indices |
| return points[indices].detach() |
|
|
|
|
| def train_burgers( |
| config: dict, |
| device: torch.device, |
| dtype: torch.dtype, |
| weight_dir: Path, |
| data_path: Path, |
| epochs_override: int | None, |
| nf_override: int | None, |
| rar_override: int | None, |
| quick: bool, |
| ) -> None: |
| layers = [int(value) for value in config["layers"]] |
| nf = nf_override if nf_override is not None else int(config["nf"]) |
| epochs = epochs_override if epochs_override is not None else int(config["epochs"]) |
| rar_rounds = rar_override if rar_override is not None else int(config["rar_rounds"]) |
| if quick: |
| rar_rounds = 0 |
| log_every = int(config["log_every"]) |
|
|
| collocation = torch.rand(nf, 2, dtype=dtype, device=device) |
| collocation[:, 0] = 2.0 * collocation[:, 0] - 1.0 |
| reference_coordinates, reference_values = load_burgers_reference(data_path, device, dtype) |
| boundary, initial, initial_values = burgers_boundary_points(device, dtype) |
| model = gPINNBurgers(layers).to(device=device, dtype=dtype) |
| optimizer = torch.optim.Adam(model.parameters(), lr=float(config["lr"])) |
| scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5000, gamma=0.5) |
| anchors: list[torch.Tensor] = [] |
|
|
| def evaluate() -> float: |
| with torch.no_grad(): |
| return ( |
| torch.linalg.vector_norm(model(reference_coordinates) - reference_values) |
| / torch.linalg.vector_norm(reference_values) |
| ).item() |
|
|
| def optimization_step(points: torch.Tensor, step: int) -> tuple[torch.Tensor, dict[str, float]]: |
| residual, residual_x, residual_t = burgers_gpinn_terms(model, points) |
| residual_loss = torch.mean(residual.square()) |
| boundary_loss = torch.mean(model(boundary).square()) + torch.mean( |
| (model(initial) - initial_values).square() |
| ) |
| gradient_loss = torch.mean(residual_x.square()) + torch.mean(residual_t.square()) |
| loss = residual_loss + boundary_loss + float(config["w_g"]) * gradient_loss |
| ensure_finite(loss, "Burgers", step) |
| optimizer.zero_grad(set_to_none=True) |
| loss.backward() |
| optimizer.step() |
| scheduler.step() |
| return loss, { |
| "residual": residual_loss.item(), |
| "boundary": boundary_loss.item(), |
| "gradient": gradient_loss.item(), |
| } |
|
|
| started = time.time() |
| print(f"[Burgers] device={device} nf={nf} epochs={epochs} rar_rounds={rar_rounds}") |
| for step in range(1, epochs + 1): |
| points = torch.cat([collocation, *anchors], dim=0) |
| loss, parts = optimization_step(points, step) |
| if step == 1 or step % log_every == 0 or step == epochs: |
| print( |
| f"[Burgers] step={step:5d} loss={loss.item():.3e} " |
| f"residual={parts['residual']:.3e} boundary={parts['boundary']:.3e} " |
| f"gradient={parts['gradient']:.3e} l2={evaluate():.3e}" |
| ) |
|
|
| rar_epochs = int(config["rar_epochs"]) |
| for round_index in range(1, rar_rounds + 1): |
| new_points = add_rar_points(model, config, device, dtype) |
| anchors.append(new_points) |
| points = torch.cat([collocation, *anchors], dim=0) |
| for rar_step in range(1, rar_epochs + 1): |
| optimization_step(points, epochs + (round_index - 1) * rar_epochs + rar_step) |
| print( |
| f"[Burgers] RAR round={round_index}/{rar_rounds} " |
| f"anchors={sum(item.shape[0] for item in anchors)} l2={evaluate():.3e}" |
| ) |
|
|
| relative_l2 = evaluate() |
| print( |
| f"[Burgers] finished in {time.time() - started:.1f}s, " |
| f"relative L2={relative_l2:.6e}" |
| ) |
| save_checkpoint( |
| weight_dir, |
| "gpinn_burgers.pt", |
| { |
| "case": "burgers", |
| "architecture": "burgers_hard_bc", |
| "model_state": model.state_dict(), |
| "layers": layers, |
| "nf": nf, |
| "w_g": float(config["w_g"]), |
| "rar_rounds": rar_rounds, |
| }, |
| ) |
|
|
|
|
| def parse_args() -> argparse.Namespace: |
| parser = argparse.ArgumentParser(description="Train gPINN PDE examples") |
| parser.add_argument( |
| "--config", |
| type=Path, |
| default=Path(os.environ.get("GPINN_CONFIG", DEFAULT_CONFIG)), |
| help="YAML configuration file", |
| ) |
| parser.add_argument("--case", choices=(*CASES, "all"), default="all") |
| parser.add_argument("--device", help="Override common.device, for example cpu or cuda:0") |
| parser.add_argument("--epochs", type=int, help="Override Adam iterations for selected cases") |
| parser.add_argument("--nf", type=int, help="Override collocation point count") |
| parser.add_argument("--lbfgs-iters", type=int, help="Override 1D Poisson L-BFGS iterations") |
| parser.add_argument("--rar-rounds", type=int, help="Override Burgers RAR rounds") |
| parser.add_argument("--quick", action="store_true", help="Skip Burgers RAR rounds") |
| parser.add_argument("--data", type=Path, help="Override Burgers.npz path") |
| parser.add_argument("--weight-dir", type=Path, help="Override checkpoint output directory") |
| return parser.parse_args() |
|
|
|
|
| def main() -> None: |
| args = parse_args() |
| config_path = args.config.expanduser().resolve() |
| config = load_config(config_path) |
| common = config["common"] |
| device = resolve_device(args.device or str(common["device"])) |
| dtype = resolve_dtype(str(common["dtype"])) |
| weight_dir = project_path(args.weight_dir or common["weight_dir"]) |
| data_path = project_path(args.data or config["burgers"]["data"]) |
| seed_everything(int(common["seed"])) |
|
|
| print(f"Config: {config_path}") |
| selected_cases = CASES if args.case == "all" else (args.case,) |
| if "1d" in selected_cases: |
| train_poisson1d( |
| config["poisson1d"], |
| device, |
| dtype, |
| weight_dir, |
| args.epochs, |
| args.nf, |
| args.lbfgs_iters, |
| ) |
| if "2d" in selected_cases: |
| train_poisson2d( |
| config["poisson2d"], |
| device, |
| dtype, |
| weight_dir, |
| args.epochs, |
| args.nf, |
| ) |
| if "burgers" in selected_cases: |
| train_burgers( |
| config["burgers"], |
| device, |
| dtype, |
| weight_dir, |
| data_path, |
| args.epochs, |
| args.nf, |
| args.rar_rounds, |
| args.quick, |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|