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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: diskpack
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: A high-performance vectorized circle packer with spatial hashing.
5
5
  Author-email: James Kelly <mrkellyjam@gmail.com>
6
6
  Requires-Python: >=3.8
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "diskpack"
7
- version = "0.4.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
- """Vectorized max radius computation for multiple points."""
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) > 0:
218
- centers_arr = np.array(self.centers)
219
- radii_arr = np.array(self.radii)
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 generate(self) -> Iterator[Circle]:
379
+ def _pack_random(self) -> Iterator[Circle]:
333
380
  """
334
- Generate circles until no more can be placed.
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())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: diskpack
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: A high-performance vectorized circle packer with spatial hashing.
5
5
  Author-email: James Kelly <mrkellyjam@gmail.com>
6
6
  Requires-Python: >=3.8
File without changes
File without changes
File without changes
File without changes