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,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"