From 8ff2876efa8f8bbebaf3b10020a6121936cfeecd Mon Sep 17 00:00:00 2001 From: valc89 <103250118+valc89@users.noreply.github.com> Date: Fri, 22 Mar 2024 12:46:03 +0100 Subject: [PATCH 01/18] Create sapinn.py --- pina/solvers/pinns/sapinn.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 pina/solvers/pinns/sapinn.py diff --git a/pina/solvers/pinns/sapinn.py b/pina/solvers/pinns/sapinn.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/pina/solvers/pinns/sapinn.py @@ -0,0 +1 @@ + From 692874015969e5a560c40cac0d2fd35d5ac2dafd Mon Sep 17 00:00:00 2001 From: valc89 Date: Fri, 22 Mar 2024 12:53:38 +0100 Subject: [PATCH 02/18] starting sapinn --- pina/solvers/pinns/sapinn.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pina/solvers/pinns/sapinn.py b/pina/solvers/pinns/sapinn.py index 8b1378917..41d024bd3 100644 --- a/pina/solvers/pinns/sapinn.py +++ b/pina/solvers/pinns/sapinn.py @@ -1 +1,7 @@ +import torch +from pina.solvers import PINN + + +class SAPINN(PINN): + pass \ No newline at end of file From e2bf66697aee1c7df57815fb6530b6a9ae987341 Mon Sep 17 00:00:00 2001 From: valc89 Date: Mon, 15 Apr 2024 15:50:15 +0200 Subject: [PATCH 03/18] First steps --- pina/solvers/pinns/sapinn.py | 61 +++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/pina/solvers/pinns/sapinn.py b/pina/solvers/pinns/sapinn.py index 41d024bd3..c9842995b 100644 --- a/pina/solvers/pinns/sapinn.py +++ b/pina/solvers/pinns/sapinn.py @@ -1,7 +1,66 @@ import torch +try: + from torch.optim.lr_scheduler import LRScheduler # torch >= 2.0 +except ImportError: + from torch.optim.lr_scheduler import ( + _LRScheduler as LRScheduler, + ) # torch < 2.0 + from pina.solvers import PINN +from torch.optim.lr_scheduler import ConstantLR class SAPINN(PINN): - pass \ No newline at end of file + """ + This class aims to implements the Self-Adaptive PINN solver, + using a user specified "model" to solve a specific "problem". + + .. seealso:: + **Original reference**: McClenny, Levi D., and Ulisses M. Braga-Neto. + "Self-adaptive physics-informed neural networks." + Journal of Computational Physics 474 (2023): 111722. + `_. + """ + + def __init__( + self, + problem, + model, + extra_features=None, + mask="polinomial", + loss=torch.nn.MSELoss(), + optimizer=torch.optim.Adam, + optimizer_kwargs={"lr" : 0.001}, + scheduler=ConstantLR, + scheduler_kwargs={"factor" : 1, "total_iters" : 0} + ): + """ + :param AbstractProblem problem: The formulation of the problem. + :param torch.nn.Module model: The neural network model to use. + :param torch.nn.Module loss: The loss function used as minimizer, + default :class:`torch.nn.MSELoss`. + :param torch.nn.Module extra_features: The additional input + features to use as augmented input. + :param str mask: type of mask applied to weights for the + self adaptive strategy + :param torch.optim.Optimizer optimizer: The neural network optimizer to + use; default is :class:`torch.optim.Adam`. + :param dict optimizer_kwargs: Optimizer constructor keyword args. + :param torch.optim.LRScheduler scheduler: Learning + rate scheduler. + :param dict scheduler_kwargs: LR scheduler constructor keyword args. + """ + super().__init__( + models=[model], + problem=problem, + optimizers=[optimizer], + optimizers_kwargs=[optimizer_kwargs], + extra_features=extra_features, + loss=loss, + scheduler=scheduler, + scheduler_kwargs=scheduler_kwargs + ) + + def _generate_weigths(self): + pass From 0f6fe91135146bf07de4c4a68e58b58ad36a8b8b Mon Sep 17 00:00:00 2001 From: valc89 Date: Mon, 15 Apr 2024 17:22:02 +0200 Subject: [PATCH 04/18] Inizialise self adaptive weights --- pina/solvers/pinns/sapinn.py | 46 +++++++++++++++++--- tests/test_solvers/test_sapinn.py | 70 +++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 tests/test_solvers/test_sapinn.py diff --git a/pina/solvers/pinns/sapinn.py b/pina/solvers/pinns/sapinn.py index c9842995b..14b7ebba0 100644 --- a/pina/solvers/pinns/sapinn.py +++ b/pina/solvers/pinns/sapinn.py @@ -7,6 +7,7 @@ _LRScheduler as LRScheduler, ) # torch < 2.0 +from pina import LabelTensor from pina.solvers import PINN from torch.optim.lr_scheduler import ConstantLR @@ -28,7 +29,7 @@ def __init__( problem, model, extra_features=None, - mask="polinomial", + mask_type={"type": "polynomial", "coefficient": [2]}, loss=torch.nn.MSELoss(), optimizer=torch.optim.Adam, optimizer_kwargs={"lr" : 0.001}, @@ -42,8 +43,10 @@ def __init__( default :class:`torch.nn.MSELoss`. :param torch.nn.Module extra_features: The additional input features to use as augmented input. - :param str mask: type of mask applied to weights for the + :param dict mask: type of mask applied to weights for the self adaptive strategy + mask_type["type"] -> polynomial, sigmoid + mask_type["coefficient"] -> list of coefficient :param torch.optim.Optimizer optimizer: The neural network optimizer to use; default is :class:`torch.optim.Adam`. :param dict optimizer_kwargs: Optimizer constructor keyword args. @@ -52,15 +55,46 @@ def __init__( :param dict scheduler_kwargs: LR scheduler constructor keyword args. """ super().__init__( - models=[model], + model=model, problem=problem, - optimizers=[optimizer], - optimizers_kwargs=[optimizer_kwargs], + optimizer=optimizer, + optimizer_kwargs=optimizer_kwargs, extra_features=extra_features, loss=loss, scheduler=scheduler, scheduler_kwargs=scheduler_kwargs ) + + # Attributes for inputs + self.mask_type = mask_type + + # Attributes + self.mask = self._initialize_mask + self.weights = self._generate_weigths() + + def _initialize_mask(self, x): + if self.mask_type["type"] == "polynomial": + if not isinstance(self.mask_type["coefficient"], list): + self.mask_type["coefficient"] = [self.mask_type["coefficient"]] + if len(self.mask_type["coefficient"]) != 1: + raise ValueError('Polynomial mask type requires only one coefficient') + return x ** self.mask_type["coefficient"][0] + if self.mask_type["type"] == "sigmoid": + sig = torch.nn.Sigmoid() + if not isinstance(self.mask_type["coefficient"], list): + raise ValueError('Sigmoid mask type has to be a list of coefficients') + if len(self.mask_type["coefficient"]) != 3: + raise ValueError('Sigmoid mask type requires three elements in the list') + coeffs = self.mask_type["coefficient"] + return coeffs[0]*sig(torch.tensor(coeffs[1]*x.numpy() + coeffs[2])) + raise ValueError("key type of mask_type Error") + def _generate_weigths(self): - pass + weigths_initilization = dict() + for key in self.problem.input_pts.keys(): + weigths_initilization[key] = LabelTensor( + self.mask(torch.rand(size = self.problem.input_pts[key].tensor.shape)), + labels=self.problem.input_variables + ) + return weigths_initilization diff --git a/tests/test_solvers/test_sapinn.py b/tests/test_solvers/test_sapinn.py new file mode 100644 index 000000000..01fb8360a --- /dev/null +++ b/tests/test_solvers/test_sapinn.py @@ -0,0 +1,70 @@ +import torch +import matplotlib.pyplot as plt + +from pina.solvers.pinns.sapinn import SAPINN +from pina.operators import laplacian +from pina.geometry import CartesianDomain +from pina import Condition, LabelTensor +from pina.problem import SpatialProblem +from pina.model import FeedForward +from pina.equation.equation import Equation +from pina.equation.equation_factory import FixedValue + +def laplace_equation(input_, output_): + force_term = (torch.sin(input_.extract(['x']) * torch.pi) * + torch.sin(input_.extract(['y']) * torch.pi)) + delta_u = laplacian(output_.extract(['u']), input_) + return delta_u - force_term + +my_laplace = Equation(laplace_equation) + +class Poisson(SpatialProblem): + output_variables = ['u'] + spatial_domain = CartesianDomain({'x': [0, 1], 'y': [0, 1]}) + + conditions = { + 'gamma1': Condition( + location=CartesianDomain({'x': [0, 1], 'y': 1}), + equation=FixedValue(0.0)), + 'gamma2': Condition( + location=CartesianDomain({'x': [0, 1], 'y': 0}), + equation=FixedValue(0.0)), + 'gamma3': Condition( + location=CartesianDomain({'x': 1, 'y': [0, 1]}), + equation=FixedValue(0.0)), + 'gamma4': Condition( + location=CartesianDomain({'x': 0, 'y': [0, 1]}), + equation=FixedValue(0.0)), + 'D': Condition( + location=CartesianDomain({'x' : [0, 1], 'y' : [0, 1]}), + equation=my_laplace) + } + + def poisson_sol(self, pts): + return -(torch.sin(pts.extract(['x']) * torch.pi) * + torch.sin(pts.extract(['y']) * torch.pi)) / (2 * torch.pi**2) + + truth_solution = poisson_sol + + +problem = Poisson() +# Discretizzazione del dominio +problem.discretise_domain(5, 'random', locations=['D']) +problem.discretise_domain(5, 'random', locations=['D', 'gamma1', 'gamma2', 'gamma3','gamma4']) + +# Definizione del modello risolutivo +model = FeedForward( + layers=[10, 10], + func=torch.nn.Tanh, + output_dimensions=len(problem.output_variables), + input_dimensions=len(problem.input_variables) +) + +# Inizializzazione SAPINN() +sapinn = SAPINN( + problem, + model, + mask_type={"type" : "sigmoid", "coefficient": [100, 1, 1]} +) + +print(sapinn.weights) \ No newline at end of file From 2064820a26989edf529afc555ed4db170ab5b9e7 Mon Sep 17 00:00:00 2001 From: valc89 Date: Mon, 15 Apr 2024 19:34:53 +0200 Subject: [PATCH 05/18] Not working SAPINN --- pina/solvers/pinns/sapinn.py | 162 ++++++++++++++++++++++++++---- tests/test_solvers/test_sapinn.py | 6 +- 2 files changed, 144 insertions(+), 24 deletions(-) diff --git a/pina/solvers/pinns/sapinn.py b/pina/solvers/pinns/sapinn.py index 14b7ebba0..5c849589b 100644 --- a/pina/solvers/pinns/sapinn.py +++ b/pina/solvers/pinns/sapinn.py @@ -8,11 +8,15 @@ ) # torch < 2.0 from pina import LabelTensor -from pina.solvers import PINN +from .basepinn import PINNInterface +from pina.utils import check_consistency +from pina.problem import InverseProblem + from torch.optim.lr_scheduler import ConstantLR -class SAPINN(PINN): + +class SAPINN(PINNInterface): """ This class aims to implements the Self-Adaptive PINN solver, using a user specified "model" to solve a specific "problem". @@ -32,7 +36,9 @@ def __init__( mask_type={"type": "polynomial", "coefficient": [2]}, loss=torch.nn.MSELoss(), optimizer=torch.optim.Adam, + optimizer_weights=torch.optim.Adam, optimizer_kwargs={"lr" : 0.001}, + optimizer_weights_kwargs={"lr" : 0.001}, scheduler=ConstantLR, scheduler_kwargs={"factor" : 1, "total_iters" : 0} ): @@ -54,47 +60,163 @@ def __init__( rate scheduler. :param dict scheduler_kwargs: LR scheduler constructor keyword args. """ + + # Attributes for inputs + self.mask_type = mask_type + + # Attributes + self.mask_model = self._initialize_mask() + super().__init__( - model=model, + models=[model, model], problem=problem, - optimizer=optimizer, - optimizer_kwargs=optimizer_kwargs, + optimizers=[optimizer, optimizer_weights], + optimizers_kwargs=[optimizer_kwargs, optimizer_weights_kwargs], extra_features=extra_features, - loss=loss, - scheduler=scheduler, - scheduler_kwargs=scheduler_kwargs + loss=loss ) + + #################################################################### + # Da PINN + # check consistency + check_consistency(scheduler, LRScheduler, subclass=True) + check_consistency(scheduler_kwargs, dict) - # Attributes for inputs - self.mask_type = mask_type + # assign variables + self._scheduler = scheduler(self.optimizers[0], **scheduler_kwargs) + self._neural_net = self.models[0] + #################################################################### - # Attributes - self.mask = self._initialize_mask self.weights = self._generate_weigths() - def _initialize_mask(self, x): + # Controllo massimizzazione + try: + optimizer_weights.maximize = True + except: + raise ValueError("Select an optimizer with the maximize attribute") + + self.configure_optimizers_weights() + + def _initialize_mask(self): if self.mask_type["type"] == "polynomial": if not isinstance(self.mask_type["coefficient"], list): self.mask_type["coefficient"] = [self.mask_type["coefficient"]] if len(self.mask_type["coefficient"]) != 1: raise ValueError('Polynomial mask type requires only one coefficient') - return x ** self.mask_type["coefficient"][0] + return lambda x: x ** self.mask_type["coefficient"][0] if self.mask_type["type"] == "sigmoid": - sig = torch.nn.Sigmoid() if not isinstance(self.mask_type["coefficient"], list): raise ValueError('Sigmoid mask type has to be a list of coefficients') if len(self.mask_type["coefficient"]) != 3: raise ValueError('Sigmoid mask type requires three elements in the list') coeffs = self.mask_type["coefficient"] - return coeffs[0]*sig(torch.tensor(coeffs[1]*x.numpy() + coeffs[2])) + return lambda x: coeffs[0]*torch.nn.Sigmoid(coeffs[1]*x+ coeffs[2]) raise ValueError("key type of mask_type Error") - def _generate_weigths(self): weigths_initilization = dict() for key in self.problem.input_pts.keys(): - weigths_initilization[key] = LabelTensor( - self.mask(torch.rand(size = self.problem.input_pts[key].tensor.shape)), - labels=self.problem.input_variables + weigths_initilization[key] = torch.nn.Parameter( + torch.rand(size = self.problem.input_pts[key].tensor.shape) ) return weigths_initilization + + def _loss_phys(self, samples, equation, condition_name): + """ + Computes the physics loss for the PINN solver based on input, + output, and condition name. This function is a wrapper of the function + :meth:`loss_phys` used internally in PINA to handle the logging step. + + :param LabelTensor samples: The samples to evaluate the physics loss. + :param EquationInterface equation: The governing equation + representing the physics. + :param str condition_name: The condition name for tracking purposes. + :return: The computed data loss. + :rtype: torch.Tensor + """ + loss_val = self.loss_phys(samples, equation, self.weights[condition_name]) + self.store_log(name=condition_name+'_loss', loss_val=float(loss_val)) + return loss_val.as_subclass(torch.Tensor) + + def loss_phys(self, samples, equation, weights): + try: + residual = weights * equation.residual(samples, self.forward(samples)) + except ( + TypeError + ): # this occurs when the function has three inputs, i.e. inverse problem + residual = weights * equation.residual( + samples, self.forward(samples), self._params + ) + return self.loss( + torch.zeros_like(residual, requires_grad=True), residual + ) + + def configure_optimizers_weights(self): + """ + Optimizer configuration for the SA-PINN + solver related to adaptive weights. + + :return: The optimizers and the schedulers + :rtype: tuple(list, list) + """ + self.optimizers[1].add_param_group( + { + "params": [ + self.weights[key] + for key in self.weights.keys() + ] + } + ) + return self.optimizers, [self.scheduler] + + ####################################################################### + # DA PINN + def forward(self, x): + """ + Forward pass implementation for the PINN + solver. + + :param LabelTensor x: Input tensor for the PINN solver. It expects + a tensor :math:`N \times D`, where :math:`N` the number of points + in the mesh, :math:`D` the dimension of the problem, + :return: PINN solution. + :rtype: LabelTensor + """ + return self.neural_net(x) + + def configure_optimizers(self): + """ + Optimizer configuration for the PINN + solver. + + :return: The optimizers and the schedulers + :rtype: tuple(list, list) + """ + # if the problem is an InverseProblem, add the unknown parameters + # to the parameters that the optimizer needs to optimize + if isinstance(self.problem, InverseProblem): + self.optimizers[0].add_param_group( + { + "params": [ + self._params[var] + for var in self.problem.unknown_variables + ] + } + ) + return self.optimizers, [self.scheduler] + + @property + def scheduler(self): + """ + Scheduler for the PINN training. + """ + return self._scheduler + + + @property + def neural_net(self): + """ + Neural network for the PINN training. + """ + return self._neural_net + ####################################################################### \ No newline at end of file diff --git a/tests/test_solvers/test_sapinn.py b/tests/test_solvers/test_sapinn.py index 01fb8360a..5bc0e10a8 100644 --- a/tests/test_solvers/test_sapinn.py +++ b/tests/test_solvers/test_sapinn.py @@ -64,7 +64,5 @@ def poisson_sol(self, pts): sapinn = SAPINN( problem, model, - mask_type={"type" : "sigmoid", "coefficient": [100, 1, 1]} -) - -print(sapinn.weights) \ No newline at end of file + mask_type={"type" : "sigmoid", "coefficient": [2, 1, 1]} +) \ No newline at end of file From 096dd0c78e92bd86d0986b223a96376046b1213b Mon Sep 17 00:00:00 2001 From: valc89 Date: Wed, 17 Apr 2024 10:30:01 +0200 Subject: [PATCH 06/18] Update sapinn solver --- pina/solvers/pinns/sapinn.py | 203 ++++++++++++++++-------------- tests/test_solvers/test_sapinn.py | 21 +++- 2 files changed, 128 insertions(+), 96 deletions(-) diff --git a/pina/solvers/pinns/sapinn.py b/pina/solvers/pinns/sapinn.py index 5c849589b..8ea7da6b2 100644 --- a/pina/solvers/pinns/sapinn.py +++ b/pina/solvers/pinns/sapinn.py @@ -14,7 +14,48 @@ from torch.optim.lr_scheduler import ConstantLR +class SAPINNWeightsModel(torch.nn.Module): + """ + This class aims to implements the weights of the Self-Adaptive + PINN solver. + """ + def __init__(self, dict_mask : dict, size : tuple) -> None: + super().__init__() + self.type_mask = dict_mask["type"] + self.weigth_of_mask = dict_mask["coefficient"] + self.sa_weights = torch.nn.Parameter(torch.randn(size=size)) + + if self.type_mask == "polynomial" and self._polynomial_consistency(): + self.func = self._polynomial_func + elif self.type_mask == "sigmoid" and self._sigmoidal_consistency(): + self.func = self._sigmoid_func + pass + else: + raise ValueError("type key of dict_mask not allowed") + + def _polynomial_consistency(self): + if not isinstance(self.weigth_of_mask, list): + self.weigth_of_mask = [self.weigth_of_mask] + if len(self.weigth_of_mask) != 1: + raise ValueError("coefficient key of dict_mask not coherent with type key. Polynomial mask type requires only one coefficient") + return True + + def _polynomial_func(self, x): + return x ** self.weigth_of_mask[0] + + def _sigmoidal_consistency(self): + if not isinstance(self.weigth_of_mask, list): + raise ValueError('Sigmoid mask type has to be a list of coefficients') + if len(self.weigth_of_mask) != 3: + raise ValueError('Sigmoid mask type requires three elements in the list') + return True + + def _sigmoid_func(self, x): + return self.weigth_of_mask[0]*torch.nn.Sigmoid(self.weigth_of_mask[1]*x+ self.weigth_of_mask[2]) + + def forward(self, x): + return self.func(self.sa_weights * x) class SAPINN(PINNInterface): """ @@ -60,24 +101,22 @@ def __init__( rate scheduler. :param dict scheduler_kwargs: LR scheduler constructor keyword args. """ - - # Attributes for inputs - self.mask_type = mask_type - - # Attributes - self.mask_model = self._initialize_mask() - super().__init__( - models=[model, model], + models=self._interface_models(problem, model, mask_type), problem=problem, - optimizers=[optimizer, optimizer_weights], - optimizers_kwargs=[optimizer_kwargs, optimizer_weights_kwargs], + optimizers=self._interface_optimizers(problem, optimizer, optimizer_weights), + optimizers_kwargs=self._interface_optimizers_kwargs(problem, optimizer_kwargs, optimizer_weights_kwargs), extra_features=extra_features, loss=loss ) - - #################################################################### - # Da PINN + + # Controllo massimizzazione + try: + for idx in range(1, len(self.optimizers)): + self.optimizers[idx].maximize = True + except: + raise ValueError("Select an optimizer with the maximize attribute") + # check consistency check_consistency(scheduler, LRScheduler, subclass=True) check_consistency(scheduler_kwargs, dict) @@ -85,92 +124,39 @@ def __init__( # assign variables self._scheduler = scheduler(self.optimizers[0], **scheduler_kwargs) self._neural_net = self.models[0] - #################################################################### - - self.weights = self._generate_weigths() - # Controllo massimizzazione - try: - optimizer_weights.maximize = True - except: - raise ValueError("Select an optimizer with the maximize attribute") - - self.configure_optimizers_weights() - - def _initialize_mask(self): - if self.mask_type["type"] == "polynomial": - if not isinstance(self.mask_type["coefficient"], list): - self.mask_type["coefficient"] = [self.mask_type["coefficient"]] - if len(self.mask_type["coefficient"]) != 1: - raise ValueError('Polynomial mask type requires only one coefficient') - return lambda x: x ** self.mask_type["coefficient"][0] - if self.mask_type["type"] == "sigmoid": - if not isinstance(self.mask_type["coefficient"], list): - raise ValueError('Sigmoid mask type has to be a list of coefficients') - if len(self.mask_type["coefficient"]) != 3: - raise ValueError('Sigmoid mask type requires three elements in the list') - coeffs = self.mask_type["coefficient"] - return lambda x: coeffs[0]*torch.nn.Sigmoid(coeffs[1]*x+ coeffs[2]) - raise ValueError("key type of mask_type Error") - - def _generate_weigths(self): - weigths_initilization = dict() + # dict - condition_name : index in self.models + self.dict_condition_idx = dict() + i = 0 for key in self.problem.input_pts.keys(): - weigths_initilization[key] = torch.nn.Parameter( - torch.rand(size = self.problem.input_pts[key].tensor.shape) + self.dict_condition_idx[key] = 1+i + i += 1 + + def _interface_models(self, problem, model, mask_type): + weights_models = [ + SAPINNWeightsModel( + dict_mask=mask_type, + size=value.tensor.shape ) - return weigths_initilization + for _, value in problem.input_pts.items() + ] + interface_models = [model] + interface_models.extend(weights_models) + return interface_models - def _loss_phys(self, samples, equation, condition_name): - """ - Computes the physics loss for the PINN solver based on input, - output, and condition name. This function is a wrapper of the function - :meth:`loss_phys` used internally in PINA to handle the logging step. - - :param LabelTensor samples: The samples to evaluate the physics loss. - :param EquationInterface equation: The governing equation - representing the physics. - :param str condition_name: The condition name for tracking purposes. - :return: The computed data loss. - :rtype: torch.Tensor - """ - loss_val = self.loss_phys(samples, equation, self.weights[condition_name]) - self.store_log(name=condition_name+'_loss', loss_val=float(loss_val)) - return loss_val.as_subclass(torch.Tensor) + def _interface_optimizers(self, problem, optimizer, optimizer_weights): + interface_optimizers = [optimizer] + interface_optimizers.extend([optimizer_weights for _, _ in problem.input_pts.items()]) + return interface_optimizers - def loss_phys(self, samples, equation, weights): - try: - residual = weights * equation.residual(samples, self.forward(samples)) - except ( - TypeError - ): # this occurs when the function has three inputs, i.e. inverse problem - residual = weights * equation.residual( - samples, self.forward(samples), self._params - ) - return self.loss( - torch.zeros_like(residual, requires_grad=True), residual - ) + def _interface_optimizers_kwargs(self, problem, optimizer_kwargs, optimizer_weights_kwargs): + interface_optimizers_kwargs = [optimizer_kwargs] + interface_optimizers_kwargs.extend([optimizer_weights_kwargs for _, _ in problem.input_pts.items()]) + return interface_optimizers_kwargs - def configure_optimizers_weights(self): - """ - Optimizer configuration for the SA-PINN - solver related to adaptive weights. - - :return: The optimizers and the schedulers - :rtype: tuple(list, list) - """ - self.optimizers[1].add_param_group( - { - "params": [ - self.weights[key] - for key in self.weights.keys() - ] - } - ) - return self.optimizers, [self.scheduler] + ########################################################################################################### + # DA pinn.py - ####################################################################### - # DA PINN def forward(self, x): """ Forward pass implementation for the PINN @@ -219,4 +205,35 @@ def neural_net(self): Neural network for the PINN training. """ return self._neural_net - ####################################################################### \ No newline at end of file + + ########################################################################################################### + + def _loss_phys(self, samples, equation, condition_name): + """ + Computes the physics loss for the PINN solver based on input, + output, and condition name. This function is a wrapper of the function + :meth:`loss_phys` used internally in PINA to handle the logging step. + + :param LabelTensor samples: The samples to evaluate the physics loss. + :param EquationInterface equation: The governing equation + representing the physics. + :param str condition_name: The condition name for tracking purposes. + :return: The computed data loss. + :rtype: torch.Tensor + """ + loss_val = self.loss_phys(samples, equation, self.models[self.dict_condition_idx[condition_name]]) + self.store_log(name=condition_name+'_loss', loss_val=float(loss_val)) + return loss_val.as_subclass(torch.Tensor) + + def loss_phys(self, samples, equation, weight_model): + try: + residual = weight_model(equation.residual(samples, self.forward(samples))) + except ( + TypeError + ): # this occurs when the function has three inputs, i.e. inverse problem + residual = weight_model(equation.residual( + samples, self.forward(samples), self._params + )) + return self.loss( + torch.zeros_like(residual, requires_grad=True), residual + ) \ No newline at end of file diff --git a/tests/test_solvers/test_sapinn.py b/tests/test_solvers/test_sapinn.py index 5bc0e10a8..200823553 100644 --- a/tests/test_solvers/test_sapinn.py +++ b/tests/test_solvers/test_sapinn.py @@ -1,10 +1,10 @@ import torch import matplotlib.pyplot as plt -from pina.solvers.pinns.sapinn import SAPINN +from pina.solvers.pinns.sapinn import SAPINN, SAPINNWeightsModel from pina.operators import laplacian from pina.geometry import CartesianDomain -from pina import Condition, LabelTensor +from pina import Condition, Trainer from pina.problem import SpatialProblem from pina.model import FeedForward from pina.equation.equation import Equation @@ -61,8 +61,23 @@ def poisson_sol(self, pts): ) # Inizializzazione SAPINN() +"""weights_model = SAPINNWeightsModel( + dict_mask={"type" : "polynomial", "coefficient": [2]}, + size=(250, 2) +)""" + sapinn = SAPINN( problem, model, mask_type={"type" : "sigmoid", "coefficient": [2, 1, 1]} -) \ No newline at end of file +) + +# Creaimo il trainer +trainer = Trainer( +solver=sapinn, +max_epochs=10, +accelerator='cpu', +enable_model_summary=False +) + +trainer.train() From de3fbe8d701b106731b60828c781c7a941ebcb24 Mon Sep 17 00:00:00 2001 From: valc89 Date: Wed, 17 Apr 2024 10:54:02 +0200 Subject: [PATCH 07/18] Update weights class --- pina/solvers/pinns/sapinn.py | 44 ++++++++++++++++++------------- tests/test_solvers/test_sapinn.py | 4 ++- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/pina/solvers/pinns/sapinn.py b/pina/solvers/pinns/sapinn.py index 8ea7da6b2..c59ba8812 100644 --- a/pina/solvers/pinns/sapinn.py +++ b/pina/solvers/pinns/sapinn.py @@ -21,36 +21,36 @@ class SAPINNWeightsModel(torch.nn.Module): """ def __init__(self, dict_mask : dict, size : tuple) -> None: + """ + :param dict dict_mask: Dict of keys "type" and "coefficient" + :param tuple size: The size of the self-adaptive weights coefficients. + """ super().__init__() self.type_mask = dict_mask["type"] + self.coefficient = self._type_coefficient() self.weigth_of_mask = dict_mask["coefficient"] + self._consistency() self.sa_weights = torch.nn.Parameter(torch.randn(size=size)) - - if self.type_mask == "polynomial" and self._polynomial_consistency(): + + def _type_coefficient(self): + if self.type_mask == "polynomial": self.func = self._polynomial_func - elif self.type_mask == "sigmoid" and self._sigmoidal_consistency(): + return 1 + if self.type_mask == "sigmoid": self.func = self._sigmoid_func - pass - else: - raise ValueError("type key of dict_mask not allowed") + return 3 + raise ValueError("Type of mask_type not allowed") - def _polynomial_consistency(self): + def _consistency(self): if not isinstance(self.weigth_of_mask, list): self.weigth_of_mask = [self.weigth_of_mask] - if len(self.weigth_of_mask) != 1: - raise ValueError("coefficient key of dict_mask not coherent with type key. Polynomial mask type requires only one coefficient") + if len(self.weigth_of_mask) != self.coefficient: + raise ValueError("coefficient key of dict_mask not coherent with type key.") return True def _polynomial_func(self, x): return x ** self.weigth_of_mask[0] - def _sigmoidal_consistency(self): - if not isinstance(self.weigth_of_mask, list): - raise ValueError('Sigmoid mask type has to be a list of coefficients') - if len(self.weigth_of_mask) != 3: - raise ValueError('Sigmoid mask type requires three elements in the list') - return True - def _sigmoid_func(self, x): return self.weigth_of_mask[0]*torch.nn.Sigmoid(self.weigth_of_mask[1]*x+ self.weigth_of_mask[2]) @@ -95,12 +95,17 @@ def __init__( mask_type["type"] -> polynomial, sigmoid mask_type["coefficient"] -> list of coefficient :param torch.optim.Optimizer optimizer: The neural network optimizer to - use; default is :class:`torch.optim.Adam`. - :param dict optimizer_kwargs: Optimizer constructor keyword args. + use for the model; default is :class:`torch.optim.Adam`. + :param torch.optim.Optimizer optimizer_weights: The neural network optimizer + to use for self-adaptive weights; default is :class `torch.optim.Adam`. + :param dict optimizer_kwargs: Optimizer constructor keyword args for the model. + :param dict optimizer_weights_kwargs: Optimizer constructor keyword + args for the self-adaptive weights. :param torch.optim.LRScheduler scheduler: Learning rate scheduler. :param dict scheduler_kwargs: LR scheduler constructor keyword args. """ + super().__init__( models=self._interface_models(problem, model, mask_type), problem=problem, @@ -116,6 +121,9 @@ def __init__( self.optimizers[idx].maximize = True except: raise ValueError("Select an optimizer with the maximize attribute") + + # set automatic optimization + self.automatic_optimization = False # check consistency check_consistency(scheduler, LRScheduler, subclass=True) diff --git a/tests/test_solvers/test_sapinn.py b/tests/test_solvers/test_sapinn.py index 200823553..e96031efc 100644 --- a/tests/test_solvers/test_sapinn.py +++ b/tests/test_solvers/test_sapinn.py @@ -62,7 +62,7 @@ def poisson_sol(self, pts): # Inizializzazione SAPINN() """weights_model = SAPINNWeightsModel( - dict_mask={"type" : "polynomial", "coefficient": [2]}, + dict_mask={"type" : "sigmoid", "coefficient": [2, 2, 1]}, size=(250, 2) )""" @@ -72,6 +72,7 @@ def poisson_sol(self, pts): mask_type={"type" : "sigmoid", "coefficient": [2, 1, 1]} ) + # Creaimo il trainer trainer = Trainer( solver=sapinn, @@ -80,4 +81,5 @@ def poisson_sol(self, pts): enable_model_summary=False ) + trainer.train() From 36fb57e1d260621c2a6de5ca36771d9926ff2f2a Mon Sep 17 00:00:00 2001 From: valc89 Date: Tue, 23 Apr 2024 15:19:59 +0200 Subject: [PATCH 08/18] Integrazione SAPINN --- pina/solvers/pinns/sapinn.py | 328 +++++++++++++++++------------- tests/test_solvers/test_sapinn.py | 85 ++++---- 2 files changed, 239 insertions(+), 174 deletions(-) diff --git a/pina/solvers/pinns/sapinn.py b/pina/solvers/pinns/sapinn.py index c59ba8812..8f7ffebd2 100644 --- a/pina/solvers/pinns/sapinn.py +++ b/pina/solvers/pinns/sapinn.py @@ -1,4 +1,5 @@ import torch +from copy import deepcopy try: from torch.optim.lr_scheduler import LRScheduler # torch >= 2.0 @@ -7,55 +8,31 @@ _LRScheduler as LRScheduler, ) # torch < 2.0 -from pina import LabelTensor from .basepinn import PINNInterface from pina.utils import check_consistency from pina.problem import InverseProblem from torch.optim.lr_scheduler import ConstantLR -class SAPINNWeightsModel(torch.nn.Module): +class Weights(torch.nn.Module): """ This class aims to implements the weights of the Self-Adaptive PINN solver. """ - def __init__(self, dict_mask : dict, size : tuple) -> None: + def __init__(self, func): """ - :param dict dict_mask: Dict of keys "type" and "coefficient" - :param tuple size: The size of the self-adaptive weights coefficients. + TODO """ super().__init__() - self.type_mask = dict_mask["type"] - self.coefficient = self._type_coefficient() - self.weigth_of_mask = dict_mask["coefficient"] - self._consistency() - self.sa_weights = torch.nn.Parameter(torch.randn(size=size)) - - def _type_coefficient(self): - if self.type_mask == "polynomial": - self.func = self._polynomial_func - return 1 - if self.type_mask == "sigmoid": - self.func = self._sigmoid_func - return 3 - raise ValueError("Type of mask_type not allowed") - - def _consistency(self): - if not isinstance(self.weigth_of_mask, list): - self.weigth_of_mask = [self.weigth_of_mask] - if len(self.weigth_of_mask) != self.coefficient: - raise ValueError("coefficient key of dict_mask not coherent with type key.") - return True - - def _polynomial_func(self, x): - return x ** self.weigth_of_mask[0] - - def _sigmoid_func(self, x): - return self.weigth_of_mask[0]*torch.nn.Sigmoid(self.weigth_of_mask[1]*x+ self.weigth_of_mask[2]) + check_consistency(func, torch.nn.Module) + self.sa_weights = torch.nn.Parameter( + torch.Tensor() + ) + self.func = func - def forward(self, x): - return self.func(self.sa_weights * x) + def forward(self): + return self.func(self.sa_weights) class SAPINN(PINNInterface): """ @@ -73,97 +50,94 @@ def __init__( self, problem, model, + weights_function=torch.nn.Sigmoid(), extra_features=None, - mask_type={"type": "polynomial", "coefficient": [2]}, loss=torch.nn.MSELoss(), - optimizer=torch.optim.Adam, + optimizer_model=torch.optim.Adam, + optimizer_model_kwargs={"lr" : 0.001}, optimizer_weights=torch.optim.Adam, - optimizer_kwargs={"lr" : 0.001}, optimizer_weights_kwargs={"lr" : 0.001}, - scheduler=ConstantLR, - scheduler_kwargs={"factor" : 1, "total_iters" : 0} + scheduler_model=ConstantLR, + scheduler_model_kwargs={"factor" : 1, "total_iters" : 0}, + scheduler_weights=ConstantLR, + scheduler_weights_kwargs={"factor" : 1, "total_iters" : 0} ): """ - :param AbstractProblem problem: The formulation of the problem. - :param torch.nn.Module model: The neural network model to use. - :param torch.nn.Module loss: The loss function used as minimizer, - default :class:`torch.nn.MSELoss`. - :param torch.nn.Module extra_features: The additional input - features to use as augmented input. - :param dict mask: type of mask applied to weights for the - self adaptive strategy - mask_type["type"] -> polynomial, sigmoid - mask_type["coefficient"] -> list of coefficient - :param torch.optim.Optimizer optimizer: The neural network optimizer to - use for the model; default is :class:`torch.optim.Adam`. - :param torch.optim.Optimizer optimizer_weights: The neural network optimizer - to use for self-adaptive weights; default is :class `torch.optim.Adam`. - :param dict optimizer_kwargs: Optimizer constructor keyword args for the model. - :param dict optimizer_weights_kwargs: Optimizer constructor keyword - args for the self-adaptive weights. - :param torch.optim.LRScheduler scheduler: Learning - rate scheduler. - :param dict scheduler_kwargs: LR scheduler constructor keyword args. + weights_function - torch.nn.___ funzione di attivazione per il modello sui pesi per ogni peso + # number of points fixed in the training """ + # check consistency weitghs_function + check_consistency(weights_function, torch.nn.Module) + + # create models for weights + weights_dict = {} + for condition_name in problem.conditions: + weights_dict[condition_name] = Weights(weights_function) + weights_dict = torch.nn.ModuleDict(weights_dict) + + super().__init__( - models=self._interface_models(problem, model, mask_type), + models=[model, weights_dict], problem=problem, - optimizers=self._interface_optimizers(problem, optimizer, optimizer_weights), - optimizers_kwargs=self._interface_optimizers_kwargs(problem, optimizer_kwargs, optimizer_weights_kwargs), + optimizers=[optimizer_model, optimizer_weights], + optimizers_kwargs=[optimizer_model_kwargs, optimizer_weights_kwargs], extra_features=extra_features, loss=loss ) - - # Controllo massimizzazione - try: - for idx in range(1, len(self.optimizers)): - self.optimizers[idx].maximize = True - except: - raise ValueError("Select an optimizer with the maximize attribute") # set automatic optimization self.automatic_optimization = False # check consistency - check_consistency(scheduler, LRScheduler, subclass=True) - check_consistency(scheduler_kwargs, dict) - - # assign variables - self._scheduler = scheduler(self.optimizers[0], **scheduler_kwargs) - self._neural_net = self.models[0] - - # dict - condition_name : index in self.models - self.dict_condition_idx = dict() - i = 0 - for key in self.problem.input_pts.keys(): - self.dict_condition_idx[key] = 1+i - i += 1 - - def _interface_models(self, problem, model, mask_type): - weights_models = [ - SAPINNWeightsModel( - dict_mask=mask_type, - size=value.tensor.shape - ) - for _, value in problem.input_pts.items() + check_consistency(scheduler_model, LRScheduler, subclass=True) + check_consistency(scheduler_model_kwargs, dict) + check_consistency(scheduler_weights, LRScheduler, subclass=True) + check_consistency(scheduler_weights_kwargs, dict) + + # assign schedulers + self._schedulers = [ + scheduler_model( + self.optimizers[0], **scheduler_model_kwargs + ), + scheduler_weights( + self.optimizers[1], **scheduler_weights_kwargs + ), ] - interface_models = [model] - interface_models.extend(weights_models) - return interface_models - - def _interface_optimizers(self, problem, optimizer, optimizer_weights): - interface_optimizers = [optimizer] - interface_optimizers.extend([optimizer_weights for _, _ in problem.input_pts.items()]) - return interface_optimizers + + self._model = self.models[0] + self._weights = self.models[1] + + self._vectorial_loss = deepcopy(loss) + self._vectorial_loss.reduction = "none" - def _interface_optimizers_kwargs(self, problem, optimizer_kwargs, optimizer_weights_kwargs): - interface_optimizers_kwargs = [optimizer_kwargs] - interface_optimizers_kwargs.extend([optimizer_weights_kwargs for _, _ in problem.input_pts.items()]) - return interface_optimizers_kwargs + def on_train_start(self): + for condition_name, tensor in self.problem.input_pts.items(): + self.weights_dict.torchmodel[condition_name].sa_weights.data = torch.rand( + (tensor.shape[0], 1), + dtype = tensor.dtype, + device = tensor.device + ) + return super().on_train_start() - ########################################################################################################### - # DA pinn.py + def on_train_batch_end(self,outputs, batch, batch_idx): + """ + This method is called at the end of each training batch, and ovverides + the PytorchLightining implementation for logging the checkpoints. + + :param outputs: The output from the model for the current batch. + :type outputs: Any + :param batch: The current batch of data. + :type batch: Any + :param batch_idx: The index of the current batch. + :type batch_idx: int + :return: Whatever is returned by the parent + method ``on_train_batch_end``. + :rtype: Any + """ + # increase by one the counter of optimization to save loggers + self.trainer.fit_loop.epoch_loop.manual_optimization.optim_step_progress.total.completed += 1 + return super().on_train_batch_end(outputs, batch, batch_idx) def forward(self, x): """ @@ -197,51 +171,131 @@ def configure_optimizers(self): ] } ) - return self.optimizers, [self.scheduler] + return self.optimizers, self._schedulers - @property - def scheduler(self): + def _loss_data(self, input_tensor, output_tensor): """ - Scheduler for the PINN training. + TODO """ - return self._scheduler + residual = self.forward(input_tensor) - output_tensor + return self._compute_loss(residual) + def _compute_loss(self, residual): + weights = self.weights_dict.torchmodel[self.current_condition_name].forward() + loss_value = self._vectorial_loss(torch.zeros_like(residual, requires_grad=True), residual) + return self._vect_to_scalar(weights * loss_value), self._vect_to_scalar(loss_value) - @property - def neural_net(self): + def loss_data(self, input_tensor, output_tensor): """ - Neural network for the PINN training. + Computes the data loss for the PINN solver based on input, + output, and condition name. This function is a wrapper of the function + :meth:`loss_data` used internally in PINA to handle the logging step. + + :param LabelTensor input_tensor: The input to the neural networks. + :param LabelTensor output_tensor: The true solution to compare the + network solution. + :return: The computed data loss. + :rtype: torch.Tensor """ - return self._neural_net - - ########################################################################################################### + # train weights + self.optimizer_weights.zero_grad() + weighted_loss, _ = self._loss_data(input_tensor, output_tensor) + loss_value = - weighted_loss.as_subclass(torch.Tensor) + self.manual_backward(loss_value) + self.optimizer_weights.step() - def _loss_phys(self, samples, equation, condition_name): + # detaching samples from the computational graph to erase it and setting + # the gradient to true to create a new computational graph. + # In alternative set `retain_graph=True`. + samples = samples.detach() + samples.requires_grad = True + + # train model + self.optimizer_model.zero_grad() + weighted_loss, loss = self._loss_data(input_tensor, output_tensor) + loss_value = weighted_loss.as_subclass(torch.Tensor) + self.manual_backward(loss_value) + self.optimizer_model.step() + + # store loss without weights + self.store_log(loss_value=float(loss)) + return loss_value + + def _vect_to_scalar(self, loss_value): + if self.loss.reduction == "mean": + ret = torch.mean(loss_value) + elif self.loss.reduction == "sum": + ret = torch.sum(loss_value) + else: + raise RuntimeError(f"Invalid reduction, got {self.loss.reduction} but expected mean or sum.") + return ret + + + def _loss_phys(self, samples, equation): """ - Computes the physics loss for the PINN solver based on input, - output, and condition name. This function is a wrapper of the function - :meth:`loss_phys` used internally in PINA to handle the logging step. + TODO + """ + residual = self.compute_residual(samples, equation) + return self._compute_loss(residual) + + def loss_phys(self, samples, equation): + """ + Computes the physics loss for the PINN solver based on given + samples and equation. :param LabelTensor samples: The samples to evaluate the physics loss. :param EquationInterface equation: The governing equation representing the physics. - :param str condition_name: The condition name for tracking purposes. - :return: The computed data loss. - :rtype: torch.Tensor + :return: The physics loss calculated based on given + samples and equation. + :rtype: LabelTensor""" + # train weights + self.optimizer_weights.zero_grad() + weighted_loss, _ = self._loss_phys(samples, equation) + loss_value = - weighted_loss.as_subclass(torch.Tensor) + self.manual_backward(loss_value) + self.optimizer_weights.step() + + # detaching samples from the computational graph to erase it and setting + # the gradient to true to create a new computational graph. + # In alternative set `retain_graph=True`. + samples = samples.detach() + samples.requires_grad = True + + # train model + self.optimizer_model.zero_grad() + weighted_loss, loss = self._loss_phys(samples, equation) + loss_value = weighted_loss.as_subclass(torch.Tensor) + self.manual_backward(loss_value) + self.optimizer_model.step() + + # store loss without weights + self.store_log(loss_value=float(loss)) + return loss_value + + @property + def neural_net(self): """ - loss_val = self.loss_phys(samples, equation, self.models[self.dict_condition_idx[condition_name]]) - self.store_log(name=condition_name+'_loss', loss_val=float(loss_val)) - return loss_val.as_subclass(torch.Tensor) + Neural network for the PINN training. + """ + return self.models[0] + + @property + def weights_dict(self): + return self.models[1] + + @property + def scheduler_model(self): + return self._scheduler[0] - def loss_phys(self, samples, equation, weight_model): - try: - residual = weight_model(equation.residual(samples, self.forward(samples))) - except ( - TypeError - ): # this occurs when the function has three inputs, i.e. inverse problem - residual = weight_model(equation.residual( - samples, self.forward(samples), self._params - )) - return self.loss( - torch.zeros_like(residual, requires_grad=True), residual - ) \ No newline at end of file + @property + def scheduler_weights(self): + return self._scheduler[1] + + @property + def optimizer_model(self): + return self.optimizers[0] + + @property + def optimizer_weights(self): + return self.optimizers[1] \ No newline at end of file diff --git a/tests/test_solvers/test_sapinn.py b/tests/test_solvers/test_sapinn.py index e96031efc..129dbe69f 100644 --- a/tests/test_solvers/test_sapinn.py +++ b/tests/test_solvers/test_sapinn.py @@ -1,75 +1,76 @@ import torch import matplotlib.pyplot as plt -from pina.solvers.pinns.sapinn import SAPINN, SAPINNWeightsModel -from pina.operators import laplacian +from pina.solvers.pinns.sapinn import SAPINN +from pina.solvers.pinns.pinn import PINN +from pina.operators import laplacian, grad from pina.geometry import CartesianDomain -from pina import Condition, Trainer -from pina.problem import SpatialProblem +from pina import Condition, Trainer, Plotter +from pina.problem import SpatialProblem, TimeDependentProblem from pina.model import FeedForward from pina.equation.equation import Equation from pina.equation.equation_factory import FixedValue -def laplace_equation(input_, output_): +"""def laplace_equation(input_, output_): force_term = (torch.sin(input_.extract(['x']) * torch.pi) * torch.sin(input_.extract(['y']) * torch.pi)) delta_u = laplacian(output_.extract(['u']), input_) return delta_u - force_term -my_laplace = Equation(laplace_equation) +my_laplace = Equation(laplace_equation)""" -class Poisson(SpatialProblem): +class Burgers(SpatialProblem,TimeDependentProblem): output_variables = ['u'] - spatial_domain = CartesianDomain({'x': [0, 1], 'y': [0, 1]}) + spatial_domain = CartesianDomain({'x': [-1, 1]}) + temporal_domanin = CartesianDomain({'t' : [0, 1]}) + + def initial_condition(input_, output_): + u_expected = - torch.sin(torch.pi * input_.extract('x')) + return output_.extract('u') - u_expected + + def burger_equation(input_, output_): + u_t = grad(output_, input_, components=['u'], d=['t']) + u_x = grad(output_, input_, components=['u'], d=['x']) + u_xx = laplacian(output_, input_, components=['u'], d=['x']) + return u_t + output_.extract('u') * u_x - (0.01/torch.pi) * u_xx conditions = { 'gamma1': Condition( - location=CartesianDomain({'x': [0, 1], 'y': 1}), - equation=FixedValue(0.0)), + location=CartesianDomain({'x': -1, 't': [0, 1]}), + equation=FixedValue(0.0) + ), 'gamma2': Condition( - location=CartesianDomain({'x': [0, 1], 'y': 0}), - equation=FixedValue(0.0)), - 'gamma3': Condition( - location=CartesianDomain({'x': 1, 'y': [0, 1]}), - equation=FixedValue(0.0)), - 'gamma4': Condition( - location=CartesianDomain({'x': 0, 'y': [0, 1]}), - equation=FixedValue(0.0)), + location=CartesianDomain({'x': 1, 't': [0, 1]}), + equation=FixedValue(0.0) + ), + 't0' : Condition( + location=CartesianDomain({'x' : [-1, 1], 't' : 0}), + equation=Equation(initial_condition) + ), 'D': Condition( - location=CartesianDomain({'x' : [0, 1], 'y' : [0, 1]}), - equation=my_laplace) + location=CartesianDomain({'x' : [-1, 1], 't' : [0, 1]}), + equation=Equation(burger_equation)) } - def poisson_sol(self, pts): - return -(torch.sin(pts.extract(['x']) * torch.pi) * - torch.sin(pts.extract(['y']) * torch.pi)) / (2 * torch.pi**2) - - truth_solution = poisson_sol - -problem = Poisson() +problem = Burgers() # Discretizzazione del dominio -problem.discretise_domain(5, 'random', locations=['D']) -problem.discretise_domain(5, 'random', locations=['D', 'gamma1', 'gamma2', 'gamma3','gamma4']) +problem.discretise_domain(10000, 'random', locations=['D']) +problem.discretise_domain(100, 'random', locations=['t0']) +problem.discretise_domain(200, 'random', locations=['gamma1', 'gamma2']) # Definizione del modello risolutivo model = FeedForward( - layers=[10, 10], + layers=[20, 20], func=torch.nn.Tanh, output_dimensions=len(problem.output_variables), input_dimensions=len(problem.input_variables) ) # Inizializzazione SAPINN() -"""weights_model = SAPINNWeightsModel( - dict_mask={"type" : "sigmoid", "coefficient": [2, 2, 1]}, - size=(250, 2) -)""" - sapinn = SAPINN( problem, - model, - mask_type={"type" : "sigmoid", "coefficient": [2, 1, 1]} + model ) @@ -83,3 +84,13 @@ def poisson_sol(self, pts): trainer.train() + +pl = Plotter() + +pl.plot(sapinn) + +for key, value in sapinn.models[1].torchmodel.items(): + plt.scatter(problem.input_pts[key].extract('x').tensor.detach().numpy(), problem.input_pts[key].extract('y').tensor.detach().numpy(), c=value.forward().detach().numpy()) +plt.colorbar() + +plt.show() \ No newline at end of file From 2e92d77cddbf43ecac9badd1e09582865da0efd2 Mon Sep 17 00:00:00 2001 From: valc89 Date: Tue, 23 Apr 2024 15:58:14 +0200 Subject: [PATCH 09/18] Finalize sapinn --- tests/test_solvers/test_sapinn.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_solvers/test_sapinn.py b/tests/test_solvers/test_sapinn.py index 129dbe69f..da62d0570 100644 --- a/tests/test_solvers/test_sapinn.py +++ b/tests/test_solvers/test_sapinn.py @@ -22,7 +22,7 @@ class Burgers(SpatialProblem,TimeDependentProblem): output_variables = ['u'] spatial_domain = CartesianDomain({'x': [-1, 1]}) - temporal_domanin = CartesianDomain({'t' : [0, 1]}) + temporal_domain = CartesianDomain({'t' : [0, 1]}) def initial_condition(input_, output_): u_expected = - torch.sin(torch.pi * input_.extract('x')) @@ -77,7 +77,7 @@ def burger_equation(input_, output_): # Creaimo il trainer trainer = Trainer( solver=sapinn, -max_epochs=10, +max_epochs=10000, accelerator='cpu', enable_model_summary=False ) @@ -90,7 +90,7 @@ def burger_equation(input_, output_): pl.plot(sapinn) for key, value in sapinn.models[1].torchmodel.items(): - plt.scatter(problem.input_pts[key].extract('x').tensor.detach().numpy(), problem.input_pts[key].extract('y').tensor.detach().numpy(), c=value.forward().detach().numpy()) + plt.scatter(problem.input_pts[key].extract('t').tensor.detach().numpy(), problem.input_pts[key].extract('x').tensor.detach().numpy(), c=value.forward().detach().numpy()) plt.colorbar() plt.show() \ No newline at end of file From fbefda41131c0f056561e178500bb271c7aa4fdb Mon Sep 17 00:00:00 2001 From: valc89 Date: Wed, 24 Apr 2024 13:24:54 +0200 Subject: [PATCH 10/18] SaPinn testing --- tests/test_solvers/test_sapinn.py | 70 +++++++++++++------------------ 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/tests/test_solvers/test_sapinn.py b/tests/test_solvers/test_sapinn.py index da62d0570..6e4faddb6 100644 --- a/tests/test_solvers/test_sapinn.py +++ b/tests/test_solvers/test_sapinn.py @@ -11,70 +11,59 @@ from pina.equation.equation import Equation from pina.equation.equation_factory import FixedValue -"""def laplace_equation(input_, output_): - force_term = (torch.sin(input_.extract(['x']) * torch.pi) * - torch.sin(input_.extract(['y']) * torch.pi)) - delta_u = laplacian(output_.extract(['u']), input_) - return delta_u - force_term +class Burgers(TimeDependentProblem, SpatialProblem): -my_laplace = Equation(laplace_equation)""" + # define the burger equation + def burger_equation(input_, output_): + du = grad(output_, input_) + ddu = grad(du, input_, components=['dudx']) + return ( + du.extract(['dudt']) + + output_.extract(['u'])*du.extract(['dudx']) - + (0.01/torch.pi)*ddu.extract(['ddudxdx']) + ) + + # define initial condition + def initial_condition(input_, output_): + u_expected = -torch.sin(torch.pi*input_.extract(['x'])) + return output_.extract(['u']) - u_expected -class Burgers(SpatialProblem,TimeDependentProblem): + # assign output/ spatial and temporal variables output_variables = ['u'] spatial_domain = CartesianDomain({'x': [-1, 1]}) - temporal_domain = CartesianDomain({'t' : [0, 1]}) - - def initial_condition(input_, output_): - u_expected = - torch.sin(torch.pi * input_.extract('x')) - return output_.extract('u') - u_expected - - def burger_equation(input_, output_): - u_t = grad(output_, input_, components=['u'], d=['t']) - u_x = grad(output_, input_, components=['u'], d=['x']) - u_xx = laplacian(output_, input_, components=['u'], d=['x']) - return u_t + output_.extract('u') * u_x - (0.01/torch.pi) * u_xx + temporal_domain = CartesianDomain({'t': [0, 1]}) + # problem condition statement conditions = { - 'gamma1': Condition( - location=CartesianDomain({'x': -1, 't': [0, 1]}), - equation=FixedValue(0.0) - ), - 'gamma2': Condition( - location=CartesianDomain({'x': 1, 't': [0, 1]}), - equation=FixedValue(0.0) - ), - 't0' : Condition( - location=CartesianDomain({'x' : [-1, 1], 't' : 0}), - equation=Equation(initial_condition) - ), - 'D': Condition( - location=CartesianDomain({'x' : [-1, 1], 't' : [0, 1]}), - equation=Equation(burger_equation)) + 'gamma1': Condition(location=CartesianDomain({'x': -1, 't': [0, 1]}), equation=FixedValue(0.)), + 'gamma2': Condition(location=CartesianDomain({'x': 1, 't': [0, 1]}), equation=FixedValue(0.)), + 't0': Condition(location=CartesianDomain({'x': [-1, 1], 't': 0}), equation=Equation(initial_condition)), + 'D': Condition(location=CartesianDomain({'x': [-1, 1], 't': [0, 1]}), equation=Equation(burger_equation)), } problem = Burgers() -# Discretizzazione del dominio +# discretizztion of the domain problem.discretise_domain(10000, 'random', locations=['D']) problem.discretise_domain(100, 'random', locations=['t0']) problem.discretise_domain(200, 'random', locations=['gamma1', 'gamma2']) -# Definizione del modello risolutivo +# definining the model model = FeedForward( - layers=[20, 20], + layers=[20, 20, 20, 20, 20, 20, 20, 20], func=torch.nn.Tanh, output_dimensions=len(problem.output_variables), input_dimensions=len(problem.input_variables) ) -# Inizializzazione SAPINN() +# inizialize sapinn sapinn = SAPINN( problem, model ) -# Creaimo il trainer +# Cration of the trainer trainer = Trainer( solver=sapinn, max_epochs=10000, @@ -82,13 +71,14 @@ def burger_equation(input_, output_): enable_model_summary=False ) - +# training trainer.train() +# plot of the approximate solution pl = Plotter() - pl.plot(sapinn) +# plot of weights for key, value in sapinn.models[1].torchmodel.items(): plt.scatter(problem.input_pts[key].extract('t').tensor.detach().numpy(), problem.input_pts[key].extract('x').tensor.detach().numpy(), c=value.forward().detach().numpy()) plt.colorbar() From 6b5985be92b8a1d71fd4855e0b966084bc1f28f7 Mon Sep 17 00:00:00 2001 From: valc89 Date: Wed, 24 Apr 2024 14:57:20 +0200 Subject: [PATCH 11/18] Update documentation --- pina/solvers/pinns/sapinn.py | 374 +++++++++++++++++++++++++---------- 1 file changed, 267 insertions(+), 107 deletions(-) diff --git a/pina/solvers/pinns/sapinn.py b/pina/solvers/pinns/sapinn.py index 8f7ffebd2..8657f4668 100644 --- a/pina/solvers/pinns/sapinn.py +++ b/pina/solvers/pinns/sapinn.py @@ -35,9 +35,64 @@ def forward(self): return self.func(self.sa_weights) class SAPINN(PINNInterface): - """ - This class aims to implements the Self-Adaptive PINN solver, - using a user specified "model" to solve a specific "problem". + r""" + Self Adaptive Physics Informed Neural Network (SAPINN) solver class. + This class implements Self-Adaptive Physics Informed Neural + Network solvers, using a user specified ``model`` to solve a specific + ``problem``. It can be used for solving both forward and inverse problems. + + The Self Adapive Physics Informed Neural Network aims to find + the solution :math:`\mathbf{u}:\Omega\rightarrow\mathbb{R}^m` + of the differential problem: + + .. math:: + + \begin{cases} + \mathcal{A}[\mathbf{u}](\mathbf{x})=0\quad,\mathbf{x}\in\Omega\\ + \mathcal{B}[\mathbf{u}](\mathbf{x})=0\quad, + \mathbf{x}\in\partial\Omega + \end{cases} + + integrating the pointwise loss evaluation through a mask :math:`m` and + self adaptive weights that permit to focus the loss function on + specific training samples. + The loss function to solve the problem is + + .. math:: + + \mathcal{L} = \mathcal{L}_\Omega + + \mathcal{L}_{\partial \Omega} , + + where + + .. math:: + + \mathcal{L}_\Omega = \frac{1}{N} \sum_{i=1}^{N_\Omega} m + \left( \lambda_{\Omega}^{i} \right) \left| \mathcal{A}[\mathbf{u}](\mathbf{x}) + \right|^2 + + and + + .. math:: + + \mathcal{L}_{\partial\Omega} = \frac{1}{N} \sum_{i=1}^{N_{\partial\Omega}} + m \left( \lambda_{\partial\Omega}^{i} \right) \left| \mathcal{B}[\mathbf{u}](\mathbf{x}) + \right|^2 , + + denoting the self adaptive weights as + :math:`\lambda_{\Omega}^1, \dots, \lambda_{\Omega}^{N_\Omega}` and + :math:`\lambda_{\partial \Omega}^1, \dots, \lambda_{\Omega}^{N_\partial \Omega}` + for :math:`\Omega` and :math:`\partial \Omega`, respectively. + + Self Adaptive Physics Informed Neural Network identifies the solution + and appropriate self adaptive weights by solving the following problem + + .. math:: + + \min_{w} \max_{\lambda_{\Omega}^k, \lambda_{\partial \Omega}^s} + \mathcal{L} , + + where :math:`w` denotes the network parameters. .. seealso:: **Original reference**: McClenny, Levi D., and Ulisses M. Braga-Neto. @@ -63,8 +118,38 @@ def __init__( scheduler_weights_kwargs={"factor" : 1, "total_iters" : 0} ): """ - weights_function - torch.nn.___ funzione di attivazione per il modello sui pesi per ogni peso - # number of points fixed in the training + :param AbstractProblem problem: The formualation of the problem. + :param torch.nn.Module model: The neural network model to use + for the model. + :param torch.nn.Module weights_function: The neural network model + related to the mask of SAPINN. + default :class:`torch.nn.Sigmoid()`. + :param list(torch.nn.Module) extra_features: The additional input + features to use as augmented input. If ``None`` no extra features + are passed. If it is a list of :class:`torch.nn.Module`, + the extra feature list is passed to all models. If it is a list + of extra features' lists, each single list of extra feature + is passed to a model. + :param torch.nn.Module loss: The loss function used as minimizer, + default :class:`torch.nn.MSELoss`. + :param torch.optim.Optimizer optimizer_model: The neural + network optimizer to use for the model network + , default is `torch.optim.Adam`. + :param dict optimizer_model_kwargs: Optimizer constructor keyword + args. for the model. + :param torch.optim.Optimizer optimizer_weights: The neural + network optimizer to use for mask model model, + default is `torch.optim.Adam`. + :param dict optimizer_weights_kwargs: Optimizer constructor + keyword args. for the mask module. + :param torch.optim.LRScheduler scheduler_model: Learning + rate scheduler for the model. + :param dict scheduler_model_kwargs: LR scheduler constructor + keyword args. + :param torch.optim.LRScheduler scheduler_weights: Learning + rate scheduler for the mask model. + :param dict scheduler_model_kwargs: LR scheduler constructor + keyword args. """ # check consistency weitghs_function @@ -111,40 +196,14 @@ def __init__( self._vectorial_loss = deepcopy(loss) self._vectorial_loss.reduction = "none" - def on_train_start(self): - for condition_name, tensor in self.problem.input_pts.items(): - self.weights_dict.torchmodel[condition_name].sa_weights.data = torch.rand( - (tensor.shape[0], 1), - dtype = tensor.dtype, - device = tensor.device - ) - return super().on_train_start() - - def on_train_batch_end(self,outputs, batch, batch_idx): - """ - This method is called at the end of each training batch, and ovverides - the PytorchLightining implementation for logging the checkpoints. - - :param outputs: The output from the model for the current batch. - :type outputs: Any - :param batch: The current batch of data. - :type batch: Any - :param batch_idx: The index of the current batch. - :type batch_idx: int - :return: Whatever is returned by the parent - method ``on_train_batch_end``. - :rtype: Any - """ - # increase by one the counter of optimization to save loggers - self.trainer.fit_loop.epoch_loop.manual_optimization.optim_step_progress.total.completed += 1 - return super().on_train_batch_end(outputs, batch, batch_idx) - def forward(self, x): """ Forward pass implementation for the PINN - solver. + solver. It returns the function + evaluation :math:`\mathbf{u}(\mathbf{x})` at the control points + :math:`\mathbf{x}`. - :param LabelTensor x: Input tensor for the PINN solver. It expects + :param LabelTensor x: Input tensor for the SAPINN solver. It expects a tensor :math:`N \times D`, where :math:`N` the number of points in the mesh, :math:`D` the dimension of the problem, :return: PINN solution. @@ -152,44 +211,46 @@ def forward(self, x): """ return self.neural_net(x) - def configure_optimizers(self): + def loss_phys(self, samples, equation): """ - Optimizer configuration for the PINN - solver. + Computes the physics loss for the SAPINN solver based on given + samples and equation. - :return: The optimizers and the schedulers - :rtype: tuple(list, list) - """ - # if the problem is an InverseProblem, add the unknown parameters - # to the parameters that the optimizer needs to optimize - if isinstance(self.problem, InverseProblem): - self.optimizers[0].add_param_group( - { - "params": [ - self._params[var] - for var in self.problem.unknown_variables - ] - } - ) - return self.optimizers, self._schedulers - - def _loss_data(self, input_tensor, output_tensor): - """ - TODO - """ - residual = self.forward(input_tensor) - output_tensor - return self._compute_loss(residual) + :param LabelTensor samples: The samples to evaluate the physics loss. + :param EquationInterface equation: The governing equation + representing the physics. + :return: The physics loss calculated based on given + samples and equation. + :rtype: LabelTensor""" + # train weights + self.optimizer_weights.zero_grad() + weighted_loss, _ = self._loss_phys(samples, equation) + loss_value = - weighted_loss.as_subclass(torch.Tensor) + self.manual_backward(loss_value) + self.optimizer_weights.step() - def _compute_loss(self, residual): - weights = self.weights_dict.torchmodel[self.current_condition_name].forward() - loss_value = self._vectorial_loss(torch.zeros_like(residual, requires_grad=True), residual) - return self._vect_to_scalar(weights * loss_value), self._vect_to_scalar(loss_value) + # detaching samples from the computational graph to erase it and setting + # the gradient to true to create a new computational graph. + # In alternative set `retain_graph=True`. + samples = samples.detach() + samples.requires_grad = True + + # train model + self.optimizer_model.zero_grad() + weighted_loss, loss = self._loss_phys(samples, equation) + loss_value = weighted_loss.as_subclass(torch.Tensor) + self.manual_backward(loss_value) + self.optimizer_model.step() + + # store loss without weights + self.store_log(loss_value=float(loss)) + return loss_value def loss_data(self, input_tensor, output_tensor): """ - Computes the data loss for the PINN solver based on input, - output, and condition name. This function is a wrapper of the function - :meth:`loss_data` used internally in PINA to handle the logging step. + Computes the data loss for the SAPINN solver based on input and + output. It computes the loss between the + network output against the true solution. :param LabelTensor input_tensor: The input to the neural networks. :param LabelTensor output_tensor: The true solution to compare the @@ -220,82 +281,181 @@ def loss_data(self, input_tensor, output_tensor): # store loss without weights self.store_log(loss_value=float(loss)) return loss_value + + def configure_optimizers(self): + """ + Optimizer configuration for the SAPINN + solver. - def _vect_to_scalar(self, loss_value): - if self.loss.reduction == "mean": - ret = torch.mean(loss_value) - elif self.loss.reduction == "sum": - ret = torch.sum(loss_value) - else: - raise RuntimeError(f"Invalid reduction, got {self.loss.reduction} but expected mean or sum.") - return ret - + :return: The optimizers and the schedulers + :rtype: tuple(list, list) + """ + # if the problem is an InverseProblem, add the unknown parameters + # to the parameters that the optimizer needs to optimize + if isinstance(self.problem, InverseProblem): + self.optimizers[0].add_param_group( + { + "params": [ + self._params[var] + for var in self.problem.unknown_variables + ] + } + ) + return self.optimizers, self._schedulers + + def on_train_batch_end(self,outputs, batch, batch_idx): + """ + This method is called at the end of each training batch, and ovverides + the PytorchLightining implementation for logging the checkpoints. + :param torch.Tensor outputs: The output from the model for the + current batch. + :param tuple batch: The current batch of data. + :param int batch_idx: The index of the current batch. + :return: Whatever is returned by the parent + method ``on_train_batch_end``. + :rtype: Any + """ + # increase by one the counter of optimization to save loggers + self.trainer.fit_loop.epoch_loop.manual_optimization.optim_step_progress.total.completed += 1 + return super().on_train_batch_end(outputs, batch, batch_idx) + + def on_train_start(self): + """ + This method is called at the start of the training for setting + the self adaptive weights as parameters of the mask model. + + :return: Whatever is returned by the parent + method ``on_train_start``. + :rtype: Any + """ + for condition_name, tensor in self.problem.input_pts.items(): + self.weights_dict.torchmodel[condition_name].sa_weights.data = torch.rand( + (tensor.shape[0], 1), + dtype = tensor.dtype, + device = tensor.device + ) + return super().on_train_start() + def _loss_phys(self, samples, equation): """ - TODO + Elaboration of the physical loss for the SAPINN solver. + + :param LabelTensor samples: Input samples to evaluate the physics loss. + :param EquationInterface equation: the governing equation representing + the physics. + + :return: the scalar physical loss for the trainign step + :rtype: torch.Tensor """ residual = self.compute_residual(samples, equation) return self._compute_loss(residual) + + def _loss_data(self, input_tensor, output_tensor): + """ + Elaboration of the loss related to data for the SAPINN solver. - def loss_phys(self, samples, equation): + :param LabelTensor input_tensor: The input to the neural networks. + :param LabelTensor output_tensor: The true solution to compare the + network solution. + + :return: the scalar data loss for the trainign step + :rtype: torch.Tensor """ - Computes the physics loss for the PINN solver based on given - samples and equation. + residual = self.forward(input_tensor) - output_tensor + return self._compute_loss(residual) - :param LabelTensor samples: The samples to evaluate the physics loss. - :param EquationInterface equation: The governing equation - representing the physics. - :return: The physics loss calculated based on given - samples and equation. - :rtype: LabelTensor""" - # train weights - self.optimizer_weights.zero_grad() - weighted_loss, _ = self._loss_phys(samples, equation) - loss_value = - weighted_loss.as_subclass(torch.Tensor) - self.manual_backward(loss_value) - self.optimizer_weights.step() + def _compute_loss(self, residual): + """ + Elaboration of the pointwise loss through the mask model and the + self adaptive weights - # detaching samples from the computational graph to erase it and setting - # the gradient to true to create a new computational graph. - # In alternative set `retain_graph=True`. - samples = samples.detach() - samples.requires_grad = True + :param LabelTensor residual: the matrix of residuals that have to be weighted - # train model - self.optimizer_model.zero_grad() - weighted_loss, loss = self._loss_phys(samples, equation) - loss_value = weighted_loss.as_subclass(torch.Tensor) - self.manual_backward(loss_value) - self.optimizer_model.step() + :return: tuple with weighted and not weighted loss + :rtype List[torch.Tensor, torch.Tensor] + """ + weights = self.weights_dict.torchmodel[self.current_condition_name].forward() + loss_value = self._vectorial_loss(torch.zeros_like(residual, requires_grad=True), residual) + return self._vect_to_scalar(weights * loss_value), self._vect_to_scalar(loss_value) - # store loss without weights - self.store_log(loss_value=float(loss)) - return loss_value + def _vect_to_scalar(self, loss_value): + """ + Elaboration of the pointwise loss through the mask model and the + self adaptive weights + + :param torch.Tensor loss_value: the matrix of pointwise loss + + :return: the scalar loss + :rtype torch.Tensor, torch.Tensor + """ + if self.loss.reduction == "mean": + ret = torch.mean(loss_value) + elif self.loss.reduction == "sum": + ret = torch.sum(loss_value) + else: + raise RuntimeError(f"Invalid reduction, got {self.loss.reduction} but expected mean or sum.") + return ret + @property def neural_net(self): """ - Neural network for the PINN training. + Returns the neural network model. + + :return: The neural network model. + :rtype: torch.nn.Module """ return self.models[0] @property def weights_dict(self): + """ + Return the mask models associate to the application of + the mask to the self adaptive weights for each loss that + compones the global loss of the problem. + + :return: The ModuleDict for mask models. + :rtype: torch.nn.ModuleDict + """ return self.models[1] @property def scheduler_model(self): + """ + Returns the scheduler associated with the neural network model. + + :return: The scheduler for the neural network model. + :rtype: torch.optim.lr_scheduler._LRScheduler + """ return self._scheduler[0] @property def scheduler_weights(self): + """ + Returns the scheduler associated with the mask model (if applicable). + + :return: The scheduler for the mask model. + :rtype: torch.optim.lr_scheduler._LRScheduler + """ return self._scheduler[1] @property def optimizer_model(self): + """ + Returns the optimizer associated with the neural network model. + + :return: The optimizer for the neural network model. + :rtype: torch.optim.Optimizer + """ return self.optimizers[0] @property def optimizer_weights(self): + """ + Returns the optimizer associated with the mask model (if applicable). + + :return: The optimizer for the mask model. + :rtype: torch.optim.Optimizer + """ return self.optimizers[1] \ No newline at end of file From 9a44b58f1e155e778824fb173ebc7f1051b45056 Mon Sep 17 00:00:00 2001 From: valc89 Date: Wed, 24 Apr 2024 17:07:39 +0200 Subject: [PATCH 12/18] correction of documentation --- pina/solvers/pinns/sapinn.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pina/solvers/pinns/sapinn.py b/pina/solvers/pinns/sapinn.py index 8657f4668..dd075585d 100644 --- a/pina/solvers/pinns/sapinn.py +++ b/pina/solvers/pinns/sapinn.py @@ -221,7 +221,8 @@ def loss_phys(self, samples, equation): representing the physics. :return: The physics loss calculated based on given samples and equation. - :rtype: LabelTensor""" + :rtype: torch.Tensor + """ # train weights self.optimizer_weights.zero_grad() weighted_loss, _ = self._loss_phys(samples, equation) @@ -345,8 +346,8 @@ def _loss_phys(self, samples, equation): :param EquationInterface equation: the governing equation representing the physics. - :return: the scalar physical loss for the trainign step - :rtype: torch.Tensor + :return: tuple with weighted and not weighted scalar loss + :rtype: List[LabelTensor, LabelTensor] """ residual = self.compute_residual(samples, equation) return self._compute_loss(residual) @@ -359,8 +360,8 @@ def _loss_data(self, input_tensor, output_tensor): :param LabelTensor output_tensor: The true solution to compare the network solution. - :return: the scalar data loss for the trainign step - :rtype: torch.Tensor + :return: tuple with weighted and not weighted scalar loss + :rtype: List[LabelTensor, LabelTensor] """ residual = self.forward(input_tensor) - output_tensor return self._compute_loss(residual) @@ -373,7 +374,7 @@ def _compute_loss(self, residual): :param LabelTensor residual: the matrix of residuals that have to be weighted :return: tuple with weighted and not weighted loss - :rtype List[torch.Tensor, torch.Tensor] + :rtype List[LabelTensor, LabelTensor] """ weights = self.weights_dict.torchmodel[self.current_condition_name].forward() loss_value = self._vectorial_loss(torch.zeros_like(residual, requires_grad=True), residual) @@ -384,10 +385,10 @@ def _vect_to_scalar(self, loss_value): Elaboration of the pointwise loss through the mask model and the self adaptive weights - :param torch.Tensor loss_value: the matrix of pointwise loss + :param LabelTensor loss_value: the matrix of pointwise loss :return: the scalar loss - :rtype torch.Tensor, torch.Tensor + :rtype LabelTensor """ if self.loss.reduction == "mean": ret = torch.mean(loss_value) From 5984d41dacc2e8f03bbd9f4f7860ac5f16eac6f9 Mon Sep 17 00:00:00 2001 From: valc89 Date: Wed, 24 Apr 2024 17:37:20 +0200 Subject: [PATCH 13/18] update of the documentation of mask module --- pina/solvers/pinns/sapinn.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pina/solvers/pinns/sapinn.py b/pina/solvers/pinns/sapinn.py index dd075585d..8fe5417a6 100644 --- a/pina/solvers/pinns/sapinn.py +++ b/pina/solvers/pinns/sapinn.py @@ -16,13 +16,14 @@ class Weights(torch.nn.Module): """ - This class aims to implements the weights of the Self-Adaptive + This class aims to implements the mask model for + self adaptive weights of the Self-Adaptive PINN solver. """ def __init__(self, func): """ - TODO + :param torch.nn.Module func: the mask module of SAPINN """ super().__init__() check_consistency(func, torch.nn.Module) @@ -32,6 +33,14 @@ def __init__(self, func): self.func = func def forward(self): + """ + Forward pass implementation for the mask module. + It returns the function on the weights + evaluation. + + :return: evaluation of self adaptive weights through the mask. + :rtype: torch.Tensor + """ return self.func(self.sa_weights) class SAPINN(PINNInterface): From a4dc7927672d89f434e9dd03e50c85048a1bc17d Mon Sep 17 00:00:00 2001 From: valc89 Date: Tue, 30 Apr 2024 11:20:33 +0200 Subject: [PATCH 14/18] Final sapinn --- docs/source/_rst/_code.rst | 1 + docs/source/_rst/solvers/sapinn.rst | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 docs/source/_rst/solvers/sapinn.rst diff --git a/docs/source/_rst/_code.rst b/docs/source/_rst/_code.rst index fd858300f..3aa1f9203 100644 --- a/docs/source/_rst/_code.rst +++ b/docs/source/_rst/_code.rst @@ -40,6 +40,7 @@ Solvers GPINN CausalPINN CompetitivePINN + SAPINN Supervised solver GAROM diff --git a/docs/source/_rst/solvers/sapinn.rst b/docs/source/_rst/solvers/sapinn.rst new file mode 100644 index 000000000..b20891fff --- /dev/null +++ b/docs/source/_rst/solvers/sapinn.rst @@ -0,0 +1,7 @@ +SAPINN +====== +.. currentmodule:: pina.solvers.pinns.sapinn + +.. autoclass:: SAPINN + :members: + :show-inheritance: \ No newline at end of file From 6fc64d5b127a372015b73f64fa1cce8035af846f Mon Sep 17 00:00:00 2001 From: valc89 Date: Tue, 30 Apr 2024 11:26:28 +0200 Subject: [PATCH 15/18] Update doc --- pina/solvers/pinns/sapinn.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pina/solvers/pinns/sapinn.py b/pina/solvers/pinns/sapinn.py index 8fe5417a6..f89c7bfaf 100644 --- a/pina/solvers/pinns/sapinn.py +++ b/pina/solvers/pinns/sapinn.py @@ -104,10 +104,10 @@ class SAPINN(PINNInterface): where :math:`w` denotes the network parameters. .. seealso:: - **Original reference**: McClenny, Levi D., and Ulisses M. Braga-Neto. - "Self-adaptive physics-informed neural networks." - Journal of Computational Physics 474 (2023): 111722. - `_. + **Original reference**: McClenny, Levi D., and Ulisses M. Braga-Neto. + "Self-adaptive physics-informed neural networks." + Journal of Computational Physics 474 (2023): 111722. + `_. """ def __init__( From c8592092a8a72e0854a8fc3900055d0749b5009e Mon Sep 17 00:00:00 2001 From: valc89 Date: Tue, 30 Apr 2024 11:29:47 +0200 Subject: [PATCH 16/18] Update doc --- pina/solvers/pinns/sapinn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pina/solvers/pinns/sapinn.py b/pina/solvers/pinns/sapinn.py index f89c7bfaf..6e20b80bd 100644 --- a/pina/solvers/pinns/sapinn.py +++ b/pina/solvers/pinns/sapinn.py @@ -107,7 +107,7 @@ class SAPINN(PINNInterface): **Original reference**: McClenny, Levi D., and Ulisses M. Braga-Neto. "Self-adaptive physics-informed neural networks." Journal of Computational Physics 474 (2023): 111722. - `_. + DOI: `10.1016/j.jcp.2022.111722 `_. """ def __init__( @@ -132,7 +132,7 @@ def __init__( for the model. :param torch.nn.Module weights_function: The neural network model related to the mask of SAPINN. - default :class:`torch.nn.Sigmoid()`. + default :obj:`~torch.nn.Sigmoid`. :param list(torch.nn.Module) extra_features: The additional input features to use as augmented input. If ``None`` no extra features are passed. If it is a list of :class:`torch.nn.Module`, From 2236c6fc59b111779e09ad82f8f207ddaae4804c Mon Sep 17 00:00:00 2001 From: valc89 Date: Tue, 30 Apr 2024 11:41:27 +0200 Subject: [PATCH 17/18] Update doc --- pina/solvers/pinns/sapinn.py | 61 ++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/pina/solvers/pinns/sapinn.py b/pina/solvers/pinns/sapinn.py index 6e20b80bd..a553a6b6e 100644 --- a/pina/solvers/pinns/sapinn.py +++ b/pina/solvers/pinns/sapinn.py @@ -69,28 +69,19 @@ class SAPINN(PINNInterface): .. math:: - \mathcal{L} = \mathcal{L}_\Omega - + \mathcal{L}_{\partial \Omega} , + \mathcal{L}_{\rm{problem}} = \frac{1}{N} \sum_{i=1}^{N_\Omega} m + \left( \lambda_{\Omega}^{i} \right) \mathcal{L} \left( \mathcal{A} + [\mathbf{u}](\mathbf{x}) \right) + \frac{1}{N} + \sum_{i=1}^{N_{\partial\Omega}} + m \left( \lambda_{\partial\Omega}^{i} \right) \mathcal{L} + \left( \mathcal{B}[\mathbf{u}](\mathbf{x}) + \right), , - where - - .. math:: - - \mathcal{L}_\Omega = \frac{1}{N} \sum_{i=1}^{N_\Omega} m - \left( \lambda_{\Omega}^{i} \right) \left| \mathcal{A}[\mathbf{u}](\mathbf{x}) - \right|^2 - - and - - .. math:: - - \mathcal{L}_{\partial\Omega} = \frac{1}{N} \sum_{i=1}^{N_{\partial\Omega}} - m \left( \lambda_{\partial\Omega}^{i} \right) \left| \mathcal{B}[\mathbf{u}](\mathbf{x}) - \right|^2 , denoting the self adaptive weights as :math:`\lambda_{\Omega}^1, \dots, \lambda_{\Omega}^{N_\Omega}` and - :math:`\lambda_{\partial \Omega}^1, \dots, \lambda_{\Omega}^{N_\partial \Omega}` + :math:`\lambda_{\partial \Omega}^1, \dots, + \lambda_{\Omega}^{N_\partial \Omega}` for :math:`\Omega` and :math:`\partial \Omega`, respectively. Self Adaptive Physics Informed Neural Network identifies the solution @@ -101,13 +92,19 @@ class SAPINN(PINNInterface): \min_{w} \max_{\lambda_{\Omega}^k, \lambda_{\partial \Omega}^s} \mathcal{L} , - where :math:`w` denotes the network parameters. + where :math:`w` denotes the network parameters, and + :math:`\mathcal{L}` is a specific loss + function, default Mean Square Error: + + .. math:: + \mathcal{L}(v) = \| v \|^2_2. .. seealso:: **Original reference**: McClenny, Levi D., and Ulisses M. Braga-Neto. "Self-adaptive physics-informed neural networks." Journal of Computational Physics 474 (2023): 111722. - DOI: `10.1016/j.jcp.2022.111722 `_. + DOI: `10.1016/ + j.jcp.2022.111722 `_. """ def __init__( @@ -175,7 +172,10 @@ def __init__( models=[model, weights_dict], problem=problem, optimizers=[optimizer_model, optimizer_weights], - optimizers_kwargs=[optimizer_model_kwargs, optimizer_weights_kwargs], + optimizers_kwargs=[ + optimizer_model_kwargs, + optimizer_weights_kwargs + ], extra_features=extra_features, loss=loss ) @@ -213,7 +213,7 @@ def forward(self, x): :math:`\mathbf{x}`. :param LabelTensor x: Input tensor for the SAPINN solver. It expects - a tensor :math:`N \times D`, where :math:`N` the number of points + a tensor :math:`N \\times D`, where :math:`N` the number of points in the mesh, :math:`D` the dimension of the problem, :return: PINN solution. :rtype: LabelTensor @@ -380,14 +380,20 @@ def _compute_loss(self, residual): Elaboration of the pointwise loss through the mask model and the self adaptive weights - :param LabelTensor residual: the matrix of residuals that have to be weighted + :param LabelTensor residual: the matrix of residuals that have to + be weighted :return: tuple with weighted and not weighted loss :rtype List[LabelTensor, LabelTensor] """ - weights = self.weights_dict.torchmodel[self.current_condition_name].forward() - loss_value = self._vectorial_loss(torch.zeros_like(residual, requires_grad=True), residual) - return self._vect_to_scalar(weights * loss_value), self._vect_to_scalar(loss_value) + weights = self.weights_dict.torchmodel[ + self.current_condition_name].forward() + loss_value = self._vectorial_loss(torch.zeros_like( + residual, requires_grad=True), residual) + return ( + self._vect_to_scalar(weights * loss_value), + self._vect_to_scalar(loss_value) + ) def _vect_to_scalar(self, loss_value): """ @@ -404,7 +410,8 @@ def _vect_to_scalar(self, loss_value): elif self.loss.reduction == "sum": ret = torch.sum(loss_value) else: - raise RuntimeError(f"Invalid reduction, got {self.loss.reduction} but expected mean or sum.") + raise RuntimeError(f"Invalid reduction, got {self.loss.reduction} " + "but expected mean or sum.") return ret From 0fe9c032a5f8826b0fe51c5afffac92d93a3cfce Mon Sep 17 00:00:00 2001 From: valc89 Date: Tue, 30 Apr 2024 11:43:21 +0200 Subject: [PATCH 18/18] Update doc --- pina/solvers/pinns/sapinn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pina/solvers/pinns/sapinn.py b/pina/solvers/pinns/sapinn.py index a553a6b6e..9d4239408 100644 --- a/pina/solvers/pinns/sapinn.py +++ b/pina/solvers/pinns/sapinn.py @@ -75,7 +75,7 @@ class SAPINN(PINNInterface): \sum_{i=1}^{N_{\partial\Omega}} m \left( \lambda_{\partial\Omega}^{i} \right) \mathcal{L} \left( \mathcal{B}[\mathbf{u}](\mathbf{x}) - \right), , + \right), denoting the self adaptive weights as