supervised working
This commit is contained in:
21
pina/domain/__init__.py
Normal file
21
pina/domain/__init__.py
Normal 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
279
pina/domain/cartesian.py
Normal 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)
|
||||
103
pina/domain/difference_domain.py
Normal file
103
pina/domain/difference_domain.py
Normal 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)
|
||||
31
pina/domain/domain_interface.py
Normal file
31
pina/domain/domain_interface.py
Normal 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
292
pina/domain/ellipsoid.py
Normal 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.")
|
||||
108
pina/domain/exclusion_domain.py
Normal file
108
pina/domain/exclusion_domain.py
Normal 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)
|
||||
109
pina/domain/intersection_domain.py
Normal file
109
pina/domain/intersection_domain.py
Normal 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)
|
||||
68
pina/domain/operation_interface.py
Normal file
68
pina/domain/operation_interface.py
Normal 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
248
pina/domain/simplex.py
Normal 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
104
pina/domain/union_domain.py
Normal 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)
|
||||
Reference in New Issue
Block a user