diff --git a/docs/source/_rst/_code.rst b/docs/source/_rst/_code.rst index e4a5f8a61..bfc69d009 100644 --- a/docs/source/_rst/_code.rst +++ b/docs/source/_rst/_code.rst @@ -195,19 +195,35 @@ Equations and Differential Operators .. toctree:: :titlesonly: - EquationInterface + Equation Interface + Base Equation Equation - SystemEquation + System Equation Equation Factory Differential Operators +Equations Zoo +--------------------------------------- + +.. toctree:: + :titlesonly: + + Acoustic Wave Equation + Advection Equation + Allen-Cahn Equation + Diffusion-Reaction Equation + Helmholtz Equation + Poisson Equation + + Problems -------------- .. toctree:: :titlesonly: + ProblemInterface AbstractProblem InverseProblem ParametricProblem @@ -220,13 +236,13 @@ Problems Zoo .. toctree:: :titlesonly: - AcousticWaveProblem - AdvectionProblem - AllenCahnProblem - DiffusionReactionProblem - HelmholtzProblem - InversePoisson2DSquareProblem - Poisson2DSquareProblem + AcousticWaveProblem + AdvectionProblem + AllenCahnProblem + DiffusionReactionProblem + HelmholtzProblem + InversePoisson2DSquareProblem + Poisson2DSquareProblem SupervisedProblem diff --git a/docs/source/_rst/equation/base_equation.rst b/docs/source/_rst/equation/base_equation.rst new file mode 100644 index 000000000..5bb98901f --- /dev/null +++ b/docs/source/_rst/equation/base_equation.rst @@ -0,0 +1,7 @@ +Base Equation +==================== + +.. currentmodule:: pina.equation.base_equation +.. autoclass:: pina._src.equation.base_equation.BaseEquation + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/equation/equation_factory.rst b/docs/source/_rst/equation/equation_factory.rst index 5282aa948..c5024b308 100644 --- a/docs/source/_rst/equation/equation_factory.rst +++ b/docs/source/_rst/equation/equation_factory.rst @@ -21,23 +21,3 @@ Equation Factory .. autoclass:: pina._src.equation.equation_factory.Laplace :members: :show-inheritance: - -.. autoclass:: pina._src.equation.equation_factory.Advection - :members: - :show-inheritance: - -.. autoclass:: pina._src.equation.equation_factory.AllenCahn - :members: - :show-inheritance: - -.. autoclass:: pina._src.equation.equation_factory.DiffusionReaction - :members: - :show-inheritance: - -.. autoclass:: pina._src.equation.equation_factory.Helmholtz - :members: - :show-inheritance: - -.. autoclass:: pina._src.equation.equation_factory.Poisson - :members: - :show-inheritance: \ No newline at end of file diff --git a/docs/source/_rst/equation/zoo/acoustic_wave_equation.rst b/docs/source/_rst/equation/zoo/acoustic_wave_equation.rst new file mode 100644 index 000000000..5bc19d920 --- /dev/null +++ b/docs/source/_rst/equation/zoo/acoustic_wave_equation.rst @@ -0,0 +1,7 @@ +AcousticWaveEquation +===================== +.. currentmodule:: pina.equation.zoo.acoustic_wave_equation + +.. automodule:: pina._src.equation.zoo.acoustic_wave_equation + :members: + :show-inheritance: diff --git a/docs/source/_rst/equation/zoo/advection_equation.rst b/docs/source/_rst/equation/zoo/advection_equation.rst new file mode 100644 index 000000000..4386b3a3d --- /dev/null +++ b/docs/source/_rst/equation/zoo/advection_equation.rst @@ -0,0 +1,7 @@ +Advection Equation +===================== +.. currentmodule:: pina.equation.zoo.advection_equation + +.. automodule:: pina._src.equation.zoo.advection_equation + :members: + :show-inheritance: diff --git a/docs/source/_rst/equation/zoo/allen_cahn_equation.rst b/docs/source/_rst/equation/zoo/allen_cahn_equation.rst new file mode 100644 index 000000000..fff220811 --- /dev/null +++ b/docs/source/_rst/equation/zoo/allen_cahn_equation.rst @@ -0,0 +1,7 @@ +Allen Cahn Equation +===================== +.. currentmodule:: pina.equation.zoo.allen_cahn_equation + +.. automodule:: pina._src.equation.zoo.allen_cahn_equation + :members: + :show-inheritance: diff --git a/docs/source/_rst/equation/zoo/diffusion_reaction_equation.rst b/docs/source/_rst/equation/zoo/diffusion_reaction_equation.rst new file mode 100644 index 000000000..d45143074 --- /dev/null +++ b/docs/source/_rst/equation/zoo/diffusion_reaction_equation.rst @@ -0,0 +1,7 @@ +Diffusion Reaction Equation +============================== +.. currentmodule:: pina.equation.zoo.diffusion_reaction_equation + +.. automodule:: pina._src.equation.zoo.diffusion_reaction_equation + :members: + :show-inheritance: diff --git a/docs/source/_rst/equation/zoo/helmholtz_equation.rst b/docs/source/_rst/equation/zoo/helmholtz_equation.rst new file mode 100644 index 000000000..7728b60ed --- /dev/null +++ b/docs/source/_rst/equation/zoo/helmholtz_equation.rst @@ -0,0 +1,9 @@ +Helmholtz Equation +===================== +.. currentmodule:: pina.equation.zoo.helmholtz_equation + +.. automodule:: pina._src.equation.zoo.helmholtz_equation + +.. autoclass:: pina._src.equation.zoo.helmholtz_equation.HelmholtzEquation + :members: + :show-inheritance: diff --git a/docs/source/_rst/equation/zoo/poisson_equation.rst b/docs/source/_rst/equation/zoo/poisson_equation.rst new file mode 100644 index 000000000..f23796450 --- /dev/null +++ b/docs/source/_rst/equation/zoo/poisson_equation.rst @@ -0,0 +1,9 @@ +Poisson Equation +===================== +.. currentmodule:: pina.equation.zoo.poisson_equation + +.. automodule:: pina._src.equation.zoo.poisson_equation + +.. autoclass:: pina._src.equation.zoo.poisson_equation.PoissonEquation + :members: + :show-inheritance: diff --git a/docs/source/_rst/problem/problem_interface.rst b/docs/source/_rst/problem/problem_interface.rst new file mode 100644 index 000000000..08136e23c --- /dev/null +++ b/docs/source/_rst/problem/problem_interface.rst @@ -0,0 +1,9 @@ +ProblemInterface +=================== +.. currentmodule:: pina.problem.problem_interface + +.. automodule:: pina._src.problem.problem_interface + +.. autoclass:: pina._src.problem.problem_interface.ProblemInterface + :members: + :show-inheritance: diff --git a/docs/source/_rst/problem/zoo/acoustic_wave.rst b/docs/source/_rst/problem/zoo/acoustic_wave.rst deleted file mode 100644 index 34fd46895..000000000 --- a/docs/source/_rst/problem/zoo/acoustic_wave.rst +++ /dev/null @@ -1,9 +0,0 @@ -AcousticWaveProblem -===================== -.. currentmodule:: pina.problem.zoo.acoustic_wave - -.. automodule:: pina._src.problem.zoo.acoustic_wave - -.. autoclass:: pina._src.problem.zoo.acoustic_wave.AcousticWaveProblem - :members: - :show-inheritance: diff --git a/docs/source/_rst/problem/zoo/acoustic_wave_problem.rst b/docs/source/_rst/problem/zoo/acoustic_wave_problem.rst new file mode 100644 index 000000000..c6acb93f1 --- /dev/null +++ b/docs/source/_rst/problem/zoo/acoustic_wave_problem.rst @@ -0,0 +1,9 @@ +AcousticWaveProblem +===================== +.. currentmodule:: pina.problem.zoo.acoustic_wave_problem + +.. automodule:: pina._src.problem.zoo.acoustic_wave_problem + +.. autoclass:: pina._src.problem.zoo.acoustic_wave_problem.AcousticWaveProblem + :members: + :show-inheritance: diff --git a/docs/source/_rst/problem/zoo/advection.rst b/docs/source/_rst/problem/zoo/advection.rst deleted file mode 100644 index 07d0cd45d..000000000 --- a/docs/source/_rst/problem/zoo/advection.rst +++ /dev/null @@ -1,9 +0,0 @@ -AdvectionProblem -================== -.. currentmodule:: pina.problem.zoo.advection - -.. automodule:: pina._src.problem.zoo.advection - -.. autoclass:: pina._src.problem.zoo.advection.AdvectionProblem - :members: - :show-inheritance: diff --git a/docs/source/_rst/problem/zoo/advection_problem.rst b/docs/source/_rst/problem/zoo/advection_problem.rst new file mode 100644 index 000000000..df37679cb --- /dev/null +++ b/docs/source/_rst/problem/zoo/advection_problem.rst @@ -0,0 +1,9 @@ +AdvectionProblem +================== +.. currentmodule:: pina.problem.zoo.advection_problem + +.. automodule:: pina._src.problem.zoo.advection_problem + +.. autoclass:: pina._src.problem.zoo.advection_problem.AdvectionProblem + :members: + :show-inheritance: diff --git a/docs/source/_rst/problem/zoo/allen_cahn.rst b/docs/source/_rst/problem/zoo/allen_cahn.rst deleted file mode 100644 index 7be2104bf..000000000 --- a/docs/source/_rst/problem/zoo/allen_cahn.rst +++ /dev/null @@ -1,9 +0,0 @@ -AllenCahnProblem -================== -.. currentmodule:: pina.problem.zoo.allen_cahn - -.. automodule:: pina._src.problem.zoo.allen_cahn - -.. autoclass:: pina._src.problem.zoo.allen_cahn.AllenCahnProblem - :members: - :show-inheritance: diff --git a/docs/source/_rst/problem/zoo/allen_cahn_problem.rst b/docs/source/_rst/problem/zoo/allen_cahn_problem.rst new file mode 100644 index 000000000..463be3a55 --- /dev/null +++ b/docs/source/_rst/problem/zoo/allen_cahn_problem.rst @@ -0,0 +1,9 @@ +AllenCahnProblem +================== +.. currentmodule:: pina.problem.zoo.allen_cahn_problem + +.. automodule:: pina._src.problem.zoo.allen_cahn_problem + +.. autoclass:: pina._src.problem.zoo.allen_cahn_problem.AllenCahnProblem + :members: + :show-inheritance: diff --git a/docs/source/_rst/problem/zoo/diffusion_reaction.rst b/docs/source/_rst/problem/zoo/diffusion_reaction.rst deleted file mode 100644 index d5269edd7..000000000 --- a/docs/source/_rst/problem/zoo/diffusion_reaction.rst +++ /dev/null @@ -1,9 +0,0 @@ -DiffusionReactionProblem -========================= -.. currentmodule:: pina.problem.zoo.diffusion_reaction - -.. automodule:: pina._src.problem.zoo.diffusion_reaction - -.. autoclass:: pina._src.problem.zoo.diffusion_reaction.DiffusionReactionProblem - :members: - :show-inheritance: diff --git a/docs/source/_rst/problem/zoo/diffusion_reaction_problem.rst b/docs/source/_rst/problem/zoo/diffusion_reaction_problem.rst new file mode 100644 index 000000000..307a56c52 --- /dev/null +++ b/docs/source/_rst/problem/zoo/diffusion_reaction_problem.rst @@ -0,0 +1,9 @@ +DiffusionReactionProblem +========================= +.. currentmodule:: pina.problem.zoo.diffusion_reaction_problem + +.. automodule:: pina._src.problem.zoo.diffusion_reaction_problem + +.. autoclass:: pina._src.problem.zoo.diffusion_reaction_problem.DiffusionReactionProblem + :members: + :show-inheritance: diff --git a/docs/source/_rst/problem/zoo/helmholtz.rst b/docs/source/_rst/problem/zoo/helmholtz.rst deleted file mode 100644 index 06724f83b..000000000 --- a/docs/source/_rst/problem/zoo/helmholtz.rst +++ /dev/null @@ -1,9 +0,0 @@ -HelmholtzProblem -================== -.. currentmodule:: pina.problem.zoo.helmholtz - -.. automodule:: pina._src.problem.zoo.helmholtz - -.. autoclass:: pina._src.problem.zoo.helmholtz.HelmholtzProblem - :members: - :show-inheritance: diff --git a/docs/source/_rst/problem/zoo/helmholtz_problem.rst b/docs/source/_rst/problem/zoo/helmholtz_problem.rst new file mode 100644 index 000000000..952578a2b --- /dev/null +++ b/docs/source/_rst/problem/zoo/helmholtz_problem.rst @@ -0,0 +1,9 @@ +HelmholtzProblem +================== +.. currentmodule:: pina.problem.zoo.helmholtz_problem + +.. automodule:: pina._src.problem.zoo.helmholtz_problem + +.. autoclass:: pina._src.problem.zoo.helmholtz_problem.HelmholtzProblem + :members: + :show-inheritance: diff --git a/docs/source/_rst/problem/zoo/inverse_poisson_2d_square.rst b/docs/source/_rst/problem/zoo/inverse_poisson_2d_square.rst deleted file mode 100644 index d4885ff0c..000000000 --- a/docs/source/_rst/problem/zoo/inverse_poisson_2d_square.rst +++ /dev/null @@ -1,9 +0,0 @@ -InversePoisson2DSquareProblem -============================== -.. currentmodule:: pina.problem.zoo.inverse_poisson_2d_square - -.. automodule:: pina._src.problem.zoo.inverse_poisson_2d_square - -.. autoclass:: pina._src.problem.zoo.inverse_poisson_2d_square.InversePoisson2DSquareProblem - :members: - :show-inheritance: diff --git a/docs/source/_rst/problem/zoo/inverse_poisson_problem.rst b/docs/source/_rst/problem/zoo/inverse_poisson_problem.rst new file mode 100644 index 000000000..503eb21bf --- /dev/null +++ b/docs/source/_rst/problem/zoo/inverse_poisson_problem.rst @@ -0,0 +1,9 @@ +InversePoisson2DSquareProblem +============================== +.. currentmodule:: pina.problem.zoo.inverse_poisson_problem + +.. automodule:: pina._src.problem.zoo.inverse_poisson_problem + +.. autoclass:: pina._src.problem.zoo.inverse_poisson_problem.InversePoisson2DSquareProblem + :members: + :show-inheritance: diff --git a/docs/source/_rst/problem/zoo/poisson_2d_square.rst b/docs/source/_rst/problem/zoo/poisson_2d_square.rst deleted file mode 100644 index 96b5e4397..000000000 --- a/docs/source/_rst/problem/zoo/poisson_2d_square.rst +++ /dev/null @@ -1,9 +0,0 @@ -Poisson2DSquareProblem -======================== -.. currentmodule:: pina.problem.zoo.poisson_2d_square - -.. automodule:: pina._src.problem.zoo.poisson_2d_square - -.. autoclass:: pina._src.problem.zoo.poisson_2d_square.Poisson2DSquareProblem - :members: - :show-inheritance: diff --git a/docs/source/_rst/problem/zoo/poisson_problem.rst b/docs/source/_rst/problem/zoo/poisson_problem.rst new file mode 100644 index 000000000..a480a8953 --- /dev/null +++ b/docs/source/_rst/problem/zoo/poisson_problem.rst @@ -0,0 +1,9 @@ +Poisson2DSquareProblem +======================== +.. currentmodule:: pina.problem.zoo.poisson_problem + +.. automodule:: pina._src.problem.zoo.poisson_problem + +.. autoclass:: pina._src.problem.zoo.poisson_problem.Poisson2DSquareProblem + :members: + :show-inheritance: diff --git a/pina/_src/condition/data_manager.py b/pina/_src/condition/data_manager.py index 2d80a5b6f..2f7095fa1 100644 --- a/pina/_src/condition/data_manager.py +++ b/pina/_src/condition/data_manager.py @@ -7,7 +7,7 @@ from torch_geometric.data.batch import Batch from pina import LabelTensor from pina._src.core.graph import Graph, LabelBatch -from ..equation.equation_interface import EquationInterface +from pina._src.equation.base_equation import BaseEquation from .batch_manager import _BatchManager @@ -39,7 +39,7 @@ def __new__(cls, **kwargs): # Does the data contain only tensors/LabelTensors/Equations? is_tensor_only = all( - isinstance(v, (torch.Tensor, LabelTensor, EquationInterface)) + isinstance(v, (torch.Tensor, LabelTensor, BaseEquation)) for v in kwargs.values() ) # Choose the appropriate subclass, GraphDataManager or TensorDataManager diff --git a/pina/_src/condition/domain_equation_condition.py b/pina/_src/condition/domain_equation_condition.py index 08095bbcd..42b448ce6 100644 --- a/pina/_src/condition/domain_equation_condition.py +++ b/pina/_src/condition/domain_equation_condition.py @@ -2,7 +2,7 @@ from pina._src.condition.condition_base import ConditionBase from pina._src.domain.domain_interface import DomainInterface -from pina._src.equation.equation_interface import EquationInterface +from pina._src.equation.base_equation import BaseEquation class DomainEquationCondition(ConditionBase): @@ -32,7 +32,7 @@ class DomainEquationCondition(ConditionBase): __fields__ = ["domain", "equation"] _avail_domain_cls = (DomainInterface, str) - _avail_equation_cls = EquationInterface + _avail_equation_cls = BaseEquation def __new__(cls, domain, equation): """ @@ -52,7 +52,7 @@ def __new__(cls, domain, equation): if not isinstance(equation, cls._avail_equation_cls): raise ValueError( - "The equation must be an instance of EquationInterface." + "The equation must be an instance of BaseEquation." ) return super().__new__(cls) diff --git a/pina/_src/condition/input_equation_condition.py b/pina/_src/condition/input_equation_condition.py index 62dac3a30..965501e1a 100644 --- a/pina/_src/condition/input_equation_condition.py +++ b/pina/_src/condition/input_equation_condition.py @@ -3,7 +3,7 @@ from pina._src.condition.condition_base import ConditionBase from pina._src.core.label_tensor import LabelTensor from pina._src.core.graph import Graph -from pina._src.equation.equation_interface import EquationInterface +from pina._src.equation.base_equation import BaseEquation from pina._src.condition.data_manager import _DataManager @@ -32,7 +32,7 @@ class InputEquationCondition(ConditionBase): # Available input data types __fields__ = ["input", "equation"] _avail_input_cls = (LabelTensor, Graph) - _avail_equation_cls = EquationInterface + _avail_equation_cls = BaseEquation def __new__(cls, input, equation): """ @@ -41,7 +41,7 @@ def __new__(cls, input, equation): :param input: The input data for the condition. :type input: LabelTensor | Graph | list[Graph] | tuple[Graph] - :param EquationInterface equation: The equation to be satisfied over the + :param BaseEquation equation: The equation to be satisfied over the specified ``input`` data. :return: The subclass of InputEquationCondition. :rtype: pina.condition.input_equation_condition. @@ -61,7 +61,7 @@ def __new__(cls, input, equation): # Check equation type if not isinstance(equation, cls._avail_equation_cls): raise ValueError( - "The equation must be an instance of EquationInterface." + "The equation must be an instance of BaseEquation." ) return super().__new__(cls) @@ -90,7 +90,7 @@ def equation(self): Return the equation associated with this condition. :return: Equation associated with this condition. - :rtype: EquationInterface + :rtype: BaseEquation """ return self._equation @@ -99,11 +99,9 @@ def equation(self, value): """ Set the equation associated with this condition. - :param EquationInterface value: The equation to associate with this + :param BaseEquation value: The equation to associate with this condition """ - if not isinstance(value, EquationInterface): - raise TypeError( - "The equation must be an instance of EquationInterface." - ) + if not isinstance(value, BaseEquation): + raise TypeError("The equation must be an instance of BaseEquation.") self._equation = value diff --git a/pina/_src/equation/base_equation.py b/pina/_src/equation/base_equation.py new file mode 100644 index 000000000..4fff8dd3b --- /dev/null +++ b/pina/_src/equation/base_equation.py @@ -0,0 +1,67 @@ +"""Module for the Base Equation.""" + +from abc import ABCMeta, abstractmethod +import torch + + +class BaseEquation(metaclass=ABCMeta): + """ + Base class for all equations, implementing common functionality. + + Equations are fundamental components in PINA, representing mathematical + constraints that must be satisfied by the model outputs. They can be passed + to :class:`~pina.condition.condition.Condition` objects to define the + conditions under which the model is trained. + + All specific equation types should inherit from this class and implement its + abstract methods. + + This class is not meant to be instantiated directly. + """ + + @abstractmethod + def residual(self, input_, output_, params_): + """ + Evaluate the equation residual at the given inputs. + + :param LabelTensor input_: The input points where the residual is + computed. + :param LabelTensor output_: The output tensor, potentially produced by a + :class:`torch.nn.Module` instance. + :param dict params_: An optional dictionary of unknown parameters, used + in :class:`~pina.problem.inverse_problem.InverseProblem` settings. + If the equation is not related to an inverse problem, this should be + set to ``None``. Default is ``None``. + :return: The residual values of the equation. + :rtype: LabelTensor + """ + + def to(self, device): + """ + Move all tensor attributes to the specified device. + + :param torch.device device: The target device to move the tensors to. + :return: The instance moved to the specified device. + :rtype: BaseEquation + """ + # Iterate over all attributes of the Equation + for key, val in self.__dict__.items(): + + # Move tensors in dictionaries to the specified device + if isinstance(val, dict): + self.__dict__[key] = { + k: v.to(device) if torch.is_tensor(v) else v + for k, v in val.items() + } + + # Move tensors in lists to the specified device + elif isinstance(val, list): + self.__dict__[key] = [ + v.to(device) if torch.is_tensor(v) else v for v in val + ] + + # Move tensor attributes to the specified device + elif torch.is_tensor(val): + self.__dict__[key] = val.to(device) + + return self diff --git a/pina/_src/equation/equation.py b/pina/_src/equation/equation.py index a1d67628c..d10da2bbe 100644 --- a/pina/_src/equation/equation.py +++ b/pina/_src/equation/equation.py @@ -1,62 +1,65 @@ """Module for the Equation.""" import inspect -from pina._src.equation.equation_interface import EquationInterface +from pina._src.equation.base_equation import BaseEquation -class Equation(EquationInterface): +class Equation(BaseEquation): """ - Implementation of the Equation class. Every ``equation`` passed to a - :class:`~pina.condition.condition.Condition` object must be either an - instance of :class:`Equation` or - :class:`~pina.equation.system_equation.SystemEquation`. + Implementation of the Equation class, representing a single mathematical + equation to be satisfied by the model outputs. + + It can be passed to a :class:`~pina.condition.condition.Condition` object to + define the conditions under which the model is trained. """ def __init__(self, equation): """ Initialization of the :class:`Equation` class. - :param Callable equation: A ``torch`` callable function used to compute - the residual of a mathematical equation. + :param Callable equation: A callable function used to compute the + residual of a mathematical equation. :raises ValueError: If the equation is not a callable function. """ + # Check consistency if not callable(equation): - raise ValueError( - "equation must be a callable function." - "Expected a callable function, got " - f"{equation}" - ) - # compute the signature + raise ValueError(f"Expected a callable function, got {equation}") + + # Compute the signature length sig = inspect.signature(equation) self.__len_sig = len(sig.parameters) self.__equation = equation def residual(self, input_, output_, params_=None): """ - Compute the residual of the equation. + Evaluate the equation residual at the given inputs. - :param LabelTensor input_: Input points where the equation is evaluated. - :param LabelTensor output_: Output tensor, eventually produced by a + :param LabelTensor input_: The input points where the residual is + computed. + :param LabelTensor output_: The output tensor, potentially produced by a :class:`torch.nn.Module` instance. - :param dict params_: Dictionary of unknown parameters, associated with a - :class:`~pina.problem.inverse_problem.InverseProblem` instance. - If the equation is not related to a - :class:`~pina.problem.inverse_problem.InverseProblem` instance, the - parameters must be initialized to ``None``. Default is ``None``. - :return: The computed residual of the equation. + :param dict params_: An optional dictionary of unknown parameters, used + in :class:`~pina.problem.inverse_problem.InverseProblem` settings. + If the equation is not related to an inverse problem, this should be + set to ``None``. Default is ``None``. + :raises RuntimeError: If the underlying equation signature is neither of + length 2 for direct problems nor of length 3 for inverse problems. + :return: The residual values of the equation. :rtype: LabelTensor - :raises RuntimeError: If the underlying equation signature length is not - 2 (direct problem) or 3 (inverse problem). """ # Move the equation to the input_ device self.to(input_.device) - # Call the underlying equation based on its signature length + # Evaluate the equation for direct problems if self.__len_sig == 2: return self.__equation(input_, output_) + + # Evaluate the equation for inverse problems if self.__len_sig == 3: return self.__equation(input_, output_, params_) + + # Raise an error if the signature length is unexpected raise RuntimeError( f"Unexpected number of arguments in equation: {self.__len_sig}. " - "Expected either 2 (direct problem) or 3 (inverse problem)." + "Expected either 2 for direct problems, or 3 for inverse problems." ) diff --git a/pina/_src/equation/equation_factory.py b/pina/_src/equation/equation_factory.py index fccd2520f..f52f108ab 100644 --- a/pina/_src/equation/equation_factory.py +++ b/pina/_src/equation/equation_factory.py @@ -1,10 +1,7 @@ """Module for defining various general equations.""" -from typing import Callable -import torch from pina._src.equation.equation import Equation from pina._src.core.operator import grad, div, laplacian -from pina._src.core.utils import check_consistency class FixedValue(Equation): # pylint: disable=R0903 @@ -28,11 +25,11 @@ def equation(_, output_): """ Definition of the equation to enforce a fixed value. - :param LabelTensor input_: Input points where the equation is - evaluated. - :param LabelTensor output_: Output tensor, eventually produced by a - :class:`torch.nn.Module` instance. - :return: The computed residual of the equation. + :param LabelTensor input_: The input points where the residual is + computed. + :param LabelTensor output_: The output tensor, potentially produced + by a :class:`torch.nn.Module` instance. + :return: The residual values of the equation. :rtype: LabelTensor """ if components is None: @@ -66,11 +63,11 @@ def equation(input_, output_): """ Definition of the equation to enforce a fixed gradient. - :param LabelTensor input_: Input points where the equation is - evaluated. - :param LabelTensor output_: Output tensor, eventually produced by a - :class:`torch.nn.Module` instance. - :return: The computed residual of the equation. + :param LabelTensor input_: The input points where the residual is + computed. + :param LabelTensor output_: The output tensor, potentially produced + by a :class:`torch.nn.Module` instance. + :return: The residual values of the equation. :rtype: LabelTensor """ return grad(output_, input_, components=components, d=d) - value @@ -101,11 +98,11 @@ def equation(input_, output_): """ Definition of the equation to enforce a fixed flux. - :param LabelTensor input_: Input points where the equation is - evaluated. - :param LabelTensor output_: Output tensor, eventually produced by a - :class:`torch.nn.Module` instance. - :return: The computed residual of the equation. + :param LabelTensor input_: The input points where the residual is + computed. + :param LabelTensor output_: The output tensor, potentially produced + by a :class:`torch.nn.Module` instance. + :return: The residual values of the equation. :rtype: LabelTensor """ return div(output_, input_, components=components, d=d) - value @@ -137,11 +134,11 @@ def equation(input_, output_): """ Definition of the equation to enforce a fixed laplacian. - :param LabelTensor input_: Input points where the equation is - evaluated. - :param LabelTensor output_: Output tensor, eventually produced by a - :class:`torch.nn.Module` instance. - :return: The computed residual of the equation. + :param LabelTensor input_: The input points where the residual is + computed. + :param LabelTensor output_: The output tensor, potentially produced + by a :class:`torch.nn.Module` instance. + :return: The residual values of the equation. :rtype: LabelTensor """ return ( @@ -158,7 +155,7 @@ class Laplace(FixedLaplacian): # pylint: disable=R0903 .. math:: - \delta u = 0 + \Delta u = 0 """ @@ -176,333 +173,3 @@ def __init__(self, components=None, d=None): Default is ``None``. """ super().__init__(0.0, components=components, d=d) - - -class Advection(Equation): # pylint: disable=R0903 - r""" - Implementation of the N-dimensional advection equation with constant - velocity parameter. The equation is defined as follows: - - .. math:: - - \frac{\partial u}{\partial t} + c \cdot \nabla u = 0 - - Here, :math:`c` is the advection velocity parameter. - """ - - def __init__(self, c): - """ - Initialization of the :class:`Advection` class. - - :param c: The advection velocity. If a scalar is provided, the same - velocity is applied to all spatial dimensions. If a list is - provided, it must contain one value per spatial dimension. - :type c: float | int | List[float] | List[int] - :raises ValueError: If ``c`` is an empty list. - """ - # Check consistency - check_consistency(c, (float, int, list)) - if isinstance(c, list): - all(check_consistency(ci, (float, int)) for ci in c) - if len(c) < 1: - raise ValueError("'c' cannot be an empty list.") - else: - c = [c] - - # Store advection velocity parameter - self.c = torch.tensor(c).unsqueeze(0) - - def equation(input_, output_): - """ - Implementation of the advection equation. - - :param LabelTensor input_: The input data of the problem. - :param LabelTensor output_: The output data of the problem. - :return: The residual of the advection equation. - :rtype: LabelTensor - :raises ValueError: If the ``input_`` labels do not contain the time - variable 't'. - :raises ValueError: If ``c`` is a list and its length is not - consistent with the number of spatial dimensions. - """ - # Store labels - input_lbl = input_.labels - spatial_d = [di for di in input_lbl if di != "t"] - - # Ensure time is passed as input - if "t" not in input_lbl: - raise ValueError( - "The ``input_`` labels must contain the time 't' variable." - ) - - # Ensure consistency of c length - if self.c.shape[-1] != len(input_lbl) - 1 and self.c.shape[-1] > 1: - raise ValueError( - "If 'c' is passed as a list, its length must be equal to " - "the number of spatial dimensions." - ) - - # Repeat c to ensure consistent shape for advection - c = self.c.repeat(output_.shape[0], 1) - if c.shape[1] != (len(input_lbl) - 1): - c = c.repeat(1, len(input_lbl) - 1) - - # Add a dimension to c for the following operations - c = c.unsqueeze(-1) - - # Compute the time derivative and the spatial gradient - time_der = grad(output_, input_, components=None, d="t") - grads = grad(output_=output_, input_=input_, d=spatial_d) - - # Reshape and transpose - tmp = grads.reshape(*output_.shape, len(spatial_d)) - tmp = tmp.transpose(-1, -2) - - # Compute advection term - adv = (tmp * c).sum(dim=tmp.tensor.ndim - 2) - - return time_der + adv - - super().__init__(equation) - - -class AllenCahn(Equation): # pylint: disable=R0903 - r""" - Implementation of the N-dimensional Allen-Cahn equation, defined as follows: - - .. math:: - - \frac{\partial u}{\partial t} - \alpha \Delta u + \beta(u^3 - u) = 0 - - Here, :math:`\alpha` and :math:`\beta` are parameters of the equation. - """ - - def __init__(self, alpha, beta): - """ - Initialization of the :class:`AllenCahn` class. - - :param alpha: The diffusion coefficient. - :type alpha: float | int - :param beta: The reaction coefficient. - :type beta: float | int - """ - check_consistency(alpha, (float, int)) - check_consistency(beta, (float, int)) - self.alpha = alpha - self.beta = beta - - def equation(input_, output_): - """ - Implementation of the Allen-Cahn equation. - - :param LabelTensor input_: The input data of the problem. - :param LabelTensor output_: The output data of the problem. - :return: The residual of the Allen-Cahn equation. - :rtype: LabelTensor - :raises ValueError: If the ``input_`` labels do not contain the time - variable 't'. - """ - # Ensure time is passed as input - if "t" not in input_.labels: - raise ValueError( - "The ``input_`` labels must contain the time 't' variable." - ) - - # Compute the time derivative and the spatial laplacian - u_t = grad(output_, input_, d=["t"]) - u_xx = laplacian( - output_, input_, d=[di for di in input_.labels if di != "t"] - ) - - return u_t - self.alpha * u_xx + self.beta * (output_**3 - output_) - - super().__init__(equation) - - -class DiffusionReaction(Equation): # pylint: disable=R0903 - r""" - Implementation of the N-dimensional Diffusion-Reaction equation, - defined as follows: - - .. math:: - - \frac{\partial u}{\partial t} - \alpha \Delta u - f = 0 - - Here, :math:`\alpha` is a parameter of the equation, while :math:`f` is the - reaction term. - """ - - def __init__(self, alpha, forcing_term): - """ - Initialization of the :class:`DiffusionReaction` class. - - :param alpha: The diffusion coefficient. - :type alpha: float | int - :param Callable forcing_term: The forcing field function, taking as - input the points on which evaluation is required. - """ - check_consistency(alpha, (float, int)) - check_consistency(forcing_term, (Callable)) - self.alpha = alpha - self.forcing_term = forcing_term - - def equation(input_, output_): - """ - Implementation of the Diffusion-Reaction equation. - - :param LabelTensor input_: The input data of the problem. - :param LabelTensor output_: The output data of the problem. - :return: The residual of the Diffusion-Reaction equation. - :rtype: LabelTensor - :raises ValueError: If the ``input_`` labels do not contain the time - variable 't'. - """ - # Ensure time is passed as input - if "t" not in input_.labels: - raise ValueError( - "The ``input_`` labels must contain the time 't' variable." - ) - - # Compute the time derivative and the spatial laplacian - u_t = grad(output_, input_, d=["t"]) - u_xx = laplacian( - output_, input_, d=[di for di in input_.labels if di != "t"] - ) - - return u_t - self.alpha * u_xx - self.forcing_term(input_) - - super().__init__(equation) - - -class Helmholtz(Equation): # pylint: disable=R0903 - r""" - Implementation of the Helmholtz equation, defined as follows: - - .. math:: - - \Delta u + k u - f = 0 - - Here, :math:`k` is the squared wavenumber, while :math:`f` is the - forcing term. - """ - - def __init__(self, k, forcing_term): - """ - Initialization of the :class:`Helmholtz` class. - - :param k: The squared wavenumber. - :type k: float | int - :param Callable forcing_term: The forcing field function, taking as - input the points on which evaluation is required. - """ - check_consistency(k, (int, float)) - check_consistency(forcing_term, (Callable)) - self.k = k - self.forcing_term = forcing_term - - def equation(input_, output_): - """ - Implementation of the Helmholtz equation. - - :param LabelTensor input_: The input data of the problem. - :param LabelTensor output_: The output data of the problem. - :return: The residual of the Helmholtz equation. - :rtype: LabelTensor - """ - lap = laplacian(output_, input_) - return lap + self.k * output_ - self.forcing_term(input_) - - super().__init__(equation) - - -class Poisson(Equation): # pylint: disable=R0903 - r""" - Implementation of the Poisson equation, defined as follows: - - .. math:: - - \Delta u - f = 0 - - Here, :math:`f` is the forcing term. - """ - - def __init__(self, forcing_term): - """ - Initialization of the :class:`Poisson` class. - - :param Callable forcing_term: The forcing field function, taking as - input the points on which evaluation is required. - """ - check_consistency(forcing_term, (Callable)) - self.forcing_term = forcing_term - - def equation(input_, output_): - """ - Implementation of the Poisson equation. - - :param LabelTensor input_: The input data of the problem. - :param LabelTensor output_: The output data of the problem. - :return: The residual of the Poisson equation. - :rtype: LabelTensor - """ - lap = laplacian(output_, input_) - return lap - self.forcing_term(input_) - - super().__init__(equation) - - -class AcousticWave(Equation): # pylint: disable=R0903 - r""" - Implementation of the N-dimensional isotropic acoustic wave equation. - The equation is defined as follows: - - .. math:: - - \frac{\partial^2 u}{\partial t^2} - c^2 \Delta u = 0 - - or alternatively: - - .. math:: - - \Box u = 0 - - Here, :math:`c` is the wave propagation speed, and :math:`\Box` is the - d'Alembert operator. - """ - - def __init__(self, c): - """ - Initialization of the :class:`AcousticWaveEquation` class. - - :param c: The wave propagation speed. - :type c: float | int - """ - check_consistency(c, (float, int)) - self.c = c - - def equation(input_, output_): - """ - Implementation of the acoustic wave equation. - - :param LabelTensor input_: The input data of the problem. - :param LabelTensor output_: The output data of the problem. - :return: The residual of the acoustic wave equation. - :rtype: LabelTensor - :raises ValueError: If the ``input_`` labels do not contain the time - variable 't'. - """ - # Ensure time is passed as input - if "t" not in input_.labels: - raise ValueError( - "The ``input_`` labels must contain the time 't' variable." - ) - - # Compute the time second derivative and the spatial laplacian - u_tt = laplacian(output_, input_, d=["t"]) - u_xx = laplacian( - output_, input_, d=[di for di in input_.labels if di != "t"] - ) - - return u_tt - self.c**2 * u_xx - - super().__init__(equation) diff --git a/pina/_src/equation/equation_interface.py b/pina/_src/equation/equation_interface.py index 82b86dbd0..fa59de678 100644 --- a/pina/_src/equation/equation_interface.py +++ b/pina/_src/equation/equation_interface.py @@ -1,40 +1,31 @@ """Module for the Equation Interface.""" from abc import ABCMeta, abstractmethod -import torch class EquationInterface(metaclass=ABCMeta): """ - Abstract base class for equations. - - Equations in PINA simplify the training process. When defining a problem, - each equation passed to a :class:`~pina.condition.condition.Condition` - object must be either an :class:`~pina.equation.equation.Equation` or a - :class:`~pina.equation.system_equation.SystemEquation` instance. - - An :class:`~pina.equation.equation.Equation` is a wrapper for a callable - function, while :class:`~pina.equation.system_equation.SystemEquation` - wraps a list of callable functions. To streamline code writing, PINA - provides a diverse set of pre-implemented equations, such as - :class:`~pina.equation.equation_factory.FixedValue`, - :class:`~pina.equation.equation_factory.FixedGradient`, and many others. + Abstract interface for all equations. """ @abstractmethod - def residual(self, input_, output_, params_): + def residual(self, input_, output_, params_=None): """ - Abstract method to compute the residual of an equation. + Evaluate the equation residual at the given inputs. - :param LabelTensor input_: Input points where the equation is evaluated. - :param LabelTensor output_: Output tensor, eventually produced by a + :param LabelTensor input_: The input points where the residual is + computed. + :param LabelTensor output_: The output tensor, potentially produced by a :class:`torch.nn.Module` instance. - :param dict params_: Dictionary of unknown parameters, associated with a - :class:`~pina.problem.inverse_problem.InverseProblem` instance. - :return: The computed residual of the equation. + :param dict params_: An optional dictionary of unknown parameters, used + in :class:`~pina.problem.inverse_problem.InverseProblem` settings. + If the equation is not related to an inverse problem, this should be + set to ``None``. Default is ``None``. + :return: The residual values of the equation. :rtype: LabelTensor """ + @abstractmethod def to(self, device): """ Move all tensor attributes to the specified device. @@ -43,24 +34,3 @@ def to(self, device): :return: The instance moved to the specified device. :rtype: EquationInterface """ - # Iterate over all attributes of the Equation - for key, val in self.__dict__.items(): - - # Move tensors in dictionaries to the specified device - if isinstance(val, dict): - self.__dict__[key] = { - k: v.to(device) if torch.is_tensor(v) else v - for k, v in val.items() - } - - # Move tensors in lists to the specified device - elif isinstance(val, list): - self.__dict__[key] = [ - v.to(device) if torch.is_tensor(v) else v for v in val - ] - - # Move tensor attributes to the specified device - elif torch.is_tensor(val): - self.__dict__[key] = val.to(device) - - return self diff --git a/pina/_src/equation/system_equation.py b/pina/_src/equation/system_equation.py index adaeca444..7d3bdafd4 100644 --- a/pina/_src/equation/system_equation.py +++ b/pina/_src/equation/system_equation.py @@ -1,35 +1,31 @@ """Module for the System of Equation.""" +from typing import Callable import torch -from pina._src.equation.equation_interface import EquationInterface -from pina._src.equation.equation import Equation +from pina._src.equation.base_equation import BaseEquation from pina._src.core.utils import check_consistency +from pina._src.equation.equation import Equation -class SystemEquation(EquationInterface): +class SystemEquation(BaseEquation): """ - Implementation of the System of Equations, to be passed to a - :class:`~pina.condition.condition.Condition` object. + Implementation of the SystemEquation class, representing a system of + mathematical equation to be satisfied by the model outputs. It is useful for + multi-component outputs or coupled problems, where multiple constraints must + be evaluated together. - Unlike the :class:`~pina.equation.equation.Equation` class, which represents - a single equation, the :class:`SystemEquation` class allows multiple - equations to be grouped together into a system. This is particularly useful - when dealing with multi-component outputs or coupled physical models, where - the residual must be computed collectively across several constraints. + It can be passed to a :class:`~pina.condition.condition.Condition` object to + define the conditions under which the model is trained. - Each equation in the system must be either: - - An instance of :class:`~pina.equation.equation.Equation`; - - A callable function. + Each equation in the system must be either an instance of + :class:`~pina.equation.equation.Equation`, or a callable function. - The residuals from each equation are computed independently and then - aggregated using an optional reduction strategy (e.g., ``mean``, ``sum``). - The resulting residual is returned as a single :class:`~pina.LabelTensor`. + Residuals are computed independently for each equation and then aggregated + using an optional reduction (e.g., ``mean``, ``sum``). The final result is + returned as a single :class:`~pina.LabelTensor`. :Example: - >>> from pina.equation import SystemEquation, FixedValue, FixedGradient - >>> from pina import LabelTensor - >>> import torch >>> pts = LabelTensor(torch.rand(10, 2), labels=["x", "y"]) >>> pts.requires_grad = True >>> output_ = torch.pow(pts, 2) @@ -37,40 +33,44 @@ class SystemEquation(EquationInterface): >>> system_equation = SystemEquation( ... [ ... FixedValue(value=1.0, components=["u"]), - ... FixedGradient(value=0.0, components=["v"],d=["y"]), + ... FixedGradient(value=0.0, components=["v"], d=["y"]), ... ], ... reduction="mean", ... ) >>> residual = system_equation.residual(pts, output_) - """ def __init__(self, list_equation, reduction=None): """ Initialization of the :class:`SystemEquation` class. - :param list_equation: A list containing either callable functions or - instances of :class:`~pina.equation.equation.Equation`, used to - compute the residuals of mathematical equations. + :param list_equation: The list of equations used for the computation of + the residuals. Each element of the list can be either a callable + function or a :class:`~pina.equation.equation.Equation` instance. :type list_equation: list[Callable] | list[Equation] - :param str reduction: The reduction method to aggregate the residuals of - each equation. Available options are: ``None``, ``mean``, ``sum``, - ``callable``. - If ``None``, no reduction is applied. If ``mean``, the output sum is - divided by the number of elements in the output. If ``sum``, the - output is summed. ``callable`` is a user-defined callable function - to perform reduction, no checks guaranteed. Default is ``None``. - :raises NotImplementedError: If the reduction is not implemented. + :param reduction: The method used to combine the residuals from each + equation. Available options are: ``None``, ``"mean"``, ``"sum"``, or + a custom callable. If ``None``, no reduction is applied. If + ``"mean"``, the residuals are averaged. If ``"sum"``, the residuals + are summed. If a callable is provided, it is used as a custom + reduction (no validation is performed). + :raises ValueError: If the list of equations is not a list. + :raises ValueError: If any element of the list of equations is not a + callable function or a :class:`~pina.equation.equation.Equation` + instance. + :raises ValueError: If an invalid reduction method is used. """ + # Check consistency check_consistency([list_equation], list) + check_consistency(list_equation, (Callable, Equation)) - # equations definition + # Convert all callable functions to Equation instances, if necessary self.equations = [ equation if isinstance(equation, Equation) else Equation(equation) for equation in list_equation ] - # possible reduction + # Validate and set the reduction method if reduction == "mean": self.reduction = torch.mean elif reduction == "sum": @@ -78,26 +78,24 @@ def __init__(self, list_equation, reduction=None): elif (reduction is None) or callable(reduction): self.reduction = reduction else: - raise NotImplementedError( - "Only mean and sum reductions are currenly supported." + raise ValueError( + "Invalid reduction method. Available options include: None, " + "'mean', 'sum', or a custom callable." ) def residual(self, input_, output_, params_=None): """ - Compute the residual for each equation in the system of equations and - aggregate it according to the ``reduction`` specified in the - ``__init__`` method. + Evaluate each equation residual from the system of equations at the + given inputs and aggregate it according to the specified ``reduction``. - :param LabelTensor input_: Input points where each equation of the - system is evaluated. - :param LabelTensor output_: Output tensor, eventually produced by a + :param LabelTensor input_: The input points where the residual is + computed. + :param LabelTensor output_: The output tensor, potentially produced by a :class:`torch.nn.Module` instance. - :param dict params_: Dictionary of unknown parameters, associated with a - :class:`~pina.problem.inverse_problem.InverseProblem` instance. - If the equation is not related to a - :class:`~pina.problem.inverse_problem.InverseProblem` instance, the - parameters must be initialized to ``None``. Default is ``None``. - + :param dict params_: An optional dictionary of unknown parameters, used + in :class:`~pina.problem.inverse_problem.InverseProblem` settings. + If the equation is not related to an inverse problem, this should be + set to ``None``. Default is ``None``. :return: The aggregated residuals of the system of equations. :rtype: LabelTensor """ diff --git a/pina/_src/equation/zoo/__init__.py b/pina/_src/equation/zoo/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pina/_src/equation/zoo/acoustic_wave_equation.py b/pina/_src/equation/zoo/acoustic_wave_equation.py new file mode 100644 index 000000000..45614cb5d --- /dev/null +++ b/pina/_src/equation/zoo/acoustic_wave_equation.py @@ -0,0 +1,62 @@ +"""Module for defining the acoustic wave equation.""" + +from pina._src.equation.equation import Equation +from pina._src.core.operator import laplacian +from pina._src.core.utils import check_consistency + + +class AcousticWaveEquation(Equation): # pylint: disable=R0903 + r""" + Implementation of the N-dimensional isotropic acoustic wave equation. + The equation is defined as follows: + + .. math:: + + \frac{\partial^2 u}{\partial t^2} - c^2 \Delta u = 0 + + or alternatively: + + .. math:: + + \Box u = 0 + + Here, :math:`c` is the wave propagation speed, and :math:`\Box` is the + d'Alembert operator. + """ + + def __init__(self, c): + """ + Initialization of the :class:`AcousticWaveEquation` class. + + :param c: The wave propagation speed. + :type c: float | int + """ + check_consistency(c, (float, int)) + self.c = c + + def equation(input_, output_): + """ + Implementation of the acoustic wave equation. + + :param LabelTensor input_: The input data of the problem. + :param LabelTensor output_: The output data of the problem. + :return: The residual of the acoustic wave equation. + :rtype: LabelTensor + :raises ValueError: If the ``input_`` labels do not contain the time + variable 't'. + """ + # Ensure time is passed as input + if "t" not in input_.labels: + raise ValueError( + "The ``input_`` labels must contain the time 't' variable." + ) + + # Compute the time second derivative and the spatial laplacian + u_tt = laplacian(output_, input_, d=["t"]) + u_xx = laplacian( + output_, input_, d=[di for di in input_.labels if di != "t"] + ) + + return u_tt - self.c**2 * u_xx + + super().__init__(equation) diff --git a/pina/_src/equation/zoo/advection_equation.py b/pina/_src/equation/zoo/advection_equation.py new file mode 100644 index 000000000..73fb3de99 --- /dev/null +++ b/pina/_src/equation/zoo/advection_equation.py @@ -0,0 +1,93 @@ +"""Module for defining the advection equation.""" + +import torch +from pina._src.equation.equation import Equation +from pina._src.core.operator import grad +from pina._src.core.utils import check_consistency + + +class AdvectionEquation(Equation): # pylint: disable=R0903 + r""" + Implementation of the N-dimensional advection equation with constant + velocity parameter. The equation is defined as follows: + + .. math:: + + \frac{\partial u}{\partial t} + c \cdot \nabla u = 0 + + Here, :math:`c` is the advection velocity parameter. + """ + + def __init__(self, c): + """ + Initialization of the :class:`AdvectionEquation` class. + + :param c: The advection velocity. If a scalar is provided, the same + velocity is applied to all spatial dimensions. If a list is + provided, it must contain one value per spatial dimension. + :type c: float | int | List[float] | List[int] + :raises ValueError: If ``c`` is an empty list. + """ + # Check consistency + check_consistency(c, (float, int)) + if isinstance(c, list): + if len(c) < 1: + raise ValueError("'c' cannot be an empty list.") + else: + c = [c] + + # Store advection velocity parameter + self.c = torch.tensor(c).unsqueeze(0) + + def equation(input_, output_): + """ + Implementation of the advection equation. + + :param LabelTensor input_: The input data of the problem. + :param LabelTensor output_: The output data of the problem. + :return: The residual of the advection equation. + :rtype: LabelTensor + :raises ValueError: If the ``input_`` labels do not contain the time + variable 't'. + :raises ValueError: If ``c`` is a list and its length is not + consistent with the number of spatial dimensions. + """ + # Store labels + input_lbl = input_.labels + spatial_d = [di for di in input_lbl if di != "t"] + + # Ensure time is passed as input + if "t" not in input_lbl: + raise ValueError( + "The ``input_`` labels must contain the time 't' variable." + ) + + # Ensure consistency of c length + if self.c.shape[-1] != len(input_lbl) - 1 and self.c.shape[-1] > 1: + raise ValueError( + "If 'c' is passed as a list, its length must be equal to " + "the number of spatial dimensions." + ) + + # Repeat c to ensure consistent shape for advection + c = self.c.repeat(output_.shape[0], 1) + if c.shape[1] != (len(input_lbl) - 1): + c = c.repeat(1, len(input_lbl) - 1) + + # Add a dimension to c for the following operations + c = c.unsqueeze(-1) + + # Compute the time derivative and the spatial gradient + time_der = grad(output_, input_, components=None, d="t") + grads = grad(output_=output_, input_=input_, d=spatial_d) + + # Reshape and transpose + tmp = grads.reshape(*output_.shape, len(spatial_d)) + tmp = tmp.transpose(-1, -2) + + # Compute advection term + adv = (tmp * c).sum(dim=tmp.tensor.ndim - 2) + + return time_der + adv + + super().__init__(equation) diff --git a/pina/_src/equation/zoo/allen_cahn_equation.py b/pina/_src/equation/zoo/allen_cahn_equation.py new file mode 100644 index 000000000..81c6c82b1 --- /dev/null +++ b/pina/_src/equation/zoo/allen_cahn_equation.py @@ -0,0 +1,58 @@ +"""Module for defining the Allen-Cahn equation.""" + +from pina._src.equation.equation import Equation +from pina._src.core.operator import grad, laplacian +from pina._src.core.utils import check_consistency + + +class AllenCahnEquation(Equation): # pylint: disable=R0903 + r""" + Implementation of the N-dimensional Allen-Cahn equation, defined as follows: + + .. math:: + + \frac{\partial u}{\partial t} - \alpha \Delta u + \beta(u^3 - u) = 0 + + Here, :math:`\alpha` and :math:`\beta` are parameters of the equation. + """ + + def __init__(self, alpha, beta): + """ + Initialization of the :class:`AllenCahnEquation` class. + + :param alpha: The diffusion coefficient. + :type alpha: float | int + :param beta: The reaction coefficient. + :type beta: float | int + """ + check_consistency(alpha, (float, int)) + check_consistency(beta, (float, int)) + self.alpha = alpha + self.beta = beta + + def equation(input_, output_): + """ + Implementation of the Allen-Cahn equation. + + :param LabelTensor input_: The input data of the problem. + :param LabelTensor output_: The output data of the problem. + :return: The residual of the Allen-Cahn equation. + :rtype: LabelTensor + :raises ValueError: If the ``input_`` labels do not contain the time + variable 't'. + """ + # Ensure time is passed as input + if "t" not in input_.labels: + raise ValueError( + "The ``input_`` labels must contain the time 't' variable." + ) + + # Compute the time derivative and the spatial laplacian + u_t = grad(output_, input_, d=["t"]) + u_xx = laplacian( + output_, input_, d=[di for di in input_.labels if di != "t"] + ) + + return u_t - self.alpha * u_xx + self.beta * (output_**3 - output_) + + super().__init__(equation) diff --git a/pina/_src/equation/zoo/diffusion_reaction_equation.py b/pina/_src/equation/zoo/diffusion_reaction_equation.py new file mode 100644 index 000000000..76768088a --- /dev/null +++ b/pina/_src/equation/zoo/diffusion_reaction_equation.py @@ -0,0 +1,61 @@ +"""Module for defining the Diffusion-Reaction equation.""" + +from typing import Callable +from pina._src.equation.equation import Equation +from pina._src.core.operator import grad, laplacian +from pina._src.core.utils import check_consistency + + +class DiffusionReactionEquation(Equation): # pylint: disable=R0903 + r""" + Implementation of the N-dimensional Diffusion-Reaction equation, + defined as follows: + + .. math:: + + \frac{\partial u}{\partial t} - \alpha \Delta u - f = 0 + + Here, :math:`\alpha` is a parameter of the equation, while :math:`f` is the + reaction term. + """ + + def __init__(self, alpha, forcing_term): + """ + Initialization of the :class:`DiffusionReactionEquation` class. + + :param alpha: The diffusion coefficient. + :type alpha: float | int + :param Callable forcing_term: The forcing field function, taking as + input the points on which evaluation is required. + """ + check_consistency(alpha, (float, int)) + check_consistency(forcing_term, (Callable)) + self.alpha = alpha + self.forcing_term = forcing_term + + def equation(input_, output_): + """ + Implementation of the Diffusion-Reaction equation. + + :param LabelTensor input_: The input data of the problem. + :param LabelTensor output_: The output data of the problem. + :return: The residual of the Diffusion-Reaction equation. + :rtype: LabelTensor + :raises ValueError: If the ``input_`` labels do not contain the time + variable 't'. + """ + # Ensure time is passed as input + if "t" not in input_.labels: + raise ValueError( + "The ``input_`` labels must contain the time 't' variable." + ) + + # Compute the time derivative and the spatial laplacian + u_t = grad(output_, input_, d=["t"]) + u_xx = laplacian( + output_, input_, d=[di for di in input_.labels if di != "t"] + ) + + return u_t - self.alpha * u_xx - self.forcing_term(input_) + + super().__init__(equation) diff --git a/pina/_src/equation/zoo/helmholtz_equation.py b/pina/_src/equation/zoo/helmholtz_equation.py new file mode 100644 index 000000000..3b628728a --- /dev/null +++ b/pina/_src/equation/zoo/helmholtz_equation.py @@ -0,0 +1,47 @@ +"""Module for defining the Helmholtz equation.""" + +from typing import Callable +from pina._src.equation.equation import Equation +from pina._src.core.operator import laplacian +from pina._src.core.utils import check_consistency + + +class HelmholtzEquation(Equation): # pylint: disable=R0903 + r""" + Implementation of the Helmholtz equation, defined as follows: + + .. math:: + + \Delta u + k u - f = 0 + + Here, :math:`k` is the squared wavenumber, while :math:`f` is the + forcing term. + """ + + def __init__(self, k, forcing_term): + """ + Initialization of the :class:`HelmholtzEquation` class. + + :param k: The squared wavenumber. + :type k: float | int + :param Callable forcing_term: The forcing field function, taking as + input the points on which evaluation is required. + """ + check_consistency(k, (int, float)) + check_consistency(forcing_term, (Callable)) + self.k = k + self.forcing_term = forcing_term + + def equation(input_, output_): + """ + Implementation of the Helmholtz equation. + + :param LabelTensor input_: The input data of the problem. + :param LabelTensor output_: The output data of the problem. + :return: The residual of the Helmholtz equation. + :rtype: LabelTensor + """ + lap = laplacian(output_, input_) + return lap + self.k * output_ - self.forcing_term(input_) + + super().__init__(equation) diff --git a/pina/_src/equation/zoo/poisson_equation.py b/pina/_src/equation/zoo/poisson_equation.py new file mode 100644 index 000000000..15713539f --- /dev/null +++ b/pina/_src/equation/zoo/poisson_equation.py @@ -0,0 +1,42 @@ +"""Module for defining the Poisson equation.""" + +from typing import Callable +from pina._src.equation.equation import Equation +from pina._src.core.operator import laplacian +from pina._src.core.utils import check_consistency + + +class PoissonEquation(Equation): # pylint: disable=R0903 + r""" + Implementation of the Poisson equation, defined as follows: + + .. math:: + + \Delta u - f = 0 + + Here, :math:`f` is the forcing term. + """ + + def __init__(self, forcing_term): + """ + Initialization of the :class:`PoissonEquation` class. + + :param Callable forcing_term: The forcing field function, taking as + input the points on which evaluation is required. + """ + check_consistency(forcing_term, (Callable)) + self.forcing_term = forcing_term + + def equation(input_, output_): + """ + Implementation of the Poisson equation. + + :param LabelTensor input_: The input data of the problem. + :param LabelTensor output_: The output data of the problem. + :return: The residual of the Poisson equation. + :rtype: LabelTensor + """ + lap = laplacian(output_, input_) + return lap - self.forcing_term(input_) + + super().__init__(equation) diff --git a/pina/_src/problem/abstract_problem.py b/pina/_src/problem/abstract_problem.py index 28bccf089..72d8d22a1 100644 --- a/pina/_src/problem/abstract_problem.py +++ b/pina/_src/problem/abstract_problem.py @@ -1,26 +1,32 @@ """Module for the AbstractProblem class.""" -from abc import ABCMeta, abstractmethod -import warnings from copy import deepcopy -from pina._src.core.utils import check_consistency +from pina._src.problem.problem_interface import ProblemInterface from pina._src.domain.domain_interface import DomainInterface -from pina._src.domain.cartesian_domain import CartesianDomain +from pina._src.core.label_tensor import LabelTensor +from pina._src.condition.condition import Condition from pina._src.condition.domain_equation_condition import ( DomainEquationCondition, ) -from pina._src.core.label_tensor import LabelTensor -from pina._src.core.utils import merge_tensors, custom_warning_format -from pina._src.condition.condition import Condition +from pina._src.core.utils import ( + check_consistency, + check_positive_integer, + merge_tensors, +) -class AbstractProblem(metaclass=ABCMeta): +class AbstractProblem(ProblemInterface): """ - Abstract base class for PINA problems. All specific problem types should - inherit from this class. + Base class for all problems, implementing common functionality. + + A problem is defined by core components, including input and output + variables, a set of conditions to be satisfied, and optionally the domains + on which these conditions are defined. - A PINA problem is defined by key components, which typically include output - variables, conditions, and domains over which the conditions are applied. + All problems must inherit from this class and implement abstract methods + defined in :class:`~pina.problem.problem_interface.ProblemInterface`. + + This class is not meant to be instantiated directly. """ def __init__(self): @@ -29,284 +35,262 @@ def __init__(self): """ self._discretised_domains = {} - # create hook conditions <-> problems + # Create a correspondence between the problem and the conditions for condition_name in self.conditions: self.conditions[condition_name].problem = self - # Store in domains dict all the domains object directly passed to - # ConditionInterface. Done for back compatibility with PINA <0.2 + # Create a dictionary to store the domains of the problem if not hasattr(self, "domains"): self.domains = {} - for cond_name, cond in self.conditions.items(): + + # Store all the domains object passed to the problem's conditions + for name, cond in self.conditions.items(): if isinstance(cond, DomainEquationCondition): if isinstance(cond.domain, DomainInterface): - self.domains[cond_name] = cond.domain - cond.domain = cond_name - - # # back compatibility 0.1 - # @property - # def input_pts(self): - # """ - # Return a dictionary mapping condition names to their corresponding - # input points. If some domains are not sampled, they will not be returned - # and the corresponding condition will be empty. - - # :return: The input points of the problem. - # :rtype: dict - # """ - # to_return = {} - # for cond_name, data in self.collected_data.items(): - # to_return[cond_name] = data["input"] - # return to_return - - @property - def discretised_domains(self): - """ - Return a dictionary mapping domains to their corresponding sampled - points. - - :return: The discretised domains. - :rtype: dict - """ - return self._discretised_domains + self.domains[name] = cond.domain + cond.domain = name def __deepcopy__(self, memo): """ - Perform a deep copy of the :class:`AbstractProblem` instance. + Create a deep copy of the problem instance. - :param dict memo: A dictionary used to track objects already copied - during the deep copy process to prevent redundant copies. - :return: A deep copy of the :class:`AbstractProblem` instance. - :rtype: AbstractProblem + :param dict memo: The memorization dictionary used by the deepcopy + function. + :return: A deep copy of the problem instance. + :rtype: ProblemInterface """ - cls = self.__class__ - result = cls.__new__(cls) + # Create a new instance of the same class and store it in a dictionary + result = self.__class__.__new__(self.__class__) memo[id(self)] = result + + # Set the attributes of the new instance to deep copies of the original for k, v in self.__dict__.items(): setattr(result, k, deepcopy(v, memo)) - return result - @property - def are_all_domains_discretised(self): - """ - Check if all the domains are discretised. - - :return: ``True`` if all domains are discretised, ``False`` otherwise. - :rtype: bool - """ - return all( - domain in self.discretised_domains for domain in self.domains - ) - - @property - def input_variables(self): - """ - Get the input variables of the problem. - - :return: The input variables of the problem. - :rtype: list[str] - """ - variables = [] - - if hasattr(self, "spatial_variables"): - variables += self.spatial_variables - if hasattr(self, "temporal_variable"): - variables += self.temporal_variable - if hasattr(self, "parameters"): - variables += self.parameters - - return variables - - @input_variables.setter - def input_variables(self, variables): - """ - Set the input variables of the AbstractProblem. - - :param list[str] variables: The input variables of the problem. - :raises RuntimeError: Not implemented. - """ - raise RuntimeError - - @property - @abstractmethod - def output_variables(self): - """ - Get the output variables of the problem. - """ - - @property - @abstractmethod - def conditions(self): - """ - Get the conditions of the problem. - - :return: The conditions of the problem. - :rtype: dict - """ - return self.conditions + return result def discretise_domain( - self, n=None, mode="random", domains="all", sample_rules=None + self, n=None, mode="random", domains=None, sample_rules=None ): """ - Discretize the problem's domains by sampling a specified number of + Discretise the problem's domains by sampling a specified number of points according to the selected sampling mode. - :param int n: The number of points to sample. - :param mode: The sampling method. Default is ``random``. - Available modes include: random sampling, ``random``; - latin hypercube sampling, ``latin`` or ``lh``; - chebyshev sampling, ``chebyshev``; grid sampling ``grid``. - :param domains: The domains from which to sample. Default is ``all``. + :param int n: The number of points to sample. This is ignored if + ``sample_rules`` is provided. Default is ``None``. + :param str mode: The sampling method. Available modes include: + ``"random"`` for random sampling, ``"latin"`` or ``"lh"`` for latin + hypercube sampling, ``"chebyshev"`` for Chebyshev sampling, and + ``"grid"`` for grid sampling. Default is ``"random"``. + :param domains: The domains from which to sample. If ``None``, all + domains are considered for sampling. Default is ``None``. :type domains: str | list[str] - :param dict sample_rules: A dictionary defining custom sampling rules - for input variables. If provided, it must contain a dictionary - specifying the sampling rule for each variable, overriding the - ``n`` and ``mode`` arguments. Each key must correspond to the - input variables from - :meth:~pina.problem.AbstractProblem.input_variables, and its value - should be another dictionary with - two keys: ``n`` (number of points to sample) and ``mode`` - (sampling method). Defaults to None. - :raises RuntimeError: If both ``n`` and ``sample_rules`` are specified. - :raises RuntimeError: If neither ``n`` nor ``sample_rules`` are set. + :param dict sample_rules: The dictionary specifying custom sampling + rules for each input variable. When provided, it overrides the + global ``n`` and ``mode`` arguments. Each key in the dictionary must + match one of the variables defined in :meth:`input_variables`, and + each value must be a dictionary containing two keys: ``n`` for the + number of points to sample for that variable, and ``mode`` for the + sampling method to use. If ``None``, the global ``n`` and ``mode`` + parameters are used for all variables. Default is ``None``. + :raises ValueError: If ``sample_rules`` is provided but it is not a + dictionary. + :raises ValueError: If ``sample_rules`` is provided but its keys do not + match the input variables of the problem. + :raises ValueError: If ``sample_rules`` is provided but any of its rules + is not a dictionary containing both ``n`` and ``mode`` keys, with + ``n`` being a positive integer and ``mode`` being a string. + :raises AssertionError: If ``n`` is not a positive integer. + :raises ValueError: If ``mode`` is not a string + :raises ValueError: If ``domains`` is provided by it is neither a string + nor a list of strings. + + .. warning:: + ``"random"`` is the only supported ``mode`` across all geometries: + :class:`~pina.domain.cartesian_domain.CartesianDomain`, + :class:`~pina.domain.ellipsoid_domain.EllipsoidDomain`, and + :class:`~pina.domain.simplex_domain.SimplexDomain`. + Sampling modes such as ``"latin"``, ``"chebyshev"``, and ``"grid"`` + are only implemented for + :class:`~pina.domain.cartesian_domain.CartesianDomain`. + When custom discretisation is specified via ``sample_rules``, the + domain to be discretised must be an instance of + :class:`~pina.domain.cartesian_domain.CartesianDomain`. :Example: - >>> problem.discretise_domain(n=10, mode='grid') - >>> problem.discretise_domain(n=10, mode='grid', domains=['gamma1']) + >>> problem.discretise_domain(n=10, mode="random") + >>> problem.discretise_domain(n=10, mode="lh", domains=["boundary"]) >>> problem.discretise_domain( ... sample_rules={ ... 'x': {'n': 10, 'mode': 'grid'}, ... 'y': {'n': 100, 'mode': 'grid'} ... }, - ... domains=['D'] ... ) - - .. warning:: - ``random`` is currently the only implemented ``mode`` for all - geometries, i.e. :class:`~pina.domain.ellipsoid.EllipsoidDomain`, - :class:`~pina.domain.cartesian.CartesianDomain`, - :class:`~pina.domain.simplex.SimplexDomain`, and geometry - compositions :class:`~pina.domain.union_domain.Union`, - :class:`~pina.domain.difference_domain.Difference`, - :class:`~pina.domain.exclusion_domain.Exclusion`, and - :class:`~pina.domain.intersection_domain.Intersection`. - The modes ``latin`` or ``lh``, ``chebyshev``, ``grid`` are only - implemented for :class:`~pina.domain.cartesian.CartesianDomain`. - - .. warning:: - If custom discretisation is applied by setting ``sample_rules`` not - to ``None``, then the discretised domain must be of class - :class:`~pina.domain.cartesian.CartesianDomain` """ + # Initialize the domains to be discretised + if domains is None: + domains = list(self.domains) + if not isinstance(domains, (list)): + domains = [domains] - # check consistecy n, mode, variables, locations + # Check sampling rules if sample_rules is not None: check_consistency(sample_rules, dict) - if mode is not None: - check_consistency(mode, str) - check_consistency(domains, (list, str)) - - # check correct location - if domains == "all": - domains = self.domains.keys() - elif not isinstance(domains, (list)): - domains = [domains] - if n is not None and sample_rules is None: - self._apply_default_discretization(n, mode, domains) - if n is None and sample_rules is not None: - self._apply_custom_discretization(sample_rules, domains) - elif n is not None and sample_rules is not None: - raise RuntimeError( - "You can't specify both n and sample_rules at the same time." - ) - elif n is None and sample_rules is None: - raise RuntimeError("You have to specify either n or sample_rules.") - def _apply_default_discretization(self, n, mode, domains): - """ - Apply default discretization to the problem's domains. + # Check that the keys of sample_rules match the input variables + if sorted(list(sample_rules.keys())) != sorted( + self.input_variables + ): + raise ValueError( + "The keys of the sample_rules dictionary must match the " + "input variables." + ) - :param int n: The number of points to sample. - :param mode: The sampling method. - :param domains: The domains from which to sample. - :type domains: str | list[str] - """ - for domain in domains: - self.discretised_domains[domain] = ( - self.domains[domain].sample(n, mode).sort_labels() - ) + # Check that the rules for each variable are valid + for var, rules in sample_rules.items(): + check_consistency(rules, dict) + if "n" not in rules or "mode" not in rules: + raise ValueError( + f"Sampling rules for variable {var} must contain 'n' " + "and 'mode' keys." + ) + check_positive_integer(rules["n"], strict=True) + check_consistency(rules["mode"], str) + + # Check n only if sample_rules is not provided + else: + check_positive_integer(n, strict=True) + + # Check consistency + check_consistency(mode, str) + check_consistency(domains, str) + + # If sample_rules is provided, apply custom discretisation + if sample_rules is not None: + for d in domains: - def _apply_custom_discretization(self, sample_rules, domains): - """ - Apply custom discretization to the problem's domains. + # Discretise each variable according to its custom rules + discretised_tensor = [ + self.domains[d].sample(rules["n"], rules["mode"], var) + for var, rules in sample_rules.items() + ] - :param dict sample_rules: A dictionary of custom sampling rules. - :param domains: The domains from which to sample. - :type domains: str | list[str] - :raises RuntimeError: If the keys of the sample_rules dictionary are not - the same as the input variables. - :raises RuntimeError: If custom discretisation is applied on a domain - that is not a CartesianDomain. - """ - if sorted(list(sample_rules.keys())) != sorted(self.input_variables): - raise RuntimeError( - "The keys of the sample_rules dictionary must be the same as " - "the input variables." - ) - for domain in domains: - if not isinstance(self.domains[domain], CartesianDomain): - raise RuntimeError( - "Custom discretisation can be applied only on Cartesian " - "domains" - ) - discretised_tensor = [] - for var, rules in sample_rules.items(): - n, mode = rules["n"], rules["mode"] - points = self.domains[domain].sample(n, mode, var) - discretised_tensor.append(points) + # Merge the discretised tensors into a single one for the domain + self.discretised_domains[d] = merge_tensors(discretised_tensor) - self.discretised_domains[domain] = merge_tensors( - discretised_tensor - ).sort_labels() + # Otherwise, apply the same n and mode to all specified domains + else: + for d in domains: + self.discretised_domains[d] = self.domains[d].sample(n, mode) def add_points(self, new_points_dict): """ - Add new points to an already sampled domain. + Append additional points to an already discretised domain. + + :param dict new_points_dict: The dictionary mapping each domain to the + corresponding set of new points to be added. Each key in the + dictionary must match one of the domains defined in :attr:`domains`, + and each value must be a :class:`~pina.tensor.LabelTensor` + containing the new points to be added to that domain. The labels of + the points to be added must correspond to those of the domain to + which they are being added. + :raises ValueError: If ``new_points_dict`` is not a dictionary. + :raises ValueError: If any of the values in ``new_points_dict`` is not + a :class:`~pina.tensor.LabelTensor`. + :raises ValueError: If any of the keys in ``new_points_dict`` does not + match any of the domains defined in :attr:`domains`. + :raises ValueError: If any of the domains in ``new_points_dict`` has not + been discretised yet. - :param dict new_points_dict: The dictionary mapping new points to their - corresponding domain. + :Example: + >>> additional_points = { + ... "boundary": LabelTensor(torch.rand(5, 2), labels=["x", "y"]) + ... } + >>> problem.add_points(additional_points) """ - for k, v in new_points_dict.items(): - self.discretised_domains[k] = LabelTensor.vstack( - [self.discretised_domains[k], v] + # Check consistency + check_consistency(new_points_dict, dict) + + # Check the keys and values of the dictionary + for key, value in new_points_dict.items(): + check_consistency(value, LabelTensor) + if key not in self.domains: + raise ValueError( + f"Key {key} does not match any domain of the problem." + ) + if key not in self.discretised_domains: + raise ValueError(f"Domain {key} has not been discretised yet.") + + # Append the new points to the corresponding discretised domains + for key, value in new_points_dict.items(): + self.discretised_domains[key] = LabelTensor.vstack( + [self.discretised_domains[key], value] ) def move_discretisation_into_conditions(self): """ - Move the discretised domains into their corresponding conditions. + Move the sampled points from the discretised domains into their + corresponding conditions. This ensures that the conditions are evaluated + on the correct set of points after discretisation. """ - if not self.are_all_domains_discretised: - warnings.formatwarning = custom_warning_format - warnings.filterwarnings("always", category=RuntimeWarning) - warning_message = "\n".join([f"""{" " * 13} ---> Domain {key} { - "sampled" if key in self.discretised_domains - else - "not sampled"}""" for key in self.domains]) - warnings.warn( - "Some of the domains are still not sampled. Consider calling " - "problem.discretise_domain function for all domains before " - "accessing the collected data:\n" - f"{warning_message}", - RuntimeWarning, - ) - + # Move the discretised domains into their corresponding conditions for name, cond in self.conditions.items(): if hasattr(cond, "domain"): - domain = cond.domain - self.conditions[name] = Condition( + + # Create a new condition with the discretised domain as input + new_condition = Condition( input=self.discretised_domains[cond.domain], equation=cond.equation, ) - self.conditions[name].domain = domain - self.conditions[name].problem = self + + # Set the domain and problem attributes of the new condition + new_condition.domain = cond.domain + new_condition.problem = self + + # Replace the old condition in the conditions dictionary + self.conditions[name] = new_condition + + @property + def input_variables(self): + """ + The input variables of the problem. + + :return: The input variables of the problem. + :rtype: list[str] + """ + # Define a helper function to convert a string to a list if needed + _as_list = lambda x: [x] if isinstance(x, str) else x + + # Collect the spatial, temporal, and parametric variables + variables = [] + if hasattr(self, "spatial_variables"): + variables += _as_list(self.spatial_variables) + if hasattr(self, "temporal_variables"): + variables += _as_list(self.temporal_variables) + if hasattr(self, "parameters"): + variables += _as_list(self.parameters) + + return variables + + @property + def discretised_domains(self): + """ + The dictionary containing the discretised domains of the problem. Each + key corresponds to a domain defined in :attr:`domains`, and each value + is a :class:`~pina.tensor.LabelTensor` containing the sampled points for + that domain. + + :return: The discretised domains. + :rtype: dict + """ + return self._discretised_domains + + @property + def are_all_domains_discretised(self): + """ + Whether all domains of the problem have been discretised. + + :return: ``True`` if all domains are discretised, ``False`` otherwise. + :rtype: bool + """ + return all(d in self.discretised_domains for d in self.domains) diff --git a/pina/_src/problem/inverse_problem.py b/pina/_src/problem/inverse_problem.py index fa2f3d57f..2de4e6c53 100644 --- a/pina/_src/problem/inverse_problem.py +++ b/pina/_src/problem/inverse_problem.py @@ -7,8 +7,13 @@ class InverseProblem(AbstractProblem): """ - Class for defining inverse problems, where the objective is to determine - unknown parameters through training, based on given data. + Base class for all inverse problems, extending the standard problem + definition with unknown parameters to be determined through training. + + An inverse problem is defined by a set of unknown parameters that need to be + estimated from observed data. + + This class is not meant to be instantiated directly. """ def __init__(self): @@ -16,15 +21,15 @@ def __init__(self): Initialization of the :class:`InverseProblem` class. """ super().__init__() - # storing unknown_parameters for optimization + + # Set the unknown parameters as trainable parameters self.unknown_parameters = {} for var in self.unknown_variables: - range_var = self.unknown_parameter_domain._range[var] - tensor_var = ( - torch.rand(1, requires_grad=True) * range_var[1] + range_var[0] - ) + low, high = self.unknown_parameter_domain._range[var] + tensor_var = low + (high - low) * torch.rand(1) self.unknown_parameters[var] = torch.nn.Parameter(tensor_var) + @property @abstractmethod def unknown_parameter_domain(self): """ @@ -34,7 +39,7 @@ def unknown_parameter_domain(self): @property def unknown_variables(self): """ - Get the unknown variables of the problem. + The unknown variables of the problem. :return: The unknown variables of the problem. :rtype: list[str] @@ -44,7 +49,7 @@ def unknown_variables(self): @property def unknown_parameters(self): """ - Get the unknown parameters of the problem. + The unknown parameters of the problem. :return: The unknown parameters of the problem. :rtype: torch.nn.Parameter diff --git a/pina/_src/problem/parametric_problem.py b/pina/_src/problem/parametric_problem.py index e361074b3..c5e080e24 100644 --- a/pina/_src/problem/parametric_problem.py +++ b/pina/_src/problem/parametric_problem.py @@ -1,17 +1,23 @@ """Module for the ParametricProblem class.""" from abc import abstractmethod - -from .abstract_problem import AbstractProblem +from pina._src.problem.abstract_problem import AbstractProblem class ParametricProblem(AbstractProblem): """ - Class for defining parametric problems, where certain input variables are - treated as parameters that can vary, allowing the model to adapt to - different scenarios based on the chosen parameters. + Base class for all parametric problems, extending the standard problem + definition with parameter-dependent inputs. + + A parametric problem includes additional input variables, defined over a + dedicated parameter domain, which represent external quantities + (e.g., physical coefficients or control variables) that can vary across + different evaluations and influence the solution. + + This class is not meant to be instantiated directly. """ + @property @abstractmethod def parameter_domain(self): """ @@ -21,7 +27,7 @@ def parameter_domain(self): @property def parameters(self): """ - Get the parameters of the problem. + The parameters of the problem. :return: The parameters of the problem. :rtype: list[str] diff --git a/pina/_src/problem/problem_interface.py b/pina/_src/problem/problem_interface.py new file mode 100644 index 000000000..d64130d61 --- /dev/null +++ b/pina/_src/problem/problem_interface.py @@ -0,0 +1,150 @@ +"""Module for the Problem Interface.""" + +from abc import ABCMeta, abstractmethod + + +class ProblemInterface(metaclass=ABCMeta): + """ + Abstract interface for all problems. + """ + + @abstractmethod + def __deepcopy__(self, memo): + """ + Create a deep copy of the problem instance. + + :param dict memo: The memorization dictionary used by the deepcopy + function. + :return: A deep copy of the problem instance. + :rtype: ProblemInterface + """ + + @abstractmethod + def discretise_domain( + self, n=None, mode="random", domains=None, sample_rules=None + ): + """ + Discretise the problem's domains by sampling a specified number of + points according to the selected sampling mode. + + :param int n: The number of points to sample. This is ignored if + ``sample_rules`` is provided. Default is ``None``. + :param str mode: The sampling method. Available modes include: + ``"random"`` for random sampling, ``"latin"`` or ``"lh"`` for latin + hypercube sampling, ``"chebyshev"`` for Chebyshev sampling, and + ``"grid"`` for grid sampling. Default is ``"random"``. + :param domains: The domains from which to sample. If ``None``, all + domains are considered for sampling. Default is ``None``. + :type domains: str | list[str] + :param dict sample_rules: The dictionary specifying custom sampling + rules for each input variable. When provided, it overrides the + global ``n`` and ``mode`` arguments. Each key in the dictionary must + match one of the variables defined in :meth:`input_variables`, and + each value must be a dictionary containing two keys: ``n`` for the + number of points to sample for that variable, and ``mode`` for the + sampling method to use. If ``None``, the global ``n`` and ``mode`` + parameters are used for all variables. Default is ``None``. + + .. warning:: + ``"random"`` is the only supported ``mode`` across all geometries: + :class:`~pina.domain.cartesian_domain.CartesianDomain`, + :class:`~pina.domain.ellipsoid_domain.EllipsoidDomain`, and + :class:`~pina.domain.simplex_domain.SimplexDomain`. + Sampling modes such as ``"latin"``, ``"chebyshev"``, and ``"grid"`` + are only implemented for + :class:`~pina.domain.cartesian_domain.CartesianDomain`. + When custom discretisation is specified via ``sample_rules``, the + domain to be discretised must be an instance of + :class:`~pina.domain.cartesian_domain.CartesianDomain`. + + :Example: + >>> problem.discretise_domain(n=10, mode="random") + >>> problem.discretise_domain(n=10, mode="lh", domains=["boundary"]) + >>> problem.discretise_domain( + ... sample_rules={ + ... 'x': {'n': 10, 'mode': 'grid'}, + ... 'y': {'n': 100, 'mode': 'grid'} + ... }, + ... ) + """ + + @abstractmethod + def add_points(self, new_points_dict): + """ + Append additional points to an already discretised domain. + + :param dict new_points_dict: The dictionary mapping each domain to the + corresponding set of new points to be added. Each key in the + dictionary must match one of the domains defined in :attr:`domains`, + and each value must be a :class:`~pina.tensor.LabelTensor` + containing the new points to be added to that domain. The labels of + the points to be added must correspond to those of the domain to + which they are being added. + + :Example: + >>> additional_points = { + ... "boundary": LabelTensor(torch.rand(5, 2), labels=["x", "y"]) + ... } + >>> problem.add_points(additional_points) + """ + + @abstractmethod + def move_discretisation_into_conditions(self): + """ + Move the sampled points from the discretised domains into their + corresponding conditions. This ensures that the conditions are evaluated + on the correct set of points after discretisation. + """ + + @property + @abstractmethod + def input_variables(self): + """ + The input variables of the problem. + + :return: The input variables of the problem. + :rtype: list[str] + """ + + @property + @abstractmethod + def output_variables(self): + """ + The output variables of the problem. + + :return: The output variables of the problem. + :rtype: list[str] + """ + + @property + @abstractmethod + def conditions(self): + """ + The conditions associated with the problem. + + :return: The conditions associated with the problem. + :rtype: dict + """ + + @property + @abstractmethod + def discretised_domains(self): + """ + The dictionary containing the discretised domains of the problem. Each + key corresponds to a domain defined in :attr:`domains`, and each value + is a :class:`~pina.tensor.LabelTensor` containing the sampled points for + that domain. + + :return: The discretised domains. + :rtype: dict + """ + + @property + @abstractmethod + def are_all_domains_discretised(self): + """ + Whether all domains of the problem have been discretised. + + :return: ``True`` if all domains are discretised, ``False`` otherwise. + :rtype: bool + """ diff --git a/pina/_src/problem/spatial_problem.py b/pina/_src/problem/spatial_problem.py index 608e31691..08896ebe9 100644 --- a/pina/_src/problem/spatial_problem.py +++ b/pina/_src/problem/spatial_problem.py @@ -1,26 +1,32 @@ """Module for the SpatialProblem class.""" from abc import abstractmethod - -from .abstract_problem import AbstractProblem +from pina._src.problem.abstract_problem import AbstractProblem class SpatialProblem(AbstractProblem): """ - Class for defining spatial problems, where the problem domain is defined in - terms of spatial variables. + Base class for all spatial problems, extending the standard problem + definition with spatial-dependent inputs. + + A spatial problem is defined over a spatial domain, where input variables + represent the coordinates of the system (e.g., positions in one or more + dimensions) on which the solution is evaluated. + + This class is not meant to be instantiated directly. """ + @property @abstractmethod def spatial_domain(self): """ - The spatial domain of the problem. + The domain of spatial variables of the problem. """ @property def spatial_variables(self): """ - Get the spatial input variables of the problem. + The spatial input variables of the problem. :return: The spatial input variables of the problem. :rtype: list[str] diff --git a/pina/_src/problem/time_dependent_problem.py b/pina/_src/problem/time_dependent_problem.py index ea2ad7d54..98e689641 100644 --- a/pina/_src/problem/time_dependent_problem.py +++ b/pina/_src/problem/time_dependent_problem.py @@ -1,28 +1,33 @@ """Module for the TimeDependentProblem class.""" from abc import abstractmethod - -from .abstract_problem import AbstractProblem +from pina._src.problem.abstract_problem import AbstractProblem class TimeDependentProblem(AbstractProblem): """ - Class for defining time-dependent problems, where the system's behavior - changes with respect to time. + Base class for all time-dependent problems, extending the standard problem + definition with time-dependent inputs. + + A time-dependent problem is defined over a temporal domain, where input + variables represent the time at which the solution is evaluated. + + This class is not meant to be instantiated directly. """ + @property @abstractmethod def temporal_domain(self): """ - The temporal domain of the problem. + The domain of temporal variables of the problem. """ @property - def temporal_variable(self): + def temporal_variables(self): """ - Get the time variable of the problem. + The temporal variables of the problem. - :return: The time variable of the problem. + :return: The temporal variables of the problem. :rtype: list[str] """ return self.temporal_domain.variables diff --git a/pina/_src/problem/zoo/acoustic_wave.py b/pina/_src/problem/zoo/acoustic_wave_problem.py similarity index 88% rename from pina/_src/problem/zoo/acoustic_wave.py rename to pina/_src/problem/zoo/acoustic_wave_problem.py index 44db8eb96..5445d2455 100644 --- a/pina/_src/problem/zoo/acoustic_wave.py +++ b/pina/_src/problem/zoo/acoustic_wave_problem.py @@ -1,18 +1,15 @@ """Formulation of the acoustic wave problem.""" import torch -from pina._src.condition.condition import Condition -from pina._src.problem.spatial_problem import SpatialProblem from pina._src.problem.time_dependent_problem import TimeDependentProblem -from pina._src.core.utils import check_consistency from pina._src.domain.cartesian_domain import CartesianDomain -from pina._src.equation.equation import Equation from pina._src.equation.system_equation import SystemEquation -from pina._src.equation.equation_factory import ( - FixedValue, - FixedGradient, - AcousticWave, -) +from pina._src.problem.spatial_problem import SpatialProblem +from pina._src.condition.condition import Condition +from pina._src.core.utils import check_consistency +from pina._src.equation.equation import Equation +from pina._src.equation.equation_factory import FixedValue, FixedGradient +from pina._src.equation.zoo.acoustic_wave_equation import AcousticWaveEquation def initial_condition(input_, output_): @@ -70,7 +67,7 @@ def __init__(self, c=2.0): """ Initialization of the :class:`AcousticWaveProblem` class. - :param c: The wave propagation speed. Default is 2.0. + :param c: The wave propagation speed. Default is ``2.0``. :type c: float | int """ super().__init__() @@ -78,7 +75,7 @@ def __init__(self, c=2.0): self.c = c self.conditions["D"] = Condition( - domain="D", equation=AcousticWave(self.c) + domain="D", equation=AcousticWaveEquation(self.c) ) def solution(self, pts): @@ -93,4 +90,7 @@ def solution(self, pts): arg_t = self.c * torch.pi * pts["t"] term1 = torch.sin(arg_x) * torch.cos(arg_t) term2 = 0.5 * torch.sin(4 * arg_x) * torch.cos(4 * arg_t) - return term1 + term2 + + sol = term1 + term2 + sol.labels = self.output_variables + return sol diff --git a/pina/_src/problem/zoo/advection.py b/pina/_src/problem/zoo/advection_problem.py similarity index 86% rename from pina/_src/problem/zoo/advection.py rename to pina/_src/problem/zoo/advection_problem.py index 3067ce8bf..b46eae737 100644 --- a/pina/_src/problem/zoo/advection.py +++ b/pina/_src/problem/zoo/advection_problem.py @@ -1,13 +1,13 @@ """Formulation of the advection problem.""" import torch -from pina._src.condition.condition import Condition -from pina._src.problem.spatial_problem import SpatialProblem from pina._src.problem.time_dependent_problem import TimeDependentProblem -from pina._src.equation.equation import Equation -from pina._src.equation.equation_factory import Advection -from pina._src.core.utils import check_consistency from pina._src.domain.cartesian_domain import CartesianDomain +from pina._src.problem.spatial_problem import SpatialProblem +from pina._src.equation.zoo.advection_equation import AdvectionEquation +from pina._src.condition.condition import Condition +from pina._src.core.utils import check_consistency +from pina._src.equation.equation import Equation def initial_condition(input_, output_): @@ -25,7 +25,8 @@ def initial_condition(input_, output_): class AdvectionProblem(SpatialProblem, TimeDependentProblem): r""" Implementation of the advection problem in the spatial interval - :math:`[0, 2 \pi]` and temporal interval :math:`[0, 1]`. + :math:`[0, 2 \pi]` and temporal interval :math:`[0, 1]` with periodic + boundary conditions. .. seealso:: @@ -56,14 +57,16 @@ def __init__(self, c=1.0): """ Initialization of the :class:`AdvectionProblem`. - :param c: The advection velocity parameter. Default is 1.0. + :param c: The advection velocity parameter. Default is ``1.0``. :type c: float | int """ super().__init__() check_consistency(c, (float, int)) self.c = c - self.conditions["D"] = Condition(domain="D", equation=Advection(self.c)) + self.conditions["D"] = Condition( + domain="D", equation=AdvectionEquation(self.c) + ) def solution(self, pts): """ diff --git a/pina/_src/problem/zoo/allen_cahn.py b/pina/_src/problem/zoo/allen_cahn_problem.py similarity index 85% rename from pina/_src/problem/zoo/allen_cahn.py rename to pina/_src/problem/zoo/allen_cahn_problem.py index 125a10304..5d80d8265 100644 --- a/pina/_src/problem/zoo/allen_cahn.py +++ b/pina/_src/problem/zoo/allen_cahn_problem.py @@ -1,12 +1,11 @@ """Formulation of the Allen Cahn problem.""" import torch - from pina._src.condition.condition import Condition from pina._src.problem.spatial_problem import SpatialProblem from pina._src.problem.time_dependent_problem import TimeDependentProblem from pina._src.equation.equation import Equation -from pina._src.equation.equation_factory import AllenCahn +from pina._src.equation.zoo.allen_cahn_equation import AllenCahnEquation from pina._src.core.utils import check_consistency from pina._src.domain.cartesian_domain import CartesianDomain @@ -28,7 +27,8 @@ def initial_condition(input_, output_): class AllenCahnProblem(TimeDependentProblem, SpatialProblem): r""" Implementation of the Allen Cahn problem in the spatial interval - :math:`[-1, 1]` and temporal interval :math:`[0, 1]`. + :math:`[-1, 1]` and temporal interval :math:`[0, 1]` with periodic + boundary conditions. .. seealso:: @@ -62,9 +62,9 @@ def __init__(self, alpha=1e-4, beta=5): """ Initialization of the :class:`AllenCahnProblem`. - :param alpha: The diffusion coefficient. Default is 1e-4. + :param alpha: The diffusion coefficient. Default is ``1e-4``. :type alpha: float | int - :param beta: The reaction coefficient. Default is 5.0. + :param beta: The reaction coefficient. Default is ``5.0``. :type beta: float | int """ super().__init__() @@ -75,5 +75,5 @@ def __init__(self, alpha=1e-4, beta=5): self.conditions["D"] = Condition( domain="D", - equation=AllenCahn(alpha=self.alpha, beta=self.beta), + equation=AllenCahnEquation(alpha=self.alpha, beta=self.beta), ) diff --git a/pina/_src/problem/zoo/diffusion_reaction.py b/pina/_src/problem/zoo/diffusion_reaction_problem.py similarity index 84% rename from pina/_src/problem/zoo/diffusion_reaction.py rename to pina/_src/problem/zoo/diffusion_reaction_problem.py index 443ff49c5..9b8870189 100644 --- a/pina/_src/problem/zoo/diffusion_reaction.py +++ b/pina/_src/problem/zoo/diffusion_reaction_problem.py @@ -3,11 +3,14 @@ import torch from pina._src.condition.condition import Condition from pina._src.equation.equation import Equation -from pina._src.equation.equation_factory import FixedValue, DiffusionReaction +from pina._src.equation.equation_factory import FixedValue from pina._src.problem.spatial_problem import SpatialProblem from pina._src.problem.time_dependent_problem import TimeDependentProblem from pina._src.core.utils import check_consistency from pina._src.domain.cartesian_domain import CartesianDomain +from pina._src.equation.zoo.diffusion_reaction_equation import ( + DiffusionReactionEquation, +) def initial_condition(input_, output_): @@ -65,7 +68,7 @@ def __init__(self, alpha=1e-4): """ Initialization of the :class:`DiffusionReactionProblem`. - :param alpha: The diffusion coefficient. Default is 1e-4. + :param alpha: The diffusion coefficient. Default is ``1e-4``. :type alpha: float | int """ super().__init__() @@ -82,15 +85,16 @@ def forcing_term(input_): t = input_.extract("t") return torch.exp(-t) * ( - 1.5 * torch.sin(2 * x) - + (8 / 3) * torch.sin(3 * x) - + (15 / 4) * torch.sin(4 * x) - + (63 / 8) * torch.sin(8 * x) + (self.alpha - 1) * torch.sin(x) + + ((4 * self.alpha - 1) / 2) * torch.sin(2 * x) + + ((9 * self.alpha - 1) / 3) * torch.sin(3 * x) + + ((16 * self.alpha - 1) / 4) * torch.sin(4 * x) + + ((64 * self.alpha - 1) / 8) * torch.sin(8 * x) ) self.conditions["D"] = Condition( domain="D", - equation=DiffusionReaction(self.alpha, forcing_term), + equation=DiffusionReactionEquation(self.alpha, forcing_term), ) def solution(self, pts): diff --git a/pina/_src/problem/zoo/helmholtz.py b/pina/_src/problem/zoo/helmholtz_problem.py similarity index 89% rename from pina/_src/problem/zoo/helmholtz.py rename to pina/_src/problem/zoo/helmholtz_problem.py index 992dda638..6e59a24c9 100644 --- a/pina/_src/problem/zoo/helmholtz.py +++ b/pina/_src/problem/zoo/helmholtz_problem.py @@ -1,9 +1,9 @@ """Formulation of the Helmholtz problem.""" import torch - from pina._src.condition.condition import Condition -from pina._src.equation.equation_factory import FixedValue, Helmholtz +from pina._src.equation.equation_factory import FixedValue +from pina._src.equation.zoo.helmholtz_equation import HelmholtzEquation from pina._src.problem.spatial_problem import SpatialProblem from pina._src.core.utils import check_consistency from pina._src.domain.cartesian_domain import CartesianDomain @@ -41,10 +41,10 @@ def __init__(self, k=1.0, alpha_x=1, alpha_y=4): """ Initialization of the :class:`HelmholtzProblem` class. - :param k: The squared wavenumber. Default is 1.0. + :param k: The squared wavenumber. Default is ``1.0``. :type k: float | int - :param int alpha_x: The frequency in the x-direction. Default is 1. - :param int alpha_y: The frequency in the y-direction. Default is 4. + :param int alpha_x: The frequency in the x-direction. Default is ``1``. + :param int alpha_y: The frequency in the y-direction. Default is ``4``. """ super().__init__() check_consistency(k, (int, float)) @@ -68,7 +68,7 @@ def forcing_term(input_): self.conditions["D"] = Condition( domain="D", - equation=Helmholtz(self.k, forcing_term), + equation=HelmholtzEquation(self.k, forcing_term), ) def solution(self, pts): diff --git a/pina/_src/problem/zoo/inverse_poisson_2d_square.py b/pina/_src/problem/zoo/inverse_poisson_problem.py similarity index 97% rename from pina/_src/problem/zoo/inverse_poisson_2d_square.py rename to pina/_src/problem/zoo/inverse_poisson_problem.py index 19628cae0..f0865d4cb 100644 --- a/pina/_src/problem/zoo/inverse_poisson_2d_square.py +++ b/pina/_src/problem/zoo/inverse_poisson_problem.py @@ -5,16 +5,15 @@ import torch from io import BytesIO - -from pina._src.condition.condition import Condition -from pina._src.equation.equation import Equation -from pina._src.equation.equation_factory import FixedValue -from pina._src.problem.spatial_problem import SpatialProblem -from pina._src.problem.inverse_problem import InverseProblem +from pina._src.core.utils import custom_warning_format, check_consistency from pina._src.domain.cartesian_domain import CartesianDomain +from pina._src.problem.inverse_problem import InverseProblem +from pina._src.problem.spatial_problem import SpatialProblem +from pina._src.equation.equation_factory import FixedValue +from pina._src.condition.condition import Condition from pina._src.core.label_tensor import LabelTensor +from pina._src.equation.equation import Equation from pina._src.core.operator import laplacian -from pina._src.core.utils import custom_warning_format, check_consistency warnings.formatwarning = custom_warning_format warnings.filterwarnings("always", category=ResourceWarning) @@ -32,7 +31,7 @@ def _load_tensor_from_url(url, labels, timeout=10): :param str url: URL to the remote `.pth` tensor file. :param labels: Labels for the resulting LabelTensor. :type labels: list[str] | tuple[str] - :param int timeout: Timeout for the request in seconds. Default is 10s. + :param int timeout: Timeout for the request in seconds. Default is ``10`` s. :return: A LabelTensor object if successful, otherwise None. :rtype: LabelTensor | None """ @@ -109,10 +108,10 @@ def __init__(self, load=True, data_size=1.0): :param bool load: If True, it attempts to load data from remote URLs. Set to False to skip data loading (e.g., if no internet connection). - Default is True. + Default is ``True``. :param float data_size: The fraction of the total data to use for the "data" condition. If set to 1.0, all available data is used. - If set to 0.0, no data is used. Default is 1.0. + If set to 0.0, no data is used. Default is ``1.0``. :raises ValueError: If `data_size` is not in the range [0.0, 1.0]. :raises ValueError: If `data_size` is not a float. """ diff --git a/pina/_src/problem/zoo/poisson_2d_square.py b/pina/_src/problem/zoo/poisson_problem.py similarity index 87% rename from pina/_src/problem/zoo/poisson_2d_square.py rename to pina/_src/problem/zoo/poisson_problem.py index 12b365666..b92fbce87 100644 --- a/pina/_src/problem/zoo/poisson_2d_square.py +++ b/pina/_src/problem/zoo/poisson_problem.py @@ -2,10 +2,11 @@ import torch -from pina._src.condition.condition import Condition -from pina._src.equation.equation_factory import FixedValue, Poisson -from pina._src.problem.spatial_problem import SpatialProblem +from pina._src.equation.equation_factory import FixedValue from pina._src.domain.cartesian_domain import CartesianDomain +from pina._src.problem.spatial_problem import SpatialProblem +from pina._src.condition.condition import Condition +from pina._src.equation.zoo.poisson_equation import PoissonEquation def forcing_term(input_): @@ -43,7 +44,9 @@ class Poisson2DSquareProblem(SpatialProblem): conditions = { "boundary": Condition(domain="boundary", equation=FixedValue(0.0)), - "D": Condition(domain="D", equation=Poisson(forcing_term=forcing_term)), + "D": Condition( + domain="D", equation=PoissonEquation(forcing_term=forcing_term) + ), } def solution(self, pts): diff --git a/pina/_src/problem/zoo/supervised_problem.py b/pina/_src/problem/zoo/supervised_problem.py index 81fb18a44..5ad332a45 100644 --- a/pina/_src/problem/zoo/supervised_problem.py +++ b/pina/_src/problem/zoo/supervised_problem.py @@ -8,8 +8,7 @@ class SupervisedProblem(AbstractProblem): """ Definition of a supervised-learning problem. - This class provides a simple way to define a supervised problem - using a single condition of type + This class provides a simple way to define a supervised problem using the :class:`~pina.condition.input_target_condition.InputTargetCondition`. :Example: @@ -20,6 +19,9 @@ class SupervisedProblem(AbstractProblem): >>> problem = SupervisedProblem(input_data, output_data) """ + # TODO: This is necessary to override the abstract properties of + # AbstractProblem, but it is not an ideal solution. We should consider + # a different desgin to manage input and output variables. conditions = {} output_variables = None input_variables = None @@ -36,10 +38,10 @@ def __init__( :type output_: torch.Tensor | LabelTensor | Graph | Data :param list[str] input_variables: List of names of the input variables. If None, the input variables are inferred from `input_`. - Default is None. + Default is ``None``. :param list[str] output_variables: List of names of the output variables. If None, the output variables are inferred from - `output_`. Default is None. + `output_`. Default is ``None``. """ # Set input and output variables self.input_variables = input_variables diff --git a/pina/_src/solver/ensemble_solver/ensemble_pinn.py b/pina/_src/solver/ensemble_solver/ensemble_pinn.py index f010753ec..a757d9c04 100644 --- a/pina/_src/solver/ensemble_solver/ensemble_pinn.py +++ b/pina/_src/solver/ensemble_solver/ensemble_pinn.py @@ -145,7 +145,7 @@ def loss_phys(self, samples, equation): model. This method should not be overridden, if not intentionally. :param LabelTensor samples: The samples to evaluate the physics loss. - :param EquationInterface equation: The governing equation. + :param BaseEquation equation: The governing equation. :return: The computed physics loss. :rtype: LabelTensor """ @@ -161,7 +161,7 @@ def _residual_loss(self, samples, equation): method. :param LabelTensor samples: The samples to evaluate the loss. - :param EquationInterface equation: The governing equation. + :param BaseEquation equation: The governing equation. :return: The residual loss. :rtype: torch.Tensor """ diff --git a/pina/_src/solver/physics_informed_solver/causal_pinn.py b/pina/_src/solver/physics_informed_solver/causal_pinn.py index e7e97392b..9cefe1e30 100644 --- a/pina/_src/solver/physics_informed_solver/causal_pinn.py +++ b/pina/_src/solver/physics_informed_solver/causal_pinn.py @@ -120,7 +120,7 @@ def loss_phys(self, samples, equation): provided samples and equation. :param LabelTensor samples: The samples to evaluate the physics loss. - :param EquationInterface equation: The governing equation. + :param BaseEquation equation: The governing equation. :return: The computed physics loss. :rtype: LabelTensor """ diff --git a/pina/_src/solver/physics_informed_solver/competitive_pinn.py b/pina/_src/solver/physics_informed_solver/competitive_pinn.py index 287e0fd8d..7e658f6b0 100644 --- a/pina/_src/solver/physics_informed_solver/competitive_pinn.py +++ b/pina/_src/solver/physics_informed_solver/competitive_pinn.py @@ -146,7 +146,7 @@ def loss_phys(self, samples, equation): provided samples and equation. :param LabelTensor samples: The samples to evaluate the physics loss. - :param EquationInterface equation: The governing equation. + :param BaseEquation equation: The governing equation. :return: The computed physics loss. :rtype: LabelTensor """ diff --git a/pina/_src/solver/physics_informed_solver/gradient_pinn.py b/pina/_src/solver/physics_informed_solver/gradient_pinn.py index 9583c3025..09e7b66a6 100644 --- a/pina/_src/solver/physics_informed_solver/gradient_pinn.py +++ b/pina/_src/solver/physics_informed_solver/gradient_pinn.py @@ -110,7 +110,7 @@ def loss_phys(self, samples, equation): provided samples and equation. :param LabelTensor samples: The samples to evaluate the physics loss. - :param EquationInterface equation: The governing equation. + :param BaseEquation equation: The governing equation. :return: The computed physics loss. :rtype: LabelTensor """ diff --git a/pina/_src/solver/physics_informed_solver/pinn.py b/pina/_src/solver/physics_informed_solver/pinn.py index dbea8cbe3..9bf1f8f88 100644 --- a/pina/_src/solver/physics_informed_solver/pinn.py +++ b/pina/_src/solver/physics_informed_solver/pinn.py @@ -105,7 +105,7 @@ def loss_phys(self, samples, equation): provided samples and equation. :param LabelTensor samples: The samples to evaluate the physics loss. - :param EquationInterface equation: The governing equation. + :param BaseEquation equation: The governing equation. :return: The computed physics loss. :rtype: LabelTensor """ diff --git a/pina/_src/solver/physics_informed_solver/pinn_interface.py b/pina/_src/solver/physics_informed_solver/pinn_interface.py index 517b48082..5db84a0bf 100644 --- a/pina/_src/solver/physics_informed_solver/pinn_interface.py +++ b/pina/_src/solver/physics_informed_solver/pinn_interface.py @@ -176,7 +176,7 @@ def loss_phys(self, samples, equation): subclasses. It distinguishes different types of PINN solvers. :param LabelTensor samples: The samples to evaluate the physics loss. - :param EquationInterface equation: The governing equation. + :param BaseEquation equation: The governing equation. :return: The computed physics loss. :rtype: LabelTensor """ @@ -186,7 +186,7 @@ def compute_residual(self, samples, equation): Compute the residuals of the equation. :param LabelTensor samples: The samples to evaluate the loss. - :param EquationInterface equation: The governing equation. + :param BaseEquation equation: The governing equation. :return: The residual of the solution of the model. :rtype: LabelTensor """ @@ -204,7 +204,7 @@ def _residual_loss(self, samples, equation): :param LabelTensor samples: The samples to evaluate the loss. - :param EquationInterface equation: The governing equation. + :param BaseEquation equation: The governing equation. :return: The residual loss. :rtype: torch.Tensor """ diff --git a/pina/_src/solver/physics_informed_solver/self_adaptive_pinn.py b/pina/_src/solver/physics_informed_solver/self_adaptive_pinn.py index ee7f281e6..a7ac124e1 100644 --- a/pina/_src/solver/physics_informed_solver/self_adaptive_pinn.py +++ b/pina/_src/solver/physics_informed_solver/self_adaptive_pinn.py @@ -258,7 +258,7 @@ def loss_phys(self, samples, equation): provided samples and equation. :param LabelTensor samples: The samples to evaluate the physics loss. - :param EquationInterface equation: The governing equation. + :param BaseEquation equation: The governing equation. :return: The computed physics loss. :rtype: LabelTensor """ diff --git a/pina/equation/__init__.py b/pina/equation/__init__.py index 551099af6..91b803c54 100644 --- a/pina/equation/__init__.py +++ b/pina/equation/__init__.py @@ -1,4 +1,5 @@ -"""Mathematical equations and physical laws. +""" +Mathematical equations and physical laws. This module provides a framework for defining differential equations, boundary conditions, and complex systems of equations. It includes @@ -7,33 +8,25 @@ """ __all__ = [ - "SystemEquation", + "EquationInterface", + "BaseEquation", "Equation", + "SystemEquation", "FixedValue", "FixedGradient", "FixedFlux", "FixedLaplacian", "Laplace", - "Advection", - "AllenCahn", - "DiffusionReaction", - "Helmholtz", - "Poisson", - "AcousticWave", ] +from pina._src.equation.equation_interface import EquationInterface +from pina._src.equation.base_equation import BaseEquation from pina._src.equation.equation import Equation +from pina._src.equation.system_equation import SystemEquation from pina._src.equation.equation_factory import ( FixedFlux, FixedGradient, FixedLaplacian, FixedValue, Laplace, - Advection, - AllenCahn, - DiffusionReaction, - Helmholtz, - Poisson, - AcousticWave, ) -from pina._src.equation.system_equation import SystemEquation diff --git a/pina/equation/zoo.py b/pina/equation/zoo.py new file mode 100644 index 000000000..daecc370d --- /dev/null +++ b/pina/equation/zoo.py @@ -0,0 +1,19 @@ +"""Module for implemented equations.""" + +__all__ = [ + "AdvectionEquation", + "AllenCahnEquation", + "DiffusionReactionEquation", + "HelmholtzEquation", + "PoissonEquation", + "AcousticWaveEquation", +] + +from pina._src.equation.zoo.acoustic_wave_equation import AcousticWaveEquation +from pina._src.equation.zoo.advection_equation import AdvectionEquation +from pina._src.equation.zoo.allen_cahn_equation import AllenCahnEquation +from pina._src.equation.zoo.diffusion_reaction_equation import ( + DiffusionReactionEquation, +) +from pina._src.equation.zoo.helmholtz_equation import HelmholtzEquation +from pina._src.equation.zoo.poisson_equation import PoissonEquation diff --git a/pina/problem/__init__.py b/pina/problem/__init__.py index b170bec21..f74cb7853 100644 --- a/pina/problem/__init__.py +++ b/pina/problem/__init__.py @@ -1,6 +1,7 @@ """Module for the Problems.""" __all__ = [ + "ProblemInterface", "AbstractProblem", "SpatialProblem", "TimeDependentProblem", @@ -8,6 +9,7 @@ "InverseProblem", ] +from pina._src.problem.problem_interface import ProblemInterface from pina._src.problem.abstract_problem import AbstractProblem from pina._src.problem.spatial_problem import SpatialProblem from pina._src.problem.time_dependent_problem import TimeDependentProblem diff --git a/pina/problem/zoo.py b/pina/problem/zoo.py index e5c23ae81..6c027ed54 100644 --- a/pina/problem/zoo.py +++ b/pina/problem/zoo.py @@ -11,13 +11,15 @@ "AcousticWaveProblem", ] +from pina._src.problem.zoo.acoustic_wave_problem import AcousticWaveProblem from pina._src.problem.zoo.supervised_problem import SupervisedProblem -from pina._src.problem.zoo.helmholtz import HelmholtzProblem -from pina._src.problem.zoo.allen_cahn import AllenCahnProblem -from pina._src.problem.zoo.advection import AdvectionProblem -from pina._src.problem.zoo.poisson_2d_square import Poisson2DSquareProblem -from pina._src.problem.zoo.diffusion_reaction import DiffusionReactionProblem -from pina._src.problem.zoo.inverse_poisson_2d_square import ( +from pina._src.problem.zoo.allen_cahn_problem import AllenCahnProblem +from pina._src.problem.zoo.advection_problem import AdvectionProblem +from pina._src.problem.zoo.helmholtz_problem import HelmholtzProblem +from pina._src.problem.zoo.poisson_problem import Poisson2DSquareProblem +from pina._src.problem.zoo.diffusion_reaction_problem import ( + DiffusionReactionProblem, +) +from pina._src.problem.zoo.inverse_poisson_problem import ( InversePoisson2DSquareProblem, ) -from pina._src.problem.zoo.acoustic_wave import AcousticWaveProblem diff --git a/tests/test_equation/test_equation.py b/tests/test_equation/test_equation.py index 096b2d5e7..0569d2f49 100644 --- a/tests/test_equation/test_equation.py +++ b/tests/test_equation/test_equation.py @@ -1,10 +1,11 @@ -from pina.equation import Equation -from pina.operator import grad, laplacian -from pina import LabelTensor import torch import pytest +from pina.operator import grad, laplacian +from pina.equation import Equation +from pina import LabelTensor +# Define equations for testing def eq1(input_, output_): u_grad = grad(output_, input_) u1_xx = grad(u_grad, input_, components=["du1dx"], d=["x"]) @@ -24,26 +25,38 @@ def foo(): pass -def test_constructor(): - Equation(eq1) - Equation(eq2) +@pytest.mark.parametrize("equation", [eq1, eq2]) +def test_constructor(equation): + Equation(equation) + + # Should fail if the equation is not a callable function with pytest.raises(ValueError): Equation([1, 2, 4]) + + # Should fail if the equation is not a callable function with pytest.raises(ValueError): Equation(foo()) -def test_residual(): - eq_1 = Equation(eq1) - eq_2 = Equation(eq2) +@pytest.mark.parametrize("equation, last_dim", [(eq1, 2), (eq2, 1)]) +def test_residual(equation, last_dim): - pts = LabelTensor(torch.rand(10, 2), labels=["x", "y"]) - pts.requires_grad = True - u = torch.pow(pts, 2) - u.labels = ["u1", "u2"] + # Define the equation + eq = Equation(equation) - eq_1_res = eq_1.residual(pts, u) - eq_2_res = eq_2.residual(pts, u) + # Manage number of points and variables + n_pts = 10 + input_vars = ["x", "y"] + output_vars = ["u1", "u2"] + + # Define the input and output tensors + pts = LabelTensor( + torch.rand(n_pts, len(input_vars), requires_grad=True), + labels=input_vars, + ) + u = torch.pow(pts, 2) + u.labels = output_vars - assert eq_1_res.shape == torch.Size([10, 2]) - assert eq_2_res.shape == torch.Size([10, 1]) + # Compute the residuals and check the shape + eq_res = eq.residual(pts, u) + assert eq_res.shape == torch.Size([n_pts, last_dim]) diff --git a/tests/test_equation/test_equation_factory.py b/tests/test_equation/test_equation_factory.py index 578d9ba30..3a931bd5f 100644 --- a/tests/test_equation/test_equation_factory.py +++ b/tests/test_equation/test_equation_factory.py @@ -1,15 +1,4 @@ -from pina.equation import ( - FixedValue, - FixedGradient, - FixedFlux, - FixedLaplacian, - Advection, - AllenCahn, - DiffusionReaction, - Helmholtz, - Poisson, - AcousticWave, -) +from pina.equation import FixedValue, FixedGradient, FixedFlux, FixedLaplacian from pina import LabelTensor import torch import pytest @@ -79,139 +68,3 @@ def test_fixed_laplacian(value, components, d): residual = equation.residual(pts, u) len_c = len(components) if components is not None else u.shape[1] assert residual.shape == (pts.shape[0], len_c) - - -@pytest.mark.parametrize("c", [1.0, 10, [1, 2.5]]) -def test_advection_equation(c): - - # Constructor - equation = Advection(c) - - # Should fail if c is an empty list - with pytest.raises(ValueError): - Advection([]) - - # Should fail if c is not a float, int, or list - with pytest.raises(ValueError): - Advection("invalid") - - # Residual - residual = equation.residual(pts, u) - assert residual.shape == u.shape - - # Should fail if the input has no 't' label - with pytest.raises(ValueError): - residual = equation.residual(pts["x", "y"], u) - - # Should fail if c is a list and its length != spatial dimension - with pytest.raises(ValueError): - equation = Advection([1, 2, 3]) - residual = equation.residual(pts, u) - - -@pytest.mark.parametrize("alpha", [1.0, 10, -7.5]) -@pytest.mark.parametrize("beta", [1.0, 10, -7.5]) -def test_allen_cahn_equation(alpha, beta): - - # Constructor - equation = AllenCahn(alpha=alpha, beta=beta) - - # Should fail if alpha is not a float or int - with pytest.raises(ValueError): - AllenCahn(alpha="invalid", beta=beta) - - # Should fail if beta is not a float or int - with pytest.raises(ValueError): - AllenCahn(alpha=alpha, beta="invalid") - - # Residual - residual = equation.residual(pts, u) - assert residual.shape == u.shape - - # Should fail if the input has no 't' label - with pytest.raises(ValueError): - residual = equation.residual(pts["x", "y"], u) - - -@pytest.mark.parametrize("alpha", [1.0, 10, -7.5]) -@pytest.mark.parametrize( - "forcing_term", [lambda x: torch.sin(x), lambda x: torch.exp(x)] -) -def test_diffusion_reaction_equation(alpha, forcing_term): - - # Constructor - equation = DiffusionReaction(alpha=alpha, forcing_term=forcing_term) - - # Should fail if alpha is not a float or int - with pytest.raises(ValueError): - DiffusionReaction(alpha="invalid", forcing_term=forcing_term) - - # Should fail if forcing_term is not a callable - with pytest.raises(ValueError): - DiffusionReaction(alpha=alpha, forcing_term="invalid") - - # Residual - residual = equation.residual(pts, u) - assert residual.shape == u.shape - - # Should fail if the input has no 't' label - with pytest.raises(ValueError): - residual = equation.residual(pts["x", "y"], u) - - -@pytest.mark.parametrize("k", [1.0, 10, -7.5]) -@pytest.mark.parametrize( - "forcing_term", [lambda x: torch.sin(x), lambda x: torch.exp(x)] -) -def test_helmholtz_equation(k, forcing_term): - - # Constructor - equation = Helmholtz(k=k, forcing_term=forcing_term) - - # Should fail if k is not a float or int - with pytest.raises(ValueError): - Helmholtz(k="invalid", forcing_term=forcing_term) - - # Should fail if forcing_term is not a callable - with pytest.raises(ValueError): - Helmholtz(k=k, forcing_term="invalid") - - # Residual - residual = equation.residual(pts, u) - assert residual.shape == u.shape - - -@pytest.mark.parametrize( - "forcing_term", [lambda x: torch.sin(x), lambda x: torch.exp(x)] -) -def test_poisson_equation(forcing_term): - - # Constructor - equation = Poisson(forcing_term=forcing_term) - - # Should fail if forcing_term is not a callable - with pytest.raises(ValueError): - Poisson(forcing_term="invalid") - - # Residual - residual = equation.residual(pts, u) - assert residual.shape == u.shape - - -@pytest.mark.parametrize("c", [1.0, 10, -7.5]) -def test_acoustic_wave_equation(c): - - # Constructor - equation = AcousticWave(c=c) - - # Should fail if c is not a float or int - with pytest.raises(ValueError): - AcousticWave(c="invalid") - - # Residual - residual = equation.residual(pts, u) - assert residual.shape == u.shape - - # Should fail if the input has no 't' label - with pytest.raises(ValueError): - residual = equation.residual(pts["x", "y"], u) diff --git a/tests/test_equation/test_system_equation.py b/tests/test_equation/test_system_equation.py index bf6268148..294c30d56 100644 --- a/tests/test_equation/test_system_equation.py +++ b/tests/test_equation/test_system_equation.py @@ -5,6 +5,7 @@ import pytest +# Define equations for testing def eq1(input_, output_): u_grad = grad(output_, input_) u1_xx = grad(u_grad, input_, components=["du1dx"], d=["x"]) @@ -20,82 +21,63 @@ def eq2(input_, output_): return delta_u - force_term -def foo(): - pass +def reduction_fn(residuals, dim): + return torch.sum(residuals, dim=dim) / residuals.shape[dim] -@pytest.mark.parametrize("reduction", [None, "mean", "sum"]) -def test_constructor(reduction): +# Test cases for the SystemEquation class +eq_list1 = [eq1, eq2] +eq_list2 = [FixedValue(value=0.0), FixedGradient(value=0.0, components=["u2"])] +eq_list3 = [FixedValue(value=0.0, components=["u1"]), eq1] - # Constructor with callable functions - SystemEquation([eq1, eq2], reduction=reduction) - # Constructor with Equation instances - SystemEquation( - [ - FixedValue(value=0.0, components=["u1"]), - FixedGradient(value=0.0, components=["u2"]), - ], - reduction=reduction, - ) +@pytest.mark.parametrize("eq_list", [eq_list1, eq_list2, eq_list3]) +@pytest.mark.parametrize("reduction", [None, "mean", "sum", reduction_fn]) +def test_constructor(eq_list, reduction): - # Constructor with mixed types - SystemEquation( - [ - FixedValue(value=0.0, components=["u1"]), - eq1, - ], - reduction=reduction, - ) + SystemEquation(list_equation=eq_list, reduction=reduction) - # Non-standard reduction not implemented - with pytest.raises(NotImplementedError): - SystemEquation([eq1, eq2], reduction="foo") + # Should fail if the list of equations is not a list + with pytest.raises(ValueError): + SystemEquation(list_equation=eq1, reduction=reduction) - # Invalid input type + # Should fail if any element of the list is neither callable nor Equation with pytest.raises(ValueError): - SystemEquation(foo) + SystemEquation(list_equation=[eq1, "equation"], reduction=reduction) + # Should fail if the reduction is not available + with pytest.raises(ValueError): + SystemEquation(list_equation=[eq1, eq2], reduction="foo") -@pytest.mark.parametrize("reduction", [None, "mean", "sum"]) -def test_residual(reduction): - # Generate random points and output - pts = LabelTensor(torch.rand(10, 2), labels=["x", "y"]) - pts.requires_grad = True - u = torch.pow(pts, 2) - u.labels = ["u1", "u2"] +@pytest.mark.parametrize("reduction", [None, "mean", "sum", reduction_fn]) +@pytest.mark.parametrize( + "eq_list, last_dim", + [(eq_list1, 3), (eq_list2, 4), (eq_list3, 3)], +) +def test_residual(eq_list, last_dim, reduction): - # System with callable functions - system_eq = SystemEquation([eq1, eq2], reduction=reduction) - res = system_eq.residual(pts, u) + # Define the system of equations + system_eq = SystemEquation(list_equation=eq_list, reduction=reduction) - # Checks on the shape of the residual - shape = torch.Size([10, 3]) if reduction is None else torch.Size([10]) - assert res.shape == shape + # Manage number of points and variables + n_pts = 10 + input_vars = ["x", "y"] + output_vars = ["u1", "u2"] - # System with Equation instances - system_eq = SystemEquation( - [ - FixedValue(value=0.0, components=["u1"]), - FixedGradient(value=0.0, components=["u2"]), - ], - reduction=reduction, + # Define the input and output tensors + pts = LabelTensor( + torch.rand(n_pts, len(input_vars), requires_grad=True), + labels=input_vars, ) + u = torch.pow(pts, 2) + u.labels = output_vars - # Checks on the shape of the residual - shape = torch.Size([10, 3]) if reduction is None else torch.Size([10]) - assert res.shape == shape - - # System with mixed types - system_eq = SystemEquation( - [ - FixedValue(value=0.0, components=["u1"]), - eq1, - ], - reduction=reduction, + # Compute the residuals and check the shape + res = system_eq.residual(pts, u) + shape = ( + torch.Size([n_pts, last_dim]) + if reduction is None + else torch.Size([n_pts]) ) - - # Checks on the shape of the residual - shape = torch.Size([10, 3]) if reduction is None else torch.Size([10]) assert res.shape == shape diff --git a/tests/test_equation_zoo/test_acoustic_wave_equation.py b/tests/test_equation_zoo/test_acoustic_wave_equation.py new file mode 100644 index 000000000..7d4c3b3a8 --- /dev/null +++ b/tests/test_equation_zoo/test_acoustic_wave_equation.py @@ -0,0 +1,29 @@ +import pytest +import torch +from pina import LabelTensor +from pina.equation.zoo import AcousticWaveEquation + + +# Define input and output values +pts = LabelTensor(torch.rand(10, 3, requires_grad=True), labels=["x", "y", "t"]) +u = torch.pow(pts, 2) +u.labels = ["u", "v", "w"] + + +@pytest.mark.parametrize("c", [1.0, 10, -7.5]) +def test_acoustic_wave_equation(c): + + # Constructor + equation = AcousticWaveEquation(c=c) + + # Should fail if c is not a float or int + with pytest.raises(ValueError): + AcousticWaveEquation(c="invalid") + + # Residual + residual = equation.residual(pts, u) + assert residual.shape == u.shape + + # Should fail if the input has no 't' label + with pytest.raises(ValueError): + residual = equation.residual(pts["x", "y"], u) diff --git a/tests/test_equation_zoo/test_advection_equation.py b/tests/test_equation_zoo/test_advection_equation.py new file mode 100644 index 000000000..d78aef106 --- /dev/null +++ b/tests/test_equation_zoo/test_advection_equation.py @@ -0,0 +1,38 @@ +import pytest +import torch +from pina import LabelTensor +from pina.equation.zoo import AdvectionEquation + + +# Define input and output values +pts = LabelTensor(torch.rand(10, 3, requires_grad=True), labels=["x", "y", "t"]) +u = torch.pow(pts, 2) +u.labels = ["u", "v", "w"] + + +@pytest.mark.parametrize("c", [1.0, 10, [1, 2.5]]) +def test_advection_equation(c): + + # Constructor + equation = AdvectionEquation(c) + + # Should fail if c is an empty list + with pytest.raises(ValueError): + AdvectionEquation([]) + + # Should fail if c is not a float, int, or list + with pytest.raises(ValueError): + AdvectionEquation("invalid") + + # Residual + residual = equation.residual(pts, u) + assert residual.shape == u.shape + + # Should fail if the input has no 't' label + with pytest.raises(ValueError): + residual = equation.residual(pts["x", "y"], u) + + # Should fail if c is a list and its length != spatial dimension + with pytest.raises(ValueError): + equation = AdvectionEquation([1, 2, 3]) + residual = equation.residual(pts, u) diff --git a/tests/test_equation_zoo/test_allen_cahn_equation.py b/tests/test_equation_zoo/test_allen_cahn_equation.py new file mode 100644 index 000000000..7f9bf23f2 --- /dev/null +++ b/tests/test_equation_zoo/test_allen_cahn_equation.py @@ -0,0 +1,34 @@ +import pytest +import torch +from pina import LabelTensor +from pina.equation.zoo import AllenCahnEquation + + +# Define input and output values +pts = LabelTensor(torch.rand(10, 3, requires_grad=True), labels=["x", "y", "t"]) +u = torch.pow(pts, 2) +u.labels = ["u", "v", "w"] + + +@pytest.mark.parametrize("alpha", [1.0, 10, -7.5]) +@pytest.mark.parametrize("beta", [1.0, 10, -7.5]) +def test_allen_cahn_equation(alpha, beta): + + # Constructor + equation = AllenCahnEquation(alpha=alpha, beta=beta) + + # Should fail if alpha is not a float or int + with pytest.raises(ValueError): + AllenCahnEquation(alpha="invalid", beta=beta) + + # Should fail if beta is not a float or int + with pytest.raises(ValueError): + AllenCahnEquation(alpha=alpha, beta="invalid") + + # Residual + residual = equation.residual(pts, u) + assert residual.shape == u.shape + + # Should fail if the input has no 't' label + with pytest.raises(ValueError): + residual = equation.residual(pts["x", "y"], u) diff --git a/tests/test_equation_zoo/test_diffusion_reaction_equation.py b/tests/test_equation_zoo/test_diffusion_reaction_equation.py new file mode 100644 index 000000000..ae2d7fc51 --- /dev/null +++ b/tests/test_equation_zoo/test_diffusion_reaction_equation.py @@ -0,0 +1,36 @@ +import pytest +import torch +from pina import LabelTensor +from pina.equation.zoo import DiffusionReactionEquation + + +# Define input and output values +pts = LabelTensor(torch.rand(10, 3, requires_grad=True), labels=["x", "y", "t"]) +u = torch.pow(pts, 2) +u.labels = ["u", "v", "w"] + + +@pytest.mark.parametrize("alpha", [1.0, 10, -7.5]) +@pytest.mark.parametrize( + "forcing_term", [lambda x: torch.sin(x), lambda x: torch.exp(x)] +) +def test_diffusion_reaction_equation(alpha, forcing_term): + + # Constructor + equation = DiffusionReactionEquation(alpha=alpha, forcing_term=forcing_term) + + # Should fail if alpha is not a float or int + with pytest.raises(ValueError): + DiffusionReactionEquation(alpha="invalid", forcing_term=forcing_term) + + # Should fail if forcing_term is not a callable + with pytest.raises(ValueError): + DiffusionReactionEquation(alpha=alpha, forcing_term="invalid") + + # Residual + residual = equation.residual(pts, u) + assert residual.shape == u.shape + + # Should fail if the input has no 't' label + with pytest.raises(ValueError): + residual = equation.residual(pts["x", "y"], u) diff --git a/tests/test_equation_zoo/test_helmholtz_equation.py b/tests/test_equation_zoo/test_helmholtz_equation.py new file mode 100644 index 000000000..34b1a8a9f --- /dev/null +++ b/tests/test_equation_zoo/test_helmholtz_equation.py @@ -0,0 +1,32 @@ +import pytest +import torch +from pina import LabelTensor +from pina.equation.zoo import HelmholtzEquation + + +# Define input and output values +pts = LabelTensor(torch.rand(10, 3, requires_grad=True), labels=["x", "y", "t"]) +u = torch.pow(pts, 2) +u.labels = ["u", "v", "w"] + + +@pytest.mark.parametrize("k", [1.0, 10, -7.5]) +@pytest.mark.parametrize( + "forcing_term", [lambda x: torch.sin(x), lambda x: torch.exp(x)] +) +def test_helmholtz_equation(k, forcing_term): + + # Constructor + equation = HelmholtzEquation(k=k, forcing_term=forcing_term) + + # Should fail if k is not a float or int + with pytest.raises(ValueError): + HelmholtzEquation(k="invalid", forcing_term=forcing_term) + + # Should fail if forcing_term is not a callable + with pytest.raises(ValueError): + HelmholtzEquation(k=k, forcing_term="invalid") + + # Residual + residual = equation.residual(pts, u) + assert residual.shape == u.shape diff --git a/tests/test_equation_zoo/test_poisson_equation.py b/tests/test_equation_zoo/test_poisson_equation.py new file mode 100644 index 000000000..b56a69073 --- /dev/null +++ b/tests/test_equation_zoo/test_poisson_equation.py @@ -0,0 +1,27 @@ +import pytest +import torch +from pina import LabelTensor +from pina.equation.zoo import PoissonEquation + + +# Define input and output values +pts = LabelTensor(torch.rand(10, 3, requires_grad=True), labels=["x", "y", "t"]) +u = torch.pow(pts, 2) +u.labels = ["u", "v", "w"] + + +@pytest.mark.parametrize( + "forcing_term", [lambda x: torch.sin(x), lambda x: torch.exp(x)] +) +def test_poisson_equation(forcing_term): + + # Constructor + equation = PoissonEquation(forcing_term=forcing_term) + + # Should fail if forcing_term is not a callable + with pytest.raises(ValueError): + PoissonEquation(forcing_term="invalid") + + # Residual + residual = equation.residual(pts, u) + assert residual.shape == u.shape diff --git a/tests/test_problem.py b/tests/test_problem.py deleted file mode 100644 index 53ee3bc57..000000000 --- a/tests/test_problem.py +++ /dev/null @@ -1,94 +0,0 @@ -import torch -import pytest -from pina.problem.zoo import Poisson2DSquareProblem as Poisson -from pina import LabelTensor -from pina.domain import Union, CartesianDomain, EllipsoidDomain -from pina.condition import ( - Condition, - InputTargetCondition, - DomainEquationCondition, -) - - -def test_discretise_domain(): - n = 10 - poisson_problem = Poisson() - - poisson_problem.discretise_domain(n, "grid", domains="boundary") - assert poisson_problem.discretised_domains["boundary"].shape[0] == n - - poisson_problem.discretise_domain(n, "random", domains="boundary") - assert poisson_problem.discretised_domains["boundary"].shape[0] == n - - poisson_problem.discretise_domain(n, "grid", domains=["D"]) - assert poisson_problem.discretised_domains["D"].shape[0] == n**2 - - poisson_problem.discretise_domain(n, "random", domains=["D"]) - assert poisson_problem.discretised_domains["D"].shape[0] == n - - poisson_problem.discretise_domain(n, "latin", domains=["D"]) - assert poisson_problem.discretised_domains["D"].shape[0] == n - - poisson_problem.discretise_domain(n, "lh", domains=["D"]) - assert poisson_problem.discretised_domains["D"].shape[0] == n - - poisson_problem.discretise_domain(n) - - -def test_variables_correct_order_sampling(): - n = 10 - poisson_problem = Poisson() - poisson_problem.discretise_domain(n, "grid", domains=["D"]) - assert poisson_problem.discretised_domains["D"].labels == sorted( - poisson_problem.input_variables - ) - - poisson_problem.discretise_domain(n, "grid", domains=["D"]) - assert poisson_problem.discretised_domains["D"].labels == sorted( - poisson_problem.input_variables - ) - - -def test_add_points(): - poisson_problem = Poisson() - poisson_problem.discretise_domain(1, "random", domains=["D"]) - new_pts = LabelTensor(torch.tensor([[0.5, -0.5]]), labels=["x", "y"]) - poisson_problem.add_points({"D": new_pts}) - assert torch.allclose( - poisson_problem.discretised_domains["D"]["x"][-1], - new_pts["x"], - ) - assert torch.allclose( - poisson_problem.discretised_domains["D"]["y"][-1], - new_pts["y"], - ) - - -@pytest.mark.parametrize("mode", ["random", "grid"]) -def test_custom_sampling_logic(mode): - poisson_problem = Poisson() - sampling_rules = { - "x": {"n": 100, "mode": mode}, - "y": {"n": 50, "mode": mode}, - } - poisson_problem.discretise_domain(sample_rules=sampling_rules, domains="D") - assert poisson_problem.discretised_domains["D"].shape[0] == 100 * 50 - assert poisson_problem.discretised_domains["D"].labels == ["x", "y"] - - -@pytest.mark.parametrize("mode", ["random", "grid"]) -def test_wrong_custom_sampling_logic(mode): - d2 = CartesianDomain({"x": [1, 2], "y": [0, 1]}) - poisson_problem = Poisson() - poisson_problem.domains["D"] = Union([poisson_problem.domains["D"], d2]) - sampling_rules = { - "x": {"n": 100, "mode": mode}, - "y": {"n": 50, "mode": mode}, - } - with pytest.raises(RuntimeError): - poisson_problem.domains["new"] = EllipsoidDomain({"x": [0, 1]}) - poisson_problem.discretise_domain(sample_rules=sampling_rules) - - # Necessary cleanup - if "new" in poisson_problem.domains: - del poisson_problem.domains["new"] diff --git a/tests/test_problem/test_abstract_problem.py b/tests/test_problem/test_abstract_problem.py new file mode 100644 index 000000000..25acfcadd --- /dev/null +++ b/tests/test_problem/test_abstract_problem.py @@ -0,0 +1,158 @@ +import torch +import pytest +from pina import LabelTensor +from pina.problem.zoo import Poisson2DSquareProblem as Poisson + + +# Define sampling rules +rule1 = { + "x": {"n": 10, "mode": "random"}, + "y": {"n": 5, "mode": "grid"}, +} +rule2 = { + "x": {"n": 5, "mode": "lh"}, + "y": {"n": 10, "mode": "chebyshev"}, +} + + +@pytest.mark.parametrize("n", [2, 5]) +@pytest.mark.parametrize("mode", ["grid", "random", "latin", "chebyshev", "lh"]) +@pytest.mark.parametrize("domains", ["boundary", "D", ["boundary", "D"], None]) +@pytest.mark.parametrize("sample_rules", [None, rule1, rule2]) +def test_discretise_domain(n, mode, domains, sample_rules): + + # Define the problem + poisson_problem = Poisson() + + # Discretise domains + poisson_problem.discretise_domain( + n=n, mode=mode, domains=domains, sample_rules=sample_rules + ) + + # Transform domains to list for consistent processing + _as_list = lambda x: [x] if isinstance(x, str) else x + d_list = domains if domains is not None else ["boundary", "D"] + d_list = _as_list(d_list) + + # Check that the discretised domains have the expected number of points + for d in d_list: + + # Compute expected number of points if sample rules are provided + if sample_rules is not None: + n_tot = sample_rules["x"]["n"] * sample_rules["y"]["n"] + + # Otherwise, expect n pts or n^2 based on domain and mode + else: + n_tot = n**2 if mode in ["grid", "chebyshev"] and d == "D" else n + + # Check that the number of samples matches the expected number + assert poisson_problem.discretised_domains[d].shape[0] == n_tot + + # Check labels of the discretised domains + assert poisson_problem.discretised_domains[d].labels == sorted( + poisson_problem.input_variables + ) + + # Should fail if n is not a positive integer when sample rules not provided + if sample_rules is None: + with pytest.raises(AssertionError): + poisson_problem.discretise_domain( + n=-1, mode=mode, domains=domains, sample_rules=sample_rules + ) + + # Should fail if mode is not a string + with pytest.raises(ValueError): + poisson_problem.discretise_domain( + n=n, mode=123, domains=domains, sample_rules=sample_rules + ) + + # Should fail if domains is not a string or a list of strings + with pytest.raises(ValueError): + poisson_problem.discretise_domain( + n=n, mode=mode, domains=123, sample_rules=sample_rules + ) + + # Should fail if sample rules is not a dictionary + with pytest.raises(ValueError): + poisson_problem.discretise_domain( + n=n, mode=mode, domains=domains, sample_rules="not_a_dict" + ) + + # Should fail if the keys of sample rules do not match the input variables + with pytest.raises(ValueError): + wrong_sample_rules = {"wrong_var": {"n": 10, "mode": "random"}} + poisson_problem.discretise_domain( + n=n, mode=mode, domains=domains, sample_rules=wrong_sample_rules + ) + + # Should fail if the rules do not contain both 'n' and 'mode' keys + with pytest.raises(ValueError): + incomplete_sample_rules = {"x": {"n": 10}, "y": {"mode": "random"}} + poisson_problem.discretise_domain( + n=n, + mode=mode, + domains=domains, + sample_rules=incomplete_sample_rules, + ) + + +@pytest.mark.parametrize("domains", ["boundary", "D", ["boundary", "D"], None]) +def test_add_points(domains): + + # Store initial number of points in the domains and point to add + n_init, n_add = 5, 3 + n_tot = n_init + n_add + + # Define the problem and discretise the domain + poisson_problem = Poisson() + poisson_problem.discretise_domain(n=n_init, mode="random", domains=domains) + vars = poisson_problem.input_variables + + # Transform domains to list for consistent processing + _as_list = lambda x: [x] if isinstance(x, str) else x + d_list = domains if domains is not None else ["boundary", "D"] + d_list = _as_list(d_list) + + # Iterate over the domains and add points to each + for d in d_list: + + # Add new points to the domain + new_pts = LabelTensor(torch.rand(n_add, len(vars)), labels=vars) + poisson_problem.add_points({d: new_pts}) + + # Assert that the number of points in the domain is correct + assert poisson_problem.discretised_domains[d].shape[0] == n_tot + + # Assert that the new points are in the domain + assert torch.allclose( + poisson_problem.discretised_domains[d]["x"][-n_add:], new_pts["x"] + ) + assert torch.allclose( + poisson_problem.discretised_domains[d]["y"][-n_add:], new_pts["y"] + ) + + # Should fail if new points is not a dictionary + with pytest.raises(ValueError): + poisson_problem.add_points("not_a_dict") + + # Should fail if any of the values in new points is not a LabelTensor + with pytest.raises(ValueError): + poisson_problem.add_points({d_list[0]: torch.rand(n_add, len(vars))}) + + # Should fail if any of the keys does not match any of the existing domains + with pytest.raises(ValueError): + poisson_problem.add_points( + { + "not_a_domain": LabelTensor( + torch.rand(n_add, len(vars)), labels=vars + ) + } + ) + + # Should fail if any of the domains has not been discretised yet + with pytest.raises(ValueError): + poisson_problem = Poisson() + poisson_problem.discretise_domain(n=n_init, mode="random", domains="D") + poisson_problem.add_points( + {"boundary": LabelTensor(torch.rand(n_add, len(vars)), labels=vars)} + ) diff --git a/tests/test_problem/test_inverse_problem.py b/tests/test_problem/test_inverse_problem.py new file mode 100644 index 000000000..8a91cbac0 --- /dev/null +++ b/tests/test_problem/test_inverse_problem.py @@ -0,0 +1,27 @@ +import torch +from pina.problem import InverseProblem +from pina.domain import CartesianDomain + + +# Dummy inverse problem for testing +class DummyInverseProblem(InverseProblem): + + output_variables = ["u"] + conditions = {} + + # Define the unknown parameter domain + unknown_parameter_domain = CartesianDomain({"mu": [-1, 1]}) + + +def test_inverse_problem_initialization(): + + # Initialize the dummy inverse problem + problem = DummyInverseProblem() + + # Check that the inverse problem is initialized correctly + assert problem.unknown_variables == ["mu"] + assert isinstance(problem.unknown_parameters, dict) + for k, v in problem.unknown_parameters.items(): + assert isinstance(v, torch.nn.Parameter) + range_low, range_high = problem.unknown_parameter_domain._range[k] + assert range_low <= v.item() <= range_high diff --git a/tests/test_problem/test_parametric_problem.py b/tests/test_problem/test_parametric_problem.py new file mode 100644 index 000000000..00c4568e8 --- /dev/null +++ b/tests/test_problem/test_parametric_problem.py @@ -0,0 +1,22 @@ +from pina.problem import ParametricProblem +from pina.domain import CartesianDomain + + +# Dummy parametric problem for testing +class DummyParametricProblem(ParametricProblem): + + output_variables = ["u"] + conditions = {} + + # Define the parameter domain + parameter_domain = CartesianDomain({"mu": [-1, 1]}) + + +def test_parametric_problem_initialization(): + + # Initialize the dummy parametric problem + problem = DummyParametricProblem() + + # Check that the parametric problem is initialized correctly + assert problem.parameters == ["mu"] + assert problem.input_variables == problem.parameters diff --git a/tests/test_problem/test_spatial_problem.py b/tests/test_problem/test_spatial_problem.py new file mode 100644 index 000000000..4848db018 --- /dev/null +++ b/tests/test_problem/test_spatial_problem.py @@ -0,0 +1,22 @@ +from pina.problem import SpatialProblem +from pina.domain import CartesianDomain + + +# Dummy spatial problem for testing +class DummySpatialProblem(SpatialProblem): + + output_variables = ["u"] + conditions = {} + + # Define the spatial domain + spatial_domain = CartesianDomain({"x": [-1, 1]}) + + +def test_spatial_problem_initialization(): + + # Initialize the dummy spatial problem + problem = DummySpatialProblem() + + # Check that the spatial problem is initialized correctly + assert problem.spatial_variables == ["x"] + assert problem.input_variables == problem.spatial_variables diff --git a/tests/test_problem/test_time_dependent_problem.py b/tests/test_problem/test_time_dependent_problem.py new file mode 100644 index 000000000..e041f507b --- /dev/null +++ b/tests/test_problem/test_time_dependent_problem.py @@ -0,0 +1,22 @@ +from pina.problem import TimeDependentProblem +from pina.domain import CartesianDomain + + +# Dummy time-dependent problem for testing +class DummyTimeDependentProblem(TimeDependentProblem): + + output_variables = ["u"] + conditions = {} + + # Define the temporal domain + temporal_domain = CartesianDomain({"t": [0, 1]}) + + +def test_time_dependent_problem_initialization(): + + # Initialize the dummy time-dependent problem + problem = DummyTimeDependentProblem() + + # Check that the time-dependent problem is initialized correctly + assert problem.temporal_variables == ["t"] + assert problem.input_variables == problem.temporal_variables diff --git a/tests/test_problem_zoo/test_acoustic_wave.py b/tests/test_problem_zoo/test_acoustic_wave.py deleted file mode 100644 index 0cf794d18..000000000 --- a/tests/test_problem_zoo/test_acoustic_wave.py +++ /dev/null @@ -1,19 +0,0 @@ -import pytest -from pina.problem.zoo import AcousticWaveProblem -from pina.problem import SpatialProblem, TimeDependentProblem - - -@pytest.mark.parametrize("c", [0.1, 1]) -def test_constructor(c): - - problem = AcousticWaveProblem(c=c) - problem.discretise_domain(n=10, mode="random", domains="all") - assert problem.are_all_domains_discretised - assert isinstance(problem, SpatialProblem) - assert isinstance(problem, TimeDependentProblem) - assert hasattr(problem, "conditions") - assert isinstance(problem.conditions, dict) - - # Should fail if c is not a float or int - with pytest.raises(ValueError): - AcousticWaveProblem(c="invalid") diff --git a/tests/test_problem_zoo/test_acoustic_wave_problem.py b/tests/test_problem_zoo/test_acoustic_wave_problem.py new file mode 100644 index 000000000..a5102efae --- /dev/null +++ b/tests/test_problem_zoo/test_acoustic_wave_problem.py @@ -0,0 +1,36 @@ +import pytest +import torch +from pina.problem.zoo import AcousticWaveProblem +from pina.problem import SpatialProblem, TimeDependentProblem + + +@pytest.mark.parametrize("c", [0.1, 1]) +def test_constructor(c): + + problem = AcousticWaveProblem(c=c) + problem.discretise_domain(n=10, mode="random", domains=None) + assert problem.are_all_domains_discretised + assert isinstance(problem, SpatialProblem) + assert isinstance(problem, TimeDependentProblem) + assert hasattr(problem, "conditions") + assert isinstance(problem.conditions, dict) + + # Should fail if c is not a float or int + with pytest.raises(ValueError): + AcousticWaveProblem(c="invalid") + + +@pytest.mark.parametrize("c", [0.1, 1]) +def test_solution(c): + + # Find the solution to the problem + problem = AcousticWaveProblem(c=c) + problem.discretise_domain(n=10, mode="grid", domains=None) + pts = problem.discretised_domains["D"] + solution = problem.solution(pts.requires_grad_()) + + # Compute the residual + residual = problem.conditions["D"].equation.residual(pts, solution).tensor + + # Assert the residual of the PDE is close to zero + assert torch.allclose(residual, torch.zeros_like(residual), atol=5e-5) diff --git a/tests/test_problem_zoo/test_advection.py b/tests/test_problem_zoo/test_advection.py deleted file mode 100644 index e1a656a74..000000000 --- a/tests/test_problem_zoo/test_advection.py +++ /dev/null @@ -1,19 +0,0 @@ -import pytest -from pina.problem.zoo import AdvectionProblem -from pina.problem import SpatialProblem, TimeDependentProblem - - -@pytest.mark.parametrize("c", [1.5, 3]) -def test_constructor(c): - - problem = AdvectionProblem(c=c) - problem.discretise_domain(n=10, mode="random", domains="all") - assert problem.are_all_domains_discretised - assert isinstance(problem, SpatialProblem) - assert isinstance(problem, TimeDependentProblem) - assert hasattr(problem, "conditions") - assert isinstance(problem.conditions, dict) - - # Should fail if c is not a float or int - with pytest.raises(ValueError): - AdvectionProblem(c="invalid") diff --git a/tests/test_problem_zoo/test_advection_problem.py b/tests/test_problem_zoo/test_advection_problem.py new file mode 100644 index 000000000..0d7114771 --- /dev/null +++ b/tests/test_problem_zoo/test_advection_problem.py @@ -0,0 +1,36 @@ +import pytest +import torch +from pina.problem.zoo import AdvectionProblem +from pina.problem import SpatialProblem, TimeDependentProblem + + +@pytest.mark.parametrize("c", [1.5, 3]) +def test_constructor(c): + + problem = AdvectionProblem(c=c) + problem.discretise_domain(n=10, mode="random", domains=None) + assert problem.are_all_domains_discretised + assert isinstance(problem, SpatialProblem) + assert isinstance(problem, TimeDependentProblem) + assert hasattr(problem, "conditions") + assert isinstance(problem.conditions, dict) + + # Should fail if c is not a float or int + with pytest.raises(ValueError): + AdvectionProblem(c="invalid") + + +@pytest.mark.parametrize("c", [1.5, 3]) +def test_solution(c): + + # Find the solution to the problem + problem = AdvectionProblem(c=c) + problem.discretise_domain(n=10, mode="grid", domains=None) + pts = problem.discretised_domains["D"] + solution = problem.solution(pts.requires_grad_()) + + # Compute the residual + residual = problem.conditions["D"].equation.residual(pts, solution).tensor + + # Assert the residual of the PDE is close to zero + assert torch.allclose(residual, torch.zeros_like(residual), atol=5e-5) diff --git a/tests/test_problem_zoo/test_allen_cahn.py b/tests/test_problem_zoo/test_allen_cahn_problem.py similarity index 92% rename from tests/test_problem_zoo/test_allen_cahn.py rename to tests/test_problem_zoo/test_allen_cahn_problem.py index 80c11ce5c..2406e1f75 100644 --- a/tests/test_problem_zoo/test_allen_cahn.py +++ b/tests/test_problem_zoo/test_allen_cahn_problem.py @@ -8,7 +8,7 @@ def test_constructor(alpha, beta): problem = AllenCahnProblem(alpha=alpha, beta=beta) - problem.discretise_domain(n=10, mode="random", domains="all") + problem.discretise_domain(n=10, mode="random", domains=None) assert problem.are_all_domains_discretised assert isinstance(problem, SpatialProblem) assert isinstance(problem, TimeDependentProblem) diff --git a/tests/test_problem_zoo/test_diffusion_reaction.py b/tests/test_problem_zoo/test_diffusion_reaction_problem.py similarity index 50% rename from tests/test_problem_zoo/test_diffusion_reaction.py rename to tests/test_problem_zoo/test_diffusion_reaction_problem.py index 163d30f55..d8decf697 100644 --- a/tests/test_problem_zoo/test_diffusion_reaction.py +++ b/tests/test_problem_zoo/test_diffusion_reaction_problem.py @@ -1,4 +1,5 @@ import pytest +import torch from pina.problem.zoo import DiffusionReactionProblem from pina.problem import TimeDependentProblem, SpatialProblem @@ -7,7 +8,7 @@ def test_constructor(alpha): problem = DiffusionReactionProblem(alpha=alpha) - problem.discretise_domain(n=10, mode="random", domains="all") + problem.discretise_domain(n=10, mode="random", domains=None) assert problem.are_all_domains_discretised assert isinstance(problem, TimeDependentProblem) assert isinstance(problem, SpatialProblem) @@ -17,3 +18,19 @@ def test_constructor(alpha): # Should fail if alpha is not a float or int with pytest.raises(ValueError): problem = DiffusionReactionProblem(alpha="invalid") + + +@pytest.mark.parametrize("alpha", [0.1, 1]) +def test_solution(alpha): + + # Find the solution to the problem + problem = DiffusionReactionProblem(alpha=alpha) + problem.discretise_domain(n=10, mode="grid", domains=None) + pts = problem.discretised_domains["D"] + solution = problem.solution(pts.requires_grad_()) + + # Compute the residual + residual = problem.conditions["D"].equation.residual(pts, solution).tensor + + # Assert the residual of the PDE is close to zero + assert torch.allclose(residual, torch.zeros_like(residual), atol=5e-5) diff --git a/tests/test_problem_zoo/test_helmholtz.py b/tests/test_problem_zoo/test_helmholtz.py deleted file mode 100644 index 4668c6996..000000000 --- a/tests/test_problem_zoo/test_helmholtz.py +++ /dev/null @@ -1,19 +0,0 @@ -import pytest -from pina.problem.zoo import HelmholtzProblem -from pina.problem import SpatialProblem - - -@pytest.mark.parametrize("k", [1.5, 3]) -@pytest.mark.parametrize("alpha_x", [1, 3]) -@pytest.mark.parametrize("alpha_y", [1, 3]) -def test_constructor(k, alpha_x, alpha_y): - - problem = HelmholtzProblem(k=k, alpha_x=alpha_x, alpha_y=alpha_y) - problem.discretise_domain(n=10, mode="random", domains="all") - assert problem.are_all_domains_discretised - assert isinstance(problem, SpatialProblem) - assert hasattr(problem, "conditions") - assert isinstance(problem.conditions, dict) - - with pytest.raises(ValueError): - HelmholtzProblem(k=1, alpha_x=1.5, alpha_y=1) diff --git a/tests/test_problem_zoo/test_helmholtz_problem.py b/tests/test_problem_zoo/test_helmholtz_problem.py new file mode 100644 index 000000000..408e32a33 --- /dev/null +++ b/tests/test_problem_zoo/test_helmholtz_problem.py @@ -0,0 +1,38 @@ +import pytest +import torch +from pina.problem.zoo import HelmholtzProblem +from pina.problem import SpatialProblem + + +@pytest.mark.parametrize("k", [1.5, 3]) +@pytest.mark.parametrize("alpha_x", [1, 3]) +@pytest.mark.parametrize("alpha_y", [1, 3]) +def test_constructor(k, alpha_x, alpha_y): + + problem = HelmholtzProblem(k=k, alpha_x=alpha_x, alpha_y=alpha_y) + problem.discretise_domain(n=10, mode="random", domains=None) + assert problem.are_all_domains_discretised + assert isinstance(problem, SpatialProblem) + assert hasattr(problem, "conditions") + assert isinstance(problem.conditions, dict) + + with pytest.raises(ValueError): + HelmholtzProblem(k=1, alpha_x=1.5, alpha_y=1) + + +@pytest.mark.parametrize("k", [1.5, 3]) +@pytest.mark.parametrize("alpha_x", [1, 3]) +@pytest.mark.parametrize("alpha_y", [1, 3]) +def test_solution(k, alpha_x, alpha_y): + + # Find the solution to the problem + problem = HelmholtzProblem(k=k, alpha_x=alpha_x, alpha_y=alpha_y) + problem.discretise_domain(n=10, mode="grid", domains=None) + pts = problem.discretised_domains["D"] + solution = problem.solution(pts.requires_grad_()) + + # Compute the residual + residual = problem.conditions["D"].equation.residual(pts, solution).tensor + + # Assert the residual of the PDE is close to zero + assert torch.allclose(residual, torch.zeros_like(residual), atol=5e-5) diff --git a/tests/test_problem_zoo/test_inverse_poisson_2d_square.py b/tests/test_problem_zoo/test_inverse_poisson_problem.py similarity index 93% rename from tests/test_problem_zoo/test_inverse_poisson_2d_square.py rename to tests/test_problem_zoo/test_inverse_poisson_problem.py index 423d15d74..25af3ae9e 100644 --- a/tests/test_problem_zoo/test_inverse_poisson_2d_square.py +++ b/tests/test_problem_zoo/test_inverse_poisson_problem.py @@ -11,7 +11,7 @@ def test_constructor(load, data_size): problem = InversePoisson2DSquareProblem(load=load, data_size=data_size) # Discretise the domain - problem.discretise_domain(n=10, mode="random", domains="all") + problem.discretise_domain(n=10, mode="random", domains=None) # Check if the problem is correctly set up assert problem.are_all_domains_discretised diff --git a/tests/test_problem_zoo/test_poisson_2d_square.py b/tests/test_problem_zoo/test_poisson_2d_square.py deleted file mode 100644 index a9e6fa973..000000000 --- a/tests/test_problem_zoo/test_poisson_2d_square.py +++ /dev/null @@ -1,12 +0,0 @@ -from pina.problem.zoo import Poisson2DSquareProblem -from pina.problem import SpatialProblem - - -def test_constructor(): - - problem = Poisson2DSquareProblem() - problem.discretise_domain(n=10, mode="random", domains="all") - assert problem.are_all_domains_discretised - assert isinstance(problem, SpatialProblem) - assert hasattr(problem, "conditions") - assert isinstance(problem.conditions, dict) diff --git a/tests/test_problem_zoo/test_poisson_problem.py b/tests/test_problem_zoo/test_poisson_problem.py new file mode 100644 index 000000000..b093329bd --- /dev/null +++ b/tests/test_problem_zoo/test_poisson_problem.py @@ -0,0 +1,28 @@ +import torch +from pina.problem.zoo import Poisson2DSquareProblem +from pina.problem import SpatialProblem + + +def test_constructor(): + + problem = Poisson2DSquareProblem() + problem.discretise_domain(n=10, mode="random", domains=None) + assert problem.are_all_domains_discretised + assert isinstance(problem, SpatialProblem) + assert hasattr(problem, "conditions") + assert isinstance(problem.conditions, dict) + + +def test_solution(): + + # Find the solution to the problem + problem = Poisson2DSquareProblem() + problem.discretise_domain(n=10, mode="grid", domains=None) + pts = problem.discretised_domains["D"] + solution = problem.solution(pts.requires_grad_()) + + # Compute the residual + residual = problem.conditions["D"].equation.residual(pts, solution).tensor + + # Assert the residual of the PDE is close to zero + assert torch.allclose(residual, torch.zeros_like(residual), atol=5e-5)