| from __future__ import annotations |
|
|
| from collections.abc import Mapping |
| from pathlib import Path |
|
|
| import numpy as np |
| import scipy.io |
| import torch |
|
|
|
|
| REQUIRED_FIELDS = { |
| "x_f1", |
| "y_f1", |
| "x_f2", |
| "y_f2", |
| "x_f3", |
| "y_f3", |
| "xi1", |
| "yi1", |
| "xi2", |
| "yi2", |
| "xb", |
| "yb", |
| "ub", |
| "u_exact", |
| "u_exact1", |
| "u_exact2", |
| "u_exact3", |
| } |
|
|
|
|
| def load_mat_data(path: Path) -> dict: |
| if not path.is_file(): |
| raise FileNotFoundError(f"XPINN MATLAB data not found: {path}") |
| data = scipy.io.loadmat(path) |
| missing = REQUIRED_FIELDS.difference(data) |
| if missing: |
| raise ValueError(f"MATLAB data is missing fields: {sorted(missing)}") |
| return data |
|
|
|
|
| def column(data: Mapping, key: str) -> np.ndarray: |
| return np.asarray(data[key], dtype=np.float64).reshape(-1, 1) |
|
|
|
|
| def sample_indices( |
| generator: np.random.Generator, total_size: int, sample_size: int, name: str |
| ) -> np.ndarray: |
| if sample_size <= 0: |
| raise ValueError(f"{name} sample size must be positive") |
| if sample_size > total_size: |
| raise ValueError( |
| f"{name} sample size {sample_size} exceeds available points {total_size}" |
| ) |
| return generator.choice(total_size, sample_size, replace=False) |
|
|
|
|
| def tensor( |
| values: np.ndarray, |
| device: torch.device, |
| dtype: torch.dtype, |
| requires_grad: bool = False, |
| ) -> torch.Tensor: |
| return torch.as_tensor(values, dtype=dtype, device=device).clone().requires_grad_( |
| requires_grad |
| ) |
|
|
|
|
| def paired_sample( |
| data: Mapping, |
| x_key: str, |
| y_key: str, |
| sample_size: int, |
| generator: np.random.Generator, |
| device: torch.device, |
| dtype: torch.dtype, |
| name: str, |
| ) -> tuple[torch.Tensor, torch.Tensor]: |
| x = column(data, x_key) |
| y = column(data, y_key) |
| if x.shape != y.shape: |
| raise ValueError(f"coordinate shape mismatch for {name}: {x.shape} and {y.shape}") |
| indices = sample_indices(generator, x.shape[0], sample_size, name) |
| return ( |
| tensor(x[indices], device, dtype, requires_grad=True), |
| tensor(y[indices], device, dtype, requires_grad=True), |
| ) |
|
|
|
|
| def build_training_batch( |
| data: Mapping, |
| sample_counts: Mapping[str, int], |
| seed: int, |
| device: torch.device, |
| dtype: torch.dtype, |
| ) -> dict[str, torch.Tensor]: |
| generator = np.random.default_rng(seed) |
| x1, y1 = paired_sample( |
| data, |
| "x_f1", |
| "y_f1", |
| int(sample_counts["residual_1"]), |
| generator, |
| device, |
| dtype, |
| "residual_1", |
| ) |
| x2, y2 = paired_sample( |
| data, |
| "x_f2", |
| "y_f2", |
| int(sample_counts["residual_2"]), |
| generator, |
| device, |
| dtype, |
| "residual_2", |
| ) |
| x3, y3 = paired_sample( |
| data, |
| "x_f3", |
| "y_f3", |
| int(sample_counts["residual_3"]), |
| generator, |
| device, |
| dtype, |
| "residual_3", |
| ) |
| xi1, yi1 = paired_sample( |
| data, |
| "xi1", |
| "yi1", |
| int(sample_counts["interface_1"]), |
| generator, |
| device, |
| dtype, |
| "interface_1", |
| ) |
| xi2, yi2 = paired_sample( |
| data, |
| "xi2", |
| "yi2", |
| int(sample_counts["interface_2"]), |
| generator, |
| device, |
| dtype, |
| "interface_2", |
| ) |
|
|
| boundary_x = column(data, "xb") |
| boundary_y = column(data, "yb") |
| boundary_values = column(data, "ub") |
| if boundary_x.shape != boundary_y.shape or boundary_x.shape != boundary_values.shape: |
| raise ValueError("boundary coordinate and value shapes do not match") |
| boundary_indices = sample_indices( |
| generator, |
| boundary_x.shape[0], |
| int(sample_counts["boundary"]), |
| "boundary", |
| ) |
| return { |
| "xb": tensor(boundary_x[boundary_indices], device, dtype), |
| "yb": tensor(boundary_y[boundary_indices], device, dtype), |
| "ub": tensor(boundary_values[boundary_indices], device, dtype), |
| "x1": x1, |
| "y1": y1, |
| "x2": x2, |
| "y2": y2, |
| "x3": x3, |
| "y3": y3, |
| "xi1": xi1, |
| "yi1": yi1, |
| "xi2": xi2, |
| "yi2": yi2, |
| } |
|
|
|
|
| def build_evaluation_points( |
| data: Mapping, device: torch.device, dtype: torch.dtype |
| ) -> dict[str, torch.Tensor]: |
| points = {} |
| for domain in (1, 2, 3): |
| x = column(data, f"x_f{domain}") |
| y = column(data, f"y_f{domain}") |
| if x.shape != y.shape: |
| raise ValueError(f"evaluation coordinate mismatch in domain {domain}") |
| points[f"xy{domain}"] = tensor(np.hstack((x, y)), device, dtype) |
| return points |
|
|
|
|
| def exact_subdomain_values( |
| data: Mapping, device: torch.device, dtype: torch.dtype |
| ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: |
| return tuple( |
| tensor(column(data, f"u_exact{domain}"), device, dtype) |
| for domain in (1, 2, 3) |
| ) |
|
|
|
|
| def combined_coordinates(data: Mapping) -> tuple[np.ndarray, np.ndarray]: |
| x = np.concatenate([column(data, f"x_f{domain}").reshape(-1) for domain in (1, 2, 3)]) |
| y = np.concatenate([column(data, f"y_f{domain}").reshape(-1) for domain in (1, 2, 3)]) |
| return x, y |
|
|
|
|
| def combined_exact_solution(data: Mapping) -> np.ndarray: |
| exact = column(data, "u_exact").reshape(-1) |
| expected_size = sum(column(data, f"x_f{domain}").size for domain in (1, 2, 3)) |
| if exact.size != expected_size: |
| raise ValueError( |
| f"combined exact solution has {exact.size} values, expected {expected_size}" |
| ) |
| return exact |
|
|