from __future__ import annotations from collections.abc import Mapping, Sequence import torch import torch.nn as nn class FCN(nn.Module): """Fully connected trunk used by the fuzzy PINN.""" def __init__( self, layer_sizes: Sequence[int], activation: str = "tanh", dtype: torch.dtype = torch.float32, ) -> 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 self.linears = nn.ModuleList( nn.Linear(layer_sizes[index], layer_sizes[index + 1], dtype=dtype) for index in range(len(layer_sizes) - 1) ) 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 FuzzyLayer(nn.Module): """Gaussian fuzzy membership rules followed by product inference.""" def __init__( self, input_dim: int, rule_count: int, dtype: torch.dtype = torch.float32, ) -> None: super().__init__() if min(input_dim, rule_count) <= 0: raise ValueError("input_dim and rule_count must be positive") self.input_dim = input_dim self.rule_count = rule_count self.centers = nn.Parameter(torch.empty(rule_count, input_dim, dtype=dtype)) self.sigma = nn.Parameter(torch.ones(rule_count, input_dim, dtype=dtype)) nn.init.xavier_uniform_(self.centers) def forward(self, inputs: torch.Tensor) -> torch.Tensor: differences = inputs[:, None, :] - self.centers[None, :, :] variance = self.sigma.square().clamp_min(torch.finfo(inputs.dtype).eps) memberships = torch.exp(-differences.square() / variance[None, :, :]) return memberships.prod(dim=-1) class FPINNNet(nn.Module): """Parallel neural and fuzzy feature branches with a fused linear head.""" def __init__( self, hidden_layers: Sequence[int], linear_dim: int, fuzzy_dim: int, activation: str = "tanh", dtype: torch.dtype = torch.float32, ) -> None: super().__init__() self.fcn = FCN( [2, *hidden_layers, linear_dim], activation=activation, dtype=dtype ) self.fuzzy = FuzzyLayer(2, fuzzy_dim, dtype=dtype) self.head = nn.Linear(linear_dim + fuzzy_dim, 1, dtype=dtype) def forward(self, coordinates: torch.Tensor) -> torch.Tensor: neural_features = torch.tanh(self.fcn(coordinates)) fuzzy_features = self.fuzzy(coordinates) return self.head(torch.cat((neural_features, fuzzy_features), dim=1)) class FPINNForward(nn.Module): """Forward Allen-Cahn solver with fixed PDE parameters.""" def __init__( self, model_config: Mapping, dtype: torch.dtype = torch.float32 ) -> None: super().__init__() self.net = FPINNNet( hidden_layers=model_config["hidden_layers"], linear_dim=int(model_config["linear_dim"]), fuzzy_dim=int(model_config["fuzzy_dim"]), activation=str(model_config["activation"]), dtype=dtype, ) def forward(self, coordinates: torch.Tensor) -> torch.Tensor: return self.net(coordinates) def predict_u(self, coordinates: torch.Tensor) -> torch.Tensor: return self.forward(coordinates) class FPINNInverse(FPINNForward): """Inverse Allen-Cahn solver with learnable diffusion and reaction values.""" def __init__( self, model_config: Mapping, initial_lambda_1: float = 1.0, initial_lambda_2: float = 0.0, dtype: torch.dtype = torch.float32, ) -> None: super().__init__(model_config, dtype=dtype) self.lambda_1 = nn.Parameter(torch.tensor(initial_lambda_1, dtype=dtype)) self.lambda_2 = nn.Parameter(torch.tensor(initial_lambda_2, dtype=dtype)) def build_model( task: str, model_config: Mapping, pde_config: Mapping, dtype: torch.dtype, ) -> FPINNForward: if task == "forward": return FPINNForward(model_config, dtype=dtype) if task == "inverse": return FPINNInverse( model_config, initial_lambda_1=float(pde_config["initial_lambda_1"]), initial_lambda_2=float(pde_config["initial_lambda_2"]), dtype=dtype, ) raise ValueError(f"unsupported task: {task}") def allen_cahn_residual( model: FPINNForward, coordinates: torch.Tensor, lambda_1: float | torch.Tensor, lambda_2: float | 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 - lambda_1 * prediction_xx + lambda_2 * (prediction.pow(3) - prediction) ) def loss_components( model: FPINNForward, x_data: torch.Tensor, u_data: torch.Tensor, x_pde: torch.Tensor, lambda_1: float | torch.Tensor, lambda_2: float | torch.Tensor, ) -> dict[str, torch.Tensor]: prediction = model(x_data) residual = allen_cahn_residual(model, x_pde, lambda_1, lambda_2) return { "data": torch.mean((prediction - u_data).square()), "pde": torch.mean(residual.square()), } def weighted_loss(components: Mapping[str, torch.Tensor], weights: Mapping) -> torch.Tensor: return float(weights["data"]) * components["data"] + float( weights["pde"] ) * components["pde"]