diskpack 0.4.0__tar.gz → 0.6.0__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.
- {diskpack-0.4.0/src/diskpack.egg-info → diskpack-0.6.0}/PKG-INFO +1 -1
- {diskpack-0.4.0 → diskpack-0.6.0}/pyproject.toml +1 -1
- {diskpack-0.4.0 → diskpack-0.6.0}/src/diskpack/packer.py +74 -19
- {diskpack-0.4.0 → diskpack-0.6.0/src/diskpack.egg-info}/PKG-INFO +1 -1
- {diskpack-0.4.0 → diskpack-0.6.0}/LICENSE +0 -0
- {diskpack-0.4.0 → diskpack-0.6.0}/README.md +0 -0
- {diskpack-0.4.0 → diskpack-0.6.0}/setup.cfg +0 -0
- {diskpack-0.4.0 → diskpack-0.6.0}/src/diskpack/__init__.py +0 -0
- {diskpack-0.4.0 → diskpack-0.6.0}/src/diskpack.egg-info/SOURCES.txt +0 -0
- {diskpack-0.4.0 → diskpack-0.6.0}/src/diskpack.egg-info/dependency_links.txt +0 -0
- {diskpack-0.4.0 → diskpack-0.6.0}/src/diskpack.egg-info/requires.txt +0 -0
- {diskpack-0.4.0 → diskpack-0.6.0}/src/diskpack.egg-info/top_level.txt +0 -0
- {diskpack-0.4.0 → diskpack-0.6.0}/tests/tests.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "diskpack"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
authors = [{ name="James Kelly", email="mrkellyjam@gmail.com" }]
|
|
9
9
|
description = "A high-performance vectorized circle packer with spatial hashing."
|
|
10
10
|
readme = "README.md"
|
|
@@ -8,6 +8,9 @@ Point = np.ndarray
|
|
|
8
8
|
GridKey = Tuple[int, int]
|
|
9
9
|
Circle = Tuple[float, float, float]
|
|
10
10
|
|
|
11
|
+
# Threshold for switching between vectorized and spatial index approaches
|
|
12
|
+
VECTORIZED_THRESHOLD = 300
|
|
13
|
+
|
|
11
14
|
|
|
12
15
|
@dataclass
|
|
13
16
|
class PackingConfig:
|
|
@@ -20,6 +23,7 @@ class PackingConfig:
|
|
|
20
23
|
ray_cast_epsilon: float = 1e-10
|
|
21
24
|
sample_batch_size: int = 50
|
|
22
25
|
fixed_radius: Optional[float] = None
|
|
26
|
+
use_hex_grid: bool = True
|
|
23
27
|
verbose: bool = False
|
|
24
28
|
|
|
25
29
|
|
|
@@ -191,6 +195,27 @@ class CirclePacker:
|
|
|
191
195
|
origin=self.geometry.min_coords,
|
|
192
196
|
mega_threshold=self.config.mega_circle_threshold
|
|
193
197
|
)
|
|
198
|
+
|
|
199
|
+
# Cache for numpy arrays (avoid repeated conversion)
|
|
200
|
+
self._centers_arr: Optional[np.ndarray] = None
|
|
201
|
+
self._radii_arr: Optional[np.ndarray] = None
|
|
202
|
+
self._cache_valid = False
|
|
203
|
+
|
|
204
|
+
def _invalidate_cache(self) -> None:
|
|
205
|
+
"""Mark the numpy array cache as needing refresh."""
|
|
206
|
+
self._cache_valid = False
|
|
207
|
+
|
|
208
|
+
def _get_arrays(self) -> Tuple[np.ndarray, np.ndarray]:
|
|
209
|
+
"""Get cached numpy arrays of centers and radii."""
|
|
210
|
+
if not self._cache_valid or self._centers_arr is None:
|
|
211
|
+
if len(self.centers) > 0:
|
|
212
|
+
self._centers_arr = np.array(self.centers)
|
|
213
|
+
self._radii_arr = np.array(self.radii)
|
|
214
|
+
else:
|
|
215
|
+
self._centers_arr = np.empty((0, 2))
|
|
216
|
+
self._radii_arr = np.empty(0)
|
|
217
|
+
self._cache_valid = True
|
|
218
|
+
return self._centers_arr, self._radii_arr
|
|
194
219
|
|
|
195
220
|
def _sample_candidate_points(self, count: int) -> np.ndarray:
|
|
196
221
|
points = np.random.uniform(
|
|
@@ -208,16 +233,37 @@ class CirclePacker:
|
|
|
208
233
|
return max_radius - self.config.padding
|
|
209
234
|
|
|
210
235
|
def _compute_max_radii_batch(self, points: np.ndarray) -> np.ndarray:
|
|
211
|
-
"""
|
|
236
|
+
"""
|
|
237
|
+
Vectorized max radius computation for multiple points.
|
|
238
|
+
|
|
239
|
+
Uses hybrid approach:
|
|
240
|
+
- Fully vectorized numpy for small circle counts (faster due to no Python loop)
|
|
241
|
+
- Spatial index for large circle counts (faster due to fewer distance calculations)
|
|
242
|
+
"""
|
|
212
243
|
if len(points) == 0:
|
|
213
244
|
return np.array([])
|
|
214
245
|
|
|
246
|
+
# Boundary distances (fully vectorized)
|
|
215
247
|
max_radii = self.geometry.distances_to_boundary_batch(points)
|
|
216
248
|
|
|
217
|
-
if len(self.centers)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
249
|
+
if len(self.centers) == 0:
|
|
250
|
+
return max_radii - self.config.padding
|
|
251
|
+
|
|
252
|
+
centers_arr, radii_arr = self._get_arrays()
|
|
253
|
+
|
|
254
|
+
if len(self.centers) < VECTORIZED_THRESHOLD:
|
|
255
|
+
# Fully vectorized approach for small circle counts
|
|
256
|
+
# Compute distance from each point to each circle: (n_points, n_circles)
|
|
257
|
+
dists = np.linalg.norm(
|
|
258
|
+
points[:, np.newaxis, :] - centers_arr[np.newaxis, :, :],
|
|
259
|
+
axis=2
|
|
260
|
+
) - radii_arr
|
|
261
|
+
|
|
262
|
+
# Min distance to any circle for each point
|
|
263
|
+
min_circle_dists = np.min(dists, axis=1)
|
|
264
|
+
max_radii = np.minimum(max_radii, min_circle_dists)
|
|
265
|
+
else:
|
|
266
|
+
# Spatial index approach for large circle counts
|
|
221
267
|
for i, point in enumerate(points):
|
|
222
268
|
indices = list(self.spatial_index.get_nearby_indices(point))
|
|
223
269
|
if indices:
|
|
@@ -255,6 +301,7 @@ class CirclePacker:
|
|
|
255
301
|
self.centers.append(center)
|
|
256
302
|
self.radii.append(radius)
|
|
257
303
|
self.spatial_index.add_circle(idx, center, radius)
|
|
304
|
+
self._invalidate_cache()
|
|
258
305
|
|
|
259
306
|
def _generate_hex_grid(self, radius: float) -> np.ndarray:
|
|
260
307
|
"""
|
|
@@ -329,22 +376,11 @@ class CirclePacker:
|
|
|
329
376
|
|
|
330
377
|
return circles
|
|
331
378
|
|
|
332
|
-
def
|
|
379
|
+
def _pack_random(self) -> Iterator[Circle]:
|
|
333
380
|
"""
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
For fixed_radius mode, uses optimized hex grid placement.
|
|
337
|
-
For variable radius mode, uses random sampling with best-fit selection.
|
|
338
|
-
|
|
339
|
-
Yields:
|
|
340
|
-
Tuples of (x, y, radius) for each placed circle.
|
|
381
|
+
Pack circles using random sampling.
|
|
382
|
+
Used for variable radius mode or when organic placement is desired.
|
|
341
383
|
"""
|
|
342
|
-
# Use hex grid for fixed radius - much faster and denser
|
|
343
|
-
if self.config.fixed_radius is not None:
|
|
344
|
-
yield from self._pack_hex_grid()
|
|
345
|
-
return
|
|
346
|
-
|
|
347
|
-
# Variable radius mode - use random sampling
|
|
348
384
|
self.progress = PackingProgress(max_failed_attempts=self.config.max_failed_attempts)
|
|
349
385
|
|
|
350
386
|
while self.progress.failed_attempts < self.config.max_failed_attempts:
|
|
@@ -370,6 +406,25 @@ class CirclePacker:
|
|
|
370
406
|
if self.config.verbose:
|
|
371
407
|
print(f"Done! {self.progress}")
|
|
372
408
|
|
|
409
|
+
def generate(self) -> Iterator[Circle]:
|
|
410
|
+
"""
|
|
411
|
+
Generate circles until no more can be placed.
|
|
412
|
+
|
|
413
|
+
For fixed_radius mode with use_hex_grid=True (default), uses optimized hex grid.
|
|
414
|
+
For fixed_radius mode with use_hex_grid=False, uses random sampling for organic look.
|
|
415
|
+
For variable radius mode, uses random sampling with best-fit selection.
|
|
416
|
+
|
|
417
|
+
Yields:
|
|
418
|
+
Tuples of (x, y, radius) for each placed circle.
|
|
419
|
+
"""
|
|
420
|
+
# Use hex grid for fixed radius (unless disabled)
|
|
421
|
+
if self.config.fixed_radius is not None and self.config.use_hex_grid:
|
|
422
|
+
yield from self._pack_hex_grid()
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
# Random sampling mode (variable radius OR fixed radius with organic placement)
|
|
426
|
+
yield from self._pack_random()
|
|
427
|
+
|
|
373
428
|
def pack(self) -> List[Circle]:
|
|
374
429
|
"""Pack circles and return them as a list."""
|
|
375
430
|
return list(self.generate())
|
|
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
|