supervised working

This commit is contained in:
Nicola Demo
2024-08-08 16:19:52 +02:00
parent 5245a0b68c
commit 9d9c2aa23e
61 changed files with 375 additions and 262 deletions

21
pina/domain/__init__.py Normal file
View File

@@ -0,0 +1,21 @@
__all__ = [
"DomainInterface",
"CartesianDomain",
"EllipsoidDomain",
"Union",
"Intersection",
"Exclusion",
"Difference",
"OperationInterface",
"SimplexDomain",
]
from .domain_interface import DomainInterface
from .cartesian import CartesianDomain
from .ellipsoid import EllipsoidDomain
from .exclusion_domain import Exclusion
from .intersection_domain import Intersection
from .union_domain import Union
from .difference_domain import Difference
from .operation_interface import OperationInterface
from .simplex import SimplexDomain

279
pina/domain/cartesian.py Normal file
View File

@@ -0,0 +1,279 @@
import torch
from .domain_interface import DomainInterface
from ..label_tensor import LabelTensor
from ..utils import torch_lhs, chebyshev_roots
class CartesianDomain(DomainInterface):
"""PINA implementation of Hypercube domain."""
def __init__(self, cartesian_dict):
"""
:param cartesian_dict: A dictionary with dict-key a string representing
the input variables for the pinn, and dict-value a list with
the domain extrema.
:type cartesian_dict: dict
:Example:
>>> spatial_domain = CartesianDomain({'x': [0, 1], 'y': [0, 1]})
"""
self.fixed_ = {}
self.range_ = {}
for k, v in cartesian_dict.items():
if isinstance(v, (int, float)):
self.fixed_[k] = v
elif isinstance(v, (list, tuple)) and len(v) == 2:
self.range_[k] = v
else:
raise TypeError
@property
def variables(self):
"""Spatial variables.
:return: Spatial variables defined in ``__init__()``
:rtype: list[str]
"""
return sorted(list(self.fixed_.keys()) + list(self.range_.keys()))
def update(self, new_domain):
"""Adding new dimensions on the ``CartesianDomain``
:param CartesianDomain new_domain: A new ``CartesianDomain`` object to merge
:Example:
>>> spatial_domain = CartesianDomain({'x': [0, 1], 'y': [0, 1]})
>>> spatial_domain.variables
['x', 'y']
>>> spatial_domain_2 = CartesianDomain({'z': [3, 4], 'w': [0, 1]})
>>> spatial_domain.update(spatial_domain_2)
>>> spatial_domain.variables
['x', 'y', 'z', 'w']
"""
self.fixed_.update(new_domain.fixed_)
self.range_.update(new_domain.range_)
def _sample_range(self, n, mode, bounds):
"""Rescale the samples to the correct bounds
:param n: Number of points to sample, see Note below
for reference.
:type n: int
:param mode: Mode for sampling, defaults to ``random``.
Available modes include: random sampling, ``random``;
latin hypercube sampling, ``latin`` or ``lh``;
chebyshev sampling, ``chebyshev``; grid sampling ``grid``.
:type mode: str
:param bounds: Bounds to rescale the samples.
:type bounds: torch.Tensor
:return: Rescaled sample points.
:rtype: torch.Tensor
"""
dim = bounds.shape[0]
if mode in ["chebyshev", "grid"] and dim != 1:
raise RuntimeError("Something wrong in Span...")
if mode == "random":
pts = torch.rand(size=(n, dim))
elif mode == "chebyshev":
pts = chebyshev_roots(n).mul(0.5).add(0.5).reshape(-1, 1)
elif mode == "grid":
pts = torch.linspace(0, 1, n).reshape(-1, 1)
# elif mode == 'lh' or mode == 'latin':
elif mode in ["lh", "latin"]:
pts = torch_lhs(n, dim)
pts *= bounds[:, 1] - bounds[:, 0]
pts += bounds[:, 0]
return pts
def sample(self, n, mode="random", variables="all"):
"""Sample routine.
:param n: Number of points to sample, see Note below
for reference.
:type n: int
:param mode: Mode for sampling, defaults to ``random``.
Available modes include: random sampling, ``random``;
latin hypercube sampling, ``latin`` or ``lh``;
chebyshev sampling, ``chebyshev``; grid sampling ``grid``.
:type mode: str
:param variables: pinn variable to be sampled, defaults to ``all``.
:type variables: str | list[str]
:return: Returns ``LabelTensor`` of n sampled points.
:rtype: LabelTensor
.. note::
The total number of points sampled in case of multiple variables
is not ``n``, and it depends on the chosen ``mode``. If ``mode`` is
'grid' or ``chebyshev``, the points are sampled independentely
across the variables and the results crossed together, i.e. the
final number of points is ``n`` to the power of the number of
variables. If 'mode' is 'random', ``lh`` or ``latin``, the variables
are sampled all together, and the final number of points
.. warning::
The extrema values of Span are always sampled only for ``grid`` mode.
:Example:
>>> spatial_domain = Span({'x': [0, 1], 'y': [0, 1]})
>>> spatial_domain.sample(n=4, mode='random')
tensor([[0.0108, 0.7643],
[0.4477, 0.8015],
[0.2063, 0.8087],
[0.8735, 0.6349]])
>>> spatial_domain.sample(n=4, mode='grid')
tensor([[0.0000, 0.0000],
[0.3333, 0.0000],
[0.6667, 0.0000],
[1.0000, 0.0000],
[0.0000, 0.3333],
[0.3333, 0.3333],
[0.6667, 0.3333],
[1.0000, 0.3333],
[0.0000, 0.6667],
[0.3333, 0.6667],
[0.6667, 0.6667],
[1.0000, 0.6667],
[0.0000, 1.0000],
[0.3333, 1.0000],
[0.6667, 1.0000],
[1.0000, 1.0000]])
"""
def _1d_sampler(n, mode, variables):
"""Sample independentely the variables and cross the results"""
tmp = []
for variable in variables:
if variable in self.range_.keys():
bound = torch.tensor([self.range_[variable]])
pts_variable = self._sample_range(n, mode, bound)
pts_variable = pts_variable.as_subclass(LabelTensor)
pts_variable.labels = [variable]
tmp.append(pts_variable)
result = tmp[0]
for i in tmp[1:]:
result = result.append(i, mode="cross")
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 = pts_variable.as_subclass(LabelTensor)
pts_variable.labels = [variable]
result = result.append(pts_variable, mode="std")
return result
def _Nd_sampler(n, mode, variables):
"""Sample all the variables together
:param n: Number of points to sample.
:type n: int
:param mode: Mode for sampling, defaults to ``random``.
Available modes include: random sampling, ``random``;
latin hypercube sampling, ``latin`` or ``lh``;
chebyshev sampling, ``chebyshev``; grid sampling ``grid``.
:type mode: str.
:param variables: pinn variable to be sampled, defaults to ``all``.
:type variables: str or list[str].
:return: Sample points.
:rtype: list[torch.Tensor]
"""
pairs = [(k, v) for k, v in self.range_.items() if k in variables]
keys, values = map(list, zip(*pairs))
bounds = torch.tensor(values)
result = self._sample_range(n, mode, bounds)
result = result.as_subclass(LabelTensor)
result.labels = keys
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 = pts_variable.as_subclass(LabelTensor)
pts_variable.labels = [variable]
result = result.append(pts_variable, mode="std")
return result
def _single_points_sample(n, variables):
"""Sample a single point in one dimension.
:param n: Number of points to sample.
:type n: int
:param variables: Variables to sample from.
:type variables: list[str]
:return: Sample points.
:rtype: list[torch.Tensor]
"""
tmp = []
for variable in variables:
if variable in self.fixed_.keys():
value = self.fixed_[variable]
pts_variable = torch.tensor([[value]]).repeat(n, 1)
pts_variable = pts_variable.as_subclass(LabelTensor)
pts_variable.labels = [variable]
tmp.append(pts_variable)
result = tmp[0]
for i in tmp[1:]:
result = result.append(i, mode="std")
return result
if variables == "all":
variables = self.variables
elif isinstance(variables, (list, tuple)):
variables = sorted(variables)
if self.fixed_ and (not self.range_):
return _single_points_sample(n, variables)
if mode in ["grid", "chebyshev"]:
return _1d_sampler(n, mode, variables).extract(variables)
elif mode in ["random", "lh", "latin"]:
return _Nd_sampler(n, mode, variables).extract(variables)
else:
raise ValueError(f"mode={mode} is not valid.")
def is_inside(self, point, check_border=False):
"""Check if a point is inside the ellipsoid.
:param point: Point to be checked
:type point: LabelTensor
:param check_border: Check if the point is also on the frontier
of the hypercube, default ``False``.
:type check_border: bool
:return: Returning ``True`` if the point is inside, ``False`` otherwise.
:rtype: bool
"""
is_inside = []
# check fixed variables
for variable, value in self.fixed_.items():
if variable in point.labels:
is_inside.append(point.extract([variable]) == value)
# check not fixed variables
for variable, bound in self.range_.items():
if variable in point.labels:
if check_border:
check = bound[0] <= point.extract([variable]) <= bound[1]
else:
check = bound[0] < point.extract([variable]) < bound[1]
is_inside.append(check)
return all(is_inside)

View File

@@ -0,0 +1,103 @@
"""Module for Difference class."""
import torch
from .operation_interface import OperationInterface
from ..label_tensor import LabelTensor
class Difference(OperationInterface):
def __init__(self, geometries):
r"""
PINA implementation of Difference of Domains.
Given two sets :math:`A` and :math:`B` then the
domain difference is defined as:
.. math::
A - B = \{x \mid x \in A \land x \not\in B\},
with :math:`x` a point in :math:`\mathbb{R}^N` and :math:`N`
the dimension of the geometry space.
:param list geometries: A list of geometries from ``pina.geometry``
such as ``EllipsoidDomain`` or ``CartesianDomain``. The first
geometry in the list is the geometry from which points are
sampled. The rest of the geometries are the geometries that
are excluded from the first geometry to find the difference.
:Example:
>>> # Create two ellipsoid domains
>>> ellipsoid1 = EllipsoidDomain({'x': [-1, 1], 'y': [-1, 1]})
>>> ellipsoid2 = EllipsoidDomain({'x': [0, 2], 'y': [0, 2]})
>>> # Create a Difference of the ellipsoid domains
>>> difference = Difference([ellipsoid1, ellipsoid2])
"""
super().__init__(geometries)
def is_inside(self, point, check_border=False):
"""
Check if a point is inside the ``Difference`` domain.
:param point: Point to be checked.
:type point: torch.Tensor
:param bool check_border: If ``True``, the border is considered inside.
:return: ``True`` if the point is inside the Exclusion domain, ``False`` otherwise.
:rtype: bool
"""
for geometry in self.geometries[1:]:
if geometry.is_inside(point):
return False
return self.geometries[0].is_inside(point, check_border)
def sample(self, n, mode="random", variables="all"):
"""
Sample routine for ``Difference`` domain.
:param int n: Number of points to sample in the shape.
:param str mode: Mode for sampling, defaults to ``random``. Available modes include: ``random``.
:param variables: Variables to be sampled, defaults to ``all``.
:type variables: str | list[str]
:return: Returns ``LabelTensor`` of n sampled points.
:rtype: LabelTensor
:Example:
>>> # Create two Cartesian domains
>>> cartesian1 = CartesianDomain({'x': [0, 2], 'y': [0, 2]})
>>> cartesian2 = CartesianDomain({'x': [1, 3], 'y': [1, 3]})
>>> # Create a Difference of the ellipsoid domains
>>> difference = Difference([cartesian1, cartesian2])
>>> # Sampling
>>> difference.sample(n=5)
LabelTensor([[0.8400, 0.9179],
[0.9154, 0.5769],
[1.7403, 0.4835],
[0.9545, 1.2851],
[1.3726, 0.9831]])
>>> len(difference.sample(n=5)
5
"""
if mode != "random":
raise NotImplementedError(
f"{mode} is not a valid mode for sampling."
)
sampled = []
# sample the points
while len(sampled) < n:
# get sample point from first geometry
point = self.geometries[0].sample(1, mode, variables)
is_inside = False
# check if point is inside any other geometry
for geometry in self.geometries[1:]:
# if point is inside any other geometry, break
if geometry.is_inside(point):
is_inside = True
break
# if point is not inside any other geometry, add to sampled
if not is_inside:
sampled.append(point)
return LabelTensor(torch.cat(sampled), labels=self.variables)

View File

@@ -0,0 +1,31 @@
"""Module for the DomainInterface class."""
from abc import ABCMeta, abstractmethod
class DomainInterface(metaclass=ABCMeta):
"""
Abstract Location class.
Any geometry entity should inherit from this class.
"""
@abstractmethod
def sample(self):
"""
Abstract method for sampling a point from the location. To be
implemented in the child class.
"""
pass
@abstractmethod
def is_inside(self, point, check_border=False):
"""
Abstract method for checking if a point is inside the location. To be
implemented in the child class.
:param torch.Tensor point: A tensor point to be checked.
:param bool check_border: A boolean that determines whether the border
of the location is considered checked to be considered inside or
not. Defaults to ``False``.
"""
pass

292
pina/domain/ellipsoid.py Normal file
View File

@@ -0,0 +1,292 @@
import torch
from .domain_interface import DomainInterface
from ..label_tensor import LabelTensor
from ..utils import check_consistency
class EllipsoidDomain(DomainInterface):
"""PINA implementation of Ellipsoid domain."""
def __init__(self, ellipsoid_dict, sample_surface=False):
"""PINA implementation of Ellipsoid domain.
:param ellipsoid_dict: A dictionary with dict-key a string representing
the input variables for the pinn, and dict-value a list with
the domain extrema.
:type ellipsoid_dict: dict
:param sample_surface: A variable for choosing sample strategies. If
``sample_surface=True`` only samples on the ellipsoid surface
frontier are taken. If ``sample_surface=False`` only samples on
the ellipsoid interior are taken, defaults to ``False``.
:type sample_surface: bool
.. warning::
Sampling for dimensions greater or equal to 10 could result
in a shrinking of the ellipsoid, which degrades the quality
of the samples. For dimensions higher than 10, other algorithms
for sampling should be used, such as: Dezert, Jean, and Christian
Musso. "An efficient method for generating points uniformly
distributed in hyperellipsoids." Proceedings of the Workshop on
Estimation, Tracking and Fusion: A Tribute to Yaakov Bar-Shalom.
Vol. 7. No. 8. 2001.
:Example:
>>> spatial_domain = Ellipsoid({'x':[-1, 1], 'y':[-1,1]})
"""
self.fixed_ = {}
self.range_ = {}
self._centers = None
self._axis = None
# checking consistency
check_consistency(sample_surface, bool)
self._sample_surface = sample_surface
for k, v in ellipsoid_dict.items():
if isinstance(v, (int, float)):
self.fixed_[k] = v
elif isinstance(v, (list, tuple)) and len(v) == 2:
self.range_[k] = v
else:
raise TypeError
# 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)
# get the ellipsoid center
normal_basis = torch.eye(len(list_dict_vals))
centers = torch.diag(normal_basis * tmp.mean(axis=1))
# get the ellipsoid axis
ellipsoid_axis = (tmp - centers.reshape(-1, 1))[:, -1]
# save elipsoid axis and centers as dict
self._centers = dict(zip(self.range_.keys(), centers.tolist()))
self._axis = dict(zip(self.range_.keys(), ellipsoid_axis.tolist()))
@property
def variables(self):
"""Spatial variables.
:return: Spatial variables defined in '__init__()'
:rtype: list[str]
"""
return sorted(list(self.fixed_.keys()) + list(self.range_.keys()))
def is_inside(self, point, check_border=False):
"""Check if a point is inside the ellipsoid domain.
.. note::
When ``sample_surface`` in the ``__init()__``
is set to ``True``, then the method only checks
points on the surface, and not inside the domain.
:param point: Point to be checked.
:type point: LabelTensor
:param check_border: Check if the point is also on the frontier
of the ellipsoid, default ``False``.
:type check_border: bool
:return: Returning True if the point is inside, ``False`` otherwise.
:rtype: bool
"""
# small check that point is labeltensor
check_consistency(point, LabelTensor)
# get axis ellipse as tensors
list_dict_vals = list(self._axis.values())
tmp = torch.tensor(list_dict_vals, dtype=torch.float)
ax_sq = LabelTensor(tmp.reshape(1, -1) ** 2, self.variables)
# get centers ellipse as tensors
list_dict_vals = list(self._centers.values())
tmp = torch.tensor(list_dict_vals, dtype=torch.float)
centers = LabelTensor(tmp.reshape(1, -1), self.variables)
if not all([i in ax_sq.labels for i in point.labels]):
raise ValueError(
"point labels different from constructor"
f" dictionary labels. Got {point.labels},"
f" expected {ax_sq.labels}."
)
# point square + shift center
point_sq = (point - centers).pow(2)
point_sq.labels = point.labels
# calculate ellispoid equation
eqn = torch.sum(point_sq.extract(ax_sq.labels) / ax_sq) - 1.0
# if we have sampled only the surface, we check that the
# point is inside the surface border only
if self._sample_surface:
return torch.allclose(eqn, torch.zeros_like(eqn))
# otherwise we check the ellipse
if check_border:
return bool(eqn <= 0)
return bool(eqn < 0)
def _sample_range(self, n, mode, variables):
"""Rescale the samples to the correct bounds.
:param n: Number of points to sample in the ellipsoid.
:type n: int
:param mode: Mode for sampling, defaults to ``random``.
Available modes include: random sampling, ``random``.
:type mode: str, optional
:param variables: Variables to be rescaled in the samples.
:type variables: torch.Tensor
:return: Rescaled sample points.
:rtype: torch.Tensor
"""
# =============== For Developers ================ #
#
# The sampling startegy used is fairly simple.
# For all `mode`s first we sample from the unit
# sphere and then we scale and shift according
# to self._axis.values() and self._centers.values().
#
# =============================================== #
# get dimension
dim = len(variables)
# get values center
pairs_center = [
(k, v) for k, v in self._centers.items() if k in variables
]
_, values_center = map(list, zip(*pairs_center))
values_center = torch.tensor(values_center)
# get values axis
pairs_axis = [(k, v) for k, v in self._axis.items() if k in variables]
_, values_axis = map(list, zip(*pairs_axis))
values_axis = torch.tensor(values_axis)
# Sample in the unit sphere
if mode == "random":
# 1. Sample n points from the surface of a unit sphere
# 2. Scale each dimension using torch.rand()
# (a random number between 0-1) so that it lies within
# the sphere, only if self._sample_surface=False
# 3. Multiply with self._axis.values() to make it ellipsoid
# 4. Shift the mean of the ellipse by adding self._centers.values()
# step 1.
pts = torch.randn(size=(n, dim))
pts = pts / torch.linalg.norm(pts, axis=-1).view((n, 1))
if not self._sample_surface: # step 2.
scale = torch.rand((n, 1))
pts = pts * scale
# step 3. and 4.
pts *= values_axis
pts += values_center
return pts
def sample(self, n, mode="random", variables="all"):
"""Sample routine.
:param int n: Number of points to sample in the shape.
:param str mode: Mode for sampling, defaults to ``random``. Available modes include: ``random``.
:param variables: Variables to be sampled, defaults to ``all``.
:type variables: str | list[str]
:return: Returns ``LabelTensor`` of n sampled points.
:rtype: LabelTensor
:Example:
>>> elips = Ellipsoid({'x':[1, 0], 'y':1})
>>> elips.sample(n=6)
tensor([[0.4872, 1.0000],
[0.2977, 1.0000],
[0.0422, 1.0000],
[0.6431, 1.0000],
[0.7272, 1.0000],
[0.8326, 1.0000]])
"""
def _Nd_sampler(n, mode, variables):
"""Sample all the variables together
:param n: Number of points to sample.
:type n: int
:param mode: Mode for sampling, defaults to ``random``.
Available modes include: random sampling, ``random``;
latin hypercube sampling, 'latin' or 'lh';
chebyshev sampling, 'chebyshev'; grid sampling 'grid'.
:type mode: str, optional.
:param variables: pinn variable to be sampled, defaults to ``all``.
:type variables: str or list[str], optional.
:return: Sample points.
:rtype: list[torch.Tensor]
"""
pairs = [(k, v) for k, v in self.range_.items() if k in variables]
keys, _ = map(list, zip(*pairs))
result = self._sample_range(n, mode, keys)
result = result.as_subclass(LabelTensor)
result.labels = keys
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 = pts_variable.as_subclass(LabelTensor)
pts_variable.labels = [variable]
result = result.append(pts_variable, mode="std")
return result
def _single_points_sample(n, variables):
"""Sample a single point in one dimension.
:param n: Number of points to sample.
:type n: int
:param variables: Variables to sample from.
:type variables: list[str]
:return: Sample points.
:rtype: list[torch.Tensor]
"""
tmp = []
for variable in variables:
if variable in self.fixed_.keys():
value = self.fixed_[variable]
pts_variable = torch.tensor([[value]]).repeat(n, 1)
pts_variable = pts_variable.as_subclass(LabelTensor)
pts_variable.labels = [variable]
tmp.append(pts_variable)
result = tmp[0]
for i in tmp[1:]:
result = result.append(i, mode="std")
return result
if variables == "all":
variables = self.variables
elif isinstance(variables, (list, tuple)):
variables = sorted(variables)
if self.fixed_ and (not self.range_):
return _single_points_sample(n, variables).extract(variables)
if variables == "all":
variables = self.variables
if mode in ["random"]:
return _Nd_sampler(n, mode, variables).extract(variables)
else:
raise NotImplementedError(f"mode={mode} is not implemented.")

View File

@@ -0,0 +1,108 @@
"""Module for Exclusion class. """
import torch
from ..label_tensor import LabelTensor
import random
from .operation_interface import OperationInterface
class Exclusion(OperationInterface):
def __init__(self, geometries):
r"""
PINA implementation of Exclusion of Domains.
Given two sets :math:`A` and :math:`B` then the
domain difference is defined as:
.. math::
A \setminus B = \{x \mid x \in A \land x \in B \land x \not\in (A \lor B)\},
with :math:`x` a point in :math:`\mathbb{R}^N` and :math:`N`
the dimension of the geometry space.
:param list geometries: A list of geometries from ``pina.geometry``
such as ``EllipsoidDomain`` or ``CartesianDomain``.
:Example:
>>> # Create two ellipsoid domains
>>> ellipsoid1 = EllipsoidDomain({'x': [-1, 1], 'y': [-1, 1]})
>>> ellipsoid2 = EllipsoidDomain({'x': [0, 2], 'y': [0, 2]})
>>> # Create a Exclusion of the ellipsoid domains
>>> exclusion = Exclusion([ellipsoid1, ellipsoid2])
"""
super().__init__(geometries)
def is_inside(self, point, check_border=False):
"""
Check if a point is inside the ``Exclusion`` domain.
:param point: Point to be checked.
:type point: torch.Tensor
:param bool check_border: If ``True``, the border is considered inside.
:return: ``True`` if the point is inside the Exclusion domain, ``False`` otherwise.
:rtype: bool
"""
flag = 0
for geometry in self.geometries:
if geometry.is_inside(point, check_border):
flag += 1
return flag == 1
def sample(self, n, mode="random", variables="all"):
"""
Sample routine for ``Exclusion`` domain.
:param int n: Number of points to sample in the shape.
:param str mode: Mode for sampling, defaults to ``random``. Available modes include: ``random``.
:param variables: Variables to be sampled, defaults to ``all``.
:type variables: str | list[str]
:return: Returns ``LabelTensor`` of n sampled points.
:rtype: LabelTensor
:Example:
>>> # Create two Cartesian domains
>>> cartesian1 = CartesianDomain({'x': [0, 2], 'y': [0, 2]})
>>> cartesian2 = CartesianDomain({'x': [1, 3], 'y': [1, 3]})
>>> # Create a Exclusion of the ellipsoid domains
>>> Exclusion = Exclusion([cartesian1, cartesian2])
>>> # Sample
>>> Exclusion.sample(n=5)
LabelTensor([[2.4187, 1.5792],
[2.7456, 2.3868],
[2.3830, 1.7037],
[0.8636, 1.8453],
[0.1978, 0.3526]])
>>> len(Exclusion.sample(n=5)
5
"""
if mode != "random":
raise NotImplementedError(
f"{mode} is not a valid mode for sampling."
)
sampled = []
# calculate the number of points to sample for each geometry and the remainder.
remainder = n % len(self.geometries)
num_points = n // len(self.geometries)
# sample the points
# NB. geometries as shuffled since if we sample
# multiple times just one point, we would end
# up sampling only from the first geometry.
iter_ = random.sample(self.geometries, len(self.geometries))
for i, geometry in enumerate(iter_):
sampled_points = []
# int(i < remainder) is one only if we have a remainder
# different than zero. Notice that len(geometries) is
# always smaller than remaider.
# makes sure point is uniquely inside 1 shape.
while len(sampled_points) < (num_points + int(i < remainder)):
sample = geometry.sample(1, mode, variables)
# if not self.is_inside(sample) --> will be the intersection
if self.is_inside(sample):
sampled_points.append(sample)
sampled += sampled_points
return LabelTensor(torch.cat(sampled), labels=self.variables)

View File

@@ -0,0 +1,109 @@
"""Module for Intersection class. """
import torch
from ..label_tensor import LabelTensor
from .operation_interface import OperationInterface
import random
class Intersection(OperationInterface):
def __init__(self, geometries):
r"""
PINA implementation of Intersection of Domains.
Given two sets :math:`A` and :math:`B` then the
domain difference is defined as:
.. math::
A \cap B = \{x \mid x \in A \land x \in B\},
with :math:`x` a point in :math:`\mathbb{R}^N` and :math:`N`
the dimension of the geometry space.
:param list geometries: A list of geometries from ``pina.geometry``
such as ``EllipsoidDomain`` or ``CartesianDomain``. The intersection
will be taken between all the geometries in the list. The resulting
geometry will be the intersection of all the geometries in the list.
:Example:
>>> # Create two ellipsoid domains
>>> ellipsoid1 = EllipsoidDomain({'x': [-1, 1], 'y': [-1, 1]})
>>> ellipsoid2 = EllipsoidDomain({'x': [0, 2], 'y': [0, 2]})
>>> # Create a Intersection of the ellipsoid domains
>>> intersection = Intersection([ellipsoid1, ellipsoid2])
"""
super().__init__(geometries)
def is_inside(self, point, check_border=False):
"""
Check if a point is inside the ``Intersection`` domain.
:param point: Point to be checked.
:type point: torch.Tensor
:param bool check_border: If ``True``, the border is considered inside.
:return: ``True`` if the point is inside the Intersection domain, ``False`` otherwise.
:rtype: bool
"""
flag = 0
for geometry in self.geometries:
if geometry.is_inside(point, check_border):
flag += 1
return flag == len(self.geometries)
def sample(self, n, mode="random", variables="all"):
"""
Sample routine for ``Intersection`` domain.
:param int n: Number of points to sample in the shape.
:param str mode: Mode for sampling, defaults to ``random``. Available modes include: ``random``.
:param variables: Variables to be sampled, defaults to ``all``.
:type variables: str | list[str]
:return: Returns ``LabelTensor`` of n sampled points.
:rtype: LabelTensor
:Example:
>>> # Create two Cartesian domains
>>> cartesian1 = CartesianDomain({'x': [0, 2], 'y': [0, 2]})
>>> cartesian2 = CartesianDomain({'x': [1, 3], 'y': [1, 3]})
>>> # Create a Intersection of the ellipsoid domains
>>> intersection = Intersection([cartesian1, cartesian2])
>>> # Sample
>>> intersection.sample(n=5)
LabelTensor([[1.7697, 1.8654],
[1.2841, 1.1208],
[1.7289, 1.9843],
[1.3332, 1.2448],
[1.9902, 1.4458]])
>>> len(intersection.sample(n=5)
5
"""
if mode != "random":
raise NotImplementedError(
f"{mode} is not a valid mode for sampling."
)
sampled = []
# calculate the number of points to sample for each geometry and the remainder.
remainder = n % len(self.geometries)
num_points = n // len(self.geometries)
# sample the points
# NB. geometries as shuffled since if we sample
# multiple times just one point, we would end
# up sampling only from the first geometry.
iter_ = random.sample(self.geometries, len(self.geometries))
for i, geometry in enumerate(iter_):
sampled_points = []
# int(i < remainder) is one only if we have a remainder
# different than zero. Notice that len(geometries) is
# always smaller than remaider.
# makes sure point is uniquely inside 1 shape.
while len(sampled_points) < (num_points + int(i < remainder)):
sample = geometry.sample(1, mode, variables)
if self.is_inside(sample):
sampled_points.append(sample)
sampled += sampled_points
return LabelTensor(torch.cat(sampled), labels=self.variables)

View File

@@ -0,0 +1,68 @@
""" Module for OperationInterface class. """
from .domain_interface import DomainInterface
from ..utils import check_consistency
from abc import ABCMeta, abstractmethod
class OperationInterface(DomainInterface, metaclass=ABCMeta):
def __init__(self, geometries):
"""
Abstract set operation class. Any geometry operation entity must inherit from this class.
:param list geometries: A list of geometries from ``pina.geometry``
such as ``EllipsoidDomain`` or ``CartesianDomain``.
"""
# check consistency geometries
check_consistency(geometries, DomainInterface)
# check we are passing always different
# geometries with the same labels.
self._check_dimensions(geometries)
# assign geometries
self._geometries = geometries
@property
def geometries(self):
"""
The geometries to perform set operation.
"""
return self._geometries
@property
def variables(self):
"""
Spatial variables of the domain.
:return: All the variables defined in ``__init__`` in order.
:rtype: list[str]
"""
return self.geometries[0].variables
@abstractmethod
def is_inside(self, point, check_border=False):
"""
Check if a point is inside the resulting domain after
a set operation is applied.
:param point: Point to be checked.
:type point: torch.Tensor
:param bool check_border: If ``True``, the border is considered inside.
:return: ``True`` if the point is inside the Intersection domain, ``False`` otherwise.
:rtype: bool
"""
pass
def _check_dimensions(self, geometries):
"""Check if the dimensions of the geometries are consistent.
:param geometries: Geometries to be checked.
:type geometries: list[Location]
"""
for geometry in geometries:
if geometry.variables != geometries[0].variables:
raise NotImplementedError(
f"The geometries need to have same dimensions and labels."
)

248
pina/domain/simplex.py Normal file
View File

@@ -0,0 +1,248 @@
import torch
from .domain_interface import DomainInterface
from pina.domain import CartesianDomain
from ..label_tensor import LabelTensor
from ..utils import check_consistency
class SimplexDomain(DomainInterface):
"""PINA implementation of a Simplex."""
def __init__(self, simplex_matrix, sample_surface=False):
"""
:param simplex_matrix: A matrix of LabelTensor objects representing
a vertex of the simplex (a tensor), and the coordinates of the
point (a list of labels).
:type simplex_matrix: list[LabelTensor]
:param sample_surface: A variable for choosing sample strategies. If
``sample_surface=True`` only samples on the Simplex surface
frontier are taken. If ``sample_surface=False``, no such criteria
is followed.
:type sample_surface: bool
.. warning::
Sampling for dimensions greater or equal to 10 could result
in a shrinking of the simplex, which degrades the quality
of the samples. For dimensions higher than 10, other algorithms
for sampling should be used.
:Example:
>>> spatial_domain = SimplexDomain(
[
LabelTensor(torch.tensor([[0, 0]]), labels=["x", "y"]),
LabelTensor(torch.tensor([[1, 1]]), labels=["x", "y"]),
LabelTensor(torch.tensor([[0, 2]]), labels=["x", "y"]),
], sample_surface = True
)
"""
# check consistency of sample_surface as bool
check_consistency(sample_surface, bool)
self._sample_surface = sample_surface
# check consistency of simplex_matrix as list or tuple
check_consistency([simplex_matrix], (list, tuple))
# check everything within simplex_matrix is a LabelTensor
check_consistency(simplex_matrix, LabelTensor)
# check consistency of labels
matrix_labels = simplex_matrix[0].labels
if not all(vertex.labels == matrix_labels for vertex in simplex_matrix):
raise ValueError(f"Labels don't match.")
# check consistency dimensions
dim_simplex = len(matrix_labels)
if len(simplex_matrix) != dim_simplex + 1:
raise ValueError(
"An n-dimensional simplex is composed by n + 1 tensors of dimension n."
)
# creating vertices matrix
self._vertices_matrix = LabelTensor.vstack(simplex_matrix)
# creating basis vectors for simplex
# self._vectors_shifted = (
# (self._vertices_matrix.T)[:, :-1] - (self._vertices_matrix.T)[:, None, -1]
# ) ### TODO: Remove after checking
vert = self._vertices_matrix
self._vectors_shifted = (vert[:-1] - vert[-1]).T
# build cartesian_bound
self._cartesian_bound = self._build_cartesian(self._vertices_matrix)
@property
def variables(self):
return sorted(self._vertices_matrix.labels)
def _build_cartesian(self, vertices):
"""
Build Cartesian border for Simplex domain to be used in sampling.
:param vertex_matrix: matrix of vertices
:type vertices: list[list]
:return: Cartesian border for triangular domain
:rtype: CartesianDomain
"""
span_dict = {}
for i, coord in enumerate(self.variables):
sorted_vertices = sorted(vertices, key=lambda vertex: vertex[i])
# respective coord bounded by the lowest and highest values
span_dict[coord] = [
float(sorted_vertices[0][i]),
float(sorted_vertices[-1][i]),
]
return CartesianDomain(span_dict)
def is_inside(self, point, check_border=False):
"""
Check if a point is inside the simplex.
Uses the algorithm described involving barycentric coordinates:
https://en.wikipedia.org/wiki/Barycentric_coordinate_system.
:param point: Point to be checked.
:type point: LabelTensor
:param check_border: Check if the point is also on the frontier
of the simplex, default ``False``.
:type check_border: bool
:return: Returning ``True`` if the point is inside, ``False`` otherwise.
:rtype: bool
.. note::
When ``sample_surface`` in the ``__init()__``
is set to ``True``, then the method only checks
points on the surface, and not inside the domain.
"""
if not all(label in self.variables for label in point.labels):
raise ValueError(
"Point labels different from constructor"
f" dictionary labels. Got {point.labels},"
f" expected {self.variables}."
)
point_shift = point - self._vertices_matrix[-1]
point_shift = point_shift.tensor.reshape(-1, 1)
# compute barycentric coordinates
lambda_ = torch.linalg.solve(
self._vectors_shifted * 1.0, point_shift * 1.0
)
lambda_1 = 1.0 - torch.sum(lambda_)
lambdas = torch.vstack([lambda_, lambda_1])
# perform checks
if not check_border:
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))
)
def _sample_interior_randomly(self, n, variables):
"""
Randomly sample points inside a simplex of arbitrary
dimension, without the boundary.
:param int n: Number of points to sample in the shape.
:param variables: pinn variable to be sampled, defaults to ``all``.
:type variables: str or list[str], optional
:return: Returns tensor of n sampled points.
:rtype: torch.Tensor
"""
# =============== For Developers ================ #
#
# The sampling startegy used is fairly simple.
# First we sample a random vector from the hypercube
# which contains the simplex. Then, if the point
# sampled is inside the simplex, we add it as a valid
# one.
#
# =============================================== #
sampled_points = []
while len(sampled_points) < n:
sampled_point = self._cartesian_bound.sample(
n=1, mode="random", variables=variables
)
if self.is_inside(sampled_point, self._sample_surface):
sampled_points.append(sampled_point)
return torch.cat(sampled_points, dim=0)
def _sample_boundary_randomly(self, n):
"""
Randomly sample points on the boundary of a simplex
of arbitrary dimensions.
:param int n: Number of points to sample in the shape.
:return: Returns tensor of n sampled points
:rtype: torch.Tensor
"""
# =============== For Developers ================ #
#
# The sampling startegy used is fairly simple.
# We first sample the lambdas in [0, 1] domain,
# we then set to zero only one lambda, and normalize.
# Finally, we compute the matrix product between the
# lamdas and the vertices matrix.
#
# =============================================== #
sampled_points = []
while len(sampled_points) < n:
# extract number of vertices
number_of_vertices = self._vertices_matrix.shape[0]
# extract idx lambda to set to zero randomly
idx_lambda = torch.randint(
low=0, high=number_of_vertices, size=(1,)
)
# build lambda vector
# 1. sampling [1, 2)
lambdas = torch.rand((number_of_vertices, 1))
# 2. setting lambdas[idx_lambda] to 0
lambdas[idx_lambda] = 0
# 3. normalize
lambdas /= lambdas.sum()
# 4. compute dot product
sampled_points.append(self._vertices_matrix.T @ lambdas)
return torch.cat(sampled_points, dim=1).T
def sample(self, n, mode="random", variables="all"):
"""
Sample n points from Simplex domain.
:param int n: Number of points to sample in the shape.
:param str mode: Mode for sampling, defaults to ``random``. Available modes include: ``random``.
:param variables: Variables to be sampled, defaults to ``all``.
:type variables: str | list[str]
:return: Returns ``LabelTensor`` of n sampled points.
:rtype: LabelTensor
.. warning::
When ``sample_surface = True`` in the initialization, all
the variables are sampled, despite passing different once
in ``variables``.
"""
if variables == "all":
variables = self.variables
elif isinstance(variables, (list, tuple)):
variables = sorted(variables)
if mode in ["random"]:
if self._sample_surface:
sample_pts = self._sample_boundary_randomly(n)
else:
sample_pts = self._sample_interior_randomly(n, variables)
else:
raise NotImplementedError(f"mode={mode} is not implemented.")
return LabelTensor(sample_pts, labels=self.variables)

104
pina/domain/union_domain.py Normal file
View File

@@ -0,0 +1,104 @@
"""Module for Union class. """
import torch
from .operation_interface import OperationInterface
from ..label_tensor import LabelTensor
import random
class Union(OperationInterface):
def __init__(self, geometries):
r"""
PINA implementation of Unions of Domains.
Given two sets :math:`A` and :math:`B` then the
domain difference is defined as:
.. math::
A \cup B = \{x \mid x \in A \lor x \in B\},
with :math:`x` a point in :math:`\mathbb{R}^N` and :math:`N`
the dimension of the geometry space.
:param list geometries: A list of geometries from ``pina.geometry``
such as ``EllipsoidDomain`` or ``CartesianDomain``.
:Example:
>>> # Create two ellipsoid domains
>>> ellipsoid1 = EllipsoidDomain({'x': [-1, 1], 'y': [-1, 1]})
>>> ellipsoid2 = EllipsoidDomain({'x': [0, 2], 'y': [0, 2]})
>>> # Create a union of the ellipsoid domains
>>> union = GeometryUnion([ellipsoid1, ellipsoid2])
"""
super().__init__(geometries)
def is_inside(self, point, check_border=False):
"""
Check if a point is inside the ``Union`` domain.
:param point: Point to be checked.
:type point: LabelTensor
:param check_border: Check if the point is also on the frontier
of the ellipsoid, default ``False``.
:type check_border: bool
:return: Returning ``True`` if the point is inside, ``False`` otherwise.
:rtype: bool
"""
for geometry in self.geometries:
if geometry.is_inside(point, check_border):
return True
return False
def sample(self, n, mode="random", variables="all"):
"""
Sample routine for ``Union`` domain.
:param int n: Number of points to sample in the shape.
:param str mode: Mode for sampling, defaults to ``random``. Available modes include: ``random``.
:param variables: Variables to be sampled, defaults to ``all``.
:type variables: str | list[str]
:return: Returns ``LabelTensor`` of n sampled points.
:rtype: LabelTensor
:Example:
>>> # Create two ellipsoid domains
>>> cartesian1 = CartesianDomain({'x': [0, 2], 'y': [0, 2]})
>>> cartesian2 = CartesianDomain({'x': [1, 3], 'y': [1, 3]})
>>> # Create a union of the ellipsoid domains
>>> union = Union([cartesian1, cartesian2])
>>> # Sample
>>> union.sample(n=5)
LabelTensor([[1.2128, 2.1991],
[1.3530, 2.4317],
[2.2562, 1.6605],
[0.8451, 1.9878],
[1.8623, 0.7102]])
>>> len(union.sample(n=5)
5
"""
sampled_points = []
# calculate the number of points to sample for each geometry and the remainder
remainder = n % len(self.geometries)
num_points = n // len(self.geometries)
# sample the points
# NB. geometries as shuffled since if we sample
# multiple times just one point, we would end
# up sampling only from the first geometry.
iter_ = random.sample(self.geometries, len(self.geometries))
for i, geometry in enumerate(iter_):
# int(i < remainder) is one only if we have a remainder
# different than zero. Notice that len(geometries) is
# always smaller than remaider.
sampled_points.append(
geometry.sample(
num_points + int(i < remainder), mode, variables
)
)
# in case number of sampled points is smaller than the number of geometries
if len(sampled_points) >= n:
break
return LabelTensor(torch.cat(sampled_points), labels=self.variables)