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,866 @@
1
+ from __future__ import annotations
2
+
3
+ import ctypes
4
+ import platform
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_cache import ensure_cached_native_library
16
+ from ..tiling import CpuTilePrefilterResult, OversizedPolygon, TileConfig, TileKey
17
+
18
+
19
+ class MetalCtypesUnavailable(RuntimeError):
20
+ pass
21
+
22
+
23
+ class _CPrefilterResult(ctypes.Structure):
24
+ _fields_ = [
25
+ ("candidate_tiles", ctypes.POINTER(ctypes.c_int64)),
26
+ ("candidate_count", ctypes.c_uint64),
27
+ ("oversized", ctypes.POINTER(ctypes.c_int64)),
28
+ ("oversized_count", ctypes.c_uint64),
29
+ ("assignment_count", ctypes.c_uint64),
30
+ ("collapsed_pair_count", ctypes.c_uint64),
31
+ ("oversized_matched_count", ctypes.c_uint64),
32
+ ("metal_kernel_count", ctypes.c_uint64),
33
+ ("metal_event_elapsed_ms", ctypes.c_float),
34
+ ]
35
+
36
+
37
+ class _CPolygonRecordDeltaResult(ctypes.Structure):
38
+ _fields_ = [
39
+ ("record_count", ctypes.c_uint64),
40
+ ("layers", ctypes.POINTER(ctypes.c_int32)),
41
+ ("datatypes", ctypes.POINTER(ctypes.c_int32)),
42
+ ("directions", ctypes.POINTER(ctypes.c_uint8)),
43
+ ("counts", ctypes.POINTER(ctypes.c_uint64)),
44
+ ("bboxes", ctypes.POINTER(ctypes.c_int64)),
45
+ ("vertex_counts", ctypes.POINTER(ctypes.c_uint64)),
46
+ ("flags", ctypes.POINTER(ctypes.c_uint32)),
47
+ ("hashes", ctypes.POINTER(ctypes.c_uint64)),
48
+ ("representative_indices", ctypes.POINTER(ctypes.c_uint64)),
49
+ ("old_input_count", ctypes.c_uint64),
50
+ ("new_input_count", ctypes.c_uint64),
51
+ ("matched_record_count", ctypes.c_uint64),
52
+ ("reduced_key_count", ctypes.c_uint64),
53
+ ("metal_kernel_count", ctypes.c_uint64),
54
+ ("metal_event_elapsed_ms", ctypes.c_float),
55
+ ]
56
+
57
+
58
+ class _CByteCompareResult(ctypes.Structure):
59
+ _fields_ = [
60
+ ("old_size", ctypes.c_uint64),
61
+ ("new_size", ctypes.c_uint64),
62
+ ("same", ctypes.c_uint8),
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
+ ("metal_kernel_count", ctypes.c_uint64),
76
+ ("metal_event_elapsed_ms", ctypes.c_float),
77
+ ]
78
+
79
+
80
+ class _CContextOverlapPairsResult(ctypes.Structure):
81
+ _fields_ = [
82
+ ("pair_count", ctypes.c_uint64),
83
+ ("component_indices", ctypes.POINTER(ctypes.c_uint64)),
84
+ ("polygon_indices", ctypes.POINTER(ctypes.c_uint64)),
85
+ ("metal_kernel_count", ctypes.c_uint64),
86
+ ("metal_event_elapsed_ms", ctypes.c_float),
87
+ ]
88
+
89
+
90
+ @dataclass(frozen=True)
91
+ class _PackedBuffer:
92
+ bboxes: np.ndarray
93
+ layers: np.ndarray
94
+ datatypes: np.ndarray
95
+ canonical_ids: np.ndarray
96
+
97
+
98
+ @dataclass(frozen=True)
99
+ class MetalByteCompare:
100
+ old_size: int
101
+ new_size: int
102
+ same: bool
103
+ method: str = "metal-byte-compare"
104
+
105
+
106
+ @dataclass(frozen=True)
107
+ class MetalPolygonRecordDelta:
108
+ layers: np.ndarray
109
+ datatypes: np.ndarray
110
+ directions: np.ndarray
111
+ counts: np.ndarray
112
+ bboxes: np.ndarray
113
+ vertex_counts: np.ndarray
114
+ flags: np.ndarray
115
+ hashes: np.ndarray
116
+ representative_indices: np.ndarray
117
+ old_input_count: int
118
+ new_input_count: int
119
+ matched_record_count: int
120
+ reduced_key_count: int
121
+ metal_kernel_count: int
122
+ metal_event_elapsed_s: float
123
+ transferred_bytes: int
124
+ accelerator: str = "metal"
125
+
126
+ @property
127
+ def record_count(self) -> int:
128
+ return int(len(self.directions))
129
+
130
+ @property
131
+ def cuda_kernel_count(self) -> int:
132
+ return 0
133
+
134
+ @property
135
+ def cuda_event_elapsed_s(self) -> float:
136
+ return 0.0
137
+
138
+
139
+ @dataclass(frozen=True)
140
+ class MetalBoundaryLoopResult:
141
+ fragments: tuple[DiffFragment, ...]
142
+ dropped_zero_area_loop_count: int
143
+ metal_kernel_count: int
144
+ metal_event_elapsed_s: float
145
+ transferred_bytes: int
146
+ accelerator: str = "metal"
147
+
148
+ @property
149
+ def cuda_kernel_count(self) -> int:
150
+ return 0
151
+
152
+ @property
153
+ def cuda_event_elapsed_s(self) -> float:
154
+ return 0.0
155
+
156
+
157
+ @dataclass(frozen=True)
158
+ class MetalContextOverlapPairs:
159
+ component_indices: np.ndarray
160
+ polygon_indices: np.ndarray
161
+ metal_kernel_count: int
162
+ metal_event_elapsed_s: float
163
+ transferred_bytes: int
164
+ accelerator: str = "metal"
165
+
166
+ @property
167
+ def pair_count(self) -> int:
168
+ return int(len(self.component_indices))
169
+
170
+ @property
171
+ def cuda_kernel_count(self) -> int:
172
+ return 0
173
+
174
+ @property
175
+ def cuda_event_elapsed_s(self) -> float:
176
+ return 0.0
177
+
178
+
179
+ def metal_ctypes_available() -> bool:
180
+ try:
181
+ library = _load_library()
182
+ return bool(library.gds_metal_available())
183
+ except MetalCtypesUnavailable:
184
+ return False
185
+
186
+
187
+ def release_metal_device_memory() -> None:
188
+ library = _load_library()
189
+ status = library.gds_metal_release_device_memory()
190
+ if status != 0:
191
+ message = library.gds_metal_last_error()
192
+ raise MetalCtypesUnavailable(message.decode("utf-8") if message else "Metal device memory release failed")
193
+
194
+
195
+ def compare_files_metal(old_path: str | Path, new_path: str | Path) -> MetalByteCompare:
196
+ old_size = Path(old_path).stat().st_size
197
+ new_size = Path(new_path).stat().st_size
198
+ if old_size != new_size:
199
+ return MetalByteCompare(old_size=old_size, new_size=new_size, same=False)
200
+
201
+ library = _load_library()
202
+ result = _CByteCompareResult()
203
+ status = library.gds_metal_compare_files(
204
+ str(old_path).encode("utf-8"),
205
+ str(new_path).encode("utf-8"),
206
+ ctypes.byref(result),
207
+ )
208
+ if status != 0:
209
+ message = library.gds_metal_last_error()
210
+ raise MetalCtypesUnavailable(message.decode("utf-8") if message else "Metal byte compare failed")
211
+ return MetalByteCompare(old_size=int(result.old_size), new_size=int(result.new_size), same=bool(result.same))
212
+
213
+
214
+ def run_metal_ctypes_tile_prefilter(
215
+ old_buffer: PolygonBuffer,
216
+ old_canonical_ids,
217
+ new_buffer: PolygonBuffer,
218
+ new_canonical_ids,
219
+ config: TileConfig,
220
+ ) -> CpuTilePrefilterResult:
221
+ old_packed = _pack_buffer(old_buffer, old_canonical_ids)
222
+ new_packed = _pack_buffer(new_buffer, new_canonical_ids)
223
+ library = _load_library()
224
+ result = _CPrefilterResult()
225
+ status = library.gds_metal_prefilter(
226
+ _ptr_i64(old_packed.bboxes),
227
+ _ptr_i32(old_packed.layers),
228
+ _ptr_i32(old_packed.datatypes),
229
+ _ptr_u64(old_packed.canonical_ids),
230
+ ctypes.c_uint64(len(old_packed.canonical_ids)),
231
+ _ptr_i64(new_packed.bboxes),
232
+ _ptr_i32(new_packed.layers),
233
+ _ptr_i32(new_packed.datatypes),
234
+ _ptr_u64(new_packed.canonical_ids),
235
+ ctypes.c_uint64(len(new_packed.canonical_ids)),
236
+ ctypes.c_int64(config.tile_size),
237
+ ctypes.c_uint64(config.max_tiles_per_polygon),
238
+ ctypes.byref(result),
239
+ )
240
+ if status != 0:
241
+ message = library.gds_metal_last_error()
242
+ raise MetalCtypesUnavailable(message.decode("utf-8") if message else "Metal tile prefilter failed")
243
+ try:
244
+ return CpuTilePrefilterResult(
245
+ candidate_tiles=_candidate_tiles_from_result(result),
246
+ oversized_candidates=_oversized_from_result(result),
247
+ assignment_count=int(result.assignment_count),
248
+ collapsed_pair_count=int(result.collapsed_pair_count),
249
+ oversized_matched_count=int(result.oversized_matched_count),
250
+ )
251
+ finally:
252
+ library.gds_metal_free_prefilter_result(ctypes.byref(result))
253
+
254
+
255
+ def run_metal_rect_set_exact_fragments(
256
+ old_rects: np.ndarray,
257
+ new_rects: np.ndarray,
258
+ old_counts: np.ndarray,
259
+ new_counts: np.ndarray,
260
+ *,
261
+ max_fragments: int = 4096,
262
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
263
+ library = _load_library()
264
+ old_rects = np.ascontiguousarray(old_rects, dtype=np.int64)
265
+ new_rects = np.ascontiguousarray(new_rects, dtype=np.int64)
266
+ old_counts = np.ascontiguousarray(old_counts, dtype=np.uint8)
267
+ new_counts = np.ascontiguousarray(new_counts, dtype=np.uint8)
268
+ if old_rects.ndim != 3 or old_rects.shape[2] != 4:
269
+ raise ValueError("old_rects must have shape (N, max_rects, 4)")
270
+ if new_rects.shape != old_rects.shape:
271
+ raise ValueError("new_rects must match old_rects shape")
272
+ task_count, max_rects, _ = old_rects.shape
273
+ if old_counts.shape != (task_count,) or new_counts.shape != (task_count,):
274
+ raise ValueError("count arrays must have shape (N,)")
275
+ if max_rects <= 0 or max_rects > 16:
276
+ raise ValueError("max_rects must be between 1 and 16")
277
+ if max_fragments <= 0 or max_fragments > 4096:
278
+ raise ValueError("max_fragments must be between 1 and 4096")
279
+ rects = np.zeros((task_count, 2, max_fragments, 4), dtype=np.int64)
280
+ counts = np.zeros((task_count, 2), dtype=np.uint16)
281
+ areas = np.zeros((task_count, 3), dtype=np.int64)
282
+ status = library.gds_metal_rect_set_exact_fragments(
283
+ _ptr_i64(old_rects),
284
+ _ptr_i64(new_rects),
285
+ old_counts.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
286
+ new_counts.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
287
+ ctypes.c_uint64(task_count),
288
+ ctypes.c_uint16(max_rects),
289
+ ctypes.c_uint16(max_fragments),
290
+ _ptr_i64(rects),
291
+ counts.ctypes.data_as(ctypes.POINTER(ctypes.c_uint16)),
292
+ _ptr_i64(areas),
293
+ )
294
+ if status != 0:
295
+ message = library.gds_metal_last_error()
296
+ raise MetalCtypesUnavailable(message.decode("utf-8") if message else "Metal rectangle set exact failed")
297
+ return rects, counts, areas
298
+
299
+
300
+ def run_metal_polygon_record_delta_from_hashes(old_buffer: PolygonBuffer, new_buffer: PolygonBuffer) -> MetalPolygonRecordDelta:
301
+ if old_buffer.polygon_hashes is None or new_buffer.polygon_hashes is None:
302
+ raise MetalCtypesUnavailable("native polygon hashes are unavailable")
303
+ total_count = old_buffer.polygon_count + new_buffer.polygon_count
304
+ max_records = _record_delta_max_polygons()
305
+ if total_count > max_records:
306
+ raise MetalCtypesUnavailable(
307
+ f"Metal polygon record delta is limited to {max_records} total polygons; got {total_count}"
308
+ )
309
+ library = _load_library()
310
+ old_packed = _pack_polygon_hash_record_buffer(old_buffer)
311
+ new_packed = _pack_polygon_hash_record_buffer(new_buffer)
312
+ result = _CPolygonRecordDeltaResult()
313
+ status = library.gds_metal_polygon_record_delta_from_hashes(
314
+ _ptr_u64(old_packed["offsets"]),
315
+ _ptr_i32(old_packed["layers"]),
316
+ _ptr_i32(old_packed["datatypes"]),
317
+ _ptr_i64(old_packed["bboxes"]),
318
+ _ptr_u32(old_packed["flags"]),
319
+ _ptr_u64(old_packed["hashes"]),
320
+ ctypes.c_uint64(old_buffer.polygon_count),
321
+ _ptr_u64(new_packed["offsets"]),
322
+ _ptr_i32(new_packed["layers"]),
323
+ _ptr_i32(new_packed["datatypes"]),
324
+ _ptr_i64(new_packed["bboxes"]),
325
+ _ptr_u32(new_packed["flags"]),
326
+ _ptr_u64(new_packed["hashes"]),
327
+ ctypes.c_uint64(new_buffer.polygon_count),
328
+ ctypes.byref(result),
329
+ )
330
+ if status != 0:
331
+ message = library.gds_metal_last_error()
332
+ raise MetalCtypesUnavailable(message.decode("utf-8") if message else "Metal polygon record delta failed")
333
+ try:
334
+ count = int(result.record_count)
335
+ return MetalPolygonRecordDelta(
336
+ layers=_copy_result_array(result.layers, count, np.int32),
337
+ datatypes=_copy_result_array(result.datatypes, count, np.int32),
338
+ directions=_copy_result_array(result.directions, count, np.uint8),
339
+ counts=_copy_result_array(result.counts, count, np.uint64),
340
+ bboxes=_copy_result_array(result.bboxes, count * 4, np.int64).reshape((count, 4)),
341
+ vertex_counts=_copy_result_array(result.vertex_counts, count, np.uint64),
342
+ flags=_copy_result_array(result.flags, count, np.uint32),
343
+ hashes=_copy_result_array(result.hashes, count * 2, np.uint64).reshape((count, 2)),
344
+ representative_indices=_copy_result_array(result.representative_indices, count, np.uint64),
345
+ old_input_count=int(result.old_input_count),
346
+ new_input_count=int(result.new_input_count),
347
+ matched_record_count=int(result.matched_record_count),
348
+ reduced_key_count=int(result.reduced_key_count),
349
+ metal_kernel_count=int(result.metal_kernel_count),
350
+ metal_event_elapsed_s=float(result.metal_event_elapsed_ms) / 1000.0,
351
+ transferred_bytes=_polygon_record_transfer_bytes(old_packed, new_packed),
352
+ )
353
+ finally:
354
+ library.gds_metal_free_polygon_record_delta_result(ctypes.byref(result))
355
+
356
+
357
+ def run_metal_context_overlap_pairs(
358
+ component_bboxes: np.ndarray,
359
+ component_layers: np.ndarray,
360
+ component_datatypes: np.ndarray,
361
+ polygon_bboxes: np.ndarray,
362
+ polygon_layers: np.ndarray,
363
+ polygon_datatypes: np.ndarray,
364
+ ) -> MetalContextOverlapPairs:
365
+ component_bboxes = np.ascontiguousarray(component_bboxes, dtype=np.int64)
366
+ component_layers = np.ascontiguousarray(component_layers, dtype=np.int32)
367
+ component_datatypes = np.ascontiguousarray(component_datatypes, dtype=np.int32)
368
+ polygon_bboxes = np.ascontiguousarray(polygon_bboxes, dtype=np.int64)
369
+ polygon_layers = np.ascontiguousarray(polygon_layers, dtype=np.int32)
370
+ polygon_datatypes = np.ascontiguousarray(polygon_datatypes, dtype=np.int32)
371
+ if component_bboxes.ndim != 2 or component_bboxes.shape[1] != 4:
372
+ raise ValueError("component_bboxes must have shape (N, 4)")
373
+ if polygon_bboxes.ndim != 2 or polygon_bboxes.shape[1] != 4:
374
+ raise ValueError("polygon_bboxes must have shape (M, 4)")
375
+ component_count = component_bboxes.shape[0]
376
+ polygon_count = polygon_bboxes.shape[0]
377
+ if component_layers.shape != (component_count,) or component_datatypes.shape != (component_count,):
378
+ raise ValueError("component layer/datatype arrays must have shape (N,)")
379
+ if polygon_layers.shape != (polygon_count,) or polygon_datatypes.shape != (polygon_count,):
380
+ raise ValueError("polygon layer/datatype arrays must have shape (M,)")
381
+ total_pairs = component_count * polygon_count
382
+ max_pairs = _context_overlap_max_pairs()
383
+ if total_pairs > max_pairs:
384
+ raise MetalCtypesUnavailable(
385
+ f"Metal context overlap prototype is limited to {max_pairs} component/polygon pairs; got {total_pairs}"
386
+ )
387
+ library = _load_library()
388
+ result = _CContextOverlapPairsResult()
389
+ status = library.gds_metal_context_overlap_pairs(
390
+ _ptr_i64(component_bboxes.reshape((-1,))),
391
+ _ptr_i32(component_layers),
392
+ _ptr_i32(component_datatypes),
393
+ ctypes.c_uint64(component_count),
394
+ _ptr_i64(polygon_bboxes.reshape((-1,))),
395
+ _ptr_i32(polygon_layers),
396
+ _ptr_i32(polygon_datatypes),
397
+ ctypes.c_uint64(polygon_count),
398
+ ctypes.byref(result),
399
+ )
400
+ if status != 0:
401
+ message = library.gds_metal_last_error()
402
+ raise MetalCtypesUnavailable(message.decode("utf-8") if message else "Metal context overlap pairs failed")
403
+ try:
404
+ count = int(result.pair_count)
405
+ return MetalContextOverlapPairs(
406
+ component_indices=_copy_result_array(result.component_indices, count, np.uint64),
407
+ polygon_indices=_copy_result_array(result.polygon_indices, count, np.uint64),
408
+ metal_kernel_count=int(result.metal_kernel_count),
409
+ metal_event_elapsed_s=float(result.metal_event_elapsed_ms) / 1000.0,
410
+ transferred_bytes=int(
411
+ component_bboxes.nbytes
412
+ + component_layers.nbytes
413
+ + component_datatypes.nbytes
414
+ + polygon_bboxes.nbytes
415
+ + polygon_layers.nbytes
416
+ + polygon_datatypes.nbytes
417
+ ),
418
+ )
419
+ finally:
420
+ library.gds_metal_free_context_overlap_pairs_result(ctypes.byref(result))
421
+
422
+
423
+ def run_metal_boundary_loops_from_edges(edges: np.ndarray) -> MetalBoundaryLoopResult:
424
+ edges = np.ascontiguousarray(edges, dtype=np.int64)
425
+ if edges.ndim != 2 or edges.shape[1] != 4:
426
+ raise ValueError("edges must have shape (N, 4)")
427
+ library = _load_library()
428
+ result = _CBoundaryLoopResult()
429
+ status = library.gds_metal_boundary_loops_from_edges(
430
+ _ptr_i64(edges.reshape((-1,))),
431
+ ctypes.c_uint64(len(edges)),
432
+ ctypes.byref(result),
433
+ )
434
+ if status != 0:
435
+ message = library.gds_metal_last_error()
436
+ raise MetalCtypesUnavailable(message.decode("utf-8") if message else "Metal boundary loop tracing failed")
437
+ try:
438
+ count = int(result.fragment_count)
439
+ vertex_count = int(result.vertex_count)
440
+ vertices = _copy_result_array(result.vertices_xy, vertex_count * 2, np.int64).reshape((vertex_count, 2))
441
+ offsets = _copy_result_array(result.fragment_offsets, count + 1, np.uint64)
442
+ bboxes = _copy_result_array(result.bboxes, count * 4, np.int64).reshape((count, 4))
443
+ twice_areas = _copy_result_array(result.twice_areas, count, np.int64)
444
+ fragments = []
445
+ for index in range(count):
446
+ start = int(offsets[index])
447
+ stop = int(offsets[index + 1])
448
+ points = tuple((int(x), int(y)) for x, y in vertices[start:stop])
449
+ bbox = tuple(int(value) for value in bboxes[index])
450
+ fragments.append(DiffFragment(points=points, bbox=bbox, twice_area=int(twice_areas[index])))
451
+ return MetalBoundaryLoopResult(
452
+ fragments=tuple(sorted(fragments, key=lambda fragment: (fragment.bbox, fragment.twice_area, fragment.points))),
453
+ dropped_zero_area_loop_count=int(result.dropped_zero_area_loop_count),
454
+ metal_kernel_count=int(result.metal_kernel_count),
455
+ metal_event_elapsed_s=float(result.metal_event_elapsed_ms) / 1000.0,
456
+ transferred_bytes=int(edges.nbytes),
457
+ )
458
+ finally:
459
+ library.gds_metal_free_boundary_loop_result(ctypes.byref(result))
460
+
461
+
462
+ def run_metal_one_sided_boundary_edges(
463
+ vertices_xy: np.ndarray,
464
+ polygon_offsets: np.ndarray,
465
+ bboxes: np.ndarray,
466
+ signed_twice_areas: np.ndarray,
467
+ segment_points: np.ndarray,
468
+ segment_polygon_indices: np.ndarray,
469
+ ) -> np.ndarray:
470
+ vertices_xy = np.ascontiguousarray(vertices_xy, dtype=np.int64)
471
+ polygon_offsets = np.ascontiguousarray(polygon_offsets, dtype=np.uint64)
472
+ bboxes = np.ascontiguousarray(bboxes, dtype=np.int64)
473
+ signed_twice_areas = np.ascontiguousarray(signed_twice_areas, dtype=np.int64)
474
+ segment_points = np.ascontiguousarray(segment_points, dtype=np.float64)
475
+ segment_polygon_indices = np.ascontiguousarray(segment_polygon_indices, dtype=np.uint32)
476
+ if vertices_xy.ndim != 2 or vertices_xy.shape[1] != 2:
477
+ raise ValueError("vertices_xy must have shape (N, 2)")
478
+ if polygon_offsets.ndim != 1 or len(polygon_offsets) < 1:
479
+ raise ValueError("polygon_offsets must be a non-empty vector")
480
+ polygon_count = len(polygon_offsets) - 1
481
+ if bboxes.shape != (polygon_count, 4):
482
+ raise ValueError("bboxes must have shape (polygon_count, 4)")
483
+ if signed_twice_areas.shape != (polygon_count,):
484
+ raise ValueError("signed_twice_areas must have shape (polygon_count,)")
485
+ if segment_points.ndim != 2 or segment_points.shape[1] != 4:
486
+ raise ValueError("segment_points must have shape (segment_count, 4)")
487
+ segment_count = len(segment_points)
488
+ if segment_polygon_indices.shape != (segment_count,):
489
+ raise ValueError("segment_polygon_indices must have shape (segment_count,)")
490
+ out_edges = np.zeros((segment_count, 4), dtype=np.float64)
491
+ out_keep = np.zeros((segment_count,), dtype=np.uint8)
492
+ library = _load_library()
493
+ status = library.gds_metal_one_sided_boundary_edges(
494
+ _ptr_i64(vertices_xy.reshape((-1,))),
495
+ ctypes.c_uint64(len(vertices_xy)),
496
+ _ptr_u64(polygon_offsets),
497
+ _ptr_i64(bboxes.reshape((-1,))),
498
+ _ptr_i64(signed_twice_areas),
499
+ ctypes.c_uint64(polygon_count),
500
+ _ptr_f64(segment_points.reshape((-1,))),
501
+ segment_polygon_indices.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)),
502
+ ctypes.c_uint64(segment_count),
503
+ _ptr_f64(out_edges.reshape((-1,))),
504
+ out_keep.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
505
+ )
506
+ if status != 0:
507
+ message = library.gds_metal_last_error()
508
+ raise MetalCtypesUnavailable(message.decode("utf-8") if message else "Metal one-sided boundary edges failed")
509
+ return out_edges[out_keep.astype(bool)].reshape((-1, 2, 2))
510
+
511
+
512
+ def run_metal_mixed_boundary_edges(
513
+ vertices_xy: np.ndarray,
514
+ polygon_offsets: np.ndarray,
515
+ bboxes: np.ndarray,
516
+ polygon_sides: np.ndarray,
517
+ segment_points: np.ndarray,
518
+ *,
519
+ direction: str,
520
+ ) -> np.ndarray:
521
+ vertices_xy = np.ascontiguousarray(vertices_xy, dtype=np.int64)
522
+ polygon_offsets = np.ascontiguousarray(polygon_offsets, dtype=np.uint64)
523
+ bboxes = np.ascontiguousarray(bboxes, dtype=np.int64)
524
+ polygon_sides = np.ascontiguousarray(polygon_sides, dtype=np.uint8)
525
+ segment_points = np.ascontiguousarray(segment_points, dtype=np.float64)
526
+ if direction not in {"old", "new"}:
527
+ raise ValueError("direction must be 'old' or 'new'")
528
+ if vertices_xy.ndim != 2 or vertices_xy.shape[1] != 2:
529
+ raise ValueError("vertices_xy must have shape (N, 2)")
530
+ if polygon_offsets.ndim != 1 or len(polygon_offsets) < 1:
531
+ raise ValueError("polygon_offsets must be a non-empty vector")
532
+ polygon_count = len(polygon_offsets) - 1
533
+ if bboxes.shape != (polygon_count, 4):
534
+ raise ValueError("bboxes must have shape (polygon_count, 4)")
535
+ if polygon_sides.shape != (polygon_count,):
536
+ raise ValueError("polygon_sides must have shape (polygon_count,)")
537
+ if segment_points.ndim != 2 or segment_points.shape[1] != 4:
538
+ raise ValueError("segment_points must have shape (segment_count, 4)")
539
+ segment_count = len(segment_points)
540
+ out_edges = np.zeros((segment_count, 4), dtype=np.float64)
541
+ out_keep = np.zeros((segment_count,), dtype=np.uint8)
542
+ library = _load_library()
543
+ status = library.gds_metal_mixed_boundary_edges(
544
+ _ptr_i64(vertices_xy.reshape((-1,))),
545
+ ctypes.c_uint64(len(vertices_xy)),
546
+ _ptr_u64(polygon_offsets),
547
+ _ptr_i64(bboxes.reshape((-1,))),
548
+ polygon_sides.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
549
+ ctypes.c_uint64(polygon_count),
550
+ _ptr_f64(segment_points.reshape((-1,))),
551
+ ctypes.c_uint64(segment_count),
552
+ ctypes.c_uint8(1 if direction == "old" else 2),
553
+ _ptr_f64(out_edges.reshape((-1,))),
554
+ out_keep.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
555
+ )
556
+ if status != 0:
557
+ message = library.gds_metal_last_error()
558
+ raise MetalCtypesUnavailable(message.decode("utf-8") if message else "Metal mixed boundary edges failed")
559
+ return out_edges[out_keep.astype(bool)].reshape((-1, 2, 2))
560
+
561
+
562
+ def _ptr_i64(array: np.ndarray):
563
+ return array.ctypes.data_as(ctypes.POINTER(ctypes.c_int64))
564
+
565
+
566
+ def _ptr_i32(array: np.ndarray):
567
+ return array.ctypes.data_as(ctypes.POINTER(ctypes.c_int32))
568
+
569
+
570
+ def _ptr_u64(array: np.ndarray):
571
+ return array.ctypes.data_as(ctypes.POINTER(ctypes.c_uint64))
572
+
573
+
574
+ def _ptr_u32(array: np.ndarray):
575
+ return array.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32))
576
+
577
+
578
+ def _ptr_f64(array: np.ndarray):
579
+ return array.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
580
+
581
+
582
+ def _copy_result_array(pointer, count: int, dtype) -> np.ndarray:
583
+ if count == 0:
584
+ return np.empty((0,), dtype=dtype)
585
+ if not pointer:
586
+ raise MetalCtypesUnavailable("Metal result returned null output array")
587
+ ctype = np.ctypeslib.as_ctypes_type(dtype)
588
+ array = ctypes.cast(pointer, ctypes.POINTER(ctype * count)).contents
589
+ return np.frombuffer(array, dtype=dtype).copy()
590
+
591
+
592
+ def _candidate_tiles_from_result(result: _CPrefilterResult) -> tuple[TileKey, ...]:
593
+ if result.candidate_count == 0:
594
+ return ()
595
+ array = np.ctypeslib.as_array(result.candidate_tiles, shape=(int(result.candidate_count) * 4,))
596
+ return tuple(
597
+ TileKey(LayerSpec(int(array[index]), int(array[index + 1])), int(array[index + 2]), int(array[index + 3]))
598
+ for index in range(0, len(array), 4)
599
+ )
600
+
601
+
602
+ def _oversized_from_result(result: _CPrefilterResult) -> tuple[OversizedPolygon, ...]:
603
+ if result.oversized_count == 0:
604
+ return ()
605
+ array = np.ctypeslib.as_array(result.oversized, shape=(int(result.oversized_count) * 8,))
606
+ return tuple(
607
+ OversizedPolygon(
608
+ layer=LayerSpec(int(array[index]), int(array[index + 1])),
609
+ canonical_id=int(array[index + 2]),
610
+ side_mask=int(array[index + 3]),
611
+ tile_bbox=(int(array[index + 4]), int(array[index + 5]), int(array[index + 6]), int(array[index + 7])),
612
+ )
613
+ for index in range(0, len(array), 8)
614
+ )
615
+
616
+
617
+ def _pack_buffer(buffer: PolygonBuffer, canonical_ids) -> _PackedBuffer:
618
+ ids = np.ascontiguousarray(canonical_ids, dtype=np.uint64)
619
+ if len(ids) != buffer.polygon_count:
620
+ raise ValueError("canonical id vector length must match polygon count")
621
+ layer_values = np.asarray([layer.layer for layer in buffer.layer_table.layers], dtype=np.int32)
622
+ datatype_values = np.asarray([layer.datatype for layer in buffer.layer_table.layers], dtype=np.int32)
623
+ return _PackedBuffer(
624
+ bboxes=np.ascontiguousarray(buffer.bboxes, dtype=np.int64),
625
+ layers=np.ascontiguousarray(layer_values[buffer.layer_ids], dtype=np.int32),
626
+ datatypes=np.ascontiguousarray(datatype_values[buffer.layer_ids], dtype=np.int32),
627
+ canonical_ids=ids,
628
+ )
629
+
630
+
631
+ def _pack_polygon_hash_record_buffer(buffer: PolygonBuffer) -> dict[str, np.ndarray]:
632
+ if buffer.polygon_hashes is None:
633
+ raise MetalCtypesUnavailable("native polygon hashes are unavailable")
634
+ layer_values = np.asarray([layer.layer for layer in buffer.layer_table.layers], dtype=np.int32)
635
+ datatype_values = np.asarray([layer.datatype for layer in buffer.layer_table.layers], dtype=np.int32)
636
+ layers = np.ascontiguousarray(layer_values[buffer.layer_ids], dtype=np.int32)
637
+ datatypes = np.ascontiguousarray(datatype_values[buffer.layer_ids], dtype=np.int32)
638
+ return {
639
+ "offsets": np.ascontiguousarray(buffer.polygon_offsets, dtype=np.uint64),
640
+ "layers": layers,
641
+ "datatypes": datatypes,
642
+ "bboxes": np.ascontiguousarray(buffer.bboxes.reshape((-1,)), dtype=np.int64),
643
+ "flags": np.ascontiguousarray(buffer.flags, dtype=np.uint32),
644
+ "hashes": np.ascontiguousarray(buffer.polygon_hashes.reshape((-1,)), dtype=np.uint64),
645
+ }
646
+
647
+
648
+ def _polygon_record_transfer_bytes(*packed_buffers: dict[str, np.ndarray]) -> int:
649
+ return sum(int(array.nbytes) for packed in packed_buffers for array in packed.values())
650
+
651
+
652
+ def _record_delta_max_polygons() -> int:
653
+ import os
654
+
655
+ try:
656
+ value = int(os.environ.get("GDS_METAL_RECORD_DELTA_MAX_POLYGONS", ""))
657
+ except ValueError:
658
+ return 10_000_000
659
+ return value if value > 0 else 10_000_000
660
+
661
+
662
+ def _context_overlap_max_pairs() -> int:
663
+ import os
664
+
665
+ try:
666
+ value = int(os.environ.get("GDS_METAL_CONTEXT_OVERLAP_MAX_PAIRS", ""))
667
+ except ValueError:
668
+ return 64 * 1024 * 1024
669
+ return value if value > 0 else 64 * 1024 * 1024
670
+
671
+
672
+ @lru_cache(maxsize=1)
673
+ def _load_library() -> ctypes.CDLL:
674
+ if platform.system() != "Darwin" or platform.machine() not in {"arm64", "aarch64"}:
675
+ raise MetalCtypesUnavailable("Metal ctypes backend requires macOS on Apple Silicon")
676
+ path = _library_path()
677
+ library = ctypes.CDLL(str(path))
678
+ library.gds_metal_last_error.argtypes = []
679
+ library.gds_metal_last_error.restype = ctypes.c_char_p
680
+ library.gds_metal_available.argtypes = []
681
+ library.gds_metal_available.restype = ctypes.c_int
682
+ library.gds_metal_release_device_memory.argtypes = []
683
+ library.gds_metal_release_device_memory.restype = ctypes.c_int
684
+ library.gds_metal_compare_files.argtypes = [
685
+ ctypes.c_char_p,
686
+ ctypes.c_char_p,
687
+ ctypes.POINTER(_CByteCompareResult),
688
+ ]
689
+ library.gds_metal_compare_files.restype = ctypes.c_int
690
+ library.gds_metal_prefilter.argtypes = [
691
+ ctypes.POINTER(ctypes.c_int64),
692
+ ctypes.POINTER(ctypes.c_int32),
693
+ ctypes.POINTER(ctypes.c_int32),
694
+ ctypes.POINTER(ctypes.c_uint64),
695
+ ctypes.c_uint64,
696
+ ctypes.POINTER(ctypes.c_int64),
697
+ ctypes.POINTER(ctypes.c_int32),
698
+ ctypes.POINTER(ctypes.c_int32),
699
+ ctypes.POINTER(ctypes.c_uint64),
700
+ ctypes.c_uint64,
701
+ ctypes.c_int64,
702
+ ctypes.c_uint64,
703
+ ctypes.POINTER(_CPrefilterResult),
704
+ ]
705
+ library.gds_metal_prefilter.restype = ctypes.c_int
706
+ library.gds_metal_free_prefilter_result.argtypes = [ctypes.POINTER(_CPrefilterResult)]
707
+ library.gds_metal_free_prefilter_result.restype = None
708
+ library.gds_metal_rect_set_exact_fragments.argtypes = [
709
+ ctypes.POINTER(ctypes.c_int64),
710
+ ctypes.POINTER(ctypes.c_int64),
711
+ ctypes.POINTER(ctypes.c_uint8),
712
+ ctypes.POINTER(ctypes.c_uint8),
713
+ ctypes.c_uint64,
714
+ ctypes.c_uint16,
715
+ ctypes.c_uint16,
716
+ ctypes.POINTER(ctypes.c_int64),
717
+ ctypes.POINTER(ctypes.c_uint16),
718
+ ctypes.POINTER(ctypes.c_int64),
719
+ ]
720
+ library.gds_metal_rect_set_exact_fragments.restype = ctypes.c_int
721
+ library.gds_metal_polygon_record_delta_from_hashes.argtypes = [
722
+ ctypes.POINTER(ctypes.c_uint64),
723
+ ctypes.POINTER(ctypes.c_int32),
724
+ ctypes.POINTER(ctypes.c_int32),
725
+ ctypes.POINTER(ctypes.c_int64),
726
+ ctypes.POINTER(ctypes.c_uint32),
727
+ ctypes.POINTER(ctypes.c_uint64),
728
+ ctypes.c_uint64,
729
+ ctypes.POINTER(ctypes.c_uint64),
730
+ ctypes.POINTER(ctypes.c_int32),
731
+ ctypes.POINTER(ctypes.c_int32),
732
+ ctypes.POINTER(ctypes.c_int64),
733
+ ctypes.POINTER(ctypes.c_uint32),
734
+ ctypes.POINTER(ctypes.c_uint64),
735
+ ctypes.c_uint64,
736
+ ctypes.POINTER(_CPolygonRecordDeltaResult),
737
+ ]
738
+ library.gds_metal_polygon_record_delta_from_hashes.restype = ctypes.c_int
739
+ library.gds_metal_free_polygon_record_delta_result.argtypes = [ctypes.POINTER(_CPolygonRecordDeltaResult)]
740
+ library.gds_metal_free_polygon_record_delta_result.restype = None
741
+ library.gds_metal_context_overlap_pairs.argtypes = [
742
+ ctypes.POINTER(ctypes.c_int64),
743
+ ctypes.POINTER(ctypes.c_int32),
744
+ ctypes.POINTER(ctypes.c_int32),
745
+ ctypes.c_uint64,
746
+ ctypes.POINTER(ctypes.c_int64),
747
+ ctypes.POINTER(ctypes.c_int32),
748
+ ctypes.POINTER(ctypes.c_int32),
749
+ ctypes.c_uint64,
750
+ ctypes.POINTER(_CContextOverlapPairsResult),
751
+ ]
752
+ library.gds_metal_context_overlap_pairs.restype = ctypes.c_int
753
+ library.gds_metal_free_context_overlap_pairs_result.argtypes = [ctypes.POINTER(_CContextOverlapPairsResult)]
754
+ library.gds_metal_free_context_overlap_pairs_result.restype = None
755
+ library.gds_metal_boundary_loops_from_edges.argtypes = [
756
+ ctypes.POINTER(ctypes.c_int64),
757
+ ctypes.c_uint64,
758
+ ctypes.POINTER(_CBoundaryLoopResult),
759
+ ]
760
+ library.gds_metal_boundary_loops_from_edges.restype = ctypes.c_int
761
+ library.gds_metal_free_boundary_loop_result.argtypes = [ctypes.POINTER(_CBoundaryLoopResult)]
762
+ library.gds_metal_free_boundary_loop_result.restype = None
763
+ library.gds_metal_one_sided_boundary_edges.argtypes = [
764
+ ctypes.POINTER(ctypes.c_int64),
765
+ ctypes.c_uint64,
766
+ ctypes.POINTER(ctypes.c_uint64),
767
+ ctypes.POINTER(ctypes.c_int64),
768
+ ctypes.POINTER(ctypes.c_int64),
769
+ ctypes.c_uint64,
770
+ ctypes.POINTER(ctypes.c_double),
771
+ ctypes.POINTER(ctypes.c_uint32),
772
+ ctypes.c_uint64,
773
+ ctypes.POINTER(ctypes.c_double),
774
+ ctypes.POINTER(ctypes.c_uint8),
775
+ ]
776
+ library.gds_metal_one_sided_boundary_edges.restype = ctypes.c_int
777
+ library.gds_metal_mixed_boundary_edges.argtypes = [
778
+ ctypes.POINTER(ctypes.c_int64),
779
+ ctypes.c_uint64,
780
+ ctypes.POINTER(ctypes.c_uint64),
781
+ ctypes.POINTER(ctypes.c_int64),
782
+ ctypes.POINTER(ctypes.c_uint8),
783
+ ctypes.c_uint64,
784
+ ctypes.POINTER(ctypes.c_double),
785
+ ctypes.c_uint64,
786
+ ctypes.c_uint8,
787
+ ctypes.POINTER(ctypes.c_double),
788
+ ctypes.POINTER(ctypes.c_uint8),
789
+ ]
790
+ library.gds_metal_mixed_boundary_edges.restype = ctypes.c_int
791
+ return library
792
+
793
+
794
+ def _build_library(path: Path) -> None:
795
+ source = _source_path()
796
+ path.parent.mkdir(parents=True, exist_ok=True)
797
+ compiler = _clangxx_path()
798
+ command = [
799
+ compiler,
800
+ "-std=c++17",
801
+ "-O3",
802
+ "-dynamiclib",
803
+ "-fPIC",
804
+ "-fobjc-arc",
805
+ "-isysroot",
806
+ _sdk_path(),
807
+ str(source),
808
+ "-framework",
809
+ "Foundation",
810
+ "-framework",
811
+ "Metal",
812
+ "-o",
813
+ str(path),
814
+ ]
815
+ try:
816
+ completed = subprocess.run(command, check=False, text=True, capture_output=True)
817
+ except FileNotFoundError as exc:
818
+ raise MetalCtypesUnavailable("clang++ is not available") from exc
819
+ if completed.returncode != 0:
820
+ raise MetalCtypesUnavailable((completed.stderr or completed.stdout).strip() or "clang++ failed")
821
+
822
+
823
+ def _clangxx_path() -> str:
824
+ try:
825
+ completed = subprocess.run(["xcrun", "--find", "clang++"], check=False, text=True, capture_output=True)
826
+ except FileNotFoundError:
827
+ return "clang++"
828
+ candidate = completed.stdout.strip()
829
+ return candidate if completed.returncode == 0 and candidate else "clang++"
830
+
831
+
832
+ def _sdk_path() -> str:
833
+ try:
834
+ completed = subprocess.run(["xcrun", "--show-sdk-path"], check=False, text=True, capture_output=True)
835
+ except FileNotFoundError:
836
+ return "/"
837
+ candidate = completed.stdout.strip()
838
+ return candidate if completed.returncode == 0 and candidate else "/"
839
+
840
+
841
+ def _library_path() -> Path:
842
+ compiler = _clangxx_path()
843
+ sdk = _sdk_path()
844
+ return ensure_cached_native_library(
845
+ "_gds_compare_metal_ctypes.so",
846
+ sources=(_source_path(),),
847
+ build_key=(
848
+ compiler,
849
+ "-std=c++17",
850
+ "-O3",
851
+ "-dynamiclib",
852
+ "-fPIC",
853
+ "-fobjc-arc",
854
+ "-isysroot",
855
+ sdk,
856
+ "-framework",
857
+ "Foundation",
858
+ "-framework",
859
+ "Metal",
860
+ ),
861
+ builder=_build_library,
862
+ )
863
+
864
+
865
+ def _source_path() -> Path:
866
+ return Path(__file__).resolve().parents[1] / "native_src" / "gdsdiff" / "metal_ctypes.mm"