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
gdsdiff/exact_oracle.py
ADDED
|
@@ -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)
|