polycubetools 1.1.0__py3-none-any.whl
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/__init__.py +19 -0
- polycubetools/data/heptacubes.json +945197 -0
- polycubetools/data/hexacubes.json +122667 -0
- polycubetools/data/pentacubes.json +16167 -0
- polycubetools/grid.py +192 -0
- polycubetools/hull.py +121 -0
- polycubetools/models.py +172 -0
- polycubetools/py.typed +0 -0
- polycubetools/solver.py +178 -0
- polycubetools/utils.py +198 -0
- polycubetools-1.1.0.dist-info/METADATA +44 -0
- polycubetools-1.1.0.dist-info/RECORD +15 -0
- polycubetools-1.1.0.dist-info/WHEEL +5 -0
- polycubetools-1.1.0.dist-info/licenses/LICENSE +21 -0
- polycubetools-1.1.0.dist-info/top_level.txt +1 -0
polycubetools/utils.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.resources
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from .models import TWENTY_SIX_NEIGHBORHOOD_DIRECTIONS, Polycube, RotatedPolycube, Coordinate
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from os import PathLike
|
|
12
|
+
|
|
13
|
+
_logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
NO_VOLUME = -1
|
|
16
|
+
MULTIPLE_VOLUME_COMPONENTS = -2
|
|
17
|
+
ERROR_IN_ALGORITHM = -3
|
|
18
|
+
|
|
19
|
+
__all__ = (
|
|
20
|
+
"compute_volume",
|
|
21
|
+
"get_extreme_points",
|
|
22
|
+
"is_valid_fence",
|
|
23
|
+
"load_polycubes",
|
|
24
|
+
"load_pentacubes",
|
|
25
|
+
"load_hexacubes",
|
|
26
|
+
"load_heptacubes",
|
|
27
|
+
"load_octacubes",
|
|
28
|
+
"PolycubeDecoder",
|
|
29
|
+
"PolycubeEncoder",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_valid_fence(coords: set[Coordinate]) -> bool:
|
|
34
|
+
"""Checks if the given set of coordinates form a valid fence in 3D space."""
|
|
35
|
+
# TODO build a check whether all used polycubes are only used once and are valid to their ID
|
|
36
|
+
# (is one of the known rotated polycubes)
|
|
37
|
+
return compute_volume(coords) > 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _is_border_coordinate(coord: Coordinate, max_extrem_point: Coordinate, min_extrem_point: Coordinate) -> bool:
|
|
41
|
+
"""Checks if the given coordinate is on the border of the bounding box defined by the max values."""
|
|
42
|
+
return (
|
|
43
|
+
coord.x == max_extrem_point.x
|
|
44
|
+
or coord.x == min_extrem_point.x
|
|
45
|
+
or coord.y == max_extrem_point.y
|
|
46
|
+
or coord.y == min_extrem_point.y
|
|
47
|
+
or coord.z == max_extrem_point.z
|
|
48
|
+
or coord.z == min_extrem_point.z
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _find_connected_component(visible_coords: set[Coordinate]) -> set[Coordinate]:
|
|
53
|
+
"""Finds all coordinates connected to the first coordinate in the set using 26-neighborhood."""
|
|
54
|
+
start = visible_coords.pop()
|
|
55
|
+
stack = [start]
|
|
56
|
+
component: set[Coordinate] = {start}
|
|
57
|
+
|
|
58
|
+
while stack:
|
|
59
|
+
current = stack.pop()
|
|
60
|
+
|
|
61
|
+
for delta in TWENTY_SIX_NEIGHBORHOOD_DIRECTIONS:
|
|
62
|
+
neighbor = current + delta
|
|
63
|
+
if neighbor not in visible_coords:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
stack.append(neighbor)
|
|
67
|
+
component.add(neighbor)
|
|
68
|
+
visible_coords.remove(neighbor)
|
|
69
|
+
|
|
70
|
+
return component
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_extreme_points(coords: set[Coordinate]) -> tuple[Coordinate, Coordinate]:
|
|
74
|
+
initial_coord = coords.pop()
|
|
75
|
+
x_max, y_max, z_max = initial_coord.x, initial_coord.y, initial_coord.z
|
|
76
|
+
x_min, y_min, z_min = initial_coord.x, initial_coord.y, initial_coord.z
|
|
77
|
+
for coord in coords:
|
|
78
|
+
x_max = max(x_max, coord.x)
|
|
79
|
+
y_max = max(y_max, coord.y)
|
|
80
|
+
z_max = max(z_max, coord.z)
|
|
81
|
+
x_min = min(x_min, coord.x)
|
|
82
|
+
y_min = min(y_min, coord.y)
|
|
83
|
+
z_min = min(z_min, coord.z)
|
|
84
|
+
coords.add(initial_coord)
|
|
85
|
+
|
|
86
|
+
return Coordinate(x_max, y_max, z_max), Coordinate(x_min, y_min, z_min)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def compute_volume(coords: set[Coordinate]) -> int:
|
|
90
|
+
"""Compute the volume from the hull formed by the coordinates"""
|
|
91
|
+
max_extreme_point, min_extreme_point = get_extreme_points(coords)
|
|
92
|
+
max_extreme_point = Coordinate(max_extreme_point.x + 1, max_extreme_point.y + 1, max_extreme_point.z + 1)
|
|
93
|
+
min_extreme_point = Coordinate(min_extreme_point.x - 1, min_extreme_point.y - 1, min_extreme_point.z - 1)
|
|
94
|
+
|
|
95
|
+
visible_coords = {
|
|
96
|
+
c
|
|
97
|
+
for x in range(min_extreme_point.x, max_extreme_point.x + 1)
|
|
98
|
+
for y in range(min_extreme_point.y, max_extreme_point.y + 1)
|
|
99
|
+
for z in range(min_extreme_point.z, max_extreme_point.z + 1)
|
|
100
|
+
if (c := Coordinate(x, y, z)) not in coords
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# the here popped coordinate is always outside the hull; this is because we added this outside new layer!
|
|
104
|
+
_find_connected_component(visible_coords)
|
|
105
|
+
|
|
106
|
+
# we return false because there is no second component, meaning the shape has no volume
|
|
107
|
+
if not visible_coords:
|
|
108
|
+
_logger.warning("No volume found! Only one component detected.")
|
|
109
|
+
return NO_VOLUME
|
|
110
|
+
|
|
111
|
+
volume_cubes = _find_connected_component(visible_coords)
|
|
112
|
+
if visible_coords:
|
|
113
|
+
_logger.warning(
|
|
114
|
+
f"Found {len(volume_cubes)} as volume, but still {len(visible_coords)} unvisited cubes left. Exists another enclosed area!"
|
|
115
|
+
)
|
|
116
|
+
return MULTIPLE_VOLUME_COMPONENTS
|
|
117
|
+
|
|
118
|
+
if any(_is_border_coordinate(c, max_extreme_point, min_extreme_point) for c in volume_cubes):
|
|
119
|
+
_logger.warning("Volume has been found on the border! Bug in the validation.")
|
|
120
|
+
return ERROR_IN_ALGORITHM
|
|
121
|
+
|
|
122
|
+
return len(volume_cubes)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class PolycubeEncoder(json.JSONEncoder):
|
|
126
|
+
"""JSON Encoder for Polycube objects."""
|
|
127
|
+
|
|
128
|
+
def default(self, o: Any) -> Any:
|
|
129
|
+
if isinstance(o, Polycube):
|
|
130
|
+
return {"id": o.id, "rotations": [self.default(rot) for rot in o.rotations]}
|
|
131
|
+
|
|
132
|
+
if isinstance(o, RotatedPolycube):
|
|
133
|
+
return {"id": o.id, "coords": [self.default(coord) for coord in o.coords]}
|
|
134
|
+
|
|
135
|
+
if isinstance(o, Coordinate):
|
|
136
|
+
return {"x": o.x, "y": o.y, "z": o.z}
|
|
137
|
+
|
|
138
|
+
return super().default(o)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class PolycubeDecoder(json.JSONDecoder):
|
|
142
|
+
"""JSON Decoder for Polycube objects."""
|
|
143
|
+
|
|
144
|
+
def __init__(self, *args: Any, **kwargs: Any):
|
|
145
|
+
super().__init__(object_hook=self._polycube_object_hook, *args, **kwargs)
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def _polycube_object_hook(obj: Any) -> Any:
|
|
149
|
+
# Coordinate dict -> Coordinate instance
|
|
150
|
+
if set(obj.keys()) == {"x", "y", "z"}:
|
|
151
|
+
return Coordinate(obj["x"], obj["y"], obj["z"])
|
|
152
|
+
|
|
153
|
+
# RotatedPolycube dict -> RotatedPolycube instance
|
|
154
|
+
if "id" in obj and "coords" in obj:
|
|
155
|
+
# coords expected to be list of Coordinate instances (from inner object_hook)
|
|
156
|
+
return RotatedPolycube(id=obj["id"], coords=frozenset(obj["coords"]))
|
|
157
|
+
|
|
158
|
+
# Polycube dict -> Polycube instance
|
|
159
|
+
if "id" in obj and "rotations" in obj:
|
|
160
|
+
# rotations expected to be list of RotatedPolycube instances
|
|
161
|
+
return Polycube(id=obj["id"], rotations=tuple(obj["rotations"]))
|
|
162
|
+
|
|
163
|
+
return obj
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def load_polycubes(filename: str | PathLike[str]) -> tuple[Polycube, ...]:
|
|
167
|
+
"""Load polycubes from a JSON file."""
|
|
168
|
+
with open(filename, encoding="UTF-8") as f:
|
|
169
|
+
js = json.load(f, cls=PolycubeDecoder)
|
|
170
|
+
|
|
171
|
+
return tuple(js)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _load_resource(filename: str) -> tuple[Polycube, ...]:
|
|
175
|
+
"""Load a resource file from the package's data directory."""
|
|
176
|
+
resource_path = importlib.resources.files(__package__) / "data" / filename
|
|
177
|
+
with importlib.resources.as_file(resource_path) as path:
|
|
178
|
+
return load_polycubes(path)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def load_pentacubes() -> tuple[Polycube, ...]:
|
|
182
|
+
"""Load pentacubes from the default JSON file."""
|
|
183
|
+
return _load_resource("pentacubes.json")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def load_hexacubes() -> tuple[Polycube, ...]:
|
|
187
|
+
"""Load hexacubes from the default JSON file."""
|
|
188
|
+
return _load_resource("hexacubes.json")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def load_heptacubes() -> tuple[Polycube, ...]:
|
|
192
|
+
"""Load heptacubes from the default JSON file."""
|
|
193
|
+
return _load_resource("heptacubes.json")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def load_octacubes() -> tuple[Polycube, ...]:
|
|
197
|
+
"""Load octacubes from the default JSON file."""
|
|
198
|
+
return _load_resource("octacubes.json")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: polycubetools
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Basic framework for working with polycubes in a 3-dimensional grid.
|
|
5
|
+
Author: Team Polycube
|
|
6
|
+
Classifier: Development Status :: 3 - Alpha
|
|
7
|
+
Classifier: Intended Audience :: Science/Research
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Typing :: Typed
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: coverage[toml]; extra == "test"
|
|
17
|
+
Requires-Dist: pytest; extra == "test"
|
|
18
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# polycube-framework
|
|
22
|
+
|
|
23
|
+
### Workflows
|
|
24
|
+
|
|
25
|
+
These are **only** available on github, not locally
|
|
26
|
+
|
|
27
|
+
1. lint workflow (runs automatically on PR, required for merging in main:
|
|
28
|
+
- runs pyright with the config in pyproject.toml (pyright strict)
|
|
29
|
+
- runs `black --check --line-length 120 "./src/polycubetools"`
|
|
30
|
+
2. format workflow (can be run manually)
|
|
31
|
+
- runs `black --line-length 120 "./src/polycubetools"`
|
|
32
|
+
- commits and push to the chosen branch
|
|
33
|
+
|
|
34
|
+
### Recommendations
|
|
35
|
+
|
|
36
|
+
1. PyCharm IDE
|
|
37
|
+
- EAP (EARLY ACCESS PRODUCT) version: has pyright and black tools integrated
|
|
38
|
+
- alternatively: `pip install pyright black`
|
|
39
|
+
|
|
40
|
+
### Getting started
|
|
41
|
+
|
|
42
|
+
1. Have a look at pyproject.toml
|
|
43
|
+
2. Configure a virtual environment (use python `>=3.12`)
|
|
44
|
+
3. For quick testing, feel free to create a script in `scripts/`
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
polycubetools/__init__.py,sha256=nvdLE5imasBZJQvtr79OrJ7eCPAVSirOWbGIkin96qM,372
|
|
2
|
+
polycubetools/grid.py,sha256=UbUXxQ5opEAPKlPmbv7KIorUnRw41cuK7mkVoLDhqLE,7540
|
|
3
|
+
polycubetools/hull.py,sha256=tM6pipw05fAWJ3N0KXgzCpXnZbQsRljPUXAinNXOd2w,4257
|
|
4
|
+
polycubetools/models.py,sha256=5qi9WWOxYg8666x1LOkfyQoDSLf4Qzs3Z94-8N_zRLU,5128
|
|
5
|
+
polycubetools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
polycubetools/solver.py,sha256=X_6dXQ9WBm4CRHXILvs2whXbxYjW-ZeChBczosKJ7vM,5918
|
|
7
|
+
polycubetools/utils.py,sha256=80gA7bVXO5hxLuHNbsfC_s9Wz7shBubMWyKVm52Taw8,6928
|
|
8
|
+
polycubetools/data/heptacubes.json,sha256=vQ9QA3J5vOtjqw9wFYYDLLhmw7oMmOYzNdmTRiMga20,15393824
|
|
9
|
+
polycubetools/data/hexacubes.json,sha256=klfA_tcan_J0CFsOaQdmZgerbVIWDEN1yjW2tq31kd8,1986336
|
|
10
|
+
polycubetools/data/pentacubes.json,sha256=U3lpdByY9jPlDQRcuOUxS3xQBGpPwh9MNpEeSYTq7KE,259662
|
|
11
|
+
polycubetools-1.1.0.dist-info/licenses/LICENSE,sha256=dhlyH2n5C_8e4NuZqQk-CKpYSg8BaCHKep2mPLBZHiQ,1064
|
|
12
|
+
polycubetools-1.1.0.dist-info/METADATA,sha256=Pwsw00NgRNvTq-YNjAA0KC-HhkmWMG3vQ7NBhb14qtQ,1476
|
|
13
|
+
polycubetools-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
polycubetools-1.1.0.dist-info/top_level.txt,sha256=3pdiwnGYfuq3VSO8Y5L2wJYyU2U87PrfH5BBcBZjeKI,14
|
|
15
|
+
polycubetools-1.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Sprylos
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
polycubetools
|