diskpack 0.2.0__tar.gz → 0.3.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.2.0
3
+ Version: 0.3.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.2.0"
7
+ version = "0.3.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"
@@ -46,12 +46,28 @@ class PolygonGeometry:
46
46
  self.polygons = [np.array(p, dtype=float) for p in polygons]
47
47
  self.epsilon = epsilon
48
48
  self._compute_bounds()
49
+ self._precompute_edges()
49
50
 
50
51
  def _compute_bounds(self) -> None:
51
52
  all_vertices = np.vstack(self.polygons)
52
53
  self.min_coords = np.min(all_vertices, axis=0)
53
54
  self.max_coords = np.max(all_vertices, axis=0)
54
55
 
56
+ def _precompute_edges(self) -> None:
57
+ """Precompute edge data for vectorized distance calculations."""
58
+ all_p1 = []
59
+ all_p2 = []
60
+ for poly in self.polygons:
61
+ n = len(poly)
62
+ for i in range(n):
63
+ all_p1.append(poly[i])
64
+ all_p2.append(poly[(i + 1) % n])
65
+
66
+ self.edge_starts = np.array(all_p1)
67
+ self.edge_ends = np.array(all_p2)
68
+ self.edge_vecs = self.edge_ends - self.edge_starts
69
+ self.edge_lengths_sq = np.sum(self.edge_vecs ** 2, axis=1)
70
+
55
71
  def contains_points(self, points: np.ndarray) -> np.ndarray:
56
72
  """Even-Odd Rule for interior detection, supports holes."""
57
73
  x, y = points[:, 0], points[:, 1]
@@ -69,22 +85,55 @@ class PolygonGeometry:
69
85
  return inside
70
86
 
71
87
  def distance_to_boundary(self, point: Point) -> float:
72
- min_distance = float('inf')
73
- for poly in self.polygons:
74
- for i in range(len(poly)):
75
- p1, p2 = poly[i], poly[(i + 1) % len(poly)]
76
- min_distance = min(min_distance, self._point_to_segment_distance(point, p1, p2))
77
- return min_distance
78
-
79
- @staticmethod
80
- def _point_to_segment_distance(point: Point, seg_start: Point, seg_end: Point) -> float:
81
- segment_vec = seg_end - seg_start
82
- segment_length_sq = np.sum(segment_vec ** 2)
83
- if segment_length_sq == 0:
84
- return np.linalg.norm(point - seg_start)
85
- t = np.clip(np.dot(point - seg_start, segment_vec) / segment_length_sq, 0, 1)
86
- projection = seg_start + t * segment_vec
87
- return np.linalg.norm(point - projection)
88
+ """Vectorized distance to nearest polygon edge."""
89
+ to_point = point - self.edge_starts
90
+
91
+ dots = np.sum(to_point * self.edge_vecs, axis=1)
92
+
93
+ with np.errstate(divide='ignore', invalid='ignore'):
94
+ t = np.clip(dots / self.edge_lengths_sq, 0, 1)
95
+ t = np.where(self.edge_lengths_sq == 0, 0, t)
96
+
97
+ projections = self.edge_starts + t[:, np.newaxis] * self.edge_vecs
98
+ distances = np.linalg.norm(point - projections, axis=1)
99
+
100
+ return float(np.min(distances))
101
+
102
+ def distances_to_boundary_batch(self, points: np.ndarray) -> np.ndarray:
103
+ """
104
+ Vectorized distance calculation for multiple points at once.
105
+
106
+ Args:
107
+ points: Array of shape (n_points, 2)
108
+
109
+ Returns:
110
+ Array of shape (n_points,) with distance to nearest edge for each point
111
+ """
112
+ n_points = len(points)
113
+ n_edges = len(self.edge_starts)
114
+
115
+ # Reshape for broadcasting: (n_points, 1, 2) - (n_edges, 2) -> (n_points, n_edges, 2)
116
+ to_point = points[:, np.newaxis, :] - self.edge_starts[np.newaxis, :, :]
117
+
118
+ # Dot products: (n_points, n_edges)
119
+ dots = np.sum(to_point * self.edge_vecs[np.newaxis, :, :], axis=2)
120
+
121
+ # Project onto edges
122
+ with np.errstate(divide='ignore', invalid='ignore'):
123
+ t = np.clip(dots / self.edge_lengths_sq[np.newaxis, :], 0, 1)
124
+ t = np.where(self.edge_lengths_sq[np.newaxis, :] == 0, 0, t)
125
+
126
+ # Closest points on edges: (n_points, n_edges, 2)
127
+ projections = (
128
+ self.edge_starts[np.newaxis, :, :] +
129
+ t[:, :, np.newaxis] * self.edge_vecs[np.newaxis, :, :]
130
+ )
131
+
132
+ # Distances: (n_points, n_edges)
133
+ distances = np.linalg.norm(points[:, np.newaxis, :] - projections, axis=2)
134
+
135
+ # Min distance per point
136
+ return np.min(distances, axis=1)
88
137
 
89
138
 
90
139
  @dataclass
@@ -95,8 +144,16 @@ class SpatialIndex:
95
144
  mega_threshold: float
96
145
  grid: Dict[GridKey, List[int]] = field(default_factory=dict)
97
146
  mega_circles: List[int] = field(default_factory=list)
147
+
148
+ # Store centers/radii arrays for vectorized lookup
149
+ _centers: np.ndarray = field(default_factory=lambda: np.empty((0, 2)))
150
+ _radii: np.ndarray = field(default_factory=lambda: np.empty(0))
98
151
 
99
152
  def add_circle(self, index: int, center: Point, radius: float) -> None:
153
+ # Update arrays
154
+ self._centers = np.vstack([self._centers, center]) if len(self._centers) > 0 else center.reshape(1, 2)
155
+ self._radii = np.append(self._radii, radius)
156
+
100
157
  if radius > self.cell_size * self.mega_threshold:
101
158
  self.mega_circles.append(index)
102
159
  else:
@@ -112,6 +169,43 @@ class SpatialIndex:
112
169
  if neighbor_key in self.grid:
113
170
  yield from self.grid[neighbor_key]
114
171
 
172
+ def get_nearby_indices_batch(self, points: np.ndarray) -> List[np.ndarray]:
173
+ """
174
+ Get nearby circle indices for multiple points.
175
+ Returns list of index arrays, one per point.
176
+ """
177
+ results = []
178
+ mega_set = set(self.mega_circles)
179
+
180
+ for point in points:
181
+ indices = list(self.mega_circles)
182
+ center_key = self._get_cell_key(point)
183
+ for dx in range(-1, 2):
184
+ for dy in range(-1, 2):
185
+ neighbor_key = (center_key[0] + dx, center_key[1] + dy)
186
+ if neighbor_key in self.grid:
187
+ for idx in self.grid[neighbor_key]:
188
+ if idx not in mega_set:
189
+ indices.append(idx)
190
+ results.append(np.array(indices, dtype=int))
191
+
192
+ return results
193
+
194
+ def distance_to_circles(self, point: Point) -> float:
195
+ """Get minimum distance from point to any existing circle's edge."""
196
+ if len(self._centers) == 0:
197
+ return float('inf')
198
+
199
+ indices = list(self.get_nearby_indices(point))
200
+ if not indices:
201
+ return float('inf')
202
+
203
+ centers = self._centers[indices]
204
+ radii = self._radii[indices]
205
+
206
+ distances = np.linalg.norm(centers - point, axis=1) - radii
207
+ return float(np.min(distances))
208
+
115
209
  def _get_cell_key(self, point: Point) -> GridKey:
116
210
  cell_coords = ((point - self.origin) // self.cell_size).astype(int)
117
211
  return (int(cell_coords[0]), int(cell_coords[1]))
@@ -144,26 +238,62 @@ class CirclePacker:
144
238
  return points[self.geometry.contains_points(points)]
145
239
 
146
240
  def _compute_max_radius(self, point: Point) -> float:
241
+ """Compute max radius for a single point."""
147
242
  max_radius = self.geometry.distance_to_boundary(point)
148
- for idx in self.spatial_index.get_nearby_indices(point):
149
- distance_to_circle = np.linalg.norm(self.centers[idx] - point) - self.radii[idx]
150
- max_radius = min(max_radius, distance_to_circle)
243
+ circle_dist = self.spatial_index.distance_to_circles(point)
244
+ max_radius = min(max_radius, circle_dist)
151
245
  return max_radius - self.config.padding
152
246
 
247
+ def _compute_max_radii_batch(self, points: np.ndarray) -> np.ndarray:
248
+ """
249
+ Vectorized max radius computation for multiple points.
250
+ """
251
+ if len(points) == 0:
252
+ return np.array([])
253
+
254
+ # Boundary distances (fully vectorized)
255
+ max_radii = self.geometry.distances_to_boundary_batch(points)
256
+
257
+ # Circle collision distances (per-point, but with vectorized distance calc)
258
+ if len(self.centers) > 0:
259
+ centers_arr = np.array(self.centers)
260
+ radii_arr = np.array(self.radii)
261
+
262
+ for i, point in enumerate(points):
263
+ # Get nearby indices
264
+ indices = list(self.spatial_index.get_nearby_indices(point))
265
+ if indices:
266
+ nearby_centers = centers_arr[indices]
267
+ nearby_radii = radii_arr[indices]
268
+ distances = np.linalg.norm(nearby_centers - point, axis=1) - nearby_radii
269
+ max_radii[i] = min(max_radii[i], np.min(distances))
270
+
271
+ return max_radii - self.config.padding
272
+
153
273
  def _find_best_placement(self, candidates: np.ndarray) -> Optional[Tuple[Point, float]]:
154
- best_point, best_radius = None, 0
274
+ if len(candidates) == 0:
275
+ return None
276
+
277
+ # Batch compute all radii
278
+ radii = self._compute_max_radii_batch(candidates)
155
279
  fixed = self.config.fixed_radius
156
-
157
- for point in candidates:
158
- radius = self._compute_max_radius(point)
159
- if fixed is not None:
160
- radius = fixed if radius >= fixed else -1
161
- if radius > best_radius:
162
- best_point, best_radius = point, radius
163
-
164
- if best_point is not None and best_radius >= self.config.min_radius:
165
- return best_point, best_radius
166
- return None
280
+
281
+ if fixed is not None:
282
+ # For fixed radius, filter to valid positions and pick randomly
283
+ valid_mask = radii >= fixed
284
+ if not np.any(valid_mask):
285
+ return None
286
+ valid_indices = np.where(valid_mask)[0]
287
+ best_idx = valid_indices[0] # Take first valid (or could randomize)
288
+ return candidates[best_idx], fixed
289
+ else:
290
+ # Variable radius: pick the largest
291
+ best_idx = np.argmax(radii)
292
+ best_radius = radii[best_idx]
293
+
294
+ if best_radius >= self.config.min_radius:
295
+ return candidates[best_idx], best_radius
296
+ return None
167
297
 
168
298
  def _place_circle(self, center: Point, radius: float) -> None:
169
299
  idx = len(self.centers)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: diskpack
3
- Version: 0.2.0
3
+ Version: 0.3.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