From 982af4a04def1f75702c89f90d5afc106d1c2ced Mon Sep 17 00:00:00 2001 From: Dario Coscia <93731561+dario-coscia@users.noreply.github.com> Date: Tue, 20 Jun 2023 17:30:28 +0200 Subject: [PATCH] Solving problems related to Geometry (#118) * fix and add tests * minor fix on domain classes --------- Co-authored-by: Dario Coscia Co-authored-by: Dario Coscia --- pina/geometry/cartesian.py | 24 +++++++++------- pina/geometry/difference_domain.py | 6 ++-- pina/geometry/ellipsoid.py | 44 ++++++++++++++++++++---------- pina/geometry/union_domain.py | 20 ++++++++++---- tests/test_cartesian.py | 20 ++++++++++++-- tests/test_ellipsoid.py | 30 ++++++++++++++++++++ tests/test_union.py | 13 +++++++++ 7 files changed, 122 insertions(+), 35 deletions(-) create mode 100644 tests/test_ellipsoid.py diff --git a/pina/geometry/cartesian.py b/pina/geometry/cartesian.py index f8453ec..2bf0255 100644 --- a/pina/geometry/cartesian.py +++ b/pina/geometry/cartesian.py @@ -247,23 +247,27 @@ class CartesianDomain(Location): :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. + 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 bound[0] <= point.extract([variable]) <= bound[1]: - is_inside.append(True) + + if check_border: + check = bound[0] <= point.extract([variable]) <= bound[1] else: - is_inside.append(False) + check = bound[0] < point.extract([variable]) < bound[1] + + is_inside.append(check) return all(is_inside) - - # TODO check the fixed_ dimensions - # for variable, value in self.fixed_.items(): - # if variable in point.labels: - # if not (point.extract[variable] == value): - # return False diff --git a/pina/geometry/difference_domain.py b/pina/geometry/difference_domain.py index 7b99214..52eda1b 100644 --- a/pina/geometry/difference_domain.py +++ b/pina/geometry/difference_domain.py @@ -16,13 +16,13 @@ class Difference(Location): def sample(self, n, mode='random', variables='all'): """ """ - assert mode is 'random', 'Only random mode is implemented' + assert mode == 'random', 'Only random mode is implemented' samples = [] while len(samples) < n: sample = self.first.sample(1, 'random') if not self.second.is_inside(sample): - samples.append(sample.tolist()[0]) + samples.append(sample) import torch - return LabelTensor(torch.tensor(samples), labels=['x', 'y']) + return LabelTensor(torch.cat(samples), labels=['x', 'y']) diff --git a/pina/geometry/ellipsoid.py b/pina/geometry/ellipsoid.py index 7e7391d..8eb9885 100644 --- a/pina/geometry/ellipsoid.py +++ b/pina/geometry/ellipsoid.py @@ -2,6 +2,7 @@ import torch from .location import Location from ..label_tensor import LabelTensor +from ..utils import check_consistency class EllipsoidDomain(Location): @@ -39,9 +40,8 @@ class EllipsoidDomain(Location): self._centers = None self._axis = None - if not isinstance(sample_surface, bool): - raise ValueError('sample_surface must be bool type.') - + # checking consistency + check_consistency(sample_surface, bool) self._sample_surface = sample_surface for k, v in ellipsoid_dict.items(): @@ -81,9 +81,14 @@ class EllipsoidDomain(Location): return list(self.fixed_.keys()) + list(self.range_.keys()) def is_inside(self, point, check_border=False): - """Check if a point is inside the ellipsoid. + """Check if a point is inside the ellipsoid domain. - :param point: Point to be checked + .. 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. @@ -92,29 +97,40 @@ class EllipsoidDomain(Location): :rtype: bool """ - if not isinstance(point, LabelTensor): - raise ValueError('point expected to be LabelTensor.') - - # get axis ellipse + # 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, list(self._axis.keys())) + 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 - point_sq = point.pow(2) + # 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. + # 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): @@ -265,4 +281,4 @@ class EllipsoidDomain(Location): if mode in ['random']: return _Nd_sampler(n, mode, variables) else: - raise ValueError(f'mode={mode} is not valid.') + raise NotImplemented(f'mode={mode} is not implemented.') diff --git a/pina/geometry/union_domain.py b/pina/geometry/union_domain.py index f0d92cd..c77ea49 100644 --- a/pina/geometry/union_domain.py +++ b/pina/geometry/union_domain.py @@ -2,6 +2,7 @@ import torch from .location import Location from ..utils import check_consistency from ..label_tensor import LabelTensor +import random class Union(Location): @@ -87,7 +88,7 @@ class Union(Location): >>> ellipsoid2 = EllipsoidDomain({'x': [0, 2], 'y': [0, 2]}) # Create a union of the ellipsoid domains - >>> union = GeometryUnion([ellipsoid1, ellipsoid2]) + >>> union = Union([ellipsoid1, ellipsoid2]) >>> union.sample(n=1000) LabelTensor([[-0.2025, 0.0072], @@ -108,11 +109,18 @@ class Union(Location): num_points = n // len(self.geometries) # sample the points - for i, geometry in enumerate(self.geometries): - # add to sample total if remainder is not 0 - if i < remainder: - num_points += 1 - sampled_points.append(geometry.sample(num_points, mode, variables)) + # 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=[f'{i}' for i in self.variables]) diff --git a/tests/test_cartesian.py b/tests/test_cartesian.py index ac813c8..e92a4c2 100644 --- a/tests/test_cartesian.py +++ b/tests/test_cartesian.py @@ -12,10 +12,26 @@ def test_constructor(): CartesianDomain({'x': [0, 1], 'y': [0, 1]}) -def test_is_inside(): +def test_is_inside_check_border(): pt_1 = LabelTensor(torch.tensor([[0.5, 0.5]]), ['x', 'y']) pt_2 = LabelTensor(torch.tensor([[1.0, 0.5]]), ['x', 'y']) pt_3 = LabelTensor(torch.tensor([[1.5, 0.5]]), ['x', 'y']) domain = CartesianDomain({'x': [0, 1], 'y': [0, 1]}) for pt, exp_result in zip([pt_1, pt_2, pt_3], [True, True, False]): - assert domain.is_inside(pt) == exp_result \ No newline at end of file + assert domain.is_inside(pt, check_border=True) == exp_result + +def test_is_inside_not_check_border(): + pt_1 = LabelTensor(torch.tensor([[0.5, 0.5]]), ['x', 'y']) + pt_2 = LabelTensor(torch.tensor([[1.0, 0.5]]), ['x', 'y']) + pt_3 = LabelTensor(torch.tensor([[1.5, 0.5]]), ['x', 'y']) + domain = CartesianDomain({'x': [0, 1], 'y': [0, 1]}) + for pt, exp_result in zip([pt_1, pt_2, pt_3], [True, False, False]): + assert domain.is_inside(pt, check_border=False) == exp_result + +def test_is_inside_fixed_variables(): + pt_1 = LabelTensor(torch.tensor([[0.5, 0.5]]), ['x', 'y']) + pt_2 = LabelTensor(torch.tensor([[1.0, 0.5]]), ['x', 'y']) + pt_3 = LabelTensor(torch.tensor([[1.0, 1.5]]), ['x', 'y']) + domain = CartesianDomain({'x': 1, 'y': [0, 1]}) + for pt, exp_result in zip([pt_1, pt_2, pt_3], [False, True, False]): + assert domain.is_inside(pt, check_border=False) == exp_result \ No newline at end of file diff --git a/tests/test_ellipsoid.py b/tests/test_ellipsoid.py new file mode 100644 index 0000000..293cfd9 --- /dev/null +++ b/tests/test_ellipsoid.py @@ -0,0 +1,30 @@ +import torch +import pytest + +from pina import LabelTensor +from pina.geometry import EllipsoidDomain + + + +def test_constructor(): + EllipsoidDomain({'x': [0, 1], 'y': [0, 1]}) + EllipsoidDomain({'x': [0, 1], 'y':[0, 1]}, sample_surface=True) + + +def test_is_inside_sample_surface_false(): + domain = EllipsoidDomain({'x': [0, 1], 'y':[0, 1]}, sample_surface=False) + pt_1 = LabelTensor(torch.tensor([[0.5, 0.5]]), ['x', 'y']) + pt_2 = LabelTensor(torch.tensor([[1.0, 0.5]]), ['x', 'y']) + pt_3 = LabelTensor(torch.tensor([[1.5, 0.5]]), ['x', 'y']) + for pt, exp_result in zip([pt_1, pt_2, pt_3], [True, False, False]): + assert domain.is_inside(pt) == exp_result + for pt, exp_result in zip([pt_1, pt_2, pt_3], [True, True, False]): + assert domain.is_inside(pt, check_border=True) == exp_result + +def test_is_inside_sample_surface_true(): + domain = EllipsoidDomain({'x': [0, 1], 'y':[0, 1]}, sample_surface=True) + pt_1 = LabelTensor(torch.tensor([[0.5, 0.5]]), ['x', 'y']) + pt_2 = LabelTensor(torch.tensor([[1.0, 0.5]]), ['x', 'y']) + pt_3 = LabelTensor(torch.tensor([[1.5, 0.5]]), ['x', 'y']) + for pt, exp_result in zip([pt_1, pt_2, pt_3], [False, True, False]): + assert domain.is_inside(pt) == exp_result \ No newline at end of file diff --git a/tests/test_union.py b/tests/test_union.py index ebcc770..65e3087 100644 --- a/tests/test_union.py +++ b/tests/test_union.py @@ -44,3 +44,16 @@ def test_is_inside_EllipsoidDomain_CartesianDomain(): CartesianDomain({'x': [0.6, 1.5], 'y': [-2, 0]})]) assert domain.is_inside(pt_1) == True assert domain.is_inside(pt_2) == False + +def test_sample(): + n = 100 + domain = Union([EllipsoidDomain({'x': [-1, 1], 'y': [-1, 1]}), + CartesianDomain({'x': [-0.5, 0.5], 'y': [-0.5, 0.5]})]) + pts = domain.sample(n) + assert isinstance(pts, LabelTensor) + assert pts.shape[0] == n + + n = 105 + pts = domain.sample(n) + assert isinstance(pts, LabelTensor) + assert pts.shape[0] == n \ No newline at end of file