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,26 @@
1
+ """Backend implementations for GPU compare accelerators."""
2
+
3
+ from .base import ExactComponentBackend, PrefilterBackend
4
+ from .cpu import CpuTileBackend
5
+ from .cuda import CudaBackendUnavailable, CudaTileBackend
6
+ from .exact import (
7
+ CudaManhattanRectExactComponentBackend,
8
+ GdstkExactComponentBackend,
9
+ ManhattanRectExactComponentBackend,
10
+ UnsupportedExactComponent,
11
+ )
12
+ from .metal import MetalExactUnavailable, MetalManhattanRectExactComponentBackend
13
+
14
+ __all__ = [
15
+ "CpuTileBackend",
16
+ "CudaBackendUnavailable",
17
+ "CudaTileBackend",
18
+ "CudaManhattanRectExactComponentBackend",
19
+ "ExactComponentBackend",
20
+ "GdstkExactComponentBackend",
21
+ "ManhattanRectExactComponentBackend",
22
+ "MetalExactUnavailable",
23
+ "MetalManhattanRectExactComponentBackend",
24
+ "PrefilterBackend",
25
+ "UnsupportedExactComponent",
26
+ ]
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING, Protocol
6
+
7
+ from ..geometry import PolygonBuffer
8
+ from ..tiling import CpuTilePrefilterResult, TileConfig
9
+
10
+ if TYPE_CHECKING:
11
+ from ..exact_oracle import ExactLayerTask, LayerExactDiff
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class BackendCapabilities:
16
+ name: str
17
+ deterministic: bool = True
18
+ requires_gpu: bool = False
19
+
20
+
21
+ class PrefilterBackend(Protocol):
22
+ def capabilities(self) -> BackendCapabilities:
23
+ ...
24
+
25
+ def compare_tile_contributors(
26
+ self,
27
+ old_buffer: PolygonBuffer,
28
+ old_canonical_ids,
29
+ new_buffer: PolygonBuffer,
30
+ new_canonical_ids,
31
+ config: TileConfig,
32
+ ) -> CpuTilePrefilterResult:
33
+ ...
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class FileIdentityResult:
38
+ old_size: int
39
+ new_size: int
40
+ same: bool
41
+ method: str
42
+
43
+
44
+ class FileIdentityBackend(Protocol):
45
+ def capabilities(self) -> BackendCapabilities:
46
+ ...
47
+
48
+ def compare_files(self, old_path: str | Path, new_path: str | Path) -> FileIdentityResult:
49
+ ...
50
+
51
+
52
+ class NativeGeometryBackend(Protocol):
53
+ """Backend boundary for the planned non-gdstk fast geometry path.
54
+
55
+ Implementations should consume a native GDS-derived intermediate
56
+ representation rather than Python `gdstk.Polygon` objects. CUDA, Metal, and
57
+ portable CPU backends should share this contract.
58
+ """
59
+
60
+ def capabilities(self) -> BackendCapabilities:
61
+ ...
62
+
63
+
64
+ class ExactComponentBackend(Protocol):
65
+ """Backend boundary for polygon-level exact evidence tasks.
66
+
67
+ Implementations consume independent exact component tasks and return the
68
+ same `LayerExactDiff` fragments as the `gdstk` oracle. CUDA and Metal
69
+ backends should target this contract while the CPU/gdstk backend remains
70
+ the reference oracle.
71
+ """
72
+
73
+ def capabilities(self) -> BackendCapabilities:
74
+ ...
75
+
76
+ def compare_components(self, tasks: tuple["ExactLayerTask", ...]) -> tuple["LayerExactDiff", ...]:
77
+ ...
@@ -0,0 +1,191 @@
1
+ from __future__ import annotations
2
+
3
+ import ctypes
4
+ import subprocess
5
+ from dataclasses import dataclass
6
+ from functools import lru_cache
7
+ from pathlib import Path
8
+
9
+ import numpy as np
10
+
11
+ from ..exact_oracle import DiffFragment
12
+ from ..native_cache import ensure_cached_native_library
13
+
14
+
15
+ class BoundaryLoopCtypesUnavailable(RuntimeError):
16
+ pass
17
+
18
+
19
+ class _CBoundaryLoopResult(ctypes.Structure):
20
+ _fields_ = [
21
+ ("fragment_count", ctypes.c_uint64),
22
+ ("vertex_count", ctypes.c_uint64),
23
+ ("vertices_xy", ctypes.POINTER(ctypes.c_int64)),
24
+ ("fragment_offsets", ctypes.POINTER(ctypes.c_uint64)),
25
+ ("bboxes", ctypes.POINTER(ctypes.c_int64)),
26
+ ("twice_areas", ctypes.POINTER(ctypes.c_int64)),
27
+ ("dropped_zero_area_loop_count", ctypes.c_uint64),
28
+ ]
29
+
30
+
31
+ class _CBoundaryEdgesResult(ctypes.Structure):
32
+ _fields_ = [
33
+ ("edge_count", ctypes.c_uint64),
34
+ ("edges", ctypes.POINTER(ctypes.c_int64)),
35
+ ]
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class NativeBoundaryLoopResult:
40
+ fragments: tuple[DiffFragment, ...]
41
+ dropped_zero_area_loop_count: int
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class NativeBoundaryEdgesResult:
46
+ edges: np.ndarray
47
+
48
+
49
+ def native_boundary_edges_from_fragments(fragments: tuple[DiffFragment, ...]) -> NativeBoundaryEdgesResult:
50
+ library = _load_library()
51
+ vertices, offsets = _pack_fragments(fragments)
52
+ result = _CBoundaryEdgesResult()
53
+ status = library.gds_boundary_edges_from_polygons(
54
+ vertices.ctypes.data_as(ctypes.POINTER(ctypes.c_int64)),
55
+ offsets.ctypes.data_as(ctypes.POINTER(ctypes.c_uint64)),
56
+ ctypes.c_uint64(len(fragments)),
57
+ ctypes.byref(result),
58
+ )
59
+ if status != 0:
60
+ message = library.gds_boundary_loops_last_error()
61
+ raise BoundaryLoopCtypesUnavailable(message.decode("utf-8") if message else "native boundary edge assembly failed")
62
+ try:
63
+ edge_count = int(result.edge_count)
64
+ edges = _copy_array(result.edges, edge_count * 4, np.int64).reshape((edge_count, 4))
65
+ return NativeBoundaryEdgesResult(edges=edges)
66
+ finally:
67
+ library.gds_boundary_edges_free(ctypes.byref(result))
68
+
69
+
70
+ def native_boundary_loops_from_edges(edges: np.ndarray) -> NativeBoundaryLoopResult:
71
+ library = _load_library()
72
+ edges = np.ascontiguousarray(edges, dtype=np.int64)
73
+ if edges.ndim != 2 or edges.shape[1] != 4:
74
+ raise ValueError("edges must have shape (N, 4)")
75
+ result = _CBoundaryLoopResult()
76
+ status = library.gds_boundary_loops_from_edges(
77
+ edges.ctypes.data_as(ctypes.POINTER(ctypes.c_int64)),
78
+ ctypes.c_uint64(len(edges)),
79
+ ctypes.byref(result),
80
+ )
81
+ if status != 0:
82
+ message = library.gds_boundary_loops_last_error()
83
+ raise BoundaryLoopCtypesUnavailable(message.decode("utf-8") if message else "native boundary loop assembly failed")
84
+ try:
85
+ count = int(result.fragment_count)
86
+ vertex_count = int(result.vertex_count)
87
+ vertices = _copy_array(result.vertices_xy, vertex_count * 2, np.int64).reshape((vertex_count, 2))
88
+ offsets = _copy_array(result.fragment_offsets, count + 1, np.uint64)
89
+ bboxes = _copy_array(result.bboxes, count * 4, np.int64).reshape((count, 4))
90
+ twice_areas = _copy_array(result.twice_areas, count, np.int64)
91
+ fragments = []
92
+ for index in range(count):
93
+ start = int(offsets[index])
94
+ stop = int(offsets[index + 1])
95
+ points = tuple((int(x), int(y)) for x, y in vertices[start:stop])
96
+ bbox = tuple(int(value) for value in bboxes[index])
97
+ fragments.append(DiffFragment(points=points, bbox=bbox, twice_area=int(twice_areas[index])))
98
+ return NativeBoundaryLoopResult(
99
+ fragments=tuple(fragments),
100
+ dropped_zero_area_loop_count=int(result.dropped_zero_area_loop_count),
101
+ )
102
+ finally:
103
+ library.gds_boundary_loops_free(ctypes.byref(result))
104
+
105
+
106
+ def boundary_loop_ctypes_available() -> bool:
107
+ try:
108
+ _load_library()
109
+ except BoundaryLoopCtypesUnavailable:
110
+ return False
111
+ return True
112
+
113
+
114
+ def _copy_array(pointer, count: int, dtype) -> np.ndarray:
115
+ if count == 0:
116
+ return np.empty((0,), dtype=dtype)
117
+ if not pointer:
118
+ raise BoundaryLoopCtypesUnavailable("native boundary loop result returned a null array")
119
+ ctype = np.ctypeslib.as_ctypes_type(dtype)
120
+ array = ctypes.cast(pointer, ctypes.POINTER(ctype * count)).contents
121
+ return np.frombuffer(array, dtype=dtype).copy()
122
+
123
+
124
+ @lru_cache(maxsize=1)
125
+ def _load_library() -> ctypes.CDLL:
126
+ path = _library_path()
127
+ library = ctypes.CDLL(str(path))
128
+ library.gds_boundary_edges_from_polygons.argtypes = [
129
+ ctypes.POINTER(ctypes.c_int64),
130
+ ctypes.POINTER(ctypes.c_uint64),
131
+ ctypes.c_uint64,
132
+ ctypes.POINTER(_CBoundaryEdgesResult),
133
+ ]
134
+ library.gds_boundary_edges_from_polygons.restype = ctypes.c_int
135
+ library.gds_boundary_edges_free.argtypes = [ctypes.POINTER(_CBoundaryEdgesResult)]
136
+ library.gds_boundary_edges_free.restype = None
137
+ library.gds_boundary_loops_from_edges.argtypes = [
138
+ ctypes.POINTER(ctypes.c_int64),
139
+ ctypes.c_uint64,
140
+ ctypes.POINTER(_CBoundaryLoopResult),
141
+ ]
142
+ library.gds_boundary_loops_from_edges.restype = ctypes.c_int
143
+ library.gds_boundary_loops_free.argtypes = [ctypes.POINTER(_CBoundaryLoopResult)]
144
+ library.gds_boundary_loops_free.restype = None
145
+ library.gds_boundary_loops_last_error.argtypes = []
146
+ library.gds_boundary_loops_last_error.restype = ctypes.c_char_p
147
+ return library
148
+
149
+
150
+ def _build_library(path: Path) -> None:
151
+ source = _source_path()
152
+ path.parent.mkdir(parents=True, exist_ok=True)
153
+ command = [
154
+ "g++",
155
+ "-std=c++17",
156
+ "-O3",
157
+ "-shared",
158
+ "-fPIC",
159
+ str(source),
160
+ "-o",
161
+ str(path),
162
+ ]
163
+ try:
164
+ completed = subprocess.run(command, check=False, text=True, capture_output=True)
165
+ except FileNotFoundError as exc:
166
+ raise BoundaryLoopCtypesUnavailable("g++ is not available") from exc
167
+ if completed.returncode != 0:
168
+ raise BoundaryLoopCtypesUnavailable((completed.stderr or completed.stdout).strip() or "g++ failed")
169
+
170
+
171
+ def _library_path() -> Path:
172
+ return ensure_cached_native_library(
173
+ "_gds_boundary_loops.so",
174
+ sources=(_source_path(),),
175
+ build_key=("g++", "-std=c++17", "-O3", "-shared", "-fPIC"),
176
+ builder=_build_library,
177
+ )
178
+
179
+
180
+ def _source_path() -> Path:
181
+ return Path(__file__).resolve().parents[1] / "native_src" / "gds_boundary_loops.cpp"
182
+
183
+
184
+ def _pack_fragments(fragments: tuple[DiffFragment, ...]) -> tuple[np.ndarray, np.ndarray]:
185
+ vertex_rows: list[tuple[int, int]] = []
186
+ offsets = [0]
187
+ for fragment in fragments:
188
+ vertex_rows.extend(fragment.points)
189
+ offsets.append(len(vertex_rows))
190
+ vertices = np.asarray(vertex_rows, dtype=np.int64).reshape((-1, 2))
191
+ return vertices, np.asarray(offsets, dtype=np.uint64)
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from ..geometry import PolygonBuffer
4
+ from ..tiling import CpuTilePrefilterResult, TileConfig, run_cpu_tile_prefilter
5
+ from .base import BackendCapabilities
6
+
7
+
8
+ class CpuTileBackend:
9
+ def capabilities(self) -> BackendCapabilities:
10
+ return BackendCapabilities(name="cpu", deterministic=True, requires_gpu=False)
11
+
12
+ def compare_tile_contributors(
13
+ self,
14
+ old_buffer: PolygonBuffer,
15
+ old_canonical_ids,
16
+ new_buffer: PolygonBuffer,
17
+ new_canonical_ids,
18
+ config: TileConfig,
19
+ ) -> CpuTilePrefilterResult:
20
+ return run_cpu_tile_prefilter(old_buffer, old_canonical_ids, new_buffer, new_canonical_ids, config)
@@ -0,0 +1,255 @@
1
+ from __future__ import annotations
2
+
3
+ import ctypes
4
+ import subprocess
5
+ from dataclasses import dataclass
6
+ from functools import lru_cache
7
+ from pathlib import Path
8
+
9
+ import numpy as np
10
+
11
+ from ..config import LayerSpec
12
+ from ..geometry import PolygonBuffer
13
+ from ..native_cache import ensure_cached_native_library
14
+ from ..tiling import CpuTilePrefilterResult, OversizedPolygon, TileConfig, TileKey
15
+
16
+
17
+ class CpuCtypesUnavailable(RuntimeError):
18
+ pass
19
+
20
+
21
+ class _CResult(ctypes.Structure):
22
+ _fields_ = [
23
+ ("candidate_tiles", ctypes.POINTER(ctypes.c_int64)),
24
+ ("candidate_count", ctypes.c_uint64),
25
+ ("oversized", ctypes.POINTER(ctypes.c_int64)),
26
+ ("oversized_count", ctypes.c_uint64),
27
+ ("assignment_count", ctypes.c_uint64),
28
+ ("collapsed_pair_count", ctypes.c_uint64),
29
+ ("oversized_matched_count", ctypes.c_uint64),
30
+ ]
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class _PackedBuffer:
35
+ bboxes: np.ndarray
36
+ layers: np.ndarray
37
+ datatypes: np.ndarray
38
+ canonical_ids: np.ndarray
39
+
40
+
41
+ def run_cpu_ctypes_tile_prefilter(
42
+ old_buffer: PolygonBuffer,
43
+ old_canonical_ids,
44
+ new_buffer: PolygonBuffer,
45
+ new_canonical_ids,
46
+ config: TileConfig,
47
+ ) -> CpuTilePrefilterResult:
48
+ library = _load_library()
49
+ old_packed = _pack_buffer(old_buffer, old_canonical_ids)
50
+ new_packed = _pack_buffer(new_buffer, new_canonical_ids)
51
+ old_packed, new_packed, skipped_matched = _skip_exact_tile_bbox_matches(old_packed, new_packed, config)
52
+ result = _CResult()
53
+ status = library.gds_cpu_prefilter(
54
+ _ptr_i64(old_packed.bboxes),
55
+ _ptr_i32(old_packed.layers),
56
+ _ptr_i32(old_packed.datatypes),
57
+ _ptr_u64(old_packed.canonical_ids),
58
+ ctypes.c_uint64(len(old_packed.canonical_ids)),
59
+ _ptr_i64(new_packed.bboxes),
60
+ _ptr_i32(new_packed.layers),
61
+ _ptr_i32(new_packed.datatypes),
62
+ _ptr_u64(new_packed.canonical_ids),
63
+ ctypes.c_uint64(len(new_packed.canonical_ids)),
64
+ ctypes.c_int64(config.tile_size),
65
+ ctypes.c_uint64(config.max_tiles_per_polygon),
66
+ ctypes.byref(result),
67
+ )
68
+ if status != 0:
69
+ message = library.gds_cpu_last_error()
70
+ raise CpuCtypesUnavailable(message.decode("utf-8") if message else "native CPU prefilter failed")
71
+ try:
72
+ return CpuTilePrefilterResult(
73
+ candidate_tiles=_candidate_tiles_from_result(result),
74
+ oversized_candidates=_oversized_from_result(result),
75
+ assignment_count=int(result.assignment_count),
76
+ collapsed_pair_count=int(result.collapsed_pair_count),
77
+ oversized_matched_count=int(result.oversized_matched_count) + skipped_matched,
78
+ )
79
+ finally:
80
+ library.gds_cpu_free_result(ctypes.byref(result))
81
+
82
+
83
+ def cpu_ctypes_available() -> bool:
84
+ try:
85
+ _load_library()
86
+ except CpuCtypesUnavailable:
87
+ return False
88
+ return True
89
+
90
+
91
+ def _candidate_tiles_from_result(result: _CResult) -> tuple[TileKey, ...]:
92
+ if result.candidate_count == 0:
93
+ return ()
94
+ array = np.ctypeslib.as_array(result.candidate_tiles, shape=(int(result.candidate_count) * 4,))
95
+ return tuple(
96
+ TileKey(LayerSpec(int(array[index]), int(array[index + 1])), int(array[index + 2]), int(array[index + 3]))
97
+ for index in range(0, len(array), 4)
98
+ )
99
+
100
+
101
+ def _oversized_from_result(result: _CResult) -> tuple[OversizedPolygon, ...]:
102
+ if result.oversized_count == 0:
103
+ return ()
104
+ array = np.ctypeslib.as_array(result.oversized, shape=(int(result.oversized_count) * 8,))
105
+ return tuple(
106
+ OversizedPolygon(
107
+ layer=LayerSpec(int(array[index]), int(array[index + 1])),
108
+ canonical_id=int(array[index + 2]),
109
+ side_mask=int(array[index + 3]),
110
+ tile_bbox=(int(array[index + 4]), int(array[index + 5]), int(array[index + 6]), int(array[index + 7])),
111
+ )
112
+ for index in range(0, len(array), 8)
113
+ )
114
+
115
+
116
+ def _pack_buffer(buffer: PolygonBuffer, canonical_ids) -> _PackedBuffer:
117
+ ids = np.ascontiguousarray(canonical_ids, dtype=np.uint64)
118
+ if len(ids) != buffer.polygon_count:
119
+ raise ValueError("canonical id vector length must match polygon count")
120
+ layers = np.empty(buffer.polygon_count, dtype=np.int32)
121
+ datatypes = np.empty(buffer.polygon_count, dtype=np.int32)
122
+ for index, layer_id in enumerate(buffer.layer_ids):
123
+ layer = buffer.layer_table.layers[int(layer_id)]
124
+ layers[index] = layer.layer
125
+ datatypes[index] = layer.datatype
126
+ return _PackedBuffer(
127
+ bboxes=np.ascontiguousarray(buffer.bboxes, dtype=np.int64),
128
+ layers=np.ascontiguousarray(layers, dtype=np.int32),
129
+ datatypes=np.ascontiguousarray(datatypes, dtype=np.int32),
130
+ canonical_ids=ids,
131
+ )
132
+
133
+
134
+ def _skip_exact_tile_bbox_matches(old: _PackedBuffer, new: _PackedBuffer, config: TileConfig) -> tuple[_PackedBuffer, _PackedBuffer, int]:
135
+ side_masks: dict[tuple[int, int, int, int, int, int, int], int] = {}
136
+ old_first: dict[tuple[int, int, int, int, int, int, int], int] = {}
137
+ new_first: dict[tuple[int, int, int, int, int, int, int], int] = {}
138
+ for index, key in enumerate(_tile_bbox_keys(old, config.tile_size)):
139
+ side_masks[key] = side_masks.get(key, 0) | 1
140
+ old_first.setdefault(key, index)
141
+ for index, key in enumerate(_tile_bbox_keys(new, config.tile_size)):
142
+ side_masks[key] = side_masks.get(key, 0) | 2
143
+ new_first.setdefault(key, index)
144
+ old_keep = [old_first[key] for key, mask in side_masks.items() if mask == 1]
145
+ new_keep = [new_first[key] for key, mask in side_masks.items() if mask == 2]
146
+ skipped = sum(1 for mask in side_masks.values() if mask == 3)
147
+ return _take_packed(old, old_keep), _take_packed(new, new_keep), skipped
148
+
149
+
150
+ def _tile_bbox_keys(packed: _PackedBuffer, tile_size: int):
151
+ tx0 = np.floor_divide(packed.bboxes[:, 0], tile_size)
152
+ ty0 = np.floor_divide(packed.bboxes[:, 1], tile_size)
153
+ tx1 = np.floor_divide(packed.bboxes[:, 2], tile_size)
154
+ ty1 = np.floor_divide(packed.bboxes[:, 3], tile_size)
155
+ for index in range(len(packed.canonical_ids)):
156
+ yield (
157
+ int(packed.layers[index]),
158
+ int(packed.datatypes[index]),
159
+ int(packed.canonical_ids[index]),
160
+ int(tx0[index]),
161
+ int(ty0[index]),
162
+ int(tx1[index]),
163
+ int(ty1[index]),
164
+ )
165
+
166
+
167
+ def _take_packed(packed: _PackedBuffer, indices: list[int]) -> _PackedBuffer:
168
+ if not indices:
169
+ return _PackedBuffer(
170
+ bboxes=np.empty((0, 4), dtype=np.int64),
171
+ layers=np.empty((0,), dtype=np.int32),
172
+ datatypes=np.empty((0,), dtype=np.int32),
173
+ canonical_ids=np.empty((0,), dtype=np.uint64),
174
+ )
175
+ index_array = np.asarray(indices, dtype=np.int64)
176
+ return _PackedBuffer(
177
+ bboxes=np.ascontiguousarray(packed.bboxes[index_array], dtype=np.int64),
178
+ layers=np.ascontiguousarray(packed.layers[index_array], dtype=np.int32),
179
+ datatypes=np.ascontiguousarray(packed.datatypes[index_array], dtype=np.int32),
180
+ canonical_ids=np.ascontiguousarray(packed.canonical_ids[index_array], dtype=np.uint64),
181
+ )
182
+
183
+
184
+ def _ptr_i64(array: np.ndarray):
185
+ return array.ctypes.data_as(ctypes.POINTER(ctypes.c_int64))
186
+
187
+
188
+ def _ptr_i32(array: np.ndarray):
189
+ return array.ctypes.data_as(ctypes.POINTER(ctypes.c_int32))
190
+
191
+
192
+ def _ptr_u64(array: np.ndarray):
193
+ return array.ctypes.data_as(ctypes.POINTER(ctypes.c_uint64))
194
+
195
+
196
+ @lru_cache(maxsize=1)
197
+ def _load_library() -> ctypes.CDLL:
198
+ path = _library_path()
199
+ library = ctypes.CDLL(str(path))
200
+ library.gds_cpu_prefilter.argtypes = [
201
+ ctypes.POINTER(ctypes.c_int64),
202
+ ctypes.POINTER(ctypes.c_int32),
203
+ ctypes.POINTER(ctypes.c_int32),
204
+ ctypes.POINTER(ctypes.c_uint64),
205
+ ctypes.c_uint64,
206
+ ctypes.POINTER(ctypes.c_int64),
207
+ ctypes.POINTER(ctypes.c_int32),
208
+ ctypes.POINTER(ctypes.c_int32),
209
+ ctypes.POINTER(ctypes.c_uint64),
210
+ ctypes.c_uint64,
211
+ ctypes.c_int64,
212
+ ctypes.c_uint64,
213
+ ctypes.POINTER(_CResult),
214
+ ]
215
+ library.gds_cpu_prefilter.restype = ctypes.c_int
216
+ library.gds_cpu_last_error.argtypes = []
217
+ library.gds_cpu_last_error.restype = ctypes.c_char_p
218
+ library.gds_cpu_free_result.argtypes = [ctypes.POINTER(_CResult)]
219
+ library.gds_cpu_free_result.restype = None
220
+ return library
221
+
222
+
223
+ def _build_library(path: Path) -> None:
224
+ source = _source_path()
225
+ path.parent.mkdir(parents=True, exist_ok=True)
226
+ command = [
227
+ "g++",
228
+ "-std=c++17",
229
+ "-O3",
230
+ "-fopenmp",
231
+ "-shared",
232
+ "-fPIC",
233
+ str(source),
234
+ "-o",
235
+ str(path),
236
+ ]
237
+ try:
238
+ completed = subprocess.run(command, check=False, text=True, capture_output=True)
239
+ except FileNotFoundError as exc:
240
+ raise CpuCtypesUnavailable("g++ is not available") from exc
241
+ if completed.returncode != 0:
242
+ raise CpuCtypesUnavailable((completed.stderr or completed.stdout).strip() or "g++ failed")
243
+
244
+
245
+ def _library_path() -> Path:
246
+ return ensure_cached_native_library(
247
+ "_gds_compare_cpu_ctypes.so",
248
+ sources=(_source_path(),),
249
+ build_key=("g++", "-std=c++17", "-O3", "-fopenmp", "-shared", "-fPIC"),
250
+ builder=_build_library,
251
+ )
252
+
253
+
254
+ def _source_path() -> Path:
255
+ return Path(__file__).resolve().parents[1] / "native_src" / "cpu_prefilter.cpp"
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+
5
+ from ..geometry import PolygonBuffer
6
+ from ..tiling import CpuTilePrefilterResult, TileConfig
7
+ from .base import BackendCapabilities
8
+
9
+
10
+ class CudaBackendUnavailable(RuntimeError):
11
+ pass
12
+
13
+
14
+ class CudaTileBackend:
15
+ def __init__(self) -> None:
16
+ try:
17
+ self._extension = importlib.import_module("gdsdiff._gds_compare_cuda")
18
+ except ImportError as exc:
19
+ raise CudaBackendUnavailable("CUDA backend extension is not available") from exc
20
+
21
+ def capabilities(self) -> BackendCapabilities:
22
+ return BackendCapabilities(name="cuda", deterministic=True, requires_gpu=True)
23
+
24
+ def compare_tile_contributors(
25
+ self,
26
+ old_buffer: PolygonBuffer,
27
+ old_canonical_ids,
28
+ new_buffer: PolygonBuffer,
29
+ new_canonical_ids,
30
+ config: TileConfig,
31
+ ) -> CpuTilePrefilterResult:
32
+ if not hasattr(self._extension, "compare_tile_contributors"):
33
+ raise CudaBackendUnavailable("CUDA backend extension lacks compare_tile_contributors")
34
+ return self._extension.compare_tile_contributors(
35
+ old_buffer,
36
+ old_canonical_ids,
37
+ new_buffer,
38
+ new_canonical_ids,
39
+ config,
40
+ )