polycubetools 1.1.0__tar.gz → 1.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {polycubetools-1.1.0/src/polycubetools.egg-info → polycubetools-1.1.2}/PKG-INFO +1 -1
- {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools/__init__.py +2 -1
- polycubetools-1.1.2/src/polycubetools/errors.py +18 -0
- {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools/hull.py +56 -1
- {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools/utils.py +29 -13
- {polycubetools-1.1.0 → polycubetools-1.1.2/src/polycubetools.egg-info}/PKG-INFO +1 -1
- {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools.egg-info/SOURCES.txt +2 -0
- polycubetools-1.1.2/tests/test_hull.py +151 -0
- {polycubetools-1.1.0 → polycubetools-1.1.2}/tests/test_models.py +6 -7
- {polycubetools-1.1.0 → polycubetools-1.1.2}/LICENSE +0 -0
- {polycubetools-1.1.0 → polycubetools-1.1.2}/README.md +0 -0
- {polycubetools-1.1.0 → polycubetools-1.1.2}/pyproject.toml +0 -0
- {polycubetools-1.1.0 → polycubetools-1.1.2}/requirements.txt +0 -0
- {polycubetools-1.1.0 → polycubetools-1.1.2}/setup.cfg +0 -0
- {polycubetools-1.1.0 → polycubetools-1.1.2}/setup.py +0 -0
- {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools/data/heptacubes.json +0 -0
- {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools/data/hexacubes.json +0 -0
- {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools/data/pentacubes.json +0 -0
- {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools/grid.py +0 -0
- {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools/models.py +0 -0
- {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools/py.typed +0 -0
- {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools/solver.py +0 -0
- {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools.egg-info/dependency_links.txt +0 -0
- {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools.egg-info/requires.txt +0 -0
- {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools.egg-info/top_level.txt +0 -0
- {polycubetools-1.1.0 → polycubetools-1.1.2}/tests/test_grid.py +0 -0
- {polycubetools-1.1.0 → polycubetools-1.1.2}/tests/test_parallel_solver.py +0 -0
- {polycubetools-1.1.0 → polycubetools-1.1.2}/tests/test_solver.py +0 -0
|
@@ -10,9 +10,10 @@ Basic framework for working with polycubes in a 3-dimensional grid.
|
|
|
10
10
|
__author__ = "Team Polycube"
|
|
11
11
|
__title__ = "polycubetools"
|
|
12
12
|
__license__ = "MIT"
|
|
13
|
-
__version__ = "1.1.
|
|
13
|
+
__version__ = "1.1.2"
|
|
14
14
|
|
|
15
15
|
from . import utils as utils
|
|
16
|
+
from .errors import *
|
|
16
17
|
from .grid import *
|
|
17
18
|
from .hull import *
|
|
18
19
|
from .models import *
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
__all__ = (
|
|
4
|
+
"InvalidVolumeException",
|
|
5
|
+
"PolycubeException",
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PolycubeException(Exception):
|
|
10
|
+
"""Base exception for all errors raised by this library."""
|
|
11
|
+
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InvalidVolumeException(PolycubeException):
|
|
16
|
+
"""Exception raised when failing to compute a volume."""
|
|
17
|
+
|
|
18
|
+
pass
|
|
@@ -35,6 +35,11 @@ class AbstractHull(ABC):
|
|
|
35
35
|
"""Checks if the given coordinate is inside the hull."""
|
|
36
36
|
pass
|
|
37
37
|
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def inside_hull(self, pos: Coordinate) -> bool:
|
|
40
|
+
"""Checks if the given coordinate is inside the hull or in its inner part."""
|
|
41
|
+
pass
|
|
42
|
+
|
|
38
43
|
@abstractmethod
|
|
39
44
|
def in_inner_part(self, pos: Coordinate) -> bool:
|
|
40
45
|
"""Checks if the given coordinate is inside the inner part of the hull."""
|
|
@@ -67,6 +72,9 @@ class CoordinateHull(AbstractHull):
|
|
|
67
72
|
def in_hull(self, pos: Coordinate) -> bool:
|
|
68
73
|
return pos in self.coords
|
|
69
74
|
|
|
75
|
+
def inside_hull(self, pos: Coordinate) -> bool:
|
|
76
|
+
return pos in self.coords or self.in_inner_part(pos)
|
|
77
|
+
|
|
70
78
|
def in_inner_part(self, pos: Coordinate) -> bool:
|
|
71
79
|
raise NotImplementedError(
|
|
72
80
|
"Inner part is not defined for coordinate hulls (you can implement it in a subclass if needed)"
|
|
@@ -113,9 +121,56 @@ class CuboidHull(AbstractHull):
|
|
|
113
121
|
def in_hull(self, pos: Coordinate) -> bool:
|
|
114
122
|
minc, maxc = self.min_coord, self.max_coord
|
|
115
123
|
px, py, pz = pos.x, pos.y, pos.z
|
|
116
|
-
return
|
|
124
|
+
return (
|
|
125
|
+
px == minc.x or px == maxc.x or py == minc.y or py == maxc.y or pz == minc.z or pz == maxc.z
|
|
126
|
+
) and self.inside_hull(pos)
|
|
127
|
+
|
|
128
|
+
def inside_hull(self, pos: Coordinate) -> bool:
|
|
129
|
+
minc, maxc = self.min_coord, self.max_coord
|
|
130
|
+
px, py, pz = pos.x, pos.y, pos.z
|
|
131
|
+
return minc.x <= px <= maxc.x and minc.y <= py <= maxc.y and minc.z <= pz <= maxc.z
|
|
117
132
|
|
|
118
133
|
def in_inner_part(self, pos: Coordinate) -> bool:
|
|
119
134
|
minc, maxc = self.min_coord, self.max_coord
|
|
120
135
|
px, py, pz = pos.x, pos.y, pos.z
|
|
121
136
|
return minc.x < px < maxc.x and minc.y < py < maxc.y and minc.z < pz < maxc.z
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class CoordinateSetHull(AbstractHull):
|
|
140
|
+
def __init__(
|
|
141
|
+
self,
|
|
142
|
+
coords: frozenset[Coordinate],
|
|
143
|
+
inner_coords: frozenset[Coordinate],
|
|
144
|
+
frontier: set[Coordinate] | None = None,
|
|
145
|
+
) -> None:
|
|
146
|
+
super().__init__()
|
|
147
|
+
self.coords = coords
|
|
148
|
+
self.inner_coords = inner_coords
|
|
149
|
+
if frontier is None:
|
|
150
|
+
self.frontier = set(coords)
|
|
151
|
+
else:
|
|
152
|
+
self.frontier = frontier
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def from_snapshot(cls, snapshot: SNAPSHOT) -> Self:
|
|
156
|
+
coords = frozenset(Coordinate.from_tuple(t) for t in snapshot["coords"])
|
|
157
|
+
frontier = {Coordinate.from_tuple(t) for t in snapshot["frontier"]}
|
|
158
|
+
inner_coords = frozenset(Coordinate.from_tuple(t) for t in snapshot["inner_coords"])
|
|
159
|
+
|
|
160
|
+
return cls(coords, inner_coords, frontier)
|
|
161
|
+
|
|
162
|
+
def to_snapshot(self) -> SNAPSHOT:
|
|
163
|
+
return {
|
|
164
|
+
"coords": tuple(c.to_tuple() for c in self.coords),
|
|
165
|
+
"frontier": tuple(c.to_tuple() for c in self.frontier),
|
|
166
|
+
"inner_coords": tuple(c.to_tuple() for c in self.inner_coords),
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
def in_hull(self, pos: Coordinate) -> bool:
|
|
170
|
+
return pos in self.coords
|
|
171
|
+
|
|
172
|
+
def inside_hull(self, pos: Coordinate) -> bool:
|
|
173
|
+
return pos in self.coords or self.in_inner_part(pos)
|
|
174
|
+
|
|
175
|
+
def in_inner_part(self, pos: Coordinate) -> bool:
|
|
176
|
+
return pos in self.inner_coords
|
|
@@ -5,6 +5,7 @@ import json
|
|
|
5
5
|
import logging
|
|
6
6
|
from typing import Any, TYPE_CHECKING
|
|
7
7
|
|
|
8
|
+
from .errors import InvalidVolumeException
|
|
8
9
|
from .models import TWENTY_SIX_NEIGHBORHOOD_DIRECTIONS, Polycube, RotatedPolycube, Coordinate
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
@@ -12,12 +13,10 @@ if TYPE_CHECKING:
|
|
|
12
13
|
|
|
13
14
|
_logger = logging.getLogger(__name__)
|
|
14
15
|
|
|
15
|
-
NO_VOLUME = -1
|
|
16
|
-
MULTIPLE_VOLUME_COMPONENTS = -2
|
|
17
|
-
ERROR_IN_ALGORITHM = -3
|
|
18
|
-
|
|
19
16
|
__all__ = (
|
|
17
|
+
"collect_volume",
|
|
20
18
|
"compute_volume",
|
|
19
|
+
"find_connected_component",
|
|
21
20
|
"get_extreme_points",
|
|
22
21
|
"is_valid_fence",
|
|
23
22
|
"load_polycubes",
|
|
@@ -49,7 +48,7 @@ def _is_border_coordinate(coord: Coordinate, max_extrem_point: Coordinate, min_e
|
|
|
49
48
|
)
|
|
50
49
|
|
|
51
50
|
|
|
52
|
-
def
|
|
51
|
+
def find_connected_component(visible_coords: set[Coordinate]) -> set[Coordinate]:
|
|
53
52
|
"""Finds all coordinates connected to the first coordinate in the set using 26-neighborhood."""
|
|
54
53
|
start = visible_coords.pop()
|
|
55
54
|
stack = [start]
|
|
@@ -86,7 +85,7 @@ def get_extreme_points(coords: set[Coordinate]) -> tuple[Coordinate, Coordinate]
|
|
|
86
85
|
return Coordinate(x_max, y_max, z_max), Coordinate(x_min, y_min, z_min)
|
|
87
86
|
|
|
88
87
|
|
|
89
|
-
def
|
|
88
|
+
def collect_volume(coords: set[Coordinate]) -> set[Coordinate]:
|
|
90
89
|
"""Compute the volume from the hull formed by the coordinates"""
|
|
91
90
|
max_extreme_point, min_extreme_point = get_extreme_points(coords)
|
|
92
91
|
max_extreme_point = Coordinate(max_extreme_point.x + 1, max_extreme_point.y + 1, max_extreme_point.z + 1)
|
|
@@ -101,24 +100,41 @@ def compute_volume(coords: set[Coordinate]) -> int:
|
|
|
101
100
|
}
|
|
102
101
|
|
|
103
102
|
# the here popped coordinate is always outside the hull; this is because we added this outside new layer!
|
|
104
|
-
|
|
103
|
+
first_component = find_connected_component(visible_coords)
|
|
105
104
|
|
|
106
|
-
# we return false because there is no second component, meaning the shape has no volume
|
|
107
105
|
if not visible_coords:
|
|
108
106
|
_logger.warning("No volume found! Only one component detected.")
|
|
109
|
-
return
|
|
107
|
+
return set()
|
|
108
|
+
|
|
109
|
+
second_component = find_connected_component(visible_coords)
|
|
110
|
+
|
|
111
|
+
if min_extreme_point in second_component and max_extreme_point in second_component:
|
|
112
|
+
volume_cubes = first_component
|
|
113
|
+
elif min_extreme_point in first_component and max_extreme_point in first_component:
|
|
114
|
+
volume_cubes = second_component
|
|
115
|
+
else:
|
|
116
|
+
_logger.warning("Error. Couldn't find one outside component")
|
|
117
|
+
raise InvalidVolumeException("Couldn't find one outside component")
|
|
110
118
|
|
|
111
|
-
volume_cubes = _find_connected_component(visible_coords)
|
|
112
119
|
if visible_coords:
|
|
113
120
|
_logger.warning(
|
|
114
|
-
f"Found {len(volume_cubes)} as volume, but still {len(visible_coords)} unvisited cubes left.
|
|
121
|
+
f"Found {len(volume_cubes)} as volume, but still {len(visible_coords)} unvisited cubes left. Another enclosed area exists!"
|
|
115
122
|
)
|
|
116
|
-
|
|
123
|
+
raise InvalidVolumeException("Volume has multiple components")
|
|
117
124
|
|
|
118
125
|
if any(_is_border_coordinate(c, max_extreme_point, min_extreme_point) for c in volume_cubes):
|
|
119
126
|
_logger.warning("Volume has been found on the border! Bug in the validation.")
|
|
120
|
-
|
|
127
|
+
raise InvalidVolumeException("Volume has been found on the border")
|
|
121
128
|
|
|
129
|
+
return volume_cubes
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def compute_volume(coords: set[Coordinate]) -> int:
|
|
133
|
+
"""
|
|
134
|
+
Ruft die Funktion collect_volume auf, um alle Punkte der Volumen-Komponente zu erhalten.
|
|
135
|
+
Die Anzahl dieser Punkte ist das Volumen.
|
|
136
|
+
"""
|
|
137
|
+
volume_cubes = collect_volume(coords)
|
|
122
138
|
return len(volume_cubes)
|
|
123
139
|
|
|
124
140
|
|
|
@@ -4,6 +4,7 @@ pyproject.toml
|
|
|
4
4
|
requirements.txt
|
|
5
5
|
setup.py
|
|
6
6
|
src/polycubetools/__init__.py
|
|
7
|
+
src/polycubetools/errors.py
|
|
7
8
|
src/polycubetools/grid.py
|
|
8
9
|
src/polycubetools/hull.py
|
|
9
10
|
src/polycubetools/models.py
|
|
@@ -19,6 +20,7 @@ src/polycubetools/data/heptacubes.json
|
|
|
19
20
|
src/polycubetools/data/hexacubes.json
|
|
20
21
|
src/polycubetools/data/pentacubes.json
|
|
21
22
|
tests/test_grid.py
|
|
23
|
+
tests/test_hull.py
|
|
22
24
|
tests/test_models.py
|
|
23
25
|
tests/test_parallel_solver.py
|
|
24
26
|
tests/test_solver.py
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from polycubetools import Coordinate
|
|
6
|
+
from polycubetools.hull import CuboidHull, CoordinateHull
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestCuboidHullInHull:
|
|
10
|
+
"""Tests for CuboidHull.in_hull method."""
|
|
11
|
+
|
|
12
|
+
def test_in_hull_inside_point(self):
|
|
13
|
+
"""A point strictly inside the cuboid should be in the hull."""
|
|
14
|
+
hull = CuboidHull(Coordinate(0, 0, 0), Coordinate(4, 4, 4))
|
|
15
|
+
assert hull.inside_hull(Coordinate(2, 2, 2))
|
|
16
|
+
|
|
17
|
+
def test_in_hull_on_boundary(self):
|
|
18
|
+
"""A point on the boundary should be in the hull."""
|
|
19
|
+
hull = CuboidHull(Coordinate(0, 0, 0), Coordinate(4, 4, 4))
|
|
20
|
+
# On face
|
|
21
|
+
assert hull.inside_hull(Coordinate(0, 2, 2))
|
|
22
|
+
assert hull.inside_hull(Coordinate(4, 2, 2))
|
|
23
|
+
# On edge
|
|
24
|
+
assert hull.inside_hull(Coordinate(0, 0, 2))
|
|
25
|
+
# On corner
|
|
26
|
+
assert hull.inside_hull(Coordinate(0, 0, 0))
|
|
27
|
+
assert hull.inside_hull(Coordinate(4, 4, 4))
|
|
28
|
+
|
|
29
|
+
def test_in_hull_outside_point(self):
|
|
30
|
+
"""A point outside the cuboid should not be in the hull."""
|
|
31
|
+
hull = CuboidHull(Coordinate(0, 0, 0), Coordinate(4, 4, 4))
|
|
32
|
+
# Outside in x
|
|
33
|
+
assert not hull.inside_hull(Coordinate(-1, 2, 2))
|
|
34
|
+
assert not hull.inside_hull(Coordinate(5, 2, 2))
|
|
35
|
+
# Outside in y
|
|
36
|
+
assert not hull.inside_hull(Coordinate(2, -1, 2))
|
|
37
|
+
assert not hull.inside_hull(Coordinate(2, 5, 2))
|
|
38
|
+
# Outside in z
|
|
39
|
+
assert not hull.inside_hull(Coordinate(2, 2, -1))
|
|
40
|
+
assert not hull.inside_hull(Coordinate(2, 2, 5))
|
|
41
|
+
|
|
42
|
+
def test_in_hull_with_negative_coordinates(self):
|
|
43
|
+
"""Test hull with negative coordinate bounds."""
|
|
44
|
+
hull = CuboidHull(Coordinate(-3, -3, -3), Coordinate(3, 3, 3))
|
|
45
|
+
assert hull.inside_hull(Coordinate(0, 0, 0))
|
|
46
|
+
assert hull.inside_hull(Coordinate(-3, -3, -3))
|
|
47
|
+
assert hull.inside_hull(Coordinate(3, 3, 3))
|
|
48
|
+
assert not hull.inside_hull(Coordinate(-4, 0, 0))
|
|
49
|
+
|
|
50
|
+
def test_in_hull_single_point(self):
|
|
51
|
+
"""Test a hull that is a single point."""
|
|
52
|
+
hull = CuboidHull(Coordinate(1, 1, 1), Coordinate(1, 1, 1))
|
|
53
|
+
assert hull.inside_hull(Coordinate(1, 1, 1))
|
|
54
|
+
assert not hull.inside_hull(Coordinate(0, 1, 1))
|
|
55
|
+
assert not hull.inside_hull(Coordinate(2, 1, 1))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TestCuboidHullOnHull:
|
|
59
|
+
"""Tests for CuboidHull.on_hull method."""
|
|
60
|
+
|
|
61
|
+
def test_on_hull_corner(self):
|
|
62
|
+
"""Corner points should be on the hull."""
|
|
63
|
+
hull = CuboidHull(Coordinate(0, 0, 0), Coordinate(4, 4, 4))
|
|
64
|
+
assert hull.in_hull(Coordinate(0, 0, 0))
|
|
65
|
+
assert hull.in_hull(Coordinate(4, 4, 4))
|
|
66
|
+
assert hull.in_hull(Coordinate(0, 4, 4))
|
|
67
|
+
assert hull.in_hull(Coordinate(4, 0, 0))
|
|
68
|
+
|
|
69
|
+
def test_on_hull_edge(self):
|
|
70
|
+
"""Edge points should be on the hull."""
|
|
71
|
+
hull = CuboidHull(Coordinate(0, 0, 0), Coordinate(4, 4, 4))
|
|
72
|
+
assert hull.in_hull(Coordinate(0, 0, 2))
|
|
73
|
+
assert hull.in_hull(Coordinate(4, 4, 2))
|
|
74
|
+
assert hull.in_hull(Coordinate(2, 0, 0))
|
|
75
|
+
|
|
76
|
+
def test_on_hull_face(self):
|
|
77
|
+
"""Points on faces should be on the hull."""
|
|
78
|
+
hull = CuboidHull(Coordinate(0, 0, 0), Coordinate(4, 4, 4))
|
|
79
|
+
# x-face
|
|
80
|
+
assert hull.in_hull(Coordinate(0, 2, 2))
|
|
81
|
+
assert hull.in_hull(Coordinate(4, 2, 2))
|
|
82
|
+
# y-face
|
|
83
|
+
assert hull.in_hull(Coordinate(2, 0, 2))
|
|
84
|
+
assert hull.in_hull(Coordinate(2, 4, 2))
|
|
85
|
+
# z-face
|
|
86
|
+
assert hull.in_hull(Coordinate(2, 2, 0))
|
|
87
|
+
assert hull.in_hull(Coordinate(2, 2, 4))
|
|
88
|
+
|
|
89
|
+
def test_on_hull_interior_point(self):
|
|
90
|
+
"""Interior points should NOT be on the hull."""
|
|
91
|
+
hull = CuboidHull(Coordinate(0, 0, 0), Coordinate(4, 4, 4))
|
|
92
|
+
assert not hull.in_hull(Coordinate(2, 2, 2))
|
|
93
|
+
assert not hull.in_hull(Coordinate(1, 1, 1))
|
|
94
|
+
assert not hull.in_hull(Coordinate(3, 3, 3))
|
|
95
|
+
|
|
96
|
+
def test_on_hull_outside_point(self):
|
|
97
|
+
"""Points outside the hull should NOT be on the hull."""
|
|
98
|
+
hull = CuboidHull(Coordinate(0, 0, 0), Coordinate(4, 4, 4))
|
|
99
|
+
assert not hull.in_hull(Coordinate(-1, 2, 2))
|
|
100
|
+
assert not hull.in_hull(Coordinate(5, 2, 2))
|
|
101
|
+
# Even if one coordinate matches a boundary
|
|
102
|
+
assert not hull.in_hull(Coordinate(0, 5, 2))
|
|
103
|
+
|
|
104
|
+
def test_on_hull_with_negative_coordinates(self):
|
|
105
|
+
"""Test on_hull with negative coordinate bounds."""
|
|
106
|
+
hull = CuboidHull(Coordinate(-2, -2, -2), Coordinate(2, 2, 2))
|
|
107
|
+
# Corners
|
|
108
|
+
assert hull.in_hull(Coordinate(-2, -2, -2))
|
|
109
|
+
assert hull.in_hull(Coordinate(2, 2, 2))
|
|
110
|
+
# Face
|
|
111
|
+
assert hull.in_hull(Coordinate(-2, 0, 0))
|
|
112
|
+
# Interior
|
|
113
|
+
assert not hull.in_hull(Coordinate(0, 0, 0))
|
|
114
|
+
|
|
115
|
+
def test_on_hull_single_point(self):
|
|
116
|
+
"""A single-point hull should be on the hull."""
|
|
117
|
+
hull = CuboidHull(Coordinate(1, 1, 1), Coordinate(1, 1, 1))
|
|
118
|
+
assert hull.in_hull(Coordinate(1, 1, 1))
|
|
119
|
+
assert not hull.in_hull(Coordinate(0, 1, 1))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class TestCoordinateHullInHull:
|
|
123
|
+
"""Tests for CoordinateHull.in_hull method."""
|
|
124
|
+
|
|
125
|
+
def test_in_hull_present_coordinate(self):
|
|
126
|
+
"""Coordinates in the set should be in the hull."""
|
|
127
|
+
coords = frozenset({
|
|
128
|
+
Coordinate(0, 0, 0),
|
|
129
|
+
Coordinate(1, 0, 0),
|
|
130
|
+
Coordinate(0, 1, 0),
|
|
131
|
+
})
|
|
132
|
+
hull = CoordinateHull(coords)
|
|
133
|
+
assert hull.in_hull(Coordinate(0, 0, 0))
|
|
134
|
+
assert hull.in_hull(Coordinate(1, 0, 0))
|
|
135
|
+
assert hull.in_hull(Coordinate(0, 1, 0))
|
|
136
|
+
|
|
137
|
+
def test_in_hull_absent_coordinate(self):
|
|
138
|
+
"""Coordinates not in the set should not be in the hull."""
|
|
139
|
+
coords = frozenset({
|
|
140
|
+
Coordinate(0, 0, 0),
|
|
141
|
+
Coordinate(1, 0, 0),
|
|
142
|
+
})
|
|
143
|
+
hull = CoordinateHull(coords)
|
|
144
|
+
assert not hull.in_hull(Coordinate(2, 0, 0))
|
|
145
|
+
assert not hull.in_hull(Coordinate(0, 1, 0))
|
|
146
|
+
assert not hull.in_hull(Coordinate(-1, 0, 0))
|
|
147
|
+
|
|
148
|
+
def test_in_hull_empty_hull(self):
|
|
149
|
+
"""Empty hull should contain no coordinates."""
|
|
150
|
+
hull = CoordinateHull(frozenset())
|
|
151
|
+
assert not hull.in_hull(Coordinate(0, 0, 0))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
|
-
from polycubetools import Coordinate, RotatedPolycube, utils
|
|
4
|
+
from polycubetools import Coordinate, RotatedPolycube, utils, InvalidVolumeException
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def test_validation_of_compactness():
|
|
@@ -19,13 +19,13 @@ def test_validation_of_compactness():
|
|
|
19
19
|
# remove side cube
|
|
20
20
|
cube.remove(Coordinate(2, 1, 1))
|
|
21
21
|
volume = utils.compute_volume(cube)
|
|
22
|
-
assert volume ==
|
|
22
|
+
assert volume == 0
|
|
23
23
|
cube.add(Coordinate(2, 1, 1))
|
|
24
24
|
|
|
25
25
|
# remove corner cube
|
|
26
26
|
cube.remove(Coordinate(0, 0, 0))
|
|
27
27
|
volume = utils.compute_volume(cube)
|
|
28
|
-
assert volume ==
|
|
28
|
+
assert volume == 0
|
|
29
29
|
cube.add(Coordinate(0, 0, 0))
|
|
30
30
|
|
|
31
31
|
cube.add(Coordinate(3, 1, 1))
|
|
@@ -35,7 +35,7 @@ def test_validation_of_compactness():
|
|
|
35
35
|
# now it has no volume
|
|
36
36
|
cube.add(Coordinate(1, 1, 1))
|
|
37
37
|
volume = utils.compute_volume(cube)
|
|
38
|
-
assert volume ==
|
|
38
|
+
assert volume == 0
|
|
39
39
|
cube.remove(Coordinate(1, 1, 1))
|
|
40
40
|
|
|
41
41
|
cuboid = {
|
|
@@ -49,9 +49,8 @@ def test_validation_of_compactness():
|
|
|
49
49
|
assert volume == 2
|
|
50
50
|
|
|
51
51
|
two_hulls = cube.union(cuboid)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
assert volume == utils.MULTIPLE_VOLUME_COMPONENTS
|
|
52
|
+
with pytest.raises(InvalidVolumeException, match="Volume has multiple components"):
|
|
53
|
+
volume = utils.compute_volume(two_hulls)
|
|
55
54
|
|
|
56
55
|
two_hulls_with_one_empty_hull = two_hulls
|
|
57
56
|
two_hulls_with_one_empty_hull.add(Coordinate(1, 1, 1))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|