| from __future__ import annotations |
|
|
| import math |
| from collections.abc import Mapping, Sequence |
|
|
| import torch |
| import torch.nn as nn |
|
|
|
|
| class FCN(nn.Module): |
| """Fully connected network used by each SA-PINN case.""" |
|
|
| def __init__( |
| self, |
| layer_sizes: Sequence[int], |
| activation: str = "tanh", |
| dtype: torch.dtype = torch.float64, |
| ) -> None: |
| super().__init__() |
| if len(layer_sizes) < 2: |
| raise ValueError("layer_sizes must include input and output widths") |
| if activation not in {"tanh", "sin", "sine", "relu", "gelu"}: |
| raise ValueError(f"unsupported activation: {activation}") |
| self.activation = "sin" if activation == "sine" else activation |
| stddev = math.sqrt(50.0 / layer_sizes[1]) |
| self.linears = nn.ModuleList( |
| nn.Linear(layer_sizes[index], layer_sizes[index + 1], dtype=dtype) |
| for index in range(len(layer_sizes) - 1) |
| ) |
| for linear in self.linears: |
| nn.init.trunc_normal_( |
| linear.weight, |
| mean=0.0, |
| std=stddev, |
| a=-2.0 * stddev, |
| b=2.0 * stddev, |
| ) |
| nn.init.zeros_(linear.bias) |
|
|
| def _activate(self, values: torch.Tensor) -> torch.Tensor: |
| if self.activation == "gelu": |
| return torch.nn.functional.gelu(values) |
| return getattr(torch, self.activation)(values) |
|
|
| def forward(self, inputs: torch.Tensor) -> torch.Tensor: |
| hidden = inputs |
| for linear in self.linears[:-1]: |
| hidden = self._activate(linear(hidden)) |
| return self.linears[-1](hidden) |
|
|
|
|
| class AttentionWeights(nn.Module): |
| """Positive, normalized, per-point self-adaptive loss weights.""" |
|
|
| def __init__( |
| self, |
| n_points: int, |
| dtype: torch.dtype = torch.float64, |
| initial_value: float = 0.0, |
| ) -> None: |
| super().__init__() |
| if n_points <= 0: |
| raise ValueError("attention requires a positive point count") |
| self.alpha = nn.Parameter( |
| torch.full((n_points, 1), initial_value, dtype=dtype) |
| ) |
|
|
| def forward(self) -> torch.Tensor: |
| shifted = self.alpha - self.alpha.detach().max() |
| weights = torch.exp(shifted) |
| weights = weights / (weights.mean() + torch.finfo(weights.dtype).eps) |
| return torch.clamp(weights, min=1.0e-2, max=100.0) |
|
|
|
|
| class SAPINN(nn.Module): |
| """Self-adaptive PINN with separate attention weights for each loss group.""" |
|
|
| def __init__( |
| self, |
| input_dim: int, |
| n_layers: int, |
| n_neurons: int, |
| activation: str, |
| n_pde: int, |
| n_data: int, |
| n_boundary: int, |
| attention_enabled: bool = True, |
| dtype: torch.dtype = torch.float64, |
| ) -> None: |
| super().__init__() |
| if min(input_dim, n_layers, n_neurons) <= 0: |
| raise ValueError("model dimensions must be positive") |
| self.net = FCN( |
| [input_dim, *([n_neurons] * n_layers), 1], |
| activation=activation, |
| dtype=dtype, |
| ) |
| self.att_pde = ( |
| AttentionWeights(n_pde, dtype=dtype) if attention_enabled and n_pde else None |
| ) |
| self.att_data = ( |
| AttentionWeights(n_data, dtype=dtype) if attention_enabled and n_data else None |
| ) |
| self.att_boundary = ( |
| AttentionWeights(n_boundary, dtype=dtype) |
| if attention_enabled and n_boundary |
| else None |
| ) |
|
|
| def forward(self, inputs: torch.Tensor) -> torch.Tensor: |
| return self.net(inputs) |
|
|
| def network_parameters(self): |
| return self.net.parameters() |
|
|
| def attention_parameters(self): |
| for attention in (self.att_pde, self.att_data, self.att_boundary): |
| if attention is not None: |
| yield from attention.parameters() |
|
|
| def set_attention_trainable(self, trainable: bool) -> None: |
| for parameter in self.attention_parameters(): |
| parameter.requires_grad_(trainable) |
|
|
|
|
| def build_model( |
| model_config: Mapping, |
| point_counts: Mapping[str, int], |
| attention_enabled: bool, |
| dtype: torch.dtype, |
| ) -> SAPINN: |
| return SAPINN( |
| input_dim=int(model_config["input_dim"]), |
| n_layers=int(model_config["n_layers"]), |
| n_neurons=int(model_config["n_neurons"]), |
| activation=str(model_config["activation"]), |
| n_pde=int(point_counts["pde"]), |
| n_data=int(point_counts["data"]), |
| n_boundary=int(point_counts["boundary"]), |
| attention_enabled=attention_enabled, |
| dtype=dtype, |
| ) |
|
|
|
|
| class Equation: |
| def residual( |
| self, model: SAPINN, coordinates: torch.Tensor |
| ) -> torch.Tensor: |
| raise NotImplementedError |
|
|
|
|
| class Laplace1D(Equation): |
| def __init__(self, diffusion: float = 1.0) -> None: |
| self.diffusion = diffusion |
|
|
| def residual(self, model: SAPINN, coordinates: torch.Tensor) -> torch.Tensor: |
| inputs = coordinates.detach().requires_grad_(True) |
| prediction = model(inputs) |
| first = torch.autograd.grad( |
| prediction, inputs, torch.ones_like(prediction), create_graph=True |
| )[0] |
| second = torch.autograd.grad( |
| first, inputs, torch.ones_like(first), create_graph=True |
| )[0] |
| source = torch.pi**2 * torch.sin(torch.pi * inputs) |
| return self.diffusion * second + source |
|
|
|
|
| class Helmholtz2D(Equation): |
| def __init__(self, wave_number: float = 1.0) -> None: |
| self.wave_number = wave_number |
|
|
| def residual(self, model: SAPINN, coordinates: torch.Tensor) -> torch.Tensor: |
| inputs = coordinates.detach().requires_grad_(True) |
| prediction = model(inputs) |
| gradient = torch.autograd.grad( |
| prediction, inputs, torch.ones_like(prediction), create_graph=True |
| )[0] |
| prediction_xx = torch.autograd.grad( |
| gradient[:, 0:1], |
| inputs, |
| torch.ones_like(gradient[:, 0:1]), |
| create_graph=True, |
| )[0][:, 0:1] |
| prediction_yy = torch.autograd.grad( |
| gradient[:, 1:2], |
| inputs, |
| torch.ones_like(gradient[:, 1:2]), |
| create_graph=True, |
| )[0][:, 1:2] |
| exact = torch.sin(torch.pi * inputs[:, 0:1]) * torch.sin( |
| 4.0 * torch.pi * inputs[:, 1:2] |
| ) |
| forcing = ( |
| -torch.pi**2 - (4.0 * torch.pi) ** 2 + self.wave_number**2 |
| ) * exact |
| return ( |
| prediction_xx |
| + prediction_yy |
| + self.wave_number**2 * prediction |
| - forcing |
| ) |
|
|
|
|
| class Burgers2D(Equation): |
| def __init__(self, viscosity: float = 0.01 / math.pi) -> None: |
| self.viscosity = viscosity |
|
|
| def residual(self, model: SAPINN, coordinates: torch.Tensor) -> torch.Tensor: |
| inputs = coordinates.detach().requires_grad_(True) |
| prediction = model(inputs) |
| gradient = torch.autograd.grad( |
| prediction, inputs, torch.ones_like(prediction), create_graph=True |
| )[0] |
| prediction_x = gradient[:, 0:1] |
| prediction_t = gradient[:, 1:2] |
| prediction_xx = torch.autograd.grad( |
| prediction_x, |
| inputs, |
| torch.ones_like(prediction_x), |
| create_graph=True, |
| )[0][:, 0:1] |
| return ( |
| prediction_t |
| + prediction * prediction_x |
| - self.viscosity * prediction_xx |
| ) |
|
|
|
|
| def weighted_mean_square( |
| residual: torch.Tensor, attention: AttentionWeights | None |
| ) -> torch.Tensor: |
| if attention is None: |
| return torch.mean(residual.square()) |
| weights = attention() |
| if weights.shape != residual.shape: |
| raise ValueError( |
| f"attention shape {tuple(weights.shape)} does not match residual {tuple(residual.shape)}" |
| ) |
| return torch.mean(weights * residual.square()) |
|
|
|
|
| def loss_components( |
| model: SAPINN, |
| equation: Equation, |
| tensors: Mapping[str, torch.Tensor], |
| ) -> dict[str, torch.Tensor]: |
| pde_residual = equation.residual(model, tensors["x_pde"]) |
| boundary_residual = model(tensors["x_boundary"]) - tensors["u_boundary"] |
| if tensors.get("x_data") is not None: |
| data_residual = model(tensors["x_data"]) - tensors["u_data"] |
| data_loss = weighted_mean_square(data_residual, model.att_data) |
| else: |
| data_loss = torch.zeros((), dtype=pde_residual.dtype, device=pde_residual.device) |
| return { |
| "data": data_loss, |
| "boundary": weighted_mean_square(boundary_residual, model.att_boundary), |
| "pde": weighted_mean_square(pde_residual, model.att_pde), |
| } |
|
|
|
|
| def weighted_loss(components: Mapping[str, torch.Tensor], weights: Mapping) -> torch.Tensor: |
| return ( |
| float(weights["data"]) * components["data"] |
| + float(weights["boundary"]) * components["boundary"] |
| + float(weights["pde"]) * components["pde"] |
| ) |
|
|