from __future__ import annotations from collections.abc import Mapping import numpy as np import torch from model.sa_pinn import Burgers2D, Equation, Helmholtz2D, Laplace1D CASES = ("laplace", "helmholtz", "burgers") def exact_solution(case: str, coordinates: np.ndarray) -> np.ndarray | None: if case == "laplace": return np.sin(np.pi * coordinates[:, 0:1]) if case == "helmholtz": return np.sin(np.pi * coordinates[:, 0:1]) * np.sin( 4.0 * np.pi * coordinates[:, 1:2] ) if case == "burgers": return None raise ValueError(f"unsupported case: {case}") def build_equation(case: str, data_config: Mapping) -> Equation: if case == "laplace": return Laplace1D() if case == "helmholtz": return Helmholtz2D(float(data_config["wave_number"])) if case == "burgers": return Burgers2D(float(data_config["viscosity"])) raise ValueError(f"unsupported case: {case}") def generate_data(case: str, config: Mapping, seed: int) -> dict[str, np.ndarray | None]: generator = np.random.default_rng(seed) n_pde = int(config["n_pde"]) n_solution = int(config["n_sol"]) n_boundary = int(config["n_bnd"]) test_resolution = int(config["test_res"]) if min(n_pde, n_solution, n_boundary, test_resolution) <= 0: raise ValueError("all data counts must be positive") if case == "laplace": lower, upper = map(float, config["domain"]) x_pde = generator.uniform(lower, upper, (n_pde, 1)) x_boundary = np.array([[lower], [upper]], dtype=np.float64) u_boundary = exact_solution(case, x_boundary) x_data = np.linspace(lower, upper, n_solution)[:, None] u_data = exact_solution(case, x_data) noise_std = float(config.get("noise_std", 0.0)) if noise_std: u_data = u_data + noise_std * generator.standard_normal(u_data.shape) x_test = np.linspace(lower, upper, test_resolution)[:, None] test_shape = (test_resolution,) elif case == "helmholtz": x_lower, x_upper = map(float, config["domain_x"]) y_lower, y_upper = map(float, config["domain_y"]) x_pde = np.column_stack( ( generator.uniform(x_lower, x_upper, n_pde), generator.uniform(y_lower, y_upper, n_pde), ) ) if n_boundary < 4 or n_boundary % 4: raise ValueError("Helmholtz n_bnd must be divisible by 4 and at least 4") per_edge = n_boundary // 4 x_axis = np.linspace(x_lower, x_upper, per_edge) y_axis = np.linspace(y_lower, y_upper, per_edge) x_boundary = np.vstack( ( np.column_stack((x_axis, np.full(per_edge, y_lower))), np.column_stack((x_axis, np.full(per_edge, y_upper))), np.column_stack((np.full(per_edge, x_lower), y_axis)), np.column_stack((np.full(per_edge, x_upper), y_axis)), ) ) u_boundary = exact_solution(case, x_boundary) x_data = np.column_stack( ( generator.uniform(x_lower, x_upper, n_solution), generator.uniform(y_lower, y_upper, n_solution), ) ) u_data = exact_solution(case, x_data) grid_x = np.linspace(x_lower, x_upper, test_resolution) grid_y = np.linspace(y_lower, y_upper, test_resolution) mesh_x, mesh_y = np.meshgrid(grid_x, grid_y, indexing="xy") x_test = np.column_stack((mesh_x.ravel(), mesh_y.ravel())) test_shape = mesh_x.shape elif case == "burgers": x_lower, x_upper = map(float, config["domain_x"]) t_lower, t_upper = map(float, config["domain_t"]) x_pde = np.column_stack( ( generator.uniform(x_lower, x_upper, n_pde), generator.uniform(t_lower, t_upper, n_pde), ) ) if n_boundary < 2: raise ValueError("Burgers n_bnd must be at least 2") left_count = n_boundary // 2 right_count = n_boundary - left_count left_time = generator.uniform(t_lower, t_upper, left_count) right_time = generator.uniform(t_lower, t_upper, right_count) x_boundary = np.vstack( ( np.column_stack((np.full(left_count, x_lower), left_time)), np.column_stack((np.full(right_count, x_upper), right_time)), ) ) u_boundary = np.zeros((n_boundary, 1)) initial_x = generator.uniform(x_lower, x_upper, n_solution) x_data = np.column_stack((initial_x, np.full(n_solution, t_lower))) u_data = -np.sin(np.pi * initial_x)[:, None] grid_x = np.linspace(x_lower, x_upper, test_resolution) grid_t = np.linspace(t_lower, t_upper, test_resolution) mesh_x, mesh_t = np.meshgrid(grid_x, grid_t, indexing="xy") x_test = np.column_stack((mesh_x.ravel(), mesh_t.ravel())) test_shape = mesh_x.shape else: raise ValueError(f"unsupported case: {case}") return { "x_pde": np.asarray(x_pde, dtype=np.float64), "x_boundary": np.asarray(x_boundary, dtype=np.float64), "u_boundary": np.asarray(u_boundary, dtype=np.float64), "x_data": np.asarray(x_data, dtype=np.float64), "u_data": np.asarray(u_data, dtype=np.float64), "x_test": np.asarray(x_test, dtype=np.float64), "u_exact": exact_solution(case, np.asarray(x_test, dtype=np.float64)), "test_shape": test_shape, } def to_tensors( data: Mapping[str, np.ndarray | None], device: torch.device, dtype: torch.dtype, ) -> dict[str, torch.Tensor | None]: return { key: ( torch.as_tensor(value, dtype=dtype, device=device) if isinstance(value, np.ndarray) else None ) for key, value in data.items() if key != "test_shape" } def point_counts(data: Mapping[str, np.ndarray | None]) -> dict[str, int]: return { "pde": int(data["x_pde"].shape[0]), "boundary": int(data["x_boundary"].shape[0]), "data": int(data["x_data"].shape[0]) if data.get("x_data") is not None else 0, } def relative_l2(prediction: np.ndarray, reference: np.ndarray | None) -> float | None: if reference is None: return None return float( np.linalg.norm(prediction.reshape(-1) - reference.reshape(-1)) / (np.linalg.norm(reference.reshape(-1)) + 1.0e-12) )