gdsdiff 0.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.
Files changed (85) hide show
  1. gdsdiff/__init__.py +148 -0
  2. gdsdiff/__main__.py +7 -0
  3. gdsdiff/_environment.py +12 -0
  4. gdsdiff/_version.py +34 -0
  5. gdsdiff/acceptance_evidence.py +530 -0
  6. gdsdiff/api.py +2955 -0
  7. gdsdiff/backends/__init__.py +26 -0
  8. gdsdiff/backends/base.py +77 -0
  9. gdsdiff/backends/boundary_loop_ctypes.py +191 -0
  10. gdsdiff/backends/cpu.py +20 -0
  11. gdsdiff/backends/cpu_ctypes.py +255 -0
  12. gdsdiff/backends/cuda.py +40 -0
  13. gdsdiff/backends/cuda_ctypes.py +1297 -0
  14. gdsdiff/backends/exact.py +424 -0
  15. gdsdiff/backends/geometry_ctypes.py +252 -0
  16. gdsdiff/backends/identity.py +53 -0
  17. gdsdiff/backends/metal.py +198 -0
  18. gdsdiff/backends/metal_ctypes.py +866 -0
  19. gdsdiff/backends/polygon_extract_ctypes.py +233 -0
  20. gdsdiff/backends/structural_ctypes.py +130 -0
  21. gdsdiff/boundary_loops.py +616 -0
  22. gdsdiff/cache.py +544 -0
  23. gdsdiff/canonicalize.py +176 -0
  24. gdsdiff/cli.py +352 -0
  25. gdsdiff/config.py +257 -0
  26. gdsdiff/cuda_setup.py +174 -0
  27. gdsdiff/design_history.py +65 -0
  28. gdsdiff/diagnostics.py +42 -0
  29. gdsdiff/diff_gds.py +66 -0
  30. gdsdiff/exact_diff_markdown.py +979 -0
  31. gdsdiff/exact_oracle.py +649 -0
  32. gdsdiff/extract.py +376 -0
  33. gdsdiff/gds_metadata.py +43 -0
  34. gdsdiff/geometry.py +112 -0
  35. gdsdiff/grid.py +143 -0
  36. gdsdiff/hierarchy.py +215 -0
  37. gdsdiff/hierarchy_extract.py +263 -0
  38. gdsdiff/inventory.py +153 -0
  39. gdsdiff/multiprocessing_support.py +39 -0
  40. gdsdiff/native.py +135 -0
  41. gdsdiff/native_cache.py +265 -0
  42. gdsdiff/native_src/cpu_prefilter.cpp +349 -0
  43. gdsdiff/native_src/gds_boundary_loops.cpp +505 -0
  44. gdsdiff/native_src/gds_geometry_scan.cpp +601 -0
  45. gdsdiff/native_src/gds_polygon_extract.cpp +594 -0
  46. gdsdiff/native_src/gds_structural_scan.cpp +335 -0
  47. gdsdiff/native_src/gdsdiff/CMakeLists.txt +74 -0
  48. gdsdiff/native_src/gdsdiff/core.cpp +191 -0
  49. gdsdiff/native_src/gdsdiff/core.hpp +96 -0
  50. gdsdiff/native_src/gdsdiff/cuda_assignment.cu +304 -0
  51. gdsdiff/native_src/gdsdiff/cuda_assignment.cuh +33 -0
  52. gdsdiff/native_src/gdsdiff/cuda_candidates.cu +133 -0
  53. gdsdiff/native_src/gdsdiff/cuda_candidates.cuh +17 -0
  54. gdsdiff/native_src/gdsdiff/cuda_ctypes.cu +4528 -0
  55. gdsdiff/native_src/gdsdiff/cuda_memory.cu +194 -0
  56. gdsdiff/native_src/gdsdiff/cuda_memory.cuh +34 -0
  57. gdsdiff/native_src/gdsdiff/metal_ctypes.mm +2791 -0
  58. gdsdiff/native_src/gdsdiff/python_bindings.cpp +21 -0
  59. gdsdiff/native_src/gdsdiff/test_core.cpp +93 -0
  60. gdsdiff/native_src/gdsdiff/test_cuda_assignment.cu +109 -0
  61. gdsdiff/native_src/gdsdiff/test_cuda_candidates.cu +94 -0
  62. gdsdiff/native_src/gdsdiff/test_cuda_memory.cu +97 -0
  63. gdsdiff/policy.py +305 -0
  64. gdsdiff/polygon_components.py +860 -0
  65. gdsdiff/profiles.py +152 -0
  66. gdsdiff/py.typed +1 -0
  67. gdsdiff/rect_coverage.py +384 -0
  68. gdsdiff/report.py +354 -0
  69. gdsdiff/schemas/gdsdiff_report-v1.schema.json +92 -0
  70. gdsdiff/semantics.py +75 -0
  71. gdsdiff/source_edge_diff.py +1120 -0
  72. gdsdiff/spatial.py +71 -0
  73. gdsdiff/structural_guard.py +161 -0
  74. gdsdiff/surplus_coverage.py +255 -0
  75. gdsdiff/synthetic_suite.py +1643 -0
  76. gdsdiff/templates.py +709 -0
  77. gdsdiff/tiling.py +153 -0
  78. gdsdiff/windowed_exact.py +270 -0
  79. gdsdiff/windows.py +184 -0
  80. gdsdiff-0.1.0.dist-info/METADATA +135 -0
  81. gdsdiff-0.1.0.dist-info/RECORD +85 -0
  82. gdsdiff-0.1.0.dist-info/WHEEL +5 -0
  83. gdsdiff-0.1.0.dist-info/entry_points.txt +4 -0
  84. gdsdiff-0.1.0.dist-info/licenses/LICENSE +674 -0
  85. gdsdiff-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,860 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from time import perf_counter
5
+
6
+ import numpy as np
7
+
8
+ from .boundary_loops import assemble_boundary_loops
9
+ from .config import LayerSpec
10
+ from .exact_oracle import DiffFragment, ExactOracleResult, LayerExactDiff
11
+ from .geometry import PolygonBuffer, polygon_bbox, polygon_twice_area
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class ComponentPolygon:
16
+ side: str
17
+ source_index: int
18
+ fragment: DiffFragment
19
+ role: str = "surplus"
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class PolygonComponent:
24
+ layer: LayerSpec
25
+ polygons: tuple[ComponentPolygon, ...]
26
+
27
+ @property
28
+ def old_count(self) -> int:
29
+ return sum(1 for polygon in self.polygons if polygon.side == "old")
30
+
31
+ @property
32
+ def new_count(self) -> int:
33
+ return sum(1 for polygon in self.polygons if polygon.side == "new")
34
+
35
+ @property
36
+ def bbox(self) -> tuple[int, int, int, int]:
37
+ return _bbox_union(tuple(polygon.fragment.bbox for polygon in self.polygons))
38
+
39
+ @property
40
+ def vertex_count(self) -> int:
41
+ return sum(len(polygon.fragment.points) for polygon in self.polygons)
42
+
43
+ @property
44
+ def has_positive_overlap(self) -> bool:
45
+ fragments = tuple(polygon.fragment for polygon in self.polygons)
46
+ return _has_positive_bbox_overlap(fragments)
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class PolygonComponentStats:
51
+ component_count: int
52
+ overlap_component_count: int
53
+ max_fragments_per_component: int
54
+ max_vertices_per_component: int
55
+ context_index_backend: str = "none"
56
+ context_index_s: float = 0.0
57
+ context_pair_count: int = 0
58
+ context_cuda_kernel_count: int = 0
59
+ context_cuda_event_elapsed_s: float = 0.0
60
+ context_cuda_transferred_bytes: int = 0
61
+ context_metal_kernel_count: int = 0
62
+ context_metal_event_elapsed_s: float = 0.0
63
+ context_metal_transferred_bytes: int = 0
64
+
65
+ def to_json(self) -> dict[str, int | float | str]:
66
+ return {
67
+ "component_count": self.component_count,
68
+ "overlap_component_count": self.overlap_component_count,
69
+ "max_fragments_per_component": self.max_fragments_per_component,
70
+ "max_vertices_per_component": self.max_vertices_per_component,
71
+ "context_index_backend": self.context_index_backend,
72
+ "context_index_s": self.context_index_s,
73
+ "context_pair_count": self.context_pair_count,
74
+ "context_cuda_kernel_count": self.context_cuda_kernel_count,
75
+ "context_cuda_event_elapsed_s": self.context_cuda_event_elapsed_s,
76
+ "context_cuda_transferred_bytes": self.context_cuda_transferred_bytes,
77
+ "context_metal_kernel_count": self.context_metal_kernel_count,
78
+ "context_metal_event_elapsed_s": self.context_metal_event_elapsed_s,
79
+ "context_metal_transferred_bytes": self.context_metal_transferred_bytes,
80
+ }
81
+
82
+
83
+ @dataclass(frozen=True)
84
+ class PolygonComponentBuildResult:
85
+ components: tuple[PolygonComponent, ...]
86
+ stats: PolygonComponentStats
87
+
88
+
89
+ @dataclass(frozen=True)
90
+ class LayerSideMergeStats:
91
+ input_fragment_count: int = 0
92
+ output_fragment_count: int = 0
93
+ group_count: int = 0
94
+ assembled_group_count: int = 0
95
+ cuda_edge_cancel_s: float = 0.0
96
+ cuda_edge_cancel_kernel_count: int = 0
97
+ cuda_edge_cancel_event_elapsed_s: float = 0.0
98
+ cuda_edge_cancel_transferred_bytes: int = 0
99
+ native_edge_cancel_s: float = 0.0
100
+ cuda_loop_trace_s: float = 0.0
101
+ metal_loop_trace_s: float = 0.0
102
+ metal_loop_kernel_count: int = 0
103
+ metal_loop_event_elapsed_s: float = 0.0
104
+ metal_loop_transferred_bytes: int = 0
105
+ native_loop_trace_s: float = 0.0
106
+ python_loop_trace_s: float = 0.0
107
+ hole_stitch_s: float = 0.0
108
+
109
+ def to_json(self) -> dict[str, int | float]:
110
+ return {
111
+ "input_fragment_count": self.input_fragment_count,
112
+ "output_fragment_count": self.output_fragment_count,
113
+ "group_count": self.group_count,
114
+ "assembled_group_count": self.assembled_group_count,
115
+ "cuda_edge_cancel_s": self.cuda_edge_cancel_s,
116
+ "cuda_edge_cancel_kernel_count": self.cuda_edge_cancel_kernel_count,
117
+ "cuda_edge_cancel_event_elapsed_s": self.cuda_edge_cancel_event_elapsed_s,
118
+ "cuda_edge_cancel_transferred_bytes": self.cuda_edge_cancel_transferred_bytes,
119
+ "native_edge_cancel_s": self.native_edge_cancel_s,
120
+ "cuda_loop_trace_s": self.cuda_loop_trace_s,
121
+ "metal_loop_trace_s": self.metal_loop_trace_s,
122
+ "metal_loop_kernel_count": self.metal_loop_kernel_count,
123
+ "metal_loop_event_elapsed_s": self.metal_loop_event_elapsed_s,
124
+ "metal_loop_transferred_bytes": self.metal_loop_transferred_bytes,
125
+ "native_loop_trace_s": self.native_loop_trace_s,
126
+ "python_loop_trace_s": self.python_loop_trace_s,
127
+ "hole_stitch_s": self.hole_stitch_s,
128
+ }
129
+
130
+
131
+ @dataclass(frozen=True)
132
+ class LayerSideMergeResult:
133
+ fragments: tuple[DiffFragment, ...]
134
+ stats: LayerSideMergeStats
135
+
136
+
137
+ @dataclass(frozen=True)
138
+ class _LayerBBoxIndex:
139
+ indices: np.ndarray
140
+ sorted_indices: np.ndarray
141
+ sorted_x0: np.ndarray
142
+ sorted_bboxes: np.ndarray
143
+
144
+
145
+ @dataclass(frozen=True)
146
+ class _ContextOverlapPairRun:
147
+ component_indices: np.ndarray
148
+ polygon_indices: np.ndarray
149
+ wall_s: float = 0.0
150
+ cuda_kernel_count: int = 0
151
+ cuda_event_elapsed_s: float = 0.0
152
+ metal_kernel_count: int = 0
153
+ metal_event_elapsed_s: float = 0.0
154
+ transferred_bytes: int = 0
155
+ accelerator: str = "cpu"
156
+
157
+ @property
158
+ def pair_count(self) -> int:
159
+ return int(len(self.component_indices))
160
+
161
+
162
+ def build_record_delta_surplus_components(
163
+ old_buffer: PolygonBuffer,
164
+ new_buffer: PolygonBuffer,
165
+ record_delta,
166
+ ) -> PolygonComponentBuildResult:
167
+ by_layer: dict[LayerSpec, list[ComponentPolygon]] = {}
168
+ for row in range(record_delta.record_count):
169
+ direction = int(record_delta.directions[row])
170
+ count = int(record_delta.counts[row])
171
+ representative_index = int(record_delta.representative_indices[row])
172
+ if direction == 1:
173
+ layer = old_buffer.layer_table.layers[int(old_buffer.layer_ids[representative_index])]
174
+ polygon = ComponentPolygon(
175
+ side="old",
176
+ source_index=representative_index,
177
+ fragment=_fragment_from_buffer(old_buffer, representative_index),
178
+ )
179
+ else:
180
+ layer = new_buffer.layer_table.layers[int(new_buffer.layer_ids[representative_index])]
181
+ polygon = ComponentPolygon(
182
+ side="new",
183
+ source_index=representative_index,
184
+ fragment=_fragment_from_buffer(new_buffer, representative_index),
185
+ )
186
+ by_layer.setdefault(layer, []).extend(polygon for _ in range(count))
187
+
188
+ components: list[PolygonComponent] = []
189
+ for layer in sorted(by_layer):
190
+ layer_polygons = tuple(by_layer[layer])
191
+ for component_indices in _polygon_components(layer_polygons):
192
+ components.append(
193
+ PolygonComponent(
194
+ layer=layer,
195
+ polygons=tuple(layer_polygons[index] for index in component_indices),
196
+ )
197
+ )
198
+ components_tuple = tuple(sorted(components, key=_component_sort_key))
199
+ return PolygonComponentBuildResult(
200
+ components=components_tuple,
201
+ stats=_component_stats(components_tuple),
202
+ )
203
+
204
+
205
+ def add_overlapping_context_to_components(
206
+ old_buffer: PolygonBuffer,
207
+ new_buffer: PolygonBuffer,
208
+ components: tuple[PolygonComponent, ...],
209
+ *,
210
+ preferred_accelerator: str | None = None,
211
+ ) -> PolygonComponentBuildResult:
212
+ context_start = perf_counter()
213
+ old_fragment_cache: dict[int, DiffFragment] = {}
214
+ new_fragment_cache: dict[int, DiffFragment] = {}
215
+ old_source_indices = [
216
+ {polygon.source_index for polygon in component.polygons if polygon.side == "old"} for component in components
217
+ ]
218
+ new_source_indices = [
219
+ {polygon.source_index for polygon in component.polygons if polygon.side == "new"} for component in components
220
+ ]
221
+ old_pair_run = _try_accelerated_context_pairs(components, old_buffer, preferred_accelerator=preferred_accelerator)
222
+ new_pair_run = _try_accelerated_context_pairs(components, new_buffer, preferred_accelerator=preferred_accelerator)
223
+ if old_pair_run is not None and new_pair_run is not None:
224
+ expanded_polygons = [list(component.polygons) for component in components]
225
+ _append_context_pairs(
226
+ expanded_polygons,
227
+ old_pair_run,
228
+ old_source_indices,
229
+ side="old",
230
+ buffer=old_buffer,
231
+ fragment_cache=old_fragment_cache,
232
+ )
233
+ _append_context_pairs(
234
+ expanded_polygons,
235
+ new_pair_run,
236
+ new_source_indices,
237
+ side="new",
238
+ buffer=new_buffer,
239
+ fragment_cache=new_fragment_cache,
240
+ )
241
+ expanded_tuple = tuple(
242
+ sorted(
243
+ (
244
+ PolygonComponent(layer=component.layer, polygons=tuple(sorted(polygons, key=_polygon_sort_key)))
245
+ for component, polygons in zip(components, expanded_polygons)
246
+ ),
247
+ key=_component_sort_key,
248
+ )
249
+ )
250
+ accelerator = old_pair_run.accelerator
251
+ return PolygonComponentBuildResult(
252
+ components=expanded_tuple,
253
+ stats=_component_stats(
254
+ expanded_tuple,
255
+ context_index_backend=f"{accelerator}-context-overlap-pairs",
256
+ context_index_s=old_pair_run.wall_s + new_pair_run.wall_s,
257
+ context_pair_count=old_pair_run.pair_count + new_pair_run.pair_count,
258
+ context_cuda_kernel_count=old_pair_run.cuda_kernel_count + new_pair_run.cuda_kernel_count,
259
+ context_cuda_event_elapsed_s=old_pair_run.cuda_event_elapsed_s + new_pair_run.cuda_event_elapsed_s,
260
+ context_cuda_transferred_bytes=(
261
+ old_pair_run.transferred_bytes + new_pair_run.transferred_bytes if accelerator == "cuda" else 0
262
+ ),
263
+ context_metal_kernel_count=old_pair_run.metal_kernel_count + new_pair_run.metal_kernel_count,
264
+ context_metal_event_elapsed_s=old_pair_run.metal_event_elapsed_s + new_pair_run.metal_event_elapsed_s,
265
+ context_metal_transferred_bytes=(
266
+ old_pair_run.transferred_bytes + new_pair_run.transferred_bytes if accelerator == "metal" else 0
267
+ ),
268
+ ),
269
+ )
270
+ old_layer_indices = _indices_by_layer_id(old_buffer)
271
+ new_layer_indices = _indices_by_layer_id(new_buffer)
272
+ old_layer_ids = old_buffer.layer_table.index_by_layer
273
+ new_layer_ids = new_buffer.layer_table.index_by_layer
274
+ expanded: list[PolygonComponent] = []
275
+ for component_index, component in enumerate(components):
276
+ old_indices = old_source_indices[component_index]
277
+ new_indices = new_source_indices[component_index]
278
+ polygons = list(component.polygons)
279
+ component_bbox = component.bbox
280
+ old_layer_id = old_layer_ids.get(component.layer)
281
+ new_layer_id = new_layer_ids.get(component.layer)
282
+ for index in _overlapping_layer_indices(old_buffer, old_layer_indices, old_layer_id, component_bbox):
283
+ if index not in old_indices:
284
+ polygons.append(
285
+ ComponentPolygon(
286
+ side="old",
287
+ source_index=index,
288
+ fragment=_cached_fragment(old_buffer, old_fragment_cache, index),
289
+ role="context",
290
+ )
291
+ )
292
+ for index in _overlapping_layer_indices(new_buffer, new_layer_indices, new_layer_id, component_bbox):
293
+ if index not in new_indices:
294
+ polygons.append(
295
+ ComponentPolygon(
296
+ side="new",
297
+ source_index=index,
298
+ fragment=_cached_fragment(new_buffer, new_fragment_cache, index),
299
+ role="context",
300
+ )
301
+ )
302
+ expanded.append(PolygonComponent(layer=component.layer, polygons=tuple(sorted(polygons, key=_polygon_sort_key))))
303
+ expanded_tuple = tuple(sorted(expanded, key=_component_sort_key))
304
+ return PolygonComponentBuildResult(
305
+ components=expanded_tuple,
306
+ stats=_component_stats(
307
+ expanded_tuple,
308
+ context_index_backend="cpu-layer-bbox-index",
309
+ context_index_s=perf_counter() - context_start,
310
+ ),
311
+ )
312
+
313
+
314
+ def _merge_touching_components_by_layer(components: tuple[PolygonComponent, ...]) -> tuple[PolygonComponent, ...]:
315
+ by_layer: dict[LayerSpec, list[PolygonComponent]] = {}
316
+ for component in components:
317
+ by_layer.setdefault(component.layer, []).append(component)
318
+ merged: list[PolygonComponent] = []
319
+ for layer in sorted(by_layer):
320
+ layer_components = tuple(sorted(by_layer[layer], key=_component_sort_key))
321
+ for group in _component_bbox_contact_groups(layer_components):
322
+ polygons = {}
323
+ for index in group:
324
+ for polygon in layer_components[index].polygons:
325
+ polygons[(polygon.side, polygon.source_index, polygon.role, polygon.fragment.bbox)] = polygon
326
+ merged.append(
327
+ PolygonComponent(
328
+ layer=layer,
329
+ polygons=tuple(sorted(polygons.values(), key=_polygon_sort_key)),
330
+ )
331
+ )
332
+ return tuple(sorted(merged, key=_component_sort_key))
333
+
334
+
335
+ def _component_bbox_contact_groups(components: tuple[PolygonComponent, ...]) -> tuple[tuple[int, ...], ...]:
336
+ remaining = set(range(len(components)))
337
+ groups: list[tuple[int, ...]] = []
338
+ while remaining:
339
+ seed = remaining.pop()
340
+ group = {seed}
341
+ stack = [seed]
342
+ while stack:
343
+ left = stack.pop()
344
+ touched = [
345
+ right
346
+ for right in remaining
347
+ if _bbox_contact(components[left].bbox, components[right].bbox)
348
+ ]
349
+ for right in touched:
350
+ remaining.remove(right)
351
+ group.add(right)
352
+ stack.append(right)
353
+ groups.append(tuple(sorted(group)))
354
+ return tuple(sorted(groups))
355
+
356
+
357
+ def exact_result_from_component_quads(
358
+ components: tuple[PolygonComponent, ...],
359
+ quads: np.ndarray,
360
+ fragment_areas: np.ndarray,
361
+ counts: np.ndarray,
362
+ component_areas: np.ndarray | None = None,
363
+ ) -> ExactOracleResult:
364
+ by_layer: dict[LayerSpec, dict[str, list[DiffFragment]]] = {}
365
+ for component_index, component in enumerate(components):
366
+ layer_bucket = by_layer.setdefault(component.layer, {"old": [], "new": []})
367
+ one_sided = _one_sided_nonoverlap_fragments(component)
368
+ if one_sided is not None:
369
+ side, fragments = one_sided
370
+ layer_bucket[side].extend(fragments)
371
+ continue
372
+ for side_index, side in enumerate(("old", "new")):
373
+ fragments: list[DiffFragment] = []
374
+ for fragment_index in range(int(counts[component_index, side_index])):
375
+ points = tuple((int(x), int(y)) for x, y in quads[component_index, side_index, fragment_index])
376
+ if len(set(points)) < 3:
377
+ continue
378
+ twice_area = int(fragment_areas[component_index, side_index, fragment_index])
379
+ if twice_area == 0:
380
+ continue
381
+ fragments.append(DiffFragment(points=points, bbox=polygon_bbox(points), twice_area=twice_area))
382
+ if fragments:
383
+ assembled = assemble_boundary_loops(tuple(fragments)).fragments
384
+ target_area = (
385
+ int(component_areas[component_index, side_index])
386
+ if component_areas is not None
387
+ else sum(fragment.twice_area for fragment in fragments)
388
+ )
389
+ layer_bucket[side].extend(_preserve_total_area(assembled, target_area))
390
+ layer_diffs = []
391
+ for layer in sorted(by_layer):
392
+ old_minus_new = _merge_layer_side_fragments(tuple(sorted(by_layer[layer]["old"], key=_fragment_sort_key)))
393
+ new_minus_old = _merge_layer_side_fragments(tuple(sorted(by_layer[layer]["new"], key=_fragment_sort_key)))
394
+ layer_diffs.append(
395
+ LayerExactDiff(
396
+ layer=layer,
397
+ old_minus_new=old_minus_new,
398
+ new_minus_old=new_minus_old,
399
+ xor=tuple(sorted(old_minus_new + new_minus_old, key=_fragment_sort_key)),
400
+ )
401
+ )
402
+ return ExactOracleResult(tuple(layer_diffs))
403
+
404
+
405
+ def _fragment_from_buffer(buffer: PolygonBuffer, index: int) -> DiffFragment:
406
+ points = tuple((int(x), int(y)) for x, y in buffer.polygon_vertices(index))
407
+ return DiffFragment(points=points, bbox=polygon_bbox(points), twice_area=polygon_twice_area(points))
408
+
409
+
410
+ def _cached_fragment(buffer: PolygonBuffer, cache: dict[int, DiffFragment], index: int) -> DiffFragment:
411
+ fragment = cache.get(index)
412
+ if fragment is None:
413
+ fragment = _fragment_from_buffer(buffer, index)
414
+ cache[index] = fragment
415
+ return fragment
416
+
417
+
418
+ def _one_sided_nonoverlap_fragments(component: PolygonComponent) -> tuple[str, tuple[DiffFragment, ...]] | None:
419
+ sides = {polygon.side for polygon in component.polygons}
420
+ if len(sides) != 1:
421
+ return None
422
+ fragments = tuple(polygon.fragment for polygon in component.polygons)
423
+ if _has_bbox_contact(fragments):
424
+ return None
425
+ side = next(iter(sides))
426
+ return side, tuple(sorted(fragments, key=_fragment_sort_key))
427
+
428
+
429
+ def _preserve_total_area(fragments: tuple[DiffFragment, ...], target_twice_area: int) -> tuple[DiffFragment, ...]:
430
+ if not fragments:
431
+ return ()
432
+ current = sum(fragment.twice_area for fragment in fragments)
433
+ delta = target_twice_area - current
434
+ if delta == 0:
435
+ return fragments
436
+ largest_index = max(range(len(fragments)), key=lambda index: (fragments[index].twice_area, fragments[index].bbox))
437
+ adjusted = list(fragments)
438
+ largest = adjusted[largest_index]
439
+ if largest.twice_area + delta <= 0:
440
+ return fragments
441
+ adjusted[largest_index] = DiffFragment(points=largest.points, bbox=largest.bbox, twice_area=largest.twice_area + delta)
442
+ return tuple(sorted(adjusted, key=_fragment_sort_key))
443
+
444
+
445
+ def _indices_by_layer_id(buffer: PolygonBuffer) -> dict[int, _LayerBBoxIndex]:
446
+ if buffer.polygon_count == 0:
447
+ return {}
448
+ order = np.argsort(buffer.layer_ids, kind="stable")
449
+ sorted_layer_ids = buffer.layer_ids[order]
450
+ split_points = np.flatnonzero(sorted_layer_ids[1:] != sorted_layer_ids[:-1]) + 1
451
+ buckets = np.split(order.astype(np.int64, copy=False), split_points)
452
+ layer_ids = np.split(sorted_layer_ids, split_points)
453
+ return {int(ids[0]): _build_layer_bbox_index(buffer, bucket) for ids, bucket in zip(layer_ids, buckets) if len(ids)}
454
+
455
+
456
+ def _build_layer_bbox_index(buffer: PolygonBuffer, indices: np.ndarray) -> _LayerBBoxIndex:
457
+ layer_bboxes = buffer.bboxes[indices]
458
+ order = np.argsort(layer_bboxes[:, 0], kind="stable")
459
+ sorted_indices = indices[order].astype(np.int64, copy=False)
460
+ sorted_bboxes = layer_bboxes[order].astype(np.int64, copy=False)
461
+ sorted_x0 = sorted_bboxes[:, 0]
462
+ return _LayerBBoxIndex(indices=indices, sorted_indices=sorted_indices, sorted_x0=sorted_x0, sorted_bboxes=sorted_bboxes)
463
+
464
+
465
+ def _overlapping_layer_indices(
466
+ buffer: PolygonBuffer,
467
+ indices_by_layer: dict[int, _LayerBBoxIndex],
468
+ layer_id: int | None,
469
+ bbox: tuple[int, int, int, int],
470
+ ) -> tuple[int, ...]:
471
+ if layer_id is None:
472
+ return ()
473
+ layer_index = indices_by_layer.get(layer_id)
474
+ if layer_index is None or len(layer_index.indices) == 0:
475
+ return ()
476
+ stop = int(np.searchsorted(layer_index.sorted_x0, bbox[2], side="left"))
477
+ if stop == 0:
478
+ return ()
479
+ indices = layer_index.sorted_indices[:stop]
480
+ bboxes = layer_index.sorted_bboxes[:stop]
481
+ mask = (
482
+ (bboxes[:, 2] > bbox[0])
483
+ & (np.maximum(bboxes[:, 1], bbox[1]) < np.minimum(bboxes[:, 3], bbox[3]))
484
+ )
485
+ return tuple(sorted(int(index) for index in indices[mask]))
486
+
487
+
488
+ def _try_accelerated_context_pairs(
489
+ components: tuple[PolygonComponent, ...],
490
+ buffer: PolygonBuffer,
491
+ *,
492
+ preferred_accelerator: str | None,
493
+ ) -> _ContextOverlapPairRun | None:
494
+ if preferred_accelerator == "cuda":
495
+ return _try_cuda_context_pairs(components, buffer)
496
+ if preferred_accelerator == "metal":
497
+ return _try_metal_context_pairs(components, buffer)
498
+ return _try_cuda_context_pairs(components, buffer) or _try_metal_context_pairs(components, buffer)
499
+
500
+
501
+ def _try_cuda_context_pairs(
502
+ components: tuple[PolygonComponent, ...],
503
+ buffer: PolygonBuffer,
504
+ ) -> _ContextOverlapPairRun | None:
505
+ if not components or buffer.polygon_count == 0:
506
+ return _ContextOverlapPairRun(
507
+ component_indices=np.empty((0,), dtype=np.uint64),
508
+ polygon_indices=np.empty((0,), dtype=np.uint64),
509
+ )
510
+ try:
511
+ from .backends.cuda_ctypes import CudaCtypesUnavailable, run_cuda_context_overlap_pairs
512
+
513
+ component_bboxes, component_layers, component_datatypes = _component_context_arrays(components)
514
+ polygon_layers, polygon_datatypes = _buffer_layer_datatype_arrays(buffer)
515
+ start = perf_counter()
516
+ pairs = run_cuda_context_overlap_pairs(
517
+ component_bboxes,
518
+ component_layers,
519
+ component_datatypes,
520
+ buffer.bboxes,
521
+ polygon_layers,
522
+ polygon_datatypes,
523
+ )
524
+ wall_s = perf_counter() - start
525
+ return _ContextOverlapPairRun(
526
+ component_indices=pairs.component_indices,
527
+ polygon_indices=pairs.polygon_indices,
528
+ wall_s=wall_s,
529
+ cuda_kernel_count=pairs.cuda_kernel_count,
530
+ cuda_event_elapsed_s=pairs.cuda_event_elapsed_s,
531
+ transferred_bytes=pairs.transferred_bytes,
532
+ accelerator="cuda",
533
+ )
534
+ except (CudaCtypesUnavailable, ImportError, OSError, ValueError, OverflowError):
535
+ return None
536
+
537
+
538
+ def _try_metal_context_pairs(
539
+ components: tuple[PolygonComponent, ...],
540
+ buffer: PolygonBuffer,
541
+ ) -> _ContextOverlapPairRun | None:
542
+ if not components or buffer.polygon_count == 0:
543
+ return _ContextOverlapPairRun(
544
+ component_indices=np.empty((0,), dtype=np.uint64),
545
+ polygon_indices=np.empty((0,), dtype=np.uint64),
546
+ accelerator="metal",
547
+ )
548
+ try:
549
+ from .backends.metal_ctypes import MetalCtypesUnavailable, run_metal_context_overlap_pairs
550
+
551
+ component_bboxes, component_layers, component_datatypes = _component_context_arrays(components)
552
+ polygon_layers, polygon_datatypes = _buffer_layer_datatype_arrays(buffer)
553
+ start = perf_counter()
554
+ pairs = run_metal_context_overlap_pairs(
555
+ component_bboxes,
556
+ component_layers,
557
+ component_datatypes,
558
+ buffer.bboxes,
559
+ polygon_layers,
560
+ polygon_datatypes,
561
+ )
562
+ wall_s = perf_counter() - start
563
+ return _ContextOverlapPairRun(
564
+ component_indices=pairs.component_indices,
565
+ polygon_indices=pairs.polygon_indices,
566
+ wall_s=wall_s,
567
+ metal_kernel_count=pairs.metal_kernel_count,
568
+ metal_event_elapsed_s=pairs.metal_event_elapsed_s,
569
+ transferred_bytes=pairs.transferred_bytes,
570
+ accelerator="metal",
571
+ )
572
+ except (MetalCtypesUnavailable, ImportError, OSError, ValueError, OverflowError):
573
+ return None
574
+
575
+
576
+ def _component_context_arrays(
577
+ components: tuple[PolygonComponent, ...],
578
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
579
+ bboxes = np.asarray([component.bbox for component in components], dtype=np.int64).reshape((-1, 4))
580
+ layers = np.asarray([component.layer.layer for component in components], dtype=np.int32)
581
+ datatypes = np.asarray([component.layer.datatype for component in components], dtype=np.int32)
582
+ return (
583
+ np.ascontiguousarray(bboxes, dtype=np.int64),
584
+ np.ascontiguousarray(layers, dtype=np.int32),
585
+ np.ascontiguousarray(datatypes, dtype=np.int32),
586
+ )
587
+
588
+
589
+ def _buffer_layer_datatype_arrays(buffer: PolygonBuffer) -> tuple[np.ndarray, np.ndarray]:
590
+ layer_values = np.asarray([layer.layer for layer in buffer.layer_table.layers], dtype=np.int32)
591
+ datatype_values = np.asarray([layer.datatype for layer in buffer.layer_table.layers], dtype=np.int32)
592
+ return (
593
+ np.ascontiguousarray(layer_values[buffer.layer_ids], dtype=np.int32),
594
+ np.ascontiguousarray(datatype_values[buffer.layer_ids], dtype=np.int32),
595
+ )
596
+
597
+
598
+ def _append_context_pairs(
599
+ expanded_polygons: list[list[ComponentPolygon]],
600
+ pair_run: _ContextOverlapPairRun,
601
+ source_indices: list[set[int]],
602
+ *,
603
+ side: str,
604
+ buffer: PolygonBuffer,
605
+ fragment_cache: dict[int, DiffFragment],
606
+ ) -> None:
607
+ for component_index_raw, polygon_index_raw in zip(pair_run.component_indices, pair_run.polygon_indices):
608
+ component_index = int(component_index_raw)
609
+ polygon_index = int(polygon_index_raw)
610
+ if polygon_index in source_indices[component_index]:
611
+ continue
612
+ expanded_polygons[component_index].append(
613
+ ComponentPolygon(
614
+ side=side,
615
+ source_index=polygon_index,
616
+ fragment=_cached_fragment(buffer, fragment_cache, polygon_index),
617
+ role="context",
618
+ )
619
+ )
620
+
621
+
622
+ def _polygon_components(polygons: tuple[ComponentPolygon, ...]) -> tuple[tuple[int, ...], ...]:
623
+ remaining = set(range(len(polygons)))
624
+ components: list[tuple[int, ...]] = []
625
+ while remaining:
626
+ seed = remaining.pop()
627
+ component = {seed}
628
+ stack = [seed]
629
+ while stack:
630
+ left = stack.pop()
631
+ touched = [
632
+ right
633
+ for right in remaining
634
+ if _positive_overlap(polygons[left].fragment.bbox, polygons[right].fragment.bbox)
635
+ ]
636
+ for right in touched:
637
+ remaining.remove(right)
638
+ component.add(right)
639
+ stack.append(right)
640
+ components.append(tuple(sorted(component)))
641
+ return tuple(sorted(components))
642
+
643
+
644
+ def _component_stats(
645
+ components: tuple[PolygonComponent, ...],
646
+ *,
647
+ context_index_backend: str = "none",
648
+ context_index_s: float = 0.0,
649
+ context_pair_count: int = 0,
650
+ context_cuda_kernel_count: int = 0,
651
+ context_cuda_event_elapsed_s: float = 0.0,
652
+ context_cuda_transferred_bytes: int = 0,
653
+ context_metal_kernel_count: int = 0,
654
+ context_metal_event_elapsed_s: float = 0.0,
655
+ context_metal_transferred_bytes: int = 0,
656
+ ) -> PolygonComponentStats:
657
+ return PolygonComponentStats(
658
+ component_count=len(components),
659
+ overlap_component_count=sum(1 for component in components if component.has_positive_overlap),
660
+ max_fragments_per_component=max((len(component.polygons) for component in components), default=0),
661
+ max_vertices_per_component=max((component.vertex_count for component in components), default=0),
662
+ context_index_backend=context_index_backend,
663
+ context_index_s=context_index_s,
664
+ context_pair_count=context_pair_count,
665
+ context_cuda_kernel_count=context_cuda_kernel_count,
666
+ context_cuda_event_elapsed_s=context_cuda_event_elapsed_s,
667
+ context_cuda_transferred_bytes=context_cuda_transferred_bytes,
668
+ context_metal_kernel_count=context_metal_kernel_count,
669
+ context_metal_event_elapsed_s=context_metal_event_elapsed_s,
670
+ context_metal_transferred_bytes=context_metal_transferred_bytes,
671
+ )
672
+
673
+
674
+ def _merge_layer_side_fragments(fragments: tuple[DiffFragment, ...]) -> tuple[DiffFragment, ...]:
675
+ return _merge_layer_side_fragments_with_stats(fragments).fragments
676
+
677
+
678
+ def _merge_layer_side_fragments_with_stats(fragments: tuple[DiffFragment, ...]) -> LayerSideMergeResult:
679
+ if len(fragments) < 2:
680
+ return LayerSideMergeResult(
681
+ fragments=fragments,
682
+ stats=LayerSideMergeStats(input_fragment_count=len(fragments), output_fragment_count=len(fragments)),
683
+ )
684
+ if len(fragments) > 512 or sum(len(fragment.points) for fragment in fragments) > 50_000:
685
+ return LayerSideMergeResult(
686
+ fragments=fragments,
687
+ stats=LayerSideMergeStats(input_fragment_count=len(fragments), output_fragment_count=len(fragments)),
688
+ )
689
+ merged: list[DiffFragment] = []
690
+ group_count = 0
691
+ assembled_group_count = 0
692
+ cuda_edge_cancel_s = 0.0
693
+ cuda_edge_cancel_kernel_count = 0
694
+ cuda_edge_cancel_event_elapsed_s = 0.0
695
+ cuda_edge_cancel_transferred_bytes = 0
696
+ native_edge_cancel_s = 0.0
697
+ cuda_loop_trace_s = 0.0
698
+ metal_loop_trace_s = 0.0
699
+ metal_loop_kernel_count = 0
700
+ metal_loop_event_elapsed_s = 0.0
701
+ metal_loop_transferred_bytes = 0
702
+ native_loop_trace_s = 0.0
703
+ python_loop_trace_s = 0.0
704
+ hole_stitch_s = 0.0
705
+ for group in _fragment_bbox_components(fragments):
706
+ group_count += 1
707
+ if len(group) == 1:
708
+ merged.append(_canonicalize_single_fragment(group[0]))
709
+ continue
710
+ target_area = sum(fragment.twice_area for fragment in group)
711
+ assembly = assemble_boundary_loops(group)
712
+ assembled = assembly.fragments
713
+ assembled_group_count += 1
714
+ cuda_edge_cancel_s += assembly.stats.cuda_edge_cancel_s
715
+ cuda_edge_cancel_kernel_count += assembly.stats.cuda_edge_cancel_kernel_count
716
+ cuda_edge_cancel_event_elapsed_s += assembly.stats.cuda_edge_cancel_event_elapsed_s
717
+ cuda_edge_cancel_transferred_bytes += assembly.stats.cuda_edge_cancel_transferred_bytes
718
+ native_edge_cancel_s += assembly.stats.native_edge_cancel_s
719
+ cuda_loop_trace_s += assembly.stats.cuda_loop_trace_s
720
+ metal_loop_trace_s += assembly.stats.metal_loop_trace_s
721
+ metal_loop_kernel_count += assembly.stats.metal_loop_kernel_count
722
+ metal_loop_event_elapsed_s += assembly.stats.metal_loop_event_elapsed_s
723
+ metal_loop_transferred_bytes += assembly.stats.metal_loop_transferred_bytes
724
+ native_loop_trace_s += assembly.stats.native_loop_trace_s
725
+ python_loop_trace_s += assembly.stats.python_loop_trace_s
726
+ hole_stitch_s += assembly.stats.hole_stitch_s
727
+ if not assembled:
728
+ merged.extend(group)
729
+ else:
730
+ merged.extend(_preserve_total_area(assembled, target_area))
731
+ output = tuple(sorted(merged, key=_fragment_sort_key))
732
+ return LayerSideMergeResult(
733
+ fragments=output,
734
+ stats=LayerSideMergeStats(
735
+ input_fragment_count=len(fragments),
736
+ output_fragment_count=len(output),
737
+ group_count=group_count,
738
+ assembled_group_count=assembled_group_count,
739
+ cuda_edge_cancel_s=cuda_edge_cancel_s,
740
+ cuda_edge_cancel_kernel_count=cuda_edge_cancel_kernel_count,
741
+ cuda_edge_cancel_event_elapsed_s=cuda_edge_cancel_event_elapsed_s,
742
+ cuda_edge_cancel_transferred_bytes=cuda_edge_cancel_transferred_bytes,
743
+ native_edge_cancel_s=native_edge_cancel_s,
744
+ cuda_loop_trace_s=cuda_loop_trace_s,
745
+ metal_loop_trace_s=metal_loop_trace_s,
746
+ metal_loop_kernel_count=metal_loop_kernel_count,
747
+ metal_loop_event_elapsed_s=metal_loop_event_elapsed_s,
748
+ metal_loop_transferred_bytes=metal_loop_transferred_bytes,
749
+ native_loop_trace_s=native_loop_trace_s,
750
+ python_loop_trace_s=python_loop_trace_s,
751
+ hole_stitch_s=hole_stitch_s,
752
+ ),
753
+ )
754
+
755
+
756
+ def _canonicalize_single_fragment(fragment: DiffFragment) -> DiffFragment:
757
+ points = _clean_loop_points(fragment.points)
758
+ if len(points) < 3:
759
+ return fragment
760
+ twice_area = polygon_twice_area(points)
761
+ if twice_area != fragment.twice_area:
762
+ return fragment
763
+ return DiffFragment(points=points, bbox=polygon_bbox(points), twice_area=twice_area)
764
+
765
+
766
+ def _clean_loop_points(points: tuple[tuple[int, int], ...]) -> tuple[tuple[int, int], ...]:
767
+ cleaned: list[tuple[int, int]] = []
768
+ for point in points:
769
+ if not cleaned or cleaned[-1] != point:
770
+ cleaned.append(point)
771
+ if len(cleaned) > 1 and cleaned[0] == cleaned[-1]:
772
+ cleaned.pop()
773
+ changed = True
774
+ while changed and len(cleaned) >= 3:
775
+ changed = False
776
+ next_points: list[tuple[int, int]] = []
777
+ for index, point in enumerate(cleaned):
778
+ previous = cleaned[index - 1]
779
+ following = cleaned[(index + 1) % len(cleaned)]
780
+ if _collinear(previous, point, following):
781
+ changed = True
782
+ continue
783
+ next_points.append(point)
784
+ cleaned = next_points
785
+ return tuple(cleaned)
786
+
787
+
788
+ def _collinear(
789
+ left: tuple[int, int],
790
+ middle: tuple[int, int],
791
+ right: tuple[int, int],
792
+ ) -> bool:
793
+ return (middle[0] - left[0]) * (right[1] - middle[1]) == (middle[1] - left[1]) * (right[0] - middle[0])
794
+
795
+
796
+ def _fragment_bbox_components(fragments: tuple[DiffFragment, ...]) -> tuple[tuple[DiffFragment, ...], ...]:
797
+ remaining = set(range(len(fragments)))
798
+ components: list[tuple[DiffFragment, ...]] = []
799
+ while remaining:
800
+ seed = remaining.pop()
801
+ component = {seed}
802
+ stack = [seed]
803
+ while stack:
804
+ left = stack.pop()
805
+ touched = [
806
+ right
807
+ for right in remaining
808
+ if _bbox_contact(fragments[left].bbox, fragments[right].bbox)
809
+ ]
810
+ for right in touched:
811
+ remaining.remove(right)
812
+ component.add(right)
813
+ stack.append(right)
814
+ components.append(tuple(fragments[index] for index in sorted(component)))
815
+ return tuple(sorted(components, key=lambda group: (_bbox_union(tuple(fragment.bbox for fragment in group)), len(group))))
816
+
817
+
818
+ def _has_positive_bbox_overlap(fragments: tuple[DiffFragment, ...]) -> bool:
819
+ for left_index, left in enumerate(fragments):
820
+ for right in fragments[left_index + 1 :]:
821
+ if _positive_overlap(left.bbox, right.bbox):
822
+ return True
823
+ return False
824
+
825
+
826
+ def _has_bbox_contact(fragments: tuple[DiffFragment, ...]) -> bool:
827
+ for left_index, left in enumerate(fragments):
828
+ for right in fragments[left_index + 1 :]:
829
+ if _bbox_contact(left.bbox, right.bbox):
830
+ return True
831
+ return False
832
+
833
+
834
+ def _positive_overlap(left: tuple[int, int, int, int], right: tuple[int, int, int, int]) -> bool:
835
+ return max(left[0], right[0]) < min(left[2], right[2]) and max(left[1], right[1]) < min(left[3], right[3])
836
+
837
+
838
+ def _bbox_contact(left: tuple[int, int, int, int], right: tuple[int, int, int, int]) -> bool:
839
+ return max(left[0], right[0]) <= min(left[2], right[2]) and max(left[1], right[1]) <= min(left[3], right[3])
840
+
841
+
842
+ def _bbox_union(bboxes: tuple[tuple[int, int, int, int], ...]) -> tuple[int, int, int, int]:
843
+ return (
844
+ min(bbox[0] for bbox in bboxes),
845
+ min(bbox[1] for bbox in bboxes),
846
+ max(bbox[2] for bbox in bboxes),
847
+ max(bbox[3] for bbox in bboxes),
848
+ )
849
+
850
+
851
+ def _component_sort_key(component: PolygonComponent) -> tuple[int, int, tuple[int, int, int, int], int, int]:
852
+ return component.layer.layer, component.layer.datatype, component.bbox, component.old_count, component.new_count
853
+
854
+
855
+ def _polygon_sort_key(polygon: ComponentPolygon) -> tuple[str, str, int, tuple[int, int, int, int]]:
856
+ return polygon.side, polygon.role, polygon.source_index, polygon.fragment.bbox
857
+
858
+
859
+ def _fragment_sort_key(fragment: DiffFragment) -> tuple[tuple[int, int, int, int], int, tuple[tuple[int, int], ...]]:
860
+ return fragment.bbox, fragment.twice_area, fragment.points