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.
Files changed (28) hide show
  1. {polycubetools-1.1.0/src/polycubetools.egg-info → polycubetools-1.1.2}/PKG-INFO +1 -1
  2. {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools/__init__.py +2 -1
  3. polycubetools-1.1.2/src/polycubetools/errors.py +18 -0
  4. {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools/hull.py +56 -1
  5. {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools/utils.py +29 -13
  6. {polycubetools-1.1.0 → polycubetools-1.1.2/src/polycubetools.egg-info}/PKG-INFO +1 -1
  7. {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools.egg-info/SOURCES.txt +2 -0
  8. polycubetools-1.1.2/tests/test_hull.py +151 -0
  9. {polycubetools-1.1.0 → polycubetools-1.1.2}/tests/test_models.py +6 -7
  10. {polycubetools-1.1.0 → polycubetools-1.1.2}/LICENSE +0 -0
  11. {polycubetools-1.1.0 → polycubetools-1.1.2}/README.md +0 -0
  12. {polycubetools-1.1.0 → polycubetools-1.1.2}/pyproject.toml +0 -0
  13. {polycubetools-1.1.0 → polycubetools-1.1.2}/requirements.txt +0 -0
  14. {polycubetools-1.1.0 → polycubetools-1.1.2}/setup.cfg +0 -0
  15. {polycubetools-1.1.0 → polycubetools-1.1.2}/setup.py +0 -0
  16. {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools/data/heptacubes.json +0 -0
  17. {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools/data/hexacubes.json +0 -0
  18. {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools/data/pentacubes.json +0 -0
  19. {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools/grid.py +0 -0
  20. {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools/models.py +0 -0
  21. {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools/py.typed +0 -0
  22. {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools/solver.py +0 -0
  23. {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools.egg-info/dependency_links.txt +0 -0
  24. {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools.egg-info/requires.txt +0 -0
  25. {polycubetools-1.1.0 → polycubetools-1.1.2}/src/polycubetools.egg-info/top_level.txt +0 -0
  26. {polycubetools-1.1.0 → polycubetools-1.1.2}/tests/test_grid.py +0 -0
  27. {polycubetools-1.1.0 → polycubetools-1.1.2}/tests/test_parallel_solver.py +0 -0
  28. {polycubetools-1.1.0 → polycubetools-1.1.2}/tests/test_solver.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polycubetools
3
- Version: 1.1.0
3
+ Version: 1.1.2
4
4
  Summary: Basic framework for working with polycubes in a 3-dimensional grid.
5
5
  Author: Team Polycube
6
6
  Classifier: Development Status :: 3 - Alpha
@@ -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.0"
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 px == minc.x or px == maxc.x or py == minc.y or py == maxc.y or pz == minc.z or pz == maxc.z
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 _find_connected_component(visible_coords: set[Coordinate]) -> set[Coordinate]:
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 compute_volume(coords: set[Coordinate]) -> int:
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
- _find_connected_component(visible_coords)
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 NO_VOLUME
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. Exists another enclosed area!"
121
+ f"Found {len(volume_cubes)} as volume, but still {len(visible_coords)} unvisited cubes left. Another enclosed area exists!"
115
122
  )
116
- return MULTIPLE_VOLUME_COMPONENTS
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
- return ERROR_IN_ALGORITHM
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polycubetools
3
- Version: 1.1.0
3
+ Version: 1.1.2
4
4
  Summary: Basic framework for working with polycubes in a 3-dimensional grid.
5
5
  Author: Team Polycube
6
6
  Classifier: Development Status :: 3 - Alpha
@@ -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 == utils.NO_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 == utils.NO_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 == utils.NO_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
- volume = utils.compute_volume(two_hulls)
53
- # we could also compute the sum of the enclosed area by multiple components
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