Geometry Operations Enhancement (#122)

* updating exclusion domain
- update sample/ is_inside
- create tests

* difference fixes
- random iteration list for sample

* created Intersection

* created a Difference domain

* unittest

* docstrings and minor fixes

* Refacotring Geometries
- added OperationInterface
- redid test cases
- edited Union, Intersect, Exclusion, and Difference
to inherit from OperationInterface
- simplified Union, Intersect, Exclusion, and Difference

* rm lighting logs

---------

Co-authored-by: Dario Coscia <dariocoscia@cli-10-110-16-239.WIFIeduroamSTUD.units.it>
This commit is contained in:
Kush
2023-07-04 12:08:47 +02:00
committed by Nicola Demo
parent 44cf800491
commit 2d0256a179
13 changed files with 534 additions and 80 deletions

View File

@@ -2,11 +2,18 @@ __all__ = [
'Location',
'CartesianDomain',
'EllipsoidDomain',
'Union'
'Union',
'Intersection',
'Exclusion',
'Difference',
'OperationInterface'
]
from .location import Location
from .cartesian import CartesianDomain
from .ellipsoid import EllipsoidDomain
from .difference_domain import Difference
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

View File

@@ -1,28 +1,88 @@
"""Module for Location class."""
from .location import Location
import torch
from .exclusion_domain import Exclusion
from .operation_interface import OperationInterface
from ..label_tensor import LabelTensor
class Difference(Location):
"""
"""
class Difference(OperationInterface):
""" PINA implementation of Difference of Domains."""
def __init__(self, first, second):
def __init__(self, geometries):
""" PINA implementation of Difference of Domains.
self.first = first
self.second = second
: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):
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'):
"""
"""
assert mode == 'random', 'Only random mode is implemented'
"""Sample routine for difference domain.
samples = []
while len(samples) < n:
sample = self.first.sample(1, 'random')
if not self.second.is_inside(sample):
samples.append(sample)
:param n: Number of points to sample in the shape.
:type n: int
:param mode: Mode for sampling, defaults to 'random'.
Available modes include: random sampling, 'random'.
:type mode: str, optional
:param variables: pinn variable to be sampled, defaults to 'all'.
:type variables: str or list[str], optional
import torch
return LabelTensor(torch.cat(samples), labels=['x', 'y'])
: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])
>>> 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,102 @@
"""Module for Location class."""
import torch
from .location import Location
from ..utils import check_consistency
from ..label_tensor import LabelTensor
import random
from .operation_interface import OperationInterface
class Exclusion(OperationInterface):
""" PINA implementation of Exclusion of Domains."""
def __init__(self, geometries):
""" PINA implementation of Exclusion of Domains.
: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 n: Number of points to sample in the shape.
:type n: int
:param mode: Mode for sampling, defaults to 'random'.
Available modes include: random sampling, 'random'.
:type mode: str, optional
:param variables: pinn variable to be sampled, defaults to 'all'.
:type variables: str or list[str], optional
: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])
>>> 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,102 @@
"""Module for Location class."""
import torch
from .exclusion_domain import Exclusion
from ..label_tensor import LabelTensor
from .operation_interface import OperationInterface
import random
class Intersection(OperationInterface):
""" PINA implementation of Intersection of Domains."""
def __init__(self, geometries):
""" PINA implementation of Intersection of Domains.
: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 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 == len(self.geometries)
def sample(self, n, mode='random', variables='all'):
"""Sample routine for intersection domain.
:param n: Number of points to sample in the shape.
:type n: int
:param mode: Mode for sampling, defaults to 'random'.
Available modes include: random sampling, 'random'.
:type mode: str, optional
:param variables: pinn variable to be sampled, defaults to 'all'.
:type variables: str or list[str], optional
: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])
>>> 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

@@ -14,4 +14,17 @@ class Location(metaclass=ABCMeta):
Abstract method for sampling a point from the location. To be
implemented in the child class.
"""
pass
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 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

View File

@@ -0,0 +1,53 @@
import torch
from .location import Location
from ..utils import check_consistency
from ..label_tensor import LabelTensor
from abc import ABCMeta, abstractmethod
import random
class OperationInterface(Location, metaclass=ABCMeta):
def __init__(self, geometries):
"""
Abstract 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, Location)
# 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."""
return self._geometries
@property
def variables(self):
"""
Spatial variables.
:return: All the variables defined in ``__init__`` in order.
:rtype: list[str]
"""
return self.geometries[0].variables
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.')

View File

@@ -1,11 +1,12 @@
import torch
from .location import Location
from .operation_interface import OperationInterface
from ..utils import check_consistency
from ..label_tensor import LabelTensor
import random
class Union(Location):
class Union(OperationInterface):
""" PINA implementation of Unions of Domains."""
def __init__(self, geometries):
@@ -23,37 +24,7 @@ class Union(Location):
>>> union = GeometryUnion([ellipsoid1, ellipsoid2])
"""
super().__init__()
# union checks
check_consistency(geometries, Location)
self._check_union_dimensions(geometries)
# assign geometries
self._geometries = geometries
@property
def geometries(self):
"""
The geometries."""
return self._geometries
@property
def variables(self):
"""
Spatial variables.
:return: All the spatial variables defined in '__init__()' in order.
:rtype: list[str]
"""
all_variables = []
seen_variables = set()
for geometry in self.geometries:
for variable in geometry.variables:
if variable not in seen_variables:
all_variables.append(variable)
seen_variables.add(variable)
return all_variables
super().__init__(geometries)
def is_inside(self, point, check_border=False):
"""Check if a point is inside the union domain.
@@ -72,7 +43,7 @@ class Union(Location):
return False
def sample(self, n, mode='random', variables='all'):
"""Sample routine.
"""Sample routine for union domain.
:param n: Number of points to sample in the shape.
:type n: int
@@ -84,23 +55,21 @@ class Union(Location):
:Example:
# Create two ellipsoid domains
>>> ellipsoid1 = EllipsoidDomain({'x': [-1, 1], 'y': [-1, 1]})
>>> ellipsoid2 = EllipsoidDomain({'x': [0, 2], 'y': [0, 2]})
>>> 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([ellipsoid1, ellipsoid2])
>>> union = Union([cartesian1, cartesian2])
>>> union.sample(n=1000)
LabelTensor([[-0.2025, 0.0072],
[ 0.0358, 0.5748],
[ 0.5083, 0.0482],
...,
[ 0.5857, 0.9279],
[ 1.1496, 1.7339],
[ 0.7650, 1.0469]])
>>> 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=1000)
1000
>>> len(union.sample(n=5)
5
"""
sampled_points = []
@@ -122,15 +91,4 @@ class Union(Location):
if len(sampled_points) >= n:
break
return LabelTensor(torch.cat(sampled_points), labels=[f'{i}' for i in self.variables])
def _check_union_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 be the same dimensions. {geometry.variables} is not equal to {geometries[0].variables}')
return LabelTensor(torch.cat(sampled_points), labels=self.variables)