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.
- gdsdiff/__init__.py +148 -0
- gdsdiff/__main__.py +7 -0
- gdsdiff/_environment.py +12 -0
- gdsdiff/_version.py +34 -0
- gdsdiff/acceptance_evidence.py +530 -0
- gdsdiff/api.py +2955 -0
- gdsdiff/backends/__init__.py +26 -0
- gdsdiff/backends/base.py +77 -0
- gdsdiff/backends/boundary_loop_ctypes.py +191 -0
- gdsdiff/backends/cpu.py +20 -0
- gdsdiff/backends/cpu_ctypes.py +255 -0
- gdsdiff/backends/cuda.py +40 -0
- gdsdiff/backends/cuda_ctypes.py +1297 -0
- gdsdiff/backends/exact.py +424 -0
- gdsdiff/backends/geometry_ctypes.py +252 -0
- gdsdiff/backends/identity.py +53 -0
- gdsdiff/backends/metal.py +198 -0
- gdsdiff/backends/metal_ctypes.py +866 -0
- gdsdiff/backends/polygon_extract_ctypes.py +233 -0
- gdsdiff/backends/structural_ctypes.py +130 -0
- gdsdiff/boundary_loops.py +616 -0
- gdsdiff/cache.py +544 -0
- gdsdiff/canonicalize.py +176 -0
- gdsdiff/cli.py +352 -0
- gdsdiff/config.py +257 -0
- gdsdiff/cuda_setup.py +174 -0
- gdsdiff/design_history.py +65 -0
- gdsdiff/diagnostics.py +42 -0
- gdsdiff/diff_gds.py +66 -0
- gdsdiff/exact_diff_markdown.py +979 -0
- gdsdiff/exact_oracle.py +649 -0
- gdsdiff/extract.py +376 -0
- gdsdiff/gds_metadata.py +43 -0
- gdsdiff/geometry.py +112 -0
- gdsdiff/grid.py +143 -0
- gdsdiff/hierarchy.py +215 -0
- gdsdiff/hierarchy_extract.py +263 -0
- gdsdiff/inventory.py +153 -0
- gdsdiff/multiprocessing_support.py +39 -0
- gdsdiff/native.py +135 -0
- gdsdiff/native_cache.py +265 -0
- gdsdiff/native_src/cpu_prefilter.cpp +349 -0
- gdsdiff/native_src/gds_boundary_loops.cpp +505 -0
- gdsdiff/native_src/gds_geometry_scan.cpp +601 -0
- gdsdiff/native_src/gds_polygon_extract.cpp +594 -0
- gdsdiff/native_src/gds_structural_scan.cpp +335 -0
- gdsdiff/native_src/gdsdiff/CMakeLists.txt +74 -0
- gdsdiff/native_src/gdsdiff/core.cpp +191 -0
- gdsdiff/native_src/gdsdiff/core.hpp +96 -0
- gdsdiff/native_src/gdsdiff/cuda_assignment.cu +304 -0
- gdsdiff/native_src/gdsdiff/cuda_assignment.cuh +33 -0
- gdsdiff/native_src/gdsdiff/cuda_candidates.cu +133 -0
- gdsdiff/native_src/gdsdiff/cuda_candidates.cuh +17 -0
- gdsdiff/native_src/gdsdiff/cuda_ctypes.cu +4528 -0
- gdsdiff/native_src/gdsdiff/cuda_memory.cu +194 -0
- gdsdiff/native_src/gdsdiff/cuda_memory.cuh +34 -0
- gdsdiff/native_src/gdsdiff/metal_ctypes.mm +2791 -0
- gdsdiff/native_src/gdsdiff/python_bindings.cpp +21 -0
- gdsdiff/native_src/gdsdiff/test_core.cpp +93 -0
- gdsdiff/native_src/gdsdiff/test_cuda_assignment.cu +109 -0
- gdsdiff/native_src/gdsdiff/test_cuda_candidates.cu +94 -0
- gdsdiff/native_src/gdsdiff/test_cuda_memory.cu +97 -0
- gdsdiff/policy.py +305 -0
- gdsdiff/polygon_components.py +860 -0
- gdsdiff/profiles.py +152 -0
- gdsdiff/py.typed +1 -0
- gdsdiff/rect_coverage.py +384 -0
- gdsdiff/report.py +354 -0
- gdsdiff/schemas/gdsdiff_report-v1.schema.json +92 -0
- gdsdiff/semantics.py +75 -0
- gdsdiff/source_edge_diff.py +1120 -0
- gdsdiff/spatial.py +71 -0
- gdsdiff/structural_guard.py +161 -0
- gdsdiff/surplus_coverage.py +255 -0
- gdsdiff/synthetic_suite.py +1643 -0
- gdsdiff/templates.py +709 -0
- gdsdiff/tiling.py +153 -0
- gdsdiff/windowed_exact.py +270 -0
- gdsdiff/windows.py +184 -0
- gdsdiff-0.1.0.dist-info/METADATA +135 -0
- gdsdiff-0.1.0.dist-info/RECORD +85 -0
- gdsdiff-0.1.0.dist-info/WHEEL +5 -0
- gdsdiff-0.1.0.dist-info/entry_points.txt +4 -0
- gdsdiff-0.1.0.dist-info/licenses/LICENSE +674 -0
- 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
|
+
]
|
gdsdiff/backends/base.py
ADDED
|
@@ -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)
|
gdsdiff/backends/cpu.py
ADDED
|
@@ -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"
|
gdsdiff/backends/cuda.py
ADDED
|
@@ -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
|
+
)
|