From 76d396d83510ce0c17dfa752e6cd9900ca72c8d9 Mon Sep 17 00:00:00 2001 From: Dario Coscia Date: Thu, 3 Oct 2024 21:33:37 +0200 Subject: [PATCH 01/14] new conditions --- pina/condition/__init__.py | 10 +- pina/condition/condition.py | 119 +++++++------------- pina/condition/condition_interface.py | 34 +++--- pina/condition/data_condition.py | 44 ++++++++ pina/condition/domain_equation_condition.py | 45 +++++--- pina/condition/domain_output_condition.py | 44 -------- pina/condition/input_equation_condition.py | 43 +++++-- pina/condition/input_output_condition.py | 42 +++++++ 8 files changed, 210 insertions(+), 171 deletions(-) create mode 100644 pina/condition/data_condition.py delete mode 100644 pina/condition/domain_output_condition.py create mode 100644 pina/condition/input_output_condition.py diff --git a/pina/condition/__init__.py b/pina/condition/__init__.py index ff329c1bc..4c89b75d1 100644 --- a/pina/condition/__init__.py +++ b/pina/condition/__init__.py @@ -1,10 +1,12 @@ __all__ = [ 'Condition', 'ConditionInterface', - 'DomainOutputCondition', - 'DomainEquationCondition' + 'DomainEquationCondition', + 'InputPointsEquationCondition', + 'InputOutputPointsCondition', ] from .condition_interface import ConditionInterface -from .domain_output_condition import DomainOutputCondition -from .domain_equation_condition import DomainEquationCondition \ No newline at end of file +from .domain_equation_condition import DomainEquationCondition +from .input_equation_condition import InputPointsEquationCondition +from .input_output_condition import InputOutputPointsCondition \ No newline at end of file diff --git a/pina/condition/condition.py b/pina/condition/condition.py index d815838bb..ddc722f0c 100644 --- a/pina/condition/condition.py +++ b/pina/condition/condition.py @@ -1,27 +1,21 @@ """ Condition module. """ -from ..label_tensor import LabelTensor -from ..domain import DomainInterface -from ..equation.equation import Equation - -from . import DomainOutputCondition, DomainEquationCondition - - -def dummy(a): - """Dummy function for testing purposes.""" - return None - +from .domain_equation_condition import DomainEquationCondition +from .input_equation_condition import InputPointsEquationCondition +from .input_output_condition import InputOutputPointsCondition +from .data_condition import DataConditionInterface class Condition: """ The class ``Condition`` is used to represent the constraints (physical equations, boundary conditions, etc.) that should be satisfied in the - problem at hand. Condition objects are used to formulate the PINA :obj:`pina.problem.abstract_problem.AbstractProblem` object. - Conditions can be specified in three ways: + problem at hand. Condition objects are used to formulate the + PINA :obj:`pina.problem.abstract_problem.AbstractProblem` object. + Conditions can be specified in four ways: 1. By specifying the input and output points of the condition; in such a case, the model is trained to produce the output points given the input - points. + points. Those points can either be torch.Tensor, LabelTensors, Graph 2. By specifying the location and the equation of the condition; in such a case, the model is trained to minimize the equation residual by @@ -29,79 +23,48 @@ class Condition: 3. By specifying the input points and the equation of the condition; in such a case, the model is trained to minimize the equation residual by - evaluating it at the passed input points. + evaluating it at the passed input points. The input points must be + a LabelTensor. + + 4. By specifying only the data matrix; in such a case the model is + trained with an unsupervised costum loss and uses the data in training. + Additionaly conditioning variables can be passed, whenever the model + has extra conditioning variable it depends on. Example:: - >>> example_domain = Span({'x': [0, 1], 'y': [0, 1]}) - >>> def example_dirichlet(input_, output_): - >>> value = 0.0 - >>> return output_.extract(['u']) - value - >>> example_input_pts = LabelTensor( - >>> torch.tensor([[0, 0, 0]]), ['x', 'y', 'z']) - >>> example_output_pts = LabelTensor(torch.tensor([[1, 2]]), ['a', 'b']) - >>> - >>> Condition( - >>> input_points=example_input_pts, - >>> output_points=example_output_pts) - >>> Condition( - >>> location=example_domain, - >>> equation=example_dirichlet) - >>> Condition( - >>> input_points=example_input_pts, - >>> equation=example_dirichlet) + >>> TODO """ - # def _dictvalue_isinstance(self, dict_, key_, class_): - # """Check if the value of a dictionary corresponding to `key` is an instance of `class_`.""" - # if key_ not in dict_.keys(): - # return True + __slots__ = list( + set( + InputOutputPointsCondition.__slots__, + InputPointsEquationCondition.__slots__, + DomainEquationCondition.__slots__, + DataConditionInterface.__slots__ - # return isinstance(dict_[key_], class_) - - # def __init__(self, *args, **kwargs): - # """ - # Constructor for the `Condition` class. - # """ - # self.data_weight = kwargs.pop("data_weight", 1.0) - - # if len(args) != 0: - # raise ValueError( - # f"Condition takes only the following keyword arguments: {Condition.__slots__}." - # ) + ) + ) def __new__(cls, *args, **kwargs): - - if sorted(kwargs.keys()) == sorted(["input_points", "output_points"]): - return DomainOutputCondition( - domain=kwargs["input_points"], - output_points=kwargs["output_points"] + + if len(args) != 0: + raise ValueError( + f"Condition takes only the following keyword ' + 'arguments: {Condition.__slots__}." ) - elif sorted(kwargs.keys()) == sorted(["domain", "output_points"]): - return DomainOutputCondition(**kwargs) - elif sorted(kwargs.keys()) == sorted(["domain", "equation"]): + + sorted_keys = sorted(kwargs.keys()) + if sorted_keys == sorted(InputOutputPointsCondition.__slots__): + return InputOutputPointsCondition(**kwargs) + elif sorted_keys == sorted(InputPointsEquationCondition.__slots__): + return InputPointsEquationCondition(**kwargs) + elif sorted_keys == sorted(DomainEquationCondition.__slots__): return DomainEquationCondition(**kwargs) + elif sorted_keys == sorted(DataConditionInterface.__slots__): + return DataConditionInterface(**kwargs) + elif sorted_keys == DataConditionInterface.__slots__[0]: + return DataConditionInterface(**kwargs) else: - raise ValueError(f"Invalid keyword arguments {kwargs.keys()}.") - # TODO: remove, not used anymore - ''' - if ( - sorted(kwargs.keys()) != sorted(["input_points", "output_points"]) - and sorted(kwargs.keys()) != sorted(["location", "equation"]) - and sorted(kwargs.keys()) != sorted(["input_points", "equation"]) - ): - raise ValueError(f"Invalid keyword arguments {kwargs.keys()}.") - # TODO: remove, not used anymore - if not self._dictvalue_isinstance(kwargs, "input_points", LabelTensor): - raise TypeError("`input_points` must be a torch.Tensor.") - if not self._dictvalue_isinstance(kwargs, "output_points", LabelTensor): - raise TypeError("`output_points` must be a torch.Tensor.") - if not self._dictvalue_isinstance(kwargs, "location", Location): - raise TypeError("`location` must be a Location.") - if not self._dictvalue_isinstance(kwargs, "equation", Equation): - raise TypeError("`equation` must be a Equation.") - - for key, value in kwargs.items(): - setattr(self, key, value) - ''' \ No newline at end of file + raise ValueError(f"Invalid keyword arguments {kwargs.keys()}.") \ No newline at end of file diff --git a/pina/condition/condition_interface.py b/pina/condition/condition_interface.py index 0626a6d83..f380dcf68 100644 --- a/pina/condition/condition_interface.py +++ b/pina/condition/condition_interface.py @@ -1,21 +1,25 @@ -from abc import ABCMeta, abstractmethod +from abc import ABCMeta class ConditionInterface(metaclass=ABCMeta): - def __init__(self) -> None: - self._problem = None + condition_types = ['physical', 'supervised', 'unsupervised'] + def __init__(self): + self._condition_type = None - @abstractmethod - def residual(self, model): - """ - Compute the residual of the condition. - - :param model: The model to evaluate the condition. - :return: The residual of the condition. - """ - pass - - def set_problem(self, problem): - self._problem = problem + @property + def condition_type(self): + return self._condition_type + + @condition_type.setattr + def condition_type(self, values): + if not isinstance(values, (list, tuple)): + values = [values] + for value in values: + if value not in ConditionInterface.condition_types: + raise ValueError( + 'Unavailable type of condition, expected one of' + f' {ConditionInterface.condition_types}.' + ) + self._condition_type = values \ No newline at end of file diff --git a/pina/condition/data_condition.py b/pina/condition/data_condition.py new file mode 100644 index 000000000..259eb5669 --- /dev/null +++ b/pina/condition/data_condition.py @@ -0,0 +1,44 @@ +import torch + +from . import ConditionInterface +from ..label_tensor import LabelTensor +from ..graph import Graph +from ..utils import check_consistency + +class DataConditionInterface(ConditionInterface): + """ + Condition for data. This condition must be used every + time a Unsupervised Loss is needed in the Solver. The conditionalvariable + can be passed as extra-input when the model learns a conditional + distribution + """ + + __slots__ = ["data", "conditionalvariable"] + + def __init__(self, data, conditionalvariable=None): + """ + TODO + """ + super().__init__() + self.data = data + self.conditionalvariable = conditionalvariable + self.condition_type = 'unsupervised' + + @property + def data(self): + return self._data + + @data.setter + def data(self, value): + check_consistency(value, (LabelTensor, Graph, torch.Tensor)) + self._data = value + + @property + def conditionalvariable(self): + return self._conditionalvariable + + @data.setter + def conditionalvariable(self, value): + if value is not None: + check_consistency(value, (LabelTensor, Graph, torch.Tensor)) + self._data = value \ No newline at end of file diff --git a/pina/condition/domain_equation_condition.py b/pina/condition/domain_equation_condition.py index 15df3f85f..9838ad7e5 100644 --- a/pina/condition/domain_equation_condition.py +++ b/pina/condition/domain_equation_condition.py @@ -1,34 +1,43 @@ +import torch + from .condition_interface import ConditionInterface +from ..label_tensor import LabelTensor +from ..graph import Graph +from ..utils import check_consistency +from ..domain import DomainInterface +from ..equation.equation_interface import EquationInterface class DomainEquationCondition(ConditionInterface): """ - Condition for input/output data. + Condition for domain/equation data. This condition must be used every + time a Physics Informed Loss is needed in the Solver. """ __slots__ = ["domain", "equation"] def __init__(self, domain, equation): """ - Constructor for the `InputOutputCondition` class. + TODO """ super().__init__() self.domain = domain self.equation = equation + self.condition_type = 'physics' - def residual(self, model): - """ - Compute the residual of the condition. - """ - self.batch_residual(model, self.domain, self.equation) - - @staticmethod - def batch_residual(model, input_pts, equation): - """ - Compute the residual of the condition for a single batch. Input and - output points are provided as arguments. + @property + def domain(self): + return self._domain + + @domain.setter + def domain(self, value): + check_consistency(value, (DomainInterface)) + self._domain = value - :param torch.nn.Module model: The model to evaluate the condition. - :param torch.Tensor input_pts: The input points. - :param torch.Tensor equation: The output points. - """ - return equation.residual(input_pts, model(input_pts)) \ No newline at end of file + @property + def equation(self): + return self._equation + + @equation.setter + def equation(self, value): + check_consistency(value, (EquationInterface)) + self._equation = value \ No newline at end of file diff --git a/pina/condition/domain_output_condition.py b/pina/condition/domain_output_condition.py deleted file mode 100644 index f847720b5..000000000 --- a/pina/condition/domain_output_condition.py +++ /dev/null @@ -1,44 +0,0 @@ - -from . import ConditionInterface - -class DomainOutputCondition(ConditionInterface): - """ - Condition for input/output data. - """ - - __slots__ = ["domain", "output_points"] - - def __init__(self, domain, output_points): - """ - Constructor for the `InputOutputCondition` class. - """ - super().__init__() - print(self) - self.domain = domain - self.output_points = output_points - - @property - def input_points(self): - """ - Get the input points of the condition. - """ - return self._problem.domains[self.domain] - - def residual(self, model): - """ - Compute the residual of the condition. - """ - return self.batch_residual(model, self.domain, self.output_points) - - @staticmethod - def batch_residual(model, input_points, output_points): - """ - Compute the residual of the condition for a single batch. Input and - output points are provided as arguments. - - :param torch.nn.Module model: The model to evaluate the condition. - :param torch.Tensor input_points: The input points. - :param torch.Tensor output_points: The output points. - """ - - return output_points - model(input_points) \ No newline at end of file diff --git a/pina/condition/input_equation_condition.py b/pina/condition/input_equation_condition.py index 288022c00..c4b9f8dba 100644 --- a/pina/condition/input_equation_condition.py +++ b/pina/condition/input_equation_condition.py @@ -1,23 +1,42 @@ +import torch -from . import ConditionInterface +from .condition_interface import ConditionInterface +from ..label_tensor import LabelTensor +from ..graph import Graph +from ..utils import check_consistency +from ..equation.equation_interface import EquationInterface -class InputEquationCondition(ConditionInterface): +class InputPointsEquationCondition(ConditionInterface): """ - Condition for input/output data. + Condition for input_points/equation data. This condition must be used every + time a Physics Informed Loss is needed in the Solver. """ - __slots__ = ["input_points", "output_points"] + __slots__ = ["input_points", "equation"] - def __init__(self, input_points, output_points): + def __init__(self, input_points, equation): """ - Constructor for the `InputOutputCondition` class. + TODO """ super().__init__() self.input_points = input_points - self.output_points = output_points + self.equation = equation + self.condition_type = 'physics' - def residual(self, model): - """ - Compute the residual of the condition. - """ - return self.output_points - model(self.input_points) \ No newline at end of file + @property + def input_points(self): + return self._input_points + + @input_points.setter + def input_points(self, value): + check_consistency(value, (LabelTensor)) # for now only labeltensors, we need labels for the operators! + self._input_points = value + + @property + def equation(self): + return self._equation + + @equation.setter + def equation(self, value): + check_consistency(value, (EquationInterface)) + self._equation = value \ No newline at end of file diff --git a/pina/condition/input_output_condition.py b/pina/condition/input_output_condition.py new file mode 100644 index 000000000..fd6b7a034 --- /dev/null +++ b/pina/condition/input_output_condition.py @@ -0,0 +1,42 @@ + +import torch + +from .condition_interface import ConditionInterface +from ..label_tensor import LabelTensor +from ..graph import Graph +from ..utils import check_consistency + +class InputOutputPointsCondition(ConditionInterface): + """ + Condition for domain/equation data. This condition must be used every + time a Physics Informed or a Supervised Loss is needed in the Solver. + """ + + __slots__ = ["input_points", "output_points"] + + def __init__(self, input_points, output_points): + """ + TODO + """ + super().__init__() + self.input_points = input_points + self.output_points = output_points + self.condition_type = ['supervised', 'physics'] + + @property + def input_points(self): + return self._input_points + + @input_points.setter + def input_points(self, value): + check_consistency(value, (LabelTensor, Graph, torch.Tensor)) + self._input_points = value + + @property + def output_points(self): + return self._output_points + + @output_points.setter + def output_points(self, value): + check_consistency(value, (LabelTensor, Graph, torch.Tensor)) + self._output_points = value \ No newline at end of file From 5f8ef45427bbf511e207e1161d36c4b182c91436 Mon Sep 17 00:00:00 2001 From: Dario Coscia Date: Thu, 3 Oct 2024 21:55:16 +0200 Subject: [PATCH 02/14] sampling mode domain added --- pina/domain/cartesian.py | 1 + pina/domain/difference_domain.py | 2 +- pina/domain/domain_interface.py | 24 ++++++++++++++++++++++++ pina/domain/ellipsoid.py | 3 ++- pina/domain/exclusion_domain.py | 2 +- pina/domain/intersection_domain.py | 2 +- pina/domain/operation_interface.py | 5 ++++- pina/domain/simplex.py | 5 ++++- pina/domain/union_domain.py | 3 +++ 9 files changed, 41 insertions(+), 6 deletions(-) diff --git a/pina/domain/cartesian.py b/pina/domain/cartesian.py index 51270549a..728e7cf0e 100644 --- a/pina/domain/cartesian.py +++ b/pina/domain/cartesian.py @@ -21,6 +21,7 @@ def __init__(self, cartesian_dict): """ self.fixed_ = {} self.range_ = {} + self.sample_modes = ["random", "grid", "lh", "chebyshev", "latin"] for k, v in cartesian_dict.items(): if isinstance(v, (int, float)): diff --git a/pina/domain/difference_domain.py b/pina/domain/difference_domain.py index d2ba414f0..9554aaf32 100644 --- a/pina/domain/difference_domain.py +++ b/pina/domain/difference_domain.py @@ -77,7 +77,7 @@ def sample(self, n, mode="random", variables="all"): 5 """ - if mode != "random": + if mode != self.sample_modes: raise NotImplementedError( f"{mode} is not a valid mode for sampling." ) diff --git a/pina/domain/domain_interface.py b/pina/domain/domain_interface.py index 5906d2851..208bfdfbc 100644 --- a/pina/domain/domain_interface.py +++ b/pina/domain/domain_interface.py @@ -9,6 +9,30 @@ class DomainInterface(metaclass=ABCMeta): Any geometry entity should inherit from this class. """ + __available_sampling_modes = ["random", "grid", "lh", "chebyshev", "latin"] + + @property + @abstractmethod + def sample_modes(self): + """ + Abstract method returing available samples modes for the Domain. + """ + pass + + @sample_modes.setter + def sample_modes(self, values): + """ + TODO + """ + if not isinstance(values, (list, tuple)): + values = [values] + for value in values: + if value not in DomainInterface.__available_sampling_modes: + raise TypeError(f"mode {value} not valid. Expected at least " + "one in " + f"{DomainInterface.__available_sampling_modes}." + ) + @abstractmethod def sample(self): """ diff --git a/pina/domain/ellipsoid.py b/pina/domain/ellipsoid.py index dcbc55543..ee5079fee 100644 --- a/pina/domain/ellipsoid.py +++ b/pina/domain/ellipsoid.py @@ -39,6 +39,7 @@ def __init__(self, ellipsoid_dict, sample_surface=False): self.range_ = {} self._centers = None self._axis = None + self.sample_modes = "random" # checking consistency check_consistency(sample_surface, bool) @@ -281,7 +282,7 @@ def _single_points_sample(n, variables): if variables == "all": variables = list(self.range_.keys()) + list(self.fixed_.keys()) - if mode in ["random"]: + if mode in self.sample_modes: return _Nd_sampler(n, mode, variables) else: raise NotImplementedError(f"mode={mode} is not implemented.") diff --git a/pina/domain/exclusion_domain.py b/pina/domain/exclusion_domain.py index ed63db314..4fc582cef 100644 --- a/pina/domain/exclusion_domain.py +++ b/pina/domain/exclusion_domain.py @@ -76,7 +76,7 @@ def sample(self, n, mode="random", variables="all"): 5 """ - if mode != "random": + if mode != self.sample_modes: raise NotImplementedError( f"{mode} is not a valid mode for sampling." ) diff --git a/pina/domain/intersection_domain.py b/pina/domain/intersection_domain.py index b40d36950..b580f21c3 100644 --- a/pina/domain/intersection_domain.py +++ b/pina/domain/intersection_domain.py @@ -78,7 +78,7 @@ def sample(self, n, mode="random", variables="all"): 5 """ - if mode != "random": + if mode != self.sample_modes: raise NotImplementedError( f"{mode} is not a valid mode for sampling." ) diff --git a/pina/domain/operation_interface.py b/pina/domain/operation_interface.py index acd4cf44b..edf2d484f 100644 --- a/pina/domain/operation_interface.py +++ b/pina/domain/operation_interface.py @@ -24,6 +24,9 @@ def __init__(self, geometries): # assign geometries self._geometries = geometries + # sampling mode, for now random is the only available + self.sample_modes = "random" + @property def geometries(self): """ @@ -65,4 +68,4 @@ def _check_dimensions(self, geometries): if geometry.variables != geometries[0].variables: raise NotImplementedError( f"The geometries need to have same dimensions and labels." - ) + ) \ No newline at end of file diff --git a/pina/domain/simplex.py b/pina/domain/simplex.py index 8d26422ae..2f31e4990 100644 --- a/pina/domain/simplex.py +++ b/pina/domain/simplex.py @@ -74,6 +74,9 @@ def __init__(self, simplex_matrix, sample_surface=False): # build cartesian_bound self._cartesian_bound = self._build_cartesian(self._vertices_matrix) + # sampling mode + self.sample_modes = "random" + @property def variables(self): return self._vertices_matrix.labels @@ -231,7 +234,7 @@ def sample(self, n, mode="random", variables="all"): in ``variables``. """ - if mode in ["random"]: + if mode in self.sample_modes: if self._sample_surface: sample_pts = self._sample_boundary_randomly(n) else: diff --git a/pina/domain/union_domain.py b/pina/domain/union_domain.py index da2ead90d..07a18f239 100644 --- a/pina/domain/union_domain.py +++ b/pina/domain/union_domain.py @@ -32,6 +32,9 @@ def __init__(self, geometries): """ super().__init__(geometries) + self.sample_modes = list( + set([geom.sample_modes for geom in geometries]) + ) def is_inside(self, point, check_border=False): """ From fe3abbf1c0d4cee42300bd767dd4e5bdf4f36f79 Mon Sep 17 00:00:00 2001 From: Dario Coscia Date: Fri, 4 Oct 2024 13:57:18 +0200 Subject: [PATCH 03/14] * Adding Collector for handling data sampling/collection before dataset/dataloader * Modify domain by adding sample_mode, variables as property * Small change concatenate -> cat in lno/avno * Create different factory classes for conditions --- pina/collector.py | 72 ++++++++ pina/condition/condition.py | 11 +- pina/condition/condition_interface.py | 15 +- pina/condition/data_condition.py | 20 +- pina/condition/domain_equation_condition.py | 26 +-- pina/condition/input_equation_condition.py | 24 +-- pina/condition/input_output_condition.py | 21 +-- pina/domain/cartesian.py | 5 +- pina/domain/domain_interface.py | 14 +- pina/domain/ellipsoid.py | 5 +- pina/domain/operation_interface.py | 5 +- pina/domain/simplex.py | 7 +- pina/domain/union_domain.py | 9 +- pina/model/avno.py | 6 +- pina/model/lno.py | 4 +- pina/problem/abstract_problem.py | 195 +++++--------------- pina/problem/inverse_problem.py | 14 ++ tests/test_problem.py | 28 +-- 18 files changed, 221 insertions(+), 260 deletions(-) create mode 100644 pina/collector.py diff --git a/pina/collector.py b/pina/collector.py new file mode 100644 index 000000000..fa3247e2e --- /dev/null +++ b/pina/collector.py @@ -0,0 +1,72 @@ +from .utils import check_consistency, merge_tensors + +class Collector: + def __init__(self, problem): + self.problem = problem # hook Collector <-> Problem + self.data_collections = {name : {} for name in self.problem.conditions} # collection of data + self.is_conditions_ready = { + name : False for name in self.problem.conditions} # names of the conditions that need to be sampled + self.full = False # collector full, all points for all conditions are given and the data are ready to be used in trainig + + @property + def full(self): + return all(self.is_conditions_ready.values()) + + @full.setter + def full(self, value): + check_consistency(value, bool) + self._full = value + + @property + def problem(self): + return self._problem + + @problem.setter + def problem(self, value): + self._problem = value + + def store_fixed_data(self): + # loop over all conditions + for condition_name, condition in self.problem.conditions.items(): + # if the condition is not ready and domain is not attribute + # of condition, we get and store the data + if (not self.is_conditions_ready[condition_name]) and (not hasattr(condition, "domain")): + # get data + keys = condition.__slots__ + values = [getattr(condition, name) for name in keys] + self.data_collections[condition_name] = dict(zip(keys, values)) + # condition now is ready + self.is_conditions_ready[condition_name] = True + + def store_sample_domains(self, n, mode, variables, sample_locations): + # loop over all locations + for loc in sample_locations: + # get condition + condition = self.problem.conditions[loc] + keys = ["input_points", "equation"] + # if the condition is not ready, we get and store the data + if (not self.is_conditions_ready[loc]): + # if it is the first time we sample + if not self.data_collections[loc]: + already_sampled = [] + # if we have sampled the condition but not all variables + else: + already_sampled = [self.data_collections[loc].input_points] + # if the condition is ready but we want to sample again + else: + self.is_conditions_ready[loc] = False + already_sampled = [] + + # get the samples + samples = [ + condition.domain.sample(n=n, mode=mode, variables=variables) + ] + already_sampled + pts = merge_tensors(samples) + if ( + sorted(self.data_collections[loc].input_points.labels) + == + sorted(self.problem.input_variables) + ): + self.is_conditions_ready[loc] = True + values = [pts, condition.equation] + self.data_collections[loc] = dict(zip(keys, values)) \ No newline at end of file diff --git a/pina/condition/condition.py b/pina/condition/condition.py index ddc722f0c..09180cc6e 100644 --- a/pina/condition/condition.py +++ b/pina/condition/condition.py @@ -39,11 +39,10 @@ class Condition: __slots__ = list( set( - InputOutputPointsCondition.__slots__, - InputPointsEquationCondition.__slots__, - DomainEquationCondition.__slots__, + InputOutputPointsCondition.__slots__ + + InputPointsEquationCondition.__slots__ + + DomainEquationCondition.__slots__ + DataConditionInterface.__slots__ - ) ) @@ -51,8 +50,8 @@ def __new__(cls, *args, **kwargs): if len(args) != 0: raise ValueError( - f"Condition takes only the following keyword ' - 'arguments: {Condition.__slots__}." + "Condition takes only the following keyword " + f"arguments: {Condition.__slots__}." ) sorted_keys = sorted(kwargs.keys()) diff --git a/pina/condition/condition_interface.py b/pina/condition/condition_interface.py index f380dcf68..e668b397b 100644 --- a/pina/condition/condition_interface.py +++ b/pina/condition/condition_interface.py @@ -1,18 +1,27 @@ from abc import ABCMeta - class ConditionInterface(metaclass=ABCMeta): condition_types = ['physical', 'supervised', 'unsupervised'] - def __init__(self): + + def __init__(self, *args, **wargs): self._condition_type = None + self._problem = None + + @property + def problem(self): + return self._problem + + @problem.setter + def problem(self, value): + self._problem = value @property def condition_type(self): return self._condition_type - @condition_type.setattr + @condition_type.setter def condition_type(self, values): if not isinstance(values, (list, tuple)): values = [values] diff --git a/pina/condition/data_condition.py b/pina/condition/data_condition.py index 259eb5669..b9fe1ede1 100644 --- a/pina/condition/data_condition.py +++ b/pina/condition/data_condition.py @@ -24,21 +24,7 @@ def __init__(self, data, conditionalvariable=None): self.conditionalvariable = conditionalvariable self.condition_type = 'unsupervised' - @property - def data(self): - return self._data - - @data.setter - def data(self, value): - check_consistency(value, (LabelTensor, Graph, torch.Tensor)) - self._data = value - - @property - def conditionalvariable(self): - return self._conditionalvariable - - @data.setter - def conditionalvariable(self, value): - if value is not None: + def __setattr__(self, key, value): + if (key == 'data') or (key == 'conditionalvariable'): check_consistency(value, (LabelTensor, Graph, torch.Tensor)) - self._data = value \ No newline at end of file + DataConditionInterface.__dict__[key].__set__(self, value) \ No newline at end of file diff --git a/pina/condition/domain_equation_condition.py b/pina/condition/domain_equation_condition.py index 9838ad7e5..f0ef8e07d 100644 --- a/pina/condition/domain_equation_condition.py +++ b/pina/condition/domain_equation_condition.py @@ -1,8 +1,6 @@ import torch from .condition_interface import ConditionInterface -from ..label_tensor import LabelTensor -from ..graph import Graph from ..utils import check_consistency from ..domain import DomainInterface from ..equation.equation_interface import EquationInterface @@ -24,20 +22,10 @@ def __init__(self, domain, equation): self.equation = equation self.condition_type = 'physics' - @property - def domain(self): - return self._domain - - @domain.setter - def domain(self, value): - check_consistency(value, (DomainInterface)) - self._domain = value - - @property - def equation(self): - return self._equation - - @equation.setter - def equation(self, value): - check_consistency(value, (EquationInterface)) - self._equation = value \ No newline at end of file + def __setattr__(self, key, value): + if key == 'domain': + check_consistency(value, (DomainInterface)) + DomainEquationCondition.__dict__[key].__set__(self, value) + elif key == 'equation': + check_consistency(value, (EquationInterface)) + DomainEquationCondition.__dict__[key].__set__(self, value) \ No newline at end of file diff --git a/pina/condition/input_equation_condition.py b/pina/condition/input_equation_condition.py index c4b9f8dba..f77b025dc 100644 --- a/pina/condition/input_equation_condition.py +++ b/pina/condition/input_equation_condition.py @@ -23,20 +23,10 @@ def __init__(self, input_points, equation): self.equation = equation self.condition_type = 'physics' - @property - def input_points(self): - return self._input_points - - @input_points.setter - def input_points(self, value): - check_consistency(value, (LabelTensor)) # for now only labeltensors, we need labels for the operators! - self._input_points = value - - @property - def equation(self): - return self._equation - - @equation.setter - def equation(self, value): - check_consistency(value, (EquationInterface)) - self._equation = value \ No newline at end of file + def __setattr__(self, key, value): + if key == 'input_points': + check_consistency(value, (LabelTensor)) # for now only labeltensors, we need labels for the operators! + InputPointsEquationCondition.__dict__[key].__set__(self, value) + elif key == 'equation': + check_consistency(value, (EquationInterface)) + InputPointsEquationCondition.__dict__[key].__set__(self, value) \ No newline at end of file diff --git a/pina/condition/input_output_condition.py b/pina/condition/input_output_condition.py index fd6b7a034..70388b308 100644 --- a/pina/condition/input_output_condition.py +++ b/pina/condition/input_output_condition.py @@ -23,20 +23,7 @@ def __init__(self, input_points, output_points): self.output_points = output_points self.condition_type = ['supervised', 'physics'] - @property - def input_points(self): - return self._input_points - - @input_points.setter - def input_points(self, value): - check_consistency(value, (LabelTensor, Graph, torch.Tensor)) - self._input_points = value - - @property - def output_points(self): - return self._output_points - - @output_points.setter - def output_points(self, value): - check_consistency(value, (LabelTensor, Graph, torch.Tensor)) - self._output_points = value \ No newline at end of file + def __setattr__(self, key, value): + if (key == 'input_points') or (key == 'output_points'): + check_consistency(value, (LabelTensor, Graph, torch.Tensor)) + InputOutputPointsCondition.__dict__[key].__set__(self, value) \ No newline at end of file diff --git a/pina/domain/cartesian.py b/pina/domain/cartesian.py index 728e7cf0e..6e9b81afe 100644 --- a/pina/domain/cartesian.py +++ b/pina/domain/cartesian.py @@ -21,7 +21,6 @@ def __init__(self, cartesian_dict): """ self.fixed_ = {} self.range_ = {} - self.sample_modes = ["random", "grid", "lh", "chebyshev", "latin"] for k, v in cartesian_dict.items(): if isinstance(v, (int, float)): @@ -31,6 +30,10 @@ def __init__(self, cartesian_dict): else: raise TypeError + @property + def sample_modes(self): + return ["random", "grid", "lh", "chebyshev", "latin"] + @property def variables(self): """Spatial variables. diff --git a/pina/domain/domain_interface.py b/pina/domain/domain_interface.py index 208bfdfbc..4fa70a2ba 100644 --- a/pina/domain/domain_interface.py +++ b/pina/domain/domain_interface.py @@ -9,7 +9,7 @@ class DomainInterface(metaclass=ABCMeta): Any geometry entity should inherit from this class. """ - __available_sampling_modes = ["random", "grid", "lh", "chebyshev", "latin"] + available_sampling_modes = ["random", "grid", "lh", "chebyshev", "latin"] @property @abstractmethod @@ -19,6 +19,14 @@ def sample_modes(self): """ pass + @property + @abstractmethod + def variables(self): + """ + Abstract method returing Domain variables. + """ + pass + @sample_modes.setter def sample_modes(self, values): """ @@ -27,10 +35,10 @@ def sample_modes(self, values): if not isinstance(values, (list, tuple)): values = [values] for value in values: - if value not in DomainInterface.__available_sampling_modes: + if value not in DomainInterface.available_sampling_modes: raise TypeError(f"mode {value} not valid. Expected at least " "one in " - f"{DomainInterface.__available_sampling_modes}." + f"{DomainInterface.available_sampling_modes}." ) @abstractmethod diff --git a/pina/domain/ellipsoid.py b/pina/domain/ellipsoid.py index ee5079fee..b9185fa08 100644 --- a/pina/domain/ellipsoid.py +++ b/pina/domain/ellipsoid.py @@ -39,7 +39,6 @@ def __init__(self, ellipsoid_dict, sample_surface=False): self.range_ = {} self._centers = None self._axis = None - self.sample_modes = "random" # checking consistency check_consistency(sample_surface, bool) @@ -72,6 +71,10 @@ def __init__(self, ellipsoid_dict, sample_surface=False): self._centers = dict(zip(self.range_.keys(), centers.tolist())) self._axis = dict(zip(self.range_.keys(), ellipsoid_axis.tolist())) + @property + def sample_modes(self): + return ["random"] + @property def variables(self): """Spatial variables. diff --git a/pina/domain/operation_interface.py b/pina/domain/operation_interface.py index edf2d484f..a1efec91f 100644 --- a/pina/domain/operation_interface.py +++ b/pina/domain/operation_interface.py @@ -24,8 +24,9 @@ def __init__(self, geometries): # assign geometries self._geometries = geometries - # sampling mode, for now random is the only available - self.sample_modes = "random" + @property + def sample_modes(self): + return ["random"] @property def geometries(self): diff --git a/pina/domain/simplex.py b/pina/domain/simplex.py index 2f31e4990..d6abe5e9e 100644 --- a/pina/domain/simplex.py +++ b/pina/domain/simplex.py @@ -74,9 +74,10 @@ def __init__(self, simplex_matrix, sample_surface=False): # build cartesian_bound self._cartesian_bound = self._build_cartesian(self._vertices_matrix) - # sampling mode - self.sample_modes = "random" - + @property + def sample_modes(self): + return ["random"] + @property def variables(self): return self._vertices_matrix.labels diff --git a/pina/domain/union_domain.py b/pina/domain/union_domain.py index 07a18f239..bd7fa56cb 100644 --- a/pina/domain/union_domain.py +++ b/pina/domain/union_domain.py @@ -32,9 +32,16 @@ def __init__(self, geometries): """ super().__init__(geometries) + + @property + def sample_modes(self): self.sample_modes = list( - set([geom.sample_modes for geom in geometries]) + set([geom.sample_modes for geom in self.geometries]) ) + + @property + def variables(self): + return list(set([geom.variables for geom in self.geometries])) def is_inside(self, point, check_border=False): """ diff --git a/pina/model/avno.py b/pina/model/avno.py index 2ac3b3f7e..e27ce1f1e 100644 --- a/pina/model/avno.py +++ b/pina/model/avno.py @@ -1,7 +1,7 @@ """Module Averaging Neural Operator.""" import torch -from torch import nn, concatenate +from torch import nn, cat from .layers import AVNOBlock from .base_no import KernelNeuralOperator from pina.utils import check_consistency @@ -110,9 +110,9 @@ def forward(self, x): """ points_tmp = x.extract(self.coordinates_indices) new_batch = x.extract(self.field_indices) - new_batch = concatenate((new_batch, points_tmp), dim=-1) + new_batch = cat((new_batch, points_tmp), dim=-1) new_batch = self._lifting_operator(new_batch) new_batch = self._integral_kernels(new_batch) - new_batch = concatenate((new_batch, points_tmp), dim=-1) + new_batch = cat((new_batch, points_tmp), dim=-1) new_batch = self._projection_operator(new_batch) return new_batch diff --git a/pina/model/lno.py b/pina/model/lno.py index 077a6b929..b09f1aec1 100644 --- a/pina/model/lno.py +++ b/pina/model/lno.py @@ -1,7 +1,7 @@ """Module LowRank Neural Operator.""" import torch -from torch import nn, concatenate +from torch import nn, cat from pina.utils import check_consistency @@ -145,4 +145,4 @@ def forward(self, x): for module in self._integral_kernels: x = module(x, coords) # projecting - return self._projection_operator(concatenate((x, coords), dim=-1)) + return self._projection_operator(cat((x, coords), dim=-1)) diff --git a/pina/problem/abstract_problem.py b/pina/problem/abstract_problem.py index bf0a2dccd..78464d22e 100644 --- a/pina/problem/abstract_problem.py +++ b/pina/problem/abstract_problem.py @@ -1,12 +1,11 @@ """ Module for AbstractProblem class """ from abc import ABCMeta, abstractmethod -from ..utils import merge_tensors, check_consistency +from ..utils import check_consistency +from ..domain import DomainInterface +from ..condition.domain_equation_condition import DomainEquationCondition +from ..collector import Collector from copy import deepcopy -import torch - -from .. import LabelTensor - class AbstractProblem(metaclass=ABCMeta): """ @@ -20,27 +19,25 @@ class AbstractProblem(metaclass=ABCMeta): def __init__(self): - self._discretized_domains = {} - - for name, domain in self.domains.items(): - if isinstance(domain, (torch.Tensor, LabelTensor)): - self._discretized_domains[name] = domain + # create collector to manage problem data + self.collector = Collector(self) + # create hook conditions <-> problems for condition_name in self.conditions: - self.conditions[condition_name].set_problem(self) + self.conditions[condition_name].problem = self - # # variable storing all points - self.input_pts = {} - - # # varible to check if sampling is done. If no location - # # element is presented in Condition this variable is set to true - # self._have_sampled_points = {} - for condition_name in self.conditions: - self._discretized_domains[condition_name] = False + # store in collector all the available fixed points + # note that some points could not be stored at this stage (e.g. when + # sampling locations). To check that all data points are ready for + # training all type self.collector.full, which returns true if all + # points are ready. + self.collector.store_fixed_data() - # # put in self.input_pts all the points that we don't need to sample - self._span_condition_points() + @property + def input_pts(self): + return self.collector.data_collections + def __deepcopy__(self, memo): """ Implements deepcopy for the @@ -85,19 +82,6 @@ def input_variables(self): def input_variables(self, variables): raise RuntimeError - @property - @abstractmethod - def domains(self): - """ - The domain(s) where the conditions of the AbstractProblem are valid. - If more than one domain type is passed, a list of Location is - retured. - - :return: the domain(s) of ``self`` - :rtype: list[Location] - """ - pass - @property @abstractmethod def output_variables(self): @@ -114,34 +98,8 @@ def conditions(self): """ return self._conditions - - - def _span_condition_points(self): - """ - Simple function to get the condition points - """ - for condition_name in self.conditions: - condition = self.conditions[condition_name] - if hasattr(condition, "input_points"): - samples = condition.input_points - self.input_pts[condition_name] = samples - self._discretized_domains[condition_name] = True - if hasattr(self, "unknown_parameter_domain"): - # initialize the unknown parameters of the inverse problem given - # the domain the user gives - self.unknown_parameters = {} - for i, var in enumerate(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] - ) - self.unknown_parameters[var] = torch.nn.Parameter( - tensor_var - ) - def discretise_domain( - self, n, mode="random", variables="all", domains="all" + self, n, mode="random", variables="all", locations="all" ): """ Generate a set of points to span the `Location` of all the conditions of @@ -172,103 +130,38 @@ def discretise_domain( ``CartesianDomain``. """ - # check consistecy n + # check consistecy n, mode, variables, locations check_consistency(n, int) - - # check consistency mode check_consistency(mode, str) - if mode not in ["random", "grid", "lh", "chebyshev", "latin"]: + check_consistency(variables, str) + check_consistency(locations, str) + + # check correct sampling mode + if mode not in DomainInterface.available_sampling_modes: raise TypeError(f"mode {mode} not valid.") - # check consistency variables + # check correct variables if variables == "all": variables = self.input_variables - else: - check_consistency(variables, str) - - if sorted(variables) != sorted(self.input_variables): - TypeError( - f"Wrong variables for sampling. Variables ", - f"should be in {self.input_variables}.", - ) - - # check consistency location - if domains == "all": - domains = [condition for condition in self.conditions] - else: - check_consistency(domains, str) - print(domains) - if sorted(domains) != sorted(self.conditions): - TypeError( - f"Wrong locations for sampling. Location ", - f"should be in {self.conditions}.", - ) - - # sampling - for d in domains: - condition = self.conditions[d] - - # we try to check if we have already sampled - try: - already_sampled = [self.input_pts[d]] - # if we have not sampled, a key error is thrown - except KeyError: - already_sampled = [] - - # if we have already sampled fully the condition - # but we want to sample again we set already_sampled - # to an empty list since we need to sample again, and - # self._have_sampled_points to False. - if self._discretized_domains[d]: - already_sampled = [] - self._discretized_domains[d] = False - print(condition.domain) - print(d) - # build samples - samples = [ - self.domains[d].sample(n=n, mode=mode, variables=variables) - ] + already_sampled - pts = merge_tensors(samples) - self.input_pts[d] = pts - - # the condition is sampled if input_pts contains all labels - if sorted(self.input_pts[d].labels) == sorted( - self.input_variables - ): - self._have_sampled_points[d] = True - - def add_points(self, new_points): - """ - Adding points to the already sampled points. - - :param dict new_points: a dictionary with key the location to add the points - and values the torch.Tensor points. - """ - - if sorted(new_points.keys()) != sorted(self.conditions): - TypeError( - f"Wrong locations for new points. Location ", - f"should be in {self.conditions}.", - ) - - for location in new_points.keys(): - # extract old and new points - old_pts = self.input_pts[location] - new_pts = new_points[location] - - # if they don't have the same variables error - if sorted(old_pts.labels) != sorted(new_pts.labels): + for variable in variables: + if variable not in self.input_variables: TypeError( - f"Not matching variables for old and new points " - f"in condition {location}." + f"Wrong variables for sampling. Variables ", + f"should be in {self.input_variables}.", ) - if old_pts.labels != new_pts.labels: - new_pts = torch.hstack( - [new_pts.extract([i]) for i in old_pts.labels] + + # check correct location + if locations == "all": + locations = [name for name in self.conditions.keys()] + else: + if not isinstance(locations, (list)): + locations = [locations] + for loc in locations: + if not isinstance(self.conditions[loc], DomainEquationCondition): + raise TypeError( + f"Wrong locations passed, locations for sampling " + f"should be in {[loc for loc in locations if not isinstance(self.conditions[loc], DomainEquationCondition)]}.", ) - new_pts.labels = old_pts.labels - # merging - merged_pts = torch.vstack([old_pts, new_points[location]]) - merged_pts.labels = old_pts.labels - self.input_pts[location] = merged_pts \ No newline at end of file + # store data + self.collector.store_sample_domains(n, mode, variables, locations) \ No newline at end of file diff --git a/pina/problem/inverse_problem.py b/pina/problem/inverse_problem.py index 5a83566ae..51cbd3ca6 100644 --- a/pina/problem/inverse_problem.py +++ b/pina/problem/inverse_problem.py @@ -45,6 +45,20 @@ class InverseProblem(AbstractProblem): >>> 'data': Condition(CartesianDomain({'x': [0, 1]}), Equation(solution_data)) """ + def __init__(self): + super().__init__() + # storing unknown_parameters for optimization + self.unknown_parameters = {} + for i, var in enumerate(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] + ) + self.unknown_parameters[var] = torch.nn.Parameter( + tensor_var + ) + @abstractmethod def unknown_parameter_domain(self): """ diff --git a/tests/test_problem.py b/tests/test_problem.py index 3e1e1ee23..23688b56c 100644 --- a/tests/test_problem.py +++ b/tests/test_problem.py @@ -27,31 +27,31 @@ class Poisson(SpatialProblem): conditions = { 'gamma1': - Condition(location=CartesianDomain({ + Condition(domain=CartesianDomain({ 'x': [0, 1], 'y': 1 }), equation=FixedValue(0.0)), 'gamma2': - Condition(location=CartesianDomain({ + Condition(domain=CartesianDomain({ 'x': [0, 1], 'y': 0 }), equation=FixedValue(0.0)), 'gamma3': - Condition(location=CartesianDomain({ + Condition(domain=CartesianDomain({ 'x': 1, 'y': [0, 1] }), equation=FixedValue(0.0)), 'gamma4': - Condition(location=CartesianDomain({ + Condition(domain=CartesianDomain({ 'x': 0, 'y': [0, 1] }), equation=FixedValue(0.0)), 'D': - Condition(location=CartesianDomain({ + Condition(domain=CartesianDomain({ 'x': [0, 1], 'y': [0, 1] }), @@ -69,7 +69,7 @@ def poisson_sol(self, pts): # make the problem poisson_problem = Poisson() - +print(poisson_problem.input_pts) def test_discretise_domain(): n = 10 @@ -93,14 +93,14 @@ def test_discretise_domain(): assert poisson_problem.input_pts['D'].shape[0] == n -def test_sampling_few_variables(): - n = 10 - poisson_problem.discretise_domain(n, - 'grid', - locations=['D'], - variables=['x']) - assert poisson_problem.input_pts['D'].shape[1] == 1 - assert poisson_problem._have_sampled_points['D'] is False +# def test_sampling_few_variables(): +# n = 10 +# poisson_problem.discretise_domain(n, +# 'grid', +# locations=['D'], +# variables=['x']) +# assert poisson_problem.input_pts['D'].shape[1] == 1 +# assert poisson_problem._have_sampled_points['D'] is False # def test_sampling_all_args(): From 1e16f178015a378ead67252ff59c86fa536811a9 Mon Sep 17 00:00:00 2001 From: Dario Coscia Date: Fri, 4 Oct 2024 15:39:20 +0200 Subject: [PATCH 04/14] update condition_interface --- pina/condition/condition_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pina/condition/condition_interface.py b/pina/condition/condition_interface.py index e668b397b..52699b66e 100644 --- a/pina/condition/condition_interface.py +++ b/pina/condition/condition_interface.py @@ -3,7 +3,7 @@ class ConditionInterface(metaclass=ABCMeta): - condition_types = ['physical', 'supervised', 'unsupervised'] + condition_types = ['physics', 'supervised', 'unsupervised'] def __init__(self, *args, **wargs): self._condition_type = None From f1abe806cf5f2bb858fa3ac11e85bff2ab7d7e97 Mon Sep 17 00:00:00 2001 From: Dario Coscia <93731561+dario-coscia@users.noreply.github.com> Date: Fri, 4 Oct 2024 15:59:09 +0200 Subject: [PATCH 05/14] Filippo0.2 (#361) * Add summation and remove deepcopy (only for tensors) in LabelTensor class * Update operators for compatibility with updated LabelTensor implementation * Implement labels.setter in LabelTensor class * Update LabelTensor --------- Co-authored-by: FilippoOlivo --- pina/label_tensor.py | 108 +++++++++++++++++++++++++++---------- pina/operators.py | 96 +++++++++++++++------------------ tests/test_label_tensor.py | 58 +++++++++++++++++++- tests/test_operators.py | 19 +++---- 4 files changed, 187 insertions(+), 94 deletions(-) diff --git a/pina/label_tensor.py b/pina/label_tensor.py index 7646dd8a3..08e0b03e6 100644 --- a/pina/label_tensor.py +++ b/pina/label_tensor.py @@ -35,14 +35,34 @@ def __init__(self, x, labels): {1: {"name": "space"['a', 'b', 'c']) """ - self.labels = None + self.labels = labels + + @property + def labels(self): + """Property decorator for labels + + :return: labels of self + :rtype: list + """ + return self._labels + + @labels.setter + def labels(self, labels): + """" + Set properly the parameter _labels + + :param labels: Labels to assign to the class variable _labels. + :type: labels: str | list(str) | dict + """ + if hasattr(self, 'labels') is False: + self.init_labels() if isinstance(labels, dict): - self.update_labels(labels) + self.update_labels_from_dict(labels) elif isinstance(labels, list): - self.init_labels_from_list(labels) + self.update_labels_from_list(labels) elif isinstance(labels, str): labels = [labels] - self.init_labels_from_list(labels) + self.update_labels_from_list(labels) else: raise ValueError(f"labels must be list, dict or string.") @@ -60,38 +80,38 @@ def extract(self, label_to_extract): if isinstance(label_to_extract, (str, int)): label_to_extract = [label_to_extract] if isinstance(label_to_extract, (tuple, list)): - last_dim_label = self.labels[self.tensor.ndim - 1]['dof'] + last_dim_label = self._labels[self.tensor.ndim - 1]['dof'] if set(label_to_extract).issubset(last_dim_label) is False: raise ValueError('Cannot extract a dof which is not in the original LabelTensor') idx_to_extract = [last_dim_label.index(i) for i in label_to_extract] - new_tensor = deepcopy(self.tensor) + new_tensor = self.tensor new_tensor = new_tensor[..., idx_to_extract] - new_labels = deepcopy(self.labels) + new_labels = deepcopy(self._labels) last_dim_new_label = {self.tensor.ndim - 1: { 'dof': label_to_extract, - 'name': self.labels[self.tensor.ndim - 1]['name'] + 'name': self._labels[self.tensor.ndim - 1]['name'] }} new_labels.update(last_dim_new_label) elif isinstance(label_to_extract, dict): - new_labels = (deepcopy(self.labels)) - new_tensor = deepcopy(self.tensor) + new_labels = (deepcopy(self._labels)) + new_tensor = self.tensor for k, v in label_to_extract.items(): idx_dim = None - for kl, vl in self.labels.items(): + for kl, vl in self._labels.items(): if vl['name'] == k: idx_dim = kl break - dim_labels = self.labels[idx_dim]['dof'] + dim_labels = self._labels[idx_dim]['dof'] if isinstance(label_to_extract[k], (int, str)): label_to_extract[k] = [label_to_extract[k]] if set(label_to_extract[k]).issubset(dim_labels) is False: - raise ValueError('Cannot extract a dof which is not in the original labeltensor') + raise ValueError('Cannot extract a dof which is not in the original LabelTensor') idx_to_extract = [dim_labels.index(i) for i in label_to_extract[k]] indexer = [slice(None)] * idx_dim + [idx_to_extract] + [slice(None)] * (self.tensor.ndim - idx_dim - 1) new_tensor = new_tensor[indexer] dim_new_label = {idx_dim: { 'dof': label_to_extract[k], - 'name': self.labels[idx_dim]['name'] + 'name': self._labels[idx_dim]['name'] }} new_labels.update(dim_new_label) else: @@ -104,7 +124,7 @@ def __str__(self): """ s = '' - for key, value in self.labels.items(): + for key, value in self._labels.items(): s += f"{key}: {value}\n" s += '\n' s += super().__str__() @@ -155,7 +175,7 @@ def cat(tensors, dim=0): def requires_grad_(self, mode=True): lt = super().requires_grad_(mode) - lt.labels = self.labels + lt.labels = self._labels return lt @property @@ -181,10 +201,19 @@ def clone(self, *args, **kwargs): :rtype: LabelTensor """ - out = LabelTensor(super().clone(*args, **kwargs), self.labels) + out = LabelTensor(super().clone(*args, **kwargs), self._labels) return out - def update_labels(self, labels): + + def init_labels(self): + self._labels = { + idx_: { + 'dof': range(self.tensor.shape[idx_]), + 'name': idx_ + } for idx_ in range(self.tensor.ndim) + } + + def update_labels_from_dict(self, labels): """ Update the internal label representation according to the values passed as input. @@ -192,21 +221,16 @@ def update_labels(self, labels): :type labels: dict :raises ValueError: dof list contain duplicates or number of dof does not match with tensor shape """ - self.labels = { - idx_: { - 'dof': range(self.tensor.shape[idx_]), - 'name': idx_ - } for idx_ in range(self.tensor.ndim) - } + tensor_shape = self.tensor.shape for k, v in labels.items(): if len(v['dof']) != len(set(v['dof'])): raise ValueError("dof must be unique") if len(v['dof']) != tensor_shape[k]: raise ValueError('Number of dof does not match with tensor dimension') - self.labels.update(labels) + self._labels.update(labels) - def init_labels_from_list(self, labels): + def update_labels_from_list(self, labels): """ Given a list of dof, this method update the internal label representation @@ -214,4 +238,34 @@ def init_labels_from_list(self, labels): :type labels: list """ last_dim_labels = {self.tensor.ndim - 1: {'dof': labels, 'name': self.tensor.ndim - 1}} - self.update_labels(last_dim_labels) \ No newline at end of file + self.update_labels_from_dict(last_dim_labels) + + @staticmethod + def summation(tensors): + if len(tensors) == 0: + raise ValueError('tensors list must not be empty') + if len(tensors) == 1: + return tensors[0] + labels = tensors[0].labels + for j in range(tensors[0].ndim): + for i in range(1, len(tensors)): + if labels[j] != tensors[i].labels[j]: + labels.pop(j) + break + + data = torch.zeros(tensors[0].tensor.shape) + for i in range(len(tensors)): + data += tensors[i].tensor + new_tensor = LabelTensor(data, labels) + return new_tensor + + def last_dim_dof(self): + return self._labels[self.tensor.ndim - 1]['dof'] + + def append(self, tensor, mode='std'): + print(self.labels) + print(tensor.labels) + if mode == 'std': + new_label_tensor = LabelTensor.cat([self, tensor], dim=self.tensor.ndim - 1) + + return new_label_tensor diff --git a/pina/operators.py b/pina/operators.py index 17b45d814..58dcdff37 100644 --- a/pina/operators.py +++ b/pina/operators.py @@ -1,13 +1,13 @@ """ Module for operators vectorize implementation. Differential operators are used to write any differential problem. -These operators are implemented to work on different accellerators: CPU, GPU, TPU or MPS. +These operators are implemented to work on different accelerators: CPU, GPU, TPU or MPS. All operators take as input a tensor onto which computing the operator, a tensor with respect to which computing the operator, the name of the output variables to calculate the operator for (in case of multidimensional functions), and the variables name on which the operator is calculated. """ import torch - +from copy import deepcopy from pina.label_tensor import LabelTensor @@ -49,12 +49,12 @@ def grad_scalar_output(output_, input_, d): :rtype: LabelTensor """ - if len(output_.labels) != 1: + if len(output_.labels[output_.tensor.ndim-1]['dof']) != 1: raise RuntimeError("only scalar function can be differentiated") - if not all([di in input_.labels for di in d]): + if not all([di in input_.labels[input_.tensor.ndim-1]['dof'] for di in d]): raise RuntimeError("derivative labels missing from input tensor") - output_fieldname = output_.labels[0] + output_fieldname = output_.labels[output_.ndim-1]['dof'][0] gradients = torch.autograd.grad( output_, input_, @@ -65,41 +65,35 @@ def grad_scalar_output(output_, input_, d): retain_graph=True, allow_unused=True, )[0] - - gradients.labels = input_.labels + new_labels = deepcopy(input_.labels) + gradients.labels = new_labels gradients = gradients.extract(d) - gradients.labels = [f"d{output_fieldname}d{i}" for i in d] - + new_labels[input_.tensor.ndim - 1]['dof'] = [f"d{output_fieldname}d{i}" for i in d] + gradients.labels = new_labels return gradients if not isinstance(input_, LabelTensor): raise TypeError - if d is None: - d = input_.labels + d = input_.labels[input_.tensor.ndim-1]['dof'] if components is None: - components = output_.labels + components = output_.labels[output_.tensor.ndim-1]['dof'] - if output_.shape[1] == 1: # scalar output ################################ + if output_.shape[output_.ndim-1] == 1: # scalar output ################################ - if components != output_.labels: + if components != output_.labels[output_.tensor.ndim-1]['dof']: raise RuntimeError gradients = grad_scalar_output(output_, input_, d) - elif output_.shape[1] >= 2: # vector output ############################## - + elif output_.shape[output_.ndim-1] >= 2: # vector output ############################## + tensor_to_cat = [] for i, c in enumerate(components): c_output = output_.extract([c]) - if i == 0: - gradients = grad_scalar_output(c_output, input_, d) - else: - gradients = gradients.append( - grad_scalar_output(c_output, input_, d) - ) + tensor_to_cat.append(grad_scalar_output(c_output, input_, d)) + gradients = LabelTensor.cat(tensor_to_cat, dim=output_.tensor.ndim-1) else: raise NotImplementedError - return gradients @@ -130,27 +124,29 @@ def div(output_, input_, components=None, d=None): raise TypeError if d is None: - d = input_.labels + d = input_.labels[input_.tensor.ndim-1]['dof'] if components is None: - components = output_.labels + components = output_.labels[output_.tensor.ndim-1]['dof'] - if output_.shape[1] < 2 or len(components) < 2: + if output_.shape[output_.ndim-1] < 2 or len(components) < 2: raise ValueError("div supported only for vector fields") if len(components) != len(d): raise ValueError grad_output = grad(output_, input_, components, d) - div = torch.zeros(input_.shape[0], 1, device=output_.device) - labels = [None] * len(components) + last_dim_dof = [None] * len(components) + to_sum_tensors = [] for i, (c, d) in enumerate(zip(components, d)): c_fields = f"d{c}d{d}" - div[:, 0] += grad_output.extract(c_fields).sum(axis=1) - labels[i] = c_fields + last_dim_dof[i] = c_fields + to_sum_tensors.append(grad_output.extract(c_fields)) - div = div.as_subclass(LabelTensor) - div.labels = ["+".join(labels)] + div = LabelTensor.summation(to_sum_tensors) + new_labels = deepcopy(input_.labels) + new_labels[input_.tensor.ndim-1]['dof'] = ["+".join(last_dim_dof)] + div.labels = new_labels return div @@ -177,10 +173,10 @@ def laplacian(output_, input_, components=None, d=None, method="std"): :rtype: LabelTensor """ if d is None: - d = input_.labels + d = input_.labels[input_.tensor.ndim-1]['dof'] if components is None: - components = output_.labels + components = output_.labels[output_.tensor.ndim-1]['dof'] if len(components) != len(d) and len(components) != 1: raise ValueError @@ -194,35 +190,29 @@ def laplacian(output_, input_, components=None, d=None, method="std"): if len(components) == 1: grad_output = grad(output_, input_, components=components, d=d) - result = torch.zeros(output_.shape[0], 1, device=output_.device) - for i, label in enumerate(grad_output.labels): + to_append_tensors = [] + for i, label in enumerate(grad_output.labels[grad_output.ndim-1]['dof']): gg = grad(grad_output, input_, d=d, components=[label]) - result[:, 0] += super(torch.Tensor, gg.T).__getitem__( - i - ) # TODO improve + to_append_tensors.append(gg.extract([gg.labels[gg.tensor.ndim-1]['dof'][i]])) labels = [f"dd{components[0]}"] - + result = LabelTensor.summation(tensors=to_append_tensors) + result.labels = labels else: - result = torch.empty( - input_.shape[0], len(components), device=output_.device - ) labels = [None] * len(components) + to_append_tensors = [None] * len(components) for idx, (ci, di) in enumerate(zip(components, d)): - if not isinstance(ci, list): ci = [ci] if not isinstance(di, list): di = [di] - grad_output = grad(output_, input_, components=ci, d=di) - result[:, idx] = grad(grad_output, input_, d=di).flatten() - labels[idx] = f"dd{ci}dd{di}" - - result = result.as_subclass(LabelTensor) - result.labels = labels + to_append_tensors[idx] = grad(grad_output, input_, d=di) + labels[idx] = f"dd{ci[0]}dd{di[0]}" + result = LabelTensor.cat(tensors=to_append_tensors, dim=output_.tensor.ndim-1) + result.labels = labels return result - +# TODO Fix advection operator def advection(output_, input_, velocity_field, components=None, d=None): """ Perform advection operation. The operator works for vectorial functions, @@ -244,10 +234,10 @@ def advection(output_, input_, velocity_field, components=None, d=None): :rtype: LabelTensor """ if d is None: - d = input_.labels + d = input_.labels[input_.tensor.ndim-1]['dof'] if components is None: - components = output_.labels + components = output_.labels[output_.tensor.ndim-1]['dof'] tmp = ( grad(output_, input_, components, d) diff --git a/tests/test_label_tensor.py b/tests/test_label_tensor.py index f87d3abb1..6ef484f0b 100644 --- a/tests/test_label_tensor.py +++ b/tests/test_label_tensor.py @@ -17,12 +17,14 @@ "dof": range(20) } } +labels_list = ['x', 'y', 'z'] labels_all = labels_column | labels_row -@pytest.mark.parametrize("labels", [labels_column, labels_row, labels_all]) +@pytest.mark.parametrize("labels", [labels_column, labels_row, labels_all, labels_list]) def test_constructor(labels): LabelTensor(data, labels) + def test_wrong_constructor(): with pytest.raises(ValueError): LabelTensor(data, ['a', 'b']) @@ -61,7 +63,6 @@ def test_extract_2D(labels_te): assert torch.all(torch.isclose(data[2,2].reshape(1, 1), new)) def test_extract_3D(): - labels = labels_all data = torch.rand(20, 3, 4) labels = { 1: { @@ -80,6 +81,7 @@ def test_extract_3D(): tensor = LabelTensor(data, labels) new = tensor.extract(labels_te) + tensor2 = LabelTensor(data, labels) assert new.ndim == tensor.ndim assert new.shape[0] == 20 assert new.shape[1] == 2 @@ -88,6 +90,10 @@ def test_extract_3D(): data[:, 0::2, 1:4].reshape(20, 2, 3), new )) + assert tensor2.ndim == tensor.ndim + assert tensor2.shape == tensor.shape + assert tensor.labels == tensor2.labels + assert new.shape != tensor.shape def test_concatenation_3D(): data_1 = torch.rand(20, 3, 4) @@ -146,3 +152,51 @@ def test_concatenation_3D(): assert lt_cat.labels[2]['dof'] == range(5) assert lt_cat.labels[0]['dof'] == range(20) assert lt_cat.labels[1]['dof'] == range(3) + + +def test_summation(): + lt1 = LabelTensor(torch.ones(20,3), labels_all) + lt2 = LabelTensor(torch.ones(30,3), ['x', 'y', 'z']) + with pytest.raises(RuntimeError): + LabelTensor.summation([lt1, lt2]) + lt1 = LabelTensor(torch.ones(20,3), labels_all) + lt2 = LabelTensor(torch.ones(20,3), labels_all) + lt_sum = LabelTensor.summation([lt1, lt2]) + assert lt_sum.ndim == lt_sum.ndim + assert lt_sum.shape[0] == 20 + assert lt_sum.shape[1] == 3 + assert lt_sum.labels == labels_all + assert torch.eq(lt_sum.tensor, torch.ones(20,3)*2).all() + lt1 = LabelTensor(torch.ones(20,3), labels_all) + lt2 = LabelTensor(torch.ones(20,3), labels_all) + lt3 = LabelTensor(torch.zeros(20, 3), labels_all) + lt_sum = LabelTensor.summation([lt1, lt2, lt3]) + assert lt_sum.ndim == lt_sum.ndim + assert lt_sum.shape[0] == 20 + assert lt_sum.shape[1] == 3 + assert lt_sum.labels == labels_all + assert torch.eq(lt_sum.tensor, torch.ones(20,3)*2).all() + +def test_append_3D(): + data_1 = torch.rand(20, 3, 4) + labels_1 = ['x', 'y', 'z', 'w'] + lt1 = LabelTensor(data_1, labels_1) + data_2 = torch.rand(50, 3, 4) + labels_2 = ['x', 'y', 'z', 'w'] + lt2 = LabelTensor(data_2, labels_2) + lt1 = lt1.append(lt2) + assert lt1.shape == (70, 3, 4) + assert lt1.labels[0]['dof'] == range(70) + assert lt1.labels[1]['dof'] == range(3) + assert lt1.labels[2]['dof'] == ['x', 'y', 'z', 'w'] + data_1 = torch.rand(20, 3, 2) + labels_1 = ['x', 'y'] + lt1 = LabelTensor(data_1, labels_1) + data_2 = torch.rand(20, 3, 2) + labels_2 = ['z', 'w'] + lt2 = LabelTensor(data_2, labels_2) + lt1 = lt1.append(lt2, mode='cross') + assert lt1.shape == (20, 3, 4) + assert lt1.labels[0]['dof'] == range(20) + assert lt1.labels[1]['dof'] == range(3) + assert lt1.labels[2]['dof'] == ['x', 'y', 'z', 'w'] diff --git a/tests/test_operators.py b/tests/test_operators.py index aa9ea9d8b..600dfa8a6 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -10,15 +10,14 @@ def func_vec(x): def func_scalar(x): - print('X') x_ = x.extract(['x']) y_ = x.extract(['y']) mu_ = x.extract(['mu']) return x_**2 + y_**2 + mu_**3 -data = torch.rand((20, 3), requires_grad=True) -inp = LabelTensor(data, ['x', 'y', 'mu']) +data = torch.rand((20, 3)) +inp = LabelTensor(data, ['x', 'y', 'mu']).requires_grad_(True) labels = ['a', 'b', 'c'] tensor_v = LabelTensor(func_vec(inp), labels) tensor_s = LabelTensor(func_scalar(inp).reshape(-1, 1), labels[0]) @@ -27,35 +26,31 @@ def func_scalar(x): def test_grad_scalar_output(): grad_tensor_s = grad(tensor_s, inp) assert grad_tensor_s.shape == inp.shape - assert grad_tensor_s.labels == [ - f'd{tensor_s.labels[0]}d{i}' for i in inp.labels + assert grad_tensor_s.labels[grad_tensor_s.ndim-1]['dof'] == [ + f'd{tensor_s.labels[tensor_s.ndim-1]["dof"][0]}d{i}' for i in inp.labels[inp.ndim-1]['dof'] ] grad_tensor_s = grad(tensor_s, inp, d=['x', 'y']) - assert grad_tensor_s.shape == (inp.shape[0], 2) - assert grad_tensor_s.labels == [ - f'd{tensor_s.labels[0]}d{i}' for i in ['x', 'y'] + assert grad_tensor_s.shape == (20, 2) + assert grad_tensor_s.labels[grad_tensor_s.ndim-1]['dof'] == [ + f'd{tensor_s.labels[tensor_s.ndim-1]["dof"][0]}d{i}' for i in ['x', 'y'] ] - def test_grad_vector_output(): grad_tensor_v = grad(tensor_v, inp) assert grad_tensor_v.shape == (20, 9) grad_tensor_v = grad(tensor_v, inp, d=['x', 'mu']) assert grad_tensor_v.shape == (inp.shape[0], 6) - def test_div_vector_output(): grad_tensor_v = div(tensor_v, inp) assert grad_tensor_v.shape == (20, 1) grad_tensor_v = div(tensor_v, inp, components=['a', 'b'], d=['x', 'mu']) assert grad_tensor_v.shape == (inp.shape[0], 1) - def test_laplacian_scalar_output(): laplace_tensor_v = laplacian(tensor_s, inp, components=['a'], d=['x', 'y']) assert laplace_tensor_v.shape == tensor_s.shape - def test_laplacian_vector_output(): laplace_tensor_v = laplacian(tensor_v, inp) assert laplace_tensor_v.shape == tensor_v.shape From a869d0a9294ee5c6a4c7c696ec3ada1aee4466f8 Mon Sep 17 00:00:00 2001 From: Filippo Olivo Date: Thu, 10 Oct 2024 18:26:52 +0200 Subject: [PATCH 06/14] Update of LabelTensor class and fix Simplex domain (#362) *Implement new methods in LabelTensor and fix operators --- pina/collector.py | 29 ++- pina/condition/data_condition.py | 4 +- pina/condition/domain_equation_condition.py | 4 +- pina/condition/input_equation_condition.py | 6 +- pina/condition/input_output_condition.py | 4 +- pina/domain/difference_domain.py | 2 +- pina/domain/exclusion_domain.py | 2 +- pina/domain/intersection_domain.py | 2 +- pina/domain/simplex.py | 7 +- pina/domain/union_domain.py | 5 +- pina/label_tensor.py | 244 +++++++++++++----- pina/operators.py | 75 +++--- pina/problem/abstract_problem.py | 15 +- tests/test_condition.py | 22 +- tests/test_geometry/test_simplex.py | 1 - .../test_label_tensor.py | 126 ++++++--- .../test_label_tensor/test_label_tensor_01.py | 117 +++++++++ tests/test_operators.py | 8 +- tests/test_problem.py | 136 ++++++---- 19 files changed, 585 insertions(+), 224 deletions(-) rename tests/{ => test_label_tensor}/test_label_tensor.py (56%) create mode 100644 tests/test_label_tensor/test_label_tensor_01.py diff --git a/pina/collector.py b/pina/collector.py index fa3247e2e..0f4e9da44 100644 --- a/pina/collector.py +++ b/pina/collector.py @@ -1,3 +1,6 @@ +from sympy.strategies.branch import condition + +from . import LabelTensor from .utils import check_consistency, merge_tensors class Collector: @@ -51,7 +54,7 @@ def store_sample_domains(self, n, mode, variables, sample_locations): already_sampled = [] # if we have sampled the condition but not all variables else: - already_sampled = [self.data_collections[loc].input_points] + already_sampled = [self.data_collections[loc]['input_points']] # if the condition is ready but we want to sample again else: self.is_conditions_ready[loc] = False @@ -63,10 +66,24 @@ def store_sample_domains(self, n, mode, variables, sample_locations): ] + already_sampled pts = merge_tensors(samples) if ( - sorted(self.data_collections[loc].input_points.labels) - == - sorted(self.problem.input_variables) + set(pts.labels).issubset(sorted(self.problem.input_variables)) ): - self.is_conditions_ready[loc] = True + pts = pts.sort_labels() + if sorted(pts.labels)==sorted(self.problem.input_variables): + self.is_conditions_ready[loc] = True values = [pts, condition.equation] - self.data_collections[loc] = dict(zip(keys, values)) \ No newline at end of file + self.data_collections[loc] = dict(zip(keys, values)) + else: + raise RuntimeError('Try to sample variables which are not in problem defined in the problem') + + def add_points(self, new_points_dict): + """ + Add input points to a sampled condition + + :param new_points_dict: Dictonary of input points (condition_name: LabelTensor) + :raises RuntimeError: if at least one condition is not already sampled + """ + for k,v in new_points_dict.items(): + if not self.is_conditions_ready[k]: + raise RuntimeError('Cannot add points on a non sampled condition') + self.data_collections[k]['input_points'] = self.data_collections[k]['input_points'].vstack(v) \ No newline at end of file diff --git a/pina/condition/data_condition.py b/pina/condition/data_condition.py index b9fe1ede1..d5ac63970 100644 --- a/pina/condition/data_condition.py +++ b/pina/condition/data_condition.py @@ -27,4 +27,6 @@ def __init__(self, data, conditionalvariable=None): def __setattr__(self, key, value): if (key == 'data') or (key == 'conditionalvariable'): check_consistency(value, (LabelTensor, Graph, torch.Tensor)) - DataConditionInterface.__dict__[key].__set__(self, value) \ No newline at end of file + DataConditionInterface.__dict__[key].__set__(self, value) + elif key in ('_condition_type', '_problem', 'problem', 'condition_type'): + super().__setattr__(key, value) \ No newline at end of file diff --git a/pina/condition/domain_equation_condition.py b/pina/condition/domain_equation_condition.py index f0ef8e07d..ab35d202a 100644 --- a/pina/condition/domain_equation_condition.py +++ b/pina/condition/domain_equation_condition.py @@ -28,4 +28,6 @@ def __setattr__(self, key, value): DomainEquationCondition.__dict__[key].__set__(self, value) elif key == 'equation': check_consistency(value, (EquationInterface)) - DomainEquationCondition.__dict__[key].__set__(self, value) \ No newline at end of file + DomainEquationCondition.__dict__[key].__set__(self, value) + elif key in ('_condition_type', '_problem', 'problem', 'condition_type'): + super().__setattr__(key, value) \ No newline at end of file diff --git a/pina/condition/input_equation_condition.py b/pina/condition/input_equation_condition.py index f77b025dc..dc12d0225 100644 --- a/pina/condition/input_equation_condition.py +++ b/pina/condition/input_equation_condition.py @@ -21,7 +21,7 @@ def __init__(self, input_points, equation): super().__init__() self.input_points = input_points self.equation = equation - self.condition_type = 'physics' + self._condition_type = 'physics' def __setattr__(self, key, value): if key == 'input_points': @@ -29,4 +29,6 @@ def __setattr__(self, key, value): InputPointsEquationCondition.__dict__[key].__set__(self, value) elif key == 'equation': check_consistency(value, (EquationInterface)) - InputPointsEquationCondition.__dict__[key].__set__(self, value) \ No newline at end of file + InputPointsEquationCondition.__dict__[key].__set__(self, value) + elif key in ('_condition_type', '_problem', 'problem', 'condition_type'): + super().__setattr__(key, value) \ No newline at end of file diff --git a/pina/condition/input_output_condition.py b/pina/condition/input_output_condition.py index 70388b308..a4fa48919 100644 --- a/pina/condition/input_output_condition.py +++ b/pina/condition/input_output_condition.py @@ -26,4 +26,6 @@ def __init__(self, input_points, output_points): def __setattr__(self, key, value): if (key == 'input_points') or (key == 'output_points'): check_consistency(value, (LabelTensor, Graph, torch.Tensor)) - InputOutputPointsCondition.__dict__[key].__set__(self, value) \ No newline at end of file + InputOutputPointsCondition.__dict__[key].__set__(self, value) + elif key in ('_condition_type', '_problem', 'problem', 'condition_type'): + super().__setattr__(key, value) diff --git a/pina/domain/difference_domain.py b/pina/domain/difference_domain.py index 9554aaf32..4015a3860 100644 --- a/pina/domain/difference_domain.py +++ b/pina/domain/difference_domain.py @@ -77,7 +77,7 @@ def sample(self, n, mode="random", variables="all"): 5 """ - if mode != self.sample_modes: + if mode not in self.sample_modes: raise NotImplementedError( f"{mode} is not a valid mode for sampling." ) diff --git a/pina/domain/exclusion_domain.py b/pina/domain/exclusion_domain.py index 4fc582cef..a05b1543e 100644 --- a/pina/domain/exclusion_domain.py +++ b/pina/domain/exclusion_domain.py @@ -76,7 +76,7 @@ def sample(self, n, mode="random", variables="all"): 5 """ - if mode != self.sample_modes: + if mode not in self.sample_modes: raise NotImplementedError( f"{mode} is not a valid mode for sampling." ) diff --git a/pina/domain/intersection_domain.py b/pina/domain/intersection_domain.py index b580f21c3..bb0499b50 100644 --- a/pina/domain/intersection_domain.py +++ b/pina/domain/intersection_domain.py @@ -78,7 +78,7 @@ def sample(self, n, mode="random", variables="all"): 5 """ - if mode != self.sample_modes: + if mode not in self.sample_modes: raise NotImplementedError( f"{mode} is not a valid mode for sampling." ) diff --git a/pina/domain/simplex.py b/pina/domain/simplex.py index d6abe5e9e..96cc36c0f 100644 --- a/pina/domain/simplex.py +++ b/pina/domain/simplex.py @@ -92,13 +92,12 @@ def _build_cartesian(self, vertices): """ span_dict = {} - for i, coord in enumerate(self.variables): - sorted_vertices = sorted(vertices, key=lambda vertex: vertex[i]) + sorted_vertices = torch.sort(vertices[coord].tensor.squeeze()) # respective coord bounded by the lowest and highest values span_dict[coord] = [ - float(sorted_vertices[0][i]), - float(sorted_vertices[-1][i]), + float(sorted_vertices.values[0]), + float(sorted_vertices.values[-1]), ] return CartesianDomain(span_dict) diff --git a/pina/domain/union_domain.py b/pina/domain/union_domain.py index bd7fa56cb..a72115f50 100644 --- a/pina/domain/union_domain.py +++ b/pina/domain/union_domain.py @@ -41,7 +41,10 @@ def sample_modes(self): @property def variables(self): - return list(set([geom.variables for geom in self.geometries])) + variables = [] + for geom in self.geometries: + variables+=geom.variables + return list(set(variables)) def is_inside(self, point, check_border=False): """ diff --git a/pina/label_tensor.py b/pina/label_tensor.py index 08e0b03e6..1df318ec7 100644 --- a/pina/label_tensor.py +++ b/pina/label_tensor.py @@ -1,5 +1,5 @@ """ Module for LabelTensor """ - +from copy import deepcopy, copy import torch from torch import Tensor @@ -35,12 +35,22 @@ def __init__(self, x, labels): {1: {"name": "space"['a', 'b', 'c']) """ + self.dim_names = None self.labels = labels @property def labels(self): """Property decorator for labels + :return: labels of self + :rtype: list + """ + return self._labels[self.tensor.ndim-1]['dof'] + + @property + def full_labels(self): + """Property decorator for labels + :return: labels of self :rtype: list """ @@ -65,6 +75,13 @@ def labels(self, labels): self.update_labels_from_list(labels) else: raise ValueError(f"labels must be list, dict or string.") + self.set_names() + + def set_names(self): + labels = self.full_labels + self.dim_names = {} + for dim in range(self.tensor.ndim): + self.dim_names[labels[dim]['name']] = dim def extract(self, label_to_extract): """ @@ -76,46 +93,63 @@ def extract(self, label_to_extract): :raises TypeError: Labels are not ``str``. :raises ValueError: Label to extract is not in the labels ``list``. """ - from copy import deepcopy if isinstance(label_to_extract, (str, int)): label_to_extract = [label_to_extract] if isinstance(label_to_extract, (tuple, list)): - last_dim_label = self._labels[self.tensor.ndim - 1]['dof'] - if set(label_to_extract).issubset(last_dim_label) is False: - raise ValueError('Cannot extract a dof which is not in the original LabelTensor') - idx_to_extract = [last_dim_label.index(i) for i in label_to_extract] - new_tensor = self.tensor - new_tensor = new_tensor[..., idx_to_extract] - new_labels = deepcopy(self._labels) - last_dim_new_label = {self.tensor.ndim - 1: { - 'dof': label_to_extract, - 'name': self._labels[self.tensor.ndim - 1]['name'] - }} - new_labels.update(last_dim_new_label) + return self._extract_from_list(label_to_extract) elif isinstance(label_to_extract, dict): - new_labels = (deepcopy(self._labels)) - new_tensor = self.tensor - for k, v in label_to_extract.items(): - idx_dim = None - for kl, vl in self._labels.items(): - if vl['name'] == k: - idx_dim = kl - break - dim_labels = self._labels[idx_dim]['dof'] - if isinstance(label_to_extract[k], (int, str)): - label_to_extract[k] = [label_to_extract[k]] - if set(label_to_extract[k]).issubset(dim_labels) is False: - raise ValueError('Cannot extract a dof which is not in the original LabelTensor') - idx_to_extract = [dim_labels.index(i) for i in label_to_extract[k]] - indexer = [slice(None)] * idx_dim + [idx_to_extract] + [slice(None)] * (self.tensor.ndim - idx_dim - 1) - new_tensor = new_tensor[indexer] - dim_new_label = {idx_dim: { - 'dof': label_to_extract[k], - 'name': self._labels[idx_dim]['name'] - }} - new_labels.update(dim_new_label) + return self._extract_from_dict(label_to_extract) else: raise ValueError('labels_to_extract must be str or list or dict') + + def _extract_from_list(self, labels_to_extract): + #Store locally all necessary obj/variables + ndim = self.tensor.ndim + labels = self.full_labels + tensor = self.tensor + last_dim_label = self.labels + + #Verify if all the labels in labels_to_extract are in last dimension + if set(labels_to_extract).issubset(last_dim_label) is False: + raise ValueError('Cannot extract a dof which is not in the original LabelTensor') + + #Extract index to extract + idx_to_extract = [last_dim_label.index(i) for i in labels_to_extract] + + #Perform extraction + new_tensor = tensor[..., idx_to_extract] + + #Manage labels + new_labels = copy(labels) + + last_dim_new_label = {ndim - 1: { + 'dof': list(labels_to_extract), + 'name': labels[ndim - 1]['name'] + }} + new_labels.update(last_dim_new_label) + return LabelTensor(new_tensor, new_labels) + + def _extract_from_dict(self, labels_to_extract): + labels = self.full_labels + tensor = self.tensor + ndim = tensor.ndim + new_labels = deepcopy(labels) + new_tensor = tensor + for k, _ in labels_to_extract.items(): + idx_dim = self.dim_names[k] + dim_labels = labels[idx_dim]['dof'] + if isinstance(labels_to_extract[k], (int, str)): + labels_to_extract[k] = [labels_to_extract[k]] + if set(labels_to_extract[k]).issubset(dim_labels) is False: + raise ValueError('Cannot extract a dof which is not in the original LabelTensor') + idx_to_extract = [dim_labels.index(i) for i in labels_to_extract[k]] + indexer = [slice(None)] * idx_dim + [idx_to_extract] + [slice(None)] * (ndim - idx_dim - 1) + new_tensor = new_tensor[indexer] + dim_new_label = {idx_dim: { + 'dof': labels_to_extract[k], + 'name': labels[idx_dim]['name'] + }} + new_labels.update(dim_new_label) return LabelTensor(new_tensor, new_labels) def __str__(self): @@ -147,31 +181,41 @@ def cat(tensors, dim=0): return [] if len(tensors) == 1: return tensors[0] + new_labels_cat_dim = LabelTensor._check_validity_before_cat(tensors, dim) + + # Perform cat on tensors + new_tensor = torch.cat(tensors, dim=dim) + + #Update labels + labels = tensors[0].full_labels + labels.pop(dim) + new_labels_cat_dim = new_labels_cat_dim if len(set(new_labels_cat_dim)) == len(new_labels_cat_dim) \ + else range(new_tensor.shape[dim]) + labels[dim] = {'dof': new_labels_cat_dim, + 'name': tensors[1].full_labels[dim]['name']} + return LabelTensor(new_tensor, labels) + + @staticmethod + def _check_validity_before_cat(tensors, dim): n_dims = tensors[0].ndim new_labels_cat_dim = [] + # Check if names and dof of the labels are the same in all dimensions except in dim for i in range(n_dims): - name = tensors[0].labels[i]['name'] + name = tensors[0].full_labels[i]['name'] if i != dim: - dof = tensors[0].labels[i]['dof'] + dof = tensors[0].full_labels[i]['dof'] for tensor in tensors: - dof_to_check = tensor.labels[i]['dof'] - name_to_check = tensor.labels[i]['name'] + dof_to_check = tensor.full_labels[i]['dof'] + name_to_check = tensor.full_labels[i]['name'] if dof != dof_to_check or name != name_to_check: raise ValueError('dimensions must have the same dof and name') else: for tensor in tensors: - new_labels_cat_dim += tensor.labels[i]['dof'] - name_to_check = tensor.labels[i]['name'] + new_labels_cat_dim += tensor.full_labels[i]['dof'] + name_to_check = tensor.full_labels[i]['name'] if name != name_to_check: - raise ValueError('dimensions must have the same dof and name') - new_tensor = torch.cat(tensors, dim=dim) - labels = tensors[0].labels - labels.pop(dim) - new_labels_cat_dim = new_labels_cat_dim if len(set(new_labels_cat_dim)) == len(new_labels_cat_dim) \ - else range(new_tensor.shape[dim]) - labels[dim] = {'dof': new_labels_cat_dim, - 'name': tensors[1].labels[dim]['name']} - return LabelTensor(new_tensor, labels) + raise ValueError('Dimensions to concatenate must have the same name') + return new_labels_cat_dim def requires_grad_(self, mode=True): lt = super().requires_grad_(mode) @@ -204,7 +248,6 @@ def clone(self, *args, **kwargs): out = LabelTensor(super().clone(*args, **kwargs), self._labels) return out - def init_labels(self): self._labels = { idx_: { @@ -221,13 +264,14 @@ def update_labels_from_dict(self, labels): :type labels: dict :raises ValueError: dof list contain duplicates or number of dof does not match with tensor shape """ - tensor_shape = self.tensor.shape + #Check dimensionality for k, v in labels.items(): if len(v['dof']) != len(set(v['dof'])): raise ValueError("dof must be unique") if len(v['dof']) != tensor_shape[k]: raise ValueError('Number of dof does not match with tensor dimension') + #Perform update self._labels.update(labels) def update_labels_from_list(self, labels): @@ -237,6 +281,7 @@ def update_labels_from_list(self, labels): :param labels: The label(s) to update. :type labels: list """ + # Create a dict with labels last_dim_labels = {self.tensor.ndim - 1: {'dof': labels, 'name': self.tensor.ndim - 1}} self.update_labels_from_dict(last_dim_labels) @@ -246,26 +291,103 @@ def summation(tensors): raise ValueError('tensors list must not be empty') if len(tensors) == 1: return tensors[0] - labels = tensors[0].labels + # Collect all labels + labels = tensors[0].full_labels + # Check labels of all the tensors in each dimension for j in range(tensors[0].ndim): for i in range(1, len(tensors)): - if labels[j] != tensors[i].labels[j]: + if labels[j] != tensors[i].full_labels[j]: labels.pop(j) break - + # Sum tensors data = torch.zeros(tensors[0].tensor.shape) for i in range(len(tensors)): data += tensors[i].tensor new_tensor = LabelTensor(data, labels) return new_tensor - def last_dim_dof(self): - return self._labels[self.tensor.ndim - 1]['dof'] - def append(self, tensor, mode='std'): - print(self.labels) - print(tensor.labels) if mode == 'std': + # Call cat on last dimension new_label_tensor = LabelTensor.cat([self, tensor], dim=self.tensor.ndim - 1) - + elif mode=='cross': + # Crete tensor and call cat on last dimension + tensor1 = self + tensor2 = tensor + n1 = tensor1.shape[0] + n2 = tensor2.shape[0] + tensor1 = LabelTensor(tensor1.repeat(n2, 1), labels=tensor1.labels) + tensor2 = LabelTensor(tensor2.repeat_interleave(n1, dim=0), labels=tensor2.labels) + new_label_tensor = LabelTensor.cat([tensor1, tensor2], dim=self.tensor.ndim-1) + else: + raise ValueError('mode must be either "std" or "cross"') return new_label_tensor + + @staticmethod + def vstack(label_tensors): + """ + Stack tensors vertically. For more details, see + :meth:`torch.vstack`. + + :param list(LabelTensor) label_tensors: the tensors to stack. They need + to have equal labels. + :return: the stacked tensor + :rtype: LabelTensor + """ + return LabelTensor.cat(label_tensors, dim=0) + + def __getitem__(self, index): + """ + Return a copy of the selected tensor. + """ + + if isinstance(index, str) or (isinstance(index, (tuple, list)) and all(isinstance(a, str) for a in index)): + return self.extract(index) + + selected_lt = super().__getitem__(index) + + try: + len_index = len(index) + except TypeError: + len_index = 1 + + if isinstance(index, int) or len_index == 1: + if selected_lt.ndim == 1: + selected_lt = selected_lt.reshape(1, -1) + if hasattr(self, "labels"): + new_labels = deepcopy(self.full_labels) + new_labels.pop(0) + selected_lt.labels = new_labels + elif len(index) == self.tensor.ndim: + new_labels = deepcopy(self.full_labels) + if selected_lt.ndim == 1: + selected_lt = selected_lt.reshape(-1, 1) + for j in range(selected_lt.ndim): + if hasattr(self, "labels"): + if isinstance(index[j], list): + new_labels.update({j: {'dof': [new_labels[j]['dof'][i] for i in index[1]], + 'name':new_labels[j]['name']}}) + else: + new_labels.update({j: {'dof': new_labels[j]['dof'][index[j]], + 'name':new_labels[j]['name']}}) + + selected_lt.labels = new_labels + else: + new_labels = deepcopy(self.full_labels) + new_labels.update({0: {'dof': list[index], 'name': new_labels[0]['name']}}) + selected_lt.labels = self.labels + + return selected_lt + + def sort_labels(self, dim=None): + def argsort(lst): + return sorted(range(len(lst)), key=lambda x: lst[x]) + if dim is None: + dim = self.tensor.ndim-1 + labels = self.full_labels[dim]['dof'] + sorted_index = argsort(labels) + indexer = [slice(None)] * self.tensor.ndim + indexer[dim] = sorted_index + new_labels = deepcopy(self.full_labels) + new_labels[dim] = {'dof': sorted(labels), 'name': new_labels[dim]['name']} + return LabelTensor(self.tensor[indexer], new_labels) \ No newline at end of file diff --git a/pina/operators.py b/pina/operators.py index 58dcdff37..fa32f2921 100644 --- a/pina/operators.py +++ b/pina/operators.py @@ -1,13 +1,11 @@ """ Module for operators vectorize implementation. Differential operators are used to write any differential problem. -These operators are implemented to work on different accelerators: CPU, GPU, TPU or MPS. +These operators are implemented to work on different accellerators: CPU, GPU, TPU or MPS. All operators take as input a tensor onto which computing the operator, a tensor with respect to which computing the operator, the name of the output variables to calculate the operator for (in case of multidimensional functions), and the variables name on which the operator is calculated. """ - import torch -from copy import deepcopy from pina.label_tensor import LabelTensor @@ -49,12 +47,12 @@ def grad_scalar_output(output_, input_, d): :rtype: LabelTensor """ - if len(output_.labels[output_.tensor.ndim-1]['dof']) != 1: + if len(output_.labels) != 1: raise RuntimeError("only scalar function can be differentiated") - if not all([di in input_.labels[input_.tensor.ndim-1]['dof'] for di in d]): + if not all([di in input_.labels for di in d]): raise RuntimeError("derivative labels missing from input tensor") - output_fieldname = output_.labels[output_.ndim-1]['dof'][0] + output_fieldname = output_.labels[0] gradients = torch.autograd.grad( output_, input_, @@ -65,35 +63,37 @@ def grad_scalar_output(output_, input_, d): retain_graph=True, allow_unused=True, )[0] - new_labels = deepcopy(input_.labels) - gradients.labels = new_labels + + gradients.labels = input_.labels gradients = gradients.extract(d) - new_labels[input_.tensor.ndim - 1]['dof'] = [f"d{output_fieldname}d{i}" for i in d] - gradients.labels = new_labels + gradients.labels = [f"d{output_fieldname}d{i}" for i in d] + return gradients if not isinstance(input_, LabelTensor): raise TypeError + if d is None: - d = input_.labels[input_.tensor.ndim-1]['dof'] + d = input_.labels if components is None: - components = output_.labels[output_.tensor.ndim-1]['dof'] + components = output_.labels - if output_.shape[output_.ndim-1] == 1: # scalar output ################################ + if output_.shape[1] == 1: # scalar output ################################ - if components != output_.labels[output_.tensor.ndim-1]['dof']: + if components != output_.labels: raise RuntimeError gradients = grad_scalar_output(output_, input_, d) - elif output_.shape[output_.ndim-1] >= 2: # vector output ############################## + elif output_.shape[output_.ndim - 1] >= 2: # vector output ############################## tensor_to_cat = [] for i, c in enumerate(components): c_output = output_.extract([c]) tensor_to_cat.append(grad_scalar_output(c_output, input_, d)) - gradients = LabelTensor.cat(tensor_to_cat, dim=output_.tensor.ndim-1) + gradients = LabelTensor.cat(tensor_to_cat, dim=output_.tensor.ndim - 1) else: raise NotImplementedError + return gradients @@ -124,30 +124,27 @@ def div(output_, input_, components=None, d=None): raise TypeError if d is None: - d = input_.labels[input_.tensor.ndim-1]['dof'] + d = input_.labels if components is None: - components = output_.labels[output_.tensor.ndim-1]['dof'] + components = output_.labels - if output_.shape[output_.ndim-1] < 2 or len(components) < 2: + if output_.shape[1] < 2 or len(components) < 2: raise ValueError("div supported only for vector fields") if len(components) != len(d): raise ValueError grad_output = grad(output_, input_, components, d) - last_dim_dof = [None] * len(components) - to_sum_tensors = [] + labels = [None] * len(components) + tensors_to_sum = [] for i, (c, d) in enumerate(zip(components, d)): c_fields = f"d{c}d{d}" - last_dim_dof[i] = c_fields - to_sum_tensors.append(grad_output.extract(c_fields)) - - div = LabelTensor.summation(to_sum_tensors) - new_labels = deepcopy(input_.labels) - new_labels[input_.tensor.ndim-1]['dof'] = ["+".join(last_dim_dof)] - div.labels = new_labels - return div + tensors_to_sum.append(grad_output.extract(c_fields)) + labels[i] = c_fields + div_result = LabelTensor.summation(tensors_to_sum) + div_result.labels = ["+".join(labels)] + return div_result def laplacian(output_, input_, components=None, d=None, method="std"): @@ -173,10 +170,10 @@ def laplacian(output_, input_, components=None, d=None, method="std"): :rtype: LabelTensor """ if d is None: - d = input_.labels[input_.tensor.ndim-1]['dof'] + d = input_.labels if components is None: - components = output_.labels[output_.tensor.ndim-1]['dof'] + components = output_.labels if len(components) != len(d) and len(components) != 1: raise ValueError @@ -191,28 +188,34 @@ def laplacian(output_, input_, components=None, d=None, method="std"): if len(components) == 1: grad_output = grad(output_, input_, components=components, d=d) to_append_tensors = [] - for i, label in enumerate(grad_output.labels[grad_output.ndim-1]['dof']): + for i, label in enumerate(grad_output.labels): gg = grad(grad_output, input_, d=d, components=[label]) - to_append_tensors.append(gg.extract([gg.labels[gg.tensor.ndim-1]['dof'][i]])) + to_append_tensors.append(gg.extract([gg.labels[i]])) labels = [f"dd{components[0]}"] result = LabelTensor.summation(tensors=to_append_tensors) result.labels = labels else: + result = torch.empty( + input_.shape[0], len(components), device=output_.device + ) labels = [None] * len(components) to_append_tensors = [None] * len(components) for idx, (ci, di) in enumerate(zip(components, d)): + if not isinstance(ci, list): ci = [ci] if not isinstance(di, list): di = [di] + grad_output = grad(output_, input_, components=ci, d=di) + result[:, idx] = grad(grad_output, input_, d=di).flatten() to_append_tensors[idx] = grad(grad_output, input_, d=di) labels[idx] = f"dd{ci[0]}dd{di[0]}" result = LabelTensor.cat(tensors=to_append_tensors, dim=output_.tensor.ndim-1) result.labels = labels return result -# TODO Fix advection operator + def advection(output_, input_, velocity_field, components=None, d=None): """ Perform advection operation. The operator works for vectorial functions, @@ -234,10 +237,10 @@ def advection(output_, input_, velocity_field, components=None, d=None): :rtype: LabelTensor """ if d is None: - d = input_.labels[input_.tensor.ndim-1]['dof'] + d = input_.labels if components is None: - components = output_.labels[output_.tensor.ndim-1]['dof'] + components = output_.labels tmp = ( grad(output_, input_, components, d) diff --git a/pina/problem/abstract_problem.py b/pina/problem/abstract_problem.py index 78464d22e..0e4e1e2c6 100644 --- a/pina/problem/abstract_problem.py +++ b/pina/problem/abstract_problem.py @@ -36,7 +36,15 @@ def __init__(self): @property def input_pts(self): - return self.collector.data_collections + to_return = {} + for k, v in self.collector.data_collections.items(): + if 'input_points' in v.keys(): + to_return[k] = v['input_points'] + return to_return + + @property + def _have_sampled_points(self): + return self.collector.is_conditions_ready def __deepcopy__(self, memo): """ @@ -164,4 +172,7 @@ def discretise_domain( ) # store data - self.collector.store_sample_domains(n, mode, variables, locations) \ No newline at end of file + self.collector.store_sample_domains(n, mode, variables, locations) + + def add_points(self, new_points_dict): + self.collector.add_points(new_points_dict) \ No newline at end of file diff --git a/tests/test_condition.py b/tests/test_condition.py index 5f1c6236b..f12979dc8 100644 --- a/tests/test_condition.py +++ b/tests/test_condition.py @@ -18,27 +18,27 @@ def test_init_inputoutput(): Condition(input_points=example_input_pts, output_points=example_output_pts) with pytest.raises(ValueError): Condition(example_input_pts, example_output_pts) - with pytest.raises(TypeError): + with pytest.raises(ValueError): Condition(input_points=3., output_points='example') - with pytest.raises(TypeError): + with pytest.raises(ValueError): Condition(input_points=example_domain, output_points=example_domain) +test_init_inputoutput() - -def test_init_locfunc(): - Condition(location=example_domain, equation=FixedValue(0.0)) +def test_init_domainfunc(): + Condition(domain=example_domain, equation=FixedValue(0.0)) with pytest.raises(ValueError): Condition(example_domain, FixedValue(0.0)) - with pytest.raises(TypeError): - Condition(location=3., equation='example') - with pytest.raises(TypeError): - Condition(location=example_input_pts, equation=example_output_pts) + with pytest.raises(ValueError): + Condition(domain=3., equation='example') + with pytest.raises(ValueError): + Condition(domain=example_input_pts, equation=example_output_pts) def test_init_inputfunc(): Condition(input_points=example_input_pts, equation=FixedValue(0.0)) with pytest.raises(ValueError): Condition(example_domain, FixedValue(0.0)) - with pytest.raises(TypeError): + with pytest.raises(ValueError): Condition(input_points=3., equation='example') - with pytest.raises(TypeError): + with pytest.raises(ValueError): Condition(input_points=example_domain, equation=example_output_pts) diff --git a/tests/test_geometry/test_simplex.py b/tests/test_geometry/test_simplex.py index 7fc34ce2d..25224aae3 100644 --- a/tests/test_geometry/test_simplex.py +++ b/tests/test_geometry/test_simplex.py @@ -40,7 +40,6 @@ def test_constructor(): LabelTensor(torch.tensor([[-.5, .5]]), labels=["x", "y"]), ]) - def test_sample(): # sampling inside simplex = SimplexDomain([ diff --git a/tests/test_label_tensor.py b/tests/test_label_tensor/test_label_tensor.py similarity index 56% rename from tests/test_label_tensor.py rename to tests/test_label_tensor/test_label_tensor.py index 6ef484f0b..1165594db 100644 --- a/tests/test_label_tensor.py +++ b/tests/test_label_tensor/test_label_tensor.py @@ -2,7 +2,6 @@ import pytest from pina.label_tensor import LabelTensor -#import pina data = torch.rand((20, 3)) labels_column = { @@ -22,8 +21,7 @@ @pytest.mark.parametrize("labels", [labels_column, labels_row, labels_all, labels_list]) def test_constructor(labels): - LabelTensor(data, labels) - + print(LabelTensor(data, labels)) def test_wrong_constructor(): with pytest.raises(ValueError): @@ -92,7 +90,7 @@ def test_extract_3D(): )) assert tensor2.ndim == tensor.ndim assert tensor2.shape == tensor.shape - assert tensor.labels == tensor2.labels + assert tensor.full_labels == tensor2.full_labels assert new.shape != tensor.shape def test_concatenation_3D(): @@ -104,9 +102,9 @@ def test_concatenation_3D(): lt2 = LabelTensor(data_2, labels_2) lt_cat = LabelTensor.cat([lt1, lt2]) assert lt_cat.shape == (70, 3, 4) - assert lt_cat.labels[0]['dof'] == range(70) - assert lt_cat.labels[1]['dof'] == range(3) - assert lt_cat.labels[2]['dof'] == ['x', 'y', 'z', 'w'] + assert lt_cat.full_labels[0]['dof'] == range(70) + assert lt_cat.full_labels[1]['dof'] == range(3) + assert lt_cat.full_labels[2]['dof'] == ['x', 'y', 'z', 'w'] data_1 = torch.rand(20, 3, 4) labels_1 = ['x', 'y', 'z', 'w'] @@ -116,9 +114,9 @@ def test_concatenation_3D(): lt2 = LabelTensor(data_2, labels_2) lt_cat = LabelTensor.cat([lt1, lt2], dim=1) assert lt_cat.shape == (20, 5, 4) - assert lt_cat.labels[0]['dof'] == range(20) - assert lt_cat.labels[1]['dof'] == range(5) - assert lt_cat.labels[2]['dof'] == ['x', 'y', 'z', 'w'] + assert lt_cat.full_labels[0]['dof'] == range(20) + assert lt_cat.full_labels[1]['dof'] == range(5) + assert lt_cat.full_labels[2]['dof'] == ['x', 'y', 'z', 'w'] data_1 = torch.rand(20, 3, 2) labels_1 = ['x', 'y'] @@ -128,9 +126,9 @@ def test_concatenation_3D(): lt2 = LabelTensor(data_2, labels_2) lt_cat = LabelTensor.cat([lt1, lt2], dim=2) assert lt_cat.shape == (20, 3, 5) - assert lt_cat.labels[2]['dof'] == ['x', 'y', 'z', 'w', 'a'] - assert lt_cat.labels[0]['dof'] == range(20) - assert lt_cat.labels[1]['dof'] == range(3) + assert lt_cat.full_labels[2]['dof'] == ['x', 'y', 'z', 'w', 'a'] + assert lt_cat.full_labels[0]['dof'] == range(20) + assert lt_cat.full_labels[1]['dof'] == range(3) data_1 = torch.rand(20, 2, 4) labels_1 = ['x', 'y', 'z', 'w'] @@ -140,7 +138,6 @@ def test_concatenation_3D(): lt2 = LabelTensor(data_2, labels_2) with pytest.raises(ValueError): LabelTensor.cat([lt1, lt2], dim=2) - data_1 = torch.rand(20, 3, 2) labels_1 = ['x', 'y'] lt1 = LabelTensor(data_1, labels_1) @@ -149,9 +146,9 @@ def test_concatenation_3D(): lt2 = LabelTensor(data_2, labels_2) lt_cat = LabelTensor.cat([lt1, lt2], dim=2) assert lt_cat.shape == (20, 3, 5) - assert lt_cat.labels[2]['dof'] == range(5) - assert lt_cat.labels[0]['dof'] == range(20) - assert lt_cat.labels[1]['dof'] == range(3) + assert lt_cat.full_labels[2]['dof'] == range(5) + assert lt_cat.full_labels[0]['dof'] == range(20) + assert lt_cat.full_labels[1]['dof'] == range(3) def test_summation(): @@ -165,7 +162,7 @@ def test_summation(): assert lt_sum.ndim == lt_sum.ndim assert lt_sum.shape[0] == 20 assert lt_sum.shape[1] == 3 - assert lt_sum.labels == labels_all + assert lt_sum.full_labels == labels_all assert torch.eq(lt_sum.tensor, torch.ones(20,3)*2).all() lt1 = LabelTensor(torch.ones(20,3), labels_all) lt2 = LabelTensor(torch.ones(20,3), labels_all) @@ -174,29 +171,92 @@ def test_summation(): assert lt_sum.ndim == lt_sum.ndim assert lt_sum.shape[0] == 20 assert lt_sum.shape[1] == 3 - assert lt_sum.labels == labels_all + assert lt_sum.full_labels == labels_all assert torch.eq(lt_sum.tensor, torch.ones(20,3)*2).all() def test_append_3D(): - data_1 = torch.rand(20, 3, 4) - labels_1 = ['x', 'y', 'z', 'w'] + data_1 = torch.rand(20, 3, 2) + labels_1 = ['x', 'y'] lt1 = LabelTensor(data_1, labels_1) - data_2 = torch.rand(50, 3, 4) - labels_2 = ['x', 'y', 'z', 'w'] + data_2 = torch.rand(20, 3, 2) + labels_2 = ['z', 'w'] lt2 = LabelTensor(data_2, labels_2) lt1 = lt1.append(lt2) - assert lt1.shape == (70, 3, 4) - assert lt1.labels[0]['dof'] == range(70) - assert lt1.labels[1]['dof'] == range(3) - assert lt1.labels[2]['dof'] == ['x', 'y', 'z', 'w'] - data_1 = torch.rand(20, 3, 2) + assert lt1.shape == (20, 3, 4) + assert lt1.full_labels[0]['dof'] == range(20) + assert lt1.full_labels[1]['dof'] == range(3) + assert lt1.full_labels[2]['dof'] == ['x', 'y', 'z', 'w'] + +def test_append_2D(): + data_1 = torch.rand(20, 2) labels_1 = ['x', 'y'] lt1 = LabelTensor(data_1, labels_1) - data_2 = torch.rand(20, 3, 2) + data_2 = torch.rand(20, 2) labels_2 = ['z', 'w'] lt2 = LabelTensor(data_2, labels_2) lt1 = lt1.append(lt2, mode='cross') - assert lt1.shape == (20, 3, 4) - assert lt1.labels[0]['dof'] == range(20) - assert lt1.labels[1]['dof'] == range(3) - assert lt1.labels[2]['dof'] == ['x', 'y', 'z', 'w'] + assert lt1.shape == (400, 4) + assert lt1.full_labels[0]['dof'] == range(400) + assert lt1.full_labels[1]['dof'] == ['x', 'y', 'z', 'w'] + +def test_vstack_3D(): + data_1 = torch.rand(20, 3, 2) + labels_1 = {1:{'dof': ['a', 'b', 'c'], 'name': 'first'}, 2: {'dof': ['x', 'y'], 'name': 'second'}} + lt1 = LabelTensor(data_1, labels_1) + data_2 = torch.rand(20, 3, 2) + labels_1 = {1:{'dof': ['a', 'b', 'c'], 'name': 'first'}, 2: {'dof': ['x', 'y'], 'name': 'second'}} + lt2 = LabelTensor(data_2, labels_1) + lt_stacked = LabelTensor.vstack([lt1, lt2]) + assert lt_stacked.shape == (40, 3, 2) + assert lt_stacked.full_labels[0]['dof'] == range(40) + assert lt_stacked.full_labels[1]['dof'] == ['a', 'b', 'c'] + assert lt_stacked.full_labels[2]['dof'] == ['x', 'y'] + assert lt_stacked.full_labels[1]['name'] == 'first' + assert lt_stacked.full_labels[2]['name'] == 'second' + +def test_vstack_2D(): + data_1 = torch.rand(20, 2) + labels_1 = { 1: {'dof': ['x', 'y'], 'name': 'second'}} + lt1 = LabelTensor(data_1, labels_1) + data_2 = torch.rand(20, 2) + labels_1 = { 1: {'dof': ['x', 'y'], 'name': 'second'}} + lt2 = LabelTensor(data_2, labels_1) + lt_stacked = LabelTensor.vstack([lt1, lt2]) + assert lt_stacked.shape == (40, 2) + assert lt_stacked.full_labels[0]['dof'] == range(40) + assert lt_stacked.full_labels[1]['dof'] == ['x', 'y'] + assert lt_stacked.full_labels[0]['name'] == 0 + assert lt_stacked.full_labels[1]['name'] == 'second' + +def test_sorting(): + data = torch.ones(20, 5) + data[:,0] = data[:,0]*4 + data[:,1] = data[:,1]*2 + data[:,2] = data[:,2] + data[:,3] = data[:,3]*5 + data[:,4] = data[:,4]*3 + labels = ['d', 'b', 'a', 'e', 'c'] + lt_data = LabelTensor(data, labels) + lt_sorted = LabelTensor.sort_labels(lt_data) + assert lt_sorted.shape == (20,5) + assert lt_sorted.labels == ['a', 'b', 'c', 'd', 'e'] + assert torch.eq(lt_sorted.tensor[:,0], torch.ones(20) * 1).all() + assert torch.eq(lt_sorted.tensor[:,1], torch.ones(20) * 2).all() + assert torch.eq(lt_sorted.tensor[:,2], torch.ones(20) * 3).all() + assert torch.eq(lt_sorted.tensor[:,3], torch.ones(20) * 4).all() + assert torch.eq(lt_sorted.tensor[:,4], torch.ones(20) * 5).all() + + data = torch.ones(20, 4, 5) + data[:,0,:] = data[:,0]*4 + data[:,1,:] = data[:,1]*2 + data[:,2,:] = data[:,2] + data[:,3,:] = data[:,3]*3 + labels = {1: {'dof': ['d', 'b', 'a', 'c'], 'name': 1}} + lt_data = LabelTensor(data, labels) + lt_sorted = LabelTensor.sort_labels(lt_data, dim=1) + assert lt_sorted.shape == (20,4, 5) + assert lt_sorted.full_labels[1]['dof'] == ['a', 'b', 'c', 'd'] + assert torch.eq(lt_sorted.tensor[:,0,:], torch.ones(20,5) * 1).all() + assert torch.eq(lt_sorted.tensor[:,1,:], torch.ones(20,5) * 2).all() + assert torch.eq(lt_sorted.tensor[:,2,:], torch.ones(20,5) * 3).all() + assert torch.eq(lt_sorted.tensor[:,3,:], torch.ones(20,5) * 4).all() diff --git a/tests/test_label_tensor/test_label_tensor_01.py b/tests/test_label_tensor/test_label_tensor_01.py new file mode 100644 index 000000000..a2e129d94 --- /dev/null +++ b/tests/test_label_tensor/test_label_tensor_01.py @@ -0,0 +1,117 @@ +import torch +import pytest + +from pina import LabelTensor + +data = torch.rand((20, 3)) +labels = ['a', 'b', 'c'] + + +def test_constructor(): + LabelTensor(data, labels) + + +def test_wrong_constructor(): + with pytest.raises(ValueError): + LabelTensor(data, ['a', 'b']) + + +def test_labels(): + tensor = LabelTensor(data, labels) + assert isinstance(tensor, torch.Tensor) + assert tensor.labels == labels + with pytest.raises(ValueError): + tensor.labels = labels[:-1] + + +def test_extract(): + label_to_extract = ['a', 'c'] + tensor = LabelTensor(data, labels) + new = tensor.extract(label_to_extract) + assert new.labels == label_to_extract + assert new.shape[1] == len(label_to_extract) + assert torch.all(torch.isclose(data[:, 0::2], new)) + + +def test_extract_onelabel(): + label_to_extract = ['a'] + tensor = LabelTensor(data, labels) + new = tensor.extract(label_to_extract) + assert new.ndim == 2 + assert new.labels == label_to_extract + assert new.shape[1] == len(label_to_extract) + assert torch.all(torch.isclose(data[:, 0].reshape(-1, 1), new)) + + +def test_wrong_extract(): + label_to_extract = ['a', 'cc'] + tensor = LabelTensor(data, labels) + with pytest.raises(ValueError): + tensor.extract(label_to_extract) + + +def test_extract_order(): + label_to_extract = ['c', 'a'] + tensor = LabelTensor(data, labels) + new = tensor.extract(label_to_extract) + expected = torch.cat( + (data[:, 2].reshape(-1, 1), data[:, 0].reshape(-1, 1)), + dim=1) + assert new.labels == label_to_extract + assert new.shape[1] == len(label_to_extract) + assert torch.all(torch.isclose(expected, new)) + + +def test_merge(): + tensor = LabelTensor(data, labels) + tensor_a = tensor.extract('a') + tensor_b = tensor.extract('b') + tensor_c = tensor.extract('c') + + tensor_bc = tensor_b.append(tensor_c) + assert torch.allclose(tensor_bc, tensor.extract(['b', 'c'])) + + +def test_merge2(): + tensor = LabelTensor(data, labels) + tensor_b = tensor.extract('b') + tensor_c = tensor.extract('c') + + tensor_bc = tensor_b.append(tensor_c) + assert torch.allclose(tensor_bc, tensor.extract(['b', 'c'])) + + +def test_getitem(): + tensor = LabelTensor(data, labels) + tensor_view = tensor['a'] + assert tensor_view.labels == ['a'] + assert torch.allclose(tensor_view.flatten(), data[:, 0]) + + tensor_view = tensor['a', 'c'] + assert tensor_view.labels == ['a', 'c'] + assert torch.allclose(tensor_view, data[:, 0::2]) + +def test_getitem2(): + tensor = LabelTensor(data, labels) + tensor_view = tensor[:5] + assert tensor_view.labels == labels + assert torch.allclose(tensor_view, data[:5]) + + idx = torch.randperm(tensor.shape[0]) + tensor_view = tensor[idx] + assert tensor_view.labels == labels + +def test_slice(): + tensor = LabelTensor(data, labels) + tensor_view = tensor[:5, :2] + assert tensor_view.labels == labels[:2] + assert torch.allclose(tensor_view, data[:5, :2]) + + tensor_view2 = tensor[3] + + assert tensor_view2.labels == labels + assert torch.allclose(tensor_view2, data[3]) + + tensor_view3 = tensor[:, 2] + assert tensor_view3.labels == labels[2] + assert torch.allclose(tensor_view3, data[:, 2].reshape(-1, 1)) \ No newline at end of file diff --git a/tests/test_operators.py b/tests/test_operators.py index 600dfa8a6..975f71523 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -26,13 +26,13 @@ def func_scalar(x): def test_grad_scalar_output(): grad_tensor_s = grad(tensor_s, inp) assert grad_tensor_s.shape == inp.shape - assert grad_tensor_s.labels[grad_tensor_s.ndim-1]['dof'] == [ - f'd{tensor_s.labels[tensor_s.ndim-1]["dof"][0]}d{i}' for i in inp.labels[inp.ndim-1]['dof'] + assert grad_tensor_s.labels == [ + f'd{tensor_s.labels[0]}d{i}' for i in inp.labels ] grad_tensor_s = grad(tensor_s, inp, d=['x', 'y']) assert grad_tensor_s.shape == (20, 2) - assert grad_tensor_s.labels[grad_tensor_s.ndim-1]['dof'] == [ - f'd{tensor_s.labels[tensor_s.ndim-1]["dof"][0]}d{i}' for i in ['x', 'y'] + assert grad_tensor_s.labels == [ + f'd{tensor_s.labels[0]}d{i}' for i in ['x', 'y'] ] def test_grad_vector_output(): diff --git a/tests/test_problem.py b/tests/test_problem.py index 23688b56c..217ad45e9 100644 --- a/tests/test_problem.py +++ b/tests/test_problem.py @@ -27,52 +27,49 @@ class Poisson(SpatialProblem): conditions = { 'gamma1': - Condition(domain=CartesianDomain({ - 'x': [0, 1], - 'y': 1 - }), - equation=FixedValue(0.0)), + Condition(domain=CartesianDomain({ + 'x': [0, 1], + 'y': 1 + }), + equation=FixedValue(0.0)), 'gamma2': - Condition(domain=CartesianDomain({ - 'x': [0, 1], - 'y': 0 - }), - equation=FixedValue(0.0)), + Condition(domain=CartesianDomain({ + 'x': [0, 1], + 'y': 0 + }), + equation=FixedValue(0.0)), 'gamma3': - Condition(domain=CartesianDomain({ - 'x': 1, - 'y': [0, 1] - }), - equation=FixedValue(0.0)), + Condition(domain=CartesianDomain({ + 'x': 1, + 'y': [0, 1] + }), + equation=FixedValue(0.0)), 'gamma4': - Condition(domain=CartesianDomain({ - 'x': 0, - 'y': [0, 1] - }), - equation=FixedValue(0.0)), + Condition(domain=CartesianDomain({ + 'x': 0, + 'y': [0, 1] + }), + equation=FixedValue(0.0)), 'D': - Condition(domain=CartesianDomain({ - 'x': [0, 1], - 'y': [0, 1] - }), - equation=my_laplace), + Condition(domain=CartesianDomain({ + 'x': [0, 1], + 'y': [0, 1] + }), + equation=my_laplace), 'data': - Condition(input_points=in_, output_points=out_) + Condition(input_points=in_, output_points=out_) } def poisson_sol(self, pts): return -(torch.sin(pts.extract(['x']) * torch.pi) * - torch.sin(pts.extract(['y']) * torch.pi)) / (2 * torch.pi**2) + torch.sin(pts.extract(['y']) * torch.pi)) / (2 * torch.pi ** 2) truth_solution = poisson_sol -# make the problem -poisson_problem = Poisson() -print(poisson_problem.input_pts) - def test_discretise_domain(): n = 10 + poisson_problem = Poisson() boundaries = ['gamma1', 'gamma2', 'gamma3', 'gamma4'] poisson_problem.discretise_domain(n, 'grid', locations=boundaries) for b in boundaries: @@ -82,7 +79,7 @@ def test_discretise_domain(): assert poisson_problem.input_pts[b].shape[0] == n poisson_problem.discretise_domain(n, 'grid', locations=['D']) - assert poisson_problem.input_pts['D'].shape[0] == n**2 + assert poisson_problem.input_pts['D'].shape[0] == n ** 2 poisson_problem.discretise_domain(n, 'random', locations=['D']) assert poisson_problem.input_pts['D'].shape[0] == n @@ -93,30 +90,55 @@ def test_discretise_domain(): assert poisson_problem.input_pts['D'].shape[0] == n -# def test_sampling_few_variables(): -# n = 10 -# poisson_problem.discretise_domain(n, -# 'grid', -# locations=['D'], -# variables=['x']) -# assert poisson_problem.input_pts['D'].shape[1] == 1 -# assert poisson_problem._have_sampled_points['D'] is False - - -# def test_sampling_all_args(): -# n = 10 -# poisson_problem.discretise_domain(n, 'grid', locations=['D']) - -# def test_sampling_all_kwargs(): -# n = 10 -# poisson_problem.discretise_domain(n=n, mode='latin', locations=['D']) +def test_sampling_few_variables(): + n = 10 + poisson_problem = Poisson() + poisson_problem.discretise_domain(n, + 'grid', + locations=['D'], + variables=['x']) + assert poisson_problem.input_pts['D'].shape[1] == 1 + assert poisson_problem._have_sampled_points['D'] is False -# def test_sampling_dict(): -# n = 10 -# poisson_problem.discretise_domain( -# {'variables': ['x', 'y'], 'mode': 'grid', 'n': n}, locations=['D']) -# def test_sampling_mixed_args_kwargs(): -# n = 10 -# with pytest.raises(ValueError): -# poisson_problem.discretise_domain(n, mode='latin', locations=['D']) +def test_variables_correct_order_sampling(): + n = 10 + poisson_problem = Poisson() + poisson_problem.discretise_domain(n, + 'grid', + locations=['D'], + variables=['x']) + poisson_problem.discretise_domain(n, + 'grid', + locations=['D'], + variables=['y']) + assert poisson_problem.input_pts['D'].labels == sorted( + poisson_problem.input_variables) + + poisson_problem.discretise_domain(n, + 'grid', + locations=['D']) + assert poisson_problem.input_pts['D'].labels == sorted( + poisson_problem.input_variables) + + poisson_problem.discretise_domain(n, + 'grid', + locations=['D'], + variables=['y']) + poisson_problem.discretise_domain(n, + 'grid', + locations=['D'], + variables=['x']) + assert poisson_problem.input_pts['D'].labels == sorted( + poisson_problem.input_variables) + +def test_add_points(): + poisson_problem = Poisson() + poisson_problem.discretise_domain(0, + 'random', + locations=['D'], + variables=['x', 'y']) + new_pts = LabelTensor(torch.tensor([[0.5, -0.5]]), labels=['x', 'y']) + poisson_problem.add_points({'D': new_pts}) + assert torch.isclose(poisson_problem.input_pts['D'].extract('x'), new_pts.extract('x')) + assert torch.isclose(poisson_problem.input_pts['D'].extract('y'), new_pts.extract('y')) \ No newline at end of file From 1818dc66ec5fbc014201579d8306a56dc07cbf67 Mon Sep 17 00:00:00 2001 From: Dario Coscia Date: Thu, 10 Oct 2024 19:24:46 +0200 Subject: [PATCH 07/14] minor changes/ trainer update --- pina/collector.py | 38 ++++++++++++++------- pina/condition/data_condition.py | 12 +++---- pina/condition/domain_equation_condition.py | 2 +- pina/condition/input_equation_condition.py | 2 +- pina/condition/input_output_condition.py | 2 +- pina/problem/abstract_problem.py | 17 +++++---- pina/trainer.py | 30 +++++++--------- tests/test_problem.py | 14 ++++++-- 8 files changed, 70 insertions(+), 47 deletions(-) diff --git a/pina/collector.py b/pina/collector.py index 0f4e9da44..f44c222a7 100644 --- a/pina/collector.py +++ b/pina/collector.py @@ -5,21 +5,35 @@ class Collector: def __init__(self, problem): - self.problem = problem # hook Collector <-> Problem - self.data_collections = {name : {} for name in self.problem.conditions} # collection of data - self.is_conditions_ready = { - name : False for name in self.problem.conditions} # names of the conditions that need to be sampled - self.full = False # collector full, all points for all conditions are given and the data are ready to be used in trainig + # creating a hook between collector and problem + self.problem = problem + + # this variable is used to store the data in the form: + # {'[condition_name]' : + # {'input_points' : Tensor, + # '[equation/output_points/conditional_variables]': Tensor} + # } + # those variables are used for the dataloading + self._data_collections = {name : {} for name in self.problem.conditions} + + # variables used to check that all conditions are sampled + self._is_conditions_ready = { + name : False for name in self.problem.conditions} + self.full = False @property def full(self): - return all(self.is_conditions_ready.values()) + return all(self._is_conditions_ready.values()) @full.setter def full(self, value): check_consistency(value, bool) self._full = value + @property + def data_collections(self): + return self._data_collections + @property def problem(self): return self._problem @@ -33,13 +47,13 @@ def store_fixed_data(self): for condition_name, condition in self.problem.conditions.items(): # if the condition is not ready and domain is not attribute # of condition, we get and store the data - if (not self.is_conditions_ready[condition_name]) and (not hasattr(condition, "domain")): + if (not self._is_conditions_ready[condition_name]) and (not hasattr(condition, "domain")): # get data keys = condition.__slots__ values = [getattr(condition, name) for name in keys] self.data_collections[condition_name] = dict(zip(keys, values)) # condition now is ready - self.is_conditions_ready[condition_name] = True + self._is_conditions_ready[condition_name] = True def store_sample_domains(self, n, mode, variables, sample_locations): # loop over all locations @@ -48,7 +62,7 @@ def store_sample_domains(self, n, mode, variables, sample_locations): condition = self.problem.conditions[loc] keys = ["input_points", "equation"] # if the condition is not ready, we get and store the data - if (not self.is_conditions_ready[loc]): + if (not self._is_conditions_ready[loc]): # if it is the first time we sample if not self.data_collections[loc]: already_sampled = [] @@ -57,7 +71,7 @@ def store_sample_domains(self, n, mode, variables, sample_locations): already_sampled = [self.data_collections[loc]['input_points']] # if the condition is ready but we want to sample again else: - self.is_conditions_ready[loc] = False + self._is_conditions_ready[loc] = False already_sampled = [] # get the samples @@ -70,7 +84,7 @@ def store_sample_domains(self, n, mode, variables, sample_locations): ): pts = pts.sort_labels() if sorted(pts.labels)==sorted(self.problem.input_variables): - self.is_conditions_ready[loc] = True + self._is_conditions_ready[loc] = True values = [pts, condition.equation] self.data_collections[loc] = dict(zip(keys, values)) else: @@ -84,6 +98,6 @@ def add_points(self, new_points_dict): :raises RuntimeError: if at least one condition is not already sampled """ for k,v in new_points_dict.items(): - if not self.is_conditions_ready[k]: + if not self._is_conditions_ready[k]: raise RuntimeError('Cannot add points on a non sampled condition') self.data_collections[k]['input_points'] = self.data_collections[k]['input_points'].vstack(v) \ No newline at end of file diff --git a/pina/condition/data_condition.py b/pina/condition/data_condition.py index d5ac63970..90d248b67 100644 --- a/pina/condition/data_condition.py +++ b/pina/condition/data_condition.py @@ -13,20 +13,20 @@ class DataConditionInterface(ConditionInterface): distribution """ - __slots__ = ["data", "conditionalvariable"] + __slots__ = ["input_points", "conditional_variables"] - def __init__(self, data, conditionalvariable=None): + def __init__(self, input_points, conditional_variables=None): """ TODO """ super().__init__() - self.data = data - self.conditionalvariable = conditionalvariable + self.input_points = input_points + self.conditional_variables = conditional_variables self.condition_type = 'unsupervised' def __setattr__(self, key, value): - if (key == 'data') or (key == 'conditionalvariable'): + if (key == 'input_points') or (key == 'conditional_variables'): check_consistency(value, (LabelTensor, Graph, torch.Tensor)) DataConditionInterface.__dict__[key].__set__(self, value) - elif key in ('_condition_type', '_problem', 'problem', 'condition_type'): + elif key in ('problem', 'condition_type'): super().__setattr__(key, value) \ No newline at end of file diff --git a/pina/condition/domain_equation_condition.py b/pina/condition/domain_equation_condition.py index ab35d202a..ce4c7d3fc 100644 --- a/pina/condition/domain_equation_condition.py +++ b/pina/condition/domain_equation_condition.py @@ -29,5 +29,5 @@ def __setattr__(self, key, value): elif key == 'equation': check_consistency(value, (EquationInterface)) DomainEquationCondition.__dict__[key].__set__(self, value) - elif key in ('_condition_type', '_problem', 'problem', 'condition_type'): + elif key in ('problem', 'condition_type'): super().__setattr__(key, value) \ No newline at end of file diff --git a/pina/condition/input_equation_condition.py b/pina/condition/input_equation_condition.py index dc12d0225..ac47fa2c3 100644 --- a/pina/condition/input_equation_condition.py +++ b/pina/condition/input_equation_condition.py @@ -30,5 +30,5 @@ def __setattr__(self, key, value): elif key == 'equation': check_consistency(value, (EquationInterface)) InputPointsEquationCondition.__dict__[key].__set__(self, value) - elif key in ('_condition_type', '_problem', 'problem', 'condition_type'): + elif key in ('problem', 'condition_type'): super().__setattr__(key, value) \ No newline at end of file diff --git a/pina/condition/input_output_condition.py b/pina/condition/input_output_condition.py index a4fa48919..f8fd46e86 100644 --- a/pina/condition/input_output_condition.py +++ b/pina/condition/input_output_condition.py @@ -27,5 +27,5 @@ def __setattr__(self, key, value): if (key == 'input_points') or (key == 'output_points'): check_consistency(value, (LabelTensor, Graph, torch.Tensor)) InputOutputPointsCondition.__dict__[key].__set__(self, value) - elif key in ('_condition_type', '_problem', 'problem', 'condition_type'): + elif key in ('problem', 'condition_type'): super().__setattr__(key, value) diff --git a/pina/problem/abstract_problem.py b/pina/problem/abstract_problem.py index 0e4e1e2c6..da548f1e2 100644 --- a/pina/problem/abstract_problem.py +++ b/pina/problem/abstract_problem.py @@ -20,7 +20,7 @@ class AbstractProblem(metaclass=ABCMeta): def __init__(self): # create collector to manage problem data - self.collector = Collector(self) + self._collector = Collector(self) # create hook conditions <-> problems for condition_name in self.conditions: @@ -33,7 +33,12 @@ def __init__(self): # points are ready. self.collector.store_fixed_data() + @property + def collector(self): + return self._collector + # TODO this should be erase when dataloading will interface collector, + # kept only for back compatibility @property def input_pts(self): to_return = {} @@ -41,10 +46,6 @@ def input_pts(self): if 'input_points' in v.keys(): to_return[k] = v['input_points'] return to_return - - @property - def _have_sampled_points(self): - return self.collector.is_conditions_ready def __deepcopy__(self, memo): """ @@ -160,7 +161,9 @@ def discretise_domain( # check correct location if locations == "all": - locations = [name for name in self.conditions.keys()] + locations = [name for name in self.conditions.keys() + if isinstance(self.conditions[name], + DomainEquationCondition)] else: if not isinstance(locations, (list)): locations = [locations] @@ -168,7 +171,7 @@ def discretise_domain( if not isinstance(self.conditions[loc], DomainEquationCondition): raise TypeError( f"Wrong locations passed, locations for sampling " - f"should be in {[loc for loc in locations if not isinstance(self.conditions[loc], DomainEquationCondition)]}.", + f"should be in {[loc for loc in locations if isinstance(self.conditions[loc], DomainEquationCondition)]}.", ) # store data diff --git a/pina/trainer.py b/pina/trainer.py index 758bbaaf0..ba18f3392 100644 --- a/pina/trainer.py +++ b/pina/trainer.py @@ -32,29 +32,18 @@ def __init__(self, solver, batch_size=None, **kwargs): if batch_size is not None: check_consistency(batch_size, int) - self._model = solver + self.solver = solver self.batch_size = batch_size self._create_loader() self._move_to_device() - # create dataloader - # if solver.problem.have_sampled_points is False: - # raise RuntimeError( - # f"Input points in {solver.problem.not_sampled_points} " - # "training are None. Please " - # "sample points in your problem by calling " - # "discretise_domain function before train " - # "in the provided locations." - # ) - - # self._create_or_update_loader() def _move_to_device(self): device = self._accelerator_connector._parallel_devices[0] # move parameters to device - pb = self._model.problem + pb = self.solver.problem if hasattr(pb, "unknown_parameters"): for key in pb.unknown_parameters: pb.unknown_parameters[key] = torch.nn.Parameter( @@ -67,14 +56,21 @@ def _create_loader(self): during training, there is no need to define to touch the trainer dataloader, just call the method. """ + if not self.solver.problem.collector.full: + error_message = '\n'.join( + [f'{" " * 13} ---> Condition {key} {"sampled" if value else "not sampled"}' + for key, value in self.solver.problem.collector._is_conditions_ready.items()]) + raise RuntimeError('Cannot create Trainer if not all conditions ' + 'are sampled. The Trainer got the following:\n' + f'{error_message}') devices = self._accelerator_connector._parallel_devices if len(devices) > 1: raise RuntimeError("Parallel training is not supported yet.") device = devices[0] - dataset_phys = SamplePointDataset(self._model.problem, device) - dataset_data = DataPointDataset(self._model.problem, device) + dataset_phys = SamplePointDataset(self.solver.problem, device) + dataset_data = DataPointDataset(self.solver.problem, device) self._loader = SamplePointLoader( dataset_phys, dataset_data, batch_size=self.batch_size, shuffle=True ) @@ -84,7 +80,7 @@ def train(self, **kwargs): Train the solver method. """ return super().fit( - self._model, train_dataloaders=self._loader, **kwargs + self.solver, train_dataloaders=self._loader, **kwargs ) @property @@ -92,4 +88,4 @@ def solver(self): """ Returning trainer solver. """ - return self._model + return self._solver diff --git a/tests/test_problem.py b/tests/test_problem.py index 217ad45e9..0a11e1f36 100644 --- a/tests/test_problem.py +++ b/tests/test_problem.py @@ -89,6 +89,7 @@ def test_discretise_domain(): poisson_problem.discretise_domain(n, 'lh', locations=['D']) assert poisson_problem.input_pts['D'].shape[0] == n + poisson_problem.discretise_domain(n) def test_sampling_few_variables(): n = 10 @@ -98,7 +99,7 @@ def test_sampling_few_variables(): locations=['D'], variables=['x']) assert poisson_problem.input_pts['D'].shape[1] == 1 - assert poisson_problem._have_sampled_points['D'] is False + assert poisson_problem.collector._is_conditions_ready['D'] is False def test_variables_correct_order_sampling(): @@ -141,4 +142,13 @@ def test_add_points(): new_pts = LabelTensor(torch.tensor([[0.5, -0.5]]), labels=['x', 'y']) poisson_problem.add_points({'D': new_pts}) assert torch.isclose(poisson_problem.input_pts['D'].extract('x'), new_pts.extract('x')) - assert torch.isclose(poisson_problem.input_pts['D'].extract('y'), new_pts.extract('y')) \ No newline at end of file + assert torch.isclose(poisson_problem.input_pts['D'].extract('y'), new_pts.extract('y')) + + +def test_collector(): + poisson_problem = Poisson() + collector = poisson_problem.collector + assert collector.full is False + assert collector._is_conditions_ready['data'] is True + poisson_problem.discretise_domain(10) + assert collector.full is True \ No newline at end of file From c67bb7e5303dcf481502470c6dc71909c8483eef Mon Sep 17 00:00:00 2001 From: FilippoOlivo Date: Wed, 16 Oct 2024 11:24:37 +0200 Subject: [PATCH 08/14] Implement Dataset, Dataloader and DataModule class and fix SupervisedSolver --- pina/__init__.py | 6 +- pina/collector.py | 27 +- pina/condition/condition_interface.py | 2 +- pina/condition/data_condition.py | 4 +- pina/condition/domain_equation_condition.py | 4 +- pina/condition/input_equation_condition.py | 2 +- pina/condition/input_output_condition.py | 4 +- pina/data/__init__.py | 19 +- pina/data/base_dataset.py | 107 ++++++++ pina/data/data_dataset.py | 41 --- pina/data/data_module.py | 172 +++++++++++++ pina/data/pina_batch.py | 57 ++--- pina/data/pina_dataloader.py | 220 +++------------- pina/data/pina_subset.py | 21 ++ pina/data/sample_dataset.py | 49 +--- pina/data/supervised_dataset.py | 12 + pina/data/unsupervised_dataset.py | 13 + pina/domain/cartesian.py | 2 +- pina/domain/ellipsoid.py | 3 +- pina/domain/operation_interface.py | 2 +- pina/domain/simplex.py | 4 +- pina/domain/union_domain.py | 6 +- pina/label_tensor.py | 32 +-- pina/operators.py | 2 +- pina/solvers/pinns/basepinn.py | 16 +- pina/solvers/solver.py | 255 +++++-------------- pina/solvers/supervised.py | 56 ++-- pina/trainer.py | 24 +- tests/test_dataset.py | 170 ++++++++----- tests/test_solvers/test_supervised_solver.py | 238 ++++++++--------- 30 files changed, 778 insertions(+), 792 deletions(-) create mode 100644 pina/data/base_dataset.py delete mode 100644 pina/data/data_dataset.py create mode 100644 pina/data/data_module.py create mode 100644 pina/data/pina_subset.py create mode 100644 pina/data/supervised_dataset.py create mode 100644 pina/data/unsupervised_dataset.py diff --git a/pina/__init__.py b/pina/__init__.py index 0fe93752d..d110d2842 100644 --- a/pina/__init__.py +++ b/pina/__init__.py @@ -5,7 +5,8 @@ "Plotter", "Condition", "SamplePointDataset", - "SamplePointLoader", + "PinaDataModule", + "PinaDataLoader" ] from .meta import * @@ -15,4 +16,5 @@ from .plotter import Plotter from .condition.condition import Condition from .data import SamplePointDataset -from .data import SamplePointLoader +from .data import PinaDataModule +from .data import PinaDataLoader \ No newline at end of file diff --git a/pina/collector.py b/pina/collector.py index f44c222a7..f9ef194db 100644 --- a/pina/collector.py +++ b/pina/collector.py @@ -3,10 +3,11 @@ from . import LabelTensor from .utils import check_consistency, merge_tensors + class Collector: def __init__(self, problem): # creating a hook between collector and problem - self.problem = problem + self.problem = problem # this variable is used to store the data in the form: # {'[condition_name]' : @@ -14,17 +15,17 @@ def __init__(self, problem): # '[equation/output_points/conditional_variables]': Tensor} # } # those variables are used for the dataloading - self._data_collections = {name : {} for name in self.problem.conditions} + self._data_collections = {name: {} for name in self.problem.conditions} # variables used to check that all conditions are sampled self._is_conditions_ready = { - name : False for name in self.problem.conditions} + name: False for name in self.problem.conditions} self.full = False - + @property def full(self): return all(self._is_conditions_ready.values()) - + @full.setter def full(self, value): check_consistency(value, bool) @@ -37,7 +38,7 @@ def data_collections(self): @property def problem(self): return self._problem - + @problem.setter def problem(self, value): self._problem = value @@ -76,14 +77,14 @@ def store_sample_domains(self, n, mode, variables, sample_locations): # get the samples samples = [ - condition.domain.sample(n=n, mode=mode, variables=variables) - ] + already_sampled + condition.domain.sample(n=n, mode=mode, variables=variables) + ] + already_sampled pts = merge_tensors(samples) if ( - set(pts.labels).issubset(sorted(self.problem.input_variables)) - ): + set(pts.labels).issubset(sorted(self.problem.input_variables)) + ): pts = pts.sort_labels() - if sorted(pts.labels)==sorted(self.problem.input_variables): + if sorted(pts.labels) == sorted(self.problem.input_variables): self._is_conditions_ready[loc] = True values = [pts, condition.equation] self.data_collections[loc] = dict(zip(keys, values)) @@ -97,7 +98,7 @@ def add_points(self, new_points_dict): :param new_points_dict: Dictonary of input points (condition_name: LabelTensor) :raises RuntimeError: if at least one condition is not already sampled """ - for k,v in new_points_dict.items(): + for k, v in new_points_dict.items(): if not self._is_conditions_ready[k]: raise RuntimeError('Cannot add points on a non sampled condition') - self.data_collections[k]['input_points'] = self.data_collections[k]['input_points'].vstack(v) \ No newline at end of file + self.data_collections[k]['input_points'] = self.data_collections[k]['input_points'].vstack(v) diff --git a/pina/condition/condition_interface.py b/pina/condition/condition_interface.py index 52699b66e..808c06afe 100644 --- a/pina/condition/condition_interface.py +++ b/pina/condition/condition_interface.py @@ -5,7 +5,7 @@ class ConditionInterface(metaclass=ABCMeta): condition_types = ['physics', 'supervised', 'unsupervised'] - def __init__(self, *args, **wargs): + def __init__(self, *args, **kwargs): self._condition_type = None self._problem = None diff --git a/pina/condition/data_condition.py b/pina/condition/data_condition.py index 90d248b67..3bcd4be6d 100644 --- a/pina/condition/data_condition.py +++ b/pina/condition/data_condition.py @@ -22,11 +22,11 @@ def __init__(self, input_points, conditional_variables=None): super().__init__() self.input_points = input_points self.conditional_variables = conditional_variables - self.condition_type = 'unsupervised' + self._condition_type = 'unsupervised' def __setattr__(self, key, value): if (key == 'input_points') or (key == 'conditional_variables'): check_consistency(value, (LabelTensor, Graph, torch.Tensor)) DataConditionInterface.__dict__[key].__set__(self, value) - elif key in ('problem', 'condition_type'): + elif key in ('_problem', '_condition_type'): super().__setattr__(key, value) \ No newline at end of file diff --git a/pina/condition/domain_equation_condition.py b/pina/condition/domain_equation_condition.py index ce4c7d3fc..28315655b 100644 --- a/pina/condition/domain_equation_condition.py +++ b/pina/condition/domain_equation_condition.py @@ -20,7 +20,7 @@ def __init__(self, domain, equation): super().__init__() self.domain = domain self.equation = equation - self.condition_type = 'physics' + self._condition_type = 'physics' def __setattr__(self, key, value): if key == 'domain': @@ -29,5 +29,5 @@ def __setattr__(self, key, value): elif key == 'equation': check_consistency(value, (EquationInterface)) DomainEquationCondition.__dict__[key].__set__(self, value) - elif key in ('problem', 'condition_type'): + elif key in ('_problem', '_condition_type'): super().__setattr__(key, value) \ No newline at end of file diff --git a/pina/condition/input_equation_condition.py b/pina/condition/input_equation_condition.py index ac47fa2c3..0d34dfc93 100644 --- a/pina/condition/input_equation_condition.py +++ b/pina/condition/input_equation_condition.py @@ -30,5 +30,5 @@ def __setattr__(self, key, value): elif key == 'equation': check_consistency(value, (EquationInterface)) InputPointsEquationCondition.__dict__[key].__set__(self, value) - elif key in ('problem', 'condition_type'): + elif key in ('_problem', '_condition_type'): super().__setattr__(key, value) \ No newline at end of file diff --git a/pina/condition/input_output_condition.py b/pina/condition/input_output_condition.py index f8fd46e86..8a17495dd 100644 --- a/pina/condition/input_output_condition.py +++ b/pina/condition/input_output_condition.py @@ -21,11 +21,11 @@ def __init__(self, input_points, output_points): super().__init__() self.input_points = input_points self.output_points = output_points - self.condition_type = ['supervised', 'physics'] + self._condition_type = ['supervised', 'physics'] def __setattr__(self, key, value): if (key == 'input_points') or (key == 'output_points'): check_consistency(value, (LabelTensor, Graph, torch.Tensor)) InputOutputPointsCondition.__dict__[key].__set__(self, value) - elif key in ('problem', 'condition_type'): + elif key in ('_problem', '_condition_type'): super().__setattr__(key, value) diff --git a/pina/data/__init__.py b/pina/data/__init__.py index fba19b92c..0a1b5905e 100644 --- a/pina/data/__init__.py +++ b/pina/data/__init__.py @@ -1,7 +1,20 @@ +""" +Import data classes +""" __all__ = [ + 'PinaDataLoader', + 'SupervisedDataset', + 'SamplePointDataset', + 'UnsupervisedDataset', + 'Batch', + 'PinaDataModule', + 'BaseDataset' ] -from .pina_dataloader import SamplePointLoader -from .data_dataset import DataPointDataset +from .pina_dataloader import PinaDataLoader +from .supervised_dataset import SupervisedDataset from .sample_dataset import SamplePointDataset -from .pina_batch import Batch \ No newline at end of file +from .unsupervised_dataset import UnsupervisedDataset +from .pina_batch import Batch +from .data_module import PinaDataModule +from .base_dataset import BaseDataset diff --git a/pina/data/base_dataset.py b/pina/data/base_dataset.py new file mode 100644 index 000000000..f095afa0c --- /dev/null +++ b/pina/data/base_dataset.py @@ -0,0 +1,107 @@ +""" +Basic data module implementation +""" +from torch.utils.data import Dataset +import torch +from ..label_tensor import LabelTensor + + +class BaseDataset(Dataset): + """ + BaseDataset class, which handle initialization and data retrieval + :var condition_indices: List of indices + :var device: torch.device + :var condition_names: dict of condition index and corresponding name + """ + + def __new__(cls, problem, device): + """ + Ensure correct definition of __slots__ before initialization + :param AbstractProblem problem: The formulation of the problem. + :param torch.device device: The device on which the + dataset will be loaded. + """ + if cls is BaseDataset: + raise TypeError('BaseDataset cannot be instantiated directly. Use a subclass.') + if not hasattr(cls, '__slots__'): + raise TypeError('Something is wrong, __slots__ must be defined in subclasses.') + return super().__new__(cls) + + def __init__(self, problem, device): + """" + Initialize the object based on __slots__ + :param AbstractProblem problem: The formulation of the problem. + :param torch.device device: The device on which the + dataset will be loaded. + """ + super().__init__() + + self.condition_names = {} + collector = problem.collector + for slot in self.__slots__: + setattr(self, slot, []) + + idx = 0 + for name, data in collector.data_collections.items(): + keys = [] + for k, v in data.items(): + if isinstance(v, LabelTensor): + keys.append(k) + if sorted(self.__slots__) == sorted(keys): + + for slot in self.__slots__: + current_list = getattr(self, slot) + current_list.append(data[slot]) + self.condition_names[idx] = name + idx += 1 + + if len(getattr(self, self.__slots__[0])) > 0: + input_list = getattr(self, self.__slots__[0]) + self.condition_indices = torch.cat( + [ + torch.tensor([i] * len(input_list[i]), dtype=torch.uint8) + for i in range(len(self.condition_names)) + ], + dim=0, + ) + for slot in self.__slots__: + current_attribute = getattr(self, slot) + setattr(self, slot, LabelTensor.vstack(current_attribute)) + else: + self.condition_indices = torch.tensor([], dtype=torch.uint8) + for slot in self.__slots__: + setattr(self, slot, torch.tensor([])) + + self.device = device + + def __len__(self): + return len(getattr(self, self.__slots__[0])) + + def __getattribute__(self, item): + attribute = super().__getattribute__(item) + if isinstance(attribute, LabelTensor) and attribute.dtype == torch.float32: + attribute = attribute.to(device=self.device).requires_grad_() + return attribute + + def __getitem__(self, idx): + if isinstance(idx, str): + return getattr(self, idx).to(self.device) + + if isinstance(idx, slice): + to_return_list = [] + for i in self.__slots__: + to_return_list.append(getattr(self, i)[[idx]].to(self.device)) + return to_return_list + + if isinstance(idx, (tuple, list)): + if (len(idx) == 2 and isinstance(idx[0], str) + and isinstance(idx[1], (list, slice))): + tensor = getattr(self, idx[0]) + return tensor[[idx[1]]].to(self.device) + if all(isinstance(x, int) for x in idx): + to_return_list = [] + for i in self.__slots__: + to_return_list.append(getattr(self, i)[[idx]].to(self.device)) + return to_return_list + + raise ValueError(f'Invalid index {idx}') diff --git a/pina/data/data_dataset.py b/pina/data/data_dataset.py deleted file mode 100644 index 9dff2d7ed..000000000 --- a/pina/data/data_dataset.py +++ /dev/null @@ -1,41 +0,0 @@ -from torch.utils.data import Dataset -import torch -from ..label_tensor import LabelTensor - - -class DataPointDataset(Dataset): - - def __init__(self, problem, device) -> None: - super().__init__() - input_list = [] - output_list = [] - self.condition_names = [] - - for name, condition in problem.conditions.items(): - if hasattr(condition, "output_points"): - input_list.append(problem.conditions[name].input_points) - output_list.append(problem.conditions[name].output_points) - self.condition_names.append(name) - - self.input_pts = LabelTensor.stack(input_list) - self.output_pts = LabelTensor.stack(output_list) - - if self.input_pts != []: - self.condition_indeces = torch.cat( - [ - torch.tensor([i] * len(input_list[i])) - for i in range(len(self.condition_names)) - ], - dim=0, - ) - else: # if there are no data points - self.condition_indeces = torch.tensor([]) - self.input_pts = torch.tensor([]) - self.output_pts = torch.tensor([]) - - self.input_pts = self.input_pts.to(device) - self.output_pts = self.output_pts.to(device) - self.condition_indeces = self.condition_indeces.to(device) - - def __len__(self): - return self.input_pts.shape[0] \ No newline at end of file diff --git a/pina/data/data_module.py b/pina/data/data_module.py new file mode 100644 index 000000000..e4e8a450f --- /dev/null +++ b/pina/data/data_module.py @@ -0,0 +1,172 @@ +""" +This module provide basic data management functionalities +""" + +import math +import torch +from lightning import LightningDataModule +from .sample_dataset import SamplePointDataset +from .supervised_dataset import SupervisedDataset +from .unsupervised_dataset import UnsupervisedDataset +from .pina_dataloader import PinaDataLoader +from .pina_subset import PinaSubset + + +class PinaDataModule(LightningDataModule): + """ + This class extend LightningDataModule, allowing proper creation and + management of different types of Datasets defined in PINA + """ + + def __init__(self, + problem, + device, + train_size=.7, + test_size=.2, + eval_size=.1, + batch_size=None, + shuffle=True, + datasets = None): + """ + Initialize the object, creating dataset based on input problem + :param AbstractProblem problem: PINA problem + :param device: Device used for training and testing + :param train_size: number/percentage of elements in train split + :param test_size: number/percentage of elements in test split + :param eval_size: number/percentage of elements in evaluation split + :param batch_size: batch size used for training + :param datasets: list of datasets objects + """ + super().__init__() + dataset_classes = [SupervisedDataset, UnsupervisedDataset, SamplePointDataset] + if datasets is None: + self.datasets = [DatasetClass(problem, device) for DatasetClass in dataset_classes] + else: + self.datasets = datasets + + self.split_length = [] + self.split_names = [] + if train_size > 0: + self.split_names.append('train') + self.split_length.append(train_size) + if test_size > 0: + self.split_length.append(test_size) + self.split_names.append('test') + if eval_size > 0: + self.split_length.append(eval_size) + self.split_names.append('eval') + + self.batch_size = batch_size + self.condition_names = None + self.splits = {k: {} for k in self.split_names} + self.shuffle = shuffle + + def setup(self, stage=None): + """ + Perform the splitting of the dataset + """ + self.extract_conditions() + if stage == 'fit' or stage is None: + for dataset in self.datasets: + if len(dataset) > 0: + splits = self.dataset_split(dataset, + self.split_length, + shuffle=self.shuffle) + for i in range(len(self.split_length)): + self.splits[ + self.split_names[i]][dataset.data_type] = splits[i] + elif stage == 'test': + raise NotImplementedError("Testing pipeline not implemented yet") + else: + raise ValueError("stage must be either 'fit' or 'test'") + + def extract_conditions(self): + """ + Extract conditions from dataset and update condition indices + """ + # Extract number of conditions + n_conditions = 0 + for dataset in self.datasets: + if n_conditions != 0: + dataset.condition_names = { + key + n_conditions: value + for key, value in dataset.condition_names.items() + } + n_conditions += len(dataset.condition_names) + + self.condition_names = { + key: value + for dataset in self.datasets + for key, value in dataset.condition_names.items() + } + + + + def train_dataloader(self): + """ + Return the training dataloader for the dataset + :return: data loader + :rtype: PinaDataLoader + """ + return PinaDataLoader(self.splits['train'], self.batch_size, + self.condition_names) + + def test_dataloader(self): + """ + Return the testing dataloader for the dataset + :return: data loader + :rtype: PinaDataLoader + """ + return PinaDataLoader(self.splits['test'], self.batch_size, + self.condition_names) + + def eval_dataloader(self): + """ + Return the evaluation dataloader for the dataset + :return: data loader + :rtype: PinaDataLoader + """ + return PinaDataLoader(self.splits['eval'], self.batch_size, + self.condition_names) + + @staticmethod + def dataset_split(dataset, lengths, seed=None, shuffle=True): + """ + Perform the splitting of the dataset + :param dataset: dataset object we wanted to split + :param lengths: lengths of elements in dataset + :param seed: random seed + :param shuffle: shuffle dataset + :return: split dataset + :rtype: PinaSubset + """ + if sum(lengths) - 1 < 1e-3: + lengths = [ + int(math.floor(len(dataset) * length)) for length in lengths + ] + + remainder = len(dataset) - sum(lengths) + for i in range(remainder): + lengths[i % len(lengths)] += 1 + elif sum(lengths) - 1 >= 1e-3: + raise ValueError(f"Sum of lengths is {sum(lengths)} less than 1") + + if sum(lengths) != len(dataset): + raise ValueError("Sum of lengths is not equal to dataset length") + + if shuffle: + if seed is not None: + generator = torch.Generator() + generator.manual_seed(seed) + indices = torch.randperm(sum(lengths), generator=generator).tolist() + else: + indices = torch.arange(sum(lengths)).tolist() + else: + indices = torch.arange(0, sum(lengths), 1, dtype=torch.uint8).tolist() + offsets = [ + sum(lengths[:i]) if i > 0 else 0 for i in range(len(lengths)) + ] + return [ + PinaSubset(dataset, indices[offset:offset + length]) + for offset, length in zip(offsets, lengths) + ] diff --git a/pina/data/pina_batch.py b/pina/data/pina_batch.py index cb1296ede..7e46a2218 100644 --- a/pina/data/pina_batch.py +++ b/pina/data/pina_batch.py @@ -1,36 +1,33 @@ +""" +Batch management module +""" +from .pina_subset import PinaSubset class Batch: - """ - This class is used to create a dataset of sample points. - """ + def __init__(self, dataset_dict, idx_dict): - def __init__(self, type_, idx, *args, **kwargs) -> None: - """ - """ - if type_ == "sample": - - if len(args) != 2: - raise RuntimeError - - input = args[0] - conditions = args[1] - - self.input = input[idx] - self.condition = conditions[idx] + for k, v in dataset_dict.items(): + setattr(self, k, v) - elif type_ == "data": + for k, v in idx_dict.items(): + setattr(self, k + '_idx', v) - if len(args) != 3: - raise RuntimeError - - input = args[0] - output = args[1] - conditions = args[2] - - self.input = input[idx] - self.output = output[idx] - self.condition = conditions[idx] - - else: - raise ValueError("Invalid number of arguments.") \ No newline at end of file + def __len__(self): + """ + Returns the number of elements in the batch + :return: number of elements in the batch + :rtype: int + """ + length = 0 + for dataset in dir(self): + attribute = getattr(self, dataset) + if isinstance(attribute, list): + length += len(getattr(self, dataset)) + return length + + def __getattr__(self, item): + if not item in dir(self): + raise AttributeError(f'Batch instance has no attribute {item}') + return PinaSubset(getattr(self, item).dataset, + getattr(self, item).indices[self.coordinates_dict[item]]) diff --git a/pina/data/pina_dataloader.py b/pina/data/pina_dataloader.py index 2c8967c50..d62847574 100644 --- a/pina/data/pina_dataloader.py +++ b/pina/data/pina_dataloader.py @@ -1,11 +1,11 @@ -import torch - -from .sample_dataset import SamplePointDataset -from .data_dataset import DataPointDataset +""" +This module is used to create an iterable object used during training +""" +import math from .pina_batch import Batch -class SamplePointLoader: +class PinaDataLoader: """ This class is used to create a dataloader to use during the training. @@ -14,198 +14,54 @@ class SamplePointLoader: :vartype condition_names: list[str] """ - def __init__( - self, sample_dataset, data_dataset, batch_size=None, shuffle=True - ) -> None: - """ - Constructor. - - :param SamplePointDataset sample_pts: The sample points dataset. - :param int batch_size: The batch size. If ``None``, the batch size is - set to the number of sample points. Default is ``None``. - :param bool shuffle: If ``True``, the sample points are shuffled. - Default is ``True``. - """ - if not isinstance(sample_dataset, SamplePointDataset): - raise TypeError( - f"Expected SamplePointDataset, got {type(sample_dataset)}" - ) - if not isinstance(data_dataset, DataPointDataset): - raise TypeError( - f"Expected DataPointDataset, got {type(data_dataset)}" - ) - - self.n_data_conditions = len(data_dataset.condition_names) - self.n_phys_conditions = len(sample_dataset.condition_names) - data_dataset.condition_indeces += self.n_phys_conditions - - self._prepare_sample_dataset(sample_dataset, batch_size, shuffle) - self._prepare_data_dataset(data_dataset, batch_size, shuffle) - - self.condition_names = ( - sample_dataset.condition_names + data_dataset.condition_names - ) - - self.batch_list = [] - for i in range(len(self.batch_sample_pts)): - self.batch_list.append(("sample", i)) - - for i in range(len(self.batch_input_pts)): - self.batch_list.append(("data", i)) - - if shuffle: - self.random_idx = torch.randperm(len(self.batch_list)) - else: - self.random_idx = torch.arange(len(self.batch_list)) - - self._prepare_batches() - - def _prepare_data_dataset(self, dataset, batch_size, shuffle): - """ - Prepare the dataset for data points. - - :param SamplePointDataset dataset: The dataset. - :param int batch_size: The batch size. - :param bool shuffle: If ``True``, the sample points are shuffled. - """ - self.sample_dataset = dataset - - if len(dataset) == 0: - self.batch_data_conditions = [] - self.batch_input_pts = [] - self.batch_output_pts = [] - return - - if batch_size is None: - batch_size = len(dataset) - batch_num = len(dataset) // batch_size - if len(dataset) % batch_size != 0: - batch_num += 1 - - output_labels = dataset.output_pts.labels - input_labels = dataset.input_pts.labels - self.tensor_conditions = dataset.condition_indeces - - if shuffle: - idx = torch.randperm(dataset.input_pts.shape[0]) - self.input_pts = dataset.input_pts[idx] - self.output_pts = dataset.output_pts[idx] - self.tensor_conditions = dataset.condition_indeces[idx] - - self.batch_input_pts = torch.tensor_split(dataset.input_pts, batch_num) - self.batch_output_pts = torch.tensor_split( - dataset.output_pts, batch_num - ) - #print(input_labels) - for i in range(len(self.batch_input_pts)): - self.batch_input_pts[i].labels = input_labels - self.batch_output_pts[i].labels = output_labels - - self.batch_data_conditions = torch.tensor_split( - self.tensor_conditions, batch_num - ) - - def _prepare_sample_dataset(self, dataset, batch_size, shuffle): + def __init__(self, dataset_dict, batch_size, condition_names) -> None: """ - Prepare the dataset for sample points. - - :param DataPointDataset dataset: The dataset. - :param int batch_size: The batch size. - :param bool shuffle: If ``True``, the sample points are shuffled. + Initialize local variables + :param dataset_dict: Dictionary of datasets + :type dataset_dict: dict + :param batch_size: Size of the batch + :type batch_size: int + :param condition_names: Names of the conditions + :type condition_names: list[str] """ + self.condition_names = condition_names + self.dataset_dict = dataset_dict + self._init_batches(batch_size) - self.sample_dataset = dataset - if len(dataset) == 0: - self.batch_sample_conditions = [] - self.batch_sample_pts = [] - return - - if batch_size is None: - batch_size = len(dataset) - - batch_num = len(dataset) // batch_size - if len(dataset) % batch_size != 0: - batch_num += 1 - - self.tensor_pts = dataset.pts - self.tensor_conditions = dataset.condition_indeces - - # if shuffle: - # idx = torch.randperm(self.tensor_pts.shape[0]) - # self.tensor_pts = self.tensor_pts[idx] - # self.tensor_conditions = self.tensor_conditions[idx] - - self.batch_sample_pts = torch.tensor_split(self.tensor_pts, batch_num) - for i in range(len(self.batch_sample_pts)): - self.batch_sample_pts[i].labels = dataset.pts.labels - - self.batch_sample_conditions = torch.tensor_split( - self.tensor_conditions, batch_num - ) - - def _prepare_batches(self): + def _init_batches(self, batch_size=None): """ - Prepare the batches. + Create batches according to the batch_size provided in input. """ self.batches = [] - for i in range(len(self.batch_list)): - type_, idx_ = self.batch_list[i] - - if type_ == "sample": - batch = Batch( - "sample", idx_, - self.batch_sample_pts, - self.batch_sample_conditions) + n_elements = sum([len(v) for v in self.dataset_dict.values()]) + if batch_size is None: + batch_size = n_elements + indexes_dict = {} + n_batches = int(math.ceil(n_elements / batch_size)) + for k, v in self.dataset_dict.items(): + if n_batches != 1: + indexes_dict[k] = math.floor(len(v) / (n_batches - 1)) else: - batch = Batch( - "data", idx_, - self.batch_input_pts, - self.batch_output_pts, - self.batch_data_conditions) - - self.batches.append(batch) + indexes_dict[k] = len(v) + for i in range(n_batches): + temp_dict = {} + for k, v in indexes_dict.items(): + if i != n_batches - 1: + temp_dict[k] = slice(i * v, (i + 1) * v) + else: + temp_dict[k] = slice(i * v, len(self.dataset_dict[k])) + self.batches.append(Batch(idx_dict=temp_dict, dataset_dict=self.dataset_dict)) def __iter__(self): """ - Return an iterator over the points. Any element of the iterator is a - dictionary with the following keys: - - ``pts``: The input sample points. It is a LabelTensor with the - shape ``(batch_size, input_dimension)``. - - ``output``: The output sample points. This key is present only - if data conditions are present. It is a LabelTensor with the - shape ``(batch_size, output_dimension)``. - - ``condition``: The integer condition indeces. It is a tensor - with the shape ``(batch_size, )`` of type ``torch.int64`` and - indicates for any ``pts`` the corresponding problem condition. - - :return: An iterator over the points. - :rtype: iter + Makes dataloader object iterable """ - # for i in self.random_idx: - for i in self.random_idx: - yield self.batches[i] - - # for i in range(len(self.batch_list)): - # type_, idx_ = self.batch_list[i] - - # if type_ == "sample": - # d = { - # "pts": self.batch_sample_pts[idx_].requires_grad_(True), - # "condition": self.batch_sample_conditions[idx_], - # } - # else: - # d = { - # "pts": self.batch_input_pts[idx_].requires_grad_(True), - # "output": self.batch_output_pts[idx_], - # "condition": self.batch_data_conditions[idx_], - # } - # yield d + yield from self.batches def __len__(self): """ Return the number of batches. - :return: The number of batches. :rtype: int """ - return len(self.batch_list) + return len(self.batches) diff --git a/pina/data/pina_subset.py b/pina/data/pina_subset.py new file mode 100644 index 000000000..41571f92b --- /dev/null +++ b/pina/data/pina_subset.py @@ -0,0 +1,21 @@ +class PinaSubset: + """ + TODO + """ + __slots__ = ['dataset', 'indices'] + + def __init__(self, dataset, indices): + """ + TODO + """ + self.dataset = dataset + self.indices = indices + + def __len__(self): + """ + TODO + """ + return len(self.indices) + + def __getattr__(self, name): + return self.dataset.__getattribute__(name) diff --git a/pina/data/sample_dataset.py b/pina/data/sample_dataset.py index 84af2920f..ba8bd19a9 100644 --- a/pina/data/sample_dataset.py +++ b/pina/data/sample_dataset.py @@ -1,43 +1,12 @@ -from torch.utils.data import Dataset -import torch +""" +Sample dataset module +""" +from .base_dataset import BaseDataset -from ..label_tensor import LabelTensor - - -class SamplePointDataset(Dataset): +class SamplePointDataset(BaseDataset): """ - This class is used to create a dataset of sample points. + This class extends the BaseDataset to handle physical datasets + composed of only input points. """ - - def __init__(self, problem, device) -> None: - """ - :param dict input_pts: The input points. - """ - super().__init__() - pts_list = [] - self.condition_names = [] - - for name, condition in problem.conditions.items(): - if not hasattr(condition, "output_points"): - pts_list.append(problem.input_pts[name]) - self.condition_names.append(name) - - self.pts = LabelTensor.stack(pts_list) - - if self.pts != []: - self.condition_indeces = torch.cat( - [ - torch.tensor([i] * len(pts_list[i])) - for i in range(len(self.condition_names)) - ], - dim=0, - ) - else: # if there are no sample points - self.condition_indeces = torch.tensor([]) - self.pts = torch.tensor([]) - - self.pts = self.pts.to(device) - self.condition_indeces = self.condition_indeces.to(device) - - def __len__(self): - return self.pts.shape[0] \ No newline at end of file + data_type = 'physics' + __slots__ = ['input_points'] diff --git a/pina/data/supervised_dataset.py b/pina/data/supervised_dataset.py new file mode 100644 index 000000000..2403e3d0c --- /dev/null +++ b/pina/data/supervised_dataset.py @@ -0,0 +1,12 @@ +""" +Supervised dataset module +""" +from .base_dataset import BaseDataset + + +class SupervisedDataset(BaseDataset): + """ + This class extends the BaseDataset to handle datasets that consist of input-output pairs. + """ + data_type = 'supervised' + __slots__ = ['input_points', 'output_points'] diff --git a/pina/data/unsupervised_dataset.py b/pina/data/unsupervised_dataset.py new file mode 100644 index 000000000..f4e8fb345 --- /dev/null +++ b/pina/data/unsupervised_dataset.py @@ -0,0 +1,13 @@ +""" +Unsupervised dataset module +""" +from .base_dataset import BaseDataset + + +class UnsupervisedDataset(BaseDataset): + """ + This class extend BaseDataset class to handle unsupervised dataset, + composed of input points and, optionally, conditional variables + """ + data_type = 'unsupervised' + __slots__ = ['input_points', 'conditional_variables'] diff --git a/pina/domain/cartesian.py b/pina/domain/cartesian.py index 6e9b81afe..4986ea7e5 100644 --- a/pina/domain/cartesian.py +++ b/pina/domain/cartesian.py @@ -33,7 +33,7 @@ def __init__(self, cartesian_dict): @property def sample_modes(self): return ["random", "grid", "lh", "chebyshev", "latin"] - + @property def variables(self): """Spatial variables. diff --git a/pina/domain/ellipsoid.py b/pina/domain/ellipsoid.py index b9185fa08..18e28d4b7 100644 --- a/pina/domain/ellipsoid.py +++ b/pina/domain/ellipsoid.py @@ -55,7 +55,6 @@ def __init__(self, ellipsoid_dict, sample_surface=False): # perform operation only for not fixed variables (if any) if self.range_: - # convert dict vals to torch [dim, 2] matrix list_dict_vals = list(self.range_.values()) tmp = torch.tensor(list_dict_vals, dtype=torch.float) @@ -74,7 +73,7 @@ def __init__(self, ellipsoid_dict, sample_surface=False): @property def sample_modes(self): return ["random"] - + @property def variables(self): """Spatial variables. diff --git a/pina/domain/operation_interface.py b/pina/domain/operation_interface.py index a1efec91f..0300f5248 100644 --- a/pina/domain/operation_interface.py +++ b/pina/domain/operation_interface.py @@ -69,4 +69,4 @@ def _check_dimensions(self, geometries): if geometry.variables != geometries[0].variables: raise NotImplementedError( f"The geometries need to have same dimensions and labels." - ) \ No newline at end of file + ) diff --git a/pina/domain/simplex.py b/pina/domain/simplex.py index 96cc36c0f..931f861a7 100644 --- a/pina/domain/simplex.py +++ b/pina/domain/simplex.py @@ -77,7 +77,7 @@ def __init__(self, simplex_matrix, sample_surface=False): @property def sample_modes(self): return ["random"] - + @property def variables(self): return self._vertices_matrix.labels @@ -144,7 +144,7 @@ def is_inside(self, point, check_border=False): return all(torch.gt(lambdas, 0.0)) and all(torch.lt(lambdas, 1.0)) return all(torch.ge(lambdas, 0)) and ( - any(torch.eq(lambdas, 0)) or any(torch.eq(lambdas, 1)) + any(torch.eq(lambdas, 0)) or any(torch.eq(lambdas, 1)) ) def _sample_interior_randomly(self, n, variables): diff --git a/pina/domain/union_domain.py b/pina/domain/union_domain.py index a72115f50..0af8e1bd1 100644 --- a/pina/domain/union_domain.py +++ b/pina/domain/union_domain.py @@ -37,13 +37,13 @@ def __init__(self, geometries): def sample_modes(self): self.sample_modes = list( set([geom.sample_modes for geom in self.geometries]) - ) - + ) + @property def variables(self): variables = [] for geom in self.geometries: - variables+=geom.variables + variables += geom.variables return list(set(variables)) def is_inside(self, point, check_border=False): diff --git a/pina/label_tensor.py b/pina/label_tensor.py index 1df318ec7..65655e9dc 100644 --- a/pina/label_tensor.py +++ b/pina/label_tensor.py @@ -3,6 +3,7 @@ import torch from torch import Tensor + def issubset(a, b): """ Check if a is a subset of b. @@ -45,7 +46,7 @@ def labels(self): :return: labels of self :rtype: list """ - return self._labels[self.tensor.ndim-1]['dof'] + return self._labels[self.tensor.ndim - 1]['dof'] @property def full_labels(self): @@ -103,23 +104,23 @@ def extract(self, label_to_extract): raise ValueError('labels_to_extract must be str or list or dict') def _extract_from_list(self, labels_to_extract): - #Store locally all necessary obj/variables + # Store locally all necessary obj/variables ndim = self.tensor.ndim labels = self.full_labels tensor = self.tensor last_dim_label = self.labels - #Verify if all the labels in labels_to_extract are in last dimension + # Verify if all the labels in labels_to_extract are in last dimension if set(labels_to_extract).issubset(last_dim_label) is False: raise ValueError('Cannot extract a dof which is not in the original LabelTensor') - #Extract index to extract + # Extract index to extract idx_to_extract = [last_dim_label.index(i) for i in labels_to_extract] - #Perform extraction + # Perform extraction new_tensor = tensor[..., idx_to_extract] - #Manage labels + # Manage labels new_labels = copy(labels) last_dim_new_label = {ndim - 1: { @@ -186,7 +187,7 @@ def cat(tensors, dim=0): # Perform cat on tensors new_tensor = torch.cat(tensors, dim=dim) - #Update labels + # Update labels labels = tensors[0].full_labels labels.pop(dim) new_labels_cat_dim = new_labels_cat_dim if len(set(new_labels_cat_dim)) == len(new_labels_cat_dim) \ @@ -265,13 +266,13 @@ def update_labels_from_dict(self, labels): :raises ValueError: dof list contain duplicates or number of dof does not match with tensor shape """ tensor_shape = self.tensor.shape - #Check dimensionality + # Check dimensionality for k, v in labels.items(): if len(v['dof']) != len(set(v['dof'])): raise ValueError("dof must be unique") if len(v['dof']) != tensor_shape[k]: raise ValueError('Number of dof does not match with tensor dimension') - #Perform update + # Perform update self._labels.update(labels) def update_labels_from_list(self, labels): @@ -310,7 +311,7 @@ def append(self, tensor, mode='std'): if mode == 'std': # Call cat on last dimension new_label_tensor = LabelTensor.cat([self, tensor], dim=self.tensor.ndim - 1) - elif mode=='cross': + elif mode == 'cross': # Crete tensor and call cat on last dimension tensor1 = self tensor2 = tensor @@ -318,7 +319,7 @@ def append(self, tensor, mode='std'): n2 = tensor2.shape[0] tensor1 = LabelTensor(tensor1.repeat(n2, 1), labels=tensor1.labels) tensor2 = LabelTensor(tensor2.repeat_interleave(n1, dim=0), labels=tensor2.labels) - new_label_tensor = LabelTensor.cat([tensor1, tensor2], dim=self.tensor.ndim-1) + new_label_tensor = LabelTensor.cat([tensor1, tensor2], dim=self.tensor.ndim - 1) else: raise ValueError('mode must be either "std" or "cross"') return new_label_tensor @@ -366,10 +367,10 @@ def __getitem__(self, index): if hasattr(self, "labels"): if isinstance(index[j], list): new_labels.update({j: {'dof': [new_labels[j]['dof'][i] for i in index[1]], - 'name':new_labels[j]['name']}}) + 'name': new_labels[j]['name']}}) else: new_labels.update({j: {'dof': new_labels[j]['dof'][index[j]], - 'name':new_labels[j]['name']}}) + 'name': new_labels[j]['name']}}) selected_lt.labels = new_labels else: @@ -382,12 +383,13 @@ def __getitem__(self, index): def sort_labels(self, dim=None): def argsort(lst): return sorted(range(len(lst)), key=lambda x: lst[x]) + if dim is None: - dim = self.tensor.ndim-1 + dim = self.tensor.ndim - 1 labels = self.full_labels[dim]['dof'] sorted_index = argsort(labels) indexer = [slice(None)] * self.tensor.ndim indexer[dim] = sorted_index new_labels = deepcopy(self.full_labels) new_labels[dim] = {'dof': sorted(labels), 'name': new_labels[dim]['name']} - return LabelTensor(self.tensor[indexer], new_labels) \ No newline at end of file + return LabelTensor(self.tensor[indexer], new_labels) diff --git a/pina/operators.py b/pina/operators.py index fa32f2921..9e780ec82 100644 --- a/pina/operators.py +++ b/pina/operators.py @@ -211,7 +211,7 @@ def laplacian(output_, input_, components=None, d=None, method="std"): result[:, idx] = grad(grad_output, input_, d=di).flatten() to_append_tensors[idx] = grad(grad_output, input_, d=di) labels[idx] = f"dd{ci[0]}dd{di[0]}" - result = LabelTensor.cat(tensors=to_append_tensors, dim=output_.tensor.ndim-1) + result = LabelTensor.cat(tensors=to_append_tensors, dim=output_.tensor.ndim - 1) result.labels = labels return result diff --git a/pina/solvers/pinns/basepinn.py b/pina/solvers/pinns/basepinn.py index 9e841d65d..543f823f9 100644 --- a/pina/solvers/pinns/basepinn.py +++ b/pina/solvers/pinns/basepinn.py @@ -27,13 +27,13 @@ class PINNInterface(SolverInterface, metaclass=ABCMeta): """ def __init__( - self, - models, - problem, - optimizers, - optimizers_kwargs, - extra_features, - loss, + self, + models, + problem, + optimizers, + optimizers_kwargs, + extra_features, + loss, ): """ :param models: Multiple torch neural network models instances. @@ -177,7 +177,7 @@ def compute_residual(self, samples, equation): try: residual = equation.residual(samples, self.forward(samples)) except ( - TypeError + TypeError ): # this occurs when the function has three inputs, i.e. inverse problem residual = equation.residual( samples, self.forward(samples), self._params diff --git a/pina/solvers/solver.py b/pina/solvers/solver.py index a27e93641..8b3ddae71 100644 --- a/pina/solvers/solver.py +++ b/pina/solvers/solver.py @@ -10,168 +10,6 @@ import sys -# class SolverInterface(pytorch_lightning.LightningModule, metaclass=ABCMeta): -# """ -# Solver base class. This class inherits is a wrapper of -# LightningModule class, inheriting all the -# LightningModule methods. -# """ - -# def __init__( -# self, -# models, -# problem, -# optimizers, -# optimizers_kwargs, -# extra_features=None, -# ): -# """ -# :param models: A torch neural network model instance. -# :type models: torch.nn.Module -# :param problem: A problem definition instance. -# :type problem: AbstractProblem -# :param list(torch.optim.Optimizer) optimizer: A list of neural network optimizers to -# use. -# :param list(dict) optimizer_kwargs: A list of optimizer constructor keyword args. -# :param list(torch.nn.Module) extra_features: The additional input -# features to use as augmented input. If ``None`` no extra features -# are passed. If it is a list of :class:`torch.nn.Module`, the extra feature -# list is passed to all models. If it is a list of extra features' lists, -# each single list of extra feature is passed to a model. -# """ -# super().__init__() - -# # check consistency of the inputs -# check_consistency(models, torch.nn.Module) -# check_consistency(problem, AbstractProblem) -# check_consistency(optimizers, torch.optim.Optimizer, subclass=True) -# check_consistency(optimizers_kwargs, dict) - -# # put everything in a list if only one input -# if not isinstance(models, list): -# models = [models] -# if not isinstance(optimizers, list): -# optimizers = [optimizers] -# optimizers_kwargs = [optimizers_kwargs] - -# # number of models and optimizers -# len_model = len(models) -# len_optimizer = len(optimizers) -# len_optimizer_kwargs = len(optimizers_kwargs) - -# # check length consistency optimizers -# if len_model != len_optimizer: -# raise ValueError( -# "You must define one optimizer for each model." -# f"Got {len_model} models, and {len_optimizer}" -# " optimizers." -# ) - -# # check length consistency optimizers kwargs -# if len_optimizer_kwargs != len_optimizer: -# raise ValueError( -# "You must define one dictionary of keyword" -# " arguments for each optimizers." -# f"Got {len_optimizer} optimizers, and" -# f" {len_optimizer_kwargs} dicitionaries" -# ) - -# # extra features handling -# if (extra_features is None) or (len(extra_features) == 0): -# extra_features = [None] * len_model -# else: -# # if we only have a list of extra features -# if not isinstance(extra_features[0], (tuple, list)): -# extra_features = [extra_features] * len_model -# else: # if we have a list of list extra features -# if len(extra_features) != len_model: -# raise ValueError( -# "You passed a list of extrafeatures list with len" -# f"different of models len. Expected {len_model} " -# f"got {len(extra_features)}. If you want to use " -# "the same list of extra features for all models, " -# "just pass a list of extrafeatures and not a list " -# "of list of extra features." -# ) - -# # assigning model and optimizers -# self._pina_models = [] -# self._pina_optimizers = [] - -# for idx in range(len_model): -# model_ = Network( -# model=models[idx], -# input_variables=problem.input_variables, -# output_variables=problem.output_variables, -# extra_features=extra_features[idx], -# ) -# optim_ = optimizers[idx]( -# model_.parameters(), **optimizers_kwargs[idx] -# ) -# self._pina_models.append(model_) -# self._pina_optimizers.append(optim_) - -# # assigning problem -# self._pina_problem = problem - -# @abstractmethod -# def forward(self, *args, **kwargs): -# pass - -# @abstractmethod -# def training_step(self): -# pass - -# @abstractmethod -# def configure_optimizers(self): -# pass - -# @property -# def models(self): -# """ -# The torch model.""" -# return self._pina_models - -# @property -# def optimizers(self): -# """ -# The torch model.""" -# return self._pina_optimizers - -# @property -# def problem(self): -# """ -# The problem formulation.""" -# return self._pina_problem - -# def on_train_start(self): -# """ -# On training epoch start this function is call to do global checks for -# the different solvers. -# """ - -# # 1. Check the verison for dataloader -# dataloader = self.trainer.train_dataloader -# if sys.version_info < (3, 8): -# dataloader = dataloader.loaders -# self._dataloader = dataloader - -# return super().on_train_start() - - # @model.setter - # def model(self, new_model): - # """ - # Set the torch.""" - # check_consistency(new_model, nn.Module, 'torch model') - # self._model= new_model - - # @problem.setter - # def problem(self, problem): - # """ - # Set the problem formulation.""" - # check_consistency(problem, AbstractProblem, 'pina problem') - # self._problem = problem - class SolverInterface(pytorch_lightning.LightningModule, metaclass=ABCMeta): """ Solver base class. This class inherits is a wrapper of @@ -181,10 +19,12 @@ class SolverInterface(pytorch_lightning.LightningModule, metaclass=ABCMeta): def __init__( self, - model, + models, problem, - optimizer, - scheduler, + optimizers, + schedulers, + extra_features, + use_lt=True ): """ :param model: A torch neural network model instance. @@ -197,22 +37,45 @@ def __init__( super().__init__() # check consistency of the inputs - check_consistency(model, torch.nn.Module) check_consistency(problem, AbstractProblem) - check_consistency(optimizer, Optimizer) - check_consistency(scheduler, Scheduler) - - # put everything in a list if only one input - if not isinstance(model, list): - model = [model] - if not isinstance(scheduler, list): - scheduler = [scheduler] - if not isinstance(optimizer, list): - optimizer = [optimizer] - - # number of models and optimizers - len_model = len(model) - len_optimizer = len(optimizer) + self._check_solver_consistency(problem) + + #Check consistency of models argument and encapsulate in list + if not isinstance(models, list): + check_consistency(models, torch.nn.Module) + # put everything in a list if only one input + models = [models] + else: + for idx in range(len(models)): + # Check consistency + check_consistency(models[idx], torch.nn.Module) + len_model = len(models) + + #If use_lt is true add extract operation in input + if use_lt is True: + for idx in range(len(models)): + models[idx] = Network( + model = models[idx], + input_variables=problem.input_variables, + output_variables=problem.output_variables, + extra_features=extra_features, ) + + #Check scheduler consistency + encapsulation + if not isinstance(schedulers, list): + check_consistency(schedulers, Scheduler) + schedulers = [schedulers] + else: + for scheduler in schedulers: + check_consistency(scheduler, Scheduler) + + #Check optimizer consistency + encapsulation + if not isinstance(optimizers, list): + check_consistency(optimizers, Optimizer) + optimizers = [optimizers] + else: + for optimizer in optimizers: + check_consistency(optimizer, Optimizer) + len_optimizer = len(optimizers) # check length consistency optimizers if len_model != len_optimizer: @@ -223,10 +86,12 @@ def __init__( ) # extra features handling + + self._pina_models = models + self._pina_optimizers = optimizers + self._pina_schedulers = schedulers self._pina_problem = problem - self._pina_model = model - self._pina_optimizer = optimizer - self._pina_scheduler = scheduler + @abstractmethod def forward(self, *args, **kwargs): @@ -244,13 +109,13 @@ def configure_optimizers(self): def models(self): """ The torch model.""" - return self._pina_model + return self._pina_models @property def optimizers(self): """ The torch model.""" - return self._pina_optimizer + return self._pina_optimizers @property def problem(self): @@ -272,16 +137,10 @@ def on_train_start(self): return super().on_train_start() - # @model.setter - # def model(self, new_model): - # """ - # Set the torch.""" - # check_consistency(new_model, nn.Module, 'torch model') - # self._model= new_model - - # @problem.setter - # def problem(self, problem): - # """ - # Set the problem formulation.""" - # check_consistency(problem, AbstractProblem, 'pina problem') - # self._problem = problem + def _check_solver_consistency(self, problem): + """ + TODO + """ + for _, condition in problem.conditions.items(): + if not set(self.accepted_condition_types).issubset(condition.condition_type): + raise ValueError(f'{self.__name__} support only dose not support condition {condition.condition_type}') diff --git a/pina/solvers/supervised.py b/pina/solvers/supervised.py index c44d5a1e2..32f687ed0 100644 --- a/pina/solvers/supervised.py +++ b/pina/solvers/supervised.py @@ -2,9 +2,7 @@ import torch from torch.nn.modules.loss import _Loss - - -from ..optim import Optimizer, Scheduler, TorchOptimizer, TorchScheduler +from ..optim import TorchOptimizer, TorchScheduler from .solver import SolverInterface from ..label_tensor import LabelTensor from ..utils import check_consistency @@ -39,14 +37,17 @@ class SupervisedSolver(SolverInterface): we are seeking to approximate multiple (discretised) functions given multiple (discretised) input functions. """ + accepted_condition_types = ['supervised'] + __name__ = 'SupervisedSolver' def __init__( - self, - problem, - model, - loss=None, - optimizer=None, - scheduler=None, + self, + problem, + model, + loss=None, + optimizer=None, + scheduler=None, + extra_features=None ): """ :param AbstractProblem problem: The formualation of the problem. @@ -57,11 +58,8 @@ def __init__( features to use as augmented input. :param torch.optim.Optimizer optimizer: The neural network optimizer to use; default is :class:`torch.optim.Adam`. - :param dict optimizer_kwargs: Optimizer constructor keyword args. - :param float lr: The learning rate; default is 0.001. :param torch.optim.LRScheduler scheduler: Learning rate scheduler. - :param dict scheduler_kwargs: LR scheduler constructor keyword args. """ if loss is None: loss = torch.nn.MSELoss() @@ -74,18 +72,19 @@ def __init__( torch.optim.lr_scheduler.ConstantLR) super().__init__( - model=model, + models=model, problem=problem, - optimizer=optimizer, - scheduler=scheduler, + optimizers=optimizer, + schedulers=scheduler, + extra_features=extra_features ) # check consistency check_consistency(loss, (LossInterface, _Loss), subclass=False) self._loss = loss - self._model = self._pina_model[0] - self._optimizer = self._pina_optimizer[0] - self._scheduler = self._pina_scheduler[0] + self._model = self._pina_models[0] + self._optimizer = self._pina_optimizers[0] + self._scheduler = self._pina_schedulers[0] def forward(self, x): """Forward pass implementation for the solver. @@ -97,12 +96,7 @@ def forward(self, x): output = self._model(x) - output.labels = { - 1: { - "name": "output", - "dof": self.problem.output_variables - } - } + output.labels = self.problem.output_variables return output def configure_optimizers(self): @@ -128,16 +122,14 @@ def training_step(self, batch, batch_idx): :return: The sum of the loss functions. :rtype: LabelTensor """ - - condition_idx = batch.condition + condition_idx = batch.supervised.condition_indices for condition_id in range(condition_idx.min(), condition_idx.max() + 1): condition_name = self._dataloader.condition_names[condition_id] condition = self.problem.conditions[condition_name] - pts = batch.input - out = batch.output - + pts = batch.supervised.input_points + out = batch.supervised.output_points if condition_name not in self.problem.conditions: raise RuntimeError("Something wrong happened.") @@ -167,8 +159,8 @@ def loss_data(self, input_pts, output_pts): the network output against the true solution. This function should not be override if not intentionally. - :param LabelTensor input_tensor: The input to the neural networks. - :param LabelTensor output_tensor: The true solution to compare the + :param LabelTensor input_pts: The input to the neural networks. + :param LabelTensor output_pts: The true solution to compare the network solution. :return: The residual loss averaged on the input coordinates :rtype: torch.Tensor @@ -181,7 +173,7 @@ def scheduler(self): Scheduler for training. """ return self._scheduler - + @property def optimizer(self): """ diff --git a/pina/trainer.py b/pina/trainer.py index ba18f3392..49c6a4017 100644 --- a/pina/trainer.py +++ b/pina/trainer.py @@ -3,13 +3,13 @@ import torch import pytorch_lightning from .utils import check_consistency -from .data import SamplePointDataset, SamplePointLoader, DataPointDataset +from .data import PinaDataModule from .solvers.solver import SolverInterface class Trainer(pytorch_lightning.Trainer): - def __init__(self, solver, batch_size=None, **kwargs): + def __init__(self, solver, batch_size=None, train_size=.7, test_size=.2, eval_size=.1, **kwargs): """ PINA Trainer class for costumizing every aspect of training via flags. @@ -31,10 +31,11 @@ def __init__(self, solver, batch_size=None, **kwargs): check_consistency(solver, SolverInterface) if batch_size is not None: check_consistency(batch_size, int) - + self.train_size = train_size + self.test_size = test_size + self.eval_size = eval_size self.solver = solver self.batch_size = batch_size - self._create_loader() self._move_to_device() @@ -69,11 +70,12 @@ def _create_loader(self): raise RuntimeError("Parallel training is not supported yet.") device = devices[0] - dataset_phys = SamplePointDataset(self.solver.problem, device) - dataset_data = DataPointDataset(self.solver.problem, device) - self._loader = SamplePointLoader( - dataset_phys, dataset_data, batch_size=self.batch_size, shuffle=True - ) + + data_module = PinaDataModule(problem=self.solver.problem, device=device, + train_size=self.train_size, test_size=self.test_size, + eval_size=self.eval_size) + data_module.setup() + self._loader = data_module.train_dataloader() def train(self, **kwargs): """ @@ -89,3 +91,7 @@ def solver(self): Returning trainer solver. """ return self._solver + + @solver.setter + def solver(self, solver): + self._solver = solver diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 40f219228..264f794bf 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -1,44 +1,45 @@ +import math import torch -import pytest - -from pina.data.dataset import SamplePointDataset, SamplePointLoader, DataPointDataset +from pina.data import SamplePointDataset, SupervisedDataset, PinaDataModule, UnsupervisedDataset, unsupervised_dataset +from pina.data import PinaDataLoader from pina import LabelTensor, Condition from pina.equation import Equation from pina.domain import CartesianDomain from pina.problem import SpatialProblem -from pina.model import FeedForward from pina.operators import laplacian from pina.equation.equation_factory import FixedValue def laplace_equation(input_, output_): - force_term = (torch.sin(input_.extract(['x'])*torch.pi) * - torch.sin(input_.extract(['y'])*torch.pi)) + force_term = (torch.sin(input_.extract(['x']) * torch.pi) * + torch.sin(input_.extract(['y']) * torch.pi)) delta_u = laplacian(output_.extract(['u']), input_) return delta_u - force_term + my_laplace = Equation(laplace_equation) in_ = LabelTensor(torch.tensor([[0., 1.]]), ['x', 'y']) out_ = LabelTensor(torch.tensor([[0.]]), ['u']) in2_ = LabelTensor(torch.rand(60, 2), ['x', 'y']) out2_ = LabelTensor(torch.rand(60, 1), ['u']) + class Poisson(SpatialProblem): output_variables = ['u'] spatial_domain = CartesianDomain({'x': [0, 1], 'y': [0, 1]}) conditions = { 'gamma1': Condition( - location=CartesianDomain({'x': [0, 1], 'y': 1}), + domain=CartesianDomain({'x': [0, 1], 'y': 1}), equation=FixedValue(0.0)), 'gamma2': Condition( - location=CartesianDomain({'x': [0, 1], 'y': 0}), + domain=CartesianDomain({'x': [0, 1], 'y': 0}), equation=FixedValue(0.0)), 'gamma3': Condition( - location=CartesianDomain({'x': 1, 'y': [0, 1]}), + domain=CartesianDomain({'x': 1, 'y': [0, 1]}), equation=FixedValue(0.0)), 'gamma4': Condition( - location=CartesianDomain({'x': 0, 'y': [0, 1]}), + domain=CartesianDomain({'x': 0, 'y': [0, 1]}), equation=FixedValue(0.0)), 'D': Condition( input_points=LabelTensor(torch.rand(size=(100, 2)), ['x', 'y']), @@ -48,75 +49,114 @@ class Poisson(SpatialProblem): output_points=out_), 'data2': Condition( input_points=in2_, - output_points=out2_) + output_points=out2_), + 'unsupervised': Condition( + input_points=LabelTensor(torch.rand(size=(45, 2)), ['x', 'y']), + conditional_variables=LabelTensor(torch.ones(size=(45, 1)), ['alpha']), + ), + 'unsupervised2': Condition( + input_points=LabelTensor(torch.rand(size=(90, 2)), ['x', 'y']), + conditional_variables=LabelTensor(torch.ones(size=(90, 1)), ['alpha']), + ) } + boundaries = ['gamma1', 'gamma2', 'gamma3', 'gamma4'] poisson = Poisson() poisson.discretise_domain(10, 'grid', locations=boundaries) + def test_sample(): sample_dataset = SamplePointDataset(poisson, device='cpu') assert len(sample_dataset) == 140 - assert sample_dataset.pts.shape == (140, 2) - assert sample_dataset.pts.labels == ['x', 'y'] - assert sample_dataset.condition_indeces.dtype == torch.int64 - assert sample_dataset.condition_indeces.max() == torch.tensor(4) - assert sample_dataset.condition_indeces.min() == torch.tensor(0) + assert sample_dataset.input_points.shape == (140, 2) + assert sample_dataset.input_points.labels == ['x', 'y'] + assert sample_dataset.condition_indices.dtype == torch.uint8 + assert sample_dataset.condition_indices.max() == torch.tensor(4) + assert sample_dataset.condition_indices.min() == torch.tensor(0) + def test_data(): - dataset = DataPointDataset(poisson, device='cpu') + dataset = SupervisedDataset(poisson, device='cpu') assert len(dataset) == 61 - assert dataset.input_pts.shape == (61, 2) - assert dataset.input_pts.labels == ['x', 'y'] - assert dataset.output_pts.shape == (61, 1 ) - assert dataset.output_pts.labels == ['u'] - assert dataset.condition_indeces.dtype == torch.int64 - assert dataset.condition_indeces.max() == torch.tensor(1) - assert dataset.condition_indeces.min() == torch.tensor(0) - -def test_loader(): - sample_dataset = SamplePointDataset(poisson, device='cpu') - data_dataset = DataPointDataset(poisson, device='cpu') - loader = SamplePointLoader(sample_dataset, data_dataset, batch_size=10) - + assert dataset['input_points'].shape == (61, 2) + assert dataset.input_points.shape == (61, 2) + assert dataset['input_points'].labels == ['x', 'y'] + assert dataset.input_points.labels == ['x', 'y'] + assert dataset['input_points', 3:].shape == (58, 2) + assert dataset[3:][1].labels == ['u'] + assert dataset.output_points.shape == (61, 1) + assert dataset.output_points.labels == ['u'] + assert dataset.condition_indices.dtype == torch.uint8 + assert dataset.condition_indices.max() == torch.tensor(1) + assert dataset.condition_indices.min() == torch.tensor(0) + + +def test_unsupervised(): + dataset = UnsupervisedDataset(poisson, device='cpu') + assert len(dataset) == 135 + assert dataset.input_points.shape == (135, 2) + assert dataset.input_points.labels == ['x', 'y'] + assert dataset.input_points[3:].shape == (132, 2) + + assert dataset.conditional_variables.shape == (135, 1) + assert dataset.conditional_variables.labels == ['alpha'] + assert dataset.condition_indices.dtype == torch.uint8 + assert dataset.condition_indices.max() == torch.tensor(1) + assert dataset.condition_indices.min() == torch.tensor(0) + + +def test_data_module(): + data_module = PinaDataModule(poisson, device='cpu') + data_module.setup() + loader = data_module.train_dataloader() + assert isinstance(loader, PinaDataLoader) + assert isinstance(loader, PinaDataLoader) + + data_module = PinaDataModule(poisson, device='cpu', batch_size=10, shuffle=False) + data_module.setup() + loader = data_module.train_dataloader() + assert len(loader) == 24 + for i in loader: + assert len(i) <= 10 + len_ref = sum([math.ceil(len(dataset) * 0.7) for dataset in data_module.datasets]) + len_real = sum([len(dataset) for dataset in data_module.splits['train'].values()]) + assert len_ref == len_real + + supervised_dataset = SupervisedDataset(poisson, device='cpu') + data_module = PinaDataModule(poisson, device='cpu', batch_size=10, shuffle=False, datasets=[supervised_dataset]) + data_module.setup() + loader = data_module.train_dataloader() for batch in loader: - assert len(batch) in [2, 3] - assert batch['pts'].shape[0] <= 10 - assert batch['pts'].requires_grad == True - assert batch['pts'].labels == ['x', 'y'] - - loader2 = SamplePointLoader(sample_dataset, data_dataset, batch_size=None) - assert len(list(loader2)) == 2 - -def test_loader2(): - poisson2 = Poisson() - del poisson.conditions['data2'] - del poisson2.conditions['data'] - poisson2.discretise_domain(10, 'grid', locations=boundaries) - sample_dataset = SamplePointDataset(poisson, device='cpu') - data_dataset = DataPointDataset(poisson, device='cpu') - loader = SamplePointLoader(sample_dataset, data_dataset, batch_size=10) + assert len(batch) <= 10 + physics_dataset = SamplePointDataset(poisson, device='cpu') + data_module = PinaDataModule(poisson, device='cpu', batch_size=10, shuffle=False, datasets=[physics_dataset]) + data_module.setup() + loader = data_module.train_dataloader() for batch in loader: - assert len(batch) == 2 # only phys condtions - assert batch['pts'].shape[0] <= 10 - assert batch['pts'].requires_grad == True - assert batch['pts'].labels == ['x', 'y'] - -def test_loader3(): - poisson2 = Poisson() - del poisson.conditions['gamma1'] - del poisson.conditions['gamma2'] - del poisson.conditions['gamma3'] - del poisson.conditions['gamma4'] - del poisson.conditions['D'] - sample_dataset = SamplePointDataset(poisson, device='cpu') - data_dataset = DataPointDataset(poisson, device='cpu') - loader = SamplePointLoader(sample_dataset, data_dataset, batch_size=10) + assert len(batch) <= 10 + unsupervised_dataset = UnsupervisedDataset(poisson, device='cpu') + data_module = PinaDataModule(poisson, device='cpu', batch_size=10, shuffle=False, datasets=[unsupervised_dataset]) + data_module.setup() + loader = data_module.train_dataloader() for batch in loader: - assert len(batch) == 2 # only phys condtions - assert batch['pts'].shape[0] <= 10 - assert batch['pts'].requires_grad == True - assert batch['pts'].labels == ['x', 'y'] + assert len(batch) <= 10 + + +def test_loader(): + data_module = PinaDataModule(poisson, device='cpu', batch_size=10) + data_module.setup() + loader = data_module.train_dataloader() + assert isinstance(loader, PinaDataLoader) + assert len(loader) == 24 + for i in loader: + assert len(i) <= 10 + assert i.supervised.input_points.labels == ['x', 'y'] + assert i.physics.input_points.labels == ['x', 'y'] + assert i.unsupervised.input_points.labels == ['x', 'y'] + assert i.supervised.input_points.requires_grad == True + assert i.physics.input_points.requires_grad == True + assert i.unsupervised.input_points.requires_grad == True +test_loader() \ No newline at end of file diff --git a/tests/test_solvers/test_supervised_solver.py b/tests/test_solvers/test_supervised_solver.py index 912480bb8..8ceadcd93 100644 --- a/tests/test_solvers/test_supervised_solver.py +++ b/tests/test_solvers/test_supervised_solver.py @@ -1,51 +1,28 @@ import torch - -from pina.problem import AbstractProblem +import pytest +from pina.problem import AbstractProblem, SpatialProblem from pina import Condition, LabelTensor from pina.solvers import SupervisedSolver -from pina.trainer import Trainer from pina.model import FeedForward -from pina.loss import LpLoss -from pina.solvers import GraphSupervisedSolver +from pina.equation.equation import Equation +from pina.equation.equation_factory import FixedValue +from pina.operators import laplacian +from pina.domain import CartesianDomain +from pina.trainer import Trainer + +in_ = LabelTensor(torch.tensor([[0., 1.]]), ['u_0', 'u_1']) +out_ = LabelTensor(torch.tensor([[0.]]), ['u']) + class NeuralOperatorProblem(AbstractProblem): input_variables = ['u_0', 'u_1'] output_variables = ['u'] - domains = { - 'pts': LabelTensor( - torch.rand(100, 2), - labels={1: {'name': 'space', 'dof': ['u_0', 'u_1']}} - ) - } - conditions = { - 'data' : Condition( - domain='pts', - output_points=LabelTensor( - torch.rand(100, 1), - labels={1: {'name': 'output', 'dof': ['u']}} - ) - ) - } -class NeuralOperatorProblemGraph(AbstractProblem): - input_variables = ['x', 'y', 'u_0', 'u_1'] - output_variables = ['u'] - domains = { - 'pts': LabelTensor( - torch.rand(100, 4), - labels={1: {'name': 'space', 'dof': ['x', 'y', 'u_0', 'u_1']}} - ) - } conditions = { - 'data' : Condition( - domain='pts', - output_points=LabelTensor( - torch.rand(100, 1), - labels={1: {'name': 'output', 'dof': ['u']}} - ) - ) + 'data': Condition(input_points=in_, output_points=out_), } + class myFeature(torch.nn.Module): """ Feature: sin(x) @@ -61,117 +38,106 @@ def forward(self, x): problem = NeuralOperatorProblem() -problem_graph = NeuralOperatorProblemGraph() -# make the problem + extra feats extra_feats = [myFeature()] -model = FeedForward(len(problem.input_variables), - len(problem.output_variables)) +model = FeedForward(len(problem.input_variables), len(problem.output_variables)) model_extra_feats = FeedForward( - len(problem.input_variables) + 1, - len(problem.output_variables)) + len(problem.input_variables) + 1, len(problem.output_variables)) def test_constructor(): SupervisedSolver(problem=problem, model=model) -# def test_constructor_extra_feats(): -# SupervisedSolver(problem=problem, model=model_extra_feats, extra_features=extra_feats) - -''' -class AutoSolver(SupervisedSolver): - - def forward(self, input): - from pina.graph import Graph - print(Graph) - print(input) - if not isinstance(input, Graph): - input = Graph.build('radius', nodes_coordinates=input, nodes_data=torch.rand(input.shape), radius=0.2) - print(input) - print(input.data.edge_index) - print(input.data) - g = self._model(input.data, edge_index=input.data.edge_index) - g.labels = {1: {'name': 'output', 'dof': ['u']}} - return g - du_dt_new = LabelTensor(self.model(graph).reshape(-1,1), labels = ['du']) - - return du_dt_new -''' - -class GraphModel(torch.nn.Module): - def __init__(self, in_channels, out_channels): - from torch_geometric.nn import GCNConv, NNConv - super().__init__() - self.conv1 = GCNConv(in_channels, 16) - self.conv2 = GCNConv(16, out_channels) - - def forward(self, data, edge_index): - print(data) - x = data.x - print(x) - x = self.conv1(x, edge_index) - x = x.relu() - x = self.conv2(x, edge_index) - return x - -def test_graph(): - solver = GraphSupervisedSolver(problem=problem_graph, model=GraphModel(2, 1), loss=LpLoss(), - nodes_coordinates=['x', 'y'], nodes_data=['u_0', 'u_1']) - trainer = Trainer(solver=solver, max_epochs=30, accelerator='cpu', batch_size=20) - trainer.train() +test_constructor() + + +def laplace_equation(input_, output_): + force_term = (torch.sin(input_.extract(['x']) * torch.pi) * + torch.sin(input_.extract(['y']) * torch.pi)) + delta_u = laplacian(output_.extract(['u']), input_) + return delta_u - force_term + + +my_laplace = Equation(laplace_equation) + + +class Poisson(SpatialProblem): + output_variables = ['u'] + spatial_domain = CartesianDomain({'x': [0, 1], 'y': [0, 1]}) + + conditions = { + 'gamma1': + Condition(domain=CartesianDomain({ + 'x': [0, 1], + 'y': 1 + }), + equation=FixedValue(0.0)), + 'gamma2': + Condition(domain=CartesianDomain({ + 'x': [0, 1], + 'y': 0 + }), + equation=FixedValue(0.0)), + 'gamma3': + Condition(domain=CartesianDomain({ + 'x': 1, + 'y': [0, 1] + }), + equation=FixedValue(0.0)), + 'gamma4': + Condition(domain=CartesianDomain({ + 'x': 0, + 'y': [0, 1] + }), + equation=FixedValue(0.0)), + 'D': + Condition(domain=CartesianDomain({ + 'x': [0, 1], + 'y': [0, 1] + }), + equation=my_laplace), + 'data': + Condition(input_points=in_, output_points=out_) + } + + def poisson_sol(self, pts): + return -(torch.sin(pts.extract(['x']) * torch.pi) * + torch.sin(pts.extract(['y']) * torch.pi)) / (2 * torch.pi ** 2) + + truth_solution = poisson_sol + + +def test_wrong_constructor(): + poisson_problem = Poisson() + with pytest.raises(ValueError): + SupervisedSolver(problem=poisson_problem, model=model) def test_train_cpu(): - solver = SupervisedSolver(problem = problem, model=model, loss=LpLoss()) - trainer = Trainer(solver=solver, max_epochs=300, accelerator='cpu', batch_size=20) + solver = SupervisedSolver(problem=problem, model=model) + trainer = Trainer(solver=solver, + max_epochs=200, + accelerator='gpu', + batch_size=5, + train_size=1, + test_size=0., + eval_size=0.) trainer.train() +test_train_cpu() -# def test_train_restore(): -# tmpdir = "tests/tmp_restore" -# solver = SupervisedSolver(problem=problem, -# model=model, -# extra_features=None, -# loss=LpLoss()) -# trainer = Trainer(solver=solver, -# max_epochs=5, -# accelerator='cpu', -# default_root_dir=tmpdir) -# trainer.train() -# ntrainer = Trainer(solver=solver, max_epochs=15, accelerator='cpu') -# t = ntrainer.train( -# ckpt_path=f'{tmpdir}/lightning_logs/version_0/checkpoints/epoch=4-step=5.ckpt') -# import shutil -# shutil.rmtree(tmpdir) - - -# def test_train_load(): -# tmpdir = "tests/tmp_load" -# solver = SupervisedSolver(problem=problem, -# model=model, -# extra_features=None, -# loss=LpLoss()) -# trainer = Trainer(solver=solver, -# max_epochs=15, -# accelerator='cpu', -# default_root_dir=tmpdir) -# trainer.train() -# new_solver = SupervisedSolver.load_from_checkpoint( -# f'{tmpdir}/lightning_logs/version_0/checkpoints/epoch=14-step=15.ckpt', -# problem = problem, model=model) -# test_pts = LabelTensor(torch.rand(20, 2), problem.input_variables) -# assert new_solver.forward(test_pts).shape == (20, 1) -# assert new_solver.forward(test_pts).shape == solver.forward(test_pts).shape -# torch.testing.assert_close( -# new_solver.forward(test_pts), -# solver.forward(test_pts)) -# import shutil -# shutil.rmtree(tmpdir) - -# def test_train_extra_feats_cpu(): -# pinn = SupervisedSolver(problem=problem, -# model=model_extra_feats, -# extra_features=extra_feats) -# trainer = Trainer(solver=pinn, max_epochs=5, accelerator='cpu') -# trainer.train() -test_graph() \ No newline at end of file +def test_extra_features_constructor(): + SupervisedSolver(problem=problem, + model=model_extra_feats, + extra_features=extra_feats) + + +def test_extra_features_train_cpu(): + solver = SupervisedSolver(problem=problem, + model=model_extra_feats, + extra_features=extra_feats) + trainer = Trainer(solver=solver, + max_epochs=200, + accelerator='gpu', + batch_size=5) + trainer.train() From 7dbf28914a50048d7455aa373894b0007d73355b Mon Sep 17 00:00:00 2001 From: FilippoOlivo Date: Tue, 22 Oct 2024 14:26:39 +0200 Subject: [PATCH 09/14] Correct codacy warnings --- pina/__init__.py | 12 +- pina/data/__init__.py | 9 +- pina/data/base_dataset.py | 14 +- pina/data/pina_batch.py | 6 +- pina/data/pina_dataloader.py | 3 +- pina/optim/torch_optimizer.py | 6 +- pina/optim/torch_scheduler.py | 7 +- pina/solvers/solver.py | 37 +++-- pina/solvers/supervised.py | 44 +++--- tests/test_dataset.py | 98 ++++++++----- tests/test_label_tensor/test_label_tensor.py | 134 ++++++++++-------- .../test_label_tensor/test_label_tensor_01.py | 9 +- tests/test_operators.py | 4 + tests/test_optimizer.py | 10 +- tests/test_problem.py | 70 ++++----- 15 files changed, 252 insertions(+), 211 deletions(-) diff --git a/pina/__init__.py b/pina/__init__.py index d110d2842..30f35a6a5 100644 --- a/pina/__init__.py +++ b/pina/__init__.py @@ -1,12 +1,6 @@ __all__ = [ - "PINN", - "Trainer", - "LabelTensor", - "Plotter", - "Condition", - "SamplePointDataset", - "PinaDataModule", - "PinaDataLoader" + "PINN", "Trainer", "LabelTensor", "Plotter", "Condition", + "SamplePointDataset", "PinaDataModule", "PinaDataLoader" ] from .meta import * @@ -17,4 +11,4 @@ from .condition.condition import Condition from .data import SamplePointDataset from .data import PinaDataModule -from .data import PinaDataLoader \ No newline at end of file +from .data import PinaDataLoader diff --git a/pina/data/__init__.py b/pina/data/__init__.py index 0a1b5905e..2b3a126a7 100644 --- a/pina/data/__init__.py +++ b/pina/data/__init__.py @@ -2,13 +2,8 @@ Import data classes """ __all__ = [ - 'PinaDataLoader', - 'SupervisedDataset', - 'SamplePointDataset', - 'UnsupervisedDataset', - 'Batch', - 'PinaDataModule', - 'BaseDataset' + 'PinaDataLoader', 'SupervisedDataset', 'SamplePointDataset', + 'UnsupervisedDataset', 'Batch', 'PinaDataModule', 'BaseDataset' ] from .pina_dataloader import PinaDataLoader diff --git a/pina/data/base_dataset.py b/pina/data/base_dataset.py index f095afa0c..f1d17ae31 100644 --- a/pina/data/base_dataset.py +++ b/pina/data/base_dataset.py @@ -22,10 +22,12 @@ def __new__(cls, problem, device): dataset will be loaded. """ if cls is BaseDataset: - raise TypeError('BaseDataset cannot be instantiated directly. Use a subclass.') + raise TypeError( + 'BaseDataset cannot be instantiated directly. Use a subclass.') if not hasattr(cls, '__slots__'): - raise TypeError('Something is wrong, __slots__ must be defined in subclasses.') - return super().__new__(cls) + raise TypeError( + 'Something is wrong, __slots__ must be defined in subclasses.') + return super(BaseDataset, cls).__new__(cls) def __init__(self, problem, device): """" @@ -79,7 +81,8 @@ def __len__(self): def __getattribute__(self, item): attribute = super().__getattribute__(item) - if isinstance(attribute, LabelTensor) and attribute.dtype == torch.float32: + if isinstance(attribute, + LabelTensor) and attribute.dtype == torch.float32: attribute = attribute.to(device=self.device).requires_grad_() return attribute @@ -101,7 +104,8 @@ def __getitem__(self, idx): if all(isinstance(x, int) for x in idx): to_return_list = [] for i in self.__slots__: - to_return_list.append(getattr(self, i)[[idx]].to(self.device)) + to_return_list.append( + getattr(self, i)[[idx]].to(self.device)) return to_return_list raise ValueError(f'Invalid index {idx}') diff --git a/pina/data/pina_batch.py b/pina/data/pina_batch.py index 7e46a2218..f61e0020b 100644 --- a/pina/data/pina_batch.py +++ b/pina/data/pina_batch.py @@ -5,6 +5,7 @@ class Batch: + def __init__(self, dataset_dict, idx_dict): for k, v in dataset_dict.items(): @@ -29,5 +30,6 @@ def __len__(self): def __getattr__(self, item): if not item in dir(self): raise AttributeError(f'Batch instance has no attribute {item}') - return PinaSubset(getattr(self, item).dataset, - getattr(self, item).indices[self.coordinates_dict[item]]) + return PinaSubset( + getattr(self, item).dataset, + getattr(self, item).indices[self.coordinates_dict[item]]) diff --git a/pina/data/pina_dataloader.py b/pina/data/pina_dataloader.py index d62847574..cbd8fe82d 100644 --- a/pina/data/pina_dataloader.py +++ b/pina/data/pina_dataloader.py @@ -50,7 +50,8 @@ def _init_batches(self, batch_size=None): temp_dict[k] = slice(i * v, (i + 1) * v) else: temp_dict[k] = slice(i * v, len(self.dataset_dict[k])) - self.batches.append(Batch(idx_dict=temp_dict, dataset_dict=self.dataset_dict)) + self.batches.append( + Batch(idx_dict=temp_dict, dataset_dict=self.dataset_dict)) def __iter__(self): """ diff --git a/pina/optim/torch_optimizer.py b/pina/optim/torch_optimizer.py index 239819a4f..ed90846c6 100644 --- a/pina/optim/torch_optimizer.py +++ b/pina/optim/torch_optimizer.py @@ -5,6 +5,7 @@ from ..utils import check_consistency from .optimizer_interface import Optimizer + class TorchOptimizer(Optimizer): def __init__(self, optimizer_class, **kwargs): @@ -14,6 +15,5 @@ def __init__(self, optimizer_class, **kwargs): self.kwargs = kwargs def hook(self, parameters): - self.optimizer_instance = self.optimizer_class( - parameters, **self.kwargs - ) \ No newline at end of file + self.optimizer_instance = self.optimizer_class(parameters, + **self.kwargs) diff --git a/pina/optim/torch_scheduler.py b/pina/optim/torch_scheduler.py index 50e1d91f7..9aa187d0c 100644 --- a/pina/optim/torch_scheduler.py +++ b/pina/optim/torch_scheduler.py @@ -5,13 +5,13 @@ from torch.optim.lr_scheduler import LRScheduler # torch >= 2.0 except ImportError: from torch.optim.lr_scheduler import ( - _LRScheduler as LRScheduler, - ) # torch < 2.0 + _LRScheduler as LRScheduler, ) # torch < 2.0 from ..utils import check_consistency from .optimizer_interface import Optimizer from .scheduler_interface import Scheduler + class TorchScheduler(Scheduler): def __init__(self, scheduler_class, **kwargs): @@ -23,5 +23,4 @@ def __init__(self, scheduler_class, **kwargs): def hook(self, optimizer): check_consistency(optimizer, Optimizer) self.scheduler_instance = self.scheduler_class( - optimizer.optimizer_instance, **self.kwargs - ) \ No newline at end of file + optimizer.optimizer_instance, **self.kwargs) diff --git a/pina/solvers/solver.py b/pina/solvers/solver.py index 8b3ddae71..1c6aa2b2b 100644 --- a/pina/solvers/solver.py +++ b/pina/solvers/solver.py @@ -17,15 +17,13 @@ class SolverInterface(pytorch_lightning.LightningModule, metaclass=ABCMeta): LightningModule methods. """ - def __init__( - self, - models, - problem, - optimizers, - schedulers, - extra_features, - use_lt=True - ): + def __init__(self, + models, + problem, + optimizers, + schedulers, + extra_features, + use_lt=True): """ :param model: A torch neural network model instance. :type model: torch.nn.Module @@ -55,10 +53,11 @@ def __init__( if use_lt is True: for idx in range(len(models)): models[idx] = Network( - model = models[idx], + model=models[idx], input_variables=problem.input_variables, output_variables=problem.output_variables, - extra_features=extra_features, ) + extra_features=extra_features, + ) #Check scheduler consistency + encapsulation if not isinstance(schedulers, list): @@ -79,11 +78,9 @@ def __init__( # check length consistency optimizers if len_model != len_optimizer: - raise ValueError( - "You must define one optimizer for each model." - f"Got {len_model} models, and {len_optimizer}" - " optimizers." - ) + raise ValueError("You must define one optimizer for each model." + f"Got {len_model} models, and {len_optimizer}" + " optimizers.") # extra features handling @@ -92,7 +89,6 @@ def __init__( self._pina_schedulers = schedulers self._pina_problem = problem - @abstractmethod def forward(self, *args, **kwargs): pass @@ -142,5 +138,8 @@ def _check_solver_consistency(self, problem): TODO """ for _, condition in problem.conditions.items(): - if not set(self.accepted_condition_types).issubset(condition.condition_type): - raise ValueError(f'{self.__name__} support only dose not support condition {condition.condition_type}') + if not set(self.accepted_condition_types).issubset( + condition.condition_type): + raise ValueError( + f'{self.__name__} support only dose not support condition {condition.condition_type}' + ) diff --git a/pina/solvers/supervised.py b/pina/solvers/supervised.py index 32f687ed0..a0b0f83ed 100644 --- a/pina/solvers/supervised.py +++ b/pina/solvers/supervised.py @@ -40,15 +40,13 @@ class SupervisedSolver(SolverInterface): accepted_condition_types = ['supervised'] __name__ = 'SupervisedSolver' - def __init__( - self, - problem, - model, - loss=None, - optimizer=None, - scheduler=None, - extra_features=None - ): + def __init__(self, + problem, + model, + loss=None, + optimizer=None, + scheduler=None, + extra_features=None): """ :param AbstractProblem problem: The formualation of the problem. :param torch.nn.Module model: The neural network model to use. @@ -68,16 +66,13 @@ def __init__( optimizer = TorchOptimizer(torch.optim.Adam, lr=0.001) if scheduler is None: - scheduler = TorchScheduler( - torch.optim.lr_scheduler.ConstantLR) + scheduler = TorchScheduler(torch.optim.lr_scheduler.ConstantLR) - super().__init__( - models=model, - problem=problem, - optimizers=optimizer, - schedulers=scheduler, - extra_features=extra_features - ) + super().__init__(models=model, + problem=problem, + optimizers=optimizer, + schedulers=scheduler, + extra_features=extra_features) # check consistency check_consistency(loss, (LossInterface, _Loss), subclass=False) @@ -107,10 +102,8 @@ def configure_optimizers(self): """ self._optimizer.hook(self._model.parameters()) self._scheduler.hook(self._optimizer) - return ( - [self._optimizer.optimizer_instance], - [self._scheduler.scheduler_instance] - ) + return ([self._optimizer.optimizer_instance], + [self._scheduler.scheduler_instance]) def training_step(self, batch, batch_idx): """Solver training step. @@ -136,8 +129,7 @@ def training_step(self, batch, batch_idx): # for data driven mode if not hasattr(condition, "output_points"): raise NotImplementedError( - f"{type(self).__name__} works only in data-driven mode." - ) + f"{type(self).__name__} works only in data-driven mode.") output_pts = out[condition_idx == condition_id] input_pts = pts[condition_idx == condition_id] @@ -145,9 +137,7 @@ def training_step(self, batch, batch_idx): input_pts.labels = pts.labels output_pts.labels = out.labels - loss = ( - self.loss_data(input_pts=input_pts, output_pts=output_pts) - ) + loss = (self.loss_data(input_pts=input_pts, output_pts=output_pts)) loss = loss.as_subclass(torch.Tensor) self.log("mean_loss", float(loss), prog_bar=True, logger=True) diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 264f794bf..653b0d6b6 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -29,34 +29,49 @@ class Poisson(SpatialProblem): spatial_domain = CartesianDomain({'x': [0, 1], 'y': [0, 1]}) conditions = { - 'gamma1': Condition( - domain=CartesianDomain({'x': [0, 1], 'y': 1}), - equation=FixedValue(0.0)), - 'gamma2': Condition( - domain=CartesianDomain({'x': [0, 1], 'y': 0}), - equation=FixedValue(0.0)), - 'gamma3': Condition( - domain=CartesianDomain({'x': 1, 'y': [0, 1]}), - equation=FixedValue(0.0)), - 'gamma4': Condition( - domain=CartesianDomain({'x': 0, 'y': [0, 1]}), - equation=FixedValue(0.0)), - 'D': Condition( - input_points=LabelTensor(torch.rand(size=(100, 2)), ['x', 'y']), - equation=my_laplace), - 'data': Condition( - input_points=in_, - output_points=out_), - 'data2': Condition( - input_points=in2_, - output_points=out2_), - 'unsupervised': Condition( + 'gamma1': + Condition(domain=CartesianDomain({ + 'x': [0, 1], + 'y': 1 + }), + equation=FixedValue(0.0)), + 'gamma2': + Condition(domain=CartesianDomain({ + 'x': [0, 1], + 'y': 0 + }), + equation=FixedValue(0.0)), + 'gamma3': + Condition(domain=CartesianDomain({ + 'x': 1, + 'y': [0, 1] + }), + equation=FixedValue(0.0)), + 'gamma4': + Condition(domain=CartesianDomain({ + 'x': 0, + 'y': [0, 1] + }), + equation=FixedValue(0.0)), + 'D': + Condition(input_points=LabelTensor(torch.rand(size=(100, 2)), + ['x', 'y']), + equation=my_laplace), + 'data': + Condition(input_points=in_, output_points=out_), + 'data2': + Condition(input_points=in2_, output_points=out2_), + 'unsupervised': + Condition( input_points=LabelTensor(torch.rand(size=(45, 2)), ['x', 'y']), - conditional_variables=LabelTensor(torch.ones(size=(45, 1)), ['alpha']), + conditional_variables=LabelTensor(torch.ones(size=(45, 1)), + ['alpha']), ), - 'unsupervised2': Condition( + 'unsupervised2': + Condition( input_points=LabelTensor(torch.rand(size=(90, 2)), ['x', 'y']), - conditional_variables=LabelTensor(torch.ones(size=(90, 1)), ['alpha']), + conditional_variables=LabelTensor(torch.ones(size=(90, 1)), + ['alpha']), ) } @@ -113,32 +128,49 @@ def test_data_module(): assert isinstance(loader, PinaDataLoader) assert isinstance(loader, PinaDataLoader) - data_module = PinaDataModule(poisson, device='cpu', batch_size=10, shuffle=False) + data_module = PinaDataModule(poisson, + device='cpu', + batch_size=10, + shuffle=False) data_module.setup() loader = data_module.train_dataloader() assert len(loader) == 24 for i in loader: assert len(i) <= 10 - len_ref = sum([math.ceil(len(dataset) * 0.7) for dataset in data_module.datasets]) - len_real = sum([len(dataset) for dataset in data_module.splits['train'].values()]) + len_ref = sum( + [math.ceil(len(dataset) * 0.7) for dataset in data_module.datasets]) + len_real = sum( + [len(dataset) for dataset in data_module.splits['train'].values()]) assert len_ref == len_real supervised_dataset = SupervisedDataset(poisson, device='cpu') - data_module = PinaDataModule(poisson, device='cpu', batch_size=10, shuffle=False, datasets=[supervised_dataset]) + data_module = PinaDataModule(poisson, + device='cpu', + batch_size=10, + shuffle=False, + datasets=[supervised_dataset]) data_module.setup() loader = data_module.train_dataloader() for batch in loader: assert len(batch) <= 10 physics_dataset = SamplePointDataset(poisson, device='cpu') - data_module = PinaDataModule(poisson, device='cpu', batch_size=10, shuffle=False, datasets=[physics_dataset]) + data_module = PinaDataModule(poisson, + device='cpu', + batch_size=10, + shuffle=False, + datasets=[physics_dataset]) data_module.setup() loader = data_module.train_dataloader() for batch in loader: assert len(batch) <= 10 unsupervised_dataset = UnsupervisedDataset(poisson, device='cpu') - data_module = PinaDataModule(poisson, device='cpu', batch_size=10, shuffle=False, datasets=[unsupervised_dataset]) + data_module = PinaDataModule(poisson, + device='cpu', + batch_size=10, + shuffle=False, + datasets=[unsupervised_dataset]) data_module.setup() loader = data_module.train_dataloader() for batch in loader: @@ -159,4 +191,6 @@ def test_loader(): assert i.supervised.input_points.requires_grad == True assert i.physics.input_points.requires_grad == True assert i.unsupervised.input_points.requires_grad == True -test_loader() \ No newline at end of file + + +test_loader() diff --git a/tests/test_label_tensor/test_label_tensor.py b/tests/test_label_tensor/test_label_tensor.py index 1165594db..846976730 100644 --- a/tests/test_label_tensor/test_label_tensor.py +++ b/tests/test_label_tensor/test_label_tensor.py @@ -4,29 +4,23 @@ from pina.label_tensor import LabelTensor data = torch.rand((20, 3)) -labels_column = { - 1: { - "name": "space", - "dof": ['x', 'y', 'z'] - } -} -labels_row = { - 0: { - "name": "samples", - "dof": range(20) - } -} +labels_column = {1: {"name": "space", "dof": ['x', 'y', 'z']}} +labels_row = {0: {"name": "samples", "dof": range(20)}} labels_list = ['x', 'y', 'z'] labels_all = labels_column | labels_row -@pytest.mark.parametrize("labels", [labels_column, labels_row, labels_all, labels_list]) + +@pytest.mark.parametrize("labels", + [labels_column, labels_row, labels_all, labels_list]) def test_constructor(labels): print(LabelTensor(data, labels)) + def test_wrong_constructor(): with pytest.raises(ValueError): LabelTensor(data, ['a', 'b']) + @pytest.mark.parametrize("labels", [labels_column, labels_all]) @pytest.mark.parametrize("labels_te", ['z', ['z'], {'space': ['z']}]) def test_extract_column(labels, labels_te): @@ -37,6 +31,7 @@ def test_extract_column(labels, labels_te): assert new.shape[0] == 20 assert torch.all(torch.isclose(data[:, 2].reshape(-1, 1), new)) + @pytest.mark.parametrize("labels", [labels_row, labels_all]) @pytest.mark.parametrize("labels_te", [{'samples': [2]}]) def test_extract_row(labels, labels_te): @@ -47,10 +42,14 @@ def test_extract_row(labels, labels_te): assert new.shape[0] == 1 assert torch.all(torch.isclose(data[2].reshape(1, -1), new)) -@pytest.mark.parametrize("labels_te", [ - {'samples': [2], 'space': ['z']}, - {'space': 'z', 'samples': 2} -]) + +@pytest.mark.parametrize("labels_te", [{ + 'samples': [2], + 'space': ['z'] +}, { + 'space': 'z', + 'samples': 2 +}]) def test_extract_2D(labels_te): labels = labels_all tensor = LabelTensor(data, labels) @@ -58,7 +57,8 @@ def test_extract_2D(labels_te): assert new.ndim == tensor.ndim assert new.shape[1] == 1 assert new.shape[0] == 1 - assert torch.all(torch.isclose(data[2,2].reshape(1, 1), new)) + assert torch.all(torch.isclose(data[2, 2].reshape(1, 1), new)) + def test_extract_3D(): data = torch.rand(20, 3, 4) @@ -72,10 +72,7 @@ def test_extract_3D(): "dof": range(4) }, } - labels_te = { - 'space': ['x', 'z'], - 'time': range(1, 4) - } + labels_te = {'space': ['x', 'z'], 'time': range(1, 4)} tensor = LabelTensor(data, labels) new = tensor.extract(labels_te) @@ -84,15 +81,13 @@ def test_extract_3D(): assert new.shape[0] == 20 assert new.shape[1] == 2 assert new.shape[2] == 3 - assert torch.all(torch.isclose( - data[:, 0::2, 1:4].reshape(20, 2, 3), - new - )) + assert torch.all(torch.isclose(data[:, 0::2, 1:4].reshape(20, 2, 3), new)) assert tensor2.ndim == tensor.ndim assert tensor2.shape == tensor.shape assert tensor.full_labels == tensor2.full_labels assert new.shape != tensor.shape + def test_concatenation_3D(): data_1 = torch.rand(20, 3, 4) labels_1 = ['x', 'y', 'z', 'w'] @@ -152,27 +147,28 @@ def test_concatenation_3D(): def test_summation(): - lt1 = LabelTensor(torch.ones(20,3), labels_all) - lt2 = LabelTensor(torch.ones(30,3), ['x', 'y', 'z']) + lt1 = LabelTensor(torch.ones(20, 3), labels_all) + lt2 = LabelTensor(torch.ones(30, 3), ['x', 'y', 'z']) with pytest.raises(RuntimeError): LabelTensor.summation([lt1, lt2]) - lt1 = LabelTensor(torch.ones(20,3), labels_all) - lt2 = LabelTensor(torch.ones(20,3), labels_all) + lt1 = LabelTensor(torch.ones(20, 3), labels_all) + lt2 = LabelTensor(torch.ones(20, 3), labels_all) lt_sum = LabelTensor.summation([lt1, lt2]) assert lt_sum.ndim == lt_sum.ndim assert lt_sum.shape[0] == 20 assert lt_sum.shape[1] == 3 assert lt_sum.full_labels == labels_all - assert torch.eq(lt_sum.tensor, torch.ones(20,3)*2).all() - lt1 = LabelTensor(torch.ones(20,3), labels_all) - lt2 = LabelTensor(torch.ones(20,3), labels_all) + assert torch.eq(lt_sum.tensor, torch.ones(20, 3) * 2).all() + lt1 = LabelTensor(torch.ones(20, 3), labels_all) + lt2 = LabelTensor(torch.ones(20, 3), labels_all) lt3 = LabelTensor(torch.zeros(20, 3), labels_all) lt_sum = LabelTensor.summation([lt1, lt2, lt3]) assert lt_sum.ndim == lt_sum.ndim assert lt_sum.shape[0] == 20 assert lt_sum.shape[1] == 3 assert lt_sum.full_labels == labels_all - assert torch.eq(lt_sum.tensor, torch.ones(20,3)*2).all() + assert torch.eq(lt_sum.tensor, torch.ones(20, 3) * 2).all() + def test_append_3D(): data_1 = torch.rand(20, 3, 2) @@ -187,6 +183,7 @@ def test_append_3D(): assert lt1.full_labels[1]['dof'] == range(3) assert lt1.full_labels[2]['dof'] == ['x', 'y', 'z', 'w'] + def test_append_2D(): data_1 = torch.rand(20, 2) labels_1 = ['x', 'y'] @@ -199,12 +196,31 @@ def test_append_2D(): assert lt1.full_labels[0]['dof'] == range(400) assert lt1.full_labels[1]['dof'] == ['x', 'y', 'z', 'w'] + def test_vstack_3D(): data_1 = torch.rand(20, 3, 2) - labels_1 = {1:{'dof': ['a', 'b', 'c'], 'name': 'first'}, 2: {'dof': ['x', 'y'], 'name': 'second'}} + labels_1 = { + 1: { + 'dof': ['a', 'b', 'c'], + 'name': 'first' + }, + 2: { + 'dof': ['x', 'y'], + 'name': 'second' + } + } lt1 = LabelTensor(data_1, labels_1) data_2 = torch.rand(20, 3, 2) - labels_1 = {1:{'dof': ['a', 'b', 'c'], 'name': 'first'}, 2: {'dof': ['x', 'y'], 'name': 'second'}} + labels_1 = { + 1: { + 'dof': ['a', 'b', 'c'], + 'name': 'first' + }, + 2: { + 'dof': ['x', 'y'], + 'name': 'second' + } + } lt2 = LabelTensor(data_2, labels_1) lt_stacked = LabelTensor.vstack([lt1, lt2]) assert lt_stacked.shape == (40, 3, 2) @@ -214,12 +230,13 @@ def test_vstack_3D(): assert lt_stacked.full_labels[1]['name'] == 'first' assert lt_stacked.full_labels[2]['name'] == 'second' + def test_vstack_2D(): data_1 = torch.rand(20, 2) - labels_1 = { 1: {'dof': ['x', 'y'], 'name': 'second'}} + labels_1 = {1: {'dof': ['x', 'y'], 'name': 'second'}} lt1 = LabelTensor(data_1, labels_1) data_2 = torch.rand(20, 2) - labels_1 = { 1: {'dof': ['x', 'y'], 'name': 'second'}} + labels_1 = {1: {'dof': ['x', 'y'], 'name': 'second'}} lt2 = LabelTensor(data_2, labels_1) lt_stacked = LabelTensor.vstack([lt1, lt2]) assert lt_stacked.shape == (40, 2) @@ -228,35 +245,36 @@ def test_vstack_2D(): assert lt_stacked.full_labels[0]['name'] == 0 assert lt_stacked.full_labels[1]['name'] == 'second' + def test_sorting(): data = torch.ones(20, 5) - data[:,0] = data[:,0]*4 - data[:,1] = data[:,1]*2 - data[:,2] = data[:,2] - data[:,3] = data[:,3]*5 - data[:,4] = data[:,4]*3 + data[:, 0] = data[:, 0] * 4 + data[:, 1] = data[:, 1] * 2 + data[:, 2] = data[:, 2] + data[:, 3] = data[:, 3] * 5 + data[:, 4] = data[:, 4] * 3 labels = ['d', 'b', 'a', 'e', 'c'] lt_data = LabelTensor(data, labels) lt_sorted = LabelTensor.sort_labels(lt_data) - assert lt_sorted.shape == (20,5) + assert lt_sorted.shape == (20, 5) assert lt_sorted.labels == ['a', 'b', 'c', 'd', 'e'] - assert torch.eq(lt_sorted.tensor[:,0], torch.ones(20) * 1).all() - assert torch.eq(lt_sorted.tensor[:,1], torch.ones(20) * 2).all() - assert torch.eq(lt_sorted.tensor[:,2], torch.ones(20) * 3).all() - assert torch.eq(lt_sorted.tensor[:,3], torch.ones(20) * 4).all() - assert torch.eq(lt_sorted.tensor[:,4], torch.ones(20) * 5).all() + assert torch.eq(lt_sorted.tensor[:, 0], torch.ones(20) * 1).all() + assert torch.eq(lt_sorted.tensor[:, 1], torch.ones(20) * 2).all() + assert torch.eq(lt_sorted.tensor[:, 2], torch.ones(20) * 3).all() + assert torch.eq(lt_sorted.tensor[:, 3], torch.ones(20) * 4).all() + assert torch.eq(lt_sorted.tensor[:, 4], torch.ones(20) * 5).all() data = torch.ones(20, 4, 5) - data[:,0,:] = data[:,0]*4 - data[:,1,:] = data[:,1]*2 - data[:,2,:] = data[:,2] - data[:,3,:] = data[:,3]*3 + data[:, 0, :] = data[:, 0] * 4 + data[:, 1, :] = data[:, 1] * 2 + data[:, 2, :] = data[:, 2] + data[:, 3, :] = data[:, 3] * 3 labels = {1: {'dof': ['d', 'b', 'a', 'c'], 'name': 1}} lt_data = LabelTensor(data, labels) lt_sorted = LabelTensor.sort_labels(lt_data, dim=1) - assert lt_sorted.shape == (20,4, 5) + assert lt_sorted.shape == (20, 4, 5) assert lt_sorted.full_labels[1]['dof'] == ['a', 'b', 'c', 'd'] - assert torch.eq(lt_sorted.tensor[:,0,:], torch.ones(20,5) * 1).all() - assert torch.eq(lt_sorted.tensor[:,1,:], torch.ones(20,5) * 2).all() - assert torch.eq(lt_sorted.tensor[:,2,:], torch.ones(20,5) * 3).all() - assert torch.eq(lt_sorted.tensor[:,3,:], torch.ones(20,5) * 4).all() + assert torch.eq(lt_sorted.tensor[:, 0, :], torch.ones(20, 5) * 1).all() + assert torch.eq(lt_sorted.tensor[:, 1, :], torch.ones(20, 5) * 2).all() + assert torch.eq(lt_sorted.tensor[:, 2, :], torch.ones(20, 5) * 3).all() + assert torch.eq(lt_sorted.tensor[:, 3, :], torch.ones(20, 5) * 4).all() diff --git a/tests/test_label_tensor/test_label_tensor_01.py b/tests/test_label_tensor/test_label_tensor_01.py index a2e129d94..57aafb8c9 100644 --- a/tests/test_label_tensor/test_label_tensor_01.py +++ b/tests/test_label_tensor/test_label_tensor_01.py @@ -54,9 +54,8 @@ def test_extract_order(): label_to_extract = ['c', 'a'] tensor = LabelTensor(data, labels) new = tensor.extract(label_to_extract) - expected = torch.cat( - (data[:, 2].reshape(-1, 1), data[:, 0].reshape(-1, 1)), - dim=1) + expected = torch.cat((data[:, 2].reshape(-1, 1), data[:, 0].reshape(-1, 1)), + dim=1) assert new.labels == label_to_extract assert new.shape[1] == len(label_to_extract) assert torch.all(torch.isclose(expected, new)) @@ -91,6 +90,7 @@ def test_getitem(): assert tensor_view.labels == ['a', 'c'] assert torch.allclose(tensor_view, data[:, 0::2]) + def test_getitem2(): tensor = LabelTensor(data, labels) tensor_view = tensor[:5] @@ -101,6 +101,7 @@ def test_getitem2(): tensor_view = tensor[idx] assert tensor_view.labels == labels + def test_slice(): tensor = LabelTensor(data, labels) tensor_view = tensor[:5, :2] @@ -114,4 +115,4 @@ def test_slice(): tensor_view3 = tensor[:, 2] assert tensor_view3.labels == labels[2] - assert torch.allclose(tensor_view3, data[:, 2].reshape(-1, 1)) \ No newline at end of file + assert torch.allclose(tensor_view3, data[:, 2].reshape(-1, 1)) diff --git a/tests/test_operators.py b/tests/test_operators.py index 975f71523..e446c8f37 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -35,22 +35,26 @@ def test_grad_scalar_output(): f'd{tensor_s.labels[0]}d{i}' for i in ['x', 'y'] ] + def test_grad_vector_output(): grad_tensor_v = grad(tensor_v, inp) assert grad_tensor_v.shape == (20, 9) grad_tensor_v = grad(tensor_v, inp, d=['x', 'mu']) assert grad_tensor_v.shape == (inp.shape[0], 6) + def test_div_vector_output(): grad_tensor_v = div(tensor_v, inp) assert grad_tensor_v.shape == (20, 1) grad_tensor_v = div(tensor_v, inp, components=['a', 'b'], d=['x', 'mu']) assert grad_tensor_v.shape == (inp.shape[0], 1) + def test_laplacian_scalar_output(): laplace_tensor_v = laplacian(tensor_s, inp, components=['a'], d=['x', 'y']) assert laplace_tensor_v.shape == tensor_s.shape + def test_laplacian_vector_output(): laplace_tensor_v = laplacian(tensor_v, inp) assert laplace_tensor_v.shape == tensor_v.shape diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index 489bbdc05..bdc87cac5 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -1,20 +1,18 @@ - import torch import pytest from pina import TorchOptimizer opt_list = [ - torch.optim.Adam, - torch.optim.AdamW, - torch.optim.SGD, - torch.optim.RMSprop + torch.optim.Adam, torch.optim.AdamW, torch.optim.SGD, torch.optim.RMSprop ] + @pytest.mark.parametrize("optimizer_class", opt_list) def test_constructor(optimizer_class): TorchOptimizer(optimizer_class, lr=1e-3) + @pytest.mark.parametrize("optimizer_class", opt_list) def test_hook(optimizer_class): opt = TorchOptimizer(optimizer_class, lr=1e-3) - opt.hook(torch.nn.Linear(10, 10).parameters()) \ No newline at end of file + opt.hook(torch.nn.Linear(10, 10).parameters()) diff --git a/tests/test_problem.py b/tests/test_problem.py index 0a11e1f36..f1aafcd98 100644 --- a/tests/test_problem.py +++ b/tests/test_problem.py @@ -27,42 +27,42 @@ class Poisson(SpatialProblem): conditions = { 'gamma1': - Condition(domain=CartesianDomain({ - 'x': [0, 1], - 'y': 1 - }), - equation=FixedValue(0.0)), + Condition(domain=CartesianDomain({ + 'x': [0, 1], + 'y': 1 + }), + equation=FixedValue(0.0)), 'gamma2': - Condition(domain=CartesianDomain({ - 'x': [0, 1], - 'y': 0 - }), - equation=FixedValue(0.0)), + Condition(domain=CartesianDomain({ + 'x': [0, 1], + 'y': 0 + }), + equation=FixedValue(0.0)), 'gamma3': - Condition(domain=CartesianDomain({ - 'x': 1, - 'y': [0, 1] - }), - equation=FixedValue(0.0)), + Condition(domain=CartesianDomain({ + 'x': 1, + 'y': [0, 1] + }), + equation=FixedValue(0.0)), 'gamma4': - Condition(domain=CartesianDomain({ - 'x': 0, - 'y': [0, 1] - }), - equation=FixedValue(0.0)), + Condition(domain=CartesianDomain({ + 'x': 0, + 'y': [0, 1] + }), + equation=FixedValue(0.0)), 'D': - Condition(domain=CartesianDomain({ - 'x': [0, 1], - 'y': [0, 1] - }), - equation=my_laplace), + Condition(domain=CartesianDomain({ + 'x': [0, 1], + 'y': [0, 1] + }), + equation=my_laplace), 'data': - Condition(input_points=in_, output_points=out_) + Condition(input_points=in_, output_points=out_) } def poisson_sol(self, pts): return -(torch.sin(pts.extract(['x']) * torch.pi) * - torch.sin(pts.extract(['y']) * torch.pi)) / (2 * torch.pi ** 2) + torch.sin(pts.extract(['y']) * torch.pi)) / (2 * torch.pi**2) truth_solution = poisson_sol @@ -79,7 +79,7 @@ def test_discretise_domain(): assert poisson_problem.input_pts[b].shape[0] == n poisson_problem.discretise_domain(n, 'grid', locations=['D']) - assert poisson_problem.input_pts['D'].shape[0] == n ** 2 + assert poisson_problem.input_pts['D'].shape[0] == n**2 poisson_problem.discretise_domain(n, 'random', locations=['D']) assert poisson_problem.input_pts['D'].shape[0] == n @@ -91,6 +91,7 @@ def test_discretise_domain(): poisson_problem.discretise_domain(n) + def test_sampling_few_variables(): n = 10 poisson_problem = Poisson() @@ -116,9 +117,7 @@ def test_variables_correct_order_sampling(): assert poisson_problem.input_pts['D'].labels == sorted( poisson_problem.input_variables) - poisson_problem.discretise_domain(n, - 'grid', - locations=['D']) + poisson_problem.discretise_domain(n, 'grid', locations=['D']) assert poisson_problem.input_pts['D'].labels == sorted( poisson_problem.input_variables) @@ -133,6 +132,7 @@ def test_variables_correct_order_sampling(): assert poisson_problem.input_pts['D'].labels == sorted( poisson_problem.input_variables) + def test_add_points(): poisson_problem = Poisson() poisson_problem.discretise_domain(0, @@ -141,8 +141,10 @@ def test_add_points(): variables=['x', 'y']) new_pts = LabelTensor(torch.tensor([[0.5, -0.5]]), labels=['x', 'y']) poisson_problem.add_points({'D': new_pts}) - assert torch.isclose(poisson_problem.input_pts['D'].extract('x'), new_pts.extract('x')) - assert torch.isclose(poisson_problem.input_pts['D'].extract('y'), new_pts.extract('y')) + assert torch.isclose(poisson_problem.input_pts['D'].extract('x'), + new_pts.extract('x')) + assert torch.isclose(poisson_problem.input_pts['D'].extract('y'), + new_pts.extract('y')) def test_collector(): @@ -151,4 +153,4 @@ def test_collector(): assert collector.full is False assert collector._is_conditions_ready['data'] is True poisson_problem.discretise_domain(10) - assert collector.full is True \ No newline at end of file + assert collector.full is True From 9dc76f7c9e4f75c47eed56d86d554593815e1150 Mon Sep 17 00:00:00 2001 From: FilippoOlivo Date: Tue, 22 Oct 2024 14:54:22 +0200 Subject: [PATCH 10/14] Correct codacy warnings --- pina/collector.py | 21 ++++++++++++++------- pina/condition/condition.py | 23 ++++++++++++----------- pina/data/base_dataset.py | 2 +- pina/data/data_module.py | 16 +++++++++------- pina/data/pina_batch.py | 3 +++ pina/data/pina_dataloader.py | 2 +- pina/data/pina_subset.py | 5 +++++ pina/data/unsupervised_dataset.py | 5 +++-- pina/optim/torch_optimizer.py | 1 + pina/trainer.py | 19 +++++++++++-------- 10 files changed, 60 insertions(+), 37 deletions(-) diff --git a/pina/collector.py b/pina/collector.py index f9ef194db..4ebf236c8 100644 --- a/pina/collector.py +++ b/pina/collector.py @@ -48,7 +48,8 @@ def store_fixed_data(self): for condition_name, condition in self.problem.conditions.items(): # if the condition is not ready and domain is not attribute # of condition, we get and store the data - if (not self._is_conditions_ready[condition_name]) and (not hasattr(condition, "domain")): + if (not self._is_conditions_ready[condition_name]) and ( + not hasattr(condition, "domain")): # get data keys = condition.__slots__ values = [getattr(condition, name) for name in keys] @@ -69,7 +70,8 @@ def store_sample_domains(self, n, mode, variables, sample_locations): already_sampled = [] # if we have sampled the condition but not all variables else: - already_sampled = [self.data_collections[loc]['input_points']] + already_sampled = [ + self.data_collections[loc]['input_points']] # if the condition is ready but we want to sample again else: self._is_conditions_ready[loc] = False @@ -77,11 +79,13 @@ def store_sample_domains(self, n, mode, variables, sample_locations): # get the samples samples = [ - condition.domain.sample(n=n, mode=mode, variables=variables) + condition.domain.sample(n=n, mode=mode, + variables=variables) ] + already_sampled pts = merge_tensors(samples) if ( - set(pts.labels).issubset(sorted(self.problem.input_variables)) + set(pts.labels).issubset( + sorted(self.problem.input_variables)) ): pts = pts.sort_labels() if sorted(pts.labels) == sorted(self.problem.input_variables): @@ -89,7 +93,8 @@ def store_sample_domains(self, n, mode, variables, sample_locations): values = [pts, condition.equation] self.data_collections[loc] = dict(zip(keys, values)) else: - raise RuntimeError('Try to sample variables which are not in problem defined in the problem') + raise RuntimeError( + 'Try to sample variables which are not in problem defined in the problem') def add_points(self, new_points_dict): """ @@ -100,5 +105,7 @@ def add_points(self, new_points_dict): """ for k, v in new_points_dict.items(): if not self._is_conditions_ready[k]: - raise RuntimeError('Cannot add points on a non sampled condition') - self.data_collections[k]['input_points'] = self.data_collections[k]['input_points'].vstack(v) + raise RuntimeError( + 'Cannot add points on a non sampled condition') + self.data_collections[k]['input_points'] = self.data_collections[k][ + 'input_points'].vstack(v) diff --git a/pina/condition/condition.py b/pina/condition/condition.py index 09180cc6e..01965fe0d 100644 --- a/pina/condition/condition.py +++ b/pina/condition/condition.py @@ -5,6 +5,7 @@ from .input_output_condition import InputOutputPointsCondition from .data_condition import DataConditionInterface + class Condition: """ The class ``Condition`` is used to represent the constraints (physical @@ -38,23 +39,23 @@ class Condition: """ __slots__ = list( - set( - InputOutputPointsCondition.__slots__ + - InputPointsEquationCondition.__slots__ + - DomainEquationCondition.__slots__ + - DataConditionInterface.__slots__ - ) - ) + set( + InputOutputPointsCondition.__slots__ + + InputPointsEquationCondition.__slots__ + + DomainEquationCondition.__slots__ + + DataConditionInterface.__slots__ + ) + ) def __new__(cls, *args, **kwargs): - + if len(args) != 0: raise ValueError( "Condition takes only the following keyword " f"arguments: {Condition.__slots__}." ) - - sorted_keys = sorted(kwargs.keys()) + + sorted_keys = sorted(kwargs.keys()) if sorted_keys == sorted(InputOutputPointsCondition.__slots__): return InputOutputPointsCondition(**kwargs) elif sorted_keys == sorted(InputPointsEquationCondition.__slots__): @@ -66,4 +67,4 @@ def __new__(cls, *args, **kwargs): elif sorted_keys == DataConditionInterface.__slots__[0]: return DataConditionInterface(**kwargs) else: - raise ValueError(f"Invalid keyword arguments {kwargs.keys()}.") \ No newline at end of file + raise ValueError(f"Invalid keyword arguments {kwargs.keys()}.") diff --git a/pina/data/base_dataset.py b/pina/data/base_dataset.py index f1d17ae31..b15a0be2a 100644 --- a/pina/data/base_dataset.py +++ b/pina/data/base_dataset.py @@ -27,7 +27,7 @@ def __new__(cls, problem, device): if not hasattr(cls, '__slots__'): raise TypeError( 'Something is wrong, __slots__ must be defined in subclasses.') - return super(BaseDataset, cls).__new__(cls) + return object.__new__(cls) def __init__(self, problem, device): """" diff --git a/pina/data/data_module.py b/pina/data/data_module.py index e4e8a450f..25c7e54ed 100644 --- a/pina/data/data_module.py +++ b/pina/data/data_module.py @@ -26,7 +26,7 @@ def __init__(self, eval_size=.1, batch_size=None, shuffle=True, - datasets = None): + datasets=None): """ Initialize the object, creating dataset based on input problem :param AbstractProblem problem: PINA problem @@ -38,9 +38,11 @@ def __init__(self, :param datasets: list of datasets objects """ super().__init__() - dataset_classes = [SupervisedDataset, UnsupervisedDataset, SamplePointDataset] + dataset_classes = [SupervisedDataset, UnsupervisedDataset, + SamplePointDataset] if datasets is None: - self.datasets = [DatasetClass(problem, device) for DatasetClass in dataset_classes] + self.datasets = [DatasetClass(problem, device) for DatasetClass in + dataset_classes] else: self.datasets = datasets @@ -100,8 +102,6 @@ def extract_conditions(self): for key, value in dataset.condition_names.items() } - - def train_dataloader(self): """ Return the training dataloader for the dataset @@ -158,11 +158,13 @@ def dataset_split(dataset, lengths, seed=None, shuffle=True): if seed is not None: generator = torch.Generator() generator.manual_seed(seed) - indices = torch.randperm(sum(lengths), generator=generator).tolist() + indices = torch.randperm(sum(lengths), + generator=generator).tolist() else: indices = torch.arange(sum(lengths)).tolist() else: - indices = torch.arange(0, sum(lengths), 1, dtype=torch.uint8).tolist() + indices = torch.arange(0, sum(lengths), 1, + dtype=torch.uint8).tolist() offsets = [ sum(lengths[:i]) if i > 0 else 0 for i in range(len(lengths)) ] diff --git a/pina/data/pina_batch.py b/pina/data/pina_batch.py index f61e0020b..ed34a910b 100644 --- a/pina/data/pina_batch.py +++ b/pina/data/pina_batch.py @@ -5,6 +5,9 @@ class Batch: + """ + Implementation of the Batch class used during training to perform SGD optimization. + """ def __init__(self, dataset_dict, idx_dict): diff --git a/pina/data/pina_dataloader.py b/pina/data/pina_dataloader.py index cbd8fe82d..e2d3fb76e 100644 --- a/pina/data/pina_dataloader.py +++ b/pina/data/pina_dataloader.py @@ -33,7 +33,7 @@ def _init_batches(self, batch_size=None): Create batches according to the batch_size provided in input. """ self.batches = [] - n_elements = sum([len(v) for v in self.dataset_dict.values()]) + n_elements = sum(len(v) for v in self.dataset_dict.values()) if batch_size is None: batch_size = n_elements indexes_dict = {} diff --git a/pina/data/pina_subset.py b/pina/data/pina_subset.py index 41571f92b..844321bd2 100644 --- a/pina/data/pina_subset.py +++ b/pina/data/pina_subset.py @@ -1,3 +1,8 @@ +""" +Module for PinaSubset class +""" + + class PinaSubset: """ TODO diff --git a/pina/data/unsupervised_dataset.py b/pina/data/unsupervised_dataset.py index f4e8fb345..18cf296f5 100644 --- a/pina/data/unsupervised_dataset.py +++ b/pina/data/unsupervised_dataset.py @@ -6,8 +6,9 @@ class UnsupervisedDataset(BaseDataset): """ - This class extend BaseDataset class to handle unsupervised dataset, - composed of input points and, optionally, conditional variables + This class extend BaseDataset class to handle + unsupervised dataset,composed of input points + and, optionally, conditional variables """ data_type = 'unsupervised' __slots__ = ['input_points', 'conditional_variables'] diff --git a/pina/optim/torch_optimizer.py b/pina/optim/torch_optimizer.py index ed90846c6..54818d5a5 100644 --- a/pina/optim/torch_optimizer.py +++ b/pina/optim/torch_optimizer.py @@ -13,6 +13,7 @@ def __init__(self, optimizer_class, **kwargs): self.optimizer_class = optimizer_class self.kwargs = kwargs + self.optimizer_instance = None def hook(self, parameters): self.optimizer_instance = self.optimizer_class(parameters, diff --git a/pina/trainer.py b/pina/trainer.py index 49c6a4017..884eef77e 100644 --- a/pina/trainer.py +++ b/pina/trainer.py @@ -9,7 +9,8 @@ class Trainer(pytorch_lightning.Trainer): - def __init__(self, solver, batch_size=None, train_size=.7, test_size=.2, eval_size=.1, **kwargs): + def __init__(self, solver, batch_size=None, train_size=.7, test_size=.2, + eval_size=.1, **kwargs): """ PINA Trainer class for costumizing every aspect of training via flags. @@ -39,10 +40,9 @@ def __init__(self, solver, batch_size=None, train_size=.7, test_size=.2, eval_si self._create_loader() self._move_to_device() - def _move_to_device(self): device = self._accelerator_connector._parallel_devices[0] - + # move parameters to device pb = self.solver.problem if hasattr(pb, "unknown_parameters"): @@ -59,11 +59,13 @@ def _create_loader(self): """ if not self.solver.problem.collector.full: error_message = '\n'.join( - [f'{" " * 13} ---> Condition {key} {"sampled" if value else "not sampled"}' - for key, value in self.solver.problem.collector._is_conditions_ready.items()]) + [ + f'{" " * 13} ---> Condition {key} {"sampled" if value else "not sampled"}' + for key, value in + self.solver.problem.collector._is_conditions_ready.items()]) raise RuntimeError('Cannot create Trainer if not all conditions ' - 'are sampled. The Trainer got the following:\n' - f'{error_message}') + 'are sampled. The Trainer got the following:\n' + f'{error_message}') devices = self._accelerator_connector._parallel_devices if len(devices) > 1: @@ -72,7 +74,8 @@ def _create_loader(self): device = devices[0] data_module = PinaDataModule(problem=self.solver.problem, device=device, - train_size=self.train_size, test_size=self.test_size, + train_size=self.train_size, + test_size=self.test_size, eval_size=self.eval_size) data_module.setup() self._loader = data_module.train_dataloader() From 0a2d26a406240fcd084170bc1d5fb1d6ec2de1af Mon Sep 17 00:00:00 2001 From: FilippoOlivo Date: Wed, 23 Oct 2024 14:54:31 +0200 Subject: [PATCH 11/14] Fix bug and improve __getitem__ --- pina/label_tensor.py | 172 +++++++++++++++++++++++++++++-------------- 1 file changed, 115 insertions(+), 57 deletions(-) diff --git a/pina/label_tensor.py b/pina/label_tensor.py index 65655e9dc..62d87950a 100644 --- a/pina/label_tensor.py +++ b/pina/label_tensor.py @@ -22,9 +22,6 @@ def __new__(cls, x, labels, *args, **kwargs): def tensor(self): return self.as_subclass(Tensor) - def __len__(self) -> int: - return super().__len__() - def __init__(self, x, labels): """ Construct a `LabelTensor` by passing a dict of the labels @@ -75,7 +72,7 @@ def labels(self, labels): labels = [labels] self.update_labels_from_list(labels) else: - raise ValueError(f"labels must be list, dict or string.") + raise ValueError("labels must be list, dict or string.") self.set_names() def set_names(self): @@ -98,10 +95,9 @@ def extract(self, label_to_extract): label_to_extract = [label_to_extract] if isinstance(label_to_extract, (tuple, list)): return self._extract_from_list(label_to_extract) - elif isinstance(label_to_extract, dict): + if isinstance(label_to_extract, dict): return self._extract_from_dict(label_to_extract) - else: - raise ValueError('labels_to_extract must be str or list or dict') + raise ValueError('labels_to_extract must be str or list or dict') def _extract_from_list(self, labels_to_extract): # Store locally all necessary obj/variables @@ -112,7 +108,8 @@ def _extract_from_list(self, labels_to_extract): # Verify if all the labels in labels_to_extract are in last dimension if set(labels_to_extract).issubset(last_dim_label) is False: - raise ValueError('Cannot extract a dof which is not in the original LabelTensor') + raise ValueError( + 'Cannot extract a dof which is not in the original LabelTensor') # Extract index to extract idx_to_extract = [last_dim_label.index(i) for i in labels_to_extract] @@ -142,9 +139,12 @@ def _extract_from_dict(self, labels_to_extract): if isinstance(labels_to_extract[k], (int, str)): labels_to_extract[k] = [labels_to_extract[k]] if set(labels_to_extract[k]).issubset(dim_labels) is False: - raise ValueError('Cannot extract a dof which is not in the original LabelTensor') + raise ValueError( + 'Cannot extract a dof which is not in the original ' + 'LabelTensor') idx_to_extract = [dim_labels.index(i) for i in labels_to_extract[k]] - indexer = [slice(None)] * idx_dim + [idx_to_extract] + [slice(None)] * (ndim - idx_dim - 1) + indexer = [slice(None)] * idx_dim + [idx_to_extract] + [ + slice(None)] * (ndim - idx_dim - 1) new_tensor = new_tensor[indexer] dim_new_label = {idx_dim: { 'dof': labels_to_extract[k], @@ -168,7 +168,8 @@ def __str__(self): @staticmethod def cat(tensors, dim=0): """ - Stack a list of tensors. For example, given a tensor `a` of shape `(n,m,dof)` and a tensor `b` of dimension `(n',m,dof)` + Stack a list of tensors. For example, given a tensor `a` of shape + `(n,m,dof)` and a tensor `b` of dimension `(n',m,dof)` the resulting tensor is of shape `(n+n',m,dof)` :param tensors: tensors to concatenate @@ -182,7 +183,8 @@ def cat(tensors, dim=0): return [] if len(tensors) == 1: return tensors[0] - new_labels_cat_dim = LabelTensor._check_validity_before_cat(tensors, dim) + new_labels_cat_dim = LabelTensor._check_validity_before_cat(tensors, + dim) # Perform cat on tensors new_tensor = torch.cat(tensors, dim=dim) @@ -190,7 +192,8 @@ def cat(tensors, dim=0): # Update labels labels = tensors[0].full_labels labels.pop(dim) - new_labels_cat_dim = new_labels_cat_dim if len(set(new_labels_cat_dim)) == len(new_labels_cat_dim) \ + new_labels_cat_dim = new_labels_cat_dim if len( + set(new_labels_cat_dim)) == len(new_labels_cat_dim) \ else range(new_tensor.shape[dim]) labels[dim] = {'dof': new_labels_cat_dim, 'name': tensors[1].full_labels[dim]['name']} @@ -200,7 +203,8 @@ def cat(tensors, dim=0): def _check_validity_before_cat(tensors, dim): n_dims = tensors[0].ndim new_labels_cat_dim = [] - # Check if names and dof of the labels are the same in all dimensions except in dim + # Check if names and dof of the labels are the same in all dimensions + # except in dim for i in range(n_dims): name = tensors[0].full_labels[i]['name'] if i != dim: @@ -209,13 +213,15 @@ def _check_validity_before_cat(tensors, dim): dof_to_check = tensor.full_labels[i]['dof'] name_to_check = tensor.full_labels[i]['name'] if dof != dof_to_check or name != name_to_check: - raise ValueError('dimensions must have the same dof and name') + raise ValueError( + 'dimensions must have the same dof and name') else: for tensor in tensors: new_labels_cat_dim += tensor.full_labels[i]['dof'] name_to_check = tensor.full_labels[i]['name'] if name != name_to_check: - raise ValueError('Dimensions to concatenate must have the same name') + raise ValueError( + 'Dimensions to concatenate must have the same name') return new_labels_cat_dim def requires_grad_(self, mode=True): @@ -259,11 +265,13 @@ def init_labels(self): def update_labels_from_dict(self, labels): """ - Update the internal label representation according to the values passed as input. + Update the internal label representation according to the values passed + as input. :param labels: The label(s) to update. :type labels: dict - :raises ValueError: dof list contain duplicates or number of dof does not match with tensor shape + :raises ValueError: dof list contain duplicates or number of dof does + not match with tensor shape """ tensor_shape = self.tensor.shape # Check dimensionality @@ -271,19 +279,22 @@ def update_labels_from_dict(self, labels): if len(v['dof']) != len(set(v['dof'])): raise ValueError("dof must be unique") if len(v['dof']) != tensor_shape[k]: - raise ValueError('Number of dof does not match with tensor dimension') + raise ValueError( + 'Number of dof does not match with tensor dimension') # Perform update self._labels.update(labels) def update_labels_from_list(self, labels): """ - Given a list of dof, this method update the internal label representation + Given a list of dof, this method update the internal label + representation :param labels: The label(s) to update. :type labels: list """ # Create a dict with labels - last_dim_labels = {self.tensor.ndim - 1: {'dof': labels, 'name': self.tensor.ndim - 1}} + last_dim_labels = { + self.tensor.ndim - 1: {'dof': labels, 'name': self.tensor.ndim - 1}} self.update_labels_from_dict(last_dim_labels) @staticmethod @@ -302,15 +313,16 @@ def summation(tensors): break # Sum tensors data = torch.zeros(tensors[0].tensor.shape) - for i in range(len(tensors)): - data += tensors[i].tensor + for tensor in tensors: + data += tensor.tensor new_tensor = LabelTensor(data, labels) return new_tensor def append(self, tensor, mode='std'): if mode == 'std': # Call cat on last dimension - new_label_tensor = LabelTensor.cat([self, tensor], dim=self.tensor.ndim - 1) + new_label_tensor = LabelTensor.cat([self, tensor], + dim=self.tensor.ndim - 1) elif mode == 'cross': # Crete tensor and call cat on last dimension tensor1 = self @@ -318,8 +330,10 @@ def append(self, tensor, mode='std'): n1 = tensor1.shape[0] n2 = tensor2.shape[0] tensor1 = LabelTensor(tensor1.repeat(n2, 1), labels=tensor1.labels) - tensor2 = LabelTensor(tensor2.repeat_interleave(n1, dim=0), labels=tensor2.labels) - new_label_tensor = LabelTensor.cat([tensor1, tensor2], dim=self.tensor.ndim - 1) + tensor2 = LabelTensor(tensor2.repeat_interleave(n1, dim=0), + labels=tensor2.labels) + new_label_tensor = LabelTensor.cat([tensor1, tensor2], + dim=self.tensor.ndim - 1) else: raise ValueError('mode must be either "std" or "cross"') return new_label_tensor @@ -339,47 +353,90 @@ def vstack(label_tensors): def __getitem__(self, index): """ - Return a copy of the selected tensor. + TODO: Complete docstring + :param index: + :return: """ - if isinstance(index, str) or (isinstance(index, (tuple, list)) and all(isinstance(a, str) for a in index)): + if isinstance(index, str) or (isinstance(index, (tuple, list)) and all( + isinstance(a, str) for a in index)): return self.extract(index) - selected_lt = super().__getitem__(index) - try: - len_index = len(index) - except TypeError: - len_index = 1 - - if isinstance(index, int) or len_index == 1: - if selected_lt.ndim == 1: - selected_lt = selected_lt.reshape(1, -1) - if hasattr(self, "labels"): - new_labels = deepcopy(self.full_labels) - new_labels.pop(0) - selected_lt.labels = new_labels - elif len(index) == self.tensor.ndim: + if isinstance(index, (int, slice)): + return self._getitem_int_slice(index, selected_lt) + + if len(index) == self.tensor.ndim: + return self._getitem_full_dim_indexing(index, selected_lt) + + if isinstance(index, torch.Tensor) or ( + isinstance(index, (tuple, list)) and all( + isinstance(x, int) for x in index)): + return self._getitem_permutation(index, selected_lt) + raise ValueError('Not recognized index type') + + def _getitem_int_slice(self, index, selected_lt): + """ + :param index: + :param selected_lt: + :return: + """ + if selected_lt.ndim == 1: + selected_lt = selected_lt.reshape(1, -1) + if hasattr(self, "labels"): new_labels = deepcopy(self.full_labels) - if selected_lt.ndim == 1: - selected_lt = selected_lt.reshape(-1, 1) - for j in range(selected_lt.ndim): + to_update_dof = new_labels[0]['dof'][index] + to_update_dof = to_update_dof if isinstance(to_update_dof, ( + tuple, list, range)) else [to_update_dof] + new_labels.update( + {0: {'dof': to_update_dof, 'name': new_labels[0]['name']}} + ) + selected_lt.labels = new_labels + return selected_lt + + def _getitem_full_dim_indexing(self, index, selected_lt): + new_labels = {} + old_labels = self.full_labels + if selected_lt.ndim == 1: + selected_lt = selected_lt.reshape(-1, 1) + new_labels = deepcopy(old_labels) + new_labels[1].update({'dof': old_labels[1]['dof'][index[1]], + 'name': old_labels[1]['name']}) + idx = 0 + for j in range(selected_lt.ndim): + if not isinstance(index[j], int): if hasattr(self, "labels"): - if isinstance(index[j], list): - new_labels.update({j: {'dof': [new_labels[j]['dof'][i] for i in index[1]], - 'name': new_labels[j]['name']}}) - else: - new_labels.update({j: {'dof': new_labels[j]['dof'][index[j]], - 'name': new_labels[j]['name']}}) + new_labels.update( + self._update_label_for_dim(old_labels, index[j], idx)) + idx += 1 + selected_lt.labels = new_labels + return selected_lt - selected_lt.labels = new_labels - else: - new_labels = deepcopy(self.full_labels) - new_labels.update({0: {'dof': list[index], 'name': new_labels[0]['name']}}) - selected_lt.labels = self.labels + def _getitem_permutation(self, index, selected_lt): + new_labels = deepcopy(self.full_labels) + new_labels.update(self._update_label_for_dim(self.full_labels, index, + 0)) + selected_lt.labels = self.labels return selected_lt + @staticmethod + def _update_label_for_dim(old_labels, index, dim): + """ + TODO + :param old_labels: + :param index: + :param dim: + :return: + """ + if isinstance(index, list): + return {dim: {'dof': [old_labels[dim]['dof'][i] for i in index], + 'name': old_labels[dim]['name']}} + else: + return {dim: {'dof': old_labels[dim]['dof'][index], + 'name': old_labels[dim]['name']}} + + def sort_labels(self, dim=None): def argsort(lst): return sorted(range(len(lst)), key=lambda x: lst[x]) @@ -391,5 +448,6 @@ def argsort(lst): indexer = [slice(None)] * self.tensor.ndim indexer[dim] = sorted_index new_labels = deepcopy(self.full_labels) - new_labels[dim] = {'dof': sorted(labels), 'name': new_labels[dim]['name']} + new_labels[dim] = {'dof': sorted(labels), + 'name': new_labels[dim]['name']} return LabelTensor(self.tensor[indexer], new_labels) From 5c0d839291a2b10ed8d77fd3adbe049f19c4d3ba Mon Sep 17 00:00:00 2001 From: FilippoOlivo Date: Wed, 23 Oct 2024 15:04:28 +0200 Subject: [PATCH 12/14] Add Graph support in Dataset and Dataloader --- pina/collector.py | 5 +- pina/data/base_dataset.py | 37 ++++++----- pina/data/pina_batch.py | 3 +- pina/data/pina_subset.py | 9 ++- pina/data/sample_dataset.py | 4 +- pina/data/supervised_dataset.py | 3 +- pina/label_tensor.py | 4 +- pina/solvers/solver.py | 15 +++-- pina/solvers/supervised.py | 3 +- pina/trainer.py | 7 +- tests/test_dataset.py | 110 ++++++++++++++++++++------------ 11 files changed, 125 insertions(+), 75 deletions(-) diff --git a/pina/collector.py b/pina/collector.py index 4ebf236c8..c48c674e8 100644 --- a/pina/collector.py +++ b/pina/collector.py @@ -49,7 +49,7 @@ def store_fixed_data(self): # if the condition is not ready and domain is not attribute # of condition, we get and store the data if (not self._is_conditions_ready[condition_name]) and ( - not hasattr(condition, "domain")): + not hasattr(condition, "domain")): # get data keys = condition.__slots__ values = [getattr(condition, name) for name in keys] @@ -94,7 +94,8 @@ def store_sample_domains(self, n, mode, variables, sample_locations): self.data_collections[loc] = dict(zip(keys, values)) else: raise RuntimeError( - 'Try to sample variables which are not in problem defined in the problem') + 'Try to sample variables which are not in problem defined ' + 'in the problem') def add_points(self, new_points_dict): """ diff --git a/pina/data/base_dataset.py b/pina/data/base_dataset.py index b15a0be2a..d859aac00 100644 --- a/pina/data/base_dataset.py +++ b/pina/data/base_dataset.py @@ -4,6 +4,7 @@ from torch.utils.data import Dataset import torch from ..label_tensor import LabelTensor +from ..graph import Graph class BaseDataset(Dataset): @@ -42,38 +43,43 @@ def __init__(self, problem, device): collector = problem.collector for slot in self.__slots__: setattr(self, slot, []) - + num_el_per_condition = [] idx = 0 for name, data in collector.data_collections.items(): - keys = [] - for k, v in data.items(): - if isinstance(v, LabelTensor): - keys.append(k) + keys = list(data.keys()) + current_cond_num_el = None if sorted(self.__slots__) == sorted(keys): - for slot in self.__slots__: + slot_data = data[slot] + if isinstance(slot_data, (LabelTensor, torch.Tensor, + Graph)): + if current_cond_num_el is None: + current_cond_num_el = len(slot_data) + elif current_cond_num_el != len(slot_data): + raise ValueError('Different number of conditions') current_list = getattr(self, slot) - current_list.append(data[slot]) + current_list += [data[slot]] if not ( + isinstance(data[slot], list)) else data[slot] + num_el_per_condition.append(current_cond_num_el) self.condition_names[idx] = name idx += 1 - - if len(getattr(self, self.__slots__[0])) > 0: - input_list = getattr(self, self.__slots__[0]) + if num_el_per_condition: self.condition_indices = torch.cat( [ - torch.tensor([i] * len(input_list[i]), dtype=torch.uint8) - for i in range(len(self.condition_names)) + torch.tensor([i] * num_el_per_condition[i], + dtype=torch.uint8) + for i in range(len(num_el_per_condition)) ], dim=0, ) for slot in self.__slots__: current_attribute = getattr(self, slot) - setattr(self, slot, LabelTensor.vstack(current_attribute)) + if all(isinstance(a, LabelTensor) for a in current_attribute): + setattr(self, slot, LabelTensor.vstack(current_attribute)) else: self.condition_indices = torch.tensor([], dtype=torch.uint8) for slot in self.__slots__: setattr(self, slot, torch.tensor([])) - self.device = device def __len__(self): @@ -89,11 +95,10 @@ def __getattribute__(self, item): def __getitem__(self, idx): if isinstance(idx, str): return getattr(self, idx).to(self.device) - if isinstance(idx, slice): to_return_list = [] for i in self.__slots__: - to_return_list.append(getattr(self, i)[[idx]].to(self.device)) + to_return_list.append(getattr(self, i)[idx].to(self.device)) return to_return_list if isinstance(idx, (tuple, list)): diff --git a/pina/data/pina_batch.py b/pina/data/pina_batch.py index ed34a910b..65b5ac5ba 100644 --- a/pina/data/pina_batch.py +++ b/pina/data/pina_batch.py @@ -6,7 +6,8 @@ class Batch: """ - Implementation of the Batch class used during training to perform SGD optimization. + Implementation of the Batch class used during training to perform SGD + optimization. """ def __init__(self, dataset_dict, idx_dict): diff --git a/pina/data/pina_subset.py b/pina/data/pina_subset.py index 844321bd2..f1347b6c5 100644 --- a/pina/data/pina_subset.py +++ b/pina/data/pina_subset.py @@ -1,6 +1,8 @@ """ Module for PinaSubset class """ +from pina import LabelTensor +from torch import Tensor class PinaSubset: @@ -23,4 +25,9 @@ def __len__(self): return len(self.indices) def __getattr__(self, name): - return self.dataset.__getattribute__(name) + tensor = self.dataset.__getattribute__(name) + if isinstance(tensor, (LabelTensor, Tensor)): + return tensor[self.indices] + if isinstance(tensor, list): + return [tensor[i] for i in self.indices] + raise AttributeError("No attribute named {}".format(name)) diff --git a/pina/data/sample_dataset.py b/pina/data/sample_dataset.py index ba8bd19a9..99811cac8 100644 --- a/pina/data/sample_dataset.py +++ b/pina/data/sample_dataset.py @@ -2,6 +2,8 @@ Sample dataset module """ from .base_dataset import BaseDataset +from ..condition.input_equation_condition import InputPointsEquationCondition + class SamplePointDataset(BaseDataset): """ @@ -9,4 +11,4 @@ class SamplePointDataset(BaseDataset): composed of only input points. """ data_type = 'physics' - __slots__ = ['input_points'] + __slots__ = InputPointsEquationCondition.__slots__ diff --git a/pina/data/supervised_dataset.py b/pina/data/supervised_dataset.py index 2403e3d0c..be601050a 100644 --- a/pina/data/supervised_dataset.py +++ b/pina/data/supervised_dataset.py @@ -6,7 +6,8 @@ class SupervisedDataset(BaseDataset): """ - This class extends the BaseDataset to handle datasets that consist of input-output pairs. + This class extends the BaseDataset to handle datasets that consist of + input-output pairs. """ data_type = 'supervised' __slots__ = ['input_points', 'output_points'] diff --git a/pina/label_tensor.py b/pina/label_tensor.py index 62d87950a..87def2f8e 100644 --- a/pina/label_tensor.py +++ b/pina/label_tensor.py @@ -413,7 +413,6 @@ def _getitem_full_dim_indexing(self, index, selected_lt): return selected_lt def _getitem_permutation(self, index, selected_lt): - new_labels = deepcopy(self.full_labels) new_labels.update(self._update_label_for_dim(self.full_labels, index, 0)) @@ -429,6 +428,8 @@ def _update_label_for_dim(old_labels, index, dim): :param dim: :return: """ + if isinstance(index, torch.Tensor): + index = index.nonzero() if isinstance(index, list): return {dim: {'dof': [old_labels[dim]['dof'][i] for i in index], 'name': old_labels[dim]['name']}} @@ -436,7 +437,6 @@ def _update_label_for_dim(old_labels, index, dim): return {dim: {'dof': old_labels[dim]['dof'][index], 'name': old_labels[dim]['name']}} - def sort_labels(self, dim=None): def argsort(lst): return sorted(range(len(lst)), key=lambda x: lst[x]) diff --git a/pina/solvers/solver.py b/pina/solvers/solver.py index 1c6aa2b2b..6f55dedf0 100644 --- a/pina/solvers/solver.py +++ b/pina/solvers/solver.py @@ -38,7 +38,7 @@ def __init__(self, check_consistency(problem, AbstractProblem) self._check_solver_consistency(problem) - #Check consistency of models argument and encapsulate in list + # Check consistency of models argument and encapsulate in list if not isinstance(models, list): check_consistency(models, torch.nn.Module) # put everything in a list if only one input @@ -49,17 +49,17 @@ def __init__(self, check_consistency(models[idx], torch.nn.Module) len_model = len(models) - #If use_lt is true add extract operation in input + # If use_lt is true add extract operation in input if use_lt is True: - for idx in range(len(models)): + for idx, model in enumerate(models): models[idx] = Network( - model=models[idx], + model=model, input_variables=problem.input_variables, output_variables=problem.output_variables, extra_features=extra_features, ) - #Check scheduler consistency + encapsulation + # Check scheduler consistency + encapsulation if not isinstance(schedulers, list): check_consistency(schedulers, Scheduler) schedulers = [schedulers] @@ -67,7 +67,7 @@ def __init__(self, for scheduler in schedulers: check_consistency(scheduler, Scheduler) - #Check optimizer consistency + encapsulation + # Check optimizer consistency + encapsulation if not isinstance(optimizers, list): check_consistency(optimizers, Optimizer) optimizers = [optimizers] @@ -141,5 +141,6 @@ def _check_solver_consistency(self, problem): if not set(self.accepted_condition_types).issubset( condition.condition_type): raise ValueError( - f'{self.__name__} support only dose not support condition {condition.condition_type}' + f'{self.__name__} support only dose not support condition ' + f'{condition.condition_type}' ) diff --git a/pina/solvers/supervised.py b/pina/solvers/supervised.py index a0b0f83ed..62fc99149 100644 --- a/pina/solvers/supervised.py +++ b/pina/solvers/supervised.py @@ -130,14 +130,13 @@ def training_step(self, batch, batch_idx): if not hasattr(condition, "output_points"): raise NotImplementedError( f"{type(self).__name__} works only in data-driven mode.") - output_pts = out[condition_idx == condition_id] input_pts = pts[condition_idx == condition_id] input_pts.labels = pts.labels output_pts.labels = out.labels - loss = (self.loss_data(input_pts=input_pts, output_pts=output_pts)) + loss = self.loss_data(input_pts=input_pts, output_pts=output_pts) loss = loss.as_subclass(torch.Tensor) self.log("mean_loss", float(loss), prog_bar=True, logger=True) diff --git a/pina/trainer.py b/pina/trainer.py index 884eef77e..3de0d7e80 100644 --- a/pina/trainer.py +++ b/pina/trainer.py @@ -60,9 +60,12 @@ def _create_loader(self): if not self.solver.problem.collector.full: error_message = '\n'.join( [ - f'{" " * 13} ---> Condition {key} {"sampled" if value else "not sampled"}' + f"""{" " * 13} ---> Condition {key} {"sampled" if value else + "not sampled"}""" for key, value in - self.solver.problem.collector._is_conditions_ready.items()]) + self._solver.problem.collector._is_conditions_ready.items() + ] + ) raise RuntimeError('Cannot create Trainer if not all conditions ' 'are sampled. The Trainer got the following:\n' f'{error_message}') diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 653b0d6b6..503ddd683 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -1,13 +1,15 @@ import math import torch -from pina.data import SamplePointDataset, SupervisedDataset, PinaDataModule, UnsupervisedDataset, unsupervised_dataset +from pina.data import SamplePointDataset, SupervisedDataset, PinaDataModule, \ + UnsupervisedDataset from pina.data import PinaDataLoader from pina import LabelTensor, Condition from pina.equation import Equation from pina.domain import CartesianDomain -from pina.problem import SpatialProblem +from pina.problem import SpatialProblem, AbstractProblem from pina.operators import laplacian from pina.equation.equation_factory import FixedValue +from pina.graph import Graph def laplace_equation(input_, output_): @@ -30,49 +32,49 @@ class Poisson(SpatialProblem): conditions = { 'gamma1': - Condition(domain=CartesianDomain({ - 'x': [0, 1], - 'y': 1 - }), - equation=FixedValue(0.0)), + Condition(domain=CartesianDomain({ + 'x': [0, 1], + 'y': 1 + }), + equation=FixedValue(0.0)), 'gamma2': - Condition(domain=CartesianDomain({ - 'x': [0, 1], - 'y': 0 - }), - equation=FixedValue(0.0)), + Condition(domain=CartesianDomain({ + 'x': [0, 1], + 'y': 0 + }), + equation=FixedValue(0.0)), 'gamma3': - Condition(domain=CartesianDomain({ - 'x': 1, - 'y': [0, 1] - }), - equation=FixedValue(0.0)), + Condition(domain=CartesianDomain({ + 'x': 1, + 'y': [0, 1] + }), + equation=FixedValue(0.0)), 'gamma4': - Condition(domain=CartesianDomain({ - 'x': 0, - 'y': [0, 1] - }), - equation=FixedValue(0.0)), + Condition(domain=CartesianDomain({ + 'x': 0, + 'y': [0, 1] + }), + equation=FixedValue(0.0)), 'D': - Condition(input_points=LabelTensor(torch.rand(size=(100, 2)), - ['x', 'y']), - equation=my_laplace), + Condition(input_points=LabelTensor(torch.rand(size=(100, 2)), + ['x', 'y']), + equation=my_laplace), 'data': - Condition(input_points=in_, output_points=out_), + Condition(input_points=in_, output_points=out_), 'data2': - Condition(input_points=in2_, output_points=out2_), + Condition(input_points=in2_, output_points=out2_), 'unsupervised': - Condition( - input_points=LabelTensor(torch.rand(size=(45, 2)), ['x', 'y']), - conditional_variables=LabelTensor(torch.ones(size=(45, 1)), - ['alpha']), - ), + Condition( + input_points=LabelTensor(torch.rand(size=(45, 2)), ['x', 'y']), + conditional_variables=LabelTensor(torch.ones(size=(45, 1)), + ['alpha']), + ), 'unsupervised2': - Condition( - input_points=LabelTensor(torch.rand(size=(90, 2)), ['x', 'y']), - conditional_variables=LabelTensor(torch.ones(size=(90, 1)), - ['alpha']), - ) + Condition( + input_points=LabelTensor(torch.rand(size=(90, 2)), ['x', 'y']), + conditional_variables=LabelTensor(torch.ones(size=(90, 1)), + ['alpha']), + ) } @@ -98,8 +100,8 @@ def test_data(): assert dataset.input_points.shape == (61, 2) assert dataset['input_points'].labels == ['x', 'y'] assert dataset.input_points.labels == ['x', 'y'] - assert dataset['input_points', 3:].shape == (58, 2) - assert dataset[3:][1].labels == ['u'] + assert dataset.input_points[3:].shape == (58, 2) + assert dataset.output_points[:3].labels == ['u'] assert dataset.output_points.shape == (61, 1) assert dataset.output_points.labels == ['u'] assert dataset.condition_indices.dtype == torch.uint8 @@ -193,4 +195,32 @@ def test_loader(): assert i.unsupervised.input_points.requires_grad == True -test_loader() +coordinates = LabelTensor(torch.rand((100, 100, 2)), labels=['x', 'y']) +data = LabelTensor(torch.rand((100, 100, 3)), labels=['ux', 'uy', 'p']) + + +class GraphProblem(AbstractProblem): + output = LabelTensor(torch.rand((100, 3)), labels=['ux', 'uy', 'p']) + input = [Graph.build('radius', + nodes_coordinates=coordinates[i, :, :], + nodes_data=data[i, :, :], radius=0.2) + for i in + range(100)] + output_variables = ['u'] + + conditions = { + 'graph_data': Condition(input_points=input, output_points=output) + } + + +graph_problem = GraphProblem() + + +def test_loader_graph(): + data_module = PinaDataModule(graph_problem, device='cpu', batch_size=10) + data_module.setup() + loader = data_module.train_dataloader() + for i in loader: + assert len(i) <= 10 + assert isinstance(i.supervised.input_points, list) + assert all(isinstance(x, Graph) for x in i.supervised.input_points) From 4c9b6d0ea62a1341d4da745e63123c1cc71ed262 Mon Sep 17 00:00:00 2001 From: FilippoOlivo Date: Wed, 23 Oct 2024 15:04:28 +0200 Subject: [PATCH 13/14] Improve efficiency and refact LabelTensor, codacy correction and fix bug in PinaBatch --- pina/__init__.py | 8 +- pina/collector.py | 8 +- pina/data/base_dataset.py | 166 ++++--- pina/data/data_module.py | 148 +++--- pina/data/pina_batch.py | 25 +- pina/data/pina_subset.py | 13 +- pina/data/sample_dataset.py | 21 +- pina/label_tensor.py | 462 ++++++++++--------- pina/operators.py | 7 +- pina/problem/abstract_problem.py | 9 + pina/solvers/solver.py | 2 +- pina/trainer.py | 2 +- tests/test_label_tensor/test_label_tensor.py | 12 +- 13 files changed, 492 insertions(+), 391 deletions(-) diff --git a/pina/__init__.py b/pina/__init__.py index 30f35a6a5..c02e6debd 100644 --- a/pina/__init__.py +++ b/pina/__init__.py @@ -1,6 +1,7 @@ __all__ = [ - "PINN", "Trainer", "LabelTensor", "Plotter", "Condition", - "SamplePointDataset", "PinaDataModule", "PinaDataLoader" + "Trainer", "LabelTensor", "Plotter", "Condition", + "SamplePointDataset", "PinaDataModule", "PinaDataLoader", + 'TorchOptimizer', 'Graph' ] from .meta import * @@ -12,3 +13,6 @@ from .data import SamplePointDataset from .data import PinaDataModule from .data import PinaDataLoader +from .optim import TorchOptimizer +from .optim import TorchScheduler +from .graph import Graph \ No newline at end of file diff --git a/pina/collector.py b/pina/collector.py index c48c674e8..e75b49c8b 100644 --- a/pina/collector.py +++ b/pina/collector.py @@ -1,6 +1,3 @@ -from sympy.strategies.branch import condition - -from . import LabelTensor from .utils import check_consistency, merge_tensors @@ -16,6 +13,8 @@ def __init__(self, problem): # } # those variables are used for the dataloading self._data_collections = {name: {} for name in self.problem.conditions} + self.conditions_name = {i: name for i, name in + enumerate(self.problem.conditions)} # variables used to check that all conditions are sampled self._is_conditions_ready = { @@ -101,7 +100,8 @@ def add_points(self, new_points_dict): """ Add input points to a sampled condition - :param new_points_dict: Dictonary of input points (condition_name: LabelTensor) + :param new_points_dict: Dictonary of input points (condition_name: + LabelTensor) :raises RuntimeError: if at least one condition is not already sampled """ for k, v in new_points_dict.items(): diff --git a/pina/data/base_dataset.py b/pina/data/base_dataset.py index d859aac00..5e27fc91e 100644 --- a/pina/data/base_dataset.py +++ b/pina/data/base_dataset.py @@ -1,10 +1,12 @@ """ Basic data module implementation """ -from torch.utils.data import Dataset import torch +import logging + +from torch.utils.data import Dataset + from ..label_tensor import LabelTensor -from ..graph import Graph class BaseDataset(Dataset): @@ -12,10 +14,9 @@ class BaseDataset(Dataset): BaseDataset class, which handle initialization and data retrieval :var condition_indices: List of indices :var device: torch.device - :var condition_names: dict of condition index and corresponding name """ - def __new__(cls, problem, device): + def __new__(cls, problem=None, device=torch.device('cpu')): """ Ensure correct definition of __slots__ before initialization :param AbstractProblem problem: The formulation of the problem. @@ -30,7 +31,7 @@ def __new__(cls, problem, device): 'Something is wrong, __slots__ must be defined in subclasses.') return object.__new__(cls) - def __init__(self, problem, device): + def __init__(self, problem=None, device=torch.device('cpu')): """" Initialize the object based on __slots__ :param AbstractProblem problem: The formulation of the problem. @@ -38,79 +39,118 @@ def __init__(self, problem, device): dataset will be loaded. """ super().__init__() - - self.condition_names = {} - collector = problem.collector + self.empty = True + self.problem = problem + self.device = device + self.condition_indices = None for slot in self.__slots__: setattr(self, slot, []) - num_el_per_condition = [] - idx = 0 - for name, data in collector.data_collections.items(): + self.num_el_per_condition = [] + self.conditions_idx = [] + if self.problem is not None: + self._init_from_problem(self.problem.collector.data_collections) + self.initialized = False + + def _init_from_problem(self, collector_dict): + """ + TODO + """ + for name, data in collector_dict.items(): keys = list(data.keys()) - current_cond_num_el = None - if sorted(self.__slots__) == sorted(keys): - for slot in self.__slots__: - slot_data = data[slot] - if isinstance(slot_data, (LabelTensor, torch.Tensor, - Graph)): - if current_cond_num_el is None: - current_cond_num_el = len(slot_data) - elif current_cond_num_el != len(slot_data): - raise ValueError('Different number of conditions') - current_list = getattr(self, slot) - current_list += [data[slot]] if not ( - isinstance(data[slot], list)) else data[slot] - num_el_per_condition.append(current_cond_num_el) - self.condition_names[idx] = name - idx += 1 - if num_el_per_condition: + if set(self.__slots__) == set(keys): + self._populate_init_list(data) + idx = [key for key, val in + self.problem.collector.conditions_name.items() if + val == name] + self.conditions_idx.append(idx) + self.initialize() + + def add_points(self, data_dict, condition_idx, batching_dim=0): + """ + This method filled internal lists of data points + :param data_dict: dictionary containing data points + :param condition_idx: index of the condition to which the data points + belong to + :param batching_dim: dimension of the batching + :raises: ValueError if the dataset has already been initialized + """ + if not self.initialized: + self._populate_init_list(data_dict, batching_dim) + self.conditions_idx.append(condition_idx) + self.empty = False + else: + raise ValueError('Dataset already initialized') + + def _populate_init_list(self, data_dict, batching_dim=0): + current_cond_num_el = None + for slot in data_dict.keys(): + slot_data = data_dict[slot] + if batching_dim != 0: + if isinstance(slot_data, (LabelTensor, torch.Tensor)): + dims = len(slot_data.size()) + slot_data = slot_data.permute( + [batching_dim] + [dim for dim in range(dims) if + dim != batching_dim]) + if current_cond_num_el is None: + current_cond_num_el = len(slot_data) + elif current_cond_num_el != len(slot_data): + raise ValueError('Different dimension in same condition') + current_list = getattr(self, slot) + current_list += [slot_data] if not ( + isinstance(slot_data, list)) else slot_data + self.num_el_per_condition.append(current_cond_num_el) + + def initialize(self): + """ + Initialize the datasets tensors/LabelTensors/lists given the lists + already filled + """ + logging.debug(f'Initialize dataset {self.__class__.__name__}') + + if self.num_el_per_condition: self.condition_indices = torch.cat( [ - torch.tensor([i] * num_el_per_condition[i], + torch.tensor([i] * self.num_el_per_condition[i], dtype=torch.uint8) - for i in range(len(num_el_per_condition)) + for i in range(len(self.num_el_per_condition)) ], - dim=0, + dim=0 ) for slot in self.__slots__: current_attribute = getattr(self, slot) if all(isinstance(a, LabelTensor) for a in current_attribute): setattr(self, slot, LabelTensor.vstack(current_attribute)) - else: - self.condition_indices = torch.tensor([], dtype=torch.uint8) - for slot in self.__slots__: - setattr(self, slot, torch.tensor([])) - self.device = device + self.initialized = True def __len__(self): + """ + :return: Number of elements in the dataset + """ return len(getattr(self, self.__slots__[0])) - def __getattribute__(self, item): - attribute = super().__getattribute__(item) - if isinstance(attribute, - LabelTensor) and attribute.dtype == torch.float32: - attribute = attribute.to(device=self.device).requires_grad_() - return attribute - def __getitem__(self, idx): - if isinstance(idx, str): - return getattr(self, idx).to(self.device) - if isinstance(idx, slice): - to_return_list = [] - for i in self.__slots__: - to_return_list.append(getattr(self, i)[idx].to(self.device)) - return to_return_list - - if isinstance(idx, (tuple, list)): - if (len(idx) == 2 and isinstance(idx[0], str) - and isinstance(idx[1], (list, slice))): - tensor = getattr(self, idx[0]) - return tensor[[idx[1]]].to(self.device) - if all(isinstance(x, int) for x in idx): - to_return_list = [] - for i in self.__slots__: - to_return_list.append( - getattr(self, i)[[idx]].to(self.device)) - return to_return_list + """ + :param idx: + :return: + """ + if not isinstance(idx, (tuple, list, slice, int)): + raise IndexError("Invalid index") + tensors = [] + for attribute in self.__slots__: + tensor = getattr(self, attribute) + if isinstance(attribute, (LabelTensor, torch.Tensor)): + tensors.append(tensor.__getitem__(idx)) + elif isinstance(attribute, list): + if isinstance(idx, (list, tuple)): + tensor = [tensor[i] for i in idx] + tensors.append(tensor) + return tensors - raise ValueError(f'Invalid index {idx}') + def apply_shuffle(self, indices): + for slot in self.__slots__: + if slot != 'equation': + attribute = getattr(self, slot) + if isinstance(attribute, (LabelTensor, torch.Tensor)): + setattr(self, 'slot', attribute[[indices]]) + if isinstance(attribute, list): + setattr(self, 'slot', [attribute[i] for i in indices]) diff --git a/pina/data/data_module.py b/pina/data/data_module.py index 25c7e54ed..98460ae70 100644 --- a/pina/data/data_module.py +++ b/pina/data/data_module.py @@ -4,7 +4,8 @@ import math import torch -from lightning import LightningDataModule +import logging +from pytorch_lightning import LightningDataModule from .sample_dataset import SamplePointDataset from .supervised_dataset import SupervisedDataset from .unsupervised_dataset import UnsupervisedDataset @@ -22,8 +23,9 @@ def __init__(self, problem, device, train_size=.7, - test_size=.2, - eval_size=.1, + test_size=.1, + val_size=.2, + predict_size=0., batch_size=None, shuffle=True, datasets=None): @@ -37,37 +39,64 @@ def __init__(self, :param batch_size: batch size used for training :param datasets: list of datasets objects """ + logging.debug('Start initialization of Pina DataModule') + logging.info('Start initialization of Pina DataModule') super().__init__() - dataset_classes = [SupervisedDataset, UnsupervisedDataset, - SamplePointDataset] + self.problem = problem + self.device = device + self.dataset_classes = [SupervisedDataset, UnsupervisedDataset, + SamplePointDataset] if datasets is None: - self.datasets = [DatasetClass(problem, device) for DatasetClass in - dataset_classes] + self.datasets = None else: self.datasets = datasets self.split_length = [] self.split_names = [] + self.loader_functions = {} + self.batch_size = batch_size + self.condition_names = problem.collector.conditions_name + if train_size > 0: self.split_names.append('train') self.split_length.append(train_size) + self.loader_functions['train_dataloader'] = lambda: PinaDataLoader( + self.splits['train'], self.batch_size, self.condition_names) if test_size > 0: self.split_length.append(test_size) self.split_names.append('test') - if eval_size > 0: - self.split_length.append(eval_size) - self.split_names.append('eval') - - self.batch_size = batch_size - self.condition_names = None + self.loader_functions['test_dataloader'] = lambda: PinaDataLoader( + self.splits['test'], self.batch_size, self.condition_names) + if val_size > 0: + self.split_length.append(val_size) + self.split_names.append('val') + self.loader_functions['val_dataloader'] = lambda: PinaDataLoader( + self.splits['val'], self.batch_size, + self.condition_names) + if predict_size > 0: + self.split_length.append(predict_size) + self.split_names.append('predict') + self.loader_functions[ + 'predict_dataloader'] = lambda: PinaDataLoader( + self.splits['predict'], self.batch_size, + self.condition_names) self.splits = {k: {} for k in self.split_names} self.shuffle = shuffle + for k, v in self.loader_functions.items(): + setattr(self, k, v) + + def prepare_data(self): + if self.datasets is None: + self._create_datasets() + def setup(self, stage=None): """ Perform the splitting of the dataset """ - self.extract_conditions() + logging.debug('Start setup of Pina DataModule obj') + if self.datasets is None: + self._create_datasets() if stage == 'fit' or stage is None: for dataset in self.datasets: if len(dataset) > 0: @@ -82,53 +111,6 @@ def setup(self, stage=None): else: raise ValueError("stage must be either 'fit' or 'test'") - def extract_conditions(self): - """ - Extract conditions from dataset and update condition indices - """ - # Extract number of conditions - n_conditions = 0 - for dataset in self.datasets: - if n_conditions != 0: - dataset.condition_names = { - key + n_conditions: value - for key, value in dataset.condition_names.items() - } - n_conditions += len(dataset.condition_names) - - self.condition_names = { - key: value - for dataset in self.datasets - for key, value in dataset.condition_names.items() - } - - def train_dataloader(self): - """ - Return the training dataloader for the dataset - :return: data loader - :rtype: PinaDataLoader - """ - return PinaDataLoader(self.splits['train'], self.batch_size, - self.condition_names) - - def test_dataloader(self): - """ - Return the testing dataloader for the dataset - :return: data loader - :rtype: PinaDataLoader - """ - return PinaDataLoader(self.splits['test'], self.batch_size, - self.condition_names) - - def eval_dataloader(self): - """ - Return the evaluation dataloader for the dataset - :return: data loader - :rtype: PinaDataLoader - """ - return PinaDataLoader(self.splits['eval'], self.batch_size, - self.condition_names) - @staticmethod def dataset_split(dataset, lengths, seed=None, shuffle=True): """ @@ -141,30 +123,28 @@ def dataset_split(dataset, lengths, seed=None, shuffle=True): :rtype: PinaSubset """ if sum(lengths) - 1 < 1e-3: + len_dataset = len(dataset) lengths = [ - int(math.floor(len(dataset) * length)) for length in lengths + int(math.floor(len_dataset * length)) for length in lengths ] - remainder = len(dataset) - sum(lengths) for i in range(remainder): lengths[i % len(lengths)] += 1 elif sum(lengths) - 1 >= 1e-3: raise ValueError(f"Sum of lengths is {sum(lengths)} less than 1") - if sum(lengths) != len(dataset): - raise ValueError("Sum of lengths is not equal to dataset length") - if shuffle: if seed is not None: generator = torch.Generator() generator.manual_seed(seed) indices = torch.randperm(sum(lengths), - generator=generator).tolist() + generator=generator) else: - indices = torch.arange(sum(lengths)).tolist() - else: - indices = torch.arange(0, sum(lengths), 1, - dtype=torch.uint8).tolist() + indices = torch.randperm(sum(lengths)) + dataset.apply_shuffle(indices) + + indices = torch.arange(0, sum(lengths), 1, + dtype=torch.uint8).tolist() offsets = [ sum(lengths[:i]) if i > 0 else 0 for i in range(len(lengths)) ] @@ -172,3 +152,29 @@ def dataset_split(dataset, lengths, seed=None, shuffle=True): PinaSubset(dataset, indices[offset:offset + length]) for offset, length in zip(offsets, lengths) ] + + def _create_datasets(self): + """ + Create the dataset objects putting data + """ + logging.debug('Dataset creation in PinaDataModule obj') + collector = self.problem.collector + batching_dim = self.problem.batching_dimension + datasets_slots = [i.__slots__ for i in self.dataset_classes] + self.datasets = [dataset(device=self.device) for dataset in + self.dataset_classes] + logging.debug('Filling datasets in PinaDataModule obj') + for name, data in collector.data_collections.items(): + keys = list(data.keys()) + idx = [key for key, val in collector.conditions_name.items() if + val == name] + for i, slot in enumerate(datasets_slots): + if slot == keys: + self.datasets[i].add_points(data, idx[0], batching_dim) + continue + datasets = [] + for dataset in self.datasets: + if not dataset.empty: + dataset.initialize() + datasets.append(dataset) + self.datasets = datasets diff --git a/pina/data/pina_batch.py b/pina/data/pina_batch.py index 65b5ac5ba..6fb74f922 100644 --- a/pina/data/pina_batch.py +++ b/pina/data/pina_batch.py @@ -10,13 +10,15 @@ class Batch: optimization. """ - def __init__(self, dataset_dict, idx_dict): - + def __init__(self, dataset_dict, idx_dict, require_grad=True): + self.attributes = [] for k, v in dataset_dict.items(): setattr(self, k, v) + self.attributes.append(k) for k, v in idx_dict.items(): setattr(self, k + '_idx', v) + self.require_grad = require_grad def __len__(self): """ @@ -31,9 +33,18 @@ def __len__(self): length += len(getattr(self, dataset)) return length + def __getattribute__(self, item): + if item in super().__getattribute__('attributes'): + dataset = super().__getattribute__(item) + index = super().__getattribute__(item + '_idx') + return PinaSubset( + dataset.dataset, + dataset.indices[index]) + else: + return super().__getattribute__(item) + def __getattr__(self, item): - if not item in dir(self): - raise AttributeError(f'Batch instance has no attribute {item}') - return PinaSubset( - getattr(self, item).dataset, - getattr(self, item).indices[self.coordinates_dict[item]]) + if item == 'data' and len(self.attributes) == 1: + item = self.attributes[0] + return super().__getattribute__(item) + raise AttributeError(f"'Batch' object has no attribute '{item}'") \ No newline at end of file diff --git a/pina/data/pina_subset.py b/pina/data/pina_subset.py index f1347b6c5..275541e97 100644 --- a/pina/data/pina_subset.py +++ b/pina/data/pina_subset.py @@ -2,21 +2,22 @@ Module for PinaSubset class """ from pina import LabelTensor -from torch import Tensor +from torch import Tensor, float32 class PinaSubset: """ TODO """ - __slots__ = ['dataset', 'indices'] + __slots__ = ['dataset', 'indices', 'require_grad'] - def __init__(self, dataset, indices): + def __init__(self, dataset, indices, require_grad=True): """ TODO """ self.dataset = dataset self.indices = indices + self.require_grad = require_grad def __len__(self): """ @@ -27,7 +28,9 @@ def __len__(self): def __getattr__(self, name): tensor = self.dataset.__getattribute__(name) if isinstance(tensor, (LabelTensor, Tensor)): - return tensor[self.indices] + tensor = tensor[[self.indices]].to(self.dataset.device) + return tensor.requires_grad_( + self.require_grad) if tensor.dtype == float32 else tensor if isinstance(tensor, list): return [tensor[i] for i in self.indices] - raise AttributeError("No attribute named {}".format(name)) + raise AttributeError(f"No attribute named {name}") diff --git a/pina/data/sample_dataset.py b/pina/data/sample_dataset.py index 99811cac8..5c47a14e9 100644 --- a/pina/data/sample_dataset.py +++ b/pina/data/sample_dataset.py @@ -1,8 +1,9 @@ """ Sample dataset module """ +from copy import deepcopy from .base_dataset import BaseDataset -from ..condition.input_equation_condition import InputPointsEquationCondition +from ..condition import InputPointsEquationCondition class SamplePointDataset(BaseDataset): @@ -12,3 +13,21 @@ class SamplePointDataset(BaseDataset): """ data_type = 'physics' __slots__ = InputPointsEquationCondition.__slots__ + + def add_points(self, data_dict, condition_idx, batching_dim=0): + data_dict = deepcopy(data_dict) + data_dict.pop('equation') + super().add_points(data_dict, condition_idx) + + def _init_from_problem(self, collector_dict, batching_dim=0): + for name, data in collector_dict.items(): + keys = list(data.keys()) + if set(self.__slots__) == set(keys): + data = deepcopy(data) + data.pop('equation') + self._populate_init_list(data) + idx = [key for key, val in + self.problem.collector.conditions_name.items() if + val == name] + self.conditions_idx.append(idx) + self.initialize() diff --git a/pina/label_tensor.py b/pina/label_tensor.py index 87def2f8e..a28a3eaf3 100644 --- a/pina/label_tensor.py +++ b/pina/label_tensor.py @@ -1,5 +1,5 @@ """ Module for LabelTensor """ -from copy import deepcopy, copy +from copy import copy import torch from torch import Tensor @@ -8,21 +8,29 @@ def issubset(a, b): """ Check if a is a subset of b. """ - return set(a).issubset(set(b)) + if isinstance(a, list) and isinstance(b, list): + return set(a).issubset(set(b)) + elif isinstance(a, range) and isinstance(b, range): + return a.start <= b.start and a.stop >= b.stop + else: + return False class LabelTensor(torch.Tensor): """Torch tensor with a label for any column.""" @staticmethod - def __new__(cls, x, labels, *args, **kwargs): - return super().__new__(cls, x, *args, **kwargs) + def __new__(cls, x, labels, full=True, *args, **kwargs): + if isinstance(x, LabelTensor): + return x + else: + return super().__new__(cls, x, *args, **kwargs) @property def tensor(self): return self.as_subclass(Tensor) - def __init__(self, x, labels): + def __init__(self, x, labels, full=False): """ Construct a `LabelTensor` by passing a dict of the labels @@ -34,8 +42,17 @@ def __init__(self, x, labels): """ self.dim_names = None + self.full = full self.labels = labels + @classmethod + def __internal_init__(cls, x, labels, dim_names ,full=False, *args, **kwargs): + lt = cls.__new__(cls, x, labels, full, *args, **kwargs) + lt._labels = labels + lt.full = full + lt.dim_names = dim_names + return lt + @property def labels(self): """Property decorator for labels @@ -43,12 +60,29 @@ def labels(self): :return: labels of self :rtype: list """ - return self._labels[self.tensor.ndim - 1]['dof'] + if self.ndim - 1 in self._labels.keys(): + return self._labels[self.ndim - 1]['dof'] @property def full_labels(self): """Property decorator for labels + :return: labels of self + :rtype: list + """ + to_return_dict = {} + shape_tensor = self.shape + for i in range(len(shape_tensor)): + if i in self._labels.keys(): + to_return_dict[i] = self._labels[i] + else: + to_return_dict[i] = {'dof': range(shape_tensor[i]), 'name': i} + return to_return_dict + + @property + def stored_labels(self): + """Property decorator for labels + :return: labels of self :rtype: list """ @@ -62,26 +96,77 @@ def labels(self, labels): :param labels: Labels to assign to the class variable _labels. :type: labels: str | list(str) | dict """ - if hasattr(self, 'labels') is False: - self.init_labels() + if not hasattr(self, '_labels'): + self._labels = {} if isinstance(labels, dict): - self.update_labels_from_dict(labels) + self._init_labels_from_dict(labels) elif isinstance(labels, list): - self.update_labels_from_list(labels) + self._init_labels_from_list(labels) elif isinstance(labels, str): labels = [labels] - self.update_labels_from_list(labels) + self._init_labels_from_list(labels) else: raise ValueError("labels must be list, dict or string.") self.set_names() + def _init_labels_from_dict(self, labels): + """ + Update the internal label representation according to the values + passed as input. + + :param labels: The label(s) to update. + :type labels: dict + :raises ValueError: dof list contain duplicates or number of dof + does not match with tensor shape + """ + tensor_shape = self.shape + + if hasattr(self, 'full') and self.full: + labels = {i: labels[i] if i in labels else {'name': i} for i in + labels.keys()} + for k, v in labels.items(): + # Init labels from str + if isinstance(v, str): + v = {'name': v, 'dof': range(tensor_shape[k])} + # Init labels from dict + elif isinstance(v, dict) and list(v.keys()) == ['name']: + # Init from dict with only name key + v['dof'] = range(tensor_shape[k]) + # Init from dict with both name and dof keys + elif isinstance(v, dict) and sorted(list(v.keys())) == ['dof', + 'name']: + dof_list = v['dof'] + dof_len = len(dof_list) + if dof_len != len(set(dof_list)): + raise ValueError("dof must be unique") + if dof_len != tensor_shape[k]: + raise ValueError( + 'Number of dof does not match tensor shape') + else: + ValueError('Illegal labels initialization') + # Perform update + self._labels[k] = v + + def _init_labels_from_list(self, labels): + """ + Given a list of dof, this method update the internal label + representation + + :param labels: The label(s) to update. + :type labels: list + """ + # Create a dict with labels + last_dim_labels = { + self.ndim - 1: {'dof': labels, 'name': self.ndim - 1}} + self._init_labels_from_dict(last_dim_labels) + def set_names(self): - labels = self.full_labels + labels = self.stored_labels self.dim_names = {} - for dim in range(self.tensor.ndim): + for dim in labels.keys(): self.dim_names[labels[dim]['name']] = dim - def extract(self, label_to_extract): + def extract(self, labels_to_extract): """ Extract the subset of the original tensor by returning all the columns corresponding to the passed ``label_to_extract``. @@ -91,78 +176,68 @@ def extract(self, label_to_extract): :raises TypeError: Labels are not ``str``. :raises ValueError: Label to extract is not in the labels ``list``. """ - if isinstance(label_to_extract, (str, int)): - label_to_extract = [label_to_extract] - if isinstance(label_to_extract, (tuple, list)): - return self._extract_from_list(label_to_extract) - if isinstance(label_to_extract, dict): - return self._extract_from_dict(label_to_extract) - raise ValueError('labels_to_extract must be str or list or dict') - - def _extract_from_list(self, labels_to_extract): - # Store locally all necessary obj/variables - ndim = self.tensor.ndim - labels = self.full_labels - tensor = self.tensor - last_dim_label = self.labels + # Convert str/int to string + if isinstance(labels_to_extract, (str, int)): + labels_to_extract = [labels_to_extract] + + # Store useful variables + labels = self.stored_labels + stored_keys = labels.keys() + dim_names = self.dim_names + ndim = len(super().shape) + + # Convert tuple/list to dict + if isinstance(labels_to_extract, (tuple, list)): + if not ndim - 1 in stored_keys: + raise ValueError( + "LabelTensor does not have labels in last dimension") + name = labels[max(stored_keys)]['name'] + labels_to_extract = {name: list(labels_to_extract)} + + # If labels_to_extract is not dict then rise error + if not isinstance(labels_to_extract, dict): + raise ValueError('labels_to_extract must be str or list or dict') - # Verify if all the labels in labels_to_extract are in last dimension - if set(labels_to_extract).issubset(last_dim_label) is False: - raise ValueError( - 'Cannot extract a dof which is not in the original LabelTensor') + # Make copy of labels (avoid issue in consistency) + updated_labels = {k: copy(v) for k, v in labels.items()} - # Extract index to extract - idx_to_extract = [last_dim_label.index(i) for i in labels_to_extract] + # Initialize list used to perform extraction + extractor = [slice(None) for _ in range(ndim)] - # Perform extraction - new_tensor = tensor[..., idx_to_extract] + # Loop over labels_to_extract dict + for k, v in labels_to_extract.items(): - # Manage labels - new_labels = copy(labels) + # If label is not find raise value error + idx_dim = dim_names.get(k) + if idx_dim is None: + raise ValueError( + 'Cannot extract label with is not in original labels') + + dim_labels = labels[idx_dim]['dof'] + v = [v] if isinstance(v, (int, str)) else v + + if not isinstance(v, range): + extractor[idx_dim] = [dim_labels.index(i) for i in v] if len( + v) > 1 else slice(dim_labels.index(v[0]), + dim_labels.index(v[0]) + 1) + else: + extractor[idx_dim] = slice(v.start, v.stop) - last_dim_new_label = {ndim - 1: { - 'dof': list(labels_to_extract), - 'name': labels[ndim - 1]['name'] - }} - new_labels.update(last_dim_new_label) - return LabelTensor(new_tensor, new_labels) + updated_labels.update({idx_dim: {'dof': v, 'name': k}}) - def _extract_from_dict(self, labels_to_extract): - labels = self.full_labels tensor = self.tensor - ndim = tensor.ndim - new_labels = deepcopy(labels) - new_tensor = tensor - for k, _ in labels_to_extract.items(): - idx_dim = self.dim_names[k] - dim_labels = labels[idx_dim]['dof'] - if isinstance(labels_to_extract[k], (int, str)): - labels_to_extract[k] = [labels_to_extract[k]] - if set(labels_to_extract[k]).issubset(dim_labels) is False: - raise ValueError( - 'Cannot extract a dof which is not in the original ' - 'LabelTensor') - idx_to_extract = [dim_labels.index(i) for i in labels_to_extract[k]] - indexer = [slice(None)] * idx_dim + [idx_to_extract] + [ - slice(None)] * (ndim - idx_dim - 1) - new_tensor = new_tensor[indexer] - dim_new_label = {idx_dim: { - 'dof': labels_to_extract[k], - 'name': labels[idx_dim]['name'] - }} - new_labels.update(dim_new_label) - return LabelTensor(new_tensor, new_labels) + tensor = tensor[extractor] + return LabelTensor.__internal_init__(tensor, updated_labels, dim_names) def __str__(self): """ returns a string with the representation of the class """ - s = '' for key, value in self._labels.items(): s += f"{key}: {value}\n" s += '\n' - s += super().__str__() + s += self.tensor.__str__() return s @staticmethod @@ -174,55 +249,44 @@ def cat(tensors, dim=0): :param tensors: tensors to concatenate :type tensors: list(LabelTensor) - :param dim: dimensions on which you want to perform the operation (default 0) + :param dim: dimensions on which you want to perform the operation + (default 0) :type dim: int :rtype: LabelTensor :raises ValueError: either number dof or dimensions names differ """ if len(tensors) == 0: return [] - if len(tensors) == 1: + if len(tensors) == 1 or isinstance(tensors, LabelTensor): return tensors[0] - new_labels_cat_dim = LabelTensor._check_validity_before_cat(tensors, - dim) - # Perform cat on tensors new_tensor = torch.cat(tensors, dim=dim) # Update labels - labels = tensors[0].full_labels - labels.pop(dim) - new_labels_cat_dim = new_labels_cat_dim if len( - set(new_labels_cat_dim)) == len(new_labels_cat_dim) \ - else range(new_tensor.shape[dim]) - labels[dim] = {'dof': new_labels_cat_dim, - 'name': tensors[1].full_labels[dim]['name']} - return LabelTensor(new_tensor, labels) + labels = LabelTensor.__create_labels_cat(tensors, + dim) + + return LabelTensor.__internal_init__(new_tensor, labels, tensors[0].dim_names) @staticmethod - def _check_validity_before_cat(tensors, dim): - n_dims = tensors[0].ndim - new_labels_cat_dim = [] + def __create_labels_cat(tensors, dim): # Check if names and dof of the labels are the same in all dimensions # except in dim - for i in range(n_dims): - name = tensors[0].full_labels[i]['name'] - if i != dim: - dof = tensors[0].full_labels[i]['dof'] - for tensor in tensors: - dof_to_check = tensor.full_labels[i]['dof'] - name_to_check = tensor.full_labels[i]['name'] - if dof != dof_to_check or name != name_to_check: - raise ValueError( - 'dimensions must have the same dof and name') - else: - for tensor in tensors: - new_labels_cat_dim += tensor.full_labels[i]['dof'] - name_to_check = tensor.full_labels[i]['name'] - if name != name_to_check: - raise ValueError( - 'Dimensions to concatenate must have the same name') - return new_labels_cat_dim + stored_labels = [tensor.stored_labels for tensor in tensors] + + # check if: + # - labels dict have same keys + # - all labels are the same expect for dimension dim + if not all(all(stored_labels[i][k] == stored_labels[0][k] + for i in range(len(stored_labels))) + for k in stored_labels[0].keys() if k != dim): + raise RuntimeError('tensors must have the same shape and dof') + + labels = {k: copy(v) for k, v in tensors[0].stored_labels.items()} + if dim in labels.keys(): + last_dim_dof = [i for j in stored_labels for i in j[dim]['dof']] + labels[dim]['dof'] = last_dim_dof + return labels def requires_grad_(self, mode=True): lt = super().requires_grad_(mode) @@ -251,52 +315,10 @@ def clone(self, *args, **kwargs): :return: A copy of the tensor. :rtype: LabelTensor """ - - out = LabelTensor(super().clone(*args, **kwargs), self._labels) + labels = {k: copy(v) for k, v in self._labels.items()} + out = LabelTensor(super().clone(*args, **kwargs), labels) return out - def init_labels(self): - self._labels = { - idx_: { - 'dof': range(self.tensor.shape[idx_]), - 'name': idx_ - } for idx_ in range(self.tensor.ndim) - } - - def update_labels_from_dict(self, labels): - """ - Update the internal label representation according to the values passed - as input. - - :param labels: The label(s) to update. - :type labels: dict - :raises ValueError: dof list contain duplicates or number of dof does - not match with tensor shape - """ - tensor_shape = self.tensor.shape - # Check dimensionality - for k, v in labels.items(): - if len(v['dof']) != len(set(v['dof'])): - raise ValueError("dof must be unique") - if len(v['dof']) != tensor_shape[k]: - raise ValueError( - 'Number of dof does not match with tensor dimension') - # Perform update - self._labels.update(labels) - - def update_labels_from_list(self, labels): - """ - Given a list of dof, this method update the internal label - representation - - :param labels: The label(s) to update. - :type labels: list - """ - # Create a dict with labels - last_dim_labels = { - self.tensor.ndim - 1: {'dof': labels, 'name': self.tensor.ndim - 1}} - self.update_labels_from_dict(last_dim_labels) - @staticmethod def summation(tensors): if len(tensors) == 0: @@ -304,25 +326,30 @@ def summation(tensors): if len(tensors) == 1: return tensors[0] # Collect all labels - labels = tensors[0].full_labels + # Check labels of all the tensors in each dimension - for j in range(tensors[0].ndim): - for i in range(1, len(tensors)): - if labels[j] != tensors[i].full_labels[j]: - labels.pop(j) - break - # Sum tensors + if not all(tensor.shape == tensors[0].shape for tensor in tensors) or \ + not all(tensor.full_labels[i] == tensors[0].full_labels[i] for + tensor in tensors for i in range(tensors[0].ndim - 1)): + raise RuntimeError('Tensors must have the same shape and labels') + + last_dim_labels = [] data = torch.zeros(tensors[0].tensor.shape) for tensor in tensors: data += tensor.tensor - new_tensor = LabelTensor(data, labels) - return new_tensor + last_dim_labels.append(tensor.labels) + + last_dim_labels = ['+'.join(items) for items in zip(*last_dim_labels)] + labels = {k: copy(v) for k, v in tensors[0].stored_labels.items()} + labels.update({tensors[0].ndim - 1: {'dof': last_dim_labels, + 'name': tensors[0].name}}) + return LabelTensor(data, labels) def append(self, tensor, mode='std'): if mode == 'std': # Call cat on last dimension new_label_tensor = LabelTensor.cat([self, tensor], - dim=self.tensor.ndim - 1) + dim=self.ndim - 1) elif mode == 'cross': # Crete tensor and call cat on last dimension tensor1 = self @@ -333,7 +360,7 @@ def append(self, tensor, mode='std'): tensor2 = LabelTensor(tensor2.repeat_interleave(n1, dim=0), labels=tensor2.labels) new_label_tensor = LabelTensor.cat([tensor1, tensor2], - dim=self.tensor.ndim - 1) + dim=self.ndim - 1) else: raise ValueError('mode must be either "std" or "cross"') return new_label_tensor @@ -357,97 +384,76 @@ def __getitem__(self, index): :param index: :return: """ - if isinstance(index, str) or (isinstance(index, (tuple, list)) and all( isinstance(a, str) for a in index)): return self.extract(index) + selected_lt = super().__getitem__(index) if isinstance(index, (int, slice)): - return self._getitem_int_slice(index, selected_lt) - - if len(index) == self.tensor.ndim: - return self._getitem_full_dim_indexing(index, selected_lt) + index = [index] - if isinstance(index, torch.Tensor) or ( - isinstance(index, (tuple, list)) and all( - isinstance(x, int) for x in index)): - return self._getitem_permutation(index, selected_lt) - raise ValueError('Not recognized index type') + if index[0] == Ellipsis: + index = [slice(None)] * (self.ndim - 1) + [index[1]] - def _getitem_int_slice(self, index, selected_lt): - """ - :param index: - :param selected_lt: - :return: - """ - if selected_lt.ndim == 1: - selected_lt = selected_lt.reshape(1, -1) if hasattr(self, "labels"): - new_labels = deepcopy(self.full_labels) - to_update_dof = new_labels[0]['dof'][index] - to_update_dof = to_update_dof if isinstance(to_update_dof, ( - tuple, list, range)) else [to_update_dof] - new_labels.update( - {0: {'dof': to_update_dof, 'name': new_labels[0]['name']}} - ) - selected_lt.labels = new_labels - return selected_lt - - def _getitem_full_dim_indexing(self, index, selected_lt): - new_labels = {} - old_labels = self.full_labels - if selected_lt.ndim == 1: - selected_lt = selected_lt.reshape(-1, 1) - new_labels = deepcopy(old_labels) - new_labels[1].update({'dof': old_labels[1]['dof'][index[1]], - 'name': old_labels[1]['name']}) - idx = 0 - for j in range(selected_lt.ndim): - if not isinstance(index[j], int): - if hasattr(self, "labels"): - new_labels.update( - self._update_label_for_dim(old_labels, index[j], idx)) - idx += 1 - selected_lt.labels = new_labels - return selected_lt - - def _getitem_permutation(self, index, selected_lt): - new_labels = deepcopy(self.full_labels) - new_labels.update(self._update_label_for_dim(self.full_labels, index, - 0)) - selected_lt.labels = self.labels + labels = {k: copy(v) for k, v in self.stored_labels.items()} + for j, idx in enumerate(index): + if isinstance(idx, int): + selected_lt = selected_lt.unsqueeze(j) + if j in labels.keys() and idx != slice(None): + self._update_single_label(labels, labels, idx, j) + selected_lt = LabelTensor.__internal_init__(selected_lt, labels, + self.dim_names) return selected_lt @staticmethod - def _update_label_for_dim(old_labels, index, dim): + def _update_single_label(old_labels, to_update_labels, index, dim): """ TODO - :param old_labels: - :param index: - :param dim: + :param old_labels: labels from which retrieve data + :param to_update_labels: labels to update + :param index: index of dof to retain + :param dim: label index :return: """ + old_dof = old_labels[dim]['dof'] + if not isinstance(index, (int, slice)) and len(index) == len( + old_dof) and isinstance(old_dof, range): + return if isinstance(index, torch.Tensor): - index = index.nonzero() + index = index.nonzero(as_tuple=True)[ + 0] if index.dtype == torch.bool else index.tolist() if isinstance(index, list): - return {dim: {'dof': [old_labels[dim]['dof'][i] for i in index], - 'name': old_labels[dim]['name']}} + to_update_labels.update({dim: { + 'dof': [old_dof[i] for i in index], + 'name': old_labels[dim]['name']}}) else: - return {dim: {'dof': old_labels[dim]['dof'][index], - 'name': old_labels[dim]['name']}} + to_update_labels.update({dim: {'dof': old_dof[index], + 'name': old_labels[dim]['name']}}) def sort_labels(self, dim=None): - def argsort(lst): + def arg_sort(lst): return sorted(range(len(lst)), key=lambda x: lst[x]) if dim is None: - dim = self.tensor.ndim - 1 - labels = self.full_labels[dim]['dof'] - sorted_index = argsort(labels) - indexer = [slice(None)] * self.tensor.ndim + dim = self.ndim - 1 + labels = self.stored_labels[dim]['dof'] + sorted_index = arg_sort(labels) + indexer = [slice(None)] * self.ndim indexer[dim] = sorted_index - new_labels = deepcopy(self.full_labels) - new_labels[dim] = {'dof': sorted(labels), - 'name': new_labels[dim]['name']} - return LabelTensor(self.tensor[indexer], new_labels) + return self.__getitem__(indexer) + + def __deepcopy__(self, memo): + from copy import deepcopy + cls = self.__class__ + result = cls(deepcopy(self.tensor), deepcopy(self.stored_labels)) + return result + + def permute(self, *dims): + tensor = super().permute(*dims) + stored_labels = self.stored_labels + keys_list = list(*dims) + labels = {keys_list.index(k): copy(stored_labels[k]) for k in + stored_labels.keys()} + return LabelTensor.__internal_init__(tensor, labels, self.dim_names) diff --git a/pina/operators.py b/pina/operators.py index 9e780ec82..780aec8ef 100644 --- a/pina/operators.py +++ b/pina/operators.py @@ -85,7 +85,8 @@ def grad_scalar_output(output_, input_, d): raise RuntimeError gradients = grad_scalar_output(output_, input_, d) - elif output_.shape[output_.ndim - 1] >= 2: # vector output ############################## + elif output_.shape[ + output_.ndim - 1] >= 2: # vector output ############################## tensor_to_cat = [] for i, c in enumerate(components): c_output = output_.extract([c]) @@ -143,7 +144,6 @@ def div(output_, input_, components=None, d=None): tensors_to_sum.append(grad_output.extract(c_fields)) labels[i] = c_fields div_result = LabelTensor.summation(tensors_to_sum) - div_result.labels = ["+".join(labels)] return div_result @@ -211,7 +211,8 @@ def laplacian(output_, input_, components=None, d=None, method="std"): result[:, idx] = grad(grad_output, input_, d=di).flatten() to_append_tensors[idx] = grad(grad_output, input_, d=di) labels[idx] = f"dd{ci[0]}dd{di[0]}" - result = LabelTensor.cat(tensors=to_append_tensors, dim=output_.tensor.ndim - 1) + result = LabelTensor.cat(tensors=to_append_tensors, + dim=output_.tensor.ndim - 1) result.labels = labels return result diff --git a/pina/problem/abstract_problem.py b/pina/problem/abstract_problem.py index da548f1e2..9e18dbaf9 100644 --- a/pina/problem/abstract_problem.py +++ b/pina/problem/abstract_problem.py @@ -32,11 +32,20 @@ def __init__(self): # training all type self.collector.full, which returns true if all # points are ready. self.collector.store_fixed_data() + self._batching_dimension = 0 @property def collector(self): return self._collector + @property + def batching_dimension(self): + return self._batching_dimension + + @batching_dimension.setter + def batching_dimension(self, value): + self._batching_dimension = value + # TODO this should be erase when dataloading will interface collector, # kept only for back compatibility @property diff --git a/pina/solvers/solver.py b/pina/solvers/solver.py index 6f55dedf0..2d9b4a57e 100644 --- a/pina/solvers/solver.py +++ b/pina/solvers/solver.py @@ -94,7 +94,7 @@ def forward(self, *args, **kwargs): pass @abstractmethod - def training_step(self): + def training_step(self, batch, batch_idx): pass @abstractmethod diff --git a/pina/trainer.py b/pina/trainer.py index 3de0d7e80..1601d771b 100644 --- a/pina/trainer.py +++ b/pina/trainer.py @@ -79,7 +79,7 @@ def _create_loader(self): data_module = PinaDataModule(problem=self.solver.problem, device=device, train_size=self.train_size, test_size=self.test_size, - eval_size=self.eval_size) + val_size=self.eval_size) data_module.setup() self._loader = data_module.train_dataloader() diff --git a/tests/test_label_tensor/test_label_tensor.py b/tests/test_label_tensor/test_label_tensor.py index 846976730..61e479951 100644 --- a/tests/test_label_tensor/test_label_tensor.py +++ b/tests/test_label_tensor/test_label_tensor.py @@ -131,17 +131,17 @@ def test_concatenation_3D(): data_2 = torch.rand(20, 3, 4) labels_2 = ['x', 'y', 'z', 'w'] lt2 = LabelTensor(data_2, labels_2) - with pytest.raises(ValueError): + with pytest.raises(RuntimeError): LabelTensor.cat([lt1, lt2], dim=2) data_1 = torch.rand(20, 3, 2) labels_1 = ['x', 'y'] lt1 = LabelTensor(data_1, labels_1) data_2 = torch.rand(20, 3, 3) - labels_2 = ['x', 'w', 'a'] + labels_2 = ['z', 'w', 'a'] lt2 = LabelTensor(data_2, labels_2) lt_cat = LabelTensor.cat([lt1, lt2], dim=2) assert lt_cat.shape == (20, 3, 5) - assert lt_cat.full_labels[2]['dof'] == range(5) + assert lt_cat.full_labels[2]['dof'] == ['x', 'y', 'z', 'w', 'a'] assert lt_cat.full_labels[0]['dof'] == range(20) assert lt_cat.full_labels[1]['dof'] == range(3) @@ -157,7 +157,8 @@ def test_summation(): assert lt_sum.ndim == lt_sum.ndim assert lt_sum.shape[0] == 20 assert lt_sum.shape[1] == 3 - assert lt_sum.full_labels == labels_all + assert lt_sum.full_labels[0] == labels_all[0] + assert lt_sum.labels == ['x+x', 'y+y', 'z+z'] assert torch.eq(lt_sum.tensor, torch.ones(20, 3) * 2).all() lt1 = LabelTensor(torch.ones(20, 3), labels_all) lt2 = LabelTensor(torch.ones(20, 3), labels_all) @@ -166,7 +167,8 @@ def test_summation(): assert lt_sum.ndim == lt_sum.ndim assert lt_sum.shape[0] == 20 assert lt_sum.shape[1] == 3 - assert lt_sum.full_labels == labels_all + assert lt_sum.full_labels[0] == labels_all[0] + assert lt_sum.labels == ['x+x+x', 'y+y+y', 'z+z+z'] assert torch.eq(lt_sum.tensor, torch.ones(20, 3) * 2).all() From 113e5592bb6751082a1a996033b2f88709b6c2bd Mon Sep 17 00:00:00 2001 From: FilippoOlivo Date: Thu, 31 Oct 2024 09:50:19 +0100 Subject: [PATCH 14/14] Codacy correction --- pina/__init__.py | 7 +- pina/collector.py | 32 +++--- pina/condition/condition.py | 13 +-- pina/condition/condition_interface.py | 15 ++- pina/condition/data_condition.py | 3 +- pina/condition/domain_equation_condition.py | 3 +- pina/condition/input_equation_condition.py | 7 +- pina/condition/input_output_condition.py | 2 +- pina/data/base_dataset.py | 31 +++--- pina/data/data_module.py | 35 +++--- pina/data/pina_batch.py | 9 +- pina/data/sample_dataset.py | 10 +- pina/domain/cartesian.py | 10 +- pina/domain/domain_interface.py | 3 +- pina/label_tensor.py | 112 +++++++++++++------- pina/operators.py | 23 ++-- pina/problem/abstract_problem.py | 20 ++-- pina/solvers/solver.py | 3 +- pina/trainer.py | 34 +++--- pina/utils.py | 11 +- tests/test_condition.py | 3 + tests/test_dataset.py | 81 +++++++------- tests/test_geometry/test_cartesian.py | 1 + 23 files changed, 250 insertions(+), 218 deletions(-) diff --git a/pina/__init__.py b/pina/__init__.py index c02e6debd..3bc28ae6c 100644 --- a/pina/__init__.py +++ b/pina/__init__.py @@ -1,7 +1,6 @@ __all__ = [ - "Trainer", "LabelTensor", "Plotter", "Condition", - "SamplePointDataset", "PinaDataModule", "PinaDataLoader", - 'TorchOptimizer', 'Graph' + "Trainer", "LabelTensor", "Plotter", "Condition", "SamplePointDataset", + "PinaDataModule", "PinaDataLoader", 'TorchOptimizer', 'Graph' ] from .meta import * @@ -15,4 +14,4 @@ from .data import PinaDataLoader from .optim import TorchOptimizer from .optim import TorchScheduler -from .graph import Graph \ No newline at end of file +from .graph import Graph diff --git a/pina/collector.py b/pina/collector.py index e75b49c8b..3219b2b6a 100644 --- a/pina/collector.py +++ b/pina/collector.py @@ -2,23 +2,28 @@ class Collector: + def __init__(self, problem): # creating a hook between collector and problem self.problem = problem # this variable is used to store the data in the form: - # {'[condition_name]' : - # {'input_points' : Tensor, + # {'[condition_name]' : + # {'input_points' : Tensor, # '[equation/output_points/conditional_variables]': Tensor} # } # those variables are used for the dataloading self._data_collections = {name: {} for name in self.problem.conditions} - self.conditions_name = {i: name for i, name in - enumerate(self.problem.conditions)} + self.conditions_name = { + i: name + for i, name in enumerate(self.problem.conditions) + } # variables used to check that all conditions are sampled self._is_conditions_ready = { - name: False for name in self.problem.conditions} + name: False + for name in self.problem.conditions + } self.full = False @property @@ -47,8 +52,8 @@ def store_fixed_data(self): for condition_name, condition in self.problem.conditions.items(): # if the condition is not ready and domain is not attribute # of condition, we get and store the data - if (not self._is_conditions_ready[condition_name]) and ( - not hasattr(condition, "domain")): + if (not self._is_conditions_ready[condition_name]) and (not hasattr( + condition, "domain")): # get data keys = condition.__slots__ values = [getattr(condition, name) for name in keys] @@ -70,7 +75,8 @@ def store_sample_domains(self, n, mode, variables, sample_locations): # if we have sampled the condition but not all variables else: already_sampled = [ - self.data_collections[loc]['input_points']] + self.data_collections[loc]['input_points'] + ] # if the condition is ready but we want to sample again else: self._is_conditions_ready[loc] = False @@ -78,14 +84,10 @@ def store_sample_domains(self, n, mode, variables, sample_locations): # get the samples samples = [ - condition.domain.sample(n=n, mode=mode, - variables=variables) - ] + already_sampled + condition.domain.sample(n=n, mode=mode, variables=variables) + ] + already_sampled pts = merge_tensors(samples) - if ( - set(pts.labels).issubset( - sorted(self.problem.input_variables)) - ): + if (set(pts.labels).issubset(sorted(self.problem.input_variables))): pts = pts.sort_labels() if sorted(pts.labels) == sorted(self.problem.input_variables): self._is_conditions_ready[loc] = True diff --git a/pina/condition/condition.py b/pina/condition/condition.py index 01965fe0d..3a62143e4 100644 --- a/pina/condition/condition.py +++ b/pina/condition/condition.py @@ -39,21 +39,16 @@ class Condition: """ __slots__ = list( - set( - InputOutputPointsCondition.__slots__ + + set(InputOutputPointsCondition.__slots__ + InputPointsEquationCondition.__slots__ + DomainEquationCondition.__slots__ + - DataConditionInterface.__slots__ - ) - ) + DataConditionInterface.__slots__)) def __new__(cls, *args, **kwargs): if len(args) != 0: - raise ValueError( - "Condition takes only the following keyword " - f"arguments: {Condition.__slots__}." - ) + raise ValueError("Condition takes only the following keyword " + f"arguments: {Condition.__slots__}.") sorted_keys = sorted(kwargs.keys()) if sorted_keys == sorted(InputOutputPointsCondition.__slots__): diff --git a/pina/condition/condition_interface.py b/pina/condition/condition_interface.py index 808c06afe..f2fe5db97 100644 --- a/pina/condition/condition_interface.py +++ b/pina/condition/condition_interface.py @@ -1,6 +1,6 @@ - from abc import ABCMeta + class ConditionInterface(metaclass=ABCMeta): condition_types = ['physics', 'supervised', 'unsupervised'] @@ -12,7 +12,7 @@ def __init__(self, *args, **kwargs): @property def problem(self): return self._problem - + @problem.setter def problem(self, value): self._problem = value @@ -20,15 +20,14 @@ def problem(self, value): @property def condition_type(self): return self._condition_type - + @condition_type.setter def condition_type(self, values): if not isinstance(values, (list, tuple)): values = [values] for value in values: if value not in ConditionInterface.condition_types: - raise ValueError( - 'Unavailable type of condition, expected one of' - f' {ConditionInterface.condition_types}.' - ) - self._condition_type = values \ No newline at end of file + raise ValueError( + 'Unavailable type of condition, expected one of' + f' {ConditionInterface.condition_types}.') + self._condition_type = values diff --git a/pina/condition/data_condition.py b/pina/condition/data_condition.py index 3bcd4be6d..c6777231c 100644 --- a/pina/condition/data_condition.py +++ b/pina/condition/data_condition.py @@ -5,6 +5,7 @@ from ..graph import Graph from ..utils import check_consistency + class DataConditionInterface(ConditionInterface): """ Condition for data. This condition must be used every @@ -29,4 +30,4 @@ def __setattr__(self, key, value): check_consistency(value, (LabelTensor, Graph, torch.Tensor)) DataConditionInterface.__dict__[key].__set__(self, value) elif key in ('_problem', '_condition_type'): - super().__setattr__(key, value) \ No newline at end of file + super().__setattr__(key, value) diff --git a/pina/condition/domain_equation_condition.py b/pina/condition/domain_equation_condition.py index 28315655b..58dca70be 100644 --- a/pina/condition/domain_equation_condition.py +++ b/pina/condition/domain_equation_condition.py @@ -5,6 +5,7 @@ from ..domain import DomainInterface from ..equation.equation_interface import EquationInterface + class DomainEquationCondition(ConditionInterface): """ Condition for domain/equation data. This condition must be used every @@ -30,4 +31,4 @@ def __setattr__(self, key, value): check_consistency(value, (EquationInterface)) DomainEquationCondition.__dict__[key].__set__(self, value) elif key in ('_problem', '_condition_type'): - super().__setattr__(key, value) \ No newline at end of file + super().__setattr__(key, value) diff --git a/pina/condition/input_equation_condition.py b/pina/condition/input_equation_condition.py index 0d34dfc93..bf05130c0 100644 --- a/pina/condition/input_equation_condition.py +++ b/pina/condition/input_equation_condition.py @@ -6,6 +6,7 @@ from ..utils import check_consistency from ..equation.equation_interface import EquationInterface + class InputPointsEquationCondition(ConditionInterface): """ Condition for input_points/equation data. This condition must be used every @@ -25,10 +26,12 @@ def __init__(self, input_points, equation): def __setattr__(self, key, value): if key == 'input_points': - check_consistency(value, (LabelTensor)) # for now only labeltensors, we need labels for the operators! + check_consistency( + value, (LabelTensor) + ) # for now only labeltensors, we need labels for the operators! InputPointsEquationCondition.__dict__[key].__set__(self, value) elif key == 'equation': check_consistency(value, (EquationInterface)) InputPointsEquationCondition.__dict__[key].__set__(self, value) elif key in ('_problem', '_condition_type'): - super().__setattr__(key, value) \ No newline at end of file + super().__setattr__(key, value) diff --git a/pina/condition/input_output_condition.py b/pina/condition/input_output_condition.py index 8a17495dd..08ed21d96 100644 --- a/pina/condition/input_output_condition.py +++ b/pina/condition/input_output_condition.py @@ -1,4 +1,3 @@ - import torch from .condition_interface import ConditionInterface @@ -6,6 +5,7 @@ from ..graph import Graph from ..utils import check_consistency + class InputOutputPointsCondition(ConditionInterface): """ Condition for domain/equation data. This condition must be used every diff --git a/pina/data/base_dataset.py b/pina/data/base_dataset.py index 5e27fc91e..2c28ba30b 100644 --- a/pina/data/base_dataset.py +++ b/pina/data/base_dataset.py @@ -59,9 +59,11 @@ def _init_from_problem(self, collector_dict): keys = list(data.keys()) if set(self.__slots__) == set(keys): self._populate_init_list(data) - idx = [key for key, val in - self.problem.collector.conditions_name.items() if - val == name] + idx = [ + key for key, val in + self.problem.collector.conditions_name.items() + if val == name + ] self.conditions_idx.append(idx) self.initialize() @@ -89,15 +91,16 @@ def _populate_init_list(self, data_dict, batching_dim=0): if isinstance(slot_data, (LabelTensor, torch.Tensor)): dims = len(slot_data.size()) slot_data = slot_data.permute( - [batching_dim] + [dim for dim in range(dims) if - dim != batching_dim]) + [batching_dim] + + [dim for dim in range(dims) if dim != batching_dim]) if current_cond_num_el is None: current_cond_num_el = len(slot_data) elif current_cond_num_el != len(slot_data): raise ValueError('Different dimension in same condition') current_list = getattr(self, slot) - current_list += [slot_data] if not ( - isinstance(slot_data, list)) else slot_data + current_list += [ + slot_data + ] if not (isinstance(slot_data, list)) else slot_data self.num_el_per_condition.append(current_cond_num_el) def initialize(self): @@ -108,14 +111,12 @@ def initialize(self): logging.debug(f'Initialize dataset {self.__class__.__name__}') if self.num_el_per_condition: - self.condition_indices = torch.cat( - [ - torch.tensor([i] * self.num_el_per_condition[i], - dtype=torch.uint8) - for i in range(len(self.num_el_per_condition)) - ], - dim=0 - ) + self.condition_indices = torch.cat([ + torch.tensor([i] * self.num_el_per_condition[i], + dtype=torch.uint8) + for i in range(len(self.num_el_per_condition)) + ], + dim=0) for slot in self.__slots__: current_attribute = getattr(self, slot) if all(isinstance(a, LabelTensor) for a in current_attribute): diff --git a/pina/data/data_module.py b/pina/data/data_module.py index 98460ae70..bd117b54b 100644 --- a/pina/data/data_module.py +++ b/pina/data/data_module.py @@ -44,8 +44,9 @@ def __init__(self, super().__init__() self.problem = problem self.device = device - self.dataset_classes = [SupervisedDataset, UnsupervisedDataset, - SamplePointDataset] + self.dataset_classes = [ + SupervisedDataset, UnsupervisedDataset, SamplePointDataset + ] if datasets is None: self.datasets = None else: @@ -71,15 +72,12 @@ def __init__(self, self.split_length.append(val_size) self.split_names.append('val') self.loader_functions['val_dataloader'] = lambda: PinaDataLoader( - self.splits['val'], self.batch_size, - self.condition_names) + self.splits['val'], self.batch_size, self.condition_names) if predict_size > 0: self.split_length.append(predict_size) self.split_names.append('predict') - self.loader_functions[ - 'predict_dataloader'] = lambda: PinaDataLoader( - self.splits['predict'], self.batch_size, - self.condition_names) + self.loader_functions['predict_dataloader'] = lambda: PinaDataLoader( + self.splits['predict'], self.batch_size, self.condition_names) self.splits = {k: {} for k in self.split_names} self.shuffle = shuffle @@ -104,8 +102,8 @@ def setup(self, stage=None): self.split_length, shuffle=self.shuffle) for i in range(len(self.split_length)): - self.splits[ - self.split_names[i]][dataset.data_type] = splits[i] + self.splits[self.split_names[i]][ + dataset.data_type] = splits[i] elif stage == 'test': raise NotImplementedError("Testing pipeline not implemented yet") else: @@ -137,14 +135,12 @@ def dataset_split(dataset, lengths, seed=None, shuffle=True): if seed is not None: generator = torch.Generator() generator.manual_seed(seed) - indices = torch.randperm(sum(lengths), - generator=generator) + indices = torch.randperm(sum(lengths), generator=generator) else: indices = torch.randperm(sum(lengths)) dataset.apply_shuffle(indices) - indices = torch.arange(0, sum(lengths), 1, - dtype=torch.uint8).tolist() + indices = torch.arange(0, sum(lengths), 1, dtype=torch.uint8).tolist() offsets = [ sum(lengths[:i]) if i > 0 else 0 for i in range(len(lengths)) ] @@ -161,13 +157,16 @@ def _create_datasets(self): collector = self.problem.collector batching_dim = self.problem.batching_dimension datasets_slots = [i.__slots__ for i in self.dataset_classes] - self.datasets = [dataset(device=self.device) for dataset in - self.dataset_classes] + self.datasets = [ + dataset(device=self.device) for dataset in self.dataset_classes + ] logging.debug('Filling datasets in PinaDataModule obj') for name, data in collector.data_collections.items(): keys = list(data.keys()) - idx = [key for key, val in collector.conditions_name.items() if - val == name] + idx = [ + key for key, val in collector.conditions_name.items() + if val == name + ] for i, slot in enumerate(datasets_slots): if slot == keys: self.datasets[i].add_points(data, idx[0], batching_dim) diff --git a/pina/data/pina_batch.py b/pina/data/pina_batch.py index 6fb74f922..c5d1b61dc 100644 --- a/pina/data/pina_batch.py +++ b/pina/data/pina_batch.py @@ -37,14 +37,11 @@ def __getattribute__(self, item): if item in super().__getattribute__('attributes'): dataset = super().__getattribute__(item) index = super().__getattribute__(item + '_idx') - return PinaSubset( - dataset.dataset, - dataset.indices[index]) - else: - return super().__getattribute__(item) + return PinaSubset(dataset.dataset, dataset.indices[index]) + return super().__getattribute__(item) def __getattr__(self, item): if item == 'data' and len(self.attributes) == 1: item = self.attributes[0] return super().__getattribute__(item) - raise AttributeError(f"'Batch' object has no attribute '{item}'") \ No newline at end of file + raise AttributeError(f"'Batch' object has no attribute '{item}'") diff --git a/pina/data/sample_dataset.py b/pina/data/sample_dataset.py index 5c47a14e9..bc3bca335 100644 --- a/pina/data/sample_dataset.py +++ b/pina/data/sample_dataset.py @@ -19,15 +19,17 @@ def add_points(self, data_dict, condition_idx, batching_dim=0): data_dict.pop('equation') super().add_points(data_dict, condition_idx) - def _init_from_problem(self, collector_dict, batching_dim=0): + def _init_from_problem(self, collector_dict): for name, data in collector_dict.items(): keys = list(data.keys()) if set(self.__slots__) == set(keys): data = deepcopy(data) data.pop('equation') self._populate_init_list(data) - idx = [key for key, val in - self.problem.collector.conditions_name.items() if - val == name] + idx = [ + key for key, val in + self.problem.collector.conditions_name.items() + if val == name + ] self.conditions_idx.append(idx) self.initialize() diff --git a/pina/domain/cartesian.py b/pina/domain/cartesian.py index 4986ea7e5..5fe99d644 100644 --- a/pina/domain/cartesian.py +++ b/pina/domain/cartesian.py @@ -168,9 +168,8 @@ def _1d_sampler(n, mode, variables): for variable in variables: if variable in self.fixed_.keys(): value = self.fixed_[variable] - pts_variable = torch.tensor([[value]]).repeat( - result.shape[0], 1 - ) + pts_variable = torch.tensor([[value] + ]).repeat(result.shape[0], 1) pts_variable = pts_variable.as_subclass(LabelTensor) pts_variable.labels = [variable] @@ -203,9 +202,8 @@ def _Nd_sampler(n, mode, variables): for variable in variables: if variable in self.fixed_.keys(): value = self.fixed_[variable] - pts_variable = torch.tensor([[value]]).repeat( - result.shape[0], 1 - ) + pts_variable = torch.tensor([[value] + ]).repeat(result.shape[0], 1) pts_variable = pts_variable.as_subclass(LabelTensor) pts_variable.labels = [variable] diff --git a/pina/domain/domain_interface.py b/pina/domain/domain_interface.py index 4fa70a2ba..916bf3e90 100644 --- a/pina/domain/domain_interface.py +++ b/pina/domain/domain_interface.py @@ -38,8 +38,7 @@ def sample_modes(self, values): if value not in DomainInterface.available_sampling_modes: raise TypeError(f"mode {value} not valid. Expected at least " "one in " - f"{DomainInterface.available_sampling_modes}." - ) + f"{DomainInterface.available_sampling_modes}.") @abstractmethod def sample(self): diff --git a/pina/label_tensor.py b/pina/label_tensor.py index a28a3eaf3..719975c51 100644 --- a/pina/label_tensor.py +++ b/pina/label_tensor.py @@ -1,5 +1,5 @@ """ Module for LabelTensor """ -from copy import copy +from copy import copy, deepcopy import torch from torch import Tensor @@ -10,17 +10,16 @@ def issubset(a, b): """ if isinstance(a, list) and isinstance(b, list): return set(a).issubset(set(b)) - elif isinstance(a, range) and isinstance(b, range): + if isinstance(a, range) and isinstance(b, range): return a.start <= b.start and a.stop >= b.stop - else: - return False + return False class LabelTensor(torch.Tensor): """Torch tensor with a label for any column.""" @staticmethod - def __new__(cls, x, labels, full=True, *args, **kwargs): + def __new__(cls, x, labels, *args, **kwargs): if isinstance(x, LabelTensor): return x else: @@ -30,7 +29,7 @@ def __new__(cls, x, labels, full=True, *args, **kwargs): def tensor(self): return self.as_subclass(Tensor) - def __init__(self, x, labels, full=False): + def __init__(self, x, labels, **kwargs): """ Construct a `LabelTensor` by passing a dict of the labels @@ -42,14 +41,19 @@ def __init__(self, x, labels, full=False): """ self.dim_names = None - self.full = full + self.full = kwargs.get('full', True) self.labels = labels @classmethod - def __internal_init__(cls, x, labels, dim_names ,full=False, *args, **kwargs): - lt = cls.__new__(cls, x, labels, full, *args, **kwargs) + def __internal_init__(cls, + x, + labels, + dim_names, + *args, + **kwargs): + lt = cls.__new__(cls, x, labels, *args, **kwargs) lt._labels = labels - lt.full = full + lt.full = kwargs.get('full', True) lt.dim_names = dim_names return lt @@ -122,8 +126,12 @@ def _init_labels_from_dict(self, labels): tensor_shape = self.shape if hasattr(self, 'full') and self.full: - labels = {i: labels[i] if i in labels else {'name': i} for i in - labels.keys()} + labels = { + i: labels[i] if i in labels else { + 'name': i + } + for i in labels.keys() + } for k, v in labels.items(): # Init labels from str if isinstance(v, str): @@ -133,8 +141,8 @@ def _init_labels_from_dict(self, labels): # Init from dict with only name key v['dof'] = range(tensor_shape[k]) # Init from dict with both name and dof keys - elif isinstance(v, dict) and sorted(list(v.keys())) == ['dof', - 'name']: + elif isinstance(v, dict) and sorted(list( + v.keys())) == ['dof', 'name']: dof_list = v['dof'] dof_len = len(dof_list) if dof_len != len(set(dof_list)): @@ -143,7 +151,7 @@ def _init_labels_from_dict(self, labels): raise ValueError( 'Number of dof does not match tensor shape') else: - ValueError('Illegal labels initialization') + raise ValueError('Illegal labels initialization') # Perform update self._labels[k] = v @@ -157,7 +165,11 @@ def _init_labels_from_list(self, labels): """ # Create a dict with labels last_dim_labels = { - self.ndim - 1: {'dof': labels, 'name': self.ndim - 1}} + self.ndim - 1: { + 'dof': labels, + 'name': self.ndim - 1 + } + } self._init_labels_from_dict(last_dim_labels) def set_names(self): @@ -217,9 +229,10 @@ def extract(self, labels_to_extract): v = [v] if isinstance(v, (int, str)) else v if not isinstance(v, range): - extractor[idx_dim] = [dim_labels.index(i) for i in v] if len( - v) > 1 else slice(dim_labels.index(v[0]), - dim_labels.index(v[0]) + 1) + extractor[idx_dim] = [dim_labels.index(i) + for i in v] if len(v) > 1 else slice( + dim_labels.index(v[0]), + dim_labels.index(v[0]) + 1) else: extractor[idx_dim] = slice(v.start, v.stop) @@ -263,10 +276,10 @@ def cat(tensors, dim=0): new_tensor = torch.cat(tensors, dim=dim) # Update labels - labels = LabelTensor.__create_labels_cat(tensors, - dim) + labels = LabelTensor.__create_labels_cat(tensors, dim) - return LabelTensor.__internal_init__(new_tensor, labels, tensors[0].dim_names) + return LabelTensor.__internal_init__(new_tensor, labels, + tensors[0].dim_names) @staticmethod def __create_labels_cat(tensors, dim): @@ -277,9 +290,10 @@ def __create_labels_cat(tensors, dim): # check if: # - labels dict have same keys # - all labels are the same expect for dimension dim - if not all(all(stored_labels[i][k] == stored_labels[0][k] - for i in range(len(stored_labels))) - for k in stored_labels[0].keys() if k != dim): + if not all( + all(stored_labels[i][k] == stored_labels[0][k] + for i in range(len(stored_labels))) + for k in stored_labels[0].keys() if k != dim): raise RuntimeError('tensors must have the same shape and dof') labels = {k: copy(v) for k, v in tensors[0].stored_labels.items()} @@ -341,8 +355,12 @@ def summation(tensors): last_dim_labels = ['+'.join(items) for items in zip(*last_dim_labels)] labels = {k: copy(v) for k, v in tensors[0].stored_labels.items()} - labels.update({tensors[0].ndim - 1: {'dof': last_dim_labels, - 'name': tensors[0].name}}) + labels.update({ + tensors[0].ndim - 1: { + 'dof': last_dim_labels, + 'name': tensors[0].name + } + }) return LabelTensor(data, labels) def append(self, tensor, mode='std'): @@ -384,8 +402,9 @@ def __getitem__(self, index): :param index: :return: """ - if isinstance(index, str) or (isinstance(index, (tuple, list)) and all( - isinstance(a, str) for a in index)): + if isinstance(index, + str) or (isinstance(index, (tuple, list)) + and all(isinstance(a, str) for a in index)): return self.extract(index) selected_lt = super().__getitem__(index) @@ -418,21 +437,31 @@ def _update_single_label(old_labels, to_update_labels, index, dim): :return: """ old_dof = old_labels[dim]['dof'] - if not isinstance(index, (int, slice)) and len(index) == len( - old_dof) and isinstance(old_dof, range): + if not isinstance( + index, + (int, slice)) and len(index) == len(old_dof) and isinstance( + old_dof, range): return if isinstance(index, torch.Tensor): - index = index.nonzero(as_tuple=True)[ - 0] if index.dtype == torch.bool else index.tolist() + index = index.nonzero( + as_tuple=True + )[0] if index.dtype == torch.bool else index.tolist() if isinstance(index, list): - to_update_labels.update({dim: { - 'dof': [old_dof[i] for i in index], - 'name': old_labels[dim]['name']}}) + to_update_labels.update({ + dim: { + 'dof': [old_dof[i] for i in index], + 'name': old_labels[dim]['name'] + } + }) else: - to_update_labels.update({dim: {'dof': old_dof[index], - 'name': old_labels[dim]['name']}}) + to_update_labels.update( + {dim: { + 'dof': old_dof[index], + 'name': old_labels[dim]['name'] + }}) def sort_labels(self, dim=None): + def arg_sort(lst): return sorted(range(len(lst)), key=lambda x: lst[x]) @@ -445,7 +474,6 @@ def arg_sort(lst): return self.__getitem__(indexer) def __deepcopy__(self, memo): - from copy import deepcopy cls = self.__class__ result = cls(deepcopy(self.tensor), deepcopy(self.stored_labels)) return result @@ -454,6 +482,8 @@ def permute(self, *dims): tensor = super().permute(*dims) stored_labels = self.stored_labels keys_list = list(*dims) - labels = {keys_list.index(k): copy(stored_labels[k]) for k in - stored_labels.keys()} + labels = { + keys_list.index(k): copy(stored_labels[k]) + for k in stored_labels.keys() + } return LabelTensor.__internal_init__(tensor, labels, self.dim_names) diff --git a/pina/operators.py b/pina/operators.py index 780aec8ef..0b306dfb9 100644 --- a/pina/operators.py +++ b/pina/operators.py @@ -56,9 +56,9 @@ def grad_scalar_output(output_, input_, d): gradients = torch.autograd.grad( output_, input_, - grad_outputs=torch.ones( - output_.size(), dtype=output_.dtype, device=output_.device - ), + grad_outputs=torch.ones(output_.size(), + dtype=output_.dtype, + device=output_.device), create_graph=True, retain_graph=True, allow_unused=True, @@ -85,8 +85,8 @@ def grad_scalar_output(output_, input_, d): raise RuntimeError gradients = grad_scalar_output(output_, input_, d) - elif output_.shape[ - output_.ndim - 1] >= 2: # vector output ############################## + elif output_.shape[output_.ndim - + 1] >= 2: # vector output ############################## tensor_to_cat = [] for i, c in enumerate(components): c_output = output_.extract([c]) @@ -195,9 +195,9 @@ def laplacian(output_, input_, components=None, d=None, method="std"): result = LabelTensor.summation(tensors=to_append_tensors) result.labels = labels else: - result = torch.empty( - input_.shape[0], len(components), device=output_.device - ) + result = torch.empty(input_.shape[0], + len(components), + device=output_.device) labels = [None] * len(components) to_append_tensors = [None] * len(components) for idx, (ci, di) in enumerate(zip(components, d)): @@ -243,11 +243,8 @@ def advection(output_, input_, velocity_field, components=None, d=None): if components is None: components = output_.labels - tmp = ( - grad(output_, input_, components, d) - .reshape(-1, len(components), len(d)) - .transpose(0, 1) - ) + tmp = (grad(output_, input_, components, d).reshape(-1, len(components), + len(d)).transpose(0, 1)) tmp *= output_.extract(velocity_field) return tmp.sum(dim=2).T diff --git a/pina/problem/abstract_problem.py b/pina/problem/abstract_problem.py index 9e18dbaf9..6897fbb74 100644 --- a/pina/problem/abstract_problem.py +++ b/pina/problem/abstract_problem.py @@ -7,6 +7,7 @@ from ..collector import Collector from copy import deepcopy + class AbstractProblem(metaclass=ABCMeta): """ The abstract `AbstractProblem` class. All the class defining a PINA Problem @@ -55,7 +56,7 @@ def input_pts(self): if 'input_points' in v.keys(): to_return[k] = v['input_points'] return to_return - + def __deepcopy__(self, memo): """ Implements deepcopy for the @@ -116,9 +117,11 @@ def conditions(self): """ return self._conditions - def discretise_domain( - self, n, mode="random", variables="all", locations="all" - ): + def discretise_domain(self, + n, + mode="random", + variables="all", + locations="all"): """ Generate a set of points to span the `Location` of all the conditions of the problem. @@ -170,9 +173,10 @@ def discretise_domain( # check correct location if locations == "all": - locations = [name for name in self.conditions.keys() - if isinstance(self.conditions[name], - DomainEquationCondition)] + locations = [ + name for name in self.conditions.keys() + if isinstance(self.conditions[name], DomainEquationCondition) + ] else: if not isinstance(locations, (list)): locations = [locations] @@ -187,4 +191,4 @@ def discretise_domain( self.collector.store_sample_domains(n, mode, variables, locations) def add_points(self, new_points_dict): - self.collector.add_points(new_points_dict) \ No newline at end of file + self.collector.add_points(new_points_dict) diff --git a/pina/solvers/solver.py b/pina/solvers/solver.py index 2d9b4a57e..e00bc8d59 100644 --- a/pina/solvers/solver.py +++ b/pina/solvers/solver.py @@ -142,5 +142,4 @@ def _check_solver_consistency(self, problem): condition.condition_type): raise ValueError( f'{self.__name__} support only dose not support condition ' - f'{condition.condition_type}' - ) + f'{condition.condition_type}') diff --git a/pina/trainer.py b/pina/trainer.py index 1601d771b..58c66f67c 100644 --- a/pina/trainer.py +++ b/pina/trainer.py @@ -9,8 +9,13 @@ class Trainer(pytorch_lightning.Trainer): - def __init__(self, solver, batch_size=None, train_size=.7, test_size=.2, - eval_size=.1, **kwargs): + def __init__(self, + solver, + batch_size=None, + train_size=.7, + test_size=.2, + eval_size=.1, + **kwargs): """ PINA Trainer class for costumizing every aspect of training via flags. @@ -48,8 +53,7 @@ def _move_to_device(self): if hasattr(pb, "unknown_parameters"): for key in pb.unknown_parameters: pb.unknown_parameters[key] = torch.nn.Parameter( - pb.unknown_parameters[key].data.to(device) - ) + pb.unknown_parameters[key].data.to(device)) def _create_loader(self): """ @@ -58,14 +62,11 @@ def _create_loader(self): trainer dataloader, just call the method. """ if not self.solver.problem.collector.full: - error_message = '\n'.join( - [ - f"""{" " * 13} ---> Condition {key} {"sampled" if value else - "not sampled"}""" - for key, value in - self._solver.problem.collector._is_conditions_ready.items() - ] - ) + error_message = '\n'.join([ + f"""{" " * 13} ---> Condition {key} {"sampled" if value else + "not sampled"}""" for key, value in + self._solver.problem.collector._is_conditions_ready.items() + ]) raise RuntimeError('Cannot create Trainer if not all conditions ' 'are sampled. The Trainer got the following:\n' f'{error_message}') @@ -76,7 +77,8 @@ def _create_loader(self): device = devices[0] - data_module = PinaDataModule(problem=self.solver.problem, device=device, + data_module = PinaDataModule(problem=self.solver.problem, + device=device, train_size=self.train_size, test_size=self.test_size, val_size=self.eval_size) @@ -87,9 +89,9 @@ def train(self, **kwargs): """ Train the solver method. """ - return super().fit( - self.solver, train_dataloaders=self._loader, **kwargs - ) + return super().fit(self.solver, + train_dataloaders=self._loader, + **kwargs) @property def solver(self): diff --git a/pina/utils.py b/pina/utils.py index 282dd5332..84d3e7419 100644 --- a/pina/utils.py +++ b/pina/utils.py @@ -40,9 +40,9 @@ def check_consistency(object, object_instance, subclass=False): raise ValueError(f"{type(obj).__name__} must be {object_instance}.") -def number_parameters( - model, aggregate=True, only_trainable=True -): # TODO: check +def number_parameters(model, + aggregate=True, + only_trainable=True): # TODO: check """ Return the number of parameters of a given `model`. @@ -80,9 +80,8 @@ def merge_two_tensors(tensor1, tensor2): n2 = tensor2.shape[0] tensor1 = LabelTensor(tensor1.repeat(n2, 1), labels=tensor1.labels) - tensor2 = LabelTensor( - tensor2.repeat_interleave(n1, dim=0), labels=tensor2.labels - ) + tensor2 = LabelTensor(tensor2.repeat_interleave(n1, dim=0), + labels=tensor2.labels) return tensor1.append(tensor2) diff --git a/tests/test_condition.py b/tests/test_condition.py index f12979dc8..9165c3fa1 100644 --- a/tests/test_condition.py +++ b/tests/test_condition.py @@ -22,8 +22,11 @@ def test_init_inputoutput(): Condition(input_points=3., output_points='example') with pytest.raises(ValueError): Condition(input_points=example_domain, output_points=example_domain) + + test_init_inputoutput() + def test_init_domainfunc(): Condition(domain=example_domain, equation=FixedValue(0.0)) with pytest.raises(ValueError): diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 503ddd683..87fd9a15b 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -32,49 +32,49 @@ class Poisson(SpatialProblem): conditions = { 'gamma1': - Condition(domain=CartesianDomain({ - 'x': [0, 1], - 'y': 1 - }), - equation=FixedValue(0.0)), + Condition(domain=CartesianDomain({ + 'x': [0, 1], + 'y': 1 + }), + equation=FixedValue(0.0)), 'gamma2': - Condition(domain=CartesianDomain({ - 'x': [0, 1], - 'y': 0 - }), - equation=FixedValue(0.0)), + Condition(domain=CartesianDomain({ + 'x': [0, 1], + 'y': 0 + }), + equation=FixedValue(0.0)), 'gamma3': - Condition(domain=CartesianDomain({ - 'x': 1, - 'y': [0, 1] - }), - equation=FixedValue(0.0)), + Condition(domain=CartesianDomain({ + 'x': 1, + 'y': [0, 1] + }), + equation=FixedValue(0.0)), 'gamma4': - Condition(domain=CartesianDomain({ - 'x': 0, - 'y': [0, 1] - }), - equation=FixedValue(0.0)), + Condition(domain=CartesianDomain({ + 'x': 0, + 'y': [0, 1] + }), + equation=FixedValue(0.0)), 'D': - Condition(input_points=LabelTensor(torch.rand(size=(100, 2)), - ['x', 'y']), - equation=my_laplace), + Condition(input_points=LabelTensor(torch.rand(size=(100, 2)), + ['x', 'y']), + equation=my_laplace), 'data': - Condition(input_points=in_, output_points=out_), + Condition(input_points=in_, output_points=out_), 'data2': - Condition(input_points=in2_, output_points=out2_), + Condition(input_points=in2_, output_points=out2_), 'unsupervised': - Condition( - input_points=LabelTensor(torch.rand(size=(45, 2)), ['x', 'y']), - conditional_variables=LabelTensor(torch.ones(size=(45, 1)), - ['alpha']), - ), + Condition( + input_points=LabelTensor(torch.rand(size=(45, 2)), ['x', 'y']), + conditional_variables=LabelTensor(torch.ones(size=(45, 1)), + ['alpha']), + ), 'unsupervised2': - Condition( - input_points=LabelTensor(torch.rand(size=(90, 2)), ['x', 'y']), - conditional_variables=LabelTensor(torch.ones(size=(90, 1)), - ['alpha']), - ) + Condition( + input_points=LabelTensor(torch.rand(size=(90, 2)), ['x', 'y']), + conditional_variables=LabelTensor(torch.ones(size=(90, 1)), + ['alpha']), + ) } @@ -201,11 +201,12 @@ def test_loader(): class GraphProblem(AbstractProblem): output = LabelTensor(torch.rand((100, 3)), labels=['ux', 'uy', 'p']) - input = [Graph.build('radius', - nodes_coordinates=coordinates[i, :, :], - nodes_data=data[i, :, :], radius=0.2) - for i in - range(100)] + input = [ + Graph.build('radius', + nodes_coordinates=coordinates[i, :, :], + nodes_data=data[i, :, :], + radius=0.2) for i in range(100) + ] output_variables = ['u'] conditions = { diff --git a/tests/test_geometry/test_cartesian.py b/tests/test_geometry/test_cartesian.py index 65026c332..fc30757b6 100644 --- a/tests/test_geometry/test_cartesian.py +++ b/tests/test_geometry/test_cartesian.py @@ -3,6 +3,7 @@ from pina import LabelTensor from pina.domain import CartesianDomain + def test_constructor(): CartesianDomain({'x': [0, 1], 'y': [0, 1]})