diskpack 0.1.0__tar.gz → 0.2.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.1.0
3
+ Version: 0.2.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.1.0"
7
+ version = "0.2.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"
@@ -19,6 +19,8 @@ class PackingConfig:
19
19
  mega_circle_threshold: float = 0.5
20
20
  ray_cast_epsilon: float = 1e-10
21
21
  sample_batch_size: int = 50
22
+ fixed_radius: Optional[float] = None
23
+ verbose: bool = False
22
24
 
23
25
 
24
26
  @dataclass
@@ -27,12 +29,12 @@ class PackingProgress:
27
29
  circles_placed: int = 0
28
30
  failed_attempts: int = 0
29
31
  max_failed_attempts: int = 200
30
-
32
+
31
33
  @property
32
34
  def progress_ratio(self) -> float:
33
35
  """How close we are to stopping (0.0 = just started, 1.0 = about to stop)."""
34
36
  return self.failed_attempts / self.max_failed_attempts
35
-
37
+
36
38
  def __str__(self) -> str:
37
39
  return f"Placed: {self.circles_placed} | Failed attempts: {self.failed_attempts}/{self.max_failed_attempts} ({self.progress_ratio:.0%})"
38
40
 
@@ -148,12 +150,14 @@ class CirclePacker:
148
150
  max_radius = min(max_radius, distance_to_circle)
149
151
  return max_radius - self.config.padding
150
152
 
151
- def _find_best_placement(self, candidates: np.ndarray, fixed_radius: Optional[float]) -> Optional[Tuple[Point, float]]:
153
+ def _find_best_placement(self, candidates: np.ndarray) -> Optional[Tuple[Point, float]]:
152
154
  best_point, best_radius = None, 0
155
+ fixed = self.config.fixed_radius
156
+
153
157
  for point in candidates:
154
158
  radius = self._compute_max_radius(point)
155
- if fixed_radius is not None:
156
- radius = fixed_radius if radius >= fixed_radius else -1
159
+ if fixed is not None:
160
+ radius = fixed if radius >= fixed else -1
157
161
  if radius > best_radius:
158
162
  best_point, best_radius = point, radius
159
163
 
@@ -167,42 +171,38 @@ class CirclePacker:
167
171
  self.radii.append(radius)
168
172
  self.spatial_index.add_circle(idx, center, radius)
169
173
 
170
- def generate(self, fixed_radius: Optional[float] = None, verbose: bool = False) -> Iterator[Circle]:
174
+ def generate(self) -> Iterator[Circle]:
171
175
  """
172
176
  Generate circles until no more can be placed.
173
-
174
- Args:
175
- fixed_radius: If provided, all circles will have this exact radius.
176
- verbose: If True, print progress updates periodically.
177
-
177
+
178
178
  Yields:
179
179
  Tuples of (x, y, radius) for each placed circle.
180
180
  """
181
181
  self.progress = PackingProgress(max_failed_attempts=self.config.max_failed_attempts)
182
-
182
+
183
183
  while self.progress.failed_attempts < self.config.max_failed_attempts:
184
184
  candidates = self._sample_candidate_points(self.config.sample_batch_size)
185
- result = self._find_best_placement(candidates, fixed_radius)
186
-
185
+ result = self._find_best_placement(candidates)
186
+
187
187
  if result is not None:
188
188
  center, radius = result
189
189
  self._place_circle(center, radius)
190
190
  self.progress.circles_placed += 1
191
191
  self.progress.failed_attempts = 0
192
-
193
- if verbose and self.progress.circles_placed % 25 == 0:
192
+
193
+ if self.config.verbose and self.progress.circles_placed % 25 == 0:
194
194
  print(self.progress)
195
-
195
+
196
196
  yield (float(center[0]), float(center[1]), float(radius))
197
197
  else:
198
198
  self.progress.failed_attempts += 1
199
-
200
- if verbose and self.progress.failed_attempts % 50 == 0:
199
+
200
+ if self.config.verbose and self.progress.failed_attempts % 50 == 0:
201
201
  print(self.progress)
202
202
 
203
- if verbose:
203
+ if self.config.verbose:
204
204
  print(f"Done! {self.progress}")
205
205
 
206
- def pack(self, fixed_radius: Optional[float] = None, verbose: bool = False) -> List[Circle]:
206
+ def pack(self) -> List[Circle]:
207
207
  """Pack circles and return them as a list."""
208
- return list(self.generate(fixed_radius, verbose=verbose))
208
+ return list(self.generate())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: diskpack
3
- Version: 0.1.0
3
+ Version: 0.2.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
@@ -3,14 +3,13 @@ import numpy as np
3
3
  from diskpack.packer import CirclePacker, PackingConfig, PolygonGeometry
4
4
 
5
5
 
6
-
7
6
  class TestCirclePacker(unittest.TestCase):
8
7
  def setUp(self):
9
8
  """Set up a simple 10x10 square for testing."""
10
9
  self.square_verts = np.array([[0, 0], [10, 0], [10, 10], [0, 10]])
11
10
  self.square = [self.square_verts]
12
11
  # Use small padding/min_radius to allow for dense filling
13
- self.config = PackingConfig(padding=0.1, min_radius=0.5, patience_before_stop=100)
12
+ self.config = PackingConfig(padding=0.1, min_radius=0.5, max_failed_attempts=100)
14
13
 
15
14
  def _calculate_poly_area(self, segments: list) -> float:
16
15
  """Calculates area of polygons (including holes) using the Shoelace Formula."""
@@ -18,20 +17,15 @@ class TestCirclePacker(unittest.TestCase):
18
17
  for poly in segments:
19
18
  x = poly[:, 0]
20
19
  y = poly[:, 1]
21
- # Area = 0.5 * |sum(x_i * y_{i+1} - x_{i+1} * y_i)|
22
20
  area = 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))
23
- # If it's a hole (clockwise), it naturally subtracts if we don't use abs here,
24
- # but since our parser handles holes via Even-Odd, we treat all as positive
25
- # and subtract holes manually if needed. For this test, we assume shells.
26
21
  total_area += area
27
22
  return total_area
28
23
 
29
24
  def test_filling_density(self):
30
25
  """Verify that the packer fills a minimum percentage of the polygon area."""
31
26
  packer = CirclePacker(self.square, self.config)
32
- circles = list(packer.generate())
27
+ circles = packer.pack()
33
28
 
34
- # Calculate areas
35
29
  poly_area = self._calculate_poly_area(self.square)
36
30
  circle_area = sum(np.pi * (r**2) for _, _, r in circles)
37
31
 
@@ -41,8 +35,6 @@ class TestCirclePacker(unittest.TestCase):
41
35
  print(f"Total Area: {poly_area:.2f} | Circle Area: {circle_area:.2f}")
42
36
  print(f"Packing Density: {fill_percentage:.2f}%")
43
37
 
44
- # Assert a minimum density threshold.
45
- # For a random packer, 30-50% is a reasonable 'success' floor depending on radius.
46
38
  self.assertGreater(fill_percentage, 25.0, f"Packing density too low: {fill_percentage:.2f}%")
47
39
 
48
40
  def test_geometry_containment(self):
@@ -61,15 +53,16 @@ class TestCirclePacker(unittest.TestCase):
61
53
  def test_no_overlap_integrity(self):
62
54
  """Mathematically verify no circles overlap including padding."""
63
55
  packer = CirclePacker(self.square, self.config)
64
- circles = list(packer.generate())
56
+ circles = packer.pack()
65
57
 
66
58
  for i, (x1, y1, r1) in enumerate(circles):
67
59
  for j, (x2, y2, r2) in enumerate(circles):
68
- if i == j: continue
60
+ if i == j:
61
+ continue
69
62
  dist = np.sqrt((x1 - x2)**2 + (y1 - y2)**2)
70
- # Distance must be >= sum of radii + padding
71
63
  min_sep = r1 + r2 + self.config.padding
72
64
  self.assertGreaterEqual(dist, min_sep - 1e-9)
73
65
 
66
+
74
67
  if __name__ == '__main__':
75
68
  unittest.main()
File without changes
File without changes
File without changes