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,649 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass, field
5
+ from typing import Callable
6
+
7
+ from .config import LayerSpec
8
+ from .geometry import PolygonBuffer, polygon_bbox, polygon_twice_area
9
+ from .grid import snap_to_grid
10
+ from .multiprocessing_support import fork_context
11
+
12
+ _WORKER_OLD_BY_LAYER = None
13
+ _WORKER_NEW_BY_LAYER = None
14
+ _WORKER_PRECISION = 1.0
15
+ _WORKER_BUFFER = None
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class DiffFragment:
20
+ points: tuple[tuple[int, int], ...]
21
+ bbox: tuple[int, int, int, int]
22
+ twice_area: int
23
+
24
+ @property
25
+ def area_dbu2_string(self) -> str:
26
+ return format_twice_area(self.twice_area)
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class LayerExactDiff:
31
+ layer: LayerSpec
32
+ old_minus_new: tuple[DiffFragment, ...] = field(default_factory=tuple)
33
+ new_minus_old: tuple[DiffFragment, ...] = field(default_factory=tuple)
34
+ xor: tuple[DiffFragment, ...] = field(default_factory=tuple)
35
+
36
+ @property
37
+ def old_minus_new_twice_area(self) -> int:
38
+ return sum(fragment.twice_area for fragment in self.old_minus_new)
39
+
40
+ @property
41
+ def new_minus_old_twice_area(self) -> int:
42
+ return sum(fragment.twice_area for fragment in self.new_minus_old)
43
+
44
+ @property
45
+ def xor_twice_area(self) -> int:
46
+ return sum(fragment.twice_area for fragment in self.xor)
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class ExactOracleResult:
51
+ layers: tuple[LayerExactDiff, ...]
52
+
53
+ @property
54
+ def equivalent(self) -> bool:
55
+ return all(layer.xor_twice_area == 0 for layer in self.layers)
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class ExactLayerTask:
60
+ layer: LayerSpec
61
+ precision: float
62
+ old_points: tuple[tuple[tuple[int, int], ...], ...] | None = None
63
+ new_points: tuple[tuple[tuple[int, int], ...], ...] | None = None
64
+ clip_bbox: tuple[int, int, int, int] | None = None
65
+ partition: str = "layer"
66
+
67
+
68
+ def run_full_exact_oracle(
69
+ old_buffer: PolygonBuffer,
70
+ new_buffer: PolygonBuffer,
71
+ *,
72
+ precision: float = 1.0,
73
+ layers: set[LayerSpec] | None = None,
74
+ workers: int | None = None,
75
+ component_partition: bool = False,
76
+ component_executor: Callable[[tuple[ExactLayerTask, ...]], tuple[LayerExactDiff, ...]] | None = None,
77
+ force_small_component_partition: bool = False,
78
+ ) -> ExactOracleResult:
79
+ old_by_layer = _buffer_points_by_layer(old_buffer)
80
+ new_by_layer = _buffer_points_by_layer(new_buffer)
81
+ selected_layers = tuple(sorted((set(old_by_layer) | set(new_by_layer)) if layers is None else set(layers)))
82
+ worker_count = _resolve_worker_count(workers, len(selected_layers))
83
+ tasks_by_layer = {
84
+ layer: _layer_task_specs(
85
+ layer,
86
+ old_by_layer.get(layer, ()),
87
+ new_by_layer.get(layer, ()),
88
+ precision,
89
+ worker_count,
90
+ component_partition=component_partition,
91
+ prefer_rectangle_pairs=component_executor is not None,
92
+ force_small_component_partition=force_small_component_partition,
93
+ )
94
+ for layer in selected_layers
95
+ }
96
+ flat_tasks = tuple(task for layer_tasks in tasks_by_layer.values() for task in layer_tasks)
97
+ if component_executor is not None:
98
+ stripe_diffs = component_executor(flat_tasks) if flat_tasks else ()
99
+ by_layer: dict[LayerSpec, list[LayerExactDiff]] = {layer: [] for layer in selected_layers}
100
+ for diff in stripe_diffs:
101
+ by_layer[diff.layer].append(diff)
102
+ diffs = tuple(
103
+ _merge_layer_stripes(by_layer[layer]) if by_layer[layer] else LayerExactDiff(layer=layer)
104
+ for layer in selected_layers
105
+ )
106
+ elif worker_count <= 1:
107
+ diffs = tuple(
108
+ _merge_layer_stripes(_exact_layer_diff_task(task) for task in tasks_by_layer[layer])
109
+ if tasks_by_layer[layer]
110
+ else LayerExactDiff(layer=layer)
111
+ for layer in selected_layers
112
+ )
113
+ else:
114
+ context = fork_context()
115
+ global _WORKER_OLD_BY_LAYER, _WORKER_NEW_BY_LAYER, _WORKER_PRECISION
116
+ _WORKER_OLD_BY_LAYER = old_by_layer
117
+ _WORKER_NEW_BY_LAYER = new_by_layer
118
+ _WORKER_PRECISION = precision
119
+ try:
120
+ if context is None:
121
+ stripe_diffs = tuple(_exact_layer_diff_task(task) for task in flat_tasks)
122
+ else:
123
+ try:
124
+ with context.Pool(processes=worker_count) as pool:
125
+ stripe_diffs = tuple(pool.map(_exact_layer_diff_task, flat_tasks, chunksize=1))
126
+ except OSError:
127
+ stripe_diffs = tuple(_exact_layer_diff_task(task) for task in flat_tasks)
128
+ finally:
129
+ _WORKER_OLD_BY_LAYER = None
130
+ _WORKER_NEW_BY_LAYER = None
131
+ by_layer: dict[LayerSpec, list[LayerExactDiff]] = {layer: [] for layer in selected_layers}
132
+ for diff in stripe_diffs:
133
+ by_layer[diff.layer].append(diff)
134
+ diffs = tuple(
135
+ _merge_layer_stripes(by_layer[layer]) if by_layer[layer] else LayerExactDiff(layer=layer)
136
+ for layer in selected_layers
137
+ )
138
+ return ExactOracleResult(tuple(sorted(diffs, key=lambda diff: diff.layer)))
139
+
140
+
141
+ def _resolve_worker_count(workers: int | None, layer_count: int) -> int:
142
+ if layer_count <= 0:
143
+ return 1
144
+ if workers is None:
145
+ workers = os.cpu_count() or 1
146
+ return max(1, int(workers))
147
+
148
+
149
+ def _layer_task_specs(
150
+ layer: LayerSpec,
151
+ old_points: list[tuple[tuple[int, int], ...]] | tuple[tuple[tuple[int, int], ...], ...],
152
+ new_points: list[tuple[tuple[int, int], ...]] | tuple[tuple[tuple[int, int], ...], ...],
153
+ precision: float,
154
+ worker_count: int,
155
+ component_partition: bool = False,
156
+ prefer_rectangle_pairs: bool = False,
157
+ force_small_component_partition: bool = False,
158
+ ) -> tuple[ExactLayerTask, ...]:
159
+ old_tuple = tuple(old_points)
160
+ new_tuple = tuple(new_points)
161
+ total = len(old_tuple) + len(new_tuple)
162
+ if not force_small_component_partition and (worker_count <= 1 or total < 2048):
163
+ return (ExactLayerTask(layer, precision, old_tuple, new_tuple),)
164
+ if component_partition:
165
+ if prefer_rectangle_pairs:
166
+ rectangle_pair_tasks = _rectangle_pair_task_specs(layer, old_tuple, new_tuple, precision)
167
+ if rectangle_pair_tasks is not None:
168
+ return rectangle_pair_tasks
169
+ sparse_rectangle_tasks = _sparse_rectangle_residual_task_specs(layer, old_tuple, new_tuple, precision)
170
+ if sparse_rectangle_tasks is not None:
171
+ return sparse_rectangle_tasks
172
+ component_tasks, component_applicable = _component_task_specs_result(layer, old_tuple, new_tuple, precision)
173
+ if component_applicable and (prefer_rectangle_pairs or len(component_tasks) > 1 or not component_tasks):
174
+ return component_tasks
175
+ if worker_count <= 1 or total < 2048:
176
+ return (ExactLayerTask(layer, precision, old_tuple, new_tuple),)
177
+ bbox = _points_bbox(old_tuple + new_tuple)
178
+ if bbox is None:
179
+ return (ExactLayerTask(layer, precision, old_tuple, new_tuple),)
180
+ stripes = _stripe_tasks(layer, old_tuple, new_tuple, precision, bbox, worker_count)
181
+ return stripes or (ExactLayerTask(layer, precision, old_tuple, new_tuple),)
182
+
183
+
184
+ def _component_task_specs(
185
+ layer: LayerSpec,
186
+ old_tuple: tuple[tuple[tuple[int, int], ...], ...],
187
+ new_tuple: tuple[tuple[tuple[int, int], ...], ...],
188
+ precision: float,
189
+ ) -> tuple[ExactLayerTask, ...]:
190
+ return _component_task_specs_result(layer, old_tuple, new_tuple, precision)[0]
191
+
192
+
193
+ def _component_task_specs_result(
194
+ layer: LayerSpec,
195
+ old_tuple: tuple[tuple[tuple[int, int], ...], ...],
196
+ new_tuple: tuple[tuple[tuple[int, int], ...], ...],
197
+ precision: float,
198
+ ) -> tuple[tuple[ExactLayerTask, ...], bool]:
199
+ components = _independent_bbox_components(old_tuple, new_tuple)
200
+ if len(components) <= 1:
201
+ return (), False
202
+ tasks = []
203
+ for old_component, new_component in components:
204
+ if _is_identical_component_source_set(old_component, new_component):
205
+ continue
206
+ tasks.append(ExactLayerTask(layer, precision, old_component, new_component, partition="bbox-component"))
207
+ return tuple(tasks), True
208
+
209
+
210
+ def _rectangle_pair_task_specs(
211
+ layer: LayerSpec,
212
+ old_tuple: tuple[tuple[tuple[int, int], ...], ...],
213
+ new_tuple: tuple[tuple[tuple[int, int], ...], ...],
214
+ precision: float,
215
+ ) -> tuple[ExactLayerTask, ...] | None:
216
+ if len(old_tuple) != len(new_tuple) or not old_tuple:
217
+ return None
218
+ tasks = []
219
+ for old_points, new_points in zip(old_tuple, new_tuple):
220
+ old_rect = _axis_aligned_rectangle_bbox(old_points)
221
+ new_rect = _axis_aligned_rectangle_bbox(new_points)
222
+ if old_rect is None or new_rect is None:
223
+ return None
224
+ if old_rect == new_rect:
225
+ continue
226
+ tasks.append(ExactLayerTask(layer, precision, (old_points,), (new_points,), partition="rectangle-pair"))
227
+ return tuple(tasks)
228
+
229
+
230
+ def _sparse_rectangle_residual_task_specs(
231
+ layer: LayerSpec,
232
+ old_tuple: tuple[tuple[tuple[int, int], ...], ...],
233
+ new_tuple: tuple[tuple[tuple[int, int], ...], ...],
234
+ precision: float,
235
+ ) -> tuple[ExactLayerTask, ...] | None:
236
+ old_rects, old_non_rects = _split_rectangles(old_tuple)
237
+ new_rects, new_non_rects = _split_rectangles(new_tuple)
238
+ if not old_rects and not new_rects:
239
+ return None
240
+ common_rects = set(old_rects) & set(new_rects)
241
+ if not common_rects:
242
+ return None
243
+ old_residual: list[tuple[tuple[int, int], ...]] = list(old_non_rects)
244
+ new_residual: list[tuple[tuple[int, int], ...]] = list(new_non_rects)
245
+ residual_bboxes = [polygon_bbox(points) for points in old_non_rects + new_non_rects]
246
+ for rect, old_polygons in old_rects.items():
247
+ if rect not in new_rects:
248
+ residual_bboxes.append(rect)
249
+ old_residual.append(old_polygons[0])
250
+ for rect, new_polygons in new_rects.items():
251
+ if rect not in old_rects:
252
+ residual_bboxes.append(rect)
253
+ new_residual.append(new_polygons[0])
254
+ for rect, old_polygons in old_rects.items():
255
+ if rect in new_rects and _bbox_overlaps_any(rect, residual_bboxes):
256
+ old_residual.append(old_polygons[0])
257
+ for rect, new_polygons in new_rects.items():
258
+ if rect in old_rects and _bbox_overlaps_any(rect, residual_bboxes):
259
+ new_residual.append(new_polygons[0])
260
+ if len(old_residual) == len(old_tuple) and len(new_residual) == len(new_tuple):
261
+ return None
262
+ if not old_residual and not new_residual:
263
+ return ()
264
+ component_tasks, component_applicable = _component_task_specs_result(layer, tuple(old_residual), tuple(new_residual), precision)
265
+ if component_applicable:
266
+ return component_tasks
267
+ return (ExactLayerTask(layer, precision, tuple(old_residual), tuple(new_residual), partition="rectangle-residual"),)
268
+
269
+
270
+ def _split_rectangles(
271
+ polygons: tuple[tuple[tuple[int, int], ...], ...],
272
+ ) -> tuple[dict[tuple[int, int, int, int], list[tuple[tuple[int, int], ...]]], list[tuple[tuple[int, int], ...]]]:
273
+ rects: dict[tuple[int, int, int, int], list[tuple[tuple[int, int], ...]]] = {}
274
+ non_rects: list[tuple[tuple[int, int], ...]] = []
275
+ for points in polygons:
276
+ rect = _axis_aligned_rectangle_bbox(points)
277
+ if rect is None:
278
+ non_rects.append(points)
279
+ else:
280
+ rects.setdefault(rect, []).append(points)
281
+ return rects, non_rects
282
+
283
+
284
+ def _bbox_overlaps_any(
285
+ bbox: tuple[int, int, int, int],
286
+ others: list[tuple[int, int, int, int]],
287
+ ) -> bool:
288
+ return any(_bbox_overlaps_closed(bbox, other) for other in others)
289
+
290
+
291
+ def _is_axis_aligned_rectangle_points(points: tuple[tuple[int, int], ...]) -> bool:
292
+ return _axis_aligned_rectangle_bbox(points) is not None
293
+
294
+
295
+ def _axis_aligned_rectangle_bbox(points: tuple[tuple[int, int], ...]) -> tuple[int, int, int, int] | None:
296
+ if len(points) != 4:
297
+ return None
298
+ xs = {point[0] for point in points}
299
+ ys = {point[1] for point in points}
300
+ if len(xs) != 2 or len(ys) != 2:
301
+ return None
302
+ x0, x1 = sorted(xs)
303
+ y0, y1 = sorted(ys)
304
+ if set(points) != {(x0, y0), (x0, y1), (x1, y0), (x1, y1)}:
305
+ return None
306
+ return x0, y0, x1, y1
307
+
308
+
309
+ def _is_identical_single_rectangle_component(
310
+ old_component: tuple[tuple[tuple[int, int], ...], ...],
311
+ new_component: tuple[tuple[tuple[int, int], ...], ...],
312
+ ) -> bool:
313
+ if len(old_component) != 1 or len(new_component) != 1:
314
+ return False
315
+ old_rect = _axis_aligned_rectangle_bbox(old_component[0])
316
+ new_rect = _axis_aligned_rectangle_bbox(new_component[0])
317
+ return old_rect is not None and old_rect == new_rect
318
+
319
+
320
+ def _is_identical_component_source_set(
321
+ old_component: tuple[tuple[tuple[int, int], ...], ...],
322
+ new_component: tuple[tuple[tuple[int, int], ...], ...],
323
+ ) -> bool:
324
+ if not old_component and not new_component:
325
+ return True
326
+ if _is_identical_single_rectangle_component(old_component, new_component):
327
+ return True
328
+ return {_canonical_polygon_key(points) for points in old_component} == {
329
+ _canonical_polygon_key(points) for points in new_component
330
+ }
331
+
332
+
333
+ def _canonical_polygon_key(points: tuple[tuple[int, int], ...]) -> tuple[tuple[int, int], ...]:
334
+ cleaned = _clean_polygon_points(points)
335
+ if len(cleaned) < 2:
336
+ return cleaned
337
+ candidates = []
338
+ for sequence in (cleaned, tuple(reversed(cleaned))):
339
+ for index in range(len(sequence)):
340
+ candidates.append(sequence[index:] + sequence[:index])
341
+ return min(candidates)
342
+
343
+
344
+ def _clean_polygon_points(points: tuple[tuple[int, int], ...]) -> tuple[tuple[int, int], ...]:
345
+ cleaned: list[tuple[int, int]] = []
346
+ for point in points:
347
+ if not cleaned or cleaned[-1] != point:
348
+ cleaned.append(point)
349
+ if len(cleaned) > 1 and cleaned[0] == cleaned[-1]:
350
+ cleaned.pop()
351
+ return tuple(cleaned)
352
+
353
+
354
+ def _independent_bbox_components(
355
+ old_points: tuple[tuple[tuple[int, int], ...], ...],
356
+ new_points: tuple[tuple[tuple[int, int], ...], ...],
357
+ ) -> tuple[tuple[tuple[tuple[tuple[int, int], ...], ...], tuple[tuple[tuple[int, int], ...], ...]], ...]:
358
+ records: list[tuple[int, int, tuple[int, int, int, int], tuple[tuple[int, int], ...]]] = []
359
+ for side, polygons in ((0, old_points), (1, new_points)):
360
+ for index, points in enumerate(polygons):
361
+ records.append((side, index, polygon_bbox(points), points))
362
+ if not records:
363
+ return ()
364
+ order = sorted(range(len(records)), key=lambda idx: records[idx][2][0])
365
+ parent = list(range(len(records)))
366
+
367
+ def find(item: int) -> int:
368
+ while parent[item] != item:
369
+ parent[item] = parent[parent[item]]
370
+ item = parent[item]
371
+ return item
372
+
373
+ def union(left: int, right: int) -> None:
374
+ root_left = find(left)
375
+ root_right = find(right)
376
+ if root_left != root_right:
377
+ parent[root_right] = root_left
378
+
379
+ active: list[int] = []
380
+ for idx in order:
381
+ bbox = records[idx][2]
382
+ active = [other for other in active if records[other][2][2] >= bbox[0]]
383
+ for other in active:
384
+ if _bbox_overlaps_closed(bbox, records[other][2]):
385
+ union(idx, other)
386
+ active.append(idx)
387
+ grouped: dict[int, tuple[list[tuple[tuple[int, int], ...]], list[tuple[tuple[int, int], ...]]]] = {}
388
+ for idx, record in enumerate(records):
389
+ root = find(idx)
390
+ old_group, new_group = grouped.setdefault(root, ([], []))
391
+ if record[0] == 0:
392
+ old_group.append(record[3])
393
+ else:
394
+ new_group.append(record[3])
395
+ return tuple((tuple(old_group), tuple(new_group)) for old_group, new_group in grouped.values())
396
+
397
+
398
+ def _points_bbox(polygons: tuple[tuple[tuple[int, int], ...], ...]) -> tuple[int, int, int, int] | None:
399
+ if not polygons:
400
+ return None
401
+ bboxes = [polygon_bbox(points) for points in polygons if points]
402
+ if not bboxes:
403
+ return None
404
+ return (
405
+ min(bbox[0] for bbox in bboxes),
406
+ min(bbox[1] for bbox in bboxes),
407
+ max(bbox[2] for bbox in bboxes),
408
+ max(bbox[3] for bbox in bboxes),
409
+ )
410
+
411
+
412
+ def _stripe_tasks(
413
+ layer: LayerSpec,
414
+ old_tuple: tuple[tuple[tuple[int, int], ...], ...],
415
+ new_tuple: tuple[tuple[tuple[int, int], ...], ...],
416
+ precision: float,
417
+ bbox: tuple[int, int, int, int],
418
+ worker_count: int,
419
+ ) -> tuple[ExactLayerTask, ...]:
420
+ x0, y0, x1, y1 = bbox
421
+ if x0 == x1:
422
+ return ()
423
+ stripe_count = worker_count
424
+ width = x1 - x0 + 1
425
+ tasks = []
426
+ for index in range(stripe_count):
427
+ sx0 = x0 + (width * index) // stripe_count
428
+ sx1 = x0 + (width * (index + 1)) // stripe_count - 1
429
+ if index == stripe_count - 1:
430
+ sx1 = x1
431
+ core = (sx0, y0, sx1, y1)
432
+ tasks.append(ExactLayerTask(layer, precision, clip_bbox=core, partition="stripe"))
433
+ return tuple(tasks)
434
+
435
+
436
+ def _bbox_overlaps_closed(left: tuple[int, int, int, int], right: tuple[int, int, int, int]) -> bool:
437
+ return not (left[2] < right[0] or right[2] < left[0] or left[3] < right[1] or right[3] < left[1])
438
+
439
+
440
+ def _exact_layer_diff_task(
441
+ task: ExactLayerTask,
442
+ ) -> LayerExactDiff:
443
+ import gdstk
444
+
445
+ layer = task.layer
446
+ if task.old_points is None or task.new_points is None:
447
+ if _WORKER_OLD_BY_LAYER is None or _WORKER_NEW_BY_LAYER is None:
448
+ raise RuntimeError("exact oracle worker globals are not initialized")
449
+ old_points = tuple(_WORKER_OLD_BY_LAYER.get(layer, ()))
450
+ new_points = tuple(_WORKER_NEW_BY_LAYER.get(layer, ()))
451
+ precision = _WORKER_PRECISION
452
+ else:
453
+ old_points = task.old_points
454
+ new_points = task.new_points
455
+ precision = task.precision
456
+ clip_bbox = task.clip_bbox
457
+ if clip_bbox is not None:
458
+ old_points = tuple(points for points in old_points if _bbox_overlaps_closed(polygon_bbox(points), clip_bbox))
459
+ new_points = tuple(points for points in new_points if _bbox_overlaps_closed(polygon_bbox(points), clip_bbox))
460
+ if not old_points and not new_points:
461
+ return LayerExactDiff(layer=layer)
462
+ origin = _layer_origin(old_points, new_points)
463
+ old_polys = _to_gdstk_polygons(old_points, layer, origin)
464
+ new_polys = _to_gdstk_polygons(new_points, layer, origin)
465
+ old_minus = gdstk.boolean(old_polys, new_polys, "not", precision=precision) or []
466
+ new_minus = gdstk.boolean(new_polys, old_polys, "not", precision=precision) or []
467
+ if clip_bbox is not None:
468
+ clip_rect = [_bbox_rectangle(clip_bbox, layer, origin)]
469
+ old_minus = gdstk.boolean(old_minus, clip_rect, "and", precision=precision) or []
470
+ new_minus = gdstk.boolean(new_minus, clip_rect, "and", precision=precision) or []
471
+ xor = gdstk.boolean(old_minus, new_minus, "or", precision=precision) or []
472
+ old_fragments, new_fragments = _cancel_symmetric_direction_fragments(
473
+ _fragments_from_gdstk(old_minus, origin),
474
+ _fragments_from_gdstk(new_minus, origin),
475
+ )
476
+ return LayerExactDiff(
477
+ layer=layer,
478
+ old_minus_new=old_fragments,
479
+ new_minus_old=new_fragments,
480
+ xor=tuple(sorted(old_fragments + new_fragments, key=_fragment_sort_key)),
481
+ )
482
+
483
+
484
+ def _merge_layer_stripes(stripes) -> LayerExactDiff:
485
+ stripes = tuple(stripes)
486
+ if not stripes:
487
+ raise ValueError("cannot merge empty layer stripe list")
488
+ if len(stripes) == 1:
489
+ return stripes[0]
490
+ layer = stripes[0].layer
491
+ old_minus_points = tuple(fragment.points for stripe in stripes for fragment in stripe.old_minus_new)
492
+ new_minus_points = tuple(fragment.points for stripe in stripes for fragment in stripe.new_minus_old)
493
+ old_minus = _union_fragments(old_minus_points, layer)
494
+ new_minus = _union_fragments(new_minus_points, layer)
495
+ old_minus, new_minus = _cancel_symmetric_direction_fragments(old_minus, new_minus)
496
+ xor = _union_fragments(tuple(fragment.points for fragment in old_minus + new_minus), layer)
497
+ return LayerExactDiff(layer=layer, old_minus_new=old_minus, new_minus_old=new_minus, xor=xor)
498
+
499
+
500
+ def _union_fragments(polygons: tuple[tuple[tuple[int, int], ...], ...], layer: LayerSpec) -> tuple[DiffFragment, ...]:
501
+ if not polygons:
502
+ return ()
503
+ import gdstk
504
+
505
+ origin = _layer_origin(polygons)
506
+ merged = gdstk.boolean(_to_gdstk_polygons(polygons, layer, origin), [], "or", precision=1.0) or []
507
+ return _fragments_from_gdstk(merged, origin)
508
+
509
+
510
+ def _cancel_symmetric_direction_fragments(
511
+ old_fragments: tuple[DiffFragment, ...],
512
+ new_fragments: tuple[DiffFragment, ...],
513
+ ) -> tuple[tuple[DiffFragment, ...], tuple[DiffFragment, ...]]:
514
+ if not old_fragments or not new_fragments:
515
+ return old_fragments, new_fragments
516
+ old_counts: dict[tuple[tuple[int, int, int, int], int, tuple[tuple[int, int], ...]], int] = {}
517
+ new_counts: dict[tuple[tuple[int, int, int, int], int, tuple[tuple[int, int], ...]], int] = {}
518
+ for fragment in old_fragments:
519
+ old_counts[_direction_fragment_key(fragment)] = old_counts.get(_direction_fragment_key(fragment), 0) + 1
520
+ for fragment in new_fragments:
521
+ new_counts[_direction_fragment_key(fragment)] = new_counts.get(_direction_fragment_key(fragment), 0) + 1
522
+ old_out = []
523
+ for fragment in old_fragments:
524
+ key = _direction_fragment_key(fragment)
525
+ if new_counts.get(key, 0):
526
+ new_counts[key] -= 1
527
+ else:
528
+ old_out.append(fragment)
529
+ new_out = []
530
+ for fragment in new_fragments:
531
+ key = _direction_fragment_key(fragment)
532
+ if old_counts.get(key, 0):
533
+ old_counts[key] -= 1
534
+ else:
535
+ new_out.append(fragment)
536
+ return tuple(old_out), tuple(new_out)
537
+
538
+
539
+ def _direction_fragment_key(fragment: DiffFragment) -> tuple[tuple[int, int, int, int], int, tuple[tuple[int, int], ...]]:
540
+ return fragment.bbox, fragment.twice_area, _canonical_polygon_key(fragment.points)
541
+
542
+
543
+ def _fragment_sort_key(fragment: DiffFragment) -> tuple[tuple[int, int, int, int], int, tuple[tuple[int, int], ...]]:
544
+ return fragment.bbox, fragment.twice_area, fragment.points
545
+
546
+
547
+ def _bbox_rectangle(bbox: tuple[int, int, int, int], layer: LayerSpec, origin: tuple[int, int]) -> gdstk.Polygon:
548
+ import gdstk
549
+
550
+ ox, oy = origin
551
+ x0, y0, x1, y1 = bbox
552
+ return gdstk.rectangle((x0 - ox, y0 - oy), (x1 + 1 - ox, y1 + 1 - oy), layer=layer.layer, datatype=layer.datatype)
553
+
554
+
555
+
556
+
557
+ def format_twice_area(twice_area: int) -> str:
558
+ whole, remainder = divmod(abs(twice_area), 2)
559
+ sign = "-" if twice_area < 0 else ""
560
+ if remainder:
561
+ return f"{sign}{whole}.5"
562
+ return f"{sign}{whole}"
563
+
564
+
565
+ def _buffer_points_by_layer(buffer: PolygonBuffer) -> dict[LayerSpec, list[tuple[tuple[int, int], ...]]]:
566
+ worker_count = _resolve_worker_count(None, buffer.polygon_count)
567
+ if worker_count <= 1 or buffer.polygon_count < 4096:
568
+ return _buffer_points_by_layer_range(buffer, 0, buffer.polygon_count)
569
+
570
+ chunk_count = max(worker_count, worker_count * 8)
571
+ chunk_size = max(1, (buffer.polygon_count + chunk_count - 1) // chunk_count)
572
+ chunks = tuple((start, min(start + chunk_size, buffer.polygon_count)) for start in range(0, buffer.polygon_count, chunk_size))
573
+ context = fork_context()
574
+ if context is None:
575
+ return _buffer_points_by_layer_range(buffer, 0, buffer.polygon_count)
576
+
577
+ global _WORKER_BUFFER
578
+ _WORKER_BUFFER = buffer
579
+ try:
580
+ try:
581
+ with context.Pool(processes=worker_count) as pool:
582
+ chunk_results = pool.map(_buffer_points_by_layer_worker, chunks, chunksize=1)
583
+ except OSError:
584
+ return _buffer_points_by_layer_range(buffer, 0, buffer.polygon_count)
585
+ finally:
586
+ _WORKER_BUFFER = None
587
+
588
+ out: dict[LayerSpec, list[tuple[tuple[int, int], ...]]] = {}
589
+ for chunk in chunk_results:
590
+ for layer, polygons in chunk.items():
591
+ out.setdefault(layer, []).extend(polygons)
592
+ return out
593
+
594
+
595
+ def _buffer_points_by_layer_worker(chunk: tuple[int, int]) -> dict[LayerSpec, list[tuple[tuple[int, int], ...]]]:
596
+ if _WORKER_BUFFER is None:
597
+ raise RuntimeError("exact oracle buffer worker globals are not initialized")
598
+ start, stop = chunk
599
+ return _buffer_points_by_layer_range(_WORKER_BUFFER, start, stop)
600
+
601
+
602
+ def _buffer_points_by_layer_range(
603
+ buffer: PolygonBuffer,
604
+ start: int,
605
+ stop: int,
606
+ ) -> dict[LayerSpec, list[tuple[tuple[int, int], ...]]]:
607
+ out: dict[LayerSpec, list[tuple[tuple[int, int], ...]]] = {}
608
+ for index in range(start, stop):
609
+ layer = buffer.layer_table.layers[int(buffer.layer_ids[index])]
610
+ vertices = tuple((int(x), int(y)) for x, y in buffer.polygon_vertices(index))
611
+ out.setdefault(layer, []).append(vertices)
612
+ return out
613
+
614
+
615
+ def _layer_origin(*collections: list[tuple[tuple[int, int], ...]] | tuple[tuple[tuple[int, int], ...], ...]) -> tuple[int, int]:
616
+ xs = []
617
+ ys = []
618
+ for polygons in collections:
619
+ for points in polygons:
620
+ xs.extend(point[0] for point in points)
621
+ ys.extend(point[1] for point in points)
622
+ if not xs:
623
+ return 0, 0
624
+ return min(xs), min(ys)
625
+
626
+
627
+ def _to_gdstk_polygons(
628
+ polygons: list[tuple[tuple[int, int], ...]] | tuple[tuple[tuple[int, int], ...], ...],
629
+ layer: LayerSpec,
630
+ origin: tuple[int, int],
631
+ ) -> list[gdstk.Polygon]:
632
+ import gdstk
633
+
634
+ ox, oy = origin
635
+ return [
636
+ gdstk.Polygon([(x - ox, y - oy) for x, y in points], layer=layer.layer, datatype=layer.datatype)
637
+ for points in polygons
638
+ ]
639
+
640
+
641
+ def _fragments_from_gdstk(polygons: list[gdstk.Polygon], origin: tuple[int, int]) -> tuple[DiffFragment, ...]:
642
+ ox, oy = origin
643
+ fragments = []
644
+ for polygon in polygons:
645
+ points = tuple((snap_to_grid(point[0], 1) + ox, snap_to_grid(point[1], 1) + oy) for point in polygon.points)
646
+ if not points:
647
+ continue
648
+ fragments.append(DiffFragment(points=points, bbox=polygon_bbox(points), twice_area=polygon_twice_area(points)))
649
+ return tuple(fragments)