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/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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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