r"""Solves a 2D reaction-diffusion equation. .. math:::: \partial_t u - D \Delta u - u = 0 The equation is solved using an explicit discrete PINN, and a Natural Gradient optimizer. """ import torch from scimba_torch.approximation_space.nn_space import NNxtSpace from scimba_torch.domain.meshless_domain.domain_2d import Square2D from scimba_torch.integration.monte_carlo import DomainSampler, TensorizedSampler from scimba_torch.integration.monte_carlo_parameters import UniformParametricSampler from scimba_torch.integration.monte_carlo_time import UniformTimeSampler from scimba_torch.neural_nets.coordinates_based_nets.mlp import GenericMLP from scimba_torch.numerical_solvers.temporal_pde.pinns import ( NaturalGradientTemporalPinns, ) from scimba_torch.physical_models.elliptic_pde.abstract_elliptic_pde import ( StrongFormEllipticPDE, ) from scimba_torch.physical_models.temporal_pde.abstract_temporal_pde import ( GenericFirstOrderTemporalPDE, ) from scimba_torch.plots.plots_nd import plot_abstract_approx_spaces from scimba_torch.utils.scimba_tensors import LabelTensor def zero_rhs_stationary(x, mu): return torch.zeros(x.shape[0]) def zeros_bc_rhs_stationary(x, n, mu): return torch.zeros(x.shape[0]) def zeros_rhs(w, t, x, mu, nb_func: int = 1): return torch.zeros(x.shape[0], nb_func) def zeros_bc_rhs(w, t, x, n, mu, nb_func: int = 1): return torch.zeros(x.shape[0], nb_func) class ReactionDiffusionNDSpaceComponent(StrongFormEllipticPDE): def __init__( self, space, f=zero_rhs_stationary, g=zeros_bc_rhs_stationary, **kwargs ): super().__init__( space, linear=True, residual_size=1, bc_residual_size=1, **kwargs ) self.f = f self.g = g def rhs(self, w, x, mu): return self.f(x, mu) def operator(self, w, x, mu): u = w.get_components() D = mu.get_components() grad_u = torch.cat(tuple(self.grad(u, x)), dim=-1) space_dim = grad_u.shape[1] laplacian_u = [tuple(self.grad(grad_u[:, i], x))[i] for i in range(space_dim)] laplacian_u = torch.sum(torch.stack(laplacian_u, dim=-1), dim=-1) return -D * laplacian_u - u def bc_rhs(self, w, x, n, mu): return self.g(x, mu) def bc_operator(self, w, x, n, mu): u = w.get_components() grad_u = torch.cat(tuple(self.grad(u, x)), dim=-1) n_ = torch.cat(tuple(n.get_components()), dim=-1) return torch.sum(grad_u * n_, dim=-1, keepdim=True) def functional_operator(self, func, x, mu, theta): u = func(x, mu, theta) D = mu[0] grad_u = torch.func.jacrev(func, 0) grad_grad_u = torch.func.jacrev(grad_u, 0)(x, mu, theta).squeeze() laplacian_u = torch.einsum("ii", grad_grad_u) return -D * laplacian_u[None] - u def functional_operator_bc(self, func, x, n, mu, theta): grad_u = torch.func.jacrev(func, 0)(x, mu, theta) return grad_u @ n class ReactionDiffusionND(GenericFirstOrderTemporalPDE): def __init__(self, space, init, f=zeros_rhs, g=zeros_bc_rhs, **kwargs): # Below we initialize rhs and bc_rhs of the # space component to default values because it will not be used; # rhs and bc_rhs of the time-dependent PDE are used instead space_component = ReactionDiffusionNDSpaceComponent(space) super().__init__(space, space_component, init, f, g, **kwargs) def f_exact(t: LabelTensor, x: LabelTensor, mu: LabelTensor): x1, x2 = x.get_components() D = mu.get_components() t_ = t.get_components() cos = torch.cos(torch.pi * x1) * torch.cos(torch.pi * x2) exp = torch.exp((1 - 2 * D[0] * torch.pi**2) * t_) return cos * exp def f_ini(x: LabelTensor, mu): t = LabelTensor(torch.zeros((x.shape[0], 1)), x.labels) return f_exact(t, x, mu) def solve() -> NaturalGradientTemporalPinns: """Solves the problem for the reaction-diffusion equation.""" domain_x = Square2D([(0, 1), (0, 1)], is_main_domain=True) domain_mu = [(0.1, 0.1 + 1e-5)] t_min, t_max = 0.0, 1.0 sampler = TensorizedSampler( ( UniformTimeSampler((t_min, t_max)), DomainSampler(domain_x), UniformParametricSampler(domain_mu), ) ) space = NNxtSpace(1, 1, GenericMLP, domain_x, sampler, layer_sizes=[16, 16]) pde = ReactionDiffusionND(space, init=f_ini) pinn = NaturalGradientTemporalPinns( pde, ic_type="weak", bc_type="weak", bc_weight=10, ic_weight=10 ) pinn.solve( epochs=100, n_collocation=2_000, n_bc_collocation=2_000, n_ic_collocation=2_000 ) return {"x": domain_x, "mu": domain_mu, "t": (t_min, t_max)}, pinn # %% if __name__ == "__main__": domains, pinn = solve() plot_abstract_approx_spaces( pinn.space, domains["x"], domains["mu"], domains["t"], loss=pinn.losses, exact=f_exact, error=f_exact, )