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,424 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from ..exact_oracle import DiffFragment, ExactLayerTask, LayerExactDiff, _exact_layer_diff_task
|
|
8
|
+
from ..geometry import polygon_bbox, polygon_twice_area
|
|
9
|
+
from .base import BackendCapabilities
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GdstkExactComponentBackend:
|
|
13
|
+
"""Reference exact-component executor.
|
|
14
|
+
|
|
15
|
+
This backend intentionally uses the existing `gdstk` exact task
|
|
16
|
+
implementation. It is not an accelerator, but it defines the result
|
|
17
|
+
contract that CUDA and Metal exact-component executors must match.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def capabilities(self) -> BackendCapabilities:
|
|
21
|
+
return BackendCapabilities(name="gdstk-exact-component", deterministic=True, requires_gpu=False)
|
|
22
|
+
|
|
23
|
+
def compare_components(self, tasks: tuple[ExactLayerTask, ...]) -> tuple[LayerExactDiff, ...]:
|
|
24
|
+
return tuple(_exact_layer_diff_task(task) for task in tasks)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UnsupportedExactComponent(RuntimeError):
|
|
28
|
+
"""Raised when an exact-component backend cannot prove a task exactly."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class RectExactStats:
|
|
33
|
+
accelerated_tasks: int
|
|
34
|
+
fallback_tasks: int
|
|
35
|
+
unsupported_tasks: int
|
|
36
|
+
identical_skipped_tasks: int = 0
|
|
37
|
+
cpu_rect_set_tasks: int = 0
|
|
38
|
+
cuda_rect_set_tasks: int = 0
|
|
39
|
+
metal_rect_set_tasks: int = 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ManhattanRectExactComponentBackend:
|
|
43
|
+
"""Exact component executor for single axis-aligned rectangle tasks.
|
|
44
|
+
|
|
45
|
+
This is a small, deterministic reference for the first accelerator kernel
|
|
46
|
+
target. It handles one old rectangle versus one new rectangle, or a
|
|
47
|
+
rectangle present on only one side. Anything more complex is unsupported
|
|
48
|
+
unless `fallback_to_gdstk=True` is set.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, *, fallback_to_gdstk: bool = False) -> None:
|
|
52
|
+
self.fallback_to_gdstk = fallback_to_gdstk
|
|
53
|
+
self.stats = RectExactStats(accelerated_tasks=0, fallback_tasks=0, unsupported_tasks=0)
|
|
54
|
+
|
|
55
|
+
def capabilities(self) -> BackendCapabilities:
|
|
56
|
+
return BackendCapabilities(name="manhattan-rect-exact-component", deterministic=True, requires_gpu=False)
|
|
57
|
+
|
|
58
|
+
def compare_components(self, tasks: tuple[ExactLayerTask, ...]) -> tuple[LayerExactDiff, ...]:
|
|
59
|
+
diffs: list[LayerExactDiff] = []
|
|
60
|
+
accelerated = 0
|
|
61
|
+
fallback = 0
|
|
62
|
+
unsupported = 0
|
|
63
|
+
rect_set = 0
|
|
64
|
+
for task in tasks:
|
|
65
|
+
try:
|
|
66
|
+
diffs.append(_compare_rect_task(task))
|
|
67
|
+
accelerated += 1
|
|
68
|
+
except UnsupportedExactComponent:
|
|
69
|
+
try:
|
|
70
|
+
diffs.append(_compare_rect_set_task(task))
|
|
71
|
+
rect_set += 1
|
|
72
|
+
continue
|
|
73
|
+
except UnsupportedExactComponent:
|
|
74
|
+
pass
|
|
75
|
+
unsupported += 1
|
|
76
|
+
if not self.fallback_to_gdstk:
|
|
77
|
+
self.stats = RectExactStats(accelerated, fallback, unsupported, cpu_rect_set_tasks=rect_set)
|
|
78
|
+
raise
|
|
79
|
+
diffs.append(_exact_layer_diff_task(task))
|
|
80
|
+
fallback += 1
|
|
81
|
+
self.stats = RectExactStats(accelerated, fallback, unsupported, cpu_rect_set_tasks=rect_set)
|
|
82
|
+
return tuple(diffs)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class CudaManhattanRectExactComponentBackend:
|
|
86
|
+
"""CUDA-assisted exact component executor for single rectangle tasks.
|
|
87
|
+
|
|
88
|
+
CUDA emits the directional rectangle difference pieces for supported
|
|
89
|
+
components. Python converts those piece rectangles into `DiffFragment`
|
|
90
|
+
objects and accepts them only when their totals match the CUDA-reported
|
|
91
|
+
areas.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(self, *, fallback_to_gdstk: bool = False) -> None:
|
|
95
|
+
self.fallback_to_gdstk = fallback_to_gdstk
|
|
96
|
+
self.stats = RectExactStats(accelerated_tasks=0, fallback_tasks=0, unsupported_tasks=0)
|
|
97
|
+
|
|
98
|
+
def capabilities(self) -> BackendCapabilities:
|
|
99
|
+
return BackendCapabilities(name="cuda-manhattan-rect-exact-component", deterministic=True, requires_gpu=True)
|
|
100
|
+
|
|
101
|
+
def compare_components(self, tasks: tuple[ExactLayerTask, ...]) -> tuple[LayerExactDiff, ...]:
|
|
102
|
+
from .cuda_ctypes import run_cuda_rect_set_exact_fragments
|
|
103
|
+
|
|
104
|
+
diffs: list[LayerExactDiff | None] = [None] * len(tasks)
|
|
105
|
+
cuda_set_items: list[tuple[int, ExactLayerTask, tuple[tuple[int, int, int, int], ...], tuple[tuple[int, int, int, int], ...]]] = []
|
|
106
|
+
fallback = 0
|
|
107
|
+
unsupported = 0
|
|
108
|
+
skipped = 0
|
|
109
|
+
rect_set = 0
|
|
110
|
+
cuda_rect_set = 0
|
|
111
|
+
for index, task in enumerate(tasks):
|
|
112
|
+
try:
|
|
113
|
+
old_rect, new_rect = _task_rect_pair(task)
|
|
114
|
+
except UnsupportedExactComponent:
|
|
115
|
+
rect_sets = _task_rect_sets(task)
|
|
116
|
+
if rect_sets is not None:
|
|
117
|
+
cuda_set_items.append((index, task, rect_sets[0], rect_sets[1]))
|
|
118
|
+
continue
|
|
119
|
+
try:
|
|
120
|
+
diffs[index] = _compare_rect_or_rect_set_task(task)
|
|
121
|
+
rect_set += 1
|
|
122
|
+
continue
|
|
123
|
+
except UnsupportedExactComponent:
|
|
124
|
+
pass
|
|
125
|
+
unsupported += 1
|
|
126
|
+
if not self.fallback_to_gdstk:
|
|
127
|
+
self.stats = RectExactStats(0, fallback, unsupported, skipped, rect_set, cuda_rect_set)
|
|
128
|
+
raise
|
|
129
|
+
diffs[index] = _exact_layer_diff_task(task)
|
|
130
|
+
fallback += 1
|
|
131
|
+
continue
|
|
132
|
+
if old_rect is not None and old_rect == new_rect:
|
|
133
|
+
diffs[index] = LayerExactDiff(layer=task.layer)
|
|
134
|
+
skipped += 1
|
|
135
|
+
continue
|
|
136
|
+
old_rects = () if old_rect is None else (old_rect,)
|
|
137
|
+
new_rects = () if new_rect is None else (new_rect,)
|
|
138
|
+
cuda_set_items.append((index, task, old_rects, new_rects))
|
|
139
|
+
if cuda_set_items:
|
|
140
|
+
max_rects = max(max(len(old_rects), len(new_rects)) for _index, _task, old_rects, new_rects in cuda_set_items)
|
|
141
|
+
if max_rects > 16:
|
|
142
|
+
for index, task, _old_rects, _new_rects in cuda_set_items:
|
|
143
|
+
try:
|
|
144
|
+
diffs[index] = _compare_rect_or_rect_set_task(task)
|
|
145
|
+
rect_set += 1
|
|
146
|
+
except UnsupportedExactComponent:
|
|
147
|
+
unsupported += 1
|
|
148
|
+
if not self.fallback_to_gdstk:
|
|
149
|
+
self.stats = RectExactStats(0, fallback, unsupported, skipped, rect_set, cuda_rect_set)
|
|
150
|
+
raise
|
|
151
|
+
diffs[index] = _exact_layer_diff_task(task)
|
|
152
|
+
fallback += 1
|
|
153
|
+
else:
|
|
154
|
+
old_rects_array = np.zeros((len(cuda_set_items), max_rects, 4), dtype=np.int64)
|
|
155
|
+
new_rects_array = np.zeros((len(cuda_set_items), max_rects, 4), dtype=np.int64)
|
|
156
|
+
old_counts = np.zeros((len(cuda_set_items),), dtype=np.uint8)
|
|
157
|
+
new_counts = np.zeros((len(cuda_set_items),), dtype=np.uint8)
|
|
158
|
+
for row, (_index, _task, old_rects, new_rects) in enumerate(cuda_set_items):
|
|
159
|
+
old_counts[row] = len(old_rects)
|
|
160
|
+
new_counts[row] = len(new_rects)
|
|
161
|
+
for col, rect in enumerate(old_rects):
|
|
162
|
+
old_rects_array[row, col] = rect
|
|
163
|
+
for col, rect in enumerate(new_rects):
|
|
164
|
+
new_rects_array[row, col] = rect
|
|
165
|
+
try:
|
|
166
|
+
cuda_rects, cuda_counts, cuda_areas = run_cuda_rect_set_exact_fragments(
|
|
167
|
+
old_rects_array,
|
|
168
|
+
new_rects_array,
|
|
169
|
+
old_counts,
|
|
170
|
+
new_counts,
|
|
171
|
+
)
|
|
172
|
+
except Exception:
|
|
173
|
+
cuda_rects = cuda_counts = cuda_areas = None
|
|
174
|
+
if cuda_rects is None or cuda_counts is None or cuda_areas is None:
|
|
175
|
+
for index, task, _old_rects, _new_rects in cuda_set_items:
|
|
176
|
+
try:
|
|
177
|
+
diffs[index] = _compare_rect_or_rect_set_task(task)
|
|
178
|
+
rect_set += 1
|
|
179
|
+
except UnsupportedExactComponent:
|
|
180
|
+
unsupported += 1
|
|
181
|
+
if not self.fallback_to_gdstk:
|
|
182
|
+
self.stats = RectExactStats(0, fallback, unsupported, skipped, rect_set, cuda_rect_set)
|
|
183
|
+
raise
|
|
184
|
+
diffs[index] = _exact_layer_diff_task(task)
|
|
185
|
+
fallback += 1
|
|
186
|
+
else:
|
|
187
|
+
for row, (index, task, _old_rects, _new_rects) in enumerate(cuda_set_items):
|
|
188
|
+
old_count = int(cuda_counts[row, 0])
|
|
189
|
+
new_count = int(cuda_counts[row, 1])
|
|
190
|
+
old_fragments = tuple(
|
|
191
|
+
_fragment_from_rect(tuple(int(v) for v in cuda_rects[row, 0, piece]))
|
|
192
|
+
for piece in range(old_count)
|
|
193
|
+
)
|
|
194
|
+
new_fragments = tuple(
|
|
195
|
+
_fragment_from_rect(tuple(int(v) for v in cuda_rects[row, 1, piece]))
|
|
196
|
+
for piece in range(new_count)
|
|
197
|
+
)
|
|
198
|
+
diff = LayerExactDiff(
|
|
199
|
+
layer=task.layer,
|
|
200
|
+
old_minus_new=old_fragments,
|
|
201
|
+
new_minus_old=new_fragments,
|
|
202
|
+
xor=old_fragments + new_fragments,
|
|
203
|
+
)
|
|
204
|
+
expected = np.asarray(
|
|
205
|
+
[
|
|
206
|
+
diff.old_minus_new_twice_area,
|
|
207
|
+
diff.new_minus_old_twice_area,
|
|
208
|
+
diff.xor_twice_area,
|
|
209
|
+
],
|
|
210
|
+
dtype=np.int64,
|
|
211
|
+
)
|
|
212
|
+
if not np.array_equal(cuda_areas[row], expected):
|
|
213
|
+
raise UnsupportedExactComponent(
|
|
214
|
+
f"CUDA rectangle set exact area mismatch for task {index}: "
|
|
215
|
+
f"cuda={cuda_areas[row].tolist()} expected={expected.tolist()}"
|
|
216
|
+
)
|
|
217
|
+
diffs[index] = diff
|
|
218
|
+
cuda_rect_set += 1
|
|
219
|
+
self.stats = RectExactStats(0, fallback, unsupported, skipped, rect_set, cuda_rect_set)
|
|
220
|
+
return tuple(diff for diff in diffs if diff is not None)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _compare_rect_task(task: ExactLayerTask) -> LayerExactDiff:
|
|
224
|
+
if task.clip_bbox is not None:
|
|
225
|
+
raise UnsupportedExactComponent("rectangle exact backend does not support clipped stripe tasks")
|
|
226
|
+
old_rects = tuple(_as_rect(points) for points in (task.old_points or ()))
|
|
227
|
+
new_rects = tuple(_as_rect(points) for points in (task.new_points or ()))
|
|
228
|
+
if any(rect is None for rect in old_rects + new_rects):
|
|
229
|
+
raise UnsupportedExactComponent("component contains non-rectangle polygon")
|
|
230
|
+
if len(old_rects) > 1 or len(new_rects) > 1:
|
|
231
|
+
raise UnsupportedExactComponent("component contains multiple polygons on one side")
|
|
232
|
+
old_rect = old_rects[0] if old_rects else None
|
|
233
|
+
new_rect = new_rects[0] if new_rects else None
|
|
234
|
+
if old_rect is None and new_rect is None:
|
|
235
|
+
return LayerExactDiff(layer=task.layer)
|
|
236
|
+
old_minus = _rect_minus_rect(old_rect, new_rect) if old_rect is not None else ()
|
|
237
|
+
new_minus = _rect_minus_rect(new_rect, old_rect) if new_rect is not None else ()
|
|
238
|
+
old_fragments = tuple(_fragment_from_rect(rect) for rect in old_minus)
|
|
239
|
+
new_fragments = tuple(_fragment_from_rect(rect) for rect in new_minus)
|
|
240
|
+
return LayerExactDiff(
|
|
241
|
+
layer=task.layer,
|
|
242
|
+
old_minus_new=old_fragments,
|
|
243
|
+
new_minus_old=new_fragments,
|
|
244
|
+
xor=old_fragments + new_fragments,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _compare_rect_set_task(task: ExactLayerTask) -> LayerExactDiff:
|
|
249
|
+
if task.clip_bbox is not None:
|
|
250
|
+
raise UnsupportedExactComponent("rectangle set exact backend does not support clipped stripe tasks")
|
|
251
|
+
old_rects = tuple(_as_rect(points) for points in (task.old_points or ()))
|
|
252
|
+
new_rects = tuple(_as_rect(points) for points in (task.new_points or ()))
|
|
253
|
+
if any(rect is None for rect in old_rects + new_rects):
|
|
254
|
+
raise UnsupportedExactComponent("rectangle set contains non-rectangle polygon")
|
|
255
|
+
old_rect_tuple = tuple(rect for rect in old_rects if rect is not None)
|
|
256
|
+
new_rect_tuple = tuple(rect for rect in new_rects if rect is not None)
|
|
257
|
+
if len(old_rect_tuple) <= 1 and len(new_rect_tuple) <= 1:
|
|
258
|
+
raise UnsupportedExactComponent("single rectangle task is handled by the rectangle pair path")
|
|
259
|
+
old_minus = _rect_set_minus_rect_set(old_rect_tuple, new_rect_tuple)
|
|
260
|
+
new_minus = _rect_set_minus_rect_set(new_rect_tuple, old_rect_tuple)
|
|
261
|
+
old_fragments = tuple(_fragment_from_rect(rect) for rect in old_minus)
|
|
262
|
+
new_fragments = tuple(_fragment_from_rect(rect) for rect in new_minus)
|
|
263
|
+
return LayerExactDiff(
|
|
264
|
+
layer=task.layer,
|
|
265
|
+
old_minus_new=old_fragments,
|
|
266
|
+
new_minus_old=new_fragments,
|
|
267
|
+
xor=old_fragments + new_fragments,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _compare_rect_or_rect_set_task(task: ExactLayerTask) -> LayerExactDiff:
|
|
272
|
+
try:
|
|
273
|
+
return _compare_rect_task(task)
|
|
274
|
+
except UnsupportedExactComponent:
|
|
275
|
+
return _compare_rect_set_task(task)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _task_rect_pair(task: ExactLayerTask) -> tuple[tuple[int, int, int, int] | None, tuple[int, int, int, int] | None]:
|
|
279
|
+
if task.clip_bbox is not None:
|
|
280
|
+
raise UnsupportedExactComponent("rectangle exact backend does not support clipped stripe tasks")
|
|
281
|
+
old_rects = tuple(_as_rect(points) for points in (task.old_points or ()))
|
|
282
|
+
new_rects = tuple(_as_rect(points) for points in (task.new_points or ()))
|
|
283
|
+
if any(rect is None for rect in old_rects + new_rects):
|
|
284
|
+
raise UnsupportedExactComponent("component contains non-rectangle polygon")
|
|
285
|
+
if len(old_rects) > 1 or len(new_rects) > 1:
|
|
286
|
+
raise UnsupportedExactComponent("component contains multiple polygons on one side")
|
|
287
|
+
old_rect = old_rects[0] if old_rects else None
|
|
288
|
+
new_rect = new_rects[0] if new_rects else None
|
|
289
|
+
if old_rect is None and new_rect is None:
|
|
290
|
+
return None, None
|
|
291
|
+
return old_rect, new_rect
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _task_rect_sets(
|
|
295
|
+
task: ExactLayerTask,
|
|
296
|
+
) -> tuple[tuple[tuple[int, int, int, int], ...], tuple[tuple[int, int, int, int], ...]] | None:
|
|
297
|
+
if task.clip_bbox is not None:
|
|
298
|
+
return None
|
|
299
|
+
old_rects = tuple(_as_rect(points) for points in (task.old_points or ()))
|
|
300
|
+
new_rects = tuple(_as_rect(points) for points in (task.new_points or ()))
|
|
301
|
+
if any(rect is None for rect in old_rects + new_rects):
|
|
302
|
+
return None
|
|
303
|
+
old_rect_tuple = tuple(rect for rect in old_rects if rect is not None)
|
|
304
|
+
new_rect_tuple = tuple(rect for rect in new_rects if rect is not None)
|
|
305
|
+
if len(old_rect_tuple) <= 1 and len(new_rect_tuple) <= 1:
|
|
306
|
+
return None
|
|
307
|
+
return old_rect_tuple, new_rect_tuple
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _as_rect(points: tuple[tuple[int, int], ...]) -> tuple[int, int, int, int] | None:
|
|
311
|
+
if len(points) != 4:
|
|
312
|
+
return None
|
|
313
|
+
xs = sorted({int(x) for x, _ in points})
|
|
314
|
+
ys = sorted({int(y) for _, y in points})
|
|
315
|
+
if len(xs) != 2 or len(ys) != 2:
|
|
316
|
+
return None
|
|
317
|
+
expected = {(xs[0], ys[0]), (xs[0], ys[1]), (xs[1], ys[0]), (xs[1], ys[1])}
|
|
318
|
+
if set(points) != expected:
|
|
319
|
+
return None
|
|
320
|
+
if xs[0] == xs[1] or ys[0] == ys[1]:
|
|
321
|
+
return None
|
|
322
|
+
return xs[0], ys[0], xs[1], ys[1]
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _rect_minus_rect(
|
|
326
|
+
rect: tuple[int, int, int, int],
|
|
327
|
+
cutter: tuple[int, int, int, int] | None,
|
|
328
|
+
) -> tuple[tuple[int, int, int, int], ...]:
|
|
329
|
+
if cutter is None:
|
|
330
|
+
return (rect,)
|
|
331
|
+
ax0, ay0, ax1, ay1 = rect
|
|
332
|
+
bx0, by0, bx1, by1 = cutter
|
|
333
|
+
ix0 = max(ax0, bx0)
|
|
334
|
+
iy0 = max(ay0, by0)
|
|
335
|
+
ix1 = min(ax1, bx1)
|
|
336
|
+
iy1 = min(ay1, by1)
|
|
337
|
+
if ix0 >= ix1 or iy0 >= iy1:
|
|
338
|
+
return (rect,)
|
|
339
|
+
pieces = (
|
|
340
|
+
(ax0, ay0, ix0, ay1),
|
|
341
|
+
(ix1, ay0, ax1, ay1),
|
|
342
|
+
(ix0, ay0, ix1, iy0),
|
|
343
|
+
(ix0, iy1, ix1, ay1),
|
|
344
|
+
)
|
|
345
|
+
return tuple(piece for piece in pieces if _rect_area(piece) > 0)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _rect_area(rect: tuple[int, int, int, int]) -> int:
|
|
349
|
+
x0, y0, x1, y1 = rect
|
|
350
|
+
return max(0, x1 - x0) * max(0, y1 - y0)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _rect_set_minus_rect_set(
|
|
354
|
+
source: tuple[tuple[int, int, int, int], ...],
|
|
355
|
+
cutters: tuple[tuple[int, int, int, int], ...],
|
|
356
|
+
) -> tuple[tuple[int, int, int, int], ...]:
|
|
357
|
+
if not source:
|
|
358
|
+
return ()
|
|
359
|
+
xs = sorted({x for rect in source + cutters for x in (rect[0], rect[2])})
|
|
360
|
+
ys = sorted({y for rect in source + cutters for y in (rect[1], rect[3])})
|
|
361
|
+
if len(xs) < 2 or len(ys) < 2:
|
|
362
|
+
return ()
|
|
363
|
+
cells = []
|
|
364
|
+
for x0, x1 in zip(xs, xs[1:]):
|
|
365
|
+
if x0 == x1:
|
|
366
|
+
continue
|
|
367
|
+
for y0, y1 in zip(ys, ys[1:]):
|
|
368
|
+
if y0 == y1:
|
|
369
|
+
continue
|
|
370
|
+
inside_source = any(_rect_contains_cell(rect, x0, y0, x1, y1) for rect in source)
|
|
371
|
+
if not inside_source:
|
|
372
|
+
continue
|
|
373
|
+
inside_cutter = any(_rect_contains_cell(rect, x0, y0, x1, y1) for rect in cutters)
|
|
374
|
+
if not inside_cutter:
|
|
375
|
+
cells.append((x0, y0, x1, y1))
|
|
376
|
+
return _merge_rect_cells(tuple(cells))
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _rect_contains_cell(rect: tuple[int, int, int, int], x0: int, y0: int, x1: int, y1: int) -> bool:
|
|
380
|
+
return rect[0] <= x0 and x1 <= rect[2] and rect[1] <= y0 and y1 <= rect[3]
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _merge_rect_cells(cells: tuple[tuple[int, int, int, int], ...]) -> tuple[tuple[int, int, int, int], ...]:
|
|
384
|
+
if not cells:
|
|
385
|
+
return ()
|
|
386
|
+
current = sorted(cells)
|
|
387
|
+
changed = True
|
|
388
|
+
while changed:
|
|
389
|
+
changed = False
|
|
390
|
+
used = [False] * len(current)
|
|
391
|
+
merged: list[tuple[int, int, int, int]] = []
|
|
392
|
+
for i, left in enumerate(current):
|
|
393
|
+
if used[i]:
|
|
394
|
+
continue
|
|
395
|
+
candidate = left
|
|
396
|
+
for j in range(i + 1, len(current)):
|
|
397
|
+
if used[j]:
|
|
398
|
+
continue
|
|
399
|
+
combined = _try_merge_rect_pair(candidate, current[j])
|
|
400
|
+
if combined is not None:
|
|
401
|
+
candidate = combined
|
|
402
|
+
used[j] = True
|
|
403
|
+
changed = True
|
|
404
|
+
used[i] = True
|
|
405
|
+
merged.append(candidate)
|
|
406
|
+
current = sorted(merged)
|
|
407
|
+
return tuple(current)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _try_merge_rect_pair(
|
|
411
|
+
left: tuple[int, int, int, int],
|
|
412
|
+
right: tuple[int, int, int, int],
|
|
413
|
+
) -> tuple[int, int, int, int] | None:
|
|
414
|
+
if left[1] == right[1] and left[3] == right[3] and (left[2] == right[0] or right[2] == left[0]):
|
|
415
|
+
return min(left[0], right[0]), left[1], max(left[2], right[2]), left[3]
|
|
416
|
+
if left[0] == right[0] and left[2] == right[2] and (left[3] == right[1] or right[3] == left[1]):
|
|
417
|
+
return left[0], min(left[1], right[1]), left[2], max(left[3], right[3])
|
|
418
|
+
return None
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _fragment_from_rect(rect: tuple[int, int, int, int]) -> DiffFragment:
|
|
422
|
+
x0, y0, x1, y1 = rect
|
|
423
|
+
points = ((x0, y0), (x1, y0), (x1, y1), (x0, y1))
|
|
424
|
+
return DiffFragment(points=points, bbox=polygon_bbox(points), twice_area=polygon_twice_area(points))
|
|
@@ -0,0 +1,252 @@
|
|
|
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
|
+
from ..config import LayerSpec
|
|
10
|
+
from ..native_cache import ensure_cached_native_library
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GeometryCtypesUnavailable(RuntimeError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _CGeometryLayerResult(ctypes.Structure):
|
|
18
|
+
_fields_ = [
|
|
19
|
+
("layer", ctypes.c_int32),
|
|
20
|
+
("datatype", ctypes.c_int32),
|
|
21
|
+
("count", ctypes.c_uint64),
|
|
22
|
+
("x0", ctypes.c_int64),
|
|
23
|
+
("y0", ctypes.c_int64),
|
|
24
|
+
("x1", ctypes.c_int64),
|
|
25
|
+
("y1", ctypes.c_int64),
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _CGeometrySummaryResult(ctypes.Structure):
|
|
30
|
+
_fields_ = [
|
|
31
|
+
("flattened_shape_count", ctypes.c_uint64),
|
|
32
|
+
("layer_count", ctypes.c_uint64),
|
|
33
|
+
("geometry_hash", ctypes.c_uint64),
|
|
34
|
+
("layers", ctypes.POINTER(_CGeometryLayerResult)),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _CGeometryCompareResult(ctypes.Structure):
|
|
39
|
+
_fields_ = [
|
|
40
|
+
("old_flattened_shape_count", ctypes.c_uint64),
|
|
41
|
+
("new_flattened_shape_count", ctypes.c_uint64),
|
|
42
|
+
("old_layer_count", ctypes.c_uint64),
|
|
43
|
+
("new_layer_count", ctypes.c_uint64),
|
|
44
|
+
("old_geometry_hash", ctypes.c_uint64),
|
|
45
|
+
("new_geometry_hash", ctypes.c_uint64),
|
|
46
|
+
("summaries_match", ctypes.c_uint8),
|
|
47
|
+
("detail_computed", ctypes.c_uint8),
|
|
48
|
+
("old_layers", ctypes.POINTER(_CGeometryLayerResult)),
|
|
49
|
+
("new_layers", ctypes.POINTER(_CGeometryLayerResult)),
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class NativeGeometryLayerSummary:
|
|
55
|
+
layer: LayerSpec
|
|
56
|
+
count: int
|
|
57
|
+
bbox: tuple[int, int, int, int]
|
|
58
|
+
|
|
59
|
+
def to_json(self) -> dict[str, object]:
|
|
60
|
+
return {"layer": self.layer.to_json(), "count": self.count, "bbox": list(self.bbox)}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class NativeGeometrySummary:
|
|
65
|
+
path: Path
|
|
66
|
+
flattened_shape_count: int
|
|
67
|
+
geometry_hash: int
|
|
68
|
+
layers: tuple[NativeGeometryLayerSummary, ...]
|
|
69
|
+
|
|
70
|
+
def to_json(self) -> dict[str, object]:
|
|
71
|
+
return {
|
|
72
|
+
"path": str(self.path),
|
|
73
|
+
"flattened_shape_count": self.flattened_shape_count,
|
|
74
|
+
"geometry_hash": f"{self.geometry_hash:016x}",
|
|
75
|
+
"layers": [layer.to_json() for layer in self.layers],
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class NativeGeometryCompare:
|
|
81
|
+
old_summary: NativeGeometrySummary
|
|
82
|
+
new_summary: NativeGeometrySummary
|
|
83
|
+
summaries_match: bool
|
|
84
|
+
detail_computed: bool
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def summarize_gds_geometry_native(path: str | Path, *, top_name: str | None = None, counts_only: bool = False) -> NativeGeometrySummary:
|
|
88
|
+
library = _load_library()
|
|
89
|
+
result = _CGeometrySummaryResult()
|
|
90
|
+
top_bytes = top_name.encode("utf-8") if top_name else None
|
|
91
|
+
function = library.gds_geometry_summary_counts_only if counts_only else library.gds_geometry_summary
|
|
92
|
+
status = function(str(path).encode("utf-8"), top_bytes, ctypes.byref(result))
|
|
93
|
+
if status != 0:
|
|
94
|
+
message = library.gds_geometry_last_error()
|
|
95
|
+
raise GeometryCtypesUnavailable(message.decode("utf-8") if message else "native geometry scan failed")
|
|
96
|
+
try:
|
|
97
|
+
layers = []
|
|
98
|
+
if result.layer_count:
|
|
99
|
+
array = ctypes.cast(
|
|
100
|
+
result.layers,
|
|
101
|
+
ctypes.POINTER(_CGeometryLayerResult * int(result.layer_count)),
|
|
102
|
+
).contents
|
|
103
|
+
for item in array:
|
|
104
|
+
layers.append(
|
|
105
|
+
NativeGeometryLayerSummary(
|
|
106
|
+
layer=LayerSpec(int(item.layer), int(item.datatype)),
|
|
107
|
+
count=int(item.count),
|
|
108
|
+
bbox=(int(item.x0), int(item.y0), int(item.x1), int(item.y1)),
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
return NativeGeometrySummary(
|
|
112
|
+
path=Path(path),
|
|
113
|
+
flattened_shape_count=int(result.flattened_shape_count),
|
|
114
|
+
geometry_hash=int(result.geometry_hash),
|
|
115
|
+
layers=tuple(sorted(layers, key=lambda item: item.layer)),
|
|
116
|
+
)
|
|
117
|
+
finally:
|
|
118
|
+
library.gds_geometry_free_summary(ctypes.byref(result))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def compare_gds_geometry_native(
|
|
122
|
+
old_path: str | Path,
|
|
123
|
+
new_path: str | Path,
|
|
124
|
+
*,
|
|
125
|
+
old_top_name: str | None = None,
|
|
126
|
+
new_top_name: str | None = None,
|
|
127
|
+
) -> NativeGeometryCompare:
|
|
128
|
+
library = _load_library()
|
|
129
|
+
result = _CGeometryCompareResult()
|
|
130
|
+
old_top_bytes = old_top_name.encode("utf-8") if old_top_name else None
|
|
131
|
+
new_top_bytes = new_top_name.encode("utf-8") if new_top_name else None
|
|
132
|
+
status = library.gds_geometry_compare(
|
|
133
|
+
str(old_path).encode("utf-8"),
|
|
134
|
+
str(new_path).encode("utf-8"),
|
|
135
|
+
old_top_bytes,
|
|
136
|
+
new_top_bytes,
|
|
137
|
+
ctypes.byref(result),
|
|
138
|
+
)
|
|
139
|
+
if status != 0:
|
|
140
|
+
message = library.gds_geometry_last_error()
|
|
141
|
+
raise GeometryCtypesUnavailable(message.decode("utf-8") if message else "native geometry compare failed")
|
|
142
|
+
try:
|
|
143
|
+
old_layers = _layers_from_pointer(result.old_layers, int(result.old_layer_count))
|
|
144
|
+
new_layers = _layers_from_pointer(result.new_layers, int(result.new_layer_count))
|
|
145
|
+
return NativeGeometryCompare(
|
|
146
|
+
old_summary=NativeGeometrySummary(
|
|
147
|
+
path=Path(old_path),
|
|
148
|
+
flattened_shape_count=int(result.old_flattened_shape_count),
|
|
149
|
+
geometry_hash=int(result.old_geometry_hash),
|
|
150
|
+
layers=old_layers,
|
|
151
|
+
),
|
|
152
|
+
new_summary=NativeGeometrySummary(
|
|
153
|
+
path=Path(new_path),
|
|
154
|
+
flattened_shape_count=int(result.new_flattened_shape_count),
|
|
155
|
+
geometry_hash=int(result.new_geometry_hash),
|
|
156
|
+
layers=new_layers,
|
|
157
|
+
),
|
|
158
|
+
summaries_match=bool(result.summaries_match),
|
|
159
|
+
detail_computed=bool(result.detail_computed),
|
|
160
|
+
)
|
|
161
|
+
finally:
|
|
162
|
+
library.gds_geometry_free_compare(ctypes.byref(result))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def geometry_ctypes_available() -> bool:
|
|
166
|
+
try:
|
|
167
|
+
_load_library()
|
|
168
|
+
except GeometryCtypesUnavailable:
|
|
169
|
+
return False
|
|
170
|
+
return True
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@lru_cache(maxsize=1)
|
|
174
|
+
def _load_library() -> ctypes.CDLL:
|
|
175
|
+
path = _library_path()
|
|
176
|
+
library = ctypes.CDLL(str(path))
|
|
177
|
+
library.gds_geometry_summary.argtypes = [
|
|
178
|
+
ctypes.c_char_p,
|
|
179
|
+
ctypes.c_char_p,
|
|
180
|
+
ctypes.POINTER(_CGeometrySummaryResult),
|
|
181
|
+
]
|
|
182
|
+
library.gds_geometry_summary.restype = ctypes.c_int
|
|
183
|
+
library.gds_geometry_summary_counts_only.argtypes = [
|
|
184
|
+
ctypes.c_char_p,
|
|
185
|
+
ctypes.c_char_p,
|
|
186
|
+
ctypes.POINTER(_CGeometrySummaryResult),
|
|
187
|
+
]
|
|
188
|
+
library.gds_geometry_summary_counts_only.restype = ctypes.c_int
|
|
189
|
+
library.gds_geometry_compare.argtypes = [
|
|
190
|
+
ctypes.c_char_p,
|
|
191
|
+
ctypes.c_char_p,
|
|
192
|
+
ctypes.c_char_p,
|
|
193
|
+
ctypes.c_char_p,
|
|
194
|
+
ctypes.POINTER(_CGeometryCompareResult),
|
|
195
|
+
]
|
|
196
|
+
library.gds_geometry_compare.restype = ctypes.c_int
|
|
197
|
+
library.gds_geometry_free_summary.argtypes = [ctypes.POINTER(_CGeometrySummaryResult)]
|
|
198
|
+
library.gds_geometry_free_summary.restype = None
|
|
199
|
+
library.gds_geometry_free_compare.argtypes = [ctypes.POINTER(_CGeometryCompareResult)]
|
|
200
|
+
library.gds_geometry_free_compare.restype = None
|
|
201
|
+
library.gds_geometry_last_error.argtypes = []
|
|
202
|
+
library.gds_geometry_last_error.restype = ctypes.c_char_p
|
|
203
|
+
return library
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _layers_from_pointer(pointer, count: int) -> tuple[NativeGeometryLayerSummary, ...]:
|
|
207
|
+
if count == 0:
|
|
208
|
+
return ()
|
|
209
|
+
array = ctypes.cast(pointer, ctypes.POINTER(_CGeometryLayerResult * count)).contents
|
|
210
|
+
layers = [
|
|
211
|
+
NativeGeometryLayerSummary(
|
|
212
|
+
layer=LayerSpec(int(item.layer), int(item.datatype)),
|
|
213
|
+
count=int(item.count),
|
|
214
|
+
bbox=(int(item.x0), int(item.y0), int(item.x1), int(item.y1)),
|
|
215
|
+
)
|
|
216
|
+
for item in array
|
|
217
|
+
]
|
|
218
|
+
return tuple(sorted(layers, key=lambda item: item.layer))
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _build_library(path: Path) -> None:
|
|
222
|
+
source = _source_path()
|
|
223
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
224
|
+
command = [
|
|
225
|
+
"g++",
|
|
226
|
+
"-std=c++17",
|
|
227
|
+
"-O3",
|
|
228
|
+
"-shared",
|
|
229
|
+
"-fPIC",
|
|
230
|
+
str(source),
|
|
231
|
+
"-o",
|
|
232
|
+
str(path),
|
|
233
|
+
]
|
|
234
|
+
try:
|
|
235
|
+
completed = subprocess.run(command, check=False, text=True, capture_output=True)
|
|
236
|
+
except FileNotFoundError as exc:
|
|
237
|
+
raise GeometryCtypesUnavailable("g++ is not available") from exc
|
|
238
|
+
if completed.returncode != 0:
|
|
239
|
+
raise GeometryCtypesUnavailable((completed.stderr or completed.stdout).strip() or "g++ failed")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _library_path() -> Path:
|
|
243
|
+
return ensure_cached_native_library(
|
|
244
|
+
"_gds_geometry_scan.so",
|
|
245
|
+
sources=(_source_path(),),
|
|
246
|
+
build_key=("g++", "-std=c++17", "-O3", "-shared", "-fPIC"),
|
|
247
|
+
builder=_build_library,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _source_path() -> Path:
|
|
252
|
+
return Path(__file__).resolve().parents[1] / "native_src" / "gds_geometry_scan.cpp"
|