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,1297 @@
1
+ from __future__ import annotations
2
+
3
+ import ctypes
4
+ import os
5
+ import subprocess
6
+ from dataclasses import dataclass
7
+ from functools import lru_cache
8
+ from pathlib import Path
9
+
10
+ import numpy as np
11
+
12
+ from ..config import LayerSpec
13
+ from ..exact_oracle import DiffFragment
14
+ from ..geometry import PolygonBuffer
15
+ from ..native import nvcc_architecture_flags, resolve_nvcc_path
16
+ from ..native_cache import content_addressed_library_path, ensure_cached_native_library, native_cache_dir
17
+ from ..polygon_components import PolygonComponent
18
+ from ..tiling import CpuTilePrefilterResult, OversizedPolygon, TileConfig, TileKey
19
+
20
+
21
+ class CudaCtypesUnavailable(RuntimeError):
22
+ pass
23
+
24
+
25
+ class _CResult(ctypes.Structure):
26
+ _fields_ = [
27
+ ("candidate_tiles", ctypes.POINTER(ctypes.c_int64)),
28
+ ("candidate_count", ctypes.c_uint64),
29
+ ("oversized", ctypes.POINTER(ctypes.c_int64)),
30
+ ("oversized_count", ctypes.c_uint64),
31
+ ("assignment_count", ctypes.c_uint64),
32
+ ("collapsed_pair_count", ctypes.c_uint64),
33
+ ("oversized_matched_count", ctypes.c_uint64),
34
+ ]
35
+
36
+
37
+ class _CByteCompareResult(ctypes.Structure):
38
+ _fields_ = [
39
+ ("old_size", ctypes.c_uint64),
40
+ ("new_size", ctypes.c_uint64),
41
+ ("same", ctypes.c_uint8),
42
+ ]
43
+
44
+
45
+ class _CPolygonRecordDeltaResult(ctypes.Structure):
46
+ _fields_ = [
47
+ ("record_count", ctypes.c_uint64),
48
+ ("layers", ctypes.POINTER(ctypes.c_int32)),
49
+ ("datatypes", ctypes.POINTER(ctypes.c_int32)),
50
+ ("directions", ctypes.POINTER(ctypes.c_uint8)),
51
+ ("counts", ctypes.POINTER(ctypes.c_uint64)),
52
+ ("bboxes", ctypes.POINTER(ctypes.c_int64)),
53
+ ("vertex_counts", ctypes.POINTER(ctypes.c_uint64)),
54
+ ("flags", ctypes.POINTER(ctypes.c_uint32)),
55
+ ("hashes", ctypes.POINTER(ctypes.c_uint64)),
56
+ ("representative_indices", ctypes.POINTER(ctypes.c_uint64)),
57
+ ("old_input_count", ctypes.c_uint64),
58
+ ("new_input_count", ctypes.c_uint64),
59
+ ("matched_record_count", ctypes.c_uint64),
60
+ ("reduced_key_count", ctypes.c_uint64),
61
+ ("cuda_kernel_count", ctypes.c_uint64),
62
+ ("cuda_event_elapsed_ms", ctypes.c_float),
63
+ ]
64
+
65
+
66
+ class _CBoundaryLoopResult(ctypes.Structure):
67
+ _fields_ = [
68
+ ("fragment_count", ctypes.c_uint64),
69
+ ("vertex_count", ctypes.c_uint64),
70
+ ("vertices_xy", ctypes.POINTER(ctypes.c_int64)),
71
+ ("fragment_offsets", ctypes.POINTER(ctypes.c_uint64)),
72
+ ("bboxes", ctypes.POINTER(ctypes.c_int64)),
73
+ ("twice_areas", ctypes.POINTER(ctypes.c_int64)),
74
+ ("dropped_zero_area_loop_count", ctypes.c_uint64),
75
+ ("cuda_kernel_count", ctypes.c_uint64),
76
+ ("cuda_event_elapsed_ms", ctypes.c_float),
77
+ ]
78
+
79
+
80
+ class _CBoundaryEdgesResult(ctypes.Structure):
81
+ _fields_ = [
82
+ ("edge_count", ctypes.c_uint64),
83
+ ("edges", ctypes.POINTER(ctypes.c_int64)),
84
+ ("cuda_kernel_count", ctypes.c_uint64),
85
+ ("cuda_event_elapsed_ms", ctypes.c_float),
86
+ ]
87
+
88
+
89
+ class _CContextOverlapPairsResult(ctypes.Structure):
90
+ _fields_ = [
91
+ ("pair_count", ctypes.c_uint64),
92
+ ("component_indices", ctypes.POINTER(ctypes.c_uint64)),
93
+ ("polygon_indices", ctypes.POINTER(ctypes.c_uint64)),
94
+ ("cuda_kernel_count", ctypes.c_uint64),
95
+ ("cuda_event_elapsed_ms", ctypes.c_float),
96
+ ]
97
+
98
+
99
+ @dataclass(frozen=True)
100
+ class _PackedBuffer:
101
+ bboxes: np.ndarray
102
+ layers: np.ndarray
103
+ datatypes: np.ndarray
104
+ canonical_ids: np.ndarray
105
+
106
+
107
+ @dataclass(frozen=True)
108
+ class CudaByteCompare:
109
+ old_size: int
110
+ new_size: int
111
+ same: bool
112
+ method: str = "cuda-byte-compare"
113
+
114
+
115
+ @dataclass(frozen=True)
116
+ class CudaPolygonRecordDelta:
117
+ layers: np.ndarray
118
+ datatypes: np.ndarray
119
+ directions: np.ndarray
120
+ counts: np.ndarray
121
+ bboxes: np.ndarray
122
+ vertex_counts: np.ndarray
123
+ flags: np.ndarray
124
+ hashes: np.ndarray
125
+ representative_indices: np.ndarray
126
+ old_input_count: int
127
+ new_input_count: int
128
+ matched_record_count: int
129
+ reduced_key_count: int
130
+ cuda_kernel_count: int
131
+ cuda_event_elapsed_s: float
132
+ transferred_bytes: int
133
+
134
+ @property
135
+ def record_count(self) -> int:
136
+ return int(len(self.directions))
137
+
138
+
139
+ @dataclass(frozen=True)
140
+ class CudaBoundaryLoopResult:
141
+ fragments: tuple[DiffFragment, ...]
142
+ dropped_zero_area_loop_count: int
143
+ cuda_kernel_count: int
144
+ cuda_event_elapsed_s: float
145
+ transferred_bytes: int
146
+
147
+
148
+ @dataclass(frozen=True)
149
+ class CudaBoundaryEdgesResult:
150
+ edges: np.ndarray
151
+ cuda_kernel_count: int
152
+ cuda_event_elapsed_s: float
153
+ transferred_bytes: int
154
+
155
+
156
+ @dataclass(frozen=True)
157
+ class CudaContextOverlapPairs:
158
+ component_indices: np.ndarray
159
+ polygon_indices: np.ndarray
160
+ cuda_kernel_count: int
161
+ cuda_event_elapsed_s: float
162
+ transferred_bytes: int
163
+
164
+ @property
165
+ def pair_count(self) -> int:
166
+ return int(len(self.component_indices))
167
+
168
+
169
+ def run_cuda_ctypes_tile_prefilter(
170
+ old_buffer: PolygonBuffer,
171
+ old_canonical_ids,
172
+ new_buffer: PolygonBuffer,
173
+ new_canonical_ids,
174
+ config: TileConfig,
175
+ ) -> CpuTilePrefilterResult:
176
+ library = _load_library()
177
+ old_packed = _pack_buffer(old_buffer, old_canonical_ids)
178
+ new_packed = _pack_buffer(new_buffer, new_canonical_ids)
179
+ old_packed, new_packed, skipped_matched = _skip_exact_tile_bbox_matches(old_packed, new_packed, config)
180
+ result = _CResult()
181
+ status = library.gds_cuda_prefilter(
182
+ _ptr_i64(old_packed.bboxes),
183
+ _ptr_i32(old_packed.layers),
184
+ _ptr_i32(old_packed.datatypes),
185
+ _ptr_u64(old_packed.canonical_ids),
186
+ ctypes.c_uint64(len(old_packed.canonical_ids)),
187
+ _ptr_i64(new_packed.bboxes),
188
+ _ptr_i32(new_packed.layers),
189
+ _ptr_i32(new_packed.datatypes),
190
+ _ptr_u64(new_packed.canonical_ids),
191
+ ctypes.c_uint64(len(new_packed.canonical_ids)),
192
+ ctypes.c_int64(config.tile_size),
193
+ ctypes.c_uint64(config.max_tiles_per_polygon),
194
+ ctypes.byref(result),
195
+ )
196
+ if status != 0:
197
+ message = library.gds_cuda_last_error()
198
+ raise CudaCtypesUnavailable(message.decode("utf-8") if message else "CUDA prefilter failed")
199
+ try:
200
+ candidate_tiles = _candidate_tiles_from_result(result)
201
+ oversized = _oversized_from_result(result)
202
+ return CpuTilePrefilterResult(
203
+ candidate_tiles=candidate_tiles,
204
+ oversized_candidates=oversized,
205
+ assignment_count=int(result.assignment_count),
206
+ collapsed_pair_count=int(result.collapsed_pair_count),
207
+ oversized_matched_count=int(result.oversized_matched_count) + skipped_matched,
208
+ )
209
+ finally:
210
+ library.gds_cuda_free_result(ctypes.byref(result))
211
+
212
+
213
+ def cuda_ctypes_available() -> bool:
214
+ try:
215
+ _load_library()
216
+ except CudaCtypesUnavailable:
217
+ return False
218
+ return True
219
+
220
+
221
+ def release_cuda_device_memory() -> None:
222
+ library = _load_library()
223
+ status = library.gds_cuda_release_device_memory()
224
+ if status != 0:
225
+ message = library.gds_cuda_last_error()
226
+ raise CudaCtypesUnavailable(message.decode("utf-8") if message else "CUDA device memory release failed")
227
+
228
+
229
+ def compare_files_cuda(old_path: str | Path, new_path: str | Path) -> CudaByteCompare:
230
+ old_size = Path(old_path).stat().st_size
231
+ new_size = Path(new_path).stat().st_size
232
+ if old_size != new_size:
233
+ return CudaByteCompare(old_size=old_size, new_size=new_size, same=False)
234
+
235
+ library = _load_library()
236
+ result = _CByteCompareResult()
237
+ status = library.gds_cuda_compare_files(
238
+ str(old_path).encode("utf-8"),
239
+ str(new_path).encode("utf-8"),
240
+ ctypes.byref(result),
241
+ )
242
+ if status != 0:
243
+ message = library.gds_cuda_last_error()
244
+ raise CudaCtypesUnavailable(message.decode("utf-8") if message else "CUDA byte compare failed")
245
+ return CudaByteCompare(old_size=int(result.old_size), new_size=int(result.new_size), same=bool(result.same))
246
+
247
+
248
+ def run_cuda_polygon_record_delta(old_buffer: PolygonBuffer, new_buffer: PolygonBuffer) -> CudaPolygonRecordDelta:
249
+ library = _load_library()
250
+ old_packed = _pack_polygon_record_buffer(old_buffer)
251
+ new_packed = _pack_polygon_record_buffer(new_buffer)
252
+ result = _CPolygonRecordDeltaResult()
253
+ status = library.gds_cuda_polygon_record_delta(
254
+ _ptr_i64(old_packed["vertices"]),
255
+ ctypes.c_uint64(old_buffer.vertex_count),
256
+ _ptr_u64(old_packed["offsets"]),
257
+ _ptr_i32(old_packed["layers"]),
258
+ _ptr_i32(old_packed["datatypes"]),
259
+ _ptr_i64(old_packed["bboxes"]),
260
+ _ptr_u32(old_packed["flags"]),
261
+ ctypes.c_uint64(old_buffer.polygon_count),
262
+ _ptr_i64(new_packed["vertices"]),
263
+ ctypes.c_uint64(new_buffer.vertex_count),
264
+ _ptr_u64(new_packed["offsets"]),
265
+ _ptr_i32(new_packed["layers"]),
266
+ _ptr_i32(new_packed["datatypes"]),
267
+ _ptr_i64(new_packed["bboxes"]),
268
+ _ptr_u32(new_packed["flags"]),
269
+ ctypes.c_uint64(new_buffer.polygon_count),
270
+ ctypes.byref(result),
271
+ )
272
+ if status != 0:
273
+ message = library.gds_cuda_last_error()
274
+ raise CudaCtypesUnavailable(message.decode("utf-8") if message else "CUDA polygon record delta failed")
275
+ try:
276
+ count = int(result.record_count)
277
+ return CudaPolygonRecordDelta(
278
+ layers=_copy_result_array(result.layers, count, np.int32),
279
+ datatypes=_copy_result_array(result.datatypes, count, np.int32),
280
+ directions=_copy_result_array(result.directions, count, np.uint8),
281
+ counts=_copy_result_array(result.counts, count, np.uint64),
282
+ bboxes=_copy_result_array(result.bboxes, count * 4, np.int64).reshape((count, 4)),
283
+ vertex_counts=_copy_result_array(result.vertex_counts, count, np.uint64),
284
+ flags=_copy_result_array(result.flags, count, np.uint32),
285
+ hashes=_copy_result_array(result.hashes, count * 2, np.uint64).reshape((count, 2)),
286
+ representative_indices=_copy_result_array(result.representative_indices, count, np.uint64),
287
+ old_input_count=int(result.old_input_count),
288
+ new_input_count=int(result.new_input_count),
289
+ matched_record_count=int(result.matched_record_count),
290
+ reduced_key_count=int(result.reduced_key_count),
291
+ cuda_kernel_count=int(result.cuda_kernel_count),
292
+ cuda_event_elapsed_s=float(result.cuda_event_elapsed_ms) / 1000.0,
293
+ transferred_bytes=_polygon_record_transfer_bytes(old_packed, new_packed),
294
+ )
295
+ finally:
296
+ library.gds_cuda_free_polygon_record_delta_result(ctypes.byref(result))
297
+
298
+
299
+ def run_cuda_polygon_record_delta_from_hashes(old_buffer: PolygonBuffer, new_buffer: PolygonBuffer) -> CudaPolygonRecordDelta:
300
+ if old_buffer.polygon_hashes is None or new_buffer.polygon_hashes is None:
301
+ return run_cuda_polygon_record_delta(old_buffer, new_buffer)
302
+ library = _load_library()
303
+ old_packed = _pack_polygon_hash_record_buffer(old_buffer)
304
+ new_packed = _pack_polygon_hash_record_buffer(new_buffer)
305
+ result = _CPolygonRecordDeltaResult()
306
+ status = library.gds_cuda_polygon_record_delta_from_hashes(
307
+ _ptr_u64(old_packed["offsets"]),
308
+ _ptr_i32(old_packed["layers"]),
309
+ _ptr_i32(old_packed["datatypes"]),
310
+ _ptr_i64(old_packed["bboxes"]),
311
+ _ptr_u32(old_packed["flags"]),
312
+ _ptr_u64(old_packed["hashes"]),
313
+ ctypes.c_uint64(old_buffer.polygon_count),
314
+ _ptr_u64(new_packed["offsets"]),
315
+ _ptr_i32(new_packed["layers"]),
316
+ _ptr_i32(new_packed["datatypes"]),
317
+ _ptr_i64(new_packed["bboxes"]),
318
+ _ptr_u32(new_packed["flags"]),
319
+ _ptr_u64(new_packed["hashes"]),
320
+ ctypes.c_uint64(new_buffer.polygon_count),
321
+ ctypes.byref(result),
322
+ )
323
+ if status != 0:
324
+ message = library.gds_cuda_last_error()
325
+ raise CudaCtypesUnavailable(message.decode("utf-8") if message else "CUDA polygon hash record delta failed")
326
+ try:
327
+ count = int(result.record_count)
328
+ return CudaPolygonRecordDelta(
329
+ layers=_copy_result_array(result.layers, count, np.int32),
330
+ datatypes=_copy_result_array(result.datatypes, count, np.int32),
331
+ directions=_copy_result_array(result.directions, count, np.uint8),
332
+ counts=_copy_result_array(result.counts, count, np.uint64),
333
+ bboxes=_copy_result_array(result.bboxes, count * 4, np.int64).reshape((count, 4)),
334
+ vertex_counts=_copy_result_array(result.vertex_counts, count, np.uint64),
335
+ flags=_copy_result_array(result.flags, count, np.uint32),
336
+ hashes=_copy_result_array(result.hashes, count * 2, np.uint64).reshape((count, 2)),
337
+ representative_indices=_copy_result_array(result.representative_indices, count, np.uint64),
338
+ old_input_count=int(result.old_input_count),
339
+ new_input_count=int(result.new_input_count),
340
+ matched_record_count=int(result.matched_record_count),
341
+ reduced_key_count=int(result.reduced_key_count),
342
+ cuda_kernel_count=int(result.cuda_kernel_count),
343
+ cuda_event_elapsed_s=float(result.cuda_event_elapsed_ms) / 1000.0,
344
+ transferred_bytes=_polygon_record_transfer_bytes(old_packed, new_packed),
345
+ )
346
+ finally:
347
+ library.gds_cuda_free_polygon_record_delta_result(ctypes.byref(result))
348
+
349
+
350
+ def run_cuda_rect_exact_areas(
351
+ old_rects: np.ndarray,
352
+ new_rects: np.ndarray,
353
+ old_present: np.ndarray,
354
+ new_present: np.ndarray,
355
+ ) -> np.ndarray:
356
+ library = _load_library()
357
+ old_rects = np.ascontiguousarray(old_rects, dtype=np.int64)
358
+ new_rects = np.ascontiguousarray(new_rects, dtype=np.int64)
359
+ old_present = np.ascontiguousarray(old_present, dtype=np.uint8)
360
+ new_present = np.ascontiguousarray(new_present, dtype=np.uint8)
361
+ if old_rects.ndim != 2 or old_rects.shape[1] != 4:
362
+ raise ValueError("old_rects must have shape (N, 4)")
363
+ if new_rects.shape != old_rects.shape:
364
+ raise ValueError("new_rects must match old_rects shape")
365
+ count = old_rects.shape[0]
366
+ if old_present.shape != (count,) or new_present.shape != (count,):
367
+ raise ValueError("presence arrays must have shape (N,)")
368
+ out = np.empty((count, 3), dtype=np.int64)
369
+ status = library.gds_cuda_rect_exact_areas(
370
+ _ptr_i64(old_rects),
371
+ _ptr_i64(new_rects),
372
+ old_present.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
373
+ new_present.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
374
+ ctypes.c_uint64(count),
375
+ _ptr_i64(out),
376
+ )
377
+ if status != 0:
378
+ message = library.gds_cuda_last_error()
379
+ raise CudaCtypesUnavailable(message.decode("utf-8") if message else "CUDA rect exact failed")
380
+ return out
381
+
382
+
383
+ def run_cuda_rect_exact_fragments(
384
+ old_rects: np.ndarray,
385
+ new_rects: np.ndarray,
386
+ old_present: np.ndarray,
387
+ new_present: np.ndarray,
388
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
389
+ library = _load_library()
390
+ old_rects = np.ascontiguousarray(old_rects, dtype=np.int64)
391
+ new_rects = np.ascontiguousarray(new_rects, dtype=np.int64)
392
+ old_present = np.ascontiguousarray(old_present, dtype=np.uint8)
393
+ new_present = np.ascontiguousarray(new_present, dtype=np.uint8)
394
+ if old_rects.ndim != 2 or old_rects.shape[1] != 4:
395
+ raise ValueError("old_rects must have shape (N, 4)")
396
+ if new_rects.shape != old_rects.shape:
397
+ raise ValueError("new_rects must match old_rects shape")
398
+ count = old_rects.shape[0]
399
+ if old_present.shape != (count,) or new_present.shape != (count,):
400
+ raise ValueError("presence arrays must have shape (N,)")
401
+ rects = np.empty((count, 8, 4), dtype=np.int64)
402
+ counts = np.empty((count, 2), dtype=np.uint8)
403
+ areas = np.empty((count, 3), dtype=np.int64)
404
+ status = library.gds_cuda_rect_exact_fragments(
405
+ _ptr_i64(old_rects),
406
+ _ptr_i64(new_rects),
407
+ old_present.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
408
+ new_present.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
409
+ ctypes.c_uint64(count),
410
+ _ptr_i64(rects),
411
+ counts.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
412
+ _ptr_i64(areas),
413
+ )
414
+ if status != 0:
415
+ message = library.gds_cuda_last_error()
416
+ raise CudaCtypesUnavailable(message.decode("utf-8") if message else "CUDA rect exact fragments failed")
417
+ return rects, counts, areas
418
+
419
+
420
+ def run_cuda_rect_set_exact_fragments(
421
+ old_rects: np.ndarray,
422
+ new_rects: np.ndarray,
423
+ old_counts: np.ndarray,
424
+ new_counts: np.ndarray,
425
+ *,
426
+ max_fragments: int = 128,
427
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
428
+ library = _load_library()
429
+ old_rects = np.ascontiguousarray(old_rects, dtype=np.int64)
430
+ new_rects = np.ascontiguousarray(new_rects, dtype=np.int64)
431
+ old_counts = np.ascontiguousarray(old_counts, dtype=np.uint8)
432
+ new_counts = np.ascontiguousarray(new_counts, dtype=np.uint8)
433
+ if old_rects.ndim != 3 or old_rects.shape[2] != 4:
434
+ raise ValueError("old_rects must have shape (N, max_rects, 4)")
435
+ if new_rects.shape != old_rects.shape:
436
+ raise ValueError("new_rects must match old_rects shape")
437
+ task_count, max_rects, _ = old_rects.shape
438
+ if old_counts.shape != (task_count,) or new_counts.shape != (task_count,):
439
+ raise ValueError("count arrays must have shape (N,)")
440
+ if max_rects <= 0 or max_rects > 16:
441
+ raise ValueError("max_rects must be between 1 and 16")
442
+ if max_fragments <= 0 or max_fragments > 4096:
443
+ raise ValueError("max_fragments must be between 1 and 4096")
444
+ rects = np.empty((task_count, 2, max_fragments, 4), dtype=np.int64)
445
+ counts = np.empty((task_count, 2), dtype=np.uint16)
446
+ areas = np.empty((task_count, 3), dtype=np.int64)
447
+ status = library.gds_cuda_rect_set_exact_fragments(
448
+ _ptr_i64(old_rects),
449
+ _ptr_i64(new_rects),
450
+ old_counts.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
451
+ new_counts.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
452
+ ctypes.c_uint64(task_count),
453
+ ctypes.c_uint16(max_rects),
454
+ ctypes.c_uint16(max_fragments),
455
+ _ptr_i64(rects),
456
+ counts.ctypes.data_as(ctypes.POINTER(ctypes.c_uint16)),
457
+ _ptr_i64(areas),
458
+ )
459
+ if status != 0:
460
+ message = library.gds_cuda_last_error()
461
+ raise CudaCtypesUnavailable(message.decode("utf-8") if message else "CUDA rect set exact fragments failed")
462
+ return rects, counts, areas
463
+
464
+
465
+ def run_cuda_polygon_component_areas(components: tuple[PolygonComponent, ...]) -> np.ndarray:
466
+ library = _load_library()
467
+ packed = _pack_polygon_components(components)
468
+ component_count = len(components)
469
+ areas = np.zeros((component_count, 3), dtype=np.int64)
470
+ status = library.gds_cuda_polygon_component_areas(
471
+ _ptr_i64(packed["vertices"]),
472
+ ctypes.c_uint64(len(packed["vertices"]) // 2),
473
+ _ptr_u64(packed["polygon_offsets"]),
474
+ packed["polygon_sides"].ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
475
+ ctypes.c_uint64(len(packed["polygon_sides"])),
476
+ _ptr_u64(packed["component_offsets"]),
477
+ ctypes.c_uint64(component_count),
478
+ _ptr_i64(areas),
479
+ )
480
+ if status != 0:
481
+ message = library.gds_cuda_last_error()
482
+ raise CudaCtypesUnavailable(message.decode("utf-8") if message else "CUDA polygon component areas failed")
483
+ return areas
484
+
485
+
486
+ def run_cuda_polygon_component_fragments(
487
+ components: tuple[PolygonComponent, ...],
488
+ *,
489
+ max_fragments: int = 4096,
490
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
491
+ library = _load_library()
492
+ if max_fragments <= 0 or max_fragments > 65535:
493
+ raise ValueError("max_fragments must be between 1 and 65535")
494
+ packed = _pack_polygon_components(components)
495
+ component_count = len(components)
496
+ quads = np.zeros((component_count, 2, max_fragments, 4, 2), dtype=np.int64)
497
+ fragment_areas = np.zeros((component_count, 2, max_fragments), dtype=np.int64)
498
+ counts = np.zeros((component_count, 2), dtype=np.uint16)
499
+ areas = np.zeros((component_count, 3), dtype=np.int64)
500
+ status = library.gds_cuda_polygon_component_fragments(
501
+ _ptr_i64(packed["vertices"]),
502
+ ctypes.c_uint64(len(packed["vertices"]) // 2),
503
+ _ptr_u64(packed["polygon_offsets"]),
504
+ packed["polygon_sides"].ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
505
+ ctypes.c_uint64(len(packed["polygon_sides"])),
506
+ _ptr_u64(packed["component_offsets"]),
507
+ ctypes.c_uint64(component_count),
508
+ ctypes.c_uint16(max_fragments),
509
+ _ptr_i64(quads),
510
+ _ptr_i64(fragment_areas),
511
+ counts.ctypes.data_as(ctypes.POINTER(ctypes.c_uint16)),
512
+ _ptr_i64(areas),
513
+ )
514
+ if status != 0:
515
+ message = library.gds_cuda_last_error()
516
+ raise CudaCtypesUnavailable(message.decode("utf-8") if message else "CUDA polygon component fragments failed")
517
+ return quads, fragment_areas, counts, areas
518
+
519
+
520
+ def run_cuda_one_sided_boundary_edges(
521
+ vertices_xy: np.ndarray,
522
+ polygon_offsets: np.ndarray,
523
+ bboxes: np.ndarray,
524
+ signed_twice_areas: np.ndarray,
525
+ segment_points: np.ndarray,
526
+ segment_polygon_indices: np.ndarray,
527
+ ) -> np.ndarray:
528
+ library = _load_library()
529
+ vertices_xy = np.ascontiguousarray(vertices_xy, dtype=np.int64)
530
+ polygon_offsets = np.ascontiguousarray(polygon_offsets, dtype=np.uint64)
531
+ bboxes = np.ascontiguousarray(bboxes, dtype=np.int64)
532
+ signed_twice_areas = np.ascontiguousarray(signed_twice_areas, dtype=np.int64)
533
+ segment_points = np.ascontiguousarray(segment_points, dtype=np.float64)
534
+ segment_polygon_indices = np.ascontiguousarray(segment_polygon_indices, dtype=np.uint32)
535
+ if vertices_xy.ndim != 2 or vertices_xy.shape[1] != 2:
536
+ raise ValueError("vertices_xy must have shape (N, 2)")
537
+ if polygon_offsets.ndim != 1 or len(polygon_offsets) < 1:
538
+ raise ValueError("polygon_offsets must be a non-empty vector")
539
+ polygon_count = len(polygon_offsets) - 1
540
+ if bboxes.shape != (polygon_count, 4):
541
+ raise ValueError("bboxes must have shape (polygon_count, 4)")
542
+ if signed_twice_areas.shape != (polygon_count,):
543
+ raise ValueError("signed_twice_areas must have shape (polygon_count,)")
544
+ if segment_points.ndim != 2 or segment_points.shape[1] != 4:
545
+ raise ValueError("segment_points must have shape (segment_count, 4)")
546
+ segment_count = len(segment_points)
547
+ if segment_polygon_indices.shape != (segment_count,):
548
+ raise ValueError("segment_polygon_indices must have shape (segment_count,)")
549
+ out_edges = np.zeros((segment_count, 4), dtype=np.float64)
550
+ out_keep = np.zeros((segment_count,), dtype=np.uint8)
551
+ status = library.gds_cuda_one_sided_boundary_edges(
552
+ _ptr_i64(vertices_xy.reshape((-1,))),
553
+ ctypes.c_uint64(len(vertices_xy)),
554
+ _ptr_u64(polygon_offsets),
555
+ _ptr_i64(bboxes.reshape((-1,))),
556
+ _ptr_i64(signed_twice_areas),
557
+ ctypes.c_uint64(polygon_count),
558
+ _ptr_f64(segment_points.reshape((-1,))),
559
+ segment_polygon_indices.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)),
560
+ ctypes.c_uint64(segment_count),
561
+ _ptr_f64(out_edges.reshape((-1,))),
562
+ out_keep.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
563
+ )
564
+ if status != 0:
565
+ message = library.gds_cuda_last_error()
566
+ raise CudaCtypesUnavailable(message.decode("utf-8") if message else "CUDA one-sided boundary edges failed")
567
+ return out_edges[out_keep.astype(bool)].reshape((-1, 2, 2))
568
+
569
+
570
+ def run_cuda_mixed_boundary_edges(
571
+ vertices_xy: np.ndarray,
572
+ polygon_offsets: np.ndarray,
573
+ bboxes: np.ndarray,
574
+ polygon_sides: np.ndarray,
575
+ segment_points: np.ndarray,
576
+ *,
577
+ direction: str,
578
+ ) -> np.ndarray:
579
+ library = _load_library()
580
+ vertices_xy = np.ascontiguousarray(vertices_xy, dtype=np.int64)
581
+ polygon_offsets = np.ascontiguousarray(polygon_offsets, dtype=np.uint64)
582
+ bboxes = np.ascontiguousarray(bboxes, dtype=np.int64)
583
+ polygon_sides = np.ascontiguousarray(polygon_sides, dtype=np.uint8)
584
+ segment_points = np.ascontiguousarray(segment_points, dtype=np.float64)
585
+ if direction not in {"old", "new"}:
586
+ raise ValueError("direction must be 'old' or 'new'")
587
+ if vertices_xy.ndim != 2 or vertices_xy.shape[1] != 2:
588
+ raise ValueError("vertices_xy must have shape (N, 2)")
589
+ if polygon_offsets.ndim != 1 or len(polygon_offsets) < 1:
590
+ raise ValueError("polygon_offsets must be a non-empty vector")
591
+ polygon_count = len(polygon_offsets) - 1
592
+ if bboxes.shape != (polygon_count, 4):
593
+ raise ValueError("bboxes must have shape (polygon_count, 4)")
594
+ if polygon_sides.shape != (polygon_count,):
595
+ raise ValueError("polygon_sides must have shape (polygon_count,)")
596
+ if segment_points.ndim != 2 or segment_points.shape[1] != 4:
597
+ raise ValueError("segment_points must have shape (segment_count, 4)")
598
+ segment_count = len(segment_points)
599
+ out_edges = np.zeros((segment_count, 4), dtype=np.float64)
600
+ out_keep = np.zeros((segment_count,), dtype=np.uint8)
601
+ status = library.gds_cuda_mixed_boundary_edges(
602
+ _ptr_i64(vertices_xy.reshape((-1,))),
603
+ ctypes.c_uint64(len(vertices_xy)),
604
+ _ptr_u64(polygon_offsets),
605
+ _ptr_i64(bboxes.reshape((-1,))),
606
+ polygon_sides.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
607
+ ctypes.c_uint64(polygon_count),
608
+ _ptr_f64(segment_points.reshape((-1,))),
609
+ ctypes.c_uint64(segment_count),
610
+ ctypes.c_uint8(1 if direction == "old" else 2),
611
+ _ptr_f64(out_edges.reshape((-1,))),
612
+ out_keep.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
613
+ )
614
+ if status != 0:
615
+ message = library.gds_cuda_last_error()
616
+ raise CudaCtypesUnavailable(message.decode("utf-8") if message else "CUDA mixed boundary edges failed")
617
+ return out_edges[out_keep.astype(bool)].reshape((-1, 2, 2))
618
+
619
+
620
+ def run_cuda_one_sided_boundary_edges_unsplit_if_safe(
621
+ vertices_xy: np.ndarray,
622
+ polygon_offsets: np.ndarray,
623
+ bboxes: np.ndarray,
624
+ signed_twice_areas: np.ndarray,
625
+ segment_points: np.ndarray,
626
+ segment_polygon_indices: np.ndarray,
627
+ ) -> tuple[np.ndarray, bool]:
628
+ library = _load_library()
629
+ vertices_xy = np.ascontiguousarray(vertices_xy, dtype=np.int64)
630
+ polygon_offsets = np.ascontiguousarray(polygon_offsets, dtype=np.uint64)
631
+ bboxes = np.ascontiguousarray(bboxes, dtype=np.int64)
632
+ signed_twice_areas = np.ascontiguousarray(signed_twice_areas, dtype=np.int64)
633
+ segment_points = np.ascontiguousarray(segment_points, dtype=np.float64)
634
+ segment_polygon_indices = np.ascontiguousarray(segment_polygon_indices, dtype=np.uint32)
635
+ if vertices_xy.ndim != 2 or vertices_xy.shape[1] != 2:
636
+ raise ValueError("vertices_xy must have shape (N, 2)")
637
+ if polygon_offsets.ndim != 1 or len(polygon_offsets) < 1:
638
+ raise ValueError("polygon_offsets must be a non-empty vector")
639
+ polygon_count = len(polygon_offsets) - 1
640
+ if bboxes.shape != (polygon_count, 4):
641
+ raise ValueError("bboxes must have shape (polygon_count, 4)")
642
+ if signed_twice_areas.shape != (polygon_count,):
643
+ raise ValueError("signed_twice_areas must have shape (polygon_count,)")
644
+ if segment_points.ndim != 2 or segment_points.shape[1] != 4:
645
+ raise ValueError("segment_points must have shape (segment_count, 4)")
646
+ segment_count = len(segment_points)
647
+ if segment_polygon_indices.shape != (segment_count,):
648
+ raise ValueError("segment_polygon_indices must have shape (segment_count,)")
649
+ out_edges = np.zeros((segment_count, 4), dtype=np.float64)
650
+ out_keep = np.zeros((segment_count,), dtype=np.uint8)
651
+ out_safe = ctypes.c_uint8(0)
652
+ status = library.gds_cuda_one_sided_boundary_edges_unsplit_if_safe(
653
+ _ptr_i64(vertices_xy.reshape((-1,))),
654
+ ctypes.c_uint64(len(vertices_xy)),
655
+ _ptr_u64(polygon_offsets),
656
+ _ptr_i64(bboxes.reshape((-1,))),
657
+ _ptr_i64(signed_twice_areas),
658
+ ctypes.c_uint64(polygon_count),
659
+ _ptr_f64(segment_points.reshape((-1,))),
660
+ segment_polygon_indices.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)),
661
+ ctypes.c_uint64(segment_count),
662
+ _ptr_f64(out_edges.reshape((-1,))),
663
+ out_keep.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
664
+ ctypes.byref(out_safe),
665
+ )
666
+ if status != 0:
667
+ message = library.gds_cuda_last_error()
668
+ raise CudaCtypesUnavailable(message.decode("utf-8") if message else "CUDA one-sided unsplit boundary edges failed")
669
+ return out_edges[out_keep.astype(bool)].reshape((-1, 2, 2)), bool(out_safe.value)
670
+
671
+
672
+ def run_cuda_boundary_loops_from_edges(edges: np.ndarray) -> CudaBoundaryLoopResult:
673
+ library = _load_library()
674
+ edges = np.ascontiguousarray(edges, dtype=np.int64)
675
+ if edges.ndim != 2 or edges.shape[1] != 4:
676
+ raise ValueError("edges must have shape (N, 4)")
677
+ result = _CBoundaryLoopResult()
678
+ status = library.gds_cuda_boundary_loops_from_edges(
679
+ _ptr_i64(edges.reshape((-1,))),
680
+ ctypes.c_uint64(len(edges)),
681
+ ctypes.byref(result),
682
+ )
683
+ if status != 0:
684
+ message = library.gds_cuda_last_error()
685
+ raise CudaCtypesUnavailable(message.decode("utf-8") if message else "CUDA boundary loop tracing failed")
686
+ try:
687
+ count = int(result.fragment_count)
688
+ vertex_count = int(result.vertex_count)
689
+ vertices = _copy_result_array(result.vertices_xy, vertex_count * 2, np.int64).reshape((vertex_count, 2))
690
+ offsets = _copy_result_array(result.fragment_offsets, count + 1, np.uint64)
691
+ bboxes = _copy_result_array(result.bboxes, count * 4, np.int64).reshape((count, 4))
692
+ twice_areas = _copy_result_array(result.twice_areas, count, np.int64)
693
+ fragments = []
694
+ for index in range(count):
695
+ start = int(offsets[index])
696
+ stop = int(offsets[index + 1])
697
+ points = tuple((int(x), int(y)) for x, y in vertices[start:stop])
698
+ bbox = tuple(int(value) for value in bboxes[index])
699
+ fragments.append(DiffFragment(points=points, bbox=bbox, twice_area=int(twice_areas[index])))
700
+ return CudaBoundaryLoopResult(
701
+ fragments=tuple(sorted(fragments, key=lambda fragment: (fragment.bbox, fragment.twice_area, fragment.points))),
702
+ dropped_zero_area_loop_count=int(result.dropped_zero_area_loop_count),
703
+ cuda_kernel_count=int(result.cuda_kernel_count),
704
+ cuda_event_elapsed_s=float(result.cuda_event_elapsed_ms) / 1000.0,
705
+ transferred_bytes=int(edges.nbytes),
706
+ )
707
+ finally:
708
+ library.gds_cuda_free_boundary_loop_result(ctypes.byref(result))
709
+
710
+
711
+ def run_cuda_axis_aligned_boundary_edges_from_fragments(fragments: tuple[DiffFragment, ...]) -> CudaBoundaryEdgesResult:
712
+ library = _load_library()
713
+ vertices, offsets = _pack_diff_fragments(fragments)
714
+ result = _CBoundaryEdgesResult()
715
+ status = library.gds_cuda_axis_aligned_boundary_edges_from_polygons(
716
+ _ptr_i64(vertices.reshape((-1,))),
717
+ _ptr_u64(offsets),
718
+ ctypes.c_uint64(len(fragments)),
719
+ ctypes.byref(result),
720
+ )
721
+ if status != 0:
722
+ message = library.gds_cuda_last_error()
723
+ raise CudaCtypesUnavailable(message.decode("utf-8") if message else "CUDA axis boundary edge assembly failed")
724
+ try:
725
+ count = int(result.edge_count)
726
+ return CudaBoundaryEdgesResult(
727
+ edges=_copy_result_array(result.edges, count * 4, np.int64).reshape((count, 4)),
728
+ cuda_kernel_count=int(result.cuda_kernel_count),
729
+ cuda_event_elapsed_s=float(result.cuda_event_elapsed_ms) / 1000.0,
730
+ transferred_bytes=int(vertices.nbytes + offsets.nbytes),
731
+ )
732
+ finally:
733
+ library.gds_cuda_free_boundary_edges_result(ctypes.byref(result))
734
+
735
+
736
+ def run_cuda_split_free_boundary_edges_from_fragments(fragments: tuple[DiffFragment, ...]) -> CudaBoundaryEdgesResult:
737
+ library = _load_library()
738
+ vertices, offsets = _pack_diff_fragments(fragments)
739
+ result = _CBoundaryEdgesResult()
740
+ status = library.gds_cuda_split_free_boundary_edges_from_polygons(
741
+ _ptr_i64(vertices.reshape((-1,))),
742
+ _ptr_u64(offsets),
743
+ ctypes.c_uint64(len(fragments)),
744
+ ctypes.byref(result),
745
+ )
746
+ if status != 0:
747
+ message = library.gds_cuda_last_error()
748
+ raise CudaCtypesUnavailable(message.decode("utf-8") if message else "CUDA split-free boundary edge assembly failed")
749
+ try:
750
+ count = int(result.edge_count)
751
+ return CudaBoundaryEdgesResult(
752
+ edges=_copy_result_array(result.edges, count * 4, np.int64).reshape((count, 4)),
753
+ cuda_kernel_count=int(result.cuda_kernel_count),
754
+ cuda_event_elapsed_s=float(result.cuda_event_elapsed_ms) / 1000.0,
755
+ transferred_bytes=int(vertices.nbytes + offsets.nbytes),
756
+ )
757
+ finally:
758
+ library.gds_cuda_free_boundary_edges_result(ctypes.byref(result))
759
+
760
+
761
+ def run_cuda_context_overlap_pairs(
762
+ component_bboxes: np.ndarray,
763
+ component_layers: np.ndarray,
764
+ component_datatypes: np.ndarray,
765
+ polygon_bboxes: np.ndarray,
766
+ polygon_layers: np.ndarray,
767
+ polygon_datatypes: np.ndarray,
768
+ ) -> CudaContextOverlapPairs:
769
+ library = _load_library()
770
+ component_bboxes = np.ascontiguousarray(component_bboxes, dtype=np.int64)
771
+ component_layers = np.ascontiguousarray(component_layers, dtype=np.int32)
772
+ component_datatypes = np.ascontiguousarray(component_datatypes, dtype=np.int32)
773
+ polygon_bboxes = np.ascontiguousarray(polygon_bboxes, dtype=np.int64)
774
+ polygon_layers = np.ascontiguousarray(polygon_layers, dtype=np.int32)
775
+ polygon_datatypes = np.ascontiguousarray(polygon_datatypes, dtype=np.int32)
776
+ if component_bboxes.ndim != 2 or component_bboxes.shape[1] != 4:
777
+ raise ValueError("component_bboxes must have shape (N, 4)")
778
+ if polygon_bboxes.ndim != 2 or polygon_bboxes.shape[1] != 4:
779
+ raise ValueError("polygon_bboxes must have shape (M, 4)")
780
+ component_count = component_bboxes.shape[0]
781
+ polygon_count = polygon_bboxes.shape[0]
782
+ if component_layers.shape != (component_count,) or component_datatypes.shape != (component_count,):
783
+ raise ValueError("component layer/datatype arrays must have shape (N,)")
784
+ if polygon_layers.shape != (polygon_count,) or polygon_datatypes.shape != (polygon_count,):
785
+ raise ValueError("polygon layer/datatype arrays must have shape (M,)")
786
+ result = _CContextOverlapPairsResult()
787
+ status = library.gds_cuda_context_overlap_pairs(
788
+ _ptr_i64(component_bboxes.reshape((-1,))),
789
+ _ptr_i32(component_layers),
790
+ _ptr_i32(component_datatypes),
791
+ ctypes.c_uint64(component_count),
792
+ _ptr_i64(polygon_bboxes.reshape((-1,))),
793
+ _ptr_i32(polygon_layers),
794
+ _ptr_i32(polygon_datatypes),
795
+ ctypes.c_uint64(polygon_count),
796
+ ctypes.byref(result),
797
+ )
798
+ if status != 0:
799
+ message = library.gds_cuda_last_error()
800
+ raise CudaCtypesUnavailable(message.decode("utf-8") if message else "CUDA context overlap pairs failed")
801
+ try:
802
+ count = int(result.pair_count)
803
+ return CudaContextOverlapPairs(
804
+ component_indices=_copy_result_array(result.component_indices, count, np.uint64),
805
+ polygon_indices=_copy_result_array(result.polygon_indices, count, np.uint64),
806
+ cuda_kernel_count=int(result.cuda_kernel_count),
807
+ cuda_event_elapsed_s=float(result.cuda_event_elapsed_ms) / 1000.0,
808
+ transferred_bytes=int(
809
+ component_bboxes.nbytes
810
+ + component_layers.nbytes
811
+ + component_datatypes.nbytes
812
+ + polygon_bboxes.nbytes
813
+ + polygon_layers.nbytes
814
+ + polygon_datatypes.nbytes
815
+ ),
816
+ )
817
+ finally:
818
+ library.gds_cuda_free_context_overlap_pairs_result(ctypes.byref(result))
819
+
820
+
821
+ def _candidate_tiles_from_result(result: _CResult) -> tuple[TileKey, ...]:
822
+ if result.candidate_count == 0:
823
+ return ()
824
+ array = np.ctypeslib.as_array(result.candidate_tiles, shape=(int(result.candidate_count) * 4,))
825
+ tiles = [
826
+ TileKey(LayerSpec(int(array[index]), int(array[index + 1])), int(array[index + 2]), int(array[index + 3]))
827
+ for index in range(0, len(array), 4)
828
+ ]
829
+ return tuple(tiles)
830
+
831
+
832
+ def _oversized_from_result(result: _CResult) -> tuple[OversizedPolygon, ...]:
833
+ if result.oversized_count == 0:
834
+ return ()
835
+ array = np.ctypeslib.as_array(result.oversized, shape=(int(result.oversized_count) * 8,))
836
+ items = [
837
+ OversizedPolygon(
838
+ layer=LayerSpec(int(array[index]), int(array[index + 1])),
839
+ canonical_id=int(array[index + 2]),
840
+ side_mask=int(array[index + 3]),
841
+ tile_bbox=(int(array[index + 4]), int(array[index + 5]), int(array[index + 6]), int(array[index + 7])),
842
+ )
843
+ for index in range(0, len(array), 8)
844
+ ]
845
+ return tuple(items)
846
+
847
+
848
+ def _pack_buffer(buffer: PolygonBuffer, canonical_ids) -> _PackedBuffer:
849
+ ids = np.ascontiguousarray(canonical_ids, dtype=np.uint64)
850
+ if len(ids) != buffer.polygon_count:
851
+ raise ValueError("canonical id vector length must match polygon count")
852
+ layers = np.empty(buffer.polygon_count, dtype=np.int32)
853
+ datatypes = np.empty(buffer.polygon_count, dtype=np.int32)
854
+ for index, layer_id in enumerate(buffer.layer_ids):
855
+ layer = buffer.layer_table.layers[int(layer_id)]
856
+ layers[index] = layer.layer
857
+ datatypes[index] = layer.datatype
858
+ return _PackedBuffer(
859
+ bboxes=np.ascontiguousarray(buffer.bboxes, dtype=np.int64),
860
+ layers=np.ascontiguousarray(layers, dtype=np.int32),
861
+ datatypes=np.ascontiguousarray(datatypes, dtype=np.int32),
862
+ canonical_ids=ids,
863
+ )
864
+
865
+
866
+ def _skip_exact_tile_bbox_matches(old: _PackedBuffer, new: _PackedBuffer, config: TileConfig) -> tuple[_PackedBuffer, _PackedBuffer, int]:
867
+ side_masks: dict[tuple[int, int, int, int, int, int, int], int] = {}
868
+ old_first: dict[tuple[int, int, int, int, int, int, int], int] = {}
869
+ new_first: dict[tuple[int, int, int, int, int, int, int], int] = {}
870
+ for index, key in enumerate(_tile_bbox_keys(old, config.tile_size)):
871
+ side_masks[key] = side_masks.get(key, 0) | 1
872
+ old_first.setdefault(key, index)
873
+ for index, key in enumerate(_tile_bbox_keys(new, config.tile_size)):
874
+ side_masks[key] = side_masks.get(key, 0) | 2
875
+ new_first.setdefault(key, index)
876
+ old_keep = [old_first[key] for key, mask in side_masks.items() if mask == 1]
877
+ new_keep = [new_first[key] for key, mask in side_masks.items() if mask == 2]
878
+ skipped = sum(1 for mask in side_masks.values() if mask == 3)
879
+ return _take_packed(old, old_keep), _take_packed(new, new_keep), skipped
880
+
881
+
882
+ def _tile_bbox_keys(packed: _PackedBuffer, tile_size: int):
883
+ tx0 = np.floor_divide(packed.bboxes[:, 0], tile_size)
884
+ ty0 = np.floor_divide(packed.bboxes[:, 1], tile_size)
885
+ tx1 = np.floor_divide(packed.bboxes[:, 2], tile_size)
886
+ ty1 = np.floor_divide(packed.bboxes[:, 3], tile_size)
887
+ for index in range(len(packed.canonical_ids)):
888
+ yield (
889
+ int(packed.layers[index]),
890
+ int(packed.datatypes[index]),
891
+ int(packed.canonical_ids[index]),
892
+ int(tx0[index]),
893
+ int(ty0[index]),
894
+ int(tx1[index]),
895
+ int(ty1[index]),
896
+ )
897
+
898
+
899
+ def _take_packed(packed: _PackedBuffer, indices: list[int]) -> _PackedBuffer:
900
+ if not indices:
901
+ return _PackedBuffer(
902
+ bboxes=np.empty((0, 4), dtype=np.int64),
903
+ layers=np.empty((0,), dtype=np.int32),
904
+ datatypes=np.empty((0,), dtype=np.int32),
905
+ canonical_ids=np.empty((0,), dtype=np.uint64),
906
+ )
907
+ index_array = np.asarray(indices, dtype=np.int64)
908
+ return _PackedBuffer(
909
+ bboxes=np.ascontiguousarray(packed.bboxes[index_array], dtype=np.int64),
910
+ layers=np.ascontiguousarray(packed.layers[index_array], dtype=np.int32),
911
+ datatypes=np.ascontiguousarray(packed.datatypes[index_array], dtype=np.int32),
912
+ canonical_ids=np.ascontiguousarray(packed.canonical_ids[index_array], dtype=np.uint64),
913
+ )
914
+
915
+
916
+ def _ptr_i64(array: np.ndarray):
917
+ return array.ctypes.data_as(ctypes.POINTER(ctypes.c_int64))
918
+
919
+
920
+ def _ptr_i32(array: np.ndarray):
921
+ return array.ctypes.data_as(ctypes.POINTER(ctypes.c_int32))
922
+
923
+
924
+ def _ptr_u64(array: np.ndarray):
925
+ return array.ctypes.data_as(ctypes.POINTER(ctypes.c_uint64))
926
+
927
+
928
+ def _ptr_u32(array: np.ndarray):
929
+ return array.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32))
930
+
931
+
932
+ def _ptr_f64(array: np.ndarray):
933
+ return array.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
934
+
935
+
936
+ def _copy_result_array(pointer, count: int, dtype) -> np.ndarray:
937
+ if count == 0:
938
+ return np.empty((0,), dtype=dtype)
939
+ if not pointer:
940
+ raise CudaCtypesUnavailable("CUDA result returned null output array")
941
+ ctype = np.ctypeslib.as_ctypes_type(dtype)
942
+ array = ctypes.cast(pointer, ctypes.POINTER(ctype * count)).contents
943
+ return np.frombuffer(array, dtype=dtype).copy()
944
+
945
+
946
+ def _pack_polygon_record_buffer(buffer: PolygonBuffer) -> dict[str, np.ndarray]:
947
+ layer_values = np.asarray([layer.layer for layer in buffer.layer_table.layers], dtype=np.int32)
948
+ datatype_values = np.asarray([layer.datatype for layer in buffer.layer_table.layers], dtype=np.int32)
949
+ layers = np.ascontiguousarray(layer_values[buffer.layer_ids], dtype=np.int32)
950
+ datatypes = np.ascontiguousarray(datatype_values[buffer.layer_ids], dtype=np.int32)
951
+ return {
952
+ "vertices": np.ascontiguousarray(buffer.vertices_xy.reshape((-1,)), dtype=np.int64),
953
+ "offsets": np.ascontiguousarray(buffer.polygon_offsets, dtype=np.uint64),
954
+ "layers": layers,
955
+ "datatypes": datatypes,
956
+ "bboxes": np.ascontiguousarray(buffer.bboxes.reshape((-1,)), dtype=np.int64),
957
+ "flags": np.ascontiguousarray(buffer.flags, dtype=np.uint32),
958
+ }
959
+
960
+
961
+ def _pack_polygon_hash_record_buffer(buffer: PolygonBuffer) -> dict[str, np.ndarray]:
962
+ if buffer.polygon_hashes is None:
963
+ raise CudaCtypesUnavailable("native polygon hashes are unavailable")
964
+ layer_values = np.asarray([layer.layer for layer in buffer.layer_table.layers], dtype=np.int32)
965
+ datatype_values = np.asarray([layer.datatype for layer in buffer.layer_table.layers], dtype=np.int32)
966
+ layers = np.ascontiguousarray(layer_values[buffer.layer_ids], dtype=np.int32)
967
+ datatypes = np.ascontiguousarray(datatype_values[buffer.layer_ids], dtype=np.int32)
968
+ return {
969
+ "offsets": np.ascontiguousarray(buffer.polygon_offsets, dtype=np.uint64),
970
+ "layers": layers,
971
+ "datatypes": datatypes,
972
+ "bboxes": np.ascontiguousarray(buffer.bboxes.reshape((-1,)), dtype=np.int64),
973
+ "flags": np.ascontiguousarray(buffer.flags, dtype=np.uint32),
974
+ "hashes": np.ascontiguousarray(buffer.polygon_hashes.reshape((-1,)), dtype=np.uint64),
975
+ }
976
+
977
+
978
+ def _pack_polygon_components(components: tuple[PolygonComponent, ...]) -> dict[str, np.ndarray]:
979
+ vertices: list[tuple[int, int]] = []
980
+ polygon_offsets = [0]
981
+ polygon_sides: list[int] = []
982
+ component_offsets = [0]
983
+ polygon_count = 0
984
+ for component in components:
985
+ for polygon in component.polygons:
986
+ vertices.extend(polygon.fragment.points)
987
+ polygon_offsets.append(len(vertices))
988
+ polygon_sides.append(1 if polygon.side == "old" else 2)
989
+ polygon_count += 1
990
+ component_offsets.append(polygon_count)
991
+ return {
992
+ "vertices": np.ascontiguousarray(np.asarray(vertices, dtype=np.int64).reshape((-1,)), dtype=np.int64),
993
+ "polygon_offsets": np.ascontiguousarray(np.asarray(polygon_offsets, dtype=np.uint64), dtype=np.uint64),
994
+ "polygon_sides": np.ascontiguousarray(np.asarray(polygon_sides, dtype=np.uint8), dtype=np.uint8),
995
+ "component_offsets": np.ascontiguousarray(np.asarray(component_offsets, dtype=np.uint64), dtype=np.uint64),
996
+ }
997
+
998
+
999
+ def _pack_diff_fragments(fragments: tuple[DiffFragment, ...]) -> tuple[np.ndarray, np.ndarray]:
1000
+ vertices: list[tuple[int, int]] = []
1001
+ offsets = [0]
1002
+ for fragment in fragments:
1003
+ vertices.extend(fragment.points)
1004
+ offsets.append(len(vertices))
1005
+ return (
1006
+ np.ascontiguousarray(np.asarray(vertices, dtype=np.int64).reshape((-1, 2)), dtype=np.int64),
1007
+ np.ascontiguousarray(np.asarray(offsets, dtype=np.uint64), dtype=np.uint64),
1008
+ )
1009
+
1010
+
1011
+ def _polygon_record_transfer_bytes(*packed_buffers: dict[str, np.ndarray]) -> int:
1012
+ return sum(int(array.nbytes) for packed in packed_buffers for array in packed.values())
1013
+
1014
+
1015
+ @lru_cache(maxsize=1)
1016
+ def _load_library() -> ctypes.CDLL:
1017
+ path = _library_path()
1018
+ library = ctypes.CDLL(str(path))
1019
+ library.gds_cuda_prefilter.argtypes = [
1020
+ ctypes.POINTER(ctypes.c_int64),
1021
+ ctypes.POINTER(ctypes.c_int32),
1022
+ ctypes.POINTER(ctypes.c_int32),
1023
+ ctypes.POINTER(ctypes.c_uint64),
1024
+ ctypes.c_uint64,
1025
+ ctypes.POINTER(ctypes.c_int64),
1026
+ ctypes.POINTER(ctypes.c_int32),
1027
+ ctypes.POINTER(ctypes.c_int32),
1028
+ ctypes.POINTER(ctypes.c_uint64),
1029
+ ctypes.c_uint64,
1030
+ ctypes.c_int64,
1031
+ ctypes.c_uint64,
1032
+ ctypes.POINTER(_CResult),
1033
+ ]
1034
+ library.gds_cuda_prefilter.restype = ctypes.c_int
1035
+ library.gds_cuda_last_error.argtypes = []
1036
+ library.gds_cuda_last_error.restype = ctypes.c_char_p
1037
+ library.gds_cuda_release_device_memory.argtypes = []
1038
+ library.gds_cuda_release_device_memory.restype = ctypes.c_int
1039
+ library.gds_cuda_free_result.argtypes = [ctypes.POINTER(_CResult)]
1040
+ library.gds_cuda_free_result.restype = None
1041
+ library.gds_cuda_compare_files.argtypes = [
1042
+ ctypes.c_char_p,
1043
+ ctypes.c_char_p,
1044
+ ctypes.POINTER(_CByteCompareResult),
1045
+ ]
1046
+ library.gds_cuda_compare_files.restype = ctypes.c_int
1047
+ library.gds_cuda_polygon_record_delta.argtypes = [
1048
+ ctypes.POINTER(ctypes.c_int64),
1049
+ ctypes.c_uint64,
1050
+ ctypes.POINTER(ctypes.c_uint64),
1051
+ ctypes.POINTER(ctypes.c_int32),
1052
+ ctypes.POINTER(ctypes.c_int32),
1053
+ ctypes.POINTER(ctypes.c_int64),
1054
+ ctypes.POINTER(ctypes.c_uint32),
1055
+ ctypes.c_uint64,
1056
+ ctypes.POINTER(ctypes.c_int64),
1057
+ ctypes.c_uint64,
1058
+ ctypes.POINTER(ctypes.c_uint64),
1059
+ ctypes.POINTER(ctypes.c_int32),
1060
+ ctypes.POINTER(ctypes.c_int32),
1061
+ ctypes.POINTER(ctypes.c_int64),
1062
+ ctypes.POINTER(ctypes.c_uint32),
1063
+ ctypes.c_uint64,
1064
+ ctypes.POINTER(_CPolygonRecordDeltaResult),
1065
+ ]
1066
+ library.gds_cuda_polygon_record_delta.restype = ctypes.c_int
1067
+ library.gds_cuda_polygon_record_delta_from_hashes.argtypes = [
1068
+ ctypes.POINTER(ctypes.c_uint64),
1069
+ ctypes.POINTER(ctypes.c_int32),
1070
+ ctypes.POINTER(ctypes.c_int32),
1071
+ ctypes.POINTER(ctypes.c_int64),
1072
+ ctypes.POINTER(ctypes.c_uint32),
1073
+ ctypes.POINTER(ctypes.c_uint64),
1074
+ ctypes.c_uint64,
1075
+ ctypes.POINTER(ctypes.c_uint64),
1076
+ ctypes.POINTER(ctypes.c_int32),
1077
+ ctypes.POINTER(ctypes.c_int32),
1078
+ ctypes.POINTER(ctypes.c_int64),
1079
+ ctypes.POINTER(ctypes.c_uint32),
1080
+ ctypes.POINTER(ctypes.c_uint64),
1081
+ ctypes.c_uint64,
1082
+ ctypes.POINTER(_CPolygonRecordDeltaResult),
1083
+ ]
1084
+ library.gds_cuda_polygon_record_delta_from_hashes.restype = ctypes.c_int
1085
+ library.gds_cuda_free_polygon_record_delta_result.argtypes = [ctypes.POINTER(_CPolygonRecordDeltaResult)]
1086
+ library.gds_cuda_free_polygon_record_delta_result.restype = None
1087
+ library.gds_cuda_boundary_loops_from_edges.argtypes = [
1088
+ ctypes.POINTER(ctypes.c_int64),
1089
+ ctypes.c_uint64,
1090
+ ctypes.POINTER(_CBoundaryLoopResult),
1091
+ ]
1092
+ library.gds_cuda_boundary_loops_from_edges.restype = ctypes.c_int
1093
+ library.gds_cuda_free_boundary_loop_result.argtypes = [ctypes.POINTER(_CBoundaryLoopResult)]
1094
+ library.gds_cuda_free_boundary_loop_result.restype = None
1095
+ library.gds_cuda_axis_aligned_boundary_edges_from_polygons.argtypes = [
1096
+ ctypes.POINTER(ctypes.c_int64),
1097
+ ctypes.POINTER(ctypes.c_uint64),
1098
+ ctypes.c_uint64,
1099
+ ctypes.POINTER(_CBoundaryEdgesResult),
1100
+ ]
1101
+ library.gds_cuda_axis_aligned_boundary_edges_from_polygons.restype = ctypes.c_int
1102
+ library.gds_cuda_split_free_boundary_edges_from_polygons.argtypes = [
1103
+ ctypes.POINTER(ctypes.c_int64),
1104
+ ctypes.POINTER(ctypes.c_uint64),
1105
+ ctypes.c_uint64,
1106
+ ctypes.POINTER(_CBoundaryEdgesResult),
1107
+ ]
1108
+ library.gds_cuda_split_free_boundary_edges_from_polygons.restype = ctypes.c_int
1109
+ library.gds_cuda_free_boundary_edges_result.argtypes = [ctypes.POINTER(_CBoundaryEdgesResult)]
1110
+ library.gds_cuda_free_boundary_edges_result.restype = None
1111
+ library.gds_cuda_context_overlap_pairs.argtypes = [
1112
+ ctypes.POINTER(ctypes.c_int64),
1113
+ ctypes.POINTER(ctypes.c_int32),
1114
+ ctypes.POINTER(ctypes.c_int32),
1115
+ ctypes.c_uint64,
1116
+ ctypes.POINTER(ctypes.c_int64),
1117
+ ctypes.POINTER(ctypes.c_int32),
1118
+ ctypes.POINTER(ctypes.c_int32),
1119
+ ctypes.c_uint64,
1120
+ ctypes.POINTER(_CContextOverlapPairsResult),
1121
+ ]
1122
+ library.gds_cuda_context_overlap_pairs.restype = ctypes.c_int
1123
+ library.gds_cuda_free_context_overlap_pairs_result.argtypes = [ctypes.POINTER(_CContextOverlapPairsResult)]
1124
+ library.gds_cuda_free_context_overlap_pairs_result.restype = None
1125
+ library.gds_cuda_rect_exact_areas.argtypes = [
1126
+ ctypes.POINTER(ctypes.c_int64),
1127
+ ctypes.POINTER(ctypes.c_int64),
1128
+ ctypes.POINTER(ctypes.c_uint8),
1129
+ ctypes.POINTER(ctypes.c_uint8),
1130
+ ctypes.c_uint64,
1131
+ ctypes.POINTER(ctypes.c_int64),
1132
+ ]
1133
+ library.gds_cuda_rect_exact_areas.restype = ctypes.c_int
1134
+ library.gds_cuda_rect_exact_fragments.argtypes = [
1135
+ ctypes.POINTER(ctypes.c_int64),
1136
+ ctypes.POINTER(ctypes.c_int64),
1137
+ ctypes.POINTER(ctypes.c_uint8),
1138
+ ctypes.POINTER(ctypes.c_uint8),
1139
+ ctypes.c_uint64,
1140
+ ctypes.POINTER(ctypes.c_int64),
1141
+ ctypes.POINTER(ctypes.c_uint8),
1142
+ ctypes.POINTER(ctypes.c_int64),
1143
+ ]
1144
+ library.gds_cuda_rect_exact_fragments.restype = ctypes.c_int
1145
+ library.gds_cuda_polygon_component_areas.argtypes = [
1146
+ ctypes.POINTER(ctypes.c_int64),
1147
+ ctypes.c_uint64,
1148
+ ctypes.POINTER(ctypes.c_uint64),
1149
+ ctypes.POINTER(ctypes.c_uint8),
1150
+ ctypes.c_uint64,
1151
+ ctypes.POINTER(ctypes.c_uint64),
1152
+ ctypes.c_uint64,
1153
+ ctypes.POINTER(ctypes.c_int64),
1154
+ ]
1155
+ library.gds_cuda_polygon_component_areas.restype = ctypes.c_int
1156
+ library.gds_cuda_polygon_component_fragments.argtypes = [
1157
+ ctypes.POINTER(ctypes.c_int64),
1158
+ ctypes.c_uint64,
1159
+ ctypes.POINTER(ctypes.c_uint64),
1160
+ ctypes.POINTER(ctypes.c_uint8),
1161
+ ctypes.c_uint64,
1162
+ ctypes.POINTER(ctypes.c_uint64),
1163
+ ctypes.c_uint64,
1164
+ ctypes.c_uint16,
1165
+ ctypes.POINTER(ctypes.c_int64),
1166
+ ctypes.POINTER(ctypes.c_int64),
1167
+ ctypes.POINTER(ctypes.c_uint16),
1168
+ ctypes.POINTER(ctypes.c_int64),
1169
+ ]
1170
+ library.gds_cuda_polygon_component_fragments.restype = ctypes.c_int
1171
+ library.gds_cuda_one_sided_boundary_edges.argtypes = [
1172
+ ctypes.POINTER(ctypes.c_int64),
1173
+ ctypes.c_uint64,
1174
+ ctypes.POINTER(ctypes.c_uint64),
1175
+ ctypes.POINTER(ctypes.c_int64),
1176
+ ctypes.POINTER(ctypes.c_int64),
1177
+ ctypes.c_uint64,
1178
+ ctypes.POINTER(ctypes.c_double),
1179
+ ctypes.POINTER(ctypes.c_uint32),
1180
+ ctypes.c_uint64,
1181
+ ctypes.POINTER(ctypes.c_double),
1182
+ ctypes.POINTER(ctypes.c_uint8),
1183
+ ]
1184
+ library.gds_cuda_one_sided_boundary_edges.restype = ctypes.c_int
1185
+ library.gds_cuda_mixed_boundary_edges.argtypes = [
1186
+ ctypes.POINTER(ctypes.c_int64),
1187
+ ctypes.c_uint64,
1188
+ ctypes.POINTER(ctypes.c_uint64),
1189
+ ctypes.POINTER(ctypes.c_int64),
1190
+ ctypes.POINTER(ctypes.c_uint8),
1191
+ ctypes.c_uint64,
1192
+ ctypes.POINTER(ctypes.c_double),
1193
+ ctypes.c_uint64,
1194
+ ctypes.c_uint8,
1195
+ ctypes.POINTER(ctypes.c_double),
1196
+ ctypes.POINTER(ctypes.c_uint8),
1197
+ ]
1198
+ library.gds_cuda_mixed_boundary_edges.restype = ctypes.c_int
1199
+ library.gds_cuda_one_sided_boundary_edges_unsplit_if_safe.argtypes = [
1200
+ ctypes.POINTER(ctypes.c_int64),
1201
+ ctypes.c_uint64,
1202
+ ctypes.POINTER(ctypes.c_uint64),
1203
+ ctypes.POINTER(ctypes.c_int64),
1204
+ ctypes.POINTER(ctypes.c_int64),
1205
+ ctypes.c_uint64,
1206
+ ctypes.POINTER(ctypes.c_double),
1207
+ ctypes.POINTER(ctypes.c_uint32),
1208
+ ctypes.c_uint64,
1209
+ ctypes.POINTER(ctypes.c_double),
1210
+ ctypes.POINTER(ctypes.c_uint8),
1211
+ ctypes.POINTER(ctypes.c_uint8),
1212
+ ]
1213
+ library.gds_cuda_one_sided_boundary_edges_unsplit_if_safe.restype = ctypes.c_int
1214
+ library.gds_cuda_rect_set_exact_fragments.argtypes = [
1215
+ ctypes.POINTER(ctypes.c_int64),
1216
+ ctypes.POINTER(ctypes.c_int64),
1217
+ ctypes.POINTER(ctypes.c_uint8),
1218
+ ctypes.POINTER(ctypes.c_uint8),
1219
+ ctypes.c_uint64,
1220
+ ctypes.c_uint16,
1221
+ ctypes.c_uint16,
1222
+ ctypes.POINTER(ctypes.c_int64),
1223
+ ctypes.POINTER(ctypes.c_uint16),
1224
+ ctypes.POINTER(ctypes.c_int64),
1225
+ ]
1226
+ library.gds_cuda_rect_set_exact_fragments.restype = ctypes.c_int
1227
+ return library
1228
+
1229
+
1230
+ def _build_library(path: Path) -> None:
1231
+ source = _source_path()
1232
+ architecture_flags = nvcc_architecture_flags()
1233
+ path.parent.mkdir(parents=True, exist_ok=True)
1234
+ nvcc = resolve_nvcc_path()
1235
+ if nvcc is None:
1236
+ raise CudaCtypesUnavailable("nvcc is not available; install the CUDA toolkit or set CUDA_HOME")
1237
+ command = [
1238
+ nvcc,
1239
+ "-std=c++17",
1240
+ "-O3",
1241
+ "--shared",
1242
+ "-Xcompiler",
1243
+ "-fPIC",
1244
+ *architecture_flags,
1245
+ str(source),
1246
+ "-o",
1247
+ str(path),
1248
+ ]
1249
+ try:
1250
+ completed = subprocess.run(command, check=False, text=True, capture_output=True)
1251
+ except FileNotFoundError as exc:
1252
+ raise CudaCtypesUnavailable("nvcc is not available") from exc
1253
+ if completed.returncode != 0:
1254
+ raise CudaCtypesUnavailable((completed.stderr or completed.stdout).strip() or "nvcc failed")
1255
+
1256
+
1257
+ def _library_path() -> Path:
1258
+ build_key = _cuda_build_key()
1259
+ return ensure_cached_native_library(
1260
+ "_gds_compare_cuda_ctypes.so",
1261
+ sources=(_source_path(),),
1262
+ build_key=build_key,
1263
+ builder=_build_library,
1264
+ )
1265
+
1266
+
1267
+ def prepared_cuda_library_path() -> Path:
1268
+ """Return the current toolchain/device cache target without building it."""
1269
+
1270
+ return _prepared_cuda_library_path_cached(_cuda_build_key(), str(native_cache_dir()))
1271
+
1272
+
1273
+ @lru_cache(maxsize=16)
1274
+ def _prepared_cuda_library_path_cached(
1275
+ build_key: tuple[str, ...], cache_directory: str
1276
+ ) -> Path:
1277
+ return content_addressed_library_path(
1278
+ "_gds_compare_cuda_ctypes.so", sources=(_source_path(),), build_key=build_key
1279
+ )
1280
+
1281
+
1282
+ def _cuda_build_key() -> tuple[str, ...]:
1283
+ return (
1284
+ resolve_nvcc_path() or "nvcc",
1285
+ "-std=c++17",
1286
+ "-O3",
1287
+ "--shared",
1288
+ "-Xcompiler",
1289
+ "-fPIC",
1290
+ *nvcc_architecture_flags(),
1291
+ f"NVCC_PREPEND_FLAGS={os.environ.get('NVCC_PREPEND_FLAGS', '')}",
1292
+ f"NVCC_APPEND_FLAGS={os.environ.get('NVCC_APPEND_FLAGS', '')}",
1293
+ )
1294
+
1295
+
1296
+ def _source_path() -> Path:
1297
+ return Path(__file__).resolve().parents[1] / "native_src" / "gdsdiff" / "cuda_ctypes.cu"